From dcbf9d3dcddc3e7ebb5c1c5fad2408cf4956600e Mon Sep 17 00:00:00 2001 From: Ronald Klarenbeek Date: Fri, 26 Dec 2025 20:39:30 +0100 Subject: [PATCH 01/52] 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. --- messageDisplay/message-content-script.js | 105 ++++++++++++++++++++++ messageDisplay/message-content-styles.css | 20 +++++ mzta-background.js | 84 +++++++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 messageDisplay/message-content-script.js create mode 100644 messageDisplay/message-content-styles.css diff --git a/messageDisplay/message-content-script.js b/messageDisplay/message-content-script.js new file mode 100644 index 00000000..30fc5763 --- /dev/null +++ b/messageDisplay/message-content-script.js @@ -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(); \ No newline at end of file diff --git a/messageDisplay/message-content-styles.css b/messageDisplay/message-content-styles.css new file mode 100644 index 00000000..6a4019c8 --- /dev/null +++ b/messageDisplay/message-content-styles.css @@ -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; +} \ No newline at end of file diff --git a/mzta-background.js b/mzta-background.js index e1d63747..c0199a7d 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -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; +} From bd66a37d354197390b8181d9dfaa89e63d8025eb Mon Sep 17 00:00:00 2001 From: Ronald Klarenbeek Date: Fri, 26 Dec 2025 22:20:13 +0100 Subject: [PATCH 02/52] feat(summary): add auto-summary preference for message previews - Add new preference option to enable automatic AI summarization - Update all localization files with new preference text - Implement preference check in message display script - Add default preference setting in options configuration - Include preference toggle in options UI This adds a user-configurable option to automatically generate and display AI summaries when viewing email messages, with appropriate warnings about data transmission to AI services. --- _locales/cs/messages.json | 6 ++++++ _locales/de/messages.json | 8 ++++++++ _locales/en/messages.json | 8 ++++++++ _locales/es/messages.json | 8 ++++++++ _locales/fr/messages.json | 8 ++++++++ _locales/it/messages.json | 8 ++++++++ _locales/pl/messages.json | 8 ++++++++ _locales/pt-br/messages.json | 6 ++++++ _locales/pt/messages.json | 8 ++++++++ _locales/ru/messages.json | 6 ++++++ _locales/zh_Hans/messages.json | 6 ++++++ messageDisplay/message-content-script.js | 8 ++++++++ options/mzta-options-default.js | 1 + options/mzta-options.html | 11 +++++++++++ 14 files changed, 100 insertions(+) diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index d0b12318..2ee16360 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -1166,5 +1166,11 @@ }, "prefs_OptionText_get_calendar_event_Sparks_wrong_version": { "message": "Pro používání funkcí událostí v kalendáři a úloh, nainstalujte aktualizovanou verzi doplňku ThunderAI Sparks." + }, + "prefs_OptionText_auto_summary": { + "message": "Povolit automatické AI shrnutí pro náhledy zpráv" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Pokud je zaškrtnuto, ThunderAI automaticky vygeneruje a zobrazí AI shrnutí nad e-mailovými zprávami, když jsou otevřeny. Mějte na paměti, že to znamená, že všechny zprávy, které si prohlížíte, budou okamžitě odeslány do nakonfigurované AI služby." } } diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 11ae9c45..ec0bf31a 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -1311,5 +1311,13 @@ }, "Optional_Permission_Denied_Model_Fetching": { "message": "Sie haben die optionale Berechtigung verweigert, die zum Abrufen der Modelle für diese Integration erforderlich ist." + }, + "prefs_OptionText_auto_summary": { + "message": "Automatische KI-Zusammenfassung für Nachrichten-Vorschau aktivieren", + "description": "" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Wenn aktiviert, wird ThunderAI automatisch KI-Zusammenfassungen über E-Mail-Nachrichten generieren und anzeigen, wenn sie geöffnet werden. Beachten Sie, dass dies bedeutet, dass alle Nachrichten, die Sie in der Vorschau anzeigen, sofort an den konfigurierten KI-Dienst gesendet werden.", + "description": "" } } diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 904229c1..99116405 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1700,5 +1700,13 @@ "Optional_Permission_Denied_Model_Fetching": { "message": "You have denied the optional permission needed to fetch models for this integration.", "description": "" + }, + "prefs_OptionText_auto_summary": { + "message": "Enable automatic AI summarization for message previews", + "description": "" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "If checked, ThunderAI will automatically generate and display AI summaries above email messages when they are opened. Note that this means all messages you preview will immediately be sent to the configured AI service.", + "description": "" } } diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 92c85674..ea8bb1ca 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -187,5 +187,13 @@ }, "chatgpt_win_model_warning": { "message": "Por alguna razón, no es posible verificar si el modelo correcto está cargado. Por ahora, puedes pulsar el botón azul para continuar." + }, + "prefs_OptionText_auto_summary": { + "message": "Habilitar resumen automático de IA para vistas previas de mensajes", + "description": "" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Si está activado, ThunderAI generará y mostrará automáticamente resúmenes de IA sobre los mensajes de correo electrónico cuando se abran. Ten en cuenta que esto significa que todos los mensajes que previsualices se enviarán inmediatamente al servicio de IA configurado.", + "description": "" } } diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 2b3f8a52..140dc7e1 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -1311,5 +1311,13 @@ }, "Optional_Permission_Denied_Model_Fetching": { "message": "Vous avez refusé l’autorisation facultative nécessaire pour récupérer les modèles pour cette intégration." + }, + "prefs_OptionText_auto_summary": { + "message": "Activer la synthèse automatique par IA pour les aperçus de messages", + "description": "" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Si cette option est cochée, ThunderAI générera et affichera automatiquement des synthèses par IA au-dessus des messages électroniques lorsqu'ils sont ouverts. Notez que cela signifie que tous les messages que vous prévisualisez seront immédiatement envoyés au service IA configuré.", + "description": "" } } diff --git a/_locales/it/messages.json b/_locales/it/messages.json index ae811d3f..06744e9a 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -1314,5 +1314,13 @@ }, "Optional_Permission_Denied_Model_Fetching": { "message": "Hai negato l’autorizzazione necessaria per recuperare i modelli per questa integrazione." + }, + "prefs_OptionText_auto_summary": { + "message": "Abilita il riassunto automatico AI per le anteprime dei messaggi", + "description": "" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Se abilitata, ThunderAI genererà e mostrerà automaticamente i riassunti AI sopra le email quando vengono aperte. Nota che questo significa che tutte le email che visualizzi in anteprima verranno immediatamente inviate al servizio AI configurato.", + "description": "" } } diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index 0b7b90c1..592fdc55 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -908,5 +908,13 @@ }, "placeholder_selected_html": { "message": "Zaznaczony HTML" + }, + "prefs_OptionText_auto_summary": { + "message": "Włącz automatyczne podsumowanie AI dla podglądów wiadomości", + "description": "" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Jeśli zaznaczone, ThunderAI automatycznie wygeneruje i wyświetli podsumowania AI nad wiadomościami e-mail, gdy zostaną otwarte. Pamiętaj, że oznacza to, że wszystkie wiadomości, które przeglądasz, zostaną natychmiast wysłane do skonfigurowanej usługi AI.", + "description": "" } } diff --git a/_locales/pt-br/messages.json b/_locales/pt-br/messages.json index d9941262..8d8159bc 100644 --- a/_locales/pt-br/messages.json +++ b/_locales/pt-br/messages.json @@ -854,5 +854,11 @@ }, "SpamFilter_PageTitle": { "message": "Gerenciar configurações do filtro de spam" + }, + "prefs_OptionText_auto_summary": { + "message": "Mostrar resumo automático na visualização de mensagens" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Se marcado, um resumo gerado por IA será exibido automaticamente acima das mensagens na visualização." } } diff --git a/_locales/pt/messages.json b/_locales/pt/messages.json index f6656907..fdd892a0 100644 --- a/_locales/pt/messages.json +++ b/_locales/pt/messages.json @@ -172,5 +172,13 @@ }, "chatgpt_win_send": { "message": "Enviar" + }, + "prefs_OptionText_auto_summary": { + "message": "Ativar resumo automático de IA para pré-visualizações de mensagens", + "description": "" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Se ativado, o ThunderAI gerará e mostrará automaticamente resumos de IA acima das mensagens de e-mail quando forem abertas. Note que isso significa que todas as mensagens que você visualizar serão enviadas imediatamente para o serviço de IA configurado.", + "description": "" } } diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 3b17cff4..08be72f1 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -1223,5 +1223,11 @@ }, "OpenAIComp_ClearModelsList_Confirm": { "message": "Вы уверены, что хотите очистить список моделей? Это действие не может быть отменено." + }, + "prefs_OptionText_auto_summary": { + "message": "Показывать автоматическое резюме в просмотре сообщений" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Если отмечено, резюме, сгенерированное ИИ, будет автоматически отображаться над сообщениями в просмотре." } } diff --git a/_locales/zh_Hans/messages.json b/_locales/zh_Hans/messages.json index ccbdaa63..37f8fc25 100644 --- a/_locales/zh_Hans/messages.json +++ b/_locales/zh_Hans/messages.json @@ -919,5 +919,11 @@ }, "customPrompts_form_label_use_diff_viewer_title": { "message": "当操作设置为“替换文本”时,可以选择差异查看器。" + }, + "prefs_OptionText_auto_summary": { + "message": "在消息预览中显示自动摘要" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "如果选中,AI生成的摘要将自动显示在消息预览上方。" } } diff --git a/messageDisplay/message-content-script.js b/messageDisplay/message-content-script.js index 30fc5763..b5ac35b8 100644 --- a/messageDisplay/message-content-script.js +++ b/messageDisplay/message-content-script.js @@ -1,4 +1,12 @@ async function showSummaryPane() { + // Check if auto-summary is enabled in user preferences + const result = await browser.storage.sync.get('auto_summary_enabled'); + + // If auto-summary is disabled or not set, don't show anything + if (!result.auto_summary_enabled) { + return; + } + // Create the summary pane element const summaryPane = document.createElement("div"); summaryPane.className = "thunderai-summary-pane"; diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index 6405c1a0..48174267 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -90,4 +90,5 @@ export const prefs_default = { spamfilter_openai_comp_model: '', spamfilter_google_gemini_model: '', spamfilter_anthropic_model: '', + auto_summary_enabled: false, // Enable automatic AI summarization for message previews } diff --git a/options/mzta-options.html b/options/mzta-options.html index 0e24f172..377f5fe2 100644 --- a/options/mzta-options.html +++ b/options/mzta-options.html @@ -125,6 +125,17 @@ + + + + + + __MSG_prefs_OptionText_add_tags__
From a34394d4429f25fd4c3b616ef76cdf59e04bc7f8 Mon Sep 17 00:00:00 2001 From: Ronald Klarenbeek Date: Fri, 2 Jan 2026 21:57:51 +0100 Subject: [PATCH 03/52] feat(i18n): add auto summary preference strings for es, fr, and sv locales --- _locales/es/messages.json | 6 ++++++ _locales/fr/messages.json | 6 ++++++ _locales/sv/messages.json | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 3e461daf..cacd19b4 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -1301,5 +1301,11 @@ }, "Anthropic_System_Prompt": { "message": "Prompt del sistema" + }, + "prefs_OptionText_auto_summary": { + "message": "Habilitar resumen automático de IA para vistas previas de mensajes" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Si está marcado, ThunderAI generará y mostrará automáticamente resúmenes de IA sobre los mensajes de correo electrónico cuando se abran. Tenga en cuenta que esto significa que todos los mensajes que previsualice se enviarán inmediatamente al servicio de IA configurado." } } diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 89cc8a12..984c175d 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -1335,5 +1335,11 @@ }, "prefs_anthropic_temperature_Info": { "message": "Degré d'aléa injecté dans la réponse. La valeur par défaut est 1,0. La plage de valeurs s'étend di 0,0 à 1,0. Utilisez une température proche de 0,0 pour des tâches analytiques ou des choix multiples, et proche de 1,0 pour des tâches créatives et génératives. Notez que même avec une température de 0,0, les résultats ne seront pas totalement déterministes." + }, + "prefs_OptionText_auto_summary": { + "message": "Activer le résumé automatique par IA pour les aperçus de messages" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Si coché, ThunderAI générera et affichera automatiquement des résumés par IA au-dessus des messages lorsque vous les ouvrirez. Notez que cela signifie que tous les messages que vous prévisualisez seront immédiatement envoyés au service IA configuré." } } diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index 501458e0..f6d2f861 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -67,5 +67,11 @@ }, "prefs_OptionText_chatgpt_win_width": { "message": "Bredd" + }, + "prefs_OptionText_auto_summary": { + "message": "Aktivera automatisk AI-sammanfattning för meddelandeförhandsvisningar" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Om markerad kommer ThunderAI att generera och visa AI-sammanfattningar ovanför e-postmeddelanden när de öppnas. Observera att detta innebär att alla meddelanden du förhandsgranskar kommer att skickas omedelbart till den konfigurerade AI-tjänsten." } } From 3f63cc358043790e969b148f729fe671429e2783 Mon Sep 17 00:00:00 2001 From: Ronald Klarenbeek Date: Fri, 2 Jan 2026 22:42:46 +0100 Subject: [PATCH 04/52] feat(summary): implement new configuration model, fix multilingual support and fix error display - Introduce localized UI strings for auto-summary feature across multiple languages - Standardize error messages and implement consistent error propagation - Refactor summary generation to align with v3.8.0 settings architecture - Enhance user experience with localized loading and error states - Improve code reliability through structured error handling and cleanup --- _locales/en/messages.json | 20 +++++++- _locales/es/messages.json | 16 ++++++ _locales/fr/messages.json | 19 ++++++- _locales/sv/messages.json | 16 ++++++ messageDisplay/message-content-script.js | 22 +++----- mzta-background.js | 64 ++++++++++++++++++------ 6 files changed, 126 insertions(+), 31 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index c94bc27c..77300ad7 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1742,7 +1742,23 @@ "description": "" }, "prefs_OptionText_auto_summary_Info": { - "message": "If checked, ThunderAI will automatically generate and display AI summaries above email messages when they are opened. Note that this means all messages you preview will immediately be sent to the configured AI service.", - "description": "" + "message": "If checked, ThunderAI will automatically generate and display AI summaries above email messages when they are opened. Note that this means all messages you preview will immediately be sent to the configured AI service.", + "description": "" + }, + "auto_summary_title": { + "message": "ThunderAI Summary", + "description": "Title for the auto-summary pane" + }, + "auto_summary_generating": { + "message": "Generating AI summary...", + "description": "Loading text shown while generating summary" + }, + "auto_summary_failed": { + "message": "Failed to generate AI summary. Please confirm your settings and try again.", + "description": "Error message when summary generation fails" + }, + "auto_summary_prompt": { + "message": "Please provide a concise summary of the following email message. The summary should be 3-5 sentences maximum and capture the main points:\n\n", + "description": "Prompt template for AI summary generation" } } diff --git a/_locales/es/messages.json b/_locales/es/messages.json index cacd19b4..86df6187 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -1307,5 +1307,21 @@ }, "prefs_OptionText_auto_summary_Info": { "message": "Si está marcado, ThunderAI generará y mostrará automáticamente resúmenes de IA sobre los mensajes de correo electrónico cuando se abran. Tenga en cuenta que esto significa que todos los mensajes que previsualice se enviarán inmediatamente al servicio de IA configurado." + }, + "auto_summary_title": { + "message": "Resumen de ThunderAI", + "description": "Título del panel de resumen automático" + }, + "auto_summary_generating": { + "message": "Generando resumen de IA...", + "description": "Texto de carga mostrado mientras se genera el resumen" + }, + "auto_summary_failed": { + "message": "Error al generar el resumen de IA. Verifica tu configuración e inténtalo de nuevo.", + "description": "Mensaje de error cuando falla la generación del resumen" + }, + "auto_summary_prompt": { + "message": "Proporciona un resumen conciso del siguiente mensaje de correo electrónico. El resumen debe tener un máximo de 3-5 oraciones y capturar los puntos principales:\n\n", + "description": "Plantilla de prompt para la generación de resúmenes de IA" } } diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 984c175d..dd71b608 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -1340,6 +1340,23 @@ "message": "Activer le résumé automatique par IA pour les aperçus de messages" }, "prefs_OptionText_auto_summary_Info": { - "message": "Si coché, ThunderAI générera et affichera automatiquement des résumés par IA au-dessus des messages lorsque vous les ouvrirez. Notez que cela signifie que tous les messages que vous prévisualisez seront immédiatement envoyés au service IA configuré." + "message": "Si coché, ThunderAI générera et affichera automatiquement des résumés par IA au-dessus des messages lorsque vous les ouvrirez. Notez que cela signifie que tous les messages que vous prévisualisez seront immédiatement envoyés au service IA configuré.", + "description": "" + }, + "auto_summary_title": { + "message": "Résumé ThunderAI", + "description": "Titre du panneau de résumé automatique" + }, + "auto_summary_generating": { + "message": "Génération du résumé IA...", + "description": "Texte de chargement affiché pendant la génération du résumé" + }, + "auto_summary_failed": { + "message": "Échec de la génération du résumé IA. Veuillez vérifier vos paramètres et réessayer.", + "description": "Message d'erreur lorsque la génération du résumé échoue" + }, + "auto_summary_prompt": { + "message": "Veuillez fournir un résumé concis du message suivant. Le résumé doit comporter au maximum 3 à 5 phrases et capturer les points principaux :\n\n", + "description": "Modèle d'invite pour la génération de résumés IA" } } diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index f6d2f861..cee60b99 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -73,5 +73,21 @@ }, "prefs_OptionText_auto_summary_Info": { "message": "Om markerad kommer ThunderAI att generera och visa AI-sammanfattningar ovanför e-postmeddelanden när de öppnas. Observera att detta innebär att alla meddelanden du förhandsgranskar kommer att skickas omedelbart till den konfigurerade AI-tjänsten." + }, + "auto_summary_title": { + "message": "ThunderAI Sammanfattning", + "description": "Titel för den automatiska sammanfattningspanelen" + }, + "auto_summary_generating": { + "message": "Genererar AI-sammanfattning...", + "description": "Laddningstext som visas medan sammanfattningen genereras" + }, + "auto_summary_failed": { + "message": "Misslyckades med att generera AI-sammanfattning. Kontrollera dina inställningar och försök igen.", + "description": "Felmeddelande när sammanfattningsgenereringen misslyckas" + }, + "auto_summary_prompt": { + "message": "Ge en koncis sammanfattning av följande e-postmeddelande. Sammanfattningen bör vara max 3-5 meningar och fånga huvudpunkterna:\n\n", + "description": "Promptmall för AI-sammanfattningsgenerering" } } diff --git a/messageDisplay/message-content-script.js b/messageDisplay/message-content-script.js index b5ac35b8..7bb92c3d 100644 --- a/messageDisplay/message-content-script.js +++ b/messageDisplay/message-content-script.js @@ -14,12 +14,12 @@ async function showSummaryPane() { // Create the title element const summaryTitle = document.createElement("div"); summaryTitle.className = "thunderai-summary-title"; - summaryTitle.innerText = "ThunderAI Summary"; + summaryTitle.innerText = browser.i18n.getMessage("auto_summary_title"); // Create a loading indicator const loadingIndicator = document.createElement("div"); loadingIndicator.className = "thunderai-summary-content"; - loadingIndicator.innerText = "Generating AI summary..."; + loadingIndicator.innerText = browser.i18n.getMessage("auto_summary_generating"); // Create the content element (initially hidden) const summaryContent = document.createElement("div"); @@ -45,12 +45,9 @@ async function showSummaryPane() { 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.innerText = browser.i18n.getMessage("auto_summary_failed"); loadingIndicator.style.color = '#d70022'; - // Fallback to showing truncated message content - const messageContent = getMessageContent(); - loadingIndicator.innerText += "\n\n" + truncateMessageContent(messageContent); } } @@ -83,12 +80,8 @@ 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:`; + // Create a simple summary prompt using localized string + const summaryPrompt = browser.i18n.getMessage("auto_summary_prompt") + cleanedContent; // Request AI summary from the background script return new Promise((resolve, reject) => { @@ -101,9 +94,10 @@ Summary:`; if (response && response.summary) { resolve(response.summary); } else if (response && response.error) { - reject(new Error(response.error)); + // Use the localized error message + reject(new Error(browser.i18n.getMessage("auto_summary_failed"))); } else { - reject(new Error("Failed to get AI summary")); + reject(new Error(browser.i18n.getMessage("auto_summary_failed"))); } }); }); diff --git a/mzta-background.js b/mzta-background.js index daab589e..826091a3 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -250,16 +250,23 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { }); // Use the existing ThunderAI infrastructure + // We need to adapt to the new v3.8.0 settings structure const summary = await generateAISummaryUsingThunderAIInfrastructure( message.content, message.prompt, - prefs + { + ...prefs, + // Add the dynamic settings that the new system expects + connection_type: prefs.connection_type, + // For summary, we don't have specific integration settings yet, + // so we'll use the global connection type + } ); return { summary: summary }; } catch (error) { console.error("[ThunderAI] Error generating summary:", error); - return { error: "Failed to generate AI summary: " + error.message }; + return { error: "Failed to generate AI summary. Please confirm your settings and try again." }; } } return _generate_summary(message); @@ -1226,27 +1233,56 @@ async function generateAISummaryUsingThunderAIInfrastructure(content, prompt, pr // 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 prompt config for summary (similar to how other features do it) + // This adapts to the new v3.8.0 dynamic settings system + const summaryPromptConfig = { + id: 'auto_summary', + name: 'Auto Summary', + model: '', // Model will be determined by getConnectionType + connection_type: prefs.connection_type + }; - // Create a special command instance + // Determine which LLM to use based on user preferences using the new v3.8.0 pattern + const llmType = getConnectionType(prefs, summaryPromptConfig, 'auto_summary'); + + // Get the appropriate model based on the connection type using dynamic settings + let model = ''; + if (prefs.connection_type === 'chatgpt_api') { + model = prefs.chatgpt_model; + } else if (prefs.connection_type === 'ollama_api') { + model = prefs.ollama_model; + } else if (prefs.connection_type === 'openai_comp_api') { + model = prefs.openai_comp_model; + } else if (prefs.connection_type === 'google_gemini_api') { + model = prefs.google_gemini_model; + } else if (prefs.connection_type === 'anthropic_api') { + model = prefs.anthropic_model; + } + + // Create a special command instance with the correct v3.8.0 pattern const summaryCommand = new mzta_specialCommand({ prompt: prompt, llm: llmType, - custom_model: prefs.chatgpt_model, - do_debug: prefs.do_debug + custom_model: model, + do_debug: prefs.do_debug, + config: summaryPromptConfig }); - // Initialize the worker - await summaryCommand.initWorker(); + try { + // Initialize the worker + await summaryCommand.initWorker(); - // Send the prompt and get the AI response - const aiResponse = await summaryCommand.sendPrompt(); + // 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); + // Clean up the response - extract just the summary content + const cleanedResponse = cleanAISummaryResponse(aiResponse); - return cleanedResponse; + return cleanedResponse; + } catch (error) { + console.error("[ThunderAI] Error in AI summary generation:", error); + throw error; // Re-throw to be handled by the caller + } } /** From 90efefd363aef0fdc4e34ebad0f0e1f7f74fde9c Mon Sep 17 00:00:00 2001 From: Ronald Klarenbeek Date: Fri, 2 Jan 2026 23:26:17 +0100 Subject: [PATCH 05/52] feat(i18n): add auto summary feature strings for multiple locales - Add new translation strings for auto summary feature in bg, de, el, eo, es, fr, hr, hu, it, pl, pt-br, pt, ro, sk, and sv locales - Include preference options, title, status messages, and prompt templates - Remove unnecessary description fields from existing strings --- _locales/bg/messages.json | 20 +++++++++++++++++++- _locales/de/messages.json | 18 ++++++++++++++---- _locales/el/messages.json | 20 +++++++++++++++++++- _locales/eo/messages.json | 18 ++++++++++++++++++ _locales/es/messages.json | 12 ++++-------- _locales/fr/messages.json | 15 +++++---------- _locales/hr/messages.json | 20 +++++++++++++++++++- _locales/hu/messages.json | 20 +++++++++++++++++++- _locales/it/messages.json | 12 ++++++++++++ _locales/pl/messages.json | 6 ++---- _locales/pt-br/messages.json | 12 ++++++++++++ _locales/pt/messages.json | 18 ++++++++++++++---- _locales/ro/messages.json | 20 +++++++++++++++++++- _locales/sk/messages.json | 21 ++++++++++++++++++++- _locales/sv/messages.json | 12 ++++-------- 15 files changed, 200 insertions(+), 44 deletions(-) diff --git a/_locales/bg/messages.json b/_locales/bg/messages.json index 70b3549f..7e70cdfc 100644 --- a/_locales/bg/messages.json +++ b/_locales/bg/messages.json @@ -199,5 +199,23 @@ }, "prefs_google_gemini_thinking_budget": { "message": "Бюджет за мислене" + }, + "prefs_OptionText_auto_summary": { + "message": "Показване на автоматично AI резюме за прегледи на съобщения" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Ако е отметнато, ThunderAI автоматично ще генерира и показва AI резюмета над е-пощта, когато те бъдат отворени. Имайте предвид, че това означава, че всички съобщения, които преглеждате, ще бъдат незабавно изпратени до конфигурираната AI услуга." + }, + "auto_summary_title": { + "message": "ThunderAI Резюме" + }, + "auto_summary_generating": { + "message": "Генериране на AI резюме..." + }, + "auto_summary_failed": { + "message": "Неуспешно генериране на AI резюме. Моля, потвърдете настройките си и опитайте отново." + }, + "auto_summary_prompt": { + "message": "Моля, предоставете кратко резюме на следващото съобщение. Резюмето трябва да бъде максимум 3-5 изречения и да обхваща основните точки:\n\n" } -} + } diff --git a/_locales/de/messages.json b/_locales/de/messages.json index ec0bf31a..3421145d 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -1313,11 +1313,21 @@ "message": "Sie haben die optionale Berechtigung verweigert, die zum Abrufen der Modelle für diese Integration erforderlich ist." }, "prefs_OptionText_auto_summary": { - "message": "Automatische KI-Zusammenfassung für Nachrichten-Vorschau aktivieren", - "description": "" + "message": "Automatische KI-Zusammenfassung für Nachrichten-Vorschau aktivieren" }, "prefs_OptionText_auto_summary_Info": { - "message": "Wenn aktiviert, wird ThunderAI automatisch KI-Zusammenfassungen über E-Mail-Nachrichten generieren und anzeigen, wenn sie geöffnet werden. Beachten Sie, dass dies bedeutet, dass alle Nachrichten, die Sie in der Vorschau anzeigen, sofort an den konfigurierten KI-Dienst gesendet werden.", - "description": "" + "message": "Wenn aktiviert, wird ThunderAI automatisch KI-Zusammenfassungen über E-Mail-Nachrichten generieren und anzeigen, wenn sie geöffnet werden. Beachten Sie, dass dies bedeutet, dass alle Nachrichten, die Sie in der Vorschau anzeigen, sofort an den konfigurierten KI-Dienst gesendet werden." + }, + "auto_summary_title": { + "message": "ThunderAI Zusammenfassung" + }, + "auto_summary_generating": { + "message": "KI-Zusammenfassung wird generiert..." + }, + "auto_summary_failed": { + "message": "Fehler beim Generieren der KI-Zusammenfassung. Bitte überprüfen Sie Ihre Einstellungen und versuchen Sie es erneut." + }, + "auto_summary_prompt": { + "message": "Bitte geben Sie eine prägnante Zusammenfassung der folgenden E-Mail-Nachricht. Die Zusammenfassung sollte maximal 3-5 Sätze umfassen und die Hauptpunkte erfassen:\n\n" } } diff --git a/_locales/el/messages.json b/_locales/el/messages.json index a4ed4972..d6c9445a 100644 --- a/_locales/el/messages.json +++ b/_locales/el/messages.json @@ -1277,5 +1277,23 @@ }, "Optional_Permission_Denied_Model_Fetching": { "message": "Έχετε αρνηθεί την προαιρετική άδεια που απαιτείται για την τοποθέτηση μοντέλων για αυτήν την ενσωμάτωση." + }, + "prefs_OptionText_auto_summary": { + "message": "Ενεργοποίηση αυτόματου AI περιλήψεων για προεπισκόπηση μηνυμάτων" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Εάν είναι επιλεγμένο, το ThunderAI θα δημιουργεί και θα εμφανίζει αυτόματα AI περιλήψεις πάνω από τα μηνύματα ηλεκτρονικού ταχυδρομείου όταν ανοίγουν. Σημειώστε ότι αυτό σημαίνει ότι όλα τα μηνύματα που προεπισκοπείτε θα σταλούν αμέσως στην υπηρεσία AI που έχετε διαμορφώσει." + }, + "auto_summary_title": { + "message": "Περίληψη ThunderAI" + }, + "auto_summary_generating": { + "message": "Δημιουργία AI περίληψης..." + }, + "auto_summary_failed": { + "message": "Αποτυχία δημιουργίας AI περίληψης. Παρακαλώ ελέγξτε τις ρυθμίσεις σας και προσπαθήστε ξανά." + }, + "auto_summary_prompt": { + "message": "Παρακαλώ παρέχετε μια σύντομη περίληψη του ακόλουθου μηνύματος ηλεκτρονικού ταχυδρομείου. Η περίληψη θα πρέπει να είναι το πολύ 3-5 προτάσεις και να καταγράφει τα κύρια σημεία:\n\n" } -} + } diff --git a/_locales/eo/messages.json b/_locales/eo/messages.json index 07eeb95f..036ad476 100644 --- a/_locales/eo/messages.json +++ b/_locales/eo/messages.json @@ -172,5 +172,23 @@ }, "customPrompts_substitute_text": { "message": "Anstataŭigi tekston" + }, + "prefs_OptionText_auto_summary": { + "message": "Aŭtomata AI resumo por mesaĝaj antaŭrigardoj" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Se markita, ThunderAI aŭtomate generos kaj montros AI resumojn super retpoŝtaj mesaĝoj kiam ili estas malfermitaj. Bonvolu noti ke tio signifas ke ĉiuj mesaĝoj kiun vi antaŭrigardas estos tuj senditaj al la agordita AI servo." + }, + "auto_summary_title": { + "message": "ThunderAI Resumo" + }, + "auto_summary_generating": { + "message": "Generante AI resumon..." + }, + "auto_summary_failed": { + "message": "Malsukcesis generi AI resumon. Bonvolu kontroli viajn agordojn kaj reprovu." + }, + "auto_summary_prompt": { + "message": "Bonvolu provizi koncizan resumon de la sekva retpoŝta mesaĝo. La resumo devus esti maksimume 3-5 frazoj kaj kapti la ĉefajn punktojn:\n\n" } } diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 86df6187..f5efc168 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -1309,19 +1309,15 @@ "message": "Si está marcado, ThunderAI generará y mostrará automáticamente resúmenes de IA sobre los mensajes de correo electrónico cuando se abran. Tenga en cuenta que esto significa que todos los mensajes que previsualice se enviarán inmediatamente al servicio de IA configurado." }, "auto_summary_title": { - "message": "Resumen de ThunderAI", - "description": "Título del panel de resumen automático" + "message": "Resumen de ThunderAI" }, "auto_summary_generating": { - "message": "Generando resumen de IA...", - "description": "Texto de carga mostrado mientras se genera el resumen" + "message": "Generando resumen de IA..." }, "auto_summary_failed": { - "message": "Error al generar el resumen de IA. Verifica tu configuración e inténtalo de nuevo.", - "description": "Mensaje de error cuando falla la generación del resumen" + "message": "Error al generar el resumen de IA. Verifica tu configuración e inténtalo de nuevo." }, "auto_summary_prompt": { - "message": "Proporciona un resumen conciso del siguiente mensaje de correo electrónico. El resumen debe tener un máximo de 3-5 oraciones y capturar los puntos principales:\n\n", - "description": "Plantilla de prompt para la generación de resúmenes de IA" + "message": "Proporciona un resumen conciso del siguiente mensaje de correo electrónico. El resumen debe tener un máximo de 3-5 oraciones y capturar los puntos principales:\n\n" } } diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index dd71b608..e117832c 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -1340,23 +1340,18 @@ "message": "Activer le résumé automatique par IA pour les aperçus de messages" }, "prefs_OptionText_auto_summary_Info": { - "message": "Si coché, ThunderAI générera et affichera automatiquement des résumés par IA au-dessus des messages lorsque vous les ouvrirez. Notez que cela signifie que tous les messages que vous prévisualisez seront immédiatement envoyés au service IA configuré.", - "description": "" + "message": "Si coché, ThunderAI générera et affichera automatiquement des résumés par IA au-dessus des messages lorsque vous les ouvrirez. Notez que cela signifie que tous les messages que vous prévisualisez seront immédiatement envoyés au service IA configuré." }, "auto_summary_title": { - "message": "Résumé ThunderAI", - "description": "Titre du panneau de résumé automatique" + "message": "Résumé ThunderAI" }, "auto_summary_generating": { - "message": "Génération du résumé IA...", - "description": "Texte de chargement affiché pendant la génération du résumé" + "message": "Génération du résumé IA..." }, "auto_summary_failed": { - "message": "Échec de la génération du résumé IA. Veuillez vérifier vos paramètres et réessayer.", - "description": "Message d'erreur lorsque la génération du résumé échoue" + "message": "Échec de la génération du résumé IA. Veuillez vérifier vos paramètres et réessayer." }, "auto_summary_prompt": { - "message": "Veuillez fournir un résumé concis du message suivant. Le résumé doit comporter au maximum 3 à 5 phrases et capturer les points principaux :\n\n", - "description": "Modèle d'invite pour la génération de résumés IA" + "message": "Veuillez fournir un résumé concis du message suivant. Le résumé doit comporter au maximum 3 à 5 phrases et capturer les points principaux :\n\n" } } diff --git a/_locales/hr/messages.json b/_locales/hr/messages.json index 2b67fc51..eda8e3a1 100644 --- a/_locales/hr/messages.json +++ b/_locales/hr/messages.json @@ -854,5 +854,23 @@ }, "prefs_OptionText_openai_comp_info_remote": { "message": "Ovdje možete unijeti i adresu udaljenog poslužitelja." + }, + "prefs_OptionText_auto_summary": { + "message": "Omogući automatsko AI sažimanje za pregled poruka" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Ako je označeno, ThunderAI će automatski generirati i prikazivati AI sažetke iznad e-poruka kada se otvore. Imajte na umu da će to značiti da će sve poruke koje pregledavate biti odmah poslane na konfiguriranu AI uslugu." + }, + "auto_summary_title": { + "message": "ThunderAI Sažetak" + }, + "auto_summary_generating": { + "message": "Generiranje AI sažetka..." + }, + "auto_summary_failed": { + "message": "Neuspjelo generiranje AI sažetka. Molimo provjerite svoje postavke i pokušajte ponovno." + }, + "auto_summary_prompt": { + "message": "Molimo pružite sažetak sljedeće e-poruke. Sažetak bi trebao biti maksimalno 3-5 rečenica i sadržavati glavne točke:\n\n" } -} + } diff --git a/_locales/hu/messages.json b/_locales/hu/messages.json index 62e99942..9c05d5ce 100644 --- a/_locales/hu/messages.json +++ b/_locales/hu/messages.json @@ -13,5 +13,23 @@ }, "prompt_rewrite_formal": { "message": "Újraírás formálisan" + }, + "prefs_OptionText_auto_summary": { + "message": "Automatikus AI összefoglaló engedélyezése az üzenetelőnézetekhez" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Ha be van jelölve, a ThunderAI automatikusan generál és megjelenít AI összefoglalókat az e-mail üzenetek felett, amikor megnyitják őket. Vegye figyelembe, hogy ez azt jelenti, hogy az összes üzenet, amelyet előnézetben megtekint, azonnal elküldésre kerül a konfigurált AI szolgáltatáshoz." + }, + "auto_summary_title": { + "message": "ThunderAI Összefoglaló" + }, + "auto_summary_generating": { + "message": "AI összefoglaló generálása..." + }, + "auto_summary_failed": { + "message": "Nem sikerült generálni az AI összefoglalót. Kérjük, ellenőrizze a beállításait, és próbálja újra." + }, + "auto_summary_prompt": { + "message": "Kérjük, adjon egy rövid összefoglalót a következő e-mail üzenetről. Az összefoglaló maximum 3-5 mondatból állhat, és tartalmazza a fő pontokat:\n\n" } -} + } diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 06744e9a..c695914b 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -1322,5 +1322,17 @@ "prefs_OptionText_auto_summary_Info": { "message": "Se abilitata, ThunderAI genererà e mostrerà automaticamente i riassunti AI sopra le email quando vengono aperte. Nota che questo significa che tutte le email che visualizzi in anteprima verranno immediatamente inviate al servizio AI configurato.", "description": "" + }, + "auto_summary_title": { + "message": "Riassunto ThunderAI" + }, + "auto_summary_generating": { + "message": "Generazione del riassunto AI..." + }, + "auto_summary_failed": { + "message": "Impossibile generare il riassunto AI. Verificare le impostazioni e riprovare." + }, + "auto_summary_prompt": { + "message": "Fornire un riassunto conciso del seguente messaggio email. Il riassunto dovrebbe essere di massimo 3-5 frasi e catturare i punti principali:\n\n" } } diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index 592fdc55..88650f76 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -910,11 +910,9 @@ "message": "Zaznaczony HTML" }, "prefs_OptionText_auto_summary": { - "message": "Włącz automatyczne podsumowanie AI dla podglądów wiadomości", - "description": "" + "message": "Włącz automatyczne podsumowanie AI dla podglądów wiadomości" }, "prefs_OptionText_auto_summary_Info": { - "message": "Jeśli zaznaczone, ThunderAI automatycznie wygeneruje i wyświetli podsumowania AI nad wiadomościami e-mail, gdy zostaną otwarte. Pamiętaj, że oznacza to, że wszystkie wiadomości, które przeglądasz, zostaną natychmiast wysłane do skonfigurowanej usługi AI.", - "description": "" + "message": "Jeśli zaznaczone, ThunderAI automatycznie wygeneruje i wyświetli podsumowania AI nad wiadomościami e-mail, gdy zostaną otwarte. Pamiętaj, że oznacza to, że wszystkie wiadomości, które przeglądasz, zostaną natychmiast wysłane do skonfigurowanej usługi AI." } } diff --git a/_locales/pt-br/messages.json b/_locales/pt-br/messages.json index 8d8159bc..a27d6a8c 100644 --- a/_locales/pt-br/messages.json +++ b/_locales/pt-br/messages.json @@ -860,5 +860,17 @@ }, "prefs_OptionText_auto_summary_Info": { "message": "Se marcado, um resumo gerado por IA será exibido automaticamente acima das mensagens na visualização." + }, + "auto_summary_title": { + "message": "Resumo ThunderAI" + }, + "auto_summary_generating": { + "message": "Gerando resumo de IA..." + }, + "auto_summary_failed": { + "message": "Falha ao gerar o resumo de IA. Verifique suas configurações e tente novamente." + }, + "auto_summary_prompt": { + "message": "Forneça um resumo conciso da seguinte mensagem de e-mail. O resumo deve ter no máximo 3-5 frases e capturar os pontos principais:\n\n" } } diff --git a/_locales/pt/messages.json b/_locales/pt/messages.json index fdd892a0..dfbe2fdf 100644 --- a/_locales/pt/messages.json +++ b/_locales/pt/messages.json @@ -174,11 +174,21 @@ "message": "Enviar" }, "prefs_OptionText_auto_summary": { - "message": "Ativar resumo automático de IA para pré-visualizações de mensagens", - "description": "" + "message": "Ativar resumo automático de IA para pré-visualizações de mensagens" }, "prefs_OptionText_auto_summary_Info": { - "message": "Se ativado, o ThunderAI gerará e mostrará automaticamente resumos de IA acima das mensagens de e-mail quando forem abertas. Note que isso significa que todas as mensagens que você visualizar serão enviadas imediatamente para o serviço de IA configurado.", - "description": "" + "message": "Se ativado, o ThunderAI gerará e mostrará automaticamente resumos de IA acima das mensagens de e-mail quando forem abertas. Note que isso significa que todas as mensagens que você visualizar serão enviadas imediatamente para o serviço de IA configurado." + }, + "auto_summary_title": { + "message": "Resumo ThunderAI" + }, + "auto_summary_generating": { + "message": "Gerando resumo de IA..." + }, + "auto_summary_failed": { + "message": "Falha ao gerar o resumo de IA. Verifique suas configurações e tente novamente." + }, + "auto_summary_prompt": { + "message": "Forneça um resumo conciso da seguinte mensagem de e-mail. O resumo deve ter no máximo 3-5 frases e capturar os pontos principais:\n\n" } } diff --git a/_locales/ro/messages.json b/_locales/ro/messages.json index dbecf17f..f98b7f55 100644 --- a/_locales/ro/messages.json +++ b/_locales/ro/messages.json @@ -229,5 +229,23 @@ }, "prefsInfoDesc_1": { "message": "S-ar putea ca interfața web ChatGPT să se modifice într-un mod care să perturbe funcționarea addon-ului. Verificați pagina \"Starea serviciului\" accesibilă prin legătura din partea de jos a acestei pagini. De asemenea, rețineți că prima dată când utilizați ThunderAI, trebuie să vă conectați la ChatGPT." + }, + "prefs_OptionText_auto_summary": { + "message": "Activează rezumatul automat AI pentru previzualizările mesajelor" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Dacă este bifată, ThunderAI va genera și afișa automat rezumate AI deasupra mesajelor de e-mail atunci când sunt deschise. Rețineți că acest lucru înseamnă că toate mesajele pe care le previzualizați vor fi trimise imediat către serviciul AI configurat." + }, + "auto_summary_title": { + "message": "Rezumat ThunderAI" + }, + "auto_summary_generating": { + "message": "Generare rezumat AI..." + }, + "auto_summary_failed": { + "message": "Nu s-a putut genera rezumatul AI. Vă rugăm să verificați setările și să încercați din nou." + }, + "auto_summary_prompt": { + "message": "Vă rugăm să furnizați un rezumat concis al următorului mesaj de e-mail. Rezumatul ar trebui să aibă maximum 3-5 propoziții și să cuprindă punctele principale:\n\n" } -} + } diff --git a/_locales/sk/messages.json b/_locales/sk/messages.json index 0967ef42..b076c05d 100644 --- a/_locales/sk/messages.json +++ b/_locales/sk/messages.json @@ -1 +1,20 @@ -{} +{ + "prefs_OptionText_auto_summary": { + "message": "Povoliť automatické AI zhrnutie pre náhľady správ" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Ak je zaškrtnuté, ThunderAI automaticky vygeneruje a zobrazí AI zhrnutia nad e-mailovými správami, keď sú otvorené. Mějte na pamäti, že to znamená, že všetky správy, ktoré si prezeráte, budú okamžite odoslané do nakonfigurovanej AI služby." + }, + "auto_summary_title": { + "message": "Zhrnutie ThunderAI" + }, + "auto_summary_generating": { + "message": "Generovanie AI zhrnutia..." + }, + "auto_summary_failed": { + "message": "Zlyhanie generovania AI zhrnutia. Skontrolujte svoje nastavenia a skúste to znova." + }, + "auto_summary_prompt": { + "message": "Poskytnite stručné zhrnutie nasledujúcej e-mailovej správy. Zhrnutie by malo mať maximálne 3-5 viet a zachytiť hlavné body:\n\n" + } +} diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index cee60b99..8ded988f 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -75,19 +75,15 @@ "message": "Om markerad kommer ThunderAI att generera och visa AI-sammanfattningar ovanför e-postmeddelanden när de öppnas. Observera att detta innebär att alla meddelanden du förhandsgranskar kommer att skickas omedelbart till den konfigurerade AI-tjänsten." }, "auto_summary_title": { - "message": "ThunderAI Sammanfattning", - "description": "Titel för den automatiska sammanfattningspanelen" + "message": "ThunderAI Sammanfattning" }, "auto_summary_generating": { - "message": "Genererar AI-sammanfattning...", - "description": "Laddningstext som visas medan sammanfattningen genereras" + "message": "Genererar AI-sammanfattning..." }, "auto_summary_failed": { - "message": "Misslyckades med att generera AI-sammanfattning. Kontrollera dina inställningar och försök igen.", - "description": "Felmeddelande när sammanfattningsgenereringen misslyckas" + "message": "Misslyckades med att generera AI-sammanfattning. Kontrollera dina inställningar och försök igen." }, "auto_summary_prompt": { - "message": "Ge en koncis sammanfattning av följande e-postmeddelande. Sammanfattningen bör vara max 3-5 meningar och fånga huvudpunkterna:\n\n", - "description": "Promptmall för AI-sammanfattningsgenerering" + "message": "Ge en koncis sammanfattning av följande e-postmeddelande. Sammanfattningen bör vara max 3-5 meningar och fånga huvudpunkterna:\n\n" } } From e5f5d08582251647d2c68ad4042e3c69169e43b6 Mon Sep 17 00:00:00 2001 From: Ronald Klarenbeek Date: Fri, 2 Jan 2026 23:40:35 +0100 Subject: [PATCH 06/52] feat(i18n): add auto summary strings for cs, ru, zh_Hans, and zh_Hant locales - Add new translation strings for auto summary feature - Include title, generating, failed, and prompt messages - Remove unnecessary description fields from en locale - Add missing auto summary preference strings for zh_Hant locale --- _locales/cs/messages.json | 12 ++++++++++++ _locales/en/messages.json | 6 ------ _locales/ru/messages.json | 12 ++++++++++++ _locales/zh_Hans/messages.json | 12 ++++++++++++ _locales/zh_Hant/messages.json | 18 ++++++++++++++++++ 5 files changed, 54 insertions(+), 6 deletions(-) diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index 2ee16360..3586bea0 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -1172,5 +1172,17 @@ }, "prefs_OptionText_auto_summary_Info": { "message": "Pokud je zaškrtnuto, ThunderAI automaticky vygeneruje a zobrazí AI shrnutí nad e-mailovými zprávami, když jsou otevřeny. Mějte na paměti, že to znamená, že všechny zprávy, které si prohlížíte, budou okamžitě odeslány do nakonfigurované AI služby." + }, + "auto_summary_title": { + "message": "ThunderAI Souhrn" + }, + "auto_summary_generating": { + "message": "Generování AI souhrnu..." + }, + "auto_summary_failed": { + "message": "Nepodařilo se vygenerovat AI souhrn. Zkontrolujte prosím nastavení a zkuste to znovu." + }, + "auto_summary_prompt": { + "message": "Poskytněte stručný souhrn následující e-mailové zprávy. Souhrn by měl obsahovat maximálně 3-5 vět a zachytit hlavní body:\n\n" } } diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 77300ad7..d15784e9 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1739,26 +1739,20 @@ }, "prefs_OptionText_auto_summary": { "message": "Enable automatic AI summarization for message previews", - "description": "" }, "prefs_OptionText_auto_summary_Info": { "message": "If checked, ThunderAI will automatically generate and display AI summaries above email messages when they are opened. Note that this means all messages you preview will immediately be sent to the configured AI service.", - "description": "" }, "auto_summary_title": { "message": "ThunderAI Summary", - "description": "Title for the auto-summary pane" }, "auto_summary_generating": { "message": "Generating AI summary...", - "description": "Loading text shown while generating summary" }, "auto_summary_failed": { "message": "Failed to generate AI summary. Please confirm your settings and try again.", - "description": "Error message when summary generation fails" }, "auto_summary_prompt": { "message": "Please provide a concise summary of the following email message. The summary should be 3-5 sentences maximum and capture the main points:\n\n", - "description": "Prompt template for AI summary generation" } } diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 08be72f1..6f93f42b 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -1229,5 +1229,17 @@ }, "prefs_OptionText_auto_summary_Info": { "message": "Если отмечено, резюме, сгенерированное ИИ, будет автоматически отображаться над сообщениями в просмотре." + }, + "auto_summary_title": { + "message": "ThunderAI Резюме" + }, + "auto_summary_generating": { + "message": "Генерация AI резюме..." + }, + "auto_summary_failed": { + "message": "Не удалось сгенерировать AI резюме. Пожалуйста, проверьте настройки и попробуйте снова." + }, + "auto_summary_prompt": { + "message": "Пожалуйста, предоставьте краткое резюме следующего сообщения электронной почты. Резюме должно содержать максимум 3-5 предложений и отражать основные моменты:\n\n" } } diff --git a/_locales/zh_Hans/messages.json b/_locales/zh_Hans/messages.json index 37f8fc25..9f5aa903 100644 --- a/_locales/zh_Hans/messages.json +++ b/_locales/zh_Hans/messages.json @@ -925,5 +925,17 @@ }, "prefs_OptionText_auto_summary_Info": { "message": "如果选中,AI生成的摘要将自动显示在消息预览上方。" + }, + "auto_summary_title": { + "message": "ThunderAI 摘要" + }, + "auto_summary_generating": { + "message": "正在生成AI摘要..." + }, + "auto_summary_failed": { + "message": "生成AI摘要失败。请确认您的设置并重试。" + }, + "auto_summary_prompt": { + "message": "请提供以下电子邮件消息的简明摘要。摘要应最多包含3-5个句子,并捕捉主要要点:\n\n" } } diff --git a/_locales/zh_Hant/messages.json b/_locales/zh_Hant/messages.json index 22158154..59e2dfbc 100644 --- a/_locales/zh_Hant/messages.json +++ b/_locales/zh_Hant/messages.json @@ -1241,5 +1241,23 @@ }, "OpenAIComp_ClearModelsList_Confirm": { "message": "確定要清除模型清單嗎?這個動作無法復原。" + }, + "prefs_OptionText_auto_summary": { + "message": "啟用郵件預覽的自動 AI 摘要" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "如果勾選,ThunderAI 將自動生成並顯示 AI 摘要在電子郵件訊息上方。請注意,這意味著您預覽的所有訊息將立即被發送到已配置的 AI 服務。" + }, + "auto_summary_title": { + "message": "ThunderAI 摘要" + }, + "auto_summary_generating": { + "message": "正在產生 AI 摘要..." + }, + "auto_summary_failed": { + "message": "產生 AI 摘要失敗。請確認您的設定並重試。" + }, + "auto_summary_prompt": { + "message": "請提供以下電子郵件訊息的簡明摘要。摘要應最多包含3-5個句子,並捕捉主要要點:\n\n" } } From af2ae047dd50c5bbee2bfd89476aab5f4c6f24b4 Mon Sep 17 00:00:00 2001 From: Ronald Klarenbeek Date: Fri, 2 Jan 2026 23:52:08 +0100 Subject: [PATCH 07/52] fix en locale structure breaking the extension --- _locales/en/messages.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d15784e9..4a7e3984 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1738,21 +1738,21 @@ "description": "" }, "prefs_OptionText_auto_summary": { - "message": "Enable automatic AI summarization for message previews", + "message": "Enable automatic AI summarization for message previews" }, "prefs_OptionText_auto_summary_Info": { - "message": "If checked, ThunderAI will automatically generate and display AI summaries above email messages when they are opened. Note that this means all messages you preview will immediately be sent to the configured AI service.", + "message": "If checked, ThunderAI will automatically generate and display AI summaries above email messages when they are opened. Note that this means all messages you preview will immediately be sent to the configured AI service." }, "auto_summary_title": { - "message": "ThunderAI Summary", + "message": "ThunderAI Summary" }, "auto_summary_generating": { - "message": "Generating AI summary...", + "message": "Generating AI summary..." }, "auto_summary_failed": { - "message": "Failed to generate AI summary. Please confirm your settings and try again.", + "message": "Failed to generate AI summary. Please confirm your settings and try again." }, "auto_summary_prompt": { - "message": "Please provide a concise summary of the following email message. The summary should be 3-5 sentences maximum and capture the main points:\n\n", + "message": "Please provide a concise summary of the following email message. The summary should be 3-5 sentences maximum and capture the main points:\n\n" } } From b60e7588025af2aa4d99284a50a53e6fe0ae89e9 Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 17 Feb 2026 22:55:26 +0100 Subject: [PATCH 08/52] removing auto translation from locales files --- _locales/bg/messages.json | 20 +------------------- _locales/cs/messages.json | 18 ------------------ _locales/de/messages.json | 18 ------------------ _locales/el/messages.json | 20 +------------------- _locales/eo/messages.json | 18 ------------------ _locales/es/messages.json | 18 ------------------ _locales/fr/messages.json | 18 ------------------ _locales/hr/messages.json | 20 +------------------- _locales/hu/messages.json | 20 +------------------- _locales/it/messages.json | 20 -------------------- _locales/pl/messages.json | 6 ------ _locales/pt-br/messages.json | 18 ------------------ _locales/pt/messages.json | 18 ------------------ _locales/ro/messages.json | 20 +------------------- _locales/ru/messages.json | 18 ------------------ _locales/sk/messages.json | 21 +-------------------- _locales/sv/messages.json | 18 ------------------ _locales/zh_Hans/messages.json | 18 ------------------ _locales/zh_Hant/messages.json | 18 ------------------ 19 files changed, 6 insertions(+), 339 deletions(-) diff --git a/_locales/bg/messages.json b/_locales/bg/messages.json index 7e70cdfc..70b3549f 100644 --- a/_locales/bg/messages.json +++ b/_locales/bg/messages.json @@ -199,23 +199,5 @@ }, "prefs_google_gemini_thinking_budget": { "message": "Бюджет за мислене" - }, - "prefs_OptionText_auto_summary": { - "message": "Показване на автоматично AI резюме за прегледи на съобщения" - }, - "prefs_OptionText_auto_summary_Info": { - "message": "Ако е отметнато, ThunderAI автоматично ще генерира и показва AI резюмета над е-пощта, когато те бъдат отворени. Имайте предвид, че това означава, че всички съобщения, които преглеждате, ще бъдат незабавно изпратени до конфигурираната AI услуга." - }, - "auto_summary_title": { - "message": "ThunderAI Резюме" - }, - "auto_summary_generating": { - "message": "Генериране на AI резюме..." - }, - "auto_summary_failed": { - "message": "Неуспешно генериране на AI резюме. Моля, потвърдете настройките си и опитайте отново." - }, - "auto_summary_prompt": { - "message": "Моля, предоставете кратко резюме на следващото съобщение. Резюмето трябва да бъде максимум 3-5 изречения и да обхваща основните точки:\n\n" } - } +} diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index 8daf45f7..faa6b283 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -1130,23 +1130,5 @@ }, "prefs_OptionText_get_calendar_event_Sparks_wrong_version": { "message": "Pro používání funkcí událostí v kalendáři a úloh, nainstalujte aktualizovanou verzi doplňku ThunderAI Sparks." - }, - "prefs_OptionText_auto_summary": { - "message": "Povolit automatické AI shrnutí pro náhledy zpráv" - }, - "prefs_OptionText_auto_summary_Info": { - "message": "Pokud je zaškrtnuto, ThunderAI automaticky vygeneruje a zobrazí AI shrnutí nad e-mailovými zprávami, když jsou otevřeny. Mějte na paměti, že to znamená, že všechny zprávy, které si prohlížíte, budou okamžitě odeslány do nakonfigurované AI služby." - }, - "auto_summary_title": { - "message": "ThunderAI Souhrn" - }, - "auto_summary_generating": { - "message": "Generování AI souhrnu..." - }, - "auto_summary_failed": { - "message": "Nepodařilo se vygenerovat AI souhrn. Zkontrolujte prosím nastavení a zkuste to znovu." - }, - "auto_summary_prompt": { - "message": "Poskytněte stručný souhrn následující e-mailové zprávy. Souhrn by měl obsahovat maximálně 3-5 vět a zachytit hlavní body:\n\n" } } diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 2067f85a..7515b3a0 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -1276,24 +1276,6 @@ "Optional_Permission_Denied_Model_Fetching": { "message": "Sie haben die optionale Berechtigung verweigert, die zum Abrufen der Modelle für diese Integration erforderlich ist." }, - "prefs_OptionText_auto_summary": { - "message": "Automatische KI-Zusammenfassung für Nachrichten-Vorschau aktivieren" - }, - "prefs_OptionText_auto_summary_Info": { - "message": "Wenn aktiviert, wird ThunderAI automatisch KI-Zusammenfassungen über E-Mail-Nachrichten generieren und anzeigen, wenn sie geöffnet werden. Beachten Sie, dass dies bedeutet, dass alle Nachrichten, die Sie in der Vorschau anzeigen, sofort an den konfigurierten KI-Dienst gesendet werden." - }, - "auto_summary_title": { - "message": "ThunderAI Zusammenfassung" - }, - "auto_summary_generating": { - "message": "KI-Zusammenfassung wird generiert..." - }, - "auto_summary_failed": { - "message": "Fehler beim Generieren der KI-Zusammenfassung. Bitte überprüfen Sie Ihre Einstellungen und versuchen Sie es erneut." - }, - "auto_summary_prompt": { - "message": "Bitte geben Sie eine prägnante Zusammenfassung der folgenden E-Mail-Nachricht. Die Zusammenfassung sollte maximal 3-5 Sätze umfassen und die Hauptpunkte erfassen:\n\n" - }, "prompt_string": { "message": "Prompt" }, diff --git a/_locales/el/messages.json b/_locales/el/messages.json index 7c29bdd5..5f6e056f 100644 --- a/_locales/el/messages.json +++ b/_locales/el/messages.json @@ -1260,24 +1260,6 @@ "Optional_Permission_Denied_Model_Fetching": { "message": "Έχετε αρνηθεί την προαιρετική άδεια που απαιτείται για την τοποθέτηση μοντέλων για αυτήν την ενσωμάτωση." }, - "prefs_OptionText_auto_summary": { - "message": "Ενεργοποίηση αυτόματου AI περιλήψεων για προεπισκόπηση μηνυμάτων" - }, - "prefs_OptionText_auto_summary_Info": { - "message": "Εάν είναι επιλεγμένο, το ThunderAI θα δημιουργεί και θα εμφανίζει αυτόματα AI περιλήψεις πάνω από τα μηνύματα ηλεκτρονικού ταχυδρομείου όταν ανοίγουν. Σημειώστε ότι αυτό σημαίνει ότι όλα τα μηνύματα που προεπισκοπείτε θα σταλούν αμέσως στην υπηρεσία AI που έχετε διαμορφώσει." - }, - "auto_summary_title": { - "message": "Περίληψη ThunderAI" - }, - "auto_summary_generating": { - "message": "Δημιουργία AI περίληψης..." - }, - "auto_summary_failed": { - "message": "Αποτυχία δημιουργίας AI περίληψης. Παρακαλώ ελέγξτε τις ρυθμίσεις σας και προσπαθήστε ξανά." - }, - "auto_summary_prompt": { - "message": "Παρακαλώ παρέχετε μια σύντομη περίληψη του ακόλουθου μηνύματος ηλεκτρονικού ταχυδρομείου. Η περίληψη θα πρέπει να είναι το πολύ 3-5 προτάσεις και να καταγράφει τα κύρια σημεία:\n\n" - }, "reset": { "message": "Επαναφορά" }, @@ -1431,4 +1413,4 @@ "copy_text": { "message": "αντιγραφή" } - } +} diff --git a/_locales/eo/messages.json b/_locales/eo/messages.json index 123f9bf6..34747628 100644 --- a/_locales/eo/messages.json +++ b/_locales/eo/messages.json @@ -172,23 +172,5 @@ }, "customPrompts_substitute_text": { "message": "Anstataŭigi tekston" - }, - "prefs_OptionText_auto_summary": { - "message": "Aŭtomata AI resumo por mesaĝaj antaŭrigardoj" - }, - "prefs_OptionText_auto_summary_Info": { - "message": "Se markita, ThunderAI aŭtomate generos kaj montros AI resumojn super retpoŝtaj mesaĝoj kiam ili estas malfermitaj. Bonvolu noti ke tio signifas ke ĉiuj mesaĝoj kiun vi antaŭrigardas estos tuj senditaj al la agordita AI servo." - }, - "auto_summary_title": { - "message": "ThunderAI Resumo" - }, - "auto_summary_generating": { - "message": "Generante AI resumon..." - }, - "auto_summary_failed": { - "message": "Malsukcesis generi AI resumon. Bonvolu kontroli viajn agordojn kaj reprovu." - }, - "auto_summary_prompt": { - "message": "Bonvolu provizi koncizan resumon de la sekva retpoŝta mesaĝo. La resumo devus esti maksimume 3-5 frazoj kaj kapti la ĉefajn punktojn:\n\n" } } diff --git a/_locales/es/messages.json b/_locales/es/messages.json index ece49459..e0c082c6 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -1284,24 +1284,6 @@ "Anthropic_System_Prompt": { "message": "Prompt del sistema" }, - "prefs_OptionText_auto_summary": { - "message": "Habilitar resumen automático de IA para vistas previas de mensajes" - }, - "prefs_OptionText_auto_summary_Info": { - "message": "Si está marcado, ThunderAI generará y mostrará automáticamente resúmenes de IA sobre los mensajes de correo electrónico cuando se abran. Tenga en cuenta que esto significa que todos los mensajes que previsualice se enviarán inmediatamente al servicio de IA configurado." - }, - "auto_summary_title": { - "message": "Resumen de ThunderAI" - }, - "auto_summary_generating": { - "message": "Generando resumen de IA..." - }, - "auto_summary_failed": { - "message": "Error al generar el resumen de IA. Verifica tu configuración e inténtalo de nuevo." - }, - "auto_summary_prompt": { - "message": "Proporciona un resumen conciso del siguiente mensaje de correo electrónico. El resumen debe tener un máximo de 3-5 oraciones y capturar los puntos principales:\n\n" - }, "reset": { "message": "Reiniciar" } diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index b1614cea..6a3ef008 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -1300,24 +1300,6 @@ "prefs_anthropic_temperature_Info": { "message": "Degré d'aléa injecté dans la réponse. La valeur par défaut est 1,0. La plage de valeurs s'étend di 0,0 à 1,0. Utilisez une température proche de 0,0 pour des tâches analytiques ou des choix multiples, et proche de 1,0 pour des tâches créatives et génératives. Notez que même avec une température de 0,0, les résultats ne seront pas totalement déterministes." }, - "prefs_OptionText_auto_summary": { - "message": "Activer le résumé automatique par IA pour les aperçus de messages" - }, - "prefs_OptionText_auto_summary_Info": { - "message": "Si coché, ThunderAI générera et affichera automatiquement des résumés par IA au-dessus des messages lorsque vous les ouvrirez. Notez que cela signifie que tous les messages que vous prévisualisez seront immédiatement envoyés au service IA configuré." - }, - "auto_summary_title": { - "message": "Résumé ThunderAI" - }, - "auto_summary_generating": { - "message": "Génération du résumé IA..." - }, - "auto_summary_failed": { - "message": "Échec de la génération du résumé IA. Veuillez vérifier vos paramètres et réessayer." - }, - "auto_summary_prompt": { - "message": "Veuillez fournir un résumé concis du message suivant. Le résumé doit comporter au maximum 3 à 5 phrases et capturer les points principaux :\n\n" - }, "reset": { "message": "Réinitialiser" } diff --git a/_locales/hr/messages.json b/_locales/hr/messages.json index a4b191fb..570131bd 100644 --- a/_locales/hr/messages.json +++ b/_locales/hr/messages.json @@ -836,23 +836,5 @@ }, "prefs_OptionText_openai_comp_info_remote": { "message": "Ovdje možete unijeti i adresu udaljenog poslužitelja." - }, - "prefs_OptionText_auto_summary": { - "message": "Omogući automatsko AI sažimanje za pregled poruka" - }, - "prefs_OptionText_auto_summary_Info": { - "message": "Ako je označeno, ThunderAI će automatski generirati i prikazivati AI sažetke iznad e-poruka kada se otvore. Imajte na umu da će to značiti da će sve poruke koje pregledavate biti odmah poslane na konfiguriranu AI uslugu." - }, - "auto_summary_title": { - "message": "ThunderAI Sažetak" - }, - "auto_summary_generating": { - "message": "Generiranje AI sažetka..." - }, - "auto_summary_failed": { - "message": "Neuspjelo generiranje AI sažetka. Molimo provjerite svoje postavke i pokušajte ponovno." - }, - "auto_summary_prompt": { - "message": "Molimo pružite sažetak sljedeće e-poruke. Sažetak bi trebao biti maksimalno 3-5 rečenica i sadržavati glavne točke:\n\n" } - } +} diff --git a/_locales/hu/messages.json b/_locales/hu/messages.json index 9c05d5ce..62e99942 100644 --- a/_locales/hu/messages.json +++ b/_locales/hu/messages.json @@ -13,23 +13,5 @@ }, "prompt_rewrite_formal": { "message": "Újraírás formálisan" - }, - "prefs_OptionText_auto_summary": { - "message": "Automatikus AI összefoglaló engedélyezése az üzenetelőnézetekhez" - }, - "prefs_OptionText_auto_summary_Info": { - "message": "Ha be van jelölve, a ThunderAI automatikusan generál és megjelenít AI összefoglalókat az e-mail üzenetek felett, amikor megnyitják őket. Vegye figyelembe, hogy ez azt jelenti, hogy az összes üzenet, amelyet előnézetben megtekint, azonnal elküldésre kerül a konfigurált AI szolgáltatáshoz." - }, - "auto_summary_title": { - "message": "ThunderAI Összefoglaló" - }, - "auto_summary_generating": { - "message": "AI összefoglaló generálása..." - }, - "auto_summary_failed": { - "message": "Nem sikerült generálni az AI összefoglalót. Kérjük, ellenőrizze a beállításait, és próbálja újra." - }, - "auto_summary_prompt": { - "message": "Kérjük, adjon egy rövid összefoglalót a következő e-mail üzenetről. Az összefoglaló maximum 3-5 mondatból állhat, és tartalmazza a fő pontokat:\n\n" } - } +} diff --git a/_locales/it/messages.json b/_locales/it/messages.json index bf419805..25ed2bf6 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -1276,26 +1276,6 @@ "Optional_Permission_Denied_Model_Fetching": { "message": "Hai negato l’autorizzazione necessaria per recuperare i modelli per questa integrazione." }, - "prefs_OptionText_auto_summary": { - "message": "Abilita il riassunto automatico AI per le anteprime dei messaggi", - "description": "" - }, - "prefs_OptionText_auto_summary_Info": { - "message": "Se abilitata, ThunderAI genererà e mostrerà automaticamente i riassunti AI sopra le email quando vengono aperte. Nota che questo significa che tutte le email che visualizzi in anteprima verranno immediatamente inviate al servizio AI configurato.", - "description": "" - }, - "auto_summary_title": { - "message": "Riassunto ThunderAI" - }, - "auto_summary_generating": { - "message": "Generazione del riassunto AI..." - }, - "auto_summary_failed": { - "message": "Impossibile generare il riassunto AI. Verificare le impostazioni e riprovare." - }, - "auto_summary_prompt": { - "message": "Fornire un riassunto conciso del seguente messaggio email. Il riassunto dovrebbe essere di massimo 3-5 frasi e catturare i punti principali:\n\n" - }, "prompt_string": { "message": "Prompt" }, diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index 57a90446..b60db714 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -890,11 +890,5 @@ }, "placeholder_selected_html": { "message": "Zaznaczony HTML" - }, - "prefs_OptionText_auto_summary": { - "message": "Włącz automatyczne podsumowanie AI dla podglądów wiadomości" - }, - "prefs_OptionText_auto_summary_Info": { - "message": "Jeśli zaznaczone, ThunderAI automatycznie wygeneruje i wyświetli podsumowania AI nad wiadomościami e-mail, gdy zostaną otwarte. Pamiętaj, że oznacza to, że wszystkie wiadomości, które przeglądasz, zostaną natychmiast wysłane do skonfigurowanej usługi AI." } } diff --git a/_locales/pt-br/messages.json b/_locales/pt-br/messages.json index b69a9f1b..c6730a35 100644 --- a/_locales/pt-br/messages.json +++ b/_locales/pt-br/messages.json @@ -836,23 +836,5 @@ }, "SpamFilter_PageTitle": { "message": "Gerenciar configurações do filtro de spam" - }, - "prefs_OptionText_auto_summary": { - "message": "Mostrar resumo automático na visualização de mensagens" - }, - "prefs_OptionText_auto_summary_Info": { - "message": "Se marcado, um resumo gerado por IA será exibido automaticamente acima das mensagens na visualização." - }, - "auto_summary_title": { - "message": "Resumo ThunderAI" - }, - "auto_summary_generating": { - "message": "Gerando resumo de IA..." - }, - "auto_summary_failed": { - "message": "Falha ao gerar o resumo de IA. Verifique suas configurações e tente novamente." - }, - "auto_summary_prompt": { - "message": "Forneça um resumo conciso da seguinte mensagem de e-mail. O resumo deve ter no máximo 3-5 frases e capturar os pontos principais:\n\n" } } diff --git a/_locales/pt/messages.json b/_locales/pt/messages.json index dfbe2fdf..f6656907 100644 --- a/_locales/pt/messages.json +++ b/_locales/pt/messages.json @@ -172,23 +172,5 @@ }, "chatgpt_win_send": { "message": "Enviar" - }, - "prefs_OptionText_auto_summary": { - "message": "Ativar resumo automático de IA para pré-visualizações de mensagens" - }, - "prefs_OptionText_auto_summary_Info": { - "message": "Se ativado, o ThunderAI gerará e mostrará automaticamente resumos de IA acima das mensagens de e-mail quando forem abertas. Note que isso significa que todas as mensagens que você visualizar serão enviadas imediatamente para o serviço de IA configurado." - }, - "auto_summary_title": { - "message": "Resumo ThunderAI" - }, - "auto_summary_generating": { - "message": "Gerando resumo de IA..." - }, - "auto_summary_failed": { - "message": "Falha ao gerar o resumo de IA. Verifique suas configurações e tente novamente." - }, - "auto_summary_prompt": { - "message": "Forneça um resumo conciso da seguinte mensagem de e-mail. O resumo deve ter no máximo 3-5 frases e capturar os pontos principais:\n\n" } } diff --git a/_locales/ro/messages.json b/_locales/ro/messages.json index f98b7f55..dbecf17f 100644 --- a/_locales/ro/messages.json +++ b/_locales/ro/messages.json @@ -229,23 +229,5 @@ }, "prefsInfoDesc_1": { "message": "S-ar putea ca interfața web ChatGPT să se modifice într-un mod care să perturbe funcționarea addon-ului. Verificați pagina \"Starea serviciului\" accesibilă prin legătura din partea de jos a acestei pagini. De asemenea, rețineți că prima dată când utilizați ThunderAI, trebuie să vă conectați la ChatGPT." - }, - "prefs_OptionText_auto_summary": { - "message": "Activează rezumatul automat AI pentru previzualizările mesajelor" - }, - "prefs_OptionText_auto_summary_Info": { - "message": "Dacă este bifată, ThunderAI va genera și afișa automat rezumate AI deasupra mesajelor de e-mail atunci când sunt deschise. Rețineți că acest lucru înseamnă că toate mesajele pe care le previzualizați vor fi trimise imediat către serviciul AI configurat." - }, - "auto_summary_title": { - "message": "Rezumat ThunderAI" - }, - "auto_summary_generating": { - "message": "Generare rezumat AI..." - }, - "auto_summary_failed": { - "message": "Nu s-a putut genera rezumatul AI. Vă rugăm să verificați setările și să încercați din nou." - }, - "auto_summary_prompt": { - "message": "Vă rugăm să furnizați un rezumat concis al următorului mesaj de e-mail. Rezumatul ar trebui să aibă maximum 3-5 propoziții și să cuprindă punctele principale:\n\n" } - } +} diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index e7a27af6..31b4fe78 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -1205,23 +1205,5 @@ }, "OpenAIComp_ClearModelsList_Confirm": { "message": "Вы уверены, что хотите очистить список моделей? Это действие не может быть отменено." - }, - "prefs_OptionText_auto_summary": { - "message": "Показывать автоматическое резюме в просмотре сообщений" - }, - "prefs_OptionText_auto_summary_Info": { - "message": "Если отмечено, резюме, сгенерированное ИИ, будет автоматически отображаться над сообщениями в просмотре." - }, - "auto_summary_title": { - "message": "ThunderAI Резюме" - }, - "auto_summary_generating": { - "message": "Генерация AI резюме..." - }, - "auto_summary_failed": { - "message": "Не удалось сгенерировать AI резюме. Пожалуйста, проверьте настройки и попробуйте снова." - }, - "auto_summary_prompt": { - "message": "Пожалуйста, предоставьте краткое резюме следующего сообщения электронной почты. Резюме должно содержать максимум 3-5 предложений и отражать основные моменты:\n\n" } } diff --git a/_locales/sk/messages.json b/_locales/sk/messages.json index b076c05d..0967ef42 100644 --- a/_locales/sk/messages.json +++ b/_locales/sk/messages.json @@ -1,20 +1 @@ -{ - "prefs_OptionText_auto_summary": { - "message": "Povoliť automatické AI zhrnutie pre náhľady správ" - }, - "prefs_OptionText_auto_summary_Info": { - "message": "Ak je zaškrtnuté, ThunderAI automaticky vygeneruje a zobrazí AI zhrnutia nad e-mailovými správami, keď sú otvorené. Mějte na pamäti, že to znamená, že všetky správy, ktoré si prezeráte, budú okamžite odoslané do nakonfigurovanej AI služby." - }, - "auto_summary_title": { - "message": "Zhrnutie ThunderAI" - }, - "auto_summary_generating": { - "message": "Generovanie AI zhrnutia..." - }, - "auto_summary_failed": { - "message": "Zlyhanie generovania AI zhrnutia. Skontrolujte svoje nastavenia a skúste to znova." - }, - "auto_summary_prompt": { - "message": "Poskytnite stručné zhrnutie nasledujúcej e-mailovej správy. Zhrnutie by malo mať maximálne 3-5 viet a zachytiť hlavné body:\n\n" - } -} +{} diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index 77d83a93..f4066bc8 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -68,24 +68,6 @@ "prefs_OptionText_chatgpt_win_width": { "message": "Bredd" }, - "prefs_OptionText_auto_summary": { - "message": "Aktivera automatisk AI-sammanfattning för meddelandeförhandsvisningar" - }, - "prefs_OptionText_auto_summary_Info": { - "message": "Om markerad kommer ThunderAI att generera och visa AI-sammanfattningar ovanför e-postmeddelanden när de öppnas. Observera att detta innebär att alla meddelanden du förhandsgranskar kommer att skickas omedelbart till den konfigurerade AI-tjänsten." - }, - "auto_summary_title": { - "message": "ThunderAI Sammanfattning" - }, - "auto_summary_generating": { - "message": "Genererar AI-sammanfattning..." - }, - "auto_summary_failed": { - "message": "Misslyckades med att generera AI-sammanfattning. Kontrollera dina inställningar och försök igen." - }, - "auto_summary_prompt": { - "message": "Ge en koncis sammanfattning av följande e-postmeddelande. Sammanfattningen bör vara max 3-5 meningar och fånga huvudpunkterna:\n\n" - }, "prompt_summarize_this": { "message": "Sammanfatta detta" }, diff --git a/_locales/zh_Hans/messages.json b/_locales/zh_Hans/messages.json index 34de7636..5c187541 100644 --- a/_locales/zh_Hans/messages.json +++ b/_locales/zh_Hans/messages.json @@ -889,23 +889,5 @@ }, "customPrompts_form_label_use_diff_viewer_title": { "message": "当操作设置为“替换文本”时,可以选择差异查看器。" - }, - "prefs_OptionText_auto_summary": { - "message": "在消息预览中显示自动摘要" - }, - "prefs_OptionText_auto_summary_Info": { - "message": "如果选中,AI生成的摘要将自动显示在消息预览上方。" - }, - "auto_summary_title": { - "message": "ThunderAI 摘要" - }, - "auto_summary_generating": { - "message": "正在生成AI摘要..." - }, - "auto_summary_failed": { - "message": "生成AI摘要失败。请确认您的设置并重试。" - }, - "auto_summary_prompt": { - "message": "请提供以下电子邮件消息的简明摘要。摘要应最多包含3-5个句子,并捕捉主要要点:\n\n" } } diff --git a/_locales/zh_Hant/messages.json b/_locales/zh_Hant/messages.json index 70b11ccb..50827707 100644 --- a/_locales/zh_Hant/messages.json +++ b/_locales/zh_Hant/messages.json @@ -1220,23 +1220,5 @@ }, "OpenAIComp_ClearModelsList_Confirm": { "message": "確定要清除模型清單嗎?這個動作無法復原。" - }, - "prefs_OptionText_auto_summary": { - "message": "啟用郵件預覽的自動 AI 摘要" - }, - "prefs_OptionText_auto_summary_Info": { - "message": "如果勾選,ThunderAI 將自動生成並顯示 AI 摘要在電子郵件訊息上方。請注意,這意味著您預覽的所有訊息將立即被發送到已配置的 AI 服務。" - }, - "auto_summary_title": { - "message": "ThunderAI 摘要" - }, - "auto_summary_generating": { - "message": "正在產生 AI 摘要..." - }, - "auto_summary_failed": { - "message": "產生 AI 摘要失敗。請確認您的設定並重試。" - }, - "auto_summary_prompt": { - "message": "請提供以下電子郵件訊息的簡明摘要。摘要應最多包含3-5個句子,並捕捉主要要點:\n\n" } } From 7e758a51e6e64a373756a9698baeef1148ce2fc7 Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 17 Feb 2026 23:19:58 +0100 Subject: [PATCH 09/52] removed a method call for Thudnerbird 115 --- mzta-background.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/mzta-background.js b/mzta-background.js index 66da201c..f47e746a 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -1391,12 +1391,7 @@ async function processEmails(args) { -try { - browser.messages.onNewMailReceived.addListener(newEmailListener, !prefs_init.add_tags_auto_only_inbox); -} catch (e) { - taLog.log("Using browser.messages.onNewMailReceived.addListener with one agrument for Thunderbird 115."); - browser.messages.onNewMailReceived.addListener(newEmailListener); -} +browser.messages.onNewMailReceived.addListener(newEmailListener, !prefs_init.add_tags_auto_only_inbox); /** * AI summary generation function using ThunderAI infrastructure From 2df835d46e59672e7357818028bde753486fcfd8 Mon Sep 17 00:00:00 2001 From: Ronald Klarenbeek Date: Tue, 24 Feb 2026 22:17:47 +0100 Subject: [PATCH 10/52] Fix auto-summary: inline styles, refresh button, and proper headerMessageId passing - Move CSS inline as requested by maintainer (no external CSS file) - Add fixed font sizing (14px) to prevent inheritance from email content - Add refresh button to regenerate summaries - Fix refresh bug: use summaryData.headerMessageId instead of message.headerMessageId - Remove deleted message display script and CSS files --- _locales/en/messages.json | 48 ++++- js/mzta-compose-script.js | 138 +++++++++++- js/mzta-summary-cache.js | 115 ++++++++++ messageDisplay/message-content-script.js | 107 ---------- messageDisplay/message-content-styles.css | 20 -- mzta-background.js | 244 +++++++++++----------- options/mzta-options-default.js | 2 +- options/mzta-options.html | 11 - pages/summarize/mzta-summarize.html | 13 ++ pages/summarize/mzta-summarize.js | 18 +- 10 files changed, 450 insertions(+), 266 deletions(-) create mode 100644 js/mzta-summary-cache.js delete mode 100644 messageDisplay/message-content-script.js delete mode 100644 messageDisplay/message-content-styles.css diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 2b112549..0396b7b5 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1910,5 +1910,49 @@ "spam_check_in_progress": { "message": "Spam check in progress...", "description": "" - } -} + }, + "prefs_OptionText_summarize_auto": { + "message": "Auto-summarize messages", + "description": "" + }, + "prefs_OptionText_summarize_auto_disabled": { + "message": "Disabled", + "description": "" + }, + "prefs_OptionText_summarize_auto_manual": { + "message": "Show summary button", + "description": "" + }, + "prefs_OptionText_summarize_auto_automatic": { + "message": "Generate automatically", + "description": "" + }, + "prefs_OptionText_summarize_auto_Info": { + "message": "Choose whether to automatically generate summaries when viewing messages. Requires an API-based connection (not ChatGPT Web).", + "description": "" + }, + "summarize_title": { + "message": "Summary", + "description": "" + }, + "summarize_generating": { + "message": "Generating summary...", + "description": "" + }, + "summarize_error": { + "message": "Failed to generate summary", + "description": "" + }, + "summarize_click_to_generate": { + "message": "Click here to generate a summary", + "description": "" + }, + "summarize_chatgpt_web_not_supported": { + "message": "Auto-summary requires an API-based connection. Please configure an API connection in ThunderAI settings.", + "description": "" + }, + "summarize_refresh": { + "message": "Refresh summary", + "description": "" + } + } diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index e30298fd..1303866d 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -709,6 +709,141 @@ switch (message.command) { document.body.insertBefore(container, document.body.firstChild); return Promise.resolve(true); + case "showSummary": + const generatingBanner = document.getElementById('mzta-summary-generating'); + if(generatingBanner) generatingBanner.remove(); + + const triggerBtn = document.getElementById('mzta-summary-trigger'); + if(triggerBtn) triggerBtn.remove(); + + const summaryBanner = document.getElementById('mzta-summary-banner'); + if(summaryBanner) summaryBanner.remove(); + + const summaryData = message.data; + const summaryContainer = document.createElement('div'); + summaryContainer.id = 'mzta-summary-banner'; + + const isDarkSummary = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + + let bgColorSummary = isDarkSummary ? '#2a2a2a' : '#f0f0f0'; + let textColorSummary = isDarkSummary ? '#e0e0e0' : '#333'; + let borderColorSummary = isDarkSummary ? '#444' : '#ddd'; + let titleColor = isDarkSummary ? '#ff6b6b' : '#d70022'; + + if (summaryData.error) { + bgColorSummary = isDarkSummary ? '#3a1a1a' : '#f7e6e6'; + textColorSummary = isDarkSummary ? '#ffcccc' : '#660000'; + borderColorSummary = '#660000'; + titleColor = isDarkSummary ? '#ffcccc' : '#660000'; + } + + summaryContainer.className = 'thunderai-summary-pane'; + summaryContainer.style.cssText = `background-color: ${bgColorSummary}; color: ${textColorSummary}; padding: 0.5rem; margin-bottom: 1rem; border-radius: 4px; border: 1px solid ${borderColorSummary}; font-family: system-ui, -apple-system, sans-serif; font-size: 14px;`; + + const summaryHeader = document.createElement('div'); + summaryHeader.style.cssText = `display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;`; + + const summaryTitle = document.createElement('div'); + summaryTitle.className = 'thunderai-summary-title'; + summaryTitle.textContent = browser.i18n.getMessage("summarize_title"); + summaryTitle.style.cssText = `font-weight: bold; font-size: 14px; color: ${titleColor};`; + + const refreshBtn = document.createElement('span'); + refreshBtn.textContent = '↻'; + refreshBtn.title = browser.i18n.getMessage("summarize_refresh") || 'Refresh summary'; + refreshBtn.style.cssText = `cursor: pointer; opacity: 0.6; font-size: 16px; transition: opacity 0.2s;`; + refreshBtn.onmouseover = () => refreshBtn.style.opacity = '1'; + refreshBtn.onmouseout = () => refreshBtn.style.opacity = '0.6'; + refreshBtn.onclick = async () => { + refreshBtn.onclick = null; + refreshBtn.style.opacity = '0.6'; + browser.runtime.sendMessage({ + command: "refreshSummary", + headerMessageId: summaryData.headerMessageId + }); + }; + + summaryHeader.appendChild(summaryTitle); + summaryHeader.appendChild(refreshBtn); + summaryContainer.appendChild(summaryHeader); + + const summaryText = document.createElement('div'); + summaryText.className = 'thunderai-summary-content'; + if (summaryData.error) { + summaryText.textContent = summaryData.message || browser.i18n.getMessage("summarize_error"); + } else { + summaryText.textContent = summaryData.summary; + } + summaryText.style.cssText = `font-size: 14px; line-height: 1.4;`; + + summaryContainer.appendChild(summaryText); + + document.body.insertBefore(summaryContainer, document.body.firstChild); + return Promise.resolve(true); + + case "showSummaryGenerating": + const existingGenerating = document.getElementById('mzta-summary-generating'); + if(existingGenerating) return Promise.resolve(true); + + const existingSummary = document.getElementById('mzta-summary-banner'); + if(existingSummary) existingSummary.remove(); + + const isDarkGen = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + + let bgColorGen = isDarkGen ? '#2a2a2a' : '#f0f0f0'; + let textColorGen = isDarkGen ? '#e0e0e0' : '#333'; + let borderColorGen = isDarkGen ? '#444' : '#ddd'; + let titleColorGen = isDarkGen ? '#ff6b6b' : '#d70022'; + + const generatingContainer = document.createElement('div'); + generatingContainer.id = 'mzta-summary-generating'; + generatingContainer.className = 'thunderai-summary-pane'; + generatingContainer.style.cssText = `background-color: ${bgColorGen}; color: ${textColorGen}; padding: 0.5rem; margin-bottom: 1rem; border-radius: 4px; border: 1px solid ${borderColorGen}; font-family: system-ui, -apple-system, sans-serif; font-size: 14px;`; + + const generatingTitle = document.createElement('div'); + generatingTitle.className = 'thunderai-summary-title'; + generatingTitle.textContent = browser.i18n.getMessage("summarize_generating"); + generatingTitle.style.cssText = `font-weight: bold; font-size: 14px; margin-bottom: 0.5rem; color: ${titleColorGen};`; + + generatingContainer.appendChild(generatingTitle); + + document.body.insertBefore(generatingContainer, document.body.firstChild); + return Promise.resolve(true); + + case "showSummaryButton": + const existingButton = document.getElementById('mzta-summary-trigger'); + if(existingButton) return Promise.resolve(true); + + const isDarkBtn = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + + let bgColorBtn = isDarkBtn ? '#2a2a2a' : '#f0f0f0'; + let textColorBtn = isDarkBtn ? '#e0e0e0' : '#333'; + let borderColorBtn = isDarkBtn ? '#444' : '#ddd'; + let titleColorBtn = isDarkBtn ? '#ff6b6b' : '#d70022'; + + const triggerContainer = document.createElement('div'); + triggerContainer.id = 'mzta-summary-trigger'; + triggerContainer.className = 'thunderai-summary-pane'; + triggerContainer.style.cssText = `background-color: ${bgColorBtn}; color: ${textColorBtn}; padding: 0.5rem; margin-bottom: 1rem; border-radius: 4px; border: 1px solid ${borderColorBtn}; cursor: pointer; font-family: system-ui, -apple-system, sans-serif; font-size: 14px;`; + + const triggerText = document.createElement('div'); + triggerText.className = 'thunderai-summary-title'; + triggerText.textContent = browser.i18n.getMessage("summarize_click_to_generate"); + triggerText.style.cssText = `font-weight: bold; font-size: 14px; margin-bottom: 0; color: ${titleColorBtn};`; + + triggerContainer.appendChild(triggerText); + triggerContainer.onclick = async () => { + triggerContainer.onclick = null; + triggerText.textContent = browser.i18n.getMessage("summarize_generating"); + browser.runtime.sendMessage({ + command: "triggerSummaryGeneration", + headerMessageId: message.headerMessageId + }); + }; + + document.body.insertBefore(triggerContainer, document.body.firstChild); + return Promise.resolve(true); + default: // do nothing return Promise.resolve(false); @@ -716,4 +851,5 @@ switch (message.command) { } }); -browser.runtime.sendMessage({ command: "checkSpamReport" }); \ No newline at end of file +browser.runtime.sendMessage({ command: "checkSpamReport" }); +browser.runtime.sendMessage({ command: "initSummary" }); \ No newline at end of file diff --git a/js/mzta-summary-cache.js b/js/mzta-summary-cache.js new file mode 100644 index 00000000..cacc2f02 --- /dev/null +++ b/js/mzta-summary-cache.js @@ -0,0 +1,115 @@ +/* + * ThunderAI [https://micz.it/thunderbird-addon-thunderai/] + * Copyright (C) 2024 - 2026 Mic (m@micz.it) + + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +export const taSummaryCache = { + logger: console, + _data_prefix: 'mzta-summary-', + _processing_prefix: 'mzta-summary-processing-', + _max_summaries: 100, + + async setProcessing(data_id) { + const key = this._processing_prefix + data_id; + await browser.storage.session.set({ [key]: true }); + }, + + async isProcessing(data_id) { + const key = this._processing_prefix + data_id; + let output = await browser.storage.session.get(key); + return output[key] || false; + }, + + async saveSummary(data, data_id) { + const key = this._data_prefix + data_id; + await browser.storage.session.set({ [key]: data }); + await browser.storage.session.remove(this._processing_prefix + data_id); + }, + + async saveError(data_id, error_message) { + let data = { + error: true, + message: error_message, + summary_date: new Date(), + headerMessageId: data_id + }; + await this.saveSummary(data, data_id); + return data; + }, + + async loadSummary(data_id) { + const key = this._data_prefix + data_id; + let output = await browser.storage.session.get(key); + return output[key] || null; + }, + + async removeSummary(data_id) { + const key = this._data_prefix + data_id; + await browser.storage.session.remove(key); + await browser.storage.session.remove(this._processing_prefix + data_id); + }, + + async getAllSummaries() { + let allData = await browser.storage.session.get(null); + let summaryData = {}; + + for (const [key, value] of Object.entries(allData)) { + if (key.startsWith(this._data_prefix)) { + summaryData[key.replace(this._data_prefix, '')] = value; + } + } + + return summaryData; + }, + + async clearSummaries() { + let allData = await browser.storage.session.get(null); + let keysToDelete = Object.keys(allData).filter(key => key.startsWith(this._data_prefix) || key.startsWith(this._processing_prefix)); + + for (let key of keysToDelete) { + await browser.storage.session.remove(key); + } + }, + + async truncSummaries() { + let data = await this.getAllSummaries(); + let sortedData = this.sortSummariesByDate(data); + let keys = Object.keys(sortedData); + + if (keys.length > this._max_summaries) { + for (let i = this._max_summaries; i < keys.length; i++) { + await browser.storage.session.remove(this._data_prefix + keys[i]); + } + } + }, + + sortSummariesByDate(data) { + if (!data) return {}; + const summaryKeys = Object.keys(data); + summaryKeys.sort((a, b) => { + const dateA = new Date(data[a].summary_date); + const dateB = new Date(data[b].summary_date); + return dateB - dateA; + }); + + let sortedSummaries = {}; + summaryKeys.forEach((key) => { + sortedSummaries[key] = data[key]; + }); + + return sortedSummaries; + } +}; \ No newline at end of file diff --git a/messageDisplay/message-content-script.js b/messageDisplay/message-content-script.js deleted file mode 100644 index 7bb92c3d..00000000 --- a/messageDisplay/message-content-script.js +++ /dev/null @@ -1,107 +0,0 @@ -async function showSummaryPane() { - // Check if auto-summary is enabled in user preferences - const result = await browser.storage.sync.get('auto_summary_enabled'); - - // If auto-summary is disabled or not set, don't show anything - if (!result.auto_summary_enabled) { - return; - } - - // 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 = browser.i18n.getMessage("auto_summary_title"); - - // Create a loading indicator - const loadingIndicator = document.createElement("div"); - loadingIndicator.className = "thunderai-summary-content"; - loadingIndicator.innerText = browser.i18n.getMessage("auto_summary_generating"); - - // 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 = browser.i18n.getMessage("auto_summary_failed"); - loadingIndicator.style.color = '#d70022'; - - } -} - -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 using localized string - const summaryPrompt = browser.i18n.getMessage("auto_summary_prompt") + cleanedContent; - - // 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) { - // Use the localized error message - reject(new Error(browser.i18n.getMessage("auto_summary_failed"))); - } else { - reject(new Error(browser.i18n.getMessage("auto_summary_failed"))); - } - }); - }); -} - -// Call the function to show the pane -showSummaryPane(); \ No newline at end of file diff --git a/messageDisplay/message-content-styles.css b/messageDisplay/message-content-styles.css deleted file mode 100644 index 6a4019c8..00000000 --- a/messageDisplay/message-content-styles.css +++ /dev/null @@ -1,20 +0,0 @@ -.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; -} \ No newline at end of file diff --git a/mzta-background.js b/mzta-background.js index f47e746a..fc12acfa 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -63,6 +63,7 @@ import { getSpecialPrompts } from './js/mzta-prompts.js'; import { taSpamReport } from './js/mzta-spamreport.js'; +import { taSummaryCache } from './js/mzta-summary-cache.js'; import { taWorkingStatus } from './js/mzta-working-status.js'; import { addTags_getExclusionList, @@ -110,17 +111,10 @@ 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" }] + js: [{ file: "js/mzta-compose-script.js" }] }); -// 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 in all already open message tabs. let openTabs = await messenger.tabs.query(); let messageTabs = openTabs.filter( tab => ["mail", "messageDisplay"].includes(tab.type) @@ -133,13 +127,6 @@ 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); @@ -248,38 +235,60 @@ 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) { + case 'initSummary': + async function _initSummary() { 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 - }); + let tabId = sender.tab.id; + let prefs = await browser.storage.sync.get({ summarize_auto: 0 }); + if (prefs.summarize_auto === 0) return; - // Use the existing ThunderAI infrastructure - // We need to adapt to the new v3.8.0 settings structure - const summary = await generateAISummaryUsingThunderAIInfrastructure( - message.content, - message.prompt, - { - ...prefs, - // Add the dynamic settings that the new system expects - connection_type: prefs.connection_type, - // For summary, we don't have specific integration settings yet, - // so we'll use the global connection type - } - ); + let message = await browser.messageDisplay.getDisplayedMessage(tabId); + if (!message) return; - return { summary: summary }; - } catch (error) { - console.error("[ThunderAI] Error generating summary:", error); - return { error: "Failed to generate AI summary. Please confirm your settings and try again." }; + let cachedSummary = await taSummaryCache.loadSummary(message.headerMessageId); + if (cachedSummary && !cachedSummary.error) { + browser.tabs.sendMessage(tabId, { command: "showSummary", data: cachedSummary }); + return; + } + + if (await taSummaryCache.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) { + _generateSummaryForMessage(message.headerMessageId, tabId); + } + } catch (e) { + taLog.error("Error in initSummary: " + e); } } - return _generate_summary(message); + _initSummary(); + break; + case 'triggerSummaryGeneration': + async function _triggerSummaryGeneration(message) { + let tabId = sender.tab.id; + browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); + await _generateSummaryForMessage(message.headerMessageId, tabId); + } + _triggerSummaryGeneration(message); + break; + case 'generate_summary': + async function _generate_summary(message) { + await _generateSummaryForMessage(message.headerMessageId, message.tabId); + } + _generate_summary(message); + break; + case 'refreshSummary': + async function _refreshSummary(message) { + let tabId = sender.tab.id; + await taSummaryCache.removeSummary(message.headerMessageId); + await _generateSummaryForMessage(message.headerMessageId, tabId); + } + _refreshSummary(message); + break; // case 'chatgpt_open': // openChatGPT(message.prompt,message.action,message.tabId); // return true; @@ -433,10 +442,83 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { break; } } - // Return false if the message was not handled by this listener. return false; }); +async function _generateSummaryForMessage(headerMessageId, tabId) { + try { + let prefs = await browser.storage.sync.get({ + connection_type: prefs_default.connection_type, + do_debug: prefs_default.do_debug, + default_chatgpt_lang: prefs_default.default_chatgpt_lang, + ...getDynamicSettingsDefaults(['use_specific_integration', 'connection_type']) + }); + + let cachedSummary = await taSummaryCache.loadSummary(headerMessageId); + if (cachedSummary && !cachedSummary.error) { + browser.tabs.sendMessage(tabId, { command: "showSummary", data: cachedSummary }); + return; + } + + if (await taSummaryCache.isProcessing(headerMessageId)) { + browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); + return; + } + + await taSummaryCache.setProcessing(headerMessageId); + browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); + + const messageResult = await browser.messages.query({ headerMessageId: headerMessageId }); + if (!messageResult || messageResult.messages.length === 0) { + await taSummaryCache.saveError(headerMessageId, "Message not found"); + browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: "Message not found" } }); + return; + } + + const fullMessage = await browser.messages.getFull(messageResult.messages[0].id); + const mailBody = getMailBody(fullMessage); + let bodyText = htmlBodyToPlainText(mailBody.html); + if (bodyText.length === 0) { + bodyText = mailBody.text.replace(/\s+/g, ' ').trim(); + } + + const promptText = browser.i18n.getMessage('auto_summary_prompt') + bodyText; + + const connectionType = getConnectionType(prefs, {}, 'summarize'); + + if (connectionType === 'chatgpt_web') { + const errorMsg = browser.i18n.getMessage('summarize_chatgpt_web_not_supported'); + await taSummaryCache.saveError(headerMessageId, errorMsg); + browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: errorMsg } }); + return; + } + + const cmd = new mzta_specialCommand({ + prompt: promptText, + llm: connectionType, + do_debug: prefs.do_debug, + config: {} + }); + + await cmd.initWorker(); + const aiResponse = await cmd.sendPrompt(); + const cleanedSummary = aiResponse.replace(/\s+/g, ' ').trim(); + + const summaryData = { + summary: cleanedSummary, + summary_date: new Date(), + headerMessageId: headerMessageId + }; + await taSummaryCache.saveSummary(summaryData, headerMessageId); + browser.tabs.sendMessage(tabId, { command: "showSummary", data: summaryData }); + + } catch (error) { + console.error("[ThunderAI] Error generating summary:", error); + await taSummaryCache.saveError(headerMessageId, error.message || String(error)); + browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: error.message || "Failed to generate summary" } }); + } +} + // Listen for messages from ThunderAI-Sparks browser.runtime.onMessageExternal.addListener((message, sender, sendResponse) => { switch (message.action) { @@ -1392,77 +1474,3 @@ async function processEmails(args) { browser.messages.onNewMailReceived.addListener(newEmailListener, !prefs_init.add_tags_auto_only_inbox); - -/** - * 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'); - - // Create a prompt config for summary (similar to how other features do it) - // This adapts to the new v3.8.0 dynamic settings system - const summaryPromptConfig = { - id: 'auto_summary', - name: 'Auto Summary', - model: '', // Model will be determined by getConnectionType - connection_type: prefs.connection_type - }; - - // Determine which LLM to use based on user preferences using the new v3.8.0 pattern - const llmType = getConnectionType(prefs, summaryPromptConfig, 'auto_summary'); - - // Get the appropriate model based on the connection type using dynamic settings - let model = ''; - if (prefs.connection_type === 'chatgpt_api') { - model = prefs.chatgpt_model; - } else if (prefs.connection_type === 'ollama_api') { - model = prefs.ollama_model; - } else if (prefs.connection_type === 'openai_comp_api') { - model = prefs.openai_comp_model; - } else if (prefs.connection_type === 'google_gemini_api') { - model = prefs.google_gemini_model; - } else if (prefs.connection_type === 'anthropic_api') { - model = prefs.anthropic_model; - } - - // Create a special command instance with the correct v3.8.0 pattern - const summaryCommand = new mzta_specialCommand({ - prompt: prompt, - llm: llmType, - custom_model: model, - do_debug: prefs.do_debug, - config: summaryPromptConfig - }); - - try { - // 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; - } catch (error) { - console.error("[ThunderAI] Error in AI summary generation:", error); - throw error; // Re-throw to be handled by the caller - } -} - -/** - * 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; -} diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index 23957310..a961db22 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -134,7 +134,7 @@ export const prefs_default = { spamfilter: false, spamfilter_threshold: 70, spamfilter_enabled_accounts: [], - auto_summary_enabled: false, // Enable automatic AI summarization for message previews + summarize_auto: 0, // 0: disabled, 1: manual button, 2: automatic spamfilter_show_msg_panel: true, summarize: false, ...generated_prefs diff --git a/options/mzta-options.html b/options/mzta-options.html index 08a143fa..c809c1e3 100644 --- a/options/mzta-options.html +++ b/options/mzta-options.html @@ -125,17 +125,6 @@ - - - - - - __MSG_prefs_OptionText_add_tags__
diff --git a/pages/summarize/mzta-summarize.html b/pages/summarize/mzta-summarize.html index a8db111d..ddb5cf41 100644 --- a/pages/summarize/mzta-summarize.html +++ b/pages/summarize/mzta-summarize.html @@ -26,6 +26,19 @@ + + __MSG_prefs_OptionText_summarize_auto__ + + + + diff --git a/pages/summarize/mzta-summarize.js b/pages/summarize/mzta-summarize.js index b41d9084..5ecf11dd 100644 --- a/pages/summarize/mzta-summarize.js +++ b/pages/summarize/mzta-summarize.js @@ -194,8 +194,11 @@ function saveOptions(e) { options[element.id] = element.value.trim(); break; case 'select-one': - // console.log(">>>>>>>>>> Saving option [select-one]: " + element.id + " = " + element.value); - options[element.id] = element.value; + if (element.id === 'summarize_auto') { + options[element.id] = parseInt(element.value, 10); + } else { + options[element.id] = element.value; + } break; case 'textarea': options[element.id] = normalizeStringList(element.value); @@ -231,10 +234,13 @@ async function restoreOptions() { break; default: if (element.tagName === 'SELECT') { - let default_select_value = ''; - const restoreValue = result[element.id] || default_select_value; + let default_select_value = 0; + if (element.id === 'summarize_auto') { + default_select_value = prefs_default.summarize_auto; + } + const restoreValue = result[element.id] ?? default_select_value; // Check if option exists - let optionExists = Array.from(element.options).some(opt => opt.value === restoreValue); + let optionExists = Array.from(element.options).some(opt => opt.value === String(restoreValue)); // If it doesn't exist and restoreValue is not empty, create it if (!optionExists && restoreValue !== '') { let newOption = new Option(restoreValue, restoreValue); @@ -243,7 +249,7 @@ async function restoreOptions() { // Set value element.value = restoreValue; if (element.value === '') { - element.selectedIndex = -1; + element.selectedIndex = 0; } if (element.tomselect) { element.tomselect.setValue(element.value, true); From ca07cf5ed0ffd4b5ddd1b7b3bb7237478a47b0de Mon Sep 17 00:00:00 2001 From: Ronald Klarenbeek Date: Tue, 24 Feb 2026 22:32:44 +0100 Subject: [PATCH 11/52] Fix duplicate 'Generating summary' when using manual mode button --- js/mzta-compose-script.js | 5 +++++ mzta-background.js | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 1d6579e8..b5751a2c 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -798,6 +798,9 @@ switch (message.command) { const existingSummary = document.getElementById('mzta-summary-banner'); if(existingSummary) existingSummary.remove(); + const existingTrigger = document.getElementById('mzta-summary-trigger'); + if(existingTrigger) existingTrigger.remove(); + const isDarkGen = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; let bgColorGen = isDarkGen ? '#2a2a2a' : '#f0f0f0'; @@ -844,6 +847,8 @@ switch (message.command) { triggerContainer.appendChild(triggerText); triggerContainer.onclick = async () => { triggerContainer.onclick = null; + triggerContainer.id = 'mzta-summary-generating'; + triggerContainer.style.cursor = 'default'; triggerText.textContent = browser.i18n.getMessage("summarize_generating"); browser.runtime.sendMessage({ command: "triggerSummaryGeneration", diff --git a/mzta-background.js b/mzta-background.js index fc12acfa..da73d096 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -270,7 +270,6 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { case 'triggerSummaryGeneration': async function _triggerSummaryGeneration(message) { let tabId = sender.tab.id; - browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); await _generateSummaryForMessage(message.headerMessageId, tabId); } _triggerSummaryGeneration(message); From 7dfdc2ea54c03bda2896fba1d480197cac2f3ac6 Mon Sep 17 00:00:00 2001 From: Ronald Klarenbeek Date: Fri, 27 Feb 2026 15:28:56 +0100 Subject: [PATCH 12/52] Fix auto-summary: strip markdown formatting from AI responses --- mzta-background.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mzta-background.js b/mzta-background.js index da73d096..47f192ae 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -501,7 +501,10 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { await cmd.initWorker(); const aiResponse = await cmd.sendPrompt(); - const cleanedSummary = aiResponse.replace(/\s+/g, ' ').trim(); + let cleanedSummary = aiResponse.replace(/```[\s\S]*?```/g, ''); + cleanedSummary = cleanedSummary.replace(/[\*#_~`]/g, ''); + cleanedSummary = cleanedSummary.replace(/\s+/g, ' ').trim(); + cleanedSummary = cleanedSummary.replace(/^Summary:\s*/i, ''); const summaryData = { summary: cleanedSummary, From 6d53e108d050479d6eee4fbe8c839af228eef675 Mon Sep 17 00:00:00 2001 From: Mic Date: Mon, 23 Mar 2026 18:25:35 +0100 Subject: [PATCH 13/52] taSummaryCache renamed to taSummaryStore --- ...-summary-cache.js => mzta-summarystore.js} | 2 +- mzta-background.js | 22 +++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) rename js/{mzta-summary-cache.js => mzta-summarystore.js} (99%) diff --git a/js/mzta-summary-cache.js b/js/mzta-summarystore.js similarity index 99% rename from js/mzta-summary-cache.js rename to js/mzta-summarystore.js index cacc2f02..c3f07ef8 100644 --- a/js/mzta-summary-cache.js +++ b/js/mzta-summarystore.js @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -export const taSummaryCache = { +export const taSummaryStore = { logger: console, _data_prefix: 'mzta-summary-', _processing_prefix: 'mzta-summary-processing-', diff --git a/mzta-background.js b/mzta-background.js index 652c4f16..9836e444 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -63,7 +63,7 @@ import { getSpecialPrompts } from './js/mzta-prompts.js'; import { taSpamReport } from './js/mzta-spamreport.js'; -import { taSummaryCache } from './js/mzta-summary-cache.js'; +import { taSummaryStore } from './js/mzta-summarystore.js'; import { taWorkingStatus } from './js/mzta-working-status.js'; import { addTags_getExclusionList, @@ -227,13 +227,13 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { let message = await browser.messageDisplay.getDisplayedMessage(tabId); if (!message) return; - let cachedSummary = await taSummaryCache.loadSummary(message.headerMessageId); + let cachedSummary = await taSummaryStore.loadSummary(message.headerMessageId); if (cachedSummary && !cachedSummary.error) { browser.tabs.sendMessage(tabId, { command: "showSummary", data: cachedSummary }); return; } - if (await taSummaryCache.isProcessing(message.headerMessageId)) { + if (await taSummaryStore.isProcessing(message.headerMessageId)) { browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); return; } @@ -265,7 +265,7 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { case 'refreshSummary': async function _refreshSummary(message) { let tabId = sender.tab.id; - await taSummaryCache.removeSummary(message.headerMessageId); + await taSummaryStore.removeSummary(message.headerMessageId); await _generateSummaryForMessage(message.headerMessageId, tabId); } _refreshSummary(message); @@ -445,23 +445,23 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { ...getDynamicSettingsDefaults(['use_specific_integration', 'connection_type']) }); - let cachedSummary = await taSummaryCache.loadSummary(headerMessageId); + let cachedSummary = await taSummaryStore.loadSummary(headerMessageId); if (cachedSummary && !cachedSummary.error) { browser.tabs.sendMessage(tabId, { command: "showSummary", data: cachedSummary }); return; } - if (await taSummaryCache.isProcessing(headerMessageId)) { + if (await taSummaryStore.isProcessing(headerMessageId)) { browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); return; } - await taSummaryCache.setProcessing(headerMessageId); + await taSummaryStore.setProcessing(headerMessageId); browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); const messageResult = await browser.messages.query({ headerMessageId: headerMessageId }); if (!messageResult || messageResult.messages.length === 0) { - await taSummaryCache.saveError(headerMessageId, "Message not found"); + await taSummaryStore.saveError(headerMessageId, "Message not found"); browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: "Message not found" } }); return; } @@ -479,7 +479,7 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { if (connectionType === 'chatgpt_web') { const errorMsg = browser.i18n.getMessage('summarize_chatgpt_web_not_supported'); - await taSummaryCache.saveError(headerMessageId, errorMsg); + await taSummaryStore.saveError(headerMessageId, errorMsg); browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: errorMsg } }); return; } @@ -503,12 +503,12 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { summary_date: new Date(), headerMessageId: headerMessageId }; - await taSummaryCache.saveSummary(summaryData, headerMessageId); + await taSummaryStore.saveSummary(summaryData, headerMessageId); browser.tabs.sendMessage(tabId, { command: "showSummary", data: summaryData }); } catch (error) { console.error("[ThunderAI] Error generating summary:", error); - await taSummaryCache.saveError(headerMessageId, error.message || String(error)); + await taSummaryStore.saveError(headerMessageId, error.message || String(error)); browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: error.message || "Failed to generate summary" } }); } } From 83b854440419ddfe6391c8cbee6ce95d2310ff0b Mon Sep 17 00:00:00 2001 From: Mic Date: Mon, 23 Mar 2026 18:39:21 +0100 Subject: [PATCH 14/52] taSummaryStore is now a Class and is using taStorage. see #675 #580 --- js/mzta-storage.js | 76 +++++++++++++++++++++++++-- js/mzta-summarystore.js | 111 ++++++++++++++++++++++++++-------------- mzta-background.js | 21 ++++---- 3 files changed, 156 insertions(+), 52 deletions(-) diff --git a/js/mzta-storage.js b/js/mzta-storage.js index 983a3451..5d12a54f 100644 --- a/js/mzta-storage.js +++ b/js/mzta-storage.js @@ -28,6 +28,9 @@ export class taStorage { taLog = null; + /** + * @param {boolean} [do_debug=false] - Enable debug logging. + */ constructor(do_debug = false) { this.taLog = new taLogger("mzta-storage", do_debug); } @@ -179,10 +182,11 @@ export class taStorage { /** * Write the summary field for a given Message-ID. * @param {string} messageId - The Message-ID header string. - * @param {string} text - The summary text. + * @param {object} summary_data - The summary data object with fields: + * summary, error, message, summary_date. * @param {boolean} [force=true] - If true, overwrite existing summary data. */ - async writeSummary(messageId, text, force = true) { + async writeSummary(messageId, summary_data, force = true) { this.taLog.log('[writeSummary] messageId: ' + messageId + ', force: ' + force); try { let key = this._buildKey(messageId); @@ -192,7 +196,15 @@ export class taStorage { return; } let now = Date.now(); - record[taStorage.FIELD_SUMMARY] = { text: text, ts: now }; + record[taStorage.FIELD_SUMMARY] = { + summary: summary_data.summary, + error: summary_data.error || false, + message: summary_data.message || '', + summary_date: summary_data.summary_date instanceof Date + ? summary_data.summary_date.toISOString() + : summary_data.summary_date, + ts: now, + }; record.ts = now; await messenger.storage.local.set({ [key]: record }); } catch (e) { @@ -200,6 +212,64 @@ export class taStorage { } } + /** + * Get all records that contain a summary field. + * @returns {Promise} Map of messageId -> summary data object. + */ + async getAllSummaryRecords() { + this.taLog.log('[getAllSummaryRecords] loading all summary records'); + try { + let all = await messenger.storage.local.get(null); + let result = {}; + for (let [key, record] of Object.entries(all)) { + if (!key.startsWith(taStorage.STORAGE_KEY_PREFIX)) continue; + if (!this.hasField(record, taStorage.FIELD_SUMMARY)) continue; + let messageId = key.slice(taStorage.STORAGE_KEY_PREFIX.length); + let summary = record[taStorage.FIELD_SUMMARY]; + result[messageId] = { + headerMessageId: messageId, + summary: summary.summary, + error: summary.error || false, + message: summary.message || '', + summary_date: new Date(summary.summary_date || summary.ts), + }; + } + this.taLog.log('[getAllSummaryRecords] found ' + Object.keys(result).length + ' summary records'); + return result; + } catch (e) { + this.taLog.error('getAllSummaryRecords error: ' + e); + return {}; + } + } + + /** + * Delete only the summary field from a record. + * Deletes the entire record if no other data fields remain. + * @param {string} messageId - The Message-ID header string. + */ + async deleteSummaryField(messageId) { + this.taLog.log('[deleteSummaryField] messageId: ' + messageId); + try { + let key = this._buildKey(messageId); + let record = await this.getRecord(messageId); + if (!record || !(taStorage.FIELD_SUMMARY in record)) { + this.taLog.log('[deleteSummaryField] no summary field found for messageId: ' + messageId); + return; + } + delete record[taStorage.FIELD_SUMMARY]; + const remainingFields = Object.keys(record).filter(k => k !== 'v' && k !== 'ts'); + if (remainingFields.length === 0) { + this.taLog.log('[deleteSummaryField] no remaining fields, deleting entire record'); + await messenger.storage.local.remove(key); + } else { + this.taLog.log('[deleteSummaryField] remaining fields: ' + remainingFields.join(', ')); + await messenger.storage.local.set({ [key]: record }); + } + } catch (e) { + this.taLog.error('deleteSummaryField error: ' + e); + } + } + /** * Write the translation field for a given Message-ID. * @param {string} messageId - The Message-ID header string. diff --git a/js/mzta-summarystore.js b/js/mzta-summarystore.js index c3f07ef8..52e3bc2a 100644 --- a/js/mzta-summarystore.js +++ b/js/mzta-summarystore.js @@ -16,30 +16,49 @@ * along with this program. If not, see . */ -export const taSummaryStore = { - logger: console, - _data_prefix: 'mzta-summary-', - _processing_prefix: 'mzta-summary-processing-', - _max_summaries: 100, +import { taStorage } from './mzta-storage.js'; +import { taLogger } from './mzta-logger.js'; + +export class taSummaryStore { + + _processing_prefix = 'mzta-summary-processing-'; + _max_summaries = 100; + _storage = null; + taLog = null; + + constructor(do_debug = false) { + this._storage = new taStorage(do_debug); + this.taLog = new taLogger('mzta-summarystore', do_debug); + } async setProcessing(data_id) { + this.taLog.log("[setProcessing] data_id: " + data_id); const key = this._processing_prefix + data_id; await browser.storage.session.set({ [key]: true }); - }, + } async isProcessing(data_id) { + this.taLog.log("[isProcessing] data_id: " + data_id); const key = this._processing_prefix + data_id; let output = await browser.storage.session.get(key); - return output[key] || false; - }, + let result = output[key] || false; + this.taLog.log("[isProcessing] result: " + result); + return result; + } async saveSummary(data, data_id) { - const key = this._data_prefix + data_id; - await browser.storage.session.set({ [key]: data }); - await browser.storage.session.remove(this._processing_prefix + data_id); - }, + this.taLog.log("[saveSummary] data_id: " + data_id); + try { + await this._storage.writeSummary(data_id, data, true); + await browser.storage.session.remove(this._processing_prefix + data_id); + } catch (e) { + this.taLog.error("[saveSummary] error: " + e); + throw e; + } + } async saveError(data_id, error_message) { + this.taLog.log("[saveError] data_id: " + data_id + ", error_message: " + error_message); let data = { error: true, message: error_message, @@ -48,53 +67,67 @@ export const taSummaryStore = { }; await this.saveSummary(data, data_id); return data; - }, + } async loadSummary(data_id) { - const key = this._data_prefix + data_id; - let output = await browser.storage.session.get(key); - return output[key] || null; - }, + this.taLog.log("[loadSummary] data_id: " + data_id); + let record = await this._storage.getRecord(data_id); + if (!record || !this._storage.hasField(record, taStorage.FIELD_SUMMARY)) { + this.taLog.log("[loadSummary] no record found for data_id: " + data_id); + return null; + } + let summary = record.summary; + return { + headerMessageId: data_id, + summary: summary.summary, + error: summary.error || false, + message: summary.message || '', + summary_date: new Date(summary.summary_date || summary.ts), + }; + } async removeSummary(data_id) { - const key = this._data_prefix + data_id; - await browser.storage.session.remove(key); + this.taLog.log("[removeSummary] data_id: " + data_id); + await this._storage.deleteSummaryField(data_id); await browser.storage.session.remove(this._processing_prefix + data_id); - }, + } async getAllSummaries() { - let allData = await browser.storage.session.get(null); - let summaryData = {}; - - for (const [key, value] of Object.entries(allData)) { - if (key.startsWith(this._data_prefix)) { - summaryData[key.replace(this._data_prefix, '')] = value; - } - } - - return summaryData; - }, + this.taLog.log("[getAllSummaries] loading all summaries"); + return await this._storage.getAllSummaryRecords(); + } async clearSummaries() { - let allData = await browser.storage.session.get(null); - let keysToDelete = Object.keys(allData).filter(key => key.startsWith(this._data_prefix) || key.startsWith(this._processing_prefix)); - + this.taLog.log("[clearSummaries] clearing all summary data"); + let allSummaries = await this._storage.getAllSummaryRecords(); + let summaryKeys = Object.keys(allSummaries); + this.taLog.log("[clearSummaries] deleting " + summaryKeys.length + " summary records"); + for (let messageId of summaryKeys) { + await this._storage.deleteSummaryField(messageId); + } + let allSession = await browser.storage.session.get(null); + let keysToDelete = Object.keys(allSession).filter(k => k.startsWith(this._processing_prefix)); + this.taLog.log("[clearSummaries] deleting " + keysToDelete.length + " session keys"); for (let key of keysToDelete) { await browser.storage.session.remove(key); } - }, + } async truncSummaries() { - let data = await this.getAllSummaries(); + this.taLog.log("[truncSummaries] checking summary count"); + let data = await this._storage.getAllSummaryRecords(); let sortedData = this.sortSummariesByDate(data); let keys = Object.keys(sortedData); + this.taLog.log("[truncSummaries] total summaries: " + keys.length + ", max: " + this._max_summaries); if (keys.length > this._max_summaries) { + let toDelete = keys.length - this._max_summaries; + this.taLog.log("[truncSummaries] truncating " + toDelete + " oldest summaries"); for (let i = this._max_summaries; i < keys.length; i++) { - await browser.storage.session.remove(this._data_prefix + keys[i]); + await this._storage.deleteSummaryField(keys[i]); } } - }, + } sortSummariesByDate(data) { if (!data) return {}; @@ -112,4 +145,4 @@ export const taSummaryStore = { return sortedSummaries; } -}; \ No newline at end of file +} diff --git a/mzta-background.js b/mzta-background.js index 9836e444..ec5adfea 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -96,6 +96,7 @@ await reload_pref_init(); let taLog = new taLogger("mzta-background",prefs_init.do_debug); taWorkingStatus.taLog = taLog; let spamReport = new taSpamReport(prefs_init.do_debug); +let summaryStore = new taSummaryStore(prefs_init.do_debug); let special_prompts_ids = getActiveSpecialPromptsIDs({ addtags: prefs_init.add_tags, @@ -227,13 +228,13 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { let message = await browser.messageDisplay.getDisplayedMessage(tabId); if (!message) return; - let cachedSummary = await taSummaryStore.loadSummary(message.headerMessageId); + let cachedSummary = await summaryStore.loadSummary(message.headerMessageId); if (cachedSummary && !cachedSummary.error) { browser.tabs.sendMessage(tabId, { command: "showSummary", data: cachedSummary }); return; } - if (await taSummaryStore.isProcessing(message.headerMessageId)) { + if (await summaryStore.isProcessing(message.headerMessageId)) { browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); return; } @@ -265,7 +266,7 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { case 'refreshSummary': async function _refreshSummary(message) { let tabId = sender.tab.id; - await taSummaryStore.removeSummary(message.headerMessageId); + await summaryStore.removeSummary(message.headerMessageId); await _generateSummaryForMessage(message.headerMessageId, tabId); } _refreshSummary(message); @@ -445,23 +446,23 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { ...getDynamicSettingsDefaults(['use_specific_integration', 'connection_type']) }); - let cachedSummary = await taSummaryStore.loadSummary(headerMessageId); + let cachedSummary = await summaryStore.loadSummary(headerMessageId); if (cachedSummary && !cachedSummary.error) { browser.tabs.sendMessage(tabId, { command: "showSummary", data: cachedSummary }); return; } - if (await taSummaryStore.isProcessing(headerMessageId)) { + if (await summaryStore.isProcessing(headerMessageId)) { browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); return; } - await taSummaryStore.setProcessing(headerMessageId); + await summaryStore.setProcessing(headerMessageId); browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); const messageResult = await browser.messages.query({ headerMessageId: headerMessageId }); if (!messageResult || messageResult.messages.length === 0) { - await taSummaryStore.saveError(headerMessageId, "Message not found"); + await summaryStore.saveError(headerMessageId, "Message not found"); browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: "Message not found" } }); return; } @@ -479,7 +480,7 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { if (connectionType === 'chatgpt_web') { const errorMsg = browser.i18n.getMessage('summarize_chatgpt_web_not_supported'); - await taSummaryStore.saveError(headerMessageId, errorMsg); + await summaryStore.saveError(headerMessageId, errorMsg); browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: errorMsg } }); return; } @@ -503,12 +504,12 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { summary_date: new Date(), headerMessageId: headerMessageId }; - await taSummaryStore.saveSummary(summaryData, headerMessageId); + await summaryStore.saveSummary(summaryData, headerMessageId); browser.tabs.sendMessage(tabId, { command: "showSummary", data: summaryData }); } catch (error) { console.error("[ThunderAI] Error generating summary:", error); - await taSummaryStore.saveError(headerMessageId, error.message || String(error)); + await summaryStore.saveError(headerMessageId, error.message || String(error)); browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: error.message || "Failed to generate summary" } }); } } From 7bac86cc82b502f3bc9e94a1b9bf290236fc12f7 Mon Sep 17 00:00:00 2001 From: mic Date: Mon, 23 Mar 2026 22:21:03 +0100 Subject: [PATCH 15/52] spec updated --- claude-spec/01-architecture.md | 34 +++++++++++++++++++++++++++++++++- claude-spec/02-prompts.md | 23 +++++++++++++++++++++++ claude-spec/05-options.md | 17 +++++++++++++++++ 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/claude-spec/01-architecture.md b/claude-spec/01-architecture.md index 94dcdead..cf3b98d6 100644 --- a/claude-spec/01-architecture.md +++ b/claude-spec/01-architecture.md @@ -41,6 +41,30 @@ js/mzta-prompts.js (builds final prompt string) js/mzta-compose-script.js (inserts text into Thunderbird compose window) ``` +### Data Flow: Inline Summary on Message Display + +``` +User opens/selects a message in Thunderbird + ↓ +mzta-compose-script.js (sends "initSummary" to background) + ↓ +mzta-background.js (checks summarize_auto pref) + ↓ + ┌────────────────────────────────────────────────┐ + │ summarize_auto = 0 → do nothing │ + │ summarize_auto = 1 → show "click to generate" │ + │ summarize_auto = 2 → generate immediately │ + └────────────────────────────────────────────────┘ + ↓ (if generating) + taSummaryStore (check cache / set processing) + ↓ (cache miss) + mzta-special-commands (via Web Worker, NOT chatgpt_web) + ↓ + taSummaryStore (save result via taStorage) + ↓ + mzta-compose-script.js (render summary banner in message body) +``` + ## Key Modules | File | Role | @@ -51,13 +75,15 @@ js/mzta-compose-script.js (inserts text into Thunderbird compose window) | `js/mzta-placeholders.js` | Placeholder definitions and resolution logic | | `js/mzta-utils.js` | General utilities (email parsing, storage helpers, etc.) | | `js/mzta-utils-prompt.js` | Prompt-specific utilities (text truncation, lang injection) | -| `js/mzta-compose-script.js` | Injects AI response into Thunderbird compose window | +| `js/mzta-compose-script.js` | Content script for compose and message display: injects AI response into compose window, renders summary/spam banners in message display | | `js/mzta-chatgpt.js` | ChatGPT Web integration (opens browser window, reads DOM) | | `js/mzta-special-commands.js` | Handles special prompt actions (add_tags, calendar, task) | | `js/mzta-spamreport.js` | Spam filter logic | | `js/mzta-i18n.js` | i18n helper (wraps `browser.i18n.getMessage`) | | `js/mzta-logger.js` | Debug logging (gated by `do_debug` pref) | | `js/mzta-store.js` | Storage abstraction helpers | +| `js/mzta-storage.js` | Unified per-message storage layer (`taStorage` class) for summary, spam, and translation data | +| `js/mzta-summarystore.js` | Summary-specific storage wrapper (`taSummaryStore` class) with caching, truncation, and processing-state tracking | | `js/mzta-working-status.js` | Visual status indicator during AI processing | | `js/mzta-addatags-exclusion-list.js` | Tag exclusion list management | | `js/mzta-placeholders-autocomplete.js` | Autocomplete for placeholders in prompt editor | @@ -106,3 +132,9 @@ Each subdirectory is a self-contained settings/UI page for a specific feature: ## Storage All preferences are stored via `browser.storage.local`. The keys and default values are defined in `options/mzta-options-default.js` (`prefs_default` export). Custom prompts and custom placeholders are stored separately in storage under their own keys. + +### Per-Message Data Storage + +Per-message data (summaries, spam reports, translations) is stored via `js/mzta-storage.js` (`taStorage` class). Each record is keyed by `msg:` in `messenger.storage.local` and follows schema version 1. Records contain optional fields: `summary`, `spam`, `translation`, plus metadata (`v`, `ts`). The `taStorage` class provides typed read/write/delete methods per field, automatic record cleanup when all fields are removed, and age-based cleanup. + +`js/mzta-summarystore.js` (`taSummaryStore` class) wraps `taStorage` for summary-specific operations: load/save/remove summaries, track in-flight generation state via `browser.storage.session`, enforce a 100-entry cache limit with oldest-first truncation, and store error states. diff --git a/claude-spec/02-prompts.md b/claude-spec/02-prompts.md index 2e37e189..ad368cb3 100644 --- a/claude-spec/02-prompts.md +++ b/claude-spec/02-prompts.md @@ -58,6 +58,29 @@ Some prompts trigger additional Thunderbird actions beyond just sending text to These special prompts can have their own dedicated API integration settings (configured in the Options page). The list of these special prompts is in `options/mzta-options-default.js` as `special_prompts_with_integration`. +### Summarize: Dual-Mode Prompt System + +The summarize feature uses two distinct prompt pathways: + +**Context Menu Summarize** (right-click on messages in message list): +- Activated via the `summarize` context menu item, controlled by the `summarize` feature flag +- Uses 3 special prompts stored in `specialPrompts`: + - `prompt_summarize` — the main instruction prompt for the LLM + - `prompt_summarize_email_template` — template for formatting each email's content + - `prompt_summarize_email_separator` — separator text between multiple emails +- Supports multi-email summarization: each selected message is formatted with the email template, joined by the separator, then prepended with the instruction prompt +- All 3 prompts support placeholder autocomplete (`{%placeholder%}` syntax) +- Result is displayed via `openChatGPT()` in the standard chat output window (not inline) +- Default prompt texts are stored as i18n keys: `prompt_summarize_full_text`, `prompt_summarize_email_template_full_text`, `prompt_summarize_email_separator_full_text` + +**Inline Summary on Message Display** (automatic or manual per `summarize_auto` pref): +- Uses a single i18n string `auto_summary_prompt` concatenated with the message body text +- Does **not** use the 3 special prompts above +- Does **not** support `chatgpt_web` connection type (shows error if configured) +- Result is rendered as a styled banner at the top of the message body via `mzta-compose-script.js` +- Banner includes a refresh button (↻) to regenerate the summary +- Cached per-message via `taSummaryStore` / `taStorage` (max 100 entries) + ## Prompt Types Reference ``` diff --git a/claude-spec/05-options.md b/claude-spec/05-options.md index f1dc571c..7f399f6f 100644 --- a/claude-spec/05-options.md +++ b/claude-spec/05-options.md @@ -95,6 +95,23 @@ 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 Settings Page (`pages/summarize/`) + +The summarize settings page provides: + +1. **Specific integration checkbox** — enables per-feature API override (like other special prompts) +2. **Auto-summarize dropdown** (`summarize_auto`) — three modes: + - `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): + - Summarize instruction prompt (`prompt_summarize`) + - Email template prompt (`prompt_summarize_email_template`) + - Email separator prompt (`prompt_summarize_email_separator`) + - Each has Save/Reset buttons and placeholder autocomplete + - Default text comes from i18n strings (`prompt_summarize_full_text`, etc.) ## Adding a New Preference From 58437ad16853d3e257cb79bf5426ecca5a6be504 Mon Sep 17 00:00:00 2001 From: mic Date: Mon, 23 Mar 2026 23:24:54 +0100 Subject: [PATCH 16/52] summarize_display_mode added. see #580 --- _locales/en/messages.json | 16 ++ claude-spec/01-architecture.md | 27 +++- claude-spec/05-options.md | 7 +- js/mzta-compose-script.js | 6 +- mzta-background.js | 231 +++++++++++++++++++--------- options/mzta-options-default.js | 1 + pages/summarize/mzta-summarize.html | 12 ++ pages/summarize/mzta-summarize.js | 3 + 8 files changed, 218 insertions(+), 85 deletions(-) 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 @@ + + __MSG_prefs_OptionText_summarize_display_mode__ + + + + diff --git a/pages/summarize/mzta-summarize.js b/pages/summarize/mzta-summarize.js index fc47b540..93a3cdc6 100644 --- a/pages/summarize/mzta-summarize.js +++ b/pages/summarize/mzta-summarize.js @@ -240,6 +240,9 @@ async function restoreOptions() { if (element.id === '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; // Check if option exists let optionExists = Array.from(element.options).some(opt => opt.value === String(restoreValue)); From 6e30ea915996d1f9381b95b7fb05f6803aa35919 Mon Sep 17 00:00:00 2001 From: mic Date: Mon, 23 Mar 2026 23:28:38 +0100 Subject: [PATCH 17/52] always showing a summary if present. see #580 --- mzta-background.js | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/mzta-background.js b/mzta-background.js index e221d674..2d08d08c 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -223,37 +223,33 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { try { let tabId = sender.tab.id; 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; + // Always show cached summary if available, regardless of summarize_auto + 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 summarize_auto is disabled, don't show button or auto-generate + if (prefs.summarize_auto === 0) return; + // 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 }); From a505f22261d61febc464b5c3ee490c0d63b2af40 Mon Sep 17 00:00:00 2001 From: Mic Date: Mon, 23 Mar 2026 23:59:00 +0100 Subject: [PATCH 18/52] minor fixes --- mzta-background.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mzta-background.js b/mzta-background.js index 2d08d08c..06c6d50b 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -19,7 +19,6 @@ import { mzta_script } from './js/mzta-chatgpt.js'; import { prefs_default, - getDynamicSettingValue, getDynamicSettingsDefaults } from './options/mzta-options-default.js'; import { mzta_Menus } from './js/mzta-menus.js'; @@ -222,7 +221,7 @@ 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, summarize_display_mode: prefs_default.summarize_display_mode }); + let prefs = await browser.storage.sync.get({ summarize_auto: prefs_default.summarize_auto, summarize_display_mode: prefs_default.summarize_display_mode }); let message = await browser.messageDisplay.getDisplayedMessage(tabId); if (!message) return; From b08b8563d0d3c3a4c72195eab0cceefcc90ba0d0 Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 24 Mar 2026 00:01:00 +0100 Subject: [PATCH 19/52] code refactored to have one method to calculate the final prompt. see #580 --- _locales/en/messages.json | 3 - claude-spec/01-architecture.md | 2 +- claude-spec/02-prompts.md | 8 ++- js/mzta-utils-prompt.js | 49 ++++++++++++++- mzta-background.js | 109 ++++----------------------------- 5 files changed, 67 insertions(+), 104 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 093c95c7..2e219924 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1900,9 +1900,6 @@ "auto_summary_failed": { "message": "Failed to generate AI summary. Please confirm your settings and try again." }, - "auto_summary_prompt": { - "message": "Please provide a concise summary of the following email message. The summary should be 3-5 sentences maximum and capture the main points:\n\n" - }, "customPrompts_export_include_api_settings": { "message": "Do you want to include the API settings in the export? Be aware that also the API Key will be saved in the file!", "description": "" diff --git a/claude-spec/01-architecture.md b/claude-spec/01-architecture.md index 7426a4c2..085affed 100644 --- a/claude-spec/01-architecture.md +++ b/claude-spec/01-architecture.md @@ -87,7 +87,7 @@ mzta-background.js (checks summarize_auto + summarize_display_mode prefs) | `js/mzta-prompts.js` | Prompt definitions (built-in) and custom prompt loading | | `js/mzta-placeholders.js` | Placeholder definitions and resolution logic | | `js/mzta-utils.js` | General utilities (email parsing, storage helpers, etc.) | -| `js/mzta-utils-prompt.js` | Prompt-specific utilities (text truncation, lang injection) | +| `js/mzta-utils-prompt.js` | Prompt-specific utilities (text truncation, lang injection, `buildSummaryPrompt()` for unified summary prompt assembly) | | `js/mzta-compose-script.js` | Content script for compose and message display: injects AI response into compose window, renders summary/spam banners in message display | | `js/mzta-chatgpt.js` | ChatGPT Web integration (opens browser window, reads DOM) | | `js/mzta-special-commands.js` | Handles special prompt actions (add_tags, calendar, task) | diff --git a/claude-spec/02-prompts.md b/claude-spec/02-prompts.md index ad368cb3..aa2b3864 100644 --- a/claude-spec/02-prompts.md +++ b/claude-spec/02-prompts.md @@ -74,13 +74,17 @@ The summarize feature uses two distinct prompt pathways: - Default prompt texts are stored as i18n keys: `prompt_summarize_full_text`, `prompt_summarize_email_template_full_text`, `prompt_summarize_email_separator_full_text` **Inline Summary on Message Display** (automatic or manual per `summarize_auto` pref): -- Uses a single i18n string `auto_summary_prompt` concatenated with the message body text -- Does **not** use the 3 special prompts above +- Uses the same 3 special prompts as webchat mode, via `taPromptUtils.buildSummaryPrompt()` in `js/mzta-utils-prompt.js` - Does **not** support `chatgpt_web` connection type (shows error if configured) - Result is rendered as a styled banner at the top of the message body via `mzta-compose-script.js` - Banner includes a refresh button (↻) to regenerate the summary - Cached per-message via `taSummaryStore` / `taStorage` (max 100 entries) +**Unified Prompt Building** — `taPromptUtils.buildSummaryPrompt(messageDataArray)`: +- All summary paths (inline, webchat single, webchat multi) use this single method +- Accepts an array of `{ message, fullMessage }` entries +- Returns `{ promptText, promptInfo }` where `promptInfo` is the `prompt_summarize` prompt object + ## Prompt Types Reference ``` diff --git a/js/mzta-utils-prompt.js b/js/mzta-utils-prompt.js index 28978662..c81588c2 100644 --- a/js/mzta-utils-prompt.js +++ b/js/mzta-utils-prompt.js @@ -17,7 +17,12 @@ */ import { placeholdersUtils } from './mzta-placeholders.js'; -import { extractJsonObject } from './mzta-utils.js'; +import { + extractJsonObject, + getMailBody, + htmlBodyToPlainText +} from './mzta-utils.js'; +import { getSpecialPrompts } from './mzta-prompts.js'; import { prefs_default } from '../options/mzta-options-default.js'; export const taPromptUtils = { @@ -126,6 +131,48 @@ export const taPromptUtils = { return chatgpt_lang; }, + + async buildSummaryPrompt(messageDataArray) { + 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 messages_list = []; + for (let entry of messageDataArray) { + const bodyHtml = getMailBody(entry.fullMessage); + let bodyText = htmlBodyToPlainText(bodyHtml.html); + if (bodyText.length === 0) { + bodyText = bodyHtml.text || ''; + } + + messages_list.push(await taPromptUtils.preparePrompt({ + curr_prompt: prompt_email, + curr_message: entry.message, + chatgpt_lang: chatgpt_lang, + body_text: bodyText, + subject_text: entry.fullMessage.headers.subject, + msg_text: bodyHtml, + })); + } + + const messages_string = messages_list.join(prompt_email_separator_string); + const promptText = prompt_string + prompt_email_separator_string + messages_string; + + return { promptText, promptInfo: prompt }; + }, + /** * Extracts tags from the response text. * @param {string} response_text - The response text from which to extract tags. diff --git a/mzta-background.js b/mzta-background.js index 06c6d50b..0138c34b 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -57,9 +57,8 @@ import { } from './js/mzta-utils.js'; import { taPromptUtils } from './js/mzta-utils-prompt.js'; import { mzta_specialCommand } from './js/mzta-special-commands.js'; -import { - getSpamFilterPrompt, - getSpecialPrompts +import { + getSpamFilterPrompt } from './js/mzta-prompts.js'; import { taSpamReport } from './js/mzta-spamreport.js'; import { taSummaryStore } from './js/mzta-summarystore.js'; @@ -484,13 +483,6 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { } const fullMessage = await browser.messages.getFull(messageResult.messages[0].id); - const mailBody = getMailBody(fullMessage); - let bodyText = htmlBodyToPlainText(mailBody.html); - if (bodyText.length === 0) { - bodyText = mailBody.text.replace(/\s+/g, ' ').trim(); - } - - const promptText = browser.i18n.getMessage('auto_summary_prompt') + bodyText; const connectionType = getConnectionType(prefs, {}, 'summarize'); @@ -501,6 +493,8 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { return; } + const { promptText } = await taPromptUtils.buildSummaryPrompt([{ message: messageResult.messages[0], fullMessage }]); + const cmd = new mzta_specialCommand({ prompt: promptText, llm: connectionType, @@ -532,22 +526,6 @@ 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); @@ -556,24 +534,10 @@ async function _openSummaryWebchat(headerMessageId, tabId) { 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 { promptText, promptInfo } = await taPromptUtils.buildSummaryPrompt([{ message: curr_message, fullMessage: curr_message_full }]); - const full_prompt = prompt_string + prompt_email_separator_string + email_text; - - openChatGPT(full_prompt, prompt.action, tabId, prompt.name, prompt.need_custom_text, prompt); + openChatGPT(promptText, promptInfo.action, tabId, promptInfo.name, promptInfo.need_custom_text, promptInfo); } catch (error) { console.error("[ThunderAI] Error opening summary webchat:", error); } @@ -1464,63 +1428,14 @@ async function processEmails(args) { 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 = []; + const messageDataArray = []; for (let curr_message of messageArray) { + const fullMessage = await browser.messages.getFull(curr_message.id); + messageDataArray.push({ message: curr_message, fullMessage }); + } + const { promptText, promptInfo } = await taPromptUtils.buildSummaryPrompt(messageDataArray); - // 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 - ); + openChatGPT(promptText, promptInfo.action, tabId, promptInfo.name, promptInfo.need_custom_text, promptInfo); } } From 9e0fb5bf7362186681b34057e2931ac7360077e6 Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 24 Mar 2026 00:12:00 +0100 Subject: [PATCH 20/52] improved spamreport code --- _locales/en/messages.json | 4 + js/mzta-compose-script.js | 29 +++++- mzta-background.js | 205 ++++++++++++++++++++++++-------------- 3 files changed, 164 insertions(+), 74 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 2e219924..287ef765 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1988,6 +1988,10 @@ "message": "Refresh summary", "description": "" }, + "spamfilter_refresh": { + "message": "Refresh spam report", + "description": "" + }, "antispam_by": { "message": "Antispam by", "description": "" diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index f7f106db..cd3e2812 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -702,6 +702,18 @@ switch (message.command) { branding.textContent = browser.i18n.getMessage("antispam_by") + " ThunderAI"; branding.style.cssText = 'margin-left: auto; font-style: italic; font-size: 10px; opacity: 0.5;'; + const spamRefreshBtn = document.createElement('span'); + spamRefreshBtn.textContent = '↻'; + spamRefreshBtn.title = browser.i18n.getMessage("spamfilter_refresh") || 'Refresh spam report'; + spamRefreshBtn.style.cssText = 'cursor: pointer; opacity: 0.6; font-size: 16px; padding: 0 5px; transition: opacity 0.2s;'; + spamRefreshBtn.onmouseover = () => spamRefreshBtn.style.opacity = '1'; + spamRefreshBtn.onmouseout = () => spamRefreshBtn.style.opacity = '0.6'; + spamRefreshBtn.onclick = function() { + spamRefreshBtn.onclick = null; + spamRefreshBtn.style.opacity = '0.6'; + browser.runtime.sendMessage({ command: "refreshSpamReport", headerMessageId: data.headerMessageId }); + }; + const closeBtn = document.createElement('span'); closeBtn.textContent = '×'; closeBtn.style.cssText = 'cursor: pointer; font-weight: bold; font-size: 16px; padding: 0 5px;'; @@ -714,6 +726,7 @@ switch (message.command) { container.appendChild(scoreText); container.appendChild(reasonText); container.appendChild(branding); + container.appendChild(spamRefreshBtn); container.appendChild(closeBtn); document.body.insertBefore(container, document.body.firstChild); @@ -773,8 +786,22 @@ switch (message.command) { }); }; + const summaryCloseBtn = document.createElement('span'); + summaryCloseBtn.textContent = '×'; + summaryCloseBtn.style.cssText = 'cursor: pointer; font-weight: bold; font-size: 16px; padding: 0 5px;'; + summaryCloseBtn.title = browser.i18n.getMessage("chatgpt_win_close"); + summaryCloseBtn.onclick = function() { + summaryContainer.remove(); + browser.runtime.sendMessage({ command: "removeSummary", headerMessageId: summaryData.headerMessageId }); + }; + + const summaryBtnGroup = document.createElement('span'); + summaryBtnGroup.style.cssText = 'display: flex; align-items: center; gap: 5px;'; + summaryBtnGroup.appendChild(refreshBtn); + summaryBtnGroup.appendChild(summaryCloseBtn); + summaryHeader.appendChild(summaryTitle); - summaryHeader.appendChild(refreshBtn); + summaryHeader.appendChild(summaryBtnGroup); summaryContainer.appendChild(summaryHeader); const summaryText = document.createElement('div'); diff --git a/mzta-background.js b/mzta-background.js index 0138c34b..57487ef2 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -286,6 +286,9 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { } _refreshSummary(message); break; + case 'removeSummary': + summaryStore.removeSummary(message.headerMessageId); + break; // case 'chatgpt_open': // openChatGPT(message.prompt,message.action,message.tabId); // return true; @@ -445,6 +448,9 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { case 'removeSpamReport': spamReport.removeReportData(message.headerMessageId); break; + case 'refreshSpamReport': + _generateSpamReportForMessage(message.headerMessageId); + break; default: break; } @@ -524,6 +530,124 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { } } +// options.messageData: { message, fullMessage, body_text, msg_text } — pass pre-fetched data to avoid re-querying +// options.prefs: pass pre-fetched prefs to avoid re-querying +// options.autoMove: if true, move spam messages to junk folder (default: false) +async function _generateSpamReportForMessage(headerMessageId, options = {}) { + try { + let prefs = options.prefs || await browser.storage.sync.get({ + connection_type: prefs_default.connection_type, + do_debug: prefs_default.do_debug, + default_chatgpt_lang: prefs_default.default_chatgpt_lang, + spamfilter_threshold: prefs_default.spamfilter_threshold, + ...getDynamicSettingsDefaults(['use_specific_integration', 'connection_type']), + }); + + await spamReport.removeReportData(headerMessageId); + await spamReport.setProcessing(headerMessageId); + + await updateSpamPanel(headerMessageId, "showSpamCheckInProgress"); + + let message, curr_fullMessage, msg_text, body_text; + + if (options.messageData) { + message = options.messageData.message; + curr_fullMessage = options.messageData.fullMessage; + msg_text = options.messageData.msg_text; + body_text = options.messageData.body_text; + } else { + const messageResult = await browser.messages.query({ headerMessageId: headerMessageId }); + if (!messageResult || messageResult.messages.length === 0) { + let err_data = await spamReport.saveError(headerMessageId, "Message not found"); + await updateSpamPanel(headerMessageId, "showSpamReport", err_data); + return { success: false }; + } + message = messageResult.messages[0]; + curr_fullMessage = await browser.messages.getFull(message.id); + msg_text = getMailBody(curr_fullMessage); + body_text = htmlBodyToPlainText(msg_text.html); + if (body_text.length == 0) { + body_text = msg_text.text.replace(/\s+/g, ' ').trim(); + } + } + + let curr_prompt_spamfilter = await getSpamFilterPrompt(); + let chatgpt_lang = await taPromptUtils.getDefaultLang(curr_prompt_spamfilter); + let specialFullPrompt_spamfilter = await taPromptUtils.preparePrompt({ + curr_prompt: curr_prompt_spamfilter, + curr_message: message, + chatgpt_lang: chatgpt_lang, + body_text: body_text, + subject_text: curr_fullMessage.headers.subject, + msg_text: msg_text + }); + taLog.log("Special prompt: " + specialFullPrompt_spamfilter); + + let cmd_spamfilter = new mzta_specialCommand({ + prompt: specialFullPrompt_spamfilter, + llm: getConnectionType(prefs, curr_prompt_spamfilter, 'spamfilter'), + custom_model: curr_prompt_spamfilter.model ? curr_prompt_spamfilter.model : '', + do_debug: prefs.do_debug, + config: curr_prompt_spamfilter + }); + await cmd_spamfilter.initWorker(); + + let spamfilter_result = ''; + taLog.log("Sending the prompt..."); + try { + spamfilter_result = (await cmd_spamfilter.sendPrompt()).trim(); + } catch (err) { + console.error("[ThunderAI | SpamFilter] Error getting spamfilter: ", err); + let err_data = await spamReport.saveError(headerMessageId, err.message || String(err)); + await updateSpamPanel(headerMessageId, "showSpamReport", err_data); + return { success: false }; + } + taLog.log("spamfilter_result: " + spamfilter_result); + + let jsonObj = {}; + taLog.log("Decoding the AI response..."); + try { + jsonObj = extractJsonObject(spamfilter_result); + } catch (e) { + console.error("[ThunderAI | SpamFilter] Error extracting JSON from AI response: ", e); + let err_data = await spamReport.saveError(headerMessageId, e.message || String(e)); + await updateSpamPanel(headerMessageId, "showSpamReport", err_data); + return { success: false }; + } + taLog.log("SpamFilter jsonObj: " + JSON.stringify(jsonObj)); + + let report_data = {}; + report_data.report_date = new Date(); + report_data.headerMessageId = headerMessageId; + report_data.spamValue = jsonObj.spamValue; + report_data.explanation = jsonObj.explanation; + report_data.subject = curr_fullMessage.headers.subject; + report_data.from = curr_fullMessage.headers.from; + report_data.message_date = new Date(message.date); + report_data.moved = false; + report_data.SpamThreshold = prefs.spamfilter_threshold || prefs_init.spamfilter_threshold; + + if (options.autoMove && jsonObj.spamValue >= report_data.SpamThreshold) { + taLog.log("Marking as spam [" + headerMessageId + "]"); + messenger.messages.update(message.id, { junk: true }); + let spamFolder = await messenger.folders.query({ accountId: message.folder.accountId, specialUse: ['junk'] }); + messenger.messages.move([message.id], spamFolder[0].id); + report_data.moved = true; + taLog.log("Marked as spam [" + headerMessageId + "]"); + } + + spamReport.saveReportData(report_data, headerMessageId); + await updateSpamPanel(headerMessageId, "showSpamReport", report_data); + return { success: true }; + + } catch (error) { + console.error("[ThunderAI] Error generating spam report:", error); + let err_data = await spamReport.saveError(headerMessageId, error.message || String(error)); + await updateSpamPanel(headerMessageId, "showSpamReport", err_data); + return { success: false }; + } +} + async function _openSummaryWebchat(headerMessageId, tabId) { try { const messageResult = await browser.messages.query({ headerMessageId: headerMessageId }); @@ -1335,79 +1459,14 @@ async function processEmails(args) { } } - await spamReport.removeReportData(message.headerMessageId); - await spamReport.setProcessing(message.headerMessageId); - - await updateSpamPanel(message.headerMessageId, "showSpamCheckInProgress"); - - let curr_prompt_spamfilter = await getSpamFilterPrompt(); - // console.log(">>>>>>>>>>>>> curr_prompt_spamfilter: " + JSON.stringify(curr_prompt_spamfilter)); - let chatgpt_lang = await taPromptUtils.getDefaultLang(curr_prompt_spamfilter); - let specialFullPrompt_spamfilter = await taPromptUtils.preparePrompt({ - curr_prompt: curr_prompt_spamfilter, - curr_message: message, - chatgpt_lang: chatgpt_lang, - body_text: body_text, - subject_text: curr_fullMessage.headers.subject, - msg_text: msg_text - }); - taLog.log("Special prompt: " + specialFullPrompt_spamfilter); - // console.log(">>>>>>>> Special prompt for spamfilter: " + specialFullPrompt_spamfilter); - let cmd_spamfilter = new mzta_specialCommand({ - prompt: specialFullPrompt_spamfilter, - llm: getConnectionType(prefs_aats, curr_prompt_spamfilter, 'spamfilter'), - custom_model: curr_prompt_spamfilter.model ? curr_prompt_spamfilter.model : '', - do_debug: prefs_aats.do_debug, - config: curr_prompt_spamfilter - }); - await cmd_spamfilter.initWorker(); - let spamfilter_result = ''; - taLog.log("Sending the prompt..."); - try { - spamfilter_result = (await cmd_spamfilter.sendPrompt()).trim(); - } catch (err) { - console.error("[ThunderAI | SpamFilter] Error getting spamfilter: ", err); - let err_data = await spamReport.saveError(message.headerMessageId, err.message || String(err)); - await updateSpamPanel(message.headerMessageId, "showSpamReport", err_data); - continue; - } - taLog.log("spamfilter_result: " + spamfilter_result); - let jsonObj = {}; - taLog.log("Decoding the AI response..."); - try { - jsonObj = extractJsonObject(spamfilter_result); - } catch (e) { - console.error("[ThunderAI | SpamFilter] Error extracting JSON from AI response: ", e); - let err_data = await spamReport.saveError(message.headerMessageId, e.message || String(e)); - await updateSpamPanel(message.headerMessageId, "showSpamReport", err_data); - continue; - } - taLog.log("SpamFilter jsonObj: " + JSON.stringify(jsonObj)); - - let report_data = {}; - report_data.report_date = new Date(); - report_data.headerMessageId = message.headerMessageId; - report_data.spamValue = jsonObj.spamValue; - report_data.explanation = jsonObj.explanation; - report_data.subject = curr_fullMessage.headers.subject; - report_data.from = curr_fullMessage.headers.from; - report_data.message_date = new Date(message.date); - report_data.moved = false; - report_data.SpamThreshold = prefs_init.spamfilter_threshold; - - if (jsonObj.spamValue >= prefs_init.spamfilter_threshold) { - taLog.log("Marking as spam [" + message.headerMessageId + "]"); - messenger.messages.update(message.id, { junk: true }); - let spamFolder = await messenger.folders.query({ accountId: message.folder.accountId, specialUse: ['junk'] }); - messenger.messages.move([message.id], spamFolder[0].id); - report_data.moved = true; - taLog.log("Marked as spam [" + message.headerMessageId + "]"); - } - - spamReport.saveReportData(report_data, message.headerMessageId); - - // Check if the message is currently displayed and update the banner - await updateSpamPanel(message.headerMessageId, "showSpamReport", report_data); + let result = await _generateSpamReportForMessage( + message.headerMessageId, + { + messageData: { message, fullMessage: curr_fullMessage, body_text, msg_text }, + prefs: prefs_aats, + autoMove: true + }); + if (!result.success) continue; } } } From 8d166b314eab91d15bf0ca9701a8ba68b5c1ad56 Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 24 Mar 2026 00:14:00 +0100 Subject: [PATCH 21/52] delete and refresh buttons are now styled the same --- _locales/en/messages.json | 8 ++++++++ js/mzta-compose-script.js | 12 ++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 287ef765..a5fd03f1 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1992,6 +1992,14 @@ "message": "Refresh spam report", "description": "" }, + "spamfilter_delete": { + "message": "Delete spam report", + "description": "" + }, + "summarize_delete": { + "message": "Delete summary", + "description": "" + }, "antispam_by": { "message": "Antispam by", "description": "" diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index cd3e2812..c78721e9 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -716,8 +716,10 @@ switch (message.command) { const closeBtn = document.createElement('span'); closeBtn.textContent = '×'; - closeBtn.style.cssText = 'cursor: pointer; font-weight: bold; font-size: 16px; padding: 0 5px;'; - closeBtn.title = browser.i18n.getMessage("chatgpt_win_close"); + closeBtn.style.cssText = 'cursor: pointer; opacity: 0.6; font-size: 16px; padding: 0 5px; transition: opacity 0.2s;'; + closeBtn.title = browser.i18n.getMessage("spamfilter_delete"); + closeBtn.onmouseover = () => closeBtn.style.opacity = '1'; + closeBtn.onmouseout = () => closeBtn.style.opacity = '0.6'; closeBtn.onclick = function() { container.remove(); browser.runtime.sendMessage({ command: "removeSpamReport", headerMessageId: data.headerMessageId }); @@ -788,8 +790,10 @@ switch (message.command) { const summaryCloseBtn = document.createElement('span'); summaryCloseBtn.textContent = '×'; - summaryCloseBtn.style.cssText = 'cursor: pointer; font-weight: bold; font-size: 16px; padding: 0 5px;'; - summaryCloseBtn.title = browser.i18n.getMessage("chatgpt_win_close"); + summaryCloseBtn.style.cssText = 'cursor: pointer; opacity: 0.6; font-size: 16px; padding: 0 5px; transition: opacity 0.2s;'; + summaryCloseBtn.title = browser.i18n.getMessage("summarize_delete"); + summaryCloseBtn.onmouseover = () => summaryCloseBtn.style.opacity = '1'; + summaryCloseBtn.onmouseout = () => summaryCloseBtn.style.opacity = '0.6'; summaryCloseBtn.onclick = function() { summaryContainer.remove(); browser.runtime.sendMessage({ command: "removeSummary", headerMessageId: summaryData.headerMessageId }); From fe0804a9ddba7baf1a90f8ba0fc9902293db0b65 Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 24 Mar 2026 00:19:00 +0100 Subject: [PATCH 22/52] collapse icon added to summary report. see #580 --- _locales/en/messages.json | 6 +++++- js/mzta-compose-script.js | 20 ++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index a5fd03f1..82119473 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1965,7 +1965,11 @@ "description": "" }, "summarize_title": { - "message": "Summary", + "message": "ThunderAI Overview", + "description": "" + }, + "summarize_collapse": { + "message": "Collapse summary", "description": "" }, "summarize_generating": { diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index c78721e9..d0ab5a6b 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -799,10 +799,18 @@ switch (message.command) { browser.runtime.sendMessage({ command: "removeSummary", headerMessageId: summaryData.headerMessageId }); }; + const collapseBtn = document.createElement('span'); + collapseBtn.textContent = '∧'; + collapseBtn.title = browser.i18n.getMessage("summarize_collapse") || 'Collapse summary'; + collapseBtn.style.cssText = `cursor: pointer; opacity: 0.6; font-size: 16px; transition: opacity 0.2s;`; + collapseBtn.onmouseover = () => collapseBtn.style.opacity = '1'; + collapseBtn.onmouseout = () => collapseBtn.style.opacity = '0.6'; + const summaryBtnGroup = document.createElement('span'); summaryBtnGroup.style.cssText = 'display: flex; align-items: center; gap: 5px;'; summaryBtnGroup.appendChild(refreshBtn); summaryBtnGroup.appendChild(summaryCloseBtn); + summaryBtnGroup.appendChild(collapseBtn); summaryHeader.appendChild(summaryTitle); summaryHeader.appendChild(summaryBtnGroup); @@ -817,6 +825,18 @@ switch (message.command) { } summaryText.style.cssText = `font-size: 14px; line-height: 1.4;`; + collapseBtn.onclick = () => { + if (summaryText.style.display === 'none') { + summaryText.style.display = ''; + summaryHeader.style.marginBottom = '0.5rem'; + collapseBtn.textContent = '∧'; + } else { + summaryText.style.display = 'none'; + summaryHeader.style.marginBottom = '0'; + collapseBtn.textContent = '∨'; + } + }; + summaryContainer.appendChild(summaryText); document.body.insertBefore(summaryContainer, document.body.firstChild); From 9a00477067f0209bf5d7805850cc05a2928962c3 Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 24 Mar 2026 00:27:00 +0100 Subject: [PATCH 23/52] hover colors improved --- js/mzta-compose-script.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index d0ab5a6b..d4460e82 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -776,9 +776,9 @@ switch (message.command) { const refreshBtn = document.createElement('span'); refreshBtn.textContent = '↻'; refreshBtn.title = browser.i18n.getMessage("summarize_refresh") || 'Refresh summary'; - refreshBtn.style.cssText = `cursor: pointer; opacity: 0.6; font-size: 16px; transition: opacity 0.2s;`; - refreshBtn.onmouseover = () => refreshBtn.style.opacity = '1'; - refreshBtn.onmouseout = () => refreshBtn.style.opacity = '0.6'; + refreshBtn.style.cssText = `cursor: pointer; opacity: 0.6; font-size: 16px; transition: opacity 0.2s, color 0.2s;`; + refreshBtn.onmouseover = () => { refreshBtn.style.opacity = '1'; refreshBtn.style.color = isDarkSummary ? '#4d9de0' : '#1a5fa8'; }; + refreshBtn.onmouseout = () => { refreshBtn.style.opacity = '0.6'; refreshBtn.style.color = ''; }; refreshBtn.onclick = async () => { refreshBtn.onclick = null; refreshBtn.style.opacity = '0.6'; @@ -790,10 +790,10 @@ switch (message.command) { const summaryCloseBtn = document.createElement('span'); summaryCloseBtn.textContent = '×'; - summaryCloseBtn.style.cssText = 'cursor: pointer; opacity: 0.6; font-size: 16px; padding: 0 5px; transition: opacity 0.2s;'; + summaryCloseBtn.style.cssText = 'cursor: pointer; opacity: 0.6; font-size: 16px; padding: 0 5px; transition: opacity 0.2s, color 0.2s;'; summaryCloseBtn.title = browser.i18n.getMessage("summarize_delete"); - summaryCloseBtn.onmouseover = () => summaryCloseBtn.style.opacity = '1'; - summaryCloseBtn.onmouseout = () => summaryCloseBtn.style.opacity = '0.6'; + summaryCloseBtn.onmouseover = () => { summaryCloseBtn.style.opacity = '1'; summaryCloseBtn.style.color = '#cc0000'; }; + summaryCloseBtn.onmouseout = () => { summaryCloseBtn.style.opacity = '0.6'; summaryCloseBtn.style.color = ''; }; summaryCloseBtn.onclick = function() { summaryContainer.remove(); browser.runtime.sendMessage({ command: "removeSummary", headerMessageId: summaryData.headerMessageId }); From e02a8b1c142b764ab83695961669f3029f434248 Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 24 Mar 2026 00:31:00 +0100 Subject: [PATCH 24/52] loading indicator for summary added. see #580 --- js/mzta-compose-script.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index d4460e82..be26c351 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -862,13 +862,18 @@ switch (message.command) { const generatingContainer = document.createElement('div'); generatingContainer.id = 'mzta-summary-generating'; generatingContainer.className = 'thunderai-summary-pane'; - generatingContainer.style.cssText = `background-color: ${bgColorGen}; color: ${textColorGen}; padding: 0.5rem; margin-bottom: 1rem; border-radius: 4px; border: 1px solid ${borderColorGen}; font-family: system-ui, -apple-system, sans-serif; font-size: 14px;`; + generatingContainer.style.cssText = `background-color: ${bgColorGen}; color: ${textColorGen}; padding: 0.5rem; margin-bottom: 1rem; border-radius: 4px; border: 1px solid ${borderColorGen}; font-family: system-ui, -apple-system, sans-serif; font-size: 14px; display: flex; align-items: center; gap: 10px;`; - const generatingTitle = document.createElement('div'); + const generatingLoadingImg = document.createElement('img'); + generatingLoadingImg.src = browser.runtime.getURL("/images/loading.gif"); + generatingLoadingImg.style.cssText = "height: 16px; width: 16px;"; + + const generatingTitle = document.createElement('strong'); generatingTitle.className = 'thunderai-summary-title'; generatingTitle.textContent = browser.i18n.getMessage("summarize_generating"); - generatingTitle.style.cssText = `font-weight: bold; font-size: 14px; margin-bottom: 0.5rem; color: ${titleColorGen};`; + generatingTitle.style.cssText = `font-size: 14px; color: ${titleColorGen};`; + generatingContainer.appendChild(generatingLoadingImg); generatingContainer.appendChild(generatingTitle); document.body.insertBefore(generatingContainer, document.body.firstChild); From 438670d28cd49e75ea6941e294cc60206b388dda Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 24 Mar 2026 18:46:54 +0100 Subject: [PATCH 25/52] css fix --- js/mzta-compose-script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index be26c351..55db71ee 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -682,7 +682,7 @@ switch (message.command) { borderColor = '#006600'; } - container.style.cssText = `background-color: ${bgColor}; color: ${textColor}; border-bottom: 1px solid ${borderColor}; padding: 8px 12px; font-family: system-ui, -apple-system, sans-serif; font-size: 13px; display: flex; align-items: center; gap: 15px; width: 100%; box-sizing: border-box;`; + container.style.cssText = `background-color: ${bgColor}; color: ${textColor}; border-bottom: 1px solid ${borderColor}; border-radius:4px; padding: 8px 12px; font-family: system-ui, -apple-system, sans-serif; font-size: 13px; display: flex; align-items: center; gap: 15px; width: 100%; box-sizing: border-box;`; const scoreText = document.createElement('strong'); if (data.spamValue == -999) { From 630938e30f93739597466292baf9088ca0a1b445 Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 24 Mar 2026 18:50:19 +0100 Subject: [PATCH 26/52] hover color added --- js/mzta-compose-script.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 55db71ee..9c1cb7ab 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -705,9 +705,9 @@ switch (message.command) { const spamRefreshBtn = document.createElement('span'); spamRefreshBtn.textContent = '↻'; spamRefreshBtn.title = browser.i18n.getMessage("spamfilter_refresh") || 'Refresh spam report'; - spamRefreshBtn.style.cssText = 'cursor: pointer; opacity: 0.6; font-size: 16px; padding: 0 5px; transition: opacity 0.2s;'; - spamRefreshBtn.onmouseover = () => spamRefreshBtn.style.opacity = '1'; - spamRefreshBtn.onmouseout = () => spamRefreshBtn.style.opacity = '0.6'; + spamRefreshBtn.style.cssText = 'cursor: pointer; opacity: 0.6; font-size: 16px; padding: 0 5px; transition: opacity 0.2s, color 0.2s;'; + spamRefreshBtn.onmouseover = () => { spamRefreshBtn.style.opacity = '1'; spamRefreshBtn.style.color = isDark ? '#4d9de0' : '#1a5fa8'; }; + spamRefreshBtn.onmouseout = () => { spamRefreshBtn.style.opacity = '0.6'; spamRefreshBtn.style.color = ''; }; spamRefreshBtn.onclick = function() { spamRefreshBtn.onclick = null; spamRefreshBtn.style.opacity = '0.6'; @@ -716,10 +716,10 @@ switch (message.command) { const closeBtn = document.createElement('span'); closeBtn.textContent = '×'; - closeBtn.style.cssText = 'cursor: pointer; opacity: 0.6; font-size: 16px; padding: 0 5px; transition: opacity 0.2s;'; + closeBtn.style.cssText = 'cursor: pointer; opacity: 0.6; font-size: 16px; padding: 0 5px; transition: opacity 0.2s, color 0.2s;'; closeBtn.title = browser.i18n.getMessage("spamfilter_delete"); - closeBtn.onmouseover = () => closeBtn.style.opacity = '1'; - closeBtn.onmouseout = () => closeBtn.style.opacity = '0.6'; + closeBtn.onmouseover = () => { closeBtn.style.opacity = '1'; closeBtn.style.color = '#cc0000'; }; + closeBtn.onmouseout = () => { closeBtn.style.opacity = '0.6'; closeBtn.style.color = ''; }; closeBtn.onclick = function() { container.remove(); browser.runtime.sendMessage({ command: "removeSpamReport", headerMessageId: data.headerMessageId }); From 6b37ba894bc7a8de4060cb2d3257da7a927050fd Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 24 Mar 2026 22:15:32 +0100 Subject: [PATCH 27/52] icons are now in a menu --- js/mzta-compose-script.js | 199 ++++++++++++++++++++++++-------------- 1 file changed, 124 insertions(+), 75 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 9c1cb7ab..86dc7dd5 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -16,6 +16,85 @@ * along with this program. If not, see . */ +function createThreeDotsMenu(isDark, menuItems, panelColors) { + const wrapper = document.createElement('div'); + wrapper.style.cssText = 'position: relative; display: inline-block;'; + + const dotsBtn = document.createElement('span'); + dotsBtn.textContent = '\u22EE'; + dotsBtn.style.cssText = 'cursor: pointer; opacity: 0.7; font-size: 18px; padding: 2px 6px; line-height: 1; user-select: none; transition: opacity 0.2s;'; + dotsBtn.onmouseover = () => dotsBtn.style.opacity = '1'; + dotsBtn.onmouseout = () => dotsBtn.style.opacity = '0.7'; + + const dropdown = document.createElement('div'); + const dropdownBg = panelColors.bg; + const dropdownBorder = panelColors.border; + const defaultTextColor = panelColors.text; + dropdown.style.cssText = `display: none; position: absolute; right: 0; top: 100%; z-index: 9999; min-width: 180px; background-color: ${dropdownBg}; border: 1px solid ${dropdownBorder}; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); overflow: hidden;`; + + menuItems.forEach(item => { + const row = document.createElement('div'); + row.style.cssText = `display: flex; align-items: center; gap: 8px; padding: 8px 12px; cursor: pointer; font-size: 13px; color: ${defaultTextColor}; transition: background-color 0.15s, color 0.15s;`; + + const iconSpan = document.createElement('span'); + iconSpan.textContent = item.icon; + iconSpan.style.cssText = 'font-size: 15px; width: 18px; text-align: center;'; + + const labelSpan = document.createElement('span'); + labelSpan.textContent = item.label; + + row.appendChild(iconSpan); + row.appendChild(labelSpan); + + const hoverBg = item.hoverColor === '#cc0000' + ? (isDark ? 'rgba(204,0,0,0.2)' : 'rgba(204,0,0,0.1)') + : (isDark ? 'rgba(77,157,224,0.2)' : 'rgba(26,95,168,0.1)'); + + row.onmouseover = () => { + row.style.backgroundColor = hoverBg; + row.style.color = item.hoverColor; + }; + row.onmouseout = () => { + row.style.backgroundColor = ''; + row.style.color = defaultTextColor; + }; + + row.onclick = (e) => { + e.stopPropagation(); + dropdown.style.display = 'none'; + if (item.disableAfterClick) { + row.onclick = null; + row.style.opacity = '0.5'; + row.style.pointerEvents = 'none'; + } + item.onClick(); + }; + + dropdown.appendChild(row); + }); + + dotsBtn.onclick = (e) => { + e.stopPropagation(); + dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none'; + }; + + document.addEventListener('click', (e) => { + if (!wrapper.contains(e.target)) { + dropdown.style.display = 'none'; + } + }, true); + + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && dropdown.style.display !== 'none') { + dropdown.style.display = 'none'; + } + }); + + wrapper.appendChild(dotsBtn); + wrapper.appendChild(dropdown); + return wrapper; +} + browser.runtime.onMessage.addListener((message) => { switch (message.command) { case "getSelectedText": { @@ -702,34 +781,31 @@ switch (message.command) { branding.textContent = browser.i18n.getMessage("antispam_by") + " ThunderAI"; branding.style.cssText = 'margin-left: auto; font-style: italic; font-size: 10px; opacity: 0.5;'; - const spamRefreshBtn = document.createElement('span'); - spamRefreshBtn.textContent = '↻'; - spamRefreshBtn.title = browser.i18n.getMessage("spamfilter_refresh") || 'Refresh spam report'; - spamRefreshBtn.style.cssText = 'cursor: pointer; opacity: 0.6; font-size: 16px; padding: 0 5px; transition: opacity 0.2s, color 0.2s;'; - spamRefreshBtn.onmouseover = () => { spamRefreshBtn.style.opacity = '1'; spamRefreshBtn.style.color = isDark ? '#4d9de0' : '#1a5fa8'; }; - spamRefreshBtn.onmouseout = () => { spamRefreshBtn.style.opacity = '0.6'; spamRefreshBtn.style.color = ''; }; - spamRefreshBtn.onclick = function() { - spamRefreshBtn.onclick = null; - spamRefreshBtn.style.opacity = '0.6'; - browser.runtime.sendMessage({ command: "refreshSpamReport", headerMessageId: data.headerMessageId }); - }; - - const closeBtn = document.createElement('span'); - closeBtn.textContent = '×'; - closeBtn.style.cssText = 'cursor: pointer; opacity: 0.6; font-size: 16px; padding: 0 5px; transition: opacity 0.2s, color 0.2s;'; - closeBtn.title = browser.i18n.getMessage("spamfilter_delete"); - closeBtn.onmouseover = () => { closeBtn.style.opacity = '1'; closeBtn.style.color = '#cc0000'; }; - closeBtn.onmouseout = () => { closeBtn.style.opacity = '0.6'; closeBtn.style.color = ''; }; - closeBtn.onclick = function() { - container.remove(); - browser.runtime.sendMessage({ command: "removeSpamReport", headerMessageId: data.headerMessageId }); - }; + const spamMenu = createThreeDotsMenu(isDark, [ + { + icon: '↻', + label: browser.i18n.getMessage("spamfilter_refresh") || 'Refresh spam report', + hoverColor: isDark ? '#4d9de0' : '#1a5fa8', + disableAfterClick: true, + onClick: () => { + browser.runtime.sendMessage({ command: "refreshSpamReport", headerMessageId: data.headerMessageId }); + } + }, + { + icon: '×', + label: browser.i18n.getMessage("spamfilter_delete") || 'Delete spam report', + hoverColor: '#cc0000', + onClick: () => { + container.remove(); + browser.runtime.sendMessage({ command: "removeSpamReport", headerMessageId: data.headerMessageId }); + } + } + ], { bg: bgColor, border: borderColor, text: textColor }); container.appendChild(scoreText); container.appendChild(reasonText); container.appendChild(branding); - container.appendChild(spamRefreshBtn); - container.appendChild(closeBtn); + container.appendChild(spamMenu); document.body.insertBefore(container, document.body.firstChild); return Promise.resolve(true); @@ -773,47 +849,32 @@ switch (message.command) { summaryTitle.textContent = browser.i18n.getMessage("summarize_title"); summaryTitle.style.cssText = `font-weight: bold; font-size: 14px; color: ${titleColor};`; - const refreshBtn = document.createElement('span'); - refreshBtn.textContent = '↻'; - refreshBtn.title = browser.i18n.getMessage("summarize_refresh") || 'Refresh summary'; - refreshBtn.style.cssText = `cursor: pointer; opacity: 0.6; font-size: 16px; transition: opacity 0.2s, color 0.2s;`; - refreshBtn.onmouseover = () => { refreshBtn.style.opacity = '1'; refreshBtn.style.color = isDarkSummary ? '#4d9de0' : '#1a5fa8'; }; - refreshBtn.onmouseout = () => { refreshBtn.style.opacity = '0.6'; refreshBtn.style.color = ''; }; - refreshBtn.onclick = async () => { - refreshBtn.onclick = null; - refreshBtn.style.opacity = '0.6'; - browser.runtime.sendMessage({ - command: "refreshSummary", - headerMessageId: summaryData.headerMessageId - }); - }; - - const summaryCloseBtn = document.createElement('span'); - summaryCloseBtn.textContent = '×'; - summaryCloseBtn.style.cssText = 'cursor: pointer; opacity: 0.6; font-size: 16px; padding: 0 5px; transition: opacity 0.2s, color 0.2s;'; - summaryCloseBtn.title = browser.i18n.getMessage("summarize_delete"); - summaryCloseBtn.onmouseover = () => { summaryCloseBtn.style.opacity = '1'; summaryCloseBtn.style.color = '#cc0000'; }; - summaryCloseBtn.onmouseout = () => { summaryCloseBtn.style.opacity = '0.6'; summaryCloseBtn.style.color = ''; }; - summaryCloseBtn.onclick = function() { - summaryContainer.remove(); - browser.runtime.sendMessage({ command: "removeSummary", headerMessageId: summaryData.headerMessageId }); - }; - - const collapseBtn = document.createElement('span'); - collapseBtn.textContent = '∧'; - collapseBtn.title = browser.i18n.getMessage("summarize_collapse") || 'Collapse summary'; - collapseBtn.style.cssText = `cursor: pointer; opacity: 0.6; font-size: 16px; transition: opacity 0.2s;`; - collapseBtn.onmouseover = () => collapseBtn.style.opacity = '1'; - collapseBtn.onmouseout = () => collapseBtn.style.opacity = '0.6'; - - const summaryBtnGroup = document.createElement('span'); - summaryBtnGroup.style.cssText = 'display: flex; align-items: center; gap: 5px;'; - summaryBtnGroup.appendChild(refreshBtn); - summaryBtnGroup.appendChild(summaryCloseBtn); - summaryBtnGroup.appendChild(collapseBtn); + const summaryMenu = createThreeDotsMenu(isDarkSummary, [ + { + icon: '↻', + label: browser.i18n.getMessage("summarize_refresh") || 'Refresh summary', + hoverColor: isDarkSummary ? '#4d9de0' : '#1a5fa8', + disableAfterClick: true, + onClick: () => { + browser.runtime.sendMessage({ + command: "refreshSummary", + headerMessageId: summaryData.headerMessageId + }); + } + }, + { + icon: '×', + label: browser.i18n.getMessage("summarize_delete") || 'Delete summary', + hoverColor: '#cc0000', + onClick: () => { + summaryContainer.remove(); + browser.runtime.sendMessage({ command: "removeSummary", headerMessageId: summaryData.headerMessageId }); + } + } + ], { bg: bgColorSummary, border: borderColorSummary, text: textColorSummary }); summaryHeader.appendChild(summaryTitle); - summaryHeader.appendChild(summaryBtnGroup); + summaryHeader.appendChild(summaryMenu); summaryContainer.appendChild(summaryHeader); const summaryText = document.createElement('div'); @@ -825,18 +886,6 @@ switch (message.command) { } summaryText.style.cssText = `font-size: 14px; line-height: 1.4;`; - collapseBtn.onclick = () => { - if (summaryText.style.display === 'none') { - summaryText.style.display = ''; - summaryHeader.style.marginBottom = '0.5rem'; - collapseBtn.textContent = '∧'; - } else { - summaryText.style.display = 'none'; - summaryHeader.style.marginBottom = '0'; - collapseBtn.textContent = '∨'; - } - }; - summaryContainer.appendChild(summaryText); document.body.insertBefore(summaryContainer, document.body.firstChild); From db41bce9283a24b8ae12dbb311527508e82d4715 Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 24 Mar 2026 23:11:22 +0100 Subject: [PATCH 28/52] spamreport olways on top. see #706 --- js/mzta-compose-script.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 86dc7dd5..4c4caede 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -888,7 +888,8 @@ switch (message.command) { summaryContainer.appendChild(summaryText); - document.body.insertBefore(summaryContainer, document.body.firstChild); + const spamBanner = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); + document.body.insertBefore(summaryContainer, spamBanner ? spamBanner.nextSibling : document.body.firstChild); return Promise.resolve(true); case "showSummaryGenerating": @@ -925,7 +926,8 @@ switch (message.command) { generatingContainer.appendChild(generatingLoadingImg); generatingContainer.appendChild(generatingTitle); - document.body.insertBefore(generatingContainer, document.body.firstChild); + const spamBannerGen = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); + document.body.insertBefore(generatingContainer, spamBannerGen ? spamBannerGen.nextSibling : document.body.firstChild); return Promise.resolve(true); case "showSummaryButton": @@ -961,7 +963,8 @@ switch (message.command) { }); }; - document.body.insertBefore(triggerContainer, document.body.firstChild); + const spamBannerTrigger = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); + document.body.insertBefore(triggerContainer, spamBannerTrigger ? spamBannerTrigger.nextSibling : document.body.firstChild); return Promise.resolve(true); default: From 8494113886e6362999d26aaa53d65247ef0911ba Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 24 Mar 2026 23:20:51 +0100 Subject: [PATCH 29/52] css fixes --- js/mzta-compose-script.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 4c4caede..7ef594d0 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -18,11 +18,11 @@ function createThreeDotsMenu(isDark, menuItems, panelColors) { const wrapper = document.createElement('div'); - wrapper.style.cssText = 'position: relative; display: inline-block;'; + wrapper.style.cssText = 'position: relative; display: inline-flex; align-items: center;'; const dotsBtn = document.createElement('span'); dotsBtn.textContent = '\u22EE'; - dotsBtn.style.cssText = 'cursor: pointer; opacity: 0.7; font-size: 18px; padding: 2px 6px; line-height: 1; user-select: none; transition: opacity 0.2s;'; + dotsBtn.style.cssText = 'cursor: pointer; opacity: 0.7; font-size: 18px; padding: 2px 7px; line-height: 1; user-select: none; transition: opacity 0.2s; display: flex; align-items: center;'; dotsBtn.onmouseover = () => dotsBtn.style.opacity = '1'; dotsBtn.onmouseout = () => dotsBtn.style.opacity = '0.7'; @@ -761,7 +761,7 @@ switch (message.command) { borderColor = '#006600'; } - container.style.cssText = `background-color: ${bgColor}; color: ${textColor}; border-bottom: 1px solid ${borderColor}; border-radius:4px; padding: 8px 12px; font-family: system-ui, -apple-system, sans-serif; font-size: 13px; display: flex; align-items: center; gap: 15px; width: 100%; box-sizing: border-box;`; + container.style.cssText = `background-color: ${bgColor}; color: ${textColor}; border-bottom: 1px solid ${borderColor}; border-radius:4px; padding: 8px 0.5rem; font-family: system-ui, -apple-system, sans-serif; font-size: 13px; display: flex; align-items: center; gap: 15px; width: 100%; box-sizing: border-box;`; const scoreText = document.createElement('strong'); if (data.spamValue == -999) { @@ -802,10 +802,15 @@ switch (message.command) { } ], { bg: bgColor, border: borderColor, text: textColor }); + const spamRightGroup = document.createElement('span'); + spamRightGroup.style.cssText = 'margin-left: auto; margin-right:1px; display: flex; align-items: center; gap: 5px;'; + branding.style.cssText = 'font-style: italic; font-size: 10px; opacity: 0.5;'; + spamRightGroup.appendChild(branding); + spamRightGroup.appendChild(spamMenu); + container.appendChild(scoreText); container.appendChild(reasonText); - container.appendChild(branding); - container.appendChild(spamMenu); + container.appendChild(spamRightGroup); document.body.insertBefore(container, document.body.firstChild); return Promise.resolve(true); From 0ea8c02202f6bb71abc03033a1f74ab3e8dec11a Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 24 Mar 2026 23:22:53 +0100 Subject: [PATCH 30/52] summary branding added. see #580 --- _locales/en/messages.json | 4 ++++ js/mzta-compose-script.js | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 82119473..51bb7091 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2008,6 +2008,10 @@ "message": "Antispam by", "description": "" }, + "summary_by": { + "message": "Summary by", + "description": "" + }, "prefs_THStats_1": { "message": "Do you want beautiful statistics about your emails?", "description": "" diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 7ef594d0..1295148f 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -878,8 +878,17 @@ switch (message.command) { } ], { bg: bgColorSummary, border: borderColorSummary, text: textColorSummary }); + const summaryBranding = document.createElement('span'); + summaryBranding.textContent = browser.i18n.getMessage("summary_by") + " ThunderAI"; + summaryBranding.style.cssText = 'font-style: italic; font-size: 10px; opacity: 0.5;'; + + const summaryRightGroup = document.createElement('span'); + summaryRightGroup.style.cssText = 'display: flex; align-items: center; gap: 5px;'; + summaryRightGroup.appendChild(summaryBranding); + summaryRightGroup.appendChild(summaryMenu); + summaryHeader.appendChild(summaryTitle); - summaryHeader.appendChild(summaryMenu); + summaryHeader.appendChild(summaryRightGroup); summaryContainer.appendChild(summaryHeader); const summaryText = document.createElement('div'); From ea0d1e5e86c22615fc12785a8dd27541bf671347 Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 24 Mar 2026 23:25:47 +0100 Subject: [PATCH 31/52] summary title removed. see #580 --- js/mzta-compose-script.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 1295148f..27b3a905 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -847,12 +847,7 @@ switch (message.command) { summaryContainer.style.cssText = `background-color: ${bgColorSummary}; color: ${textColorSummary}; padding: 0.5rem; margin-bottom: 1rem; border-radius: 4px; border: 1px solid ${borderColorSummary}; font-family: system-ui, -apple-system, sans-serif; font-size: 14px;`; const summaryHeader = document.createElement('div'); - summaryHeader.style.cssText = `display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem;`; - - const summaryTitle = document.createElement('div'); - summaryTitle.className = 'thunderai-summary-title'; - summaryTitle.textContent = browser.i18n.getMessage("summarize_title"); - summaryTitle.style.cssText = `font-weight: bold; font-size: 14px; color: ${titleColor};`; + summaryHeader.style.cssText = `display: flex; justify-content: flex-end; align-items: center; margin-bottom: 0.25rem; float:right;`; const summaryMenu = createThreeDotsMenu(isDarkSummary, [ { @@ -887,7 +882,6 @@ switch (message.command) { summaryRightGroup.appendChild(summaryBranding); summaryRightGroup.appendChild(summaryMenu); - summaryHeader.appendChild(summaryTitle); summaryHeader.appendChild(summaryRightGroup); summaryContainer.appendChild(summaryHeader); From 6bfeeca90d198470c3d874052c599a7a53fea85a Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 24 Mar 2026 23:36:25 +0100 Subject: [PATCH 32/52] summary butoon with an ai sumamry icon added. see #580 --- _locales/en/messages.json | 4 ++++ images/ai_summary.png | Bin 0 -> 306 bytes js/mzta-compose-script.js | 48 ++++++++++++++++++++------------------ 3 files changed, 29 insertions(+), 23 deletions(-) create mode 100644 images/ai_summary.png diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 51bb7091..7bbd2509 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1968,6 +1968,10 @@ "message": "ThunderAI Overview", "description": "" }, + "get_ai_summary": { + "message": "AI Summary", + "description": "" + }, "summarize_collapse": { "message": "Collapse summary", "description": "" diff --git a/images/ai_summary.png b/images/ai_summary.png new file mode 100644 index 0000000000000000000000000000000000000000..1a13f7d41a6837c621aa507ba641c706b567ceb8 GIT binary patch literal 306 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=fdz&H|6fVg?5GL=a}2dU(e+prB-l zYeY$Kep*R+Vo@qXd3m{BW?pu2a$-TMUVc&f>~}U&Kt=mKT^vIqTHj9bjiI}JY8ou zFnx29^F82sVsQbB6dSkH0*<>OjOhm$%2)5yJyc@)$VX@CuS*}or##4KyXBFt_EPu4 zp5OV=3pi5_s;wDFPMVR(N3EytFPueOFh@92IIG}XD!Bwz8L)A+SozN6aY?Fy3qHx4e^C-@-#B+x4ip00i_>zopr0K>a? AGynhq literal 0 HcmV?d00001 diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 27b3a905..4b3e90e2 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -819,8 +819,8 @@ switch (message.command) { const generatingBanner = document.getElementById('mzta-summary-generating'); if(generatingBanner) generatingBanner.remove(); - const triggerBtn = document.getElementById('mzta-summary-trigger'); - if(triggerBtn) triggerBtn.remove(); + const existingTriggerBtn = document.getElementById('mzta-summary-trigger'); + if(existingTriggerBtn) existingTriggerBtn.remove(); const summaryBanner = document.getElementById('mzta-summary-banner'); if(summaryBanner) summaryBanner.remove(); @@ -834,13 +834,11 @@ switch (message.command) { let bgColorSummary = isDarkSummary ? '#2a2a2a' : '#f0f0f0'; let textColorSummary = isDarkSummary ? '#e0e0e0' : '#333'; let borderColorSummary = isDarkSummary ? '#444' : '#ddd'; - let titleColor = isDarkSummary ? '#ff6b6b' : '#d70022'; if (summaryData.error) { bgColorSummary = isDarkSummary ? '#3a1a1a' : '#f7e6e6'; textColorSummary = isDarkSummary ? '#ffcccc' : '#660000'; borderColorSummary = '#660000'; - titleColor = isDarkSummary ? '#ffcccc' : '#660000'; } summaryContainer.className = 'thunderai-summary-pane'; @@ -943,36 +941,40 @@ switch (message.command) { if(existingButton) return Promise.resolve(true); const isDarkBtn = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; - + let bgColorBtn = isDarkBtn ? '#2a2a2a' : '#f0f0f0'; let textColorBtn = isDarkBtn ? '#e0e0e0' : '#333'; let borderColorBtn = isDarkBtn ? '#444' : '#ddd'; - let titleColorBtn = isDarkBtn ? '#ff6b6b' : '#d70022'; - - const triggerContainer = document.createElement('div'); - triggerContainer.id = 'mzta-summary-trigger'; - triggerContainer.className = 'thunderai-summary-pane'; - triggerContainer.style.cssText = `background-color: ${bgColorBtn}; color: ${textColorBtn}; padding: 0.5rem; margin-bottom: 1rem; border-radius: 4px; border: 1px solid ${borderColorBtn}; cursor: pointer; font-family: system-ui, -apple-system, sans-serif; font-size: 14px;`; - const triggerText = document.createElement('div'); - triggerText.className = 'thunderai-summary-title'; - triggerText.textContent = browser.i18n.getMessage("summarize_click_to_generate"); - triggerText.style.cssText = `font-weight: bold; font-size: 14px; margin-bottom: 0; color: ${titleColorBtn};`; + const triggerBtn = document.createElement('div'); + triggerBtn.id = 'mzta-summary-trigger'; + triggerBtn.title = browser.i18n.getMessage("summarize_click_to_generate"); + triggerBtn.style.cssText = `position: fixed; top: 8px; right: 8px; z-index: 9998; background-color: ${bgColorBtn}; border: 1px solid ${borderColorBtn}; border-radius: 4px; padding: 6px 10px; cursor: pointer; font-family: system-ui, -apple-system, sans-serif; font-size: 12px; font-style: italic; opacity: 0.7; transition: opacity 0.2s; color: ${textColorBtn}; display: flex; align-items: center; gap: 6px;`; - triggerContainer.appendChild(triggerText); - triggerContainer.onclick = async () => { - triggerContainer.onclick = null; - triggerContainer.id = 'mzta-summary-generating'; - triggerContainer.style.cursor = 'default'; - triggerText.textContent = browser.i18n.getMessage("summarize_generating"); + const triggerIcon = document.createElement('img'); + triggerIcon.src = browser.runtime.getURL("/images/ai_summary.png"); + triggerIcon.style.cssText = `height: 14px; width: 14px;${isDarkBtn ? ' filter: invert(1);' : ''}`; + triggerBtn.appendChild(triggerIcon); + + const triggerLabel = document.createElement('span'); + triggerLabel.textContent = browser.i18n.getMessage("get_ai_summary"); + triggerBtn.appendChild(triggerLabel); + triggerBtn.onmouseover = () => { triggerBtn.style.opacity = '1'; }; + triggerBtn.onmouseout = () => { triggerBtn.style.opacity = '0.7'; }; + triggerBtn.onclick = async () => { + triggerBtn.onclick = null; + triggerBtn.style.cursor = 'default'; + triggerBtn.style.opacity = '0.7'; + triggerBtn.onmouseover = null; + triggerBtn.onmouseout = null; + triggerBtn.remove(); browser.runtime.sendMessage({ command: message.webchat ? "triggerSummaryWebchat" : "triggerSummaryGeneration", headerMessageId: message.headerMessageId }); }; - const spamBannerTrigger = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); - document.body.insertBefore(triggerContainer, spamBannerTrigger ? spamBannerTrigger.nextSibling : document.body.firstChild); + document.body.appendChild(triggerBtn); return Promise.resolve(true); default: From c0a5be76b74db101892f83929c8b011c653a309e Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 24 Mar 2026 23:37:18 +0100 Subject: [PATCH 33/52] ai summary icon added. see #580 --- js/mzta-compose-script.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 4b3e90e2..028b2c8e 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -883,6 +883,13 @@ switch (message.command) { summaryHeader.appendChild(summaryRightGroup); summaryContainer.appendChild(summaryHeader); + const summaryBody = document.createElement('div'); + summaryBody.style.cssText = 'display: flex; gap: 8px; align-items: flex-start;'; + + const summaryIcon = document.createElement('img'); + summaryIcon.src = browser.runtime.getURL("/images/ai_summary.png"); + summaryIcon.style.cssText = `height: 16px; width: 16px; margin-top: 2px; flex-shrink: 0;${isDarkSummary ? ' filter: invert(1);' : ''}`; + const summaryText = document.createElement('div'); summaryText.className = 'thunderai-summary-content'; if (summaryData.error) { @@ -892,7 +899,9 @@ switch (message.command) { } summaryText.style.cssText = `font-size: 14px; line-height: 1.4;`; - summaryContainer.appendChild(summaryText); + summaryBody.appendChild(summaryIcon); + summaryBody.appendChild(summaryText); + summaryContainer.appendChild(summaryBody); const spamBanner = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); document.body.insertBefore(summaryContainer, spamBanner ? spamBanner.nextSibling : document.body.firstChild); @@ -920,6 +929,10 @@ switch (message.command) { generatingContainer.className = 'thunderai-summary-pane'; generatingContainer.style.cssText = `background-color: ${bgColorGen}; color: ${textColorGen}; padding: 0.5rem; margin-bottom: 1rem; border-radius: 4px; border: 1px solid ${borderColorGen}; font-family: system-ui, -apple-system, sans-serif; font-size: 14px; display: flex; align-items: center; gap: 10px;`; + const generatingIcon = document.createElement('img'); + generatingIcon.src = browser.runtime.getURL("/images/ai_summary.png"); + generatingIcon.style.cssText = `height: 16px; width: 16px; flex-shrink: 0;${isDarkGen ? ' filter: invert(1);' : ''}`; + const generatingLoadingImg = document.createElement('img'); generatingLoadingImg.src = browser.runtime.getURL("/images/loading.gif"); generatingLoadingImg.style.cssText = "height: 16px; width: 16px;"; @@ -929,6 +942,7 @@ switch (message.command) { generatingTitle.textContent = browser.i18n.getMessage("summarize_generating"); generatingTitle.style.cssText = `font-size: 14px; color: ${titleColorGen};`; + generatingContainer.appendChild(generatingIcon); generatingContainer.appendChild(generatingLoadingImg); generatingContainer.appendChild(generatingTitle); From d66c897ad1befffa891e2603ece5a3fab42cd391 Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 24 Mar 2026 23:38:43 +0100 Subject: [PATCH 34/52] readme updated --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5ece8058..563205bc 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,7 @@ _The language status represents the percentage of translated strings in the late - [Iconka.com](https://www.iconarchive.com/artist/iconka.html) for the autotag context menu icon - [Icojam](https://www.iconarchive.com/artist/icojam.html) for the spam filter context menu icon - [Roundicons](https://www.flaticon.com/authors/roundicons) for the summarize context menu icon +- [HideMau](https://www.flaticon.com/authors/hidemaru) for the ai summarize icon
From b2eb87255723594f64191f36b0d754bbadf180e3 Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 24 Mar 2026 23:48:13 +0100 Subject: [PATCH 35/52] positioning fixed. see #580 --- js/mzta-compose-script.js | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 028b2c8e..1e0b9280 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -844,9 +844,6 @@ switch (message.command) { summaryContainer.className = 'thunderai-summary-pane'; summaryContainer.style.cssText = `background-color: ${bgColorSummary}; color: ${textColorSummary}; padding: 0.5rem; margin-bottom: 1rem; border-radius: 4px; border: 1px solid ${borderColorSummary}; font-family: system-ui, -apple-system, sans-serif; font-size: 14px;`; - const summaryHeader = document.createElement('div'); - summaryHeader.style.cssText = `display: flex; justify-content: flex-end; align-items: center; margin-bottom: 0.25rem; float:right;`; - const summaryMenu = createThreeDotsMenu(isDarkSummary, [ { icon: '↻', @@ -873,22 +870,21 @@ switch (message.command) { const summaryBranding = document.createElement('span'); summaryBranding.textContent = browser.i18n.getMessage("summary_by") + " ThunderAI"; - summaryBranding.style.cssText = 'font-style: italic; font-size: 10px; opacity: 0.5;'; + summaryBranding.style.cssText = 'font-style: italic; font-size: 10px; opacity: 0.5; white-space: nowrap;'; const summaryRightGroup = document.createElement('span'); - summaryRightGroup.style.cssText = 'display: flex; align-items: center; gap: 5px;'; + summaryRightGroup.style.cssText = 'display: flex; align-items: center; gap: 5px; float: right; margin-left: 10px;'; summaryRightGroup.appendChild(summaryBranding); summaryRightGroup.appendChild(summaryMenu); - summaryHeader.appendChild(summaryRightGroup); - summaryContainer.appendChild(summaryHeader); - - const summaryBody = document.createElement('div'); - summaryBody.style.cssText = 'display: flex; gap: 8px; align-items: flex-start;'; - const summaryIcon = document.createElement('img'); summaryIcon.src = browser.runtime.getURL("/images/ai_summary.png"); - summaryIcon.style.cssText = `height: 16px; width: 16px; margin-top: 2px; flex-shrink: 0;${isDarkSummary ? ' filter: invert(1);' : ''}`; + summaryIcon.style.cssText = `height: 16px; width: 16px; flex-shrink: 0; margin-top: 2px;${isDarkSummary ? ' filter: invert(1);' : ''}`; + + const summaryTextWrapper = document.createElement('div'); + summaryTextWrapper.style.cssText = 'flex: 1; min-width: 0;'; + + summaryTextWrapper.appendChild(summaryRightGroup); const summaryText = document.createElement('div'); summaryText.className = 'thunderai-summary-content'; @@ -899,8 +895,12 @@ switch (message.command) { } summaryText.style.cssText = `font-size: 14px; line-height: 1.4;`; + summaryTextWrapper.appendChild(summaryText); + + const summaryBody = document.createElement('div'); + summaryBody.style.cssText = 'display: flex; gap: 8px; align-items: flex-start;'; summaryBody.appendChild(summaryIcon); - summaryBody.appendChild(summaryText); + summaryBody.appendChild(summaryTextWrapper); summaryContainer.appendChild(summaryBody); const spamBanner = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); From d2486f7f5ee915ecd6f87e471f38f3b443207f92 Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 24 Mar 2026 23:52:57 +0100 Subject: [PATCH 36/52] summary generating messaage color fixed. see #580 --- js/mzta-compose-script.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 1e0b9280..70a2a707 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -937,10 +937,10 @@ switch (message.command) { generatingLoadingImg.src = browser.runtime.getURL("/images/loading.gif"); generatingLoadingImg.style.cssText = "height: 16px; width: 16px;"; - const generatingTitle = document.createElement('strong'); + const generatingTitle = document.createElement('span'); generatingTitle.className = 'thunderai-summary-title'; generatingTitle.textContent = browser.i18n.getMessage("summarize_generating"); - generatingTitle.style.cssText = `font-size: 14px; color: ${titleColorGen};`; + generatingTitle.style.cssText = `font-size: 14px;`; generatingContainer.appendChild(generatingIcon); generatingContainer.appendChild(generatingLoadingImg); @@ -960,10 +960,15 @@ switch (message.command) { let textColorBtn = isDarkBtn ? '#e0e0e0' : '#333'; let borderColorBtn = isDarkBtn ? '#444' : '#ddd'; + const spamBannerTrigger = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); const triggerBtn = document.createElement('div'); triggerBtn.id = 'mzta-summary-trigger'; triggerBtn.title = browser.i18n.getMessage("summarize_click_to_generate"); - triggerBtn.style.cssText = `position: fixed; top: 8px; right: 8px; z-index: 9998; background-color: ${bgColorBtn}; border: 1px solid ${borderColorBtn}; border-radius: 4px; padding: 6px 10px; cursor: pointer; font-family: system-ui, -apple-system, sans-serif; font-size: 12px; font-style: italic; opacity: 0.7; transition: opacity 0.2s; color: ${textColorBtn}; display: flex; align-items: center; gap: 6px;`; + if (spamBannerTrigger) { + triggerBtn.style.cssText = `background-color: ${bgColorBtn}; border: 1px solid ${borderColorBtn}; border-radius: 4px; padding: 6px 10px; cursor: pointer; font-family: system-ui, -apple-system, sans-serif; font-size: 12px; font-style: italic; opacity: 0.7; transition: opacity 0.2s; color: ${textColorBtn}; display: flex; align-items: center; gap: 6px; justify-content: flex-end;`; + } else { + triggerBtn.style.cssText = `position: fixed; top: 8px; right: 8px; z-index: 9998; background-color: ${bgColorBtn}; border: 1px solid ${borderColorBtn}; border-radius: 4px; padding: 6px 10px; cursor: pointer; font-family: system-ui, -apple-system, sans-serif; font-size: 12px; font-style: italic; opacity: 0.7; transition: opacity 0.2s; color: ${textColorBtn}; display: flex; align-items: center; gap: 6px;`; + } const triggerIcon = document.createElement('img'); triggerIcon.src = browser.runtime.getURL("/images/ai_summary.png"); From 8ba35a34ba3998b39b8cd73f7960bcde5ed16d48 Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 24 Mar 2026 23:55:44 +0100 Subject: [PATCH 37/52] summary btn position fixed --- js/mzta-compose-script.js | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 70a2a707..cb8135c5 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -819,6 +819,8 @@ switch (message.command) { const generatingBanner = document.getElementById('mzta-summary-generating'); if(generatingBanner) generatingBanner.remove(); + const existingTriggerWrapper = document.getElementById('mzta-summary-trigger-wrapper'); + if(existingTriggerWrapper) existingTriggerWrapper.remove(); const existingTriggerBtn = document.getElementById('mzta-summary-trigger'); if(existingTriggerBtn) existingTriggerBtn.remove(); @@ -914,6 +916,8 @@ switch (message.command) { const existingSummary = document.getElementById('mzta-summary-banner'); if(existingSummary) existingSummary.remove(); + const existingTriggerWrap = document.getElementById('mzta-summary-trigger-wrapper'); + if(existingTriggerWrap) existingTriggerWrap.remove(); const existingTrigger = document.getElementById('mzta-summary-trigger'); if(existingTrigger) existingTrigger.remove(); @@ -964,10 +968,11 @@ switch (message.command) { const triggerBtn = document.createElement('div'); triggerBtn.id = 'mzta-summary-trigger'; triggerBtn.title = browser.i18n.getMessage("summarize_click_to_generate"); + const triggerBtnBase = `background-color: ${bgColorBtn}; border: 1px solid ${borderColorBtn}; border-radius: 4px; padding: 6px 10px; cursor: pointer; font-family: system-ui, -apple-system, sans-serif; font-size: 12px; font-style: italic; opacity: 0.7; transition: opacity 0.2s; color: ${textColorBtn}; display: inline-flex; align-items: center; gap: 6px; width: fit-content;`; if (spamBannerTrigger) { - triggerBtn.style.cssText = `background-color: ${bgColorBtn}; border: 1px solid ${borderColorBtn}; border-radius: 4px; padding: 6px 10px; cursor: pointer; font-family: system-ui, -apple-system, sans-serif; font-size: 12px; font-style: italic; opacity: 0.7; transition: opacity 0.2s; color: ${textColorBtn}; display: flex; align-items: center; gap: 6px; justify-content: flex-end;`; + triggerBtn.style.cssText = triggerBtnBase + ' margin-left: auto; margin-top: 4px;'; } else { - triggerBtn.style.cssText = `position: fixed; top: 8px; right: 8px; z-index: 9998; background-color: ${bgColorBtn}; border: 1px solid ${borderColorBtn}; border-radius: 4px; padding: 6px 10px; cursor: pointer; font-family: system-ui, -apple-system, sans-serif; font-size: 12px; font-style: italic; opacity: 0.7; transition: opacity 0.2s; color: ${textColorBtn}; display: flex; align-items: center; gap: 6px;`; + triggerBtn.style.cssText = triggerBtnBase + ' position: fixed; top: 8px; right: 8px; z-index: 9998;'; } const triggerIcon = document.createElement('img'); @@ -986,14 +991,23 @@ switch (message.command) { triggerBtn.style.opacity = '0.7'; triggerBtn.onmouseover = null; triggerBtn.onmouseout = null; - triggerBtn.remove(); + const wrapper = document.getElementById('mzta-summary-trigger-wrapper'); + if (wrapper) wrapper.remove(); else triggerBtn.remove(); browser.runtime.sendMessage({ command: message.webchat ? "triggerSummaryWebchat" : "triggerSummaryGeneration", headerMessageId: message.headerMessageId }); }; - document.body.appendChild(triggerBtn); + if (spamBannerTrigger) { + const triggerWrapper = document.createElement('div'); + triggerWrapper.id = 'mzta-summary-trigger-wrapper'; + triggerWrapper.style.cssText = 'display: flex; justify-content: flex-end; padding: 4px 0.5rem;'; + triggerWrapper.appendChild(triggerBtn); + document.body.insertBefore(triggerWrapper, spamBannerTrigger.nextSibling); + } else { + document.body.appendChild(triggerBtn); + } return Promise.resolve(true); default: From d7a362f5df83996972b0e9eb891b5b29dad498bc Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 24 Mar 2026 23:58:26 +0100 Subject: [PATCH 38/52] summarize btn position fixed when adding spamreport div --- js/mzta-compose-script.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index cb8135c5..fac4799c 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -729,6 +729,23 @@ switch (message.command) { containerProgress.appendChild(brandingProgress); document.body.insertBefore(containerProgress, document.body.firstChild); + + // Reposition summary trigger button if it exists as fixed + const existingFixedTriggerProgress = document.getElementById('mzta-summary-trigger'); + if (existingFixedTriggerProgress && !document.getElementById('mzta-summary-trigger-wrapper')) { + existingFixedTriggerProgress.style.position = ''; + existingFixedTriggerProgress.style.top = ''; + existingFixedTriggerProgress.style.right = ''; + existingFixedTriggerProgress.style.zIndex = ''; + existingFixedTriggerProgress.style.marginLeft = 'auto'; + existingFixedTriggerProgress.style.marginTop = '4px'; + const reposTriggerWrapperProgress = document.createElement('div'); + reposTriggerWrapperProgress.id = 'mzta-summary-trigger-wrapper'; + reposTriggerWrapperProgress.style.cssText = 'display: flex; justify-content: flex-end; padding: 4px 0.5rem;'; + reposTriggerWrapperProgress.appendChild(existingFixedTriggerProgress); + document.body.insertBefore(reposTriggerWrapperProgress, containerProgress.nextSibling); + } + return Promise.resolve(true); case "showSpamReport": @@ -813,6 +830,23 @@ switch (message.command) { container.appendChild(spamRightGroup); document.body.insertBefore(container, document.body.firstChild); + + // Reposition summary trigger button if it exists as fixed + const existingFixedTrigger = document.getElementById('mzta-summary-trigger'); + if (existingFixedTrigger && !document.getElementById('mzta-summary-trigger-wrapper')) { + existingFixedTrigger.style.position = ''; + existingFixedTrigger.style.top = ''; + existingFixedTrigger.style.right = ''; + existingFixedTrigger.style.zIndex = ''; + existingFixedTrigger.style.marginLeft = 'auto'; + existingFixedTrigger.style.marginTop = '4px'; + const reposTriggerWrapper = document.createElement('div'); + reposTriggerWrapper.id = 'mzta-summary-trigger-wrapper'; + reposTriggerWrapper.style.cssText = 'display: flex; justify-content: flex-end; padding: 4px 0.5rem;'; + reposTriggerWrapper.appendChild(existingFixedTrigger); + document.body.insertBefore(reposTriggerWrapper, container.nextSibling); + } + return Promise.resolve(true); case "showSummary": From e29079ddc9932ad7d76a16e6b003e28cd1b41836 Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 25 Mar 2026 00:06:37 +0100 Subject: [PATCH 39/52] correctly loading the working icon. see #707 --- mzta-background.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mzta-background.js b/mzta-background.js index 57487ef2..02f9bbdf 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -479,12 +479,14 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { } await summaryStore.setProcessing(headerMessageId); + taWorkingStatus.startWorking(); browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); const messageResult = await browser.messages.query({ headerMessageId: headerMessageId }); if (!messageResult || messageResult.messages.length === 0) { await summaryStore.saveError(headerMessageId, "Message not found"); browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: "Message not found" } }); + taWorkingStatus.stopWorking(); return; } @@ -496,6 +498,7 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { 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 } }); + taWorkingStatus.stopWorking(); return; } @@ -522,11 +525,13 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { }; await summaryStore.saveSummary(summaryData, headerMessageId); browser.tabs.sendMessage(tabId, { command: "showSummary", data: summaryData }); + taWorkingStatus.stopWorking(); } catch (error) { console.error("[ThunderAI] Error generating summary:", error); await summaryStore.saveError(headerMessageId, error.message || String(error)); browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: error.message || "Failed to generate summary" } }); + taWorkingStatus.stopWorking(); } } From 33fecf3afe9ebc8cfee145b94e2d93731edcebd5 Mon Sep 17 00:00:00 2001 From: Mic Date: Wed, 25 Mar 2026 00:01:00 +0100 Subject: [PATCH 40/52] summary: see more / see less added. see #580 --- _locales/en/messages.json | 16 ++++++++++++++++ claude-spec/05-options.md | 4 +++- js/mzta-compose-script.js | 27 +++++++++++++++++++++++++++ mzta-background.js | 9 +++++---- options/mzta-options-default.js | 1 + pages/summarize/mzta-summarize.html | 9 +++++++++ 6 files changed, 61 insertions(+), 5 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 7bbd2509..7f28055b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1964,6 +1964,22 @@ "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": "" }, + "prefs_OptionText_summarize_max_display_length": { + "message": "Max display length", + "description": "" + }, + "prefs_OptionText_summarize_max_display_length_Info": { + "message": "Maximum number of characters to display in the inline summary. Set to 0 for no limit.", + "description": "" + }, + "summarize_see_more": { + "message": "See more", + "description": "" + }, + "summarize_see_less": { + "message": "See less", + "description": "" + }, "summarize_title": { "message": "ThunderAI Overview", "description": "" diff --git a/claude-spec/05-options.md b/claude-spec/05-options.md index 8f402497..81b1747a 100644 --- a/claude-spec/05-options.md +++ b/claude-spec/05-options.md @@ -97,6 +97,7 @@ These are generated programmatically at the bottom of `mzta-options-default.js` | `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_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. | ### Summarize Settings Page (`pages/summarize/`) @@ -111,7 +112,8 @@ The summarize settings page provides: - `'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): +4. **Max display length** (`summarize_max_display_length`) — number input, limits inline summary text to N characters. `0` = no limit. When truncated, a "See more"/"See less" toggle link is appended. +5. **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 fac4799c..4b9f7e28 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -933,6 +933,33 @@ switch (message.command) { summaryTextWrapper.appendChild(summaryText); + 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; + + 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(); + expanded = !expanded; + summaryText.textContent = expanded ? fullText : truncated; + toggleLink.textContent = expanded + ? (browser.i18n.getMessage("summarize_see_less") || "See less") + : (browser.i18n.getMessage("summarize_see_more") || "See more"); + }); + + summaryTextWrapper.appendChild(toggleLink); + } + const summaryBody = document.createElement('div'); summaryBody.style.cssText = 'display: flex; gap: 8px; align-items: flex-start;'; summaryBody.appendChild(summaryIcon); diff --git a/mzta-background.js b/mzta-background.js index 02f9bbdf..49cc9cb8 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -220,7 +220,7 @@ 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 }); + 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 message = await browser.messageDisplay.getDisplayedMessage(tabId); if (!message) return; @@ -228,7 +228,7 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { // Always show cached summary if available, regardless of summarize_auto let cachedSummary = await summaryStore.loadSummary(message.headerMessageId); if (cachedSummary && !cachedSummary.error) { - browser.tabs.sendMessage(tabId, { command: "showSummary", data: cachedSummary }); + browser.tabs.sendMessage(tabId, { command: "showSummary", data: { ...cachedSummary, maxDisplayLength: prefs.summarize_max_display_length } }); return; } @@ -464,12 +464,13 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { connection_type: prefs_default.connection_type, do_debug: prefs_default.do_debug, default_chatgpt_lang: prefs_default.default_chatgpt_lang, + summarize_max_display_length: prefs_default.summarize_max_display_length, ...getDynamicSettingsDefaults(['use_specific_integration', 'connection_type']) }); let cachedSummary = await summaryStore.loadSummary(headerMessageId); if (cachedSummary && !cachedSummary.error) { - browser.tabs.sendMessage(tabId, { command: "showSummary", data: cachedSummary }); + browser.tabs.sendMessage(tabId, { command: "showSummary", data: { ...cachedSummary, maxDisplayLength: prefs.summarize_max_display_length } }); return; } @@ -524,7 +525,7 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { headerMessageId: headerMessageId }; await summaryStore.saveSummary(summaryData, headerMessageId); - browser.tabs.sendMessage(tabId, { command: "showSummary", data: summaryData }); + browser.tabs.sendMessage(tabId, { command: "showSummary", data: { ...summaryData, maxDisplayLength: prefs.summarize_max_display_length } }); taWorkingStatus.stopWorking(); } catch (error) { diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index 4a4cbda0..20dea23d 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -139,6 +139,7 @@ export const prefs_default = { spamfilter_enabled_accounts: [], summarize_auto: 0, // 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 diff --git a/pages/summarize/mzta-summarize.html b/pages/summarize/mzta-summarize.html index 0f02deac..ea6f59c7 100644 --- a/pages/summarize/mzta-summarize.html +++ b/pages/summarize/mzta-summarize.html @@ -51,6 +51,15 @@ + + __MSG_prefs_OptionText_summarize_max_display_length__ + + + + From 7cde1f9b621c66c2eda981c9c4b20ba46854cb75 Mon Sep 17 00:00:00 2001 From: Mic Date: Wed, 25 Mar 2026 00:05:00 +0100 Subject: [PATCH 41/52] added a transition. see #705 --- js/mzta-compose-script.js | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 4b9f7e28..097692bd 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -941,6 +941,16 @@ switch (message.command) { 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'; + }); + const toggleLink = document.createElement('a'); toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more"; toggleLink.href = '#'; @@ -950,11 +960,31 @@ switch (message.command) { 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; - summaryText.textContent = expanded ? fullText : truncated; - toggleLink.textContent = expanded - ? (browser.i18n.getMessage("summarize_see_less") || "See less") - : (browser.i18n.getMessage("summarize_see_more") || "See more"); }); summaryTextWrapper.appendChild(toggleLink); From a1bd5d71e6fcc95eb260f695352ede58abfb7d2c Mon Sep 17 00:00:00 2001 From: Mic Date: Wed, 25 Mar 2026 00:13:00 +0100 Subject: [PATCH 42/52] correctly setting the current model in the special prompts pages --- pages/addtags/mzta-add-tags.js | 24 ++++++++-------- .../mzta-get-calendar-event.js | 24 ++++++++-------- pages/get-task/mzta-get-task.js | 24 ++++++++-------- pages/spamfilter/mzta-spamfilter.js | 22 +++++++++------ pages/summarize/mzta-summarize.js | 28 +++++++++++-------- 5 files changed, 68 insertions(+), 54 deletions(-) diff --git a/pages/addtags/mzta-add-tags.js b/pages/addtags/mzta-add-tags.js index f9296c28..c57901f9 100644 --- a/pages/addtags/mzta-add-tags.js +++ b/pages/addtags/mzta-add-tags.js @@ -330,19 +330,21 @@ async function restoreOptions() { const restoreValue = result[element.id] || default_select_value; // Check if option exists let optionExists = Array.from(element.options).some(opt => opt.value === restoreValue); - // If it doesn't exist and restoreValue is not empty, create it - if (!optionExists && restoreValue !== '') { - let newOption = new Option(restoreValue, restoreValue); - element.add(newOption); - } - // Set value - element.value = restoreValue; - if (element.value === '') { - element.selectedIndex = -1; - } if (element.tomselect) { - element.tomselect.setValue(element.value, true); + if (!optionExists && restoreValue !== '') { + element.tomselect.addOption({ value: restoreValue, text: restoreValue }); + } + element.tomselect.setValue(restoreValue, true); setTomSelectBorder(element.tomselect); + } else { + if (!optionExists && restoreValue !== '') { + let newOption = new Option(restoreValue, restoreValue); + element.add(newOption); + } + element.value = restoreValue; + if (element.value === '') { + element.selectedIndex = -1; + } } }else{ console.error("[ThunderAI] Unhandled input type:", element.type); diff --git a/pages/get-calendar-event/mzta-get-calendar-event.js b/pages/get-calendar-event/mzta-get-calendar-event.js index ca3c8ef7..19e54981 100644 --- a/pages/get-calendar-event/mzta-get-calendar-event.js +++ b/pages/get-calendar-event/mzta-get-calendar-event.js @@ -227,19 +227,21 @@ async function restoreOptions() { const restoreValue = result[element.id] || default_select_value; // Check if option exists let optionExists = Array.from(element.options).some(opt => opt.value === restoreValue); - // If it doesn't exist and restoreValue is not empty, create it - if (!optionExists && restoreValue !== '') { - let newOption = new Option(restoreValue, restoreValue); - element.add(newOption); - } - // Set value - element.value = restoreValue; - if (element.value === '') { - element.selectedIndex = -1; - } if (element.tomselect) { - element.tomselect.setValue(element.value, true); + if (!optionExists && restoreValue !== '') { + element.tomselect.addOption({ value: restoreValue, text: restoreValue }); + } + element.tomselect.setValue(restoreValue, true); setTomSelectBorder(element.tomselect); + } else { + if (!optionExists && restoreValue !== '') { + let newOption = new Option(restoreValue, restoreValue); + element.add(newOption); + } + element.value = restoreValue; + if (element.value === '') { + element.selectedIndex = -1; + } } }else{ console.error("[ThunderAI] Unhandled input type:", element.type); diff --git a/pages/get-task/mzta-get-task.js b/pages/get-task/mzta-get-task.js index f5e3f257..34f818dc 100644 --- a/pages/get-task/mzta-get-task.js +++ b/pages/get-task/mzta-get-task.js @@ -180,19 +180,21 @@ async function restoreOptions() { const restoreValue = result[element.id] || default_select_value; // Check if option exists let optionExists = Array.from(element.options).some(opt => opt.value === restoreValue); - // If it doesn't exist and restoreValue is not empty, create it - if (!optionExists && restoreValue !== '') { - let newOption = new Option(restoreValue, restoreValue); - element.add(newOption); - } - // Set value - element.value = restoreValue; - if (element.value === '') { - element.selectedIndex = -1; - } if (element.tomselect) { - element.tomselect.setValue(element.value, true); + if (!optionExists && restoreValue !== '') { + element.tomselect.addOption({ value: restoreValue, text: restoreValue }); + } + element.tomselect.setValue(restoreValue, true); setTomSelectBorder(element.tomselect); + } else { + if (!optionExists && restoreValue !== '') { + let newOption = new Option(restoreValue, restoreValue); + element.add(newOption); + } + element.value = restoreValue; + if (element.value === '') { + element.selectedIndex = -1; + } } }else{ console.error("[ThunderAI] Unhandled input type:", element.type); diff --git a/pages/spamfilter/mzta-spamfilter.js b/pages/spamfilter/mzta-spamfilter.js index 54258510..fccea0cf 100644 --- a/pages/spamfilter/mzta-spamfilter.js +++ b/pages/spamfilter/mzta-spamfilter.js @@ -326,17 +326,21 @@ async function restoreOptions() { const restoreValue = result[element.id] || default_select_value; // Ensure option exists before restoring let optionExists = Array.from(element.options).some(opt => opt.value === restoreValue); - if (!optionExists && restoreValue !== '') { - let newOption = new Option(restoreValue, restoreValue); - element.add(newOption); - } - element.value = restoreValue; - if (element.value === '') { - element.selectedIndex = -1; - } if (element.tomselect) { - element.tomselect.setValue(element.value, true); + if (!optionExists && restoreValue !== '') { + element.tomselect.addOption({ value: restoreValue, text: restoreValue }); + } + element.tomselect.setValue(restoreValue, true); setTomSelectBorder(element.tomselect); + } else { + if (!optionExists && restoreValue !== '') { + let newOption = new Option(restoreValue, restoreValue); + element.add(newOption); + } + element.value = restoreValue; + if (element.value === '') { + element.selectedIndex = -1; + } } }else{ console.error("[ThunderAI] Unhandled input type:", element.type); diff --git a/pages/summarize/mzta-summarize.js b/pages/summarize/mzta-summarize.js index 93a3cdc6..edb8ffec 100644 --- a/pages/summarize/mzta-summarize.js +++ b/pages/summarize/mzta-summarize.js @@ -32,7 +32,8 @@ import { import { textareaAutocomplete } from "../../js/mzta-placeholders-autocomplete.js"; import { normalizeStringList, - isAPIKeyValue + isAPIKeyValue, + setTomSelectBorder } from "../../js/mzta-utils.js"; import { initializeSpecificIntegrationUI @@ -246,18 +247,21 @@ async function restoreOptions() { const restoreValue = result[element.id] ?? default_select_value; // Check if option exists let optionExists = Array.from(element.options).some(opt => opt.value === String(restoreValue)); - // If it doesn't exist and restoreValue is not empty, create it - if (!optionExists && restoreValue !== '') { - let newOption = new Option(restoreValue, restoreValue); - element.add(newOption); - } - // Set value - element.value = restoreValue; - if (element.value === '') { - element.selectedIndex = 0; - } if (element.tomselect) { - element.tomselect.setValue(element.value, true); + if (!optionExists && restoreValue !== '') { + element.tomselect.addOption({ value: String(restoreValue), text: String(restoreValue) }); + } + element.tomselect.setValue(String(restoreValue), true); + setTomSelectBorder(element.tomselect); + } else { + if (!optionExists && restoreValue !== '') { + let newOption = new Option(restoreValue, restoreValue); + element.add(newOption); + } + element.value = restoreValue; + if (element.value === '') { + element.selectedIndex = 0; + } } }else{ console.error("[ThunderAI] Unhandled input type:", element.type); From 8a0e6e5cae235d14e871b2420dc71677fc4f52d9 Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 25 Mar 2026 23:23:03 +0100 Subject: [PATCH 43/52] 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

    +
  • Antispam information are now permanently saved for each message [#675].
  • ...

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

    +
  • Antispam information are now permanently saved for each message [#675].
  • ...

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 44/52] 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 45/52] 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 46/52] 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 47/52] 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 } From 1d8543627e45f19293482e30d335aa45af8e83ad Mon Sep 17 00:00:00 2001 From: mic Date: Thu, 26 Mar 2026 23:36:49 +0100 Subject: [PATCH 48/52] html mail parts are now displayed as code and are not rendered. see #711 --- api_webchat/messagesArea.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/api_webchat/messagesArea.js b/api_webchat/messagesArea.js index 5d9b5bc4..52dc1532 100644 --- a/api_webchat/messagesArea.js +++ b/api_webchat/messagesArea.js @@ -273,7 +273,11 @@ class MessagesArea extends HTMLElement { const messageElement = document.createElement('div'); messageElement.classList.add('message', type); // Replace \n with
for correct HTML display - messageElement.appendChild(htmlStringToFragment(messageText)); + if (type === "info") { + messageElement.appendChild(htmlStringToFragment(messageText)); + } else { + messageElement.appendChild(textWithBrToFragment(messageText)); + } // messageElement.textContent = messageText; // // Replace \n with
elements for correct HTML display // messageElement.innerHTML = ''; @@ -576,6 +580,20 @@ class MessagesArea extends HTMLElement { customElements.define('messages-area', MessagesArea); +function textWithBrToFragment(text) { + const fragment = document.createDocumentFragment(); + const segments = text.split(//gi); + segments.forEach((segment, idx) => { + if (segment.length > 0) { + fragment.appendChild(document.createTextNode(segment)); + } + if (idx < segments.length - 1) { + fragment.appendChild(document.createElement('br')); + } + }); + return fragment; +} + function htmlStringToFragment(htmlString) { // console.log(">>>>>>>>>>>>>>>> htmlStringToFragment htmlString: " + htmlString); const normalizedHtml = htmlString.replace(/\n/g, '
'); From 5301d1c89810e3a5b12f3602f6a8dca500e87abf Mon Sep 17 00:00:00 2001 From: mic Date: Thu, 26 Mar 2026 23:41:13 +0100 Subject: [PATCH 49/52] escaping html code also in chatgpt web integration. see #711 --- js/mzta-utils.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/js/mzta-utils.js b/js/mzta-utils.js index c5dbc59e..b3048c30 100644 --- a/js/mzta-utils.js +++ b/js/mzta-utils.js @@ -285,10 +285,19 @@ function convertBrToNewlines(html) { export function convertNewlinesToParagraphs(input) { return input .split('\n') - .map(line => `

