Merge pull request #628 from micz/thread_summary_issue615

Thread summary issue 615
This commit is contained in:
Mic 2026-01-30 22:11:36 +01:00 committed by GitHub
commit bd12e430bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 904 additions and 61 deletions

36
GEMINI.md Normal file
View file

@ -0,0 +1,36 @@
# ThunderAI Project Agent Context
## Project Overview
ThunderAI is a Mozilla Thunderbird add-on that integrates Large Language Models (LLMs) such as ChatGPT, Gemini, Claude, and Ollama for advanced email management. It allows users to analyze, draft, correct, tag, and create calendar events directly from the email client.
## Technical Stack
- **Languages:** JavaScript (ES6+), HTML5, CSS3.
- **Environment:** Thunderbird MailExtension API (based on WebExtensions).
- **Core Logic:** Primarily located in `background.js` and UI-specific scripts.
- **API Integrations:** Specific logic is contained within dedicated API provider files (e.g., `api_openai.js`, `api_google_gemini.js`).
## Development Rules & Style Guide
- **Code Style:** Use modern JavaScript. Prefer `async/await` over chained Promises.
- **Naming Conventions:** Use `camelCase` for variables and functions.
- **Security:** - Never hardcode API keys in the source code.
- Use `messenger.storage.local` for data persistence.
- Strictly adhere to CORS policies for external API calls.
- **i18n:**
- The project supports multiple languages via `_locales/`. When adding UI strings, always use `messenger.i18n.getMessage()`.
- When you need to modify language file, modify only the english version.
## Files & Directories Structure
- `manifest.json`: Extension entry point and permission definitions.
- `api_*.js`: Modules for integration with different AI providers.
- `options/`: Configuration and settings pages.
- `_locales/`: Translation files (JSON format).
- `graphics/`: Visual assets and icons.
## Important Commands & Workflow
- **Testing:** Since this is a Thunderbird add-on, testing is performed by loading the extension as a "Temporary Add-on" via Thunderbird's `Debug Add-ons` menu.
- **Build:** The project does not use complex build tools (no Webpack/Vite by default); it is a "pure" MailExtension.
## Agent Instructions
1. **Context Awareness:** When modifying API-related files, ensure compatibility with the existing placeholder system (e.g., `{%mail_body%}`, `{%additional_text%}`).
2. **Permission Review:** Before modifying `manifest.json`, verify that any new permissions requested are strictly necessary for the feature.
3. **Compatibility:** Ensure code is compatible with the latest Thunderbird ESR (Extended Support Release) versions.

View file

