mail translation first try. see #247
This commit is contained in:
parent
681f0f0f18
commit
32abc00693
17 changed files with 1316 additions and 27 deletions
|
|
@ -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": ""
|
||||
|
|
|
|||
|
|
@ -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:<headerMessageId>` 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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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" });
|
||||
browser.runtime.sendMessage({ command: "initSummary" });
|
||||
browser.runtime.sendMessage({ command: "initTranslation" });
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Object>} 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.
|
||||
|
|
|
|||
150
js/mzta-translationstore.js
Normal file
150
js/mzta-translationstore.js
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,6 +203,17 @@
|
|||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="translate_tr">
|
||||
<td><span class="opt_title">__MSG_prefs_OptionText_translate__</span>
|
||||
<br><button id="btnManageTranslateInfo" class="btn_small">__MSG_prefs_OptionText_btnManageTranslateInfo__</button></td>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" id="translate" name="translate" class="option-input" />
|
||||
__MSG_prefs_OptionText_translate_Info__
|
||||
<br><span class="warn_API_needed" id="translate_warn_API_needed">__MSG_warn_API_needed__</span>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="get_calendar_event_tr">
|
||||
<td><span class="opt_title">__MSG_prefs_OptionText_get_calendar_event__</span>
|
||||
<br><button id="btnManageCalendarEventInfo" class="btn_small">__MSG_prefs_OptionText_btnManageCalendarEventInfo__</button></td>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
180
pages/translate/mzta-translate.css
Normal file
180
pages/translate/mzta-translate.css
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
97
pages/translate/mzta-translate.html
Normal file
97
pages/translate/mzta-translate.html
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>ThunderAI - __MSG_Translate_PageTitle__</title>
|
||||
<link rel="stylesheet" href="mzta-translate.css" />
|
||||
<link rel="stylesheet" href="../_lib/connection-ui.css">
|
||||
<link rel="icon" href="../../images/icon-16px.png" />
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<h1 class="page_title">__MSG_Translate_PageTitle__</h1>
|
||||
<p>__MSG_Translate_info_default__</p>
|
||||
</div>
|
||||
<div class="translate_table_title section_title">__MSG_Translate_prompt_prefs_title__</div>
|
||||
<table id="miczPrefs">
|
||||
<tr class="translate_tr specific_integration">
|
||||
<td><span class="opt_title">__MSG_prefs_OptionText_use_specific_integration__</span>
|
||||
</td>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox" id="translate_use_specific_integration" name="translate_use_specific_integration" class="option-input" />
|
||||
__MSG_prefs_OptionText_translate_use_specific_integration_Info__
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="connection_ui_anchor"></tr>
|
||||
<tr id="connection_ui_end"><td colspan="2"></td></tr>
|
||||
<tr class="translate_tr">
|
||||
<td><span class="opt_title">__MSG_prefs_OptionText_translate_auto__</span></td>
|
||||
<td>
|
||||
<label>
|
||||
<select id="translate_auto" name="translate_auto" class="option-input">
|
||||
<option value="0">__MSG_prefs_OptionText_translate_auto_disabled__</option>
|
||||
<option value="1">__MSG_prefs_OptionText_translate_auto_manual__</option>
|
||||
<option value="2">__MSG_prefs_OptionText_translate_auto_automatic__</option>
|
||||
</select>
|
||||
<br>__MSG_prefs_OptionText_translate_auto_Info__
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="translate_tr">
|
||||
<td><span class="opt_title">__MSG_prefs_OptionText_translate_lang__</span></td>
|
||||
<td>
|
||||
<label>
|
||||
<input type="text" id="translate_lang" name="translate_lang" class="option-input" />
|
||||
<br>__MSG_prefs_OptionText_translate_lang_Info__
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- PROMPTS -->
|
||||
<div id="translate_prompt_container">
|
||||
<!--title and description-->
|
||||
<span class="section_title">
|
||||
__MSG_Translate_prompt_text_title__
|
||||
</span>
|
||||
|
||||
<br>
|
||||
|
||||
<span class="infoline">
|
||||
__MSG_prefs_OptionText_btnManagePrompts_infoline__
|
||||
<a href="https://micz.it/thunderbird-addon-thunderai/data-placeholders/">
|
||||
__MSG_more_info_string__
|
||||
</a>
|
||||
</span>
|
||||
|
||||
<br>
|
||||
|
||||
<!--main prompt-->
|
||||
<span class="infoline">
|
||||
__MSG_prefs_OptionText_Translate_main_prompt__
|
||||
</span>
|
||||
|
||||
<br>
|
||||
|
||||
<div class="autocomplete-container">
|
||||
<textarea id="translate_prompt_text" rows="15"></textarea>
|
||||
<ul class="autocomplete-list hidden"></ul>
|
||||
</div>
|
||||
|
||||
<div id="translate_info_additional_statements"></div>
|
||||
<div class="btn_div">
|
||||
<button id="btn_reset_prompt" disabled>
|
||||
__MSG_reset_default__
|
||||
</button>
|
||||
<button id="btn_save_prompt" disabled>
|
||||
__MSG_save__
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<script src="mzta-translate.js" type="module"></script>
|
||||
<script src="../../js/mzta-i18n.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
232
pages/translate/mzta-translate.js
Normal file
232
pages/translate/mzta-translate.js
Normal file
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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);
|
||||
}
|
||||
Loading…
Reference in a new issue