feat(summary): add AI-powered message summary feature

Add new ThunderAI summary functionality that generates concise summaries
for email messages using the existing ThunderAI infrastructure. Includes
a new content script that creates a summary pane in the message display,
associated CSS styling, and backend integration for AI summary generation.

The feature shows a loading indicator while generating the summary and
falls back to showing truncated message content if the AI generation fails.
This commit is contained in:
Ronald Klarenbeek 2025-12-26 20:39:30 +01:00
parent b5741c61bb
commit dcbf9d3dcd
3 changed files with 209 additions and 0 deletions

View file

@ -0,0 +1,105 @@
async function showSummaryPane() {
// Create the summary pane element
const summaryPane = document.createElement("div");
summaryPane.className = "thunderai-summary-pane";
// Create the title element
const summaryTitle = document.createElement("div");
summaryTitle.className = "thunderai-summary-title";
summaryTitle.innerText = "ThunderAI Summary";
// Create a loading indicator
const loadingIndicator = document.createElement("div");
loadingIndicator.className = "thunderai-summary-content";
loadingIndicator.innerText = "Generating AI summary...";
// Create the content element (initially hidden)
const summaryContent = document.createElement("div");
summaryContent.className = "thunderai-summary-content";
summaryContent.style.display = 'none';
// Add title and loading indicator to the pane
summaryPane.appendChild(summaryTitle);
summaryPane.appendChild(loadingIndicator);
summaryPane.appendChild(summaryContent);
// Insert it as the very first element in the message
document.body.insertBefore(summaryPane, document.body.firstChild);
// Get the message content and generate summary
try {
const messageContent = getMessageContent();
const aiSummary = await generateAISummary(messageContent);
// Update the UI with the AI summary
loadingIndicator.style.display = 'none';
summaryContent.innerText = aiSummary;
summaryContent.style.display = 'block';
} catch (error) {
console.error("Error generating AI summary:", error);
loadingIndicator.innerText = "Failed to generate AI summary. Showing message preview instead.";
loadingIndicator.style.color = '#d70022';
// Fallback to showing truncated message content
const messageContent = getMessageContent();
loadingIndicator.innerText += "\n\n" + truncateMessageContent(messageContent);
}
}
function getMessageContent() {
// Get the main message content from the page
// This selects the main message body content
const messageBody = document.querySelector('.moz-text-flowed, .moz-text-plain, body');
if (messageBody) {
return messageBody.textContent || messageBody.innerText || '';
}
// Fallback: get the entire body content
return document.body.textContent || document.body.innerText || '';
}
function truncateMessageContent(content) {
// Clean up the content by removing excessive whitespace and newlines
const cleanedContent = content.replace(/\s+/g, ' ').trim();
// Truncate to a reasonable length for preview
const maxLength = 500;
if (cleanedContent.length <= maxLength) {
return cleanedContent;
}
return cleanedContent.substring(0, maxLength) + '...';
}
async function generateAISummary(messageContent) {
// Clean up the message content
const cleanedContent = messageContent.replace(/\s+/g, ' ').trim();
// Create a simple summary prompt
const summaryPrompt = `Please provide a concise summary of the following email message. The summary should be 3-5 sentences maximum and capture the main points:
${cleanedContent}
Summary:`;
// Request AI summary from the background script
return new Promise((resolve, reject) => {
// Send message to background script to get AI summary
browser.runtime.sendMessage({
command: "generate_summary",
content: cleanedContent,
prompt: summaryPrompt
}, (response) => {
if (response && response.summary) {
resolve(response.summary);
} else if (response && response.error) {
reject(new Error(response.error));
} else {
reject(new Error("Failed to get AI summary"));
}
});
});
}
// Call the function to show the pane
showSummaryPane();

View file