${line}

`) + .map(line => `

${escapeHtml(line)}

`) .join(''); } +function escapeHtml(text) { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + // This method is used to convert the model string id used in the URL // to the model string used in the webpage From 942c5a61431622217a2e802ced27d8928a1408e6 Mon Sep 17 00:00:00 2001 From: mic Date: Fri, 27 Mar 2026 00:22:43 +0100 Subject: [PATCH 50/52] removing html elements added by thunderai when extracting text or html from the email. see #710 --- js/mzta-compose-script.js | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 097692bd..34298ba3 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -16,6 +16,26 @@ * along with this program. If not, see . */ +// CSS selectors for DOM elements injected by ThunderAI. +// These are stripped when retrieving the email body content +// to avoid contaminating placeholder values sent to AI providers. +// Add new selectors here when new UI elements are injected into the email DOM. +const MZTA_INJECTED_SELECTORS = [ + '#mzta-spam-check-progress', + '#mzta-spam-report-banner', + '.mzta_dialog', +]; + +function getCleanBodyClone() { + const clone = document.body.cloneNode(true); + for (const selector of MZTA_INJECTED_SELECTORS) { + for (const el of clone.querySelectorAll(selector)) { + el.remove(); + } + } + return clone; +} + function createThreeDotsMenu(isDark, menuItems, panelColors) { const wrapper = document.createElement('div'); wrapper.style.cssText = 'position: relative; display: inline-flex; align-items: center;'; @@ -139,7 +159,7 @@ switch (message.command) { case "getText": { let t = ''; - const children = window.document.body.childNodes; + const children = getCleanBodyClone().childNodes; for (const node of children) { if (node instanceof Element) { if (node.classList.contains('moz-signature')) { @@ -152,11 +172,11 @@ switch (message.command) { } case "getTextOnly": { - return Promise.resolve(window.document.body.innerText); + return Promise.resolve(getCleanBodyClone().innerText); } case "getFullHtml": { - return Promise.resolve(window.document.body.innerHTML); + return Promise.resolve(getCleanBodyClone().innerHTML); } case "getOnlyTypedText": { From 61eb858f5a6e86eea3ca1b6a9ba6f54656775b25 Mon Sep 17 00:00:00 2001 From: Mic Date: Fri, 27 Mar 2026 00:24:00 +0100 Subject: [PATCH 51/52] stripping printing headers from the html body berfore using it in a placeholder --- js/mzta-compose-script.js | 15 +++++++++++++++ js/mzta-utils.js | 23 +++++++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 34298ba3..a5c796f3 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -26,6 +26,20 @@ const MZTA_INJECTED_SELECTORS = [ '.mzta_dialog', ]; +// Mirrors removeMozMainHeader() from mzta-utils.js. +// Removes the Thunderbird-injected header table and any preceding divs. +function removeMozMainHeader(root) { + const table = root.querySelector('table.moz-main-header'); + if (!table) return; + let sibling = table.previousElementSibling; + while (sibling && sibling.tagName === 'DIV') { + const toRemove = sibling; + sibling = sibling.previousElementSibling; + toRemove.remove(); + } + table.remove(); +} + function getCleanBodyClone() { const clone = document.body.cloneNode(true); for (const selector of MZTA_INJECTED_SELECTORS) { @@ -33,6 +47,7 @@ function getCleanBodyClone() { el.remove(); } } + removeMozMainHeader(clone); return clone; } diff --git a/js/mzta-utils.js b/js/mzta-utils.js index b3048c30..71f876b3 100644 --- a/js/mzta-utils.js +++ b/js/mzta-utils.js @@ -181,6 +181,11 @@ export function getMailBody(fullMessage){ } if(html === "") { html = text.replace(/\n/g, "
"); + } else { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + removeMozMainHeader(doc.body); + html = doc.body.innerHTML; } return {text, html}; } @@ -246,7 +251,9 @@ export function htmlBodyToPlainText(htmlString) { const parser = new DOMParser(); // Parse the HTML string const doc = parser.parseFromString(htmlString, 'text/html'); - + + removeMozMainHeader(doc.body); + // remove invisible elements https://stackoverflow.com/questions/39813081/queryselector-where-display-is-not-none // return doc; doc.querySelectorAll('[style*="display:none"]').forEach(e => e.remove());//.querySelector('html').children.not(':visible').remove() @@ -263,7 +270,19 @@ export function htmlBodyToPlainText(htmlString) { .replace(/ /gi,"") .trim(); } - + +export function removeMozMainHeader(root) { + const table = root.querySelector('table.moz-main-header'); + if (!table) return; + let sibling = table.previousElementSibling; + while (sibling && sibling.tagName === 'DIV') { + const toRemove = sibling; + sibling = sibling.previousElementSibling; + toRemove.remove(); + } + table.remove(); +} + export function cleanupNewlines(text) { return text .replace(/\r\n/g, '\n') From 2460e40bf7b8b7cd296c9bf38d1c0ea16857eca4 Mon Sep 17 00:00:00 2001 From: mic Date: Fri, 27 Mar 2026 23:00:33 +0100 Subject: [PATCH 52/52] fix removing html elements added by thunderai when extracting text or html from the email. see #710 --- js/mzta-compose-script.js | 32 +++++++++++++------------------- js/mzta-utils.js | 16 ++++++++-------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index f86325e0..1eb3de51 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -26,28 +26,22 @@ const MZTA_INJECTED_SELECTORS = [ '.mzta_dialog', ]; -// Mirrors removeMozMainHeader() from mzta-utils.js. -// Removes the Thunderbird-injected header table and any preceding divs. -function removeMozMainHeader(root) { - const table = root.querySelector('table.moz-main-header'); - if (!table) return; - let sibling = table.previousElementSibling; - while (sibling && sibling.tagName === 'DIV') { - const toRemove = sibling; - sibling = sibling.previousElementSibling; - toRemove.remove(); - } - table.remove(); -} - -function getCleanBodyClone() { +function getCleanBodyHtml() { const clone = document.body.cloneNode(true); for (const selector of MZTA_INJECTED_SELECTORS) { for (const el of clone.querySelectorAll(selector)) { el.remove(); } } - removeMozMainHeader(clone); + for (const table of clone.querySelectorAll('table.moz-main-header')) { + let sibling = table.previousElementSibling; + while (sibling && sibling.tagName === 'DIV') { + const toRemove = sibling; + sibling = sibling.previousElementSibling; + toRemove.remove(); + } + table.remove(); + } return clone; } @@ -174,7 +168,7 @@ switch (message.command) { case "getText": { let t = ''; - const children = getCleanBodyClone().childNodes; + const children = getCleanBodyHtml().childNodes; for (const node of children) { if (node instanceof Element) { if (node.classList.contains('moz-signature')) { @@ -187,11 +181,11 @@ switch (message.command) { } case "getTextOnly": { - return Promise.resolve(getCleanBodyClone().innerText); + return Promise.resolve(getCleanBodyHtml().innerText); } case "getFullHtml": { - return Promise.resolve(getCleanBodyClone().innerHTML); + return Promise.resolve(getCleanBodyHtml().innerHTML); } case "getOnlyTypedText": { diff --git a/js/mzta-utils.js b/js/mzta-utils.js index 71f876b3..985b9d44 100644 --- a/js/mzta-utils.js +++ b/js/mzta-utils.js @@ -272,15 +272,15 @@ export function htmlBodyToPlainText(htmlString) { } export function removeMozMainHeader(root) { - const table = root.querySelector('table.moz-main-header'); - if (!table) return; - let sibling = table.previousElementSibling; - while (sibling && sibling.tagName === 'DIV') { - const toRemove = sibling; - sibling = sibling.previousElementSibling; - toRemove.remove(); + for (const table of root.querySelectorAll('table.moz-main-header')) { + let sibling = table.previousElementSibling; + while (sibling && sibling.tagName === 'DIV') { + const toRemove = sibling; + sibling = sibling.previousElementSibling; + toRemove.remove(); + } + table.remove(); } - table.remove(); } export function cleanupNewlines(text) {