From 8a0e6e5cae235d14e871b2420dc71677fc4f52d9 Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 25 Mar 2026 23:23:03 +0100 Subject: [PATCH 1/5] release notes updated --- CHANGELOG.md | 1 + options/mzta-release-notes.html | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb205dff..83b00aed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@

Version 4.1.0 - ??/??/2026

Version 4.0.3 - 20/03/2026

diff --git a/options/mzta-release-notes.html b/options/mzta-release-notes.html index 36683c30..c13c26ed 100644 --- a/options/mzta-release-notes.html +++ b/options/mzta-release-notes.html @@ -9,6 +9,7 @@

ThunderAI Release Notes

Version 4.1.0 - ??/??/2026

Version 4.0.3 - 20/03/2026

From 3cb1fb0972b3e7c549c6aa1aeefaa5b4c26f0398 Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 25 Mar 2026 23:43:48 +0100 Subject: [PATCH 2/5] correctly using the ai webchat if the relative option is configured accordingly. see #580 --- _locales/en/messages.json | 10 ++--- api_webchat/messagesArea.js | 25 ++++++++++- js/mzta-prompts.js | 10 +++++ mzta-background.js | 60 ++++++++++++++++++++++++--- pages/spamfilter/mzta-spamfilter.html | 3 +- 5 files changed, 95 insertions(+), 13 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 7f28055b..620a03a0 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -235,6 +235,10 @@ "message": "Close", "description": "" }, + "webchat_save_as_summary": { + "message": "Save as Summary", + "description": "Button label in the webchat window to save the AI response as a message summary" + }, "chatgpt_textarea_not_found_error": { "message": "It seems that the ChatGPT page is taking too long to load. If it finishes loading, click the button on the right. If the problem persists, please check the service status.", "description": "" @@ -1388,17 +1392,13 @@ "description": "" }, "spamfilter_no_reports": { - "message": "No messages have screened for spam yet. Here you'll find a list of the last 100 spam reports for the current session only.", + "message": "No messages have screened for spam yet. Here you'll find a list of the last 100 spam reports.", "description": "" }, "SpamReport_Title": { "message": "Spam Filter Reports", "description": "" }, - "SpamReport_infoline": { - "message": "This information is saved only for the current session.", - "description": "" - }, "Date": { "message": "Date", "description": "" diff --git a/api_webchat/messagesArea.js b/api_webchat/messagesArea.js index 5d9b5bc4..0b2f2d7c 100644 --- a/api_webchat/messagesArea.js +++ b/api_webchat/messagesArea.js @@ -449,11 +449,34 @@ class MessagesArea extends HTMLElement { closeButton.addEventListener('click', async () => { browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id}); // close window }); - if(promptData.action != 0) { + if(promptData.action != 0) { actionButtons.appendChild(splitButton); selectionInfo.style.display = "block"; // show selection info } + // Save as Summary button (only shown for summary webchat sessions) + if(promptData.prompt_info?.headerMessageId) { + const saveSummaryButton = document.createElement('button'); + saveSummaryButton.textContent = browser.i18n.getMessage("webchat_save_as_summary"); + saveSummaryButton.classList.add('action_btn'); + saveSummaryButton.addEventListener('click', async () => { + let finalText = removeAloneBRs(fullTextHTMLAtAssignment); + const selectedHTML = this.getCurrentSelectionHTML(); + if(selectedHTML != "") { + finalText = removeAloneBRs(selectedHTML); + } + await browser.runtime.sendMessage({ + command: "chatgpt_saveSummary", + text: finalText, + headerMessageId: promptData.prompt_info.headerMessageId, + tabId: promptData.prompt_info.summaryTabId || promptData.tabId, + }); + browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id}); + }); + actionButtons.appendChild(saveSummaryButton); + selectionInfo.style.display = "block"; + } + // diff viewer button if(promptData.prompt_info?.use_diff_viewer == "1") { const diffvButton = document.createElement('button'); diff --git a/js/mzta-prompts.js b/js/mzta-prompts.js index 4a73f20a..d7603ff1 100644 --- a/js/mzta-prompts.js +++ b/js/mzta-prompts.js @@ -52,6 +52,16 @@ 0: Do not use the diff viewer 1: Use the diff viewer + ================ DYNAMIC PROPERTIES (set at runtime via prompt_info) + + headerMessageId (set by _openSummaryWebchat in mzta-background.js): + When present, the webchat UI shows a "Save as Summary" button to capture + the AI response and save it as an inline summary for the message identified + by this headerMessageId. + + summaryTabId (set by _openSummaryWebchat in mzta-background.js): + The tab ID of the message display tab to update with the saved summary. + ================ USER PROPERTIES Enabled (enabled attribute): 0: Disabled diff --git a/mzta-background.js b/mzta-background.js index 49cc9cb8..07961049 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -282,13 +282,45 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { async function _refreshSummary(message) { let tabId = sender.tab.id; await summaryStore.removeSummary(message.headerMessageId); - await _generateSummaryForMessage(message.headerMessageId, tabId); + let prefs_refresh = await browser.storage.sync.get({ summarize_display_mode: prefs_default.summarize_display_mode }); + if (prefs_refresh.summarize_display_mode === 'webchat') { + await _openSummaryWebchat(message.headerMessageId, tabId); + } else { + await _generateSummaryForMessage(message.headerMessageId, tabId); + } } _refreshSummary(message); break; case 'removeSummary': summaryStore.removeSummary(message.headerMessageId); break; + case 'chatgpt_saveSummary': + async function _saveSummaryFromWebchat(msg) { + try { + let cleanedSummary = cleanSummaryText(msg.text); + const summaryData = { + summary: cleanedSummary, + summary_date: new Date(), + headerMessageId: msg.headerMessageId + }; + await summaryStore.saveSummary(summaryData, msg.headerMessageId); + let prefs_summary = await browser.storage.sync.get({ + summarize_max_display_length: prefs_default.summarize_max_display_length + }); + try { + browser.tabs.sendMessage(msg.tabId, { + command: "showSummary", + data: { ...summaryData, maxDisplayLength: prefs_summary.summarize_max_display_length } + }); + } catch (e) { + taLog.error("Error sending showSummary to tab: " + e); + } + } catch (error) { + console.error("[ThunderAI] Error saving summary from webchat:", error); + } + } + _saveSummaryFromWebchat(message); + break; // case 'chatgpt_open': // openChatGPT(message.prompt,message.action,message.tabId); // return true; @@ -458,6 +490,17 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { return false; }); +// Clean summary text by stripping HTML, markdown, and formatting artifacts. +// Used by both inline summary generation and webchat summary save. +function cleanSummaryText(text) { + let cleaned = text.replace(/<\/?[^>]+(>|$)/g, ''); // strip HTML tags + cleaned = cleaned.replace(/```[\s\S]*?```/g, ''); + cleaned = cleaned.replace(/[\*#_~`]/g, ''); + cleaned = cleaned.replace(/\s+/g, ' ').trim(); + cleaned = cleaned.replace(/^Summary:\s*/i, ''); + return cleaned; +} + async function _generateSummaryForMessage(headerMessageId, tabId) { try { let prefs = await browser.storage.sync.get({ @@ -514,10 +557,7 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { await cmd.initWorker(); const aiResponse = await cmd.sendPrompt(); - let cleanedSummary = aiResponse.replace(/```[\s\S]*?```/g, ''); - cleanedSummary = cleanedSummary.replace(/[\*#_~`]/g, ''); - cleanedSummary = cleanedSummary.replace(/\s+/g, ' ').trim(); - cleanedSummary = cleanedSummary.replace(/^Summary:\s*/i, ''); + let cleanedSummary = cleanSummaryText(aiResponse); const summaryData = { summary: cleanedSummary, @@ -665,7 +705,17 @@ async function _openSummaryWebchat(headerMessageId, tabId) { const curr_message = messageResult.messages[0]; const curr_message_full = await browser.messages.getFull(curr_message.id); + const connectionType = getConnectionType(await browser.storage.sync.get(prefs_default), {}, 'summarize'); + if (connectionType === 'chatgpt_web') { + const errorMsg = browser.i18n.getMessage('summarize_chatgpt_web_not_supported'); + await summaryStore.saveError(headerMessageId, errorMsg); + browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: errorMsg } }); + return; + } + const { promptText, promptInfo } = await taPromptUtils.buildSummaryPrompt([{ message: curr_message, fullMessage: curr_message_full }]); + promptInfo.headerMessageId = headerMessageId; + promptInfo.summaryTabId = tabId; openChatGPT(promptText, promptInfo.action, tabId, promptInfo.name, promptInfo.need_custom_text, promptInfo); } catch (error) { diff --git a/pages/spamfilter/mzta-spamfilter.html b/pages/spamfilter/mzta-spamfilter.html index 4576314e..fb232647 100644 --- a/pages/spamfilter/mzta-spamfilter.html +++ b/pages/spamfilter/mzta-spamfilter.html @@ -66,8 +66,7 @@
-
__MSG_SpamReport_Title__ -
__MSG_SpamReport_infoline__
+
__MSG_SpamReport_Title__
From 7e9b507f0f8950bb8e229b811d7d9d5a1ee842cb Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 25 Mar 2026 23:59:20 +0100 Subject: [PATCH 3/5] keeping formatted summary #580 --- js/mzta-compose-script.js | 140 ++++++++++++++++++++++++++------------ js/mzta-storage.js | 1 + js/mzta-summarystore.js | 1 + mzta-background.html | 1 + mzta-background.js | 5 ++ 5 files changed, 103 insertions(+), 45 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 097692bd..465e95ae 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -924,8 +924,22 @@ switch (message.command) { const summaryText = document.createElement('div'); summaryText.className = 'thunderai-summary-content'; + const hasHtml = !!summaryData.summary_html; + + // Helper to set summary content using DOMParser (innerHTML is blocked in Thunderbird content scripts) + function setSummaryHtml(element, html) { + element.textContent = ''; + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + while (doc.body.firstChild) { + element.appendChild(doc.body.firstChild); + } + } + if (summaryData.error) { summaryText.textContent = summaryData.message || browser.i18n.getMessage("summarize_error"); + } else if (hasHtml) { + setSummaryHtml(summaryText, summaryData.summary_html); } else { summaryText.textContent = summaryData.summary; } @@ -936,58 +950,94 @@ switch (message.command) { const maxLen = summaryData.maxDisplayLength || 0; const fullText = summaryData.summary; if (!summaryData.error && maxLen > 0 && fullText && fullText.length > maxLen) { - let cutPos = fullText.lastIndexOf(' ', maxLen); - if (cutPos <= 0) cutPos = maxLen; - const truncated = fullText.substring(0, cutPos) + '\u2026'; - summaryText.textContent = truncated; - // Set up animated expand/collapse via max-height transition summaryText.style.overflow = 'hidden'; summaryText.style.transition = 'max-height 0.2s ease'; - // Measure truncated height after layout - requestAnimationFrame(() => { - const collapsedHeight = summaryText.scrollHeight; - summaryText.style.maxHeight = collapsedHeight + 'px'; - }); + if (!hasHtml) { + // Plain text: truncate by character position + let cutPos = fullText.lastIndexOf(' ', maxLen); + if (cutPos <= 0) cutPos = maxLen; + const truncated = fullText.substring(0, cutPos) + '\u2026'; + summaryText.textContent = truncated; - const toggleLink = document.createElement('a'); - toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more"; - toggleLink.href = '#'; - toggleLink.style.cssText = 'display: inline-block; margin-top: 4px; font-size: 13px; color: ' + - (isDarkSummary ? '#6db3f2' : '#1a5fa8') + '; cursor: pointer; text-decoration: underline;'; - - let expanded = false; - toggleLink.addEventListener('click', (e) => { - e.preventDefault(); - if (!expanded) { - // Expand: set full text, measure, animate to full height - summaryText.textContent = fullText; - const fullHeight = summaryText.scrollHeight; - summaryText.style.maxHeight = fullHeight + 'px'; - toggleLink.textContent = browser.i18n.getMessage("summarize_see_less") || "See less"; - } else { - // Collapse: measure current truncated height, then animate down - summaryText.textContent = truncated; - // Force layout to get the target height before animating + // Measure truncated height after layout + requestAnimationFrame(() => { const collapsedHeight = summaryText.scrollHeight; - summaryText.textContent = fullText; - // Set explicit current height so transition has a starting point - summaryText.style.maxHeight = summaryText.scrollHeight + 'px'; - requestAnimationFrame(() => { - summaryText.style.maxHeight = collapsedHeight + 'px'; - }); - // Swap text after transition ends - summaryText.addEventListener('transitionend', function handler() { - summaryText.removeEventListener('transitionend', handler); - summaryText.textContent = truncated; - }); - toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more"; - } - expanded = !expanded; - }); + summaryText.style.maxHeight = collapsedHeight + 'px'; + }); - summaryTextWrapper.appendChild(toggleLink); + const toggleLink = document.createElement('a'); + toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more"; + toggleLink.href = '#'; + toggleLink.style.cssText = 'display: inline-block; margin-top: 4px; font-size: 13px; color: ' + + (isDarkSummary ? '#6db3f2' : '#1a5fa8') + '; cursor: pointer; text-decoration: underline;'; + + let expanded = false; + toggleLink.addEventListener('click', (e) => { + e.preventDefault(); + if (!expanded) { + // Expand: set full text, measure, animate to full height + summaryText.textContent = fullText; + const fullHeight = summaryText.scrollHeight; + summaryText.style.maxHeight = fullHeight + 'px'; + toggleLink.textContent = browser.i18n.getMessage("summarize_see_less") || "See less"; + } else { + // Collapse: measure current truncated height, then animate down + summaryText.textContent = truncated; + // Force layout to get the target height before animating + const collapsedHeight = summaryText.scrollHeight; + summaryText.textContent = fullText; + // Set explicit current height so transition has a starting point + summaryText.style.maxHeight = summaryText.scrollHeight + 'px'; + requestAnimationFrame(() => { + summaryText.style.maxHeight = collapsedHeight + 'px'; + }); + // Swap text after transition ends + summaryText.addEventListener('transitionend', function handler() { + summaryText.removeEventListener('transitionend', handler); + summaryText.textContent = truncated; + }); + toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more"; + } + expanded = !expanded; + }); + + summaryTextWrapper.appendChild(toggleLink); + } else { + // HTML content: use max-height to collapse, preserve full HTML + const collapsedMaxHeight = '4.2em'; // ~3 lines collapsed + summaryText.style.maxHeight = collapsedMaxHeight; + + const toggleLink = document.createElement('a'); + toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more"; + toggleLink.href = '#'; + toggleLink.style.cssText = 'display: inline-block; margin-top: 4px; font-size: 13px; color: ' + + (isDarkSummary ? '#6db3f2' : '#1a5fa8') + '; cursor: pointer; text-decoration: underline;'; + + let expanded = false; + toggleLink.addEventListener('click', (e) => { + e.preventDefault(); + if (!expanded) { + summaryText.style.maxHeight = summaryText.scrollHeight + 'px'; + toggleLink.textContent = browser.i18n.getMessage("summarize_see_less") || "See less"; + } else { + summaryText.style.maxHeight = collapsedMaxHeight; + toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more"; + } + expanded = !expanded; + }); + + // Only show toggle if content is actually taller than collapsed height + requestAnimationFrame(() => { + if (summaryText.scrollHeight > summaryText.clientHeight) { + summaryTextWrapper.appendChild(toggleLink); + } else { + summaryText.style.maxHeight = ''; + summaryText.style.overflow = ''; + } + }); + } } const summaryBody = document.createElement('div'); diff --git a/js/mzta-storage.js b/js/mzta-storage.js index 5d12a54f..a94695e8 100644 --- a/js/mzta-storage.js +++ b/js/mzta-storage.js @@ -198,6 +198,7 @@ export class taStorage { let now = Date.now(); record[taStorage.FIELD_SUMMARY] = { summary: summary_data.summary, + summary_html: summary_data.summary_html || '', error: summary_data.error || false, message: summary_data.message || '', summary_date: summary_data.summary_date instanceof Date diff --git a/js/mzta-summarystore.js b/js/mzta-summarystore.js index 52e3bc2a..733cfdc3 100644 --- a/js/mzta-summarystore.js +++ b/js/mzta-summarystore.js @@ -80,6 +80,7 @@ export class taSummaryStore { return { headerMessageId: data_id, summary: summary.summary, + summary_html: summary.summary_html || '', error: summary.error || false, message: summary.message || '', summary_date: new Date(summary.summary_date || summary.ts), diff --git a/mzta-background.html b/mzta-background.html index 8ac9eda4..9ca91a34 100644 --- a/mzta-background.html +++ b/mzta-background.html @@ -3,6 +3,7 @@ + diff --git a/mzta-background.js b/mzta-background.js index 07961049..ef9f8d61 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -297,9 +297,11 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { case 'chatgpt_saveSummary': async function _saveSummaryFromWebchat(msg) { try { + let summaryHtml = msg.text.trim(); let cleanedSummary = cleanSummaryText(msg.text); const summaryData = { summary: cleanedSummary, + summary_html: summaryHtml, summary_date: new Date(), headerMessageId: msg.headerMessageId }; @@ -558,9 +560,12 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { await cmd.initWorker(); const aiResponse = await cmd.sendPrompt(); let cleanedSummary = cleanSummaryText(aiResponse); + const md = window.markdownit(); + let summaryHtml = md.render(aiResponse); const summaryData = { summary: cleanedSummary, + summary_html: summaryHtml, summary_date: new Date(), headerMessageId: headerMessageId }; From fadae813288e2443d61c01bc69978c83c89d8889 Mon Sep 17 00:00:00 2001 From: mic Date: Thu, 26 Mar 2026 00:00:48 +0100 Subject: [PATCH 4/5] css fix --- js/mzta-compose-script.js | 1 + 1 file changed, 1 insertion(+) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 465e95ae..d541e12f 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -934,6 +934,7 @@ switch (message.command) { while (doc.body.firstChild) { element.appendChild(doc.body.firstChild); } + element.querySelectorAll('p').forEach(p => { p.style.marginBlockStart = '0'; }); } if (summaryData.error) { From 0d51829c8ea3b5e088d2c7cb6aa9c971a0889d8c Mon Sep 17 00:00:00 2001 From: mic Date: Thu, 26 Mar 2026 00:09:06 +0100 Subject: [PATCH 5/5] improved _initSummary to chedk the summarize pref. changed summarize_auto, now defaults to 1. see #580 --- claude-spec/05-options.md | 2 +- mzta-background.js | 4 +++- options/mzta-options-default.js | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/claude-spec/05-options.md b/claude-spec/05-options.md index 81b1747a..b61cd92b 100644 --- a/claude-spec/05-options.md +++ b/claude-spec/05-options.md @@ -95,7 +95,7 @@ These are generated programmatically at the bottom of `mzta-options-default.js` | `spamfilter_enabled_accounts` | `[]` | Accounts where spam filter is active | | `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_auto` | `1` | 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_max_display_length` | `0` | Maximum characters shown in inline summary before truncation. `0` = no limit (show full text). When set, text is truncated at a word boundary and a "See more"/"See less" toggle link is shown. | diff --git a/mzta-background.js b/mzta-background.js index ef9f8d61..29ec2828 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -220,7 +220,9 @@ 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: prefs_default.summarize_auto, summarize_display_mode: prefs_default.summarize_display_mode, summarize_max_display_length: prefs_default.summarize_max_display_length }); + let prefs = await browser.storage.sync.get({ summarize: prefs_default.summarize, summarize_auto: prefs_default.summarize_auto, summarize_display_mode: prefs_default.summarize_display_mode, summarize_max_display_length: prefs_default.summarize_max_display_length }); + + if (!prefs.summarize) return; let message = await browser.messageDisplay.getDisplayedMessage(tabId); if (!message) return; diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index 20dea23d..c7cbfcd8 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -137,10 +137,10 @@ export const prefs_default = { spamfilter: false, spamfilter_threshold: 70, spamfilter_enabled_accounts: [], - summarize_auto: 0, // 0: disabled, 1: manual button, 2: automatic + summarize: false, + summarize_auto: 1, // 0: disabled, 1: manual button, 2: automatic summarize_display_mode: 'inline', // 'inline' or 'webchat' summarize_max_display_length: 0, // 0 = no limit, otherwise max chars shown inline spamfilter_show_msg_panel: true, - summarize: false, ...generated_prefs }