diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 4a527e1f..093c95c7 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1951,6 +1951,22 @@ "message": "Choose whether to automatically generate summaries when viewing messages. Requires an API-based connection (not ChatGPT Web).", "description": "" }, + "prefs_OptionText_summarize_display_mode": { + "message": "Display summary in", + "description": "" + }, + "prefs_OptionText_summarize_display_mode_inline": { + "message": "Message pane (inline)", + "description": "" + }, + "prefs_OptionText_summarize_display_mode_webchat": { + "message": "Chat window", + "description": "" + }, + "prefs_OptionText_summarize_display_mode_Info": { + "message": "Choose where the summary result is displayed. Inline mode shows a summary banner directly in the message pane. Chat window mode opens the AI chat window.", + "description": "" + }, "summarize_title": { "message": "Summary", "description": "" diff --git a/claude-spec/01-architecture.md b/claude-spec/01-architecture.md index cf3b98d6..7426a4c2 100644 --- a/claude-spec/01-architecture.md +++ b/claude-spec/01-architecture.md @@ -43,19 +43,32 @@ js/mzta-compose-script.js (inserts text into Thunderbird compose window) ### Data Flow: Inline Summary on Message Display +The `summarize_display_mode` preference (`'inline'` or `'webchat'`) controls where +the summary is displayed. The `summarize_auto` preference controls when it is triggered. + +- `summarize_auto = 2` (automatic) always generates inline, regardless of `summarize_display_mode`. +- `summarize_auto = 1` (manual button) respects `summarize_display_mode`: + - `'inline'` → button click triggers inline generation + - `'webchat'` → button click opens the AI chat window via `_openSummaryWebchat()` +- Context menu summarize also respects `summarize_display_mode`: + - `'inline'` with a single message → generates inline via `_generateSummaryForMessage()` + - `'webchat'` or multiple messages → opens the AI chat window via `openChatGPT()` + ``` User opens/selects a message in Thunderbird ↓ mzta-compose-script.js (sends "initSummary" to background) ↓ -mzta-background.js (checks summarize_auto pref) +mzta-background.js (checks summarize_auto + summarize_display_mode prefs) ↓ - ┌────────────────────────────────────────────────┐ - │ summarize_auto = 0 → do nothing │ - │ summarize_auto = 1 → show "click to generate" │ - │ summarize_auto = 2 → generate immediately │ - └────────────────────────────────────────────────┘ - ↓ (if generating) + ┌──────────────────────────────────────────────────────────┐ + │ summarize_auto = 0 → do nothing │ + │ summarize_auto = 1 → show "click to generate" button │ + │ display_mode = inline → click triggers inline gen │ + │ display_mode = webchat → click opens chat window │ + │ summarize_auto = 2 → generate immediately (always inline)│ + └──────────────────────────────────────────────────────────┘ + ↓ (if generating inline) taSummaryStore (check cache / set processing) ↓ (cache miss) mzta-special-commands (via Web Worker, NOT chatgpt_web) diff --git a/claude-spec/05-options.md b/claude-spec/05-options.md index 7f399f6f..8f402497 100644 --- a/claude-spec/05-options.md +++ b/claude-spec/05-options.md @@ -96,6 +96,7 @@ These are generated programmatically at the bottom of `mzta-options-default.js` | `spamfilter_show_msg_panel` | `true` | Show info panel on spam detection | | `summarize` | `false` | Enable email summarization | | `summarize_auto` | `0` | Auto-summarize mode: `0` = disabled, `1` = manual (show "click to generate" button), `2` = automatic (generate on message open) | +| `summarize_display_mode` | `'inline'` | Where to display summaries: `'inline'` = message pane banner, `'webchat'` = AI chat window. Note: `summarize_auto = 2` always uses inline regardless of this setting. | ### Summarize Settings Page (`pages/summarize/`) @@ -106,7 +107,11 @@ The summarize settings page provides: - `0` (Disabled) — no inline summaries - `1` (Manual) — shows a "Click to generate summary" button in message display - `2` (Automatic) — generates summary immediately when message is opened -3. **Three editable prompts** (used by context menu summarize, not inline): +3. **Display mode dropdown** (`summarize_display_mode`) — controls where summaries are shown: + - `'inline'` — summary banner in the message pane (default) + - `'webchat'` — opens the AI chat window + - Note: `summarize_auto = 2` always generates inline regardless of this setting. Context menu summarize with multiple messages always falls back to webchat. +4. **Three editable prompts** (used by context menu summarize and webchat mode): - Summarize instruction prompt (`prompt_summarize`) - Email template prompt (`prompt_summarize_email_template`) - Email separator prompt (`prompt_summarize_email_separator`) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 56b706ee..f7f106db 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -850,9 +850,9 @@ switch (message.command) { triggerContainer.id = 'mzta-summary-generating'; triggerContainer.style.cursor = 'default'; triggerText.textContent = browser.i18n.getMessage("summarize_generating"); - browser.runtime.sendMessage({ - command: "triggerSummaryGeneration", - headerMessageId: message.headerMessageId + browser.runtime.sendMessage({ + command: message.webchat ? "triggerSummaryWebchat" : "triggerSummaryGeneration", + headerMessageId: message.headerMessageId }); }; diff --git a/mzta-background.js b/mzta-background.js index ec5adfea..e221d674 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -222,27 +222,41 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { async function _initSummary() { try { let tabId = sender.tab.id; - let prefs = await browser.storage.sync.get({ summarize_auto: 0 }); + let prefs = await browser.storage.sync.get({ summarize_auto: 0, summarize_display_mode: prefs_default.summarize_display_mode }); if (prefs.summarize_auto === 0) return; let message = await browser.messageDisplay.getDisplayedMessage(tabId); if (!message) return; - let cachedSummary = await summaryStore.loadSummary(message.headerMessageId); - if (cachedSummary && !cachedSummary.error) { - browser.tabs.sendMessage(tabId, { command: "showSummary", data: cachedSummary }); - return; - } - - if (await summaryStore.isProcessing(message.headerMessageId)) { - browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); - return; - } - - if (prefs.summarize_auto === 1) { - browser.tabs.sendMessage(tabId, { command: "showSummaryButton", headerMessageId: message.headerMessageId }); - } else if (prefs.summarize_auto === 2) { + // Auto mode (summarize_auto === 2) always generates inline + if (prefs.summarize_auto === 2) { + let cachedSummary = await summaryStore.loadSummary(message.headerMessageId); + if (cachedSummary && !cachedSummary.error) { + browser.tabs.sendMessage(tabId, { command: "showSummary", data: cachedSummary }); + return; + } + if (await summaryStore.isProcessing(message.headerMessageId)) { + browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); + return; + } _generateSummaryForMessage(message.headerMessageId, tabId); + return; + } + + // Manual button mode (summarize_auto === 1) + if (prefs.summarize_display_mode === 'inline') { + let cachedSummary = await summaryStore.loadSummary(message.headerMessageId); + if (cachedSummary && !cachedSummary.error) { + browser.tabs.sendMessage(tabId, { command: "showSummary", data: cachedSummary }); + return; + } + if (await summaryStore.isProcessing(message.headerMessageId)) { + browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); + return; + } + browser.tabs.sendMessage(tabId, { command: "showSummaryButton", headerMessageId: message.headerMessageId }); + } else { + browser.tabs.sendMessage(tabId, { command: "showSummaryButton", headerMessageId: message.headerMessageId, webchat: true }); } } catch (e) { taLog.error("Error in initSummary: " + e); @@ -257,6 +271,13 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { } _triggerSummaryGeneration(message); break; + case 'triggerSummaryWebchat': + async function _triggerSummaryWebchat(message) { + let tabId = sender.tab.id; + await _openSummaryWebchat(message.headerMessageId, tabId); + } + _triggerSummaryWebchat(message); + break; case 'generate_summary': async function _generate_summary(message) { await _generateSummaryForMessage(message.headerMessageId, message.tabId); @@ -514,6 +535,55 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { } } +async function _openSummaryWebchat(headerMessageId, tabId) { + try { + const specialPrompts = await getSpecialPrompts(); + const prompt = specialPrompts.find(p => p.id === 'prompt_summarize'); + const prompt_email = specialPrompts.find(p => p.id === 'prompt_summarize_email_template'); + const prompt_email_separator = specialPrompts.find(p => p.id === 'prompt_summarize_email_separator'); + + const chatgpt_lang = await taPromptUtils.getDefaultLang(prompt); + + const prompt_string = await taPromptUtils.preparePrompt({ + curr_prompt: prompt, + chatgpt_lang: chatgpt_lang, + }); + const prompt_email_separator_string = await taPromptUtils.preparePrompt({ + curr_prompt: prompt_email_separator, + chatgpt_lang: chatgpt_lang, + }); + + const messageResult = await browser.messages.query({ headerMessageId: headerMessageId }); + if (!messageResult || messageResult.messages.length === 0) { + console.error("[ThunderAI] _openSummaryWebchat: Message not found for headerMessageId:", headerMessageId); + return; + } + + const curr_message = messageResult.messages[0]; + const curr_message_full = await browser.messages.getFull(curr_message.id); + const curr_body_full_html = getMailBody(curr_message_full); + let curr_body_full_text = htmlBodyToPlainText(curr_body_full_html.html); + if (curr_body_full_text.length === 0) { + curr_body_full_text = curr_body_full_html.text; + } + + const email_text = await taPromptUtils.preparePrompt({ + curr_prompt: prompt_email, + curr_message: curr_message, + chatgpt_lang: chatgpt_lang, + body_text: curr_body_full_text, + subject_text: curr_message_full.headers.subject, + msg_text: curr_body_full_html, + }); + + const full_prompt = prompt_string + prompt_email_separator_string + email_text; + + openChatGPT(full_prompt, prompt.action, tabId, prompt.name, prompt.need_custom_text, prompt); + } catch (error) { + console.error("[ThunderAI] Error opening summary webchat:", error); + } +} + // Listen for messages from ThunderAI-Sparks browser.runtime.onMessageExternal.addListener((message, sender, sendResponse) => { switch (message.action) { @@ -1384,66 +1454,79 @@ async function processEmails(args) { } if (summarize) { - // we have three prompts, the actual assignment for the LLM, the email - // template prompt, and the email separator prompt - const specialPrompts = await getSpecialPrompts(); - const prompt = specialPrompts.find((prompt) => prompt.id === 'prompt_summarize'); - const prompt_email = specialPrompts.find((prompt) => prompt.id === 'prompt_summarize_email_template'); - const prompt_email_separator = specialPrompts.find((prompt) => prompt.id === 'prompt_summarize_email_separator'); - const tabs = await browser.tabs.query({ active: true, currentWindow: true }); - const chatgpt_lang = await taPromptUtils.getDefaultLang(prompt); - - // replace placeholders in the prompts the assignment prompt and email - // separator prompt do not have a message as context, so there is only - // limited things to replace - const prompt_string = await taPromptUtils.preparePrompt({ - curr_prompt: prompt, - chatgpt_lang: chatgpt_lang, - }); - const prompt_email_separator_string = await taPromptUtils.preparePrompt({ - curr_prompt: prompt_email_separator, - chatgpt_lang: chatgpt_lang, - }); - - - // assemble all email messages into one string and add the assignment prompt - const messages_list = []; - for await (let curr_message of messages) { - - // extract body of current message as text - const curr_message_full = await browser.messages.getFull(curr_message.id); - const curr_body_full_html = getMailBody(curr_message_full); - let curr_body_full_text = htmlBodyToPlainText(curr_body_full_html.html); - if( curr_body_full_text.length === 0) { - taLog.log("No HTML found in the message body, using plain text..."); - curr_body_full_text = curr_message_full.text; - } - - messages_list.push(await taPromptUtils.preparePrompt({ - curr_prompt: prompt_email, - curr_message: curr_message, - chatgpt_lang: chatgpt_lang, - body_text: curr_body_full_text, - subject_text: curr_message_full.headers.subject, - msg_text: curr_body_full_html, - })); - }; - const messages_string = messages_list.join(prompt_email_separator_string); - - const full_prompt = prompt_string + prompt_email_separator_string + messages_string; - - // console.log(full_prompt); - - // send the prompt to the chat interface - openChatGPT( - full_prompt, - prompt.action, - tabs[0].id, - prompt.name, - prompt.need_custom_text, - prompt - ); + const tabId = tabs[0].id; + let summarize_prefs = await browser.storage.sync.get({ summarize_display_mode: prefs_default.summarize_display_mode }); + + // Collect messages into array to check count + const messageArray = []; + for await (let msg of messages) { + messageArray.push(msg); + } + + // Inline mode for single message: generate inline summary in the message pane + if (summarize_prefs.summarize_display_mode === 'inline' && messageArray.length === 1) { + await _generateSummaryForMessage(messageArray[0].headerMessageId, tabId); + } else { + // Webchat mode, or inline with multiple messages (fallback to webchat) + // we have three prompts, the actual assignment for the LLM, the email + // template prompt, and the email separator prompt + const specialPrompts = await getSpecialPrompts(); + const prompt = specialPrompts.find((prompt) => prompt.id === 'prompt_summarize'); + const prompt_email = specialPrompts.find((prompt) => prompt.id === 'prompt_summarize_email_template'); + const prompt_email_separator = specialPrompts.find((prompt) => prompt.id === 'prompt_summarize_email_separator'); + + const chatgpt_lang = await taPromptUtils.getDefaultLang(prompt); + + // replace placeholders in the prompts the assignment prompt and email + // separator prompt do not have a message as context, so there is only + // limited things to replace + const prompt_string = await taPromptUtils.preparePrompt({ + curr_prompt: prompt, + chatgpt_lang: chatgpt_lang, + }); + const prompt_email_separator_string = await taPromptUtils.preparePrompt({ + curr_prompt: prompt_email_separator, + chatgpt_lang: chatgpt_lang, + }); + + + // assemble all email messages into one string and add the assignment prompt + const messages_list = []; + for (let curr_message of messageArray) { + + // extract body of current message as text + const curr_message_full = await browser.messages.getFull(curr_message.id); + const curr_body_full_html = getMailBody(curr_message_full); + let curr_body_full_text = htmlBodyToPlainText(curr_body_full_html.html); + if( curr_body_full_text.length === 0) { + taLog.log("No HTML found in the message body, using plain text..."); + curr_body_full_text = curr_message_full.text; + } + + messages_list.push(await taPromptUtils.preparePrompt({ + curr_prompt: prompt_email, + curr_message: curr_message, + chatgpt_lang: chatgpt_lang, + body_text: curr_body_full_text, + subject_text: curr_message_full.headers.subject, + msg_text: curr_body_full_html, + })); + }; + const messages_string = messages_list.join(prompt_email_separator_string); + + const full_prompt = prompt_string + prompt_email_separator_string + messages_string; + + // send the prompt to the chat interface + openChatGPT( + full_prompt, + prompt.action, + tabId, + prompt.name, + prompt.need_custom_text, + prompt + ); + } } taWorkingStatus.stopWorking(); diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index 2298431b..4a4cbda0 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -138,6 +138,7 @@ export const prefs_default = { spamfilter_threshold: 70, spamfilter_enabled_accounts: [], summarize_auto: 0, // 0: disabled, 1: manual button, 2: automatic + summarize_display_mode: 'inline', // 'inline' or 'webchat' spamfilter_show_msg_panel: true, summarize: false, ...generated_prefs diff --git a/pages/summarize/mzta-summarize.html b/pages/summarize/mzta-summarize.html index ddb5cf41..0f02deac 100644 --- a/pages/summarize/mzta-summarize.html +++ b/pages/summarize/mzta-summarize.html @@ -39,6 +39,18 @@ +