@ -0,0 +1,20 @@
.thunderai-summary-pane {
background-color: #f0f0f0;
color: #333;
font-weight: 400;
padding: 0.5rem;
margin-bottom: 1rem;
border-radius: 4px;
border: 1px solid #ddd;
}
.thunderai-summary-title {
font-weight: bold;
margin-bottom: 0.5rem;
color: #d70022;
}
.thunderai-summary-content {
font-size: 0.9rem;
line-height: 1.4;
}

View file

@ -99,6 +99,13 @@ browser.composeScripts.register({
// Register the message display script for all newly opened message tabs.
messenger.messageDisplayScripts.register({
js: [{ file: "js/mzta-compose-script.js" }],
css: [{ file: "messageDisplay/message-content-styles.css" }]
});
// Register our new ThunderAI summary script
messenger.messageDisplayScripts.register({
js: [{ file: "messageDisplay/message-content-script.js" }],
css: [{ file: "messageDisplay/message-content-styles.css" }]
});
// Inject script and CSS in all already open message tabs.
@ -114,6 +121,13 @@ for (let messageTab of messageTabs) {
await browser.tabs.executeScript(messageTab.id, {
file: "js/mzta-compose-script.js"
})
// Inject our ThunderAI summary script
await browser.tabs.executeScript(messageTab.id, {
file: "messageDisplay/message-content-script.js"
})
await browser.tabs.insertCSS(messageTab.id, {
file: "messageDisplay/message-content-styles.css"
});
} catch (error) {
console.error("[ThunderAI] Error injecting message display script:", error);
console.error("[ThunderAI] Message tab:", messageTab.url);
@ -220,6 +234,31 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => {
// handler function.
if (message && message.hasOwnProperty("command")){
switch (message.command) {
case 'generate_summary':
async function _generate_summary(message) {
try {
// Get user preferences for AI connection
let prefs = await browser.storage.sync.get({
connection_type: prefs_default.connection_type,
chatgpt_model: prefs_default.chatgpt_model,
chatgpt_api_key: prefs_default.chatgpt_api_key,
do_debug: prefs_default.do_debug
});
// Use the existing ThunderAI infrastructure
const summary = await generateAISummaryUsingThunderAIInfrastructure(
message.content,
message.prompt,
prefs
);
return { summary: summary };
} catch (error) {
console.error("[ThunderAI] Error generating summary:", error);
return { error: "Failed to generate AI summary: " + error.message };
}
}
return _generate_summary(message);
// case 'chatgpt_open':
// openChatGPT(message.prompt,message.action,message.tabId);
// return true;
@ -1177,3 +1216,48 @@ try {
taLog.log("Using browser.messages.onNewMailReceived.addListener with one agrument for Thunderbird 115.");
browser.messages.onNewMailReceived.addListener(newEmailListener);
}
/**
* AI summary generation function using ThunderAI infrastructure
*/
async function generateAISummaryUsingThunderAIInfrastructure(content, prompt, prefs) {
// Import the special command class
const { mzta_specialCommand } = await import('./js/mzta-special-commands.js');
// Determine which LLM to use based on user preferences
const llmType = getConnectionType(prefs.connection_type, {}, '');
// Create a special command instance
const summaryCommand = new mzta_specialCommand({
prompt: prompt,
llm: llmType,
custom_model: prefs.chatgpt_model,
do_debug: prefs.do_debug
});
// Initialize the worker
await summaryCommand.initWorker();
// Send the prompt and get the AI response
const aiResponse = await summaryCommand.sendPrompt();
// Clean up the response - extract just the summary content
const cleanedResponse = cleanAISummaryResponse(aiResponse);
return cleanedResponse;
}
/**
* Helper function to clean AI response
*/
function cleanAISummaryResponse(response) {
// Remove any markdown formatting or code blocks
let cleaned = response.replace(/```[\s\S]*?```/g, '');
cleaned = cleaned.replace(/[\*#_~`]/g, '');
cleaned = cleaned.replace(/\s+/g, ' ').trim();
// Remove any "Summary:" prefixes that the AI might add
cleaned = cleaned.replace(/^Summary:\s*/i, '');
return cleaned;
}