@ -111,6 +111,7 @@ _The language status represents the percentage of translated strings in the late
- [JessiGue](https://www.flaticon.com/authors/jessigue) for the show/hide icon for api key fields - [JessiGue](https://www.flaticon.com/authors/jessigue) for the show/hide icon for api key fields
- [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
<br> <br>

View file

@ -1003,6 +1003,34 @@
"message": "Extract all relevant details required to generate a calendar event from the following text. The extracted information should include:\n- Event Title\n- Start Date and Time (including timezone, if specified)\n- End Date and Time (including timezone, if specified)\n- Full day (if mentioned)\n- Attendees\nEnsure the data is formatted clearly and consistently so that it can be directly used for creating a calendar event.\nIf there are relative time references, consider that the date and time of the email are \"{%mail_datetime%}\". Calculate the start date and time based on this reference. If the calculated start date and time are earlier than \"{%current_datetime%}\", recalculate the start date and time using \"{%current_datetime%}\" as the base.\nIf the duration is not specified, set it to one hour.\nThese are the attendees: {%author%}, {%recipients%}, {%cc_list%}. If present, exclude my address: {%account_email_address%}.\nIf you're not able to get one or more of the required information, please respond with an empty string.\nGenerate a response in JSON format only. Do not include any additional text or explanations; provide only the JSON. Here is the format to be used:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Calendar event summary here\",\n\"forceAllDay\": false,\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nHere's the text:\"{%selected_text%}\"", "message": "Extract all relevant details required to generate a calendar event from the following text. The extracted information should include:\n- Event Title\n- Start Date and Time (including timezone, if specified)\n- End Date and Time (including timezone, if specified)\n- Full day (if mentioned)\n- Attendees\nEnsure the data is formatted clearly and consistently so that it can be directly used for creating a calendar event.\nIf there are relative time references, consider that the date and time of the email are \"{%mail_datetime%}\". Calculate the start date and time based on this reference. If the calculated start date and time are earlier than \"{%current_datetime%}\", recalculate the start date and time using \"{%current_datetime%}\" as the base.\nIf the duration is not specified, set it to one hour.\nThese are the attendees: {%author%}, {%recipients%}, {%cc_list%}. If present, exclude my address: {%account_email_address%}.\nIf you're not able to get one or more of the required information, please respond with an empty string.\nGenerate a response in JSON format only. Do not include any additional text or explanations; provide only the JSON. Here is the format to be used:\n{\n\"startDate\": \"YYYYMMDDTHHMMSS\",\n\"endDate\": \"YYYYMMDDTHHMMSS\",\n\"summary\": \"Calendar event summary here\",\n\"forceAllDay\": false,\n\"attendees\": [attendee1@example.com,attendee2@example.com,attendee3@example.com]\n}\nHere's the text:\"{%selected_text%}\"",
"description": "" "description": ""
}, },
"Summarize_prompt_prefs_title": {
"message": "Add Summarization Options",
"description": ""
},
"prompt_summarize": {
"message": "Summarize this email or these emails",
"description": ""
},
"prompt_summarize_full_text": {
"message": "You are an assistant that summarizes email conversations.\n\nGiven an email thread, produce a concise, accurate summary that captures:\n\n- The main topic or purpose of the conversation\n- Key decisions, conclusions, or agreements\n- Important questions, requests, or action items\n- Who is responsible for each action item (if stated)\n\nOmit greetings, signatures, quoted text, and redundant back-and-forth.\nDo not add assumptions or information not present in the emails.\n\nWrite the summary in clear, neutral language suitable for a busy professional.",
"description": ""
},
"prompt_summarize_email_template": {
"message": "Summarize email template",
"description": ""
},
"prompt_summarize_email_template_full_text": {
"message": "From: {%author%}\nTo: {%recipients%}\nCC: {%cc_list%}\nSubject: {%mail_subject%}\nDate: {%mail_datetime%}\nAttachments:\n{%mail_attachments_info%}\n\nBody:\n{%mail_text_body%}",
"description": ""
},
"prompt_summarize_email_separator": {
"message": "Email separator",
"description": ""
},
"prompt_summarize_email_separator_full_text": {
"message": "\n\n---------- NEXT EMAIL ----------\n\n",
"description": ""
},
"prompt_get_task": { "prompt_get_task": {
"message": "Add a new task", "message": "Add a new task",
"description": "" "description": ""
@ -1067,6 +1095,22 @@
"message": "You can change the prompt as you wish, but the response received from the AI must be in JSON format as specified in the default prompt!", "message": "You can change the prompt as you wish, but the response received from the AI must be in JSON format as specified in the default prompt!",
"description": "" "description": ""
}, },
"prefs_OptionText_Summarize_infoline2": {
"message": "You can change the prompt as you wish, the first field is the main prompt, the second field is the template for a single mail. The list of emails will be appended to the main prompt. Emails will be separated by the separator specified in the third field.",
"description": ""
},
"prefs_OptionText_Summarize_main_prompt": {
"message": "The main prompt describing the task to be performed on all selected emails:",
"description": ""
},
"prefs_OptionText_Summarize_email_template": {
"message": "The template for a single email:",
"description": ""
},
"prefs_OptionText_Summarize_email_separator": {
"message": "The separator between emails:",
"description": ""
},
"prefs_OptionText_get_calendar_event_Sparks_not_present": { "prefs_OptionText_get_calendar_event_Sparks_not_present": {
"message": "To use the calendar event and task features, please install the ThunderAI Sparks addon.", "message": "To use the calendar event and task features, please install the ThunderAI Sparks addon.",
"description": "" "description": ""
@ -1195,6 +1239,22 @@
"message": "Manage spam filter settings", "message": "Manage spam filter settings",
"description": "" "description": ""
}, },
"prefs_OptionText_summarize": {
"message": "Summarize mail",
"description": ""
},
"prefs_OptionText_summarize_use_specific_integration_Info": {
"message": "If checked, the Model and API specified below will be used for summarizing email(s), regardless the one choosen in the ThunderAI options page.",
"description": ""
},
"prefs_OptionText_summarize_Info": {
"message": "If checked, adds an option to context menu to summarize mail.",
"description": ""
},
"prefs_OptionText_btnManageSummarizeInfo": {
"message": "Manage summarize settings",
"description": ""
},
"SpamFilter_PageTitle": { "SpamFilter_PageTitle": {
"message": "Manage Spam Filter Settings", "message": "Manage Spam Filter Settings",
"description": "" "description": ""
@ -1219,6 +1279,18 @@
"message": "Spam Filter Options", "message": "Spam Filter Options",
"description": "" "description": ""
}, },
"Summarize_PageTitle": {
"message": "Manage Summarize Settings",
"description": ""
},
"Summarize_info_default": {
"message": "In this page you can modify the default prompt used to summarize emails.",
"description": ""
},
"Summarize_prompt_text_title": {
"message": "Current prompt text",
"description": ""
},
"prefs_OptionText_use_specific_integration": { "prefs_OptionText_use_specific_integration": {
"message": "Use specific Model and API", "message": "Use specific Model and API",
"description": "" "description": ""
@ -1295,20 +1367,8 @@
"message": "Analyze for spam", "message": "Analyze for spam",
"description": "" "description": ""
}, },
"prefs_OptionText_add_tags_context_menu": { "context_menu_mzta-summarize": {
"message": "Show the \"Add tags\" context menu item", "message": "Summarize",
"description": ""
},
"prefs_OptionText_add_tags_context_menu_Info": {
"message": "If checked, the \"Add tags\" context menu item will be shown when right-clicking on an email in the messages list.",
"description": ""
},
"prefs_OptionText_spamfilter_context_menu": {
"message": "Show the \"Analyze for spam\" context menu item",
"description": ""
},
"prefs_OptionText_spamfilter_context_menu_Info": {
"message": "If checked, the \"Analyze for spam\" context menu item will be shown when right-clicking on an email in the messages list.",
"description": "" "description": ""
}, },
"noActiveCalendar": { "noActiveCalendar": {

BIN
images/summarize.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -324,6 +324,54 @@ const specialPrompts = [
is_default: "1", is_default: "1",
is_special: "1", is_special: "1",
}, },
{
id: 'prompt_summarize',
name: "__MSG_prompt_summarize__",
text: "prompt_summarize_full_text",
type: "1",
action: "0",
need_selected: "0",
need_signature: "0",
need_custom_text: "0",
define_response_lang: "0",
use_diff_viewer: "0",
api_type: '',
api_model: '',
is_default: "1",
is_special: "1",
},
{
id: 'prompt_summarize_email_template',
name: "__MSG_prompt_summarize_email_template__",
text: "prompt_summarize_email_template_full_text",
type: "1",
action: "0",
need_selected: "0",
need_signature: "0",
need_custom_text: "0",
define_response_lang: "0",
use_diff_viewer: "0",
api_type: '',
api_model: '',
is_default: "1",
is_special: "1",
},
{
id: 'prompt_summarize_email_separator',
name: "__MSG_prompt_summarize_email_separator__",
text: "prompt_summarize_email_separator_full_text",
type: "1",
action: "0",
need_selected: "0",
need_signature: "0",
need_custom_text: "0",
define_response_lang: "0",
use_diff_viewer: "0",
api_type: '',
api_model: '',
is_default: "1",
is_special: "1",
}
]; ];

View file

