summarize_display_mode added. see #580

This commit is contained in:
mic 2026-03-23 23:24:54 +01:00
parent 7bac86cc82
commit 58437ad168
8 changed files with 218 additions and 85 deletions

View file

@ -1951,6 +1951,22 @@
"message": "Choose whether to automatically generate summaries when viewing messages. Requires an API-based connection (not ChatGPT Web).", "message": "Choose whether to automatically generate summaries when viewing messages. Requires an API-based connection (not ChatGPT Web).",
"description": "" "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": { "summarize_title": {
"message": "Summary", "message": "Summary",
"description": "" "description": ""

View file

@ -43,19 +43,32 @@ js/mzta-compose-script.js (inserts text into Thunderbird compose window)
### Data Flow: Inline Summary on Message Display ### 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 User opens/selects a message in Thunderbird
mzta-compose-script.js (sends "initSummary" to background) 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 = 0 → do nothing │
│ summarize_auto = 1 → show "click to generate" │ │ summarize_auto = 1 → show "click to generate" button │
│ summarize_auto = 2 → generate immediately │ │ display_mode = inline → click triggers inline gen │
└────────────────────────────────────────────────┘ │ display_mode = webchat → click opens chat window │
↓ (if generating) │ summarize_auto = 2 → generate immediately (always inline)│
└──────────────────────────────────────────────────────────┘
↓ (if generating inline)
taSummaryStore (check cache / set processing) taSummaryStore (check cache / set processing)
↓ (cache miss) ↓ (cache miss)
mzta-special-commands (via Web Worker, NOT chatgpt_web) mzta-special-commands (via Web Worker, NOT chatgpt_web)

View file

@ -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 | | `spamfilter_show_msg_panel` | `true` | Show info panel on spam detection |
| `summarize` | `false` | Enable email summarization | | `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_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/`) ### Summarize Settings Page (`pages/summarize/`)
@ -106,7 +107,11 @@ The summarize settings page provides:
- `0` (Disabled) — no inline summaries - `0` (Disabled) — no inline summaries
- `1` (Manual) — shows a "Click to generate summary" button in message display - `1` (Manual) — shows a "Click to generate summary" button in message display
- `2` (Automatic) — generates summary immediately when message is opened - `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`) - Summarize instruction prompt (`prompt_summarize`)
- Email template prompt (`prompt_summarize_email_template`) - Email template prompt (`prompt_summarize_email_template`)
- Email separator prompt (`prompt_summarize_email_separator`) - Email separator prompt (`prompt_summarize_email_separator`)

View file

@ -851,7 +851,7 @@ switch (message.command) {
triggerContainer.style.cursor = 'default'; triggerContainer.style.cursor = 'default';
triggerText.textContent = browser.i18n.getMessage("summarize_generating"); triggerText.textContent = browser.i18n.getMessage("summarize_generating");
browser.runtime.sendMessage({ browser.runtime.sendMessage({
command: "triggerSummaryGeneration", command: message.webchat ? "triggerSummaryWebchat" : "triggerSummaryGeneration",
headerMessageId: message.headerMessageId headerMessageId: message.headerMessageId
}); });
}; };

View file

