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",
|
"message": "Delete summary",
|
||||||
"description": ""
|
"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": {
|
"antispam_by": {
|
||||||
"message": "Antispam by",
|
"message": "Antispam by",
|
||||||
"description": ""
|
"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)
|
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
|
## Key Modules
|
||||||
|
|
||||||
| File | Role |
|
| 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-prompts.js` | Prompt definitions (built-in) and custom prompt loading |
|
||||||
| `js/mzta-placeholders.js` | Placeholder definitions and resolution logic |
|
| `js/mzta-placeholders.js` | Placeholder definitions and resolution logic |
|
||||||
| `js/mzta-utils.js` | General utilities (email parsing, storage helpers, etc.) |
|
| `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-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-chatgpt.js` | ChatGPT Web integration (opens browser window, reads DOM) |
|
||||||
| `js/mzta-special-commands.js` | Handles special prompt actions (add_tags, calendar, task) |
|
| `js/mzta-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-store.js` | Storage abstraction helpers |
|
||||||
| `js/mzta-storage.js` | Unified per-message storage layer (`taStorage` class) for summary, spam, and translation data |
|
| `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-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-working-status.js` | Visual status indicator during AI processing |
|
||||||
| `js/mzta-addatags-exclusion-list.js` | Tag exclusion list management |
|
| `js/mzta-addatags-exclusion-list.js` | Tag exclusion list management |
|
||||||
| `js/mzta-placeholders-autocomplete.js` | Autocomplete for placeholders in prompt editor |
|
| `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 |
|
| `get-task/` | Task creation settings |
|
||||||
| `spamfilter/` | Spam filter settings |
|
| `spamfilter/` | Spam filter settings |
|
||||||
| `summarize/` | Email summarization settings |
|
| `summarize/` | Email summarization settings |
|
||||||
|
| `translate/` | Email translation settings |
|
||||||
| `onboarding/` | First-run welcome page |
|
| `onboarding/` | First-run welcome page |
|
||||||
| `_lib/` | Shared libraries used by pages |
|
| `_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.
|
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-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 |
|
| `summarize` | Summarize email content |
|
||||||
| `get_calendar_event` | Extract and create a calendar event |
|
| `get_calendar_event` | Extract and create a calendar event |
|
||||||
| `get_task` | Extract and create a task |
|
| `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`.
|
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
|
- Accepts an array of `{ message, fullMessage }` entries
|
||||||
- Returns `{ promptText, promptInfo }` where `promptInfo` is the `prompt_summarize` prompt object
|
- 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
|
## Prompt Types Reference
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ use_specific_integration (default: false)
|
||||||
|
|
||||||
### Special Prompt Integration Overrides
|
### 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)
|
{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_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_display_mode` | `'inline'` | Where to display summaries: `'inline'` = message pane banner, `'webchat'` = AI chat window. Note: `summarize_auto = 2` always uses inline regardless of this setting. |
|
||||||
| `summarize_max_display_length` | `0` | Maximum characters shown in inline summary before truncation. `0` = no limit (show full text). When set, text is truncated at a word boundary and a "See more"/"See less" toggle link is shown. |
|
| `summarize_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/`)
|
### Summarize Settings Page (`pages/summarize/`)
|
||||||
|
|
||||||
|
|
@ -120,6 +123,20 @@ The summarize settings page provides:
|
||||||
- Each has Save/Reset buttons and placeholder autocomplete
|
- Each has Save/Reset buttons and placeholder autocomplete
|
||||||
- Default text comes from i18n strings (`prompt_summarize_full_text`, etc.)
|
- 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
|
## Adding a New Preference
|
||||||
|
|
||||||
1. Add the key and default value to `prefs_default` in `options/mzta-options-default.js`
|
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);
|
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:
|
default:
|
||||||
// do nothing
|
// do nothing
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
|
|
@ -1190,3 +1393,4 @@ switch (message.command) {
|
||||||
|
|
||||||
browser.runtime.sendMessage({ command: "checkSpamReport" });
|
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){
|
switch(curr_prompt.id){
|
||||||
case 'prompt_translate_this':
|
case 'prompt_translate_this':
|
||||||
let prefs2 = await browser.storage.sync.get({default_chatgpt_lang: getLanguageDisplayName(browser.i18n.getUILanguage())});
|
let prefs2 = await browser.storage.sync.get({default_chatgpt_lang: getLanguageDisplayName(browser.i18n.getUILanguage()), translate_lang: ''});
|
||||||
let chatgpt_lang2 = prefs2.default_chatgpt_lang;
|
let chatgpt_lang2 = prefs2.translate_lang || prefs2.default_chatgpt_lang;
|
||||||
if(chatgpt_lang2 === ''){
|
if(chatgpt_lang2 === ''){
|
||||||
chatgpt_lang2 = getLanguageDisplayName(browser.i18n.getUILanguage());
|
chatgpt_lang2 = getLanguageDisplayName(browser.i18n.getUILanguage());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -235,24 +235,6 @@ const defaultPrompts = [
|
||||||
is_default: "1",
|
is_default: "1",
|
||||||
is_special: "0",
|
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',
|
id: 'prompt_this',
|
||||||
name: "__MSG_prompt_this__",
|
name: "__MSG_prompt_this__",
|
||||||
|
|
@ -396,6 +378,22 @@ const specialPrompts = [
|
||||||
api_model: '',
|
api_model: '',
|
||||||
is_default: "1",
|
is_default: "1",
|
||||||
is_special: "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 {string} lang - Target language code.
|
||||||
* @param {boolean} [force=true] - If true, overwrite existing translation data.
|
* @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);
|
this.taLog.log('[writeTranslation] messageId: ' + messageId + ', lang: ' + lang + ', force: ' + force);
|
||||||
try {
|
try {
|
||||||
let key = this._buildKey(messageId);
|
let key = this._buildKey(messageId);
|
||||||
|
|
@ -288,7 +288,7 @@ export class taStorage {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let now = Date.now();
|
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;
|
record.ts = now;
|
||||||
await messenger.storage.local.set({ [key]: record });
|
await messenger.storage.local.set({ [key]: record });
|
||||||
} catch (e) {
|
} 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.
|
* Delete the entire record for a given Message-ID.
|
||||||
* @param {string} messageId - The Message-ID header string.
|
* @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 };
|
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.
|
* Extracts tags from the response text.
|
||||||
* @param {string} response_text - The response text from which to extract tags.
|
* @param {string} response_text - The response text from which to extract tags.
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ import {
|
||||||
} from './js/mzta-prompts.js';
|
} from './js/mzta-prompts.js';
|
||||||
import { taSpamReport } from './js/mzta-spamreport.js';
|
import { taSpamReport } from './js/mzta-spamreport.js';
|
||||||
import { taSummaryStore } from './js/mzta-summarystore.js';
|
import { taSummaryStore } from './js/mzta-summarystore.js';
|
||||||
|
import { taTranslationStore } from './js/mzta-translationstore.js';
|
||||||
import { taWorkingStatus } from './js/mzta-working-status.js';
|
import { taWorkingStatus } from './js/mzta-working-status.js';
|
||||||
import {
|
import {
|
||||||
addTags_getExclusionList,
|
addTags_getExclusionList,
|
||||||
|
|
@ -95,6 +96,7 @@ let taLog = new taLogger("mzta-background",prefs_init.do_debug);
|
||||||
taWorkingStatus.taLog = taLog;
|
taWorkingStatus.taLog = taLog;
|
||||||
let spamReport = new taSpamReport(prefs_init.do_debug);
|
let spamReport = new taSpamReport(prefs_init.do_debug);
|
||||||
let summaryStore = new taSummaryStore(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({
|
let special_prompts_ids = getActiveSpecialPromptsIDs({
|
||||||
addtags: prefs_init.add_tags,
|
addtags: prefs_init.add_tags,
|
||||||
|
|
@ -328,6 +330,61 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
// case 'chatgpt_open':
|
// case 'chatgpt_open':
|
||||||
// openChatGPT(message.prompt,message.action,message.tabId);
|
// openChatGPT(message.prompt,message.action,message.tabId);
|
||||||
// return true;
|
// 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':
|
case 'chatgpt_close':
|
||||||
async function _closeChatGptWindow(window_id) {
|
async function _closeChatGptWindow(window_id) {
|
||||||
let prefs_close = await browser.storage.sync.get({chatgpt_win_save_position: prefs_default.chatgpt_win_save_position});
|
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.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.prefs: pass pre-fetched prefs to avoid re-querying
|
||||||
// options.autoMove: if true, move spam messages to junk folder (default: false)
|
// 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/>.
|
* 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 = {
|
export const integration_options_config = {
|
||||||
chatgpt: {
|
chatgpt: {
|
||||||
|
|
@ -141,6 +141,9 @@ export const prefs_default = {
|
||||||
summarize_auto: 1, // 0: disabled, 1: manual button, 2: automatic
|
summarize_auto: 1, // 0: disabled, 1: manual button, 2: automatic
|
||||||
summarize_display_mode: 'inline', // 'inline' or 'webchat'
|
summarize_display_mode: 'inline', // 'inline' or 'webchat'
|
||||||
summarize_max_display_length: 0, // 0 = no limit, otherwise max chars shown inline
|
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,
|
spamfilter_show_msg_panel: true,
|
||||||
...generated_prefs
|
...generated_prefs
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,17 @@
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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">
|
<tr class="get_calendar_event_tr">
|
||||||
<td><span class="opt_title">__MSG_prefs_OptionText_get_calendar_event__</span>
|
<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>
|
<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(){
|
async function disable_GetCalendarEvent(){
|
||||||
let get_calendar_event = document.getElementById('get_calendar_event');
|
let get_calendar_event = document.getElementById('get_calendar_event');
|
||||||
let get_task = document.getElementById('get_task');
|
let get_task = document.getElementById('get_task');
|
||||||
|
|
@ -300,6 +321,13 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
});
|
});
|
||||||
summarize_info_btn.disabled = summarize_el.checked ? '' : 'disabled';
|
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_el = document.getElementById('get_calendar_event');
|
||||||
let get_calendar_event_info_btn = document.getElementById('btnManageCalendarEventInfo');
|
let get_calendar_event_info_btn = document.getElementById('btnManageCalendarEventInfo');
|
||||||
get_calendar_event_el.addEventListener('click', (event) => {
|
get_calendar_event_el.addEventListener('click', (event) => {
|
||||||
|
|
@ -334,6 +362,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
openTab('/pages/summarize/mzta-summarize.html');
|
openTab('/pages/summarize/mzta-summarize.html');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('btnManageTranslateInfo').addEventListener('click', () => {
|
||||||
|
openTab('/pages/translate/mzta-translate.html');
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('btnManageCalendarEventInfo').addEventListener('click', () => {
|
document.getElementById('btnManageCalendarEventInfo').addEventListener('click', () => {
|
||||||
openTab('/pages/get-calendar-event/mzta-get-calendar-event.html');
|
openTab('/pages/get-calendar-event/mzta-get-calendar-event.html');
|
||||||
});
|
});
|
||||||
|
|
@ -360,6 +392,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
conntype_select.addEventListener("change", () => disable_AddTags(prefs_opt));
|
conntype_select.addEventListener("change", () => disable_AddTags(prefs_opt));
|
||||||
conntype_select.addEventListener("change", () => disable_SpamFilter(prefs_opt));
|
conntype_select.addEventListener("change", () => disable_SpamFilter(prefs_opt));
|
||||||
conntype_select.addEventListener("change", () => disable_Summarize(prefs_opt));
|
conntype_select.addEventListener("change", () => disable_Summarize(prefs_opt));
|
||||||
|
conntype_select.addEventListener("change", () => disable_Translate(prefs_opt));
|
||||||
conntype_select.addEventListener("change", disable_GetCalendarEvent);
|
conntype_select.addEventListener("change", disable_GetCalendarEvent);
|
||||||
|
|
||||||
showConnectionOptions(conntype_select);
|
showConnectionOptions(conntype_select);
|
||||||
|
|
@ -367,6 +400,7 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||||
disable_AddTags(prefs_opt);
|
disable_AddTags(prefs_opt);
|
||||||
disable_SpamFilter(prefs_opt);
|
disable_SpamFilter(prefs_opt);
|
||||||
disable_Summarize(prefs_opt);
|
disable_Summarize(prefs_opt);
|
||||||
|
disable_Translate(prefs_opt);
|
||||||
disable_GetCalendarEvent();
|
disable_GetCalendarEvent();
|
||||||
|
|
||||||
document.getElementById('reset_max_prompt_length').addEventListener('click', resetMaxPromptLength);
|
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