diff --git a/_locales/bg/messages.json b/_locales/bg/messages.json index 6adaff52..9c3c2735 100644 --- a/_locales/bg/messages.json +++ b/_locales/bg/messages.json @@ -104,6 +104,9 @@ "customPrompts_form_label_use_diff_viewer_title": { "message": "Инструментът за сравнение на текст може да бъде избран, когато действието е зададено на „Замести текст“." }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Премахване на вградените изображения от HTML на промпта" + }, "customPrompts_form_required_fields": { "message": "Задължително поле" }, diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index 6592022a..cca7af1a 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -65,6 +65,9 @@ "customPrompts_form_label_Name": { "message": "Název" }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Odebrat vložené obrázky z HTML výzvy" + }, "customPrompts_form_required_fields": { "message": "Požadovaná pole" }, diff --git a/_locales/de/messages.json b/_locales/de/messages.json index df5afd0b..09901cec 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -96,6 +96,9 @@ "customPrompts_form_label_enabled": { "message": "Aktiviert" }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Eingebettete Bilder aus dem Prompt-HTML entfernen" + }, "customPrompts_form_required_fields": { "message": "Erforderliche Felder" }, diff --git a/_locales/el/messages.json b/_locales/el/messages.json index 13cdded3..2e603980 100644 --- a/_locales/el/messages.json +++ b/_locales/el/messages.json @@ -104,6 +104,9 @@ "customPrompts_form_label_use_diff_viewer_title": { "message": "Η προβολή σύγκρισης κειμένου μπορεί να επιλεγεί όταν η ενέργεια έχει οριστεί σε \"Αντικατάσταση κειμένου\"." }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Αφαίρεση ενσωματωμένων εικόνων από το HTML του prompt" + }, "customPrompts_form_required_fields": { "message": "Απαιτούμενα πεδία" }, diff --git a/_locales/en/messages.json b/_locales/en/messages.json index f0e6e9b8..e7eb95a0 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -143,6 +143,10 @@ "message": "The text comparison viewer can be selected when the action is set to \"Substitute text\".", "description": "" }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Remove embedded images from prompt HTML", + "description": "" + }, "customPrompts_form_required_fields": { "message": "Required fields", "description": "" @@ -1080,6 +1084,10 @@ "message": "Typed text before the quoted mail body", "description": "" }, + "placeholder_mail_typed_html": { + "message": "Typed HTML formatted text before the quoted mail body. Recommended system instruction: 'Return directly in HTML code when provided with HTML email text'", + "description": "" + }, "placeholder_mail_quoted_text": { "message": "Quoted text in the mail body", "description": "" diff --git a/_locales/eo/messages.json b/_locales/eo/messages.json index 1194a5aa..0c250630 100644 --- a/_locales/eo/messages.json +++ b/_locales/eo/messages.json @@ -17,6 +17,9 @@ "customPrompts_btnCancel": { "message": "Nuligi" }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Forigi enigitajn bildojn el la prompta HTML-kodo" + }, "customPrompts_form_required_fields": { "message": "Postulataj kampoj" }, diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 759f02a0..43120169 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -104,6 +104,9 @@ "customPrompts_form_label_use_diff_viewer_title": { "message": "El visor de comparación de texto se puede seleccionar cuando la acción se establece en \"sustituir texto\"." }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Eliminar imágenes incrustadas del HTML del prompt" + }, "customPrompts_form_required_fields": { "message": "Campos obligatorios" }, diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index e5290bbd..8e79da5b 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -96,6 +96,9 @@ "customPrompts_form_label_enabled": { "message": "Activé" }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Supprimer les images intégrées du HTML du prompt" + }, "customPrompts_form_required_fields": { "message": "Champs obligatoires" }, diff --git a/_locales/hr/messages.json b/_locales/hr/messages.json index 7e55e8a5..03a487f1 100644 --- a/_locales/hr/messages.json +++ b/_locales/hr/messages.json @@ -96,6 +96,9 @@ "customPrompts_form_label_enabled": { "message": "Omogućeno" }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Ukloni ugrađene slike iz HTML-a odziva" + }, "customPrompts_form_required_fields": { "message": "Obavezna polja" }, diff --git a/_locales/hu/messages.json b/_locales/hu/messages.json index f2ed346a..4f28db6c 100644 --- a/_locales/hu/messages.json +++ b/_locales/hu/messages.json @@ -107,6 +107,9 @@ "customPrompts_form_label_use_diff_viewer_title": { "message": "A szöveg-összehasonlító nézet akkor választható ki, ha a művelet \"Szöveg helyettesítése\"-re van állítva." }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Beágyazott képek eltávolítása a prompt HTML-kódjából" + }, "customPrompts_form_required_fields": { "message": "Kötelező mezők" }, diff --git a/_locales/id/messages.json b/_locales/id/messages.json index 084d9bba..865d4be1 100644 --- a/_locales/id/messages.json +++ b/_locales/id/messages.json @@ -107,6 +107,9 @@ "customPrompts_form_label_use_diff_viewer_title": { "message": "Penampil perbandingan teks dapat dipilih ketika tindakan diatur ke \"Ganti teks\"." }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Hapus gambar tersemat dari HTML prompt" + }, "customPrompts_form_required_fields": { "message": "Kolom wajib diisi" }, diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 7c449957..7d9a96d1 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -93,6 +93,9 @@ "customPrompts_form_label_need_custom_text": { "message": "Chiedi un testo aggiuntivo" }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Rimuovi immagini incorporate dall'HTML del prompt" + }, "customPrompts_form_label_enabled": { "message": "Attivo" }, diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index b687fb24..dcb4205c 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -107,6 +107,9 @@ "customPrompts_form_label_use_diff_viewer_title": { "message": "テキスト比較ビューアは、アクションが「テキストを置換」に設定されている場合に選択できます。" }, + "customPrompts_form_label_clean_embedded_images": { + "message": "プロンプトHTMLから埋め込み画像を削除" + }, "customPrompts_form_required_fields": { "message": "必須フィールド" }, diff --git a/_locales/nb_NO/messages.json b/_locales/nb_NO/messages.json index 0967ef42..2c48b3f1 100644 --- a/_locales/nb_NO/messages.json +++ b/_locales/nb_NO/messages.json @@ -1 +1,5 @@ -{} +{ + "customPrompts_form_label_clean_embedded_images": { + "message": "Fjern innebygde bilder fra prompt-HTML" + } +} \ No newline at end of file diff --git a/_locales/nl/messages.json b/_locales/nl/messages.json index 79cc2215..16c2e2ef 100644 --- a/_locales/nl/messages.json +++ b/_locales/nl/messages.json @@ -1,5 +1,8 @@ { "extensionDescription": { "message": "Gebruik ChatGPT, Google Gemini, Claude of Ollama om uw emails te verbeteren." + }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Ingebedde afbeeldingen verwijderen uit prompt-HTML" } -} +} \ No newline at end of file diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index 89188a14..7bbbfe67 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -96,6 +96,9 @@ "customPrompts_form_label_enabled": { "message": "Włączone" }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Usuń osadzone obrazy z HTML prompta" + }, "customPrompts_form_required_fields": { "message": "Pola wymagane" }, diff --git a/_locales/pt-br/messages.json b/_locales/pt-br/messages.json index 6e3d4cfc..05f4eef0 100644 --- a/_locales/pt-br/messages.json +++ b/_locales/pt-br/messages.json @@ -96,6 +96,9 @@ "customPrompts_form_label_enabled": { "message": "Ativado" }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Remover imagens incorporadas do HTML do prompt" + }, "customPrompts_form_required_fields": { "message": "Campos obrigatórios" }, diff --git a/_locales/pt/messages.json b/_locales/pt/messages.json index fe127e3a..e81f3f4b 100644 --- a/_locales/pt/messages.json +++ b/_locales/pt/messages.json @@ -68,6 +68,9 @@ "customPrompts_form_label_enabled": { "message": "Ativo" }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Remover imagens incorporadas do HTML do prompt" + }, "customPrompts_form_required_fields": { "message": "Campos obrigatórios" }, diff --git a/_locales/ro/messages.json b/_locales/ro/messages.json index e92c8ac0..6ff2f3e3 100644 --- a/_locales/ro/messages.json +++ b/_locales/ro/messages.json @@ -77,6 +77,9 @@ "customPrompts_form_label_need_custom_text": { "message": "Solicită text suplimentar" }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Elimină imaginile încorporate din HTML-ul promptului" + }, "customPrompts_form_required_fields": { "message": "Câmpuri necesare" }, diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index 43e8f7d0..302e2415 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -98,6 +98,9 @@ "customPrompts_form_label_use_diff_viewer_title": { "message": "Средство просмотра для сравнения текста можно выбрать, если для действия установлено значение \"Заменить текст\"." }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Удалять встроенные изображения из HTML промпта" + }, "customPrompts_form_required_fields": { "message": "Обязательные поля" }, diff --git a/_locales/sk/messages.json b/_locales/sk/messages.json index 0967ef42..5de3defa 100644 --- a/_locales/sk/messages.json +++ b/_locales/sk/messages.json @@ -1 +1,5 @@ -{} +{ + "customPrompts_form_label_clean_embedded_images": { + "message": "Odstrániť vložené obrázky z HTML výzvy" + } +} \ No newline at end of file diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index e5c41d4c..15ce544b 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -80,6 +80,9 @@ "customPrompts_reloading_menus": { "message": "Laddar om menyer..." }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Ta bort inbäddade bilder från prompt-HTML" + }, "customPrompts_form_required_fields": { "message": "Obligatoriska fält" }, diff --git a/_locales/tr/messages.json b/_locales/tr/messages.json index 0127f024..28e69822 100644 --- a/_locales/tr/messages.json +++ b/_locales/tr/messages.json @@ -16,5 +16,8 @@ }, "customPrompts_start_saving": { "message": "Promptlar kaydediliyor…" + }, + "customPrompts_form_label_clean_embedded_images": { + "message": "Gömülü görselleri prompt HTML'sinden kaldır" } -} +} \ No newline at end of file diff --git a/_locales/zh_Hans/messages.json b/_locales/zh_Hans/messages.json index f04910b1..82dd170a 100644 --- a/_locales/zh_Hans/messages.json +++ b/_locales/zh_Hans/messages.json @@ -182,6 +182,9 @@ "customPrompts_form_label_need_custom_text": { "message": "要求补充文字" }, + "customPrompts_form_label_clean_embedded_images": { + "message": "从提示词 HTML 中移除嵌入的图片" + }, "customPrompts_form_required_fields": { "message": "必填字段" }, diff --git a/_locales/zh_Hant/messages.json b/_locales/zh_Hant/messages.json index 89d03bba..c20e3df6 100644 --- a/_locales/zh_Hant/messages.json +++ b/_locales/zh_Hant/messages.json @@ -92,6 +92,9 @@ "customPrompts_form_label_use_diff_viewer": { "message": "啟用文字比較檢視器" }, + "customPrompts_form_label_clean_embedded_images": { + "message": "從提示詞 HTML 中移除嵌入的圖片" + }, "customPrompts_form_required_fields": { "message": "必填欄位" }, diff --git a/api_webchat/streamingMessage.js b/api_webchat/streamingMessage.js index c4cad912..66bb391f 100644 --- a/api_webchat/streamingMessage.js +++ b/api_webchat/streamingMessage.js @@ -101,7 +101,7 @@ export class StreamingMessage { this._thinkingAccumulator = ''; // Convert Markdown to DOM nodes using the markdown-it library - const md = window.markdownit(); + const md = window.markdownit({ html: true }); const html = md.render(fullText); this._fullTextHTML += html; diff --git a/claude-spec/02-prompts.md b/claude-spec/02-prompts.md index 6dc78b5a..c3dc1d73 100644 --- a/claude-spec/02-prompts.md +++ b/claude-spec/02-prompts.md @@ -23,6 +23,7 @@ Prompts are the core user-facing feature of ThunderAI. Each prompt defines an AI | `need_custom_text` | string | `"0"` = no custom input, `"1"` = show custom text input field | | `define_response_lang` | string | `"0"` = no language hint, `"1"` = append response language instruction | | `use_diff_viewer` | string | `"0"` = normal output, `"1"` = show diff viewer (ChatGPT Web only) | +| `clean_embedded_images` | string | `"0"` = keep raw embedded images, `"1"` = tokenize embedded images in HTML placeholders and restore them in AI output | > **Note:** These numeric-looking properties are stored as **strings** (`"0"`/`"1"`/`"2"`) in the prompt objects in `js/mzta-prompts.js`, not as JS numbers. The prompt body lives in the `text` property (there is no `prompt` property). diff --git a/claude-spec/03-placeholders.md b/claude-spec/03-placeholders.md index fa18a31e..54d044e3 100644 --- a/claude-spec/03-placeholders.md +++ b/claude-spec/03-placeholders.md @@ -31,7 +31,8 @@ placeholders with `is_dynamic: "1"` (take a parameter after `:`). |----|-------------|------|-----| | `mail_text_body` | Full plain text of the email | 0 | | | `mail_html_body` | Full HTML of the email | 0 | | -| `mail_typed_text` | Text typed so far in compose window | 2 | | +| `mail_typed_text` | Text typed so far in compose window (plain text) | 2 | | +| `mail_typed_html` | HTML formatted text typed so far in compose window. *Tip: Recommended system prompt: "Return directly in HTML code when provided with HTML code as email text"* | 2 | | | `mail_quoted_text` | Quoted text in the compose window | 2 | | | `mail_subject` | Email subject line | 0 | | | `mail_folder_name` | Name of the folder containing the email | 1 | | diff --git a/js/mzta-compose-script.js b/js/mzta-compose-script.js index 42b2ff45..b3c60985 100644 --- a/js/mzta-compose-script.js +++ b/js/mzta-compose-script.js @@ -302,9 +302,43 @@ switch (message.command) { } const r = sel.getRangeAt(0); r.deleteContents(); + + let cleanText = message.text || ''; + // Strip markdown code fences if response is wrapped in ```html ... ``` or ``` ... ``` + const codeBlockMatch = cleanText.trim().match(/^```(?:html)?\s*\n?([\s\S]*?)\n?```$/i); + if (codeBlockMatch) { + cleanText = codeBlockMatch[1]; + } + + // Decode HTML entities if text contains escaped tags (e.g. <p> or <div>) + while (cleanText.includes('<') && (cleanText.includes('>') || cleanText.includes('"'))) { + const txt = document.createElement('textarea'); + txt.innerHTML = cleanText; + const decoded = txt.value; + if (decoded === cleanText) break; + cleanText = decoded; + } + const parser = new DOMParser(); - const doc = parser.parseFromString(message.text, 'text/html'); - r.insertNode(doc.body); + let doc = parser.parseFromString(cleanText, 'text/html'); + + // Remove UI elements (buttons, SVGs, forms, scripts, inputs, navigation, etc.) + doc.querySelectorAll('button, svg, script, style, form, input, textarea, header, footer, nav, use').forEach(el => el.remove()); + + // If ChatGPT/webchat wrapper container (.markdown or [data-message-author-role]) is present, extract content directly from it + let targetContainer = doc.querySelector('.markdown') || doc.querySelector('[data-message-author-role="assistant"]') || doc.body; + + // If targetContainer contains literal string HTML tags like "

", re-parse its textContent into real DOM elements + if (/^\s*<[a-z][^>]*>/i.test(targetContainer.textContent.trim())) { + const innerDoc = parser.parseFromString(targetContainer.textContent.trim(), 'text/html'); + targetContainer = innerDoc.body; + } + + const fragment = document.createDocumentFragment(); + while (targetContainer.firstChild) { + fragment.appendChild(targetContainer.firstChild); + } + r.insertNode(fragment); browser.runtime.sendMessage({command: "compose_reloadBody", tabId: message.tabId}); return Promise.resolve(true); } @@ -375,6 +409,57 @@ switch (message.command) { return Promise.resolve(t); } + case "getOnlyTypedHtml": { + const children = window.document.body.childNodes; + const selection = window.getSelection(); + + let firstNode = null; + let lastNode = null; + const container = document.createElement('div'); + + for (const node of children) { + if (node instanceof Element) { + if (node.classList.contains('moz-cite-prefix') || node.classList.contains('moz-forward-container')) { + break; + } + if (MZTA_INJECTED_SELECTORS.some(sel => node.matches && node.matches(sel))) { + continue; + } + } + container.appendChild(node.cloneNode(true)); + + if (!firstNode) { + firstNode = node; + } + if (node.textContent && node.textContent.trim() !== '') { + lastNode = node; + } + } + + if (!lastNode) { + lastNode = firstNode; + } + + if (message.do_autoselect && firstNode && lastNode) { + const range = document.createRange(); + range.setStartBefore(firstNode); + range.setEndAfter(lastNode); + selection.removeAllRanges(); + selection.addRange(range); + } + + let resultHtml = container.innerHTML; + // Un-nest ProseMirror or editor wrapper elements if present + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = resultHtml; + const pmWrapper = tempDiv.querySelector('.ProseMirror'); + if (pmWrapper) { + resultHtml = pmWrapper.innerHTML; + } + + return Promise.resolve(resultHtml); + } + case "getOnlyQuotedText": { let t = ''; const children = window.document.body.childNodes; diff --git a/js/mzta-menus.js b/js/mzta-menus.js index 07a779c1..465f9318 100644 --- a/js/mzta-menus.js +++ b/js/mzta-menus.js @@ -103,6 +103,7 @@ export class mzta_Menus { text: cleanupNewlines(await browser.tabs.sendMessage(tabs[0].id, { command: "getTextOnly" })), html: convertNewlinesToBr(await browser.tabs.sendMessage(tabs[0].id, { command: "getFullHtml" })), only_typed_text: cleanupNewlines(await browser.tabs.sendMessage(tabs[0].id, { command: "getOnlyTypedText", do_autoselect: do_autoselect })), + only_typed_html: await browser.tabs.sendMessage(tabs[0].id, { command: "getOnlyTypedHtml", do_autoselect: do_autoselect }), only_quoted_text: cleanupNewlines(await browser.tabs.sendMessage(tabs[0].id, { command: "getOnlyQuotedText" })) }; }; @@ -110,7 +111,7 @@ export class mzta_Menus { curr_menu_entry.act = async () => { taWorkingStatus.startWorking(); const tabs = await browser.tabs.query({ active: true, currentWindow: true }); - const msg_text = await getMailBody(tabs, placeholdersUtils.hasPlaceholder(curr_prompt.text,'mail_typed_text')); + const msg_text = await getMailBody(tabs, placeholdersUtils.hasPlaceholder(curr_prompt.text,'mail_typed_text') || placeholdersUtils.hasPlaceholder(curr_prompt.text,'mail_typed_html')); if (curr_prompt.id === 'prompt_get_calendar_event_from_clipboard') { try { @@ -144,12 +145,17 @@ export class mzta_Menus { let selection_text = ''; let selection_html = msg_text.selection_html; let only_typed_text = ''; + let only_typed_html = ''; let only_quoted_text = ''; only_typed_text = msg_text.only_typed_text.replace(/[ \t]+/g, ' ').trim(); + only_typed_html = msg_text.only_typed_html || ''; selection_text = msg_text.selection.replace(/[ \t]+/g, ' ').trim(); if(selection_text === ''){ if(placeholdersUtils.hasPlaceholder(curr_prompt.text, "mail_typed_text")){ selection_text = only_typed_text; + } else if(placeholdersUtils.hasPlaceholder(curr_prompt.text, "mail_typed_html")){ + selection_text = only_typed_text; + selection_html = only_typed_html; } } only_quoted_text = msg_text.only_quoted_text.replace(/[ \t]+/g, ' ').trim(); @@ -190,6 +196,7 @@ export class mzta_Menus { subject_text: await getMailSubject(tabs[0]), msg_text: msg_text, only_typed_text: only_typed_text, + only_typed_html: only_typed_html, only_quoted_text: only_quoted_text, tags_full_list: tags_full_list }); diff --git a/js/mzta-placeholders.js b/js/mzta-placeholders.js index f3faecf9..c1d5092c 100644 --- a/js/mzta-placeholders.js +++ b/js/mzta-placeholders.js @@ -19,7 +19,8 @@ import { prefs_default } from '../options/mzta-options-default.js'; import { getMailHeader, - sanitizeMailHeaders + sanitizeMailHeaders, + tokenizeHtmlImages } from './mzta-utils.js'; /* ================= PLACEHOLDERS PROPERTIES ======================================== @@ -82,6 +83,15 @@ const defaultPlaceholders = [ is_dynamic: "0", enabled: 1, }, + { + id: 'mail_typed_html', + name: "__MSG_placeholder_mail_typed_html__", + default_value: "", + type: 2, + is_default: "1", + is_dynamic: "0", + enabled: 1, + }, { id: 'mail_quoted_text', name: "__MSG_placeholder_mail_quoted_text__", @@ -536,20 +546,47 @@ export const placeholdersUtils = { async getPlaceholdersValues(args) { const { prompt_text = "", + curr_prompt = {}, curr_message = {}, mail_subject = "", body_text = "", msg_text = {}, only_typed_text = "", + only_typed_html = "", only_quoted_text = "", selection_text = "", selection_html = "", tags_full_list = ["", []] } = args || {}; + + let activeImageMap = {}; + const shouldCleanImages = String(curr_prompt?.clean_embedded_images) !== "0"; + + let htmlBody = msg_text?.html || ""; + let typedHtml = only_typed_html || ""; + let selHtml = selection_html || ""; + + if (shouldCleanImages) { + if (htmlBody) { + const res = tokenizeHtmlImages(htmlBody, activeImageMap); + htmlBody = res.cleanHtml; + } + if (typedHtml) { + const res = tokenizeHtmlImages(typedHtml, activeImageMap); + typedHtml = res.cleanHtml; + } + if (selHtml) { + const res = tokenizeHtmlImages(selHtml, activeImageMap); + selHtml = res.cleanHtml; + } + } + let currPHs = await placeholdersUtils.extractPlaceholders(prompt_text); // console.log(">>>>>>>>>> currPHs: " + JSON.stringify(currPHs)); // console.log(">>>>>>>>>> curr_message: " + JSON.stringify(curr_message)); let finalSubs = {}; + finalSubs._imageMap = activeImageMap; + for(let currPH of currPHs){ // console.log(">>>>>>>>>> currPH: " + JSON.stringify(currPH)); switch(currPH.id){ @@ -557,11 +594,14 @@ export const placeholdersUtils = { finalSubs['mail_text_body'] = placeholdersUtils.failSafePlaceholders(body_text); break; case 'mail_html_body': - finalSubs['mail_html_body'] = placeholdersUtils.failSafePlaceholders(msg_text?.html); + finalSubs['mail_html_body'] = placeholdersUtils.failSafePlaceholders(htmlBody); break; case 'mail_typed_text': finalSubs['mail_typed_text'] = placeholdersUtils.failSafePlaceholders(only_typed_text); break; + case 'mail_typed_html': + finalSubs['mail_typed_html'] = placeholdersUtils.failSafePlaceholders(typedHtml); + break; case 'mail_quoted_text': finalSubs['mail_quoted_text'] = placeholdersUtils.failSafePlaceholders(only_quoted_text); break; @@ -584,13 +624,13 @@ export const placeholdersUtils = { finalSubs['selected_text'] = placeholdersUtils.failSafePlaceholders(selection_text); break; case 'selected_html': - finalSubs['selected_html'] = placeholdersUtils.failSafePlaceholders(selection_html); + finalSubs['selected_html'] = placeholdersUtils.failSafePlaceholders(selHtml); break; case 'mail_text_body_or_selected': finalSubs['mail_text_body_or_selected'] = placeholdersUtils.failSafePlaceholders(selection_text || body_text); break; case 'mail_html_body_or_selected': - finalSubs['mail_html_body_or_selected'] = placeholdersUtils.failSafePlaceholders(selection_html || msg_text?.html); + finalSubs['mail_html_body_or_selected'] = placeholdersUtils.failSafePlaceholders(selHtml || htmlBody); break; case 'author': finalSubs['author'] = placeholdersUtils.failSafePlaceholders(sanitizeMailHeaders(curr_message.author)); diff --git a/js/mzta-prompts.js b/js/mzta-prompts.js index 1e83b545..f6c6c026 100644 --- a/js/mzta-prompts.js +++ b/js/mzta-prompts.js @@ -52,6 +52,11 @@ 0: Do not use the diff viewer 1: Use the diff viewer + Clean embedded images (clean_embedded_images attribute): + 0: Keep raw embedded images in HTML placeholders + 1: Remove base64 embedded images from HTML placeholders and restore them in response + + ================ DYNAMIC PROPERTIES (set at runtime via prompt_info) headerMessageId (set by _openSummaryWebchat in mzta-background.js): @@ -108,6 +113,7 @@ const defaultPrompts = [ need_custom_text: "0", define_response_lang: "1", use_diff_viewer: "0", + clean_embedded_images: "1", chatgpt_web_model: '', chatgpt_web_project: '', chatgpt_web_custom_gpt: '', @@ -127,6 +133,7 @@ const defaultPrompts = [ need_custom_text: "0", define_response_lang: "1", use_diff_viewer: "0", + clean_embedded_images: "1", chatgpt_web_model: '', chatgpt_web_project: '', chatgpt_web_custom_gpt: '', @@ -146,6 +153,7 @@ const defaultPrompts = [ need_custom_text: "1", define_response_lang: "1", use_diff_viewer: "0", + clean_embedded_images: "1", chatgpt_web_model: '', chatgpt_web_project: '', chatgpt_web_custom_gpt: '', @@ -165,6 +173,7 @@ const defaultPrompts = [ need_custom_text: "0", define_response_lang: "1", use_diff_viewer: "1", + clean_embedded_images: "1", chatgpt_web_model: '', chatgpt_web_project: '', chatgpt_web_custom_gpt: '', @@ -184,6 +193,7 @@ const defaultPrompts = [ need_custom_text: "0", define_response_lang: "1", use_diff_viewer: "1", + clean_embedded_images: "1", chatgpt_web_model: '', chatgpt_web_project: '', chatgpt_web_custom_gpt: '', @@ -203,6 +213,7 @@ const defaultPrompts = [ need_custom_text: "0", define_response_lang: "1", use_diff_viewer: "0", + clean_embedded_images: "1", chatgpt_web_model: '', chatgpt_web_project: '', chatgpt_web_custom_gpt: '', @@ -222,6 +233,7 @@ const defaultPrompts = [ need_custom_text: "0", define_response_lang: "1", use_diff_viewer: "1", + clean_embedded_images: "1", chatgpt_web_model: '', chatgpt_web_project: '', chatgpt_web_custom_gpt: '', @@ -241,6 +253,7 @@ const defaultPrompts = [ need_custom_text: "0", define_response_lang: "0", use_diff_viewer: "0", + clean_embedded_images: "1", chatgpt_web_model: '', chatgpt_web_project: '', chatgpt_web_custom_gpt: '', @@ -469,7 +482,7 @@ export function preparePromptsForExport(prompts, include_api_settings = false){ } if(prompt.is_default == 1){ - let allowedKeys = ['id', 'enabled', 'position_compose', 'position_display', 'position_context', 'need_custom_text', 'show_in', 'custom_icon']; + let allowedKeys = ['id', 'enabled', 'position_compose', 'position_display', 'position_context', 'need_custom_text', 'clean_embedded_images', 'show_in', 'custom_icon']; if(include_api_settings){ allowedKeys.push('api_type'); for (const [integration, options] of Object.entries(integration_options_config)) { @@ -537,6 +550,7 @@ async function getDefaultPrompts_withProps() { prompt.position_context = prefs._default_prompts_properties[prompt.id]?.position_context || prompt.position_display; prompt.enabled = prefs._default_prompts_properties[prompt.id].enabled; prompt.need_custom_text = prefs._default_prompts_properties[prompt.id].need_custom_text; + prompt.clean_embedded_images = prefs._default_prompts_properties[prompt.id]?.clean_embedded_images ?? prompt.clean_embedded_images ?? "1"; prompt.chatgpt_web_model = prefs._default_prompts_properties[prompt.id].chatgpt_web_model; prompt.chatgpt_web_project = prefs._default_prompts_properties[prompt.id].chatgpt_web_project; prompt.chatgpt_web_custom_gpt = (prefs._default_prompts_properties[prompt.id]?.chatgpt_web_custom_gpt || '').trim(); @@ -567,6 +581,9 @@ async function getCustomPrompts() { if (prompt.use_diff_viewer === undefined) { prompt.use_diff_viewer = "0"; } + if (prompt.clean_embedded_images === undefined) { + prompt.clean_embedded_images = "1"; + } if(prompt.chatgpt_web_model === undefined){ prompt.chatgpt_web_model = ""; } @@ -599,6 +616,7 @@ export async function setDefaultPromptsProperties(prompts) { position_context: (prompt.position_context === undefined || prompt.position_context === "undefined") ? "" : prompt.position_context, enabled: (prompt.enabled === undefined || prompt.enabled === "undefined") ? "" : prompt.enabled, need_custom_text: (prompt.need_custom_text === undefined || prompt.need_custom_text === "undefined") ? "" : prompt.need_custom_text, + clean_embedded_images: (prompt.clean_embedded_images === undefined || prompt.clean_embedded_images === "undefined") ? "1" : prompt.clean_embedded_images, chatgpt_web_model: (prompt.chatgpt_web_model === undefined || prompt.chatgpt_web_model === "undefined") ? "" : prompt.chatgpt_web_model, chatgpt_web_project: (prompt.chatgpt_web_project === undefined || prompt.chatgpt_web_project === "undefined") ? "" : prompt.chatgpt_web_project, chatgpt_web_custom_gpt: (prompt.chatgpt_web_custom_gpt === undefined || prompt.chatgpt_web_custom_gpt === "undefined") ? "" : prompt.chatgpt_web_custom_gpt, diff --git a/js/mzta-utils-prompt.js b/js/mzta-utils-prompt.js index 8d0ca1be..a19cb397 100644 --- a/js/mzta-utils-prompt.js +++ b/js/mzta-utils-prompt.js @@ -26,6 +26,7 @@ import { getSpecialPrompts } from './mzta-prompts.js'; import { prefs_default } from '../options/mzta-options-default.js'; export const taPromptUtils = { + lastImageMap: {}, async getDefaultSignature(){ let prefs = await browser.storage.sync.get({ default_sign_name: prefs_default.default_sign_name }); @@ -47,6 +48,7 @@ export const taPromptUtils = { subject_text = '', msg_text = {}, only_typed_text = '', + only_typed_html = '', only_quoted_text = '', tags_full_list = ["", []] } = args || {}; @@ -73,16 +75,19 @@ export const taPromptUtils = { let finalSubs = await placeholdersUtils.getPlaceholdersValues({ prompt_text: curr_prompt.text, + curr_prompt: curr_prompt, curr_message: curr_message, mail_subject: subject_text, body_text: body_text, msg_text: msg_text, only_typed_text: only_typed_text, + only_typed_html: only_typed_html, only_quoted_text: only_quoted_text, selection_text: selection_text, selection_html: selection_html, tags_full_list: tags_full_list }); + taPromptUtils.lastImageMap = finalSubs._imageMap || {}; let prefs_ph = await browser.storage.sync.get({ placeholders_use_default_value: prefs_default.placeholders_use_default_value }); fullPrompt = (placeholdersUtils.replacePlaceholders({ text: curr_prompt.text, @@ -187,9 +192,11 @@ export const taPromptUtils = { const finalSubs = await placeholdersUtils.getPlaceholdersValues({ prompt_text: promptText, + curr_prompt: prompt, msg_text: { html: bodyHtml.html, text: bodyHtml.text }, mail_subject: mailSubject, }); + taPromptUtils.lastImageMap = finalSubs._imageMap || {}; const fullPrompt = placeholdersUtils.replacePlaceholders({ text: promptText, diff --git a/js/mzta-utils.js b/js/mzta-utils.js index 6668cfde..cbdc5afc 100644 --- a/js/mzta-utils.js +++ b/js/mzta-utils.js @@ -959,3 +959,78 @@ export async function* getMessages(list) { } } } + +/** + * Scans HTML for elements with base64/data URIs or very long src attributes, + * replacing them with compact token placeholders [[THUNDERAI_IMG_N]] and returning + * a map to restore them later. + * + * @param {string} html + * @returns {{ cleanHtml: string, imageMap: Object }} + */ +export function tokenizeHtmlImages(html, imageMap = {}) { + if (!html || typeof html !== 'string') { + return { cleanHtml: html || '', imageMap }; + } + + const htmlLower = html.toLowerCase(); + if (!htmlLower.includes('data:image/') && !htmlLower.includes(' { + const src = img.getAttribute('src') || ''; + if (src.startsWith('data:image/') || src.length > 200) { + const token = `[[THUNDERAI_IMG_${counter++}]]`; + imageMap[token] = img.outerHTML; + const textNode = doc.createTextNode(token); + img.parentNode.replaceChild(textNode, img); + } + }); + + return { cleanHtml: doc.body.innerHTML, imageMap }; + } catch (e) { + // Fallback to regex if DOMParser fails + } + } + + // Regex fallback: match full tags that contain a data:image/ URI + let cleanHtml = html.replace(/]*src=["'](data:image\/[^"']+)["'][^>]*\s*\/?>/gi, (fullMatch) => { + const token = `[[THUNDERAI_IMG_${counter++}]]`; + imageMap[token] = fullMatch; + return token; + }); + + return { cleanHtml, imageMap }; +} + +/** + * Restores original tags in response text by replacing token placeholders + * [[THUNDERAI_IMG_N]] with original image HTML from imageMap. + * + * @param {string} text + * @param {Object} imageMap + * @returns {string} + */ +export function restoreHtmlImages(text, imageMap) { + if (!text || typeof text !== 'string' || !imageMap || Object.keys(imageMap).length === 0) { + return text || ''; + } + + let restored = text; + for (const [token, originalTag] of Object.entries(imageMap)) { + if (restored.includes(token)) { + restored = restored.split(token).join(originalTag); + } + } + return restored; +} + diff --git a/mzta-background.js b/mzta-background.js index 5f8330dd..4f584a76 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -49,6 +49,7 @@ import { convertNewlinesToParagraphs, getConnectionType, hasSpecificIntegration, + restoreHtmlImages, } from './js/mzta-utils.js'; import { taPromptUtils } from './js/mzta-utils-prompt.js'; import { mzta_specialCommand } from './js/mzta-special-commands.js'; @@ -455,7 +456,7 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { async function _replaceSelectedText(tabId, text) { //console.log('chatgpt_replaceSelectedText: [' + tabId +'] ' + text) taLog.log("chatgpt_replaceSelectedText text: " + text); - original_html = await getOriginalBody(tabId); + text = restoreHtmlImages(text, taPromptUtils.lastImageMap); let prefs_repl = await browser.storage.sync.get({composing_plain_text: prefs_default.composing_plain_text}); if(prefs_repl.composing_plain_text){ text = stripHtmlKeepLines(text); @@ -469,6 +470,7 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => { let paragraphsHtmlString = message.text; //console.log(">>>>>>>>>>>> paragraphsHtmlString: " + paragraphsHtmlString); taLog.log("paragraphsHtmlString: " + paragraphsHtmlString); + paragraphsHtmlString = restoreHtmlImages(paragraphsHtmlString, taPromptUtils.lastImageMap); let prefs_reply = await browser.storage.sync.get({reply_type: prefs_default.reply_type, composing_plain_text: prefs_default.composing_plain_text}); if(prefs_reply.composing_plain_text){ paragraphsHtmlString = stripHtmlKeepLines(paragraphsHtmlString); diff --git a/pages/customprompts/mzta-custom-prompts.html b/pages/customprompts/mzta-custom-prompts.html index 3f9eefd2..0afdef22 100644 --- a/pages/customprompts/mzta-custom-prompts.html +++ b/pages/customprompts/mzta-custom-prompts.html @@ -79,6 +79,8 @@

__MSG_customPrompts_managePrompts__


+
+ diff --git a/pages/customprompts/mzta-custom-prompts.js b/pages/customprompts/mzta-custom-prompts.js index e713e5cc..70b55cd3 100644 --- a/pages/customprompts/mzta-custom-prompts.js +++ b/pages/customprompts/mzta-custom-prompts.js @@ -263,6 +263,7 @@ document.addEventListener('DOMContentLoaded', async () => { need_custom_text: (checkboxNeedCustomTextNew.checked) ? 1 : 0, define_response_lang: (checkboxDefineResponseLangNew.checked) ? 1 : 0, use_diff_viewer: (checkboxUseDiffViewerNew.checked) ? 1 : 0, + clean_embedded_images: (checkboxCleanEmbeddedImagesNew.checked) ? 1 : 0, enabled: 1, position_compose: positionMax_compose + 1, position_display: positionMax_display + 1, @@ -675,6 +676,7 @@ function showItemRowEditor(tr) { tr.querySelector('input.need_custom_text').disabled = false; tr.querySelector('input.define_response_lang').disabled = false; tr.querySelector('input.use_diff_viewer').disabled = false; + tr.querySelector('input.clean_embedded_images').disabled = false; } function hideItemRowEditor(tr) { @@ -702,6 +704,7 @@ function hideItemRowEditor(tr) { tr.querySelector('input.need_custom_text').disabled = true; tr.querySelector('input.define_response_lang').disabled = true; tr.querySelector('input.use_diff_viewer').disabled = true; + tr.querySelector('input.clean_embedded_images').disabled = true; } function toggleAdditionalPropertiesShow(tr) { @@ -891,6 +894,7 @@ function handleConfirmClick(e) { newValues.need_custom_text = tr.querySelector('.need_custom_text').checked ? 1 : 0; newValues.define_response_lang = tr.querySelector('.define_response_lang').checked ? 1 : 0; newValues.use_diff_viewer = tr.querySelector('.use_diff_viewer').checked ? 1 : 0; + newValues.clean_embedded_images = tr.querySelector('.clean_embedded_images').checked ? 1 : 0; newValues.enabled = tr.querySelector('.enabled').checked ? 1 : 0; newValues.chatgpt_web_model = tr.querySelector('.chatgpt_web_model_output').value.trim(); newValues.chatgpt_web_project = tr.querySelector('.chatgpt_web_project_output').value.trim(); @@ -975,6 +979,7 @@ function handleCopyClick(e) { let need_custom_text = tr.querySelector('.need_custom_text').checked; let define_response_lang = tr.querySelector('.define_response_lang').checked; let use_diff_viewer = tr.querySelector('.use_diff_viewer').checked; + let clean_embedded_images = tr.querySelector('.clean_embedded_images').checked; let chatgpt_web_model = tr.querySelector('.chatgpt_web_model_output').value; let chatgpt_web_project = tr.querySelector('.chatgpt_web_project_output').value; @@ -999,6 +1004,7 @@ function handleCopyClick(e) { let checkboxUseDiffViewerNew = document.getElementById('checkboxUseDiffViewerNew'); checkboxUseDiffViewerNew.checked = use_diff_viewer; checkboxUseDiffViewerNew.disabled = (action !== "2"); + document.getElementById('checkboxCleanEmbeddedImagesNew').checked = clean_embedded_images; document.getElementById('chatGPTWebModelNew').value = chatgpt_web_model; document.getElementById('chatGPTWebProjectNew').value = chatgpt_web_project; @@ -1057,7 +1063,7 @@ function loadPromptsList(values){ } let options = { - valueNames: [ { data: ['idnum'] }, 'is_default', 'id', 'name', 'text', 'type', 'action', 'position_compose', 'position_display', 'show_in', { name: 'need_selected', attr: 'checked_val'}, { name: 'need_signature', attr: 'checked_val'}, { name: 'need_custom_text', attr: 'checked_val'}, { name: 'define_response_lang', attr: 'checked_val'}, { name: 'use_diff_viewer', attr: 'checked_val'}, { name: 'enabled', attr: 'checked_val'}, 'api_type', ...api_fields ], + valueNames: [ { data: ['idnum'] }, 'is_default', 'id', 'name', 'text', 'type', 'action', 'position_compose', 'position_display', 'show_in', { name: 'need_selected', attr: 'checked_val'}, { name: 'need_signature', attr: 'checked_val'}, { name: 'need_custom_text', attr: 'checked_val'}, { name: 'define_response_lang', attr: 'checked_val'}, { name: 'use_diff_viewer', attr: 'checked_val'}, { name: 'clean_embedded_images', attr: 'checked_val'}, { name: 'enabled', attr: 'checked_val'}, 'api_type', ...api_fields ], item: function(values) { let type_output = ''; switch(String(values.type)){ @@ -1174,6 +1180,8 @@ function loadPromptsList(values){

+ +
@@ -1369,6 +1377,7 @@ function checkSelectedBoxes(checkboxes = null) { ...document.querySelectorAll('.need_custom_text[type="checkbox"]'), ...document.querySelectorAll('.define_response_lang[type="checkbox"]'), ...document.querySelectorAll('.use_diff_viewer[type="checkbox"]'), + ...document.querySelectorAll('.clean_embedded_images[type="checkbox"]'), ...document.querySelectorAll('.enabled[type="checkbox"]'), ]; }