@ -222,27 +222,41 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => {
async function _initSummary() { async function _initSummary() {
try { try {
let tabId = sender.tab.id; 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; if (prefs.summarize_auto === 0) return;
let message = await browser.messageDisplay.getDisplayedMessage(tabId); let message = await browser.messageDisplay.getDisplayedMessage(tabId);
if (!message) return; if (!message) return;
let cachedSummary = await summaryStore.loadSummary(message.headerMessageId); // Auto mode (summarize_auto === 2) always generates inline
if (cachedSummary && !cachedSummary.error) { if (prefs.summarize_auto === 2) {
browser.tabs.sendMessage(tabId, { command: "showSummary", data: cachedSummary }); let cachedSummary = await summaryStore.loadSummary(message.headerMessageId);
return; 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" }); if (await summaryStore.isProcessing(message.headerMessageId)) {
return; 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) {
_generateSummaryForMessage(message.headerMessageId, tabId); _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) { } catch (e) {
taLog.error("Error in initSummary: " + e); taLog.error("Error in initSummary: " + e);
@ -257,6 +271,13 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => {
} }
_triggerSummaryGeneration(message); _triggerSummaryGeneration(message);
break; break;
case 'triggerSummaryWebchat':
async function _triggerSummaryWebchat(message) {
let tabId = sender.tab.id;
await _openSummaryWebchat(message.headerMessageId, tabId);
}
_triggerSummaryWebchat(message);
break;
case 'generate_summary': case 'generate_summary':
async function _generate_summary(message) { async function _generate_summary(message) {
await _generateSummaryForMessage(message.headerMessageId, message.tabId); 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 // Listen for messages from ThunderAI-Sparks
browser.runtime.onMessageExternal.addListener((message, sender, sendResponse) => { browser.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
switch (message.action) { switch (message.action) {
@ -1384,66 +1454,79 @@ async function processEmails(args) {
} }
if (summarize) { 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 tabs = await browser.tabs.query({ active: true, currentWindow: true });
const chatgpt_lang = await taPromptUtils.getDefaultLang(prompt); const tabId = tabs[0].id;
let summarize_prefs = await browser.storage.sync.get({ summarize_display_mode: prefs_default.summarize_display_mode });
// replace placeholders in the prompts the assignment prompt and email // Collect messages into array to check count
// separator prompt do not have a message as context, so there is only const messageArray = [];
// limited things to replace for await (let msg of messages) {
const prompt_string = await taPromptUtils.preparePrompt({ messageArray.push(msg);
curr_prompt: prompt, }
chatgpt_lang: chatgpt_lang,
}); // Inline mode for single message: generate inline summary in the message pane
const prompt_email_separator_string = await taPromptUtils.preparePrompt({ if (summarize_prefs.summarize_display_mode === 'inline' && messageArray.length === 1) {
curr_prompt: prompt_email_separator, await _generateSummaryForMessage(messageArray[0].headerMessageId, tabId);
chatgpt_lang: chatgpt_lang, } 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 // assemble all email messages into one string and add the assignment prompt
const messages_list = []; const messages_list = [];
for await (let curr_message of messages) { for (let curr_message of messageArray) {
// extract body of current message as text // extract body of current message as text
const curr_message_full = await browser.messages.getFull(curr_message.id); const curr_message_full = await browser.messages.getFull(curr_message.id);
const curr_body_full_html = getMailBody(curr_message_full); const curr_body_full_html = getMailBody(curr_message_full);
let curr_body_full_text = htmlBodyToPlainText(curr_body_full_html.html); let curr_body_full_text = htmlBodyToPlainText(curr_body_full_html.html);
if( curr_body_full_text.length === 0) { if( curr_body_full_text.length === 0) {
taLog.log("No HTML found in the message body, using plain text..."); taLog.log("No HTML found in the message body, using plain text...");
curr_body_full_text = curr_message_full.text; curr_body_full_text = curr_message_full.text;
} }
messages_list.push(await taPromptUtils.preparePrompt({ messages_list.push(await taPromptUtils.preparePrompt({
curr_prompt: prompt_email, curr_prompt: prompt_email,
curr_message: curr_message, curr_message: curr_message,
chatgpt_lang: chatgpt_lang, chatgpt_lang: chatgpt_lang,
body_text: curr_body_full_text, body_text: curr_body_full_text,
subject_text: curr_message_full.headers.subject, subject_text: curr_message_full.headers.subject,
msg_text: curr_body_full_html, msg_text: curr_body_full_html,
})); }));
}; };
const messages_string = messages_list.join(prompt_email_separator_string); const messages_string = messages_list.join(prompt_email_separator_string);
const full_prompt = prompt_string + prompt_email_separator_string + messages_string; const full_prompt = prompt_string + prompt_email_separator_string + messages_string;
// console.log(full_prompt); // send the prompt to the chat interface
openChatGPT(
// send the prompt to the chat interface full_prompt,
openChatGPT( prompt.action,
full_prompt, tabId,
prompt.action, prompt.name,
tabs[0].id, prompt.need_custom_text,
prompt.name, prompt
prompt.need_custom_text, );
prompt }
);
} }
taWorkingStatus.stopWorking(); taWorkingStatus.stopWorking();

View file

@ -138,6 +138,7 @@ export const prefs_default = {
spamfilter_threshold: 70, spamfilter_threshold: 70,
spamfilter_enabled_accounts: [], spamfilter_enabled_accounts: [],
summarize_auto: 0, // 0: disabled, 1: manual button, 2: automatic summarize_auto: 0, // 0: disabled, 1: manual button, 2: automatic
summarize_display_mode: 'inline', // 'inline' or 'webchat'
spamfilter_show_msg_panel: true, spamfilter_show_msg_panel: true,
summarize: false, summarize: false,
...generated_prefs ...generated_prefs

View file

@ -39,6 +39,18 @@
</label> </label>
</td> </td>
</tr> </tr>
<tr class="summarize_tr">
<td><span class="opt_title">__MSG_prefs_OptionText_summarize_display_mode__</span></td>
<td>
<label>
<select id="summarize_display_mode" name="summarize_display_mode" class="option-input">
<option value="inline">__MSG_prefs_OptionText_summarize_display_mode_inline__</option>
<option value="webchat">__MSG_prefs_OptionText_summarize_display_mode_webchat__</option>
</select>
<br>__MSG_prefs_OptionText_summarize_display_mode_Info__
</label>
</td>
</tr>
</table> </table>
<!-- PROMPTS --> <!-- PROMPTS -->

View file

@ -240,6 +240,9 @@ async function restoreOptions() {
if (element.id === 'summarize_auto') { if (element.id === 'summarize_auto') {
default_select_value = prefs_default.summarize_auto; default_select_value = prefs_default.summarize_auto;
} }
if (element.id === 'summarize_display_mode') {
default_select_value = prefs_default.summarize_display_mode;
}
const restoreValue = result[element.id] ?? default_select_value; const restoreValue = result[element.id] ?? default_select_value;
// Check if option exists // Check if option exists
let optionExists = Array.from(element.options).some(opt => opt.value === String(restoreValue)); let optionExists = Array.from(element.options).some(opt => opt.value === String(restoreValue));