@ -25,9 +25,11 @@ export const getMenuContextDisplay = () => 'message_display_action_menu';
export const contextMenuID_AddTags = 'mzta-add-tags'; export const contextMenuID_AddTags = 'mzta-add-tags';
export const contextMenuID_Spamfilter = 'mzta-spamfilter'; export const contextMenuID_Spamfilter = 'mzta-spamfilter';
export const contextMenuID_Summarize = 'mzta-summarize';
export const contextMenuIconsPath = { export const contextMenuIconsPath = {
[contextMenuID_AddTags]: 'moz-extension:images/autotags.png', [contextMenuID_AddTags]: 'moz-extension:images/autotags.png',
[contextMenuID_Spamfilter]: 'moz-extension:images/spamfilter.png', [contextMenuID_Spamfilter]: 'moz-extension:images/spamfilter.png',
[contextMenuID_Summarize]: 'moz-extension:images/summarize.png',
}; };
export function getLanguageDisplayName(languageCode) { export function getLanguageDisplayName(languageCode) {

View file

@ -45,6 +45,7 @@ import {
extractJsonObject, extractJsonObject,
contextMenuID_AddTags, contextMenuID_AddTags,
contextMenuID_Spamfilter, contextMenuID_Spamfilter,
contextMenuID_Summarize,
contextMenuIconsPath, contextMenuIconsPath,
sanitizeChatGPTModelData, sanitizeChatGPTModelData,
sanitizeChatGPTWebCustomData, sanitizeChatGPTWebCustomData,
@ -57,7 +58,10 @@ 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 { getSpamFilterPrompt } from './js/mzta-prompts.js'; import {
getSpamFilterPrompt,
getSpecialPrompts
} from './js/mzta-prompts.js';
import { taSpamReport } from './js/mzta-spamreport.js'; import { taSpamReport } from './js/mzta-spamreport.js';
import { taWorkingStatus } from './js/mzta-working-status.js'; import { taWorkingStatus } from './js/mzta-working-status.js';
import { addTags_getExclusionList, checkExcludedTag } from './js/mzta-addatags-exclusion-list.js'; import { addTags_getExclusionList, checkExcludedTag } from './js/mzta-addatags-exclusion-list.js';
@ -802,10 +806,9 @@ async function reload_pref_init(){
add_tags_auto_force_existing: prefs_default.add_tags_auto_force_existing, add_tags_auto_force_existing: prefs_default.add_tags_auto_force_existing,
add_tags_auto_only_inbox: prefs_default.add_tags_auto_only_inbox, add_tags_auto_only_inbox: prefs_default.add_tags_auto_only_inbox,
spamfilter: prefs_default.spamfilter, spamfilter: prefs_default.spamfilter,
summarize: prefs_default.summarize,
spamfilter_threshold: prefs_default.spamfilter_threshold, spamfilter_threshold: prefs_default.spamfilter_threshold,
dynamic_menu_force_enter: prefs_default.dynamic_menu_force_enter, dynamic_menu_force_enter: prefs_default.dynamic_menu_force_enter,
add_tags_context_menu: prefs_default.add_tags_context_menu,
spamfilter_context_menu: prefs_default.spamfilter_context_menu,
...getDynamicSettingsDefaults(['use_specific_integration', 'connection_type']) ...getDynamicSettingsDefaults(['use_specific_integration', 'connection_type'])
}); });
_process_incoming = prefs_init.add_tags_auto || prefs_init.spamfilter; _process_incoming = prefs_init.add_tags_auto || prefs_init.spamfilter;
@ -880,43 +883,33 @@ function setupStorageChangeListener() {
if(newConnectionType === "chatgpt_web"){ if(newConnectionType === "chatgpt_web"){
removeContextMenu(contextMenuID_AddTags); removeContextMenu(contextMenuID_AddTags);
removeContextMenu(contextMenuID_Spamfilter); removeContextMenu(contextMenuID_Spamfilter);
removeContextMenu(contextMenuID_Summarize);
} }
addContextMenuItems(); addContextMenuItems();
} }
// context menu changes for add_tags and spamfilter // context menu changes for add_tags and spamfilter
if (changes.add_tags_context_menu) {
if(changes.add_tags_context_menu.newValue){
addContextMenu(contextMenuID_AddTags);
}else{
removeContextMenu(contextMenuID_AddTags);
}
}
if (changes.add_tags) { if (changes.add_tags) {
if(changes.add_tags.newValue){ if(changes.add_tags.newValue){
if(prefs_init.add_tags_context_menu){
addContextMenu(contextMenuID_AddTags); addContextMenu(contextMenuID_AddTags);
}
}else{ }else{
removeContextMenu(contextMenuID_AddTags); removeContextMenu(contextMenuID_AddTags);
} }
} }
if (changes.spamfilter_context_menu) {
if(changes.spamfilter_context_menu.newValue){
addContextMenu(contextMenuID_Spamfilter);
}else{
removeContextMenu(contextMenuID_Spamfilter);
}
}
if (changes.spamfilter) { if (changes.spamfilter) {
if(changes.spamfilter.newValue){ if(changes.spamfilter.newValue){
if(prefs_init.spamfilter_context_menu){
addContextMenu(contextMenuID_Spamfilter); addContextMenu(contextMenuID_Spamfilter);
}
}else{ }else{
removeContextMenu(contextMenuID_Spamfilter); removeContextMenu(contextMenuID_Spamfilter);
} }
} }
if (changes.summarize) {
if (changes.summarize.newValue){
addContextMenu(contextMenuID_Summarize);
} else {
removeContextMenu(contextMenuID_Summarize);
}
}
reload_pref_init(); reload_pref_init();
} }
}); });
@ -969,14 +962,19 @@ function removeContextMenu(menu_id) {
function addContextMenuItems() { function addContextMenuItems() {
// Add Context menu: Add tags // Add Context menu: Add tags
if(prefs_init.add_tags && prefs_init.add_tags_context_menu && checkAPIIntegration(prefs_init.connection_type, prefs_init.add_tags_use_specific_integration,prefs_init.add_tags_connection_type)){ if(prefs_init.add_tags && checkAPIIntegration(prefs_init.connection_type, prefs_init.add_tags_use_specific_integration,prefs_init.add_tags_connection_type)){
addContextMenu(contextMenuID_AddTags); addContextMenu(contextMenuID_AddTags);
} }
// Add Context menu: Spamfilter // Add Context menu: Spamfilter
if(prefs_init.spamfilter && prefs_init.spamfilter_context_menu && checkAPIIntegration(prefs_init.connection_type, prefs_init.spamfilter_use_specific_integration,prefs_init.spamfilter_connection_type)){ if(prefs_init.spamfilter && checkAPIIntegration(prefs_init.connection_type, prefs_init.spamfilter_use_specific_integration,prefs_init.spamfilter_connection_type)){
addContextMenu(contextMenuID_Spamfilter); addContextMenu(contextMenuID_Spamfilter);
} }
// Add Context menu: Summarize
if(prefs_init.summarize && checkAPIIntegration(prefs_init.connection_type, prefs_init.summarize_use_specific_integration, prefs_init.summarize_connection_type)) {
addContextMenu(contextMenuID_Summarize);
}
} }
addContextMenuItems(); addContextMenuItems();
@ -985,17 +983,93 @@ addContextMenuItems();
browser.menus.onClicked.addListener( (info, tab) => { browser.menus.onClicked.addListener( (info, tab) => {
let _add_tags = false let _add_tags = false
let _spamfilter = false let _spamfilter = false
let _summarize = false;
if(info.menuItemId === contextMenuID_AddTags){ if(info.menuItemId === contextMenuID_AddTags){
_add_tags = true; _add_tags = true;
} }
if(info.menuItemId === contextMenuID_Spamfilter){ if(info.menuItemId === contextMenuID_Spamfilter){
_spamfilter = true; _spamfilter = true;
} }
if(info.menuItemId === contextMenuID_Summarize) {
_summarize = true;
}
if(_add_tags || _spamfilter){ if(_add_tags || _spamfilter){
processEmails(getMessages(info.selectedMessages), _add_tags, _spamfilter); processEmails(getMessages(info.selectedMessages), _add_tags, _spamfilter);
} }
if(_summarize) {
// info.selectedMessages is of type MessageList
summarizeEmails(getMessages(info.selectedMessages));
}
}); });
async function summarizeEmails(messages) {
taWorkingStatus.startWorking();
// we have three prompts, the actual assignment for the LLM, the email
// template prompt, and the email separator prompt
const specialPrompts = await getSpecialPrompts();
const prompt = specialPrompts.find((prompt) => prompt.id === 'prompt_summarize');
const prompt_email = specialPrompts.find((prompt) => prompt.id === 'prompt_summarize_email_template');
const prompt_email_separator = specialPrompts.find((prompt) => prompt.id === 'prompt_summarize_email_separator');
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
const chatgpt_lang = await taPromptUtils.getDefaultLang(prompt);
// replace placeholders in the prompts the assignment prompt and email
// separator prompt do not have a message as context, so there is only
// limited things to replace
const prompt_string = await taPromptUtils.preparePrompt({
curr_prompt: prompt,
chatgpt_lang: chatgpt_lang,
});
const prompt_email_separator_string = await taPromptUtils.preparePrompt({
curr_prompt: prompt_email_separator,
chatgpt_lang: chatgpt_lang,
});
// assemble all email messages into one string and add the assignment prompt
const messages_list = [];
for await (let curr_message of messages) {
// extract body of current message as text
const curr_message_full = await browser.messages.getFull(curr_message.id);
const curr_body_full_html = getMailBody(curr_message_full);
const curr_body_full_text = htmlBodyToPlainText(curr_body_full_html.html);
if( curr_body_full_text.length === 0) {
taLog.log("No HTML found in the message body, using plain text...");
curr_body_full_text = curr_message_full.text;
}
messages_list.push(await taPromptUtils.preparePrompt({
curr_prompt: prompt_email,
curr_message: curr_message,
chatgpt_lang: chatgpt_lang,
body_text: curr_body_full_text,
subject_text: curr_message_full.headers.subject,
msg_text: curr_body_full_html,
}));
};
const messages_string = messages_list.join(prompt_email_separator_string);
const full_prompt = prompt_string + prompt_email_separator_string + messages_string;
// console.log(full_prompt);
// send the prompt to the chat interface
openChatGPT(
full_prompt,
prompt.action,
tabs[0].id,
prompt.name,
prompt.need_custom_text,
prompt
)
taWorkingStatus.stopWorking();
return {ok : '1'};
}
// Listening for new received emails // Listening for new received emails
const newEmailListener = (folder, messagesList) => { const newEmailListener = (folder, messagesList) => {

View file

@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
const special_prompts_with_integration = ['add_tags', 'spamfilter']; const special_prompts_with_integration = ['add_tags', 'spamfilter', 'summarize'];
export const integration_options_config = { export const integration_options_config = {
chatgpt: { chatgpt: {
@ -123,7 +123,6 @@ export const prefs_default = {
add_tags_auto_only_inbox: true, add_tags_auto_only_inbox: true,
add_tags_auto_uselist: false, add_tags_auto_uselist: false,
add_tags_auto_uselist_list: '', add_tags_auto_uselist_list: '',
add_tags_context_menu: true,
add_tags_enabled_accounts: [], add_tags_enabled_accounts: [],
get_calendar_event: true, get_calendar_event: true,
get_task: true, get_task: true,
@ -131,7 +130,7 @@ export const prefs_default = {
calendar_timezone: '', calendar_timezone: '',
spamfilter: false, spamfilter: false,
spamfilter_threshold: 70, spamfilter_threshold: 70,
spamfilter_context_menu: true,
spamfilter_enabled_accounts: [], spamfilter_enabled_accounts: [],
summarize: false,
...generated_prefs ...generated_prefs
} }

View file

@ -147,6 +147,17 @@
</label> </label>
</td> </td>
</tr> </tr>
<tr class="summarize_tr">
<td><span class="opt_title">__MSG_prefs_OptionText_summarize__</span>
<br><button id="btnManageSummarizeInfo" class="btn_small">__MSG_prefs_OptionText_btnManageSummarizeInfo__</button></td>
<td>
<label>
<input type="checkbox" id="summarize" name="summarize" class="option-input" />
__MSG_prefs_OptionText_summarize_Info__
<br><span class="warn_API_needed" id="summarize_warn_API_needed">__MSG_warn_API_needed__</span>
</label>
</td>
</tr>"
<tr class="get_calendar_event_tr"> <tr class="get_calendar_event_tr">
<td><span class="opt_title">__MSG_prefs_OptionText_get_calendar_event__</span> <td><span class="opt_title">__MSG_prefs_OptionText_get_calendar_event__</span>
<br><button id="btnManageCalendarEventInfo" class="btn_small">__MSG_prefs_OptionText_btnManageCalendarEventInfo__</button></td> <br><button id="btnManageCalendarEventInfo" class="btn_small">__MSG_prefs_OptionText_btnManageCalendarEventInfo__</button></td>

View file

@ -167,6 +167,27 @@ function disable_SpamFilter(prefs_opt){
} }
} }
function disable_Summarize(prefs_opt){
let summarize = document.getElementById('summarize');
let conntype_select = document.getElementById("connection_type");
const tempPrefs = {
connection_type: conntype_select.value,
...prefs_opt
};
let summarize_disabled = (getConnectionType(tempPrefs, null, 'summarize') === "chatgpt_web");
let summarize_checked_original = summarize.checked;
summarize.checked = summarize_disabled ? false : summarize.checked;
if(!summarize.checked){
let summarize_info_btn = document.getElementById('btnManageSummarizeInfo');
summarize_info_btn.disabled = 'disabled';
}
let summarize_warn_API_needed = document.getElementById('summarize_warn_API_needed');
summarize_warn_API_needed.style.display = (summarize_disabled) ? 'inline-block' : 'none';
if(summarize_checked_original != summarize.checked){
browser.storage.sync.set({summarize: summarize.checked});
}
}
async function disable_GetCalendarEvent(){ async function disable_GetCalendarEvent(){
let get_calendar_event = document.getElementById('get_calendar_event'); let get_calendar_event = document.getElementById('get_calendar_event');
let get_task = document.getElementById('get_task'); let get_task = document.getElementById('get_task');
@ -262,6 +283,13 @@ document.addEventListener('DOMContentLoaded', async () => {
}); });
spamfilter_info_btn.disabled = spamfilter_el.checked ? '' : 'disabled'; spamfilter_info_btn.disabled = spamfilter_el.checked ? '' : 'disabled';
let summarize_el = document.getElementById('summarize');
let summarize_info_btn = document.getElementById('btnManageSummarizeInfo');
summarize_el.addEventListener('click', (event) => {
summarize_info_btn.disabled = event.target.checked ? '' : 'disabled';
});
summarize_info_btn.disabled = summarize_el.checked ? '' : 'disabled';
let get_calendar_event_el = document.getElementById('get_calendar_event'); let get_calendar_event_el = document.getElementById('get_calendar_event');
let get_calendar_event_info_btn = document.getElementById('btnManageCalendarEventInfo'); let get_calendar_event_info_btn = document.getElementById('btnManageCalendarEventInfo');
get_calendar_event_el.addEventListener('click', (event) => { get_calendar_event_el.addEventListener('click', (event) => {
@ -292,6 +320,10 @@ document.addEventListener('DOMContentLoaded', async () => {
openTab('/pages/spamfilter/mzta-spamfilter.html'); openTab('/pages/spamfilter/mzta-spamfilter.html');
}); });
document.getElementById('btnManageSummarizeInfo').addEventListener('click', () => {
openTab('/pages/summarize/mzta-summarize.html');
});
document.getElementById('btnManageCalendarEventInfo').addEventListener('click', () => { document.getElementById('btnManageCalendarEventInfo').addEventListener('click', () => {
openTab('/pages/get-calendar-event/mzta-get-calendar-event.html'); openTab('/pages/get-calendar-event/mzta-get-calendar-event.html');
}); });
@ -317,12 +349,14 @@ document.addEventListener('DOMContentLoaded', async () => {
conntype_select.addEventListener("change", disable_MaxPromptLength); conntype_select.addEventListener("change", disable_MaxPromptLength);
conntype_select.addEventListener("change", () => disable_AddTags(prefs_opt)); conntype_select.addEventListener("change", () => disable_AddTags(prefs_opt));
conntype_select.addEventListener("change", () => disable_SpamFilter(prefs_opt)); conntype_select.addEventListener("change", () => disable_SpamFilter(prefs_opt));
conntype_select.addEventListener("change", () => disable_Summarize(prefs_opt));
conntype_select.addEventListener("change", disable_GetCalendarEvent); conntype_select.addEventListener("change", disable_GetCalendarEvent);
showConnectionOptions(conntype_select); showConnectionOptions(conntype_select);
disable_MaxPromptLength(); disable_MaxPromptLength();
disable_AddTags(prefs_opt); disable_AddTags(prefs_opt);
disable_SpamFilter(prefs_opt); disable_SpamFilter(prefs_opt);
disable_Summarize(prefs_opt);
disable_GetCalendarEvent(); disable_GetCalendarEvent();
document.getElementById('reset_max_prompt_length').addEventListener('click', resetMaxPromptLength); document.getElementById('reset_max_prompt_length').addEventListener('click', resetMaxPromptLength);

View file

@ -101,15 +101,7 @@
</label> </label>
</td> </td>
</tr> </tr>
<tr class="add_tags_tr">
<td><span class="opt_title">__MSG_prefs_OptionText_add_tags_context_menu__</span></td>
<td>
<label>
<input type="checkbox" id="add_tags_context_menu" name="add_tags_context_menu" class="option-input" />
__MSG_prefs_OptionText_add_tags_context_menu_Info__
</label>
</td>
</tr>
<tr class="add_tags_tr"> <tr class="add_tags_tr">
<td><span class="opt_title">__MSG_prefs_OptionText_add_tags_auto_force_existing__</span></td> <td><span class="opt_title">__MSG_prefs_OptionText_add_tags_auto_force_existing__</span></td>
<td> <td>

View file

@ -35,15 +35,7 @@
</label> </label>
</td> </td>
</tr> </tr>
<tr class="spamfilter_tr">
<td><span class="opt_title">__MSG_prefs_OptionText_spamfilter_context_menu__</span></td>
<td>
<label>
<input type="checkbox" id="spamfilter_context_menu" name="spamfilter_context_menu" class="option-input" />
__MSG_prefs_OptionText_spamfilter_context_menu_Info__
</label>
</td>
</tr>
</table> </table>
<div id="spamfilter_container"> <div id="spamfilter_container">
<span class="section_title">__MSG_SpamFilter_prompt_text_title__</span><span id="spamfilter_prompt_unsaved" class="unsaved hidden"> __MSG_customPrompts_unsaved_changes__</span> <span class="section_title">__MSG_SpamFilter_prompt_text_title__</span><span id="spamfilter_prompt_unsaved" class="unsaved hidden"> __MSG_customPrompts_unsaved_changes__</span>

View file

@ -0,0 +1,193 @@
#summarize_prompt_container {
width: 90%;
margin: 20px auto;
}
#summarize_email_template_text {
width: 100%;
}
#summarize_prompt_text {
width: 100%;
}
#summarize_email_separator_text {
width: 100%;
}
.infoline {
font-size: 0.8em;
font-style: italic;
}
#account_selector_container{
display: none;
}
#account_selector_checkboxes{
padding: 10px;
border: 1px solid gray;
width: 24em;
margin-bottom: 10px;
margin-top: 10px;
}
.specific_integration{
background-color: #dfeaff;
}
table {
border-collapse: separate;
border-spacing: 0;
}
.group td {
border-top: none;
border-bottom: none;
}
.specific_integration_sub td:first-child {
border-left: 10px solid #dfeaff;
}
.specific_integration_sub td:last-child {
border-right: 10px solid #dfeaff;
}
#connection_ui_end td{
height: 6px;
background-color: #dfeaff;
}
#connection_ui_end{
display: none;
}
.btn_div {
width: 100%;
display: flex;
justify-content: space-between;
}
table#miczPrefs {
padding: 10px;
border-spacing: 0px;
width: 90%;
margin: 0px auto 40px auto;
border: 1px solid #ccc;
}
.addtags_table_title{
width: 90%;
margin: auto;
}
.section_title {
font-weight: bold;
}
table#miczPrefs td {
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
vertical-align: top;
padding: 2px;
}
table#miczPrefs tr:first-child td {
border-top: none;
}
table#miczPrefs tr:last-child td {
border-bottom: none;
}
.autocomplete-container {
position: relative;
}
.autocomplete-list {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: white;
border: 1px solid #ccc;
z-index: 1000;
max-height: 200px;
overflow-y: auto;
padding: 0;
margin: 0;
list-style: none;
font-size: small;
}
.autocomplete-list li {
padding: 8px;
cursor: pointer;
}
.autocomplete-list li:hover {
background-color: #f0f0f0;
}
.autocomplete-list li.active {
background-color: #ddd;
}
.hidden {
display: none;
}
.unsaved {
color: red;
}
label:has(input[type="checkbox"]) {
cursor: pointer;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #1c1b22;
color: rgb(251, 251, 254);
}
a:link { color: #409eff; }
a:visited { color: #409eff; }
a:hover { color: #66b1ff; }
a:active { color: #66b1ff; }
a:active { color: #66b1ff; }
.autocomplete-list {
background-color: #2e2f36;
border: 1px solid #2e2f36;
}
.autocomplete-list li:hover {
background-color: #4c4e58;
}
.autocomplete-list li.active {
background-color: #4c4e58;
}
.summarize_auto{
background-color: #415c35;
}
.specific_integration{
background-color: #2E3A4F;
}
.specific_integration_sub td:first-child {
border-left: 10px solid #2E3A4F;
}
.specific_integration_sub td:last-child {
border-right: 10px solid #2E3A4F;
}
#connection_ui_end td{
background-color: #2E3A4F;
}
}

View file

@ -0,0 +1,123 @@
<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<title>ThunderAI - __MSG_Summarize_PageTitle__</title>
<link rel="stylesheet" href="mzta-summarize.css" />
<link rel="stylesheet" href="../_lib/connection-ui.css">
<link rel="icon" href="../../images/icon-16px.png" />
</head>
<body>
<div>
<h1 class="page_title">__MSG_Summarize_PageTitle__</h1>
<p>__MSG_Summarize_info_default__</p>
</div>
<div class="summarize_table_title section_title">__MSG_Summarize_prompt_prefs_title__</div>
<table id="miczPrefs">
<tr class="summarize_tr specific_integration">
<td><span class="opt_title">__MSG_prefs_OptionText_use_specific_integration__</span>
</td>
<td>
<label>
<input type="checkbox" id="summarize_use_specific_integration" name="summarize_use_specific_integration" class="option-input" />
__MSG_prefs_OptionText_summarize_use_specific_integration_Info__
</label>
</td>
</tr>
<tr id="connection_ui_anchor"></tr>
<tr id="connection_ui_end"><td colspan="2"></td></tr>
</table>
<!-- PROMPTS -->
<div id="summarize_prompt_container">
<!--title and description-->
<span class="section_title">
__MSG_Summarize_prompt_text_title__
</span>
<br>
<span class="infoline">
__MSG_prefs_OptionText_btnManagePrompts_infoline__
<a href="https://micz.it/thunderbird-addon-thunderai/data-placeholders/">
__MSG_more_info_string__
</a>
<br>
<b>__MSG_prefs_OptionText_Summarize_infoline2__</b>
</span>
<br>
<!--main prompt-->
<span class="infoline">
__MSG_prefs_OptionText_Summarize_main_prompt__
</span>
<br>
<div class="autocomplete-container">
<textarea id="summarize_prompt_text" rows="15"></textarea>
<ul class="autocomplete-list hidden"></ul>
</div>
<div id="summarize_info_additional_statements"></div>
<div class="btn_div">
<button id="btn_reset_prompt" disabled>
__MSG_reset_default__
</button>
<button id="btn_save_prompt" disabled>
__MSG_save__
</button>
</div>
<!--email template-->
<span class="infoline">
__MSG_prefs_OptionText_Summarize_email_template__
</span>
<br>
<div class="autocomplete-container">
<textarea id="summarize_email_template_text" rows="15"></textarea>
<ul class="autocomplete-list hidden"></ul>
</div>
<div id="summarize_info_additional_statements"></div>
<div class="btn_div">
<button id="btn_reset_email_template" disabled>
__MSG_reset_default__
</button>
<button id="btn_save_email_template" disabled>
__MSG_save__
</button>
</div>
<!-- separator -->
<span class="infoline">
__MSG_prefs_OptionText_Summarize_email_separator__
</span>
<br>
<div class="autocomplete-container">
<textarea id="summarize_email_separator_text" rows="7"></textarea>
<ul class="autocomplete-list hidden"></ul>
</div>
<div id="summarize_info_additional_statements"></div>
<div class="btn_div">
<button id="btn_reset_email_separator" disabled>
__MSG_reset_default__
</button>
<button id="btn_save_email_separator" disabled>
__MSG_save__
</button>
</div>
</div>
<script src="mzta-summarize.js" type="module"></script>
<script src="../../js/mzta-i18n.js"></script>
</body>
</html>

View file

@ -0,0 +1,278 @@
/*
* ThunderAI [https://micz.it/thunderbird-addon-thunderai/]
* Copyright (C) 2024 - 2025 Mic (m@micz.it)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { prefs_default, integration_options_config } from '../../options/mzta-options-default.js';
import { taLogger } from "../../js/mzta-logger.js";
import {
getSpecialPrompts,
setSpecialPrompts
} from "../../js/mzta-prompts.js";
import {
getPlaceholders,
mapPlaceholderToSuggestion
} from "../../js/mzta-placeholders.js";
import { textareaAutocomplete } from "../../js/mzta-placeholders-autocomplete.js";
import {
getAccountsList,
normalizeStringList,
isAPIKeyValue
} from "../../js/mzta-utils.js";
import {
initializeSpecificIntegrationUI
} from "../_lib/connection-ui.js";
let autocompleteSuggestions = [];
let taLog = new taLogger("mzta-summarize-page", true);
document.addEventListener("DOMContentLoaded", async () => {
let specialPrompts = await getSpecialPrompts();
let summarize_prompt = specialPrompts.find((prompt) => prompt.id === 'prompt_summarize');
let summarize_email_template = specialPrompts.find((prompt) => prompt.id === 'prompt_summarize_email_template');
let summarize_email_separator = specialPrompts.find((prompt) => prompt.id === 'prompt_summarize_email_separator');
if (summarize_prompt && summarize_prompt.api_type && summarize_prompt.api_type !== '') {
let update_prefs = {};
update_prefs['summarize_connection_type'] = summarize_prompt.api_type;
let integration = summarize_prompt.api_type.replace('_api', '');
if (integration_options_config && integration_options_config[integration]) {
for (const key of Object.keys(integration_options_config[integration])) {
if (summarize_prompt[key] !== undefined) {
update_prefs[`summarize_${integration}_${key}`] = summarize_prompt[key];
}
}
}
await browser.storage.sync.set(update_prefs);
}
await initializeSpecificIntegrationUI({
prefix: 'summarize',
promptId: 'prompt_summarize',
taLog: taLog,
restoreOptionsCallback: restoreOptions
});
i18n.updateDocument();
document.querySelectorAll(".option-input").forEach(element => {
element.addEventListener("change", saveOptions);
});
let prefs_summarize = await browser.storage.sync.get({ summarize_enabled_accounts: [], connection_type: 'chatgpt_web' });
let summarize_textarea = document.getElementById("summarize_prompt_text");
let summarize_save_btn = document.getElementById("btn_save_prompt");
let summarize_reset_btn = document.getElementById("btn_reset_prompt");
let summarize_textarea_email_template = document.getElementById("summarize_email_template_text");
let summarize_reset_email_template_btn = document.getElementById("btn_reset_email_template");
let summarize_save_email_template_btn = document.getElementById("btn_save_email_template");
let summarize_email_separator_textarea = document.getElementById("summarize_email_separator_text");
let summarize_email_separator_save_btn = document.getElementById("btn_save_email_separator");
let summarize_email_separator_reset_btn = document.getElementById("btn_reset_email_separator");
// on changing textareas
summarize_textarea.addEventListener("input", (event) => {
summarize_reset_btn.disabled = (event.target.value === browser.i18n.getMessage('prompt_summarize_full_text'));
summarize_save_btn.disabled = (event.target.value === summarize_prompt.text);
});
summarize_textarea_email_template.addEventListener("input", (event) => {
summarize_reset_email_template_btn.disabled = (event.target.value === browser.i18n.getMessage('prompt_summarize_email_template_full_text'));
summarize_save_email_template_btn.disabled = (event.target.value === summarize_email_template.text);
});
summarize_email_separator_textarea.addEventListener("input", (event) => {
summarize_email_separator_reset_btn.disabled = (event.target.value === browser.i18n.getMessage('prompt_summarize_email_separator_full_text'));
summarize_email_separator_save_btn.disabled = (event.target.value === summarize_email_separator.text);
});
// on clicking buttons, reset
summarize_reset_email_template_btn.addEventListener("click", () => {
summarize_textarea_email_template.value = browser.i18n.getMessage("prompt_summarize_email_template_full_text");
summarize_reset_email_template_btn.disabled = true;
let event = new Event("input", { bubbles: true, cancelable: true });
summarize_textarea_email_template.dispatchEvent(event);
});
summarize_reset_btn.addEventListener("click", () => {
summarize_textarea.value = browser.i18n.getMessage("prompt_summarize_full_text");
summarize_reset_btn.disabled = true;
let event = new Event("input", { bubbles: true, cancelable: true });
summarize_textarea.dispatchEvent(event);
});
summarize_email_separator_reset_btn.addEventListener("click", () => {
summarize_email_separator_textarea.value = browser.i18n.getMessage("prompt_summarize_email_separator_full_text");
summarize_email_separator_reset_btn.disabled = true;
let event = new Event("input", { bubbles: true, cancelable: true });
summarize_email_separator_textarea.dispatchEvent(event);
});
// on clicking buttons, save
summarize_save_email_template_btn.addEventListener("click", () => {
specialPrompts.find(prompt => prompt.id === 'prompt_summarize_email_template').text = summarize_textarea_email_template.value;
setSpecialPrompts(specialPrompts);
summarize_save_email_template_btn.disabled = true;
browser.runtime.sendMessage({ command: "reload_menus" });
});
summarize_save_btn.addEventListener("click", () => {
specialPrompts.find(prompt => prompt.id === 'prompt_summarize').text = summarize_textarea.value;
setSpecialPrompts(specialPrompts);
summarize_save_btn.disabled = true;
browser.runtime.sendMessage({ command: "reload_menus" });
});
summarize_email_separator_save_btn.addEventListener("click", () => {
specialPrompts.find(prompt => prompt.id === 'prompt_summarize_email_separator').text = summarize_email_separator_textarea.value;
setSpecialPrompts(specialPrompts);
summarize_email_separator_save_btn.disabled = true;
browser.runtime.sendMessage({ command: "reload_menus" });
});
if(summarize_prompt.text === 'prompt_summarize_full_text'){
summarize_prompt.text = browser.i18n.getMessage(summarize_prompt.text);
}
if(summarize_email_template.text === 'prompt_summarize_email_template_full_text'){
summarize_email_template.text = browser.i18n.getMessage(summarize_email_template.text);
}
if(summarize_email_separator.text === 'prompt_summarize_email_separator_full_text'){
summarize_email_separator.text = browser.i18n.getMessage(summarize_email_separator.text);
}
summarize_textarea_email_template.value = summarize_email_template.text;
summarize_reset_email_template_btn.disabled = (summarize_email_template.value === browser.i18n.getMessage("prompt_summarize_email_template"));
summarize_textarea.value = summarize_prompt.text;
summarize_reset_btn.disabled = (summarize_textarea.value === browser.i18n.getMessage("prompt_summarize_full_text"));
summarize_email_separator_textarea.value = summarize_email_separator.text;
summarize_email_separator_reset_btn.disabled = (summarize_email_separator.value === browser.i18n.getMessage("prompt_summarize_email_separator"));
autocompleteSuggestions = (await getPlaceholders(true))
.filter((p) => p.id !== "additional_text")
.map(mapPlaceholderToSuggestion);
textareaAutocomplete(summarize_textarea, autocompleteSuggestions, 1);
textareaAutocomplete(summarize_textarea_email_template, autocompleteSuggestions, 1);
textareaAutocomplete(summarize_email_separator_textarea, autocompleteSuggestions, 1);
});
// Methods to manage options, derived from: /options/mzta-options.js
function saveOptions(e) {
e.preventDefault();
let options = {};
let element = e.target;
// console.log(">>>>>>>>>> Saving option: " + element.id + " = " + element.value);
switch (element.type) {
case 'checkbox':
options[element.id] = element.checked;
break;
case 'number':
options[element.id] = element.valueAsNumber;
break;
case 'text':
case 'password':
options[element.id] = element.value.trim();
break;
case 'select-one':
// console.log(">>>>>>>>>> Saving option [select-one]: " + element.id + " = " + element.value);
options[element.id] = element.value;
break;
case 'textarea':
options[element.id] = normalizeStringList(element.value);
break;
default:
console.error("[ThunderAI] Unhandled input type:", element.type);
}
browser.storage.sync.set(options);
}
async function restoreOptions() {
function setCurrentChoice(result) {
document.querySelectorAll(".option-input").forEach(element => {
taLog.log("Options restoring " + element.id + " = " + (isAPIKeyValue(element.id) ? "****************" : result[element.id]));
switch (element.type) {
case 'checkbox':
element.checked = result[element.id] || false;
break;
case 'number':
let default_number_value = 0;
if(element.id == 'chatgpt_win_height') default_number_value = prefs_default.chatgpt_win_height;
if(element.id == 'chatgpt_win_width') default_number_value = prefs_default.chatgpt_win_width;
element.value = result[element.id] ?? default_number_value;
break;
case 'text':
case 'textarea':
case 'password':
let default_text_value = '';
if(element.id == 'default_chatgpt_lang') default_text_value = prefs_default.default_chatgpt_lang;
element.value = result[element.id] || default_text_value;
break;
default:
if (element.tagName === 'SELECT') {
let default_select_value = '';
const restoreValue = result[element.id] || default_select_value;
// Check if option exists
let optionExists = Array.from(element.options).some(opt => opt.value === restoreValue);
// If it doesn't exist and restoreValue is not empty, create it
if (!optionExists && restoreValue !== '') {
let newOption = new Option(restoreValue, restoreValue);
element.add(newOption);
}
// Set value
element.value = restoreValue;
if (element.value === '') {
element.selectedIndex = -1;
}
}else{
console.error("[ThunderAI] Unhandled input type:", element.type);
}
}
});
}
let getting = await browser.storage.sync.get(prefs_default);
let specialPrompts = await getSpecialPrompts();
let addtags_prompt = specialPrompts.find(prompt => prompt.id === 'prompt_summarize');
if (addtags_prompt) {
if (addtags_prompt.api_type && addtags_prompt.api_type !== '') {
getting['summarize_connection_type'] = addtags_prompt.api_type;
} else {
getting['summarize_connection_type'] = getting['connection_type'];
}
for (const [integration, options] of Object.entries(integration_options_config)) {
for (const key of Object.keys(options)) {
const propName = `${integration}_${key}`;
if (addtags_prompt[propName] !== undefined && addtags_prompt[propName] !== '') {
getting[`summarize_${propName}`] = addtags_prompt[propName];
} else {
getting[`summarize_${propName}`] = getting[propName];
}
}
}
}
setCurrentChoice(getting);
}