From dcbf9d3dcddc3e7ebb5c1c5fad2408cf4956600e Mon Sep 17 00:00:00 2001 From: Ronald Klarenbeek Date: Fri, 26 Dec 2025 20:39:30 +0100 Subject: [PATCH 001/269] 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 002/269] 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 003/269] 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 004/269] 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 005/269] 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 006/269] 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 007/269] 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 008/269] 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 009/269] 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 010/269] 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 011/269] 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 012/269] 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 cf5ba3450abecd1073f940764031e6b60a56d8a4 Mon Sep 17 00:00:00 2001 From: mic Date: Fri, 20 Mar 2026 18:32:11 +0100 Subject: [PATCH 013/269] The API webchat window now has a dynamic title. see #696 --- api_webchat/controller.js | 2 ++ api_webchat/index.html | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/api_webchat/controller.js b/api_webchat/controller.js index 934f83f9..e5138da0 100644 --- a/api_webchat/controller.js +++ b/api_webchat/controller.js @@ -115,6 +115,8 @@ if (worker) { case 'anthropic': llmName = "Claude"; break; } messagesArea.setLLMName(llmName); + + document.title += " [" + llmName + " | " + decodeURIComponent(prompt_name) + "]"; let workerInitMessage = { type: 'init', diff --git a/api_webchat/index.html b/api_webchat/index.html index 5d803548..509c7017 100644 --- a/api_webchat/index.html +++ b/api_webchat/index.html @@ -3,7 +3,7 @@ - + ThunderAI Assistant From 216bdb3022e88b8e3304af25e8567b4941ee77eb Mon Sep 17 00:00:00 2001 From: mic Date: Fri, 20 Mar 2026 21:05:03 +0100 Subject: [PATCH 014/269] version set to 4.1.0 --- CHANGELOG.md | 4 ++++ manifest.json | 2 +- options/mzta-release-notes.html | 4 ++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fdbd6cf4..fb205dff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ +

Version 4.1.0 - ??/??/2026

+
    +
  • ...
  • +

Version 4.0.3 - 20/03/2026

  • Fixed a bug in creating new tags [#698].
  • diff --git a/manifest.json b/manifest.json index 85577cc7..0ea13754 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "ThunderAI", "description": "__MSG_extensionDescription__", - "version": "4.0.3", + "version": "4.1.0", "author": "Mic (m@micz.it)", "homepage_url": "https://micz.it/thunderbird-addon-thunderai/", "browser_specific_settings": { diff --git a/options/mzta-release-notes.html b/options/mzta-release-notes.html index bae3597c..36683c30 100644 --- a/options/mzta-release-notes.html +++ b/options/mzta-release-notes.html @@ -7,6 +7,10 @@

    ThunderAI Release Notes

    +

    Version 4.1.0 - ??/??/2026

    +
      +
    • ...
    • +

    Version 4.0.3 - 20/03/2026

    • Fixed a bug in creating new tags [#698].
    • From bc3acef47b76cba7551d95c2ef4b79ab4c5009d7 Mon Sep 17 00:00:00 2001 From: mic Date: Fri, 20 Mar 2026 21:24:57 +0100 Subject: [PATCH 015/269] copyright statement year updated --- pages/summarize/mzta-summarize.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/summarize/mzta-summarize.js b/pages/summarize/mzta-summarize.js index b41d9084..54544d73 100644 --- a/pages/summarize/mzta-summarize.js +++ b/pages/summarize/mzta-summarize.js @@ -1,6 +1,6 @@ /* * ThunderAI [https://micz.it/thunderbird-addon-thunderai/] - * Copyright (C) 2024 - 2025 Mic (m@micz.it) + * 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 From 64bb41633efd48e76da298d7bab3f1f775f67fac Mon Sep 17 00:00:00 2001 From: mic Date: Fri, 20 Mar 2026 21:27:14 +0100 Subject: [PATCH 016/269] CLAUDE.md updated --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CLAUDE.md b/CLAUDE.md index de33f9b2..b43e45f5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,6 +16,7 @@ ThunderAI is a **Thunderbird WebExtension (Manifest V2)** that integrates multip 4. **Placeholder format:** Placeholders in prompt text use the `{%placeholder_id%}` syntax (e.g., `{%mail_text_body_or_selected%}`). 5. **No test suite:** There is no automated test framework. Testing is done manually in Thunderbird. 6. **Settings defaults:** All new preferences must be added to `options/mzta-options-default.js` in `prefs_default`. +7. **Keep spec files up to date:** When making code changes that affect a subsystem described in claude-spec/, update the relevant spec file to reflect the new behavior. Read the spec before modifying, update it after. ## Directory Map From bb43f37815827f027539a128f533c676124fcf43 Mon Sep 17 00:00:00 2001 From: mic Date: Fri, 20 Mar 2026 21:30:35 +0100 Subject: [PATCH 017/269] removed unused import --- pages/summarize/mzta-summarize.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pages/summarize/mzta-summarize.js b/pages/summarize/mzta-summarize.js index 54544d73..53196051 100644 --- a/pages/summarize/mzta-summarize.js +++ b/pages/summarize/mzta-summarize.js @@ -16,7 +16,10 @@ * along with this program. If not, see . */ -import { prefs_default, integration_options_config } from '../../options/mzta-options-default.js'; +import { + prefs_default, + integration_options_config +} from '../../options/mzta-options-default.js'; import { taLogger } from "../../js/mzta-logger.js"; import { getSpecialPrompts, @@ -28,7 +31,6 @@ import { } from "../../js/mzta-placeholders.js"; import { textareaAutocomplete } from "../../js/mzta-placeholders-autocomplete.js"; import { - getAccountsList, normalizeStringList, isAPIKeyValue } from "../../js/mzta-utils.js"; From e95808d1fe4f98e8cd97abd211109bc3a4ebc82d Mon Sep 17 00:00:00 2001 From: mic Date: Fri, 20 Mar 2026 23:50:25 +0100 Subject: [PATCH 018/269] Storage class first try. see #675 --- js/mzta-storage.js | 187 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 js/mzta-storage.js diff --git a/js/mzta-storage.js b/js/mzta-storage.js new file mode 100644 index 00000000..21ceb5b7 --- /dev/null +++ b/js/mzta-storage.js @@ -0,0 +1,187 @@ +/* + * 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 . + */ + +import { taLogger } from './mzta-logger.js'; + +export class taStorage { + + static STORAGE_KEY_PREFIX = 'msg:'; + static SCHEMA_VERSION = 1; + + logger = null; + + constructor(do_debug = false) { + this.taLog = new taLogger("mzta-storage", do_debug); + } + + /** + * Build the storage key for a given Message-ID. + * @param {string} messageId - The Message-ID header string. + * @returns {string} The prefixed storage key. + */ + _buildKey(messageId) { + return taStorage.STORAGE_KEY_PREFIX + messageId; + } + + /** + * Read the full record for a given Message-ID. + * @param {string} messageId - The Message-ID header string. + * @returns {Promise} The record object or null if not found. + */ + async getRecord(messageId) { + try { + let key = this._buildKey(messageId); + let result = await messenger.storage.local.get(key); + return result[key] || null; + } catch (e) { + this.taLog.error('getRecord error: ' + e); + return null; + } + } + + /** + * Check if a record exists and contains the specified field. + * @param {string} messageId - The Message-ID header string. + * @param {string} field - The field name to check ("spam", "summary", or "translation"). + * @returns {Promise} True if the record exists and the field is present. + */ + async hasField(messageId, field) { + try { + let record = await this.getRecord(messageId); + return record !== null && field in record; + } catch (e) { + this.taLog.error('hasField error: ' + e); + return false; + } + } + + /** + * Write the spam field for a given Message-ID. + * @param {string} messageId - The Message-ID header string. + * @param {number} score - Spam score (float 0-1). + * @param {string} reason - Textual motivation for the score. + * @param {boolean} [force=false] - If true, overwrite existing spam data. + */ + async writeSpam(messageId, score, reason, force = true) { + try { + let key = this._buildKey(messageId); + let record = await this.getRecord(messageId) || { v: taStorage.SCHEMA_VERSION }; + if ('spam' in record && !force) { + return; + } + let now = Date.now(); + record.spam = { score: score, reason: reason, ts: now }; + record.ts = now; + await messenger.storage.local.set({ [key]: record }); + } catch (e) { + this.taLog.error('writeSpam error: ' + e); + } + } + + /** + * Write the summary field for a given Message-ID. + * @param {string} messageId - The Message-ID header string. + * @param {string} text - The summary text. + * @param {string} lang - The language of the summary. + * @param {boolean} [force=false] - If true, overwrite existing summary data. + */ + async writeSummary(messageId, text, force = true) { + try { + let key = this._buildKey(messageId); + let record = await this.getRecord(messageId) || { v: taStorage.SCHEMA_VERSION }; + if ('summary' in record && !force) { + return; + } + let now = Date.now(); + record.summary = { text: text, ts: now }; + record.ts = now; + await messenger.storage.local.set({ [key]: record }); + } catch (e) { + this.taLog.error('writeSummary error: ' + e); + } + } + + /** + * Write the translation field for a given Message-ID. + * @param {string} messageId - The Message-ID header string. + * @param {string} from - Source language code. + * @param {string} to - Target language code. + * @param {string} text - The translated text. + * @param {boolean} [force=false] - If true, overwrite existing translation data. + */ + async writeTranslation(messageId, translated_text, lang, force = true) { + try { + let key = this._buildKey(messageId); + let record = await this.getRecord(messageId) || { v: taStorage.SCHEMA_VERSION }; + if ('translation' in record && !force) { + return; + } + let now = Date.now(); + record.translation = { translated_text: translated_text, lang: lang, ts: now }; + record.ts = now; + await messenger.storage.local.set({ [key]: record }); + } catch (e) { + this.taLog.error('writeTranslation error: ' + e); + } + } + + /** + * Delete the entire record for a given Message-ID. + * @param {string} messageId - The Message-ID header string. + */ + async deleteRecord(messageId) { + try { + let key = this._buildKey(messageId); + await messenger.storage.local.remove(key); + } catch (e) { + this.taLog.error('deleteRecord error: ' + e); + } + } + + /** + * Remove all records older than maxAgeDays. + * @param {number} maxAgeDays - Maximum age in days. If 0, does nothing. + * @returns {Promise} The number of deleted records. + */ + async cleanup(maxAgeDays) { + if (maxAgeDays === 0) { + return 0; + } + try { + let all = await messenger.storage.local.get(null); + let cutoff = Date.now() - (maxAgeDays * 24 * 60 * 60 * 1000); + let keysToDelete = []; + for (let key of Object.keys(all)) { + if (!key.startsWith(taStorage.STORAGE_KEY_PREFIX)) { + continue; + } + let record = all[key]; + if (record.ts && record.ts < cutoff) { + keysToDelete.push(key); + } + } + if (keysToDelete.length > 0) { + await messenger.storage.local.remove(keysToDelete); + } + return keysToDelete.length; + } catch (e) { + this.taLog.error('cleanup error: ' + e); + return 0; + } + } +} From 08f95689558e7dac11153046dd05f5cb9b7460bd Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 22 Mar 2026 15:28:00 +0100 Subject: [PATCH 019/269] var name fixed --- js/mzta-storage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/mzta-storage.js b/js/mzta-storage.js index 21ceb5b7..989bb70a 100644 --- a/js/mzta-storage.js +++ b/js/mzta-storage.js @@ -23,7 +23,7 @@ export class taStorage { static STORAGE_KEY_PREFIX = 'msg:'; static SCHEMA_VERSION = 1; - logger = null; + taLog = null; constructor(do_debug = false) { this.taLog = new taLogger("mzta-storage", do_debug); From 038e6f6fed1fbc47103065e4b4f48d01d71a5606 Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 22 Mar 2026 15:30:40 +0100 Subject: [PATCH 020/269] hasfield gets now a record object. see #675 --- js/mzta-storage.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/js/mzta-storage.js b/js/mzta-storage.js index 989bb70a..e6fd8fa4 100644 --- a/js/mzta-storage.js +++ b/js/mzta-storage.js @@ -55,15 +55,14 @@ export class taStorage { } /** - * Check if a record exists and contains the specified field. - * @param {string} messageId - The Message-ID header string. + * Check if a record contains the specified field. + * @param {object|null} record - The record object (from getRecord). * @param {string} field - The field name to check ("spam", "summary", or "translation"). - * @returns {Promise} True if the record exists and the field is present. + * @returns {boolean} True if the record exists and the field is present. */ - async hasField(messageId, field) { + hasField(record, field) { try { - let record = await this.getRecord(messageId); - return record !== null && field in record; + return record !== null && record !== undefined && field in record; } catch (e) { this.taLog.error('hasField error: ' + e); return false; From b30da27c848e7249e9be2d83a434b36453b18229 Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 22 Mar 2026 15:32:10 +0100 Subject: [PATCH 021/269] method comments updated --- js/mzta-storage.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/js/mzta-storage.js b/js/mzta-storage.js index e6fd8fa4..2decdd72 100644 --- a/js/mzta-storage.js +++ b/js/mzta-storage.js @@ -74,7 +74,7 @@ export class taStorage { * @param {string} messageId - The Message-ID header string. * @param {number} score - Spam score (float 0-1). * @param {string} reason - Textual motivation for the score. - * @param {boolean} [force=false] - If true, overwrite existing spam data. + * @param {boolean} [force=true] - If true, overwrite existing spam data. */ async writeSpam(messageId, score, reason, force = true) { try { @@ -96,8 +96,7 @@ 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 {string} lang - The language of the summary. - * @param {boolean} [force=false] - If true, overwrite existing summary data. + * @param {boolean} [force=true] - If true, overwrite existing summary data. */ async writeSummary(messageId, text, force = true) { try { @@ -118,10 +117,9 @@ export class taStorage { /** * Write the translation field for a given Message-ID. * @param {string} messageId - The Message-ID header string. - * @param {string} from - Source language code. - * @param {string} to - Target language code. - * @param {string} text - The translated text. - * @param {boolean} [force=false] - If true, overwrite existing translation data. + * @param {string} translated_text - The translated text. + * @param {string} lang - Target language code. + * @param {boolean} [force=true] - If true, overwrite existing translation data. */ async writeTranslation(messageId, translated_text, lang, force = true) { try { From 55240868dd7e1c077eb01ec1f31c5b3cac41b8a8 Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 22 Mar 2026 22:29:31 +0100 Subject: [PATCH 022/269] spamreport is using the new storage. see #675 --- js/mzta-spamreport.js | 58 +++++++++++++++++++++------------- js/mzta-storage.js | 73 ++++++++++++++++++++++++++++++++++++++++--- mzta-background.js | 1 + 3 files changed, 106 insertions(+), 26 deletions(-) diff --git a/js/mzta-spamreport.js b/js/mzta-spamreport.js index f18a59f1..98d77b51 100644 --- a/js/mzta-spamreport.js +++ b/js/mzta-spamreport.js @@ -16,11 +16,20 @@ * along with this program. If not, see . */ +import { taStorage } from './mzta-storage.js'; + export const taSpamReport = { logger: console, + do_debug: false, _data_prefix: 'mzta-spam-report-', _processing_prefix: 'mzta-spam-processing-', _max_reports: 100, + _storage: null, + + _getStorage() { + if (!this._storage) this._storage = new taStorage(this.do_debug); + return this._storage; + }, async setProcessing(data_id) { const key = this._processing_prefix + data_id; @@ -34,8 +43,7 @@ export const taSpamReport = { }, async saveReportData(data, data_id) { - const key = this._data_prefix + data_id; - await browser.storage.session.set({ [key]: data }); + await this._getStorage().writeSpam(data_id, data, true); await browser.storage.session.remove(this._processing_prefix + data_id); }, @@ -51,47 +59,53 @@ export const taSpamReport = { }, async loadReportData(data_id) { - const key = this._data_prefix + data_id; - let output = await browser.storage.session.get(key); - return output[key] || null; + let record = await this._getStorage().getRecord(data_id); + if (!record || !this._getStorage().hasField(record, 'spam')) return null; + let spam = record.spam; + return { + headerMessageId: data_id, + spamValue: spam.spamValue, + explanation: spam.explanation, + report_date: new Date(spam.ts), + subject: spam.subject, + from: spam.from, + message_date: spam.message_date, + moved: spam.moved, + SpamThreshold: spam.SpamThreshold, + }; }, async removeReportData(data_id) { - const key = this._data_prefix + data_id; - await browser.storage.session.remove(key); + await this._getStorage().deleteSpamField(data_id); await browser.storage.session.remove(this._processing_prefix + data_id); }, async getAllReportData() { - let allData = await browser.storage.session.get(null); - let reportData = {}; - - for (const [key, value] of Object.entries(allData)) { - if (key.startsWith(this._data_prefix)) { - reportData[key.replace(this._data_prefix, '')] = value; - } - } - - return reportData; + return await this._getStorage().getAllSpamRecords(); }, async clearReportData() { - 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)); - + let storage = this._getStorage(); + let allSpam = await storage.getAllSpamRecords(); + for (let messageId of Object.keys(allSpam)) { + await storage.deleteSpamField(messageId); + } + let allSession = await browser.storage.session.get(null); + let keysToDelete = Object.keys(allSession).filter(k => k.startsWith(this._processing_prefix)); for (let key of keysToDelete) { await browser.storage.session.remove(key); } }, async truncReportData() { - let data = await this.getAllReportData(); + let data = await this._getStorage().getAllSpamRecords(); let sortedData = this.sortReportsByDate(data); let keys = Object.keys(sortedData); if (keys.length > this._max_reports) { + let storage = this._getStorage(); for (let i = this._max_reports; i < keys.length; i++) { - await browser.storage.session.remove(this._data_prefix + keys[i]); + await storage.deleteSpamField(keys[i]); } } }, diff --git a/js/mzta-storage.js b/js/mzta-storage.js index 2decdd72..44fa9b28 100644 --- a/js/mzta-storage.js +++ b/js/mzta-storage.js @@ -72,11 +72,11 @@ export class taStorage { /** * Write the spam field for a given Message-ID. * @param {string} messageId - The Message-ID header string. - * @param {number} score - Spam score (float 0-1). - * @param {string} reason - Textual motivation for the score. + * @param {object} report_data - The full spam report object with fields: + * spamValue, explanation, subject, from, message_date, moved, SpamThreshold. * @param {boolean} [force=true] - If true, overwrite existing spam data. */ - async writeSpam(messageId, score, reason, force = true) { + async writeSpam(messageId, report_data, force = true) { try { let key = this._buildKey(messageId); let record = await this.getRecord(messageId) || { v: taStorage.SCHEMA_VERSION }; @@ -84,7 +84,18 @@ export class taStorage { return; } let now = Date.now(); - record.spam = { score: score, reason: reason, ts: now }; + record.spam = { + spamValue: report_data.spamValue, + explanation: report_data.explanation, + subject: report_data.subject, + from: report_data.from, + message_date: report_data.message_date instanceof Date + ? report_data.message_date.toISOString() + : report_data.message_date, + moved: report_data.moved, + SpamThreshold: report_data.SpamThreshold, + ts: now, + }; record.ts = now; await messenger.storage.local.set({ [key]: record }); } catch (e) { @@ -92,6 +103,60 @@ export class taStorage { } } + /** + * Get all records that contain a spam field. + * @returns {Promise} Map of messageId -> spam data object (legacy shape). + */ + async getAllSpamRecords() { + 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, 'spam')) continue; + let messageId = key.slice(taStorage.STORAGE_KEY_PREFIX.length); + let spam = record.spam; + result[messageId] = { + headerMessageId: messageId, + spamValue: spam.spamValue, + explanation: spam.explanation, + report_date: new Date(spam.ts), + subject: spam.subject, + from: spam.from, + message_date: spam.message_date, + moved: spam.moved, + SpamThreshold: spam.SpamThreshold, + }; + } + return result; + } catch (e) { + this.taLog.error('getAllSpamRecords error: ' + e); + return {}; + } + } + + /** + * Delete only the spam field from a record. + * Deletes the entire record if no other data fields remain. + * @param {string} messageId - The Message-ID header string. + */ + async deleteSpamField(messageId) { + try { + let key = this._buildKey(messageId); + let record = await this.getRecord(messageId); + if (!record || !('spam' in record)) return; + delete record.spam; + const remainingFields = Object.keys(record).filter(k => k !== 'v' && k !== 'ts'); + if (remainingFields.length === 0) { + await messenger.storage.local.remove(key); + } else { + await messenger.storage.local.set({ [key]: record }); + } + } catch (e) { + this.taLog.error('deleteSpamField error: ' + e); + } + } + /** * Write the summary field for a given Message-ID. * @param {string} messageId - The Message-ID header string. diff --git a/mzta-background.js b/mzta-background.js index 7c901ac3..ef90c50e 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -1043,6 +1043,7 @@ const newEmailListener = (folder, messagesList) => { let messages = getMessages(messagesList); taSpamReport.logger = taLog; + taSpamReport.do_debug = prefs_init.do_debug; let add_tags_auto_enabled = prefs_init.add_tags && prefs_init.add_tags_auto; From 1ea548a61dbf7d1f58ce09cdb86fd0703d0f8b00 Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 22 Mar 2026 22:38:23 +0100 Subject: [PATCH 023/269] taSpamReport is now a class and not a singleton --- js/mzta-spamreport.js | 62 ++++++++++++++--------------- mzta-background.js | 22 +++++----- pages/spamfilter/mzta-spamfilter.js | 14 ++++--- 3 files changed, 49 insertions(+), 49 deletions(-) diff --git a/js/mzta-spamreport.js b/js/mzta-spamreport.js index 98d77b51..dacb6e71 100644 --- a/js/mzta-spamreport.js +++ b/js/mzta-spamreport.js @@ -17,35 +17,35 @@ */ import { taStorage } from './mzta-storage.js'; +import { taLogger } from './mzta-logger.js'; -export const taSpamReport = { - logger: console, - do_debug: false, - _data_prefix: 'mzta-spam-report-', - _processing_prefix: 'mzta-spam-processing-', - _max_reports: 100, - _storage: null, +export class taSpamReport { - _getStorage() { - if (!this._storage) this._storage = new taStorage(this.do_debug); - return this._storage; - }, + _processing_prefix = 'mzta-spam-processing-'; + _max_reports = 100; + _storage = null; + taLog = null; + + constructor(do_debug = false) { + this._storage = new taStorage(do_debug); + this.taLog = new taLogger('mzta-spamreport', do_debug); + } 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 saveReportData(data, data_id) { - await this._getStorage().writeSpam(data_id, data, true); + await this._storage.writeSpam(data_id, data, true); await browser.storage.session.remove(this._processing_prefix + data_id); - }, + } async saveError(data_id, error_message) { let data = { @@ -56,11 +56,11 @@ export const taSpamReport = { }; await this.saveReportData(data, data_id); return data; - }, + } async loadReportData(data_id) { - let record = await this._getStorage().getRecord(data_id); - if (!record || !this._getStorage().hasField(record, 'spam')) return null; + let record = await this._storage.getRecord(data_id); + if (!record || !this._storage.hasField(record, 'spam')) return null; let spam = record.spam; return { headerMessageId: data_id, @@ -73,42 +73,40 @@ export const taSpamReport = { moved: spam.moved, SpamThreshold: spam.SpamThreshold, }; - }, + } async removeReportData(data_id) { - await this._getStorage().deleteSpamField(data_id); + await this._storage.deleteSpamField(data_id); await browser.storage.session.remove(this._processing_prefix + data_id); - }, + } async getAllReportData() { - return await this._getStorage().getAllSpamRecords(); - }, + return await this._storage.getAllSpamRecords(); + } async clearReportData() { - let storage = this._getStorage(); - let allSpam = await storage.getAllSpamRecords(); + let allSpam = await this._storage.getAllSpamRecords(); for (let messageId of Object.keys(allSpam)) { - await storage.deleteSpamField(messageId); + await this._storage.deleteSpamField(messageId); } let allSession = await browser.storage.session.get(null); let keysToDelete = Object.keys(allSession).filter(k => k.startsWith(this._processing_prefix)); for (let key of keysToDelete) { await browser.storage.session.remove(key); } - }, + } async truncReportData() { - let data = await this._getStorage().getAllSpamRecords(); + let data = await this._storage.getAllSpamRecords(); let sortedData = this.sortReportsByDate(data); let keys = Object.keys(sortedData); if (keys.length > this._max_reports) { - let storage = this._getStorage(); for (let i = this._max_reports; i < keys.length; i++) { - await storage.deleteSpamField(keys[i]); + await this._storage.deleteSpamField(keys[i]); } } - }, + } sortReportsByDate(data) { if (!data) return {}; @@ -126,4 +124,4 @@ export const taSpamReport = { return sortedReports; } -}; +} diff --git a/mzta-background.js b/mzta-background.js index ef90c50e..a4f31c0c 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -94,6 +94,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 special_prompts_ids = getActiveSpecialPromptsIDs({ addtags: prefs_init.add_tags, @@ -359,10 +360,10 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { if (sender.tab.type !== 'messageDisplay' && sender.tab.type !== 'mail') return; let message = await browser.messageDisplay.getDisplayedMessage(tabId); if (!message) return; - let report = await taSpamReport.loadReportData(message.headerMessageId); + let report = await spamReport.loadReportData(message.headerMessageId); if (report) { browser.tabs.sendMessage(tabId, { command: "showSpamReport", data: report }); - } else if (await taSpamReport.isProcessing(message.headerMessageId)) { + } else if (await spamReport.isProcessing(message.headerMessageId)) { browser.tabs.sendMessage(tabId, { command: "showSpamCheckInProgress" }); } } catch (e) { @@ -372,7 +373,7 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { _checkSpamReport(sender.tab.id); break; case 'removeSpamReport': - taSpamReport.removeReportData(message.headerMessageId); + spamReport.removeReportData(message.headerMessageId); break; default: break; @@ -1042,9 +1043,6 @@ const newEmailListener = (folder, messagesList) => { async function _newEmailListener(){ let messages = getMessages(messagesList); - taSpamReport.logger = taLog; - taSpamReport.do_debug = prefs_init.do_debug; - let add_tags_auto_enabled = prefs_init.add_tags && prefs_init.add_tags_auto; await processEmails({ @@ -1054,7 +1052,7 @@ const newEmailListener = (folder, messagesList) => { }); if(prefs_init.spamfilter){ - taSpamReport.truncReportData(); + spamReport.truncReportData(); } } @@ -1177,8 +1175,8 @@ async function processEmails(args) { } } - await taSpamReport.removeReportData(message.headerMessageId); - await taSpamReport.setProcessing(message.headerMessageId); + await spamReport.removeReportData(message.headerMessageId); + await spamReport.setProcessing(message.headerMessageId); await updateSpamPanel(message.headerMessageId, "showSpamCheckInProgress"); @@ -1209,7 +1207,7 @@ async function processEmails(args) { spamfilter_result = (await cmd_spamfilter.sendPrompt()).trim(); } catch (err) { console.error("[ThunderAI | SpamFilter] Error getting spamfilter: ", err); - let err_data = await taSpamReport.saveError(message.headerMessageId, err.message || String(err)); + let err_data = await spamReport.saveError(message.headerMessageId, err.message || String(err)); await updateSpamPanel(message.headerMessageId, "showSpamReport", err_data); continue; } @@ -1220,7 +1218,7 @@ async function processEmails(args) { jsonObj = extractJsonObject(spamfilter_result); } catch (e) { console.error("[ThunderAI | SpamFilter] Error extracting JSON from AI response: ", e); - let err_data = await taSpamReport.saveError(message.headerMessageId, e.message || String(e)); + let err_data = await spamReport.saveError(message.headerMessageId, e.message || String(e)); await updateSpamPanel(message.headerMessageId, "showSpamReport", err_data); continue; } @@ -1246,7 +1244,7 @@ async function processEmails(args) { taLog.log("Marked as spam [" + message.headerMessageId + "]"); } - taSpamReport.saveReportData(report_data, 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); diff --git a/pages/spamfilter/mzta-spamfilter.js b/pages/spamfilter/mzta-spamfilter.js index 4879abcd..54258510 100644 --- a/pages/spamfilter/mzta-spamfilter.js +++ b/pages/spamfilter/mzta-spamfilter.js @@ -41,11 +41,15 @@ import { } from "../_lib/connection-ui.js"; let autocompleteSuggestions = []; -let taLog = new taLogger("mzta-spamfilter-page",true); -taSpamReport.logger = taLog; +let taLog = null; +let spamReport = null; document.addEventListener('DOMContentLoaded', async () => { + let prefs = await browser.storage.sync.get({ do_debug: prefs_default.do_debug }); + taLog = new taLogger("mzta-spamfilter-page", prefs.do_debug); + spamReport = new taSpamReport(prefs.do_debug); + let specialPrompts = await getSpecialPrompts(); let spamfilter_prompt = specialPrompts.find(prompt => prompt.id === 'prompt_spamfilter'); @@ -162,10 +166,10 @@ document.addEventListener('DOMContentLoaded', async () => { } if (selectedAccounts.length === document.querySelectorAll('.accountCheckbox').length) { browser.storage.sync.set({ spamfilter_enabled_accounts: [] }); - taSpamReport.logger.log("All accounts selected, saving spamfilter_enabled_accounts = []."); + taLog.log("All accounts selected, saving spamfilter_enabled_accounts = []."); } else { browser.storage.sync.set({ spamfilter_enabled_accounts: selectedAccounts }); - taSpamReport.logger.log("Saving spamfilter_enabled_accounts = " + JSON.stringify(selectedAccounts) + "."); + taLog.log("Saving spamfilter_enabled_accounts = " + JSON.stringify(selectedAccounts) + "."); } }); }); @@ -200,7 +204,7 @@ function check_spamfilter_threshold(event) { } async function loadSpamReport(){ - let report_data = await taSpamReport.getAllReportData(); + let report_data = await spamReport.getAllReportData(); //console.log(">>>>>>>>>>>> loadSpamReport: " + JSON.stringify(report_data)); //document.getElementById("report_data").textContent = JSON.stringify(report_data, null, 2); if(report_data == undefined){ From eba28e0919b4d4313ce4b7bb6e9f86bac17aca49 Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 22 Mar 2026 22:44:12 +0100 Subject: [PATCH 024/269] antispam panel css fix --- js/mzta-compose-script.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 8b8c3239..a4425c3e 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: start; gap: 15px; width: 100%; box-sizing: border-box;`; + 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;`; const scoreText = document.createElement('strong'); if (data.spamValue == -999) { @@ -700,7 +700,7 @@ switch (message.command) { const branding = document.createElement('span'); branding.textContent = browser.i18n.getMessage("antispam_by") + " ThunderAI"; - branding.style.cssText = 'margin-left: auto; font-style: italic; font-size: 11px; opacity: 0.7;'; + branding.style.cssText = 'margin-left: auto; font-style: italic; font-size: 10px; opacity: 0.5;'; const closeBtn = document.createElement('span'); closeBtn.textContent = '×'; From 170dddfb78eb392b4b6237693577254ed59145a1 Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 22 Mar 2026 22:49:42 +0100 Subject: [PATCH 025/269] log statements added --- js/mzta-spamreport.js | 35 ++++++++++++++++++++++++++++++----- js/mzta-storage.js | 30 +++++++++++++++++++++++++++--- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/js/mzta-spamreport.js b/js/mzta-spamreport.js index dacb6e71..953f5f37 100644 --- a/js/mzta-spamreport.js +++ b/js/mzta-spamreport.js @@ -32,22 +32,33 @@ export class taSpamReport { } 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 saveReportData(data, data_id) { - await this._storage.writeSpam(data_id, data, true); - await browser.storage.session.remove(this._processing_prefix + data_id); + this.taLog.log("[saveReportData] data_id: " + data_id); + try { + await this._storage.writeSpam(data_id, data, true); + await browser.storage.session.remove(this._processing_prefix + data_id); + } catch (e) { + this.taLog.error("[saveReportData] error: " + e); + throw e; + } } async saveError(data_id, error_message) { + this.taLog.log("[saveError] data_id: " + data_id + ", error_message: " + error_message); let data = { spamValue: -999, explanation: error_message, @@ -59,8 +70,12 @@ export class taSpamReport { } async loadReportData(data_id) { + this.taLog.log("[loadReportData] data_id: " + data_id); let record = await this._storage.getRecord(data_id); - if (!record || !this._storage.hasField(record, 'spam')) return null; + if (!record || !this._storage.hasField(record, 'spam')) { + this.taLog.log("[loadReportData] no record found for data_id: " + data_id); + return null; + } let spam = record.spam; return { headerMessageId: data_id, @@ -76,32 +91,42 @@ export class taSpamReport { } async removeReportData(data_id) { + this.taLog.log("[removeReportData] data_id: " + data_id); await this._storage.deleteSpamField(data_id); await browser.storage.session.remove(this._processing_prefix + data_id); } async getAllReportData() { + this.taLog.log("[getAllReportData] loading all reports"); return await this._storage.getAllSpamRecords(); } async clearReportData() { + this.taLog.log("[clearReportData] clearing all report data"); let allSpam = await this._storage.getAllSpamRecords(); - for (let messageId of Object.keys(allSpam)) { + let spamKeys = Object.keys(allSpam); + this.taLog.log("[clearReportData] deleting " + spamKeys.length + " spam records"); + for (let messageId of spamKeys) { await this._storage.deleteSpamField(messageId); } let allSession = await browser.storage.session.get(null); let keysToDelete = Object.keys(allSession).filter(k => k.startsWith(this._processing_prefix)); + this.taLog.log("[clearReportData] deleting " + keysToDelete.length + " session keys"); for (let key of keysToDelete) { await browser.storage.session.remove(key); } } async truncReportData() { + this.taLog.log("[truncReportData] checking report count"); let data = await this._storage.getAllSpamRecords(); let sortedData = this.sortReportsByDate(data); let keys = Object.keys(sortedData); + this.taLog.log("[truncReportData] total reports: " + keys.length + ", max: " + this._max_reports); if (keys.length > this._max_reports) { + let toDelete = keys.length - this._max_reports; + this.taLog.log("[truncReportData] truncating " + toDelete + " oldest reports"); for (let i = this._max_reports; i < keys.length; i++) { await this._storage.deleteSpamField(keys[i]); } diff --git a/js/mzta-storage.js b/js/mzta-storage.js index 44fa9b28..6692e989 100644 --- a/js/mzta-storage.js +++ b/js/mzta-storage.js @@ -44,10 +44,13 @@ export class taStorage { * @returns {Promise} The record object or null if not found. */ async getRecord(messageId) { + this.taLog.log('[getRecord] messageId: ' + messageId); try { let key = this._buildKey(messageId); let result = await messenger.storage.local.get(key); - return result[key] || null; + let record = result[key] || null; + this.taLog.log('[getRecord] record found: ' + (record !== null)); + return record; } catch (e) { this.taLog.error('getRecord error: ' + e); return null; @@ -61,8 +64,11 @@ export class taStorage { * @returns {boolean} True if the record exists and the field is present. */ hasField(record, field) { + this.taLog.log('[hasField] field: ' + field); try { - return record !== null && record !== undefined && field in record; + let result = record !== null && record !== undefined && field in record; + this.taLog.log('[hasField] result: ' + result); + return result; } catch (e) { this.taLog.error('hasField error: ' + e); return false; @@ -77,10 +83,12 @@ export class taStorage { * @param {boolean} [force=true] - If true, overwrite existing spam data. */ async writeSpam(messageId, report_data, force = true) { + this.taLog.log('[writeSpam] messageId: ' + messageId + ', force: ' + force); try { let key = this._buildKey(messageId); let record = await this.getRecord(messageId) || { v: taStorage.SCHEMA_VERSION }; if ('spam' in record && !force) { + this.taLog.log('[writeSpam] spam field already exists, skipping (force=false)'); return; } let now = Date.now(); @@ -108,6 +116,7 @@ export class taStorage { * @returns {Promise} Map of messageId -> spam data object (legacy shape). */ async getAllSpamRecords() { + this.taLog.log('[getAllSpamRecords] loading all spam records'); try { let all = await messenger.storage.local.get(null); let result = {}; @@ -128,6 +137,7 @@ export class taStorage { SpamThreshold: spam.SpamThreshold, }; } + this.taLog.log('[getAllSpamRecords] found ' + Object.keys(result).length + ' spam records'); return result; } catch (e) { this.taLog.error('getAllSpamRecords error: ' + e); @@ -141,15 +151,21 @@ export class taStorage { * @param {string} messageId - The Message-ID header string. */ async deleteSpamField(messageId) { + this.taLog.log('[deleteSpamField] messageId: ' + messageId); try { let key = this._buildKey(messageId); let record = await this.getRecord(messageId); - if (!record || !('spam' in record)) return; + if (!record || !('spam' in record)) { + this.taLog.log('[deleteSpamField] no spam field found for messageId: ' + messageId); + return; + } delete record.spam; const remainingFields = Object.keys(record).filter(k => k !== 'v' && k !== 'ts'); if (remainingFields.length === 0) { + this.taLog.log('[deleteSpamField] no remaining fields, deleting entire record'); await messenger.storage.local.remove(key); } else { + this.taLog.log('[deleteSpamField] remaining fields: ' + remainingFields.join(', ')); await messenger.storage.local.set({ [key]: record }); } } catch (e) { @@ -164,10 +180,12 @@ export class taStorage { * @param {boolean} [force=true] - If true, overwrite existing summary data. */ async writeSummary(messageId, text, force = true) { + this.taLog.log('[writeSummary] messageId: ' + messageId + ', force: ' + force); try { let key = this._buildKey(messageId); let record = await this.getRecord(messageId) || { v: taStorage.SCHEMA_VERSION }; if ('summary' in record && !force) { + this.taLog.log('[writeSummary] summary field already exists, skipping (force=false)'); return; } let now = Date.now(); @@ -187,10 +205,12 @@ export class taStorage { * @param {boolean} [force=true] - If true, overwrite existing translation data. */ async writeTranslation(messageId, translated_text, lang, force = true) { + this.taLog.log('[writeTranslation] messageId: ' + messageId + ', lang: ' + lang + ', force: ' + force); try { let key = this._buildKey(messageId); let record = await this.getRecord(messageId) || { v: taStorage.SCHEMA_VERSION }; if ('translation' in record && !force) { + this.taLog.log('[writeTranslation] translation field already exists, skipping (force=false)'); return; } let now = Date.now(); @@ -207,6 +227,7 @@ export class taStorage { * @param {string} messageId - The Message-ID header string. */ async deleteRecord(messageId) { + this.taLog.log('[deleteRecord] messageId: ' + messageId); try { let key = this._buildKey(messageId); await messenger.storage.local.remove(key); @@ -221,7 +242,9 @@ export class taStorage { * @returns {Promise} The number of deleted records. */ async cleanup(maxAgeDays) { + this.taLog.log('[cleanup] maxAgeDays: ' + maxAgeDays); if (maxAgeDays === 0) { + this.taLog.log('[cleanup] maxAgeDays is 0, skipping cleanup'); return 0; } try { @@ -237,6 +260,7 @@ export class taStorage { keysToDelete.push(key); } } + this.taLog.log('[cleanup] found ' + keysToDelete.length + ' records older than ' + maxAgeDays + ' days'); if (keysToDelete.length > 0) { await messenger.storage.local.remove(keysToDelete); } From 1ccd3d3123baea633f29ea32c780bb5c5350e8d4 Mon Sep 17 00:00:00 2001 From: Mic Date: Mon, 23 Mar 2026 00:22:00 +0100 Subject: [PATCH 026/269] taStorage constants added --- js/mzta-spamreport.js | 2 +- js/mzta-storage.js | 23 +++++++++++++---------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/js/mzta-spamreport.js b/js/mzta-spamreport.js index 953f5f37..2f4200e1 100644 --- a/js/mzta-spamreport.js +++ b/js/mzta-spamreport.js @@ -72,7 +72,7 @@ export class taSpamReport { async loadReportData(data_id) { this.taLog.log("[loadReportData] data_id: " + data_id); let record = await this._storage.getRecord(data_id); - if (!record || !this._storage.hasField(record, 'spam')) { + if (!record || !this._storage.hasField(record, taStorage.FIELD_SPAM)) { this.taLog.log("[loadReportData] no record found for data_id: " + data_id); return null; } diff --git a/js/mzta-storage.js b/js/mzta-storage.js index 6692e989..983a3451 100644 --- a/js/mzta-storage.js +++ b/js/mzta-storage.js @@ -22,6 +22,9 @@ export class taStorage { static STORAGE_KEY_PREFIX = 'msg:'; static SCHEMA_VERSION = 1; + static FIELD_SPAM = 'spam'; + static FIELD_SUMMARY = 'summary'; + static FIELD_TRANSLATION = 'translation'; taLog = null; @@ -87,12 +90,12 @@ export class taStorage { try { let key = this._buildKey(messageId); let record = await this.getRecord(messageId) || { v: taStorage.SCHEMA_VERSION }; - if ('spam' in record && !force) { + if (taStorage.FIELD_SPAM in record && !force) { this.taLog.log('[writeSpam] spam field already exists, skipping (force=false)'); return; } let now = Date.now(); - record.spam = { + record[taStorage.FIELD_SPAM] = { spamValue: report_data.spamValue, explanation: report_data.explanation, subject: report_data.subject, @@ -122,9 +125,9 @@ export class taStorage { let result = {}; for (let [key, record] of Object.entries(all)) { if (!key.startsWith(taStorage.STORAGE_KEY_PREFIX)) continue; - if (!this.hasField(record, 'spam')) continue; + if (!this.hasField(record, taStorage.FIELD_SPAM)) continue; let messageId = key.slice(taStorage.STORAGE_KEY_PREFIX.length); - let spam = record.spam; + let spam = record[taStorage.FIELD_SPAM]; result[messageId] = { headerMessageId: messageId, spamValue: spam.spamValue, @@ -155,11 +158,11 @@ export class taStorage { try { let key = this._buildKey(messageId); let record = await this.getRecord(messageId); - if (!record || !('spam' in record)) { + if (!record || !(taStorage.FIELD_SPAM in record)) { this.taLog.log('[deleteSpamField] no spam field found for messageId: ' + messageId); return; } - delete record.spam; + delete record[taStorage.FIELD_SPAM]; const remainingFields = Object.keys(record).filter(k => k !== 'v' && k !== 'ts'); if (remainingFields.length === 0) { this.taLog.log('[deleteSpamField] no remaining fields, deleting entire record'); @@ -184,12 +187,12 @@ export class taStorage { try { let key = this._buildKey(messageId); let record = await this.getRecord(messageId) || { v: taStorage.SCHEMA_VERSION }; - if ('summary' in record && !force) { + if (taStorage.FIELD_SUMMARY in record && !force) { this.taLog.log('[writeSummary] summary field already exists, skipping (force=false)'); return; } let now = Date.now(); - record.summary = { text: text, ts: now }; + record[taStorage.FIELD_SUMMARY] = { text: text, ts: now }; record.ts = now; await messenger.storage.local.set({ [key]: record }); } catch (e) { @@ -209,12 +212,12 @@ export class taStorage { try { let key = this._buildKey(messageId); let record = await this.getRecord(messageId) || { v: taStorage.SCHEMA_VERSION }; - if ('translation' in record && !force) { + if (taStorage.FIELD_TRANSLATION in record && !force) { this.taLog.log('[writeTranslation] translation field already exists, skipping (force=false)'); return; } let now = Date.now(); - record.translation = { translated_text: translated_text, lang: lang, ts: now }; + record[taStorage.FIELD_TRANSLATION] = { translated_text: translated_text, lang: lang, ts: now }; record.ts = now; await messenger.storage.local.set({ [key]: record }); } catch (e) { From 6d53e108d050479d6eee4fbe8c839af228eef675 Mon Sep 17 00:00:00 2001 From: Mic Date: Mon, 23 Mar 2026 18:25:35 +0100 Subject: [PATCH 027/269] 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 028/269] 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 029/269] 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 030/269] 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 031/269] 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 032/269] 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 033/269] 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 034/269] 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 035/269] 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 036/269] 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 037/269] 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 038/269] 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 039/269] 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 040/269] 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 041/269] 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 042/269] 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 043/269] 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 044/269] 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 045/269] 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 046/269] 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 047/269] 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 048/269] 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 049/269] 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 050/269] 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 051/269] 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 052/269] 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 053/269] 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 054/269] 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 055/269] 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 056/269] 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 057/269] 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 058/269] 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 059/269] 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 060/269] 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 061/269] 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 062/269] 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 063/269] 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 064/269] 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 065/269] 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 066/269] 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) { From 99b07fb5d20f091436da8ff34506e3058a4abfd0 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Fri, 27 Mar 2026 23:08:50 +0100 Subject: [PATCH 067/269] Update translation files Updated by "Cleanup translation files" add-on in Weblate. Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/ --- _locales/de/messages.json | 3 --- _locales/el/messages.json | 3 --- _locales/fr/messages.json | 3 --- _locales/it/messages.json | 3 --- _locales/sv/messages.json | 3 --- 5 files changed, 15 deletions(-) diff --git a/_locales/de/messages.json b/_locales/de/messages.json index e0f76e4c..053585a9 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -1399,9 +1399,6 @@ "prefs_OptionText_spamfilter_show_msg_panel_Info": { "message": "Wenn aktiviert, wird oben über der Nachricht ein Panel mit dem Spam-Bericht angezeigt." }, - "SpamReport_infoline": { - "message": "Diese Informationen werden nur für die aktuelle Sitzung gespeichert." - }, "Spam": { "message": "Spam" }, diff --git a/_locales/el/messages.json b/_locales/el/messages.json index f3dbb337..d1eaffef 100644 --- a/_locales/el/messages.json +++ b/_locales/el/messages.json @@ -1377,9 +1377,6 @@ "prefs_OptionText_spamfilter_show_msg_panel_Info": { "message": "Εάν είναι επιλεγμένο, θα εμφανίζεται ένα πλαίσιο με την αναφορά ανεπιθύμητης αλληλογραφίας στο επάνω μέρος του μηνύματος." }, - "SpamReport_infoline": { - "message": "Αυτές οι πληροφορίες αποθηκεύονται μόνο για την τρέχουσα συνεδρία." - }, "Spam": { "message": "Ανεπιθύμητα μηνύματα" }, diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 71bb28ce..54630870 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -1399,9 +1399,6 @@ "prefs_OptionText_spamfilter_show_msg_panel_Info": { "message": "Si coché, un panneau avec le rapport de spam s'affichera au-dessus du message." }, - "SpamReport_infoline": { - "message": "Ces informations ne sont enregistrées que pour la session en cours." - }, "Spam": { "message": "Indésirables" }, diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 2a762996..763844ee 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -1399,9 +1399,6 @@ "prefs_OptionText_spamfilter_show_msg_panel_Info": { "message": "Se selezionato, verrà mostrato un pannello con il report spam sopra il messaggio." }, - "SpamReport_infoline": { - "message": "Queste informazioni sono salvate solo per la sessione corrente." - }, "Spam": { "message": "Spam" }, diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index ffc560a9..41df75e4 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -1410,9 +1410,6 @@ "prefs_OptionText_spamfilter_show_msg_panel_Info": { "message": "Om markerat visas en panel med skräppostrapporten högst upp i meddelandet." }, - "SpamReport_infoline": { - "message": "Denna information sparas endast för den aktuella sessionen." - }, "prefs_OptionText_chatgpt_web_load_wait_time": { "message": "Väntetid för sidinläsning" }, From a908efc88d1b6a8489c2e031371cc9c6858b78fd Mon Sep 17 00:00:00 2001 From: mic Date: Fri, 27 Mar 2026 23:20:58 +0100 Subject: [PATCH 068/269] 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 83b00aed..0772a115 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@

      Version 4.1.0 - ??/??/2026

      • Antispam information are now permanently saved for each message [#675].
      • +
      • [All APIs] A summaru has been added above the mail content [#580].
      • ...

      Version 4.0.3 - 20/03/2026

      diff --git a/options/mzta-release-notes.html b/options/mzta-release-notes.html index c13c26ed..3f21a98c 100644 --- a/options/mzta-release-notes.html +++ b/options/mzta-release-notes.html @@ -10,6 +10,7 @@

      Version 4.1.0 - ??/??/2026

      • Antispam information are now permanently saved for each message [#675].
      • +
      • [All APIs] A summaru has been added above the mail content [#580].
      • ...

      Version 4.0.3 - 20/03/2026

      From 681f0f0f18a718fea2d073d70c06dba2f7787f65 Mon Sep 17 00:00:00 2001 From: mic Date: Fri, 27 Mar 2026 23:25:09 +0100 Subject: [PATCH 069/269] clear cache button added in the options page. see #675 --- _locales/en/messages.json | 25 +++++++++++++++++++++++++ js/mzta-storage.js | 18 ++++++++++++++++++ js/mzta-utils.js | 9 +++++++++ options/mzta-options.html | 7 +++++++ options/mzta-options.js | 21 ++++++++++++++++++++- 5 files changed, 79 insertions(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 620a03a0..15824bc7 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -331,6 +331,31 @@ "message": "Manage your data placeholders", "description": "" }, + "prefs_cache_title": { + "message": "Cache Storage", + "description": "Title for cache management section in options" + }, + "prefs_cache_storage_size": { + "message": "Cache size", + "description": "Label for cache storage size display" + }, + "prefs_cache_clear_button": { + "message": "Clear Cache", + "description": "Button to clear all cached message data" + }, + "prefs_cache_clear_confirm": { + "message": "Are you sure you want to clear all cached data (summaries, spam reports, translations)? This action cannot be undone.", + "description": "Confirmation dialog for clearing cache" + }, + "prefs_cache_clear_done": { + "message": "$COUNT$ records removed.", + "description": "Message shown after cache is cleared", + "placeholders": { + "count": { + "content": "$1" + } + } + }, "prefsInfoTitle": { "message": "Important Information", "description": "" diff --git a/js/mzta-storage.js b/js/mzta-storage.js index a94695e8..b5699618 100644 --- a/js/mzta-storage.js +++ b/js/mzta-storage.js @@ -344,4 +344,22 @@ export class taStorage { return 0; } } + + /** + * Remove all records (all keys with the storage prefix). + * @returns {Promise} The number of deleted records. + */ + static async clearAllRecords() { + try { + let all = await messenger.storage.local.get(null); + let keysToDelete = Object.keys(all).filter(k => k.startsWith(taStorage.STORAGE_KEY_PREFIX)); + if (keysToDelete.length > 0) { + await messenger.storage.local.remove(keysToDelete); + } + return keysToDelete.length; + } catch (e) { + console.error('[taStorage.clearAllRecords] error: ' + e); + return 0; + } + } } diff --git a/js/mzta-utils.js b/js/mzta-utils.js index 985b9d44..8aad2068 100644 --- a/js/mzta-utils.js +++ b/js/mzta-utils.js @@ -827,6 +827,15 @@ export async function getLocalStorageUsedSpace(){ return formatBytes(customprompts_space); } +export async function getCacheStorageUsedSpace(){ + let all = await browser.storage.local.get(null); + let cacheSpace = Object.entries(all) + .filter(([key]) => key.startsWith('msg:')) + .map(([key, value]) => key.length + JSON.stringify(value).length) + .reduce((acc, x) => acc + x, 0); + return formatBytes(cacheSpace); +} + function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes'; const step = 1024; diff --git a/options/mzta-options.html b/options/mzta-options.html index fb792f86..0033a590 100644 --- a/options/mzta-options.html +++ b/options/mzta-options.html @@ -241,6 +241,13 @@
      + + + +
      __MSG_prefs_cache_title__ + __MSG_prefs_cache_storage_size__: +   +
      diff --git a/options/mzta-options.js b/options/mzta-options.js index 289a5430..51baa538 100644 --- a/options/mzta-options.js +++ b/options/mzta-options.js @@ -26,8 +26,10 @@ import { isAPIKeyValue, getConnectionType, setTomSelectBorder, - getMiczItUrl + getMiczItUrl, + getCacheStorageUsedSpace } from '../js/mzta-utils.js'; +import { taStorage } from '../js/mzta-storage.js'; import { injectConnectionUI, varConnectionUI, @@ -220,6 +222,11 @@ function resetMaxPromptLength(){ let maxPromptLength = document.getElementById('max_prompt_length'); maxPromptLength.value = prefs_default.max_prompt_length; browser.storage.sync.set({max_prompt_length: prefs_default.max_prompt_length}); +} + +async function updateCacheSize() { + let size = await getCacheStorageUsedSpace(); + document.getElementById('cache_storage_size').textContent = size; } document.addEventListener('DOMContentLoaded', async () => { @@ -368,6 +375,18 @@ document.addEventListener('DOMContentLoaded', async () => { await browser.tabs.create({ url: "../pages/onboarding/onboarding.html" }); }); + // Cache management + updateCacheSize(); + + document.getElementById('btnClearCache').addEventListener('click', async () => { + if (!confirm(browser.i18n.getMessage("prefs_cache_clear_confirm"))) { + return; + } + let count = await taStorage.clearAllRecords(); + alert(browser.i18n.getMessage("prefs_cache_clear_done", [String(count)])); + updateCacheSize(); + }); + browser.runtime.getPlatformInfo().then(info => { taLog.log("OS: " + info.os); if ((info.os === "linux")&&(prefs_opt.chatgpt_win_height!=0)&&(prefs_opt.chatgpt_win_width!=0)){ From 32abc00693135414e07a20a28767123acf86313e Mon Sep 17 00:00:00 2001 From: mic Date: Fri, 27 Mar 2026 23:59:17 +0100 Subject: [PATCH 070/269] mail translation first try. see #247 --- _locales/en/messages.json | 96 ++++++++++++ claude-spec/01-architecture.md | 38 ++++- claude-spec/02-prompts.md | 21 +++ claude-spec/05-options.md | 19 ++- js/mzta-compose-script.js | 206 +++++++++++++++++++++++- js/mzta-menus.js | 4 +- js/mzta-prompts.js | 34 ++-- js/mzta-storage.js | 62 +++++++- js/mzta-translationstore.js | 150 ++++++++++++++++++ js/mzta-utils-prompt.js | 20 +++ mzta-background.js | 132 ++++++++++++++++ options/mzta-options-default.js | 5 +- options/mzta-options.html | 11 ++ options/mzta-options.js | 36 ++++- pages/translate/mzta-translate.css | 180 +++++++++++++++++++++ pages/translate/mzta-translate.html | 97 ++++++++++++ pages/translate/mzta-translate.js | 232 ++++++++++++++++++++++++++++ 17 files changed, 1316 insertions(+), 27 deletions(-) create mode 100644 js/mzta-translationstore.js create mode 100644 pages/translate/mzta-translate.css create mode 100644 pages/translate/mzta-translate.html create mode 100644 pages/translate/mzta-translate.js diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 15824bc7..49c64d6d 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2049,6 +2049,102 @@ "message": "Delete summary", "description": "" }, + "prefs_OptionText_translate": { + "message": "Translate email", + "description": "" + }, + "prefs_OptionText_translate_use_specific_integration_Info": { + "message": "If checked, the Model and API specified below will be used for translating email(s), regardless the one chosen in the ThunderAI options page.", + "description": "" + }, + "prefs_OptionText_translate_Info": { + "message": "If checked, adds a translate button in the message body.", + "description": "" + }, + "prefs_OptionText_btnManageTranslateInfo": { + "message": "Manage translate settings", + "description": "" + }, + "Translate_PageTitle": { + "message": "Manage Translation Settings", + "description": "" + }, + "Translate_info_default": { + "message": "In this page you can modify the default prompt used to translate emails.", + "description": "" + }, + "Translate_prompt_text_title": { + "message": "Current prompt text", + "description": "" + }, + "Translate_prompt_prefs_title": { + "message": "Translation Options", + "description": "" + }, + "prefs_OptionText_translate_auto": { + "message": "Auto-translate messages", + "description": "" + }, + "prefs_OptionText_translate_auto_disabled": { + "message": "Disabled", + "description": "" + }, + "prefs_OptionText_translate_auto_manual": { + "message": "Manual button only", + "description": "" + }, + "prefs_OptionText_translate_auto_automatic": { + "message": "Automatic", + "description": "" + }, + "prefs_OptionText_translate_auto_Info": { + "message": "Choose when to translate messages: disabled, only when clicking the button, or automatically when opening a message.", + "description": "" + }, + "prefs_OptionText_translate_lang": { + "message": "Translation target language", + "description": "" + }, + "prefs_OptionText_translate_lang_Info": { + "message": "Language to translate emails into. If empty, uses the default language setting.", + "description": "" + }, + "prefs_OptionText_Translate_main_prompt": { + "message": "The prompt describing the translation task:", + "description": "" + }, + "translate_generating": { + "message": "Translating...", + "description": "" + }, + "translate_click_to_generate": { + "message": "Click here to translate this email", + "description": "" + }, + "get_ai_translation": { + "message": "AI Translation", + "description": "" + }, + "translate_chatgpt_web_not_supported": { + "message": "Auto-translation requires an API-based connection. Please configure an API connection in ThunderAI settings.", + "description": "" + }, + "translate_refresh": { + "message": "Refresh translation", + "description": "" + }, + "translate_delete": { + "message": "Delete translation", + "description": "" + }, + "translate_banner_title": { + "message": "AI Translation", + "description": "" + }, + "translate_error": { + "message": "Translation failed.", + "description": "" + }, "antispam_by": { "message": "Antispam by", "description": "" diff --git a/claude-spec/01-architecture.md b/claude-spec/01-architecture.md index 085affed..a679df6f 100644 --- a/claude-spec/01-architecture.md +++ b/claude-spec/01-architecture.md @@ -78,6 +78,38 @@ mzta-background.js (checks summarize_auto + summarize_display_mode prefs) mzta-compose-script.js (render summary banner in message body) ``` +### Data Flow: Inline Translation on Message Display + +The `translate_auto` preference controls when translation is triggered. Unlike summarize, translation has no `display_mode` option (always inline). + +- `translate_auto = 0` (disabled) → do nothing +- `translate_auto = 1` (manual button) → show "Get AI Translation" button in message body +- `translate_auto = 2` (automatic) → generate translation immediately on message open + +The target language is determined by `translate_lang` (fallback on `default_chatgpt_lang`). + +``` +User opens/selects a message in Thunderbird + ↓ +mzta-compose-script.js (sends "initTranslation" to background) + ↓ +mzta-background.js (checks translate + translate_auto prefs) + ↓ + ┌──────────────────────────────────────────────────────────┐ + │ translate_auto = 0 → do nothing │ + │ translate_auto = 1 → show "click to translate" button │ + │ translate_auto = 2 → generate immediately (always inline)│ + └──────────────────────────────────────────────────────────┘ + ↓ (if generating) + taTranslationStore (check cache / set processing) + ↓ (cache miss) + mzta-special-commands (via Web Worker, NOT chatgpt_web) + ↓ + taTranslationStore (save result via taStorage) + ↓ + mzta-compose-script.js (render translation banner in message body) +``` + ## Key Modules | File | Role | @@ -87,7 +119,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, `buildSummaryPrompt()` for unified summary prompt assembly) | +| `js/mzta-utils-prompt.js` | Prompt-specific utilities (text truncation, lang injection, `buildSummaryPrompt()` for unified summary prompt assembly, `buildTranslationPrompt()` for translation 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) | @@ -97,6 +129,7 @@ mzta-background.js (checks summarize_auto + summarize_display_mode prefs) | `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-translationstore.js` | Translation-specific storage wrapper (`taTranslationStore` 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 | @@ -139,6 +172,7 @@ Each subdirectory is a self-contained settings/UI page for a specific feature: | `get-task/` | Task creation settings | | `spamfilter/` | Spam filter settings | | `summarize/` | Email summarization settings | +| `translate/` | Email translation settings | | `onboarding/` | First-run welcome page | | `_lib/` | Shared libraries used by pages | @@ -151,3 +185,5 @@ All preferences are stored via `browser.storage.local`. The keys and default val 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. + +`js/mzta-translationstore.js` (`taTranslationStore` class) wraps `taStorage` for translation-specific operations: load/save/remove translations, track in-flight generation state via `browser.storage.session`, enforce a 100-entry cache limit with oldest-first truncation, and store error states. Each translation record stores `translated_text`, `lang`, and optional error information. diff --git a/claude-spec/02-prompts.md b/claude-spec/02-prompts.md index aa2b3864..77b271c2 100644 --- a/claude-spec/02-prompts.md +++ b/claude-spec/02-prompts.md @@ -55,6 +55,7 @@ Some prompts trigger additional Thunderbird actions beyond just sending text to | `summarize` | Summarize email content | | `get_calendar_event` | Extract and create a calendar event | | `get_task` | Extract and create a task | +| `translate` | Translate email content into a target language | 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`. @@ -85,6 +86,26 @@ The summarize feature uses two distinct prompt pathways: - Accepts an array of `{ message, fullMessage }` entries - Returns `{ promptText, promptInfo }` where `promptInfo` is the `prompt_summarize` prompt object +### Translate: Inline-Only Prompt System + +The translate feature uses a single special prompt (`prompt_translate_this`) for inline translation in the message body. Unlike summarize, it has no context menu entry and no webchat mode. + +**Inline Translation on Message Display** (controlled by `translate_auto` pref): +- Uses a single special prompt: `prompt_translate_this` +- The prompt text is appended with the target language and the email body: `prompt_text + " " + lang + ". \"" + body_text + "\""` +- Target language is determined by `translate_lang` pref, falling back to `default_chatgpt_lang` +- Does **not** support `chatgpt_web` connection type (shows error if configured) +- Result is rendered as a styled banner (green/teal theme) in the message body via `mzta-compose-script.js` +- Banner includes refresh (↻) and delete (×) buttons +- Cached per-message via `taTranslationStore` / `taStorage` (max 100 entries) +- The prompt was originally a regular prompt (`defaultPrompts`) and was moved to `specialPrompts` with `is_special: "1"` and `type: "1"` (reading email only) + +**Prompt Building** — `taPromptUtils.buildTranslationPrompt(fullMessage, lang)`: +- Retrieves the `prompt_translate_this` special prompt text +- Extracts the email body from the full message +- Combines prompt + language + body text +- Returns `{ promptText, promptInfo }` + ## Prompt Types Reference ``` diff --git a/claude-spec/05-options.md b/claude-spec/05-options.md index b61cd92b..7dbe7217 100644 --- a/claude-spec/05-options.md +++ b/claude-spec/05-options.md @@ -35,7 +35,7 @@ use_specific_integration (default: false) ### Special Prompt Integration Overrides -The 5 special prompts (`add_tags`, `spamfilter`, `summarize`, `get_calendar_event`, `get_task`) each get their own `use_specific_integration` and `connection_type` keys: +The 6 special prompts (`add_tags`, `spamfilter`, `summarize`, `get_calendar_event`, `get_task`, `translate`) each get their own `use_specific_integration` and `connection_type` keys: ``` {prefix}_use_specific_integration (default: false) @@ -98,6 +98,9 @@ These are generated programmatically at the bottom of `mzta-options-default.js` | `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. | +| `translate` | `true` | Enable email translation | +| `translate_auto` | `0` | Auto-translate mode: `0` = disabled, `1` = manual (show button), `2` = automatic (translate on message open) | +| `translate_lang` | `''` | Target language for translation. Falls back to `default_chatgpt_lang` if empty. | ### Summarize Settings Page (`pages/summarize/`) @@ -120,6 +123,20 @@ The summarize settings page provides: - Each has Save/Reset buttons and placeholder autocomplete - Default text comes from i18n strings (`prompt_summarize_full_text`, etc.) +### Translate Settings Page (`pages/translate/`) + +The translate settings page provides: + +1. **Specific integration checkbox** — enables per-feature API override (like other special prompts) +2. **Auto-translate dropdown** (`translate_auto`) — three modes: + - `0` (Disabled) — no inline translations + - `1` (Manual) — shows a "Get AI Translation" button in message display + - `2` (Automatic) — generates translation immediately when message is opened +3. **Target language** (`translate_lang`) — text input for the destination language. If empty, falls back to `default_chatgpt_lang`. +4. **One editable prompt** — the translation instruction prompt (`prompt_translate_this`) with Save/Reset buttons and placeholder autocomplete. Default text comes from i18n string `prompt_translate_this_full_text`. + +Unlike summarize, translation has no `display_mode` option (always inline) and no max display length setting. + ## Adding a New Preference 1. Add the key and default value to `prefs_default` in `options/mzta-options-default.js` diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 1eb3de51..3fc11537 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -1181,6 +1181,209 @@ switch (message.command) { } return Promise.resolve(true); + case "showTranslation": + const existingTranslationGenerating = document.getElementById('mzta-translation-generating'); + if(existingTranslationGenerating) existingTranslationGenerating.remove(); + + const existingTranslationTriggerWrapper = document.getElementById('mzta-translation-trigger-wrapper'); + if(existingTranslationTriggerWrapper) existingTranslationTriggerWrapper.remove(); + const existingTranslationTriggerBtn = document.getElementById('mzta-translation-trigger'); + if(existingTranslationTriggerBtn) existingTranslationTriggerBtn.remove(); + + const existingTranslationBanner = document.getElementById('mzta-translation-banner'); + if(existingTranslationBanner) existingTranslationBanner.remove(); + + const translationData = message.data; + const translationContainer = document.createElement('div'); + translationContainer.id = 'mzta-translation-banner'; + + const isDarkTranslation = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + + let bgColorTranslation = isDarkTranslation ? '#1a2e2a' : '#e8f5e9'; + let textColorTranslation = isDarkTranslation ? '#c8e6c9' : '#1b5e20'; + let borderColorTranslation = isDarkTranslation ? '#2e5740' : '#a5d6a7'; + + if (translationData.error) { + bgColorTranslation = isDarkTranslation ? '#3a1a1a' : '#f7e6e6'; + textColorTranslation = isDarkTranslation ? '#ffcccc' : '#660000'; + borderColorTranslation = '#660000'; + } + + translationContainer.className = 'thunderai-translation-pane'; + translationContainer.style.cssText = `background-color: ${bgColorTranslation}; color: ${textColorTranslation}; padding: 0.5rem; margin-bottom: 1rem; border-radius: 4px; border: 1px solid ${borderColorTranslation}; font-family: system-ui, -apple-system, sans-serif; font-size: 14px;`; + + const translationHeader = document.createElement('div'); + translationHeader.style.cssText = 'display: flex; align-items: center; gap: 8px; margin-bottom: 6px;'; + + const translationIcon = document.createElement('img'); + translationIcon.src = browser.runtime.getURL("/images/ai_summary.png"); + translationIcon.style.cssText = `height: 16px; width: 16px; flex-shrink: 0;${isDarkTranslation ? ' filter: invert(1);' : ''}`; + + const translationTitleSpan = document.createElement('span'); + translationTitleSpan.style.cssText = `font-weight: bold; font-size: 14px; color: ${textColorTranslation}; flex-grow: 1;`; + translationTitleSpan.textContent = browser.i18n.getMessage("translate_banner_title") || "AI Translation"; + if (translationData.lang) { + translationTitleSpan.textContent += ' (' + translationData.lang + ')'; + } + + const translationMenu = createThreeDotsMenu(isDarkTranslation, [ + { + icon: '↻', + label: browser.i18n.getMessage("translate_refresh") || 'Refresh translation', + hoverColor: isDarkTranslation ? '#4d9de0' : '#1a5fa8', + disableAfterClick: true, + onClick: () => { + browser.runtime.sendMessage({ + command: "refreshTranslation", + headerMessageId: translationData.headerMessageId + }); + } + }, + { + icon: '×', + label: browser.i18n.getMessage("translate_delete") || 'Delete translation', + hoverColor: '#cc0000', + onClick: () => { + translationContainer.remove(); + browser.runtime.sendMessage({ + command: "removeTranslation", + headerMessageId: translationData.headerMessageId + }); + } + } + ], { bg: bgColorTranslation, border: borderColorTranslation, text: textColorTranslation }); + + translationHeader.appendChild(translationIcon); + translationHeader.appendChild(translationTitleSpan); + translationHeader.appendChild(translationMenu); + translationContainer.appendChild(translationHeader); + + const translationText = document.createElement('div'); + translationText.style.cssText = 'white-space: pre-wrap; line-height: 1.5;'; + if (translationData.error) { + translationText.textContent = translationData.message || browser.i18n.getMessage("translate_error") || "Translation failed."; + } else { + translationText.textContent = translationData.translated_text || ''; + } + translationContainer.appendChild(translationText); + + const summaryBannerForTranslation = document.getElementById('mzta-summary-banner') || document.getElementById('mzta-summary-generating'); + const spamBannerForTranslation = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); + const insertAfterTranslation = summaryBannerForTranslation || spamBannerForTranslation; + if (insertAfterTranslation) { + document.body.insertBefore(translationContainer, insertAfterTranslation.nextSibling); + } else { + document.body.insertBefore(translationContainer, document.body.firstChild); + } + return Promise.resolve(true); + + case "showTranslationGenerating": + const existingTranslationGen = document.getElementById('mzta-translation-generating'); + if(existingTranslationGen) return Promise.resolve(true); + + const existingTranslationBannerGen = document.getElementById('mzta-translation-banner'); + if(existingTranslationBannerGen) existingTranslationBannerGen.remove(); + + const existingTranslationTrigWrap = document.getElementById('mzta-translation-trigger-wrapper'); + if(existingTranslationTrigWrap) existingTranslationTrigWrap.remove(); + const existingTranslationTrig = document.getElementById('mzta-translation-trigger'); + if(existingTranslationTrig) existingTranslationTrig.remove(); + + const isDarkTranslationGen = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + + let bgColorTranslationGen = isDarkTranslationGen ? '#1a2e2a' : '#e8f5e9'; + let textColorTranslationGen = isDarkTranslationGen ? '#c8e6c9' : '#1b5e20'; + let borderColorTranslationGen = isDarkTranslationGen ? '#2e5740' : '#a5d6a7'; + + const translationGenContainer = document.createElement('div'); + translationGenContainer.id = 'mzta-translation-generating'; + translationGenContainer.className = 'thunderai-translation-pane'; + translationGenContainer.style.cssText = `background-color: ${bgColorTranslationGen}; color: ${textColorTranslationGen}; padding: 0.5rem; margin-bottom: 1rem; border-radius: 4px; border: 1px solid ${borderColorTranslationGen}; font-family: system-ui, -apple-system, sans-serif; font-size: 14px; display: flex; align-items: center; gap: 10px;`; + + const translationGenIcon = document.createElement('img'); + translationGenIcon.src = browser.runtime.getURL("/images/ai_summary.png"); + translationGenIcon.style.cssText = `height: 16px; width: 16px; flex-shrink: 0;${isDarkTranslationGen ? ' filter: invert(1);' : ''}`; + + const translationGenLoadingImg = document.createElement('img'); + translationGenLoadingImg.src = browser.runtime.getURL("/images/loading.gif"); + translationGenLoadingImg.style.cssText = "height: 16px; width: 16px;"; + + const translationGenTitle = document.createElement('span'); + translationGenTitle.textContent = browser.i18n.getMessage("translate_generating") || "Translating..."; + translationGenTitle.style.cssText = `font-size: 14px;`; + + translationGenContainer.appendChild(translationGenIcon); + translationGenContainer.appendChild(translationGenLoadingImg); + translationGenContainer.appendChild(translationGenTitle); + + const summaryBannerForGen = document.getElementById('mzta-summary-banner') || document.getElementById('mzta-summary-generating'); + const spamBannerForGen = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); + const insertAfterGen = summaryBannerForGen || spamBannerForGen; + if (insertAfterGen) { + document.body.insertBefore(translationGenContainer, insertAfterGen.nextSibling); + } else { + document.body.insertBefore(translationGenContainer, document.body.firstChild); + } + return Promise.resolve(true); + + case "showTranslationButton": + const existingTranslationButton = document.getElementById('mzta-translation-trigger'); + if(existingTranslationButton) return Promise.resolve(true); + + const isDarkTranslationBtn = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + + let bgColorTranslationBtn = isDarkTranslationBtn ? '#1a2e2a' : '#e8f5e9'; + let textColorTranslationBtn = isDarkTranslationBtn ? '#c8e6c9' : '#1b5e20'; + let borderColorTranslationBtn = isDarkTranslationBtn ? '#2e5740' : '#a5d6a7'; + + const spamBannerTranslationTrigger = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); + const summaryBannerTranslationTrigger = document.getElementById('mzta-summary-banner') || document.getElementById('mzta-summary-trigger-wrapper') || document.getElementById('mzta-summary-trigger'); + const translationTriggerBtn = document.createElement('div'); + translationTriggerBtn.id = 'mzta-translation-trigger'; + translationTriggerBtn.title = browser.i18n.getMessage("translate_click_to_generate") || "Click to translate this email"; + const translationTriggerBtnBase = `background-color: ${bgColorTranslationBtn}; border: 1px solid ${borderColorTranslationBtn}; 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: ${textColorTranslationBtn}; display: inline-flex; align-items: center; gap: 6px; width: fit-content;`; + const insertAfterTranslationBtn = summaryBannerTranslationTrigger || spamBannerTranslationTrigger; + if (insertAfterTranslationBtn) { + translationTriggerBtn.style.cssText = translationTriggerBtnBase + ' margin-left: auto; margin-top: 4px;'; + } else { + translationTriggerBtn.style.cssText = translationTriggerBtnBase + ' position: fixed; top: 8px; right: 8px; z-index: 9997;'; + } + + const translationTriggerIcon = document.createElement('img'); + translationTriggerIcon.src = browser.runtime.getURL("/images/ai_summary.png"); + translationTriggerIcon.style.cssText = `height: 14px; width: 14px;${isDarkTranslationBtn ? ' filter: invert(1);' : ''}`; + translationTriggerBtn.appendChild(translationTriggerIcon); + + const translationTriggerLabel = document.createElement('span'); + translationTriggerLabel.textContent = browser.i18n.getMessage("get_ai_translation") || "Get AI Translation"; + translationTriggerBtn.appendChild(translationTriggerLabel); + translationTriggerBtn.onmouseover = () => { translationTriggerBtn.style.opacity = '1'; }; + translationTriggerBtn.onmouseout = () => { translationTriggerBtn.style.opacity = '0.7'; }; + translationTriggerBtn.onclick = async () => { + translationTriggerBtn.onclick = null; + translationTriggerBtn.style.cursor = 'default'; + translationTriggerBtn.style.opacity = '0.7'; + translationTriggerBtn.onmouseover = null; + translationTriggerBtn.onmouseout = null; + const wrapper = document.getElementById('mzta-translation-trigger-wrapper'); + if (wrapper) wrapper.remove(); else translationTriggerBtn.remove(); + browser.runtime.sendMessage({ + command: "triggerTranslationGeneration", + headerMessageId: message.headerMessageId + }); + }; + + if (insertAfterTranslationBtn) { + const translationTriggerWrapper = document.createElement('div'); + translationTriggerWrapper.id = 'mzta-translation-trigger-wrapper'; + translationTriggerWrapper.style.cssText = 'display: flex; justify-content: flex-end; padding: 4px 0.5rem;'; + translationTriggerWrapper.appendChild(translationTriggerBtn); + document.body.insertBefore(translationTriggerWrapper, insertAfterTranslationBtn.nextSibling); + } else { + document.body.appendChild(translationTriggerBtn); + } + return Promise.resolve(true); + default: // do nothing return Promise.resolve(false); @@ -1189,4 +1392,5 @@ switch (message.command) { }); browser.runtime.sendMessage({ command: "checkSpamReport" }); -browser.runtime.sendMessage({ command: "initSummary" }); \ No newline at end of file +browser.runtime.sendMessage({ command: "initSummary" }); +browser.runtime.sendMessage({ command: "initTranslation" }); \ No newline at end of file diff --git a/js/mzta-menus.js b/js/mzta-menus.js index eed540cb..1c86f99e 100644 --- a/js/mzta-menus.js +++ b/js/mzta-menus.js @@ -202,8 +202,8 @@ export class mzta_Menus { switch(curr_prompt.id){ case 'prompt_translate_this': - let prefs2 = await browser.storage.sync.get({default_chatgpt_lang: getLanguageDisplayName(browser.i18n.getUILanguage())}); - let chatgpt_lang2 = prefs2.default_chatgpt_lang; + let prefs2 = await browser.storage.sync.get({default_chatgpt_lang: getLanguageDisplayName(browser.i18n.getUILanguage()), translate_lang: ''}); + let chatgpt_lang2 = prefs2.translate_lang || prefs2.default_chatgpt_lang; if(chatgpt_lang2 === ''){ chatgpt_lang2 = getLanguageDisplayName(browser.i18n.getUILanguage()); } diff --git a/js/mzta-prompts.js b/js/mzta-prompts.js index d7603ff1..57b14678 100644 --- a/js/mzta-prompts.js +++ b/js/mzta-prompts.js @@ -235,24 +235,6 @@ const defaultPrompts = [ is_default: "1", is_special: "0", }, - { - id: 'prompt_translate_this', - name: "__MSG_prompt_translate_this__", - text: "prompt_translate_this_full_text", - type: "0", - action: "0", - need_selected: "0", - need_signature: "0", - need_custom_text: "0", - define_response_lang: "0", - use_diff_viewer: "0", - chatgpt_web_model: '', - chatgpt_web_project: '', - chatgpt_web_custom_gpt: '', - api_type: '', - is_default: "1", - is_special: "0", - }, { id: 'prompt_this', name: "__MSG_prompt_this__", @@ -396,6 +378,22 @@ const specialPrompts = [ api_model: '', is_default: "1", is_special: "1", + }, + { + id: 'prompt_translate_this', + name: "__MSG_prompt_translate_this__", + text: "prompt_translate_this_full_text", + type: "1", + action: "0", + need_selected: "0", + need_signature: "0", + need_custom_text: "0", + define_response_lang: "0", + use_diff_viewer: "0", + api_type: '', + api_model: '', + is_default: "1", + is_special: "1", } ]; diff --git a/js/mzta-storage.js b/js/mzta-storage.js index b5699618..9b29c6b5 100644 --- a/js/mzta-storage.js +++ b/js/mzta-storage.js @@ -278,7 +278,7 @@ export class taStorage { * @param {string} lang - Target language code. * @param {boolean} [force=true] - If true, overwrite existing translation data. */ - async writeTranslation(messageId, translated_text, lang, force = true) { + async writeTranslation(messageId, translated_text, lang, force = true, error = false, error_message = '') { this.taLog.log('[writeTranslation] messageId: ' + messageId + ', lang: ' + lang + ', force: ' + force); try { let key = this._buildKey(messageId); @@ -288,7 +288,7 @@ export class taStorage { return; } let now = Date.now(); - record[taStorage.FIELD_TRANSLATION] = { translated_text: translated_text, lang: lang, ts: now }; + record[taStorage.FIELD_TRANSLATION] = { translated_text: translated_text, lang: lang, error: error, message: error_message, ts: now }; record.ts = now; await messenger.storage.local.set({ [key]: record }); } catch (e) { @@ -296,6 +296,64 @@ export class taStorage { } } + /** + * Get all records that have a translation field. + * @returns {Promise} A map of messageId → translation data. + */ + async getAllTranslationRecords() { + this.taLog.log('[getAllTranslationRecords] loading all translation 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_TRANSLATION)) continue; + let messageId = key.slice(taStorage.STORAGE_KEY_PREFIX.length); + let translation = record[taStorage.FIELD_TRANSLATION]; + result[messageId] = { + headerMessageId: messageId, + translated_text: translation.translated_text, + lang: translation.lang || '', + error: translation.error || false, + message: translation.message || '', + translation_date: new Date(translation.ts), + }; + } + return result; + } catch (e) { + this.taLog.error('getAllTranslationRecords error: ' + e); + return {}; + } + } + + /** + * Delete only the translation field from a record. + * Deletes the entire record if no other data fields remain. + * @param {string} messageId - The Message-ID header string. + */ + async deleteTranslationField(messageId) { + this.taLog.log('[deleteTranslationField] messageId: ' + messageId); + try { + let key = this._buildKey(messageId); + let record = await this.getRecord(messageId); + if (!record || !(taStorage.FIELD_TRANSLATION in record)) { + this.taLog.log('[deleteTranslationField] no translation field found for messageId: ' + messageId); + return; + } + delete record[taStorage.FIELD_TRANSLATION]; + const remainingFields = Object.keys(record).filter(k => k !== 'v' && k !== 'ts'); + if (remainingFields.length === 0) { + this.taLog.log('[deleteTranslationField] no remaining fields, deleting entire record'); + await messenger.storage.local.remove(key); + } else { + this.taLog.log('[deleteTranslationField] remaining fields: ' + remainingFields.join(', ')); + await messenger.storage.local.set({ [key]: record }); + } + } catch (e) { + this.taLog.error('deleteTranslationField error: ' + e); + } + } + /** * Delete the entire record for a given Message-ID. * @param {string} messageId - The Message-ID header string. diff --git a/js/mzta-translationstore.js b/js/mzta-translationstore.js new file mode 100644 index 00000000..4f3dbe48 --- /dev/null +++ b/js/mzta-translationstore.js @@ -0,0 +1,150 @@ +/* + * 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 . + */ + +import { taStorage } from './mzta-storage.js'; +import { taLogger } from './mzta-logger.js'; + +export class taTranslationStore { + + _processing_prefix = 'mzta-translation-processing-'; + _max_translations = 100; + _storage = null; + taLog = null; + + constructor(do_debug = false) { + this._storage = new taStorage(do_debug); + this.taLog = new taLogger('mzta-translationstore', 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); + let result = output[key] || false; + this.taLog.log("[isProcessing] result: " + result); + return result; + } + + async saveTranslation(data, data_id) { + this.taLog.log("[saveTranslation] data_id: " + data_id); + try { + await this._storage.writeTranslation(data_id, data.translated_text || '', data.lang || '', true, data.error || false, data.message || ''); + await browser.storage.session.remove(this._processing_prefix + data_id); + } catch (e) { + this.taLog.error("[saveTranslation] error: " + e); + throw e; + } + } + + async saveError(data_id, error_message) { + this.taLog.log("[saveError] data_id: " + data_id + ", error_message: " + error_message); + let data = { + translated_text: '', + lang: '', + error: true, + message: error_message, + headerMessageId: data_id + }; + await this.saveTranslation(data, data_id); + return data; + } + + async loadTranslation(data_id) { + this.taLog.log("[loadTranslation] data_id: " + data_id); + let record = await this._storage.getRecord(data_id); + if (!record || !this._storage.hasField(record, taStorage.FIELD_TRANSLATION)) { + this.taLog.log("[loadTranslation] no record found for data_id: " + data_id); + return null; + } + let translation = record[taStorage.FIELD_TRANSLATION]; + return { + headerMessageId: data_id, + translated_text: translation.translated_text || '', + lang: translation.lang || '', + error: translation.error || false, + message: translation.message || '', + translation_date: new Date(translation.ts), + }; + } + + async removeTranslation(data_id) { + this.taLog.log("[removeTranslation] data_id: " + data_id); + await this._storage.deleteTranslationField(data_id); + await browser.storage.session.remove(this._processing_prefix + data_id); + } + + async getAllTranslations() { + this.taLog.log("[getAllTranslations] loading all translations"); + return await this._storage.getAllTranslationRecords(); + } + + async clearTranslations() { + this.taLog.log("[clearTranslations] clearing all translation data"); + let allTranslations = await this._storage.getAllTranslationRecords(); + let translationKeys = Object.keys(allTranslations); + this.taLog.log("[clearTranslations] deleting " + translationKeys.length + " translation records"); + for (let messageId of translationKeys) { + await this._storage.deleteTranslationField(messageId); + } + let allSession = await browser.storage.session.get(null); + let keysToDelete = Object.keys(allSession).filter(k => k.startsWith(this._processing_prefix)); + this.taLog.log("[clearTranslations] deleting " + keysToDelete.length + " session keys"); + for (let key of keysToDelete) { + await browser.storage.session.remove(key); + } + } + + async truncTranslations() { + this.taLog.log("[truncTranslations] checking translation count"); + let data = await this._storage.getAllTranslationRecords(); + let sortedData = this.sortTranslationsByDate(data); + let keys = Object.keys(sortedData); + this.taLog.log("[truncTranslations] total translations: " + keys.length + ", max: " + this._max_translations); + + if (keys.length > this._max_translations) { + let toDelete = keys.length - this._max_translations; + this.taLog.log("[truncTranslations] truncating " + toDelete + " oldest translations"); + for (let i = this._max_translations; i < keys.length; i++) { + await this._storage.deleteTranslationField(keys[i]); + } + } + } + + sortTranslationsByDate(data) { + if (!data) return {}; + const translationKeys = Object.keys(data); + translationKeys.sort((a, b) => { + const dateA = new Date(data[a].translation_date); + const dateB = new Date(data[b].translation_date); + return dateB - dateA; + }); + + let sortedTranslations = {}; + translationKeys.forEach((key) => { + sortedTranslations[key] = data[key]; + }); + + return sortedTranslations; + } +} diff --git a/js/mzta-utils-prompt.js b/js/mzta-utils-prompt.js index c81588c2..b123ff29 100644 --- a/js/mzta-utils-prompt.js +++ b/js/mzta-utils-prompt.js @@ -173,6 +173,26 @@ export const taPromptUtils = { return { promptText, promptInfo: prompt }; }, + async buildTranslationPrompt(fullMessage, lang) { + const specialPrompts = await getSpecialPrompts(); + const prompt = specialPrompts.find(p => p.id === 'prompt_translate_this'); + + let promptText = prompt.text; + if (promptText === 'prompt_translate_this_full_text') { + promptText = browser.i18n.getMessage('prompt_translate_this_full_text'); + } + + const bodyHtml = getMailBody(fullMessage); + let bodyText = htmlBodyToPlainText(bodyHtml.html); + if (bodyText.length === 0) { + bodyText = bodyHtml.text || ''; + } + + const fullPrompt = promptText + " " + lang + ". \"" + bodyText + "\""; + + return { promptText: fullPrompt, 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 29ec2828..88d0dae4 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -62,6 +62,7 @@ import { } from './js/mzta-prompts.js'; import { taSpamReport } from './js/mzta-spamreport.js'; import { taSummaryStore } from './js/mzta-summarystore.js'; +import { taTranslationStore } from './js/mzta-translationstore.js'; import { taWorkingStatus } from './js/mzta-working-status.js'; import { addTags_getExclusionList, @@ -95,6 +96,7 @@ 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 translationStore = new taTranslationStore(prefs_init.do_debug); let special_prompts_ids = getActiveSpecialPromptsIDs({ addtags: prefs_init.add_tags, @@ -328,6 +330,61 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { // case 'chatgpt_open': // openChatGPT(message.prompt,message.action,message.tabId); // return true; + case 'initTranslation': + async function _initTranslation() { + try { + let tabId = sender.tab.id; + let prefs = await browser.storage.sync.get({ translate: prefs_default.translate, translate_auto: prefs_default.translate_auto }); + + if (!prefs.translate) return; + + let message = await browser.messageDisplay.getDisplayedMessage(tabId); + if (!message) return; + + let cachedTranslation = await translationStore.loadTranslation(message.headerMessageId); + if (cachedTranslation && !cachedTranslation.error) { + browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { ...cachedTranslation } }); + return; + } + + if (await translationStore.isProcessing(message.headerMessageId)) { + browser.tabs.sendMessage(tabId, { command: "showTranslationGenerating" }); + return; + } + + if (prefs.translate_auto === 0) return; + + if (prefs.translate_auto === 2) { + _generateTranslationForMessage(message.headerMessageId, tabId); + return; + } + + // Manual button mode (translate_auto === 1) + browser.tabs.sendMessage(tabId, { command: "showTranslationButton", headerMessageId: message.headerMessageId }); + } catch (e) { + taLog.error("Error in initTranslation: " + e); + } + } + _initTranslation(); + break; + case 'triggerTranslationGeneration': + async function _triggerTranslationGeneration(message) { + let tabId = sender.tab.id; + await _generateTranslationForMessage(message.headerMessageId, tabId); + } + _triggerTranslationGeneration(message); + break; + case 'refreshTranslation': + async function _refreshTranslation(message) { + let tabId = sender.tab.id; + await translationStore.removeTranslation(message.headerMessageId); + await _generateTranslationForMessage(message.headerMessageId, tabId); + } + _refreshTranslation(message); + break; + case 'removeTranslation': + translationStore.removeTranslation(message.headerMessageId); + break; case 'chatgpt_close': async function _closeChatGptWindow(window_id) { let prefs_close = await browser.storage.sync.get({chatgpt_win_save_position: prefs_default.chatgpt_win_save_position}); @@ -583,6 +640,81 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { } } +async function _generateTranslationForMessage(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, + translate_lang: prefs_default.translate_lang, + ...getDynamicSettingsDefaults(['use_specific_integration', 'connection_type']) + }); + + let cachedTranslation = await translationStore.loadTranslation(headerMessageId); + if (cachedTranslation && !cachedTranslation.error) { + browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { ...cachedTranslation } }); + return; + } + + if (await translationStore.isProcessing(headerMessageId)) { + browser.tabs.sendMessage(tabId, { command: "showTranslationGenerating" }); + return; + } + + await translationStore.setProcessing(headerMessageId); + taWorkingStatus.startWorking(); + browser.tabs.sendMessage(tabId, { command: "showTranslationGenerating" }); + + const messageResult = await browser.messages.query({ headerMessageId: headerMessageId }); + if (!messageResult || messageResult.messages.length === 0) { + await translationStore.saveError(headerMessageId, "Message not found"); + browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { error: true, message: "Message not found" } }); + taWorkingStatus.stopWorking(); + return; + } + + const fullMessage = await browser.messages.getFull(messageResult.messages[0].id); + + const connectionType = getConnectionType(prefs, {}, 'translate'); + + if (connectionType === 'chatgpt_web') { + const errorMsg = browser.i18n.getMessage('translate_chatgpt_web_not_supported'); + await translationStore.saveError(headerMessageId, errorMsg); + browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { error: true, message: errorMsg } }); + taWorkingStatus.stopWorking(); + return; + } + + const lang = prefs.translate_lang || prefs.default_chatgpt_lang || ''; + const { promptText } = await taPromptUtils.buildTranslationPrompt(fullMessage, lang); + + const cmd = new mzta_specialCommand({ + prompt: promptText, + llm: connectionType, + do_debug: prefs.do_debug, + config: {} + }); + + await cmd.initWorker(); + const aiResponse = await cmd.sendPrompt(); + + const translationData = { + translated_text: aiResponse, + lang: lang, + headerMessageId: headerMessageId + }; + await translationStore.saveTranslation(translationData, headerMessageId); + browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { ...translationData } }); + taWorkingStatus.stopWorking(); + + } catch (error) { + console.error("[ThunderAI] Error generating translation:", error); + await translationStore.saveError(headerMessageId, error.message || String(error)); + browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { error: true, message: error.message || "Failed to generate translation" } }); + taWorkingStatus.stopWorking(); + } +} + // 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) diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index c7cbfcd8..a9bbb91e 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -const special_prompts_with_integration = ['add_tags', 'spamfilter', 'summarize', 'get_calendar_event', 'get_task']; +const special_prompts_with_integration = ['add_tags', 'spamfilter', 'summarize', 'get_calendar_event', 'get_task', 'translate']; export const integration_options_config = { chatgpt: { @@ -141,6 +141,9 @@ export const prefs_default = { 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 + translate: true, + translate_auto: 0, // 0: disabled, 1: manual button, 2: automatic + translate_lang: '', // target language, fallback on default_chatgpt_lang spamfilter_show_msg_panel: true, ...generated_prefs } diff --git a/options/mzta-options.html b/options/mzta-options.html index 0033a590..aa2231e5 100644 --- a/options/mzta-options.html +++ b/options/mzta-options.html @@ -203,6 +203,17 @@ + + __MSG_prefs_OptionText_translate__ +
      + + + + __MSG_prefs_OptionText_get_calendar_event__
      diff --git a/options/mzta-options.js b/options/mzta-options.js index 51baa538..9231eaf9 100644 --- a/options/mzta-options.js +++ b/options/mzta-options.js @@ -195,6 +195,27 @@ function disable_Summarize(prefs_opt){ } } +function disable_Translate(prefs_opt){ + let translate = document.getElementById('translate'); + let conntype_select = document.getElementById("connection_type"); + const tempPrefs = { + connection_type: conntype_select.value, + ...prefs_opt + }; + let translate_disabled = (getConnectionType(tempPrefs, null, 'translate') === "chatgpt_web"); + let translate_checked_original = translate.checked; + translate.checked = translate_disabled ? false : translate.checked; + if(!translate.checked){ + let translate_info_btn = document.getElementById('btnManageTranslateInfo'); + translate_info_btn.disabled = 'disabled'; + } + let translate_warn_API_needed = document.getElementById('translate_warn_API_needed'); + translate_warn_API_needed.style.display = (translate_disabled) ? 'inline-block' : 'none'; + if(translate_checked_original != translate.checked){ + browser.storage.sync.set({translate: translate.checked}); + } +} + async function disable_GetCalendarEvent(){ let get_calendar_event = document.getElementById('get_calendar_event'); let get_task = document.getElementById('get_task'); @@ -300,6 +321,13 @@ document.addEventListener('DOMContentLoaded', async () => { }); summarize_info_btn.disabled = summarize_el.checked ? '' : 'disabled'; + let translate_el = document.getElementById('translate'); + let translate_info_btn = document.getElementById('btnManageTranslateInfo'); + translate_el.addEventListener('click', (event) => { + translate_info_btn.disabled = event.target.checked ? '' : 'disabled'; + }); + translate_info_btn.disabled = translate_el.checked ? '' : 'disabled'; + let get_calendar_event_el = document.getElementById('get_calendar_event'); let get_calendar_event_info_btn = document.getElementById('btnManageCalendarEventInfo'); get_calendar_event_el.addEventListener('click', (event) => { @@ -334,6 +362,10 @@ document.addEventListener('DOMContentLoaded', async () => { openTab('/pages/summarize/mzta-summarize.html'); }); + document.getElementById('btnManageTranslateInfo').addEventListener('click', () => { + openTab('/pages/translate/mzta-translate.html'); + }); + document.getElementById('btnManageCalendarEventInfo').addEventListener('click', () => { openTab('/pages/get-calendar-event/mzta-get-calendar-event.html'); }); @@ -360,13 +392,15 @@ document.addEventListener('DOMContentLoaded', async () => { conntype_select.addEventListener("change", () => disable_AddTags(prefs_opt)); conntype_select.addEventListener("change", () => disable_SpamFilter(prefs_opt)); conntype_select.addEventListener("change", () => disable_Summarize(prefs_opt)); + conntype_select.addEventListener("change", () => disable_Translate(prefs_opt)); conntype_select.addEventListener("change", disable_GetCalendarEvent); - + showConnectionOptions(conntype_select); disable_MaxPromptLength(); disable_AddTags(prefs_opt); disable_SpamFilter(prefs_opt); disable_Summarize(prefs_opt); + disable_Translate(prefs_opt); disable_GetCalendarEvent(); document.getElementById('reset_max_prompt_length').addEventListener('click', resetMaxPromptLength); diff --git a/pages/translate/mzta-translate.css b/pages/translate/mzta-translate.css new file mode 100644 index 00000000..8e8fbd16 --- /dev/null +++ b/pages/translate/mzta-translate.css @@ -0,0 +1,180 @@ +#translate_prompt_container { + width: 90%; + margin: 20px auto; +} + +#translate_prompt_text { + width: 100%; +} + +.infoline { + font-size: 0.8em; + font-style: italic; +} + +#account_selector_container{ + display: none; +} + +#account_selector_checkboxes{ + padding: 10px; + border: 1px solid gray; + width: 24em; + margin-bottom: 10px; + margin-top: 10px; +} + +.specific_integration{ + background-color: #dfeaff; +} + +table { + border-collapse: separate; + border-spacing: 0; +} + +.group td { + border-top: none; + border-bottom: none; +} + +.specific_integration_sub td:first-child { + border-left: 10px solid #dfeaff; +} + +.specific_integration_sub td:last-child { + border-right: 10px solid #dfeaff; +} + +#connection_ui_end td{ + height: 6px; + background-color: #dfeaff; +} + +#connection_ui_end{ + display: none; +} + +.btn_div { + width: 100%; + display: flex; + justify-content: space-between; +} + +table#miczPrefs { + padding: 10px; + border-spacing: 0px; + width: 90%; + margin: 0px auto 40px auto; + border: 1px solid #ccc; +} + +.translate_table_title{ + width: 90%; + margin: auto; +} + +.section_title { + font-weight: bold; +} + +table#miczPrefs td { + border-top: 1px solid #ccc; + border-bottom: 1px solid #ccc; + vertical-align: top; + padding: 2px; +} + +table#miczPrefs tr:first-child td { + border-top: none; +} + +table#miczPrefs tr:last-child td { + border-bottom: none; +} + +.autocomplete-container { + position: relative; +} + +.autocomplete-list { + position: absolute; + top: 100%; + left: 0; + right: 0; + background-color: white; + border: 1px solid #ccc; + z-index: 1000; + max-height: 200px; + overflow-y: auto; + padding: 0; + margin: 0; + list-style: none; + font-size: small; +} + +.autocomplete-list li { + padding: 8px; + cursor: pointer; +} + +.autocomplete-list li:hover { + background-color: #f0f0f0; +} + +.autocomplete-list li.active { + background-color: #ddd; +} + +.hidden { + display: none; +} + +.unsaved { + color: red; +} + +label:has(input[type="checkbox"]) { + cursor: pointer; +} + +@media (prefers-color-scheme: dark) { + body { + background-color: #1c1b22; + color: rgb(251, 251, 254); + } + + a:link { color: #409eff; } + a:visited { color: #409eff; } + a:hover { color: #66b1ff; } + a:active { color: #66b1ff; } + + .autocomplete-list { + background-color: #2e2f36; + border: 1px solid #2e2f36; + } + + .autocomplete-list li:hover { + background-color: #4c4e58; + } + + .autocomplete-list li.active { + background-color: #4c4e58; + } + + .specific_integration{ + background-color: #2E3A4F; + } + + .specific_integration_sub td:first-child { + border-left: 10px solid #2E3A4F; + } + + .specific_integration_sub td:last-child { + border-right: 10px solid #2E3A4F; + } + + #connection_ui_end td{ + background-color: #2E3A4F; + } +} diff --git a/pages/translate/mzta-translate.html b/pages/translate/mzta-translate.html new file mode 100644 index 00000000..d1a0d187 --- /dev/null +++ b/pages/translate/mzta-translate.html @@ -0,0 +1,97 @@ + + + + + ThunderAI - __MSG_Translate_PageTitle__ + + + + + +
      +

      __MSG_Translate_PageTitle__

      +

      __MSG_Translate_info_default__

      +
      +
      __MSG_Translate_prompt_prefs_title__
      + + + + + + + + + + + + + + + +
      __MSG_prefs_OptionText_use_specific_integration__ + + +
      __MSG_prefs_OptionText_translate_auto__ + +
      __MSG_prefs_OptionText_translate_lang__ + +
      + + +
      + + + __MSG_Translate_prompt_text_title__ + + +
      + + + __MSG_prefs_OptionText_btnManagePrompts_infoline__ + + __MSG_more_info_string__ + + + +
      + + + + __MSG_prefs_OptionText_Translate_main_prompt__ + + +
      + +
      + + +
      + +
      +
      + + +
      + +
      + + + + diff --git a/pages/translate/mzta-translate.js b/pages/translate/mzta-translate.js new file mode 100644 index 00000000..2c1125a9 --- /dev/null +++ b/pages/translate/mzta-translate.js @@ -0,0 +1,232 @@ +/* + * 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 . + */ + +import { + prefs_default, + integration_options_config +} from '../../options/mzta-options-default.js'; +import { taLogger } from "../../js/mzta-logger.js"; +import { + getSpecialPrompts, + setSpecialPrompts +} from "../../js/mzta-prompts.js"; +import { + getPlaceholders, + mapPlaceholderToSuggestion +} from "../../js/mzta-placeholders.js"; +import { textareaAutocomplete } from "../../js/mzta-placeholders-autocomplete.js"; +import { + normalizeStringList, + isAPIKeyValue, + setTomSelectBorder +} from "../../js/mzta-utils.js"; +import { + initializeSpecificIntegrationUI +} from "../_lib/connection-ui.js"; + +let autocompleteSuggestions = []; +let taLog = new taLogger("mzta-translate-page", true); + +document.addEventListener("DOMContentLoaded", async () => { + + let specialPrompts = await getSpecialPrompts(); + let translate_prompt = specialPrompts.find((prompt) => prompt.id === 'prompt_translate_this'); + + if (translate_prompt && translate_prompt.api_type && translate_prompt.api_type !== '') { + let update_prefs = {}; + update_prefs['translate_connection_type'] = translate_prompt.api_type; + + let integration = translate_prompt.api_type.replace('_api', ''); + if (integration_options_config && integration_options_config[integration]) { + for (const key of Object.keys(integration_options_config[integration])) { + if (translate_prompt[key] !== undefined) { + update_prefs[`translate_${integration}_${key}`] = translate_prompt[key]; + } + } + } + await browser.storage.sync.set(update_prefs); + } + + await initializeSpecificIntegrationUI({ + prefix: 'translate', + promptId: 'prompt_translate_this', + taLog: taLog, + restoreOptionsCallback: restoreOptions + }); + + i18n.updateDocument(); + + document.querySelectorAll(".option-input").forEach(element => { + element.addEventListener("change", saveOptions); + }); + + let translate_textarea = document.getElementById("translate_prompt_text"); + let translate_save_btn = document.getElementById("btn_save_prompt"); + let translate_reset_btn = document.getElementById("btn_reset_prompt"); + + // on changing textarea + translate_textarea.addEventListener("input", (event) => { + translate_reset_btn.disabled = (event.target.value === browser.i18n.getMessage('prompt_translate_this_full_text')); + translate_save_btn.disabled = (event.target.value === translate_prompt.text); + }); + + // on clicking reset button + translate_reset_btn.addEventListener("click", () => { + translate_textarea.value = browser.i18n.getMessage("prompt_translate_this_full_text"); + translate_reset_btn.disabled = true; + let event = new Event("input", { bubbles: true, cancelable: true }); + translate_textarea.dispatchEvent(event); + }); + + // on clicking save button + translate_save_btn.addEventListener("click", () => { + specialPrompts.find(prompt => prompt.id === 'prompt_translate_this').text = translate_textarea.value; + setSpecialPrompts(specialPrompts); + translate_save_btn.disabled = true; + browser.runtime.sendMessage({ command: "reload_menus" }); + }); + + if(translate_prompt.text === 'prompt_translate_this_full_text'){ + translate_prompt.text = browser.i18n.getMessage(translate_prompt.text); + } + + translate_textarea.value = translate_prompt.text; + translate_reset_btn.disabled = (translate_textarea.value === browser.i18n.getMessage("prompt_translate_this_full_text")); + + autocompleteSuggestions = (await getPlaceholders(true)) + .filter((p) => p.id !== "additional_text") + .map(mapPlaceholderToSuggestion); + + textareaAutocomplete(translate_textarea, autocompleteSuggestions, 1); + +}); + +// Methods to manage options, derived from: /options/mzta-options.js + +function saveOptions(e) { + e.preventDefault(); + let options = {}; + let element = e.target; + switch (element.type) { + case 'checkbox': + options[element.id] = element.checked; + break; + case 'number': + options[element.id] = element.valueAsNumber; + break; + case 'text': + case 'password': + options[element.id] = element.value.trim(); + break; + case 'select-one': + if (element.id === 'translate_auto') { + options[element.id] = parseInt(element.value, 10); + } else { + options[element.id] = element.value; + } + break; + case 'textarea': + options[element.id] = normalizeStringList(element.value); + break; + default: + console.error("[ThunderAI] Unhandled input type:", element.type); + } + + browser.storage.sync.set(options); +} + +async function restoreOptions() { + function setCurrentChoice(result) { + document.querySelectorAll(".option-input").forEach(element => { + if(!element.id) return; + taLog.log("Options restoring " + element.id + " = " + (isAPIKeyValue(element.id) ? "****************" : result[element.id])); + switch (element.type) { + case 'checkbox': + element.checked = result[element.id] || false; + break; + case 'number': + element.value = result[element.id] ?? 0; + break; + case 'text': + case 'textarea': + case 'password': + let default_text_value = ''; + if(element.id == 'translate_lang') default_text_value = prefs_default.default_chatgpt_lang; + element.value = result[element.id] || default_text_value; + break; + default: + if (element.tagName === 'SELECT') { + let default_select_value = 0; + if (element.id === 'translate_auto') { + default_select_value = prefs_default.translate_auto; + } + const restoreValue = result[element.id] ?? default_select_value; + let optionExists = Array.from(element.options).some(opt => opt.value === String(restoreValue)); + if (element.tomselect) { + 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); + } + } + }); + } + + let getting = await browser.storage.sync.get(prefs_default); + + let specialPrompts = await getSpecialPrompts(); + let translate_prompt = specialPrompts.find(prompt => prompt.id === 'prompt_translate_this'); + + if (translate_prompt) { + if (translate_prompt.api_type && translate_prompt.api_type !== '') { + getting['translate_connection_type'] = translate_prompt.api_type; + } else { + getting['translate_connection_type'] = getting['connection_type']; + } + for (const [integration, options] of Object.entries(integration_options_config)) { + for (const key of Object.keys(options)) { + const propName = `${integration}_${key}`; + if (translate_prompt[propName] !== undefined && translate_prompt[propName] !== '') { + getting[`translate_${propName}`] = translate_prompt[propName]; + } else { + getting[`translate_${propName}`] = getting[propName]; + } + } + } + } + + // If translate_lang is empty, show default_chatgpt_lang as placeholder/default + if (!getting['translate_lang']) { + getting['translate_lang'] = ''; + } + + setCurrentChoice(getting); +} From ef7483031d9a71fdc7521c0f4c38a37f948f3acb Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 28 Mar 2026 00:21:45 +0100 Subject: [PATCH 071/269] translate_display_mode and translate_max_display_length added. see #247 --- _locales/en/messages.json | 62 +++++++++++------- api_webchat/messagesArea.js | 25 +++++++- claude-spec/01-architecture.md | 16 +++-- claude-spec/02-prompts.md | 8 ++- claude-spec/05-options.md | 13 ++-- js/mzta-compose-script.js | 60 +++++++++++++++++- mzta-background.js | 97 +++++++++++++++++++++++++++-- options/mzta-options-default.js | 2 + pages/summarize/mzta-summarize.html | 10 +-- pages/translate/mzta-translate.html | 27 +++++++- pages/translate/mzta-translate.js | 3 + 11 files changed, 270 insertions(+), 53 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 49c64d6d..6c81b13b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -239,6 +239,10 @@ "message": "Save as Summary", "description": "Button label in the webchat window to save the AI response as a message summary" }, + "webchat_save_as_translation": { + "message": "Save as Translation", + "description": "Button label in the webchat window to save the AI response as a message translation" + }, "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": "" @@ -1957,18 +1961,6 @@ "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": "" @@ -1977,14 +1969,6 @@ "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": "" @@ -2085,15 +2069,15 @@ "message": "Auto-translate messages", "description": "" }, - "prefs_OptionText_translate_auto_disabled": { + "prefs_OptionText_action_auto_disabled": { "message": "Disabled", "description": "" }, - "prefs_OptionText_translate_auto_manual": { + "prefs_OptionText_action_auto_manual": { "message": "Manual button only", "description": "" }, - "prefs_OptionText_translate_auto_automatic": { + "prefs_OptionText_action_auto_automatic": { "message": "Automatic", "description": "" }, @@ -2101,6 +2085,38 @@ "message": "Choose when to translate messages: disabled, only when clicking the button, or automatically when opening a message.", "description": "" }, + "prefs_OptionText_translate_display_mode": { + "message": "Display mode for translations", + "description": "" + }, + "prefs_OptionText_display_mode_inline": { + "message": "Message pane (inline)", + "description": "" + }, + "prefs_OptionText_display_mode_webchat": { + "message": "Chat window", + "description": "" + }, + "prefs_OptionText_translate_display_mode_Info": { + "message": "Choose where to display translations. Note: automatic mode always uses inline display.", + "description": "" + }, + "prefs_OptionText_translate_max_display_length": { + "message": "Maximum length of displayed translation", + "description": "" + }, + "prefs_OptionText_translate_max_display_length_Info": { + "message": "Maximum number of characters shown in the inline translation. 0 = no limit. When set, longer text is truncated with a \"See more\" toggle.", + "description": "" + }, + "translate_see_more": { + "message": "See more", + "description": "" + }, + "translate_see_less": { + "message": "See less", + "description": "" + }, "prefs_OptionText_translate_lang": { "message": "Translation target language", "description": "" diff --git a/api_webchat/messagesArea.js b/api_webchat/messagesArea.js index 6c01ddca..62fffa42 100644 --- a/api_webchat/messagesArea.js +++ b/api_webchat/messagesArea.js @@ -459,7 +459,7 @@ class MessagesArea extends HTMLElement { } // Save as Summary button (only shown for summary webchat sessions) - if(promptData.prompt_info?.headerMessageId) { + if(promptData.prompt_info?.headerMessageId && promptData.prompt_info?.summaryTabId) { const saveSummaryButton = document.createElement('button'); saveSummaryButton.textContent = browser.i18n.getMessage("webchat_save_as_summary"); saveSummaryButton.classList.add('action_btn'); @@ -481,6 +481,29 @@ class MessagesArea extends HTMLElement { selectionInfo.style.display = "block"; } + // Save as Translation button (only shown for translation webchat sessions) + if(promptData.prompt_info?.headerMessageId && promptData.prompt_info?.translationTabId) { + const saveTranslationButton = document.createElement('button'); + saveTranslationButton.textContent = browser.i18n.getMessage("webchat_save_as_translation"); + saveTranslationButton.classList.add('action_btn'); + saveTranslationButton.addEventListener('click', async () => { + let finalText = removeAloneBRs(fullTextHTMLAtAssignment); + const selectedHTML = this.getCurrentSelectionHTML(); + if(selectedHTML != "") { + finalText = removeAloneBRs(selectedHTML); + } + await browser.runtime.sendMessage({ + command: "chatgpt_saveTranslation", + text: finalText, + headerMessageId: promptData.prompt_info.headerMessageId, + tabId: promptData.prompt_info.translationTabId || promptData.tabId, + }); + browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id}); + }); + actionButtons.appendChild(saveTranslationButton); + selectionInfo.style.display = "block"; + } + // diff viewer button if(promptData.prompt_info?.use_diff_viewer == "1") { const diffvButton = document.createElement('button'); diff --git a/claude-spec/01-architecture.md b/claude-spec/01-architecture.md index a679df6f..652c8fcf 100644 --- a/claude-spec/01-architecture.md +++ b/claude-spec/01-architecture.md @@ -80,11 +80,13 @@ mzta-background.js (checks summarize_auto + summarize_display_mode prefs) ### Data Flow: Inline Translation on Message Display -The `translate_auto` preference controls when translation is triggered. Unlike summarize, translation has no `display_mode` option (always inline). +The `translate_display_mode` preference (`'inline'` or `'webchat'`) controls where +the translation is displayed. The `translate_auto` preference controls when it is triggered. -- `translate_auto = 0` (disabled) → do nothing -- `translate_auto = 1` (manual button) → show "Get AI Translation" button in message body -- `translate_auto = 2` (automatic) → generate translation immediately on message open +- `translate_auto = 2` (automatic) always generates inline, regardless of `translate_display_mode`. +- `translate_auto = 1` (manual button) respects `translate_display_mode`: + - `'inline'` → button click triggers inline generation + - `'webchat'` → button click opens the AI chat window via `_openTranslationWebchat()` The target language is determined by `translate_lang` (fallback on `default_chatgpt_lang`). @@ -93,14 +95,16 @@ User opens/selects a message in Thunderbird ↓ mzta-compose-script.js (sends "initTranslation" to background) ↓ -mzta-background.js (checks translate + translate_auto prefs) +mzta-background.js (checks translate + translate_auto + translate_display_mode prefs) ↓ ┌──────────────────────────────────────────────────────────┐ │ translate_auto = 0 → do nothing │ │ translate_auto = 1 → show "click to translate" button │ + │ display_mode = inline → click triggers inline gen │ + │ display_mode = webchat → click opens chat window │ │ translate_auto = 2 → generate immediately (always inline)│ └──────────────────────────────────────────────────────────┘ - ↓ (if generating) + ↓ (if generating inline) taTranslationStore (check cache / set processing) ↓ (cache miss) mzta-special-commands (via Web Worker, NOT chatgpt_web) diff --git a/claude-spec/02-prompts.md b/claude-spec/02-prompts.md index 77b271c2..c2c3eaae 100644 --- a/claude-spec/02-prompts.md +++ b/claude-spec/02-prompts.md @@ -88,14 +88,16 @@ The summarize feature uses two distinct prompt pathways: ### Translate: Inline-Only Prompt System -The translate feature uses a single special prompt (`prompt_translate_this`) for inline translation in the message body. Unlike summarize, it has no context menu entry and no webchat mode. +The translate feature uses a single special prompt (`prompt_translate_this`) for translating emails. It supports both inline display and webchat mode, but has no context menu entry. -**Inline Translation on Message Display** (controlled by `translate_auto` pref): +**Inline Translation on Message Display** (controlled by `translate_auto` and `translate_display_mode` prefs): - Uses a single special prompt: `prompt_translate_this` - The prompt text is appended with the target language and the email body: `prompt_text + " " + lang + ". \"" + body_text + "\""` - Target language is determined by `translate_lang` pref, falling back to `default_chatgpt_lang` - Does **not** support `chatgpt_web` connection type (shows error if configured) -- Result is rendered as a styled banner (green/teal theme) in the message body via `mzta-compose-script.js` +- `translate_display_mode = 'inline'`: result is rendered as a styled banner (green/teal theme) in the message body via `mzta-compose-script.js` +- `translate_display_mode = 'webchat'`: opens AI chat window; webchat shows a "Save as Translation" button to persist the result inline +- `translate_auto = 2` (automatic) always generates inline regardless of `translate_display_mode` - Banner includes refresh (↻) and delete (×) buttons - Cached per-message via `taTranslationStore` / `taStorage` (max 100 entries) - The prompt was originally a regular prompt (`defaultPrompts`) and was moved to `specialPrompts` with `is_special: "1"` and `type: "1"` (reading email only) diff --git a/claude-spec/05-options.md b/claude-spec/05-options.md index 7dbe7217..e1af7369 100644 --- a/claude-spec/05-options.md +++ b/claude-spec/05-options.md @@ -100,6 +100,8 @@ These are generated programmatically at the bottom of `mzta-options-default.js` | `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. | | `translate` | `true` | Enable email translation | | `translate_auto` | `0` | Auto-translate mode: `0` = disabled, `1` = manual (show button), `2` = automatic (translate on message open) | +| `translate_display_mode` | `'inline'` | Where to display translations: `'inline'` = message pane banner, `'webchat'` = AI chat window. Note: `translate_auto = 2` always uses inline regardless of this setting. | +| `translate_max_display_length` | `0` | Maximum characters shown in inline translation 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. | | `translate_lang` | `''` | Target language for translation. Falls back to `default_chatgpt_lang` if empty. | ### Summarize Settings Page (`pages/summarize/`) @@ -132,10 +134,13 @@ The translate settings page provides: - `0` (Disabled) — no inline translations - `1` (Manual) — shows a "Get AI Translation" button in message display - `2` (Automatic) — generates translation immediately when message is opened -3. **Target language** (`translate_lang`) — text input for the destination language. If empty, falls back to `default_chatgpt_lang`. -4. **One editable prompt** — the translation instruction prompt (`prompt_translate_this`) with Save/Reset buttons and placeholder autocomplete. Default text comes from i18n string `prompt_translate_this_full_text`. - -Unlike summarize, translation has no `display_mode` option (always inline) and no max display length setting. +3. **Display mode dropdown** (`translate_display_mode`) — controls where translations are shown: + - `'inline'` — translation banner in the message pane (default) + - `'webchat'` — opens the AI chat window with a "Save as Translation" button + - Note: `translate_auto = 2` always generates inline regardless of this setting. +4. **Max display length** (`translate_max_display_length`) — number input, limits inline translation text to N characters. `0` = no limit. When truncated, a "See more"/"See less" toggle link is appended. +5. **Target language** (`translate_lang`) — text input for the destination language. If empty, falls back to `default_chatgpt_lang`. +6. **One editable prompt** — the translation instruction prompt (`prompt_translate_this`) with Save/Reset buttons and placeholder autocomplete. Default text comes from i18n string `prompt_translate_this_full_text`. ## Adding a New Preference diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 3fc11537..54a73a87 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -1258,6 +1258,9 @@ switch (message.command) { translationHeader.appendChild(translationMenu); translationContainer.appendChild(translationHeader); + const translationTextWrapper = document.createElement('div'); + translationTextWrapper.style.cssText = 'flex-grow: 1;'; + const translationText = document.createElement('div'); translationText.style.cssText = 'white-space: pre-wrap; line-height: 1.5;'; if (translationData.error) { @@ -1265,7 +1268,60 @@ switch (message.command) { } else { translationText.textContent = translationData.translated_text || ''; } - translationContainer.appendChild(translationText); + translationTextWrapper.appendChild(translationText); + + // Expand/collapse for long translations + const maxLenTranslation = translationData.maxDisplayLength || 0; + const fullTranslationText = translationData.translated_text || ''; + if (!translationData.error && maxLenTranslation > 0 && fullTranslationText.length > maxLenTranslation) { + translationText.style.overflow = 'hidden'; + translationText.style.transition = 'max-height 0.2s ease'; + + let cutPos = fullTranslationText.lastIndexOf(' ', maxLenTranslation); + if (cutPos <= 0) cutPos = maxLenTranslation; + const truncatedTranslation = fullTranslationText.substring(0, cutPos) + '\u2026'; + translationText.textContent = truncatedTranslation; + + requestAnimationFrame(() => { + const collapsedHeight = translationText.scrollHeight; + translationText.style.maxHeight = collapsedHeight + 'px'; + }); + + const toggleLinkTranslation = document.createElement('a'); + toggleLinkTranslation.textContent = browser.i18n.getMessage("translate_see_more") || "See more"; + toggleLinkTranslation.href = '#'; + toggleLinkTranslation.style.cssText = 'display: inline-block; margin-top: 4px; font-size: 13px; color: ' + + (isDarkTranslation ? '#6db3f2' : '#1a5fa8') + '; cursor: pointer; text-decoration: underline;'; + + let expandedTranslation = false; + toggleLinkTranslation.addEventListener('click', (e) => { + e.preventDefault(); + if (!expandedTranslation) { + translationText.textContent = fullTranslationText; + const fullHeight = translationText.scrollHeight; + translationText.style.maxHeight = fullHeight + 'px'; + toggleLinkTranslation.textContent = browser.i18n.getMessage("translate_see_less") || "See less"; + } else { + translationText.textContent = truncatedTranslation; + const collapsedHeight = translationText.scrollHeight; + translationText.textContent = fullTranslationText; + translationText.style.maxHeight = translationText.scrollHeight + 'px'; + requestAnimationFrame(() => { + translationText.style.maxHeight = collapsedHeight + 'px'; + }); + translationText.addEventListener('transitionend', function handler() { + translationText.removeEventListener('transitionend', handler); + translationText.textContent = truncatedTranslation; + }); + toggleLinkTranslation.textContent = browser.i18n.getMessage("translate_see_more") || "See more"; + } + expandedTranslation = !expandedTranslation; + }); + + translationTextWrapper.appendChild(toggleLinkTranslation); + } + + translationContainer.appendChild(translationTextWrapper); const summaryBannerForTranslation = document.getElementById('mzta-summary-banner') || document.getElementById('mzta-summary-generating'); const spamBannerForTranslation = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); @@ -1368,7 +1424,7 @@ switch (message.command) { const wrapper = document.getElementById('mzta-translation-trigger-wrapper'); if (wrapper) wrapper.remove(); else translationTriggerBtn.remove(); browser.runtime.sendMessage({ - command: "triggerTranslationGeneration", + command: message.webchat ? "triggerTranslationWebchat" : "triggerTranslationGeneration", headerMessageId: message.headerMessageId }); }; diff --git a/mzta-background.js b/mzta-background.js index 88d0dae4..b0a254d9 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -334,16 +334,17 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { async function _initTranslation() { try { let tabId = sender.tab.id; - let prefs = await browser.storage.sync.get({ translate: prefs_default.translate, translate_auto: prefs_default.translate_auto }); + let prefs = await browser.storage.sync.get({ translate: prefs_default.translate, translate_auto: prefs_default.translate_auto, translate_display_mode: prefs_default.translate_display_mode, translate_max_display_length: prefs_default.translate_max_display_length }); if (!prefs.translate) return; let message = await browser.messageDisplay.getDisplayedMessage(tabId); if (!message) return; + // Always show cached translation if available, regardless of translate_auto let cachedTranslation = await translationStore.loadTranslation(message.headerMessageId); if (cachedTranslation && !cachedTranslation.error) { - browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { ...cachedTranslation } }); + browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { ...cachedTranslation, maxDisplayLength: prefs.translate_max_display_length } }); return; } @@ -352,15 +353,21 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { return; } + // If translate_auto is disabled, don't show button or auto-generate if (prefs.translate_auto === 0) return; + // Auto mode (translate_auto === 2) always generates inline if (prefs.translate_auto === 2) { _generateTranslationForMessage(message.headerMessageId, tabId); return; } // Manual button mode (translate_auto === 1) - browser.tabs.sendMessage(tabId, { command: "showTranslationButton", headerMessageId: message.headerMessageId }); + if (prefs.translate_display_mode === 'inline') { + browser.tabs.sendMessage(tabId, { command: "showTranslationButton", headerMessageId: message.headerMessageId }); + } else { + browser.tabs.sendMessage(tabId, { command: "showTranslationButton", headerMessageId: message.headerMessageId, webchat: true }); + } } catch (e) { taLog.error("Error in initTranslation: " + e); } @@ -374,17 +381,59 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { } _triggerTranslationGeneration(message); break; + case 'triggerTranslationWebchat': + async function _triggerTranslationWebchat(message) { + let tabId = sender.tab.id; + await _openTranslationWebchat(message.headerMessageId, tabId); + } + _triggerTranslationWebchat(message); + break; case 'refreshTranslation': async function _refreshTranslation(message) { let tabId = sender.tab.id; await translationStore.removeTranslation(message.headerMessageId); - await _generateTranslationForMessage(message.headerMessageId, tabId); + let prefs_refresh_tr = await browser.storage.sync.get({ translate_display_mode: prefs_default.translate_display_mode }); + if (prefs_refresh_tr.translate_display_mode === 'webchat') { + await _openTranslationWebchat(message.headerMessageId, tabId); + } else { + await _generateTranslationForMessage(message.headerMessageId, tabId); + } } _refreshTranslation(message); break; case 'removeTranslation': translationStore.removeTranslation(message.headerMessageId); break; + case 'chatgpt_saveTranslation': + async function _saveTranslationFromWebchat(msg) { + try { + let translatedText = msg.text.trim(); + let prefs_tr = await browser.storage.sync.get({ + translate_lang: prefs_default.translate_lang, + default_chatgpt_lang: prefs_default.default_chatgpt_lang, + translate_max_display_length: prefs_default.translate_max_display_length + }); + let lang = prefs_tr.translate_lang || prefs_tr.default_chatgpt_lang || ''; + const translationData = { + translated_text: translatedText, + lang: lang, + headerMessageId: msg.headerMessageId + }; + await translationStore.saveTranslation(translationData, msg.headerMessageId); + try { + browser.tabs.sendMessage(msg.tabId, { + command: "showTranslation", + data: { ...translationData, maxDisplayLength: prefs_tr.translate_max_display_length } + }); + } catch (e) { + taLog.error("Error sending showTranslation to tab: " + e); + } + } catch (error) { + console.error("[ThunderAI] Error saving translation from webchat:", error); + } + } + _saveTranslationFromWebchat(message); + break; case 'chatgpt_close': async function _closeChatGptWindow(window_id) { let prefs_close = await browser.storage.sync.get({chatgpt_win_save_position: prefs_default.chatgpt_win_save_position}); @@ -647,12 +696,13 @@ async function _generateTranslationForMessage(headerMessageId, tabId) { do_debug: prefs_default.do_debug, default_chatgpt_lang: prefs_default.default_chatgpt_lang, translate_lang: prefs_default.translate_lang, + translate_max_display_length: prefs_default.translate_max_display_length, ...getDynamicSettingsDefaults(['use_specific_integration', 'connection_type']) }); let cachedTranslation = await translationStore.loadTranslation(headerMessageId); if (cachedTranslation && !cachedTranslation.error) { - browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { ...cachedTranslation } }); + browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { ...cachedTranslation, maxDisplayLength: prefs.translate_max_display_length } }); return; } @@ -704,7 +754,7 @@ async function _generateTranslationForMessage(headerMessageId, tabId) { headerMessageId: headerMessageId }; await translationStore.saveTranslation(translationData, headerMessageId); - browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { ...translationData } }); + browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { ...translationData, maxDisplayLength: prefs.translate_max_display_length } }); taWorkingStatus.stopWorking(); } catch (error) { @@ -862,6 +912,41 @@ async function _openSummaryWebchat(headerMessageId, tabId) { } } +async function _openTranslationWebchat(headerMessageId, tabId) { + try { + const messageResult = await browser.messages.query({ headerMessageId: headerMessageId }); + if (!messageResult || messageResult.messages.length === 0) { + console.error("[ThunderAI] _openTranslationWebchat: 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 prefs = await browser.storage.sync.get({ + ...prefs_default, + translate_lang: prefs_default.translate_lang, + default_chatgpt_lang: prefs_default.default_chatgpt_lang + }); + const connectionType = getConnectionType(prefs, {}, 'translate'); + if (connectionType === 'chatgpt_web') { + const errorMsg = browser.i18n.getMessage('translate_chatgpt_web_not_supported'); + await translationStore.saveError(headerMessageId, errorMsg); + browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { error: true, message: errorMsg } }); + return; + } + + const lang = prefs.translate_lang || prefs.default_chatgpt_lang || ''; + const { promptText, promptInfo } = await taPromptUtils.buildTranslationPrompt(curr_message_full, lang); + promptInfo.headerMessageId = headerMessageId; + promptInfo.translationTabId = tabId; + + openChatGPT(promptText, promptInfo.action, tabId, promptInfo.name, promptInfo.need_custom_text, promptInfo); + } catch (error) { + console.error("[ThunderAI] Error opening translation webchat:", error); + } +} + // Listen for messages from ThunderAI-Sparks browser.runtime.onMessageExternal.addListener((message, sender, sendResponse) => { switch (message.action) { diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index a9bbb91e..6a4b13bc 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -143,6 +143,8 @@ export const prefs_default = { summarize_max_display_length: 0, // 0 = no limit, otherwise max chars shown inline translate: true, translate_auto: 0, // 0: disabled, 1: manual button, 2: automatic + translate_display_mode: 'inline', // 'inline' or 'webchat' + translate_max_display_length: 0, // 0 = no limit, otherwise max chars shown inline translate_lang: '', // target language, fallback on default_chatgpt_lang spamfilter_show_msg_panel: true, ...generated_prefs diff --git a/pages/summarize/mzta-summarize.html b/pages/summarize/mzta-summarize.html index ea6f59c7..dd15b738 100644 --- a/pages/summarize/mzta-summarize.html +++ b/pages/summarize/mzta-summarize.html @@ -31,9 +31,9 @@ @@ -44,8 +44,8 @@ diff --git a/pages/translate/mzta-translate.html b/pages/translate/mzta-translate.html index d1a0d187..891dc485 100644 --- a/pages/translate/mzta-translate.html +++ b/pages/translate/mzta-translate.html @@ -31,14 +31,35 @@ + + __MSG_prefs_OptionText_translate_display_mode__ + + + + + + __MSG_prefs_OptionText_translate_max_display_length__ + + + + __MSG_prefs_OptionText_translate_lang__ diff --git a/pages/translate/mzta-translate.js b/pages/translate/mzta-translate.js index 2c1125a9..84e6953e 100644 --- a/pages/translate/mzta-translate.js +++ b/pages/translate/mzta-translate.js @@ -175,6 +175,9 @@ async function restoreOptions() { if (element.id === 'translate_auto') { default_select_value = prefs_default.translate_auto; } + if (element.id === 'translate_display_mode') { + default_select_value = prefs_default.translate_display_mode; + } const restoreValue = result[element.id] ?? default_select_value; let optionExists = Array.from(element.options).some(opt => opt.value === String(restoreValue)); if (element.tomselect) { From 13681c297b2d3e87f84dd4b5cf5ab0e1dfde99d9 Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 28 Mar 2026 11:03:56 +0100 Subject: [PATCH 072/269] summarize batch option added --- _locales/en/messages.json | 4 ++++ options/mzta-options-default.js | 2 +- pages/summarize/mzta-summarize.html | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 15824bc7..8693213a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2084,5 +2084,9 @@ "prefs_chatgpt_win_position_info":{ "message": "Leave empty to use the default position.", "description": "" + }, + "prefs_OptionText_action_auto_batch": { + "message": "When the email is received", + "description": "" } } \ No newline at end of file diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index c7cbfcd8..c278455c 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -138,7 +138,7 @@ export const prefs_default = { spamfilter_threshold: 70, spamfilter_enabled_accounts: [], summarize: false, - summarize_auto: 1, // 0: disabled, 1: manual button, 2: automatic + summarize_auto: 1, // 0: disabled, 1: manual button, 2: automatic, 3: batch processing 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, diff --git a/pages/summarize/mzta-summarize.html b/pages/summarize/mzta-summarize.html index ea6f59c7..2bf1d486 100644 --- a/pages/summarize/mzta-summarize.html +++ b/pages/summarize/mzta-summarize.html @@ -34,6 +34,7 @@ +
      __MSG_prefs_OptionText_summarize_auto_Info__ From 92b8853861daadca8c5cffb5be28c77e3198e5bd Mon Sep 17 00:00:00 2001 From: bittin1ddc447d824349b2 Date: Sat, 28 Mar 2026 10:42:38 +0100 Subject: [PATCH 073/269] Translated using Weblate (Swedish) Currently translated at 100.0% (519 of 519 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/sv/ --- _locales/sv/messages.json | 115 +++++++++++++++++++++++++++++++++++++- 1 file changed, 114 insertions(+), 1 deletion(-) diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index 41df75e4..ec2b7af2 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -1087,7 +1087,7 @@ "message": "Tröskel skräppost" }, "spamfilter_no_reports": { - "message": "Inga meddelanden har ännu granskats för skräppost. Här hittar du en lista över de senaste 100 skräppostrapporterna för den aktuella sessionen." + "message": "Inga meddelanden har ännu granskats för skräppost. Här hittar du en lista över de senaste 100 skräppostrapporter." }, "SpamReport_Title": { "message": "Rapporter om skräppostfilter" @@ -1457,5 +1457,118 @@ }, "prefs_chatgpt_win_position_info": { "message": "Lämna tomt för att använda standardpositionen." + }, + "webchat_save_as_summary": { + "message": "Spara som sammanfattning" + }, + "prefs_cache_title": { + "message": "Cachelagring" + }, + "prefs_cache_storage_size": { + "message": "Cachestorlek" + }, + "prefs_cache_clear_button": { + "message": "Rensa cache" + }, + "prefs_cache_clear_confirm": { + "message": "Är du säker på att du vill rensa all cachad data (sammanfattningar, skräppostrapporter, översättningar)? Den här åtgärden kan inte ångras." + }, + "prefs_cache_clear_done": { + "message": "$COUNT$ register borttagna.", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "prefs_OptionText_auto_summary": { + "message": "Aktivera automatisk AI-sammanfattning för förhandsgranskningar av meddelanden" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Om markerat kommer ThunderAI automatiskt 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 omedelbart skickas till den konfigurerade AI-tjänsten." + }, + "auto_summary_title": { + "message": "ThunderAI sammanfattning" + }, + "auto_summary_generating": { + "message": "Genererar AI-sammanfattning..." + }, + "auto_summary_failed": { + "message": "Misslyckades att generera AI-sammanfattning. Bekräfta dina inställningar och försök igen." + }, + "prefs_OptionText_summarize_auto": { + "message": "Sammanfatta meddelanden automatiskt" + }, + "prefs_OptionText_summarize_auto_disabled": { + "message": "Inaktiverad" + }, + "prefs_OptionText_summarize_auto_manual": { + "message": "Visa sammanfattningsknapp" + }, + "prefs_OptionText_summarize_auto_automatic": { + "message": "Generera automatiskt" + }, + "prefs_OptionText_summarize_auto_Info": { + "message": "Välj om sammanfattningar ska genereras automatiskt när meddelanden visas. Kräver en API-baserad anslutning (inte ChatGPT Web)." + }, + "prefs_OptionText_summarize_display_mode": { + "message": "Visa sammanfattning i" + }, + "prefs_OptionText_summarize_display_mode_inline": { + "message": "Meddelandepanel (inbäddad)" + }, + "prefs_OptionText_summarize_display_mode_webchat": { + "message": "Chatt fönster" + }, + "prefs_OptionText_summarize_display_mode_Info": { + "message": "Välj var sammanfattningsresultatet visas. I infogat läge visas en sammanfattningsbanderoll direkt i meddelandefönstret. I chattfönsterläget öppnas AI-chattfönstret." + }, + "prefs_OptionText_summarize_max_display_length": { + "message": "Max skärmlängd" + }, + "prefs_OptionText_summarize_max_display_length_Info": { + "message": "Maximalt antal tecken som ska visas i den inbäddade sammanfattningen. Ange 0 för obegränsad gräns." + }, + "summarize_see_more": { + "message": "Se mer" + }, + "summarize_see_less": { + "message": "Se mindre" + }, + "summarize_title": { + "message": "ThunderAI översikt" + }, + "get_ai_summary": { + "message": "AI sammanfattning" + }, + "summarize_collapse": { + "message": "Fäll in sammanfattning" + }, + "summarize_generating": { + "message": "Genererar sammanfattning..." + }, + "summarize_error": { + "message": "Misslyckades att generera sammanfattning" + }, + "summarize_click_to_generate": { + "message": "Klicka här för att skapa en sammanfattning" + }, + "summarize_chatgpt_web_not_supported": { + "message": "Autosammanfattning kräver en API-baserad anslutning. Konfigurera en API-anslutning i ThunderAI-inställningarna." + }, + "summarize_refresh": { + "message": "Uppdatera sammanfattning" + }, + "spamfilter_refresh": { + "message": "Uppdatera skräppostrapport" + }, + "spamfilter_delete": { + "message": "Ta bort skräppostrapport" + }, + "summarize_delete": { + "message": "Ta bort sammanfattning" + }, + "summary_by": { + "message": "Sammanfattning av" } } From b4e41c0ba0cea39dba547bd18232676b9a6bc2e7 Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 28 Mar 2026 11:03:56 +0100 Subject: [PATCH 074/269] summarize batch option added --- _locales/en/messages.json | 4 ++++ options/mzta-options-default.js | 2 +- pages/summarize/mzta-summarize.html | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 6c81b13b..cdff94a1 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2196,5 +2196,9 @@ "prefs_chatgpt_win_position_info":{ "message": "Leave empty to use the default position.", "description": "" + }, + "prefs_OptionText_action_auto_batch": { + "message": "When the email is received", + "description": "" } } \ No newline at end of file diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index 6a4b13bc..450c1f50 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -138,7 +138,7 @@ export const prefs_default = { spamfilter_threshold: 70, spamfilter_enabled_accounts: [], summarize: false, - summarize_auto: 1, // 0: disabled, 1: manual button, 2: automatic + summarize_auto: 1, // 0: disabled, 1: manual button, 2: automatic, 3: batch processing summarize_display_mode: 'inline', // 'inline' or 'webchat' summarize_max_display_length: 0, // 0 = no limit, otherwise max chars shown inline translate: true, diff --git a/pages/summarize/mzta-summarize.html b/pages/summarize/mzta-summarize.html index dd15b738..f5f369c3 100644 --- a/pages/summarize/mzta-summarize.html +++ b/pages/summarize/mzta-summarize.html @@ -34,6 +34,7 @@ +
      __MSG_prefs_OptionText_summarize_auto_Info__ From a8c69d44267af4809cfd977fe2ffb77db21549cc Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 28 Mar 2026 11:39:11 +0100 Subject: [PATCH 075/269] summarize now is possibile on aslo mail receive. see #580 #723 --- _locales/en/messages.json | 2 +- claude-spec/01-architecture.md | 23 +++++++++++ claude-spec/05-options.md | 4 +- mzta-background.js | 67 ++++++++++++++++++++----------- options/mzta-options-default.js | 2 +- pages/summarize/mzta-summarize.js | 15 +++++++ 6 files changed, 86 insertions(+), 27 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 8693213a..2c8d22d3 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1966,7 +1966,7 @@ "description": "" }, "prefs_OptionText_summarize_auto_automatic": { - "message": "Generate automatically", + "message": "When the email is opened", "description": "" }, "prefs_OptionText_summarize_auto_Info": { diff --git a/claude-spec/01-architecture.md b/claude-spec/01-architecture.md index 085affed..a5b08756 100644 --- a/claude-spec/01-architecture.md +++ b/claude-spec/01-architecture.md @@ -47,6 +47,7 @@ The `summarize_display_mode` preference (`'inline'` or `'webchat'`) controls whe 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 = 3` (on receive) pre-caches the summary silently when the email arrives via `onNewMailReceived`. When the user later opens the message, the cache hit triggers an instant display. - `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()` @@ -67,6 +68,7 @@ mzta-background.js (checks summarize_auto + summarize_display_mode prefs) │ display_mode = inline → click triggers inline gen │ │ display_mode = webchat → click opens chat window │ │ summarize_auto = 2 → generate immediately (always inline)│ + │ summarize_auto = 3 → cache hit (pre-cached on receive) │ └──────────────────────────────────────────────────────────┘ ↓ (if generating inline) taSummaryStore (check cache / set processing) @@ -78,6 +80,27 @@ mzta-background.js (checks summarize_auto + summarize_display_mode prefs) mzta-compose-script.js (render summary banner in message body) ``` +### Data Flow: Background Summary on Email Receive (summarize_auto = 3) + +When `summarize_auto = 3`, a summary is generated silently when a new email arrives. The flow mirrors `add_tags_auto`: + +``` +New email arrives + ↓ +browser.messages.onNewMailReceived + ↓ +newEmailListener (checks _process_incoming, which includes summarize_auto === 3) + ↓ +processEmails({ summarizeOnReceive: true }) + ↓ (single loop — shared with addTagsAuto / spamFilter) +_generateSummaryForMessage(headerMessageId, null, { messageData }) + ← tabId is null → no UI messages sent, silent pre-cache + ↓ +taSummaryStore.saveSummary() + ↓ +[later] user opens the message → initSummary → cache hit → showSummary instantly +``` + ## Key Modules | File | Role | diff --git a/claude-spec/05-options.md b/claude-spec/05-options.md index b61cd92b..d929ceb9 100644 --- a/claude-spec/05-options.md +++ b/claude-spec/05-options.md @@ -95,8 +95,8 @@ 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` | `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_auto` | `1` | Auto-summarize mode: `0` = disabled, `1` = manual (show "click to generate" button), `2` = automatic (generate on message open), `3` = generate on email receive (background pre-cache via `onNewMailReceived`, no UI during generation) | +| `summarize_display_mode` | `'inline'` | Where to display summaries: `'inline'` = message pane banner, `'webchat'` = AI chat window. Note: `summarize_auto = 2` and `summarize_auto = 3` always use 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/`) diff --git a/mzta-background.js b/mzta-background.js index 29ec2828..27d99f62 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -505,7 +505,9 @@ function cleanSummaryText(text) { return cleaned; } -async function _generateSummaryForMessage(headerMessageId, tabId) { +// tabId is optional — if null, runs silently (background pre-cache, no UI update) +// options.messageData: { message, fullMessage } — pass pre-fetched data to avoid re-querying +async function _generateSummaryForMessage(headerMessageId, tabId = null, options = {}) { try { let prefs = await browser.storage.sync.get({ connection_type: prefs_default.connection_type, @@ -517,40 +519,46 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { let cachedSummary = await summaryStore.loadSummary(headerMessageId); if (cachedSummary && !cachedSummary.error) { - browser.tabs.sendMessage(tabId, { command: "showSummary", data: { ...cachedSummary, maxDisplayLength: prefs.summarize_max_display_length } }); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showSummary", data: { ...cachedSummary, maxDisplayLength: prefs.summarize_max_display_length } }); return; } if (await summaryStore.isProcessing(headerMessageId)) { - browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); return; } await summaryStore.setProcessing(headerMessageId); taWorkingStatus.startWorking(); - browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); + if (tabId) 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; + let message, fullMessage; + if (options.messageData) { + message = options.messageData.message; + fullMessage = options.messageData.fullMessage; + } else { + const messageResult = await browser.messages.query({ headerMessageId: headerMessageId }); + if (!messageResult || messageResult.messages.length === 0) { + await summaryStore.saveError(headerMessageId, "Message not found"); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: "Message not found" } }); + taWorkingStatus.stopWorking(); + return; + } + message = messageResult.messages[0]; + fullMessage = await browser.messages.getFull(message.id); } - const fullMessage = await browser.messages.getFull(messageResult.messages[0].id); - const connectionType = getConnectionType(prefs, {}, '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 } }); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: errorMsg } }); taWorkingStatus.stopWorking(); return; } - const { promptText } = await taPromptUtils.buildSummaryPrompt([{ message: messageResult.messages[0], fullMessage }]); + const { promptText } = await taPromptUtils.buildSummaryPrompt([{ message, fullMessage }]); const cmd = new mzta_specialCommand({ prompt: promptText, @@ -572,13 +580,13 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { headerMessageId: headerMessageId }; await summaryStore.saveSummary(summaryData, headerMessageId); - browser.tabs.sendMessage(tabId, { command: "showSummary", data: { ...summaryData, maxDisplayLength: prefs.summarize_max_display_length } }); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showSummary", data: { ...summaryData, maxDisplayLength: prefs.summarize_max_display_length } }); 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" } }); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: error.message || "Failed to generate summary" } }); taWorkingStatus.stopWorking(); } } @@ -1162,13 +1170,14 @@ async function reload_pref_init(){ add_tags_auto_only_inbox: prefs_default.add_tags_auto_only_inbox, spamfilter: prefs_default.spamfilter, summarize: prefs_default.summarize, + summarize_auto: prefs_default.summarize_auto, spamfilter_threshold: prefs_default.spamfilter_threshold, spamfilter_show_msg_panel: prefs_default.spamfilter_show_msg_panel, dynamic_menu_force_enter: prefs_default.dynamic_menu_force_enter, chatgpt_win_save_position: prefs_default.chatgpt_win_save_position, ...getDynamicSettingsDefaults(['use_specific_integration', 'connection_type']) }); - _process_incoming = prefs_init.add_tags_auto || prefs_init.spamfilter; + _process_incoming = prefs_init.add_tags_auto || prefs_init.spamfilter || (prefs_init.summarize && prefs_init.summarize_auto === 3); _sparks_presence = await checkSparksPresence(); } @@ -1395,7 +1404,8 @@ const newEmailListener = (folder, messagesList) => { await processEmails({ messages: messages, addTagsAuto: add_tags_auto_enabled, - spamFilter: prefs_init.spamfilter + spamFilter: prefs_init.spamfilter, + summarizeOnReceive: prefs_init.summarize && prefs_init.summarize_auto === 3 }); if(prefs_init.spamfilter){ @@ -1428,15 +1438,16 @@ async function processEmails(args) { messages, addTagsAuto = false, spamFilter = false, - summarize = false + summarize = false, + summarizeOnReceive = false } = args; taWorkingStatus.startWorking(); - // We keep two different loops, one for addTagsAuto and spamFilter and one for summarize - // because summarize is never called when an email is received, but only when using the context menu item + // One loop handles addTagsAuto, spamFilter, and summarizeOnReceive (on email receive). + // The separate summarize block below handles the context menu flow. - if (addTagsAuto || spamFilter) { + if (addTagsAuto || spamFilter || summarizeOnReceive) { let prefs_aats = await browser.storage.sync.get({ add_tags_maxnum: prefs_default.add_tags_maxnum, connection_type: prefs_default.connection_type, @@ -1531,6 +1542,16 @@ async function processEmails(args) { }); if (!result.success) continue; } + + if (summarizeOnReceive) { + if (!curr_fullMessage) { + curr_fullMessage = await browser.messages.getFull(message.id); + } + taLog.log("[ThunderAI] Pre-caching summary on receive for: " + message.headerMessageId); + await _generateSummaryForMessage(message.headerMessageId, null, { + messageData: { message, fullMessage: curr_fullMessage } + }); + } } } diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index c278455c..76510994 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -138,7 +138,7 @@ export const prefs_default = { spamfilter_threshold: 70, spamfilter_enabled_accounts: [], summarize: false, - summarize_auto: 1, // 0: disabled, 1: manual button, 2: automatic, 3: batch processing + summarize_auto: 1, // 0: disabled, 1: manual button, 2: automatic on message open, 3: generate on email receive 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, diff --git a/pages/summarize/mzta-summarize.js b/pages/summarize/mzta-summarize.js index edb8ffec..dd945231 100644 --- a/pages/summarize/mzta-summarize.js +++ b/pages/summarize/mzta-summarize.js @@ -76,6 +76,7 @@ document.addEventListener("DOMContentLoaded", async () => { document.querySelectorAll(".option-input").forEach(element => { element.addEventListener("change", saveOptions); }); + document.getElementById('summarize_auto').addEventListener('change', updateDisplayModeConstraint); let prefs_summarize = await browser.storage.sync.get({ summarize_enabled_accounts: [], connection_type: 'chatgpt_web' }); let summarize_textarea = document.getElementById("summarize_prompt_text"); @@ -180,6 +181,19 @@ document.addEventListener("DOMContentLoaded", async () => { // Methods to manage options, derived from: /options/mzta-options.js +function updateDisplayModeConstraint() { + const summarize_auto_el = document.getElementById('summarize_auto'); + const display_mode_el = document.getElementById('summarize_display_mode'); + const autoVal = String(summarize_auto_el.value); + if (autoVal === '2' || autoVal === '3') { + display_mode_el.value = 'inline'; + display_mode_el.disabled = true; + browser.storage.sync.set({ summarize_display_mode: 'inline' }); + } else { + display_mode_el.disabled = false; + } +} + function saveOptions(e) { e.preventDefault(); let options = {}; @@ -294,4 +308,5 @@ async function restoreOptions() { } setCurrentChoice(getting); + updateDisplayModeConstraint(); } From a931c864d2ba9d19c0263851322881d46c78a984 Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 28 Mar 2026 11:39:11 +0100 Subject: [PATCH 076/269] summarize now is possibile on also mail receive. see #580 #723 --- claude-spec/01-architecture.md | 23 +++++++++++ claude-spec/05-options.md | 4 +- mzta-background.js | 67 ++++++++++++++++++++----------- options/mzta-options-default.js | 2 +- pages/summarize/mzta-summarize.js | 15 +++++++ 5 files changed, 85 insertions(+), 26 deletions(-) diff --git a/claude-spec/01-architecture.md b/claude-spec/01-architecture.md index 652c8fcf..30da7475 100644 --- a/claude-spec/01-architecture.md +++ b/claude-spec/01-architecture.md @@ -47,6 +47,7 @@ The `summarize_display_mode` preference (`'inline'` or `'webchat'`) controls whe 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 = 3` (on receive) pre-caches the summary silently when the email arrives via `onNewMailReceived`. When the user later opens the message, the cache hit triggers an instant display. - `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()` @@ -67,6 +68,7 @@ mzta-background.js (checks summarize_auto + summarize_display_mode prefs) │ display_mode = inline → click triggers inline gen │ │ display_mode = webchat → click opens chat window │ │ summarize_auto = 2 → generate immediately (always inline)│ + │ summarize_auto = 3 → cache hit (pre-cached on receive) │ └──────────────────────────────────────────────────────────┘ ↓ (if generating inline) taSummaryStore (check cache / set processing) @@ -114,6 +116,27 @@ mzta-background.js (checks translate + translate_auto + translate_display_m mzta-compose-script.js (render translation banner in message body) ``` +### Data Flow: Background Summary on Email Receive (summarize_auto = 3) + +When `summarize_auto = 3`, a summary is generated silently when a new email arrives. The flow mirrors `add_tags_auto`: + +``` +New email arrives + ↓ +browser.messages.onNewMailReceived + ↓ +newEmailListener (checks _process_incoming, which includes summarize_auto === 3) + ↓ +processEmails({ summarizeOnReceive: true }) + ↓ (single loop — shared with addTagsAuto / spamFilter) +_generateSummaryForMessage(headerMessageId, null, { messageData }) + ← tabId is null → no UI messages sent, silent pre-cache + ↓ +taSummaryStore.saveSummary() + ↓ +[later] user opens the message → initSummary → cache hit → showSummary instantly +``` + ## Key Modules | File | Role | diff --git a/claude-spec/05-options.md b/claude-spec/05-options.md index e1af7369..f6b12bbe 100644 --- a/claude-spec/05-options.md +++ b/claude-spec/05-options.md @@ -95,8 +95,8 @@ 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` | `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_auto` | `1` | Auto-summarize mode: `0` = disabled, `1` = manual (show "click to generate" button), `2` = automatic (generate on message open), `3` = generate on email receive (background pre-cache via `onNewMailReceived`, no UI during generation) | +| `summarize_display_mode` | `'inline'` | Where to display summaries: `'inline'` = message pane banner, `'webchat'` = AI chat window. Note: `summarize_auto = 2` and `summarize_auto = 3` always use 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. | | `translate` | `true` | Enable email translation | | `translate_auto` | `0` | Auto-translate mode: `0` = disabled, `1` = manual (show button), `2` = automatic (translate on message open) | diff --git a/mzta-background.js b/mzta-background.js index b0a254d9..ec44bd18 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -611,7 +611,9 @@ function cleanSummaryText(text) { return cleaned; } -async function _generateSummaryForMessage(headerMessageId, tabId) { +// tabId is optional — if null, runs silently (background pre-cache, no UI update) +// options.messageData: { message, fullMessage } — pass pre-fetched data to avoid re-querying +async function _generateSummaryForMessage(headerMessageId, tabId = null, options = {}) { try { let prefs = await browser.storage.sync.get({ connection_type: prefs_default.connection_type, @@ -623,40 +625,46 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { let cachedSummary = await summaryStore.loadSummary(headerMessageId); if (cachedSummary && !cachedSummary.error) { - browser.tabs.sendMessage(tabId, { command: "showSummary", data: { ...cachedSummary, maxDisplayLength: prefs.summarize_max_display_length } }); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showSummary", data: { ...cachedSummary, maxDisplayLength: prefs.summarize_max_display_length } }); return; } if (await summaryStore.isProcessing(headerMessageId)) { - browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); return; } await summaryStore.setProcessing(headerMessageId); taWorkingStatus.startWorking(); - browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" }); + if (tabId) 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; + let message, fullMessage; + if (options.messageData) { + message = options.messageData.message; + fullMessage = options.messageData.fullMessage; + } else { + const messageResult = await browser.messages.query({ headerMessageId: headerMessageId }); + if (!messageResult || messageResult.messages.length === 0) { + await summaryStore.saveError(headerMessageId, "Message not found"); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: "Message not found" } }); + taWorkingStatus.stopWorking(); + return; + } + message = messageResult.messages[0]; + fullMessage = await browser.messages.getFull(message.id); } - const fullMessage = await browser.messages.getFull(messageResult.messages[0].id); - const connectionType = getConnectionType(prefs, {}, '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 } }); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: errorMsg } }); taWorkingStatus.stopWorking(); return; } - const { promptText } = await taPromptUtils.buildSummaryPrompt([{ message: messageResult.messages[0], fullMessage }]); + const { promptText } = await taPromptUtils.buildSummaryPrompt([{ message, fullMessage }]); const cmd = new mzta_specialCommand({ prompt: promptText, @@ -678,13 +686,13 @@ async function _generateSummaryForMessage(headerMessageId, tabId) { headerMessageId: headerMessageId }; await summaryStore.saveSummary(summaryData, headerMessageId); - browser.tabs.sendMessage(tabId, { command: "showSummary", data: { ...summaryData, maxDisplayLength: prefs.summarize_max_display_length } }); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showSummary", data: { ...summaryData, maxDisplayLength: prefs.summarize_max_display_length } }); 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" } }); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: error.message || "Failed to generate summary" } }); taWorkingStatus.stopWorking(); } } @@ -1379,13 +1387,14 @@ async function reload_pref_init(){ add_tags_auto_only_inbox: prefs_default.add_tags_auto_only_inbox, spamfilter: prefs_default.spamfilter, summarize: prefs_default.summarize, + summarize_auto: prefs_default.summarize_auto, spamfilter_threshold: prefs_default.spamfilter_threshold, spamfilter_show_msg_panel: prefs_default.spamfilter_show_msg_panel, dynamic_menu_force_enter: prefs_default.dynamic_menu_force_enter, chatgpt_win_save_position: prefs_default.chatgpt_win_save_position, ...getDynamicSettingsDefaults(['use_specific_integration', 'connection_type']) }); - _process_incoming = prefs_init.add_tags_auto || prefs_init.spamfilter; + _process_incoming = prefs_init.add_tags_auto || prefs_init.spamfilter || (prefs_init.summarize && prefs_init.summarize_auto === 3); _sparks_presence = await checkSparksPresence(); } @@ -1612,7 +1621,8 @@ const newEmailListener = (folder, messagesList) => { await processEmails({ messages: messages, addTagsAuto: add_tags_auto_enabled, - spamFilter: prefs_init.spamfilter + spamFilter: prefs_init.spamfilter, + summarizeOnReceive: prefs_init.summarize && prefs_init.summarize_auto === 3 }); if(prefs_init.spamfilter){ @@ -1645,15 +1655,16 @@ async function processEmails(args) { messages, addTagsAuto = false, spamFilter = false, - summarize = false + summarize = false, + summarizeOnReceive = false } = args; taWorkingStatus.startWorking(); - // We keep two different loops, one for addTagsAuto and spamFilter and one for summarize - // because summarize is never called when an email is received, but only when using the context menu item + // One loop handles addTagsAuto, spamFilter, and summarizeOnReceive (on email receive). + // The separate summarize block below handles the context menu flow. - if (addTagsAuto || spamFilter) { + if (addTagsAuto || spamFilter || summarizeOnReceive) { let prefs_aats = await browser.storage.sync.get({ add_tags_maxnum: prefs_default.add_tags_maxnum, connection_type: prefs_default.connection_type, @@ -1748,6 +1759,16 @@ async function processEmails(args) { }); if (!result.success) continue; } + + if (summarizeOnReceive) { + if (!curr_fullMessage) { + curr_fullMessage = await browser.messages.getFull(message.id); + } + taLog.log("[ThunderAI] Pre-caching summary on receive for: " + message.headerMessageId); + await _generateSummaryForMessage(message.headerMessageId, null, { + messageData: { message, fullMessage: curr_fullMessage } + }); + } } } diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index 450c1f50..0347bc82 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -138,7 +138,7 @@ export const prefs_default = { spamfilter_threshold: 70, spamfilter_enabled_accounts: [], summarize: false, - summarize_auto: 1, // 0: disabled, 1: manual button, 2: automatic, 3: batch processing + summarize_auto: 1, // 0: disabled, 1: manual button, 2: automatic on message open, 3: generate on email receive summarize_display_mode: 'inline', // 'inline' or 'webchat' summarize_max_display_length: 0, // 0 = no limit, otherwise max chars shown inline translate: true, diff --git a/pages/summarize/mzta-summarize.js b/pages/summarize/mzta-summarize.js index edb8ffec..dd945231 100644 --- a/pages/summarize/mzta-summarize.js +++ b/pages/summarize/mzta-summarize.js @@ -76,6 +76,7 @@ document.addEventListener("DOMContentLoaded", async () => { document.querySelectorAll(".option-input").forEach(element => { element.addEventListener("change", saveOptions); }); + document.getElementById('summarize_auto').addEventListener('change', updateDisplayModeConstraint); let prefs_summarize = await browser.storage.sync.get({ summarize_enabled_accounts: [], connection_type: 'chatgpt_web' }); let summarize_textarea = document.getElementById("summarize_prompt_text"); @@ -180,6 +181,19 @@ document.addEventListener("DOMContentLoaded", async () => { // Methods to manage options, derived from: /options/mzta-options.js +function updateDisplayModeConstraint() { + const summarize_auto_el = document.getElementById('summarize_auto'); + const display_mode_el = document.getElementById('summarize_display_mode'); + const autoVal = String(summarize_auto_el.value); + if (autoVal === '2' || autoVal === '3') { + display_mode_el.value = 'inline'; + display_mode_el.disabled = true; + browser.storage.sync.set({ summarize_display_mode: 'inline' }); + } else { + display_mode_el.disabled = false; + } +} + function saveOptions(e) { e.preventDefault(); let options = {}; @@ -294,4 +308,5 @@ async function restoreOptions() { } setCurrentChoice(getting); + updateDisplayModeConstraint(); } From 6c6b57e6ea40903e30aba49a6996ba86c3d40ecf Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 28 Mar 2026 11:45:50 +0100 Subject: [PATCH 077/269] i18n updated --- _locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index cdff94a1..b8de87e5 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2078,7 +2078,7 @@ "description": "" }, "prefs_OptionText_action_auto_automatic": { - "message": "Automatic", + "message": "When the email is opened", "description": "" }, "prefs_OptionText_translate_auto_Info": { From 2d478edfb9070a30c7a25a5ed95722fbb4906474 Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 28 Mar 2026 11:47:21 +0100 Subject: [PATCH 078/269] fields handling improved --- pages/summarize/mzta-summarize.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pages/summarize/mzta-summarize.js b/pages/summarize/mzta-summarize.js index dd945231..215558a4 100644 --- a/pages/summarize/mzta-summarize.js +++ b/pages/summarize/mzta-summarize.js @@ -189,6 +189,8 @@ function updateDisplayModeConstraint() { display_mode_el.value = 'inline'; display_mode_el.disabled = true; browser.storage.sync.set({ summarize_display_mode: 'inline' }); + } else if (autoVal === '0') { + display_mode_el.disabled = true; } else { display_mode_el.disabled = false; } From f10beb809771e0e3b19a4a18241107a648443dbb Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 28 Mar 2026 11:55:06 +0100 Subject: [PATCH 079/269] onreceive translate added. see #247 #721 --- claude-spec/01-architecture.md | 23 ++++++++++- claude-spec/05-options.md | 4 +- mzta-background.js | 60 +++++++++++++++++++---------- options/mzta-options-default.js | 2 +- pages/translate/mzta-translate.html | 1 + pages/translate/mzta-translate.js | 17 ++++++++ 6 files changed, 83 insertions(+), 24 deletions(-) diff --git a/claude-spec/01-architecture.md b/claude-spec/01-architecture.md index 30da7475..7c31e7a4 100644 --- a/claude-spec/01-architecture.md +++ b/claude-spec/01-architecture.md @@ -128,7 +128,7 @@ browser.messages.onNewMailReceived newEmailListener (checks _process_incoming, which includes summarize_auto === 3) ↓ processEmails({ summarizeOnReceive: true }) - ↓ (single loop — shared with addTagsAuto / spamFilter) + ↓ (single loop — shared with addTagsAuto / spamFilter / translateOnReceive) _generateSummaryForMessage(headerMessageId, null, { messageData }) ← tabId is null → no UI messages sent, silent pre-cache ↓ @@ -137,6 +137,27 @@ taSummaryStore.saveSummary() [later] user opens the message → initSummary → cache hit → showSummary instantly ``` +### Data Flow: Background Translation on Email Receive (translate_auto = 3) + +When `translate_auto = 3`, a translation is generated silently when a new email arrives. Mirrors the summarize on-receive flow: + +``` +New email arrives + ↓ +browser.messages.onNewMailReceived + ↓ +newEmailListener (checks _process_incoming, which includes translate_auto === 3) + ↓ +processEmails({ translateOnReceive: true }) + ↓ (single loop — shared with addTagsAuto / spamFilter / summarizeOnReceive) +_generateTranslationForMessage(headerMessageId, null, { messageData }) + ← tabId is null → no UI messages sent, silent pre-cache + ↓ +taTranslationStore.saveTranslation() + ↓ +[later] user opens the message → initTranslation → cache hit → showTranslation instantly +``` + ## Key Modules | File | Role | diff --git a/claude-spec/05-options.md b/claude-spec/05-options.md index f6b12bbe..06c0d250 100644 --- a/claude-spec/05-options.md +++ b/claude-spec/05-options.md @@ -99,8 +99,8 @@ These are generated programmatically at the bottom of `mzta-options-default.js` | `summarize_display_mode` | `'inline'` | Where to display summaries: `'inline'` = message pane banner, `'webchat'` = AI chat window. Note: `summarize_auto = 2` and `summarize_auto = 3` always use 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. | | `translate` | `true` | Enable email translation | -| `translate_auto` | `0` | Auto-translate mode: `0` = disabled, `1` = manual (show button), `2` = automatic (translate on message open) | -| `translate_display_mode` | `'inline'` | Where to display translations: `'inline'` = message pane banner, `'webchat'` = AI chat window. Note: `translate_auto = 2` always uses inline regardless of this setting. | +| `translate_auto` | `0` | Auto-translate mode: `0` = disabled, `1` = manual (show button), `2` = automatic (translate on message open), `3` = generate on email receive (background pre-cache via `onNewMailReceived`, no UI during generation) | +| `translate_display_mode` | `'inline'` | Where to display translations: `'inline'` = message pane banner, `'webchat'` = AI chat window. Note: `translate_auto = 2` and `translate_auto = 3` always use inline regardless of this setting. | | `translate_max_display_length` | `0` | Maximum characters shown in inline translation 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. | | `translate_lang` | `''` | Target language for translation. Falls back to `default_chatgpt_lang` if empty. | diff --git a/mzta-background.js b/mzta-background.js index ec44bd18..5aaad026 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -697,7 +697,9 @@ async function _generateSummaryForMessage(headerMessageId, tabId = null, options } } -async function _generateTranslationForMessage(headerMessageId, tabId) { +// tabId is optional — if null, runs silently (background pre-cache, no UI update) +// options.messageData: { fullMessage } — pass pre-fetched data to avoid re-querying +async function _generateTranslationForMessage(headerMessageId, tabId = null, options = {}) { try { let prefs = await browser.storage.sync.get({ connection_type: prefs_default.connection_type, @@ -710,35 +712,39 @@ async function _generateTranslationForMessage(headerMessageId, tabId) { let cachedTranslation = await translationStore.loadTranslation(headerMessageId); if (cachedTranslation && !cachedTranslation.error) { - browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { ...cachedTranslation, maxDisplayLength: prefs.translate_max_display_length } }); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { ...cachedTranslation, maxDisplayLength: prefs.translate_max_display_length } }); return; } if (await translationStore.isProcessing(headerMessageId)) { - browser.tabs.sendMessage(tabId, { command: "showTranslationGenerating" }); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showTranslationGenerating" }); return; } await translationStore.setProcessing(headerMessageId); taWorkingStatus.startWorking(); - browser.tabs.sendMessage(tabId, { command: "showTranslationGenerating" }); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showTranslationGenerating" }); - const messageResult = await browser.messages.query({ headerMessageId: headerMessageId }); - if (!messageResult || messageResult.messages.length === 0) { - await translationStore.saveError(headerMessageId, "Message not found"); - browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { error: true, message: "Message not found" } }); - taWorkingStatus.stopWorking(); - return; + let fullMessage; + if (options.messageData) { + fullMessage = options.messageData.fullMessage; + } else { + const messageResult = await browser.messages.query({ headerMessageId: headerMessageId }); + if (!messageResult || messageResult.messages.length === 0) { + await translationStore.saveError(headerMessageId, "Message not found"); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { error: true, message: "Message not found" } }); + taWorkingStatus.stopWorking(); + return; + } + fullMessage = await browser.messages.getFull(messageResult.messages[0].id); } - const fullMessage = await browser.messages.getFull(messageResult.messages[0].id); - const connectionType = getConnectionType(prefs, {}, 'translate'); if (connectionType === 'chatgpt_web') { const errorMsg = browser.i18n.getMessage('translate_chatgpt_web_not_supported'); await translationStore.saveError(headerMessageId, errorMsg); - browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { error: true, message: errorMsg } }); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { error: true, message: errorMsg } }); taWorkingStatus.stopWorking(); return; } @@ -762,13 +768,13 @@ async function _generateTranslationForMessage(headerMessageId, tabId) { headerMessageId: headerMessageId }; await translationStore.saveTranslation(translationData, headerMessageId); - browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { ...translationData, maxDisplayLength: prefs.translate_max_display_length } }); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { ...translationData, maxDisplayLength: prefs.translate_max_display_length } }); taWorkingStatus.stopWorking(); } catch (error) { console.error("[ThunderAI] Error generating translation:", error); await translationStore.saveError(headerMessageId, error.message || String(error)); - browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { error: true, message: error.message || "Failed to generate translation" } }); + if (tabId) browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { error: true, message: error.message || "Failed to generate translation" } }); taWorkingStatus.stopWorking(); } } @@ -1388,13 +1394,15 @@ async function reload_pref_init(){ spamfilter: prefs_default.spamfilter, summarize: prefs_default.summarize, summarize_auto: prefs_default.summarize_auto, + translate: prefs_default.translate, + translate_auto: prefs_default.translate_auto, spamfilter_threshold: prefs_default.spamfilter_threshold, spamfilter_show_msg_panel: prefs_default.spamfilter_show_msg_panel, dynamic_menu_force_enter: prefs_default.dynamic_menu_force_enter, chatgpt_win_save_position: prefs_default.chatgpt_win_save_position, ...getDynamicSettingsDefaults(['use_specific_integration', 'connection_type']) }); - _process_incoming = prefs_init.add_tags_auto || prefs_init.spamfilter || (prefs_init.summarize && prefs_init.summarize_auto === 3); + _process_incoming = prefs_init.add_tags_auto || prefs_init.spamfilter || (prefs_init.summarize && prefs_init.summarize_auto === 3) || (prefs_init.translate && prefs_init.translate_auto === 3); _sparks_presence = await checkSparksPresence(); } @@ -1622,7 +1630,8 @@ const newEmailListener = (folder, messagesList) => { messages: messages, addTagsAuto: add_tags_auto_enabled, spamFilter: prefs_init.spamfilter, - summarizeOnReceive: prefs_init.summarize && prefs_init.summarize_auto === 3 + summarizeOnReceive: prefs_init.summarize && prefs_init.summarize_auto === 3, + translateOnReceive: prefs_init.translate && prefs_init.translate_auto === 3 }); if(prefs_init.spamfilter){ @@ -1656,15 +1665,16 @@ async function processEmails(args) { addTagsAuto = false, spamFilter = false, summarize = false, - summarizeOnReceive = false + summarizeOnReceive = false, + translateOnReceive = false } = args; taWorkingStatus.startWorking(); - // One loop handles addTagsAuto, spamFilter, and summarizeOnReceive (on email receive). + // One loop handles addTagsAuto, spamFilter, summarizeOnReceive, and translateOnReceive (on email receive). // The separate summarize block below handles the context menu flow. - if (addTagsAuto || spamFilter || summarizeOnReceive) { + if (addTagsAuto || spamFilter || summarizeOnReceive || translateOnReceive) { let prefs_aats = await browser.storage.sync.get({ add_tags_maxnum: prefs_default.add_tags_maxnum, connection_type: prefs_default.connection_type, @@ -1769,6 +1779,16 @@ async function processEmails(args) { messageData: { message, fullMessage: curr_fullMessage } }); } + + if (translateOnReceive) { + if (!curr_fullMessage) { + curr_fullMessage = await browser.messages.getFull(message.id); + } + taLog.log("[ThunderAI] Pre-caching translation on receive for: " + message.headerMessageId); + await _generateTranslationForMessage(message.headerMessageId, null, { + messageData: { fullMessage: curr_fullMessage } + }); + } } } diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index 0347bc82..17a7da3a 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -142,7 +142,7 @@ export const prefs_default = { summarize_display_mode: 'inline', // 'inline' or 'webchat' summarize_max_display_length: 0, // 0 = no limit, otherwise max chars shown inline translate: true, - translate_auto: 0, // 0: disabled, 1: manual button, 2: automatic + translate_auto: 0, // 0: disabled, 1: manual button, 2: automatic on message open, 3: generate on email receive translate_display_mode: 'inline', // 'inline' or 'webchat' translate_max_display_length: 0, // 0 = no limit, otherwise max chars shown inline translate_lang: '', // target language, fallback on default_chatgpt_lang diff --git a/pages/translate/mzta-translate.html b/pages/translate/mzta-translate.html index 891dc485..b424bd9f 100644 --- a/pages/translate/mzta-translate.html +++ b/pages/translate/mzta-translate.html @@ -34,6 +34,7 @@ +
      __MSG_prefs_OptionText_translate_auto_Info__ diff --git a/pages/translate/mzta-translate.js b/pages/translate/mzta-translate.js index 84e6953e..a4812808 100644 --- a/pages/translate/mzta-translate.js +++ b/pages/translate/mzta-translate.js @@ -74,6 +74,7 @@ document.addEventListener("DOMContentLoaded", async () => { document.querySelectorAll(".option-input").forEach(element => { element.addEventListener("change", saveOptions); }); + document.getElementById('translate_auto').addEventListener('change', updateDisplayModeConstraint); let translate_textarea = document.getElementById("translate_prompt_text"); let translate_save_btn = document.getElementById("btn_save_prompt"); @@ -118,6 +119,21 @@ document.addEventListener("DOMContentLoaded", async () => { // Methods to manage options, derived from: /options/mzta-options.js +function updateDisplayModeConstraint() { + const translate_auto_el = document.getElementById('translate_auto'); + const display_mode_el = document.getElementById('translate_display_mode'); + const autoVal = String(translate_auto_el.value); + if (autoVal === '2' || autoVal === '3') { + display_mode_el.value = 'inline'; + display_mode_el.disabled = true; + browser.storage.sync.set({ translate_display_mode: 'inline' }); + } else if (autoVal === '0') { + display_mode_el.disabled = true; + } else { + display_mode_el.disabled = false; + } +} + function saveOptions(e) { e.preventDefault(); let options = {}; @@ -232,4 +248,5 @@ async function restoreOptions() { } setCurrentChoice(getting); + updateDisplayModeConstraint(); } From 1b0da7ce619f4fedef7d6ed35a1b6f93e8a1f694 Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 28 Mar 2026 12:02:34 +0100 Subject: [PATCH 080/269] string added to i18n --- 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 54a73a87..8c90dffa 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -1264,7 +1264,7 @@ switch (message.command) { const translationText = document.createElement('div'); translationText.style.cssText = 'white-space: pre-wrap; line-height: 1.5;'; if (translationData.error) { - translationText.textContent = translationData.message || browser.i18n.getMessage("translate_error") || "Translation failed."; + translationText.textContent = translationData.message || browser.i18n.getMessage("translate_error") || browser.i18n.getMessage("translate_error"); } else { translationText.textContent = translationData.translated_text || ''; } From fa6b5c95a9e4a0282c9a779a43575b427ce87572 Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 28 Mar 2026 12:10:00 +0100 Subject: [PATCH 081/269] panels on message ordered --- js/mzta-compose-script.js | 51 ++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 8c90dffa..b4cad1e6 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -45,6 +45,23 @@ function getCleanBodyHtml() { return clone; } +function _updatePanelMargins() { + const panelIds = [ + 'mzta-spam-check-progress', 'mzta-spam-report-banner', + 'mzta-translation-generating', 'mzta-translation-banner', + 'mzta-summary-generating', 'mzta-summary-banner' + ]; + let lastPanel = null; + for (const id of panelIds) { + const el = document.getElementById(id); + if (el) { + el.style.marginBottom = ''; + lastPanel = el; + } + } + if (lastPanel) lastPanel.style.marginBottom = '1rem'; +} + function createThreeDotsMenu(isDark, menuItems, panelColors) { const wrapper = document.createElement('div'); wrapper.style.cssText = 'position: relative; display: inline-flex; align-items: center;'; @@ -775,6 +792,7 @@ switch (message.command) { document.body.insertBefore(reposTriggerWrapperProgress, containerProgress.nextSibling); } + _updatePanelMargins(); return Promise.resolve(true); case "showSpamReport": @@ -876,6 +894,7 @@ switch (message.command) { document.body.insertBefore(reposTriggerWrapper, container.nextSibling); } + _updatePanelMargins(); return Promise.resolve(true); case "showSummary": @@ -907,7 +926,7 @@ 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;`; + summaryContainer.style.cssText = `background-color: ${bgColorSummary}; color: ${textColorSummary}; padding: 0.5rem; border-radius: 4px; border: 1px solid ${borderColorSummary}; font-family: system-ui, -apple-system, sans-serif; font-size: 14px;`; const summaryMenu = createThreeDotsMenu(isDarkSummary, [ { @@ -1076,8 +1095,11 @@ switch (message.command) { summaryBody.appendChild(summaryTextWrapper); summaryContainer.appendChild(summaryBody); + const translationBannerForSummary = document.getElementById('mzta-translation-banner') || document.getElementById('mzta-translation-generating'); const spamBanner = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); - document.body.insertBefore(summaryContainer, spamBanner ? spamBanner.nextSibling : document.body.firstChild); + const insertAfterSummary = translationBannerForSummary || spamBanner; + document.body.insertBefore(summaryContainer, insertAfterSummary ? insertAfterSummary.nextSibling : document.body.firstChild); + _updatePanelMargins(); return Promise.resolve(true); case "showSummaryGenerating": @@ -1102,7 +1124,7 @@ 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; display: flex; align-items: center; gap: 10px;`; + generatingContainer.style.cssText = `background-color: ${bgColorGen}; color: ${textColorGen}; padding: 0.5rem; 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"); @@ -1121,8 +1143,11 @@ switch (message.command) { generatingContainer.appendChild(generatingLoadingImg); generatingContainer.appendChild(generatingTitle); + const translationBannerForSummaryGen = document.getElementById('mzta-translation-banner') || document.getElementById('mzta-translation-generating'); const spamBannerGen = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); - document.body.insertBefore(generatingContainer, spamBannerGen ? spamBannerGen.nextSibling : document.body.firstChild); + const insertAfterSummaryGen = translationBannerForSummaryGen || spamBannerGen; + document.body.insertBefore(generatingContainer, insertAfterSummaryGen ? insertAfterSummaryGen.nextSibling : document.body.firstChild); + _updatePanelMargins(); return Promise.resolve(true); case "showSummaryButton": @@ -1210,7 +1235,7 @@ switch (message.command) { } translationContainer.className = 'thunderai-translation-pane'; - translationContainer.style.cssText = `background-color: ${bgColorTranslation}; color: ${textColorTranslation}; padding: 0.5rem; margin-bottom: 1rem; border-radius: 4px; border: 1px solid ${borderColorTranslation}; font-family: system-ui, -apple-system, sans-serif; font-size: 14px;`; + translationContainer.style.cssText = `background-color: ${bgColorTranslation}; color: ${textColorTranslation}; padding: 0.5rem; border-radius: 4px; border: 1px solid ${borderColorTranslation}; font-family: system-ui, -apple-system, sans-serif; font-size: 14px;`; const translationHeader = document.createElement('div'); translationHeader.style.cssText = 'display: flex; align-items: center; gap: 8px; margin-bottom: 6px;'; @@ -1323,14 +1348,13 @@ switch (message.command) { translationContainer.appendChild(translationTextWrapper); - const summaryBannerForTranslation = document.getElementById('mzta-summary-banner') || document.getElementById('mzta-summary-generating'); const spamBannerForTranslation = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); - const insertAfterTranslation = summaryBannerForTranslation || spamBannerForTranslation; - if (insertAfterTranslation) { - document.body.insertBefore(translationContainer, insertAfterTranslation.nextSibling); + if (spamBannerForTranslation) { + document.body.insertBefore(translationContainer, spamBannerForTranslation.nextSibling); } else { document.body.insertBefore(translationContainer, document.body.firstChild); } + _updatePanelMargins(); return Promise.resolve(true); case "showTranslationGenerating": @@ -1354,7 +1378,7 @@ switch (message.command) { const translationGenContainer = document.createElement('div'); translationGenContainer.id = 'mzta-translation-generating'; translationGenContainer.className = 'thunderai-translation-pane'; - translationGenContainer.style.cssText = `background-color: ${bgColorTranslationGen}; color: ${textColorTranslationGen}; padding: 0.5rem; margin-bottom: 1rem; border-radius: 4px; border: 1px solid ${borderColorTranslationGen}; font-family: system-ui, -apple-system, sans-serif; font-size: 14px; display: flex; align-items: center; gap: 10px;`; + translationGenContainer.style.cssText = `background-color: ${bgColorTranslationGen}; color: ${textColorTranslationGen}; padding: 0.5rem; border-radius: 4px; border: 1px solid ${borderColorTranslationGen}; font-family: system-ui, -apple-system, sans-serif; font-size: 14px; display: flex; align-items: center; gap: 10px;`; const translationGenIcon = document.createElement('img'); translationGenIcon.src = browser.runtime.getURL("/images/ai_summary.png"); @@ -1372,14 +1396,13 @@ switch (message.command) { translationGenContainer.appendChild(translationGenLoadingImg); translationGenContainer.appendChild(translationGenTitle); - const summaryBannerForGen = document.getElementById('mzta-summary-banner') || document.getElementById('mzta-summary-generating'); const spamBannerForGen = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); - const insertAfterGen = summaryBannerForGen || spamBannerForGen; - if (insertAfterGen) { - document.body.insertBefore(translationGenContainer, insertAfterGen.nextSibling); + if (spamBannerForGen) { + document.body.insertBefore(translationGenContainer, spamBannerForGen.nextSibling); } else { document.body.insertBefore(translationGenContainer, document.body.firstChild); } + _updatePanelMargins(); return Promise.resolve(true); case "showTranslationButton": From 13d82d8dbfdbffe67836eb07e1b7218d16d1503a Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 28 Mar 2026 12:16:36 +0100 Subject: [PATCH 082/269] "translation by" added. translation icon added. --- _locales/en/messages.json | 4 ++++ images/ai_translation.png | Bin 0 -> 740 bytes js/mzta-compose-script.js | 11 ++++++++--- 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 images/ai_translation.png diff --git a/_locales/en/messages.json b/_locales/en/messages.json index b8de87e5..a4b1f66a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2169,6 +2169,10 @@ "message": "Summary by", "description": "" }, + "translate_by": { + "message": "Translation by", + "description": "" + }, "prefs_THStats_1": { "message": "Do you want beautiful statistics about your emails?", "description": "" diff --git a/images/ai_translation.png b/images/ai_translation.png new file mode 100644 index 0000000000000000000000000000000000000000..4b5d6cd7418beacff9a62257a1d1f7d98c11e0c9 GIT binary patch literal 740 zcmVIs0k5&VVBAJu%oaRYlVQ|jyHHVcD|uirJ{7W!Xp6*B=#Lo{E_NLU z$$OwZvMa<`yvMN;#9ozAz5rJ$5O{&%oMpcC`45q*SG8-UZ8Ucor*)HUHI z$GxVM@f4N`<#H4sFuG{}r_!}P1Rv%5!C^dCx;>}L9!wGX`VTBEv7aIU7w`yc1h1}1 z$dx5PaQn2xUW!0JA&ebSrdj&k_)Q?yif@)R@-?f?~7^ zKd3_7!J=sJfe^ldn3%B9fE!iVMtG1~g;t#P{jbNCNN`TI1P0-Z?mawbQ5VlWjcZ>N zwDOv2SqNRTQQ8Z2*pGWcc-pZ?@Zj4&u$u{!g-^^S;r2`ZmEmF%EkX!}{SN{?D2rd3 WQK7{K+fP>j0000 Date: Sat, 28 Mar 2026 12:18:15 +0100 Subject: [PATCH 083/269] graphic attribution updated --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 563205bc..c252873c 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ _The language status represents the percentage of translated strings in the late - [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 +- [Hilmy Abiyyu A.](https://www.flaticon.com/authors/hilmy-abiyyu-a) for the ai translate icon
      From 03c241c9743500e082b88b13398541045ec3f122 Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 28 Mar 2026 12:41:59 +0100 Subject: [PATCH 084/269] css fix --- js/mzta-compose-script.js | 65 ++++++++++++++++++++++++++++++++++----- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 0ec1b4b7..74e9a86b 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -784,13 +784,29 @@ switch (message.command) { existingFixedTriggerProgress.style.right = ''; existingFixedTriggerProgress.style.zIndex = ''; existingFixedTriggerProgress.style.marginLeft = 'auto'; - existingFixedTriggerProgress.style.marginTop = '4px'; + existingFixedTriggerProgress.style.marginTop = ''; 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.style.cssText = 'display: flex; justify-content: flex-end; padding: 2px 0.5rem;'; reposTriggerWrapperProgress.appendChild(existingFixedTriggerProgress); document.body.insertBefore(reposTriggerWrapperProgress, containerProgress.nextSibling); } + // Reposition translation trigger button if it exists as fixed + const existingFixedTranslTriggerProgress = document.getElementById('mzta-translation-trigger'); + if (existingFixedTranslTriggerProgress && !document.getElementById('mzta-translation-trigger-wrapper')) { + existingFixedTranslTriggerProgress.style.position = ''; + existingFixedTranslTriggerProgress.style.top = ''; + existingFixedTranslTriggerProgress.style.right = ''; + existingFixedTranslTriggerProgress.style.zIndex = ''; + existingFixedTranslTriggerProgress.style.marginLeft = 'auto'; + existingFixedTranslTriggerProgress.style.marginTop = ''; + const reposTranslTriggerWrapperProgress = document.createElement('div'); + reposTranslTriggerWrapperProgress.id = 'mzta-translation-trigger-wrapper'; + reposTranslTriggerWrapperProgress.style.cssText = 'display: flex; justify-content: flex-end; padding: 2px 0.5rem;'; + reposTranslTriggerWrapperProgress.appendChild(existingFixedTranslTriggerProgress); + const insertAfterForTranslProgress = document.getElementById('mzta-summary-trigger-wrapper') || containerProgress; + document.body.insertBefore(reposTranslTriggerWrapperProgress, insertAfterForTranslProgress.nextSibling); + } _updatePanelMargins(); return Promise.resolve(true); @@ -886,13 +902,29 @@ switch (message.command) { existingFixedTrigger.style.right = ''; existingFixedTrigger.style.zIndex = ''; existingFixedTrigger.style.marginLeft = 'auto'; - existingFixedTrigger.style.marginTop = '4px'; + existingFixedTrigger.style.marginTop = ''; 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.style.cssText = 'display: flex; justify-content: flex-end; padding: 2px 0.5rem;'; reposTriggerWrapper.appendChild(existingFixedTrigger); document.body.insertBefore(reposTriggerWrapper, container.nextSibling); } + // Reposition translation trigger button if it exists as fixed + const existingFixedTranslTrigger = document.getElementById('mzta-translation-trigger'); + if (existingFixedTranslTrigger && !document.getElementById('mzta-translation-trigger-wrapper')) { + existingFixedTranslTrigger.style.position = ''; + existingFixedTranslTrigger.style.top = ''; + existingFixedTranslTrigger.style.right = ''; + existingFixedTranslTrigger.style.zIndex = ''; + existingFixedTranslTrigger.style.marginLeft = 'auto'; + existingFixedTranslTrigger.style.marginTop = ''; + const reposTranslTriggerWrapper = document.createElement('div'); + reposTranslTriggerWrapper.id = 'mzta-translation-trigger-wrapper'; + reposTranslTriggerWrapper.style.cssText = 'display: flex; justify-content: flex-end; padding: 2px 0.5rem;'; + reposTranslTriggerWrapper.appendChild(existingFixedTranslTrigger); + const insertAfterForTransl = document.getElementById('mzta-summary-trigger-wrapper') || container; + document.body.insertBefore(reposTranslTriggerWrapper, insertAfterForTransl.nextSibling); + } _updatePanelMargins(); return Promise.resolve(true); @@ -906,6 +938,12 @@ switch (message.command) { const existingTriggerBtn = document.getElementById('mzta-summary-trigger'); if(existingTriggerBtn) existingTriggerBtn.remove(); + // If translation button is fixed at top:40px (was offset for summary button), move it back to top:8px + const existingTranslTriggerShowSummary = document.getElementById('mzta-translation-trigger'); + if (existingTranslTriggerShowSummary && !document.getElementById('mzta-translation-trigger-wrapper')) { + existingTranslTriggerShowSummary.style.top = '8px'; + } + const summaryBanner = document.getElementById('mzta-summary-banner'); if(summaryBanner) summaryBanner.remove(); @@ -1114,6 +1152,12 @@ switch (message.command) { const existingTrigger = document.getElementById('mzta-summary-trigger'); if(existingTrigger) existingTrigger.remove(); + // If translation button is fixed at top:40px (was offset for summary button), move it back to top:8px + const existingTranslTriggerShowSummaryGen = document.getElementById('mzta-translation-trigger'); + if (existingTranslTriggerShowSummaryGen && !document.getElementById('mzta-translation-trigger-wrapper')) { + existingTranslTriggerShowSummaryGen.style.top = '8px'; + } + const isDarkGen = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; let bgColorGen = isDarkGen ? '#2a2a2a' : '#f0f0f0'; @@ -1203,6 +1247,11 @@ switch (message.command) { document.body.insertBefore(triggerWrapper, spamBannerTrigger.nextSibling); } else { document.body.appendChild(triggerBtn); + // If translation button is also fixed, push it down to avoid overlap + const existingTranslFixedBtn = document.getElementById('mzta-translation-trigger'); + if (existingTranslFixedBtn && !document.getElementById('mzta-translation-trigger-wrapper')) { + existingTranslFixedBtn.style.top = '40px'; + } } return Promise.resolve(true); @@ -1421,16 +1470,18 @@ switch (message.command) { let borderColorTranslationBtn = isDarkTranslationBtn ? '#2e5740' : '#a5d6a7'; const spamBannerTranslationTrigger = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); - const summaryBannerTranslationTrigger = document.getElementById('mzta-summary-banner') || document.getElementById('mzta-summary-trigger-wrapper') || document.getElementById('mzta-summary-trigger'); + const summaryPanelTranslationTrigger = document.getElementById('mzta-summary-banner') || document.getElementById('mzta-summary-trigger-wrapper'); + const summaryFixedBtnTranslationTrigger = document.getElementById('mzta-summary-trigger'); const translationTriggerBtn = document.createElement('div'); translationTriggerBtn.id = 'mzta-translation-trigger'; translationTriggerBtn.title = browser.i18n.getMessage("translate_click_to_generate") || "Click to translate this email"; const translationTriggerBtnBase = `background-color: ${bgColorTranslationBtn}; border: 1px solid ${borderColorTranslationBtn}; 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: ${textColorTranslationBtn}; display: inline-flex; align-items: center; gap: 6px; width: fit-content;`; - const insertAfterTranslationBtn = summaryBannerTranslationTrigger || spamBannerTranslationTrigger; + const insertAfterTranslationBtn = summaryPanelTranslationTrigger || spamBannerTranslationTrigger; if (insertAfterTranslationBtn) { translationTriggerBtn.style.cssText = translationTriggerBtnBase + ' margin-left: auto; margin-top: 4px;'; } else { - translationTriggerBtn.style.cssText = translationTriggerBtnBase + ' position: fixed; top: 8px; right: 8px; z-index: 9997;'; + const topOffset = summaryFixedBtnTranslationTrigger ? '40px' : '8px'; + translationTriggerBtn.style.cssText = translationTriggerBtnBase + ` position: fixed; top: ${topOffset}; right: 8px; z-index: 9997;`; } const translationTriggerIcon = document.createElement('img'); From 7580efb6616fd5a790b25026f4a46defbfaba060 Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 28 Mar 2026 12:46:04 +0100 Subject: [PATCH 085/269] cache renamed to storage --- _locales/en/messages.json | 26 +++++++++++++------------- options/mzta-options.html | 6 +++--- options/mzta-options.js | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index a4b1f66a..0524c07b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -335,25 +335,25 @@ "message": "Manage your data placeholders", "description": "" }, - "prefs_cache_title": { - "message": "Cache Storage", - "description": "Title for cache management section in options" + "prefs_storage_title": { + "message": "Additional Info Storage", + "description": "Title for additional info storage management section in options" }, - "prefs_cache_storage_size": { - "message": "Cache size", - "description": "Label for cache storage size display" + "prefs_storage_size": { + "message": "Storage size", + "description": "Label for storage size display" }, - "prefs_cache_clear_button": { - "message": "Clear Cache", - "description": "Button to clear all cached message data" + "prefs_storage_clear_button": { + "message": "Clear Storage", + "description": "Button to clear all stored message data" }, "prefs_cache_clear_confirm": { - "message": "Are you sure you want to clear all cached data (summaries, spam reports, translations)? This action cannot be undone.", - "description": "Confirmation dialog for clearing cache" + "message": "Are you sure you want to clear all stored data (summaries, spam reports, translations)? This action cannot be undone.", + "description": "Confirmation dialog for clearing storage" }, - "prefs_cache_clear_done": { + "prefs_storage_clear_done": { "message": "$COUNT$ records removed.", - "description": "Message shown after cache is cleared", + "description": "Message shown after storage is cleared", "placeholders": { "count": { "content": "$1" diff --git a/options/mzta-options.html b/options/mzta-options.html index aa2231e5..4d6137ae 100644 --- a/options/mzta-options.html +++ b/options/mzta-options.html @@ -253,10 +253,10 @@ - __MSG_prefs_cache_title__ + __MSG_prefs_storage_title__ - __MSG_prefs_cache_storage_size__: -   + __MSG_prefs_storage_size__: +   diff --git a/options/mzta-options.js b/options/mzta-options.js index 9231eaf9..6d415f1d 100644 --- a/options/mzta-options.js +++ b/options/mzta-options.js @@ -417,7 +417,7 @@ document.addEventListener('DOMContentLoaded', async () => { return; } let count = await taStorage.clearAllRecords(); - alert(browser.i18n.getMessage("prefs_cache_clear_done", [String(count)])); + alert(browser.i18n.getMessage("prefs_storage_clear_done", [String(count)])); updateCacheSize(); }); From 226599ed2ae32cf253fa43c2e64d220990511a5d Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 28 Mar 2026 12:46:42 +0100 Subject: [PATCH 086/269] cache renamed to storage --- _locales/en/messages.json | 2 +- options/mzta-options.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 0524c07b..ae58fd9c 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -347,7 +347,7 @@ "message": "Clear Storage", "description": "Button to clear all stored message data" }, - "prefs_cache_clear_confirm": { + "prefs_storage_clear_confirm": { "message": "Are you sure you want to clear all stored data (summaries, spam reports, translations)? This action cannot be undone.", "description": "Confirmation dialog for clearing storage" }, diff --git a/options/mzta-options.js b/options/mzta-options.js index 6d415f1d..eda0c48f 100644 --- a/options/mzta-options.js +++ b/options/mzta-options.js @@ -413,7 +413,7 @@ document.addEventListener('DOMContentLoaded', async () => { updateCacheSize(); document.getElementById('btnClearCache').addEventListener('click', async () => { - if (!confirm(browser.i18n.getMessage("prefs_cache_clear_confirm"))) { + if (!confirm(browser.i18n.getMessage("prefs_storage_clear_confirm"))) { return; } let count = await taStorage.clearAllRecords(); From 374d598d70bb4b12c028e72ca6bae68af8aeec52 Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 28 Mar 2026 12:46:04 +0100 Subject: [PATCH 087/269] cache renamed to storage --- _locales/en/messages.json | 26 +++++++++++++------------- options/mzta-options.html | 6 +++--- options/mzta-options.js | 2 +- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 2c8d22d3..f0954250 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -331,25 +331,25 @@ "message": "Manage your data placeholders", "description": "" }, - "prefs_cache_title": { - "message": "Cache Storage", - "description": "Title for cache management section in options" + "prefs_storage_title": { + "message": "Additional Info Storage", + "description": "Title for additional info storage management section in options" }, - "prefs_cache_storage_size": { - "message": "Cache size", - "description": "Label for cache storage size display" + "prefs_storage_size": { + "message": "Storage size", + "description": "Label for storage size display" }, - "prefs_cache_clear_button": { - "message": "Clear Cache", - "description": "Button to clear all cached message data" + "prefs_storage_clear_button": { + "message": "Clear Storage", + "description": "Button to clear all stored message data" }, "prefs_cache_clear_confirm": { - "message": "Are you sure you want to clear all cached data (summaries, spam reports, translations)? This action cannot be undone.", - "description": "Confirmation dialog for clearing cache" + "message": "Are you sure you want to clear all stored data (summaries, spam reports, translations)? This action cannot be undone.", + "description": "Confirmation dialog for clearing storage" }, - "prefs_cache_clear_done": { + "prefs_storage_clear_done": { "message": "$COUNT$ records removed.", - "description": "Message shown after cache is cleared", + "description": "Message shown after storage is cleared", "placeholders": { "count": { "content": "$1" diff --git a/options/mzta-options.html b/options/mzta-options.html index 0033a590..3e23111e 100644 --- a/options/mzta-options.html +++ b/options/mzta-options.html @@ -242,10 +242,10 @@ - __MSG_prefs_cache_title__ + __MSG_prefs_storage_title__ - __MSG_prefs_cache_storage_size__: -   + __MSG_prefs_storage_size__: +   diff --git a/options/mzta-options.js b/options/mzta-options.js index 51baa538..9adb6099 100644 --- a/options/mzta-options.js +++ b/options/mzta-options.js @@ -383,7 +383,7 @@ document.addEventListener('DOMContentLoaded', async () => { return; } let count = await taStorage.clearAllRecords(); - alert(browser.i18n.getMessage("prefs_cache_clear_done", [String(count)])); + alert(browser.i18n.getMessage("prefs_storage_clear_done", [String(count)])); updateCacheSize(); }); From 1650322f4d860b1fc7b2c51d4613bf0f804f56f8 Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 28 Mar 2026 12:46:42 +0100 Subject: [PATCH 088/269] cache renamed to storage --- _locales/en/messages.json | 2 +- options/mzta-options.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index f0954250..88e4357a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -343,7 +343,7 @@ "message": "Clear Storage", "description": "Button to clear all stored message data" }, - "prefs_cache_clear_confirm": { + "prefs_storage_clear_confirm": { "message": "Are you sure you want to clear all stored data (summaries, spam reports, translations)? This action cannot be undone.", "description": "Confirmation dialog for clearing storage" }, diff --git a/options/mzta-options.js b/options/mzta-options.js index 9adb6099..eac61c34 100644 --- a/options/mzta-options.js +++ b/options/mzta-options.js @@ -379,7 +379,7 @@ document.addEventListener('DOMContentLoaded', async () => { updateCacheSize(); document.getElementById('btnClearCache').addEventListener('click', async () => { - if (!confirm(browser.i18n.getMessage("prefs_cache_clear_confirm"))) { + if (!confirm(browser.i18n.getMessage("prefs_storage_clear_confirm"))) { return; } let count = await taStorage.clearAllRecords(); From a1632f628edc78bb13e0528995ddcf511af60a75 Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 28 Mar 2026 12:50:06 +0100 Subject: [PATCH 089/269] sv locale updated --- _locales/sv/messages.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index ec2b7af2..8cd8802e 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -1461,19 +1461,19 @@ "webchat_save_as_summary": { "message": "Spara som sammanfattning" }, - "prefs_cache_title": { + "prefs_storage_title": { "message": "Cachelagring" }, - "prefs_cache_storage_size": { + "prefs_storage_size": { "message": "Cachestorlek" }, - "prefs_cache_clear_button": { + "prefs_storage_clear_button": { "message": "Rensa cache" }, - "prefs_cache_clear_confirm": { + "prefs_storage_clear_confirm": { "message": "Är du säker på att du vill rensa all cachad data (sammanfattningar, skräppostrapporter, översättningar)? Den här åtgärden kan inte ångras." }, - "prefs_cache_clear_done": { + "prefs_storage_clear_done": { "message": "$COUNT$ register borttagna.", "placeholders": { "count": { From 0945ea7d6e8174b04b8824d0f1ba104fb3cddaab Mon Sep 17 00:00:00 2001 From: mic Date: Sat, 28 Mar 2026 18:51:42 +0100 Subject: [PATCH 090/269] spec file updated --- claude-spec/01-architecture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/claude-spec/01-architecture.md b/claude-spec/01-architecture.md index 7c31e7a4..dcc4c2d0 100644 --- a/claude-spec/01-architecture.md +++ b/claude-spec/01-architecture.md @@ -38,7 +38,7 @@ js/mzta-prompts.js (builds final prompt string) ↓ Result returned to background ↓ -js/mzta-compose-script.js (inserts text into Thunderbird compose window) +js/mzta-compose-script.js (inserts text into Thunderbird compose window and display window) ``` ### Data Flow: Inline Summary on Message Display From 1018fa777d68f8fed2ed6646d6009f0925817883 Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 29 Mar 2026 22:25:25 +0200 Subject: [PATCH 091/269] improved the UI of the in messages panels --- claude-spec/01-architecture.md | 2 +- js/mzta-compose-script.js | 821 ++++++++++++++------------------- 2 files changed, 350 insertions(+), 473 deletions(-) diff --git a/claude-spec/01-architecture.md b/claude-spec/01-architecture.md index dcc4c2d0..8f5e3e42 100644 --- a/claude-spec/01-architecture.md +++ b/claude-spec/01-architecture.md @@ -168,7 +168,7 @@ taTranslationStore.saveTranslation() | `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, `buildSummaryPrompt()` for unified summary prompt assembly, `buildTranslationPrompt()` for translation 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-compose-script.js` | Content script for compose and message display: injects AI response into compose window, renders unified toolbar (spam badge, summary/translation trigger buttons) and content panels (spam explanation, summary, translation) in message display via `#mzta-container` | | `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 | diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 74e9a86b..663d8c42 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -21,8 +21,7 @@ // 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-container', '.mzta_dialog', ]; @@ -45,20 +44,146 @@ function getCleanBodyHtml() { return clone; } -function _updatePanelMargins() { - const panelIds = [ - 'mzta-spam-check-progress', 'mzta-spam-report-banner', - 'mzta-translation-generating', 'mzta-translation-banner', - 'mzta-summary-generating', 'mzta-summary-banner' - ]; - let lastPanel = null; - for (const id of panelIds) { - const el = document.getElementById(id); - if (el) { - el.style.marginBottom = ''; - lastPanel = el; +// ── Theme colors ──────────────────────────────────────────────────── +function _getThemeColors(spamValue, spamThreshold) { + const isDark = window.matchMedia?.('(prefers-color-scheme: dark)').matches; + const colors = { + isDark, + toolbar: { bg: isDark ? '#1e1e1e' : '#f5f5f5', text: isDark ? '#ddd' : '#333', border: isDark ? '#444' : '#ddd' }, + spamLoading: { bg: isDark ? '#003366' : '#e6f2ff', text: isDark ? '#cce5ff' : '#004085', border: isDark ? '#004085' : '#b8daff' }, + summary: { bg: isDark ? '#2a2a2a' : '#f0f0f0', text: isDark ? '#e0e0e0' : '#333', border: isDark ? '#444' : '#ddd' }, + summaryErr: { bg: isDark ? '#3a1a1a' : '#f7e6e6', text: isDark ? '#ffcccc' : '#660000', border: '#660000' }, + translation: { bg: isDark ? '#1a2e2a' : '#e8f5e9', text: isDark ? '#c8e6c9' : '#1b5e20', border: isDark ? '#2e5740' : '#a5d6a7' }, + translErr: { bg: isDark ? '#3a1a1a' : '#f7e6e6', text: isDark ? '#ffcccc' : '#660000', border: '#660000' }, + linkColor: isDark ? '#6db3f2' : '#1a5fa8', + }; + // Spam colors depend on the score + if (spamValue !== undefined) { + const threshold = spamThreshold || 50; + if (spamValue == -999) { + colors.spam = { bg: isDark ? '#332701' : '#fff3cd', text: isDark ? '#ffeb80' : '#856404', border: isDark ? '#664d03' : '#ffeeba' }; + } else if (spamValue >= threshold) { + colors.spam = { bg: isDark ? '#5a1a1a' : '#ffe6e6', text: isDark ? '#ffcccc' : '#cc0000', border: '#cc0000' }; + } else { + colors.spam = { bg: isDark ? '#1a401a' : '#e6ffe6', text: isDark ? '#ccffcc' : '#006600', border: '#006600' }; } } + return colors; +} + +// ── Container / Toolbar / Panels management ───────────────────────── +function _ensureContainer() { + let container = document.getElementById('mzta-container'); + if (!container) { + const colors = _getThemeColors(); + container = document.createElement('div'); + container.id = 'mzta-container'; + container.style.cssText = 'font-family: system-ui, -apple-system, sans-serif;'; + document.body.insertBefore(container, document.body.firstChild); + + const toolbar = document.createElement('div'); + toolbar.id = 'mzta-toolbar'; + toolbar.style.cssText = `display: none; align-items: center; gap: 8px; padding: 6px 0.5rem; background-color: ${colors.toolbar.bg}; border-bottom: 1px solid ${colors.toolbar.border}; font-size: 13px; color: ${colors.toolbar.text};`; + container.appendChild(toolbar); + + const panels = document.createElement('div'); + panels.id = 'mzta-panels'; + panels.style.cssText = 'display: flex; flex-direction: column; gap: 4px; padding: 4px 0;'; + container.appendChild(panels); + } + return { + toolbar: document.getElementById('mzta-toolbar'), + panels: document.getElementById('mzta-panels'), + }; +} + +function _updateToolbarVisibility() { + const toolbar = document.getElementById('mzta-toolbar'); + if (!toolbar) return; + const hasItems = toolbar.querySelector('#mzta-toolbar-spam, #mzta-toolbar-summary, #mzta-toolbar-translation'); + toolbar.style.display = hasItems ? 'flex' : 'none'; +} + +function _ensureBranding(toolbar) { + if (document.getElementById('mzta-toolbar-branding')) return; + const branding = document.createElement('span'); + branding.id = 'mzta-toolbar-branding'; + branding.textContent = 'by ThunderAI'; + branding.style.cssText = 'margin-left: auto; font-style: italic; font-size: 10px; opacity: 0.5; white-space: nowrap;'; + toolbar.appendChild(branding); +} + +const _TOOLBAR_SLOT_ORDER = ['mzta-toolbar-spam', 'mzta-toolbar-summary', 'mzta-toolbar-translation', 'mzta-toolbar-branding']; + +function _addToolbarItem(id, element) { + const { toolbar } = _ensureContainer(); + const existing = document.getElementById(id); + if (existing) existing.remove(); + element.id = id; + + const myIndex = _TOOLBAR_SLOT_ORDER.indexOf(id); + let insertBefore = null; + for (let i = myIndex + 1; i < _TOOLBAR_SLOT_ORDER.length; i++) { + const later = document.getElementById(_TOOLBAR_SLOT_ORDER[i]); + if (later) { insertBefore = later; break; } + } + if (insertBefore) toolbar.insertBefore(element, insertBefore); + else toolbar.appendChild(element); + + _ensureBranding(toolbar); + _updateToolbarVisibility(); +} + +function _removeToolbarItem(id) { + const el = document.getElementById(id); + if (el) el.remove(); + // Remove branding if toolbar is now empty of content slots + const toolbar = document.getElementById('mzta-toolbar'); + if (toolbar && !toolbar.querySelector('#mzta-toolbar-spam, #mzta-toolbar-summary, #mzta-toolbar-translation')) { + const branding = document.getElementById('mzta-toolbar-branding'); + if (branding) branding.remove(); + } + _updateToolbarVisibility(); +} + +const _PANEL_ORDER = [ + 'mzta-spam-check-progress', 'mzta-spam-report-banner', + 'mzta-translation-generating', 'mzta-translation-banner', + 'mzta-summary-generating', 'mzta-summary-banner' +]; + +function _addPanel(id, element) { + const { panels } = _ensureContainer(); + const existing = document.getElementById(id); + if (existing) existing.remove(); + element.id = id; + + const myIndex = _PANEL_ORDER.indexOf(id); + let insertBefore = null; + for (let i = myIndex + 1; i < _PANEL_ORDER.length; i++) { + const later = document.getElementById(_PANEL_ORDER[i]); + if (later) { insertBefore = later; break; } + } + if (insertBefore) panels.insertBefore(element, insertBefore); + else panels.appendChild(element); + + _updatePanelMargins(); +} + +function _removePanel(id) { + const el = document.getElementById(id); + if (el) el.remove(); + _updatePanelMargins(); +} + +function _updatePanelMargins() { + const panels = document.getElementById('mzta-panels'); + if (!panels) return; + let lastPanel = null; + for (const child of panels.children) { + child.style.marginBottom = ''; + lastPanel = child; + } if (lastPanel) lastPanel.style.marginBottom = '1rem'; } @@ -742,114 +867,81 @@ switch (message.command) { break; - case "showSpamCheckInProgress": - const oldBanner = document.getElementById('mzta-spam-report-banner'); - if(oldBanner) oldBanner.remove(); + case "showSpamCheckInProgress": { + _removePanel('mzta-spam-report-banner'); + _removeToolbarItem('mzta-toolbar-spam'); + if (document.getElementById('mzta-spam-check-progress')) return Promise.resolve(true); - if(document.getElementById('mzta-spam-check-progress')) return Promise.resolve(true); + const colors = _getThemeColors(); - const containerProgress = document.createElement('div'); - containerProgress.id = 'mzta-spam-check-progress'; - - const isDarkProgress = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; - - let bgColorProgress = isDarkProgress ? '#003366' : '#e6f2ff'; - let textColorProgress = isDarkProgress ? '#cce5ff' : '#004085'; - let borderColorProgress = isDarkProgress ? '#004085' : '#b8daff'; + // Loading badge in toolbar + const badge = document.createElement('div'); + badge.style.cssText = `background-color: ${colors.spamLoading.bg}; color: ${colors.spamLoading.text}; border: 1px solid ${colors.spamLoading.border}; border-radius: 4px; padding: 2px 8px; font-size: 12px; display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;`; + const badgeLoading = document.createElement('img'); + badgeLoading.src = browser.runtime.getURL("/images/loading.gif"); + badgeLoading.style.cssText = "height: 14px; width: 14px;"; + badge.appendChild(badgeLoading); + const badgeText = document.createElement('span'); + badgeText.textContent = browser.i18n.getMessage("spam_check_in_progress"); + badge.appendChild(badgeText); + _addToolbarItem('mzta-toolbar-spam', badge); - containerProgress.style.cssText = `background-color: ${bgColorProgress}; color: ${textColorProgress}; border-bottom: 1px solid ${borderColorProgress}; 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;`; + // Loading panel + const panel = document.createElement('div'); + panel.style.cssText = `background-color: ${colors.spamLoading.bg}; color: ${colors.spamLoading.text}; border: 1px solid ${colors.spamLoading.border}; border-radius: 4px; padding: 8px 0.5rem; font-size: 13px; display: flex; align-items: center; gap: 15px; width: 100%; box-sizing: border-box;`; + const panelLoading = document.createElement('img'); + panelLoading.src = browser.runtime.getURL("/images/loading.gif"); + panelLoading.style.cssText = "height: 16px; width: 16px;"; + const panelText = document.createElement('strong'); + panelText.textContent = browser.i18n.getMessage("spam_check_in_progress"); + panel.appendChild(panelLoading); + panel.appendChild(panelText); - const textProgress = document.createElement('strong'); - textProgress.textContent = browser.i18n.getMessage("spam_check_in_progress"); - - const loadingImg = document.createElement('img'); - loadingImg.src = browser.runtime.getURL("/images/loading.gif"); - loadingImg.style.cssText = "height: 16px; width: 16px;"; - - const brandingProgress = document.createElement('span'); - brandingProgress.textContent = browser.i18n.getMessage("antispam_by") + " ThunderAI"; - brandingProgress.style.cssText = 'margin-left: auto; font-style: italic; font-size: 11px; opacity: 0.7;'; - - containerProgress.appendChild(loadingImg); - containerProgress.appendChild(textProgress); - 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 = ''; - const reposTriggerWrapperProgress = document.createElement('div'); - reposTriggerWrapperProgress.id = 'mzta-summary-trigger-wrapper'; - reposTriggerWrapperProgress.style.cssText = 'display: flex; justify-content: flex-end; padding: 2px 0.5rem;'; - reposTriggerWrapperProgress.appendChild(existingFixedTriggerProgress); - document.body.insertBefore(reposTriggerWrapperProgress, containerProgress.nextSibling); - } - // Reposition translation trigger button if it exists as fixed - const existingFixedTranslTriggerProgress = document.getElementById('mzta-translation-trigger'); - if (existingFixedTranslTriggerProgress && !document.getElementById('mzta-translation-trigger-wrapper')) { - existingFixedTranslTriggerProgress.style.position = ''; - existingFixedTranslTriggerProgress.style.top = ''; - existingFixedTranslTriggerProgress.style.right = ''; - existingFixedTranslTriggerProgress.style.zIndex = ''; - existingFixedTranslTriggerProgress.style.marginLeft = 'auto'; - existingFixedTranslTriggerProgress.style.marginTop = ''; - const reposTranslTriggerWrapperProgress = document.createElement('div'); - reposTranslTriggerWrapperProgress.id = 'mzta-translation-trigger-wrapper'; - reposTranslTriggerWrapperProgress.style.cssText = 'display: flex; justify-content: flex-end; padding: 2px 0.5rem;'; - reposTranslTriggerWrapperProgress.appendChild(existingFixedTranslTriggerProgress); - const insertAfterForTranslProgress = document.getElementById('mzta-summary-trigger-wrapper') || containerProgress; - document.body.insertBefore(reposTranslTriggerWrapperProgress, insertAfterForTranslProgress.nextSibling); - } - - _updatePanelMargins(); + _addPanel('mzta-spam-check-progress', panel); return Promise.resolve(true); + } - case "showSpamReport": - const progressBanner = document.getElementById('mzta-spam-check-progress'); - if(progressBanner) progressBanner.remove(); + case "showSpamReport": { + _removePanel('mzta-spam-check-progress'); + _removeToolbarItem('mzta-toolbar-spam'); const data = message.data; - if(document.getElementById('mzta-spam-report-banner')) return Promise.resolve(true); + if (document.getElementById('mzta-spam-report-banner')) return Promise.resolve(true); - const container = document.createElement('div'); - container.id = 'mzta-spam-report-banner'; - - const isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; - - let bgColor = '#f8f9fa'; - let textColor = '#333'; - let borderColor = '#ccc'; - + const colors = _getThemeColors(data.spamValue, data.SpamThreshold); + const sc = colors.spam; + + // Spam badge in toolbar (clickable to toggle explanation panel) + const badge = document.createElement('div'); + badge.style.cssText = `background-color: ${sc.bg}; color: ${sc.text}; border: 1px solid ${sc.border}; border-radius: 4px; padding: 2px 8px; font-size: 12px; font-weight: bold; display: inline-flex; align-items: center; gap: 4px; cursor: pointer; white-space: nowrap; transition: opacity 0.2s;`; if (data.spamValue == -999) { - bgColor = isDark ? '#332701' : '#fff3cd'; - textColor = isDark ? '#ffeb80' : '#856404'; - borderColor = isDark ? '#664d03' : '#ffeeba'; - } else if (data.spamValue >= (data.SpamThreshold || 50)) { - bgColor = isDark ? '#5a1a1a' : '#ffe6e6'; - textColor = isDark ? '#ffcccc' : '#cc0000'; - borderColor = '#cc0000'; + badge.textContent = browser.i18n.getMessage("apiwebchat_error"); } else { - bgColor = isDark ? '#1a401a' : '#e6ffe6'; - textColor = isDark ? '#ccffcc' : '#006600'; - borderColor = '#006600'; + badge.textContent = ((data.spamValue >= (data.SpamThreshold || 50)) ? "\u26A0\uFE0F " + browser.i18n.getMessage("Spam") : "\uD83D\uDEE1\uFE0F " + browser.i18n.getMessage("Valid")) + " [" + data.spamValue + "/100]"; } + const chevron = document.createElement('span'); + chevron.textContent = ' \u25BC'; + chevron.style.cssText = 'font-size: 10px; transition: transform 0.2s; transform: rotate(-90deg);'; + badge.appendChild(chevron); - 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;`; + let spamExpanded = false; + badge.onclick = () => { + const panel = document.getElementById('mzta-spam-report-banner'); + if (panel) { + spamExpanded = !spamExpanded; + panel.style.display = spamExpanded ? 'flex' : 'none'; + chevron.style.transform = spamExpanded ? '' : 'rotate(-90deg)'; + } + }; + badge.onmouseover = () => { badge.style.opacity = '0.8'; }; + badge.onmouseout = () => { badge.style.opacity = '1'; }; + + _addToolbarItem('mzta-toolbar-spam', badge); + + // Explanation panel (collapsed by default) + const panel = document.createElement('div'); + panel.style.cssText = `background-color: ${sc.bg}; color: ${sc.text}; border: 1px solid ${sc.border}; border-radius: 4px; padding: 8px 0.5rem; font-size: 13px; display: none; align-items: center; gap: 15px; width: 100%; box-sizing: border-box;`; - const scoreText = document.createElement('strong'); - if (data.spamValue == -999) { - scoreText.textContent = browser.i18n.getMessage("apiwebchat_error"); - } else { - scoreText.textContent = ((data.spamValue >= (data.SpamThreshold || 50)) ? "⚠️ " + browser.i18n.getMessage("Spam") : "🛡️ " + browser.i18n.getMessage("Valid")) + " [" + data.spamValue + "/100]"; - } - const reasonText = document.createElement('span'); if (data.spamValue == -999) { reasonText.textContent = data.explanation; @@ -857,138 +949,76 @@ switch (message.command) { reasonText.textContent = browser.i18n.getMessage("Explanation") + ": " + data.explanation; } - const branding = document.createElement('span'); - branding.textContent = browser.i18n.getMessage("antispam_by") + " ThunderAI"; - branding.style.cssText = 'margin-left: auto; font-style: italic; font-size: 10px; opacity: 0.5;'; - - const spamMenu = createThreeDotsMenu(isDark, [ + const spamMenu = createThreeDotsMenu(colors.isDark, [ { - icon: '↻', + icon: '\u21BB', label: browser.i18n.getMessage("spamfilter_refresh") || 'Refresh spam report', - hoverColor: isDark ? '#4d9de0' : '#1a5fa8', + hoverColor: colors.isDark ? '#4d9de0' : '#1a5fa8', disableAfterClick: true, onClick: () => { browser.runtime.sendMessage({ command: "refreshSpamReport", headerMessageId: data.headerMessageId }); } }, { - icon: '×', + icon: '\u00D7', label: browser.i18n.getMessage("spamfilter_delete") || 'Delete spam report', hoverColor: '#cc0000', onClick: () => { - container.remove(); + _removePanel('mzta-spam-report-banner'); + _removeToolbarItem('mzta-toolbar-spam'); browser.runtime.sendMessage({ command: "removeSpamReport", headerMessageId: data.headerMessageId }); } } - ], { bg: bgColor, border: borderColor, text: textColor }); + ], { bg: sc.bg, border: sc.border, text: sc.text }); - const spamRightGroup = document.createElement('span'); - spamRightGroup.style.cssText = 'margin-left: auto; margin-right:1px; display: flex; align-items: center; gap: 5px;'; + const rightGroup = document.createElement('span'); + rightGroup.style.cssText = 'margin-left: auto; display: flex; align-items: center; gap: 5px;'; + const branding = document.createElement('span'); + branding.textContent = browser.i18n.getMessage("antispam_by") + " ThunderAI"; branding.style.cssText = 'font-style: italic; font-size: 10px; opacity: 0.5;'; - spamRightGroup.appendChild(branding); - spamRightGroup.appendChild(spamMenu); + rightGroup.appendChild(branding); + rightGroup.appendChild(spamMenu); - container.appendChild(scoreText); - container.appendChild(reasonText); - container.appendChild(spamRightGroup); + panel.appendChild(reasonText); + panel.appendChild(rightGroup); - 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 = ''; - const reposTriggerWrapper = document.createElement('div'); - reposTriggerWrapper.id = 'mzta-summary-trigger-wrapper'; - reposTriggerWrapper.style.cssText = 'display: flex; justify-content: flex-end; padding: 2px 0.5rem;'; - reposTriggerWrapper.appendChild(existingFixedTrigger); - document.body.insertBefore(reposTriggerWrapper, container.nextSibling); - } - // Reposition translation trigger button if it exists as fixed - const existingFixedTranslTrigger = document.getElementById('mzta-translation-trigger'); - if (existingFixedTranslTrigger && !document.getElementById('mzta-translation-trigger-wrapper')) { - existingFixedTranslTrigger.style.position = ''; - existingFixedTranslTrigger.style.top = ''; - existingFixedTranslTrigger.style.right = ''; - existingFixedTranslTrigger.style.zIndex = ''; - existingFixedTranslTrigger.style.marginLeft = 'auto'; - existingFixedTranslTrigger.style.marginTop = ''; - const reposTranslTriggerWrapper = document.createElement('div'); - reposTranslTriggerWrapper.id = 'mzta-translation-trigger-wrapper'; - reposTranslTriggerWrapper.style.cssText = 'display: flex; justify-content: flex-end; padding: 2px 0.5rem;'; - reposTranslTriggerWrapper.appendChild(existingFixedTranslTrigger); - const insertAfterForTransl = document.getElementById('mzta-summary-trigger-wrapper') || container; - document.body.insertBefore(reposTranslTriggerWrapper, insertAfterForTransl.nextSibling); - } - - _updatePanelMargins(); + _addPanel('mzta-spam-report-banner', panel); return Promise.resolve(true); + } - case "showSummary": - 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(); - - // If translation button is fixed at top:40px (was offset for summary button), move it back to top:8px - const existingTranslTriggerShowSummary = document.getElementById('mzta-translation-trigger'); - if (existingTranslTriggerShowSummary && !document.getElementById('mzta-translation-trigger-wrapper')) { - existingTranslTriggerShowSummary.style.top = '8px'; - } - - const summaryBanner = document.getElementById('mzta-summary-banner'); - if(summaryBanner) summaryBanner.remove(); + case "showSummary": { + _removePanel('mzta-summary-generating'); + _removePanel('mzta-summary-banner'); + _removeToolbarItem('mzta-toolbar-summary'); const summaryData = message.data; + const colors = _getThemeColors(); + const sc = summaryData.error ? colors.summaryErr : colors.summary; + 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'; - - if (summaryData.error) { - bgColorSummary = isDarkSummary ? '#3a1a1a' : '#f7e6e6'; - textColorSummary = isDarkSummary ? '#ffcccc' : '#660000'; - borderColorSummary = '#660000'; - } - summaryContainer.className = 'thunderai-summary-pane'; - summaryContainer.style.cssText = `background-color: ${bgColorSummary}; color: ${textColorSummary}; padding: 0.5rem; border-radius: 4px; border: 1px solid ${borderColorSummary}; font-family: system-ui, -apple-system, sans-serif; font-size: 14px;`; + summaryContainer.style.cssText = `background-color: ${sc.bg}; color: ${sc.text}; padding: 0.5rem; border-radius: 4px; border: 1px solid ${sc.border}; font-size: 14px;`; - const summaryMenu = createThreeDotsMenu(isDarkSummary, [ + const summaryMenu = createThreeDotsMenu(colors.isDark, [ { - icon: '↻', + icon: '\u21BB', label: browser.i18n.getMessage("summarize_refresh") || 'Refresh summary', - hoverColor: isDarkSummary ? '#4d9de0' : '#1a5fa8', + hoverColor: colors.isDark ? '#4d9de0' : '#1a5fa8', disableAfterClick: true, onClick: () => { - browser.runtime.sendMessage({ - command: "refreshSummary", - headerMessageId: summaryData.headerMessageId - }); + browser.runtime.sendMessage({ command: "refreshSummary", headerMessageId: summaryData.headerMessageId }); } }, { - icon: '×', + icon: '\u00D7', label: browser.i18n.getMessage("summarize_delete") || 'Delete summary', hoverColor: '#cc0000', onClick: () => { - summaryContainer.remove(); + _removePanel('mzta-summary-banner'); browser.runtime.sendMessage({ command: "removeSummary", headerMessageId: summaryData.headerMessageId }); } } - ], { bg: bgColorSummary, border: borderColorSummary, text: textColorSummary }); + ], { bg: sc.bg, border: sc.border, text: sc.text }); const summaryBranding = document.createElement('span'); summaryBranding.textContent = browser.i18n.getMessage("summary_by") + " ThunderAI"; @@ -1001,18 +1031,16 @@ switch (message.command) { const summaryIcon = document.createElement('img'); summaryIcon.src = browser.runtime.getURL("/images/ai_summary.png"); - summaryIcon.style.cssText = `height: 16px; width: 16px; flex-shrink: 0; margin-top: 2px;${isDarkSummary ? ' filter: invert(1);' : ''}`; + summaryIcon.style.cssText = `height: 16px; width: 16px; flex-shrink: 0; margin-top: 2px;${colors.isDark ? ' 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'; 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(); @@ -1030,57 +1058,45 @@ switch (message.command) { } else { summaryText.textContent = summaryData.summary; } - summaryText.style.cssText = `font-size: 14px; line-height: 1.4;`; - + summaryText.style.cssText = 'font-size: 14px; line-height: 1.4;'; summaryTextWrapper.appendChild(summaryText); const maxLen = summaryData.maxDisplayLength || 0; const fullText = summaryData.summary; if (!summaryData.error && maxLen > 0 && fullText && fullText.length > maxLen) { - // Set up animated expand/collapse via max-height transition summaryText.style.overflow = 'hidden'; summaryText.style.transition = 'max-height 0.2s ease'; 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; - // Measure truncated height after layout requestAnimationFrame(() => { - const collapsedHeight = summaryText.scrollHeight; - summaryText.style.maxHeight = collapsedHeight + 'px'; + summaryText.style.maxHeight = summaryText.scrollHeight + 'px'; }); 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;'; + toggleLink.style.cssText = `display: inline-block; margin-top: 4px; font-size: 13px; color: ${colors.linkColor}; 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'; + summaryText.style.maxHeight = summaryText.scrollHeight + '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; @@ -1089,18 +1105,15 @@ switch (message.command) { } expanded = !expanded; }); - summaryTextWrapper.appendChild(toggleLink); } else { - // HTML content: use max-height to collapse, preserve full HTML - const collapsedMaxHeight = '4.2em'; // ~3 lines collapsed + const collapsedMaxHeight = '4.2em'; 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;'; + toggleLink.style.cssText = `display: inline-block; margin-top: 4px; font-size: 13px; color: ${colors.linkColor}; cursor: pointer; text-decoration: underline;`; let expanded = false; toggleLink.addEventListener('click', (e) => { @@ -1115,7 +1128,6 @@ switch (message.command) { expanded = !expanded; }); - // Only show toggle if content is actually taller than collapsed height requestAnimationFrame(() => { if (summaryText.scrollHeight > summaryText.clientHeight) { summaryTextWrapper.appendChild(toggleLink); @@ -1133,199 +1145,120 @@ switch (message.command) { summaryBody.appendChild(summaryTextWrapper); summaryContainer.appendChild(summaryBody); - const translationBannerForSummary = document.getElementById('mzta-translation-banner') || document.getElementById('mzta-translation-generating'); - const spamBanner = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); - const insertAfterSummary = translationBannerForSummary || spamBanner; - document.body.insertBefore(summaryContainer, insertAfterSummary ? insertAfterSummary.nextSibling : document.body.firstChild); - _updatePanelMargins(); + _addPanel('mzta-summary-banner', summaryContainer); return Promise.resolve(true); + } - case "showSummaryGenerating": - const existingGenerating = document.getElementById('mzta-summary-generating'); - if(existingGenerating) return Promise.resolve(true); + case "showSummaryGenerating": { + if (document.getElementById('mzta-summary-generating')) return Promise.resolve(true); - const existingSummary = document.getElementById('mzta-summary-banner'); - if(existingSummary) existingSummary.remove(); + _removePanel('mzta-summary-banner'); + _removeToolbarItem('mzta-toolbar-summary'); - const existingTriggerWrap = document.getElementById('mzta-summary-trigger-wrapper'); - if(existingTriggerWrap) existingTriggerWrap.remove(); - const existingTrigger = document.getElementById('mzta-summary-trigger'); - if(existingTrigger) existingTrigger.remove(); + const colors = _getThemeColors(); + const genContainer = document.createElement('div'); + genContainer.className = 'thunderai-summary-pane'; + genContainer.style.cssText = `background-color: ${colors.summary.bg}; color: ${colors.summary.text}; padding: 0.5rem; border-radius: 4px; border: 1px solid ${colors.summary.border}; font-size: 14px; display: flex; align-items: center; gap: 10px;`; - // If translation button is fixed at top:40px (was offset for summary button), move it back to top:8px - const existingTranslTriggerShowSummaryGen = document.getElementById('mzta-translation-trigger'); - if (existingTranslTriggerShowSummaryGen && !document.getElementById('mzta-translation-trigger-wrapper')) { - existingTranslTriggerShowSummaryGen.style.top = '8px'; - } + const genIcon = document.createElement('img'); + genIcon.src = browser.runtime.getURL("/images/ai_summary.png"); + genIcon.style.cssText = `height: 16px; width: 16px; flex-shrink: 0;${colors.isDark ? ' filter: invert(1);' : ''}`; - 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 genLoading = document.createElement('img'); + genLoading.src = browser.runtime.getURL("/images/loading.gif"); + genLoading.style.cssText = "height: 16px; width: 16px;"; - 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; 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 genTitle = document.createElement('span'); + genTitle.className = 'thunderai-summary-title'; + genTitle.textContent = browser.i18n.getMessage("summarize_generating"); + genTitle.style.cssText = 'font-size: 14px;'; - 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);' : ''}`; + genContainer.appendChild(genIcon); + genContainer.appendChild(genLoading); + genContainer.appendChild(genTitle); - const generatingLoadingImg = document.createElement('img'); - generatingLoadingImg.src = browser.runtime.getURL("/images/loading.gif"); - generatingLoadingImg.style.cssText = "height: 16px; width: 16px;"; - - const generatingTitle = document.createElement('span'); - generatingTitle.className = 'thunderai-summary-title'; - generatingTitle.textContent = browser.i18n.getMessage("summarize_generating"); - generatingTitle.style.cssText = `font-size: 14px;`; - - generatingContainer.appendChild(generatingIcon); - generatingContainer.appendChild(generatingLoadingImg); - generatingContainer.appendChild(generatingTitle); - - const translationBannerForSummaryGen = document.getElementById('mzta-translation-banner') || document.getElementById('mzta-translation-generating'); - const spamBannerGen = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); - const insertAfterSummaryGen = translationBannerForSummaryGen || spamBannerGen; - document.body.insertBefore(generatingContainer, insertAfterSummaryGen ? insertAfterSummaryGen.nextSibling : document.body.firstChild); - _updatePanelMargins(); + _addPanel('mzta-summary-generating', genContainer); return Promise.resolve(true); + } - case "showSummaryButton": - const existingButton = document.getElementById('mzta-summary-trigger'); - if(existingButton) return Promise.resolve(true); + case "showSummaryButton": { + if (document.getElementById('mzta-toolbar-summary')) 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'; - - const spamBannerTrigger = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); + const colors = _getThemeColors(); 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 = triggerBtnBase + ' margin-left: auto; margin-top: 4px;'; - } else { - triggerBtn.style.cssText = triggerBtnBase + ' position: fixed; top: 8px; right: 8px; z-index: 9998;'; - } + triggerBtn.style.cssText = `background-color: ${colors.summary.bg}; border: 1px solid ${colors.summary.border}; border-radius: 4px; padding: 4px 8px; cursor: pointer; font-size: 12px; font-style: italic; opacity: 0.7; transition: opacity 0.2s; color: ${colors.summary.text}; display: inline-flex; align-items: center; gap: 6px;`; 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);' : ''}`; + triggerIcon.style.cssText = `height: 14px; width: 14px;${colors.isDark ? ' 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; - const wrapper = document.getElementById('mzta-summary-trigger-wrapper'); - if (wrapper) wrapper.remove(); else triggerBtn.remove(); + triggerBtn.onclick = () => { + _removeToolbarItem('mzta-toolbar-summary'); browser.runtime.sendMessage({ command: message.webchat ? "triggerSummaryWebchat" : "triggerSummaryGeneration", headerMessageId: message.headerMessageId }); }; - 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); - // If translation button is also fixed, push it down to avoid overlap - const existingTranslFixedBtn = document.getElementById('mzta-translation-trigger'); - if (existingTranslFixedBtn && !document.getElementById('mzta-translation-trigger-wrapper')) { - existingTranslFixedBtn.style.top = '40px'; - } - } + _addToolbarItem('mzta-toolbar-summary', triggerBtn); return Promise.resolve(true); + } - case "showTranslation": - const existingTranslationGenerating = document.getElementById('mzta-translation-generating'); - if(existingTranslationGenerating) existingTranslationGenerating.remove(); - - const existingTranslationTriggerWrapper = document.getElementById('mzta-translation-trigger-wrapper'); - if(existingTranslationTriggerWrapper) existingTranslationTriggerWrapper.remove(); - const existingTranslationTriggerBtn = document.getElementById('mzta-translation-trigger'); - if(existingTranslationTriggerBtn) existingTranslationTriggerBtn.remove(); - - const existingTranslationBanner = document.getElementById('mzta-translation-banner'); - if(existingTranslationBanner) existingTranslationBanner.remove(); + case "showTranslation": { + _removePanel('mzta-translation-generating'); + _removePanel('mzta-translation-banner'); + _removeToolbarItem('mzta-toolbar-translation'); const translationData = message.data; + const colors = _getThemeColors(); + const tc = translationData.error ? colors.translErr : colors.translation; + const translationContainer = document.createElement('div'); - translationContainer.id = 'mzta-translation-banner'; - - const isDarkTranslation = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; - - let bgColorTranslation = isDarkTranslation ? '#1a2e2a' : '#e8f5e9'; - let textColorTranslation = isDarkTranslation ? '#c8e6c9' : '#1b5e20'; - let borderColorTranslation = isDarkTranslation ? '#2e5740' : '#a5d6a7'; - - if (translationData.error) { - bgColorTranslation = isDarkTranslation ? '#3a1a1a' : '#f7e6e6'; - textColorTranslation = isDarkTranslation ? '#ffcccc' : '#660000'; - borderColorTranslation = '#660000'; - } - translationContainer.className = 'thunderai-translation-pane'; - translationContainer.style.cssText = `background-color: ${bgColorTranslation}; color: ${textColorTranslation}; padding: 0.5rem; border-radius: 4px; border: 1px solid ${borderColorTranslation}; font-family: system-ui, -apple-system, sans-serif; font-size: 14px;`; + translationContainer.style.cssText = `background-color: ${tc.bg}; color: ${tc.text}; padding: 0.5rem; border-radius: 4px; border: 1px solid ${tc.border}; font-size: 14px;`; const translationHeader = document.createElement('div'); translationHeader.style.cssText = 'display: flex; align-items: center; gap: 8px; margin-bottom: 6px;'; const translationIcon = document.createElement('img'); translationIcon.src = browser.runtime.getURL("/images/ai_translation.png"); - translationIcon.style.cssText = `height: 16px; width: 16px; flex-shrink: 0;${isDarkTranslation ? ' filter: invert(1);' : ''}`; + translationIcon.style.cssText = `height: 16px; width: 16px; flex-shrink: 0;${colors.isDark ? ' filter: invert(1);' : ''}`; const translationTitleSpan = document.createElement('span'); - translationTitleSpan.style.cssText = `font-weight: bold; font-size: 14px; color: ${textColorTranslation}; flex-grow: 1;`; + translationTitleSpan.style.cssText = `font-weight: bold; font-size: 14px; color: ${tc.text}; flex-grow: 1;`; translationTitleSpan.textContent = browser.i18n.getMessage("translate_banner_title") || "AI Translation"; if (translationData.lang) { translationTitleSpan.textContent += ' (' + translationData.lang + ')'; } - const translationMenu = createThreeDotsMenu(isDarkTranslation, [ + const translationMenu = createThreeDotsMenu(colors.isDark, [ { - icon: '↻', + icon: '\u21BB', label: browser.i18n.getMessage("translate_refresh") || 'Refresh translation', - hoverColor: isDarkTranslation ? '#4d9de0' : '#1a5fa8', + hoverColor: colors.isDark ? '#4d9de0' : '#1a5fa8', disableAfterClick: true, onClick: () => { - browser.runtime.sendMessage({ - command: "refreshTranslation", - headerMessageId: translationData.headerMessageId - }); + browser.runtime.sendMessage({ command: "refreshTranslation", headerMessageId: translationData.headerMessageId }); } }, { - icon: '×', + icon: '\u00D7', label: browser.i18n.getMessage("translate_delete") || 'Delete translation', hoverColor: '#cc0000', onClick: () => { - translationContainer.remove(); - browser.runtime.sendMessage({ - command: "removeTranslation", - headerMessageId: translationData.headerMessageId - }); + _removePanel('mzta-translation-banner'); + browser.runtime.sendMessage({ command: "removeTranslation", headerMessageId: translationData.headerMessageId }); } } - ], { bg: bgColorTranslation, border: borderColorTranslation, text: textColorTranslation }); + ], { bg: tc.bg, border: tc.border, text: tc.text }); const translationBranding = document.createElement('span'); translationBranding.textContent = browser.i18n.getMessage("translate_by") + " ThunderAI"; @@ -1343,13 +1276,12 @@ switch (message.command) { const translationText = document.createElement('div'); translationText.style.cssText = 'white-space: pre-wrap; line-height: 1.5;'; if (translationData.error) { - translationText.textContent = translationData.message || browser.i18n.getMessage("translate_error") || browser.i18n.getMessage("translate_error"); + translationText.textContent = translationData.message || browser.i18n.getMessage("translate_error"); } else { translationText.textContent = translationData.translated_text || ''; } translationTextWrapper.appendChild(translationText); - // Expand/collapse for long translations const maxLenTranslation = translationData.maxDisplayLength || 0; const fullTranslationText = translationData.translated_text || ''; if (!translationData.error && maxLenTranslation > 0 && fullTranslationText.length > maxLenTranslation) { @@ -1362,24 +1294,21 @@ switch (message.command) { translationText.textContent = truncatedTranslation; requestAnimationFrame(() => { - const collapsedHeight = translationText.scrollHeight; - translationText.style.maxHeight = collapsedHeight + 'px'; + translationText.style.maxHeight = translationText.scrollHeight + 'px'; }); - const toggleLinkTranslation = document.createElement('a'); - toggleLinkTranslation.textContent = browser.i18n.getMessage("translate_see_more") || "See more"; - toggleLinkTranslation.href = '#'; - toggleLinkTranslation.style.cssText = 'display: inline-block; margin-top: 4px; font-size: 13px; color: ' + - (isDarkTranslation ? '#6db3f2' : '#1a5fa8') + '; cursor: pointer; text-decoration: underline;'; + const toggleLink = document.createElement('a'); + toggleLink.textContent = browser.i18n.getMessage("translate_see_more") || "See more"; + toggleLink.href = '#'; + toggleLink.style.cssText = `display: inline-block; margin-top: 4px; font-size: 13px; color: ${colors.linkColor}; cursor: pointer; text-decoration: underline;`; - let expandedTranslation = false; - toggleLinkTranslation.addEventListener('click', (e) => { + let expanded = false; + toggleLink.addEventListener('click', (e) => { e.preventDefault(); - if (!expandedTranslation) { + if (!expanded) { translationText.textContent = fullTranslationText; - const fullHeight = translationText.scrollHeight; - translationText.style.maxHeight = fullHeight + 'px'; - toggleLinkTranslation.textContent = browser.i18n.getMessage("translate_see_less") || "See less"; + translationText.style.maxHeight = translationText.scrollHeight + 'px'; + toggleLink.textContent = browser.i18n.getMessage("translate_see_less") || "See less"; } else { translationText.textContent = truncatedTranslation; const collapsedHeight = translationText.scrollHeight; @@ -1392,132 +1321,80 @@ switch (message.command) { translationText.removeEventListener('transitionend', handler); translationText.textContent = truncatedTranslation; }); - toggleLinkTranslation.textContent = browser.i18n.getMessage("translate_see_more") || "See more"; + toggleLink.textContent = browser.i18n.getMessage("translate_see_more") || "See more"; } - expandedTranslation = !expandedTranslation; + expanded = !expanded; }); - - translationTextWrapper.appendChild(toggleLinkTranslation); + translationTextWrapper.appendChild(toggleLink); } translationContainer.appendChild(translationTextWrapper); - const spamBannerForTranslation = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); - if (spamBannerForTranslation) { - document.body.insertBefore(translationContainer, spamBannerForTranslation.nextSibling); - } else { - document.body.insertBefore(translationContainer, document.body.firstChild); - } - _updatePanelMargins(); + _addPanel('mzta-translation-banner', translationContainer); return Promise.resolve(true); + } - case "showTranslationGenerating": - const existingTranslationGen = document.getElementById('mzta-translation-generating'); - if(existingTranslationGen) return Promise.resolve(true); + case "showTranslationGenerating": { + if (document.getElementById('mzta-translation-generating')) return Promise.resolve(true); - const existingTranslationBannerGen = document.getElementById('mzta-translation-banner'); - if(existingTranslationBannerGen) existingTranslationBannerGen.remove(); + _removePanel('mzta-translation-banner'); + _removeToolbarItem('mzta-toolbar-translation'); - const existingTranslationTrigWrap = document.getElementById('mzta-translation-trigger-wrapper'); - if(existingTranslationTrigWrap) existingTranslationTrigWrap.remove(); - const existingTranslationTrig = document.getElementById('mzta-translation-trigger'); - if(existingTranslationTrig) existingTranslationTrig.remove(); + const colors = _getThemeColors(); + const genContainer = document.createElement('div'); + genContainer.className = 'thunderai-translation-pane'; + genContainer.style.cssText = `background-color: ${colors.translation.bg}; color: ${colors.translation.text}; padding: 0.5rem; border-radius: 4px; border: 1px solid ${colors.translation.border}; font-size: 14px; display: flex; align-items: center; gap: 10px;`; - const isDarkTranslationGen = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + const genIcon = document.createElement('img'); + genIcon.src = browser.runtime.getURL("/images/ai_translation.png"); + genIcon.style.cssText = `height: 16px; width: 16px; flex-shrink: 0;${colors.isDark ? ' filter: invert(1);' : ''}`; - let bgColorTranslationGen = isDarkTranslationGen ? '#1a2e2a' : '#e8f5e9'; - let textColorTranslationGen = isDarkTranslationGen ? '#c8e6c9' : '#1b5e20'; - let borderColorTranslationGen = isDarkTranslationGen ? '#2e5740' : '#a5d6a7'; + const genLoading = document.createElement('img'); + genLoading.src = browser.runtime.getURL("/images/loading.gif"); + genLoading.style.cssText = "height: 16px; width: 16px;"; - const translationGenContainer = document.createElement('div'); - translationGenContainer.id = 'mzta-translation-generating'; - translationGenContainer.className = 'thunderai-translation-pane'; - translationGenContainer.style.cssText = `background-color: ${bgColorTranslationGen}; color: ${textColorTranslationGen}; padding: 0.5rem; border-radius: 4px; border: 1px solid ${borderColorTranslationGen}; font-family: system-ui, -apple-system, sans-serif; font-size: 14px; display: flex; align-items: center; gap: 10px;`; + const genTitle = document.createElement('span'); + genTitle.textContent = browser.i18n.getMessage("translate_generating") || "Translating..."; + genTitle.style.cssText = 'font-size: 14px;'; - const translationGenIcon = document.createElement('img'); - translationGenIcon.src = browser.runtime.getURL("/images/ai_translation.png"); - translationGenIcon.style.cssText = `height: 16px; width: 16px; flex-shrink: 0;${isDarkTranslationGen ? ' filter: invert(1);' : ''}`; + genContainer.appendChild(genIcon); + genContainer.appendChild(genLoading); + genContainer.appendChild(genTitle); - const translationGenLoadingImg = document.createElement('img'); - translationGenLoadingImg.src = browser.runtime.getURL("/images/loading.gif"); - translationGenLoadingImg.style.cssText = "height: 16px; width: 16px;"; - - const translationGenTitle = document.createElement('span'); - translationGenTitle.textContent = browser.i18n.getMessage("translate_generating") || "Translating..."; - translationGenTitle.style.cssText = `font-size: 14px;`; - - translationGenContainer.appendChild(translationGenIcon); - translationGenContainer.appendChild(translationGenLoadingImg); - translationGenContainer.appendChild(translationGenTitle); - - const spamBannerForGen = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); - if (spamBannerForGen) { - document.body.insertBefore(translationGenContainer, spamBannerForGen.nextSibling); - } else { - document.body.insertBefore(translationGenContainer, document.body.firstChild); - } - _updatePanelMargins(); + _addPanel('mzta-translation-generating', genContainer); return Promise.resolve(true); + } - case "showTranslationButton": - const existingTranslationButton = document.getElementById('mzta-translation-trigger'); - if(existingTranslationButton) return Promise.resolve(true); + case "showTranslationButton": { + if (document.getElementById('mzta-toolbar-translation')) return Promise.resolve(true); - const isDarkTranslationBtn = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + const colors = _getThemeColors(); + const triggerBtn = document.createElement('div'); + triggerBtn.title = browser.i18n.getMessage("translate_click_to_generate") || "Click to translate this email"; + triggerBtn.style.cssText = `background-color: ${colors.translation.bg}; border: 1px solid ${colors.translation.border}; border-radius: 4px; padding: 4px 8px; cursor: pointer; font-size: 12px; font-style: italic; opacity: 0.7; transition: opacity 0.2s; color: ${colors.translation.text}; display: inline-flex; align-items: center; gap: 6px;`; - let bgColorTranslationBtn = isDarkTranslationBtn ? '#1a2e2a' : '#e8f5e9'; - let textColorTranslationBtn = isDarkTranslationBtn ? '#c8e6c9' : '#1b5e20'; - let borderColorTranslationBtn = isDarkTranslationBtn ? '#2e5740' : '#a5d6a7'; + const triggerIcon = document.createElement('img'); + triggerIcon.src = browser.runtime.getURL("/images/ai_translation.png"); + triggerIcon.style.cssText = `height: 14px; width: 14px;${colors.isDark ? ' filter: invert(1);' : ''}`; + triggerBtn.appendChild(triggerIcon); - const spamBannerTranslationTrigger = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress'); - const summaryPanelTranslationTrigger = document.getElementById('mzta-summary-banner') || document.getElementById('mzta-summary-trigger-wrapper'); - const summaryFixedBtnTranslationTrigger = document.getElementById('mzta-summary-trigger'); - const translationTriggerBtn = document.createElement('div'); - translationTriggerBtn.id = 'mzta-translation-trigger'; - translationTriggerBtn.title = browser.i18n.getMessage("translate_click_to_generate") || "Click to translate this email"; - const translationTriggerBtnBase = `background-color: ${bgColorTranslationBtn}; border: 1px solid ${borderColorTranslationBtn}; 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: ${textColorTranslationBtn}; display: inline-flex; align-items: center; gap: 6px; width: fit-content;`; - const insertAfterTranslationBtn = summaryPanelTranslationTrigger || spamBannerTranslationTrigger; - if (insertAfterTranslationBtn) { - translationTriggerBtn.style.cssText = translationTriggerBtnBase + ' margin-left: auto; margin-top: 4px;'; - } else { - const topOffset = summaryFixedBtnTranslationTrigger ? '40px' : '8px'; - translationTriggerBtn.style.cssText = translationTriggerBtnBase + ` position: fixed; top: ${topOffset}; right: 8px; z-index: 9997;`; - } + const triggerLabel = document.createElement('span'); + triggerLabel.textContent = browser.i18n.getMessage("get_ai_translation") || "Get AI Translation"; + triggerBtn.appendChild(triggerLabel); - const translationTriggerIcon = document.createElement('img'); - translationTriggerIcon.src = browser.runtime.getURL("/images/ai_translation.png"); - translationTriggerIcon.style.cssText = `height: 14px; width: 14px;${isDarkTranslationBtn ? ' filter: invert(1);' : ''}`; - translationTriggerBtn.appendChild(translationTriggerIcon); - - const translationTriggerLabel = document.createElement('span'); - translationTriggerLabel.textContent = browser.i18n.getMessage("get_ai_translation") || "Get AI Translation"; - translationTriggerBtn.appendChild(translationTriggerLabel); - translationTriggerBtn.onmouseover = () => { translationTriggerBtn.style.opacity = '1'; }; - translationTriggerBtn.onmouseout = () => { translationTriggerBtn.style.opacity = '0.7'; }; - translationTriggerBtn.onclick = async () => { - translationTriggerBtn.onclick = null; - translationTriggerBtn.style.cursor = 'default'; - translationTriggerBtn.style.opacity = '0.7'; - translationTriggerBtn.onmouseover = null; - translationTriggerBtn.onmouseout = null; - const wrapper = document.getElementById('mzta-translation-trigger-wrapper'); - if (wrapper) wrapper.remove(); else translationTriggerBtn.remove(); + triggerBtn.onmouseover = () => { triggerBtn.style.opacity = '1'; }; + triggerBtn.onmouseout = () => { triggerBtn.style.opacity = '0.7'; }; + triggerBtn.onclick = () => { + _removeToolbarItem('mzta-toolbar-translation'); browser.runtime.sendMessage({ command: message.webchat ? "triggerTranslationWebchat" : "triggerTranslationGeneration", headerMessageId: message.headerMessageId }); }; - if (insertAfterTranslationBtn) { - const translationTriggerWrapper = document.createElement('div'); - translationTriggerWrapper.id = 'mzta-translation-trigger-wrapper'; - translationTriggerWrapper.style.cssText = 'display: flex; justify-content: flex-end; padding: 4px 0.5rem;'; - translationTriggerWrapper.appendChild(translationTriggerBtn); - document.body.insertBefore(translationTriggerWrapper, insertAfterTranslationBtn.nextSibling); - } else { - document.body.appendChild(translationTriggerBtn); - } + _addToolbarItem('mzta-toolbar-translation', triggerBtn); return Promise.resolve(true); + } default: // do nothing From 45da8622aa47e5eebc0c8551e154a75546ae2b88 Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 29 Mar 2026 22:37:28 +0200 Subject: [PATCH 092/269] in-message UI improved --- _locales/en/messages.json | 4 ++++ js/mzta-compose-script.js | 38 ++++++++------------------------------ 2 files changed, 12 insertions(+), 30 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index ae58fd9c..5728914a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2165,6 +2165,10 @@ "message": "Antispam by", "description": "" }, + "spam_badge_tooltip": { + "message": "Spam score — Click to see the explanation", + "description": "Tooltip shown on the spam score badge in the toolbar" + }, "summary_by": { "message": "Summary by", "description": "" diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 663d8c42..58eecbbb 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -102,18 +102,15 @@ function _updateToolbarVisibility() { if (!toolbar) return; const hasItems = toolbar.querySelector('#mzta-toolbar-spam, #mzta-toolbar-summary, #mzta-toolbar-translation'); toolbar.style.display = hasItems ? 'flex' : 'none'; + // Push the first button (summary or translation) to the right + const summary = document.getElementById('mzta-toolbar-summary'); + const translation = document.getElementById('mzta-toolbar-translation'); + const firstBtn = summary || translation; + if (summary) summary.style.marginLeft = (firstBtn === summary) ? 'auto' : ''; + if (translation) translation.style.marginLeft = (firstBtn === translation) ? 'auto' : ''; } -function _ensureBranding(toolbar) { - if (document.getElementById('mzta-toolbar-branding')) return; - const branding = document.createElement('span'); - branding.id = 'mzta-toolbar-branding'; - branding.textContent = 'by ThunderAI'; - branding.style.cssText = 'margin-left: auto; font-style: italic; font-size: 10px; opacity: 0.5; white-space: nowrap;'; - toolbar.appendChild(branding); -} - -const _TOOLBAR_SLOT_ORDER = ['mzta-toolbar-spam', 'mzta-toolbar-summary', 'mzta-toolbar-translation', 'mzta-toolbar-branding']; +const _TOOLBAR_SLOT_ORDER = ['mzta-toolbar-spam', 'mzta-toolbar-summary', 'mzta-toolbar-translation']; function _addToolbarItem(id, element) { const { toolbar } = _ensureContainer(); @@ -130,19 +127,12 @@ function _addToolbarItem(id, element) { if (insertBefore) toolbar.insertBefore(element, insertBefore); else toolbar.appendChild(element); - _ensureBranding(toolbar); _updateToolbarVisibility(); } function _removeToolbarItem(id) { const el = document.getElementById(id); if (el) el.remove(); - // Remove branding if toolbar is now empty of content slots - const toolbar = document.getElementById('mzta-toolbar'); - if (toolbar && !toolbar.querySelector('#mzta-toolbar-spam, #mzta-toolbar-summary, #mzta-toolbar-translation')) { - const branding = document.getElementById('mzta-toolbar-branding'); - if (branding) branding.remove(); - } _updateToolbarVisibility(); } @@ -885,19 +875,6 @@ switch (message.command) { badgeText.textContent = browser.i18n.getMessage("spam_check_in_progress"); badge.appendChild(badgeText); _addToolbarItem('mzta-toolbar-spam', badge); - - // Loading panel - const panel = document.createElement('div'); - panel.style.cssText = `background-color: ${colors.spamLoading.bg}; color: ${colors.spamLoading.text}; border: 1px solid ${colors.spamLoading.border}; border-radius: 4px; padding: 8px 0.5rem; font-size: 13px; display: flex; align-items: center; gap: 15px; width: 100%; box-sizing: border-box;`; - const panelLoading = document.createElement('img'); - panelLoading.src = browser.runtime.getURL("/images/loading.gif"); - panelLoading.style.cssText = "height: 16px; width: 16px;"; - const panelText = document.createElement('strong'); - panelText.textContent = browser.i18n.getMessage("spam_check_in_progress"); - panel.appendChild(panelLoading); - panel.appendChild(panelText); - - _addPanel('mzta-spam-check-progress', panel); return Promise.resolve(true); } @@ -913,6 +890,7 @@ switch (message.command) { // Spam badge in toolbar (clickable to toggle explanation panel) const badge = document.createElement('div'); + badge.title = browser.i18n.getMessage("spam_badge_tooltip"); badge.style.cssText = `background-color: ${sc.bg}; color: ${sc.text}; border: 1px solid ${sc.border}; border-radius: 4px; padding: 2px 8px; font-size: 12px; font-weight: bold; display: inline-flex; align-items: center; gap: 4px; cursor: pointer; white-space: nowrap; transition: opacity 0.2s;`; if (data.spamValue == -999) { badge.textContent = browser.i18n.getMessage("apiwebchat_error"); From 4ce2a0fc6e7cd6d4b72dbee3b0b3e07fe4fa75c9 Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 29 Mar 2026 22:41:05 +0200 Subject: [PATCH 093/269] storage info improved. see #580 --- _locales/en/messages.json | 8 ++++++-- options/mzta-options.html | 3 ++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 5728914a..b456df8d 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -336,8 +336,12 @@ "description": "" }, "prefs_storage_title": { - "message": "Additional Info Storage", - "description": "Title for additional info storage management section in options" + "message": "Storage", + "description": "Title for storage management section in options" + }, + "prefs_storage_info": { + "message": "The storage is used to save information about spam score, summaries and translations of each message.", + "description": "Info text for storage management section in options" }, "prefs_storage_size": { "message": "Storage size", diff --git a/options/mzta-options.html b/options/mzta-options.html index 4d6137ae..2d247247 100644 --- a/options/mzta-options.html +++ b/options/mzta-options.html @@ -253,10 +253,11 @@ - __MSG_prefs_storage_title__ + ThunderAI __MSG_prefs_storage_title__ __MSG_prefs_storage_size__:   +
      __MSG_prefs_storage_info__ From f1b42f871480dec1aff9e8c887125ecd3c7044c7 Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 29 Mar 2026 22:52:47 +0200 Subject: [PATCH 094/269] warning on empty lang. see #247 #716 --- _locales/en/messages.json | 4 ++++ mzta-background.js | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index b456df8d..96064f2a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2165,6 +2165,10 @@ "message": "Translation failed.", "description": "" }, + "translate_no_language_configured": { + "message": "Translation language is not configured. Please set a language in the Translation settings or set a default language in the General settings.", + "description": "" + }, "antispam_by": { "message": "Antispam by", "description": "" diff --git a/mzta-background.js b/mzta-background.js index 5aaad026..24898e05 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -377,6 +377,17 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { case 'triggerTranslationGeneration': async function _triggerTranslationGeneration(message) { let tabId = sender.tab.id; + let prefs_tl = await browser.storage.sync.get({ + translate_lang: prefs_default.translate_lang, + default_chatgpt_lang: prefs_default.default_chatgpt_lang + }); + const lang_tl = prefs_tl.translate_lang || prefs_tl.default_chatgpt_lang || ''; + if (!lang_tl) { + let tabs = await browser.tabs.query({ active: true, currentWindow: true }); + browser.tabs.sendMessage(tabId, { command: "sendAlert", curr_tab_type: tabs[0].type, message: browser.i18n.getMessage('translate_no_language_configured') }); + browser.tabs.sendMessage(tabId, { command: "showTranslationButton", headerMessageId: message.headerMessageId }); + return; + } await _generateTranslationForMessage(message.headerMessageId, tabId); } _triggerTranslationGeneration(message); @@ -384,6 +395,17 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { case 'triggerTranslationWebchat': async function _triggerTranslationWebchat(message) { let tabId = sender.tab.id; + let prefs_tw = await browser.storage.sync.get({ + translate_lang: prefs_default.translate_lang, + default_chatgpt_lang: prefs_default.default_chatgpt_lang + }); + const lang_tw = prefs_tw.translate_lang || prefs_tw.default_chatgpt_lang || ''; + if (!lang_tw) { + let tabs = await browser.tabs.query({ active: true, currentWindow: true }); + browser.tabs.sendMessage(tabId, { command: "sendAlert", curr_tab_type: tabs[0].type, message: browser.i18n.getMessage('translate_no_language_configured') }); + browser.tabs.sendMessage(tabId, { command: "showTranslationButton", headerMessageId: message.headerMessageId, webchat: true }); + return; + } await _openTranslationWebchat(message.headerMessageId, tabId); } _triggerTranslationWebchat(message); @@ -716,6 +738,12 @@ async function _generateTranslationForMessage(headerMessageId, tabId = null, opt return; } + const lang = prefs.translate_lang || prefs.default_chatgpt_lang || ''; + if (!lang) { + taLog.warn("Translation skipped: no language configured (translate_lang and default_chatgpt_lang are both empty)."); + return; + } + if (await translationStore.isProcessing(headerMessageId)) { if (tabId) browser.tabs.sendMessage(tabId, { command: "showTranslationGenerating" }); return; @@ -748,8 +776,6 @@ async function _generateTranslationForMessage(headerMessageId, tabId = null, opt taWorkingStatus.stopWorking(); return; } - - const lang = prefs.translate_lang || prefs.default_chatgpt_lang || ''; const { promptText } = await taPromptUtils.buildTranslationPrompt(fullMessage, lang); const cmd = new mzta_specialCommand({ @@ -951,6 +977,10 @@ async function _openTranslationWebchat(headerMessageId, tabId) { } const lang = prefs.translate_lang || prefs.default_chatgpt_lang || ''; + if (!lang) { + taLog.warn("Translation skipped: no language configured (translate_lang and default_chatgpt_lang are both empty)."); + return; + } const { promptText, promptInfo } = await taPromptUtils.buildTranslationPrompt(curr_message_full, lang); promptInfo.headerMessageId = headerMessageId; promptInfo.translationTabId = tabId; From 0b5d410afd7faeef5bc6e20211abfc126a95553a Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 29 Mar 2026 23:00:01 +0200 Subject: [PATCH 095/269] translate_exclude_lang option added (but not used at the moment). see #247 #720 --- _locales/en/messages.json | 12 ++++++++++++ options/mzta-options-default.js | 1 + pages/translate/mzta-translate.html | 11 +++++++++++ 3 files changed, 24 insertions(+) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 96064f2a..d85181d5 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2129,6 +2129,14 @@ "message": "Language to translate emails into. If empty, uses the default language setting.", "description": "" }, + "prefs_OptionText_translate_exclude_lang": { + "message": "Exclude languages", + "description": "" + }, + "prefs_OptionText_translate_exclude_lang_Info": { + "message": "Comma-separated list of language codes (e.g., en, fr, it) to skip for automatic translation. If the email is in one of these languages, it won't be translated automatically or the manual button won't be shown.", + "description": "" + }, "prefs_OptionText_Translate_main_prompt": { "message": "The prompt describing the translation task:", "description": "" @@ -2216,5 +2224,9 @@ "prefs_OptionText_action_auto_batch": { "message": "When the email is received", "description": "" + }, + "placeholder_string": { + "message": "Placeholder", + "description": "" } } \ No newline at end of file diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index 17a7da3a..c1015507 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -146,6 +146,7 @@ export const prefs_default = { translate_display_mode: 'inline', // 'inline' or 'webchat' translate_max_display_length: 0, // 0 = no limit, otherwise max chars shown inline translate_lang: '', // target language, fallback on default_chatgpt_lang + translate_exclude_lang: '', // languages to do not translate spamfilter_show_msg_panel: true, ...generated_prefs } diff --git a/pages/translate/mzta-translate.html b/pages/translate/mzta-translate.html index b424bd9f..dc7a4e9d 100644 --- a/pages/translate/mzta-translate.html +++ b/pages/translate/mzta-translate.html @@ -67,6 +67,17 @@ + + + + __MSG_prefs_OptionText_translate_exclude_lang__ + + From 5eab777885064a0ca1c83daf784eb05dca261f76 Mon Sep 17 00:00:00 2001 From: Andreas Pettersson Date: Sun, 29 Mar 2026 18:44:19 +0200 Subject: [PATCH 096/269] Translated using Weblate (Swedish) Currently translated at 100.0% (520 of 520 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/sv/ --- _locales/sv/messages.json | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index 8cd8802e..5a7071cd 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -1462,16 +1462,16 @@ "message": "Spara som sammanfattning" }, "prefs_storage_title": { - "message": "Cachelagring" + "message": "Ytterligare informationslagring" }, "prefs_storage_size": { - "message": "Cachestorlek" + "message": "Lagringsstorlek" }, "prefs_storage_clear_button": { - "message": "Rensa cache" + "message": "Rensa lagring" }, "prefs_storage_clear_confirm": { - "message": "Är du säker på att du vill rensa all cachad data (sammanfattningar, skräppostrapporter, översättningar)? Den här åtgärden kan inte ångras." + "message": "Är du säker på att du vill rensa all lagrad data (sammanfattningar, skräppostrapporter, översättningar)? Den här åtgärden kan inte ångras." }, "prefs_storage_clear_done": { "message": "$COUNT$ register borttagna.", @@ -1506,7 +1506,7 @@ "message": "Visa sammanfattningsknapp" }, "prefs_OptionText_summarize_auto_automatic": { - "message": "Generera automatiskt" + "message": "När e-postmeddelandet öppnas" }, "prefs_OptionText_summarize_auto_Info": { "message": "Välj om sammanfattningar ska genereras automatiskt när meddelanden visas. Kräver en API-baserad anslutning (inte ChatGPT Web)." @@ -1570,5 +1570,8 @@ }, "summary_by": { "message": "Sammanfattning av" + }, + "prefs_OptionText_action_auto_batch": { + "message": "När e-postmeddelandet tas emot" } } From 41071e325642d1a8caa6b530d294ad3780d95919 Mon Sep 17 00:00:00 2001 From: mic Date: Mon, 30 Mar 2026 21:45:42 +0200 Subject: [PATCH 097/269] translation id fixed --- _locales/cs/messages.json | 2 +- _locales/de/messages.json | 2 +- _locales/el/messages.json | 2 +- _locales/es/messages.json | 2 +- _locales/fr/messages.json | 2 +- _locales/hr/messages.json | 2 +- _locales/it/messages.json | 2 +- _locales/ja/messages.json | 2 +- _locales/pl/messages.json | 2 +- _locales/pt-br/messages.json | 2 +- _locales/ru/messages.json | 2 +- _locales/sv/messages.json | 2 +- _locales/zh_Hans/messages.json | 2 +- _locales/zh_Hant/messages.json | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index faa6b283..edc7e471 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -629,7 +629,7 @@ "prefs_OptionText_add_tags_auto_only_inbox_Info": { "message": "Pokud je zaškrtnuto, AI bude přidávat štítky pouze e-mailům přijatým ve složce Doručená pošta." }, - "thunderai_def_lang": { + "placeholder_thunderai_def_lang": { "message": "Výchozí jazyk podle nastavení ThunderAI." }, "StorageSpace": { diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 053585a9..fc6ba9fe 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -753,7 +753,7 @@ "placeholder_thunderai_def_sign": { "message": "Standardsignatur wie in den ThunderAI-Optionen definiert." }, - "thunderai_def_lang": { + "placeholder_thunderai_def_lang": { "message": "Standardsprache wie in den ThunderAI-Optionen definiert." }, "prefs_OptionText_spamfilter": { diff --git a/_locales/el/messages.json b/_locales/el/messages.json index d1eaffef..d66cfbc4 100644 --- a/_locales/el/messages.json +++ b/_locales/el/messages.json @@ -692,7 +692,7 @@ "placeholder_thunderai_def_sign": { "message": "Προεπιλεγμένη υπογραφή όπως ορίζεται στις επιλογές ThunderAI." }, - "thunderai_def_lang": { + "placeholder_thunderai_def_lang": { "message": "Προεπιλεγμένη γλώσσα όπως ορίζεται στις επιλογές ThunderAI." }, "placeholder_mail_attachments_info": { diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 37105671..5753820c 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -875,7 +875,7 @@ "placeholder_thunderai_def_sign": { "message": "Firma predeterminada según lo definido en las opciones de ThunderAI." }, - "thunderai_def_lang": { + "placeholder_thunderai_def_lang": { "message": "Idioma predeterminado según lo definido en las opciones de ThunderAI." }, "placeholder_mail_attachments_info": { diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 54630870..976c8149 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -810,7 +810,7 @@ "SpamFilter_prompt_text_title": { "message": "Texte du prompt actuelle" }, - "thunderai_def_lang": { + "placeholder_thunderai_def_lang": { "message": "Langue par défaut telle que définie dans les options de ThunderAI." }, "prefs_OptionText_btnManageSpamFilterInfo": { diff --git a/_locales/hr/messages.json b/_locales/hr/messages.json index 570131bd..d7121ee5 100644 --- a/_locales/hr/messages.json +++ b/_locales/hr/messages.json @@ -759,7 +759,7 @@ "placeholder_thunderai_def_sign": { "message": "Zadani potpis kako je određeno u mogućnostima ThunderAI." }, - "thunderai_def_lang": { + "placeholder_thunderai_def_lang": { "message": "Zadani jezik kako je određeno u mogućnostima ThunderAI." }, "prefs_OptionText_spamfilter": { diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 763844ee..68ea230e 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -804,7 +804,7 @@ "SpamFilter_info_default": { "message": "In questa pagina puoi modificare il prompt predefinito utilizzato per rilevare le email di spam." }, - "thunderai_def_lang": { + "placeholder_thunderai_def_lang": { "message": "Lingua predefinita come impostata nelle opzioni di ThunderAI." }, "spamfilter_no_reports": { diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 07746075..3007f38b 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -878,7 +878,7 @@ "placeholder_thunderai_def_sign": { "message": "ThunderAIオプションで定義されたデフォルトの署名。" }, - "thunderai_def_lang": { + "placeholder_thunderai_def_lang": { "message": "ThunderAIオプションで定義されたデフォルト言語。" }, "placeholder_mail_attachments_info": { diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index b60db714..b2af15a5 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -756,7 +756,7 @@ "Spam_Value": { "message": "Wartość spamu" }, - "thunderai_def_lang": { + "placeholder_thunderai_def_lang": { "message": "Domyślny język zgodnie z opcjami ThunderAI." }, "prompt_spamfilter": { diff --git a/_locales/pt-br/messages.json b/_locales/pt-br/messages.json index c6730a35..5fe6b00c 100644 --- a/_locales/pt-br/messages.json +++ b/_locales/pt-br/messages.json @@ -822,7 +822,7 @@ "prefs_OptionText_add_tags_auto_only_inbox_Info": { "message": "Se marcado, a IA adicionará tags apenas aos e-mails recebidos na pasta da caixa de entrada." }, - "thunderai_def_lang": { + "placeholder_thunderai_def_lang": { "message": "Idioma padrão conforme definido nas opções do ThunderAI." }, "placeholder_thunderai_def_sign": { diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 31b4fe78..d08caa6d 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -848,7 +848,7 @@ "placeholder_thunderai_def_sign": { "message": "Подпись по умолчанию, определенная в опциях ThunderAI." }, - "thunderai_def_lang": { + "placeholder_thunderai_def_lang": { "message": "Язык по умолчанию, определенный в опциях ThunderAI." }, "empty": { diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index 41df75e4..1c53b67f 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -734,7 +734,7 @@ "prefs_OptionText_get_calendar_event": { "message": "Lägg till en ny kalenderhändelse från markerad text" }, - "thunderai_def_lang": { + "placeholder_thunderai_def_lang": { "message": "Standardspråk som definierat i ThunderAI-alternativ." }, "placeholder_mail_attachments_info": { diff --git a/_locales/zh_Hans/messages.json b/_locales/zh_Hans/messages.json index 22d6bd28..9579793e 100644 --- a/_locales/zh_Hans/messages.json +++ b/_locales/zh_Hans/messages.json @@ -611,7 +611,7 @@ "prefs_OptionText_placeholders_use_default_value": { "message": "占位符:使用默认值" }, - "thunderai_def_lang": { + "placeholder_thunderai_def_lang": { "message": "ThunderAI 选项中定义的默认语言。" }, "prefs_OptionText_openai_comp_info_remote": { diff --git a/_locales/zh_Hant/messages.json b/_locales/zh_Hant/messages.json index 50827707..819af18e 100644 --- a/_locales/zh_Hant/messages.json +++ b/_locales/zh_Hant/messages.json @@ -1002,7 +1002,7 @@ "prefs_OptionText_chatgpt_web_custom_gpt": { "message": "ChatGPT 網頁自訂 GPT" }, - "thunderai_def_lang": { + "placeholder_thunderai_def_lang": { "message": "ThunderAI 選項中定義的預設語言。" }, "prefs_SurveyLinkText2": { From 7e5ba820c297565cb9d7184298b24f2ae628d28a Mon Sep 17 00:00:00 2001 From: mic Date: Mon, 30 Mar 2026 21:49:16 +0200 Subject: [PATCH 098/269] thunderai_translate_lang and thunderai_translate_exclude_lang placeholders added. see #247 #726 #727 --- _locales/en/messages.json | 10 +++++++++- js/mzta-placeholders.js | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d85181d5..44f7054c 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1316,10 +1316,18 @@ "message": "Default signature as defined in ThunderAI options.", "description": "" }, - "thunderai_def_lang": { + "placeholder_thunderai_def_lang": { "message": "Default language as defined in ThunderAI options.", "description": "" }, + "placeholder_thunderai_translate_lang": { + "message": "The language to be used in the mail translations.", + "description": "" + }, + "placeholder_thunderai_translate_exclude_lang": { + "message": "The languages code to do not translate when found.", + "description": "" + }, "placeholder_mail_attachments_info": { "message": "Information about the attachments in the email", "description": "" diff --git a/js/mzta-placeholders.js b/js/mzta-placeholders.js index 48de04a3..e46a6406 100644 --- a/js/mzta-placeholders.js +++ b/js/mzta-placeholders.js @@ -253,6 +253,24 @@ const defaultPlaceholders = [ is_dynamic: "0", enabled: 1, }, + { + id: 'thunderai_translate_lang', + name: "__MSG_placeholder_thunderai_translate_lang__", + default_value: "", + type: 0, + is_default: "1", + is_dynamic: "0", + enabled: 1, + }, + { + id: 'thunderai_translate_exclude_lang', + name: "__MSG_placeholder_thunderai_translate_exclude_lang__", + default_value: "", + type: 0, + is_default: "1", + is_dynamic: "0", + enabled: 1, + }, { id: 'empty', name: "__MSG_placeholder_empty__", @@ -599,6 +617,14 @@ export const placeholdersUtils = { let prefs_def_lang = await browser.storage.sync.get({ default_chatgpt_lang: prefs_default.default_chatgpt_lang }); finalSubs['thunderai_def_lang'] = placeholdersUtils.failSafePlaceholders(prefs_def_lang.default_chatgpt_lang); break; + case 'thunderai_translate_lang': + let prefs_translate_lang = await browser.storage.sync.get({ translate_lang: prefs_default.translate_lang }); + finalSubs['thunderai_translate_lang'] = placeholdersUtils.failSafePlaceholders(prefs_translate_lang.translate_lang); + break; + case 'thunderai_translate_exclude_lang': + let prefs_translate_exclude_lang = await browser.storage.sync.get({ translate_exclude_lang: prefs_default.translate_exclude_lang }); + finalSubs['thunderai_translate_exclude_lang'] = placeholdersUtils.failSafePlaceholders(prefs_translate_exclude_lang.translate_exclude_lang); + break; case 'mail_attachments_info': let attachments_info_string = ""; let attachments_info = await browser.messages.listAttachments(curr_message.id); From c5c7694d519d365235eec766a55e9cd8749056cb Mon Sep 17 00:00:00 2001 From: mic Date: Mon, 30 Mar 2026 23:02:22 +0200 Subject: [PATCH 099/269] tranlsate context menu added. see #247 #719 --- _locales/en/messages.json | 4 ++++ js/mzta-utils.js | 2 ++ mzta-background.js | 33 ++++++++++++++++++++++++++------- 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 44f7054c..12bdb601 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1496,6 +1496,10 @@ "message": "Summarize", "description": "" }, + "context_menu_mzta-translate": { + "message": "Translate", + "description": "" + }, "noActiveCalendar": { "message": "No editable calendar found!", "description": "" diff --git a/js/mzta-utils.js b/js/mzta-utils.js index 8aad2068..74d62eb4 100644 --- a/js/mzta-utils.js +++ b/js/mzta-utils.js @@ -27,10 +27,12 @@ export const getMenuContextDisplay = () => 'message_display_action_menu'; export const contextMenuID_AddTags = 'mzta-add-tags'; export const contextMenuID_Spamfilter = 'mzta-spamfilter'; export const contextMenuID_Summarize = 'mzta-summarize'; +export const contextMenuID_Translate = 'mzta-translate'; export const contextMenuIconsPath = { [contextMenuID_AddTags]: 'moz-extension:images/autotags.png', [contextMenuID_Spamfilter]: 'moz-extension:images/spamfilter.png', [contextMenuID_Summarize]: 'moz-extension:images/summarize.png', + [contextMenuID_Translate]: 'moz-extension:images/ai_translation.png', }; export function getLanguageDisplayName(languageCode) { diff --git a/mzta-background.js b/mzta-background.js index 24898e05..ffb87c08 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -45,6 +45,7 @@ import { contextMenuID_AddTags, contextMenuID_Spamfilter, contextMenuID_Summarize, + contextMenuID_Translate, contextMenuIconsPath, sanitizeChatGPTModelData, sanitizeChatGPTWebCustomData, @@ -1603,6 +1604,13 @@ function addContextMenuItems() { removeContextMenu(contextMenuID_Summarize); } + // Add Context menu: Translate + if(prefs_init.translate && checkAPIIntegration(prefs_init.connection_type, prefs_init.translate_use_specific_integration, prefs_init.translate_connection_type)){ + itemsToAdd.push(contextMenuID_Translate); + } else { + removeContextMenu(contextMenuID_Translate); + } + itemsToAdd.sort((a, b) => { let titleA = browser.i18n.getMessage("context_menu_" + a); let titleB = browser.i18n.getMessage("context_menu_" + b); @@ -1621,6 +1629,7 @@ browser.menus.onClicked.addListener( (info, tab) => { let _add_tags = false let _spamfilter = false let _summarize = false; + let _translate = false; if(info.menuItemId === contextMenuID_AddTags){ _add_tags = true; } @@ -1630,12 +1639,16 @@ browser.menus.onClicked.addListener( (info, tab) => { if(info.menuItemId === contextMenuID_Summarize) { _summarize = true; } - if(_add_tags || _spamfilter || _summarize){ + if(info.menuItemId === contextMenuID_Translate) { + _translate = true; + } + if(_add_tags || _spamfilter || _summarize || _translate){ processEmails({ messages: getMessages(info.selectedMessages), addTagsAuto: _add_tags, spamFilter: _spamfilter, - summarize: _summarize + summarize: _summarize, + translate: _translate }); } }); @@ -1696,7 +1709,8 @@ async function processEmails(args) { spamFilter = false, summarize = false, summarizeOnReceive = false, - translateOnReceive = false + translateOnReceive = false, + translate = false } = args; taWorkingStatus.startWorking(); @@ -1704,7 +1718,7 @@ async function processEmails(args) { // One loop handles addTagsAuto, spamFilter, summarizeOnReceive, and translateOnReceive (on email receive). // The separate summarize block below handles the context menu flow. - if (addTagsAuto || spamFilter || summarizeOnReceive || translateOnReceive) { + if (addTagsAuto || spamFilter || summarizeOnReceive || translateOnReceive || translate) { let prefs_aats = await browser.storage.sync.get({ add_tags_maxnum: prefs_default.add_tags_maxnum, connection_type: prefs_default.connection_type, @@ -1810,12 +1824,17 @@ async function processEmails(args) { }); } - if (translateOnReceive) { + if (translateOnReceive || translate) { if (!curr_fullMessage) { curr_fullMessage = await browser.messages.getFull(message.id); } - taLog.log("[ThunderAI] Pre-caching translation on receive for: " + message.headerMessageId); - await _generateTranslationForMessage(message.headerMessageId, null, { + let translateTabId = null; + if (translate) { + const tabs = await browser.tabs.query({ active: true, currentWindow: true }); + translateTabId = tabs[0].id; + } + taLog.log("[ThunderAI] Generating translation for: " + message.headerMessageId); + await _generateTranslationForMessage(message.headerMessageId, translateTabId, { messageData: { fullMessage: curr_fullMessage } }); } From 894d3475dd153fb3f292193dcd04336a8fc220ba Mon Sep 17 00:00:00 2001 From: mic Date: Mon, 30 Mar 2026 23:07:53 +0200 Subject: [PATCH 100/269] iconse renamed and translate for context menu added --- README.md | 2 +- images/{ai_translation.png => ai_translate.png} | Bin images/{autotags.png => menu_autotags.png} | Bin images/{spamfilter.png => menu_spamfilter.png} | Bin images/{summarize.png => menu_summarize.png} | Bin images/menu_translate.png | Bin 0 -> 1201 bytes js/mzta-compose-script.js | 6 +++--- js/mzta-utils.js | 8 ++++---- 8 files changed, 8 insertions(+), 8 deletions(-) rename images/{ai_translation.png => ai_translate.png} (100%) rename images/{autotags.png => menu_autotags.png} (100%) rename images/{spamfilter.png => menu_spamfilter.png} (100%) rename images/{summarize.png => menu_summarize.png} (100%) create mode 100644 images/menu_translate.png diff --git a/README.md b/README.md index c252873c..f52bf24a 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ _The language status represents the percentage of translated strings in the late - [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 -- [Hilmy Abiyyu A.](https://www.flaticon.com/authors/hilmy-abiyyu-a) for the ai translate icon +- [Hilmy Abiyyu A.](https://www.flaticon.com/authors/hilmy-abiyyu-a) for the ai translate and context menu icons
      diff --git a/images/ai_translation.png b/images/ai_translate.png similarity index 100% rename from images/ai_translation.png rename to images/ai_translate.png diff --git a/images/autotags.png b/images/menu_autotags.png similarity index 100% rename from images/autotags.png rename to images/menu_autotags.png diff --git a/images/spamfilter.png b/images/menu_spamfilter.png similarity index 100% rename from images/spamfilter.png rename to images/menu_spamfilter.png diff --git a/images/summarize.png b/images/menu_summarize.png similarity index 100% rename from images/summarize.png rename to images/menu_summarize.png diff --git a/images/menu_translate.png b/images/menu_translate.png new file mode 100644 index 0000000000000000000000000000000000000000..91abf898a457e40b6dea4b751bc447c7be0e2043 GIT binary patch literal 1201 zcmV;i1Wx;jP)o&+$;#U+FH|9GK-4l=2mk#-GKwOdh?DL@3`VL=l(oDNLAy&P+76heq{pk*-zbnd<~Iyxq_p`}H7 zAYuifYN*FqFxawn8-R48rR80#)w(DHvb#@B&&ZJUdOg!t8``9Xa5_CnLI`T!uf}3F z`z6ZF&&Ou7r7T9=+khe~vL|@Y?qFQgusiGkbbi-CozcjFPd@fbq{s?F2-$DK0tW7b zQwS!sNv5q{gTN|UJql?v8ngMWabCb!$k3BuqHZ%|w6-DFG`hV4cZlQ$c z#s=1?sm#hz8XRl5K9? zv<>=x_01U`3cE@nD-}Di1!gy)HmOa{Nxo^ZU@#aceW`3td~AG*xRtJNX$o}xAdk~wS8jW@I43v1KzcMSjd%9$#WFB}@z5b`Hful+M|)s@ z$J|)ihi(HQ&}D?2G~cz}SH2D?2TFmW%fDTvwQdY=M2&-`q9D zhQeexeHr54gXC4vzYiR-;EW93fM0~Gj1ApkrFSeV0oq@Ocmu491z7^^1-LTeGKCYA zVURR%tbrRN*nkmO_!s%YLl+0h&QC;&H@L)fifiUE5g@tGm0p2310;COS{no7X*lr% zGEfCrmY`2Wm`W!>JXklUBea7EQHw}i<%;QK?7*%Wvc)|8=3}k9`CG*>H3Vbf!>nk3 z4Z?Q=un#Z-fBXa8Md2|ugo%x*o>kfUUkT4K5gy8|FqKgt=KnCTjDYwX5EOF;qHdZ6 P00000NkvXXu0mjf5YQ$W literal 0 HcmV?d00001 diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 58eecbbb..831e7654 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -1207,7 +1207,7 @@ switch (message.command) { translationHeader.style.cssText = 'display: flex; align-items: center; gap: 8px; margin-bottom: 6px;'; const translationIcon = document.createElement('img'); - translationIcon.src = browser.runtime.getURL("/images/ai_translation.png"); + translationIcon.src = browser.runtime.getURL("/images/ai_translate.png"); translationIcon.style.cssText = `height: 16px; width: 16px; flex-shrink: 0;${colors.isDark ? ' filter: invert(1);' : ''}`; const translationTitleSpan = document.createElement('span'); @@ -1324,7 +1324,7 @@ switch (message.command) { genContainer.style.cssText = `background-color: ${colors.translation.bg}; color: ${colors.translation.text}; padding: 0.5rem; border-radius: 4px; border: 1px solid ${colors.translation.border}; font-size: 14px; display: flex; align-items: center; gap: 10px;`; const genIcon = document.createElement('img'); - genIcon.src = browser.runtime.getURL("/images/ai_translation.png"); + genIcon.src = browser.runtime.getURL("/images/ai_translate.png"); genIcon.style.cssText = `height: 16px; width: 16px; flex-shrink: 0;${colors.isDark ? ' filter: invert(1);' : ''}`; const genLoading = document.createElement('img'); @@ -1352,7 +1352,7 @@ switch (message.command) { triggerBtn.style.cssText = `background-color: ${colors.translation.bg}; border: 1px solid ${colors.translation.border}; border-radius: 4px; padding: 4px 8px; cursor: pointer; font-size: 12px; font-style: italic; opacity: 0.7; transition: opacity 0.2s; color: ${colors.translation.text}; display: inline-flex; align-items: center; gap: 6px;`; const triggerIcon = document.createElement('img'); - triggerIcon.src = browser.runtime.getURL("/images/ai_translation.png"); + triggerIcon.src = browser.runtime.getURL("/images/ai_translate.png"); triggerIcon.style.cssText = `height: 14px; width: 14px;${colors.isDark ? ' filter: invert(1);' : ''}`; triggerBtn.appendChild(triggerIcon); diff --git a/js/mzta-utils.js b/js/mzta-utils.js index 74d62eb4..30139131 100644 --- a/js/mzta-utils.js +++ b/js/mzta-utils.js @@ -29,10 +29,10 @@ export const contextMenuID_Spamfilter = 'mzta-spamfilter'; export const contextMenuID_Summarize = 'mzta-summarize'; export const contextMenuID_Translate = 'mzta-translate'; export const contextMenuIconsPath = { - [contextMenuID_AddTags]: 'moz-extension:images/autotags.png', - [contextMenuID_Spamfilter]: 'moz-extension:images/spamfilter.png', - [contextMenuID_Summarize]: 'moz-extension:images/summarize.png', - [contextMenuID_Translate]: 'moz-extension:images/ai_translation.png', + [contextMenuID_AddTags]: 'moz-extension:images/menu_autotags.png', + [contextMenuID_Spamfilter]: 'moz-extension:images/menu_spamfilter.png', + [contextMenuID_Summarize]: 'moz-extension:images/menu_summarize.png', + [contextMenuID_Translate]: 'moz-extension:images/menu_translate.png', }; export function getLanguageDisplayName(languageCode) { From 38f6b1987dd107180aad61bab0f960a62fcd2f1c Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 1 Apr 2026 00:19:35 +0200 Subject: [PATCH 101/269] the translation prompt now returns a JSON. see #247 --- _locales/en/messages.json | 6 +++++- js/mzta-compose-script.js | 10 +++++++++- js/mzta-storage.js | 20 ++++++++++++++++++-- js/mzta-translationstore.js | 4 +++- js/mzta-utils-prompt.js | 19 +++++++++++++------ mzta-background.js | 16 +++++++++++++++- 6 files changed, 63 insertions(+), 12 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 12bdb601..2e9377f2 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -821,7 +821,7 @@ "description": "" }, "prompt_translate_this_full_text": { - "message": "Translate the following email in", + "message": "Translate the email below into {%thunderai_translate_lang%}.\n\nRules:\n- Translate both the subject and the body.\n- Return the result as a JSON object with three fields: \"subject\", \"body\" and \"status\".\n- If the translation has been done the status is equal to 1.\n- If the email is written in one of these languages \"{%thunderai_translate_exclude_lang%}\" or in the {%thunderai_translate_lang%} language, return an empty string for the body and the subject and set the status to -1.\n- Do not add explanations, notes, or any text outside the JSON.\n\nMail subject: {%mail_subject%}\n\nMail body: {%mail_html_body%}\n\nGenerate a response in JSON format only. The output should be only a JSON object. Here is an example of the JSON format to be used:\n{\n\"subject\": \"subject translation\",\n\"body\": \"body translation\",\n\"status\": \"status result\"\n}", "description": "" }, "prompt_this_full_text": { @@ -2189,6 +2189,10 @@ "message": "Translation language is not configured. Please set a language in the Translation settings or set a default language in the General settings.", "description": "" }, + "translate_skipped": { + "message": "Translation skipped: Language excluded or identical to target.", + "description": "Shown in the translation banner when the email language matches the excluded or target language" + }, "antispam_by": { "message": "Antispam by", "description": "" diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 831e7654..a3183507 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -1255,14 +1255,22 @@ switch (message.command) { translationText.style.cssText = 'white-space: pre-wrap; line-height: 1.5;'; if (translationData.error) { translationText.textContent = translationData.message || browser.i18n.getMessage("translate_error"); + } else if (translationData.translation_status === '-1') { + translationText.textContent = browser.i18n.getMessage("translate_skipped"); } else { + if (translationData.translated_subject) { + const subjectEl = document.createElement('div'); + subjectEl.style.cssText = 'font-weight: bold; margin-bottom: 4px;'; + subjectEl.textContent = translationData.translated_subject; + translationTextWrapper.appendChild(subjectEl); + } translationText.textContent = translationData.translated_text || ''; } translationTextWrapper.appendChild(translationText); const maxLenTranslation = translationData.maxDisplayLength || 0; const fullTranslationText = translationData.translated_text || ''; - if (!translationData.error && maxLenTranslation > 0 && fullTranslationText.length > maxLenTranslation) { + if (!translationData.error && translationData.translation_status !== '-1' && maxLenTranslation > 0 && fullTranslationText.length > maxLenTranslation) { translationText.style.overflow = 'hidden'; translationText.style.transition = 'max-height 0.2s ease'; diff --git a/js/mzta-storage.js b/js/mzta-storage.js index 9b29c6b5..ba332f35 100644 --- a/js/mzta-storage.js +++ b/js/mzta-storage.js @@ -278,7 +278,15 @@ export class taStorage { * @param {string} lang - Target language code. * @param {boolean} [force=true] - If true, overwrite existing translation data. */ - async writeTranslation(messageId, translated_text, lang, force = true, error = false, error_message = '') { + async writeTranslation(messageId, data, force = true) { + const { + translated_text = '', + translated_subject = '', + translation_status = '', + lang = '', + error = false, + message = '', + } = data || {}; this.taLog.log('[writeTranslation] messageId: ' + messageId + ', lang: ' + lang + ', force: ' + force); try { let key = this._buildKey(messageId); @@ -288,7 +296,15 @@ export class taStorage { return; } let now = Date.now(); - record[taStorage.FIELD_TRANSLATION] = { translated_text: translated_text, lang: lang, error: error, message: error_message, ts: now }; + record[taStorage.FIELD_TRANSLATION] = { + translated_text, + translated_subject, + translation_status, + lang, + error, + message, + ts: now + }; record.ts = now; await messenger.storage.local.set({ [key]: record }); } catch (e) { diff --git a/js/mzta-translationstore.js b/js/mzta-translationstore.js index 4f3dbe48..1307596a 100644 --- a/js/mzta-translationstore.js +++ b/js/mzta-translationstore.js @@ -49,7 +49,7 @@ export class taTranslationStore { async saveTranslation(data, data_id) { this.taLog.log("[saveTranslation] data_id: " + data_id); try { - await this._storage.writeTranslation(data_id, data.translated_text || '', data.lang || '', true, data.error || false, data.message || ''); + await this._storage.writeTranslation(data_id, data, true); await browser.storage.session.remove(this._processing_prefix + data_id); } catch (e) { this.taLog.error("[saveTranslation] error: " + e); @@ -81,6 +81,8 @@ export class taTranslationStore { return { headerMessageId: data_id, translated_text: translation.translated_text || '', + translated_subject: translation.translated_subject || '', + translation_status: translation.translation_status || '', lang: translation.lang || '', error: translation.error || false, message: translation.message || '', diff --git a/js/mzta-utils-prompt.js b/js/mzta-utils-prompt.js index b123ff29..a100e265 100644 --- a/js/mzta-utils-prompt.js +++ b/js/mzta-utils-prompt.js @@ -173,7 +173,7 @@ export const taPromptUtils = { return { promptText, promptInfo: prompt }; }, - async buildTranslationPrompt(fullMessage, lang) { + async buildTranslationPrompt(fullMessage) { const specialPrompts = await getSpecialPrompts(); const prompt = specialPrompts.find(p => p.id === 'prompt_translate_this'); @@ -183,12 +183,19 @@ export const taPromptUtils = { } const bodyHtml = getMailBody(fullMessage); - let bodyText = htmlBodyToPlainText(bodyHtml.html); - if (bodyText.length === 0) { - bodyText = bodyHtml.text || ''; - } + const mailSubject = fullMessage.headers?.subject?.[0] || ''; - const fullPrompt = promptText + " " + lang + ". \"" + bodyText + "\""; + const finalSubs = await placeholdersUtils.getPlaceholdersValues({ + prompt_text: promptText, + msg_text: { html: bodyHtml.html, text: bodyHtml.text }, + mail_subject: mailSubject, + }); + + const fullPrompt = placeholdersUtils.replacePlaceholders({ + text: promptText, + replacements: finalSubs, + use_default_value: false, + }); return { promptText: fullPrompt, promptInfo: prompt }; }, diff --git a/mzta-background.js b/mzta-background.js index ffb87c08..a78699f7 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -789,8 +789,22 @@ async function _generateTranslationForMessage(headerMessageId, tabId = null, opt await cmd.initWorker(); const aiResponse = await cmd.sendPrompt(); + let translatedBody = ''; + let translatedSubject = ''; + let translationStatus = ''; + try { + const parsed = JSON.parse(aiResponse); + translatedBody = parsed.body || ''; + translatedSubject = parsed.subject || ''; + translationStatus = String(parsed.status || ''); + } catch (e) { + translatedBody = aiResponse; + } + const translationData = { - translated_text: aiResponse, + translated_text: translatedBody, + translated_subject: translatedSubject, + translation_status: translationStatus, lang: lang, headerMessageId: headerMessageId }; From c0ebc646b862829294439b66bd0769a9727fce43 Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 1 Apr 2026 00:27:47 +0200 Subject: [PATCH 102/269] some fixes on JSON translation. see #247 --- js/mzta-compose-script.js | 114 +++++++++++++++++++++++++++----------- js/mzta-storage.js | 9 ++- mzta-background.js | 21 +++++-- 3 files changed, 106 insertions(+), 38 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index a3183507..b60d006b 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -177,6 +177,21 @@ function _updatePanelMargins() { if (lastPanel) lastPanel.style.marginBottom = '1rem'; } +function _isHtml(text) { + return /<[a-z][^>]*>/i.test(text); +} + +function _renderSafeHtml(container, html) { + container.textContent = ''; + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + doc.querySelectorAll('script, img').forEach(el => el.remove()); + while (doc.body.firstChild) { + container.appendChild(doc.body.firstChild); + } + container.querySelectorAll('p').forEach(p => { p.style.marginBlockStart = '0'; }); +} + function createThreeDotsMenu(isDark, menuItems, panelColors) { const wrapper = document.createElement('div'); wrapper.style.cssText = 'position: relative; display: inline-flex; align-items: center;'; @@ -1264,54 +1279,89 @@ switch (message.command) { subjectEl.textContent = translationData.translated_subject; translationTextWrapper.appendChild(subjectEl); } - translationText.textContent = translationData.translated_text || ''; + const bodyText = translationData.translated_text || ''; + const bodyIsHtml = _isHtml(bodyText); + if (bodyIsHtml) { + translationText.style.whiteSpace = ''; + _renderSafeHtml(translationText, bodyText); + } else { + translationText.textContent = bodyText; + } } translationTextWrapper.appendChild(translationText); const maxLenTranslation = translationData.maxDisplayLength || 0; const fullTranslationText = translationData.translated_text || ''; + const fullTranslationIsHtml = _isHtml(fullTranslationText); if (!translationData.error && translationData.translation_status !== '-1' && maxLenTranslation > 0 && fullTranslationText.length > maxLenTranslation) { translationText.style.overflow = 'hidden'; translationText.style.transition = 'max-height 0.2s ease'; - let cutPos = fullTranslationText.lastIndexOf(' ', maxLenTranslation); - if (cutPos <= 0) cutPos = maxLenTranslation; - const truncatedTranslation = fullTranslationText.substring(0, cutPos) + '\u2026'; - translationText.textContent = truncatedTranslation; - - requestAnimationFrame(() => { - translationText.style.maxHeight = translationText.scrollHeight + 'px'; - }); - const toggleLink = document.createElement('a'); toggleLink.textContent = browser.i18n.getMessage("translate_see_more") || "See more"; toggleLink.href = '#'; toggleLink.style.cssText = `display: inline-block; margin-top: 4px; font-size: 13px; color: ${colors.linkColor}; cursor: pointer; text-decoration: underline;`; - let expanded = false; - toggleLink.addEventListener('click', (e) => { - e.preventDefault(); - if (!expanded) { - translationText.textContent = fullTranslationText; + if (fullTranslationIsHtml) { + const collapsedMaxHeight = '4.2em'; + translationText.style.maxHeight = collapsedMaxHeight; + + let expanded = false; + toggleLink.addEventListener('click', (e) => { + e.preventDefault(); + if (!expanded) { + translationText.style.maxHeight = translationText.scrollHeight + 'px'; + toggleLink.textContent = browser.i18n.getMessage("translate_see_less") || "See less"; + } else { + translationText.style.maxHeight = collapsedMaxHeight; + toggleLink.textContent = browser.i18n.getMessage("translate_see_more") || "See more"; + } + expanded = !expanded; + }); + + requestAnimationFrame(() => { + if (translationText.scrollHeight > translationText.clientHeight) { + translationTextWrapper.appendChild(toggleLink); + } else { + translationText.style.maxHeight = ''; + translationText.style.overflow = ''; + } + }); + } else { + let cutPos = fullTranslationText.lastIndexOf(' ', maxLenTranslation); + if (cutPos <= 0) cutPos = maxLenTranslation; + const truncatedTranslation = fullTranslationText.substring(0, cutPos) + '\u2026'; + translationText.textContent = truncatedTranslation; + + requestAnimationFrame(() => { translationText.style.maxHeight = translationText.scrollHeight + 'px'; - toggleLink.textContent = browser.i18n.getMessage("translate_see_less") || "See less"; - } else { - translationText.textContent = truncatedTranslation; - const collapsedHeight = translationText.scrollHeight; - translationText.textContent = fullTranslationText; - translationText.style.maxHeight = translationText.scrollHeight + 'px'; - requestAnimationFrame(() => { - translationText.style.maxHeight = collapsedHeight + 'px'; - }); - translationText.addEventListener('transitionend', function handler() { - translationText.removeEventListener('transitionend', handler); + }); + + let expanded = false; + toggleLink.addEventListener('click', (e) => { + e.preventDefault(); + if (!expanded) { + translationText.textContent = fullTranslationText; + translationText.style.maxHeight = translationText.scrollHeight + 'px'; + toggleLink.textContent = browser.i18n.getMessage("translate_see_less") || "See less"; + } else { translationText.textContent = truncatedTranslation; - }); - toggleLink.textContent = browser.i18n.getMessage("translate_see_more") || "See more"; - } - expanded = !expanded; - }); - translationTextWrapper.appendChild(toggleLink); + const collapsedHeight = translationText.scrollHeight; + translationText.textContent = fullTranslationText; + translationText.style.maxHeight = translationText.scrollHeight + 'px'; + requestAnimationFrame(() => { + translationText.style.maxHeight = collapsedHeight + 'px'; + }); + translationText.addEventListener('transitionend', function handler() { + translationText.removeEventListener('transitionend', handler); + translationText.textContent = truncatedTranslation; + }); + toggleLink.textContent = browser.i18n.getMessage("translate_see_more") || "See more"; + } + expanded = !expanded; + }); + translationTextWrapper.appendChild(toggleLink); + } } translationContainer.appendChild(translationTextWrapper); diff --git a/js/mzta-storage.js b/js/mzta-storage.js index ba332f35..ce3bfa08 100644 --- a/js/mzta-storage.js +++ b/js/mzta-storage.js @@ -274,8 +274,13 @@ export class taStorage { /** * Write the translation field for a given Message-ID. * @param {string} messageId - The Message-ID header string. - * @param {string} translated_text - The translated text. - * @param {string} lang - Target language code. + * @param {Object} data - Translation data object. + * @param {string} [data.translated_text=''] - The translated body text. + * @param {string} [data.translated_subject=''] - The translated subject. + * @param {string} [data.translation_status=''] - Status: "1" = ok, "-1" = skipped. + * @param {string} [data.lang=''] - Target language code. + * @param {boolean} [data.error=false] - Whether an error occurred. + * @param {string} [data.message=''] - Error message. * @param {boolean} [force=true] - If true, overwrite existing translation data. */ async writeTranslation(messageId, data, force = true) { diff --git a/mzta-background.js b/mzta-background.js index a78699f7..5e9344b1 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -430,7 +430,18 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { case 'chatgpt_saveTranslation': async function _saveTranslationFromWebchat(msg) { try { - let translatedText = msg.text.trim(); + let rawText = msg.text.trim(); + let translatedBody = ''; + let translatedSubject = ''; + let translationStatus = ''; + try { + const parsed = JSON.parse(rawText); + translatedBody = parsed.body || ''; + translatedSubject = parsed.subject || ''; + translationStatus = String(parsed.status || ''); + } catch (e) { + translatedBody = rawText; + } let prefs_tr = await browser.storage.sync.get({ translate_lang: prefs_default.translate_lang, default_chatgpt_lang: prefs_default.default_chatgpt_lang, @@ -438,7 +449,9 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { }); let lang = prefs_tr.translate_lang || prefs_tr.default_chatgpt_lang || ''; const translationData = { - translated_text: translatedText, + translated_text: translatedBody, + translated_subject: translatedSubject, + translation_status: translationStatus, lang: lang, headerMessageId: msg.headerMessageId }; @@ -777,7 +790,7 @@ async function _generateTranslationForMessage(headerMessageId, tabId = null, opt taWorkingStatus.stopWorking(); return; } - const { promptText } = await taPromptUtils.buildTranslationPrompt(fullMessage, lang); + const { promptText } = await taPromptUtils.buildTranslationPrompt(fullMessage); const cmd = new mzta_specialCommand({ prompt: promptText, @@ -996,7 +1009,7 @@ async function _openTranslationWebchat(headerMessageId, tabId) { taLog.warn("Translation skipped: no language configured (translate_lang and default_chatgpt_lang are both empty)."); return; } - const { promptText, promptInfo } = await taPromptUtils.buildTranslationPrompt(curr_message_full, lang); + const { promptText, promptInfo } = await taPromptUtils.buildTranslationPrompt(curr_message_full); promptInfo.headerMessageId = headerMessageId; promptInfo.translationTabId = tabId; From 83f856d1ae6ce24b37225a0a83c75a3a3a29eb4a Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 1 Apr 2026 00:34:26 +0200 Subject: [PATCH 103/269] trasnlate webchat removed. see #247 --- _locales/en/messages.json | 12 --- api_webchat/messagesArea.js | 23 ------ claude-spec/01-architecture.md | 17 ++-- claude-spec/02-prompts.md | 18 ++--- claude-spec/05-options.md | 11 +-- js/mzta-compose-script.js | 2 +- mzta-background.js | 115 +--------------------------- options/mzta-options-default.js | 1 - pages/translate/mzta-translate.html | 12 --- pages/translate/mzta-translate.js | 21 ----- 10 files changed, 21 insertions(+), 211 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 2e9377f2..92e9bad0 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -239,10 +239,6 @@ "message": "Save as Summary", "description": "Button label in the webchat window to save the AI response as a message summary" }, - "webchat_save_as_translation": { - "message": "Save as Translation", - "description": "Button label in the webchat window to save the AI response as a message translation" - }, "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": "" @@ -2101,10 +2097,6 @@ "message": "Choose when to translate messages: disabled, only when clicking the button, or automatically when opening a message.", "description": "" }, - "prefs_OptionText_translate_display_mode": { - "message": "Display mode for translations", - "description": "" - }, "prefs_OptionText_display_mode_inline": { "message": "Message pane (inline)", "description": "" @@ -2113,10 +2105,6 @@ "message": "Chat window", "description": "" }, - "prefs_OptionText_translate_display_mode_Info": { - "message": "Choose where to display translations. Note: automatic mode always uses inline display.", - "description": "" - }, "prefs_OptionText_translate_max_display_length": { "message": "Maximum length of displayed translation", "description": "" diff --git a/api_webchat/messagesArea.js b/api_webchat/messagesArea.js index 62fffa42..e2611801 100644 --- a/api_webchat/messagesArea.js +++ b/api_webchat/messagesArea.js @@ -481,29 +481,6 @@ class MessagesArea extends HTMLElement { selectionInfo.style.display = "block"; } - // Save as Translation button (only shown for translation webchat sessions) - if(promptData.prompt_info?.headerMessageId && promptData.prompt_info?.translationTabId) { - const saveTranslationButton = document.createElement('button'); - saveTranslationButton.textContent = browser.i18n.getMessage("webchat_save_as_translation"); - saveTranslationButton.classList.add('action_btn'); - saveTranslationButton.addEventListener('click', async () => { - let finalText = removeAloneBRs(fullTextHTMLAtAssignment); - const selectedHTML = this.getCurrentSelectionHTML(); - if(selectedHTML != "") { - finalText = removeAloneBRs(selectedHTML); - } - await browser.runtime.sendMessage({ - command: "chatgpt_saveTranslation", - text: finalText, - headerMessageId: promptData.prompt_info.headerMessageId, - tabId: promptData.prompt_info.translationTabId || promptData.tabId, - }); - browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id}); - }); - actionButtons.appendChild(saveTranslationButton); - selectionInfo.style.display = "block"; - } - // diff viewer button if(promptData.prompt_info?.use_diff_viewer == "1") { const diffvButton = document.createElement('button'); diff --git a/claude-spec/01-architecture.md b/claude-spec/01-architecture.md index 8f5e3e42..636bda65 100644 --- a/claude-spec/01-architecture.md +++ b/claude-spec/01-architecture.md @@ -82,13 +82,8 @@ mzta-background.js (checks summarize_auto + summarize_display_mode prefs) ### Data Flow: Inline Translation on Message Display -The `translate_display_mode` preference (`'inline'` or `'webchat'`) controls where -the translation is displayed. The `translate_auto` preference controls when it is triggered. - -- `translate_auto = 2` (automatic) always generates inline, regardless of `translate_display_mode`. -- `translate_auto = 1` (manual button) respects `translate_display_mode`: - - `'inline'` → button click triggers inline generation - - `'webchat'` → button click opens the AI chat window via `_openTranslationWebchat()` +The `translate_auto` preference controls when translation is triggered. +Translation always renders inline (webchat mode has been removed). The target language is determined by `translate_lang` (fallback on `default_chatgpt_lang`). @@ -97,16 +92,14 @@ User opens/selects a message in Thunderbird ↓ mzta-compose-script.js (sends "initTranslation" to background) ↓ -mzta-background.js (checks translate + translate_auto + translate_display_mode prefs) +mzta-background.js (checks translate + translate_auto prefs) ↓ ┌──────────────────────────────────────────────────────────┐ │ translate_auto = 0 → do nothing │ │ translate_auto = 1 → show "click to translate" button │ - │ display_mode = inline → click triggers inline gen │ - │ display_mode = webchat → click opens chat window │ - │ translate_auto = 2 → generate immediately (always inline)│ + │ translate_auto = 2 → generate immediately │ └──────────────────────────────────────────────────────────┘ - ↓ (if generating inline) + ↓ taTranslationStore (check cache / set processing) ↓ (cache miss) mzta-special-commands (via Web Worker, NOT chatgpt_web) diff --git a/claude-spec/02-prompts.md b/claude-spec/02-prompts.md index c2c3eaae..60b1624f 100644 --- a/claude-spec/02-prompts.md +++ b/claude-spec/02-prompts.md @@ -88,24 +88,24 @@ The summarize feature uses two distinct prompt pathways: ### Translate: Inline-Only Prompt System -The translate feature uses a single special prompt (`prompt_translate_this`) for translating emails. It supports both inline display and webchat mode, but has no context menu entry. +The translate feature uses a single special prompt (`prompt_translate_this`) for translating emails. Translation always renders inline (no webchat mode). -**Inline Translation on Message Display** (controlled by `translate_auto` and `translate_display_mode` prefs): +**Inline Translation on Message Display** (controlled by `translate_auto` pref): - Uses a single special prompt: `prompt_translate_this` -- The prompt text is appended with the target language and the email body: `prompt_text + " " + lang + ". \"" + body_text + "\""` +- The prompt uses placeholders (`{%mail_subject%}`, `{%mail_html_body%}`, `{%thunderai_translate_lang%}`, `{%thunderai_translate_exclude_lang%}`) resolved via the standard placeholder system +- The AI response is a JSON object: `{ "subject": "...", "body": "...", "status": "1"|"-1" }` + - `status = "1"`: translation completed, subject and body are displayed + - `status = "-1"`: translation skipped (excluded/target language), a "skipped" message is shown - Target language is determined by `translate_lang` pref, falling back to `default_chatgpt_lang` - Does **not** support `chatgpt_web` connection type (shows error if configured) -- `translate_display_mode = 'inline'`: result is rendered as a styled banner (green/teal theme) in the message body via `mzta-compose-script.js` -- `translate_display_mode = 'webchat'`: opens AI chat window; webchat shows a "Save as Translation" button to persist the result inline -- `translate_auto = 2` (automatic) always generates inline regardless of `translate_display_mode` +- Result is rendered as a styled banner (green/teal theme) in the message body via `mzta-compose-script.js` - Banner includes refresh (↻) and delete (×) buttons - Cached per-message via `taTranslationStore` / `taStorage` (max 100 entries) - The prompt was originally a regular prompt (`defaultPrompts`) and was moved to `specialPrompts` with `is_special: "1"` and `type: "1"` (reading email only) -**Prompt Building** — `taPromptUtils.buildTranslationPrompt(fullMessage, lang)`: +**Prompt Building** — `taPromptUtils.buildTranslationPrompt(fullMessage)`: - Retrieves the `prompt_translate_this` special prompt text -- Extracts the email body from the full message -- Combines prompt + language + body text +- Resolves placeholders via `placeholdersUtils.getPlaceholdersValues()` + `replacePlaceholders()` - Returns `{ promptText, promptInfo }` ## Prompt Types Reference diff --git a/claude-spec/05-options.md b/claude-spec/05-options.md index 06c0d250..d8d14ef3 100644 --- a/claude-spec/05-options.md +++ b/claude-spec/05-options.md @@ -100,7 +100,6 @@ These are generated programmatically at the bottom of `mzta-options-default.js` | `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. | | `translate` | `true` | Enable email translation | | `translate_auto` | `0` | Auto-translate mode: `0` = disabled, `1` = manual (show button), `2` = automatic (translate on message open), `3` = generate on email receive (background pre-cache via `onNewMailReceived`, no UI during generation) | -| `translate_display_mode` | `'inline'` | Where to display translations: `'inline'` = message pane banner, `'webchat'` = AI chat window. Note: `translate_auto = 2` and `translate_auto = 3` always use inline regardless of this setting. | | `translate_max_display_length` | `0` | Maximum characters shown in inline translation 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. | | `translate_lang` | `''` | Target language for translation. Falls back to `default_chatgpt_lang` if empty. | @@ -134,13 +133,9 @@ The translate settings page provides: - `0` (Disabled) — no inline translations - `1` (Manual) — shows a "Get AI Translation" button in message display - `2` (Automatic) — generates translation immediately when message is opened -3. **Display mode dropdown** (`translate_display_mode`) — controls where translations are shown: - - `'inline'` — translation banner in the message pane (default) - - `'webchat'` — opens the AI chat window with a "Save as Translation" button - - Note: `translate_auto = 2` always generates inline regardless of this setting. -4. **Max display length** (`translate_max_display_length`) — number input, limits inline translation text to N characters. `0` = no limit. When truncated, a "See more"/"See less" toggle link is appended. -5. **Target language** (`translate_lang`) — text input for the destination language. If empty, falls back to `default_chatgpt_lang`. -6. **One editable prompt** — the translation instruction prompt (`prompt_translate_this`) with Save/Reset buttons and placeholder autocomplete. Default text comes from i18n string `prompt_translate_this_full_text`. +3. **Max display length** (`translate_max_display_length`) — number input, limits inline translation text to N characters. `0` = no limit. When truncated, a "See more"/"See less" toggle link is appended. +4. **Target language** (`translate_lang`) — text input for the destination language. If empty, falls back to `default_chatgpt_lang`. +5. **One editable prompt** — the translation instruction prompt (`prompt_translate_this`) with Save/Reset buttons and placeholder autocomplete. Default text comes from i18n string `prompt_translate_this_full_text`. ## Adding a New Preference diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index b60d006b..46e344a4 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -1423,7 +1423,7 @@ switch (message.command) { triggerBtn.onclick = () => { _removeToolbarItem('mzta-toolbar-translation'); browser.runtime.sendMessage({ - command: message.webchat ? "triggerTranslationWebchat" : "triggerTranslationGeneration", + command: "triggerTranslationGeneration", headerMessageId: message.headerMessageId }); }; diff --git a/mzta-background.js b/mzta-background.js index 5e9344b1..7a4b0a27 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -335,7 +335,7 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { async function _initTranslation() { try { let tabId = sender.tab.id; - let prefs = await browser.storage.sync.get({ translate: prefs_default.translate, translate_auto: prefs_default.translate_auto, translate_display_mode: prefs_default.translate_display_mode, translate_max_display_length: prefs_default.translate_max_display_length }); + let prefs = await browser.storage.sync.get({ translate: prefs_default.translate, translate_auto: prefs_default.translate_auto, translate_max_display_length: prefs_default.translate_max_display_length }); if (!prefs.translate) return; @@ -364,11 +364,7 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { } // Manual button mode (translate_auto === 1) - if (prefs.translate_display_mode === 'inline') { - browser.tabs.sendMessage(tabId, { command: "showTranslationButton", headerMessageId: message.headerMessageId }); - } else { - browser.tabs.sendMessage(tabId, { command: "showTranslationButton", headerMessageId: message.headerMessageId, webchat: true }); - } + browser.tabs.sendMessage(tabId, { command: "showTranslationButton", headerMessageId: message.headerMessageId }); } catch (e) { taLog.error("Error in initTranslation: " + e); } @@ -393,83 +389,17 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { } _triggerTranslationGeneration(message); break; - case 'triggerTranslationWebchat': - async function _triggerTranslationWebchat(message) { - let tabId = sender.tab.id; - let prefs_tw = await browser.storage.sync.get({ - translate_lang: prefs_default.translate_lang, - default_chatgpt_lang: prefs_default.default_chatgpt_lang - }); - const lang_tw = prefs_tw.translate_lang || prefs_tw.default_chatgpt_lang || ''; - if (!lang_tw) { - let tabs = await browser.tabs.query({ active: true, currentWindow: true }); - browser.tabs.sendMessage(tabId, { command: "sendAlert", curr_tab_type: tabs[0].type, message: browser.i18n.getMessage('translate_no_language_configured') }); - browser.tabs.sendMessage(tabId, { command: "showTranslationButton", headerMessageId: message.headerMessageId, webchat: true }); - return; - } - await _openTranslationWebchat(message.headerMessageId, tabId); - } - _triggerTranslationWebchat(message); - break; case 'refreshTranslation': async function _refreshTranslation(message) { let tabId = sender.tab.id; await translationStore.removeTranslation(message.headerMessageId); - let prefs_refresh_tr = await browser.storage.sync.get({ translate_display_mode: prefs_default.translate_display_mode }); - if (prefs_refresh_tr.translate_display_mode === 'webchat') { - await _openTranslationWebchat(message.headerMessageId, tabId); - } else { - await _generateTranslationForMessage(message.headerMessageId, tabId); - } + await _generateTranslationForMessage(message.headerMessageId, tabId); } _refreshTranslation(message); break; case 'removeTranslation': translationStore.removeTranslation(message.headerMessageId); break; - case 'chatgpt_saveTranslation': - async function _saveTranslationFromWebchat(msg) { - try { - let rawText = msg.text.trim(); - let translatedBody = ''; - let translatedSubject = ''; - let translationStatus = ''; - try { - const parsed = JSON.parse(rawText); - translatedBody = parsed.body || ''; - translatedSubject = parsed.subject || ''; - translationStatus = String(parsed.status || ''); - } catch (e) { - translatedBody = rawText; - } - let prefs_tr = await browser.storage.sync.get({ - translate_lang: prefs_default.translate_lang, - default_chatgpt_lang: prefs_default.default_chatgpt_lang, - translate_max_display_length: prefs_default.translate_max_display_length - }); - let lang = prefs_tr.translate_lang || prefs_tr.default_chatgpt_lang || ''; - const translationData = { - translated_text: translatedBody, - translated_subject: translatedSubject, - translation_status: translationStatus, - lang: lang, - headerMessageId: msg.headerMessageId - }; - await translationStore.saveTranslation(translationData, msg.headerMessageId); - try { - browser.tabs.sendMessage(msg.tabId, { - command: "showTranslation", - data: { ...translationData, maxDisplayLength: prefs_tr.translate_max_display_length } - }); - } catch (e) { - taLog.error("Error sending showTranslation to tab: " + e); - } - } catch (error) { - console.error("[ThunderAI] Error saving translation from webchat:", error); - } - } - _saveTranslationFromWebchat(message); - break; case 'chatgpt_close': async function _closeChatGptWindow(window_id) { let prefs_close = await browser.storage.sync.get({chatgpt_win_save_position: prefs_default.chatgpt_win_save_position}); @@ -980,45 +910,6 @@ async function _openSummaryWebchat(headerMessageId, tabId) { } } -async function _openTranslationWebchat(headerMessageId, tabId) { - try { - const messageResult = await browser.messages.query({ headerMessageId: headerMessageId }); - if (!messageResult || messageResult.messages.length === 0) { - console.error("[ThunderAI] _openTranslationWebchat: 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 prefs = await browser.storage.sync.get({ - ...prefs_default, - translate_lang: prefs_default.translate_lang, - default_chatgpt_lang: prefs_default.default_chatgpt_lang - }); - const connectionType = getConnectionType(prefs, {}, 'translate'); - if (connectionType === 'chatgpt_web') { - const errorMsg = browser.i18n.getMessage('translate_chatgpt_web_not_supported'); - await translationStore.saveError(headerMessageId, errorMsg); - browser.tabs.sendMessage(tabId, { command: "showTranslation", data: { error: true, message: errorMsg } }); - return; - } - - const lang = prefs.translate_lang || prefs.default_chatgpt_lang || ''; - if (!lang) { - taLog.warn("Translation skipped: no language configured (translate_lang and default_chatgpt_lang are both empty)."); - return; - } - const { promptText, promptInfo } = await taPromptUtils.buildTranslationPrompt(curr_message_full); - promptInfo.headerMessageId = headerMessageId; - promptInfo.translationTabId = tabId; - - openChatGPT(promptText, promptInfo.action, tabId, promptInfo.name, promptInfo.need_custom_text, promptInfo); - } catch (error) { - console.error("[ThunderAI] Error opening translation webchat:", error); - } -} - // Listen for messages from ThunderAI-Sparks browser.runtime.onMessageExternal.addListener((message, sender, sendResponse) => { switch (message.action) { diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index c1015507..4ac38c96 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -143,7 +143,6 @@ export const prefs_default = { summarize_max_display_length: 0, // 0 = no limit, otherwise max chars shown inline translate: true, translate_auto: 0, // 0: disabled, 1: manual button, 2: automatic on message open, 3: generate on email receive - translate_display_mode: 'inline', // 'inline' or 'webchat' translate_max_display_length: 0, // 0 = no limit, otherwise max chars shown inline translate_lang: '', // target language, fallback on default_chatgpt_lang translate_exclude_lang: '', // languages to do not translate diff --git a/pages/translate/mzta-translate.html b/pages/translate/mzta-translate.html index dc7a4e9d..b582b36e 100644 --- a/pages/translate/mzta-translate.html +++ b/pages/translate/mzta-translate.html @@ -40,18 +40,6 @@ - - __MSG_prefs_OptionText_translate_display_mode__ - - - - __MSG_prefs_OptionText_translate_max_display_length__ diff --git a/pages/translate/mzta-translate.js b/pages/translate/mzta-translate.js index a4812808..df415879 100644 --- a/pages/translate/mzta-translate.js +++ b/pages/translate/mzta-translate.js @@ -74,8 +74,6 @@ document.addEventListener("DOMContentLoaded", async () => { document.querySelectorAll(".option-input").forEach(element => { element.addEventListener("change", saveOptions); }); - document.getElementById('translate_auto').addEventListener('change', updateDisplayModeConstraint); - let translate_textarea = document.getElementById("translate_prompt_text"); let translate_save_btn = document.getElementById("btn_save_prompt"); let translate_reset_btn = document.getElementById("btn_reset_prompt"); @@ -119,21 +117,6 @@ document.addEventListener("DOMContentLoaded", async () => { // Methods to manage options, derived from: /options/mzta-options.js -function updateDisplayModeConstraint() { - const translate_auto_el = document.getElementById('translate_auto'); - const display_mode_el = document.getElementById('translate_display_mode'); - const autoVal = String(translate_auto_el.value); - if (autoVal === '2' || autoVal === '3') { - display_mode_el.value = 'inline'; - display_mode_el.disabled = true; - browser.storage.sync.set({ translate_display_mode: 'inline' }); - } else if (autoVal === '0') { - display_mode_el.disabled = true; - } else { - display_mode_el.disabled = false; - } -} - function saveOptions(e) { e.preventDefault(); let options = {}; @@ -191,9 +174,6 @@ async function restoreOptions() { if (element.id === 'translate_auto') { default_select_value = prefs_default.translate_auto; } - if (element.id === 'translate_display_mode') { - default_select_value = prefs_default.translate_display_mode; - } const restoreValue = result[element.id] ?? default_select_value; let optionExists = Array.from(element.options).some(opt => opt.value === String(restoreValue)); if (element.tomselect) { @@ -248,5 +228,4 @@ async function restoreOptions() { } setCurrentChoice(getting); - updateDisplayModeConstraint(); } From f1f8d4d3eb614383545241fdfe06758a32b3bd7a Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 1 Apr 2026 23:10:18 +0200 Subject: [PATCH 104/269] typo fixed --- CHANGELOG.md | 2 +- options/mzta-release-notes.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0772a115..8c45aef8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@

      Version 4.1.0 - ??/??/2026

      • Antispam information are now permanently saved for each message [#675].
      • -
      • [All APIs] A summaru has been added above the mail content [#580].
      • +
      • [All APIs] A summary has been added above the mail content [#580].
      • ...

      Version 4.0.3 - 20/03/2026

      diff --git a/options/mzta-release-notes.html b/options/mzta-release-notes.html index 3f21a98c..a27a7b28 100644 --- a/options/mzta-release-notes.html +++ b/options/mzta-release-notes.html @@ -10,7 +10,7 @@

      Version 4.1.0 - ??/??/2026

      • Antispam information are now permanently saved for each message [#675].
      • -
      • [All APIs] A summaru has been added above the mail content [#580].
      • +
      • [All APIs] A summary has been added above the mail content [#580].
      • ...

      Version 4.0.3 - 20/03/2026

      From c50406bae025a5b3d0c1e53cf576bf54a9bb9e86 Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 1 Apr 2026 23:03:34 +0200 Subject: [PATCH 105/269] normalizing also accented characters when creating new tags. see #732 --- js/mzta-utils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/js/mzta-utils.js b/js/mzta-utils.js index 30139131..ce5dc9d4 100644 --- a/js/mzta-utils.js +++ b/js/mzta-utils.js @@ -519,6 +519,8 @@ function getTagsKeyFromLabel(tag_names, all_tags_list) { } function sanitizeString(input) { + // Replaces accented characters with their non-accented version + input = input.normalize("NFD").replace(/[\u0300-\u036f]/g, ""); // Define the regex to match valid characters const validChar = /^[^ ()/{%*<>"]+$/; // Filter out invalid characters from the string From e33095ca23c344cb9629647262123cf685e64935 Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 1 Apr 2026 23:19:58 +0200 Subject: [PATCH 106/269] translated subject visualization improved. see #247 --- js/mzta-compose-script.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 46e344a4..849d514c 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -1228,10 +1228,7 @@ switch (message.command) { const translationTitleSpan = document.createElement('span'); translationTitleSpan.style.cssText = `font-weight: bold; font-size: 14px; color: ${tc.text}; flex-grow: 1;`; translationTitleSpan.textContent = browser.i18n.getMessage("translate_banner_title") || "AI Translation"; - if (translationData.lang) { - translationTitleSpan.textContent += ' (' + translationData.lang + ')'; - } - + const translationMenu = createThreeDotsMenu(colors.isDark, [ { icon: '\u21BB', @@ -1274,10 +1271,13 @@ switch (message.command) { translationText.textContent = browser.i18n.getMessage("translate_skipped"); } else { if (translationData.translated_subject) { - const subjectEl = document.createElement('div'); - subjectEl.style.cssText = 'font-weight: bold; margin-bottom: 4px;'; - subjectEl.textContent = translationData.translated_subject; - translationTextWrapper.appendChild(subjectEl); + // const subjectEl = document.createElement('div'); + // subjectEl.style.cssText = 'font-weight: bold; margin-bottom: 4px;'; + if (translationData.lang) { + translationTitleSpan.textContent = '[' + translationData.lang + '] '; + } + translationTitleSpan.textContent += translationData.translated_subject; + // translationTextWrapper.appendChild(subjectEl); } const bodyText = translationData.translated_text || ''; const bodyIsHtml = _isHtml(bodyText); From 78dfaae3f1a786a27b02eb6db4bd7f6cde6675d6 Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 1 Apr 2026 23:24:16 +0200 Subject: [PATCH 107/269] 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 8c45aef8..73d65735 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@
      • Antispam information are now permanently saved for each message [#675].
      • [All APIs] A summary has been added above the mail content [#580].
      • +
      • [All APIs] Inline auto translation for emails added [#247].
      • ...

      Version 4.0.3 - 20/03/2026

      diff --git a/options/mzta-release-notes.html b/options/mzta-release-notes.html index a27a7b28..7a4c2276 100644 --- a/options/mzta-release-notes.html +++ b/options/mzta-release-notes.html @@ -11,6 +11,7 @@
      • Antispam information are now permanently saved for each message [#675].
      • [All APIs] A summary has been added above the mail content [#580].
      • +
      • [All APIs] Inline auto translation for emails added [#247].
      • ...

      Version 4.0.3 - 20/03/2026

      From 6f74c32c2e8330c69d58acb7080d1047ee081921 Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 1 Apr 2026 23:24:28 +0200 Subject: [PATCH 108/269] version set to 4.1.0pre1 --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index 0ea13754..8bc70f4f 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "ThunderAI", "description": "__MSG_extensionDescription__", - "version": "4.1.0", + "version": "4.1.0pre1", "author": "Mic (m@micz.it)", "homepage_url": "https://micz.it/thunderbird-addon-thunderai/", "browser_specific_settings": { From 89de33ebdef56317959f9acb2382ae539bd5720e Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 1 Apr 2026 23:30:08 +0200 Subject: [PATCH 109/269] Update translation files Updated by "Cleanup translation files" add-on in Weblate. Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/ --- _locales/sv/messages.json | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index 039877e5..9e82a2c6 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -1499,27 +1499,12 @@ "prefs_OptionText_summarize_auto": { "message": "Sammanfatta meddelanden automatiskt" }, - "prefs_OptionText_summarize_auto_disabled": { - "message": "Inaktiverad" - }, - "prefs_OptionText_summarize_auto_manual": { - "message": "Visa sammanfattningsknapp" - }, - "prefs_OptionText_summarize_auto_automatic": { - "message": "När e-postmeddelandet öppnas" - }, "prefs_OptionText_summarize_auto_Info": { "message": "Välj om sammanfattningar ska genereras automatiskt när meddelanden visas. Kräver en API-baserad anslutning (inte ChatGPT Web)." }, "prefs_OptionText_summarize_display_mode": { "message": "Visa sammanfattning i" }, - "prefs_OptionText_summarize_display_mode_inline": { - "message": "Meddelandepanel (inbäddad)" - }, - "prefs_OptionText_summarize_display_mode_webchat": { - "message": "Chatt fönster" - }, "prefs_OptionText_summarize_display_mode_Info": { "message": "Välj var sammanfattningsresultatet visas. I infogat läge visas en sammanfattningsbanderoll direkt i meddelandefönstret. I chattfönsterläget öppnas AI-chattfönstret." }, From 28e61d7a402d792c936b99e80e9e26913ef5b36e Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 2 Apr 2026 00:04:00 +0200 Subject: [PATCH 110/269] correctly showing connection info. see #730 --- _locales/en/messages.json | 8 ++++++++ options/mzta-options.html | 4 +++- options/mzta-options.js | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 92e9bad0..58bab701 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -388,6 +388,14 @@ "message": "You can change the shortcut clicking on the cogwheel icon at the top right of this page and choosing \"Manage Extension Shortcut\".", "description": "" }, + "prefsInfoDesc_7": { + "message": "To use the Google Gemini API, you need a Google Gemini API Key and must choose a model.", + "description": "" + }, + "prefsInfoDesc_8": { + "message": "To use the Claude API, you need an Anthropic Claude API Key and must choose a model.", + "description": "" + }, "prefsDonation_1": { "message": "Do you like this addon?", "description": "" diff --git a/options/mzta-options.html b/options/mzta-options.html index 2d247247..139b9c8e 100644 --- a/options/mzta-options.html +++ b/options/mzta-options.html @@ -272,7 +272,9 @@ __MSG_prefsInfoDesc_1__
      __MSG_prefsInfoDesc_2__
      __MSG_prefsInfoDesc_3__ [__MSG_more_info_string__]
      - __MSG_prefsInfoDesc_4__ + __MSG_prefsInfoDesc_4__
      + __MSG_prefsInfoDesc_7__
      + __MSG_prefsInfoDesc_8__

      __MSG_prefsInfoDesc_5__
      __MSG_prefsInfoDesc_6__

      diff --git a/options/mzta-options.js b/options/mzta-options.js index eda0c48f..caafc539 100644 --- a/options/mzta-options.js +++ b/options/mzta-options.js @@ -239,6 +239,18 @@ async function disable_GetCalendarEvent(){ wrong_sparks_text.style.display = (is_spark_present == 0) ? 'inline' : 'none'; } +function updateDescription(){ + let conntype_select = document.getElementById("connection_type"); + let conntype = conntype_select.value; + let desc = document.getElementById("miczDescription"); + const types = ["chatgpt_web", "chatgpt_api", "ollama_api", "openai_comp_api", "google_gemini_api", "anthropic_api"]; + for(let t of types){ + desc.querySelectorAll(".conntype_" + t).forEach(el => { + el.style.display = (conntype === t) ? "" : "none"; + }); + } +} + function resetMaxPromptLength(){ let maxPromptLength = document.getElementById('max_prompt_length'); maxPromptLength.value = prefs_default.max_prompt_length; @@ -394,8 +406,10 @@ document.addEventListener('DOMContentLoaded', async () => { conntype_select.addEventListener("change", () => disable_Summarize(prefs_opt)); conntype_select.addEventListener("change", () => disable_Translate(prefs_opt)); conntype_select.addEventListener("change", disable_GetCalendarEvent); + conntype_select.addEventListener("change", updateDescription); showConnectionOptions(conntype_select); + updateDescription(); disable_MaxPromptLength(); disable_AddTags(prefs_opt); disable_SpamFilter(prefs_opt); From ca56c2d971366e51657a98dde21558eb8d883ca7 Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 2 Apr 2026 00:09:00 +0200 Subject: [PATCH 111/269] now the popup closes immediatly. see #677 --- _locales/en/messages.json | 4 ---- popup/mzta-popup.css | 4 ---- popup/mzta-popup.html | 1 - popup/mzta-popup.js | 9 ++------- 4 files changed, 2 insertions(+), 16 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 58bab701..73e3a74b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -752,10 +752,6 @@ "message": "Use the current model", "description": "" }, - "SendingPrompt": { - "message": "Sending prompt...", - "description": "" - }, "AllowedValues": { "message": "Allowed values", "description": "" diff --git a/popup/mzta-popup.css b/popup/mzta-popup.css index 2ff9569e..163ed194 100644 --- a/popup/mzta-popup.css +++ b/popup/mzta-popup.css @@ -47,10 +47,6 @@ body { display: none; } - #mzta_sending_prompt{ - display: none; - } - .mzta_autocomplete-item { padding: 4px; cursor: pointer; diff --git a/popup/mzta-popup.html b/popup/mzta-popup.html index 02caba36..ec72d3a4 100644 --- a/popup/mzta-popup.html +++ b/popup/mzta-popup.html @@ -19,7 +19,6 @@
       
      -
      diff --git a/popup/mzta-popup.js b/popup/mzta-popup.js index c50bc028..71322f28 100644 --- a/popup/mzta-popup.js +++ b/popup/mzta-popup.js @@ -362,13 +362,8 @@ document.body.insertBefore(banner, document.body.firstChild); async function sendPrompt(prompt_id, tabId){ taLog.log("sendPrompt: " + prompt_id); - document.getElementById('mzta_search_input').style.display = 'none'; - document.getElementById('mzta_sending_prompt').style.display = 'block'; - let response = await browser.runtime.sendMessage({command: "shortcut_do_prompt", tabId: tabId, promptId: prompt_id}); -// console.log(">>>>>>>>>>>>>>>>> response: " + JSON.stringify(response)); - if(response.ok == '1'){ - window.close(); - } + browser.runtime.sendMessage({command: "shortcut_do_prompt", tabId: tabId, promptId: prompt_id}); + window.close(); } function filterPromptsForTab(prompts_data, filtering){ From ce041fad64f9a8cb69e9aebe6d4544345aad71cf Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 2 Apr 2026 00:09:00 +0200 Subject: [PATCH 112/269] format: json added to Ollama. see #703 --- _locales/en/messages.json | 8 +++++++ claude-spec/04-api-integrations.md | 2 +- js/api/ollama.js | 6 ++++- options/mzta-options-default.js | 3 ++- pages/_lib/connection-ui.js | 35 ++++++++++++++++++++---------- 5 files changed, 39 insertions(+), 15 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 73e3a74b..23520466 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1790,6 +1790,14 @@ "message": "If checked, the Model will think before answering. This option works only with models that support the 'think' feature.", "description": "" }, + "prefs_ollama_format_json": { + "message": "Force JSON output", + "description": "" + }, + "prefs_ollama_format_json_Info": { + "message": "If checked, Ollama will be forced to return a valid JSON response. This option works only with models that support structured output.", + "description": "" + }, "chatgpt_win_change_reply_type": { "message": "Click to change the reply type", "description": "" diff --git a/claude-spec/04-api-integrations.md b/claude-spec/04-api-integrations.md index 318eadbf..d4ef33e6 100644 --- a/claude-spec/04-api-integrations.md +++ b/claude-spec/04-api-integrations.md @@ -32,7 +32,7 @@ Content script `js/lib/diff.js` is injected into ChatGPT pages for diff-view sup ### Ollama (`ollama_api`) - Module: `js/api/ollama.js` - Worker: `js/workers/model-worker-ollama.js` -- Settings keys: `ollama_host`, `ollama_model`, `ollama_num_ctx`, `ollama_temperature`, `ollama_think` +- Settings keys: `ollama_host`, `ollama_model`, `ollama_num_ctx`, `ollama_temperature`, `ollama_think`, `ollama_format_json` - Requires CORS to be configured on the Ollama server ### OpenAI-Compatible (`openai_comp_api`) diff --git a/js/api/ollama.js b/js/api/ollama.js index 07d5a066..32433dda 100644 --- a/js/api/ollama.js +++ b/js/api/ollama.js @@ -24,7 +24,8 @@ export class Ollama { num_ctx = 0; temperature = ''; think = false; - + format_json = false; + constructor({ host = '', model = '', @@ -32,6 +33,7 @@ export class Ollama { num_ctx = 0, temperature = '', think = false, + format_json = false, } = {}) { this.host = (host || '').trim().replace(/\/+$/, ""); this.model = model; @@ -39,6 +41,7 @@ export class Ollama { this.num_ctx = num_ctx; this.temperature = temperature; this.think = think; + this.format_json = format_json; } fetchModels = async () => { @@ -93,6 +96,7 @@ export class Ollama { messages: messages, stream: this.stream, think: this.think, + ...(this.format_json ? { format: "json" } : {}), ...(this.num_ctx > 0 ? { options: { num_ctx: parseInt(this.num_ctx) } } : {}), ...(this.temperature != '' && !Number.isNaN(tempFloat) ? { options: { temperature: tempFloat } } : {}), }), diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index 4ac38c96..8776c8b6 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -31,7 +31,8 @@ export const integration_options_config = { model: '', num_ctx: 0, temperature: '', - think: false + think: false, + format_json: false }, openai_comp: { host: '', diff --git a/pages/_lib/connection-ui.js b/pages/_lib/connection-ui.js index 64a0d0eb..5ff56863 100644 --- a/pages/_lib/connection-ui.js +++ b/pages/_lib/connection-ui.js @@ -358,6 +358,17 @@ export async function injectConnectionUI({ + + + + + +
      +
      + __MSG_SpamFilter_skip_addresses_title__ +
      __MSG_SpamFilter_skip_addresses_infoline__ +
      __MSG_SpamFilter_skip_addresses_infoline2__
      +
      +
      +
      __MSG_SpamReport_Title__
      diff --git a/pages/spamfilter/mzta-spamfilter.js b/pages/spamfilter/mzta-spamfilter.js index fccea0cf..382690e6 100644 --- a/pages/spamfilter/mzta-spamfilter.js +++ b/pages/spamfilter/mzta-spamfilter.js @@ -34,6 +34,7 @@ import { taSpamReport } from '../../js/mzta-spamreport.js'; import { getAccountsList, isAPIKeyValue, + normalizeStringList, setTomSelectBorder } from "../../js/mzta-utils.js"; import { @@ -130,6 +131,33 @@ document.addEventListener('DOMContentLoaded', async () => { autocompleteSuggestions = (await getPlaceholders(true)).filter(p => !(p.id === 'additional_text')).map(mapPlaceholderToSuggestion); textareaAutocomplete(spamfilter_textarea, autocompleteSuggestions, 1); // type_value = 1, only when reading an email + // Skip addresses list + let skip_addresses_textarea = document.getElementById('spamfilter_skip_addresses'); + let skip_addresses_save_btn = document.getElementById('btn_save_skip_addresses'); + + let skip_addresses_value = await spamfilter_getSkipAddresses(); + let skip_addresses_string = skip_addresses_value.join('\n'); + + skip_addresses_textarea.value = skip_addresses_string; + + skip_addresses_textarea.addEventListener('input', (event) => { + skip_addresses_save_btn.disabled = (event.target.value === skip_addresses_string); + if(skip_addresses_save_btn.disabled){ + document.getElementById('skip_addresses_unsaved').classList.add('hidden'); + } else { + document.getElementById('skip_addresses_unsaved').classList.remove('hidden'); + } + }); + + skip_addresses_save_btn.addEventListener('click', () => { + let skip_array_new = normalizeStringList(skip_addresses_textarea.value, 2); + spamfilter_setSkipAddresses(skip_array_new); + skip_addresses_save_btn.disabled = true; + skip_addresses_string = skip_array_new.join('\n'); + skip_addresses_textarea.value = skip_addresses_string; + document.getElementById('skip_addresses_unsaved').classList.add('hidden'); + }); + //Accounts manager let accounts = await getAccountsList(); const accountsContainer = document.getElementById('account_selector_checkboxes'); @@ -374,3 +402,12 @@ async function restoreOptions() { setCurrentChoice(getting); } + +async function spamfilter_getSkipAddresses() { + let prefs = await browser.storage.sync.get({spamfilter_skip_addresses: []}); + return prefs.spamfilter_skip_addresses; +} + +function spamfilter_setSkipAddresses(spamfilter_skip_addresses) { + browser.storage.sync.set({spamfilter_skip_addresses: spamfilter_skip_addresses}); +} From 41697280caf95f1228e061775309d77491c9fd1d Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 7 Apr 2026 23:05:32 +0200 Subject: [PATCH 132/269] 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 5b86de51..28068179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@
    • [Ollama API] Added format: json option [#703].
    • Fix: The "Important Information" section in the options page now updates correctly when choosing an integration [#730].
    • In the options page now is visible if a special prompt is using a specific API integration [#676].
    • +
    • Added an antispam skip list to ensure messages from designated addresses are not forwarded to the AI [#743].
    • ...
    • Version 4.0.3 - 20/03/2026

      diff --git a/options/mzta-release-notes.html b/options/mzta-release-notes.html index 9887e3fd..1a5f314f 100644 --- a/options/mzta-release-notes.html +++ b/options/mzta-release-notes.html @@ -16,6 +16,7 @@
    • [Ollama API] Added format: json option [#703].
    • Fix: The "Important Information" section in the options page now updates correctly when choosing an integration [#730].
    • In the options page now is visible if a special prompt is using a specific API integration [#676].
    • +
    • Added an antispam skip list to ensure messages from designated addresses are not forwarded to the AI [#743].
    • ...
    • Version 4.0.3 - 20/03/2026

      From c3d0610d9408727ececa93287e215ac3afc80c58 Mon Sep 17 00:00:00 2001 From: mic Date: Tue, 7 Apr 2026 23:18:02 +0200 Subject: [PATCH 133/269] spam filter prompt updated --- _locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 72ea3f47..09cb2ef6 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1381,7 +1381,7 @@ "description" : "" }, "prompt_spamfilter_full_text" : { - "message" : "Analyze the following email and determine if it is spam or not. Consider factors such as suspicious keywords, excessive promotional language, misleading subject lines, requests for personal information, and unusual sender addresses.\nProvide a value from 0 (not spam) to 100 (spam) and an explanation of no more than 10 words.\nIn case of missing message data, set the value to 0 (not spam), and give the reason.\nGenerate a response in JSON format only. Do not include any additional text or explanation; provide only the JSON. Here is the format to be used:\n{\n\"spamValue\": ,\n\"explanation\": \"Brief explanation of your reasoning\"\n}\nHere there are the mail information:\nSender: \"{%author%}\"\nSubject: \"{%mail_subject%}\"\nHtml body: \"{%mail_html_body%}\"", + "message" : "Analyze the following email and determine if it is spam or not. Consider factors such as suspicious keywords, excessive promotional language, misleading subject lines, requests for personal information, and unusual sender addresses.\nProvide a value from 0 (not spam) to 100 (spam) and an explanation of no more than 10 words.\nIn case of missing message data, set the value to 0 (not spam), and give the reason.\nGenerate a response in JSON format only. Do not include any additional text or explanation; provide only the JSON. Here is the format to be used:\n{\n\"explanation\": \"Brief explanation of your reasoning\",\n\"spamValue\": \n}\nHere there are the mail information:\nSender: \"{%author%}\"\nSubject: \"{%mail_subject%}\"\nHtml body: \"{%mail_html_body%}\"", "description" : "" }, "SpamFilter_prompt_prefs_title": { From 79f34b6ed8db454bcdca185e257bdb92ca6179d2 Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 7 Apr 2026 23:19:15 +0200 Subject: [PATCH 134/269] Translated using Weblate (Italian) Currently translated at 84.6% (479 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/it/ --- _locales/it/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 6f31f4d1..451c86dc 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -823,7 +823,7 @@ "message": "Valore soglia per lo spam" }, "prompt_spamfilter_full_text": { - "message": "Analizza la seguente email e determina se si tratta di spam o meno. Considera fattori come parole chiave sospette, linguaggio promozionale eccessivo, linee oggetto fuorvianti, richieste di informazioni personali e indirizzi del mittente insoliti.\nFornisci un valore da 0 (non spam) a 100 (spam) e una spiegazione di non più di 10 parole.\nIn caso di dati del messaggio mancanti, imposta il valore a 0 (non spam) e fornisci la motivazione.\nGenera una risposta esclusivamente in formato JSON. Non includere alcun testo o spiegazione aggiuntiva; fornisci solo il JSON. Ecco il formato da utilizzare:\n{\n\"spamValue\": ,\n\"explanation\": \"Breve spiegazione del tuo ragionamento\"\n}\nQui ci sono le informazioni sull'email:\nMittente: \"{%author%}\"\nOggetto: \"{%mail_subject%}\"\nCorpo HTML: \"{%mail_html_body%}\"" + "message": "Analizza la seguente email e determina se si tratta di spam o meno. Considera fattori come parole chiave sospette, linguaggio promozionale eccessivo, linee oggetto fuorvianti, richieste di informazioni personali e indirizzi del mittente insoliti.\nFornisci un valore da 0 (non spam) a 100 (spam) e una spiegazione di non più di 10 parole.\nIn caso di dati del messaggio mancanti, imposta il valore a 0 (non spam) e fornisci la motivazione.\nGenera una risposta esclusivamente in formato JSON. Non includere alcun testo o spiegazione aggiuntiva; fornisci solo il JSON. Ecco il formato da utilizzare:\n{\n\"explanation\": \"Breve spiegazione del tuo ragionamento\",\n\"spamValue\": \n}\nQui ci sono le informazioni sull'email:\nMittente: \"{%author%}\"\nOggetto: \"{%mail_subject%}\"\nCorpo HTML: \"{%mail_html_body%}\"" }, "prefs_OptionText_add_tags_auto_force_existing_Info": { "message": "Se selezionato, l'IA aggiungerà solo i tag esistenti e non creerà nuovi tag." From bfe206295f58d1ba425410409a79255e4db7d0b9 Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 7 Apr 2026 23:21:56 +0200 Subject: [PATCH 135/269] Translated using Weblate (Spanish) Currently translated at 74.7% (423 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/es/ --- _locales/es/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/es/messages.json b/_locales/es/messages.json index e0ef581e..32bf175d 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -903,7 +903,7 @@ "message": "Detectar correos spam" }, "prompt_spamfilter_full_text": { - "message": "Analiza el siguiente correo electrónico y determina si es spam o no. Considera factores como palabras clave sospechosas, lenguaje promocional excesivo, líneas de asunto engañosas, solicitudes de información personal y direcciones de remitente inusuales. \nProporciona un valor de 0 (no es spam) a 100 (spam) y una explicación de no más de 10 palabras. \nEn caso de que falten datos del mensaje, establece el valor en 0 (no es spam) y da la razón. \nGenera una respuesta únicamente en formato JSON. No incluyas texto adicional ni explicación; proporciona solo el JSON. El formato a usar es: \n{\n\"spamValue\": ,\n\"explanation\": \"Breve explicación de tu razonamiento\"\n} \nAquí está la información del correo: \nRemitente: \"{%author%}\" \nAsunto: \"{%mail_subject%}\" \nCuerpo HTML: \"{%mail_html_body%}\"" + "message": "Analiza el siguiente correo electrónico y determina si es spam o no. Considera factores como palabras clave sospechosas, lenguaje promocional excesivo, líneas de asunto engañosas, solicitudes de información personal y direcciones de remitente inusuales. \nProporciona un valor de 0 (no es spam) a 100 (spam) y una explicación de no más de 10 palabras. \nEn caso de que falten datos del mensaje, establece el valor en 0 (no es spam) y da la razón. \nGenera una respuesta únicamente en formato JSON. No incluyas texto adicional ni explicación; proporciona solo el JSON. El formato a usar es: \n{\n\"explanation\": \"Breve explicación de tu razonamiento\",\n\"spamValue\": \n} \nAquí está la información del correo: \nRemitente: \"{%author%}\" \nAsunto: \"{%mail_subject%}\" \nCuerpo HTML: \"{%mail_html_body%}\"" }, "SpamFilter_prompt_prefs_title": { "message": "Opciones del filtro de spam" From 796a599cc6d0c37972234a857c04e5b309a3a5b6 Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 7 Apr 2026 23:20:48 +0200 Subject: [PATCH 136/269] Translated using Weblate (Japanese) Currently translated at 74.5% (422 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/ja/ --- _locales/ja/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 4557e3f0..34177c75 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -906,7 +906,7 @@ "message": "スパムメールを検出" }, "prompt_spamfilter_full_text": { - "message": "以下のメールを分析し、スパムかどうかを判断してください。不審なキーワード、過度な宣伝文句、誤解を招く件名、個人情報の要求、不審な送信者アドレスなどの要素を考慮してください。\n0(スパムではない)から100(スパム)までの値と、10語以内の説明を提供してください。\nメッセージデータが欠落している場合は、値を0(スパムではない)に設定し、理由を示してください。\nJSON形式のみで応答を生成してください。追加のテキストや説明は含めず、JSONのみを提供してください。使用する形式:\n{\n\"spamValue\": <0から100の整数>,\n\"explanation\": \"判断理由の簡単な説明\"\n}\n以下がメール情報です:\n送信者:「{%author%}」\n件名:「{%mail_subject%}」\nHTML本文:「{%mail_html_body%}」" + "message": "以下のメールを分析し、スパムかどうかを判断してください。不審なキーワード、過度な宣伝文句、誤解を招く件名、個人情報の要求、不審な送信者アドレスなどの要素を考慮してください。\n0(スパムではない)から100(スパム)までの値と、10語以内の説明を提供してください。\nメッセージデータが欠落している場合は、値を0(スパムではない)に設定し、理由を示してください。\nJSON形式のみで応答を生成してください。追加のテキストや説明は含めず、JSONのみを提供してください。使用する形式:\n{\n\"explanation\": \"判断理由の簡単な説明\",\n\"spamValue\": <0から100の整数>\n}\n以下がメール情報です:\n送信者:「{%author%}」\n件名:「{%mail_subject%}」\nHTML本文:「{%mail_html_body%}」" }, "SpamFilter_prompt_prefs_title": { "message": "スパムフィルターオプション" From 7855250d8463e49f931d86a5b640972bd2cc08df Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 7 Apr 2026 23:20:28 +0200 Subject: [PATCH 137/269] Translated using Weblate (Croatian) Currently translated at 47.8% (271 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/hr/ --- _locales/hr/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/hr/messages.json b/_locales/hr/messages.json index e3fd83c7..bfaa4747 100644 --- a/_locales/hr/messages.json +++ b/_locales/hr/messages.json @@ -781,7 +781,7 @@ "message": "Prepoznaj neželjenu poštu" }, "prompt_spamfilter_full_text": { - "message": "Analiziraj sljedeću e-poruku i utvrdi je li neželjena ili ne. Razmotri čimbenike kao što su sumnjive ključne riječi, pretjerani promotivni jezik, zavaravajuće linije predmeta, zahtjevi za osobnim podacima i neobične adrese pošiljatelja.\nNavedi vrijednost od 0 (nije spam) do 100 (neželjena pošta) i objašnjenje od najviše 10 riječi.\nU slučaju nedostatka podataka poruke, postavite vrijednost na 0 (nije spam) i navedite razlog.\nGeneriraj odgovor samo u JSON formatu. Nemoj uključivati nikakav dodatni tekst ili objašnjenje; pruži samo JSON. Ovdje je format koji treba koristiti:\n{\n\"spamValue\": ,\n\"explanation\": \"Kratko objašnjenje vašeg obrazloženja\"\n}\nOvdje su informacije o e-poruci:\nŠalje: \"{%author%}\"\nNaslov: \"{%mail_subject%}\"\nHtml tijelo: \"{%mail_html_body%}\"" + "message": "Analiziraj sljedeću e-poruku i utvrdi je li neželjena ili ne. Razmotri čimbenike kao što su sumnjive ključne riječi, pretjerani promotivni jezik, zavaravajuće linije predmeta, zahtjevi za osobnim podacima i neobične adrese pošiljatelja.\nNavedi vrijednost od 0 (nije spam) do 100 (neželjena pošta) i objašnjenje od najviše 10 riječi.\nU slučaju nedostatka podataka poruke, postavite vrijednost na 0 (nije spam) i navedite razlog.\nGeneriraj odgovor samo u JSON formatu. Nemoj uključivati nikakav dodatni tekst ili objašnjenje; pruži samo JSON. Ovdje je format koji treba koristiti:\n{\n\"explanation\": \"Kratko objašnjenje vašeg obrazloženja\",\n\"spamValue\": \n}\nOvdje su informacije o e-poruci:\nŠalje: \"{%author%}\"\nNaslov: \"{%mail_subject%}\"\nHtml tijelo: \"{%mail_html_body%}\"" }, "SpamFilter_prompt_prefs_title": { "message": "Mogućnosti filtera neželjene pošte" From f0ca5807a2ea402f1461c1d70aca132b190f0ef8 Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 7 Apr 2026 23:20:59 +0200 Subject: [PATCH 138/269] Translated using Weblate (Greek) Currently translated at 81.2% (460 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/el/ --- _locales/el/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/el/messages.json b/_locales/el/messages.json index 9a762374..c1f7d025 100644 --- a/_locales/el/messages.json +++ b/_locales/el/messages.json @@ -720,7 +720,7 @@ "message": "Εντοπισμός ανεπιθύμητων μηνυμάτων ηλεκτρονικού ταχυδρομείου" }, "prompt_spamfilter_full_text": { - "message": "Αναλύστε το ακόλουθο email και προσδιορίστε εάν είναι spam ή όχι. Λάβετε υπόψη παράγοντες όπως ύποπτες λέξεις-κλειδιά, υπερβολική διαφημιστική γλώσσα, παραπλανητικές γραμμές θέματος, αιτήματα για προσωπικά στοιχεία και ασυνήθιστες διευθύνσεις αποστολέα.\nΔώστε μια τιμή από 0 (όχι spam) έως 100 (spam) και μια εξήγηση που δεν υπερβαίνει τις 10 λέξεις.\nΣε περίπτωση που λείπουν δεδομένα μηνύματος, ορίστε την τιμή σε 0 (όχι spam) και δώστε τον λόγο.\nΔημιουργήστε μια απάντηση μόνο σε μορφή JSON. Μην συμπεριλάβετε κανένα επιπλέον κείμενο ή εξήγηση. Δώστε μόνο το JSON. Ακολουθεί η μορφή που θα χρησιμοποιηθεί:\n{\n\"spamValue\": <ακέραιος αριθμός από 0 έως 100>,\n\"explanation\": \"Σύντομη εξήγηση του συλλογισμού σας\"\n}\nΕδώ βρίσκονται οι πληροφορίες του email:\nΑποστολέας: \"{%author%}\"\nΘέμα: \"{%mail_subject%}\"\nΣώμα Html: \"{%mail_html_body%}\"" + "message": "Αναλύστε το ακόλουθο email και προσδιορίστε εάν είναι spam ή όχι. Λάβετε υπόψη παράγοντες όπως ύποπτες λέξεις-κλειδιά, υπερβολική διαφημιστική γλώσσα, παραπλανητικές γραμμές θέματος, αιτήματα για προσωπικά στοιχεία και ασυνήθιστες διευθύνσεις αποστολέα.\nΔώστε μια τιμή από 0 (όχι spam) έως 100 (spam) και μια εξήγηση που δεν υπερβαίνει τις 10 λέξεις.\nΣε περίπτωση που λείπουν δεδομένα μηνύματος, ορίστε την τιμή σε 0 (όχι spam) και δώστε τον λόγο.\nΔημιουργήστε μια απάντηση μόνο σε μορφή JSON. Μην συμπεριλάβετε κανένα επιπλέον κείμενο ή εξήγηση. Δώστε μόνο το JSON. Ακολουθεί η μορφή που θα χρησιμοποιηθεί:\n{\n\"explanation\": \"Σύντομη εξήγηση του συλλογισμού σας\",\n\"spamValue\": <ακέραιος αριθμός από 0 έως 100>\n}\nΕδώ βρίσκονται οι πληροφορίες του email:\nΑποστολέας: \"{%author%}\"\nΘέμα: \"{%mail_subject%}\"\nΣώμα Html: \"{%mail_html_body%}\"" }, "SpamFilter_prompt_prefs_title": { "message": "Επιλογές φίλτρου ανεπιθύμητης αλληλογραφίας" From d91e5b5499a480009143f8e18d4d5b27c2189c38 Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 7 Apr 2026 23:20:06 +0200 Subject: [PATCH 139/269] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 53.1% (301 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/zh_Hans/ --- _locales/zh_Hans/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/zh_Hans/messages.json b/_locales/zh_Hans/messages.json index 97f2e704..e6c3afbc 100644 --- a/_locales/zh_Hans/messages.json +++ b/_locales/zh_Hans/messages.json @@ -807,7 +807,7 @@ "message": "获取日历事件数据时出错" }, "prompt_spamfilter_full_text": { - "message": "分析以下邮件并判断它是否为垃圾邮件。考虑因素包括可疑关键词、过多的宣传语言、误导性的主题行、索取个人信息的请求以及异常的发件人地址。\n提供一个从 0(非垃圾邮件)到 100(垃圾邮件)的值,并附上不超过 10 个单词的解释。\n如果消息数据缺失,将数值设为0(非垃圾邮件),并说明原因。\n仅以 JSON 格式生成响应。不要包含任何额外文本或解释;仅提供 JSON。以下是使用的格式:\n{\n\"spamValue\": <0 到 100 的整数>,\n\"explanation\": \"简要说明您的判断理由\"\n}\n以下是邮件信息:\n发件人:“{%author%}”\n主题:“{%mail_subject%}”\nHTML 正文:“{%mail_html_body%}”" + "message": "分析以下邮件并判断它是否为垃圾邮件。考虑因素包括可疑关键词、过多的宣传语言、误导性的主题行、索取个人信息的请求以及异常的发件人地址。\n提供一个从 0(非垃圾邮件)到 100(垃圾邮件)的值,并附上不超过 10 个单词的解释。\n如果消息数据缺失,将数值设为0(非垃圾邮件),并说明原因。\n仅以 JSON 格式生成响应。不要包含任何额外文本或解释;仅提供 JSON。以下是使用的格式:\n{\n\"explanation\": \"简要说明您的判断理由\",\n\"spamValue\": <0 到 100 的整数>\n}\n以下是邮件信息:\n发件人:“{%author%}”\n主题:“{%mail_subject%}”\nHTML 正文:“{%mail_html_body%}”" }, "prefs_OptionText_spamfilter_threshold_Info": { "message": "如果 AI 返回的值高于此阈值,电子邮件将被移至垃圾邮件文件夹。" From 8740438c2d885c31b91bc4a6323087667316203d Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 7 Apr 2026 23:21:23 +0200 Subject: [PATCH 140/269] Translated using Weblate (Polish) Currently translated at 49.8% (282 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/pl/ --- _locales/pl/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index 491ddfdb..e300eb87 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -826,7 +826,7 @@ "message": "Zarządzaj ustawieniami filtra spamu" }, "prompt_spamfilter_full_text": { - "message": "Przeanalizuj poniższy e-mail i określ, czy jest to spam, czy nie. Weź pod uwagę takie czynniki jak podejrzane słowa kluczowe, nadmierny język promocyjny, wprowadzające w błąd tematy, prośby o podanie danych osobowych i nietypowe adresy nadawców.\nPodaj wartość od 0 (nie spam) do 100 (spam) oraz wyjaśnienie nie dłuższe niż 10 słów.\nW przypadku braku danych wiadomości ustaw wartość na 0 (nie spam) i podaj powód.\nWygeneruj odpowiedź tylko w formacie JSON. Nie dodawaj żadnego dodatkowego tekstu ani wyjaśnień; podaj tylko JSON. Oto format, który należy użyć:\n{\n\"spamValue\": ,\n\"explanation\": \"Krótkie wyjaśnienie twojego rozumowania\"\n}\nOto informacje o e-mailu:\nNadawca: \"{%author%}\"\nTemat: \"{%mail_subject%}\"\nTreść HTML: \"{%mail_html_body%}\"" + "message": "Przeanalizuj poniższy e-mail i określ, czy jest to spam, czy nie. Weź pod uwagę takie czynniki jak podejrzane słowa kluczowe, nadmierny język promocyjny, wprowadzające w błąd tematy, prośby o podanie danych osobowych i nietypowe adresy nadawców.\nPodaj wartość od 0 (nie spam) do 100 (spam) oraz wyjaśnienie nie dłuższe niż 10 słów.\nW przypadku braku danych wiadomości ustaw wartość na 0 (nie spam) i podaj powód.\nWygeneruj odpowiedź tylko w formacie JSON. Nie dodawaj żadnego dodatkowego tekstu ani wyjaśnień; podaj tylko JSON. Oto format, który należy użyć:\n{\n\"explanation\": \"Krótkie wyjaśnienie twojego rozumowania\",\n\"spamValue\": \n}\nOto informacje o e-mailu:\nNadawca: \"{%author%}\"\nTemat: \"{%mail_subject%}\"\nTreść HTML: \"{%mail_html_body%}\"" }, "prefs_OptionText_spamfilter_threshold_Info": { "message": "Jeśli wartość zwrócona przez AI przekroczy ten próg, e-mail zostanie przeniesiony do folderu spamu." From 09aa9b49420474e5b4c644f44f2d9dfc1065fc0b Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 7 Apr 2026 23:22:06 +0200 Subject: [PATCH 141/269] Translated using Weblate (German) Currently translated at 84.6% (479 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/de/ --- _locales/de/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 536f1801..85c64b44 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -829,7 +829,7 @@ "message": "Wenn ausgewählt, wird ThunderAI Spam-E-Mails automatisch in den Spam-Ordner verschieben." }, "prompt_spamfilter_full_text": { - "message": "Analysieren Sie die folgende E-Mail und bestimmen Sie, ob es sich um Spam handelt oder nicht. Berücksichtigen Sie Faktoren wie verdächtige Schlüsselwörter, übermäßige Werbesprache, irreführende Betreffzeilen, Anfragen nach persönlichen Informationen und ungewöhnliche Absenderadressen.\nGeben Sie einen Wert von 0 (kein Spam) bis 100 (Spam) und eine Erklärung mit maximal 10 Wörtern an.\nFalls Nachrichtendaten fehlen, setzen Sie den Wert auf 0 (kein Spam) und geben Sie den Grund an.\nGenerieren Sie die Antwort ausschließlich im JSON-Format. Fügen Sie keinen zusätzlichen Text oder Erklärungen hinzu; liefern Sie nur das JSON. Hier ist das zu verwendende Format:\n{\n\"spamValue\": ,\n\"explanation\": \"Kurze Erklärung Ihrer Begründung\"\n}\nHier sind die Mail-Informationen:\nAbsender: \"{%author%}\"\nBetreff: \"{%mail_subject%}\"\nHTML-Inhalt: \"{%mail_html_body%}\"" + "message": "Analysieren Sie die folgende E-Mail und bestimmen Sie, ob es sich um Spam handelt oder nicht. Berücksichtigen Sie Faktoren wie verdächtige Schlüsselwörter, übermäßige Werbesprache, irreführende Betreffzeilen, Anfragen nach persönlichen Informationen und ungewöhnliche Absenderadressen.\nGeben Sie einen Wert von 0 (kein Spam) bis 100 (Spam) und eine Erklärung mit maximal 10 Wörtern an.\nFalls Nachrichtendaten fehlen, setzen Sie den Wert auf 0 (kein Spam) und geben Sie den Grund an.\nGenerieren Sie die Antwort ausschließlich im JSON-Format. Fügen Sie keinen zusätzlichen Text oder Erklärungen hinzu; liefern Sie nur das JSON. Hier ist das zu verwendende Format:\n{\n\"explanation\": \"Kurze Erklärung Ihrer Begründung\",\n\"spamValue\": \n}\nHier sind die Mail-Informationen:\nAbsender: \"{%author%}\"\nBetreff: \"{%mail_subject%}\"\nHTML-Inhalt: \"{%mail_html_body%}\"" }, "prefs_OptionText_openai_comp_info_remote": { "message": "Hier können Sie auch die Adresse eines entfernten Servers eingeben." From 6cbd97f7171103b23a090c11d47cb81c21590b6f Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 7 Apr 2026 23:20:15 +0200 Subject: [PATCH 142/269] Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 69.4% (393 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/zh_Hant/ --- _locales/zh_Hant/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/zh_Hant/messages.json b/_locales/zh_Hant/messages.json index 1778bf49..dda1ea7a 100644 --- a/_locales/zh_Hant/messages.json +++ b/_locales/zh_Hant/messages.json @@ -1093,7 +1093,7 @@ "message": "ChatGPT 網頁介面可能會發生一些變化,導致附加元件無法正常運作。請查看此頁面底部連結的「服務狀態」頁面。另外,請記住,首次使用 ThunderAI 時,您需要登入 ChatGPT。" }, "prompt_spamfilter_full_text": { - "message": "分析以下 Email 並判斷是否為垃圾郵件。 考慮因素包括可疑關鍵字、過度推銷性語言、誤導性主旨、要求個人資訊以及異常的寄件人地址。\n提供一個 0 (非垃圾郵件) 到 100 (垃圾郵件) 的分數,並提供一段不超過 10 字的說明。\n如果缺少訊息資料,則設定分數為 0 (非垃圾郵件),並說明原因。\n請以 JSON 格式回覆,不包含任何額外的文字或說明,提供僅 JSON。以下是將要使用的格式:\n{\n\"spamValue\": <由 0 到 100 的整數>,\n\"explanation\": \"簡短說明您的理由\"\n}\n以下是郵件資訊:\n寄件人:「{%author%}」\n主旨:「{%mail_subject%}」\nHTML 內容:「{%mail_html_body%}」" + "message": "分析以下 Email 並判斷是否為垃圾郵件。 考慮因素包括可疑關鍵字、過度推銷性語言、誤導性主旨、要求個人資訊以及異常的寄件人地址。\n提供一個 0 (非垃圾郵件) 到 100 (垃圾郵件) 的分數,並提供一段不超過 10 字的說明。\n如果缺少訊息資料,則設定分數為 0 (非垃圾郵件),並說明原因。\n請以 JSON 格式回覆,不包含任何額外的文字或說明,提供僅 JSON。以下是將要使用的格式:\n{\n\"explanation\": \"簡短說明您的理由\",\n\"spamValue\": <由 0 到 100 的整數>\n}\n以下是郵件資訊:\n寄件人:「{%author%}」\n主旨:「{%mail_subject%}」\nHTML 內容:「{%mail_html_body%}」" }, "task_getting_data_error": { "message": "取得取得任務資料時出錯" From 869054c6f74d52e0c803e1d59574911378ac45bd Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 7 Apr 2026 23:18:45 +0200 Subject: [PATCH 143/269] Translated using Weblate (Swedish) Currently translated at 99.2% (562 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/sv/ --- _locales/sv/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index e721754f..3646c7b4 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -1282,7 +1282,7 @@ "message": "Extrahera alla relevanta detaljer som krävs för att generera en uppgift från följande text. Den extraherade informationen bör innehålla:\n- Förfallodatum och tid (inklusive tidszon, om angiven)\n- Uppgiftssammanfattning\n- Initialt datum och tid (inklusive tidszon, om angiven)\nSe till att informationen är tydligt och konsekvent formaterad så att den kan användas direkt för att skapa en uppgift.\nOm det finns relativa tidsreferenser, tänk på att datum och tid för e-postmeddelandet är \"{%mail_datetime%}\". Beräkna startdatum och tid baserat på denna referens. Om det beräknade startdatumet och tiden är tidigare än \"{%current_datetime%}\", beräkna om startdatumet och tiden med \"{%current_datetime%}\" som bas.\nOm du inte kan få en eller flera av de obligatoriska uppgifterna, vänligen svara med en tom sträng.\nGenerera ett svar endast i JSON-format. Inkludera inte ytterligare text eller förklaringar; ange endast JSON. Här är formatet som ska användas:\n{\n\"InitialDate\": \"ÅÅÅÅMMDDTHHMMSS\",\n\"dueDate\": \"ÅÅÅÅMMDDTHHMMSS\",\n\"summary\": \"Uppgiftssammanfattning här\"\n}\nOm det inte finns någon information om datumen, ta bort dem.\nHär är texten: \"{%selected_text%}\"" }, "prompt_spamfilter_full_text": { - "message": "Analysera följande e-postmeddelande och avgör om det är skräppost eller inte. Tänk på faktorer som misstänkta sökord, överdrivet reklamspråk, vilseledande ämnesrader, förfrågningar om personlig information och ovanliga avsändaradresser.\nAnge ett värde från 0 (inte skräppost) till 100 (skräppost) och en förklaring på högst 10 ord.\nOm meddelandedata saknas, sätt värdet till 0 (inte skräppost) och ange orsaken.\nGenerera ett svar endast i JSON-format. Inkludera inte ytterligare text eller förklaring; ange endast JSON. Här är formatet som ska användas:\n{\n\"spamValue\": ,\n\"explanation\": \"Kort förklaring av ditt resonemang\"\n}\nHär är e-postinformationen:\nAvsändare: \"{%author%}\"\nÄmne: \"{%mail_subject%}\"\nHtml-text: \"{%mail_html_body%}\"" + "message": "Analysera följande e-postmeddelande och avgör om det är skräppost eller inte. Tänk på faktorer som misstänkta sökord, överdrivet reklamspråk, vilseledande ämnesrader, förfrågningar om personlig information och ovanliga avsändaradresser.\nAnge ett värde från 0 (inte skräppost) till 100 (skräppost) och en förklaring på högst 10 ord.\nOm meddelandedata saknas, sätt värdet till 0 (inte skräppost) och ange orsaken.\nGenerera ett svar endast i JSON-format. Inkludera inte ytterligare text eller förklaring; ange endast JSON. Här är formatet som ska användas:\n{\n\"explanation\": \"Kort förklaring av ditt resonemang\",\n\"spamValue\": \n}\nHär är e-postinformationen:\nAvsändare: \"{%author%}\"\nÄmne: \"{%mail_subject%}\"\nHtml-text: \"{%mail_html_body%}\"" }, "Summarize_prompt_prefs_title": { "message": "Sammanfattningsalternativ" From 79e63a6bd544961cfbb823cede5230c395800ebe Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 7 Apr 2026 23:21:45 +0200 Subject: [PATCH 144/269] Translated using Weblate (Russian) Currently translated at 68.5% (388 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/ru/ --- _locales/ru/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 4cce1af6..9084297c 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -873,7 +873,7 @@ "message": "Обнаружение спама в эл. почте" }, "prompt_spamfilter_full_text": { - "message": "Проанализируйте следующее письмо и определите, является ли оно спамом или нет. Учитывайте такие факторы, как подозрительные ключевые слова, излишняя рекламная лексика, вводящие в заблуждение тематические строки, запросы личной информации и необычные адреса отправителей.\nУкажите значение от 0 (не спам) до 100 (спам) и объяснение, состоящее не более чем из 10 слов.\nВ случае отсутствия данных о сообщении установите значение 0 (не спам) и укажите причину.\nГенерируйте ответ только в формате JSON. Не включайте никаких доп. текстов или объяснений; предоставляйте только JSON. Вот формат, который следует использовать:\n{\n\"spamValue\": <целое число от 0 до 100>,\n\"explanation\": \"Краткое объяснение ваших рассуждений\"\n}\nЗдесь находится информация о почте:\nОтправитель: \"{%author%}\"\nТема: \"{%mail_subject%}\"\nHtml-тело: \"{%mail_html_body%}\"" + "message": "Проанализируйте следующее письмо и определите, является ли оно спамом или нет. Учитывайте такие факторы, как подозрительные ключевые слова, излишняя рекламная лексика, вводящие в заблуждение тематические строки, запросы личной информации и необычные адреса отправителей.\nУкажите значение от 0 (не спам) до 100 (спам) и объяснение, состоящее не более чем из 10 слов.\nВ случае отсутствия данных о сообщении установите значение 0 (не спам) и укажите причину.\nГенерируйте ответ только в формате JSON. Не включайте никаких доп. текстов или объяснений; предоставляйте только JSON. Вот формат, который следует использовать:\n{\n\"explanation\": \"Краткое объяснение ваших рассуждений\",\n\"spamValue\": <целое число от 0 до 100>\n}\nЗдесь находится информация о почте:\nОтправитель: \"{%author%}\"\nТема: \"{%mail_subject%}\"\nHtml-тело: \"{%mail_html_body%}\"" }, "SpamFilter_prompt_prefs_title": { "message": "Параметры спам-фильтра" From 13c8aeb86eeed93bc6a9b7ebf9b7f388ce90f2ac Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 7 Apr 2026 23:21:31 +0200 Subject: [PATCH 145/269] Translated using Weblate (Portuguese (Brazil)) Currently translated at 47.3% (268 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/pt_BR/ --- _locales/pt-br/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/pt-br/messages.json b/_locales/pt-br/messages.json index d28e7ad5..b07a6a2e 100644 --- a/_locales/pt-br/messages.json +++ b/_locales/pt-br/messages.json @@ -811,7 +811,7 @@ "message": "Filtro de spam automático" }, "prompt_spamfilter_full_text": { - "message": "Analise o seguinte e-mail e determine se é spam ou não. Considere fatores como palavras-chave suspeitas, linguagem promocional excessiva, linhas de assunto enganosas, solicitações de informações pessoais e endereços de remetentes incomuns.\nForneça um valor de 0 (não é spam) a 100 (spam) e uma explicação de no máximo 10 palavras.\nEm caso de ausência de dados da mensagem, defina o valor como 0 (não é spam) e informe o motivo.\nGere uma resposta apenas no formato JSON. Não inclua nenhum texto ou explicação adicional; forneça apenas o JSON. Aqui está o formato a ser usado:\n{\n\"spamValue\": ,\n\"explanation\": \"Breve explicação do seu raciocínio\"\n}\nAqui estão as informações do e-mail:\nRemetente: \"{%author%}\"\nAssunto: \"{%mail_subject%}\"\nCorpo HTML: \"{%mail_html_body%}\"" + "message": "Analise o seguinte e-mail e determine se é spam ou não. Considere fatores como palavras-chave suspeitas, linguagem promocional excessiva, linhas de assunto enganosas, solicitações de informações pessoais e endereços de remetentes incomuns.\nForneça um valor de 0 (não é spam) a 100 (spam) e uma explicação de no máximo 10 palavras.\nEm caso de ausência de dados da mensagem, defina o valor como 0 (não é spam) e informe o motivo.\nGere uma resposta apenas no formato JSON. Não inclua nenhum texto ou explicação adicional; forneça apenas o JSON. Aqui está o formato a ser usado:\n{\n\"explanation\": \"Breve explicação do seu raciocínio\",\n\"spamValue\": \n}\nAqui estão as informações do e-mail:\nRemetente: \"{%author%}\"\nAssunto: \"{%mail_subject%}\"\nCorpo HTML: \"{%mail_html_body%}\"" }, "prefs_OptionText_spamfilter_threshold_Info": { "message": "Se o valor retornado pela IA estiver acima deste limite, o e-mail será movido para a pasta de spam." From f31edbc5695530386d40abbbcce53f15097c4ab8 Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 7 Apr 2026 23:19:50 +0200 Subject: [PATCH 146/269] Translated using Weblate (Czech) Currently translated at 63.4% (359 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/cs/ --- _locales/cs/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index 3d1f2beb..39e78265 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -840,7 +840,7 @@ "message": "Vynutit existující štítky při automatickém označování nebo použití kontextového menu" }, "prompt_spamfilter_full_text": { - "message": "Analyzuj následující e-mail a urči, zda se jedná o spam či nikoli. Zvaž faktory, jako jsou podezřelá klíčová slova, nadměrné propagační výrazy, zavádějící řádky předmětu, žádosti o osobní informace a neobvyklé adresy odesílatele.\nZadej hodnotu od 0 (není spam) do 100 (spam) a vysvětlení o maximálně 10 slovech. \nV případě chybějících údajů zprávy nastavte hodnotu na 0 (není spam) a uveďte důvod.\nVygeneruj odpověď pouze ve formátu JSON. Neuváděj žádný další text nebo vysvětlení; uveď pouze JSON. Zde je formát, který se má použít:\n{\n\"spamValue\": ,\n\"explanation\": \"Stručné vysvětlení vašeho zdůvodnění\"\n}\nZde jsou informace o e-mailu:\nOdesílatel: \"{%author%}\"\nPředmět: \"{%mail_subject%}\"\nText HTML: \"{%mail_html_body%}\"" + "message": "Analyzuj následující e-mail a urči, zda se jedná o spam či nikoli. Zvaž faktory, jako jsou podezřelá klíčová slova, nadměrné propagační výrazy, zavádějící řádky předmětu, žádosti o osobní informace a neobvyklé adresy odesílatele.\nZadej hodnotu od 0 (není spam) do 100 (spam) a vysvětlení o maximálně 10 slovech. \nV případě chybějících údajů zprávy nastavte hodnotu na 0 (není spam) a uveďte důvod.\nVygeneruj odpověď pouze ve formátu JSON. Neuváděj žádný další text nebo vysvětlení; uveď pouze JSON. Zde je formát, který se má použít:\n{\n\"explanation\": \"Stručné vysvětlení vašeho zdůvodnění\",\n\"spamValue\": \n}\nZde jsou informace o e-mailu:\nOdesílatel: \"{%author%}\"\nPředmět: \"{%mail_subject%}\"\nText HTML: \"{%mail_html_body%}\"" }, "context_menu_mzta-add-tags": { "message": "Přidat štítky" From 2f0d4331d6bc5960af5b19b1bf75516f96ec21d2 Mon Sep 17 00:00:00 2001 From: Mic Date: Tue, 7 Apr 2026 23:20:39 +0200 Subject: [PATCH 147/269] Translated using Weblate (French) Currently translated at 84.6% (479 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/fr/ --- _locales/fr/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 9ff26474..902a3d84 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -832,7 +832,7 @@ "message": "Date du rapport" }, "prompt_spamfilter_full_text": { - "message": "Analysez l'e-mail suivant et déterminez s'il s'agit d'un spam ou non. Prenez en compte des facteurs tels que des mots-clés suspects, un langage promotionnel excessif, des lignes d'objet trompeuses, des demandes d'informations personnelles et des adresses d'expéditeurs inhabituelles.\nFournissez une valeur de 0 (non spam) à 100 (spam) et une explication de 10 mots maximum.\nEn cas de données de message manquantes, définissez la valeur à 0 (non spam) et indiquez la raison.\nGénérez une réponse uniquement au format JSON. N'incluez aucun texte ou explication supplémentaire ; fournissez uniquement le JSON. Voici le format à utiliser :\n{\n\"spamValue\" : ,\n\"explanation\" : \"Brève explication de votre raisonnement\"\n}\nVoici les informations de l'e-mail :\nExpéditeur : \"{%author%}\"\nObjet : \"{%mail_subject%}\"\nCorps HTML : \"{%mail_html_body%}\"" + "message": "Analysez l'e-mail suivant et déterminez s'il s'agit d'un spam ou non. Prenez en compte des facteurs tels que des mots-clés suspects, un langage promotionnel excessif, des lignes d'objet trompeuses, des demandes d'informations personnelles et des adresses d'expéditeurs inhabituelles.\nFournissez une valeur de 0 (non spam) à 100 (spam) et une explication de 10 mots maximum.\nEn cas de données de message manquantes, définissez la valeur à 0 (non spam) et indiquez la raison.\nGénérez une réponse uniquement au format JSON. N'incluez aucun texte ou explication supplémentaire ; fournissez uniquement le JSON. Voici le format à utiliser :\n{\n\"explanation\" : \"Brève explication de votre raisonnement\",\n\"spamValue\" : \n}\nVoici les informations de l'e-mail :\nExpéditeur : \"{%author%}\"\nObjet : \"{%mail_subject%}\"\nCorps HTML : \"{%mail_html_body%}\"" }, "context_menu_mzta-add-tags": { "message": "Ajouter des étiquettes" From 5ea940ee740396978a0900b5c1438d3db69a0877 Mon Sep 17 00:00:00 2001 From: bittin1ddc447d824349b2 Date: Wed, 8 Apr 2026 11:20:41 +0200 Subject: [PATCH 148/269] Translated using Weblate (Swedish) Currently translated at 99.8% (565 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/sv/ --- _locales/sv/messages.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index 3646c7b4..f56140d9 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -1704,5 +1704,14 @@ }, "prefs_OptionText_summarize_strip_formatting_Info": { "message": "Ta bort HTML och Markdown formatering från den AI-genererade sammanfattningen, så att endast vanlig text visas." + }, + "SpamFilter_skip_addresses_title": { + "message": "Lista över e-postadresser att hoppa över" + }, + "SpamFilter_skip_addresses_infoline": { + "message": "E-postmeddelanden från dessa adresser kommer inte att skickas till AI:n för skräppostfiltrering." + }, + "SpamFilter_skip_addresses_infoline2": { + "message": "Lägg till en e-postadress per rad, eller separera med kommatecken." } } From 7a5de636b58a3107a2503ec59130d0553f67a4ce Mon Sep 17 00:00:00 2001 From: 67 Date: Wed, 8 Apr 2026 10:08:27 +0200 Subject: [PATCH 149/269] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 65.5% (371 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/zh_Hans/ --- _locales/zh_Hans/messages.json | 213 ++++++++++++++++++++++++++++++++- 1 file changed, 211 insertions(+), 2 deletions(-) diff --git a/_locales/zh_Hans/messages.json b/_locales/zh_Hans/messages.json index e6c3afbc..46bf312e 100644 --- a/_locales/zh_Hans/messages.json +++ b/_locales/zh_Hans/messages.json @@ -486,7 +486,7 @@ "message": "如果在 ThunderAI 窗口中遇到登录问题,请使用右侧的按钮在新标签页中打开 ChatGPT,完成登录后关闭该标签页,然后继续使用 ThunderAI。" }, "prompt_translate_this_full_text": { - "message": "将以下电子邮件翻译成" + "message": "将以下电子邮件翻译成 {%thunderai_translate_lang%}。\n\n规则:\n- 翻译主题和正文。\n- 以包含三个字段(“subject”、“body”和“status”)的 JSON 对象形式返回结果。\n- 如果翻译已完成,则状态等于 1。\n- 如果电子邮件是以这些语言“{%thunderai_translate_exclude_lang%}”之一或 {%thunderai_translate_lang%} 语言编写的,请为主体和主题返回空字符串,并将状态设置为 -1。\n- 请勿在 JSON 之外添加解释、注释或任何文本。\n\n邮件主题:{%mail_subject%}\n\n邮件正文:{%mail_html_body%}\n\n仅以 JSON 格式生成响应。输出应仅为一个 JSON 对象。以下是要使用的 JSON 格式示例:\n\n{\n\n\"subject\": \"主题翻译\",\n\"body\": \"正文翻译\",\n\"status\": \"状态结果\"\n}" }, "prompt_add_tags": { "message": "为这封电子邮件添加标签" @@ -621,7 +621,7 @@ "message": "单击此处,只需一分钟!" }, "prompt_add_tags_full_text": { - "message": "请分析以下电子邮件文本,并生成一个 JSON 数组的标签,总结其内容。请使用主题、关键话题和相关描述词作为标签。确保标签简洁且与邮件内容密切相关。\n邮件文本:{%mail_text_body%}\n考虑以下细节以获取上下文:\n- 发件人:{%author%}\n- 收件人:{%recipients%}\n- 抄送列表:{%cc_list%}\n- 邮件主题:{%mail_subject%}\n请仅根据邮件正文和上下文生成标签,忽略无关信息或琐碎细节。\n请仅以 JSON 格式生成回复。输出应仅包含标签的 JSON 数组,不包含任何额外注释或文本。以下是需使用的 JSON 格式示例:\n{\n\"tags\": [\"tag1\", \"tag2\", \"tag3\", \"tag4\", \"tag5\"]\n}" + "message": "分析以下邮件正文,并生成一个总结其内容的 JSON 标签数组。使用主题、关键话题和相关描述符作为标签。确保标签简洁且与邮件内容相关。\n邮件正文:{%mail_text_body%}\n考虑以下背景详情:\n- 发件人:{%author%}\n- 收件人:{%recipients%}\n- 抄送列表:{%cc_list%}\n- 邮件主题:{%mail_subject%}\n请根据邮件的正文和背景信息生成标签,忽略不必要的信息或琐碎的细节。\n仅以 JSON 格式生成响应。输出应仅为标签的 JSON 数组,不含任何额外注释或文本。以下是要使用的 JSON 格式示例:\n{\n\"tags\": [\"tag1\", \"tag2\", \"tag3\", \"tag4\", \"tag5\"]\n}" }, "placeholder_tags_full_list": { "message": "现有标签" @@ -925,5 +925,214 @@ }, "prefs_storage_clear_confirm": { "message": "您确定要清除所有已存储的数据(包含:摘要、垃圾邮件报告、翻译等)吗?此操作无法撤销。" + }, + "prefs_storage_clear_done": { + "message": "清除存储后显示的消息", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "prefsInfoDesc_7": { + "message": "要使用 Google Gemini API,您需要一个 Google Gemini API 密钥,并且必须选择一个模型。" + }, + "prefsInfoDesc_8": { + "message": "要使用 Claude API,您需要一个 Anthropic Claude API 密钥,并且必须选择一个模型。" + }, + "placeholder_mail_text_body_or_selected": { + "message": "邮件正文或选定文本" + }, + "placeholder_mail_html_body_or_selected": { + "message": "邮件正文或选定的 HTML" + }, + "prefs_OptionText_chatgpt_web_load_wait_time": { + "message": "页面加载等待时间" + }, + "prefs_OptionText_chatgpt_web_load_wait_time_info": { + "message": "在加载附加内容之前等待 ChatGPT 页面加载的时间(以毫秒为单位)。默认值为 1000 毫秒。如果定义了自定义 GPT 或项目,则该值将额外增加 1000 毫秒。" + }, + "sign_msg_as": { + "message": "使用以下身份签名" + }, + "prompt_reply_custom_command_full_text": { + "message": "请回复以下邮件 \"{%mail_text_body%}\"。{%additional_text%}。仅回复所需文本,不要包含额外的评论或其他文字。" + }, + "prompt_proofread_this": { + "message": "校对这封邮件" + }, + "prompt_proofread_this_full_text": { + "message": "请校对以下电子邮件,并纠正任何拼写或语法错误。仅回复更正后的文本,不要包含任何额外评论或其他文字。\n\n“{%mail_typed_text%}”" + }, + "reset": { + "message": "重置" + }, + "prefs_doc_title": { + "message": "文档" + }, + "prefs_doc_setup_guide": { + "message": "设置指南" + }, + "prefs_doc_custom_prompt_tutorial": { + "message": "自定义提示词教程" + }, + "prefs_doc_open_welcome": { + "message": "打开欢迎页面" + }, + "prompt_add_tags_force_lang": { + "message": "标签必须用以下方式编写:" + }, + "placeholder_mail_quoted_text": { + "message": "邮件正文中的引用文本" + }, + "prompt_get_calendar_event_from_clipboard": { + "message": "从剪贴板添加日历事件" + }, + "clipboard_read_error": { + "message": "无法读取剪贴板。请检查权限。" + }, + "clipboard_empty_error": { + "message": "剪贴板为空。请先复制一些文本。" + }, + "clipboard_permission_denied": { + "message": "剪贴板权限被拒绝。请在设置中重新启用该功能以授予权限。" + }, + "clipboard_permission_error": { + "message": "请求剪贴板权限时出错,请重试。" + }, + "prefs_OptionText_get_calendar_event_from_clipboard": { + "message": "从剪贴板获取日历事件" + }, + "prefs_OptionText_get_calendar_event_from_clipboard_Info": { + "message": "显示一个额外的菜单项,用于根据剪贴板文本内容创建日历事件。" + }, + "Summarize_prompt_prefs_title": { + "message": "摘要选项" + }, + "prompt_summarize": { + "message": "总结这封或这些邮件" + }, + "prompt_summarize_full_text": { + "message": "请提供以下电子邮件的简明摘要。摘要应不超过 3-5 句话,并概括要点。请使用纯段落格式,不要使用项目符号、列表或 Markdown 格式。\n\n" + }, + "prompt_summarize_email_template": { + "message": "邮件模板摘要" + }, + "prompt_summarize_email_template_full_text": { + "message": "发件人:{%author%} \n收件人:{%recipients%} \n抄送:{%cc_list%} \n主题:{%mail_subject%} \n日期:{%mail_datetime%} \n附件: {%mail_attachments_info%} \n\n正文:\n{%mail_text_body%}" + }, + "prompt_summarize_email_separator": { + "message": "电子邮件分隔符" + }, + "prompt_summarize_email_separator_full_text": { + "message": "\n\n----------下一封邮件----------\n\n" + }, + "prompt_get_task": { + "message": "添加新任务" + }, + "prompt_get_task_full_text": { + "message": "从以下文本中提取生成任务所需的所有相关详细信息。提取的信息应包括:\n- 截止日期和时间(如果指定,包括时区)\n- 任务摘要\n- 开始日期和时间(如果指定,包括时区)\n- 确保数据格式清晰且一致,以便直接用于创建任务。\n如果存在相对时间引用,请认为电子邮件的日期和时间为“{%mail_datetime%}”。根据此参考计算开始日期和时间。如果计算出的开始日期和时间早于“{%current_datetime%}”,请使用“{%current_datetime%}”作为基准重新计算开始日期和时间。\n如果您无法获取一项或多项所需信息,请回复空字符串。\n仅以 JSON 格式生成响应。不要包含任何额外的文本或说明;仅提供 JSON。以下是要使用的格式:\n{\n\"InitialDate\": \"YYYYMMDDTHHMMSS\",\n\"dueDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"在此处填写任务摘要\"\n}\n如果没有关于日期的信息,请将其删除。\n以下是文本:“{%selected_text%}”" + }, + "prefs_OptionText_get_task": { + "message": "从选定文本添加新任务" + }, + "prefs_OptionText_get_task_Info": { + "message": "如果选中,则会在菜单中添加一个项目,以便从选定的文本获取任务信息。" + }, + "get_task_prompt_prefs_title": { + "message": "任务选项" + }, + "prefs_OptionText_Summarize_infoline2": { + "message": "您可以根据需要更改提示词,第一个字段是主提示词,第二个字段是单封邮件的模板。邮件列表将附加到主提示词中。邮件将由第三个字段中指定的间隔符分隔。" + }, + "prefs_OptionText_Summarize_main_prompt": { + "message": "针对所有选定电子邮件,描述要执行的任务的主要提示:" + }, + "prefs_OptionText_Summarize_email_template": { + "message": "单封邮件的模板:" + }, + "prefs_OptionText_Summarize_email_separator": { + "message": "电子邮件地址之间的分隔符:" + }, + "prefs_OptionText_get_calendar_event_Sparks_wrong_version": { + "message": "要使用日历事件和任务功能,请安装最新版本的 ThunderAI Sparks 插件。" + }, + "GetTask_PageTitle": { + "message": "管理任务设置" + }, + "GetTask_info_default": { + "message": "在此页面中,您可以修改用于从选定文本获取任务的默认提示。" + }, + "prefs_OptionText_btnManageTaskInfo": { + "message": "管理任务设置" + }, + "task_getting_data_error": { + "message": "获取任务数据时出错" + }, + "task_opening_dialog_error": { + "message": "打开任务对话框时出错" + }, + "no_valid_data_received": { + "message": "未收到来自 AI 的有效数据。" + }, + "prefs_OptionText_add_tags_auto_Info2": { + "message": "请在页面底部选择要为其激活此功能的帐户。" + }, + "prefs_OptionText_add_tags_auto_uselist": { + "message": "仅使用这些标签" + }, + "prefs_OptionText_add_tags_auto_uselist_Info": { + "message": "如果选中此项,AI 将仅添加以下列表中的标签。" + }, + "prefs_OptionText_add_tags_auto_uselist_list_Info": { + "message": "列表中必须至少包含一个标签。每行添加一个标签,标签之间用逗号分隔。" + }, + "prompt_add_tags_use_list": { + "message": "仅使用此逗号分隔列表中的标签" + }, + "prefs_OptionText_add_tags_use_specific_integration_Info": { + "message": "如果选中此项,则无论在 ThunderAI 选项页面中选择哪个模型和 API,都将使用下面指定的模型和 API 向电子邮件添加标签。" + }, + "SpamFilter_skip_addresses_infoline2": { + "message": "每行添加一个电子邮件地址,或用逗号分隔。" + }, + "spamfilter_skip_addresses_explanation": { + "message": "发件人已在反垃圾邮件跳过列表中。" + }, + "Valid": { + "message": "有效的" + }, + "context_menu_mzta-summarize": { + "message": "总结" + }, + "context_menu_mzta-translate": { + "message": "翻译" + }, + "hyprland_warning": { + "message": "如果您在打开 AI 聊天窗口时遇到问题,请尝试将高度和宽度值设置为 0。此问题可能在某些 Linux 环境下出现,例如在使用 Hyprland 时。" + }, + "remember_CORS": { + "message": "记住,您需要在服务器上设置 CORS 设置!" + }, + "maybe_CORS_openai_comp": { + "message": "使用 OpenAI 兼容 API 可能需要在服务器上设置 CORS 设置。" + }, + "CORS_alternative_1": { + "message": "CORS设置有问题吗?" + }, + "CORS_alternative_2_new": { + "message": "点击下方按钮授予当前主机权限,以避免任何 CORS 问题。" + }, + "CORS_give_host_perm": { + "message": "授予当前主机权限" + }, + "CORS_localhost_warn": { + "message": "如果您使用 localhost 或 127.0.0.1,因为 AI 服务器托管在您的 PC 上,则需要 权限。" + }, + "prefs_OptionText_composing_plain_text": { + "message": "以纯文本编写" + }, + "prefs_OptionText_composing_plain_text_Info": { + "message": "如果您以纯文本格式编写电子邮件,请选中此选项。" } } From b0b85b33539491bdcd1ea2cc05ef0fcbc2da730f Mon Sep 17 00:00:00 2001 From: mic Date: Thu, 9 Apr 2026 22:09:12 +0200 Subject: [PATCH 150/269] get calendar event prompt updated for a full day event. see #750 --- _locales/en/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 09cb2ef6..d6fef07e 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -1069,7 +1069,7 @@ "description": "" }, "prompt_get_calendar_event_full_text": { - "message": "Extract all relevant details required to generate a calendar event from the following text. The extracted information should include:\n- Event Title\n- Start Date and Time (including timezone, if specified)\n- End Date and Time (including timezone, if specified)\n- Full day (if mentioned)\n- Attendees\nEnsure the data is formatted clearly and consistently so that it can be directly used for creating a calendar event.\nIf there are relative time references, consider that the date and time of the email are \"{%mail_datetime%}\". Calculate the start date and time based on this reference. If the calculated start date and time are earlier than \"{%current_datetime%}\", recalculate the start date and time using \"{%current_datetime%}\" as the base.\nIf the duration is not specified, set it to one hour.\nThese are the attendees: {%author%}, {%recipients%}, {%cc_list%}. If present, exclude my address: {%account_email_address%}.\nIf you're not able to get one or more of the required information, please respond with an empty string.\nGenerate a response in JSON format only. Do not include any additional text or explanations; provide only the JSON. Here is the format to be used:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Calendar event summary here\",\n\"forceAllDay\": false,\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nHere's the text:\"{%mail_text_body_or_selected%}\"", + "message": "Extract all relevant details required to generate a calendar event from the following text. The extracted information should include:\n- Event Title\n- Start Date and Time (including timezone, if specified)\n- End Date and Time (including timezone, if specified)\n- Full day (if mentioned)\n- Attendees\nEnsure the data is formatted clearly and consistently so that it can be directly used for creating a calendar event.\nIf there are relative time references, consider that the date and time of the email are \"{%mail_datetime%}\". Calculate the start date and time based on this reference. If the calculated start date and time are earlier than \"{%current_datetime%}\", recalculate the start date and time using \"{%current_datetime%}\" as the base.\nIf the duration is not specified, set it to one hour.\nThese are the attendees: {%author%}, {%recipients%}, {%cc_list%}. If present, exclude my address: {%account_email_address%}.\nIf the event is a full day event, endDate must be one day after startDate with time set to \"T000000\".\nIf you're not able to get one or more of the required information, please respond with an empty string.\nGenerate a response in JSON format only. Do not include any additional text or explanations; provide only the JSON. Here is the format to be used:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Calendar event summary here\",\n\"forceAllDay\": false,\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nHere's the text:\"{%mail_text_body_or_selected%}\"", "description": "" }, "prompt_get_calendar_event_from_clipboard": { From df0e26ebee9aea57161e8d46acddccb0dd570b4b Mon Sep 17 00:00:00 2001 From: mic Date: Thu, 9 Apr 2026 22:10:08 +0200 Subject: [PATCH 151/269] 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 28068179..8a04b767 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@
    • Fix: The "Important Information" section in the options page now updates correctly when choosing an integration [#730].
    • In the options page now is visible if a special prompt is using a specific API integration [#676].
    • Added an antispam skip list to ensure messages from designated addresses are not forwarded to the AI [#743].
    • +
    • Fix: Correctly setting the end date for a new calendar event [#750].
    • ...
    • Version 4.0.3 - 20/03/2026

      diff --git a/options/mzta-release-notes.html b/options/mzta-release-notes.html index 1a5f314f..96f231e5 100644 --- a/options/mzta-release-notes.html +++ b/options/mzta-release-notes.html @@ -17,6 +17,7 @@
    • Fix: The "Important Information" section in the options page now updates correctly when choosing an integration [#730].
    • In the options page now is visible if a special prompt is using a specific API integration [#676].
    • Added an antispam skip list to ensure messages from designated addresses are not forwarded to the AI [#743].
    • +
    • Fix: Correctly setting the end date for a new calendar event [#750].
    • ...
    • Version 4.0.3 - 20/03/2026

      From fd8afc5d1bc150c52ea17921f02be67b2eae3f6b Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 9 Apr 2026 22:17:34 +0200 Subject: [PATCH 152/269] Translated using Weblate (Greek) Currently translated at 81.2% (460 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/el/ --- _locales/el/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/el/messages.json b/_locales/el/messages.json index c1f7d025..491caeda 100644 --- a/_locales/el/messages.json +++ b/_locales/el/messages.json @@ -561,7 +561,7 @@ "message": "Προσθήκη νέου συμβάντος ημερολογίου" }, "prompt_get_calendar_event_full_text": { - "message": "Εξαγάγετε όλες τις σχετικές λεπτομέρειες που απαιτούνται για τη δημιουργία ενός συμβάντος ημερολογίου από το ακόλουθο κείμενο. Οι εξαγόμενες πληροφορίες θα πρέπει να περιλαμβάνουν:\n- Τίτλο συμβάντος\n- Ημερομηνία και ώρα έναρξης (συμπεριλαμβανομένης της ζώνης ώρας, εάν καθορίζεται)\n- Ημερομηνία και ώρα λήξης (συμπεριλαμβανομένης της ζώνης ώρας, εάν καθορίζεται)\n- Ολόκληρη ημέρα (εάν αναφέρεται)\n- Συμμετέχοντες\nΒεβαιωθείτε ότι τα δεδομένα έχουν μορφοποιηθεί με σαφήνεια και συνέπεια, ώστε να μπορούν να χρησιμοποιηθούν άμεσα για τη δημιουργία ενός συμβάντος ημερολογίου.\nΕάν υπάρχουν σχετικές χρονικές αναφορές, λάβετε υπόψη ότι η ημερομηνία και η ώρα του email είναι \"{%mail_datetime%}\". Υπολογίστε την ημερομηνία και την ώρα έναρξης με βάση αυτήν την αναφορά. Εάν η υπολογισμένη ημερομηνία και ώρα έναρξης είναι προγενέστερες από το \"{%current_datetime%}\", υπολογίστε ξανά την ημερομηνία και την ώρα έναρξης χρησιμοποιώντας το \"{%current_datetime%}\" ως βάση.\nΕάν η διάρκεια δεν έχει καθοριστεί, ορίστε την σε μία ώρα.\nΑυτοί είναι οι συμμετέχοντες: {%author%}, {%recipients%}, {%cc_list%}. Εάν υπάρχει, εξαιρέστε τη διεύθυνσή μου: {%account_email_address%}.\nΕάν δεν μπορείτε να λάβετε μία ή περισσότερες από τις απαιτούμενες πληροφορίες, απαντήστε με μια κενή συμβολοσειρά.\nΔημιουργήστε μια απάντηση μόνο σε μορφή JSON. Μην συμπεριλάβετε κανένα επιπλέον κείμενο ή εξηγήσεις. Δώστε μόνο το JSON. Η μορφή που θα χρησιμοποιηθεί είναι η εξής:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Σύνοψη συμβάντος ημερολογίου εδώ\",\n\"forceAllDay\": false,\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nΤο κείμενο είναι: \"{%mail_text_body_or_selected%}\"" + "message": "Εξαγάγετε όλες τις σχετικές λεπτομέρειες που απαιτούνται για τη δημιουργία ενός συμβάντος ημερολογίου από το ακόλουθο κείμενο. Οι εξαγόμενες πληροφορίες θα πρέπει να περιλαμβάνουν:\n- Τίτλο συμβάντος\n- Ημερομηνία και ώρα έναρξης (συμπεριλαμβανομένης της ζώνης ώρας, εάν καθορίζεται)\n- Ημερομηνία και ώρα λήξης (συμπεριλαμβανομένης της ζώνης ώρας, εάν καθορίζεται)\n- Ολόκληρη ημέρα (εάν αναφέρεται)\n- Συμμετέχοντες\nΒεβαιωθείτε ότι τα δεδομένα έχουν μορφοποιηθεί με σαφήνεια και συνέπεια, ώστε να μπορούν να χρησιμοποιηθούν άμεσα για τη δημιουργία ενός συμβάντος ημερολογίου.\nΕάν υπάρχουν σχετικές χρονικές αναφορές, λάβετε υπόψη ότι η ημερομηνία και η ώρα του email είναι \"{%mail_datetime%}\". Υπολογίστε την ημερομηνία και την ώρα έναρξης με βάση αυτήν την αναφορά. Εάν η υπολογισμένη ημερομηνία και ώρα έναρξης είναι προγενέστερες από το \"{%current_datetime%}\", υπολογίστε ξανά την ημερομηνία και την ώρα έναρξης χρησιμοποιώντας το \"{%current_datetime%}\" ως βάση.\nΕάν η διάρκεια δεν έχει καθοριστεί, ορίστε την σε μία ώρα.\nΑυτοί είναι οι συμμετέχοντες: {%author%}, {%recipients%}, {%cc_list%}. Εάν υπάρχει, εξαιρέστε τη διεύθυνσή μου: {%account_email_address%}.\nΕάν το συμβάν είναι ολοήμερο, η ημερομηνία λήξης (endDate) πρέπει να είναι μία ημέρα μετά την ημερομηνία έναρξης (startDate) με την ώρα να έχει οριστεί σε \"T000000\".\nΕάν δεν μπορείτε να λάβετε μία ή περισσότερες από τις απαιτούμενες πληροφορίες, απαντήστε με μια κενή συμβολοσειρά.\nΔημιουργήστε μια απάντηση μόνο σε μορφή JSON. Μην συμπεριλάβετε κανένα επιπλέον κείμενο ή εξηγήσεις. Δώστε μόνο το JSON. Η μορφή που θα χρησιμοποιηθεί είναι η εξής:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Σύνοψη συμβάντος ημερολογίου εδώ\",\n\"forceAllDay\": false,\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nΤο κείμενο είναι: \"{%mail_text_body_or_selected%}\"" }, "prompt_get_task": { "message": "Προσθήκη νέας εργασίας" From 024260009d5d83d759731e76cc440ded1b02d915 Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 9 Apr 2026 22:15:13 +0200 Subject: [PATCH 153/269] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 65.5% (371 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/zh_Hans/ --- _locales/zh_Hans/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/zh_Hans/messages.json b/_locales/zh_Hans/messages.json index 46bf312e..bf7bc82a 100644 --- a/_locales/zh_Hans/messages.json +++ b/_locales/zh_Hans/messages.json @@ -762,7 +762,7 @@ "message": "管理垃圾邮件过滤器设置" }, "prompt_get_calendar_event_full_text": { - "message": "从以下文本中提取生成日历事件所需的所有相关细节。提取的信息应包括:\n- 事件标题\n- 开始日期和时间(如果指定时区,则包括时区)\n- 结束日期和时间(如果指定时区,则包括时区)\n- 全天事件(如果提及)\n- 参与者 \n确保数据格式清晰且一致,以便可以直接用于创建日历事件。\n如果存在相对时间的引用,请注意邮件的日期和时间为“{%mail_datetime%}”。基于此参考计算开始日期和时间。如果计算出的开始日期和时间早于“{%current_datetime%}”,则使用“{%current_datetime%}”作为基准重新计算开始日期和时间。\n如果未指定持续时间,请将其设置为一小时。\n以下是参与者:{%author%}, {%recipients%}, {%cc_list%}。如有,请排除我的地址:{%account_email_address%}。\n如果无法获取一个或多个所需信息,请以空字符串响应。\n仅以 JSON 格式生成响应。不要包含任何额外的文本或说明,仅提供 JSON。以下是使用的格式:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"日历事件摘要\",\n\"forceAllDay\": false\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\n以下是文本:“{%mail_text_body_or_selected%}”" + "message": "从以下文本中提取生成日历事件所需的所有相关细节。提取的信息应包括:\n- 事件标题\n- 开始日期和时间(如果指定时区,则包括时区)\n- 结束日期和时间(如果指定时区,则包括时区)\n- 全天事件(如果提及)\n- 参与者 \n确保数据格式清晰且一致,以便可以直接用于创建日历事件。\n如果存在相对时间的引用,请注意邮件的日期和时间为“{%mail_datetime%}”。基于此参考计算开始日期和时间。如果计算出的开始日期和时间早于“{%current_datetime%}”,则使用“{%current_datetime%}”作为基准重新计算开始日期和时间。\n如果未指定持续时间,请将其设置为一小时。\n以下是参与者:{%author%}, {%recipients%}, {%cc_list%}。如有,请排除我的地址:{%account_email_address%}。\n如果该活动为全天活动,endDate 必须为 startDate 的后一天,且时间设置为 \"T000000\"。\n如果无法获取一个或多个所需信息,请以空字符串响应。\n仅以 JSON 格式生成响应。不要包含任何额外的文本或说明,仅提供 JSON。以下是使用的格式:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"日历事件摘要\",\n\"forceAllDay\": false\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\n以下是文本:“{%mail_text_body_or_selected%}”" }, "prefs_OptionText_get_calendar_event": { "message": "从所选文本添加新日历事件" From 5310be10594d2780e1defea3113e4ddd569ce3c2 Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 9 Apr 2026 22:19:27 +0200 Subject: [PATCH 154/269] Translated using Weblate (German) Currently translated at 84.6% (479 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/de/ --- _locales/de/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 85c64b44..18aab34a 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -694,7 +694,7 @@ "message": "Ein neues Kalenderevent hinzufügen" }, "prompt_get_calendar_event_full_text": { - "message": "Extrahieren Sie alle relevanten Details, die erforderlich sind, um ein Kalenderevent aus dem folgenden Text zu erstellen. Die extrahierten Informationen sollten Folgendes enthalten:\n- Ereignistitel\n- Startdatum und -uhrzeit (einschließlich Zeitzone, falls angegeben)\n- Enddatum und -uhrzeit (einschließlich Zeitzone, falls angegeben)\n- Ganztägig (falls erwähnt)\n- Teilnehmer\nStellen Sie sicher, dass die Daten klar und konsistent formatiert sind, sodass sie direkt für die Erstellung eines Kalenderevents verwendet werden können.\nFalls relative Zeitangaben enthalten sind, beachten Sie, dass Datum und Uhrzeit der E-Mail \"{%mail_datetime%}\" sind. Berechnen Sie das Startdatum und die Startzeit basierend auf diesem Bezugspunkt. Falls das berechnete Startdatum und die Startzeit vor \"{%current_datetime%}\" liegen, berechnen Sie Startdatum und -zeit erneut, wobei Sie \"{%current_datetime%}\" als Grundlage verwenden.\nFalls keine Dauer angegeben ist, setzen Sie sie auf eine Stunde.\nDas sind die Teilnehmer: {%author%}, {%recipients%}, {%cc_list%}. Falls vorhanden, meine Adresse ausschließen: {%account_email_address%}.\nFalls Sie eine oder mehrere der erforderlichen Informationen nicht erhalten können, antworten Sie mit einem leeren String.\nErstellen Sie eine Antwort ausschließlich im JSON-Format. Fügen Sie keinen zusätzlichen Text oder Erklärungen hinzu; geben Sie nur das JSON an. Verwenden Sie folgendes Format:\n{\n \"startDate\": \"YYYYMMDDTHHMMSS\",\n \"endDate\": \"YYYYMMDDTHHMMSS\",\n \"summary\": \"Zusammenfassung des Kalenderevents hier\",\n \"forceAllDay\": false\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nHier ist der Text: \"{%mail_text_body_or_selected%}\"" + "message": "Extrahieren Sie alle relevanten Details, die erforderlich sind, um ein Kalenderevent aus dem folgenden Text zu erstellen. Die extrahierten Informationen sollten Folgendes enthalten:\n- Ereignistitel\n- Startdatum und -uhrzeit (einschließlich Zeitzone, falls angegeben)\n- Enddatum und -uhrzeit (einschließlich Zeitzone, falls angegeben)\n- Ganztägig (falls erwähnt)\n- Teilnehmer\nStellen Sie sicher, dass die Daten klar und konsistent formatiert sind, sodass sie direkt für die Erstellung eines Kalenderevents verwendet werden können.\nFalls relative Zeitangaben enthalten sind, beachten Sie, dass Datum und Uhrzeit der E-Mail \"{%mail_datetime%}\" sind. Berechnen Sie das Startdatum und die Startzeit basierend auf diesem Bezugspunkt. Falls das berechnete Startdatum und die Startzeit vor \"{%current_datetime%}\" liegen, berechnen Sie Startdatum und -zeit erneut, wobei Sie \"{%current_datetime%}\" als Grundlage verwenden.\nFalls keine Dauer angegeben ist, setzen Sie sie auf eine Stunde.\nDas sind die Teilnehmer: {%author%}, {%recipients%}, {%cc_list%}. Falls vorhanden, meine Adresse ausschließen: {%account_email_address%}.\nWenn es sich um ein ganztägiges Ereignis handelt, muss endDate ein Tag nach startDate liegen und die Zeit auf \"T000000\" eingestellt sein.\nFalls Sie eine oder mehrere der erforderlichen Informationen nicht erhalten können, antworten Sie mit einem leeren String.\nErstellen Sie eine Antwort ausschließlich im JSON-Format. Fügen Sie keinen zusätzlichen Text oder Erklärungen hinzu; geben Sie nur das JSON an. Verwenden Sie folgendes Format:\n{\n \"startDate\": \"YYYYMMDDTHHMMSS\",\n \"endDate\": \"YYYYMMDDTHHMMSS\",\n \"summary\": \"Zusammenfassung des Kalenderevents hier\",\n \"forceAllDay\": false\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nHier ist der Text: \"{%mail_text_body_or_selected%}\"" }, "prefs_OptionText_get_calendar_event": { "message": "Ein neues Kalenderevent aus ausgewähltem Text hinzufügen" From 01fda7a43a2c44b4493d9402ca748c1601ee05d3 Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 9 Apr 2026 22:16:44 +0200 Subject: [PATCH 155/269] Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 69.4% (393 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/zh_Hant/ --- _locales/zh_Hant/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/zh_Hant/messages.json b/_locales/zh_Hant/messages.json index dda1ea7a..502a6903 100644 --- a/_locales/zh_Hant/messages.json +++ b/_locales/zh_Hant/messages.json @@ -1072,7 +1072,7 @@ "message": "您尚未選擇 ChatGPT API 的模型。請在選項頁面中選擇一個。" }, "prompt_get_calendar_event_full_text": { - "message": "從以下文字中提取所有需要生成日曆事件的相關資訊。 提取的資訊應包含:\n- 事件名稱\n- 起始日期和時間(包含時區,如果指定)\n- 結束日期和時間(包含時間區,如果指定)\n- 整天事件(如果提及\n- 參與者\n確保數據以清晰一致的方式格式化,以便直接用於創建日曆事件。\n如果有相對時間參考,請考慮電子郵件的日期和時間是 「{%mail_datetime%}」。 計算基於此參考的起日期和時間。 如果計算出的起日期和時間早於「{%current_datetime%}」,則使用「{%current_datetime%}」作為基準重新計算起日期和時間。\n如果持續時間沒有指定,則設定為一小時。\n參與者:{%author%}, {%recipients%}, {%cc_list%}。如果存在,請排除我的地址:{%account_email_address%}。\n如果無法獲得其中一個或多個所需的資訊,請回覆一個空字串。\n請以 JSON 格式回覆,不要包含任何額外的文字或說明,提供僅 JSON。 以下是將要使用的格式:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"日曆事件摘要在此\",\n\"forceAllDay\": false,\n\"attendees\": [\"attendee1@example.com \",\"attendee2@example.com \",\"attendee3@example.com \"]\n}\n這裡是文字:「{%mail_text_body_or_selected%}」" + "message": "從以下文字中提取所有需要生成日曆事件的相關資訊。 提取的資訊應包含:\n- 事件名稱\n- 起始日期和時間(包含時區,如果指定)\n- 結束日期和時間(包含時間區,如果指定)\n- 整天事件(如果提及\n- 參與者\n確保數據以清晰一致的方式格式化,以便直接用於創建日曆事件。\n如果有相對時間參考,請考慮電子郵件的日期和時間是 「{%mail_datetime%}」。 計算基於此參考的起日期和時間。 如果計算出的起日期和時間早於「{%current_datetime%}」,則使用「{%current_datetime%}」作為基準重新計算起日期和時間。\n如果持續時間沒有指定,則設定為一小時。\n參與者:{%author%}, {%recipients%}, {%cc_list%}。如果存在,請排除我的地址:{%account_email_address%}。\n如果該活動為全天活動,endDate 必須為 startDate 的後一天,且時間設置為 \"T000000\"。\n如果無法獲得其中一個或多個所需的資訊,請回覆一個空字串。\n請以 JSON 格式回覆,不要包含任何額外的文字或說明,提供僅 JSON。 以下是將要使用的格式:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"日曆事件摘要在此\",\n\"forceAllDay\": false,\n\"attendees\": [\"attendee1@example.com \",\"attendee2@example.com \",\"attendee3@example.com \"]\n}\n這裡是文字:「{%mail_text_body_or_selected%}」" }, "TranslateText": { "message": "您願意幫忙翻譯這個附加元件嗎?" From cfda255495b488266d34bcdb766925f773dd269f Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 9 Apr 2026 22:17:52 +0200 Subject: [PATCH 156/269] Translated using Weblate (Polish) Currently translated at 49.8% (282 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/pl/ --- _locales/pl/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index e300eb87..005cb54b 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -685,7 +685,7 @@ "message": "Dodaj nowe wydarzenie w kalendarzu" }, "prompt_get_calendar_event_full_text": { - "message": "Wyodrębnij wszystkie istotne szczegóły wymagane do wygenerowania wydarzenia w kalendarzu z poniższego tekstu. Wyodrębnione informacje powinny obejmować:\n- Tytuł wydarzenia\n- Datę i godzinę rozpoczęcia (w tym strefę czasową, jeśli została określona)\n- Datę i godzinę zakończenia (w tym strefę czasową, jeśli została określona)\n- Cały dzień (jeśli jest podany)\n- Uczestnicy\nUpewnij się, że dane są sformatowane w sposób jasny i spójny, tak aby można go bezpośrednio wykorzystać do utworzenia wydarzenia w kalendarzu.\nJeśli istnieją odniesienia do czasu względnego, pamiętaj, że data i godzina wysłania wiadomości e-mail to „{%mail_datetime%}”. Oblicz datę i godzinę rozpoczęcia na podstawie tego odniesienia. Jeśli obliczona data i godzina rozpoczęcia są wcześniejsze niż „{%current_datetime%}”, oblicz ponownie datę i godzinę rozpoczęcia, stosując jako podstawę „{%current_datetime%}”.\nJeśli czas trwania nie jest określony, ustaw go na jedną godzinę.\nOto uczestnicy: {%author%}, {%recipients%}, {%cc_list%}. Jeśli jest obecny, wyklucz mój adres: {%account_email_address%}.\nJeśli nie możesz uzyskać co najmniej jednej z wymaganych informacji, w odpowiedzi wpisz pusty ciąg znaków.\nWygeneruj odpowiedź tylko w formacie JSON. Nie dołączaj żadnego dodatkowego tekstu ani wyjaśnień; podaj tylko JSON. Oto format, którego należy użyć:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Tutaj podsumowanie wydarzenia w kalendarzu\",\n\"forceAllDay\": false\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nOto tekst:\"{%mail_text_body_or_selected%}\"" + "message": "Wyodrębnij wszystkie istotne szczegóły wymagane do wygenerowania wydarzenia w kalendarzu z poniższego tekstu. Wyodrębnione informacje powinny obejmować:\n- Tytuł wydarzenia\n- Datę i godzinę rozpoczęcia (w tym strefę czasową, jeśli została określona)\n- Datę i godzinę zakończenia (w tym strefę czasową, jeśli została określona)\n- Cały dzień (jeśli jest podany)\n- Uczestnicy\nUpewnij się, że dane są sformatowane w sposób jasny i spójny, tak aby można go bezpośrednio wykorzystać do utworzenia wydarzenia w kalendarzu.\nJeśli istnieją odniesienia do czasu względnego, pamiętaj, że data i godzina wysłania wiadomości e-mail to „{%mail_datetime%}”. Oblicz datę i godzinę rozpoczęcia na podstawie tego odniesienia. Jeśli obliczona data i godzina rozpoczęcia są wcześniejsze niż „{%current_datetime%}”, oblicz ponownie datę i godzinę rozpoczęcia, stosując jako podstawę „{%current_datetime%}”.\nJeśli wydarzenie jest całodniowe, data zakończenia (endDate) musi przypadać na dzień po dacie rozpoczęcia (startDate), a godzina musi być ustawiona na \"T000000\".\nJeśli czas trwania nie jest określony, ustaw go na jedną godzinę.\nOto uczestnicy: {%author%}, {%recipients%}, {%cc_list%}. Jeśli jest obecny, wyklucz mój adres: {%account_email_address%}.\nJeśli nie możesz uzyskać co najmniej jednej z wymaganych informacji, w odpowiedzi wpisz pusty ciąg znaków.\nWygeneruj odpowiedź tylko w formacie JSON. Nie dołączaj żadnego dodatkowego tekstu ani wyjaśnień; podaj tylko JSON. Oto format, którego należy użyć:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Tutaj podsumowanie wydarzenia w kalendarzu\",\n\"forceAllDay\": false\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nOto tekst:\"{%mail_text_body_or_selected%}\"" }, "prefs_OptionText_get_calendar_event": { "message": "Dodaj nowe wydarzenie w kalendarzu z zaznaczonego tekstu" From e48f3a339d2456945a19026048d31ce606e02343 Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 9 Apr 2026 22:17:17 +0200 Subject: [PATCH 157/269] Translated using Weblate (Japanese) Currently translated at 74.5% (422 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/ja/ --- _locales/ja/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 34177c75..2b8c891b 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -747,7 +747,7 @@ "message": "新しいカレンダーイベントを追加" }, "prompt_get_calendar_event_full_text": { - "message": "以下のテキストからカレンダーイベントを生成するために必要なすべての関連詳細を抽出してください。抽出情報には以下を含めてください:\n- イベントタイトル\n- 開始日時(指定されている場合はタイムゾーンを含む)\n- 終了日時(指定されている場合はタイムゾーンを含む)\n- 終日(言及されている場合)\n- 出席者\nデータはカレンダーイベントの作成に直接使用できるよう、明確かつ一貫した形式にしてください。\n相対的な時間参照がある場合、メールの日時は「{%mail_datetime%}」であることを考慮してください。この参照に基づいて開始日時を計算してください。計算された開始日時が「{%current_datetime%}」より前の場合、「{%current_datetime%}」を基準として開始日時を再計算してください。\n期間が指定されていない場合は、1時間に設定してください。\n出席者:{%author%}、{%recipients%}、{%cc_list%}。存在する場合、私のアドレス{%account_email_address%}を除外してください。\n必要な情報の1つ以上を取得できない場合は、空の文字列で応答してください。\nJSON形式のみで応答を生成してください。追加のテキストや説明は含めず、JSONのみを提供してください。使用する形式:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"カレンダーイベントの概要をここに\",\n\"forceAllDay\": false,\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nテキスト:「{%mail_text_body_or_selected%}」" + "message": "以下のテキストからカレンダーイベントを生成するために必要なすべての関連詳細を抽出してください。抽出情報には以下を含めてください:\n- イベントタイトル\n- 開始日時(指定されている場合はタイムゾーンを含む)\n- 終了日時(指定されている場合はタイムゾーンを含む)\n- 終日(言及されている場合)\n- 出席者\nデータはカレンダーイベントの作成に直接使用できるよう、明確かつ一貫した形式にしてください。\n相対的な時間参照がある場合、メールの日時は「{%mail_datetime%}」であることを考慮してください。この参照に基づいて開始日時を計算してください。計算された開始日時が「{%current_datetime%}」より前の場合、「{%current_datetime%}」を基準として開始日時を再計算してください。\n期間が指定されていない場合は、1時間に設定してください。\n出席者:{%author%}、{%recipients%}、{%cc_list%}。存在する場合、私のアドレス{%account_email_address%}を除外してください。\n終日イベントの場合、endDateはstartDateの翌日に設定し、時刻を「T000000」とする必要があります。\n必要な情報の1つ以上を取得できない場合は、空の文字列で応答してください。\nJSON形式のみで応答を生成してください。追加のテキストや説明は含めず、JSONのみを提供してください。使用する形式:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"カレンダーイベントの概要をここに\",\n\"forceAllDay\": false,\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nテキスト:「{%mail_text_body_or_selected%}」" }, "prompt_get_task": { "message": "新しいタスクを追加" From 31283f7f7ee7b5824aa3f18b63b7b13acc18ac0a Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 9 Apr 2026 22:14:50 +0200 Subject: [PATCH 158/269] Translated using Weblate (Czech) Currently translated at 63.4% (359 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/cs/ --- _locales/cs/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index 39e78265..ee7c8c0c 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -792,7 +792,7 @@ "message": "Spravovat nastavení štítků" }, "prompt_get_calendar_event_full_text": { - "message": "Z následujícího textu extrahuj všechny relevantní podrobnosti potřebné k vygenerování události kalendáře. Extrahované informace by měly zahrnovat:\n- Název události\n- Datum a čas zahájení (včetně časového pásma, pokud je uvedeno)\n- Datum a čas ukončení (včetně časového pásma, pokud je uvedeno)\n- Celý den (pokud je zmíněn)\n- Účastníci\nZajisti, aby byla data formátována jasně a konzistentně, aby je bylo možné přímo použít k vytvoření události kalendáře.\nPokud existují relativní časové odkazy, vezmi v úvahu, že datum a čas e-mailu jsou \"{%mail_datetime%}\". Vypočítej datum a čas zahájení na základě tohoto odkazu. Pokud jsou vypočítané datum a čas zahájení dřívější než \"{%current_datetime%}\", přepočti datum a čas zahájení pomocí \"{%current_datetime%}\" jako základu.\nPokud není zadána doba trvání, nastav ji na jednu hodinu.\nToto jsou účastníci: {%author%}, {%recipients%}, {%cc_list%}. Pokud je přítomna, nezahrnuj mou adresu: {%account_email_address%}.\nPokud nejsi schopen získat jednu nebo více požadovaných informací, odpověz prázdným řetězcem.\nVygeneruj odpověď pouze ve formátu JSON. Nezahrnuj žádný další text ani vysvětlení; poskytni pouze JSON. Zde je formát, který se má použít:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Zde je souhrn události kalendáře\",\n\"forceAllDay\": false,\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nZde je text:\"{%mail_text_body_or_selected%}\"" + "message": "Z následujícího textu extrahuj všechny relevantní podrobnosti potřebné k vygenerování události kalendáře. Extrahované informace by měly zahrnovat:\n- Název události\n- Datum a čas zahájení (včetně časového pásma, pokud je uvedeno)\n- Datum a čas ukončení (včetně časového pásma, pokud je uvedeno)\n- Celý den (pokud je zmíněn)\n- Účastníci\nZajisti, aby byla data formátována jasně a konzistentně, aby je bylo možné přímo použít k vytvoření události kalendáře.\nPokud existují relativní časové odkazy, vezmi v úvahu, že datum a čas e-mailu jsou \"{%mail_datetime%}\". Vypočítej datum a čas zahájení na základě tohoto odkazu. Pokud jsou vypočítané datum a čas zahájení dřívější než \"{%current_datetime%}\", přepočti datum a čas zahájení pomocí \"{%current_datetime%}\" jako základu.\nPokud není zadána doba trvání, nastav ji na jednu hodinu.\nToto jsou účastníci: {%author%}, {%recipients%}, {%cc_list%}. Pokud je přítomna, nezahrnuj mou adresu: {%account_email_address%}.\nPokud se jedná o celodenní událost, musí být endDate jeden den po startDate s časem nastaveným na \"T000000\".\nPokud nejsi schopen získat jednu nebo více požadovaných informací, odpověz prázdným řetězcem.\nVygeneruj odpověď pouze ve formátu JSON. Nezahrnuj žádný další text ani vysvětlení; poskytni pouze JSON. Zde je formát, který se má použít:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Zde je souhrn události kalendáře\",\n\"forceAllDay\": false,\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nZde je text:\"{%mail_text_body_or_selected%}\"" }, "prefs_OptionText_get_calendar_event": { "message": "Přidat novou událost do kalendáře z vybraného textu" From 7c7e83457ff03052463c4b01357bf3968a47397c Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 9 Apr 2026 22:15:45 +0200 Subject: [PATCH 159/269] Translated using Weblate (Croatian) Currently translated at 47.8% (271 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/hr/ --- _locales/hr/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/hr/messages.json b/_locales/hr/messages.json index bfaa4747..59a22516 100644 --- a/_locales/hr/messages.json +++ b/_locales/hr/messages.json @@ -694,7 +694,7 @@ "message": "Dodaj novi kalendarski događaj" }, "prompt_get_calendar_event_full_text": { - "message": "Izdvoji sve relevantne detalje potrebne za generiranje kalendarskog događaja iz sljedećeg teksta. Izdvojene informacije trebaju uključivati:\n- Naslov događaja\n- Datum i vrijeme početka (uključujući vremensku zonu, ako je navedeno)\n- Datum i vrijeme završetka (uključujući vremensku zonu, ako je navedeno)\n- Cijeli dan (ako je navedeno)\n- Sudionici\nOsiguraj da su podaci oblikovani jasno i dosljedno kako bi se mogli izravno koristiti za stvaranje kalendarskog događaja.\nAko postoje relativne vremenske napomene, smatraj da su datum i vrijeme e-poruke \"{%mail_datetime%}\". Izračunajte datum i vrijeme početka na temelju ove napomene. Ako su izračunati početni datum i vrijeme raniji od \"{%current_datetime%}\", ponovno izračunaj početni datum i vrijeme koristeći \"{%current_datetime%}\" kao osnovu.\nAko trajanje nije navedeno, postavi ga na jedan sat.\nOvo su sudionici: {%author%}, {%recipients%}, {%cc_list%}. Ako je prisutna, isključi moju adresu: {%account_email_address%}.\nAko ne možeš dobiti jednu ili više potrebnih informacija, odgovori praznim nizom.\nGeneriraj odgovor samo u JSON formatu. Nemoj uključivati nikakav dodatni tekst ili objašnjenja; pruži samo JSON. Ovo je format koji će se koristiti:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Sažetak kalendarskih događaja ovdje\",\n\"forceAllDay\": false\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nOvo je tekst:\"{%mail_text_body_or_selected%}\"" + "message": "Izdvoji sve relevantne detalje potrebne za generiranje kalendarskog događaja iz sljedećeg teksta. Izdvojene informacije trebaju uključivati:\n- Naslov događaja\n- Datum i vrijeme početka (uključujući vremensku zonu, ako je navedeno)\n- Datum i vrijeme završetka (uključujući vremensku zonu, ako je navedeno)\n- Cijeli dan (ako je navedeno)\n- Sudionici\nOsiguraj da su podaci oblikovani jasno i dosljedno kako bi se mogli izravno koristiti za stvaranje kalendarskog događaja.\nAko postoje relativne vremenske napomene, smatraj da su datum i vrijeme e-poruke \"{%mail_datetime%}\". Izračunajte datum i vrijeme početka na temelju ove napomene. Ako su izračunati početni datum i vrijeme raniji od \"{%current_datetime%}\", ponovno izračunaj početni datum i vrijeme koristeći \"{%current_datetime%}\" kao osnovu.\nAko trajanje nije navedeno, postavi ga na jedan sat.\nOvo su sudionici: {%author%}, {%recipients%}, {%cc_list%}. Ako je prisutna, isključi moju adresu: {%account_email_address%}.\nAko je događaj cjelodnevni, **endDate** mora biti jedan dan nakon **startDate** s vremenom postavljenim na **\"T000000\"**.\nAko ne možeš dobiti jednu ili više potrebnih informacija, odgovori praznim nizom.\nGeneriraj odgovor samo u JSON formatu. Nemoj uključivati nikakav dodatni tekst ili objašnjenja; pruži samo JSON. Ovo je format koji će se koristiti:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Sažetak kalendarskih događaja ovdje\",\n\"forceAllDay\": false\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nOvo je tekst:\"{%mail_text_body_or_selected%}\"" }, "prefs_OptionText_get_calendar_event": { "message": "Dodaj novi kalendarski događaj iz odabranog teksta" From 33c60ec189e86dd9d34048f60db36e8d2930d1da Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 9 Apr 2026 22:18:57 +0200 Subject: [PATCH 160/269] Translated using Weblate (Spanish) Currently translated at 74.7% (423 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/es/ --- _locales/es/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 32bf175d..cf9c7611 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -741,7 +741,7 @@ "message": "Agregar un nuevo evento de calendario" }, "prompt_get_calendar_event_full_text": { - "message": "Extrae todos los detalles relevantes necesarios para generar un evento de calendario a partir del siguiente texto. La información extraída debe incluir:\n- Título del evento\n- Fecha y hora de inicio (incluyendo zona horaria, si se especifica)\n- Fecha y hora de fin (incluyendo zona horaria, si se especifica)\n- Día completo (si se menciona)\n- Asistentes\nAsegúrate de que los datos estén formateados de manera clara y consistente para que puedan usarse directamente en la creación de un evento de calendario.\nSi hay referencias de tiempo relativas, considera que la fecha y hora del correo son \"{%mail_datetime%}\". Calcula la fecha y hora de inicio basándote en esta referencia. Si la fecha y hora de inicio calculadas son anteriores a \"{%current_datetime%}\", recalcula la fecha y hora de inicio usando \"{%current_datetime%}\" como base.\nSi la duración no se especifica, establécela en una hora.\nEstos son los asistentes: {%author%}, {%recipients%}, {%cc_list%}. Si están presentes, excluye mi dirección: {%account_email_address%}.\nSi no puedes obtener uno o más de los datos requeridos, responde con una cadena vacía.\nGenera una respuesta solo en formato JSON. No incluyas texto adicional ni explicaciones; proporciona únicamente el JSON. El formato a usar es:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Resumen del evento de calendario aquí\",\n\"forceAllDay\": false,\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nEste es el texto:\"{%mail_text_body_or_selected%}\"" + "message": "Extrae todos los detalles relevantes necesarios para generar un evento de calendario a partir del siguiente texto. La información extraída debe incluir:\n- Título del evento\n- Fecha y hora de inicio (incluyendo zona horaria, si se especifica)\n- Fecha y hora de fin (incluyendo zona horaria, si se especifica)\n- Día completo (si se menciona)\n- Asistentes\nAsegúrate de que los datos estén formateados de manera clara y consistente para que puedan usarse directamente en la creación de un evento de calendario.\nSi hay referencias de tiempo relativas, considera que la fecha y hora del correo son \"{%mail_datetime%}\". Calcula la fecha y hora de inicio basándote en esta referencia. Si la fecha y hora de inicio calculadas son anteriores a \"{%current_datetime%}\", recalcula la fecha y hora de inicio usando \"{%current_datetime%}\" como base.\nSi la duración no se especifica, establécela en una hora.\nEstos son los asistentes: {%author%}, {%recipients%}, {%cc_list%}. Si están presentes, excluye mi dirección: {%account_email_address%}.\nSi el evento es de día completo, endDate debe ser un día después de startDate con la hora establecida en \"T000000\".\nSi no puedes obtener uno o más de los datos requeridos, responde con una cadena vacía.\nGenera una respuesta solo en formato JSON. No incluyas texto adicional ni explicaciones; proporciona únicamente el JSON. El formato a usar es:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Resumen del evento de calendario aquí\",\n\"forceAllDay\": false,\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nEste es el texto:\"{%mail_text_body_or_selected%}\"" }, "prompt_get_task": { "message": "Agregar una nueva tarea" From 551e69747885aa58918dbfef52d8a5a642f1235a Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 9 Apr 2026 22:16:15 +0200 Subject: [PATCH 161/269] Translated using Weblate (French) Currently translated at 84.6% (479 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/fr/ --- _locales/fr/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 902a3d84..0eab3567 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -694,7 +694,7 @@ "message": "Ajouter un nouvel événement au calendrier" }, "prompt_get_calendar_event_full_text": { - "message": "Extrayez tous les détails pertinents nécessaires pour générer un événement de calendrier à partir du texte suivant. Les informations extraites doivent inclure :\n- Titre de l'événement\n- Date et heure de début (y compris le fuseau horaire, si spécifié)\n- Date et heure de fin (y compris le fuseau horaire, si spécifié)\n- Journée entière (si mentionnée)\n- Participants\nAssurez-vous que les données sont formatées clairement et de manière cohérente afin qu'elles puissent être utilisées directement pour créer un événement de calendrier.\nS'il y a des références temporelles relatives, considérez que la date et l'heure de l'email sont \"{%mail_datetime%}\". Calculez la date et l'heure de début sur cette base. Si la date et l'heure de début calculées sont antérieures à \"{%current_datetime%}\", recalculez-les en utilisant \"{%current_datetime%}\" comme base.\nSi la durée n'est pas spécifiée, définissez-la sur une heure.\nVoici les participants : {%author%}, {%recipients%}, {%cc_list%}. Si elle est présente, exclure mon adresse : {%account_email_address%}.\nSi vous n'êtes pas en mesure d'obtenir une ou plusieurs des informations requises, répondez avec une chaîne vide.\nGénérez une réponse uniquement au format JSON. N'incluez aucun texte ou explication supplémentaire ; fournissez uniquement le JSON. Voici le format à utiliser :\n{\n\"startDate\" : \"YYYYMMDDTHHMMSS\",\n\"endDate\" : \"YYYYMMDDTHHMMSS\",\n\"summary\" : \"Résumé de l'événement du calendrier ici\",\n\"forceAllDay\" : false\n\"attendees\" : [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nVoici le texte : \"{%mail_text_body_or_selected%}\"" + "message": "Extrayez tous les détails pertinents nécessaires pour générer un événement de calendrier à partir du texte suivant. Les informations extraites doivent inclure :\n- Titre de l'événement\n- Date et heure de début (y compris le fuseau horaire, si spécifié)\n- Date et heure de fin (y compris le fuseau horaire, si spécifié)\n- Journée entière (si mentionnée)\n- Participants\nAssurez-vous que les données sont formatées clairement et de manière cohérente afin qu'elles puissent être utilisées directement pour créer un événement de calendrier.\nS'il y a des références temporelles relatives, considérez que la date et l'heure de l'email sont \"{%mail_datetime%}\". Calculez la date et l'heure de début sur cette base. Si la date et l'heure de début calculées sont antérieures à \"{%current_datetime%}\", recalculez-les en utilisant \"{%current_datetime%}\" comme base.\nSi la durée n'est pas spécifiée, définissez-la sur une heure.\nVoici les participants : {%author%}, {%recipients%}, {%cc_list%}. Si elle est présente, exclure mon adresse : {%account_email_address%}.\nSi l'événement dure toute la journée, endDate doit être le lendemain de startDate avec l'heure définie sur \"T000000\".\nSi vous n'êtes pas en mesure d'obtenir une ou plusieurs des informations requises, répondez avec une chaîne vide.\nGénérez une réponse uniquement au format JSON. N'incluez aucun texte ou explication supplémentaire ; fournissez uniquement le JSON. Voici le format à utiliser :\n{\n\"startDate\" : \"YYYYMMDDTHHMMSS\",\n\"endDate\" : \"YYYYMMDDTHHMMSS\",\n\"summary\" : \"Résumé de l'événement du calendrier ici\",\n\"forceAllDay\" : false\n\"attendees\" : [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nVoici le texte : \"{%mail_text_body_or_selected%}\"" }, "prefs_OptionText_get_calendar_event": { "message": "Ajouter un nouvel événement au calendrier à partir du texte sélectionné" From 50dbc6a6d9112425d9d8a6373aefae4e893d4a6d Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 9 Apr 2026 22:19:10 +0200 Subject: [PATCH 162/269] Translated using Weblate (Swedish) Currently translated at 99.8% (565 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/sv/ --- _locales/sv/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index f56140d9..902c6d56 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -1276,7 +1276,7 @@ "message": "Citerad text i meddelandets brödtext" }, "prompt_get_calendar_event_full_text": { - "message": "Extrahera all relevant information som krävs för att generera en kalenderhändelse från följande text. Den extraherade informationen bör innehålla:\n- Händelsetitel\n- Startdatum och tid (inklusive tidszon, om angiven)\n- Slutdatum och tid (inklusive tidszon, om angiven)\n- Heldag (om angiven)\n- Deltagare\nSe till att informationen är tydligt och konsekvent formaterad så att den kan användas direkt för att skapa en kalenderhändelse.\nOm det finns relativa tidsreferenser, tänk på att datum och tid för e-postmeddelandet är \"{%mail_datetime%}\". Beräkna startdatum och tid baserat på denna referens. Om det beräknade startdatumet och tiden är tidigare än \"{%current_datetime%}\", beräkna om startdatumet och tiden med \"{%current_datetime%}\" som bas.\nOm varaktigheten inte anges, sätt den till en timme.\nDessa är deltagarna: {%author%}, {%recipients%}, {%cc_list%}. Om det finns, exkludera min adress: {%account_email_address%}.\nOm du inte kan få en eller flera av de obligatoriska uppgifterna, vänligen svara med en tom sträng.\nGenerera ett svar endast i JSON-format. Inkludera inte ytterligare text eller förklaringar; ange endast JSON. Här är formatet som ska användas:\n{\n\"startDate\": \"ÅÅÅÅMMDDTHHMMSS\",\n\"endDate\": \"ÅÅÅÅMMDDTHHMMSS\",\n\"summary\": \"Sammanfattning av kalenderhändelse här\",\n\"forceAllDay\": false,\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nHär är texten: \"{%mail_text_body_or_selected%}\"" + "message": "Extrahera all relevant information som krävs för att generera en kalenderhändelse från följande text. Den extraherade informationen bör innehålla:\n- Händelsetitel\n- Startdatum och tid (inklusive tidszon, om angiven)\n- Slutdatum och tid (inklusive tidszon, om angiven)\n- Heldag (om angiven)\n- Deltagare\nSe till att informationen är tydligt och konsekvent formaterad så att den kan användas direkt för att skapa en kalenderhändelse.\nOm det finns relativa tidsreferenser, tänk på att datum och tid för e-postmeddelandet är \"{%mail_datetime%}\". Beräkna startdatum och tid baserat på denna referens. Om det beräknade startdatumet och tiden är tidigare än \"{%current_datetime%}\", beräkna om startdatumet och tiden med \"{%current_datetime%}\" som bas.\nOm varaktigheten inte anges, sätt den till en timme.\nDessa är deltagarna: {%author%}, {%recipients%}, {%cc_list%}. Om det finns, exkludera min adress: {%account_email_address%}.\nOm händelsen är en heldagshändelse måste endDate vara en dag efter startDate med tiden inställd på \"T000000\".\nOm du inte kan få en eller flera av de obligatoriska uppgifterna, vänligen svara med en tom sträng.\nGenerera ett svar endast i JSON-format. Inkludera inte ytterligare text eller förklaringar; ange endast JSON. Här är formatet som ska användas:\n{\n\"startDate\": \"ÅÅÅÅMMDDTHHMMSS\",\n\"endDate\": \"ÅÅÅÅMMDDTHHMMSS\",\n\"summary\": \"Sammanfattning av kalenderhändelse här\",\n\"forceAllDay\": false,\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nHär är texten: \"{%mail_text_body_or_selected%}\"" }, "prompt_get_task_full_text": { "message": "Extrahera alla relevanta detaljer som krävs för att generera en uppgift från följande text. Den extraherade informationen bör innehålla:\n- Förfallodatum och tid (inklusive tidszon, om angiven)\n- Uppgiftssammanfattning\n- Initialt datum och tid (inklusive tidszon, om angiven)\nSe till att informationen är tydligt och konsekvent formaterad så att den kan användas direkt för att skapa en uppgift.\nOm det finns relativa tidsreferenser, tänk på att datum och tid för e-postmeddelandet är \"{%mail_datetime%}\". Beräkna startdatum och tid baserat på denna referens. Om det beräknade startdatumet och tiden är tidigare än \"{%current_datetime%}\", beräkna om startdatumet och tiden med \"{%current_datetime%}\" som bas.\nOm du inte kan få en eller flera av de obligatoriska uppgifterna, vänligen svara med en tom sträng.\nGenerera ett svar endast i JSON-format. Inkludera inte ytterligare text eller förklaringar; ange endast JSON. Här är formatet som ska användas:\n{\n\"InitialDate\": \"ÅÅÅÅMMDDTHHMMSS\",\n\"dueDate\": \"ÅÅÅÅMMDDTHHMMSS\",\n\"summary\": \"Uppgiftssammanfattning här\"\n}\nOm det inte finns någon information om datumen, ta bort dem.\nHär är texten: \"{%selected_text%}\"" From a3b8ccd588506f5ad2d1a8eaf18ef361b87036b2 Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 9 Apr 2026 22:18:43 +0200 Subject: [PATCH 163/269] Translated using Weblate (Russian) Currently translated at 68.5% (388 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/ru/ --- _locales/ru/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 9084297c..a5c2e345 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -732,7 +732,7 @@ "message": "Добавьте новое событие календаря" }, "prompt_get_calendar_event_full_text": { - "message": "Извлеките из следующего текста все необходимые сведения, необходимые для создания календарного события. Извлеченная информация должна включать:\n- Название события\n- Дата и время начала (включая часовой пояс, если он указан)\n- Дата и время окончания (включая часовой пояс, если он указан)\n- Полный день (если указано)\n- Участники\nУбедитесь, что данные отформатированы четко и последовательно, чтобы их можно было напрямую использовать для создания календарного события.\nЕсли есть относительные временные ссылки, считайте, что дата и время письма - это \"{%mail_datetime%}\". Рассчитайте дату и время начала на основе этой ссылки. Если вычисленные дата и время начала раньше, чем \"{%current_datetime%}\", пересчитайте дату и время начала, взяв за основу \"{%current_datetime%}\".\nЕсли продолжительность не указана, установите ее равной одному часу.\nК ним относятся: {%author%}, {%recipients%}, {%cc_list%}. Если присутствует, исключите мой адрес: {%account_email_address%}.\nЕсли вы не можете получить одну или несколько требуемых данных, ответьте пустой строкой.\nГенерируйте ответ только в формате JSON. Не включайте никаких дополнительных текстов или пояснений; предоставляйте только JSON. Вот формат, который следует использовать:\n{\n\"startDate\": \"ГГГГММДДДХММССС\",\n\"endDate\": \"ГГГГММДДДХММССС\",\n\"summary\": \"Здесь выводится краткое описание события календаря\",\n\"forceAllDay\": false,\n\"attendees\": [участник1@example.com,участник2@example.com,участник3@example.com]\n}\nВот текст: \"{%mail_text_body_or_selected%}\"" + "message": "Извлеките из следующего текста все необходимые сведения, необходимые для создания календарного события. Извлеченная информация должна включать:\n- Название события\n- Дата и время начала (включая часовой пояс, если он указан)\n- Дата и время окончания (включая часовой пояс, если он указан)\n- Полный день (если указано)\n- Участники\nУбедитесь, что данные отформатированы четко и последовательно, чтобы их можно было напрямую использовать для создания календарного события.\nЕсли есть относительные временные ссылки, считайте, что дата и время письма - это \"{%mail_datetime%}\". Рассчитайте дату и время начала на основе этой ссылки. Если вычисленные дата и время начала раньше, чем \"{%current_datetime%}\", пересчитайте дату и время начала, взяв за основу \"{%current_datetime%}\".\nЕсли продолжительность не указана, установите ее равной одному часу.\nК ним относятся: {%author%}, {%recipients%}, {%cc_list%}. Если присутствует, исключите мой адрес: {%account_email_address%}.\nЕсли это полнодневное событие, endDate должен быть на один день позже startDate с указанием времени \"T000000\".\nЕсли вы не можете получить одну или несколько требуемых данных, ответьте пустой строкой.\nГенерируйте ответ только в формате JSON. Не включайте никаких дополнительных текстов или пояснений; предоставляйте только JSON. Вот формат, который следует использовать:\n{\n\"startDate\": \"ГГГГММДДДХММССС\",\n\"endDate\": \"ГГГГММДДДХММССС\",\n\"summary\": \"Здесь выводится краткое описание события календаря\",\n\"forceAllDay\": false,\n\"attendees\": [участник1@example.com,участник2@example.com,участник3@example.com]\n}\nВот текст: \"{%mail_text_body_or_selected%}\"" }, "prompt_get_task": { "message": "Добавить новую задачу" From 678b1c0723b4e43b749e6c05f39826db7a3e446d Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 9 Apr 2026 22:13:34 +0200 Subject: [PATCH 164/269] Translated using Weblate (Italian) Currently translated at 84.6% (479 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/it/ --- _locales/it/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 451c86dc..c712561a 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -694,7 +694,7 @@ "message": "Aggiungi un nuovo evento al calendario" }, "prompt_get_calendar_event_full_text": { - "message": "Estrai tutti i dettagli rilevanti necessari per generare un evento del calendario dal seguente testo. Le informazioni estratte devono includere:\n- Titolo dell'evento\n- Data e ora di inizio (incluso il fuso orario, se specificato)\n- Data e ora di fine (incluso il fuso orario, se specificato)\n- Giornata intera (se menzionato)\n- I partecipanti\nAssicurati che i dati siano formattati in modo chiaro e coerente in modo che possano essere utilizzati direttamente per creare un evento del calendario.\nSe ci sono riferimenti temporali relativi, considera che la data e l'ora dell'email sono \"{%mail_datetime%}\". Calcola la data e l'ora di inizio in base a questo riferimento. Se la data e l'ora di inizio calcolate sono precedenti a \"{%current_datetime%}\", ricalcolale utilizzando \"{%current_datetime%}\" come base.\nSe la durata non è specificata, impostala a un'ora.\nQuesti sono i partecipanti: {%author%}, {%recipients%}, {%cc_list%}. Se presente, escludi il mio indirizzo: {%account_email_address%}.\nSe non riesci a ottenere una o più delle informazioni richieste, rispondi con una stringa vuota.\nGenera una risposta solo in formato JSON. Non includere testo o spiegazioni aggiuntive; fornisci solo il JSON. Ecco il formato da utilizzare:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Riassunto evento calendario qui\",\n\"forceAllDay\": false\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nEcco il testo: \"{%mail_text_body_or_selected%}\"" + "message": "Estrai tutti i dettagli rilevanti necessari per generare un evento del calendario dal seguente testo. Le informazioni estratte devono includere:\n- Titolo dell'evento\n- Data e ora di inizio (incluso il fuso orario, se specificato)\n- Data e ora di fine (incluso il fuso orario, se specificato)\n- Giornata intera (se menzionato)\n- I partecipanti\nAssicurati che i dati siano formattati in modo chiaro e coerente in modo che possano essere utilizzati direttamente per creare un evento del calendario.\nSe ci sono riferimenti temporali relativi, considera che la data e l'ora dell'email sono \"{%mail_datetime%}\". Calcola la data e l'ora di inizio in base a questo riferimento. Se la data e l'ora di inizio calcolate sono precedenti a \"{%current_datetime%}\", ricalcolale utilizzando \"{%current_datetime%}\" come base.\nSe la durata non è specificata, impostala a un'ora.\nQuesti sono i partecipanti: {%author%}, {%recipients%}, {%cc_list%}. Se presente, escludi il mio indirizzo: {%account_email_address%}.\nSe l'evento dura tutto il giorno, endDate deve essere il giorno successivo a startDate con l'orario impostato su \"T000000\".\nSe non riesci a ottenere una o più delle informazioni richieste, rispondi con una stringa vuota.\nGenera una risposta solo in formato JSON. Non includere testo o spiegazioni aggiuntive; fornisci solo il JSON. Ecco il formato da utilizzare:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Riassunto evento calendario qui\",\n\"forceAllDay\": false\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nEcco il testo: \"{%mail_text_body_or_selected%}\"" }, "prefs_OptionText_get_calendar_event": { "message": "Aggiungi un nuovo evento al calendario dal testo selezionato" From cba4eb2490985bafce24557c8fbc4545ca17ace2 Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 9 Apr 2026 22:18:19 +0200 Subject: [PATCH 165/269] Translated using Weblate (Portuguese (Brazil)) Currently translated at 47.3% (268 of 566 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/pt_BR/ --- _locales/pt-br/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/pt-br/messages.json b/_locales/pt-br/messages.json index b07a6a2e..8e149817 100644 --- a/_locales/pt-br/messages.json +++ b/_locales/pt-br/messages.json @@ -694,7 +694,7 @@ "message": "Adicionar um novo evento ao calendário" }, "prompt_get_calendar_event_full_text": { - "message": "Extraia todos os detalhes relevantes necessários para gerar um evento de calendário a partir do texto a seguir. As informações extraídas devem incluir:\n- Título do evento\n- Data e hora de início (incluindo fuso horário, se especificado)\n- Data e hora de término (incluindo fuso horário, se especificado)\n- Dia inteiro (se mencionado)\n- Participantes\nCertifique-se de que os dados estejam formatados de maneira clara e consistente para que possam ser usados diretamente para criar um evento de calendário.\nSe houver referências de tempo relativas, considere que a data e a hora do e-mail são \"{%mail_datetime%}\". Calcule a data e a hora de início com base nessa referência. Se a data e a hora de início calculadas forem anteriores a \"{%current_datetime%}\", recalcule a data e a hora de início usando \"{%current_datetime%}\" como base.\nSe a duração não for especificada, defina-a como uma hora.\nEstes são os participantes: {%author%}, {%recipients%}, {%cc_list%}. Se estiver presente, exclua meu endereço: {%account_email_address%}.\nSe você não conseguir obter uma ou mais informações necessárias, responda com uma string vazia.\nGere uma resposta apenas no formato JSON. Não inclua texto ou explicações adicionais; forneça apenas o JSON. Aqui está o formato a ser usado:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Resumo do evento do calendário aqui\",\n\"forceAllDay\": false\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nAqui está o texto: \"{%mail_text_body_or_selected%}\"" + "message": "Extraia todos os detalhes relevantes necessários para gerar um evento de calendário a partir do texto a seguir. As informações extraídas devem incluir:\n- Título do evento\n- Data e hora de início (incluindo fuso horário, se especificado)\n- Data e hora de término (incluindo fuso horário, se especificado)\n- Dia inteiro (se mencionado)\n- Participantes\nCertifique-se de que os dados estejam formatados de maneira clara e consistente para que possam ser usados diretamente para criar um evento de calendário.\nSe houver referências de tempo relativas, considere que a data e a hora do e-mail são \"{%mail_datetime%}\". Calcule a data e a hora de início com base nessa referência. Se a data e a hora de início calculadas forem anteriores a \"{%current_datetime%}\", recalcule a data e a hora de início usando \"{%current_datetime%}\" como base.\nSe a duração não for especificada, defina-a como uma hora.\nEstes são os participantes: {%author%}, {%recipients%}, {%cc_list%}. Se estiver presente, exclua meu endereço: {%account_email_address%}.\nSe o evento for de dia inteiro, o campo endDate deve ser o dia seguinte ao startDate com o horário definido como \"T000000\".\nSe você não conseguir obter uma ou mais informações necessárias, responda com uma string vazia.\nGere uma resposta apenas no formato JSON. Não inclua texto ou explicações adicionais; forneça apenas o JSON. Aqui está o formato a ser usado:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Resumo do evento do calendário aqui\",\n\"forceAllDay\": false\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nAqui está o texto: \"{%mail_text_body_or_selected%}\"" }, "prefs_OptionText_get_calendar_event": { "message": "Adicionar um novo evento ao calendário a partir do texto selecionado" From e6d6a095ac3309f34fea393ab3ba5591206a0aad Mon Sep 17 00:00:00 2001 From: mic Date: Thu, 9 Apr 2026 23:41:07 +0200 Subject: [PATCH 166/269] ChatGPT web models accelerators removed. see #692 --- _locales/en/messages.json | 4 -- js/mzta-utils.js | 41 ------------------- options/mzta-options.css | 4 -- options/mzta-options.js | 3 -- pages/_lib/connection-ui.js | 1 - .../mzta-custom-dataplaceholders.css | 15 +------ pages/customprompts/mzta-custom-prompts.css | 15 +------ pages/customprompts/mzta-custom-prompts.html | 1 - pages/customprompts/mzta-custom-prompts.js | 25 ----------- 9 files changed, 2 insertions(+), 107 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index d6fef07e..dc5e251a 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -776,10 +776,6 @@ "message": "It appears that at least one of your accounts is using the Owl for Exchange add-on. There is a known issue between Thunderbird and Owl, which is currently being addressed. At this time, you can use ThunderAI while composing emails, but not when reading them.", "description": "" }, - "prefs_OptionText_chatgpt_web_model_tooltip": { - "message": "Click on a value to set it.", - "description": "" - }, "prompt_reply_full_text": { "message": "Reply to the following email. Reply with only the needed text and with no extra comments or other text.", "description": "" diff --git a/js/mzta-utils.js b/js/mzta-utils.js index ce5dc9d4..003733f4 100644 --- a/js/mzta-utils.js +++ b/js/mzta-utils.js @@ -18,7 +18,6 @@ import { prefs_default, getDynamicSettingValue } from '../options/mzta-options-default.js'; const sparks_min = '1.2.0'; // Minimum version of ThunderAI-Sparks required for the add-on to work -export const ChatGPTWeb_models = ['gpt-5','gpt-5-instant','gpt-5-t-mini','gpt-5-thinking']; // List of models available in ChatGPT Web const MICZ_IT_LOCALIZED_LANGS = ['es', 'de', 'fr', 'it']; export const getMenuContextCompose = () => 'compose_action_menu'; @@ -339,46 +338,6 @@ export function getGPTWebModelString(model) { } } -export function getChatGPTWebModelsList_HTML(values, targetRowId) { - const rowElement = document.getElementById(targetRowId); - if (!rowElement) return; - - // Clears any existing td elements - rowElement.innerHTML = ''; - - // First TD: label - const labelTd = document.createElement('td'); - const label = document.createElement('i'); - label.className = 'small_info'; - const labelNobr = document.createElement('nobr'); - labelNobr.textContent = browser.i18n.getMessage("AllowedValues") + ":"; - label.appendChild(labelNobr); - labelTd.appendChild(label); - - // Second TD: values - const valuesTd = document.createElement('td'); - const valuesContainer = document.createElement('i'); - valuesContainer.className = 'small_info'; - - values.forEach(value => { - const nbspBefore = document.createTextNode(' \u00A0 '); // "   " - const valueNobr = document.createElement('nobr'); - valueNobr.className = 'conntype_chatgpt_web_option'; - valueNobr.textContent = value; - const nbspAfter = document.createTextNode(' \u00A0 '); - - valuesContainer.appendChild(nbspBefore); - valuesContainer.appendChild(valueNobr); - valuesContainer.appendChild(nbspAfter); - }); - - valuesTd.appendChild(valuesContainer); - - // Adds the td elements to the row - rowElement.appendChild(labelTd); - rowElement.appendChild(valuesTd); -} - export function openTab(url){ // check if the tab is already there browser.tabs.query({url: browser.runtime.getURL(url)}).then((tabs) => { diff --git a/options/mzta-options.css b/options/mzta-options.css index 1515e0a0..df76bdc0 100644 --- a/options/mzta-options.css +++ b/options/mzta-options.css @@ -39,10 +39,6 @@ table#miczPrefs td.nt{ border-top: none; } -tr#chatgpt_web_models_list td{ - border: none; -} - div#miczRelNotes{ position:absolute; right: 0px; diff --git a/options/mzta-options.js b/options/mzta-options.js index 0567e870..c54620ea 100644 --- a/options/mzta-options.js +++ b/options/mzta-options.js @@ -24,10 +24,8 @@ import { } from './mzta-options-default.js'; import { taLogger } from '../js/mzta-logger.js'; import { - ChatGPTWeb_models, checkSparksPresence, openTab, - getChatGPTWebModelsList_HTML, isAPIKeyValue, getConnectionType, setTomSelectBorder, @@ -436,7 +434,6 @@ document.addEventListener('DOMContentLoaded', async () => { openTab('/pages/get-task/mzta-get-task.html'); }); - getChatGPTWebModelsList_HTML(ChatGPTWeb_models, 'chatgpt_web_models_list'); document.querySelectorAll(".conntype_chatgpt_web_option").forEach(element => { element.addEventListener("click", () => { let el = document.getElementById("chatgpt_web_model"); diff --git a/pages/_lib/connection-ui.js b/pages/_lib/connection-ui.js index 5ff56863..80c8ec1c 100644 --- a/pages/_lib/connection-ui.js +++ b/pages/_lib/connection-ui.js @@ -104,7 +104,6 @@ export async function injectConnectionUI({
      - - - - + + + + + + + + + + + + `; const template = document.createElement('template'); From 2d61e730e577630c6b619fbeac3ebd3a931942af Mon Sep 17 00:00:00 2001 From: mic Date: Mon, 20 Apr 2026 23:06:40 +0200 Subject: [PATCH 243/269] fix translation json parse. see #247 --- mzta-background.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mzta-background.js b/mzta-background.js index f6ee06a9..156b3378 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -784,10 +784,10 @@ async function _generateTranslationForMessage(headerMessageId, tabId = null, opt let translatedSubject = ''; let translationStatus = ''; try { - const parsed = JSON.parse(aiResponse); + const parsed = extractJsonObject(aiResponse); translatedBody = parsed.body || ''; translatedSubject = parsed.subject || ''; - translationStatus = String(parsed.status || ''); + translationStatus = String(parsed.status ?? ''); } catch (e) { translatedBody = aiResponse; } From 8fd5825a8ebe333e35220bf7b8ec192e2539fd64 Mon Sep 17 00:00:00 2001 From: mic Date: Mon, 20 Apr 2026 23:10:39 +0200 Subject: [PATCH 244/269] version set to 4.1.0pre4 --- manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manifest.json b/manifest.json index ca185929..a14246d2 100644 --- a/manifest.json +++ b/manifest.json @@ -2,7 +2,7 @@ "manifest_version": 2, "name": "ThunderAI", "description": "__MSG_extensionDescription__", - "version": "4.1.0pre3", + "version": "4.1.0pre4", "author": "Mic (m@micz.it)", "homepage_url": "https://micz.it/thunderbird-addon-thunderai/", "browser_specific_settings": { From 58e4f5130ad6bf9012a2d6835723fbe47028ffc5 Mon Sep 17 00:00:00 2001 From: mic Date: Mon, 20 Apr 2026 23:26:14 +0200 Subject: [PATCH 245/269] prefs_OptionText_hide_thinking_info fixed --- _locales/en/messages.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 6f010bf0..3c03ddf2 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -725,11 +725,11 @@ "description": "" }, "prefs_OptionText_hide_thinking": { - "message": "Hide thinking output", + "message": "Collapse thinking block by default", "description": "" }, "prefs_OptionText_hide_thinking_info": { - "message": "If checked, reasoning/thinking output produced by the model is completely removed. If unchecked, it is shown in a collapsed block above the answer.", + "message": "Controls the initial state of the thinking block shown above the answer. If checked, the block is collapsed by default and can be opened with a click. If unchecked, the block is open by default and can be collapsed with a click. The thinking content is always preserved.", "description": "" }, "prefs_OptionText_thinking_summary": { From 1b8ebb756f5cfbc36fc4ae24a491d49195953a7b Mon Sep 17 00:00:00 2001 From: Mic Date: Mon, 20 Apr 2026 23:23:43 +0200 Subject: [PATCH 246/269] Translated using Weblate (Italian) Currently translated at 81.4% (482 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/it/ --- _locales/it/messages.json | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 3b075fca..476e223e 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -1446,5 +1446,55 @@ }, "prefs_chatgpt_win_position_info": { "message": "Lascia vuoto per usare la posizione di default." + }, + "show_in": { + "message": "Mostra in" + }, + "show_in_popup": { + "message": "Solo popup" + }, + "show_in_context": { + "message": "Solo menu contestuale" + }, + "show_in_both": { + "message": "Entrambi" + }, + "webchat_save_as_summary": { + "message": "Salva come Riepilogo" + }, + "prefs_storage_title": { + "message": "Archiviazione" + }, + "prefs_storage_info": { + "message": "L'archiviazione viene utilizzata per salvare informazioni sul punteggio di spam, i riepiloghi e le traduzioni di ogni messaggio." + }, + "prefs_storage_size": { + "message": "Dimensione archiviazione" + }, + "prefs_storage_clear_button": { + "message": "Svuota Archiviazione" + }, + "prefs_storage_clear_confirm": { + "message": "Sei sicuro di voler cancellare tutti i dati memorizzati (riepiloghi, segnalazioni spam, traduzioni)? L'azione è irreversibile." + }, + "prefs_storage_clear_done": { + "message": "$COUNT$ elementi cancellati.", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "prefsInfoDesc_7": { + "message": "Per utilizzare le API di Google Gemini, è necessario avere una chiave API (API Key) di Google Gemini e scegliere un modello." + }, + "prefsInfoDesc_8": { + "message": "Per utilizzare le API di Claude, è necessario avere una chiave API (API Key) di Anthropic Claude e scegliere un modello." + }, + "placeholder_mail_full_headers": { + "message": "Tutte le intestazioni email" + }, + "prefs_OptionText_hide_thinking": { + "message": "Nascondi l'output del ragionamento" } } From 2d924ce74caca30816f018fe9bd0a82cbd2a11bf Mon Sep 17 00:00:00 2001 From: Mic Date: Mon, 20 Apr 2026 23:34:57 +0200 Subject: [PATCH 247/269] Translated using Weblate (Greek) Currently translated at 75.6% (448 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/el/ --- _locales/el/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/el/messages.json b/_locales/el/messages.json index 20c840be..5826d0a4 100644 --- a/_locales/el/messages.json +++ b/_locales/el/messages.json @@ -1033,7 +1033,7 @@ "message": "Ταξινομήστε το ακόλουθο κείμενο με βάση την Ευγένεια, τη Ζεστασιά, την Τυπικότητα, την Επιθετικότητα και την Προσβλητικότητα, δίνοντας ένα ποσοστό για κάθε κατηγορία. Απαντήστε μόνο με την κατηγορία και βαθμολογήστε χωρίς επιπλέον σχόλια ή άλλο κείμενο." }, "prompt_translate_this_full_text": { - "message": "Μεταφράστε το ακόλουθο μήνυμα ηλεκτρονικού ταχυδρομείου στα" + "message": "Μεταφράστε το παρακάτω μήνυμα ηλεκτρονικού ταχυδρομείου στη γλώσσα {%thunderai_translate_lang%}.\n\nΚανόνες:\n- Μεταφράστε τόσο το θέμα όσο και το σώμα του μηνύματος.\n- Επιστρέψτε το αποτέλεσμα ως αντικείμενο JSON με τρία πεδία: \"subject\", \"body\" και \"status\".\n- Εάν η μετάφραση έχει ολοκληρωθεί, το status είναι ίσο με 1.\n- Εάν το μήνυμα είναι γραμμένο σε μία από αυτές τις γλώσσες \"{%thunderai_translate_exclude_lang%}\" ή στη γλώσσα {%thunderai_translate_lang%}, επιστρέψτε μια κενή συμβολοσειρά για το σώμα και το θέμα και ορίστε το status σε -1.\n- Μην προσθέτετε εξηγήσεις, σημειώσεις ή οποιοδήποτε κείμενο εκτός του JSON.\n\nΘέμα μηνύματος: {%mail_subject%}\n\nΣώμα μηνύματος: {%mail_html_body%}\n\nΔημιουργήστε μια απάντηση μόνο σε μορφή JSON. Η έξοδος πρέπει να είναι μόνο ένα αντικείμενο JSON. Ακολουθεί ένα παράδειγμα της μορφής JSON που πρέπει να χρησιμοποιηθεί:\n{\n\"subject\": \"subject translation\",\n\"body\": \"body translation\",\n\"status\": \"status result\"\n}" }, "prompt_this_full_text": { "message": "Απαντήστε μόνο με το απαραίτητο κείμενο και χωρίς επιπλέον σχόλια ή άλλο κείμενο." From d3086a7711b1d316d0b25c0ebff493d469fc81bc Mon Sep 17 00:00:00 2001 From: Mic Date: Mon, 20 Apr 2026 23:34:27 +0200 Subject: [PATCH 248/269] Translated using Weblate (Japanese) Currently translated at 69.5% (412 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/ja/ --- _locales/ja/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index d3e2bdbc..5267e2cb 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -558,7 +558,7 @@ "message": "以下のテキストを丁寧さ、温かさ、フォーマルさ、自己主張、攻撃性の観点から分類し、各カテゴリのパーセンテージを示してください。カテゴリとスコアのみで、余分なコメントや他のテキストなしで返信してください。" }, "prompt_translate_this_full_text": { - "message": "以下のメールを次の言語に翻訳してください:" + "message": "以下のメールを {%thunderai_translate_lang%} に翻訳してください。\n\nルール:\n- 件名と本文の両方を翻訳してください。\n- 結果は \"subject\"、\"body\"、\"status\" の3つのフィールドを持つ JSON オブジェクトとして返してください。\n- 翻訳が実行された場合、status は 1 になります。\n- メールが \"{%thunderai_translate_exclude_lang%}\" のいずれかの言語、または {%thunderai_translate_lang%} 言語で書かれている場合は、本文と件名に空の文字列を返し、status を -1 に設定してください。\n- JSON 以外の説明、メモ、テキストを追加しないでください。\n\nメール件名:{%mail_subject%}\n\nメール本文:{%mail_html_body%}\n\nJSON 形式のみで回答を生成してください。出力は JSON オブジェクトのみである必要があります。使用する JSON 形式の例は次のとおりです:\n{\n\"subject\": \"subject translation\",\n\"body\": \"body translation\",\n\"status\": \"status result\"\n}" }, "prompt_this_full_text": { "message": "必要なテキストのみで、余分なコメントや他のテキストなしで返信してください。" From 286d127bcd7110893419ae0deba36a417e51caf7 Mon Sep 17 00:00:00 2001 From: Mic Date: Mon, 20 Apr 2026 23:33:52 +0200 Subject: [PATCH 249/269] Translated using Weblate (French) Currently translated at 78.8% (467 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/fr/ --- _locales/fr/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index d873a6c3..02c85093 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -517,7 +517,7 @@ "message": "Classifiez le texte suivant en termes de politesse, chaleur, formalité, assertivité, caractère offensant en donnant un pourcentage pour chaque catégorie. Répondez uniquement avec la catégorie et le score, sans commentaires ou autre texte." }, "prompt_translate_this_full_text": { - "message": "Traduisez le courriel suivant en" + "message": "Traduisez l'e-mail ci-dessous en {%thunderai_translate_lang%}.\n\nRègles :\n- Traduisez à la fois l'objet et le corps du message.\n- Renvoyez le résultat sous forme d'objet JSON avec trois champs : \"subject\", \"body\" et \"status\".\n- Si la traduction a été effectuée, le statut est égal à 1.\n- Si l'e-mail est écrit dans l'une de ces langues \"{%thunderai_translate_exclude_lang%}\" ou dans la langue {%thunderai_translate_lang%}, renvoyez une chaîne vide pour le corps et l'objet et réglez le statut sur -1.\n- N'ajoutez pas d'explications, de notes ou de texte en dehors du JSON.\n\nObjet du mail : {%mail_subject%}\n\nCorps du mail : {%mail_html_body%}\n\nGénérez une réponse au format JSON uniquement. La sortie doit être exclusivement un objet JSON. Voici un exemple du format JSON à utiliser :\n{\n\"subject\" : \"subject translation\",\n\"body\" : \"body translation\",\n\"status\" : \"status result\"\n}" }, "prompt_this_full_text": { "message": "Répondez uniquement avec le texte nécessaire, sans commentaires ou autre texte." From 2f7055db78e4a8f5e1ad66ca7d318b984e835371 Mon Sep 17 00:00:00 2001 From: Mic Date: Mon, 20 Apr 2026 23:28:50 +0200 Subject: [PATCH 250/269] Translated using Weblate (Italian) Currently translated at 81.4% (482 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/it/ --- _locales/it/messages.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 476e223e..69b4d830 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -517,7 +517,7 @@ "message": "Classifica il seguente testo in termini di Cortesia, Calore, Formalità, Assertività, Offensività, indicando una percentuale per ciascuna categoria. Rispondi solo con la categoria e il punteggio, senza commenti aggiuntivi o altro testo." }, "prompt_translate_this_full_text": { - "message": "Traduci la seguente email in" + "message": "Traduci l'email qui sotto in italiano.\n\nRegole:\n- Traduci sia l'oggetto che il corpo dell'email.\n- Restituisci il risultato come un oggetto JSON con tre campi: \"subject\", \"body\" e \"status\".\n- Se la traduzione viene effettuata, lo stato è uguale a 1.\n- Se l'email è scritta in una di queste lingue \"{%thunderai_translate_exclude_lang%}\" o nella lingua {%thunderai_translate_lang%}, restituisci una stringa vuota per il corpo e l'oggetto e imposta lo stato a 1.\n- Non aggiungere spiegazioni, note o alcun testo al di fuori del JSON.\n\nOggetto dell'email: {%mail_subject%}\n\nCorpo dell'email: {%mail_html_body%}\n\nGenera una risposta esclusivamente in formato JSON. L'output deve essere solo un oggetto JSON. Ecco un esempio del formato JSON da utilizzare:\n{\n\"subject\": \"traduzione oggetto\",\n\"body\": \"traduzione corpo\",\n\"status\": \"risultato stato\"\n}" }, "prompt_this_full_text": { "message": "Rispondi solo con il testo necessario, senza commenti aggiuntivi o altro testo." @@ -1496,5 +1496,8 @@ }, "prefs_OptionText_hide_thinking": { "message": "Nascondi l'output del ragionamento" + }, + "prefs_OptionText_thinking_summary": { + "message": "Ragionamento" } } From 9f3671e13f0b2a1b8e800de01a9f43a81104e5d6 Mon Sep 17 00:00:00 2001 From: Mic Date: Mon, 20 Apr 2026 23:35:57 +0200 Subject: [PATCH 251/269] Translated using Weblate (Portuguese (Brazil)) Currently translated at 44.0% (261 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/pt_BR/ --- _locales/pt-br/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/pt-br/messages.json b/_locales/pt-br/messages.json index 746ecafc..6bbceed3 100644 --- a/_locales/pt-br/messages.json +++ b/_locales/pt-br/messages.json @@ -517,7 +517,7 @@ "message": "Classifique o texto a seguir em termos de Educação, Calor, Formalidade, Assertividade e Ofensividade, atribuindo uma porcentagem para cada categoria. Responda apenas com as categorias e as pontuações, sem comentários adicionais ou outros textos." }, "prompt_translate_this_full_text": { - "message": "Traduza o e-mail a seguir para" + "message": "Traduza o e-mail abaixo para o idioma {%thunderai_translate_lang%}.\n\nRegras:\n- Traduza tanto o assunto quanto o corpo do e-mail.\n- Retorne o resultado como um objeto JSON com três campos: \"subject\", \"body\" e \"status\".\n- Se a tradução for realizada, o status é igual a 1.\n- Se o e-mail estiver escrito em um destes idiomas \"{%thunderai_translate_exclude_lang%}\" ou no idioma {%thunderai_translate_lang%}, retorne uma string vazia para o corpo e o assunto e defina o status como -1.\n- Não adicione explicações, notas ou qualquer texto fora do JSON.\n\nAssunto do e-mail: {%mail_subject%}\n\nCorpo do e-mail: {%mail_html_body%}\n\nGere uma resposta apenas em formato JSON. A saída deve ser apenas um objeto JSON. Aqui está um exemplo do formato JSON a ser usado:\n{\n\"subject\": \"subject translation\",\n\"body\": \"body translation\",\n\"status\": \"status result\"\n}" }, "prompt_this_full_text": { "message": "Responda apenas com o texto necessário e sem comentários adicionais ou outros textos." From 0eaa27f8aa73185333a7bead2ebcd959a49ec83a Mon Sep 17 00:00:00 2001 From: Mic Date: Mon, 20 Apr 2026 23:36:25 +0200 Subject: [PATCH 252/269] Translated using Weblate (Russian) Currently translated at 63.8% (378 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/ru/ --- _locales/ru/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index bf87b805..43e8f7d0 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -546,7 +546,7 @@ "message": "Классифицируйте следующий текст с точки зрения вежливости, теплоты, формальности, настойчивости, оскорбительности, указав процентное соотношение для каждой категории. В ответе укажите только категорию и оценку, без доп. комментариев или др. текста." }, "prompt_translate_this_full_text": { - "message": "Переведите следующее письмо на" + "message": "Переведите указанное ниже электронное письмо на язык {%thunderai_translate_lang%}.\n\nПравила:\n- Переведите и тему, и текст письма.\n- Верните результат в виде JSON-объекта с тремя полями: \"subject\", \"body\" и \"status\".\n- Если перевод выполнен, статус равен 1.\n- Если письмо написано на одном из этих языков \"{%thunderai_translate_exclude_lang%}\" или на языке {%thunderai_translate_lang%}, верните пустую строку для тела и темы и установите статус -1.\n- Не добавляйте никаких объяснений, заметок или любого текста вне JSON.\n\nТема письма: {%mail_subject%}\n\nТекст письма: {%mail_html_body%}\n\nСгенерируйте ответ только в формате JSON. На выходе должен быть только JSON-объект. Вот пример формата JSON, который необходимо использовать:\n{\n\"subject\": \"subject translation\",\n\"body\": \"body translation\",\n\"status\": \"status result\"\n}" }, "prompt_this_full_text": { "message": "Отвечайте только нужным текстом, без лишних комментариев и прочего." From 6f0308bbf7e29bf741604b29dd05568177812266 Mon Sep 17 00:00:00 2001 From: Mic Date: Mon, 20 Apr 2026 23:35:25 +0200 Subject: [PATCH 253/269] Translated using Weblate (Polish) Currently translated at 46.1% (273 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/pl/ --- _locales/pl/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index d831b652..310b72ff 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -517,7 +517,7 @@ "message": "Sklasyfikuj poniższy tekst pod względem uprzejmości, serdeczności, formalności, stanowczości, obraźliwości, podając procent dla każdej kategorii. Odpowiedz wyłącznie kategorią i wynikiem, bez dodatkowych komentarzy ani innego tekstu." }, "prompt_translate_this_full_text": { - "message": "Przetłumacz poniższy e-mail na" + "message": "Przetłumacz poniższą wiadomość e-mail na język {%thunderai_translate_lang%}.\n\nZasady:\n- Przetłumacz zarówno temat, jak i treść wiadomości.\n- Zwróć wynik jako obiekt JSON z trzema polami: \"subject\", \"body\" i \"status\".\n- Jeśli tłumaczenie zostało wykonane, status wynosi 1.\n- Jeśli wiadomość e-mail jest napisana w jednym z tych języków \"{%thunderai_translate_exclude_lang%}\" lub w języku {%thunderai_translate_lang%}, zwróć pusty ciąg znaków dla treści i tematu oraz ustaw status na -1.\n- Nie dodawaj wyjaśnień, notatek ani żadnego tekstu poza formatem JSON.\n\nTemat wiadomości: {%mail_subject%}\n\nTreść wiadomości: {%mail_html_body%}\n\nWygeneruj odpowiedź wyłącznie w formacie JSON. Wynikiem powinien być tylko obiekt JSON. Oto przykład formatu JSON, którego należy użyć:\n{\n\"subject\": \"subject translation\",\n\"body\": \"body translation\",\n\"status\": \"status result\"\n}" }, "prompt_this_full_text": { "message": "Odpowiedz wyłącznie wymaganym tekstem, bez dodatkowych komentarzy ani innego tekstu." From cccdc8442accb06d455516df5477e760e75ba2c4 Mon Sep 17 00:00:00 2001 From: Mic Date: Mon, 20 Apr 2026 23:32:28 +0200 Subject: [PATCH 254/269] Translated using Weblate (Croatian) Currently translated at 44.5% (264 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/hr/ --- _locales/hr/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/hr/messages.json b/_locales/hr/messages.json index 15247e29..5081da83 100644 --- a/_locales/hr/messages.json +++ b/_locales/hr/messages.json @@ -517,7 +517,7 @@ "message": "Klasificiraj sljedeći tekst u smislu ljubaznosti, topline, formalnosti, asertivnosti, uvredljivosti dajući postotak za svaku kategoriju. Odgovori samo kategorijom i ocijeni bez dodatnih komentara ili drugog teksta." }, "prompt_translate_this_full_text": { - "message": "Prevedi sljedeću e-poruku na" + "message": "Prevedite donju e-poštu na {%thunderai_translate_lang%}.\n\nPravila:\n- Prevedite i predmet i tijelo e-pošte.\n- Vratite rezultat kao JSON objekt s tri polja: \"subject\", \"body\" i \"status\".\n- Ako je prijevod izvršen, status je jedan 1.\n- Ako je e-pošta napisana na jednom od ovih jezika \"{%thunderai_translate_exclude_lang%}\" ili na jeziku {%thunderai_translate_lang%}, vratite prazan niz za tijelo i predmet i postavite status na -1.\n- Nemojte dodavati objašnjenja, bilješke ili bilo kakav tekst izvan JSON-a.\n\nPredmet e-pošte: {%mail_subject%}\n\nTijelo e-pošte: {%mail_html_body%}\n\nGenerirajte odgovor isključivo u JSON formatu. Izlaz treba biti samo JSON objekt. Evo primjera JSON formata koji treba koristiti:\n{\n\"subject\": \"subject translation\",\n\"body\": \"body translation\",\n\"status\": \"status result\"\n}" }, "prompt_this_full_text": { "message": "Odgovori samo s potrebnim tekstom i bez dodatnih komentara ili drugog teksta." From 71848558cd971bb5b11c6163a8cfdd3414e95273 Mon Sep 17 00:00:00 2001 From: Mic Date: Mon, 20 Apr 2026 23:36:49 +0200 Subject: [PATCH 255/269] Translated using Weblate (German) Currently translated at 78.8% (467 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/de/ --- _locales/de/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/de/messages.json b/_locales/de/messages.json index cfcd4a3c..7098a10c 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -517,7 +517,7 @@ "message": "Klassifizieren Sie den folgenden Text nach Höflichkeit, Wärme, Formalität, Bestimmtheit und Anstößigkeit und geben Sie einen Prozentsatz für jede Kategorie an. Antworten Sie nur mit der Kategorie und der Punktzahl ohne zusätzliche Kommentare oder andere Texte." }, "prompt_translate_this_full_text": { - "message": "Übersetzen Sie die folgende E-Mail in" + "message": "Übersetzen Sie die unten stehende E-Mail in die Sprache {%thunderai_translate_lang%}.\n\nRegeln:\n- Übersetzen Sie sowohl den Betreff als auch den Textkörper.\n- Geben Sie das Ergebnis als JSON-Objekt mit drei Feldern zurück: \"subject\", \"body\" und \"status\".\n- Wenn die Übersetzung erstellt wurde, ist der Status gleich 1.\n- Wenn die E-Mail in einer dieser Sprachen \"{%thunderai_translate_exclude_lang%}\" oder in der Sprache {%thunderai_translate_lang%} verfasst ist, geben Sie eine leere Zeichenfolge für den Textkörper und den Betreff zurück und setzen Sie den Status auf -1.\n- Fügen Sie keine Erklärungen, Notizen oder Texte außerhalb des JSON-Objekts hinzu.\n\nE-Mail-Betreff: {%mail_subject%}\n\nE-Mail-Textkörper: {%mail_html_body%}\n\nGenerieren Sie die Antwort ausschließlich im JSON-Format. Die Ausgabe darf nur ein JSON-Objekt sein. Hier ist ein Beispiel für das zu verwendende JSON-Format:\n{\n\"subject\": \"subject translation\",\n\"body\": \"body translation\",\n\"status\": \"status result\"\n}" }, "prompt_this_full_text": { "message": "Antworten Sie nur mit dem benötigten Text und ohne zusätzliche Kommentare oder andere Texte." From 48f9002f879a8777224fc167ebcb62550f448b98 Mon Sep 17 00:00:00 2001 From: Mic Date: Mon, 20 Apr 2026 23:31:04 +0200 Subject: [PATCH 256/269] Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 64.6% (383 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/zh_Hant/ --- _locales/zh_Hant/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/zh_Hant/messages.json b/_locales/zh_Hant/messages.json index a0a123f4..1e1ae265 100644 --- a/_locales/zh_Hant/messages.json +++ b/_locales/zh_Hant/messages.json @@ -656,7 +656,7 @@ "message": "回覆以下郵件。僅回覆所需內容,不要提供任何註解或其他文字。" }, "prompt_translate_this_full_text": { - "message": "將以下電子郵件翻譯成" + "message": "請將以下電子郵件翻譯成 **{%thunderai_translate_lang%}**。\n\n**規則:**\n- 同時翻譯主題(Subject)與正文(Body)。\n- 以 JSON 物件格式回傳結果,包含三個欄位:\"subject\"、\"body\" 以及 \"status\"。\n- 如果完成翻譯,status 等於 1。\n- 如果郵件是以 \"{%thunderai_translate_exclude_lang%}\" 其中之一的語言或 {%thunderai_translate_lang%} 語言編寫,請將 body 和 subject 設為空字串,並將 status 設為 -1。\n- 請勿在 JSON 之外添加任何說明、備註或文字。\n\n郵件主題:{%mail_subject%}\n\n郵件正文:{%mail_html_body%}\n\n請僅以 JSON 格式生成回應。輸出應僅包含一個 JSON 物件。以下是要使用的 JSON 格式範例:\n{\n\"subject\": \"subject translation\",\n\"body\": \"body translation\",\n\"status\": \"status result\"\n}" }, "prompt_rewrite_full_text": { "message": "請重寫以下文字,使其更有禮貌。僅回覆重寫的文字,不要提供任何額外的註解或其他文字。" From 4f620e2cce95cd02b285b4be445524abca55cf78 Mon Sep 17 00:00:00 2001 From: bittin1ddc447d824349b2 Date: Tue, 21 Apr 2026 10:18:41 +0200 Subject: [PATCH 257/269] Translated using Weblate (Swedish) Currently translated at 100.0% (592 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/sv/ --- _locales/sv/messages.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index 4bb6904e..197587b0 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -1776,5 +1776,23 @@ }, "menu_order_icon_none": { "message": "(ingen)" + }, + "prefs_OptionText_hide_thinking": { + "message": "Fäll in tankeblock som standard" + }, + "prefs_OptionText_hide_thinking_info": { + "message": "Styr det initiala tillståndet för tankeblocket som visas ovanför svaret. Om markerat är blocket hopfällt som standard och kan öppnas med ett klick. Om det avmarkeras är blocket öppet som standard och kan hopfällas med ett klick. Tankeinnehållet bevaras alltid." + }, + "prefs_OptionText_thinking_summary": { + "message": "Tänker" + }, + "prefs_OptionText_anthropic_extended_thinking_budget": { + "message": "Utökad tankebudget (tokens)" + }, + "prefs_OptionText_anthropic_extended_thinking_budget_Info": { + "message": "Maximalt antal tokens som modellen kan spendera på utökat tänkande. Ställ in på 0 för att inaktivera utökat tänkande. När det är aktiverat ignoreras temperaturvärdet av Claude API." + }, + "generic_error_dismiss": { + "message": "Avfärda" } } From e7ec1476d7ef4716a1da70cd795003304d837592 Mon Sep 17 00:00:00 2001 From: Christos Kanotidis Date: Thu, 23 Apr 2026 21:36:41 +0200 Subject: [PATCH 258/269] Translated using Weblate (Greek) Currently translated at 99.4% (589 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/el/ --- _locales/el/messages.json | 426 +++++++++++++++++++++++++++++++++++++- 1 file changed, 419 insertions(+), 7 deletions(-) diff --git a/_locales/el/messages.json b/_locales/el/messages.json index 5826d0a4..3fd77b71 100644 --- a/_locales/el/messages.json +++ b/_locales/el/messages.json @@ -27,7 +27,7 @@ "message": "Ταξινόμησε" }, "prompt_translate_this": { - "message": "Μετάφρασε το" + "message": "Μετάφρασε" }, "prompt_this": { "message": "Μήνυμα για" @@ -705,7 +705,7 @@ "message": "Τρέχον κείμενο προτροπής" }, "prompt_spamfilter": { - "message": "Εντοπισμός ανεπιθύμητων μηνυμάτων ηλεκτρονικού ταχυδρομείου" + "message": "Ανάλυση για ανεπιθύμητα μηνύματα" }, "prompt_spamfilter_full_text": { "message": "Αναλύστε το ακόλουθο email και προσδιορίστε εάν είναι spam ή όχι. Λάβετε υπόψη παράγοντες όπως ύποπτες λέξεις-κλειδιά, υπερβολική διαφημιστική γλώσσα, παραπλανητικές γραμμές θέματος, αιτήματα για προσωπικά στοιχεία και ασυνήθιστες διευθύνσεις αποστολέα.\nΔώστε μια τιμή από 0 (όχι spam) έως 100 (spam) και μια εξήγηση που δεν υπερβαίνει τις 10 λέξεις.\nΣε περίπτωση που λείπουν δεδομένα μηνύματος, ορίστε την τιμή σε 0 (όχι spam) και δώστε τον λόγο.\nΔημιουργήστε μια απάντηση μόνο σε μορφή JSON. Μην συμπεριλάβετε κανένα επιπλέον κείμενο ή εξήγηση. Δώστε μόνο το JSON. Ακολουθεί η μορφή που θα χρησιμοποιηθεί:\n{\n\"explanation\": \"Σύντομη εξήγηση του συλλογισμού σας\",\n\"spamValue\": <ακέραιος αριθμός από 0 έως 100>\n}\nΕδώ βρίσκονται οι πληροφορίες του email:\nΑποστολέας: \"{%author%}\"\nΘέμα: \"{%mail_subject%}\"\nΣώμα Html: \"{%mail_html_body%}\"" @@ -732,7 +732,7 @@ "message": "Το όριο ανεπιθύμητης αλληλογραφίας είναι μηδέν! Θα επισημάνετε όλα τα μηνύματα ως ανεπιθύμητα!" }, "spamfilter_no_reports": { - "message": "Δεν έχουν ελεγχθεί ακόμη μηνύματα για ανεπιθύμητα. Εδώ θα βρείτε μια λίστα με τις τελευταίες 100 αναφορές ανεπιθύμητων μηνυμάτων μόνο για την τρέχουσα συνεδρία." + "message": "Δεν έχουν ελεγχθεί ακόμη μηνύματα για ανεπιθύμητα. Εδώ θα βρείτε μια λίστα με τις τελευταίες 100 αναφορές ανεπιθύμητων μηνυμάτων." }, "SpamReport_Title": { "message": "Αναφορές φίλτρου ανεπιθύμητης αλληλογραφίας" @@ -1045,7 +1045,7 @@ "message": "Εάν είναι επιλεγμένο, θα συμπεριληφθεί ένα στοιχείο στο μενού για την εφαρμογή ετικετών σε μηνύματα ηλεκτρονικού ταχυδρομείου." }, "prompt_add_tags": { - "message": "Προσθήκη ετικετών σε αυτό το μήνυμα ηλεκτρονικού ταχυδρομείου" + "message": "Προσθήκη ετικετών" }, "prompt_add_tags_full_text": { "message": "Αναλύστε το ακόλουθο κείμενο email και δημιουργήστε έναν πίνακα ετικετών JSON που συνοψίζει το περιεχόμενό του. Χρησιμοποιήστε θέματα, βασικά θέματα και σχετικές περιγραφές ως ετικέτες. Βεβαιωθείτε ότι οι ετικέτες είναι συνοπτικές και σχετικές με το περιεχόμενο του email.\nΚείμενο email: {%mail_text_body%}\nΛάβετε υπόψη τις ακόλουθες λεπτομέρειες για το περιεχόμενο:\n- Αποστολέας: {%author%}\n- Παραλήπτες: {%recipients%}\n- Λίστα CC: {%cc_list%}\n- Θέμα email: {%mail_subject%}\nΒασίστε τις ετικέτες σας στο κείμενο και το περιεχόμενο του email, αγνοώντας περιττές πληροφορίες ή ασήμαντες λεπτομέρειες.\nΔημιουργήστε μια απάντηση μόνο σε μορφή JSON. Η έξοδος θα πρέπει να είναι μόνο ένας πίνακας ετικετών JSON χωρίς κανένα επιπλέον σχόλιο ή κείμενο. Ακολουθεί ένα παράδειγμα της μορφής JSON που θα χρησιμοποιηθεί:\n{\n\"tags\": [\"tag1\", \"tag2\", \"tag3\", \"tag4\", \"tag5\"]\n}" @@ -1291,13 +1291,13 @@ "message": "Εμφάνιση ενός επιπλέον στοιχείου μενού για τη δημιουργία συμβάντων ημερολογίου από περιεχόμενο κειμένου στο πρόχειρο." }, "Summarize_prompt_prefs_title": { - "message": "Προσθήκη επιλογών σύνοψης" + "message": "Επιλογές σύνοψης" }, "prompt_summarize": { - "message": "Συνοψίστε αυτό το μήνυμα ηλεκτρονικού ταχυδρομείου ή αυτά τα μηνύματα ηλεκτρονικού ταχυδρομείου" + "message": "Συνοψίστε" }, "prompt_summarize_full_text": { - "message": "Είστε βοηθός που συνοψίζει συνομιλίες μέσω email. \nΔεδομένου ενός νήματος email, δημιουργήστε μια συνοπτική και ακριβή περίληψη που να καταγράφει: \n- Το κύριο θέμα ή τον σκοπό της συζήτησης \n- Βασικές αποφάσεις, συμπεράσματα ή συμφωνίες \n- Σημαντικές ερωτήσεις, αιτήματα ή ενέργειες \n- Ποιος είναι υπεύθυνος για κάθε ενέργεια (εάν αναφέρεται) \nΠαραλείψτε χαιρετισμούς, υπογραφές, παρατιθέμενο κείμενο και περιττές ανταλλαγές απόψεων. Μην προσθέτετε υποθέσεις ή πληροφορίες που δεν υπάρχουν στα email. \nΓράψτε την περίληψη σε σαφή, ουδέτερη γλώσσα, κατάλληλη για έναν πολυάσχολο επαγγελματία." + "message": "Δώστε μια συνοπτική περίληψη των ακόλουθων μηνυμάτων ηλεκτρονικού ταχυδρομείου. Η περίληψη πρέπει να έχει μέγιστο μήκος 3-5 προτάσεις και να καταγράφει τα κύρια σημεία. Γράψτε σε απλές παραγράφους χωρίς κουκκίδες, λίστες ή μορφοποίηση markdown:\n\n" }, "prompt_summarize_email_template": { "message": "Σύνοψη προτύπου email" @@ -1382,5 +1382,417 @@ }, "copy_text": { "message": "αντιγραφή" + }, + "show_in": { + "message": "Εμφάνιση σε" + }, + "show_in_popup": { + "message": "Μόνο αναδυόμενο παράθυρο" + }, + "show_in_context": { + "message": "Μόνο μενού περιβάλλοντος" + }, + "show_in_both": { + "message": "Και τα δύο" + }, + "webchat_save_as_summary": { + "message": "Αποθήκευση ως Σύνοψη" + }, + "prefs_storage_title": { + "message": "Αποθήκευση" + }, + "prefs_storage_info": { + "message": "Ο χώρος αποθήκευσης χρησιμοποιείται για την αποθήκευση πληροφοριών σχετικά με τη βαθμολογία ανεπιθύμητης αλληλογραφίας, τις περιλήψεις και τις μεταφράσεις κάθε μηνύματος." + }, + "prefs_storage_size": { + "message": "Μέγεθος αποθήκευσης" + }, + "prefs_storage_clear_button": { + "message": "Εκκαθάριση χώρου αποθήκευσης" + }, + "prefs_storage_clear_confirm": { + "message": "Είστε βέβαιοι ότι θέλετε να διαγράψετε όλα τα αποθηκευμένα δεδομένα (περιλήψεις, αναφορές ανεπιθύμητων μηνυμάτων, μεταφράσεις); Αυτή η ενέργεια δεν μπορεί να αναιρεθεί." + }, + "prefs_storage_clear_done": { + "message": "$COUNT$ εγγραφές καταργήθηκαν.", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "prefsInfoDesc_7": { + "message": "Για να χρησιμοποιήσετε το Google Gemini API, χρειάζεστε ένα κλειδί Google Gemini API και πρέπει να επιλέξετε ένα μοντέλο." + }, + "prefsInfoDesc_8": { + "message": "Για να χρησιμοποιήσετε το Claude API, χρειάζεστε ένα Anthropic Claude API Key και πρέπει να επιλέξετε ένα μοντέλο." + }, + "placeholder_mail_full_headers": { + "message": "Όλες οι κεφαλίδες μηνυμάτων" + }, + "prefs_OptionText_hide_thinking": { + "message": "Σύμπτυξη μπλοκ σκέψης από προεπιλογή" + }, + "prefs_OptionText_hide_thinking_info": { + "message": "Ελέγχει την αρχική κατάσταση του μπλοκ σκέψης που εμφανίζεται πάνω από την απάντηση. Εάν είναι επιλεγμένο, το μπλοκ συμπτύσσεται από προεπιλογή και μπορεί να ανοιχτεί με ένα κλικ. Εάν δεν είναι επιλεγμένο, το μπλοκ είναι ανοιχτό από προεπιλογή και μπορεί να συμπτυχθεί με ένα κλικ. Το περιεχόμενο της σκέψης διατηρείται πάντα." + }, + "prefs_OptionText_thinking_summary": { + "message": "Σκέψη" + }, + "prefs_OptionText_chatgpt_web_load_wait_time": { + "message": "Χρόνος αναμονής για φόρτωση σελίδας" + }, + "prefs_OptionText_chatgpt_web_load_wait_time_info": { + "message": "Χρόνος σε χιλιοστά του δευτερολέπτου για την αναμονή φόρτωσης της σελίδας ChatGPT πριν από τη φόρτωση του πρόσθετου περιεχομένου. Η προεπιλογή είναι 1000ms. Εάν έχει οριστεί ένα προσαρμοσμένο GPT ή έργο, θα προστεθούν επιπλέον 1000ms σε αυτήν την τιμή." + }, + "prefs_doc_title": { + "message": "Documentation" + }, + "prefs_doc_setup_guide": { + "message": "Οδηγοί εγκατάστασης" + }, + "prefs_doc_custom_prompt_tutorial": { + "message": "Εκπαιδευτικό σεμινάριο προσαρμοσμένης προτροπής" + }, + "prefs_doc_open_welcome": { + "message": "Άνοιγμα της σελίδας υποδοχής" + }, + "placeholder_thunderai_translate_lang": { + "message": "Η γλώσσα που θα χρησιμοποιηθεί στις μεταφράσεις αλληλογραφίας." + }, + "placeholder_thunderai_translate_exclude_lang": { + "message": "Ο κώδικας γλώσσας δεν θα μεταφράζεται όταν βρεθεί." + }, + "SpamFilter_skip_addresses_title": { + "message": "Λίστα παράλειψης διεύθυνσης ηλεκτρονικού ταχυδρομείου" + }, + "SpamFilter_skip_addresses_infoline": { + "message": "Τα email από αυτές τις διευθύνσεις δεν θα αποστέλλονται στην Τεχνητή Νοημοσύνη για φιλτράρισμα ανεπιθύμητης αλληλογραφίας." + }, + "SpamFilter_skip_addresses_infoline2": { + "message": "Προσθέστε μία διεύθυνση ηλεκτρονικού ταχυδρομείου ανά γραμμή ή διαχωρισμένη με κόμμα." + }, + "spamfilter_skip_addresses_explanation": { + "message": "Ο αποστολέας βρίσκεται στη λίστα παράλειψης ανεπιθύμητης αλληλογραφίας της διεύθυνσης ηλεκτρονικού ταχυδρομείου." + }, + "prefs_OptionText_spamfilter_skip_addressbook": { + "message": "Παράλειψη διευθύνσεων βιβλίων διευθύνσεων" + }, + "prefs_OptionText_spamfilter_skip_addressbook_Info": { + "message": "Εάν είναι επιλεγμένο, τα email από αποστολείς στα βιβλία διευθύνσεών σας δεν θα αποστέλλονται στην Τεχνητή Νοημοσύνη για φιλτράρισμα ανεπιθύμητης αλληλογραφίας." + }, + "spamfilter_skip_addressbook_explanation": { + "message": "Ο αποστολέας είναι μια επαφή στο βιβλίο διευθύνσεων." + }, + "addressbook_permission_denied": { + "message": "Η άδεια χρήσης του βιβλίου διευθύνσεων απορρίφθηκε. Ενεργοποιήστε ξανά τη λειτουργία για να παραχωρήσετε άδεια." + }, + "addressbook_permission_error": { + "message": "Σφάλμα κατά την αίτηση άδειας για το βιβλίο διευθύνσεων. Δοκιμάστε ξανά." + }, + "apiwebchat_done": { + "message": "Έγινε!" + }, + "prefs_OptionText_anthropic_extended_thinking_budget": { + "message": "Προϋπολογισμός εκτεταμένης σκέψης (tokens)" + }, + "prefs_OptionText_anthropic_extended_thinking_budget_Info": { + "message": "Μέγιστος αριθμός διακριτικών που μπορεί να δαπανήσει το μοντέλο για εκτεταμένη σκέψη. Ορίστε σε 0 για να απενεργοποιήσετε την εκτεταμένη σκέψη. Όταν είναι ενεργοποιημένη, η τιμή θερμοκρασίας αγνοείται από το Claude API." + }, + "prefs_ollama_format_json": { + "message": "Επιβολή εξόδου JSON" + }, + "prefs_ollama_format_json_Info": { + "message": "Εάν επιλεγεί, το Ollama θα αναγκαστεί να επιστρέψει μια έγκυρη απόκριση JSON. Αυτή η επιλογή λειτουργεί μόνο με μοντέλα που υποστηρίζουν δομημένη έξοδο." + }, + "prefs_specific_api_indicator": { + "message": "Χρησιμοποιώντας 1$", + "placeholders": { + "1": { + "content": "$1" + } + } + }, + "prefs_OptionText_auto_summary": { + "message": "Ενεργοποίηση αυτόματης σύνοψης με τεχνητή νοημοσύνη για προεπισκοπήσεις μηνυμάτων" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Εάν είναι επιλεγμένο, το ThunderAI θα δημιουργεί και θα εμφανίζει αυτόματα περιλήψεις AI πάνω από τα μηνύματα email όταν αυτά ανοίγονται. Σημειώστε ότι αυτό σημαίνει ότι όλα τα μηνύματα που βλέπετε σε προεπισκόπηση θα αποστέλλονται αμέσως στην διαμορφωμένη υπηρεσία AI." + }, + "auto_summary_title": { + "message": "Σύνοψη ThunderAI" + }, + "auto_summary_generating": { + "message": "Δημιουργία σύνοψης τεχνητής νοημοσύνης..." + }, + "auto_summary_failed": { + "message": "Η δημιουργία σύνοψης τεχνητής νοημοσύνης απέτυχε. Επιβεβαιώστε τις ρυθμίσεις σας και δοκιμάστε ξανά." + }, + "prefs_OptionText_calendar_no_selection_missing_placeholder": { + "message": "Η προτροπή πρέπει να περιέχει το σύμβολο κράτησης θέσης {%mail_text_body_or_selected%} ή {%mail_html_body_or_selected%} για να ενεργοποιηθεί αυτή η επιλογή. Προσθέστε ένα από αυτά τα σύμβολα κράτησης θέσης στην προτροπή ή επαναφέρετέ το στην προεπιλεγμένη τιμή." + }, + "spam_check_in_progress": { + "message": "Έλεγχος ανεπιθύμητης αλληλογραφίας σε εξέλιξη..." + }, + "prefs_OptionText_summarize_auto": { + "message": "Αυτόματη σύνοψη μηνυμάτων" + }, + "prefs_OptionText_summarize_auto_Info": { + "message": "Επιλέξτε εάν θα δημιουργούνται αυτόματα συνόψεις κατά την προβολή μηνυμάτων. Απαιτείται σύνδεση που βασίζεται σε API (όχι ChatGPT Web)." + }, + "prefs_OptionText_summarize_display_mode": { + "message": "Εμφάνιση σύνοψης σε" + }, + "prefs_OptionText_summarize_display_mode_Info": { + "message": "Επιλέξτε πού θα εμφανίζεται το αποτέλεσμα σύνοψης. Η λειτουργία Inline εμφανίζει ένα banner σύνοψης απευθείας στο παράθυρο μηνύματος. Η λειτουργία παραθύρου συνομιλίας ανοίγει το παράθυρο συνομιλίας με τεχνητή νοημοσύνη." + }, + "prefs_OptionText_summarize_max_display_length": { + "message": "Μέγιστο μήκος οθόνης" + }, + "prefs_OptionText_summarize_max_display_length_Info": { + "message": "Μέγιστος αριθμός χαρακτήρων που θα εμφανίζονται στην ενσωματωμένη σύνοψη. Ορίστε σε 0 για χωρίς όριο." + }, + "prefs_OptionText_summarize_strip_formatting": { + "message": "Μορφοποίηση λωρίδας" + }, + "prefs_OptionText_summarize_strip_formatting_Info": { + "message": "Αφαιρέστε τη μορφοποίηση HTML και Markdown από τη σύνοψη που δημιουργείται από την τεχνητή νοημοσύνη, εμφανίζοντας μόνο απλό κείμενο." + }, + "summarize_see_more": { + "message": "Δείτε περισσότερα" + }, + "summarize_see_less": { + "message": "Δείτε λιγότερα" + }, + "summarize_title": { + "message": "Επισκόπηση ThunderAI" + }, + "get_ai_summary": { + "message": "Σύνοψη Τεχνητής Νοημοσύνης" + }, + "summarize_collapse": { + "message": "Σύμπτυξη σύνοψης" + }, + "summarize_generating": { + "message": "Δημιουργία σύνοψης..." + }, + "summarize_error": { + "message": "Η δημιουργία σύνοψης απέτυχε" + }, + "summarize_click_to_generate": { + "message": "Κάντε κλικ εδώ για να δημιουργήσετε μια σύνοψη" + }, + "summarize_chatgpt_web_not_supported": { + "message": "Η αυτόματη σύνοψη απαιτεί σύνδεση που βασίζεται σε API. Παρακαλούμε διαμορφώστε μια σύνδεση API στις ρυθμίσεις του ThunderAI." + }, + "summarize_refresh": { + "message": "Ανανέωση σύνοψης" + }, + "spamfilter_refresh": { + "message": "Ανανέωση αναφοράς ανεπιθύμητων μηνυμάτων" + }, + "spamfilter_delete": { + "message": "Διαγραφή αναφοράς ανεπιθύμητων μηνυμάτων" + }, + "summarize_delete": { + "message": "Διαγραφή σύνοψης" + }, + "generic_error_dismiss": { + "message": "Απόριψη" + }, + "prefs_OptionText_translate": { + "message": "Μετάφραση email" + }, + "prefs_OptionText_translate_use_specific_integration_Info": { + "message": "Εάν επιλεγεί, το μοντέλο και το API που καθορίζονται παρακάτω θα χρησιμοποιηθούν για τη μετάφραση email(s), ανεξάρτητα από αυτό που έχει επιλεγεί στη σελίδα επιλογών ThunderAI." + }, + "prefs_OptionText_translate_Info": { + "message": "Εάν είναι επιλεγμένο, προσθέτει ένα κουμπί μετάφρασης στο σώμα του μηνύματος." + }, + "prefs_OptionText_btnManageTranslateInfo": { + "message": "Διαχείριση ρυθμίσεων μετάφρασης" + }, + "Translate_PageTitle": { + "message": "Διαχείριση ρυθμίσεων μετάφρασης" + }, + "Translate_info_default": { + "message": "Σε αυτήν τη σελίδα μπορείτε να τροποποιήσετε την προεπιλεγμένη προτροπή που χρησιμοποιείται για τη μετάφραση μηνυμάτων ηλεκτρονικού ταχυδρομείου." + }, + "Translate_prompt_text_title": { + "message": "Τρέχον κείμενο προτροπής" + }, + "Translate_prompt_prefs_title": { + "message": "Επιλογές μετάφρασης" + }, + "prefs_OptionText_translate_auto": { + "message": "Αυτόματη μετάφραση μηνυμάτων" + }, + "prefs_OptionText_action_auto_disabled": { + "message": "Απενεργοποιημένο" + }, + "prefs_OptionText_action_auto_manual": { + "message": "Μόνο χειροκίνητο κουμπί" + }, + "prefs_OptionText_action_auto_automatic": { + "message": "Όταν ανοίξει το email" + }, + "prefs_OptionText_translate_auto_Info": { + "message": "Επιλέξτε πότε θα μεταφράζονται τα μηνύματα: απενεργοποιημένη, μόνο όταν κάνετε κλικ στο κουμπί ή αυτόματα κατά το άνοιγμα ενός μηνύματος." + }, + "prefs_OptionText_display_mode_inline": { + "message": "Παράθυρο μηνύματος (ενσωματωμένο)" + }, + "prefs_OptionText_display_mode_webchat": { + "message": "Παράθυρο συνομιλίας" + }, + "prefs_OptionText_translate_max_display_length": { + "message": "Μέγιστο μήκος εμφανιζόμενης μετάφρασης" + }, + "prefs_OptionText_translate_max_display_length_Info": { + "message": "Μέγιστος αριθμός χαρακτήρων που εμφανίζονται στην ενσωματωμένη μετάφραση. 0 = χωρίς όριο. Όταν οριστεί, το μεγαλύτερο κείμενο περικόπτεται με την επιλογή \"Δείτε περισσότερα\"." + }, + "translate_see_more": { + "message": "Δείτε περισσότερα" + }, + "translate_see_less": { + "message": "Δείτε λιγότερα" + }, + "prefs_OptionText_translate_lang": { + "message": "Γλώσσα-στόχος μετάφρασης" + }, + "prefs_OptionText_translate_lang_Info": { + "message": "Γλώσσα στην οποία θα μεταφραστούν τα μηνύματα ηλεκτρονικού ταχυδρομείου. Εάν είναι κενό, χρησιμοποιείται η προεπιλεγμένη ρύθμιση γλώσσας." + }, + "prefs_OptionText_translate_exclude_lang": { + "message": "Εξαίρεση γλωσσών" + }, + "prefs_OptionText_translate_exclude_lang_Info": { + "message": "Λίστα κωδικών γλώσσας (π.χ., en, fr, it) διαχωρισμένων με κόμμα για παράλειψη για αυτόματη μετάφραση. Εάν το μήνυμα ηλεκτρονικού ταχυδρομείου είναι σε μία από αυτές τις γλώσσες, δεν θα μεταφραστεί αυτόματα ή το κουμπί χειροκίνητης μετάφρασης δεν θα εμφανιστεί." + }, + "prefs_OptionText_Translate_main_prompt": { + "message": "Η προτροπή που περιγράφει την εργασία μετάφρασης:" + }, + "translate_generating": { + "message": "Μεταφράζοντας..." + }, + "translate_click_to_generate": { + "message": "Κάντε κλικ εδώ για να μεταφράσετε αυτό το μήνυμα ηλεκτρονικού ταχυδρομείου" + }, + "get_ai_translation": { + "message": "Μετάφραση Τεχνητής Νοημοσύνης" + }, + "translate_chatgpt_web_not_supported": { + "message": "Η αυτόματη μετάφραση απαιτεί σύνδεση που βασίζεται σε API. Παρακαλούμε διαμορφώστε μια σύνδεση API στις ρυθμίσεις του ThunderAI." + }, + "translate_refresh": { + "message": "Ανανέωση μετάφρασης" + }, + "translate_delete": { + "message": "Διαγραφή μετάφρασης" + }, + "translate_banner_title": { + "message": "Μετάφραση Τεχνητής Νοημοσύνης" + }, + "translate_error": { + "message": "Η μετάφραση απέτυχε." + }, + "translate_no_language_configured": { + "message": "Η γλώσσα μετάφρασης δεν έχει ρυθμιστεί. Ορίστε μια γλώσσα στις ρυθμίσεις μετάφρασης ή ορίστε μια προεπιλεγμένη γλώσσα στις Γενικές ρυθμίσεις." + }, + "translate_skipped": { + "message": "Η μετάφραση παραλείφθηκε: Η γλώσσα εξαιρείται ή είναι πανομοιότυπη με τη γλώσσα-στόχο." + }, + "antispam_by": { + "message": "Antispam από" + }, + "spam_badge_tooltip": { + "message": "Βαθμολογία ανεπιθύμητης αλληλογραφίας — Κάντε κλικ για να δείτε την εξήγηση" + }, + "summary_by": { + "message": "Σύνοψη από" + }, + "translate_by": { + "message": "Μετάφραση από" + }, + "prefs_THStats_1": { + "message": "Θέλετε όμορφα στατιστικά στοιχεία για τα email σας;" + }, + "prefs_THStats_2": { + "message": "Κάντε κλικ εδώ! Δοκιμάστε το ThunderStats!" + }, + "prefs_OptionText_chatgpt_win_pos_text": { + "message": "Θέση παραθύρου συνομιλίας με τεχνητή νοημοσύνη" + }, + "prefs_OptionText_chatgpt_win_top": { + "message": "Κορυφαία" + }, + "prefs_OptionText_chatgpt_win_left": { + "message": "Αριστερά" + }, + "prefs_chatgpt_win_save_position": { + "message": "Αυτόματη αποθήκευση της θέσης του παραθύρου όταν χρησιμοποιείται το κουμπί κλεισίματος." + }, + "prefs_chatgpt_win_position_info": { + "message": "Αφήστε το κενό για να χρησιμοποιήσετε την προεπιλεγμένη θέση." + }, + "prefs_OptionText_action_auto_batch": { + "message": "Όταν ληφθεί το email" + }, + "placeholder_string": { + "message": "Θέση κράτησης" + }, + "menu_order_title": { + "message": "Σειρά μενού" + }, + "menu_order_popup_list_title": { + "message": "Αναδυόμενο μενού" + }, + "menu_order_context_list_title": { + "message": "Μενού περιβάλλοντος" + }, + "menu_order_saved": { + "message": "Η σειρά μενού αποθηκεύτηκε!" + }, + "menu_order_tab_reading": { + "message": "Ανάγνωση" + }, + "menu_order_tab_composing": { + "message": "Σύνθεση" + }, + "menu_order_badge_default": { + "message": "Προεπιλογή" + }, + "menu_order_badge_special": { + "message": "Σπέσιαλ" + }, + "menu_order_badge_custom": { + "message": "Ειδικό" + }, + "menu_order_type_reading": { + "message": "Ανάγνωση" + }, + "menu_order_type_composing": { + "message": "Σύνθεση" + }, + "menu_order_type_always": { + "message": "Πάντοτε" + }, + "menu_order_btn_label": { + "message": "Διαχείριση ρυθμίσεων σειράς μενού" + }, + "menu_order_info": { + "message": "Σύρετε και αποθέστε στοιχεία για να τα αναδιατάξετε. Χρησιμοποιήστε την εναλλαγή για να εμφανίσετε ή να αποκρύψετε στοιχεία σε κάθε μενού." + }, + "menu_order_active_items": { + "message": "Ορατά στοιχεία" + }, + "menu_order_hidden_items": { + "message": "Κρυμμένα αντικείμενα" + }, + "menu_order_icon_label": { + "message": "Επιλέξτε ένα εικονίδιο" + }, + "menu_order_icon_none": { + "message": "(τίποτα)" } } From d6eaa5842b33e091379385180a84382b973a7aba Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 23 Apr 2026 23:08:39 +0200 Subject: [PATCH 259/269] Translated using Weblate (Italian) Currently translated at 100.0% (592 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/it/ --- _locales/it/messages.json | 323 +++++++++++++++++++++++++++++++++++++- 1 file changed, 317 insertions(+), 6 deletions(-) diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 69b4d830..d1e774e7 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -529,7 +529,7 @@ "message": "Se selezionato, verrà incluso nel menu un elemento per applicare i tag alle email." }, "prompt_add_tags": { - "message": "Aggiungi tag a questa email" + "message": "Aggiungi tag" }, "prompt_add_tags_full_text": { "message": "Analizza il seguente testo email e genera un array JSON di tag che ne riassumano il contenuto. Utilizza come tag i temi, gli argomenti principali e i descrizioni rilevanti. Assicurati che i tag siano concisi e pertinenti al contenuto dell’email.\nTesto email: {%mail_text_body%}\nConsidera i seguenti dettagli come contesto:\n- Mittente: {%author%}\n- Destinatari: {%recipients%}\n- Lista CC: {%cc_list%}\n- Oggetto dell'email: {%mail_subject%}\nBasati sul testo dell’email e sul contesto per generare i tag, ignorando le informazioni superflue o i dettagli irrilevanti.\nGenera una risposta esclusivamente in formato JSON. L’output deve essere solo un array JSON di tag, senza alcun commento o testo aggiuntivo. Ecco un esempio del formato JSON da utilizzare:\n{\n\"tags\": [\"tag1\", \"tag2\", \"tag3\", \"tag4\", \"tag5\"]\n}" @@ -748,7 +748,7 @@ "message": "Aggiungi tag solo alle email nella posta in arrivo" }, "prompt_spamfilter": { - "message": "Rileva le email di spam" + "message": "Analizza per spam" }, "Moved_to_Spam": { "message": "Spostato nello spam" @@ -790,7 +790,7 @@ "message": "Lingua predefinita come impostata nelle opzioni di ThunderAI." }, "spamfilter_no_reports": { - "message": "Nessun messaggio è stato ancora controllato per lo spam. Qui troverai un elenco degli ultimi 100 report di spam solo per la sessione corrente." + "message": "Nessun messaggio è stato ancora controllato per lo spam. Qui troverai un elenco degli ultimi 100 report di spam." }, "placeholder_thunderai_def_sign": { "message": "Firma predefinita come impostata nelle opzioni di ThunderAI." @@ -1316,10 +1316,10 @@ "message": "Impostazioni riassunto" }, "prompt_summarize": { - "message": "Riassumi email" + "message": "Riassumi" }, "prompt_summarize_full_text": { - "message": "Sei un assistente che riassume conversazioni via email.\n\nDato un thread di email, produci un riassunto conciso e accurato che includa:\n\n- L'argomento principale o lo scopo della conversazione\n- Decisioni chiave, conclusioni o accordi\n- Domande importanti, richieste o punti d'azione\n- Il responsabile per ogni punto d'azione (se indicato)\n\nOmetti saluti, firme, testo citato e scambi ridondanti.\nNon aggiungere supposizioni o informazioni non presenti nelle email.\n\nScrivi il riassunto con un linguaggio chiaro e neutro, adatto a un professionista impegnato." + "message": "Fornisci un riassunto conciso dei seguenti messaggi email. Il riassunto deve essere di massimo 3-5 frasi e deve catturare i punti principali. Scrivi in paragrafi semplici, senza elenchi puntati, liste o formattazione markdown.\n\n" }, "prompt_summarize_email_template": { "message": "Template riassunto email" @@ -1495,9 +1495,320 @@ "message": "Tutte le intestazioni email" }, "prefs_OptionText_hide_thinking": { - "message": "Nascondi l'output del ragionamento" + "message": "Nascondi l'output del ragionamento come impostazione predefinita" }, "prefs_OptionText_thinking_summary": { "message": "Ragionamento" + }, + "prefs_OptionText_hide_thinking_info": { + "message": "Controlla lo stato iniziale del ragionamento visualizzato sopra la risposta. Se selezionato, il ragionamento è compresso di default e può essere aperto con un clic. Se deselezionato, è invece aperto di default e può essere compresso con un clic. Il contenuto del ragionamento viene sempre conservato." + }, + "placeholder_thunderai_translate_lang": { + "message": "Il linguaggio da utilizzare nella traduzioni delle email." + }, + "placeholder_thunderai_translate_exclude_lang": { + "message": "I codici delle lingue da non tradurre se rilevate." + }, + "SpamFilter_skip_addresses_title": { + "message": "Elenco di esclusione indirizzi email" + }, + "SpamFilter_skip_addresses_infoline": { + "message": "Le email provenienti da questi indirizzi non saranno inviate all'IA per il filtraggio dello spam." + }, + "SpamFilter_skip_addresses_infoline2": { + "message": "Aggiungi un indirizzo email per riga, oppure separali con una virgola." + }, + "spamfilter_skip_addresses_explanation": { + "message": "Il mittente è incluso nell'elenco di esclusione antispam degli indirizzi email." + }, + "prefs_OptionText_spamfilter_skip_addressbook": { + "message": "Salta gli indirizzi in rubrica" + }, + "prefs_OptionText_spamfilter_skip_addressbook_Info": { + "message": "Se selezionato, le email provenienti da mittenti presenti nelle tue rubriche non verranno inviate all'IA per il filtraggio antispam." + }, + "spamfilter_skip_addressbook_explanation": { + "message": "Il mittente è un contatto presente in rubrica." + }, + "addressbook_permission_denied": { + "message": "Il permesso di accesso alla rubrica è stato negato. Abilita nuovamente la funzione per concedere il permesso." + }, + "addressbook_permission_error": { + "message": "Errore durante la richiesta del permesso per la rubrica. Per favore, riprova." + }, + "apiwebchat_done": { + "message": "Fatto!" + }, + "prefs_OptionText_anthropic_extended_thinking_budget": { + "message": "Budget per il pensiero esteso (token)" + }, + "prefs_OptionText_anthropic_extended_thinking_budget_Info": { + "message": "Numero massimo di token che il modello può utilizzare per il pensiero esteso. Imposta a 0 per disabilitare. Quando abilitato, il valore della temperatura viene ignorato dall'API di Claude." + }, + "prefs_ollama_format_json": { + "message": "Forza output JSON" + }, + "prefs_ollama_format_json_Info": { + "message": "Se selezionato, Ollama sarà forzato a restituire una risposta JSON valida. Questa opzione funziona solo con i modelli che supportano l'output strutturato." + }, + "prefs_specific_api_indicator": { + "message": "Utilizzando $1", + "placeholders": { + "1": { + "content": "$1" + } + } + }, + "prefs_OptionText_auto_summary": { + "message": "Abilita riassunto IA automatico per l'anteprima messaggi" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Se selezionato, ThunderAI genererà e visualizzerà automaticamente dei riassunti IA sopra i messaggi quando vengono aperti. Nota: questo comporterà l'invio immediato di ogni messaggio visualizzato al servizio IA configurato." + }, + "auto_summary_title": { + "message": "Riassunto ThunderAI" + }, + "auto_summary_generating": { + "message": "Generazione riassunto IA..." + }, + "auto_summary_failed": { + "message": "Impossibile generare il riassunto IA. Verifica le impostazioni e riprova." + }, + "prefs_OptionText_summarize_auto": { + "message": "Riassumi messaggi automaticamente" + }, + "prefs_OptionText_summarize_auto_Info": { + "message": "Scegli se generare automaticamente i riassunti durante la visualizzazione dei messaggi. Richiede una connessione basata su API (non ChatGPT Web)." + }, + "prefs_OptionText_summarize_display_mode": { + "message": "Visualizza riassunto in" + }, + "prefs_OptionText_summarize_display_mode_Info": { + "message": "Scegli dove visualizzare il risultato del riassunto. La modalità 'Inline' mostra un banner direttamente nel pannello del messaggio. La modalità 'Finestra chat' apre la finestra della chat IA." + }, + "prefs_OptionText_summarize_max_display_length": { + "message": "Lunghezza massima visualizzata" + }, + "prefs_OptionText_summarize_max_display_length_Info": { + "message": "Numero massimo di caratteri da mostrare nel riassunto inline. Imposta a 0 per nessun limite." + }, + "prefs_OptionText_summarize_strip_formatting": { + "message": "Rimuovi formattazione" + }, + "prefs_OptionText_summarize_strip_formatting_Info": { + "message": "Rimuove la formattazione HTML e Markdown dal riassunto generato dall'IA, mostrando solo testo semplice." + }, + "summarize_see_more": { + "message": "Mostra altro" + }, + "summarize_see_less": { + "message": "Mostra meno" + }, + "summarize_title": { + "message": "Panoramica ThunderAI" + }, + "get_ai_summary": { + "message": "Riassunto IA" + }, + "summarize_collapse": { + "message": "Comprimi riassunto" + }, + "summarize_generating": { + "message": "Generazione riassunto..." + }, + "summarize_error": { + "message": "Impossibile generare il riassunto" + }, + "summarize_click_to_generate": { + "message": "Clicca qui per generare un riassunto" + }, + "summarize_chatgpt_web_not_supported": { + "message": "Il riassunto automatico richiede una connessione basata su API. Configura una connessione API nelle impostazioni di ThunderAI." + }, + "summarize_refresh": { + "message": "Aggiorna riassunto" + }, + "spamfilter_refresh": { + "message": "Aggiorna rapporto spam" + }, + "spamfilter_delete": { + "message": "Elimina rapporto spam" + }, + "summarize_delete": { + "message": "Elimina riassunto" + }, + "generic_error_dismiss": { + "message": "Ignora" + }, + "prefs_OptionText_translate": { + "message": "Traduci email" + }, + "prefs_OptionText_translate_use_specific_integration_Info": { + "message": "Se selezionato, per la traduzione delle email verranno utilizzati il Modello e l'API specificati sotto, indipendentemente da quanto scelto nelle opzioni generali di ThunderAI." + }, + "prefs_OptionText_translate_Info": { + "message": "Se selezionato, aggiunge un pulsante di traduzione nel corpo del messaggio." + }, + "prefs_OptionText_btnManageTranslateInfo": { + "message": "Gestisci impostazioni traduzione" + }, + "Translate_PageTitle": { + "message": "Gestisci Impostazioni Traduzione" + }, + "Translate_info_default": { + "message": "In questa pagina puoi modificare il prompt predefinito utilizzato per tradurre le email." + }, + "Translate_prompt_text_title": { + "message": "Testo del prompt attuale" + }, + "Translate_prompt_prefs_title": { + "message": "Opzioni Traduzione" + }, + "prefs_OptionText_translate_auto": { + "message": "Traduci messaggi automaticamente" + }, + "prefs_OptionText_action_auto_disabled": { + "message": "Disabilitato" + }, + "prefs_OptionText_action_auto_manual": { + "message": "Solo pulsante manuale" + }, + "prefs_OptionText_action_auto_automatic": { + "message": "All'apertura dell'email" + }, + "prefs_OptionText_translate_auto_Info": { + "message": "Scegli quando tradurre i messaggi: disabilitato, solo al clic del pulsante, o automaticamente all'apertura di un messaggio." + }, + "prefs_OptionText_display_mode_inline": { + "message": "Pannello messaggio (inline)" + }, + "prefs_OptionText_display_mode_webchat": { + "message": "Finestra chat" + }, + "prefs_OptionText_translate_max_display_length": { + "message": "Lunghezza massima della traduzione visualizzata" + }, + "prefs_OptionText_translate_max_display_length_Info": { + "message": "Numero massimo di caratteri mostrati nella traduzione inline. 0 = nessun limite. Se impostato, il testo più lungo verrà troncato con un interruttore 'Mostra altro'." + }, + "translate_see_more": { + "message": "Mostra altro" + }, + "translate_see_less": { + "message": "Mostra meno" + }, + "prefs_OptionText_translate_lang": { + "message": "Lingua di destinazione traduzione" + }, + "prefs_OptionText_translate_lang_Info": { + "message": "Lingua in cui tradurre le email. Se vuoto, utilizza l'impostazione della lingua predefinita." + }, + "prefs_OptionText_translate_exclude_lang": { + "message": "Escludi lingue" + }, + "prefs_OptionText_translate_exclude_lang_Info": { + "message": "Elenco di codici lingua separati da virgola (es. en, fr, it) da saltare per la traduzione automatica. Se l'email è in una di queste lingue, non verrà tradotta automaticamente o il pulsante manuale non sarà mostrato." + }, + "prefs_OptionText_Translate_main_prompt": { + "message": "Prompt che descrive l'attività di traduzione:" + }, + "translate_generating": { + "message": "Traduzione in corso..." + }, + "translate_click_to_generate": { + "message": "Clicca qui per tradurre questa email" + }, + "get_ai_translation": { + "message": "Traduzione IA" + }, + "translate_chatgpt_web_not_supported": { + "message": "La traduzione automatica richiede una connessione basata su API. Configura una connessione API nelle impostazioni di ThunderAI." + }, + "translate_refresh": { + "message": "Aggiorna traduzione" + }, + "translate_delete": { + "message": "Elimina traduzione" + }, + "translate_banner_title": { + "message": "Traduzione IA" + }, + "translate_error": { + "message": "Traduzione fallita." + }, + "translate_no_language_configured": { + "message": "La lingua di traduzione non è configurata. Imposta una lingua nelle impostazioni di Traduzione o una lingua predefinita nelle impostazioni Generali." + }, + "translate_skipped": { + "message": "Traduzione saltata: lingua esclusa o identica alla destinazione." + }, + "spam_badge_tooltip": { + "message": "Punteggio spam — Clicca per vedere la spiegazione" + }, + "summary_by": { + "message": "Riassunto da" + }, + "translate_by": { + "message": "Traduzione da" + }, + "prefs_OptionText_action_auto_batch": { + "message": "Alla ricezione dell'email" + }, + "placeholder_string": { + "message": "Segnaposto" + }, + "menu_order_title": { + "message": "Ordine Menu" + }, + "menu_order_popup_list_title": { + "message": "Menu Popup" + }, + "menu_order_context_list_title": { + "message": "Menu Contestuale" + }, + "menu_order_saved": { + "message": "Ordine menu salvato!" + }, + "menu_order_tab_reading": { + "message": "Lettura" + }, + "menu_order_tab_composing": { + "message": "Composizione" + }, + "menu_order_badge_default": { + "message": "Predefinito" + }, + "menu_order_badge_special": { + "message": "Speciale" + }, + "menu_order_badge_custom": { + "message": "Personalizzato" + }, + "menu_order_type_reading": { + "message": "Lettura" + }, + "menu_order_type_composing": { + "message": "Composizione" + }, + "menu_order_type_always": { + "message": "Sempre" + }, + "menu_order_btn_label": { + "message": "Gestisci impostazioni ordine menu" + }, + "menu_order_info": { + "message": "Trascina gli elementi per riordinarli. Usa l'interruttore per mostrare o nascondere gli elementi in ogni menu." + }, + "menu_order_active_items": { + "message": "Elementi visibili" + }, + "menu_order_hidden_items": { + "message": "Elementi nascosti" + }, + "menu_order_icon_label": { + "message": "Scegli un'icona" + }, + "menu_order_icon_none": { + "message": "(nessuna)" } } From 3a0f42f4111388625bbf9335a8cd0dab92cb4c73 Mon Sep 17 00:00:00 2001 From: Mic Date: Thu, 23 Apr 2026 23:12:05 +0200 Subject: [PATCH 260/269] Translated using Weblate (French) Currently translated at 96.1% (569 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/fr/ --- _locales/fr/messages.json | 294 +++++++++++++++++++++++++++++++++++++- 1 file changed, 292 insertions(+), 2 deletions(-) diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 02c85093..6e12300f 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -961,7 +961,7 @@ "message": "Chaque modification est enregistrée immédiatement." }, "AccountSelector_Spamfilter": { - "message": "Choisissez les comptes pour lesquels le filtre anti-spam est activé" + "message": "Choisissez les comptes pour lesquels le filtre anti-spam est actif" }, "ask_integration_permission_ok": { "message": "Autorisation accordée. Vous pouvez cliquer ici pour fermer cet onglet et revenir à la fenêtre principale." @@ -1319,7 +1319,7 @@ "message": "Résumer cet e-mail ou ces e-mails" }, "prompt_summarize_full_text": { - "message": "Vous êtes un assistant qui résume les conversations par e-mail.\n\nÀ partir d'un fil de discussion, produisez un résumé concis et précis qui capture :\n\n- Le sujet principal ou l'objet de la conversation.\n- Les décisions clés, conclusions ou accords.\n- Les questions importantes, requêtes ou points à traiter.\n- Le responsable de chaque action à entreprendre (si précisé).\n\nOmettez les salutations, les signatures, le texte cité et les échanges redondants.\nN'ajoutez pas d'hypothèses ou d'informations non présentes dans les e-mails.\n\nRédigez le résumé dans un langage clair et neutre, adapté à un professionnel occupé." + "message": "Vous êtes un assistant qui résume les conversations par e-mail.\n\nÀ partir d'un fil de discussion, produisez un résumé concis et précis qui capture :\n\n- Le sujet principal ou l'objet de la conversation.\n- Les décisions clés, conclusions ou accords.\n- Les questions importantes, requêtes ou points à traiter.\n- Le responsable de chaque action à entreprendre (si précisé).\n\nOmettez les salutations, les signatures, le texte cité et les échanges redondants.\nN'ajoutez pas d'hypothèses ou d'informations non présentes dans les e-mails.\n\nRédigez le résumé dans un langage clair et neutre, adapté à un professionnel occupé.\n\n" }, "prompt_summarize_email_template": { "message": "Résumé du modèle d'e-mail" @@ -1446,5 +1446,295 @@ }, "prefs_chatgpt_win_position_info": { "message": "Laisser vide pour utiliser la position par défaut." + }, + "prefs_OptionText_spamfilter_skip_addressbook": { + "message": "Ignorer les adresses du carnet d'adresses" + }, + "prefs_OptionText_spamfilter_skip_addressbook_Info": { + "message": "Si cette option est sélectionnée, les e-mails provenant d'expéditeurs figurant dans vos carnets d'adresses ne seront pas envoyés à l'IA pour le filtrage anti-spam." + }, + "spamfilter_skip_addressbook_explanation": { + "message": "L'expéditeur est un contact dans votre carnet d'adresses." + }, + "addressbook_permission_denied": { + "message": "La permission d'accéder au carnet d'adresses a été refusée. Veuillez réactiver la fonction pour accorder la permission." + }, + "addressbook_permission_error": { + "message": "Erreur lors de la demande de permission pour le carnet d'adresses. Veuillez réessayer." + }, + "apiwebchat_done": { + "message": "Terminé !" + }, + "prefs_OptionText_anthropic_extended_thinking_budget": { + "message": "Budget de réflexion étendue (tokens)" + }, + "prefs_OptionText_anthropic_extended_thinking_budget_Info": { + "message": "Nombre maximum de tokens que le modèle peut utiliser pour la réflexion étendue. Réglez sur 0 pour désactiver. Lorsqu'il est activé, la valeur de la température est ignorée par l'API Claude." + }, + "prefs_ollama_format_json": { + "message": "Forcer le format JSON" + }, + "prefs_ollama_format_json_Info": { + "message": "Si coché, Ollama sera forcé de renvoyer une réponse JSON valide. Cette option ne fonctionne qu'avec les modèles prenant en charge la sortie structurée." + }, + "prefs_specific_api_indicator": { + "message": "Utilisation de $1", + "placeholders": { + "1": { + "content": "$1" + } + } + }, + "prefs_OptionText_auto_summary": { + "message": "Activer le résumé IA automatique pour l'aperçu des messages" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Si sélectionné, ThunderAI générera et affichera automatiquement des résumés IA au-dessus des messages lors de leur ouverture. Remarque : cela enverra immédiatement chaque message consulté 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. Vérifiez vos paramètres et réessayez." + }, + "prefs_OptionText_summarize_auto": { + "message": "Résumer les messages automatiquement" + }, + "prefs_OptionText_summarize_auto_Info": { + "message": "Choisissez de générer automatiquement des résumés lors de la visualisation des messages. Nécessite une connexion basée sur l'API (pas ChatGPT Web)." + }, + "prefs_OptionText_summarize_display_mode": { + "message": "Afficher le résumé dans" + }, + "prefs_OptionText_summarize_display_mode_Info": { + "message": "Choisissez où afficher le résultat du résumé. Le mode 'En ligne' affiche une bannière directement dans le panneau du message. Le mode 'Fenêtre de chat' ouvre la fenêtre de chat IA." + }, + "prefs_OptionText_summarize_max_display_length": { + "message": "Longueur maximale d'affichage" + }, + "prefs_OptionText_summarize_max_display_length_Info": { + "message": "Nombre maximum de caractères à afficher dans le résumé en ligne. Réglez sur 0 pour aucune limite." + }, + "prefs_OptionText_summarize_strip_formatting": { + "message": "Supprimer le formatage" + }, + "prefs_OptionText_summarize_strip_formatting_Info": { + "message": "Supprime le formatage HTML et Markdown du résumé généré par l'IA, n'affichant que du texte brut." + }, + "summarize_see_more": { + "message": "Voir plus" + }, + "summarize_see_less": { + "message": "Voir moins" + }, + "summarize_title": { + "message": "Aperçu ThunderAI" + }, + "get_ai_summary": { + "message": "Résumé IA" + }, + "summarize_collapse": { + "message": "Réduire le résumé" + }, + "summarize_generating": { + "message": "Génération du résumé..." + }, + "summarize_error": { + "message": "Impossible de générer le résumé" + }, + "summarize_click_to_generate": { + "message": "Cliquez ici pour générer un résumé" + }, + "summarize_chatgpt_web_not_supported": { + "message": "Le résumé automatique nécessite une connexion basée sur l'API. Veuillez configurer une connexion API dans les paramètres de ThunderAI." + }, + "summarize_refresh": { + "message": "Actualiser le résumé" + }, + "spamfilter_refresh": { + "message": "Actualiser le rapport spam" + }, + "spamfilter_delete": { + "message": "Supprimer le rapport spam" + }, + "summarize_delete": { + "message": "Supprimer le résumé" + }, + "generic_error_dismiss": { + "message": "Ignorer" + }, + "prefs_OptionText_translate": { + "message": "Traduire les e-mails" + }, + "prefs_OptionText_translate_use_specific_integration_Info": { + "message": "Si sélectionné, le modèle et l'API spécifiés ci-dessous seront utilisés pour la traduction des e-mails, quel que soit le choix fait dans les options générales de ThunderAI." + }, + "prefs_OptionText_translate_Info": { + "message": "Si sélectionné, ajoute un bouton de traduction dans le corps du message." + }, + "prefs_OptionText_btnManageTranslateInfo": { + "message": "Gérer les paramètres de traduction" + }, + "Translate_PageTitle": { + "message": "Gérer les paramètres de traduction" + }, + "Translate_info_default": { + "message": "Sur cette page, vous pouvez modifier le prompt par défaut utilisé pour traduire les e-mails." + }, + "Translate_prompt_text_title": { + "message": "Texte du prompt actuel" + }, + "Translate_prompt_prefs_title": { + "message": "Options de traduction" + }, + "prefs_OptionText_translate_auto": { + "message": "Traduire les messages automatiquement" + }, + "prefs_OptionText_action_auto_disabled": { + "message": "Désactivé" + }, + "prefs_OptionText_action_auto_manual": { + "message": "Bouton manuel uniquement" + }, + "prefs_OptionText_action_auto_automatic": { + "message": "À l'ouverture de l'e-mail" + }, + "prefs_OptionText_translate_auto_Info": { + "message": "Choisissez quand traduire les messages : désactivé, uniquement au clic sur le bouton, ou automatiquement à l'ouverture d'un message." + }, + "prefs_OptionText_display_mode_inline": { + "message": "Panneau de message (en ligne)" + }, + "prefs_OptionText_display_mode_webchat": { + "message": "Fenêtre de chat" + }, + "prefs_OptionText_translate_max_display_length": { + "message": "Longueur maximale de traduction affichée" + }, + "prefs_OptionText_translate_max_display_length_Info": { + "message": "Nombre maximum de caractères affichés dans la traduction en ligne. 0 = pas de limite. Si défini, le texte plus long sera tronqué avec un bouton 'Voir plus'." + }, + "translate_see_more": { + "message": "Voir plus" + }, + "translate_see_less": { + "message": "Voir moins" + }, + "prefs_OptionText_translate_lang": { + "message": "Langue de destination" + }, + "prefs_OptionText_translate_lang_Info": { + "message": "Langue dans laquelle traduire les e-mails. Si vide, utilise le paramètre de langue par défaut." + }, + "prefs_OptionText_translate_exclude_lang": { + "message": "Exclure des langues" + }, + "prefs_OptionText_translate_exclude_lang_Info": { + "message": "Liste de codes de langue séparés par des virgules (ex: en, fr, it) à ignorer pour la traduction automatique. Si l'e-mail est dans l'une de ces langues, il ne sera pas traduit automatiquement ou le bouton manuel ne sera pas affiché." + }, + "prefs_OptionText_Translate_main_prompt": { + "message": "Prompt décrivant la tâche de traduction :" + }, + "translate_generating": { + "message": "Traduction en cours..." + }, + "translate_click_to_generate": { + "message": "Cliquez ici pour traduire cet e-mail" + }, + "get_ai_translation": { + "message": "Traduction IA" + }, + "translate_chatgpt_web_not_supported": { + "message": "La traduction automatique nécessite une connexion basée sur l'API. Veuillez configurer une connexion API dans les paramètres de ThunderAI." + }, + "translate_refresh": { + "message": "Actualiser la traduction" + }, + "translate_delete": { + "message": "Supprimer la traduction" + }, + "translate_banner_title": { + "message": "Traduction IA" + }, + "translate_error": { + "message": "Échec de la traduction." + }, + "translate_no_language_configured": { + "message": "Aucune langue de traduction n'est configurée. Définissez une langue dans les paramètres de Traduction ou une langue par défaut dans les paramètres Généraux." + }, + "translate_skipped": { + "message": "Traduction ignorée : langue exclue ou identique à la destination." + }, + "spam_badge_tooltip": { + "message": "Score de spam — Cliquez pour voir l'explication" + }, + "summary_by": { + "message": "Résumé par" + }, + "translate_by": { + "message": "Traduction par" + }, + "prefs_OptionText_action_auto_batch": { + "message": "À la réception de l'e-mail" + }, + "placeholder_string": { + "message": "Espace réservé" + }, + "menu_order_title": { + "message": "Ordre du menu" + }, + "menu_order_popup_list_title": { + "message": "Menu contextuel (Popup)" + }, + "menu_order_context_list_title": { + "message": "Menu contextuel" + }, + "menu_order_saved": { + "message": "Ordre du menu enregistré !" + }, + "menu_order_tab_reading": { + "message": "Lecture" + }, + "menu_order_tab_composing": { + "message": "Rédaction" + }, + "menu_order_badge_default": { + "message": "Par défaut" + }, + "menu_order_badge_special": { + "message": "Spécial" + }, + "menu_order_badge_custom": { + "message": "Personnalisé" + }, + "menu_order_type_reading": { + "message": "Lecture" + }, + "menu_order_type_composing": { + "message": "Rédaction" + }, + "menu_order_type_always": { + "message": "Toujours" + }, + "menu_order_btn_label": { + "message": "Gérer les paramètres d'ordre du menu" + }, + "menu_order_info": { + "message": "Faites glisser et déposez les éléments pour les réordonner. Utilisez l'interrupteur pour afficher ou masquer les éléments dans chaque menu." + }, + "menu_order_active_items": { + "message": "Éléments visibles" + }, + "menu_order_hidden_items": { + "message": "Éléments masqués" + }, + "menu_order_icon_label": { + "message": "Choisir une icône" + }, + "menu_order_icon_none": { + "message": "(aucune)" } } From 39752e6b356b1c01962be5adcbac1885d8a0e86c Mon Sep 17 00:00:00 2001 From: Mic Date: Fri, 24 Apr 2026 10:50:18 +0200 Subject: [PATCH 261/269] Translated using Weblate (French) Currently translated at 96.1% (569 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/fr/ --- _locales/fr/messages.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 6e12300f..8d29e678 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -1319,7 +1319,7 @@ "message": "Résumer cet e-mail ou ces e-mails" }, "prompt_summarize_full_text": { - "message": "Vous êtes un assistant qui résume les conversations par e-mail.\n\nÀ partir d'un fil de discussion, produisez un résumé concis et précis qui capture :\n\n- Le sujet principal ou l'objet de la conversation.\n- Les décisions clés, conclusions ou accords.\n- Les questions importantes, requêtes ou points à traiter.\n- Le responsable de chaque action à entreprendre (si précisé).\n\nOmettez les salutations, les signatures, le texte cité et les échanges redondants.\nN'ajoutez pas d'hypothèses ou d'informations non présentes dans les e-mails.\n\nRédigez le résumé dans un langage clair et neutre, adapté à un professionnel occupé.\n\n" + "message": "Fournissez un résumé concis du ou des messages électroniques suivants. Le résumé doit comporter un maximum de 3 à 5 phrases et capturer les points principaux. Rédigez en paragraphes simples, sans listes à puces, énumérations ou formatage markdown.\n\n" }, "prompt_summarize_email_template": { "message": "Résumé du modèle d'e-mail" @@ -1633,7 +1633,7 @@ "message": "Exclure des langues" }, "prefs_OptionText_translate_exclude_lang_Info": { - "message": "Liste de codes de langue séparés par des virgules (ex: en, fr, it) à ignorer pour la traduction automatique. Si l'e-mail est dans l'une de ces langues, il ne sera pas traduit automatiquement ou le bouton manuel ne sera pas affiché." + "message": "Liste de codes de langue séparés par des virgules (ex : en, fr, it) à ignorer pour la traduction automatique. Si l'e-mail est dans l'une de ces langues, il ne sera pas traduit automatiquement ou le bouton manuel ne sera pas affiché." }, "prefs_OptionText_Translate_main_prompt": { "message": "Prompt décrivant la tâche de traduction :" From f7186431d3cf6f9a48003574a403445d2b07cb26 Mon Sep 17 00:00:00 2001 From: Mic Date: Fri, 24 Apr 2026 18:26:46 +0200 Subject: [PATCH 262/269] Translated using Weblate (Italian) Currently translated at 100.0% (592 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/it/ --- _locales/it/messages.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/_locales/it/messages.json b/_locales/it/messages.json index d1e774e7..8198c547 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -1504,7 +1504,7 @@ "message": "Controlla lo stato iniziale del ragionamento visualizzato sopra la risposta. Se selezionato, il ragionamento è compresso di default e può essere aperto con un clic. Se deselezionato, è invece aperto di default e può essere compresso con un clic. Il contenuto del ragionamento viene sempre conservato." }, "placeholder_thunderai_translate_lang": { - "message": "Il linguaggio da utilizzare nella traduzioni delle email." + "message": "La lingua da utilizzare nella traduzioni delle email." }, "placeholder_thunderai_translate_exclude_lang": { "message": "I codici delle lingue da non tradurre se rilevate." @@ -1513,7 +1513,7 @@ "message": "Elenco di esclusione indirizzi email" }, "SpamFilter_skip_addresses_infoline": { - "message": "Le email provenienti da questi indirizzi non saranno inviate all'IA per il filtraggio dello spam." + "message": "Le email provenienti da questi indirizzi non saranno inviate all'IA per l'analisi antispam." }, "SpamFilter_skip_addresses_infoline2": { "message": "Aggiungi un indirizzo email per riga, oppure separali con una virgola." @@ -1525,7 +1525,7 @@ "message": "Salta gli indirizzi in rubrica" }, "prefs_OptionText_spamfilter_skip_addressbook_Info": { - "message": "Se selezionato, le email provenienti da mittenti presenti nelle tue rubriche non verranno inviate all'IA per il filtraggio antispam." + "message": "Se selezionato, le email provenienti da mittenti presenti nelle tue rubriche non verranno inviate all'IA per l'analisi antispam." }, "spamfilter_skip_addressbook_explanation": { "message": "Il mittente è un contatto presente in rubrica." @@ -1677,7 +1677,7 @@ "message": "All'apertura dell'email" }, "prefs_OptionText_translate_auto_Info": { - "message": "Scegli quando tradurre i messaggi: disabilitato, solo al clic del pulsante, o automaticamente all'apertura di un messaggio." + "message": "Scegli quando tradurre i messaggi: disabilitato, solo al click del pulsante, o automaticamente all'apertura di un messaggio." }, "prefs_OptionText_display_mode_inline": { "message": "Pannello messaggio (inline)" From 9bbc5fbc693d5f0ffd0e9553c30b45a9bf32606a Mon Sep 17 00:00:00 2001 From: Mic Date: Fri, 24 Apr 2026 17:40:08 +0200 Subject: [PATCH 263/269] Translated using Weblate (German) Currently translated at 100.0% (592 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/de/ --- _locales/de/messages.json | 376 +++++++++++++++++++++++++++++++++++++- 1 file changed, 370 insertions(+), 6 deletions(-) diff --git a/_locales/de/messages.json b/_locales/de/messages.json index 7098a10c..7650c9f7 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -529,7 +529,7 @@ "message": "Wenn aktiviert, wird ein Element im Menü hinzugefügt, um Tags auf E-Mails anzuwenden." }, "prompt_add_tags": { - "message": "Tags zu dieser E-Mail hinzufügen" + "message": "Tags hinzufügen" }, "prompt_add_tags_full_text": { "message": "Analysiere den folgenden E-Mail-Text und erstelle ein JSON-Array mit Tags, die den Inhalt zusammenfassen. Verwende Themen, zentrale Schlagwörter und relevante Beschreibungen als Tags. Achte darauf, dass die Tags prägnant und inhaltlich relevant sind.\nE-Mail-Text: {%mail_text_body%}\nBerücksichtigen Sie die folgenden Details als Kontext:\n- Absender: {%author%}\n- Empfänger: {%recipients%}\n- CC-Liste: {%cc_list%}\n- E-Mail-Betreff: {%mail_subject%}\nErstelle die Tags auf Grundlage des E-Mail-Inhalts und Kontexts. Ignoriere unnötige Informationen oder unwichtige Details.\nGib die Antwort ausschließlich im JSON-Format aus. Die Ausgabe darf nur ein JSON-Array mit Tags enthalten – ohne weiteren Kommentar oder Text. Hier ein Beispiel für das zu verwendende JSON-Format:\n{\n\"tags\": [\"tag1\", \"tag2\", \"tag3\", \"tag4\", \"tag5\"]\n}" @@ -754,7 +754,7 @@ "message": "Aktueller Aufforderungstext" }, "prompt_spamfilter": { - "message": "Spam-E-Mails erkennen" + "message": "Auf Spam prüfen" }, "SpamFilter_prompt_prefs_title": { "message": "Spam-Filter-Optionen" @@ -808,7 +808,7 @@ "message": "Vorhandene Tags erzwingen" }, "spamfilter_no_reports": { - "message": "Es wurden noch keine Nachrichten auf Spam überprüft. Hier finden Sie eine Liste der letzten 100 Spam-Berichte nur für die aktuelle Sitzung." + "message": "Es wurden noch keine Nachrichten auf Spam geprüft. Hier finden Sie eine Liste der letzten 100 Spam-Berichte." }, "prefs_OptionText_spamfilter_Info": { "message": "Wenn ausgewählt, wird ThunderAI Spam-E-Mails automatisch in den Spam-Ordner verschieben." @@ -979,7 +979,7 @@ "message": "Jede Änderung wird sofort gespeichert." }, "AccountSelector_Spamfilter": { - "message": "Wähle die Konten aus, bei denen der Spamfilter aktiviert ist" + "message": "Wähle die Konten aus, bei denen der automatische Spamfilter aktiviert ist" }, "prompt_proofread_this": { "message": "Korrigiere diese E-Mail" @@ -1316,10 +1316,10 @@ "message": "Zusammenfassungseinstellungen" }, "prompt_summarize": { - "message": "Diese E-Mail o. E-Mails zusammenfassen" + "message": "Zusammenfassen" }, "prompt_summarize_full_text": { - "message": "Du bist ein Assistent, der E-Mail-Konversationen zusammenfasst.\n\nErstelle aus einem E-Mail-Verlauf eine prägnante und präzise Zusammenfassung, die Folgendes enthält:\n\n- Das Hauptthema oder den Zweck der Konversation\n- Wichtige Entscheidungen, Schlussfolgerungen oder Vereinbarungen\n- Wichtige Fragen, Anfragen oder Aufgaben (Action Items)\n- Die verantwortliche Person für jede Aufgabe (falls angegeben)\n\nLasse Begrüßungen, Signaturen, zitierten Text und redundantes Hin- und Her weg.\nFüge keine Annahmen oder Informationen hinzu, die nicht in den E-Mails enthalten sind.\n\nVerfasse die Zusammenfassung in einer klaren, neutralen Sprache, die für vielbeschäftigte Fachkräfte geeignet ist." + "message": "Geben Sie eine prägnante Zusammenfassung der folgenden E-Mail-Nachricht(en) an. Die Zusammenfassung sollte maximal 3 bis 5 Sätze umfassen und die wesentlichen Punkte enthalten. Schreiben Sie in einfachen Absätzen ohne Aufzählungszeichen, Listen oder Markdown-Formatierung.\n\n" }, "prompt_summarize_email_template": { "message": "Vorlage für E-Mail-Zusammenfassung" @@ -1446,5 +1446,369 @@ }, "prefs_chatgpt_win_position_info": { "message": "Leer lassen, um die Standardposition zu verwenden." + }, + "show_in": { + "message": "Anzeigen in" + }, + "show_in_popup": { + "message": "Nur Popup" + }, + "show_in_context": { + "message": "Nur Kontextmenü" + }, + "show_in_both": { + "message": "Beide" + }, + "webchat_save_as_summary": { + "message": "Als Zusammenfassung speichern" + }, + "prefs_storage_title": { + "message": "Speicher" + }, + "prefs_storage_info": { + "message": "Der Speicher wird verwendet, um Informationen über Spam-Scores, Zusammenfassungen und Übersetzungen jeder Nachricht zu speichern." + }, + "prefs_storage_size": { + "message": "Speichergröße" + }, + "prefs_storage_clear_button": { + "message": "Speicher leeren" + }, + "prefs_storage_clear_confirm": { + "message": "Sind Sie sicher, dass Sie alle gespeicherten Daten (Zusammenfassungen, Spam-Berichte, Übersetzungen) löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden." + }, + "prefs_storage_clear_done": { + "message": "$COUNT$ Datensätze entfernt.", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "prefsInfoDesc_7": { + "message": "Um die Google Gemini API zu nutzen, benötigen Sie einen Google Gemini API-Key und müssen ein Modell auswählen." + }, + "prefsInfoDesc_8": { + "message": "Um die Claude API zu nutzen, benötigen Sie einen Anthropic Claude API-Key und müssen ein Modell auswählen." + }, + "placeholder_mail_full_headers": { + "message": "Alle E-Mail-Header" + }, + "prefs_OptionText_hide_thinking": { + "message": "Denk-Block standardmäßig einklappen" + }, + "prefs_OptionText_hide_thinking_info": { + "message": "Steuert den Anfangszustand des Denk-Blocks, der über der Antwort angezeigt wird. Wenn aktiviert, ist der Block standardmäßig eingeklappt und kann per Klick geöffnet werden. Wenn deaktiviert, ist der Block standardmäßig geöffnet und kann per Klick eingeklappt werden. Der Inhalt des Denk-Prozesses bleibt dabei stets erhalten." + }, + "prefs_OptionText_thinking_summary": { + "message": "Denkt nach" + }, + "placeholder_thunderai_translate_lang": { + "message": "Die Sprache, die für E-Mail-Übersetzungen verwendet werden soll." + }, + "placeholder_thunderai_translate_exclude_lang": { + "message": "Sprachcodes, die nicht übersetzt werden sollen." + }, + "SpamFilter_skip_addresses_title": { + "message": "Ausschlussliste für E-Mail-Adressen" + }, + "SpamFilter_skip_addresses_infoline": { + "message": "E-Mails von diesen Adressen werden nicht zur Spam-Filterung an die KI gesendet." + }, + "SpamFilter_skip_addresses_infoline2": { + "message": "Geben Sie eine E-Mail-Adresse pro Zeile ein oder trennen Sie diese durch Kommas." + }, + "spamfilter_skip_addresses_explanation": { + "message": "Der Absender befindet sich auf der Antispam-Ausschlussliste." + }, + "prefs_OptionText_spamfilter_skip_addressbook": { + "message": "Adressbuch-Kontakte überspringen" + }, + "prefs_OptionText_spamfilter_skip_addressbook_Info": { + "message": "Wenn aktiviert, werden E-Mails von Absendern in Ihren Adressbüchern nicht zur Spam-Prüfung an die KI gesendet." + }, + "spamfilter_skip_addressbook_explanation": { + "message": "Der Absender ist ein Kontakt im Adressbuch." + }, + "addressbook_permission_denied": { + "message": "Der Zugriff auf das Adressbuch wurde verweigert. Bitte aktivieren Sie die Funktion erneut, um die Berechtigung zu erteilen." + }, + "addressbook_permission_error": { + "message": "Fehler beim Anfordern der Adressbuch-Berechtigung. Bitte versuchen Sie es erneut." + }, + "apiwebchat_done": { + "message": "Fertig!" + }, + "prefs_OptionText_anthropic_extended_thinking_budget": { + "message": "Budget für erweitertes Denken (Tokens)" + }, + "prefs_OptionText_anthropic_extended_thinking_budget_Info": { + "message": "Maximale Anzahl an Tokens, die das Modell für erweitertes Denken (Extended Thinking) verwenden darf. Auf 0 setzen, um das erweiterte Denken zu deaktivieren. Wenn diese Funktion aktiviert ist, wird der Temperature-Wert von der Claude-API ignoriert." + }, + "prefs_ollama_format_json": { + "message": "JSON-Ausgabe erzwingen" + }, + "prefs_ollama_format_json_Info": { + "message": "Wenn aktiviert, wird Ollama gezwungen, eine gültige JSON-Antwort zurückzugeben (nur für unterstützte Modelle)." + }, + "prefs_specific_api_indicator": { + "message": "Verwendet $1", + "placeholders": { + "1": { + "content": "$1" + } + } + }, + "prefs_OptionText_auto_summary": { + "message": "Automatische KI-Zusammenfassung für Nachrichtenvorschauen aktivieren" + }, + "prefs_OptionText_auto_summary_Info": { + "message": "Falls aktiviert, erstellt und zeigt ThunderAI automatisch KI-Zusammenfassungen oberhalb von E-Mails an, sobald diese geöffnet werden. Beachten Sie, dass hierbei alle von Ihnen aufgerufenen Nachrichten 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": "KI-Zusammenfassung konnte nicht generiert werden. Bitte Einstellungen prüfen." + }, + "prefs_OptionText_summarize_auto": { + "message": "Nachrichten automatisch zusammenfassen" + }, + "prefs_OptionText_summarize_auto_Info": { + "message": "Wählen Sie aus, ob beim Anzeigen von Nachrichten automatisch Zusammenfassungen erstellt werden sollen. Erfordert eine API-basierte Verbindung (kein ChatGPT Web)." + }, + "prefs_OptionText_summarize_display_mode": { + "message": "Zusammenfassung anzeigen in" + }, + "prefs_OptionText_summarize_display_mode_Info": { + "message": "Wählen Sie aus, wo das Ergebnis der Zusammenfassung angezeigt werden soll. Der Inline-Modus zeigt ein Zusammenfassungs-Banner direkt im Nachrichtenbereich an. Der Chat-Fenster-Modus öffnet das KI-Chat-Fenster." + }, + "prefs_OptionText_summarize_max_display_length": { + "message": "Maximale Anzeigelänge" + }, + "prefs_OptionText_summarize_max_display_length_Info": { + "message": "Maximale Anzahl der Zeichen in der Inline-Zusammenfassung. 0 für kein Limit." + }, + "prefs_OptionText_summarize_strip_formatting": { + "message": "Formatierung entfernen" + }, + "prefs_OptionText_summarize_strip_formatting_Info": { + "message": "Entfernt HTML und Markdown aus der Zusammenfassung, um nur reinen Text anzuzeigen." + }, + "summarize_see_more": { + "message": "Mehr sehen" + }, + "summarize_see_less": { + "message": "Weniger sehen" + }, + "summarize_title": { + "message": "ThunderAI Überblick" + }, + "get_ai_summary": { + "message": "KI-Zusammenfassung" + }, + "summarize_collapse": { + "message": "Zusammenfassung einklappen" + }, + "summarize_generating": { + "message": "Zusammenfassung wird generiert..." + }, + "summarize_error": { + "message": "Zusammenfassung konnte nicht erstellt werden" + }, + "summarize_click_to_generate": { + "message": "Klicken Sie hier, um eine Zusammenfassung zu erstellen" + }, + "summarize_chatgpt_web_not_supported": { + "message": "Die automatische Zusammenfassung erfordert eine API-basierte Verbindung. Bitte konfigurieren Sie eine API-Verbindung in den ThunderAI-Einstellungen." + }, + "summarize_refresh": { + "message": "Zusammenfassung aktualisieren" + }, + "spamfilter_refresh": { + "message": "Spam-Bericht aktualisieren" + }, + "spamfilter_delete": { + "message": "Spam-Bericht löschen" + }, + "summarize_delete": { + "message": "Zusammenfassung löschen" + }, + "generic_error_dismiss": { + "message": "Verwerfen" + }, + "prefs_OptionText_translate": { + "message": "E-Mail übersetzen" + }, + "prefs_OptionText_translate_use_specific_integration_Info": { + "message": "Wenn aktiviert, werden das unten angegebene Modell und die API für Übersetzungen verwendet." + }, + "prefs_OptionText_translate_Info": { + "message": "Wenn aktiviert, wird eine Schaltfläche zum Übersetzen im Nachrichtentext hinzugefügt." + }, + "prefs_OptionText_btnManageTranslateInfo": { + "message": "Übersetzungseinstellungen verwalten" + }, + "Translate_PageTitle": { + "message": "Übersetzungseinstellungen verwalten" + }, + "Translate_info_default": { + "message": "Auf dieser Seite können Sie den Standard-Prompt für Übersetzungen bearbeiten." + }, + "Translate_prompt_text_title": { + "message": "Aktueller Prompt-Text" + }, + "Translate_prompt_prefs_title": { + "message": "Übersetzungsoptionen" + }, + "prefs_OptionText_translate_auto": { + "message": "Nachrichten automatisch übersetzen" + }, + "prefs_OptionText_action_auto_disabled": { + "message": "Deaktiviert" + }, + "prefs_OptionText_action_auto_manual": { + "message": "Nur manuelle Schaltfläche" + }, + "prefs_OptionText_action_auto_automatic": { + "message": "Wenn die E-Mail geöffnet wird" + }, + "prefs_OptionText_translate_auto_Info": { + "message": "Wählen Sie aus, wann Nachrichten übersetzt werden sollen: deaktiviert, nur beim Klicken auf die Schaltfläche oder automatisch beim Öffnen einer Nachricht." + }, + "prefs_OptionText_display_mode_inline": { + "message": "Nachrichtenbereich (inline)" + }, + "prefs_OptionText_display_mode_webchat": { + "message": "Chat-Fenster" + }, + "prefs_OptionText_translate_max_display_length": { + "message": "Maximale Länge der Anzeige" + }, + "prefs_OptionText_translate_max_display_length_Info": { + "message": "Maximale Anzahl der Zeichen, die in der Inline-Übersetzung angezeigt werden. 0 = kein Limit. Wenn festgelegt, wird längerer Text gekürzt und mit einer „Mehr anzeigen“-Schaltfläche versehen." + }, + "translate_see_more": { + "message": "Mehr sehen" + }, + "translate_see_less": { + "message": "Weniger sehen" + }, + "prefs_OptionText_translate_lang": { + "message": "Zielsprache für Übersetzungen" + }, + "prefs_OptionText_translate_lang_Info": { + "message": "Sprache, in die E-Mails übersetzt werden sollen. Wenn das Feld leer ist, wird die Standardspracheinstellung verwendet." + }, + "prefs_OptionText_translate_exclude_lang": { + "message": "Sprachen ausschließen" + }, + "prefs_OptionText_translate_exclude_lang_Info": { + "message": "Kommagetrennte Liste von Sprachkürzeln (z. B. en, fr, it), die von der automatischen Übersetzung ausgeschlossen werden sollen. Falls eine E-Mail in einer dieser Sprachen verfasst ist, wird sie nicht automatisch übersetzt bzw. die Schaltfläche für die manuelle Übersetzung wird nicht angezeigt." + }, + "prefs_OptionText_Translate_main_prompt": { + "message": "Der Prompt für die Übersetzungsaufgabe:" + }, + "translate_generating": { + "message": "Wird übersetzt..." + }, + "translate_click_to_generate": { + "message": "Hier klicken, um diese E-Mail zu übersetzen" + }, + "get_ai_translation": { + "message": "KI-Übersetzung" + }, + "translate_chatgpt_web_not_supported": { + "message": "Die automatische Übersetzung erfordert eine API-basierte Verbindung. Bitte konfigurieren Sie eine API-Verbindung in den ThunderAI-Einstellungen." + }, + "translate_refresh": { + "message": "Übersetzung aktualisieren" + }, + "translate_delete": { + "message": "Übersetzung löschen" + }, + "translate_banner_title": { + "message": "KI-Übersetzung" + }, + "translate_error": { + "message": "Übersetzung fehlgeschlagen." + }, + "translate_no_language_configured": { + "message": "Die Übersetzungssprache ist nicht konfiguriert. Bitte legen Sie eine Sprache in den Übersetzungseinstellungen fest oder definieren Sie eine Standardsprache in den allgemeinen Einstellungen." + }, + "translate_skipped": { + "message": "Übersetzung übersprungen: Sprache ausgeschlossen oder identisch." + }, + "spam_badge_tooltip": { + "message": "Spam-Score — Klicken für Details" + }, + "summary_by": { + "message": "Zusammenfassung von" + }, + "translate_by": { + "message": "Übersetzung von" + }, + "prefs_OptionText_action_auto_batch": { + "message": "Wenn die E-Mail empfangen wird" + }, + "placeholder_string": { + "message": "Platzhalter" + }, + "menu_order_title": { + "message": "Menü-Reihenfolge" + }, + "menu_order_popup_list_title": { + "message": "Popup-Menü" + }, + "menu_order_context_list_title": { + "message": "Kontextmenü" + }, + "menu_order_saved": { + "message": "Menü-Reihenfolge gespeichert!" + }, + "menu_order_tab_reading": { + "message": "Lesen" + }, + "menu_order_tab_composing": { + "message": "Verfassen" + }, + "menu_order_badge_default": { + "message": "Standard" + }, + "menu_order_badge_special": { + "message": "Spezial" + }, + "menu_order_badge_custom": { + "message": "Benutzerdefiniert" + }, + "menu_order_type_reading": { + "message": "Lesen" + }, + "menu_order_type_composing": { + "message": "Verfassen" + }, + "menu_order_type_always": { + "message": "Immer" + }, + "menu_order_btn_label": { + "message": "Menü-Reihenfolge verwalten" + }, + "menu_order_info": { + "message": "Elemente per Drag-and-Drop neu anordnen oder über den Umschalter ein-/ausblenden." + }, + "menu_order_active_items": { + "message": "Sichtbare Elemente" + }, + "menu_order_hidden_items": { + "message": "Versteckte Elemente" + }, + "menu_order_icon_label": { + "message": "Icon wählen" + }, + "menu_order_icon_none": { + "message": "(keines)" } } From e1288d65d5f50077dd3b7c0fff90a972ce8f9d6a Mon Sep 17 00:00:00 2001 From: Mic Date: Fri, 24 Apr 2026 18:02:50 +0200 Subject: [PATCH 264/269] Translated using Weblate (Greek) Currently translated at 100.0% (592 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/el/ --- _locales/el/messages.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_locales/el/messages.json b/_locales/el/messages.json index 3fd77b71..3c809748 100644 --- a/_locales/el/messages.json +++ b/_locales/el/messages.json @@ -1303,7 +1303,7 @@ "message": "Σύνοψη προτύπου email" }, "prompt_summarize_email_template_full_text": { - "message": "Από: {%author%} \nΠρος: {%recipients%} \nΚοινοποίηση: {%cc_list%} \nΘέμα: {%mail_subject%} \nΗμερομηνία: {%mail_datetime%} \nΣυνημμένα: {%mail_attachments_info%} \nΚύριο κείμενο: \n{%mail_text_body%}" + "message": "Από: {%author%} \nΠρος: {%recipients%} \nΚοινοποίηση: {%cc_list%} \nΘέμα: {%mail_subject%} \nΗμερομηνία: {%mail_datetime%} \nΣυνημμένα:\n{%mail_attachments_info%} \n\nΚύριο κείμενο: \n{%mail_text_body%}" }, "prompt_summarize_email_separator": { "message": "Διαχωριστής email" From c7a9a7812c2a080e15bbb99cfc59614b013d8c6b Mon Sep 17 00:00:00 2001 From: Mic Date: Fri, 24 Apr 2026 17:15:09 +0200 Subject: [PATCH 265/269] Translated using Weblate (French) Currently translated at 100.0% (592 of 592 strings) Translation: ThunderAI for Thunderbird/main Translate-URL: https://hosted.weblate.org/projects/thunderai/main/fr/ --- _locales/fr/messages.json | 76 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 8d29e678..cb254054 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -1235,7 +1235,7 @@ "message": "Définissez le nombre de jetons à utiliser pour le raisonnement. Laissez ce champ vide si le modèle sélectionné ne prend pas en charge le raisonnement ou si vous souhaitez utiliser la méthode par défaut. Entrez 0 pour désactiver le raisonnement, ou -1 pour l’activer de manière dynamique." }, "prefs_google_gemini_thinking_budget": { - "message": "Thinking Budget" + "message": "Budget de réflexion" }, "SelectAll": { "message": "Tout sélectionner" @@ -1736,5 +1736,79 @@ }, "menu_order_icon_none": { "message": "(aucune)" + }, + "show_in": { + "message": "Afficher dans" + }, + "show_in_popup": { + "message": "Fenêtre contextuelle uniquement" + }, + "show_in_context": { + "message": "Menu contextuel uniquement" + }, + "show_in_both": { + "message": "Les deux" + }, + "webchat_save_as_summary": { + "message": "Enregistrer comme résumé" + }, + "prefs_storage_title": { + "message": "Stockage" + }, + "prefs_storage_info": { + "message": "Le stockage est utilisé pour sauvegarder les informations relatives au score de spam, aux résumés et aux traductions de chaque message." + }, + "prefs_storage_size": { + "message": "Taille du stockage" + }, + "prefs_storage_clear_button": { + "message": "Effacer le stockage" + }, + "prefs_storage_clear_confirm": { + "message": "Êtes-vous sûr de vouloir effacer toutes le données stockées (résumés, rapports de spam, traductions) ? Cette action est irréversible." + }, + "prefs_storage_clear_done": { + "message": "$COUNT$ enregistrements supprimés.", + "placeholders": { + "count": { + "content": "$1" + } + } + }, + "prefsInfoDesc_7": { + "message": "Pour utiliser l'API Google Gemini, vous avez besoin d'une clé API Google Gemini et vous devez choisir un modèle." + }, + "prefsInfoDesc_8": { + "message": "Pour utiliser l'API Claude, vous avez besoin d'une clé API Anthropic Claude et vous devez choisir un modèle." + }, + "placeholder_mail_full_headers": { + "message": "Tous les en-têtes du message" + }, + "prefs_OptionText_hide_thinking": { + "message": "Réduire le bloc de réflexion par défaut" + }, + "prefs_OptionText_hide_thinking_info": { + "message": "Contrôle l'état initial du bloc de réflexion affiché au-dessus de la réponse. Si coché, le bloc est réduit par défaut. Le contenu de la réflexion est toujours conservé." + }, + "prefs_OptionText_thinking_summary": { + "message": "Réflexion" + }, + "placeholder_thunderai_translate_lang": { + "message": "La langue à utiliser pour les traductions des messages." + }, + "placeholder_thunderai_translate_exclude_lang": { + "message": "Les codes de langue à ne pas traduire lorsqu'ils sont détectés." + }, + "SpamFilter_skip_addresses_title": { + "message": "Liste d'exclusion d'adresses e-mail" + }, + "SpamFilter_skip_addresses_infoline": { + "message": "Les e-mails provenant de ces adresses ne seront pas envoyés à l'IA pour le filtrage du spam." + }, + "SpamFilter_skip_addresses_infoline2": { + "message": "Ajoutez une adresse e-mail par ligne, ou séparez-les par une virgule." + }, + "spamfilter_skip_addresses_explanation": { + "message": "L'expéditeur figure dans la liste d'exclusion de l'antispam." } } From 326bc39ee657cace059b107b3d4ca401932cce23 Mon Sep 17 00:00:00 2001 From: mic Date: Sun, 10 May 2026 23:09:48 +0200 Subject: [PATCH 266/269] spamfilter_skip_addressbook is now true by default. fixes #785 --- options/mzta-options-default.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index c2207e59..d03a73cd 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -140,7 +140,8 @@ export const prefs_default = { spamfilter_threshold: 70, spamfilter_enabled_accounts: [], spamfilter_skip_addresses: [], - spamfilter_skip_addressbook: false, + spamfilter_skip_addressbook: true, + spamfilter_show_msg_panel: true, summarize: false, summarize_auto: 1, // 0: disabled, 1: manual button, 2: automatic on message open, 3: generate on email receive summarize_display_mode: 'inline', // 'inline' or 'webchat' @@ -151,6 +152,5 @@ export const prefs_default = { translate_max_display_length: 0, // 0 = no limit, otherwise max chars shown inline translate_lang: '', // target language, fallback on default_chatgpt_lang translate_exclude_lang: '', // languages to do not translate - spamfilter_show_msg_panel: true, ...generated_prefs } From ce3eb57612d8efcfcb5f19aa71625710a4d6639c Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 13 May 2026 22:40:21 +0200 Subject: [PATCH 267/269] tomselect updated. see #793 --- VENDORS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/VENDORS.md b/VENDORS.md index 42c8183f..69bc3086 100644 --- a/VENDORS.md +++ b/VENDORS.md @@ -2,10 +2,10 @@ file: pages\_lib\list.js source: https://raw.githubusercontent.com/javve/list.js/v2.3.1/dist/list.js file: pages\_lib\tom-select.base.js -source: https://cdn.jsdelivr.net/npm/tom-select@v2.6.0/dist/js/tom-select.base.js +source: https://cdn.jsdelivr.net/npm/tom-select@v2.6.1/dist/js/tom-select.base.js file: pages\_lib\tom-select.default.min.css -source: https://cdn.jsdelivr.net/npm/tom-select@v2.6.0/dist/css/tom-select.default.min.css +source: https://cdn.jsdelivr.net/npm/tom-select@v2.6.1/dist/css/tom-select.default.min.css file: js\lib\diff.js source: https://cdnjs.cloudflare.com/ajax/libs/jsdiff/7.0.0/diff.js \ No newline at end of file From ee853dd4478baa2d7ef190770b6091adf35cb238 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 13 May 2026 20:40:43 +0000 Subject: [PATCH 268/269] Update Tom Select files from VENDORS.md --- pages/_lib/tom-select.base.js | 38 +++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/pages/_lib/tom-select.base.js b/pages/_lib/tom-select.base.js index 67d53810..2603726e 100644 --- a/pages/_lib/tom-select.base.js +++ b/pages/_lib/tom-select.base.js @@ -1,5 +1,5 @@ /** -* Tom Select v2.6.0 +* Tom Select v2.6.1 * Licensed under the Apache License, Version 2.0 (the "License"); */ @@ -1685,7 +1685,8 @@ var init_textbox = () => { const data_raw = input.getAttribute(attr_data); if (!data_raw) { - var value = input.value.trim() || ''; + var _input$value$trim, _input$value; + var value = (_input$value$trim = input == null || (_input$value = input.value) == null ? void 0 : _input$value.trim()) != null ? _input$value$trim : ''; if (!settings.allowEmptyOption && !value.length) return; const values = value.split(settings.delimiter); iterate(values, value => { @@ -2024,13 +2025,6 @@ self.close(false); self.inputState(); self.isSetup = true; - if (input.disabled) { - self.disable(); - } else if (input.readOnly) { - self.setReadOnly(true); - } else { - self.enable(); //sets tabIndex - } self.on('change', this.onChange); addClasses(input, 'tomselected', 'ts-hidden-accessible'); self.trigger('initialize'); @@ -2139,6 +2133,15 @@ }) : self.settings; self.setupOptions(settings.options, settings.optgroups); self.setValue(settings.items || [], true); // silent prevents recursion + + if (self.input.disabled) { + self.disable(); + } else if (self.input.readOnly) { + self.setReadOnly(true); + } else { + self.enable(); //sets tabIndex + } + self.lastQuery = null; // so updated options will be displayed in dropdown } /** @@ -2813,14 +2816,19 @@ var self = this; if (self.isDisabled || self.isReadOnly) return; self.ignoreFocus = true; - if (self.control_input.offsetWidth) { - self.control_input.focus(); - } else { - self.focus_node.focus(); - } + const focusTarget = this.control_input.offsetWidth ? this.control_input : this.focus_node; + focusTarget.focus(); setTimeout(() => { self.ignoreFocus = false; - self.onFocus(); + // Fix https://github.com/orchidjs/tom-select/issues/806 + // Only proceed if this instance's element is still the active element. If Edge autofill + // (or anything else) has moved focus to a different element in the interim, calling + // onFocus() here would steal focus back and restart the cascade loop. + const root = focusTarget.getRootNode(); + if (root.activeElement !== focusTarget) { + return; + } + this.onFocus(); }, 0); } From 42d6b11e299faeb543029d595f6af584c73539e7 Mon Sep 17 00:00:00 2001 From: mic Date: Wed, 13 May 2026 22:47:26 +0200 Subject: [PATCH 269/269] release notes updated --- CHANGELOG.md | 8 +++----- options/mzta-release-notes.html | 8 +++----- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78b67bee..d6646595 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,12 +3,14 @@ -

      Version 4.1.0 - ??/??/2026

      +

      Version 4.1.0 - 13/05/2026

      • Antispam information are now permanently saved for each message [#675].
      • [All APIs] A summary has been added above the mail content [#580].
      • [All APIs] Added inline auto translation for emails [#247].
      • +
      • Custom menus configuration added. Now it's possibile to define which prompts show in the ThunderAI menu, which ones in the context menu and in which order [#49, #184, #680].
      • Now the popup menu closes immediatly and the working indicator is in the button icon [#677].
      • +
      • [All APIs] Error messages added also for background operations when the API has not been configured correctly [#766].
      • [Ollama API] Added format: json option [#703].
      • Fix: The "Important Information" section in the options page now updates correctly when choosing an integration [#730].
      • In the options page now is visible if a special prompt is using a specific API integration [#676].
      • @@ -18,10 +20,6 @@
      • Added the {%mail_full_headers%} placeholder to retrieve all the email headers at once [#713].
      • [All APIs] In the API webchat the status messages have different colors [#3].
      • Account exclusion lists for add tags and antispam are enforced only for automatic analysis of incoming emails and not for the context menu action that is always executed [#749].
      • -
      • [All APIs] Inline auto translation for emails added [#247].
      • -
      • Custom menus configuration added. Now it's possibile to define which prompts show in the ThunderAI menu, which ones in the context menu and in which order [#49, #184, #680].
      • -
      • [All APIs] Error messages added also for background operations when the API has not been configured correctly [#766].
      • -
      • ...

      Version 4.0.3 - 20/03/2026

        diff --git a/options/mzta-release-notes.html b/options/mzta-release-notes.html index 7ac2ede7..6d989247 100644 --- a/options/mzta-release-notes.html +++ b/options/mzta-release-notes.html @@ -7,12 +7,14 @@

        ThunderAI Release Notes

        -

        Version 4.1.0 - ??/??/2026

        +

        Version 4.1.0 - 13/05/2026

        • Antispam information are now permanently saved for each message [#675].
        • [All APIs] A summary has been added above the mail content [#580].
        • [All APIs] Added inline auto translation for emails [#247].
        • +
        • Custom menus configuration added. Now it's possibile to define which prompts show in the ThunderAI menu, which ones in the context menu and in which order [#49, #184, #680].
        • Now the popup menu closes immediatly and the working indicator is in the button icon [#677].
        • +
        • [All APIs] Error messages added also for background operations when the API has not been configured correctly [#766].
        • [Ollama API] Added format: json option [#703].
        • Fix: The "Important Information" section in the options page now updates correctly when choosing an integration [#730].
        • In the options page now is visible if a special prompt is using a specific API integration [#676].
        • @@ -22,10 +24,6 @@
        • Added the {%mail_full_headers%} placeholder to retrieve all the email headers at once [#713].
        • [All APIs] In the API webchat the status messages have different colors [#3].
        • Account exclusion lists for add tags and antispam are enforced only for automatic analysis of incoming emails and not for the context menu action that is always executed [#749].
        • -
        • [All APIs] Inline auto translation for emails added [#247].
        • -
        • Custom menus configuration added. Now it's possibile to define which prompts show in the ThunderAI menu, which ones in the context menu and in which order [#49, #184, #680].
        • -
        • [All APIs] Error messages added also for background operations when the API has not been configured correctly [#766].
        • -
        • ...

        Version 4.0.3 - 20/03/2026

      - -
      + +
      + +
      __MSG_prefs_OptionText_anthropic_extended_thinking_budget__ + +