Merge pull request #714 from micz/summary_on_message_issue580
Summary on message issue 580
This commit is contained in:
commit
12eb740219
25 changed files with 1561 additions and 232 deletions
|
|
@ -5,6 +5,7 @@
|
||||||
|
|
||||||
<h2>Version 4.1.0 - ??/??/2026</h2>
|
<h2>Version 4.1.0 - ??/??/2026</h2>
|
||||||
<ul>
|
<ul>
|
||||||
|
<li>Antispam information are now permanently saved for each message [<a href="https://github.com/micz/ThunderAI/issues/675">#675</a>].</li>
|
||||||
<li>...</li>
|
<li>...</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Version 4.0.3 - 20/03/2026</h2>
|
<h2>Version 4.0.3 - 20/03/2026</h2>
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,7 @@ _The language status represents the percentage of translated strings in the late
|
||||||
- [Iconka.com](https://www.iconarchive.com/artist/iconka.html) for the autotag context menu icon
|
- [Iconka.com](https://www.iconarchive.com/artist/iconka.html) for the autotag context menu icon
|
||||||
- [Icojam](https://www.iconarchive.com/artist/icojam.html) for the spam filter context menu icon
|
- [Icojam](https://www.iconarchive.com/artist/icojam.html) for the spam filter context menu icon
|
||||||
- [Roundicons](https://www.flaticon.com/authors/roundicons) for the summarize context menu icon
|
- [Roundicons](https://www.flaticon.com/authors/roundicons) for the summarize context menu icon
|
||||||
|
- [HideMau](https://www.flaticon.com/authors/hidemaru) for the ai summarize icon
|
||||||
|
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
|
||||||
|
|
@ -235,6 +235,10 @@
|
||||||
"message": "Close",
|
"message": "Close",
|
||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
|
"webchat_save_as_summary": {
|
||||||
|
"message": "Save as Summary",
|
||||||
|
"description": "Button label in the webchat window to save the AI response as a message summary"
|
||||||
|
},
|
||||||
"chatgpt_textarea_not_found_error": {
|
"chatgpt_textarea_not_found_error": {
|
||||||
"message": "It seems that the ChatGPT page is taking too long to load. If it finishes loading, click the button on the right. If the problem persists, please check the service status.",
|
"message": "It seems that the ChatGPT page is taking too long to load. If it finishes loading, click the button on the right. If the problem persists, please check the service status.",
|
||||||
"description": ""
|
"description": ""
|
||||||
|
|
@ -1388,17 +1392,13 @@
|
||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
"spamfilter_no_reports": {
|
"spamfilter_no_reports": {
|
||||||
"message": "No messages have screened for spam yet. Here you'll find a list of the last 100 spam reports for the current session only.",
|
"message": "No messages have screened for spam yet. Here you'll find a list of the last 100 spam reports.",
|
||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
"SpamReport_Title": {
|
"SpamReport_Title": {
|
||||||
"message": "Spam Filter Reports",
|
"message": "Spam Filter Reports",
|
||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
"SpamReport_infoline": {
|
|
||||||
"message": "This information is saved only for the current session.",
|
|
||||||
"description": ""
|
|
||||||
},
|
|
||||||
"Date": {
|
"Date": {
|
||||||
"message": "Date",
|
"message": "Date",
|
||||||
"description": ""
|
"description": ""
|
||||||
|
|
@ -1885,6 +1885,21 @@
|
||||||
"message": "You have denied the optional permission needed to fetch models for this integration.",
|
"message": "You have denied the optional permission needed to fetch models for this integration.",
|
||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
|
"prefs_OptionText_auto_summary": {
|
||||||
|
"message": "Enable automatic AI summarization for message previews"
|
||||||
|
},
|
||||||
|
"prefs_OptionText_auto_summary_Info": {
|
||||||
|
"message": "If checked, ThunderAI will automatically generate and display AI summaries above email messages when they are opened. Note that this means all messages you preview will immediately be sent to the configured AI service."
|
||||||
|
},
|
||||||
|
"auto_summary_title": {
|
||||||
|
"message": "ThunderAI Summary"
|
||||||
|
},
|
||||||
|
"auto_summary_generating": {
|
||||||
|
"message": "Generating AI summary..."
|
||||||
|
},
|
||||||
|
"auto_summary_failed": {
|
||||||
|
"message": "Failed to generate AI summary. Please confirm your settings and try again."
|
||||||
|
},
|
||||||
"customPrompts_export_include_api_settings": {
|
"customPrompts_export_include_api_settings": {
|
||||||
"message": "Do you want to include the API settings in the export? Be aware that also the API Key will be saved in the file!",
|
"message": "Do you want to include the API settings in the export? Be aware that also the API Key will be saved in the file!",
|
||||||
"description": ""
|
"description": ""
|
||||||
|
|
@ -1913,10 +1928,110 @@
|
||||||
"message": "Spam check in progress...",
|
"message": "Spam check in progress...",
|
||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
|
"prefs_OptionText_summarize_auto": {
|
||||||
|
"message": "Auto-summarize messages",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"prefs_OptionText_summarize_auto_disabled": {
|
||||||
|
"message": "Disabled",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"prefs_OptionText_summarize_auto_manual": {
|
||||||
|
"message": "Show summary button",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"prefs_OptionText_summarize_auto_automatic": {
|
||||||
|
"message": "Generate automatically",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"prefs_OptionText_summarize_auto_Info": {
|
||||||
|
"message": "Choose whether to automatically generate summaries when viewing messages. Requires an API-based connection (not ChatGPT Web).",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"prefs_OptionText_summarize_display_mode": {
|
||||||
|
"message": "Display summary in",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"prefs_OptionText_summarize_display_mode_inline": {
|
||||||
|
"message": "Message pane (inline)",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"prefs_OptionText_summarize_display_mode_webchat": {
|
||||||
|
"message": "Chat window",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"prefs_OptionText_summarize_display_mode_Info": {
|
||||||
|
"message": "Choose where the summary result is displayed. Inline mode shows a summary banner directly in the message pane. Chat window mode opens the AI chat window.",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"prefs_OptionText_summarize_max_display_length": {
|
||||||
|
"message": "Max display length",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"prefs_OptionText_summarize_max_display_length_Info": {
|
||||||
|
"message": "Maximum number of characters to display in the inline summary. Set to 0 for no limit.",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"summarize_see_more": {
|
||||||
|
"message": "See more",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"summarize_see_less": {
|
||||||
|
"message": "See less",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"summarize_title": {
|
||||||
|
"message": "ThunderAI Overview",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"get_ai_summary": {
|
||||||
|
"message": "AI Summary",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"summarize_collapse": {
|
||||||
|
"message": "Collapse summary",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"summarize_generating": {
|
||||||
|
"message": "Generating summary...",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"summarize_error": {
|
||||||
|
"message": "Failed to generate summary",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"summarize_click_to_generate": {
|
||||||
|
"message": "Click here to generate a summary",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"summarize_chatgpt_web_not_supported": {
|
||||||
|
"message": "Auto-summary requires an API-based connection. Please configure an API connection in ThunderAI settings.",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"summarize_refresh": {
|
||||||
|
"message": "Refresh summary",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"spamfilter_refresh": {
|
||||||
|
"message": "Refresh spam report",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"spamfilter_delete": {
|
||||||
|
"message": "Delete spam report",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
|
"summarize_delete": {
|
||||||
|
"message": "Delete summary",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
"antispam_by": {
|
"antispam_by": {
|
||||||
"message": "Antispam by",
|
"message": "Antispam by",
|
||||||
"description": ""
|
"description": ""
|
||||||
},
|
},
|
||||||
|
"summary_by": {
|
||||||
|
"message": "Summary by",
|
||||||
|
"description": ""
|
||||||
|
},
|
||||||
"prefs_THStats_1": {
|
"prefs_THStats_1": {
|
||||||
"message": "Do you want beautiful statistics about your emails?",
|
"message": "Do you want beautiful statistics about your emails?",
|
||||||
"description": ""
|
"description": ""
|
||||||
|
|
@ -1945,4 +2060,4 @@
|
||||||
"message": "Leave empty to use the default position.",
|
"message": "Leave empty to use the default position.",
|
||||||
"description": ""
|
"description": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -273,7 +273,11 @@ class MessagesArea extends HTMLElement {
|
||||||
const messageElement = document.createElement('div');
|
const messageElement = document.createElement('div');
|
||||||
messageElement.classList.add('message', type);
|
messageElement.classList.add('message', type);
|
||||||
// Replace \n with <br> for correct HTML display
|
// Replace \n with <br> for correct HTML display
|
||||||
messageElement.appendChild(htmlStringToFragment(messageText));
|
if (type === "info") {
|
||||||
|
messageElement.appendChild(htmlStringToFragment(messageText));
|
||||||
|
} else {
|
||||||
|
messageElement.appendChild(textWithBrToFragment(messageText));
|
||||||
|
}
|
||||||
// messageElement.textContent = messageText;
|
// messageElement.textContent = messageText;
|
||||||
// // Replace \n with <br> elements for correct HTML display
|
// // Replace \n with <br> elements for correct HTML display
|
||||||
// messageElement.innerHTML = '';
|
// messageElement.innerHTML = '';
|
||||||
|
|
@ -449,11 +453,34 @@ class MessagesArea extends HTMLElement {
|
||||||
closeButton.addEventListener('click', async () => {
|
closeButton.addEventListener('click', async () => {
|
||||||
browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id}); // close window
|
browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id}); // close window
|
||||||
});
|
});
|
||||||
if(promptData.action != 0) {
|
if(promptData.action != 0) {
|
||||||
actionButtons.appendChild(splitButton);
|
actionButtons.appendChild(splitButton);
|
||||||
selectionInfo.style.display = "block"; // show selection info
|
selectionInfo.style.display = "block"; // show selection info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save as Summary button (only shown for summary webchat sessions)
|
||||||
|
if(promptData.prompt_info?.headerMessageId) {
|
||||||
|
const saveSummaryButton = document.createElement('button');
|
||||||
|
saveSummaryButton.textContent = browser.i18n.getMessage("webchat_save_as_summary");
|
||||||
|
saveSummaryButton.classList.add('action_btn');
|
||||||
|
saveSummaryButton.addEventListener('click', async () => {
|
||||||
|
let finalText = removeAloneBRs(fullTextHTMLAtAssignment);
|
||||||
|
const selectedHTML = this.getCurrentSelectionHTML();
|
||||||
|
if(selectedHTML != "") {
|
||||||
|
finalText = removeAloneBRs(selectedHTML);
|
||||||
|
}
|
||||||
|
await browser.runtime.sendMessage({
|
||||||
|
command: "chatgpt_saveSummary",
|
||||||
|
text: finalText,
|
||||||
|
headerMessageId: promptData.prompt_info.headerMessageId,
|
||||||
|
tabId: promptData.prompt_info.summaryTabId || promptData.tabId,
|
||||||
|
});
|
||||||
|
browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id});
|
||||||
|
});
|
||||||
|
actionButtons.appendChild(saveSummaryButton);
|
||||||
|
selectionInfo.style.display = "block";
|
||||||
|
}
|
||||||
|
|
||||||
// diff viewer button
|
// diff viewer button
|
||||||
if(promptData.prompt_info?.use_diff_viewer == "1") {
|
if(promptData.prompt_info?.use_diff_viewer == "1") {
|
||||||
const diffvButton = document.createElement('button');
|
const diffvButton = document.createElement('button');
|
||||||
|
|
@ -576,6 +603,20 @@ class MessagesArea extends HTMLElement {
|
||||||
customElements.define('messages-area', MessagesArea);
|
customElements.define('messages-area', MessagesArea);
|
||||||
|
|
||||||
|
|
||||||
|
function textWithBrToFragment(text) {
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
const segments = text.split(/<br\s*\/?>/gi);
|
||||||
|
segments.forEach((segment, idx) => {
|
||||||
|
if (segment.length > 0) {
|
||||||
|
fragment.appendChild(document.createTextNode(segment));
|
||||||
|
}
|
||||||
|
if (idx < segments.length - 1) {
|
||||||
|
fragment.appendChild(document.createElement('br'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
function htmlStringToFragment(htmlString) {
|
function htmlStringToFragment(htmlString) {
|
||||||
// console.log(">>>>>>>>>>>>>>>> htmlStringToFragment htmlString: " + htmlString);
|
// console.log(">>>>>>>>>>>>>>>> htmlStringToFragment htmlString: " + htmlString);
|
||||||
const normalizedHtml = htmlString.replace(/\n/g, '<br>');
|
const normalizedHtml = htmlString.replace(/\n/g, '<br>');
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,43 @@ js/mzta-prompts.js (builds final prompt string)
|
||||||
js/mzta-compose-script.js (inserts text into Thunderbird compose window)
|
js/mzta-compose-script.js (inserts text into Thunderbird compose window)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Data Flow: Inline Summary on Message Display
|
||||||
|
|
||||||
|
The `summarize_display_mode` preference (`'inline'` or `'webchat'`) controls where
|
||||||
|
the summary is displayed. The `summarize_auto` preference controls when it is triggered.
|
||||||
|
|
||||||
|
- `summarize_auto = 2` (automatic) always generates inline, regardless of `summarize_display_mode`.
|
||||||
|
- `summarize_auto = 1` (manual button) respects `summarize_display_mode`:
|
||||||
|
- `'inline'` → button click triggers inline generation
|
||||||
|
- `'webchat'` → button click opens the AI chat window via `_openSummaryWebchat()`
|
||||||
|
- Context menu summarize also respects `summarize_display_mode`:
|
||||||
|
- `'inline'` with a single message → generates inline via `_generateSummaryForMessage()`
|
||||||
|
- `'webchat'` or multiple messages → opens the AI chat window via `openChatGPT()`
|
||||||
|
|
||||||
|
```
|
||||||
|
User opens/selects a message in Thunderbird
|
||||||
|
↓
|
||||||
|
mzta-compose-script.js (sends "initSummary" to background)
|
||||||
|
↓
|
||||||
|
mzta-background.js (checks summarize_auto + summarize_display_mode prefs)
|
||||||
|
↓
|
||||||
|
┌──────────────────────────────────────────────────────────┐
|
||||||
|
│ summarize_auto = 0 → do nothing │
|
||||||
|
│ summarize_auto = 1 → show "click to generate" button │
|
||||||
|
│ display_mode = inline → click triggers inline gen │
|
||||||
|
│ display_mode = webchat → click opens chat window │
|
||||||
|
│ summarize_auto = 2 → generate immediately (always inline)│
|
||||||
|
└──────────────────────────────────────────────────────────┘
|
||||||
|
↓ (if generating inline)
|
||||||
|
taSummaryStore (check cache / set processing)
|
||||||
|
↓ (cache miss)
|
||||||
|
mzta-special-commands (via Web Worker, NOT chatgpt_web)
|
||||||
|
↓
|
||||||
|
taSummaryStore (save result via taStorage)
|
||||||
|
↓
|
||||||
|
mzta-compose-script.js (render summary banner in message body)
|
||||||
|
```
|
||||||
|
|
||||||
## Key Modules
|
## Key Modules
|
||||||
|
|
||||||
| File | Role |
|
| File | Role |
|
||||||
|
|
@ -50,14 +87,16 @@ js/mzta-compose-script.js (inserts text into Thunderbird compose window)
|
||||||
| `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) |
|
| `js/mzta-utils-prompt.js` | Prompt-specific utilities (text truncation, lang injection, `buildSummaryPrompt()` for unified summary prompt assembly) |
|
||||||
| `js/mzta-compose-script.js` | Injects AI response into Thunderbird compose window |
|
| `js/mzta-compose-script.js` | Content script for compose and message display: injects AI response into compose window, renders summary/spam banners in message display |
|
||||||
| `js/mzta-chatgpt.js` | ChatGPT Web integration (opens browser window, reads DOM) |
|
| `js/mzta-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) |
|
||||||
| `js/mzta-spamreport.js` | Spam filter logic |
|
| `js/mzta-spamreport.js` | Spam filter logic |
|
||||||
| `js/mzta-i18n.js` | i18n helper (wraps `browser.i18n.getMessage`) |
|
| `js/mzta-i18n.js` | i18n helper (wraps `browser.i18n.getMessage`) |
|
||||||
| `js/mzta-logger.js` | Debug logging (gated by `do_debug` pref) |
|
| `js/mzta-logger.js` | Debug logging (gated by `do_debug` pref) |
|
||||||
| `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-summarystore.js` | Summary-specific storage wrapper (`taSummaryStore` class) with caching, truncation, and processing-state tracking |
|
||||||
| `js/mzta-working-status.js` | Visual status indicator during AI processing |
|
| `js/mzta-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 |
|
||||||
|
|
@ -106,3 +145,9 @@ Each subdirectory is a self-contained settings/UI page for a specific feature:
|
||||||
## Storage
|
## Storage
|
||||||
|
|
||||||
All preferences are stored via `browser.storage.local`. The keys and default values are defined in `options/mzta-options-default.js` (`prefs_default` export). Custom prompts and custom placeholders are stored separately in storage under their own keys.
|
All preferences are stored via `browser.storage.local`. The keys and default values are defined in `options/mzta-options-default.js` (`prefs_default` export). Custom prompts and custom placeholders are stored separately in storage under their own keys.
|
||||||
|
|
||||||
|
### Per-Message Data Storage
|
||||||
|
|
||||||
|
Per-message data (summaries, spam reports, translations) is stored via `js/mzta-storage.js` (`taStorage` class). Each record is keyed by `msg:<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.
|
||||||
|
|
|
||||||
|
|
@ -58,6 +58,33 @@ Some prompts trigger additional Thunderbird actions beyond just sending text to
|
||||||
|
|
||||||
These special prompts can have their own dedicated API integration settings (configured in the Options page). The list of these special prompts is in `options/mzta-options-default.js` as `special_prompts_with_integration`.
|
These special prompts can have their own dedicated API integration settings (configured in the Options page). The list of these special prompts is in `options/mzta-options-default.js` as `special_prompts_with_integration`.
|
||||||
|
|
||||||
|
### Summarize: Dual-Mode Prompt System
|
||||||
|
|
||||||
|
The summarize feature uses two distinct prompt pathways:
|
||||||
|
|
||||||
|
**Context Menu Summarize** (right-click on messages in message list):
|
||||||
|
- Activated via the `summarize` context menu item, controlled by the `summarize` feature flag
|
||||||
|
- Uses 3 special prompts stored in `specialPrompts`:
|
||||||
|
- `prompt_summarize` — the main instruction prompt for the LLM
|
||||||
|
- `prompt_summarize_email_template` — template for formatting each email's content
|
||||||
|
- `prompt_summarize_email_separator` — separator text between multiple emails
|
||||||
|
- Supports multi-email summarization: each selected message is formatted with the email template, joined by the separator, then prepended with the instruction prompt
|
||||||
|
- All 3 prompts support placeholder autocomplete (`{%placeholder%}` syntax)
|
||||||
|
- Result is displayed via `openChatGPT()` in the standard chat output window (not inline)
|
||||||
|
- Default prompt texts are stored as i18n keys: `prompt_summarize_full_text`, `prompt_summarize_email_template_full_text`, `prompt_summarize_email_separator_full_text`
|
||||||
|
|
||||||
|
**Inline Summary on Message Display** (automatic or manual per `summarize_auto` pref):
|
||||||
|
- Uses the same 3 special prompts as webchat mode, via `taPromptUtils.buildSummaryPrompt()` in `js/mzta-utils-prompt.js`
|
||||||
|
- Does **not** support `chatgpt_web` connection type (shows error if configured)
|
||||||
|
- Result is rendered as a styled banner at the top of the message body via `mzta-compose-script.js`
|
||||||
|
- Banner includes a refresh button (↻) to regenerate the summary
|
||||||
|
- Cached per-message via `taSummaryStore` / `taStorage` (max 100 entries)
|
||||||
|
|
||||||
|
**Unified Prompt Building** — `taPromptUtils.buildSummaryPrompt(messageDataArray)`:
|
||||||
|
- All summary paths (inline, webchat single, webchat multi) use this single method
|
||||||
|
- Accepts an array of `{ message, fullMessage }` entries
|
||||||
|
- Returns `{ promptText, promptInfo }` where `promptInfo` is the `prompt_summarize` prompt object
|
||||||
|
|
||||||
## Prompt Types Reference
|
## Prompt Types Reference
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,30 @@ These are generated programmatically at the bottom of `mzta-options-default.js`
|
||||||
| `spamfilter_enabled_accounts` | `[]` | Accounts where spam filter is active |
|
| `spamfilter_enabled_accounts` | `[]` | Accounts where spam filter is active |
|
||||||
| `spamfilter_show_msg_panel` | `true` | Show info panel on spam detection |
|
| `spamfilter_show_msg_panel` | `true` | Show info panel on spam detection |
|
||||||
| `summarize` | `false` | Enable email summarization |
|
| `summarize` | `false` | Enable email summarization |
|
||||||
|
| `summarize_auto` | `1` | Auto-summarize mode: `0` = disabled, `1` = manual (show "click to generate" button), `2` = automatic (generate on message open) |
|
||||||
|
| `summarize_display_mode` | `'inline'` | Where to display summaries: `'inline'` = message pane banner, `'webchat'` = AI chat window. Note: `summarize_auto = 2` always uses inline regardless of this setting. |
|
||||||
|
| `summarize_max_display_length` | `0` | Maximum characters shown in inline summary before truncation. `0` = no limit (show full text). When set, text is truncated at a word boundary and a "See more"/"See less" toggle link is shown. |
|
||||||
|
|
||||||
|
### Summarize Settings Page (`pages/summarize/`)
|
||||||
|
|
||||||
|
The summarize settings page provides:
|
||||||
|
|
||||||
|
1. **Specific integration checkbox** — enables per-feature API override (like other special prompts)
|
||||||
|
2. **Auto-summarize dropdown** (`summarize_auto`) — three modes:
|
||||||
|
- `0` (Disabled) — no inline summaries
|
||||||
|
- `1` (Manual) — shows a "Click to generate summary" button in message display
|
||||||
|
- `2` (Automatic) — generates summary immediately when message is opened
|
||||||
|
3. **Display mode dropdown** (`summarize_display_mode`) — controls where summaries are shown:
|
||||||
|
- `'inline'` — summary banner in the message pane (default)
|
||||||
|
- `'webchat'` — opens the AI chat window
|
||||||
|
- Note: `summarize_auto = 2` always generates inline regardless of this setting. Context menu summarize with multiple messages always falls back to webchat.
|
||||||
|
4. **Max display length** (`summarize_max_display_length`) — number input, limits inline summary text to N characters. `0` = no limit. When truncated, a "See more"/"See less" toggle link is appended.
|
||||||
|
5. **Three editable prompts** (used by context menu summarize and webchat mode):
|
||||||
|
- Summarize instruction prompt (`prompt_summarize`)
|
||||||
|
- Email template prompt (`prompt_summarize_email_template`)
|
||||||
|
- Email separator prompt (`prompt_summarize_email_separator`)
|
||||||
|
- Each has Save/Reset buttons and placeholder autocomplete
|
||||||
|
- Default text comes from i18n strings (`prompt_summarize_full_text`, etc.)
|
||||||
|
|
||||||
## Adding a New Preference
|
## Adding a New Preference
|
||||||
|
|
||||||
|
|
|
||||||
BIN
images/ai_summary.png
Normal file
BIN
images/ai_summary.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 306 B |
|
|
@ -16,6 +16,114 @@
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// CSS selectors for DOM elements injected by ThunderAI.
|
||||||
|
// These are stripped when retrieving the email body content
|
||||||
|
// to avoid contaminating placeholder values sent to AI providers.
|
||||||
|
// Add new selectors here when new UI elements are injected into the email DOM.
|
||||||
|
const MZTA_INJECTED_SELECTORS = [
|
||||||
|
'#mzta-spam-check-progress',
|
||||||
|
'#mzta-spam-report-banner',
|
||||||
|
'.mzta_dialog',
|
||||||
|
];
|
||||||
|
|
||||||
|
function getCleanBodyHtml() {
|
||||||
|
const clone = document.body.cloneNode(true);
|
||||||
|
for (const selector of MZTA_INJECTED_SELECTORS) {
|
||||||
|
for (const el of clone.querySelectorAll(selector)) {
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const table of clone.querySelectorAll('table.moz-main-header')) {
|
||||||
|
let sibling = table.previousElementSibling;
|
||||||
|
while (sibling && sibling.tagName === 'DIV') {
|
||||||
|
const toRemove = sibling;
|
||||||
|
sibling = sibling.previousElementSibling;
|
||||||
|
toRemove.remove();
|
||||||
|
}
|
||||||
|
table.remove();
|
||||||
|
}
|
||||||
|
return clone;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createThreeDotsMenu(isDark, menuItems, panelColors) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.style.cssText = 'position: relative; display: inline-flex; align-items: center;';
|
||||||
|
|
||||||
|
const dotsBtn = document.createElement('span');
|
||||||
|
dotsBtn.textContent = '\u22EE';
|
||||||
|
dotsBtn.style.cssText = 'cursor: pointer; opacity: 0.7; font-size: 18px; padding: 2px 7px; line-height: 1; user-select: none; transition: opacity 0.2s; display: flex; align-items: center;';
|
||||||
|
dotsBtn.onmouseover = () => dotsBtn.style.opacity = '1';
|
||||||
|
dotsBtn.onmouseout = () => dotsBtn.style.opacity = '0.7';
|
||||||
|
|
||||||
|
const dropdown = document.createElement('div');
|
||||||
|
const dropdownBg = panelColors.bg;
|
||||||
|
const dropdownBorder = panelColors.border;
|
||||||
|
const defaultTextColor = panelColors.text;
|
||||||
|
dropdown.style.cssText = `display: none; position: absolute; right: 0; top: 100%; z-index: 9999; min-width: 180px; background-color: ${dropdownBg}; border: 1px solid ${dropdownBorder}; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); overflow: hidden;`;
|
||||||
|
|
||||||
|
menuItems.forEach(item => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.style.cssText = `display: flex; align-items: center; gap: 8px; padding: 8px 12px; cursor: pointer; font-size: 13px; color: ${defaultTextColor}; transition: background-color 0.15s, color 0.15s;`;
|
||||||
|
|
||||||
|
const iconSpan = document.createElement('span');
|
||||||
|
iconSpan.textContent = item.icon;
|
||||||
|
iconSpan.style.cssText = 'font-size: 15px; width: 18px; text-align: center;';
|
||||||
|
|
||||||
|
const labelSpan = document.createElement('span');
|
||||||
|
labelSpan.textContent = item.label;
|
||||||
|
|
||||||
|
row.appendChild(iconSpan);
|
||||||
|
row.appendChild(labelSpan);
|
||||||
|
|
||||||
|
const hoverBg = item.hoverColor === '#cc0000'
|
||||||
|
? (isDark ? 'rgba(204,0,0,0.2)' : 'rgba(204,0,0,0.1)')
|
||||||
|
: (isDark ? 'rgba(77,157,224,0.2)' : 'rgba(26,95,168,0.1)');
|
||||||
|
|
||||||
|
row.onmouseover = () => {
|
||||||
|
row.style.backgroundColor = hoverBg;
|
||||||
|
row.style.color = item.hoverColor;
|
||||||
|
};
|
||||||
|
row.onmouseout = () => {
|
||||||
|
row.style.backgroundColor = '';
|
||||||
|
row.style.color = defaultTextColor;
|
||||||
|
};
|
||||||
|
|
||||||
|
row.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dropdown.style.display = 'none';
|
||||||
|
if (item.disableAfterClick) {
|
||||||
|
row.onclick = null;
|
||||||
|
row.style.opacity = '0.5';
|
||||||
|
row.style.pointerEvents = 'none';
|
||||||
|
}
|
||||||
|
item.onClick();
|
||||||
|
};
|
||||||
|
|
||||||
|
dropdown.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
dotsBtn.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (!wrapper.contains(e.target)) {
|
||||||
|
dropdown.style.display = 'none';
|
||||||
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape' && dropdown.style.display !== 'none') {
|
||||||
|
dropdown.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.appendChild(dotsBtn);
|
||||||
|
wrapper.appendChild(dropdown);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
browser.runtime.onMessage.addListener((message) => {
|
browser.runtime.onMessage.addListener((message) => {
|
||||||
switch (message.command) {
|
switch (message.command) {
|
||||||
case "getSelectedText": {
|
case "getSelectedText": {
|
||||||
|
|
@ -60,7 +168,7 @@ switch (message.command) {
|
||||||
|
|
||||||
case "getText": {
|
case "getText": {
|
||||||
let t = '';
|
let t = '';
|
||||||
const children = window.document.body.childNodes;
|
const children = getCleanBodyHtml().childNodes;
|
||||||
for (const node of children) {
|
for (const node of children) {
|
||||||
if (node instanceof Element) {
|
if (node instanceof Element) {
|
||||||
if (node.classList.contains('moz-signature')) {
|
if (node.classList.contains('moz-signature')) {
|
||||||
|
|
@ -73,11 +181,11 @@ switch (message.command) {
|
||||||
}
|
}
|
||||||
|
|
||||||
case "getTextOnly": {
|
case "getTextOnly": {
|
||||||
return Promise.resolve(window.document.body.innerText);
|
return Promise.resolve(getCleanBodyHtml().innerText);
|
||||||
}
|
}
|
||||||
|
|
||||||
case "getFullHtml": {
|
case "getFullHtml": {
|
||||||
return Promise.resolve(window.document.body.innerHTML);
|
return Promise.resolve(getCleanBodyHtml().innerHTML);
|
||||||
}
|
}
|
||||||
|
|
||||||
case "getOnlyTypedText": {
|
case "getOnlyTypedText": {
|
||||||
|
|
@ -650,6 +758,23 @@ switch (message.command) {
|
||||||
containerProgress.appendChild(brandingProgress);
|
containerProgress.appendChild(brandingProgress);
|
||||||
|
|
||||||
document.body.insertBefore(containerProgress, document.body.firstChild);
|
document.body.insertBefore(containerProgress, document.body.firstChild);
|
||||||
|
|
||||||
|
// Reposition summary trigger button if it exists as fixed
|
||||||
|
const existingFixedTriggerProgress = document.getElementById('mzta-summary-trigger');
|
||||||
|
if (existingFixedTriggerProgress && !document.getElementById('mzta-summary-trigger-wrapper')) {
|
||||||
|
existingFixedTriggerProgress.style.position = '';
|
||||||
|
existingFixedTriggerProgress.style.top = '';
|
||||||
|
existingFixedTriggerProgress.style.right = '';
|
||||||
|
existingFixedTriggerProgress.style.zIndex = '';
|
||||||
|
existingFixedTriggerProgress.style.marginLeft = 'auto';
|
||||||
|
existingFixedTriggerProgress.style.marginTop = '4px';
|
||||||
|
const reposTriggerWrapperProgress = document.createElement('div');
|
||||||
|
reposTriggerWrapperProgress.id = 'mzta-summary-trigger-wrapper';
|
||||||
|
reposTriggerWrapperProgress.style.cssText = 'display: flex; justify-content: flex-end; padding: 4px 0.5rem;';
|
||||||
|
reposTriggerWrapperProgress.appendChild(existingFixedTriggerProgress);
|
||||||
|
document.body.insertBefore(reposTriggerWrapperProgress, containerProgress.nextSibling);
|
||||||
|
}
|
||||||
|
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
|
|
||||||
case "showSpamReport":
|
case "showSpamReport":
|
||||||
|
|
@ -682,7 +807,7 @@ switch (message.command) {
|
||||||
borderColor = '#006600';
|
borderColor = '#006600';
|
||||||
}
|
}
|
||||||
|
|
||||||
container.style.cssText = `background-color: ${bgColor}; color: ${textColor}; border-bottom: 1px solid ${borderColor}; padding: 8px 12px; font-family: system-ui, -apple-system, sans-serif; font-size: 13px; display: flex; align-items: center; gap: 15px; width: 100%; box-sizing: border-box;`;
|
container.style.cssText = `background-color: ${bgColor}; color: ${textColor}; border-bottom: 1px solid ${borderColor}; border-radius:4px; padding: 8px 0.5rem; font-family: system-ui, -apple-system, sans-serif; font-size: 13px; display: flex; align-items: center; gap: 15px; width: 100%; box-sizing: border-box;`;
|
||||||
|
|
||||||
const scoreText = document.createElement('strong');
|
const scoreText = document.createElement('strong');
|
||||||
if (data.spamValue == -999) {
|
if (data.spamValue == -999) {
|
||||||
|
|
@ -702,21 +827,358 @@ switch (message.command) {
|
||||||
branding.textContent = browser.i18n.getMessage("antispam_by") + " ThunderAI";
|
branding.textContent = browser.i18n.getMessage("antispam_by") + " ThunderAI";
|
||||||
branding.style.cssText = 'margin-left: auto; font-style: italic; font-size: 10px; opacity: 0.5;';
|
branding.style.cssText = 'margin-left: auto; font-style: italic; font-size: 10px; opacity: 0.5;';
|
||||||
|
|
||||||
const closeBtn = document.createElement('span');
|
const spamMenu = createThreeDotsMenu(isDark, [
|
||||||
closeBtn.textContent = '×';
|
{
|
||||||
closeBtn.style.cssText = 'cursor: pointer; font-weight: bold; font-size: 16px; padding: 0 5px;';
|
icon: '↻',
|
||||||
closeBtn.title = browser.i18n.getMessage("chatgpt_win_close");
|
label: browser.i18n.getMessage("spamfilter_refresh") || 'Refresh spam report',
|
||||||
closeBtn.onclick = function() {
|
hoverColor: isDark ? '#4d9de0' : '#1a5fa8',
|
||||||
container.remove();
|
disableAfterClick: true,
|
||||||
browser.runtime.sendMessage({ command: "removeSpamReport", headerMessageId: data.headerMessageId });
|
onClick: () => {
|
||||||
};
|
browser.runtime.sendMessage({ command: "refreshSpamReport", headerMessageId: data.headerMessageId });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '×',
|
||||||
|
label: browser.i18n.getMessage("spamfilter_delete") || 'Delete spam report',
|
||||||
|
hoverColor: '#cc0000',
|
||||||
|
onClick: () => {
|
||||||
|
container.remove();
|
||||||
|
browser.runtime.sendMessage({ command: "removeSpamReport", headerMessageId: data.headerMessageId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
], { bg: bgColor, border: borderColor, text: textColor });
|
||||||
|
|
||||||
|
const spamRightGroup = document.createElement('span');
|
||||||
|
spamRightGroup.style.cssText = 'margin-left: auto; margin-right:1px; display: flex; align-items: center; gap: 5px;';
|
||||||
|
branding.style.cssText = 'font-style: italic; font-size: 10px; opacity: 0.5;';
|
||||||
|
spamRightGroup.appendChild(branding);
|
||||||
|
spamRightGroup.appendChild(spamMenu);
|
||||||
|
|
||||||
container.appendChild(scoreText);
|
container.appendChild(scoreText);
|
||||||
container.appendChild(reasonText);
|
container.appendChild(reasonText);
|
||||||
container.appendChild(branding);
|
container.appendChild(spamRightGroup);
|
||||||
container.appendChild(closeBtn);
|
|
||||||
|
|
||||||
document.body.insertBefore(container, document.body.firstChild);
|
document.body.insertBefore(container, document.body.firstChild);
|
||||||
|
|
||||||
|
// Reposition summary trigger button if it exists as fixed
|
||||||
|
const existingFixedTrigger = document.getElementById('mzta-summary-trigger');
|
||||||
|
if (existingFixedTrigger && !document.getElementById('mzta-summary-trigger-wrapper')) {
|
||||||
|
existingFixedTrigger.style.position = '';
|
||||||
|
existingFixedTrigger.style.top = '';
|
||||||
|
existingFixedTrigger.style.right = '';
|
||||||
|
existingFixedTrigger.style.zIndex = '';
|
||||||
|
existingFixedTrigger.style.marginLeft = 'auto';
|
||||||
|
existingFixedTrigger.style.marginTop = '4px';
|
||||||
|
const reposTriggerWrapper = document.createElement('div');
|
||||||
|
reposTriggerWrapper.id = 'mzta-summary-trigger-wrapper';
|
||||||
|
reposTriggerWrapper.style.cssText = 'display: flex; justify-content: flex-end; padding: 4px 0.5rem;';
|
||||||
|
reposTriggerWrapper.appendChild(existingFixedTrigger);
|
||||||
|
document.body.insertBefore(reposTriggerWrapper, container.nextSibling);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(true);
|
||||||
|
|
||||||
|
case "showSummary":
|
||||||
|
const generatingBanner = document.getElementById('mzta-summary-generating');
|
||||||
|
if(generatingBanner) generatingBanner.remove();
|
||||||
|
|
||||||
|
const existingTriggerWrapper = document.getElementById('mzta-summary-trigger-wrapper');
|
||||||
|
if(existingTriggerWrapper) existingTriggerWrapper.remove();
|
||||||
|
const existingTriggerBtn = document.getElementById('mzta-summary-trigger');
|
||||||
|
if(existingTriggerBtn) existingTriggerBtn.remove();
|
||||||
|
|
||||||
|
const summaryBanner = document.getElementById('mzta-summary-banner');
|
||||||
|
if(summaryBanner) summaryBanner.remove();
|
||||||
|
|
||||||
|
const summaryData = message.data;
|
||||||
|
const summaryContainer = document.createElement('div');
|
||||||
|
summaryContainer.id = 'mzta-summary-banner';
|
||||||
|
|
||||||
|
const isDarkSummary = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
|
let bgColorSummary = isDarkSummary ? '#2a2a2a' : '#f0f0f0';
|
||||||
|
let textColorSummary = isDarkSummary ? '#e0e0e0' : '#333';
|
||||||
|
let borderColorSummary = isDarkSummary ? '#444' : '#ddd';
|
||||||
|
|
||||||
|
if (summaryData.error) {
|
||||||
|
bgColorSummary = isDarkSummary ? '#3a1a1a' : '#f7e6e6';
|
||||||
|
textColorSummary = isDarkSummary ? '#ffcccc' : '#660000';
|
||||||
|
borderColorSummary = '#660000';
|
||||||
|
}
|
||||||
|
|
||||||
|
summaryContainer.className = 'thunderai-summary-pane';
|
||||||
|
summaryContainer.style.cssText = `background-color: ${bgColorSummary}; color: ${textColorSummary}; padding: 0.5rem; margin-bottom: 1rem; border-radius: 4px; border: 1px solid ${borderColorSummary}; font-family: system-ui, -apple-system, sans-serif; font-size: 14px;`;
|
||||||
|
|
||||||
|
const summaryMenu = createThreeDotsMenu(isDarkSummary, [
|
||||||
|
{
|
||||||
|
icon: '↻',
|
||||||
|
label: browser.i18n.getMessage("summarize_refresh") || 'Refresh summary',
|
||||||
|
hoverColor: isDarkSummary ? '#4d9de0' : '#1a5fa8',
|
||||||
|
disableAfterClick: true,
|
||||||
|
onClick: () => {
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
command: "refreshSummary",
|
||||||
|
headerMessageId: summaryData.headerMessageId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: '×',
|
||||||
|
label: browser.i18n.getMessage("summarize_delete") || 'Delete summary',
|
||||||
|
hoverColor: '#cc0000',
|
||||||
|
onClick: () => {
|
||||||
|
summaryContainer.remove();
|
||||||
|
browser.runtime.sendMessage({ command: "removeSummary", headerMessageId: summaryData.headerMessageId });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
], { bg: bgColorSummary, border: borderColorSummary, text: textColorSummary });
|
||||||
|
|
||||||
|
const summaryBranding = document.createElement('span');
|
||||||
|
summaryBranding.textContent = browser.i18n.getMessage("summary_by") + " ThunderAI";
|
||||||
|
summaryBranding.style.cssText = 'font-style: italic; font-size: 10px; opacity: 0.5; white-space: nowrap;';
|
||||||
|
|
||||||
|
const summaryRightGroup = document.createElement('span');
|
||||||
|
summaryRightGroup.style.cssText = 'display: flex; align-items: center; gap: 5px; float: right; margin-left: 10px;';
|
||||||
|
summaryRightGroup.appendChild(summaryBranding);
|
||||||
|
summaryRightGroup.appendChild(summaryMenu);
|
||||||
|
|
||||||
|
const summaryIcon = document.createElement('img');
|
||||||
|
summaryIcon.src = browser.runtime.getURL("/images/ai_summary.png");
|
||||||
|
summaryIcon.style.cssText = `height: 16px; width: 16px; flex-shrink: 0; margin-top: 2px;${isDarkSummary ? ' filter: invert(1);' : ''}`;
|
||||||
|
|
||||||
|
const summaryTextWrapper = document.createElement('div');
|
||||||
|
summaryTextWrapper.style.cssText = 'flex: 1; min-width: 0;';
|
||||||
|
|
||||||
|
summaryTextWrapper.appendChild(summaryRightGroup);
|
||||||
|
|
||||||
|
const summaryText = document.createElement('div');
|
||||||
|
summaryText.className = 'thunderai-summary-content';
|
||||||
|
const hasHtml = !!summaryData.summary_html;
|
||||||
|
|
||||||
|
// Helper to set summary content using DOMParser (innerHTML is blocked in Thunderbird content scripts)
|
||||||
|
function setSummaryHtml(element, html) {
|
||||||
|
element.textContent = '';
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
|
while (doc.body.firstChild) {
|
||||||
|
element.appendChild(doc.body.firstChild);
|
||||||
|
}
|
||||||
|
element.querySelectorAll('p').forEach(p => { p.style.marginBlockStart = '0'; });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summaryData.error) {
|
||||||
|
summaryText.textContent = summaryData.message || browser.i18n.getMessage("summarize_error");
|
||||||
|
} else if (hasHtml) {
|
||||||
|
setSummaryHtml(summaryText, summaryData.summary_html);
|
||||||
|
} else {
|
||||||
|
summaryText.textContent = summaryData.summary;
|
||||||
|
}
|
||||||
|
summaryText.style.cssText = `font-size: 14px; line-height: 1.4;`;
|
||||||
|
|
||||||
|
summaryTextWrapper.appendChild(summaryText);
|
||||||
|
|
||||||
|
const maxLen = summaryData.maxDisplayLength || 0;
|
||||||
|
const fullText = summaryData.summary;
|
||||||
|
if (!summaryData.error && maxLen > 0 && fullText && fullText.length > maxLen) {
|
||||||
|
// Set up animated expand/collapse via max-height transition
|
||||||
|
summaryText.style.overflow = 'hidden';
|
||||||
|
summaryText.style.transition = 'max-height 0.2s ease';
|
||||||
|
|
||||||
|
if (!hasHtml) {
|
||||||
|
// Plain text: truncate by character position
|
||||||
|
let cutPos = fullText.lastIndexOf(' ', maxLen);
|
||||||
|
if (cutPos <= 0) cutPos = maxLen;
|
||||||
|
const truncated = fullText.substring(0, cutPos) + '\u2026';
|
||||||
|
summaryText.textContent = truncated;
|
||||||
|
|
||||||
|
// Measure truncated height after layout
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
const collapsedHeight = summaryText.scrollHeight;
|
||||||
|
summaryText.style.maxHeight = collapsedHeight + 'px';
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleLink = document.createElement('a');
|
||||||
|
toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more";
|
||||||
|
toggleLink.href = '#';
|
||||||
|
toggleLink.style.cssText = 'display: inline-block; margin-top: 4px; font-size: 13px; color: ' +
|
||||||
|
(isDarkSummary ? '#6db3f2' : '#1a5fa8') + '; cursor: pointer; text-decoration: underline;';
|
||||||
|
|
||||||
|
let expanded = false;
|
||||||
|
toggleLink.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!expanded) {
|
||||||
|
// Expand: set full text, measure, animate to full height
|
||||||
|
summaryText.textContent = fullText;
|
||||||
|
const fullHeight = summaryText.scrollHeight;
|
||||||
|
summaryText.style.maxHeight = fullHeight + 'px';
|
||||||
|
toggleLink.textContent = browser.i18n.getMessage("summarize_see_less") || "See less";
|
||||||
|
} else {
|
||||||
|
// Collapse: measure current truncated height, then animate down
|
||||||
|
summaryText.textContent = truncated;
|
||||||
|
// Force layout to get the target height before animating
|
||||||
|
const collapsedHeight = summaryText.scrollHeight;
|
||||||
|
summaryText.textContent = fullText;
|
||||||
|
// Set explicit current height so transition has a starting point
|
||||||
|
summaryText.style.maxHeight = summaryText.scrollHeight + 'px';
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
summaryText.style.maxHeight = collapsedHeight + 'px';
|
||||||
|
});
|
||||||
|
// Swap text after transition ends
|
||||||
|
summaryText.addEventListener('transitionend', function handler() {
|
||||||
|
summaryText.removeEventListener('transitionend', handler);
|
||||||
|
summaryText.textContent = truncated;
|
||||||
|
});
|
||||||
|
toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more";
|
||||||
|
}
|
||||||
|
expanded = !expanded;
|
||||||
|
});
|
||||||
|
|
||||||
|
summaryTextWrapper.appendChild(toggleLink);
|
||||||
|
} else {
|
||||||
|
// HTML content: use max-height to collapse, preserve full HTML
|
||||||
|
const collapsedMaxHeight = '4.2em'; // ~3 lines collapsed
|
||||||
|
summaryText.style.maxHeight = collapsedMaxHeight;
|
||||||
|
|
||||||
|
const toggleLink = document.createElement('a');
|
||||||
|
toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more";
|
||||||
|
toggleLink.href = '#';
|
||||||
|
toggleLink.style.cssText = 'display: inline-block; margin-top: 4px; font-size: 13px; color: ' +
|
||||||
|
(isDarkSummary ? '#6db3f2' : '#1a5fa8') + '; cursor: pointer; text-decoration: underline;';
|
||||||
|
|
||||||
|
let expanded = false;
|
||||||
|
toggleLink.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!expanded) {
|
||||||
|
summaryText.style.maxHeight = summaryText.scrollHeight + 'px';
|
||||||
|
toggleLink.textContent = browser.i18n.getMessage("summarize_see_less") || "See less";
|
||||||
|
} else {
|
||||||
|
summaryText.style.maxHeight = collapsedMaxHeight;
|
||||||
|
toggleLink.textContent = browser.i18n.getMessage("summarize_see_more") || "See more";
|
||||||
|
}
|
||||||
|
expanded = !expanded;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Only show toggle if content is actually taller than collapsed height
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (summaryText.scrollHeight > summaryText.clientHeight) {
|
||||||
|
summaryTextWrapper.appendChild(toggleLink);
|
||||||
|
} else {
|
||||||
|
summaryText.style.maxHeight = '';
|
||||||
|
summaryText.style.overflow = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const summaryBody = document.createElement('div');
|
||||||
|
summaryBody.style.cssText = 'display: flex; gap: 8px; align-items: flex-start;';
|
||||||
|
summaryBody.appendChild(summaryIcon);
|
||||||
|
summaryBody.appendChild(summaryTextWrapper);
|
||||||
|
summaryContainer.appendChild(summaryBody);
|
||||||
|
|
||||||
|
const spamBanner = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress');
|
||||||
|
document.body.insertBefore(summaryContainer, spamBanner ? spamBanner.nextSibling : document.body.firstChild);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
|
||||||
|
case "showSummaryGenerating":
|
||||||
|
const existingGenerating = document.getElementById('mzta-summary-generating');
|
||||||
|
if(existingGenerating) return Promise.resolve(true);
|
||||||
|
|
||||||
|
const existingSummary = document.getElementById('mzta-summary-banner');
|
||||||
|
if(existingSummary) existingSummary.remove();
|
||||||
|
|
||||||
|
const existingTriggerWrap = document.getElementById('mzta-summary-trigger-wrapper');
|
||||||
|
if(existingTriggerWrap) existingTriggerWrap.remove();
|
||||||
|
const existingTrigger = document.getElementById('mzta-summary-trigger');
|
||||||
|
if(existingTrigger) existingTrigger.remove();
|
||||||
|
|
||||||
|
const isDarkGen = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
|
let bgColorGen = isDarkGen ? '#2a2a2a' : '#f0f0f0';
|
||||||
|
let textColorGen = isDarkGen ? '#e0e0e0' : '#333';
|
||||||
|
let borderColorGen = isDarkGen ? '#444' : '#ddd';
|
||||||
|
let titleColorGen = isDarkGen ? '#ff6b6b' : '#d70022';
|
||||||
|
|
||||||
|
const generatingContainer = document.createElement('div');
|
||||||
|
generatingContainer.id = 'mzta-summary-generating';
|
||||||
|
generatingContainer.className = 'thunderai-summary-pane';
|
||||||
|
generatingContainer.style.cssText = `background-color: ${bgColorGen}; color: ${textColorGen}; padding: 0.5rem; margin-bottom: 1rem; border-radius: 4px; border: 1px solid ${borderColorGen}; font-family: system-ui, -apple-system, sans-serif; font-size: 14px; display: flex; align-items: center; gap: 10px;`;
|
||||||
|
|
||||||
|
const generatingIcon = document.createElement('img');
|
||||||
|
generatingIcon.src = browser.runtime.getURL("/images/ai_summary.png");
|
||||||
|
generatingIcon.style.cssText = `height: 16px; width: 16px; flex-shrink: 0;${isDarkGen ? ' filter: invert(1);' : ''}`;
|
||||||
|
|
||||||
|
const generatingLoadingImg = document.createElement('img');
|
||||||
|
generatingLoadingImg.src = browser.runtime.getURL("/images/loading.gif");
|
||||||
|
generatingLoadingImg.style.cssText = "height: 16px; width: 16px;";
|
||||||
|
|
||||||
|
const generatingTitle = document.createElement('span');
|
||||||
|
generatingTitle.className = 'thunderai-summary-title';
|
||||||
|
generatingTitle.textContent = browser.i18n.getMessage("summarize_generating");
|
||||||
|
generatingTitle.style.cssText = `font-size: 14px;`;
|
||||||
|
|
||||||
|
generatingContainer.appendChild(generatingIcon);
|
||||||
|
generatingContainer.appendChild(generatingLoadingImg);
|
||||||
|
generatingContainer.appendChild(generatingTitle);
|
||||||
|
|
||||||
|
const spamBannerGen = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress');
|
||||||
|
document.body.insertBefore(generatingContainer, spamBannerGen ? spamBannerGen.nextSibling : document.body.firstChild);
|
||||||
|
return Promise.resolve(true);
|
||||||
|
|
||||||
|
case "showSummaryButton":
|
||||||
|
const existingButton = document.getElementById('mzta-summary-trigger');
|
||||||
|
if(existingButton) return Promise.resolve(true);
|
||||||
|
|
||||||
|
const isDarkBtn = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
|
let bgColorBtn = isDarkBtn ? '#2a2a2a' : '#f0f0f0';
|
||||||
|
let textColorBtn = isDarkBtn ? '#e0e0e0' : '#333';
|
||||||
|
let borderColorBtn = isDarkBtn ? '#444' : '#ddd';
|
||||||
|
|
||||||
|
const spamBannerTrigger = document.getElementById('mzta-spam-report-banner') || document.getElementById('mzta-spam-check-progress');
|
||||||
|
const triggerBtn = document.createElement('div');
|
||||||
|
triggerBtn.id = 'mzta-summary-trigger';
|
||||||
|
triggerBtn.title = browser.i18n.getMessage("summarize_click_to_generate");
|
||||||
|
const triggerBtnBase = `background-color: ${bgColorBtn}; border: 1px solid ${borderColorBtn}; border-radius: 4px; padding: 6px 10px; cursor: pointer; font-family: system-ui, -apple-system, sans-serif; font-size: 12px; font-style: italic; opacity: 0.7; transition: opacity 0.2s; color: ${textColorBtn}; display: inline-flex; align-items: center; gap: 6px; width: fit-content;`;
|
||||||
|
if (spamBannerTrigger) {
|
||||||
|
triggerBtn.style.cssText = triggerBtnBase + ' margin-left: auto; margin-top: 4px;';
|
||||||
|
} else {
|
||||||
|
triggerBtn.style.cssText = triggerBtnBase + ' position: fixed; top: 8px; right: 8px; z-index: 9998;';
|
||||||
|
}
|
||||||
|
|
||||||
|
const triggerIcon = document.createElement('img');
|
||||||
|
triggerIcon.src = browser.runtime.getURL("/images/ai_summary.png");
|
||||||
|
triggerIcon.style.cssText = `height: 14px; width: 14px;${isDarkBtn ? ' filter: invert(1);' : ''}`;
|
||||||
|
triggerBtn.appendChild(triggerIcon);
|
||||||
|
|
||||||
|
const triggerLabel = document.createElement('span');
|
||||||
|
triggerLabel.textContent = browser.i18n.getMessage("get_ai_summary");
|
||||||
|
triggerBtn.appendChild(triggerLabel);
|
||||||
|
triggerBtn.onmouseover = () => { triggerBtn.style.opacity = '1'; };
|
||||||
|
triggerBtn.onmouseout = () => { triggerBtn.style.opacity = '0.7'; };
|
||||||
|
triggerBtn.onclick = async () => {
|
||||||
|
triggerBtn.onclick = null;
|
||||||
|
triggerBtn.style.cursor = 'default';
|
||||||
|
triggerBtn.style.opacity = '0.7';
|
||||||
|
triggerBtn.onmouseover = null;
|
||||||
|
triggerBtn.onmouseout = null;
|
||||||
|
const wrapper = document.getElementById('mzta-summary-trigger-wrapper');
|
||||||
|
if (wrapper) wrapper.remove(); else triggerBtn.remove();
|
||||||
|
browser.runtime.sendMessage({
|
||||||
|
command: message.webchat ? "triggerSummaryWebchat" : "triggerSummaryGeneration",
|
||||||
|
headerMessageId: message.headerMessageId
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (spamBannerTrigger) {
|
||||||
|
const triggerWrapper = document.createElement('div');
|
||||||
|
triggerWrapper.id = 'mzta-summary-trigger-wrapper';
|
||||||
|
triggerWrapper.style.cssText = 'display: flex; justify-content: flex-end; padding: 4px 0.5rem;';
|
||||||
|
triggerWrapper.appendChild(triggerBtn);
|
||||||
|
document.body.insertBefore(triggerWrapper, spamBannerTrigger.nextSibling);
|
||||||
|
} else {
|
||||||
|
document.body.appendChild(triggerBtn);
|
||||||
|
}
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|
@ -726,4 +1188,5 @@ switch (message.command) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
browser.runtime.sendMessage({ command: "checkSpamReport" });
|
browser.runtime.sendMessage({ command: "checkSpamReport" });
|
||||||
|
browser.runtime.sendMessage({ command: "initSummary" });
|
||||||
|
|
@ -52,6 +52,16 @@
|
||||||
0: Do not use the diff viewer
|
0: Do not use the diff viewer
|
||||||
1: Use the diff viewer
|
1: Use the diff viewer
|
||||||
|
|
||||||
|
================ DYNAMIC PROPERTIES (set at runtime via prompt_info)
|
||||||
|
|
||||||
|
headerMessageId (set by _openSummaryWebchat in mzta-background.js):
|
||||||
|
When present, the webchat UI shows a "Save as Summary" button to capture
|
||||||
|
the AI response and save it as an inline summary for the message identified
|
||||||
|
by this headerMessageId.
|
||||||
|
|
||||||
|
summaryTabId (set by _openSummaryWebchat in mzta-background.js):
|
||||||
|
The tab ID of the message display tab to update with the saved summary.
|
||||||
|
|
||||||
================ USER PROPERTIES
|
================ USER PROPERTIES
|
||||||
Enabled (enabled attribute):
|
Enabled (enabled attribute):
|
||||||
0: Disabled
|
0: Disabled
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ export class taStorage {
|
||||||
|
|
||||||
taLog = null;
|
taLog = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {boolean} [do_debug=false] - Enable debug logging.
|
||||||
|
*/
|
||||||
constructor(do_debug = false) {
|
constructor(do_debug = false) {
|
||||||
this.taLog = new taLogger("mzta-storage", do_debug);
|
this.taLog = new taLogger("mzta-storage", do_debug);
|
||||||
}
|
}
|
||||||
|
|
@ -179,10 +182,11 @@ export class taStorage {
|
||||||
/**
|
/**
|
||||||
* Write the summary field for a given Message-ID.
|
* Write the summary field for a given Message-ID.
|
||||||
* @param {string} messageId - The Message-ID header string.
|
* @param {string} messageId - The Message-ID header string.
|
||||||
* @param {string} text - The summary text.
|
* @param {object} summary_data - The summary data object with fields:
|
||||||
|
* summary, error, message, summary_date.
|
||||||
* @param {boolean} [force=true] - If true, overwrite existing summary data.
|
* @param {boolean} [force=true] - If true, overwrite existing summary data.
|
||||||
*/
|
*/
|
||||||
async writeSummary(messageId, text, force = true) {
|
async writeSummary(messageId, summary_data, force = true) {
|
||||||
this.taLog.log('[writeSummary] messageId: ' + messageId + ', force: ' + force);
|
this.taLog.log('[writeSummary] messageId: ' + messageId + ', force: ' + force);
|
||||||
try {
|
try {
|
||||||
let key = this._buildKey(messageId);
|
let key = this._buildKey(messageId);
|
||||||
|
|
@ -192,7 +196,16 @@ export class taStorage {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let now = Date.now();
|
let now = Date.now();
|
||||||
record[taStorage.FIELD_SUMMARY] = { text: text, ts: now };
|
record[taStorage.FIELD_SUMMARY] = {
|
||||||
|
summary: summary_data.summary,
|
||||||
|
summary_html: summary_data.summary_html || '',
|
||||||
|
error: summary_data.error || false,
|
||||||
|
message: summary_data.message || '',
|
||||||
|
summary_date: summary_data.summary_date instanceof Date
|
||||||
|
? summary_data.summary_date.toISOString()
|
||||||
|
: summary_data.summary_date,
|
||||||
|
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) {
|
||||||
|
|
@ -200,6 +213,64 @@ export class taStorage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all records that contain a summary field.
|
||||||
|
* @returns {Promise<object>} Map of messageId -> summary data object.
|
||||||
|
*/
|
||||||
|
async getAllSummaryRecords() {
|
||||||
|
this.taLog.log('[getAllSummaryRecords] loading all summary records');
|
||||||
|
try {
|
||||||
|
let all = await messenger.storage.local.get(null);
|
||||||
|
let result = {};
|
||||||
|
for (let [key, record] of Object.entries(all)) {
|
||||||
|
if (!key.startsWith(taStorage.STORAGE_KEY_PREFIX)) continue;
|
||||||
|
if (!this.hasField(record, taStorage.FIELD_SUMMARY)) continue;
|
||||||
|
let messageId = key.slice(taStorage.STORAGE_KEY_PREFIX.length);
|
||||||
|
let summary = record[taStorage.FIELD_SUMMARY];
|
||||||
|
result[messageId] = {
|
||||||
|
headerMessageId: messageId,
|
||||||
|
summary: summary.summary,
|
||||||
|
error: summary.error || false,
|
||||||
|
message: summary.message || '',
|
||||||
|
summary_date: new Date(summary.summary_date || summary.ts),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
this.taLog.log('[getAllSummaryRecords] found ' + Object.keys(result).length + ' summary records');
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
this.taLog.error('getAllSummaryRecords error: ' + e);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete only the summary field from a record.
|
||||||
|
* Deletes the entire record if no other data fields remain.
|
||||||
|
* @param {string} messageId - The Message-ID header string.
|
||||||
|
*/
|
||||||
|
async deleteSummaryField(messageId) {
|
||||||
|
this.taLog.log('[deleteSummaryField] messageId: ' + messageId);
|
||||||
|
try {
|
||||||
|
let key = this._buildKey(messageId);
|
||||||
|
let record = await this.getRecord(messageId);
|
||||||
|
if (!record || !(taStorage.FIELD_SUMMARY in record)) {
|
||||||
|
this.taLog.log('[deleteSummaryField] no summary field found for messageId: ' + messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete record[taStorage.FIELD_SUMMARY];
|
||||||
|
const remainingFields = Object.keys(record).filter(k => k !== 'v' && k !== 'ts');
|
||||||
|
if (remainingFields.length === 0) {
|
||||||
|
this.taLog.log('[deleteSummaryField] no remaining fields, deleting entire record');
|
||||||
|
await messenger.storage.local.remove(key);
|
||||||
|
} else {
|
||||||
|
this.taLog.log('[deleteSummaryField] remaining fields: ' + remainingFields.join(', '));
|
||||||
|
await messenger.storage.local.set({ [key]: record });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.taLog.error('deleteSummaryField error: ' + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write the translation field for a given Message-ID.
|
* Write the translation field for a given Message-ID.
|
||||||
* @param {string} messageId - The Message-ID header string.
|
* @param {string} messageId - The Message-ID header string.
|
||||||
|
|
|
||||||
149
js/mzta-summarystore.js
Normal file
149
js/mzta-summarystore.js
Normal file
|
|
@ -0,0 +1,149 @@
|
||||||
|
/*
|
||||||
|
* 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 taSummaryStore {
|
||||||
|
|
||||||
|
_processing_prefix = 'mzta-summary-processing-';
|
||||||
|
_max_summaries = 100;
|
||||||
|
_storage = null;
|
||||||
|
taLog = null;
|
||||||
|
|
||||||
|
constructor(do_debug = false) {
|
||||||
|
this._storage = new taStorage(do_debug);
|
||||||
|
this.taLog = new taLogger('mzta-summarystore', do_debug);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setProcessing(data_id) {
|
||||||
|
this.taLog.log("[setProcessing] data_id: " + data_id);
|
||||||
|
const key = this._processing_prefix + data_id;
|
||||||
|
await browser.storage.session.set({ [key]: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
async isProcessing(data_id) {
|
||||||
|
this.taLog.log("[isProcessing] data_id: " + data_id);
|
||||||
|
const key = this._processing_prefix + data_id;
|
||||||
|
let output = await browser.storage.session.get(key);
|
||||||
|
let result = output[key] || false;
|
||||||
|
this.taLog.log("[isProcessing] result: " + result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveSummary(data, data_id) {
|
||||||
|
this.taLog.log("[saveSummary] data_id: " + data_id);
|
||||||
|
try {
|
||||||
|
await this._storage.writeSummary(data_id, data, true);
|
||||||
|
await browser.storage.session.remove(this._processing_prefix + data_id);
|
||||||
|
} catch (e) {
|
||||||
|
this.taLog.error("[saveSummary] error: " + e);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveError(data_id, error_message) {
|
||||||
|
this.taLog.log("[saveError] data_id: " + data_id + ", error_message: " + error_message);
|
||||||
|
let data = {
|
||||||
|
error: true,
|
||||||
|
message: error_message,
|
||||||
|
summary_date: new Date(),
|
||||||
|
headerMessageId: data_id
|
||||||
|
};
|
||||||
|
await this.saveSummary(data, data_id);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async loadSummary(data_id) {
|
||||||
|
this.taLog.log("[loadSummary] data_id: " + data_id);
|
||||||
|
let record = await this._storage.getRecord(data_id);
|
||||||
|
if (!record || !this._storage.hasField(record, taStorage.FIELD_SUMMARY)) {
|
||||||
|
this.taLog.log("[loadSummary] no record found for data_id: " + data_id);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let summary = record.summary;
|
||||||
|
return {
|
||||||
|
headerMessageId: data_id,
|
||||||
|
summary: summary.summary,
|
||||||
|
summary_html: summary.summary_html || '',
|
||||||
|
error: summary.error || false,
|
||||||
|
message: summary.message || '',
|
||||||
|
summary_date: new Date(summary.summary_date || summary.ts),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeSummary(data_id) {
|
||||||
|
this.taLog.log("[removeSummary] data_id: " + data_id);
|
||||||
|
await this._storage.deleteSummaryField(data_id);
|
||||||
|
await browser.storage.session.remove(this._processing_prefix + data_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAllSummaries() {
|
||||||
|
this.taLog.log("[getAllSummaries] loading all summaries");
|
||||||
|
return await this._storage.getAllSummaryRecords();
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearSummaries() {
|
||||||
|
this.taLog.log("[clearSummaries] clearing all summary data");
|
||||||
|
let allSummaries = await this._storage.getAllSummaryRecords();
|
||||||
|
let summaryKeys = Object.keys(allSummaries);
|
||||||
|
this.taLog.log("[clearSummaries] deleting " + summaryKeys.length + " summary records");
|
||||||
|
for (let messageId of summaryKeys) {
|
||||||
|
await this._storage.deleteSummaryField(messageId);
|
||||||
|
}
|
||||||
|
let allSession = await browser.storage.session.get(null);
|
||||||
|
let keysToDelete = Object.keys(allSession).filter(k => k.startsWith(this._processing_prefix));
|
||||||
|
this.taLog.log("[clearSummaries] deleting " + keysToDelete.length + " session keys");
|
||||||
|
for (let key of keysToDelete) {
|
||||||
|
await browser.storage.session.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async truncSummaries() {
|
||||||
|
this.taLog.log("[truncSummaries] checking summary count");
|
||||||
|
let data = await this._storage.getAllSummaryRecords();
|
||||||
|
let sortedData = this.sortSummariesByDate(data);
|
||||||
|
let keys = Object.keys(sortedData);
|
||||||
|
this.taLog.log("[truncSummaries] total summaries: " + keys.length + ", max: " + this._max_summaries);
|
||||||
|
|
||||||
|
if (keys.length > this._max_summaries) {
|
||||||
|
let toDelete = keys.length - this._max_summaries;
|
||||||
|
this.taLog.log("[truncSummaries] truncating " + toDelete + " oldest summaries");
|
||||||
|
for (let i = this._max_summaries; i < keys.length; i++) {
|
||||||
|
await this._storage.deleteSummaryField(keys[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sortSummariesByDate(data) {
|
||||||
|
if (!data) return {};
|
||||||
|
const summaryKeys = Object.keys(data);
|
||||||
|
summaryKeys.sort((a, b) => {
|
||||||
|
const dateA = new Date(data[a].summary_date);
|
||||||
|
const dateB = new Date(data[b].summary_date);
|
||||||
|
return dateB - dateA;
|
||||||
|
});
|
||||||
|
|
||||||
|
let sortedSummaries = {};
|
||||||
|
summaryKeys.forEach((key) => {
|
||||||
|
sortedSummaries[key] = data[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
return sortedSummaries;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,7 +17,12 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { placeholdersUtils } from './mzta-placeholders.js';
|
import { placeholdersUtils } from './mzta-placeholders.js';
|
||||||
import { extractJsonObject } from './mzta-utils.js';
|
import {
|
||||||
|
extractJsonObject,
|
||||||
|
getMailBody,
|
||||||
|
htmlBodyToPlainText
|
||||||
|
} from './mzta-utils.js';
|
||||||
|
import { getSpecialPrompts } from './mzta-prompts.js';
|
||||||
import { prefs_default } from '../options/mzta-options-default.js';
|
import { prefs_default } from '../options/mzta-options-default.js';
|
||||||
|
|
||||||
export const taPromptUtils = {
|
export const taPromptUtils = {
|
||||||
|
|
@ -126,6 +131,48 @@ export const taPromptUtils = {
|
||||||
return chatgpt_lang;
|
return chatgpt_lang;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
async buildSummaryPrompt(messageDataArray) {
|
||||||
|
const specialPrompts = await getSpecialPrompts();
|
||||||
|
const prompt = specialPrompts.find(p => p.id === 'prompt_summarize');
|
||||||
|
const prompt_email = specialPrompts.find(p => p.id === 'prompt_summarize_email_template');
|
||||||
|
const prompt_email_separator = specialPrompts.find(p => p.id === 'prompt_summarize_email_separator');
|
||||||
|
|
||||||
|
const chatgpt_lang = await taPromptUtils.getDefaultLang(prompt);
|
||||||
|
|
||||||
|
const prompt_string = await taPromptUtils.preparePrompt({
|
||||||
|
curr_prompt: prompt,
|
||||||
|
chatgpt_lang: chatgpt_lang,
|
||||||
|
});
|
||||||
|
const prompt_email_separator_string = await taPromptUtils.preparePrompt({
|
||||||
|
curr_prompt: prompt_email_separator,
|
||||||
|
chatgpt_lang: chatgpt_lang,
|
||||||
|
});
|
||||||
|
|
||||||
|
const messages_list = [];
|
||||||
|
for (let entry of messageDataArray) {
|
||||||
|
const bodyHtml = getMailBody(entry.fullMessage);
|
||||||
|
let bodyText = htmlBodyToPlainText(bodyHtml.html);
|
||||||
|
if (bodyText.length === 0) {
|
||||||
|
bodyText = bodyHtml.text || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
messages_list.push(await taPromptUtils.preparePrompt({
|
||||||
|
curr_prompt: prompt_email,
|
||||||
|
curr_message: entry.message,
|
||||||
|
chatgpt_lang: chatgpt_lang,
|
||||||
|
body_text: bodyText,
|
||||||
|
subject_text: entry.fullMessage.headers.subject,
|
||||||
|
msg_text: bodyHtml,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages_string = messages_list.join(prompt_email_separator_string);
|
||||||
|
const promptText = prompt_string + prompt_email_separator_string + messages_string;
|
||||||
|
|
||||||
|
return { promptText, promptInfo: prompt };
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts tags from the response text.
|
* 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.
|
||||||
|
|
|
||||||
|
|
@ -181,6 +181,11 @@ export function getMailBody(fullMessage){
|
||||||
}
|
}
|
||||||
if(html === "") {
|
if(html === "") {
|
||||||
html = text.replace(/\n/g, "<br>");
|
html = text.replace(/\n/g, "<br>");
|
||||||
|
} else {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
|
removeMozMainHeader(doc.body);
|
||||||
|
html = doc.body.innerHTML;
|
||||||
}
|
}
|
||||||
return {text, html};
|
return {text, html};
|
||||||
}
|
}
|
||||||
|
|
@ -246,7 +251,9 @@ export function htmlBodyToPlainText(htmlString) {
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
// Parse the HTML string
|
// Parse the HTML string
|
||||||
const doc = parser.parseFromString(htmlString, 'text/html');
|
const doc = parser.parseFromString(htmlString, 'text/html');
|
||||||
|
|
||||||
|
removeMozMainHeader(doc.body);
|
||||||
|
|
||||||
// remove invisible elements https://stackoverflow.com/questions/39813081/queryselector-where-display-is-not-none
|
// remove invisible elements https://stackoverflow.com/questions/39813081/queryselector-where-display-is-not-none
|
||||||
// return doc;
|
// return doc;
|
||||||
doc.querySelectorAll('[style*="display:none"]').forEach(e => e.remove());//.querySelector('html').children.not(':visible').remove()
|
doc.querySelectorAll('[style*="display:none"]').forEach(e => e.remove());//.querySelector('html').children.not(':visible').remove()
|
||||||
|
|
@ -263,7 +270,19 @@ export function htmlBodyToPlainText(htmlString) {
|
||||||
.replace(/ /gi,"")
|
.replace(/ /gi,"")
|
||||||
.trim();
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function removeMozMainHeader(root) {
|
||||||
|
for (const table of root.querySelectorAll('table.moz-main-header')) {
|
||||||
|
let sibling = table.previousElementSibling;
|
||||||
|
while (sibling && sibling.tagName === 'DIV') {
|
||||||
|
const toRemove = sibling;
|
||||||
|
sibling = sibling.previousElementSibling;
|
||||||
|
toRemove.remove();
|
||||||
|
}
|
||||||
|
table.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function cleanupNewlines(text) {
|
export function cleanupNewlines(text) {
|
||||||
return text
|
return text
|
||||||
.replace(/\r\n/g, '\n')
|
.replace(/\r\n/g, '\n')
|
||||||
|
|
@ -285,10 +304,19 @@ function convertBrToNewlines(html) {
|
||||||
export function convertNewlinesToParagraphs(input) {
|
export function convertNewlinesToParagraphs(input) {
|
||||||
return input
|
return input
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.map(line => `<p>${line}</p>`)
|
.map(line => `<p>${escapeHtml(line)}</p>`)
|
||||||
.join('');
|
.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeHtml(text) {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// This method is used to convert the model string id used in the URL
|
// This method is used to convert the model string id used in the URL
|
||||||
// to the model string used in the webpage
|
// to the model string used in the webpage
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
<script src="api_webchat/markdown-it.min.js"></script>
|
||||||
<script src="mzta-background.js" type="module"></script>
|
<script src="mzta-background.js" type="module"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@
|
||||||
import { mzta_script } from './js/mzta-chatgpt.js';
|
import { mzta_script } from './js/mzta-chatgpt.js';
|
||||||
import {
|
import {
|
||||||
prefs_default,
|
prefs_default,
|
||||||
getDynamicSettingValue,
|
|
||||||
getDynamicSettingsDefaults
|
getDynamicSettingsDefaults
|
||||||
} from './options/mzta-options-default.js';
|
} from './options/mzta-options-default.js';
|
||||||
import { mzta_Menus } from './js/mzta-menus.js';
|
import { mzta_Menus } from './js/mzta-menus.js';
|
||||||
|
|
@ -58,11 +57,11 @@ import {
|
||||||
} from './js/mzta-utils.js';
|
} from './js/mzta-utils.js';
|
||||||
import { taPromptUtils } from './js/mzta-utils-prompt.js';
|
import { taPromptUtils } from './js/mzta-utils-prompt.js';
|
||||||
import { mzta_specialCommand } from './js/mzta-special-commands.js';
|
import { mzta_specialCommand } from './js/mzta-special-commands.js';
|
||||||
import {
|
import {
|
||||||
getSpamFilterPrompt,
|
getSpamFilterPrompt
|
||||||
getSpecialPrompts
|
|
||||||
} 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 { taWorkingStatus } from './js/mzta-working-status.js';
|
import { taWorkingStatus } from './js/mzta-working-status.js';
|
||||||
import {
|
import {
|
||||||
addTags_getExclusionList,
|
addTags_getExclusionList,
|
||||||
|
|
@ -95,6 +94,7 @@ await reload_pref_init();
|
||||||
let taLog = new taLogger("mzta-background",prefs_init.do_debug);
|
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 special_prompts_ids = getActiveSpecialPromptsIDs({
|
let special_prompts_ids = getActiveSpecialPromptsIDs({
|
||||||
addtags: prefs_init.add_tags,
|
addtags: prefs_init.add_tags,
|
||||||
|
|
@ -111,7 +111,7 @@ browser.composeScripts.register({
|
||||||
|
|
||||||
// Register the message display script for all newly opened message tabs.
|
// Register the message display script for all newly opened message tabs.
|
||||||
messenger.messageDisplayScripts.register({
|
messenger.messageDisplayScripts.register({
|
||||||
js: [{ file: "js/mzta-compose-script.js" }],
|
js: [{ file: "js/mzta-compose-script.js" }]
|
||||||
});
|
});
|
||||||
|
|
||||||
browser.contentScripts.register({
|
browser.contentScripts.register({
|
||||||
|
|
@ -216,6 +216,115 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
// handler function.
|
// handler function.
|
||||||
if (message && message.hasOwnProperty("command")){
|
if (message && message.hasOwnProperty("command")){
|
||||||
switch (message.command) {
|
switch (message.command) {
|
||||||
|
case 'initSummary':
|
||||||
|
async function _initSummary() {
|
||||||
|
try {
|
||||||
|
let tabId = sender.tab.id;
|
||||||
|
let prefs = await browser.storage.sync.get({ summarize: prefs_default.summarize, summarize_auto: prefs_default.summarize_auto, summarize_display_mode: prefs_default.summarize_display_mode, summarize_max_display_length: prefs_default.summarize_max_display_length });
|
||||||
|
|
||||||
|
if (!prefs.summarize) return;
|
||||||
|
|
||||||
|
let message = await browser.messageDisplay.getDisplayedMessage(tabId);
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
|
// Always show cached summary if available, regardless of summarize_auto
|
||||||
|
let cachedSummary = await summaryStore.loadSummary(message.headerMessageId);
|
||||||
|
if (cachedSummary && !cachedSummary.error) {
|
||||||
|
browser.tabs.sendMessage(tabId, { command: "showSummary", data: { ...cachedSummary, maxDisplayLength: prefs.summarize_max_display_length } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await summaryStore.isProcessing(message.headerMessageId)) {
|
||||||
|
browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If summarize_auto is disabled, don't show button or auto-generate
|
||||||
|
if (prefs.summarize_auto === 0) return;
|
||||||
|
|
||||||
|
// Auto mode (summarize_auto === 2) always generates inline
|
||||||
|
if (prefs.summarize_auto === 2) {
|
||||||
|
_generateSummaryForMessage(message.headerMessageId, tabId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manual button mode (summarize_auto === 1)
|
||||||
|
if (prefs.summarize_display_mode === 'inline') {
|
||||||
|
browser.tabs.sendMessage(tabId, { command: "showSummaryButton", headerMessageId: message.headerMessageId });
|
||||||
|
} else {
|
||||||
|
browser.tabs.sendMessage(tabId, { command: "showSummaryButton", headerMessageId: message.headerMessageId, webchat: true });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
taLog.error("Error in initSummary: " + e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_initSummary();
|
||||||
|
break;
|
||||||
|
case 'triggerSummaryGeneration':
|
||||||
|
async function _triggerSummaryGeneration(message) {
|
||||||
|
let tabId = sender.tab.id;
|
||||||
|
await _generateSummaryForMessage(message.headerMessageId, tabId);
|
||||||
|
}
|
||||||
|
_triggerSummaryGeneration(message);
|
||||||
|
break;
|
||||||
|
case 'triggerSummaryWebchat':
|
||||||
|
async function _triggerSummaryWebchat(message) {
|
||||||
|
let tabId = sender.tab.id;
|
||||||
|
await _openSummaryWebchat(message.headerMessageId, tabId);
|
||||||
|
}
|
||||||
|
_triggerSummaryWebchat(message);
|
||||||
|
break;
|
||||||
|
case 'generate_summary':
|
||||||
|
async function _generate_summary(message) {
|
||||||
|
await _generateSummaryForMessage(message.headerMessageId, message.tabId);
|
||||||
|
}
|
||||||
|
_generate_summary(message);
|
||||||
|
break;
|
||||||
|
case 'refreshSummary':
|
||||||
|
async function _refreshSummary(message) {
|
||||||
|
let tabId = sender.tab.id;
|
||||||
|
await summaryStore.removeSummary(message.headerMessageId);
|
||||||
|
let prefs_refresh = await browser.storage.sync.get({ summarize_display_mode: prefs_default.summarize_display_mode });
|
||||||
|
if (prefs_refresh.summarize_display_mode === 'webchat') {
|
||||||
|
await _openSummaryWebchat(message.headerMessageId, tabId);
|
||||||
|
} else {
|
||||||
|
await _generateSummaryForMessage(message.headerMessageId, tabId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_refreshSummary(message);
|
||||||
|
break;
|
||||||
|
case 'removeSummary':
|
||||||
|
summaryStore.removeSummary(message.headerMessageId);
|
||||||
|
break;
|
||||||
|
case 'chatgpt_saveSummary':
|
||||||
|
async function _saveSummaryFromWebchat(msg) {
|
||||||
|
try {
|
||||||
|
let summaryHtml = msg.text.trim();
|
||||||
|
let cleanedSummary = cleanSummaryText(msg.text);
|
||||||
|
const summaryData = {
|
||||||
|
summary: cleanedSummary,
|
||||||
|
summary_html: summaryHtml,
|
||||||
|
summary_date: new Date(),
|
||||||
|
headerMessageId: msg.headerMessageId
|
||||||
|
};
|
||||||
|
await summaryStore.saveSummary(summaryData, msg.headerMessageId);
|
||||||
|
let prefs_summary = await browser.storage.sync.get({
|
||||||
|
summarize_max_display_length: prefs_default.summarize_max_display_length
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
browser.tabs.sendMessage(msg.tabId, {
|
||||||
|
command: "showSummary",
|
||||||
|
data: { ...summaryData, maxDisplayLength: prefs_summary.summarize_max_display_length }
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
taLog.error("Error sending showSummary to tab: " + e);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[ThunderAI] Error saving summary from webchat:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_saveSummaryFromWebchat(message);
|
||||||
|
break;
|
||||||
// case 'chatgpt_open':
|
// case 'chatgpt_open':
|
||||||
// openChatGPT(message.prompt,message.action,message.tabId);
|
// openChatGPT(message.prompt,message.action,message.tabId);
|
||||||
// return true;
|
// return true;
|
||||||
|
|
@ -375,14 +484,252 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||||
case 'removeSpamReport':
|
case 'removeSpamReport':
|
||||||
spamReport.removeReportData(message.headerMessageId);
|
spamReport.removeReportData(message.headerMessageId);
|
||||||
break;
|
break;
|
||||||
|
case 'refreshSpamReport':
|
||||||
|
_generateSpamReportForMessage(message.headerMessageId);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Return false if the message was not handled by this listener.
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Clean summary text by stripping HTML, markdown, and formatting artifacts.
|
||||||
|
// Used by both inline summary generation and webchat summary save.
|
||||||
|
function cleanSummaryText(text) {
|
||||||
|
let cleaned = text.replace(/<\/?[^>]+(>|$)/g, ''); // strip HTML tags
|
||||||
|
cleaned = cleaned.replace(/```[\s\S]*?```/g, '');
|
||||||
|
cleaned = cleaned.replace(/[\*#_~`]/g, '');
|
||||||
|
cleaned = cleaned.replace(/\s+/g, ' ').trim();
|
||||||
|
cleaned = cleaned.replace(/^Summary:\s*/i, '');
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _generateSummaryForMessage(headerMessageId, tabId) {
|
||||||
|
try {
|
||||||
|
let prefs = await browser.storage.sync.get({
|
||||||
|
connection_type: prefs_default.connection_type,
|
||||||
|
do_debug: prefs_default.do_debug,
|
||||||
|
default_chatgpt_lang: prefs_default.default_chatgpt_lang,
|
||||||
|
summarize_max_display_length: prefs_default.summarize_max_display_length,
|
||||||
|
...getDynamicSettingsDefaults(['use_specific_integration', 'connection_type'])
|
||||||
|
});
|
||||||
|
|
||||||
|
let cachedSummary = await summaryStore.loadSummary(headerMessageId);
|
||||||
|
if (cachedSummary && !cachedSummary.error) {
|
||||||
|
browser.tabs.sendMessage(tabId, { command: "showSummary", data: { ...cachedSummary, maxDisplayLength: prefs.summarize_max_display_length } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await summaryStore.isProcessing(headerMessageId)) {
|
||||||
|
browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await summaryStore.setProcessing(headerMessageId);
|
||||||
|
taWorkingStatus.startWorking();
|
||||||
|
browser.tabs.sendMessage(tabId, { command: "showSummaryGenerating" });
|
||||||
|
|
||||||
|
const messageResult = await browser.messages.query({ headerMessageId: headerMessageId });
|
||||||
|
if (!messageResult || messageResult.messages.length === 0) {
|
||||||
|
await summaryStore.saveError(headerMessageId, "Message not found");
|
||||||
|
browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: "Message not found" } });
|
||||||
|
taWorkingStatus.stopWorking();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullMessage = await browser.messages.getFull(messageResult.messages[0].id);
|
||||||
|
|
||||||
|
const connectionType = getConnectionType(prefs, {}, 'summarize');
|
||||||
|
|
||||||
|
if (connectionType === 'chatgpt_web') {
|
||||||
|
const errorMsg = browser.i18n.getMessage('summarize_chatgpt_web_not_supported');
|
||||||
|
await summaryStore.saveError(headerMessageId, errorMsg);
|
||||||
|
browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: errorMsg } });
|
||||||
|
taWorkingStatus.stopWorking();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { promptText } = await taPromptUtils.buildSummaryPrompt([{ message: messageResult.messages[0], fullMessage }]);
|
||||||
|
|
||||||
|
const cmd = new mzta_specialCommand({
|
||||||
|
prompt: promptText,
|
||||||
|
llm: connectionType,
|
||||||
|
do_debug: prefs.do_debug,
|
||||||
|
config: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
await cmd.initWorker();
|
||||||
|
const aiResponse = await cmd.sendPrompt();
|
||||||
|
let cleanedSummary = cleanSummaryText(aiResponse);
|
||||||
|
const md = window.markdownit();
|
||||||
|
let summaryHtml = md.render(aiResponse);
|
||||||
|
|
||||||
|
const summaryData = {
|
||||||
|
summary: cleanedSummary,
|
||||||
|
summary_html: summaryHtml,
|
||||||
|
summary_date: new Date(),
|
||||||
|
headerMessageId: headerMessageId
|
||||||
|
};
|
||||||
|
await summaryStore.saveSummary(summaryData, headerMessageId);
|
||||||
|
browser.tabs.sendMessage(tabId, { command: "showSummary", data: { ...summaryData, maxDisplayLength: prefs.summarize_max_display_length } });
|
||||||
|
taWorkingStatus.stopWorking();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[ThunderAI] Error generating summary:", error);
|
||||||
|
await summaryStore.saveError(headerMessageId, error.message || String(error));
|
||||||
|
browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: error.message || "Failed to generate summary" } });
|
||||||
|
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)
|
||||||
|
async function _generateSpamReportForMessage(headerMessageId, options = {}) {
|
||||||
|
try {
|
||||||
|
let prefs = options.prefs || await browser.storage.sync.get({
|
||||||
|
connection_type: prefs_default.connection_type,
|
||||||
|
do_debug: prefs_default.do_debug,
|
||||||
|
default_chatgpt_lang: prefs_default.default_chatgpt_lang,
|
||||||
|
spamfilter_threshold: prefs_default.spamfilter_threshold,
|
||||||
|
...getDynamicSettingsDefaults(['use_specific_integration', 'connection_type']),
|
||||||
|
});
|
||||||
|
|
||||||
|
await spamReport.removeReportData(headerMessageId);
|
||||||
|
await spamReport.setProcessing(headerMessageId);
|
||||||
|
|
||||||
|
await updateSpamPanel(headerMessageId, "showSpamCheckInProgress");
|
||||||
|
|
||||||
|
let message, curr_fullMessage, msg_text, body_text;
|
||||||
|
|
||||||
|
if (options.messageData) {
|
||||||
|
message = options.messageData.message;
|
||||||
|
curr_fullMessage = options.messageData.fullMessage;
|
||||||
|
msg_text = options.messageData.msg_text;
|
||||||
|
body_text = options.messageData.body_text;
|
||||||
|
} else {
|
||||||
|
const messageResult = await browser.messages.query({ headerMessageId: headerMessageId });
|
||||||
|
if (!messageResult || messageResult.messages.length === 0) {
|
||||||
|
let err_data = await spamReport.saveError(headerMessageId, "Message not found");
|
||||||
|
await updateSpamPanel(headerMessageId, "showSpamReport", err_data);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
message = messageResult.messages[0];
|
||||||
|
curr_fullMessage = await browser.messages.getFull(message.id);
|
||||||
|
msg_text = getMailBody(curr_fullMessage);
|
||||||
|
body_text = htmlBodyToPlainText(msg_text.html);
|
||||||
|
if (body_text.length == 0) {
|
||||||
|
body_text = msg_text.text.replace(/\s+/g, ' ').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let curr_prompt_spamfilter = await getSpamFilterPrompt();
|
||||||
|
let chatgpt_lang = await taPromptUtils.getDefaultLang(curr_prompt_spamfilter);
|
||||||
|
let specialFullPrompt_spamfilter = await taPromptUtils.preparePrompt({
|
||||||
|
curr_prompt: curr_prompt_spamfilter,
|
||||||
|
curr_message: message,
|
||||||
|
chatgpt_lang: chatgpt_lang,
|
||||||
|
body_text: body_text,
|
||||||
|
subject_text: curr_fullMessage.headers.subject,
|
||||||
|
msg_text: msg_text
|
||||||
|
});
|
||||||
|
taLog.log("Special prompt: " + specialFullPrompt_spamfilter);
|
||||||
|
|
||||||
|
let cmd_spamfilter = new mzta_specialCommand({
|
||||||
|
prompt: specialFullPrompt_spamfilter,
|
||||||
|
llm: getConnectionType(prefs, curr_prompt_spamfilter, 'spamfilter'),
|
||||||
|
custom_model: curr_prompt_spamfilter.model ? curr_prompt_spamfilter.model : '',
|
||||||
|
do_debug: prefs.do_debug,
|
||||||
|
config: curr_prompt_spamfilter
|
||||||
|
});
|
||||||
|
await cmd_spamfilter.initWorker();
|
||||||
|
|
||||||
|
let spamfilter_result = '';
|
||||||
|
taLog.log("Sending the prompt...");
|
||||||
|
try {
|
||||||
|
spamfilter_result = (await cmd_spamfilter.sendPrompt()).trim();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[ThunderAI | SpamFilter] Error getting spamfilter: ", err);
|
||||||
|
let err_data = await spamReport.saveError(headerMessageId, err.message || String(err));
|
||||||
|
await updateSpamPanel(headerMessageId, "showSpamReport", err_data);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
taLog.log("spamfilter_result: " + spamfilter_result);
|
||||||
|
|
||||||
|
let jsonObj = {};
|
||||||
|
taLog.log("Decoding the AI response...");
|
||||||
|
try {
|
||||||
|
jsonObj = extractJsonObject(spamfilter_result);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("[ThunderAI | SpamFilter] Error extracting JSON from AI response: ", e);
|
||||||
|
let err_data = await spamReport.saveError(headerMessageId, e.message || String(e));
|
||||||
|
await updateSpamPanel(headerMessageId, "showSpamReport", err_data);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
taLog.log("SpamFilter jsonObj: " + JSON.stringify(jsonObj));
|
||||||
|
|
||||||
|
let report_data = {};
|
||||||
|
report_data.report_date = new Date();
|
||||||
|
report_data.headerMessageId = headerMessageId;
|
||||||
|
report_data.spamValue = jsonObj.spamValue;
|
||||||
|
report_data.explanation = jsonObj.explanation;
|
||||||
|
report_data.subject = curr_fullMessage.headers.subject;
|
||||||
|
report_data.from = curr_fullMessage.headers.from;
|
||||||
|
report_data.message_date = new Date(message.date);
|
||||||
|
report_data.moved = false;
|
||||||
|
report_data.SpamThreshold = prefs.spamfilter_threshold || prefs_init.spamfilter_threshold;
|
||||||
|
|
||||||
|
if (options.autoMove && jsonObj.spamValue >= report_data.SpamThreshold) {
|
||||||
|
taLog.log("Marking as spam [" + headerMessageId + "]");
|
||||||
|
messenger.messages.update(message.id, { junk: true });
|
||||||
|
let spamFolder = await messenger.folders.query({ accountId: message.folder.accountId, specialUse: ['junk'] });
|
||||||
|
messenger.messages.move([message.id], spamFolder[0].id);
|
||||||
|
report_data.moved = true;
|
||||||
|
taLog.log("Marked as spam [" + headerMessageId + "]");
|
||||||
|
}
|
||||||
|
|
||||||
|
spamReport.saveReportData(report_data, headerMessageId);
|
||||||
|
await updateSpamPanel(headerMessageId, "showSpamReport", report_data);
|
||||||
|
return { success: true };
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[ThunderAI] Error generating spam report:", error);
|
||||||
|
let err_data = await spamReport.saveError(headerMessageId, error.message || String(error));
|
||||||
|
await updateSpamPanel(headerMessageId, "showSpamReport", err_data);
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _openSummaryWebchat(headerMessageId, tabId) {
|
||||||
|
try {
|
||||||
|
const messageResult = await browser.messages.query({ headerMessageId: headerMessageId });
|
||||||
|
if (!messageResult || messageResult.messages.length === 0) {
|
||||||
|
console.error("[ThunderAI] _openSummaryWebchat: Message not found for headerMessageId:", headerMessageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const curr_message = messageResult.messages[0];
|
||||||
|
const curr_message_full = await browser.messages.getFull(curr_message.id);
|
||||||
|
|
||||||
|
const connectionType = getConnectionType(await browser.storage.sync.get(prefs_default), {}, 'summarize');
|
||||||
|
if (connectionType === 'chatgpt_web') {
|
||||||
|
const errorMsg = browser.i18n.getMessage('summarize_chatgpt_web_not_supported');
|
||||||
|
await summaryStore.saveError(headerMessageId, errorMsg);
|
||||||
|
browser.tabs.sendMessage(tabId, { command: "showSummary", data: { error: true, message: errorMsg } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { promptText, promptInfo } = await taPromptUtils.buildSummaryPrompt([{ message: curr_message, fullMessage: curr_message_full }]);
|
||||||
|
promptInfo.headerMessageId = headerMessageId;
|
||||||
|
promptInfo.summaryTabId = tabId;
|
||||||
|
|
||||||
|
openChatGPT(promptText, promptInfo.action, tabId, promptInfo.name, promptInfo.need_custom_text, promptInfo);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[ThunderAI] Error opening summary webchat:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Listen for messages from ThunderAI-Sparks
|
// Listen for messages from ThunderAI-Sparks
|
||||||
browser.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
|
browser.runtime.onMessageExternal.addListener((message, sender, sendResponse) => {
|
||||||
switch (message.action) {
|
switch (message.action) {
|
||||||
|
|
@ -1175,144 +1522,43 @@ async function processEmails(args) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await spamReport.removeReportData(message.headerMessageId);
|
let result = await _generateSpamReportForMessage(
|
||||||
await spamReport.setProcessing(message.headerMessageId);
|
message.headerMessageId,
|
||||||
|
{
|
||||||
await updateSpamPanel(message.headerMessageId, "showSpamCheckInProgress");
|
messageData: { message, fullMessage: curr_fullMessage, body_text, msg_text },
|
||||||
|
prefs: prefs_aats,
|
||||||
let curr_prompt_spamfilter = await getSpamFilterPrompt();
|
autoMove: true
|
||||||
// console.log(">>>>>>>>>>>>> curr_prompt_spamfilter: " + JSON.stringify(curr_prompt_spamfilter));
|
});
|
||||||
let chatgpt_lang = await taPromptUtils.getDefaultLang(curr_prompt_spamfilter);
|
if (!result.success) continue;
|
||||||
let specialFullPrompt_spamfilter = await taPromptUtils.preparePrompt({
|
|
||||||
curr_prompt: curr_prompt_spamfilter,
|
|
||||||
curr_message: message,
|
|
||||||
chatgpt_lang: chatgpt_lang,
|
|
||||||
body_text: body_text,
|
|
||||||
subject_text: curr_fullMessage.headers.subject,
|
|
||||||
msg_text: msg_text
|
|
||||||
});
|
|
||||||
taLog.log("Special prompt: " + specialFullPrompt_spamfilter);
|
|
||||||
// console.log(">>>>>>>> Special prompt for spamfilter: " + specialFullPrompt_spamfilter);
|
|
||||||
let cmd_spamfilter = new mzta_specialCommand({
|
|
||||||
prompt: specialFullPrompt_spamfilter,
|
|
||||||
llm: getConnectionType(prefs_aats, curr_prompt_spamfilter, 'spamfilter'),
|
|
||||||
custom_model: curr_prompt_spamfilter.model ? curr_prompt_spamfilter.model : '',
|
|
||||||
do_debug: prefs_aats.do_debug,
|
|
||||||
config: curr_prompt_spamfilter
|
|
||||||
});
|
|
||||||
await cmd_spamfilter.initWorker();
|
|
||||||
let spamfilter_result = '';
|
|
||||||
taLog.log("Sending the prompt...");
|
|
||||||
try {
|
|
||||||
spamfilter_result = (await cmd_spamfilter.sendPrompt()).trim();
|
|
||||||
} catch (err) {
|
|
||||||
console.error("[ThunderAI | SpamFilter] Error getting spamfilter: ", err);
|
|
||||||
let err_data = await spamReport.saveError(message.headerMessageId, err.message || String(err));
|
|
||||||
await updateSpamPanel(message.headerMessageId, "showSpamReport", err_data);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
taLog.log("spamfilter_result: " + spamfilter_result);
|
|
||||||
let jsonObj = {};
|
|
||||||
taLog.log("Decoding the AI response...");
|
|
||||||
try {
|
|
||||||
jsonObj = extractJsonObject(spamfilter_result);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[ThunderAI | SpamFilter] Error extracting JSON from AI response: ", e);
|
|
||||||
let err_data = await spamReport.saveError(message.headerMessageId, e.message || String(e));
|
|
||||||
await updateSpamPanel(message.headerMessageId, "showSpamReport", err_data);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
taLog.log("SpamFilter jsonObj: " + JSON.stringify(jsonObj));
|
|
||||||
|
|
||||||
let report_data = {};
|
|
||||||
report_data.report_date = new Date();
|
|
||||||
report_data.headerMessageId = message.headerMessageId;
|
|
||||||
report_data.spamValue = jsonObj.spamValue;
|
|
||||||
report_data.explanation = jsonObj.explanation;
|
|
||||||
report_data.subject = curr_fullMessage.headers.subject;
|
|
||||||
report_data.from = curr_fullMessage.headers.from;
|
|
||||||
report_data.message_date = new Date(message.date);
|
|
||||||
report_data.moved = false;
|
|
||||||
report_data.SpamThreshold = prefs_init.spamfilter_threshold;
|
|
||||||
|
|
||||||
if (jsonObj.spamValue >= prefs_init.spamfilter_threshold) {
|
|
||||||
taLog.log("Marking as spam [" + message.headerMessageId + "]");
|
|
||||||
messenger.messages.update(message.id, { junk: true });
|
|
||||||
let spamFolder = await messenger.folders.query({ accountId: message.folder.accountId, specialUse: ['junk'] });
|
|
||||||
messenger.messages.move([message.id], spamFolder[0].id);
|
|
||||||
report_data.moved = true;
|
|
||||||
taLog.log("Marked as spam [" + message.headerMessageId + "]");
|
|
||||||
}
|
|
||||||
|
|
||||||
spamReport.saveReportData(report_data, message.headerMessageId);
|
|
||||||
|
|
||||||
// Check if the message is currently displayed and update the banner
|
|
||||||
await updateSpamPanel(message.headerMessageId, "showSpamReport", report_data);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (summarize) {
|
if (summarize) {
|
||||||
// we have three prompts, the actual assignment for the LLM, the email
|
|
||||||
// template prompt, and the email separator prompt
|
|
||||||
const specialPrompts = await getSpecialPrompts();
|
|
||||||
const prompt = specialPrompts.find((prompt) => prompt.id === 'prompt_summarize');
|
|
||||||
const prompt_email = specialPrompts.find((prompt) => prompt.id === 'prompt_summarize_email_template');
|
|
||||||
const prompt_email_separator = specialPrompts.find((prompt) => prompt.id === 'prompt_summarize_email_separator');
|
|
||||||
|
|
||||||
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
|
||||||
const chatgpt_lang = await taPromptUtils.getDefaultLang(prompt);
|
const tabId = tabs[0].id;
|
||||||
|
let summarize_prefs = await browser.storage.sync.get({ summarize_display_mode: prefs_default.summarize_display_mode });
|
||||||
// replace placeholders in the prompts the assignment prompt and email
|
|
||||||
// separator prompt do not have a message as context, so there is only
|
// Collect messages into array to check count
|
||||||
// limited things to replace
|
const messageArray = [];
|
||||||
const prompt_string = await taPromptUtils.preparePrompt({
|
for await (let msg of messages) {
|
||||||
curr_prompt: prompt,
|
messageArray.push(msg);
|
||||||
chatgpt_lang: chatgpt_lang,
|
}
|
||||||
});
|
|
||||||
const prompt_email_separator_string = await taPromptUtils.preparePrompt({
|
// Inline mode for single message: generate inline summary in the message pane
|
||||||
curr_prompt: prompt_email_separator,
|
if (summarize_prefs.summarize_display_mode === 'inline' && messageArray.length === 1) {
|
||||||
chatgpt_lang: chatgpt_lang,
|
await _generateSummaryForMessage(messageArray[0].headerMessageId, tabId);
|
||||||
});
|
} else {
|
||||||
|
// Webchat mode, or inline with multiple messages (fallback to webchat)
|
||||||
|
const messageDataArray = [];
|
||||||
// assemble all email messages into one string and add the assignment prompt
|
for (let curr_message of messageArray) {
|
||||||
const messages_list = [];
|
const fullMessage = await browser.messages.getFull(curr_message.id);
|
||||||
for await (let curr_message of messages) {
|
messageDataArray.push({ message: curr_message, fullMessage });
|
||||||
|
|
||||||
// extract body of current message as text
|
|
||||||
const curr_message_full = await browser.messages.getFull(curr_message.id);
|
|
||||||
const curr_body_full_html = getMailBody(curr_message_full);
|
|
||||||
let curr_body_full_text = htmlBodyToPlainText(curr_body_full_html.html);
|
|
||||||
if( curr_body_full_text.length === 0) {
|
|
||||||
taLog.log("No HTML found in the message body, using plain text...");
|
|
||||||
curr_body_full_text = curr_message_full.text;
|
|
||||||
}
|
}
|
||||||
|
const { promptText, promptInfo } = await taPromptUtils.buildSummaryPrompt(messageDataArray);
|
||||||
messages_list.push(await taPromptUtils.preparePrompt({
|
|
||||||
curr_prompt: prompt_email,
|
openChatGPT(promptText, promptInfo.action, tabId, promptInfo.name, promptInfo.need_custom_text, promptInfo);
|
||||||
curr_message: curr_message,
|
}
|
||||||
chatgpt_lang: chatgpt_lang,
|
|
||||||
body_text: curr_body_full_text,
|
|
||||||
subject_text: curr_message_full.headers.subject,
|
|
||||||
msg_text: curr_body_full_html,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
const messages_string = messages_list.join(prompt_email_separator_string);
|
|
||||||
|
|
||||||
const full_prompt = prompt_string + prompt_email_separator_string + messages_string;
|
|
||||||
|
|
||||||
// console.log(full_prompt);
|
|
||||||
|
|
||||||
// send the prompt to the chat interface
|
|
||||||
openChatGPT(
|
|
||||||
full_prompt,
|
|
||||||
prompt.action,
|
|
||||||
tabs[0].id,
|
|
||||||
prompt.name,
|
|
||||||
prompt.need_custom_text,
|
|
||||||
prompt
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
taWorkingStatus.stopWorking();
|
taWorkingStatus.stopWorking();
|
||||||
|
|
@ -1337,4 +1583,4 @@ for (let messageTab of messageTabs) {
|
||||||
console.error("[ThunderAI] Error injecting message display script:", error);
|
console.error("[ThunderAI] Error injecting message display script:", error);
|
||||||
console.error("[ThunderAI] Message tab:", messageTab.url);
|
console.error("[ThunderAI] Message tab:", messageTab.url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,10 @@ export const prefs_default = {
|
||||||
spamfilter: false,
|
spamfilter: false,
|
||||||
spamfilter_threshold: 70,
|
spamfilter_threshold: 70,
|
||||||
spamfilter_enabled_accounts: [],
|
spamfilter_enabled_accounts: [],
|
||||||
spamfilter_show_msg_panel: true,
|
|
||||||
summarize: false,
|
summarize: false,
|
||||||
|
summarize_auto: 1, // 0: disabled, 1: manual button, 2: automatic
|
||||||
|
summarize_display_mode: 'inline', // 'inline' or 'webchat'
|
||||||
|
summarize_max_display_length: 0, // 0 = no limit, otherwise max chars shown inline
|
||||||
|
spamfilter_show_msg_panel: true,
|
||||||
...generated_prefs
|
...generated_prefs
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
<div id="miczRelNotes"><h1>ThunderAI Release Notes</h1>
|
<div id="miczRelNotes"><h1>ThunderAI Release Notes</h1>
|
||||||
<h2>Version 4.1.0 - ??/??/2026</h2>
|
<h2>Version 4.1.0 - ??/??/2026</h2>
|
||||||
<ul>
|
<ul>
|
||||||
|
<li>Antispam information are now permanently saved for each message [<a href="https://github.com/micz/ThunderAI/issues/675">#675</a>].</li>
|
||||||
<li>...</li>
|
<li>...</li>
|
||||||
</ul>
|
</ul>
|
||||||
<h2>Version 4.0.3 - 20/03/2026</h2>
|
<h2>Version 4.0.3 - 20/03/2026</h2>
|
||||||
|
|
|
||||||
|
|
@ -330,19 +330,21 @@ async function restoreOptions() {
|
||||||
const restoreValue = result[element.id] || default_select_value;
|
const restoreValue = result[element.id] || default_select_value;
|
||||||
// Check if option exists
|
// Check if option exists
|
||||||
let optionExists = Array.from(element.options).some(opt => opt.value === restoreValue);
|
let optionExists = Array.from(element.options).some(opt => opt.value === restoreValue);
|
||||||
// If it doesn't exist and restoreValue is not empty, create it
|
|
||||||
if (!optionExists && restoreValue !== '') {
|
|
||||||
let newOption = new Option(restoreValue, restoreValue);
|
|
||||||
element.add(newOption);
|
|
||||||
}
|
|
||||||
// Set value
|
|
||||||
element.value = restoreValue;
|
|
||||||
if (element.value === '') {
|
|
||||||
element.selectedIndex = -1;
|
|
||||||
}
|
|
||||||
if (element.tomselect) {
|
if (element.tomselect) {
|
||||||
element.tomselect.setValue(element.value, true);
|
if (!optionExists && restoreValue !== '') {
|
||||||
|
element.tomselect.addOption({ value: restoreValue, text: restoreValue });
|
||||||
|
}
|
||||||
|
element.tomselect.setValue(restoreValue, true);
|
||||||
setTomSelectBorder(element.tomselect);
|
setTomSelectBorder(element.tomselect);
|
||||||
|
} else {
|
||||||
|
if (!optionExists && restoreValue !== '') {
|
||||||
|
let newOption = new Option(restoreValue, restoreValue);
|
||||||
|
element.add(newOption);
|
||||||
|
}
|
||||||
|
element.value = restoreValue;
|
||||||
|
if (element.value === '') {
|
||||||
|
element.selectedIndex = -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
console.error("[ThunderAI] Unhandled input type:", element.type);
|
console.error("[ThunderAI] Unhandled input type:", element.type);
|
||||||
|
|
|
||||||
|
|
@ -227,19 +227,21 @@ async function restoreOptions() {
|
||||||
const restoreValue = result[element.id] || default_select_value;
|
const restoreValue = result[element.id] || default_select_value;
|
||||||
// Check if option exists
|
// Check if option exists
|
||||||
let optionExists = Array.from(element.options).some(opt => opt.value === restoreValue);
|
let optionExists = Array.from(element.options).some(opt => opt.value === restoreValue);
|
||||||
// If it doesn't exist and restoreValue is not empty, create it
|
|
||||||
if (!optionExists && restoreValue !== '') {
|
|
||||||
let newOption = new Option(restoreValue, restoreValue);
|
|
||||||
element.add(newOption);
|
|
||||||
}
|
|
||||||
// Set value
|
|
||||||
element.value = restoreValue;
|
|
||||||
if (element.value === '') {
|
|
||||||
element.selectedIndex = -1;
|
|
||||||
}
|
|
||||||
if (element.tomselect) {
|
if (element.tomselect) {
|
||||||
element.tomselect.setValue(element.value, true);
|
if (!optionExists && restoreValue !== '') {
|
||||||
|
element.tomselect.addOption({ value: restoreValue, text: restoreValue });
|
||||||
|
}
|
||||||
|
element.tomselect.setValue(restoreValue, true);
|
||||||
setTomSelectBorder(element.tomselect);
|
setTomSelectBorder(element.tomselect);
|
||||||
|
} else {
|
||||||
|
if (!optionExists && restoreValue !== '') {
|
||||||
|
let newOption = new Option(restoreValue, restoreValue);
|
||||||
|
element.add(newOption);
|
||||||
|
}
|
||||||
|
element.value = restoreValue;
|
||||||
|
if (element.value === '') {
|
||||||
|
element.selectedIndex = -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
console.error("[ThunderAI] Unhandled input type:", element.type);
|
console.error("[ThunderAI] Unhandled input type:", element.type);
|
||||||
|
|
|
||||||
|
|
@ -180,19 +180,21 @@ async function restoreOptions() {
|
||||||
const restoreValue = result[element.id] || default_select_value;
|
const restoreValue = result[element.id] || default_select_value;
|
||||||
// Check if option exists
|
// Check if option exists
|
||||||
let optionExists = Array.from(element.options).some(opt => opt.value === restoreValue);
|
let optionExists = Array.from(element.options).some(opt => opt.value === restoreValue);
|
||||||
// If it doesn't exist and restoreValue is not empty, create it
|
|
||||||
if (!optionExists && restoreValue !== '') {
|
|
||||||
let newOption = new Option(restoreValue, restoreValue);
|
|
||||||
element.add(newOption);
|
|
||||||
}
|
|
||||||
// Set value
|
|
||||||
element.value = restoreValue;
|
|
||||||
if (element.value === '') {
|
|
||||||
element.selectedIndex = -1;
|
|
||||||
}
|
|
||||||
if (element.tomselect) {
|
if (element.tomselect) {
|
||||||
element.tomselect.setValue(element.value, true);
|
if (!optionExists && restoreValue !== '') {
|
||||||
|
element.tomselect.addOption({ value: restoreValue, text: restoreValue });
|
||||||
|
}
|
||||||
|
element.tomselect.setValue(restoreValue, true);
|
||||||
setTomSelectBorder(element.tomselect);
|
setTomSelectBorder(element.tomselect);
|
||||||
|
} else {
|
||||||
|
if (!optionExists && restoreValue !== '') {
|
||||||
|
let newOption = new Option(restoreValue, restoreValue);
|
||||||
|
element.add(newOption);
|
||||||
|
}
|
||||||
|
element.value = restoreValue;
|
||||||
|
if (element.value === '') {
|
||||||
|
element.selectedIndex = -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
console.error("[ThunderAI] Unhandled input type:", element.type);
|
console.error("[ThunderAI] Unhandled input type:", element.type);
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,7 @@
|
||||||
<button type="button" id="accounts_deselect_all">__MSG_DeselectAll__</button>
|
<button type="button" id="accounts_deselect_all">__MSG_DeselectAll__</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="spamfitler_reports_container">
|
<div id="spamfitler_reports_container">
|
||||||
<div><span class="section_title">__MSG_SpamReport_Title__</span>
|
<div><span class="section_title">__MSG_SpamReport_Title__</span></div>
|
||||||
<br><span class="infoline">__MSG_SpamReport_infoline__</span></div>
|
|
||||||
<table id="report_data">
|
<table id="report_data">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
|
|
||||||
|
|
@ -326,17 +326,21 @@ async function restoreOptions() {
|
||||||
const restoreValue = result[element.id] || default_select_value;
|
const restoreValue = result[element.id] || default_select_value;
|
||||||
// Ensure option exists before restoring
|
// Ensure option exists before restoring
|
||||||
let optionExists = Array.from(element.options).some(opt => opt.value === restoreValue);
|
let optionExists = Array.from(element.options).some(opt => opt.value === restoreValue);
|
||||||
if (!optionExists && restoreValue !== '') {
|
|
||||||
let newOption = new Option(restoreValue, restoreValue);
|
|
||||||
element.add(newOption);
|
|
||||||
}
|
|
||||||
element.value = restoreValue;
|
|
||||||
if (element.value === '') {
|
|
||||||
element.selectedIndex = -1;
|
|
||||||
}
|
|
||||||
if (element.tomselect) {
|
if (element.tomselect) {
|
||||||
element.tomselect.setValue(element.value, true);
|
if (!optionExists && restoreValue !== '') {
|
||||||
|
element.tomselect.addOption({ value: restoreValue, text: restoreValue });
|
||||||
|
}
|
||||||
|
element.tomselect.setValue(restoreValue, true);
|
||||||
setTomSelectBorder(element.tomselect);
|
setTomSelectBorder(element.tomselect);
|
||||||
|
} else {
|
||||||
|
if (!optionExists && restoreValue !== '') {
|
||||||
|
let newOption = new Option(restoreValue, restoreValue);
|
||||||
|
element.add(newOption);
|
||||||
|
}
|
||||||
|
element.value = restoreValue;
|
||||||
|
if (element.value === '') {
|
||||||
|
element.selectedIndex = -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
console.error("[ThunderAI] Unhandled input type:", element.type);
|
console.error("[ThunderAI] Unhandled input type:", element.type);
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,40 @@
|
||||||
</tr>
|
</tr>
|
||||||
<tr id="connection_ui_anchor"></tr>
|
<tr id="connection_ui_anchor"></tr>
|
||||||
<tr id="connection_ui_end"><td colspan="2"></td></tr>
|
<tr id="connection_ui_end"><td colspan="2"></td></tr>
|
||||||
|
<tr class="summarize_tr">
|
||||||
|
<td><span class="opt_title">__MSG_prefs_OptionText_summarize_auto__</span></td>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<select id="summarize_auto" name="summarize_auto" class="option-input">
|
||||||
|
<option value="0">__MSG_prefs_OptionText_summarize_auto_disabled__</option>
|
||||||
|
<option value="1">__MSG_prefs_OptionText_summarize_auto_manual__</option>
|
||||||
|
<option value="2">__MSG_prefs_OptionText_summarize_auto_automatic__</option>
|
||||||
|
</select>
|
||||||
|
<br>__MSG_prefs_OptionText_summarize_auto_Info__
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="summarize_tr">
|
||||||
|
<td><span class="opt_title">__MSG_prefs_OptionText_summarize_display_mode__</span></td>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<select id="summarize_display_mode" name="summarize_display_mode" class="option-input">
|
||||||
|
<option value="inline">__MSG_prefs_OptionText_summarize_display_mode_inline__</option>
|
||||||
|
<option value="webchat">__MSG_prefs_OptionText_summarize_display_mode_webchat__</option>
|
||||||
|
</select>
|
||||||
|
<br>__MSG_prefs_OptionText_summarize_display_mode_Info__
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="summarize_tr">
|
||||||
|
<td><span class="opt_title">__MSG_prefs_OptionText_summarize_max_display_length__</span></td>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="number" id="summarize_max_display_length" name="summarize_max_display_length" min="0" class="option-input" />
|
||||||
|
<br>__MSG_prefs_OptionText_summarize_max_display_length_Info__
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<!-- PROMPTS -->
|
<!-- PROMPTS -->
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,8 @@ import {
|
||||||
import { textareaAutocomplete } from "../../js/mzta-placeholders-autocomplete.js";
|
import { textareaAutocomplete } from "../../js/mzta-placeholders-autocomplete.js";
|
||||||
import {
|
import {
|
||||||
normalizeStringList,
|
normalizeStringList,
|
||||||
isAPIKeyValue
|
isAPIKeyValue,
|
||||||
|
setTomSelectBorder
|
||||||
} from "../../js/mzta-utils.js";
|
} from "../../js/mzta-utils.js";
|
||||||
import {
|
import {
|
||||||
initializeSpecificIntegrationUI
|
initializeSpecificIntegrationUI
|
||||||
|
|
@ -196,8 +197,11 @@ function saveOptions(e) {
|
||||||
options[element.id] = element.value.trim();
|
options[element.id] = element.value.trim();
|
||||||
break;
|
break;
|
||||||
case 'select-one':
|
case 'select-one':
|
||||||
// console.log(">>>>>>>>>> Saving option [select-one]: " + element.id + " = " + element.value);
|
if (element.id === 'summarize_auto') {
|
||||||
options[element.id] = element.value;
|
options[element.id] = parseInt(element.value, 10);
|
||||||
|
} else {
|
||||||
|
options[element.id] = element.value;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case 'textarea':
|
case 'textarea':
|
||||||
options[element.id] = normalizeStringList(element.value);
|
options[element.id] = normalizeStringList(element.value);
|
||||||
|
|
@ -233,22 +237,31 @@ async function restoreOptions() {
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if (element.tagName === 'SELECT') {
|
if (element.tagName === 'SELECT') {
|
||||||
let default_select_value = '';
|
let default_select_value = 0;
|
||||||
const restoreValue = result[element.id] || default_select_value;
|
if (element.id === 'summarize_auto') {
|
||||||
|
default_select_value = prefs_default.summarize_auto;
|
||||||
|
}
|
||||||
|
if (element.id === 'summarize_display_mode') {
|
||||||
|
default_select_value = prefs_default.summarize_display_mode;
|
||||||
|
}
|
||||||
|
const restoreValue = result[element.id] ?? default_select_value;
|
||||||
// Check if option exists
|
// Check if option exists
|
||||||
let optionExists = Array.from(element.options).some(opt => opt.value === restoreValue);
|
let optionExists = Array.from(element.options).some(opt => opt.value === String(restoreValue));
|
||||||
// If it doesn't exist and restoreValue is not empty, create it
|
|
||||||
if (!optionExists && restoreValue !== '') {
|
|
||||||
let newOption = new Option(restoreValue, restoreValue);
|
|
||||||
element.add(newOption);
|
|
||||||
}
|
|
||||||
// Set value
|
|
||||||
element.value = restoreValue;
|
|
||||||
if (element.value === '') {
|
|
||||||
element.selectedIndex = -1;
|
|
||||||
}
|
|
||||||
if (element.tomselect) {
|
if (element.tomselect) {
|
||||||
element.tomselect.setValue(element.value, true);
|
if (!optionExists && restoreValue !== '') {
|
||||||
|
element.tomselect.addOption({ value: String(restoreValue), text: String(restoreValue) });
|
||||||
|
}
|
||||||
|
element.tomselect.setValue(String(restoreValue), true);
|
||||||
|
setTomSelectBorder(element.tomselect);
|
||||||
|
} else {
|
||||||
|
if (!optionExists && restoreValue !== '') {
|
||||||
|
let newOption = new Option(restoreValue, restoreValue);
|
||||||
|
element.add(newOption);
|
||||||
|
}
|
||||||
|
element.value = restoreValue;
|
||||||
|
if (element.value === '') {
|
||||||
|
element.selectedIndex = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}else{
|
}else{
|
||||||
console.error("[ThunderAI] Unhandled input type:", element.type);
|
console.error("[ThunderAI] Unhandled input type:", element.type);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue