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:
parent
b5741c61bb
commit
dcbf9d3dcd
3 changed files with 209 additions and 0 deletions
105
messageDisplay/message-content-script.js
Normal file
105
messageDisplay/message-content-script.js
Normal 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();
|
||||||
20
messageDisplay/message-content-styles.css
Normal file
20
messageDisplay/message-content-styles.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -99,6 +99,13 @@ browser.composeScripts.register({
|
||||||
// Register the message display script for all newly opened message tabs.
|
// Register the message display script for all newly opened message tabs.
|
||||||
messenger.messageDisplayScripts.register({
|
messenger.messageDisplayScripts.register({
|
||||||
js: [{ file: "js/mzta-compose-script.js" }],
|
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.
|
// 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, {
|
await browser.tabs.executeScript(messageTab.id, {
|
||||||
file: "js/mzta-compose-script.js"
|
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) {
|
} catch (error) {
|
||||||
console.error("[ThunderAI] Error injecting message display script:", error);
|
console.error("[ThunderAI] Error injecting message display script:", error);
|
||||||
console.error("[ThunderAI] Message tab:", messageTab.url);
|
console.error("[ThunderAI] Message tab:", messageTab.url);
|
||||||
|
|
@ -220,6 +234,31 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
// handler function.
|
// handler function.
|
||||||
if (message && message.hasOwnProperty("command")){
|
if (message && message.hasOwnProperty("command")){
|
||||||
switch (message.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':
|
// case 'chatgpt_open':
|
||||||
// openChatGPT(message.prompt,message.action,message.tabId);
|
// openChatGPT(message.prompt,message.action,message.tabId);
|
||||||
// return true;
|
// return true;
|
||||||
|
|
@ -1177,3 +1216,48 @@ try {
|
||||||
taLog.log("Using browser.messages.onNewMailReceived.addListener with one agrument for Thunderbird 115.");
|
taLog.log("Using browser.messages.onNewMailReceived.addListener with one agrument for Thunderbird 115.");
|
||||||
browser.messages.onNewMailReceived.addListener(newEmailListener);
|
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue