Merge pull request #769 from micz/new_menu_issue680

New menu issue 680
This commit is contained in:
Mic 2026-04-19 22:22:37 +02:00 committed by GitHub
commit 7b99c1c5b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
68 changed files with 1639 additions and 368 deletions

View file

@ -18,6 +18,8 @@
<li>Added the <i>{%mail_full_headers%}</i> placeholder to retrieve all the email headers at once [<a href="https://github.com/micz/ThunderAI/issues/713">#713</a>].</li>
<li><i>[All APIs]</i> In the API webchat the status messages have different colors [<a href="https://github.com/micz/ThunderAI/issues/3">#3</a>].</li>
<li>Account exclusion lists for add tags and antispam are enforced only for automatic analysis of incoming emails and not for the context menu action that is always executed [<a href="https://github.com/micz/ThunderAI/issues/749">#749</a>].</li>
<li><i>[All APIs]</i> Inline auto translation for emails added [<a href="https://github.com/micz/ThunderAI/issues/247">#247</a>].</li>
<li>Custom menus configuration added. Now it's possibile to define which prompts show in the ThunderAI menu, which ones in the context menu and in which order [<a href="https://github.com/micz/ThunderAI/issues/49">#49</a>, <a href="https://github.com/micz/ThunderAI/issues/184">#184</a>, <a href="https://github.com/micz/ThunderAI/issues/680">#680</a>].</li>
<li>...</li>
</ul>
<h2>Version 4.0.3 - 20/03/2026</h2>

View file

@ -120,6 +120,8 @@ _The language status represents the percentage of translated strings in the late
- [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
- [Hilmy Abiyyu A.](https://www.flaticon.com/authors/hilmy-abiyyu-a) for the ai translate and context menu icons
- [bearicons](https://www.flaticon.com/authors/bearicons) for the empty context menu icon
- [meaicon](https://www.flaticon.com/authors/meaicon) for the add task context menu icon
<br>

View file

@ -35,12 +35,8 @@
"message": "Classify",
"description": ""
},
"prompt_summarize_this": {
"message": "Summarize this",
"description": ""
},
"prompt_translate_this": {
"message": "Translate this",
"message": "Translate",
"description": ""
},
"prompt_this": {
@ -203,6 +199,22 @@
"message": "Composing an email",
"description": ""
},
"show_in": {
"message": "Show in",
"description": ""
},
"show_in_popup": {
"message": "Popup only",
"description": ""
},
"show_in_context": {
"message": "Context menu only",
"description": ""
},
"show_in_both": {
"message": "Both",
"description": ""
},
"customPrompts_close_button": {
"message": "Close button",
"description": ""
@ -612,14 +624,6 @@
"message": "If checked, using the keyboard shortcut CTRL+ALT+A will automatically send the highlighted prompt from the menu. Otherwise, the prompt name will be displayed to the user, requiring another press of the Enter key to send it.",
"description": ""
},
"prefs_OptionText_dynamic_menu_order_alphabet": {
"message": "Menu: order alphabetically",
"description": ""
},
"prefs_OptionText_dynamic_menu_order_alphabet_info": {
"message": "If checked, the prompts in the menu will be ordered alphabetically.",
"description": ""
},
"prefs_OptionText_chatgpt_win_dims_info": {
"message": "Set to 0 if you don't want to specify the window size.",
"description": ""
@ -816,10 +820,6 @@
"message": "Classify the following text in terms of Politeness, Warmth, Formality, Assertiveness, Offensiveness giving a percentage for each category. Reply with only the category and score with no extra comments or other text.",
"description": ""
},
"prompt_summarize_this_full_text": {
"message": "Summarize the following email into a bullet point list.",
"description": ""
},
"prompt_translate_this_full_text": {
"message": "Translate the email below into {%thunderai_translate_lang%}.\n\nRules:\n- Translate both the subject and the body.\n- Return the result as a JSON object with three fields: \"subject\", \"body\" and \"status\".\n- If the translation has been done the status is equal to 1.\n- If the email is written in one of these languages \"{%thunderai_translate_exclude_lang%}\" or in the {%thunderai_translate_lang%} language, return an empty string for the body and the subject and set the status to -1.\n- Do not add explanations, notes, or any text outside the JSON.\n\nMail subject: {%mail_subject%}\n\nMail body: {%mail_html_body%}\n\nGenerate a response in JSON format only. The output should be only a JSON object. Here is an example of the JSON format to be used:\n{\n\"subject\": \"subject translation\",\n\"body\": \"body translation\",\n\"status\": \"status result\"\n}",
"description": ""
@ -837,7 +837,7 @@
"description": ""
},
"prompt_add_tags": {
"message": "Add tags to this email",
"message": "Add tags",
"description": ""
},
"prompt_add_tags_full_text": {
@ -1105,7 +1105,7 @@
"description": ""
},
"prompt_summarize": {
"message": "Summarize this email or these emails",
"message": "Summarize",
"description": ""
},
"prompt_summarize_full_text": {
@ -1377,7 +1377,7 @@
"description": ""
},
"prompt_spamfilter" : {
"message" : "Detect spam emails",
"message" : "Analyze for spam",
"description" : ""
},
"prompt_spamfilter_full_text" : {
@ -1520,22 +1520,6 @@
"message": "No",
"description": ""
},
"context_menu_mzta-add-tags": {
"message": "Add tags",
"description": ""
},
"context_menu_mzta-spamfilter": {
"message": "Analyze for spam",
"description": ""
},
"context_menu_mzta-summarize": {
"message": "Summarize",
"description": ""
},
"context_menu_mzta-translate": {
"message": "Translate",
"description": ""
},
"noActiveCalendar": {
"message": "No editable calendar found!",
"description": ""
@ -2306,5 +2290,77 @@
"placeholder_string": {
"message": "Placeholder",
"description": ""
},
"menu_order_title": {
"message": "Menu Order",
"description": "Title for the menu order page"
},
"menu_order_popup_list_title": {
"message": "Popup Menu",
"description": "Header for the popup menu list in the menu order page"
},
"menu_order_context_list_title": {
"message": "Context Menu",
"description": "Header for the context menu list in the menu order page"
},
"menu_order_saved": {
"message": "Menu order saved!",
"description": "Message shown after saving menu order"
},
"menu_order_tab_reading": {
"message": "Reading",
"description": "Sub-tab label for reading view ordering"
},
"menu_order_tab_composing": {
"message": "Composing",
"description": "Sub-tab label for composing view ordering"
},
"menu_order_badge_default": {
"message": "Default",
"description": "Badge label for default prompts"
},
"menu_order_badge_special": {
"message": "Special",
"description": "Badge label for special prompts"
},
"menu_order_badge_custom": {
"message": "Custom",
"description": "Badge label for custom prompts"
},
"menu_order_type_reading": {
"message": "Reading",
"description": "Badge label for reading-only prompts"
},
"menu_order_type_composing": {
"message": "Composing",
"description": "Badge label for composing-only prompts"
},
"menu_order_type_always": {
"message": "Always",
"description": "Badge label for prompts shown in all contexts"
},
"menu_order_btn_label": {
"message": "Manage menu order settings",
"description": "Button label to open the menu order page from options"
},
"menu_order_info": {
"message": "Drag and drop items to reorder them. Use the toggle to show or hide items in each menu.",
"description": "Info text for the menu order page"
},
"menu_order_active_items": {
"message": "Visible items",
"description": "Section header for active/visible menu items"
},
"menu_order_hidden_items": {
"message": "Hidden items",
"description": "Section header for hidden menu items"
},
"menu_order_icon_label": {
"message": "Choose an icon",
"description": "Label/tooltip for the context menu icon picker on the menu order page"
},
"menu_order_icon_none": {
"message": "(none)",
"description": "Dropdown option meaning no custom icon is selected for the context menu item"
}
}

View file

@ -210,6 +210,7 @@ Each subdirectory is a self-contained settings/UI page for a specific feature:
| `customdataplaceholders/` | Custom placeholder editor |
| `get-calendar-event/` | Calendar event extraction settings |
| `get-task/` | Task creation settings |
| `menu_order/` | Drag-and-drop reordering and visibility control for popup and context menus |
| `spamfilter/` | Spam filter settings |
| `summarize/` | Email summarization settings |
| `translate/` | Email translation settings |

View file

@ -28,9 +28,12 @@ Prompts are the core user-facing feature of ThunderAI. Each prompt defines an AI
| Property | Type | Description |
|----------|------|-------------|
| `enabled` | number | `0` = hidden, `1` = shown in popup |
| `position_display` | number | Sort order in reading view |
| `position_compose` | number | Sort order in compose view |
| `enabled` | number | `0` = hidden, `1` = shown in menus |
| `position_display` | number | Sort order for the popup menu in reading view |
| `position_compose` | number | Sort order for the popup menu in compose view |
| `position_context` | number | Sort order for the context menu |
| `show_in` | string | `"popup"` = popup only, `"context"` = context menu only, `"both"` = both, `"none"` = hidden from all menus. Default: `"popup"` for default/custom prompts, `"both"` for special prompts |
| `custom_icon` | string | Filename (with extension) of an icon in `images/context_menu/custom/` used as the context-menu icon. Empty string = no icon. Only used for non-special prompts (special prompts use their hard-coded icons in `specialPromptToContextMenuID`). Selectable from a dropdown on the Menu Order page, context-menu tab. |
### Per-Prompt API Override Properties
@ -59,6 +62,70 @@ 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`.
## Menu System
### Popup Menu
- Displays prompts filtered by `show_in` (`"popup"` or `"both"`) and by tab context (`type` property: reading view shows types `0`+`1`, compose view shows types `0`+`2`)
- Ordering: always position-based using `position_display` (reading view) or `position_compose` (compose view). Alphabetical ordering has been removed
- Special prompts retain their colored background (CSS class `special_prompt`) in the popup based on `is_special == "1"`
### Context Menu
- Dynamically built from all prompts with `show_in` set to `"context"` or `"both"`, filtered to reading types only (`type` 0 or 1)
- Appears as a "ThunderAI" submenu in the `message_list` context
- Ordering: position-based using `position_context` (fallback to alphabetical only when positions are equal)
- Special prompts (add_tags, spamfilter, summarize, translate) route through `processEmails()` for batch processing; regular prompts execute via `menus.executeMenuAction()`
- Icons: special prompts use dedicated icons (defined in `contextMenuIconsPath`); all other prompts use the addon icon (`images/icon-32.png`)
- Add Tags in context menu assigns tags automatically (`addTagsAuto: true`), while in the popup it shows the interactive tag selection form
### Menu Order Page (`pages/menu_order/`)
Dedicated page for reordering, enabling, and disabling menu items across both the popup and the context menu. Opened from the options page via the "Menu Order" button.
**UI layout** — two side-by-side panels:
- **Popup Menu panel**: sub-tabs for "Reading" / "Composing" switch the list between `position_display` / `position_compose` ordering and between the allowed types (`0`+`1` vs `0`+`2`)
- **Context Menu panel**: single list ordered by `position_context`. Items with `type: "2"` (composing-only) are never shown here
Each list has two sections:
- **Visible items**: active for the menu (`show_in` includes the menu), draggable to reorder
- **Hidden items**: inactive for the menu (`show_in` excludes the menu), sorted alphabetically, not draggable
**Toggle coordination** — flipping the checkbox updates the prompt's `show_in` with four-state logic:
- Popup ON: `"none"``"popup"`, `"context"``"both"`
- Popup OFF: `"popup"``"none"`, `"both"``"context"`
- Context ON: `"none"``"context"`, `"popup"``"both"`
- Context OFF: `"context"``"none"`, `"both"``"popup"`
**Drag and drop** — native HTML5 DnD assigns sequential position numbers (1, 2, 3, ...) to `position_display`, `position_compose`, or `position_context` depending on which list is being sorted.
**Exclusions from the UI** (preserved on save so data is not lost):
- Prompts with `enabled === 0` (disabled)
- Special prompts whose base definition has `show_in: "none"` (internal prompts like `prompt_summarize_email_template` and `prompt_summarize_email_separator`) — retrieved via `getHiddenSpecialPromptIds()`
- Special prompts whose feature is not active — retrieved from background via `get_active_special_ids` message, which calls `getActiveSpecialPromptsIDs()` with current prefs and `_sparks_presence`
**Cross-tab reload** — the page listens on `browser.storage.onChanged` for changes to `_default_prompts_properties`, `_custom_prompt`, or `_special_prompts`. When one of those keys changes (e.g. user saves from the Custom Prompts page in another tab), the page reloads its data with a 200ms debounce. Any unsaved local changes are discarded to avoid overwriting the other page's work.
**Save flow**:
1. Re-concat preserved prompts (disabled + hidden-specials + inactive-feature specials) with the UI-visible prompts
2. Split by `is_default` / `is_special` and call `setDefaultPromptsProperties()`, `setCustomPrompts()`, `setSpecialPrompts()`
3. Send `reload_menus` to the background to rebuild both menus
### Alphabetic-to-Position Migration
The `dynamic_menu_order_alphabet` preference (previously a user-facing option) has been retired and removed from the UI, but the key still exists in storage as a one-shot migration flag. At every background startup, `migrateMenuOrderAlphabetic()` in `js/mzta-prompts.js` runs:
1. Reads `dynamic_menu_order_alphabet` (defaults to `true` if unset)
2. If `true`: sorts all visible prompts with special prompts first (alphabetically), then the rest (alphabetically), and assigns sequential `position_display` = `position_compose` = `position_context` numbers. Hidden special prompts are preserved untouched.
3. Persists the new positions via `setDefaultPromptsProperties` / `setCustomPrompts` / `setSpecialPrompts`
4. Sets `dynamic_menu_order_alphabet = false` in sync storage so the migration does not run again
This ensures existing users upgrading from the previous alphabetical-default behaviour get the same visible ordering on first run, while subsequent launches keep whatever custom ordering the user has set.
### Special Prompt Visibility Dependencies
`getActiveSpecialPromptsIDs()` in `js/mzta-utils.js` maps feature prefs to active special prompt IDs. Notable dependency:
- `prompt_get_calendar_event_from_clipboard` is emitted only if **both** `get_calendar_event` and `get_calendar_event_from_clipboard` are active. If `get_calendar_event` is off, neither calendar prompt is shown regardless of the clipboard pref.
### Summarize: Dual-Mode Prompt System
The summarize feature uses two distinct prompt pathways:

View file

@ -64,7 +64,7 @@ These are generated programmatically at the bottom of `mzta-options-default.js`
| `chatgpt_web_custom_gpt` | `''` | Custom GPT URL |
| `chatgpt_web_load_wait_time` | `1000` | Wait time (ms) for ChatGPT page |
| `dynamic_menu_force_enter` | `false` | Force Enter to submit in popup |
| `dynamic_menu_order_alphabet` | `true` | Sort prompts alphabetically |
| `dynamic_menu_order_alphabet` | `true` | Internal migration flag only; no UI. Set to `false` by `migrateMenuOrderAlphabetic()` on first boot after upgrade to bootstrap position-based ordering. See `claude-spec/02-prompts.md` for details. |
| `placeholders_use_default_value` | `false` | Use placeholder defaults when empty |
| `max_prompt_length` | `30000` | Max prompt string length |
@ -126,6 +126,10 @@ The summarize settings page provides:
- Each has Save/Reset buttons and placeholder autocomplete
- Default text comes from i18n strings (`prompt_summarize_full_text`, etc.)
### Menu Order Page (`pages/menu_order/`)
Entry point from the options page via the "Menu Order" button (next to "Manage your prompts"). Provides drag-and-drop reordering and toggle-based visibility control for both the popup and the context menu. See `claude-spec/02-prompts.md` ("Menu Order Page") for the full behaviour, data flow, and exclusion rules.
### Translate Settings Page (`pages/translate/`)
The translate settings page provides:

View file

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 994 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 974 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 954 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 925 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 990 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -36,6 +36,7 @@ import {
cleanupNewlines,
checkIfTagLabelExists,
getConnectionType,
getContextMenuIcon,
} from './mzta-utils.js'
import { taPromptUtils } from './mzta-utils-prompt.js';
import { taLogger } from './mzta-logger.js';
@ -76,7 +77,6 @@ export class mzta_Menus {
this.shortcutMenu = [];
this.menu_listeners = {};
this.allPrompts = await getPrompts(true,also_special);
this.allPrompts.sort((a, b) => a.name.localeCompare(b.name));
this.allPrompts.forEach((prompt) => {
this.addAction(prompt)
});
@ -219,7 +219,7 @@ export class mzta_Menus {
// const tabs = await browser.tabs.query({ active: true, currentWindow: true });
// add custom text if needed
//browser.runtime.sendMessage({command: "chatgpt_open", prompt: fullPrompt, action: curr_prompt.action, tabId: tabs[0].id});
// browser.runtime.sendMessage({command: "chatgpt_open", prompt: fullPrompt, action: curr_prompt.action, tabId: tabs[0].id});
if(curr_prompt.is_special == '1'){ // Special prompts
switch(curr_prompt.id){
case 'prompt_add_tags': { // Add tags to the email
@ -495,15 +495,77 @@ export class mzta_Menus {
}
addShortcutMenu(prompt) {
let curr_menu_entry = {id: prompt.id, label: i18nConditionalGet(prompt.name), type: prompt.type};
let curr_menu_entry = {
id: prompt.id,
label: i18nConditionalGet(prompt.name),
type: prompt.type,
show_in: prompt.show_in || "popup",
is_special: prompt.is_special,
position_display: prompt.position_display,
position_compose: prompt.position_compose,
position_context: prompt.position_context,
};
this.shortcutMenu.push(curr_menu_entry);
}
async loadContextMenus() {
await browser.menus.removeAll();
const contextPrompts = this.allPrompts.filter(p => {
const showIn = p.show_in || "popup";
// Only show prompts that should appear in context menu and are for reading context (type 0 or 1)
return (showIn === "context" || showIn === "both") && (String(p.type) === "0" || String(p.type) === "1");
});
if (contextPrompts.length === 0) {
this.logger.log("No prompts for context menu");
return;
}
// Create parent menu
await new Promise(resolve =>
browser.menus.create({
id: 'mzta-context-parent',
title: 'ThunderAI',
contexts: ["message_list"],
}, resolve)
);
// Sort by position_context if available, otherwise alphabetically
contextPrompts.sort((a, b) => {
const posA = a.position_context || 9999;
const posB = b.position_context || 9999;
if (posA !== posB) return posA - posB;
const nameA = i18nConditionalGet(a.name);
const nameB = i18nConditionalGet(b.name);
return nameA.localeCompare(nameB);
});
// Create child menu items
for (const prompt of contextPrompts) {
const title = i18nConditionalGet(prompt.name);
const iconPath = getContextMenuIcon(prompt);
const menuOpts = {
id: 'mzta-ctx-' + prompt.id,
title: title,
contexts: ["message_list"],
parentId: 'mzta-context-parent',
};
if (iconPath) {
menuOpts.icons = iconPath;
}
await new Promise(resolve =>
browser.menus.create(menuOpts, resolve)
);
}
this.logger.log("Context menus loaded: " + contextPrompts.length + " items");
}
async loadMenus(also_special = []) {
await this.initialize(also_special);
await this.addMenu(this.rootMenu);
this.addClickListener();
this.loadShortcutMenu();
await this.loadContextMenus();
this.logger.log("Menus loaded");
}

View file

@ -62,6 +62,12 @@
summaryTabId (set by _openSummaryWebchat in mzta-background.js):
The tab ID of the message display tab to update with the saved summary.
Show in menu (show_in attribute):
"popup": Show only in the popup menu
"context": Show only in the context menu
"both": Show in both popup and context menus
"none": Do not show in any menu
================ USER PROPERTIES
Enabled (enabled attribute):
0: Disabled
@ -108,6 +114,7 @@ const defaultPrompts = [
api_type: '',
is_default: "1",
is_special: "0",
show_in: "popup",
},
{
id: 'prompt_reply_advanced',
@ -126,6 +133,7 @@ const defaultPrompts = [
api_type: '',
is_default: "1",
is_special: "0",
show_in: "popup",
},
{
id: 'prompt_reply_custom_command',
@ -144,6 +152,7 @@ const defaultPrompts = [
api_type: '',
is_default: "1",
is_special: "0",
show_in: "popup",
},
{
id: 'prompt_rewrite_polite',
@ -162,6 +171,7 @@ const defaultPrompts = [
api_type: '',
is_default: "1",
is_special: "0",
show_in: "popup",
},
{
id: 'prompt_rewrite_formal',
@ -180,6 +190,7 @@ const defaultPrompts = [
api_type: '',
is_default: "1",
is_special: "0",
show_in: "popup",
},
{
id: 'prompt_classify',
@ -198,24 +209,7 @@ const defaultPrompts = [
api_type: '',
is_default: "1",
is_special: "0",
},
{
id: 'prompt_summarize_this',
name: "__MSG_prompt_summarize_this__",
text: "prompt_summarize_this_full_text",
type: "0",
action: "0",
need_selected: "0",
need_signature: "0",
need_custom_text: "0",
define_response_lang: "1",
use_diff_viewer: "0",
chatgpt_web_model: '',
chatgpt_web_project: '',
chatgpt_web_custom_gpt: '',
api_type: '',
is_default: "1",
is_special: "0",
show_in: "popup",
},
{
id: 'prompt_proofread_this',
@ -234,6 +228,7 @@ const defaultPrompts = [
api_type: '',
is_default: "1",
is_special: "0",
show_in: "popup",
},
{
id: 'prompt_this',
@ -252,6 +247,7 @@ const defaultPrompts = [
api_type: '',
is_default: "1",
is_special: "0",
show_in: "popup",
},
];
@ -270,6 +266,7 @@ const specialPrompts = [
api_type: '',
is_default: "1",
is_special: "1",
show_in: "both",
},
{
id: 'prompt_get_calendar_event',
@ -285,6 +282,7 @@ const specialPrompts = [
api_type: '',
is_default: "1",
is_special: "1",
show_in: "popup",
},
{
id: 'prompt_get_calendar_event_from_clipboard',
@ -300,6 +298,7 @@ const specialPrompts = [
api_type: '',
is_default: "1",
is_special: "1",
show_in: "popup",
},
{
id: 'prompt_get_task',
@ -315,6 +314,7 @@ const specialPrompts = [
api_type: '',
is_default: "1",
is_special: "1",
show_in: "popup",
},
{
id: 'prompt_spamfilter',
@ -330,6 +330,7 @@ const specialPrompts = [
api_type: '',
is_default: "1",
is_special: "1",
show_in: "context",
},
{
id: 'prompt_summarize',
@ -346,6 +347,7 @@ const specialPrompts = [
api_model: '',
is_default: "1",
is_special: "1",
show_in: "context",
},
{
id: 'prompt_summarize_email_template',
@ -362,6 +364,7 @@ const specialPrompts = [
api_model: '',
is_default: "1",
is_special: "1",
show_in: "none",
},
{
id: 'prompt_summarize_email_separator',
@ -378,6 +381,7 @@ const specialPrompts = [
api_model: '',
is_default: "1",
is_special: "1",
show_in: "none",
},
{
id: 'prompt_translate_this',
@ -394,6 +398,7 @@ const specialPrompts = [
api_model: '',
is_default: "1",
is_special: "1",
show_in: "context",
}
];
@ -464,7 +469,7 @@ export function preparePromptsForExport(prompts, include_api_settings = false){
}
if(prompt.is_default == 1){
let allowedKeys = ['id', 'enabled', 'position_compose', 'position_display', 'need_custom_text'];
let allowedKeys = ['id', 'enabled', 'position_compose', 'position_display', 'position_context', 'need_custom_text', 'show_in', 'custom_icon'];
if(include_api_settings){
allowedKeys.push('api_type');
for (const [integration, options] of Object.entries(integration_options_config)) {
@ -517,6 +522,7 @@ async function getDefaultPrompts_withProps() {
prompt.text = browser.i18n.getMessage(prompt.text);
prompt.position_display = pos;
prompt.position_compose = pos;
prompt.position_context = pos;
prompt.enabled = 1;
pos++;
})
@ -528,15 +534,19 @@ async function getDefaultPrompts_withProps() {
if(prefs._default_prompts_properties?.[prompt.id]){
prompt.position_compose = prefs._default_prompts_properties[prompt.id].position_compose;
prompt.position_display = prefs._default_prompts_properties[prompt.id].position_display;
prompt.position_context = prefs._default_prompts_properties[prompt.id]?.position_context || prompt.position_display;
prompt.enabled = prefs._default_prompts_properties[prompt.id].enabled;
prompt.need_custom_text = prefs._default_prompts_properties[prompt.id].need_custom_text;
prompt.chatgpt_web_model = prefs._default_prompts_properties[prompt.id].chatgpt_web_model;
prompt.chatgpt_web_project = prefs._default_prompts_properties[prompt.id].chatgpt_web_project;
prompt.chatgpt_web_custom_gpt = (prefs._default_prompts_properties[prompt.id]?.chatgpt_web_custom_gpt || '').trim();
prompt.api_type = (prefs._default_prompts_properties[prompt.id]?.api_type || '').trim();
prompt.show_in = prefs._default_prompts_properties[prompt.id]?.show_in || prompt.show_in;
prompt.custom_icon = prefs._default_prompts_properties[prompt.id]?.custom_icon || "";
}else{
prompt.position_display = pos;
prompt.position_compose = pos;
prompt.position_context = pos;
prompt.enabled = 1;
pos++;
}
@ -569,6 +579,12 @@ async function getCustomPrompts() {
if(prompt.api_type === undefined){
prompt.api_type = "";
}
if(prompt.show_in === undefined){
prompt.show_in = "popup";
}
if(prompt.custom_icon === undefined){
prompt.custom_icon = "";
}
});
return prefs._custom_prompt;
}
@ -580,12 +596,15 @@ export async function setDefaultPromptsProperties(prompts) {
default_prompts_properties[prompt.id] = {
position_compose: (prompt.position_compose === undefined || prompt.position_compose === "undefined") ? "" : prompt.position_compose,
position_display: (prompt.position_display === undefined || prompt.position_display === "undefined") ? "" : prompt.position_display,
position_context: (prompt.position_context === undefined || prompt.position_context === "undefined") ? "" : prompt.position_context,
enabled: (prompt.enabled === undefined || prompt.enabled === "undefined") ? "" : prompt.enabled,
need_custom_text: (prompt.need_custom_text === undefined || prompt.need_custom_text === "undefined") ? "" : prompt.need_custom_text,
chatgpt_web_model: (prompt.chatgpt_web_model === undefined || prompt.chatgpt_web_model === "undefined") ? "" : prompt.chatgpt_web_model,
chatgpt_web_project: (prompt.chatgpt_web_project === undefined || prompt.chatgpt_web_project === "undefined") ? "" : prompt.chatgpt_web_project,
chatgpt_web_custom_gpt: (prompt.chatgpt_web_custom_gpt === undefined || prompt.chatgpt_web_custom_gpt === "undefined") ? "" : prompt.chatgpt_web_custom_gpt,
api_type: (prompt.api_type === undefined || prompt.api_type === "undefined") ? "" : prompt.api_type,
show_in: (prompt.show_in === undefined || prompt.show_in === "undefined") ? "popup" : prompt.show_in,
custom_icon: (prompt.custom_icon === undefined || prompt.custom_icon === "undefined") ? "" : prompt.custom_icon,
};
});
//console.log('>>>>>>>>>>>>>> default_prompts_properties: ' + JSON.stringify(default_prompts_properties));
@ -616,6 +635,13 @@ export async function getSpecialPrompts(){
updatedPrompts.push(newPrompt);
}
});
// Migrate: add show_in if missing from saved special prompts
updatedPrompts.forEach((prompt) => {
if (prompt.show_in === undefined) {
prompt.show_in = "both";
}
});
// console.log(">>>>>>>>>>>>> getSpecialPrompts updatedPrompts: " + JSON.stringify(updatedPrompts));
if (updatedPrompts.length !== prefs._special_prompts.length) {
await browser.storage.local.set({ _special_prompts: updatedPrompts });
@ -630,6 +656,56 @@ export async function setSpecialPrompts(prompts) {
await browser.storage.local.set({_special_prompts: prompts});
}
export function getHiddenSpecialPromptIds() {
return specialPrompts.filter(p => p.show_in === "none").map(p => p.id);
}
// Migration: if dynamic_menu_order_alphabet was true (or unset), assign initial positions
// so that prompts appear alphabetically with special prompts first, then disable the flag
// to switch to position-based ordering permanently.
export async function migrateMenuOrderAlphabetic() {
const prefs = await browser.storage.sync.get({ dynamic_menu_order_alphabet: true });
if (!prefs.dynamic_menu_order_alphabet) {
return;
}
const allPrompts = await getPrompts(false, [], true);
const hiddenSpecialIds = getHiddenSpecialPromptIds();
const visiblePrompts = allPrompts.filter(p => !hiddenSpecialIds.includes(p.id));
const resolveName = (p) => {
const n = p.name || '';
if (n.startsWith('__MSG_') && n.endsWith('__')) {
return browser.i18n.getMessage(n.substring(6, n.length - 2));
}
return n;
};
const specials = visiblePrompts.filter(p => String(p.is_special) === '1')
.sort((a, b) => resolveName(a).localeCompare(resolveName(b)));
const others = visiblePrompts.filter(p => String(p.is_special) !== '1')
.sort((a, b) => resolveName(a).localeCompare(resolveName(b)));
const ordered = specials.concat(others);
ordered.forEach((prompt, idx) => {
const pos = idx + 1;
prompt.position_display = pos;
prompt.position_compose = pos;
prompt.position_context = pos;
});
const defaultPromptsToSave = ordered.filter(p => String(p.is_default) === '1' && String(p.is_special) !== '1');
const customPromptsToSave = ordered.filter(p => String(p.is_default) === '0' && String(p.is_special) !== '1');
const visibleSpecialsToSave = ordered.filter(p => String(p.is_special) === '1');
const hiddenSpecialsToPreserve = allPrompts.filter(p => hiddenSpecialIds.includes(p.id));
await setDefaultPromptsProperties(defaultPromptsToSave);
await setCustomPrompts(customPromptsToSave);
await setSpecialPrompts(visibleSpecialsToSave.concat(hiddenSpecialsToPreserve));
await browser.storage.sync.set({ dynamic_menu_order_alphabet: false });
}
export async function getSpamFilterPrompt(){
return (await getSpecialPrompts()).find(prompt => prompt.id == 'prompt_spamfilter');
}

View file

@ -16,24 +16,69 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { prefs_default, getDynamicSettingValue } from '../options/mzta-options-default.js';
import {
prefs_default,
getDynamicSettingValue
} from '../options/mzta-options-default.js';
import { customMenuIconsPath } from '../pages/menu_order/mzta-custom-menu-icons.js'
const sparks_min = '1.2.0'; // Minimum version of ThunderAI-Sparks required for the add-on to work
const MICZ_IT_LOCALIZED_LANGS = ['es', 'de', 'fr', 'it'];
export const getMenuContextCompose = () => 'compose_action_menu';
export const getMenuContextDisplay = () => 'message_display_action_menu';
export const contextMenuID_GetCalendarEvent = 'mzta-get-calendar-event';
export const contextMenuID_GetCalendarEventFromClipboard = 'mzta-get-calendar-event-from-clipboard';
export const contextMenuID_GetTask = 'mzta-get-task';
export const contextMenuID_AddTags = 'mzta-add-tags';
export const contextMenuID_Spamfilter = 'mzta-spamfilter';
export const contextMenuID_Summarize = 'mzta-summarize';
export const contextMenuID_Translate = 'mzta-translate';
export const contextMenuIconsPath = {
[contextMenuID_AddTags]: 'moz-extension:images/menu_autotags.png',
[contextMenuID_Spamfilter]: 'moz-extension:images/menu_spamfilter.png',
[contextMenuID_Summarize]: 'moz-extension:images/menu_summarize.png',
[contextMenuID_Translate]: 'moz-extension:images/menu_translate.png',
[contextMenuID_GetCalendarEvent]: 'moz-extension:images/context_menu/getcalendarevent.png',
[contextMenuID_GetCalendarEventFromClipboard]: 'moz-extension:images/context_menu/getcalendareventfromclipboard.png',
[contextMenuID_GetTask]: 'moz-extension:images/context_menu/gettask.png',
[contextMenuID_AddTags]: 'moz-extension:images/context_menu/autotags.png',
[contextMenuID_Spamfilter]: 'moz-extension:images/context_menu/spamfilter.png',
[contextMenuID_Summarize]: 'moz-extension:images/context_menu/summarize.png',
[contextMenuID_Translate]: 'moz-extension:images/context_menu/translate.png',
};
// Map from special prompt IDs to context menu IDs
export const specialPromptToContextMenuID = {
'prompt_get_calendar_event': contextMenuID_GetCalendarEvent,
'prompt_get_calendar_event_from_clipboard': contextMenuID_GetCalendarEventFromClipboard,
'prompt_get_task': contextMenuID_GetTask,
'prompt_add_tags': contextMenuID_AddTags,
'prompt_spamfilter': contextMenuID_Spamfilter,
'prompt_summarize': contextMenuID_Summarize,
'prompt_translate_this': contextMenuID_Translate,
};
// const defaultContextMenuIcon = 'moz-extension:images/icon-32px.png';
const defaultContextMenuIcon = '';
export function getContextMenuIcon(prompt) {
// Back-compat: accept a plain id string too
const promptId = (typeof prompt === 'string') ? prompt : prompt?.id;
const isSpecial = (typeof prompt === 'object' && prompt !== null) ? String(prompt.is_special) === '1' : true;
if (isSpecial) {
const contextMenuId = specialPromptToContextMenuID[promptId];
if (contextMenuId && contextMenuIconsPath[contextMenuId]) {
return contextMenuIconsPath[contextMenuId];
}
}
if (typeof prompt === 'object' && prompt !== null && prompt.custom_icon) {
return 'moz-extension:' + customMenuIconsPath + prompt.custom_icon;
}
return defaultContextMenuIcon;
}
export function getLanguageDisplayName(languageCode) {
const languageDisplay = new Intl.DisplayNames([languageCode], {type: 'language'});
let lang_string = languageDisplay.of(languageCode);
@ -612,10 +657,11 @@ export function getActiveSpecialPromptsIDs(args = {}) {
get_calendar_event = false,
get_calendar_event_from_clipboard = false,
get_task = false,
spamfilter = false,
summarize = false,
translate = false,
is_chatgpt_web = false
} = args;
// The Antispam filter is not here, because this method is used only
// to reload the ThunderAI button menu, not the context menu
let output = [];
// console.log(">>>>>>>>>> getActiveSpecialPromptsIDs args: " + JSON.stringify(args));
if (is_chatgpt_web) {
@ -629,13 +675,22 @@ export function getActiveSpecialPromptsIDs(args = {}) {
}
if (get_calendar_event) {
output.push('prompt_get_calendar_event');
}
if (get_calendar_event_from_clipboard) {
output.push('prompt_get_calendar_event_from_clipboard');
}
}
if (get_task) {
output.push('prompt_get_task');
}
if (spamfilter) {
output.push('prompt_spamfilter');
}
if (summarize) {
output.push('prompt_summarize');
}
if (translate) {
output.push('prompt_translate_this');
}
// console.log(">>>>>>>>>> getActiveSpecialPromptsIDs output: " + JSON.stringify(output));
return output;
}

View file

@ -42,24 +42,19 @@ import {
getMessages,
getMailBody,
extractJsonObject,
contextMenuID_AddTags,
contextMenuID_Spamfilter,
contextMenuID_Summarize,
contextMenuID_Translate,
contextMenuIconsPath,
sanitizeChatGPTModelData,
sanitizeChatGPTWebCustomData,
stripHtmlKeepLines,
htmlBodyToPlainText,
convertNewlinesToParagraphs,
getConnectionType,
checkAPIIntegration,
hasSpecificIntegration,
} from './js/mzta-utils.js';
import { taPromptUtils } from './js/mzta-utils-prompt.js';
import { mzta_specialCommand } from './js/mzta-special-commands.js';
import {
getSpamFilterPrompt
getSpamFilterPrompt,
migrateMenuOrderAlphabetic
} from './js/mzta-prompts.js';
import { taSpamReport } from './js/mzta-spamreport.js';
import { taSummaryStore } from './js/mzta-summarystore.js';
@ -105,6 +100,9 @@ let special_prompts_ids = getActiveSpecialPromptsIDs({
get_calendar_event: doGetSparkFeature(prefs_init.get_calendar_event),
get_calendar_event_from_clipboard: doGetSparkFeature(prefs_init.get_calendar_event_from_clipboard),
get_task: doGetSparkFeature(prefs_init.get_task),
spamfilter: prefs_init.spamfilter,
summarize: prefs_init.summarize,
translate: prefs_init.translate,
is_chatgpt_web: (prefs_init.connection_type === "chatgpt_web")
});
@ -169,7 +167,7 @@ function preparePopupMenu(tab) {
}
async function _reload_menus() {
let prefs_reload = await browser.storage.sync.get({add_tags: prefs_default.add_tags, get_calendar_event: prefs_default.get_calendar_event, get_calendar_event_from_clipboard: prefs_default.get_calendar_event_from_clipboard, get_task: prefs_default.get_task, connection_type: prefs_default.connection_type});
let prefs_reload = await browser.storage.sync.get({add_tags: prefs_default.add_tags, get_calendar_event: prefs_default.get_calendar_event, get_calendar_event_from_clipboard: prefs_default.get_calendar_event_from_clipboard, get_task: prefs_default.get_task, connection_type: prefs_default.connection_type, spamfilter: prefs_default.spamfilter, summarize: prefs_default.summarize, translate: prefs_default.translate});
let getCalendarEvent = doGetSparkFeature(prefs_reload.get_calendar_event);
let getCalendarEventFromClipboard = doGetSparkFeature(prefs_reload.get_calendar_event_from_clipboard);
let getTask = doGetSparkFeature(prefs_reload.get_task);
@ -179,6 +177,9 @@ async function _reload_menus() {
get_calendar_event: getCalendarEvent,
get_calendar_event_from_clipboard: getCalendarEventFromClipboard,
get_task: getTask,
spamfilter: prefs_reload.spamfilter,
summarize: prefs_reload.summarize,
translate: prefs_reload.translate,
is_chatgpt_web: (prefs_reload.connection_type === "chatgpt_web")
});
menus.reload(special_prompts_ids);
@ -186,6 +187,24 @@ async function _reload_menus() {
return true;
}
async function _getActiveSpecialIds() {
let prefs_reload = await browser.storage.sync.get({add_tags: prefs_default.add_tags, get_calendar_event: prefs_default.get_calendar_event, get_calendar_event_from_clipboard: prefs_default.get_calendar_event_from_clipboard, get_task: prefs_default.get_task, connection_type: prefs_default.connection_type, spamfilter: prefs_default.spamfilter, summarize: prefs_default.summarize, translate: prefs_default.translate});
let getCalendarEvent = doGetSparkFeature(prefs_reload.get_calendar_event);
let getCalendarEventFromClipboard = doGetSparkFeature(prefs_reload.get_calendar_event_from_clipboard);
let getTask = doGetSparkFeature(prefs_reload.get_task);
return getActiveSpecialPromptsIDs({
addtags: prefs_reload.add_tags,
addtags_api: hasSpecificIntegration(prefs_init.add_tags_use_specific_integration, prefs_init.add_tags_connection_type),
get_calendar_event: getCalendarEvent,
get_calendar_event_from_clipboard: getCalendarEventFromClipboard,
get_task: getTask,
spamfilter: prefs_reload.spamfilter,
summarize: prefs_reload.summarize,
translate: prefs_reload.translate,
is_chatgpt_web: (prefs_reload.connection_type === "chatgpt_web")
});
}
async function _assign_tags(_data, create_new_tags = true, exclusions_exact_match = false) {
let all_tags_list = await getTagsList();
all_tags_list = all_tags_list[1];
@ -507,8 +526,26 @@ messenger.runtime.onMessage.addListener((message, sender, sendResponse) => {
case 'reload_menus':
return _reload_menus();
break;
case 'get_active_special_ids':
return _getActiveSpecialIds();
break;
case 'shortcut_do_prompt':
taLog.log("Executing shortcut, promptId: " + message.promptId);
if (message.promptId !== 'prompt_add_tags' && specialContextMenuActions[message.promptId]) { //TODO Add an option here if you want the user to decide to use the autotagging also in the popup menu
async function _shortcut_special() {
let tabId = message.tabId;
if (!tabId) {
let tabs = await browser.tabs.query({ active: true, currentWindow: true });
if (tabs.length === 0) return false;
tabId = tabs[0].id;
}
let displayedMessage = await browser.messageDisplay.getDisplayedMessage(tabId);
if (!displayedMessage) return false;
taLog.log("Displayed message found.");
return specialContextMenuActions[message.promptId]([displayedMessage]);
}
return _shortcut_special();
}
return menus.executeMenuAction(message.promptId);
break;
case 'popup_menu_ready':
@ -1436,6 +1473,9 @@ function setupStorageChangeListener() {
get_calendar_event: getCalendarEvent,
get_calendar_event_from_clipboard: getCalendarEventFromClipboard,
get_task: getTask,
spamfilter: prefs_init.spamfilter,
summarize: prefs_init.summarize,
translate: prefs_init.translate,
is_chatgpt_web: (prefs_init.connection_type === "chatgpt_web")
});
menus.reload(special_prompts_ids);
@ -1453,6 +1493,9 @@ function setupStorageChangeListener() {
get_calendar_event: getCalendarEvent,
get_calendar_event_from_clipboard: getCalendarEventFromClipboard,
get_task: getTask,
spamfilter: prefs_init.spamfilter,
summarize: prefs_init.summarize,
translate: prefs_init.translate,
is_chatgpt_web: (prefs_init.connection_type === "chatgpt_web")
});
menus.reload(special_prompts_ids);
@ -1470,6 +1513,9 @@ function setupStorageChangeListener() {
get_calendar_event: getCalendarEvent,
get_calendar_event_from_clipboard: getCalendarEventFromClipboard,
get_task: getTask,
spamfilter: prefs_init.spamfilter,
summarize: prefs_init.summarize,
translate: prefs_init.translate,
is_chatgpt_web: (prefs_init.connection_type === "chatgpt_web")
});
menus.reload(special_prompts_ids);
@ -1487,6 +1533,69 @@ function setupStorageChangeListener() {
get_calendar_event: getCalendarEvent,
get_calendar_event_from_clipboard: getCalendarEventFromClipboard,
get_task: getTask,
spamfilter: prefs_init.spamfilter,
summarize: prefs_init.summarize,
translate: prefs_init.translate,
is_chatgpt_web: (prefs_init.connection_type === "chatgpt_web")
});
menus.reload(special_prompts_ids);
}
// Process 'spamfilter' changes
if (changes.spamfilter) {
const newSpamfilter = changes.spamfilter.newValue;
let getCalendarEvent = doGetSparkFeature(prefs_init.get_calendar_event);
let getCalendarEventFromClipboard = doGetSparkFeature(prefs_init.get_calendar_event_from_clipboard);
let getTask = doGetSparkFeature(prefs_init.get_task);
const special_prompts_ids = getActiveSpecialPromptsIDs({
addtags: prefs_init.add_tags,
addtags_api: hasSpecificIntegration(prefs_init.add_tags_use_specific_integration, prefs_init.add_tags_connection_type),
get_calendar_event: getCalendarEvent,
get_calendar_event_from_clipboard: getCalendarEventFromClipboard,
get_task: getTask,
spamfilter: newSpamfilter,
summarize: prefs_init.summarize,
translate: prefs_init.translate,
is_chatgpt_web: (prefs_init.connection_type === "chatgpt_web")
});
menus.reload(special_prompts_ids);
}
// Process 'summarize' changes
if (changes.summarize) {
const newSummarize = changes.summarize.newValue;
let getCalendarEvent = doGetSparkFeature(prefs_init.get_calendar_event);
let getCalendarEventFromClipboard = doGetSparkFeature(prefs_init.get_calendar_event_from_clipboard);
let getTask = doGetSparkFeature(prefs_init.get_task);
const special_prompts_ids = getActiveSpecialPromptsIDs({
addtags: prefs_init.add_tags,
addtags_api: hasSpecificIntegration(prefs_init.add_tags_use_specific_integration, prefs_init.add_tags_connection_type),
get_calendar_event: getCalendarEvent,
get_calendar_event_from_clipboard: getCalendarEventFromClipboard,
get_task: getTask,
spamfilter: prefs_init.spamfilter,
summarize: newSummarize,
translate: prefs_init.translate,
is_chatgpt_web: (prefs_init.connection_type === "chatgpt_web")
});
menus.reload(special_prompts_ids);
}
// Process 'translate' changes
if (changes.translate) {
const newTranslate = changes.translate.newValue;
let getCalendarEvent = doGetSparkFeature(prefs_init.get_calendar_event);
let getCalendarEventFromClipboard = doGetSparkFeature(prefs_init.get_calendar_event_from_clipboard);
let getTask = doGetSparkFeature(prefs_init.get_task);
const special_prompts_ids = getActiveSpecialPromptsIDs({
addtags: prefs_init.add_tags,
addtags_api: hasSpecificIntegration(prefs_init.add_tags_use_specific_integration, prefs_init.add_tags_connection_type),
get_calendar_event: getCalendarEvent,
get_calendar_event_from_clipboard: getCalendarEventFromClipboard,
get_task: getTask,
spamfilter: prefs_init.spamfilter,
summarize: prefs_init.summarize,
translate: newTranslate,
is_chatgpt_web: (prefs_init.connection_type === "chatgpt_web")
});
menus.reload(special_prompts_ids);
@ -1504,14 +1613,15 @@ function setupStorageChangeListener() {
get_calendar_event: getCalendarEvent,
get_calendar_event_from_clipboard: getCalendarEventFromClipboard,
get_task: getTask,
spamfilter: prefs_init.spamfilter,
summarize: prefs_init.summarize,
translate: prefs_init.translate,
is_chatgpt_web: (newConnectionType === "chatgpt_web")
});
menus.reload(special_prompts_ids);
}
reload_pref_init().then(() => {
addContextMenuItems();
});
reload_pref_init();
}
});
}
@ -1540,97 +1650,34 @@ function setupPermissionsRemovedListener() {
setupPermissionsRemovedListener();
// Menus handling
await migrateMenuOrderAlphabetic();
const menus = new mzta_Menus(openChatGPT, prefs_init.do_debug);
menus.loadMenus(special_prompts_ids);
// Context Menus
function addContextMenu(menu_id) {
browser.menus.remove(menu_id);
browser.menus.create({
id: menu_id,
title: browser.i18n.getMessage("context_menu_" + menu_id),
contexts: ["message_list"],
icons: contextMenuIconsPath[menu_id],
});
taLog.log("Context menu added: " + menu_id);
// console.log(">>>>>>> contextMenuIconsPath[menu_id]: " + contextMenuIconsPath[menu_id]);
}
// Context menu click handling
// Context menus are now created dynamically by mzta_Menus.loadContextMenus()
// based on each prompt's show_in property. The menu item IDs use the format 'mzta-ctx-<prompt_id>'.
// Special prompts (add_tags, spamfilter, summarize, translate) are routed to processEmails()
// for batch processing. Regular prompts are executed via menus.executeMenuAction().
function removeContextMenu(menu_id) {
browser.menus.remove(menu_id);
taLog.log("Context menu removed: " + menu_id);
}
const specialContextMenuActions = {
'prompt_add_tags': (messages) => processEmails({ messages, addTagsAuto: true }),
'prompt_spamfilter': (messages) => processEmails({ messages, spamFilter: true }),
'prompt_summarize': (messages) => processEmails({ messages, summarize: true }),
'prompt_translate_this': (messages) => processEmails({ messages, translate: true }),
};
function addContextMenuItems() {
let itemsToAdd = [];
browser.menus.onClicked.addListener((info, tab) => {
const menuItemId = info.menuItemId;
if (typeof menuItemId !== 'string' || !menuItemId.startsWith('mzta-ctx-')) {
return;
}
const promptId = menuItemId.replace('mzta-ctx-', '');
// Add Context menu: Add tags
if(prefs_init.add_tags && checkAPIIntegration(prefs_init.connection_type, prefs_init.add_tags_use_specific_integration,prefs_init.add_tags_connection_type)){
itemsToAdd.push(contextMenuID_AddTags);
if (specialContextMenuActions[promptId]) {
specialContextMenuActions[promptId](getMessages(info.selectedMessages));
} else {
removeContextMenu(contextMenuID_AddTags);
}
// Add Context menu: Spamfilter
if(prefs_init.spamfilter && checkAPIIntegration(prefs_init.connection_type, prefs_init.spamfilter_use_specific_integration,prefs_init.spamfilter_connection_type)){
itemsToAdd.push(contextMenuID_Spamfilter);
} else {
removeContextMenu(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)) {
itemsToAdd.push(contextMenuID_Summarize);
} else {
removeContextMenu(contextMenuID_Summarize);
}
// Add Context menu: Translate
if(prefs_init.translate && checkAPIIntegration(prefs_init.connection_type, prefs_init.translate_use_specific_integration, prefs_init.translate_connection_type)){
itemsToAdd.push(contextMenuID_Translate);
} else {
removeContextMenu(contextMenuID_Translate);
}
itemsToAdd.sort((a, b) => {
let titleA = browser.i18n.getMessage("context_menu_" + a);
let titleB = browser.i18n.getMessage("context_menu_" + b);
return titleA.localeCompare(titleB);
});
itemsToAdd.forEach(menu_id => {
addContextMenu(menu_id);
});
}
addContextMenuItems();
// Listen for context menu item clicks
browser.menus.onClicked.addListener( (info, tab) => {
let _add_tags = false
let _spamfilter = false
let _summarize = false;
let _translate = false;
if(info.menuItemId === contextMenuID_AddTags){
_add_tags = true;
}
if(info.menuItemId === contextMenuID_Spamfilter){
_spamfilter = true;
}
if(info.menuItemId === contextMenuID_Summarize) {
_summarize = true;
}
if(info.menuItemId === contextMenuID_Translate) {
_translate = true;
}
if(_add_tags || _spamfilter || _summarize || _translate){
processEmails({
messages: getMessages(info.selectedMessages),
addTagsAuto: _add_tags,
spamFilter: _spamfilter,
summarize: _summarize,
translate: _translate
});
menus.executeMenuAction(promptId);
}
});

View file

@ -114,7 +114,6 @@ export const prefs_default = {
chatgpt_web_custom_gpt: '',
chatgpt_web_load_wait_time: 1000,
dynamic_menu_force_enter: false,
dynamic_menu_order_alphabet: true,
placeholders_use_default_value: false,
max_prompt_length: 30000, // max string length for prompt
add_tags: false,

View file

@ -117,12 +117,11 @@
</tr>
<tr>
<td><label>
<span class="opt_title">__MSG_prefs_OptionText_dynamic_menu_order_alphabet__</span>
<span class="opt_title">__MSG_menu_order_title__</span>
</label></td>
<td>
<label>
<input type="checkbox" id="dynamic_menu_order_alphabet" name="dynamic_menu_order_alphabet" class="option-input" />
&nbsp;<span>__MSG_prefs_OptionText_dynamic_menu_order_alphabet_info__</span>
<button id="btnMenuOrder">__MSG_menu_order_btn_label__</button>
</label>
</td>
</tr>

View file

@ -410,6 +410,10 @@ document.addEventListener('DOMContentLoaded', async () => {
openTab('/pages/customdataplaceholders/mzta-custom-dataplaceholders.html');
});
document.getElementById('btnMenuOrder').addEventListener('click', () => {
openTab('/pages/menu_order/mzta-menu-order.html');
});
document.getElementById('btnManageTagsInfo').addEventListener('click', () => {
openTab('/pages/addtags/mzta-add-tags.html');
});

View file

@ -22,6 +22,8 @@
<li>Added the <i>{%mail_full_headers%}</i> placeholder to retrieve all the email headers at once [<a href="https://github.com/micz/ThunderAI/issues/713">#713</a>].</li>
<li><i>[All APIs]</i> In the API webchat the status messages have different colors [<a href="https://github.com/micz/ThunderAI/issues/3">#3</a>].</li>
<li>Account exclusion lists for add tags and antispam are enforced only for automatic analysis of incoming emails and not for the context menu action that is always executed [<a href="https://github.com/micz/ThunderAI/issues/749">#749</a>].</li>
<li><i>[All APIs]</i> Inline auto translation for emails added [<a href="https://github.com/micz/ThunderAI/issues/247">#247</a>].</li>
<li>Custom menus configuration added. Now it's possibile to define which prompts show in the ThunderAI menu, which ones in the context menu and in which order [<a href="https://github.com/micz/ThunderAI/issues/49">#49</a>, <a href="https://github.com/micz/ThunderAI/issues/184">#184</a>, <a href="https://github.com/micz/ThunderAI/issues/680">#680</a>].</li>
<li>...</li>
</ul>
<h2>Version 4.0.3 - 20/03/2026</h2>

View file

@ -51,6 +51,14 @@
<option value="2">__MSG_customPrompts_add_to_menu_composing__</option>
</select>
<br><br>
<label for="selectShowInNew" class="field_title">__MSG_show_in__:</label>
<br>
<select id="selectShowInNew" name="show_in" tabindex="4">
<option value="popup">__MSG_show_in_popup__</option>
<option value="context">__MSG_show_in_context__</option>
<option value="both">__MSG_show_in_both__</option>
</select>
<br><br>
<label for="selectActionNew" class="field_title">__MSG_customPrompts_form_label_Action__:</label>
<br>
<select id="selectActionNew" name="action" tabindex="5">

View file

@ -288,7 +288,8 @@ document.addEventListener('DOMContentLoaded', async () => {
position_display: positionMax_display + 1,
is_default: 0,
idnum: idnumMax + 1,
api_type: document.getElementById('new_prompt_api_type').value
api_type: document.getElementById('new_prompt_api_type').value,
show_in: document.getElementById('selectShowInNew').value,
};
switch(prefs.connection_type) {
@ -663,6 +664,8 @@ function showItemRowEditor(tr) {
tr.querySelector('.api_additional_info_show').style.display = 'none';
tr.querySelector('.type_output').style.display = 'inline';
tr.querySelector('.type_show').style.display = 'none';
tr.querySelector('.show_in_output').style.display = 'inline';
tr.querySelector('.show_in_show').style.display = 'none';
const action_output = tr.querySelector('.action_output')
action_output.style.display = 'inline';
action_output.addEventListener('change', toggleDiffviewer);
@ -688,6 +691,8 @@ function hideItemRowEditor(tr) {
toggleAdditionalPropertiesShow(tr);
tr.querySelector('.type_output').style.display = 'none';
tr.querySelector('.type_show').style.display = 'inline';
tr.querySelector('.show_in_output').style.display = 'none';
tr.querySelector('.show_in_show').style.display = 'inline';
const action_output = tr.querySelector('.action_output')
action_output.style.display = 'none';
action_output.addEventListener('change', toggleDiffviewer);
@ -824,6 +829,7 @@ function handleCancelClick(e) {
tr.querySelector('.text_output').value = sanitizeHtml(tr.querySelector('.text_show').innerHTML).replace(/<br\s*\/?>/gi, "\n");
tr.querySelector('.type_output').value = tr.querySelector('.type').innerText;
// tr.querySelector('.type_output').selectedOptions[0].text = tr.querySelector('.type_show').innerText;
tr.querySelector('.show_in_output').value = tr.querySelector('.show_in').innerText || 'popup';
tr.querySelector('.action_output').value = tr.querySelector('.action').innerText;
// tr.querySelector('.action_output').selectedOptions[0].text = tr.querySelector('.action_show').innerText;
tr.querySelector('.chatgpt_web_model_output').value = tr.querySelector('.chatgpt_web_model_show').innerText;
@ -850,6 +856,7 @@ function handleConfirmClick(e) {
newValues.name = tr.querySelector('.name_output').value.trim();
newValues.text = tr.querySelector('.text_output').value;
newValues.type = tr.querySelector('.type_output').value;
newValues.show_in = tr.querySelector('.show_in_output').value;
newValues.action = tr.querySelector('.action_output').value;
newValues.need_selected = tr.querySelector('.need_selected').checked ? 1 : 0;
newValues.need_signature = tr.querySelector('.need_signature').checked ? 1 : 0;
@ -876,6 +883,8 @@ function handleConfirmClick(e) {
// Update item data
tr.querySelector('.type').innerText = tr.querySelector('.type_output').value;
tr.querySelector('.type_show').innerText = tr.querySelector('.type_output').selectedOptions[0].text;
tr.querySelector('.show_in').innerText = tr.querySelector('.show_in_output').value;
tr.querySelector('.show_in_show').innerText = tr.querySelector('.show_in_output').selectedOptions[0].text;
tr.querySelector('.action').innerText = tr.querySelector('.action_output').value;
tr.querySelector('.action_show').innerText = tr.querySelector('.action_output').selectedOptions[0].text;
if (newValues.api_type !== '') {
@ -951,6 +960,7 @@ function handleCopyClick(e) {
document.getElementById('txtNameNew').value = name + ' (' + browser.i18n.getMessage("copy_text") + ')';
document.getElementById('txtTextNew').value = text;
document.getElementById('selectTypeNew').value = type;
document.getElementById('selectShowInNew').value = tr.querySelector('.show_in_output').value || 'popup';
document.getElementById('selectActionNew').value = action;
document.getElementById('checkboxNeedSelectedNew').checked = need_selected;
@ -1019,7 +1029,7 @@ function loadPromptsList(values){
}
let options = {
valueNames: [ { data: ['idnum'] }, 'is_default', 'id', 'name', 'text', 'type', 'action', 'position_compose', 'position_display', { name: 'need_selected', attr: 'checked_val'}, { name: 'need_signature', attr: 'checked_val'}, { name: 'need_custom_text', attr: 'checked_val'}, { name: 'define_response_lang', attr: 'checked_val'}, { name: 'use_diff_viewer', attr: 'checked_val'}, { name: 'enabled', attr: 'checked_val'}, 'api_type', ...api_fields ],
valueNames: [ { data: ['idnum'] }, 'is_default', 'id', 'name', 'text', 'type', 'action', 'position_compose', 'position_display', 'show_in', { name: 'need_selected', attr: 'checked_val'}, { name: 'need_signature', attr: 'checked_val'}, { name: 'need_custom_text', attr: 'checked_val'}, { name: 'define_response_lang', attr: 'checked_val'}, { name: 'use_diff_viewer', attr: 'checked_val'}, { name: 'enabled', attr: 'checked_val'}, 'api_type', ...api_fields ],
item: function(values) {
let type_output = '';
switch(String(values.type)){
@ -1046,7 +1056,18 @@ function loadPromptsList(values){
action_output = `__MSG_customPrompts_substitute_text__`;
break;
}
//console.log('>>>>>>>>>>>>> action_output: ' + JSON.stringify(action_output));
let show_in_output = '';
switch(String(values.show_in || 'popup')){
case "popup":
show_in_output = `__MSG_show_in_popup__`;
break;
case "context":
show_in_output = `__MSG_show_in_context__`;
break;
case "both":
show_in_output = `__MSG_show_in_both__`;
break;
}
let output = `<tr ` + ((values.is_default == 1) ? 'class="is_default"':'') + `>
<td class="w08"><span class="id id_show"></span><input type="text" class="hiddendata id_output" value="` + values.id + `" /></td>
@ -1095,6 +1116,16 @@ function loadPromptsList(values){
</select>` +
`<span class="type hiddendata"></span>
<br><br>
<span class="field_title_s">__MSG_show_in__:</span>
<br>
<span class="show_in_show">` + show_in_output + `</span>
<select class="show_in_output hiddendata input_mod">
<option value="popup"` + ((values.show_in == "popup" || !values.show_in) ? ' selected':'') + `>__MSG_show_in_popup__</option>
<option value="context"` + ((values.show_in == "context") ? ' selected':'') + `>__MSG_show_in_context__</option>
<option value="both"` + ((values.show_in == "both") ? ' selected':'') + `>__MSG_show_in_both__</option>
</select>` +
`<span class="show_in hiddendata"></span>
<br><br>
<span class="field_title_s">__MSG_customPrompts_form_label_Action__:</span>
<br><span class="action_show">` + action_output + `</span>
<select class="action_output hiddendata">
@ -1251,6 +1282,7 @@ function clearFields() {
document.getElementById('chatGPTWebProjectNew').value = '';
document.getElementById('chatGPTWebCustomGPTNew').value = '';
document.getElementById('selectTypeNew').value = '0';
document.getElementById('selectShowInNew').value = 'popup';
document.getElementById('selectActionNew').value = '0';
document.getElementById('checkboxNeedSelectedNew').value = '0';
document.getElementById('checkboxNeedSignatureNew').value = '0';

View file

@ -0,0 +1,61 @@
/*
* 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/>.
*/
export const customMenuIconsPath = 'images/context_menu/custom/';
export const customMenuIcons = [
'adventure-game.png',
'airport.png',
'alert-sign.png',
'calendar.png',
'cash.png',
'clapboard.png',
'clock.png',
'coins.png',
'copywriting.png',
'customer-service.png',
'deadline.png',
'discount.png',
'earth.png',
'electric-car.png',
'experiment.png',
'express-delivery.png',
'flammable.png',
'home.png',
'idea.png',
'info.png',
'invoice.png',
'justice-scale.png',
'like.png',
'love-letter.png',
'policeman.png',
'printer.png',
'profit.png',
'puzzle-game.png',
'radiation-hazard.png',
'receipt.png',
'recycle-sign.png',
'science.png',
'scissors.png',
'star.png',
'start-up.png',
'sticky-notes.png',
'target.png',
'thermometer.png',
'trophy.png',
];

View file

@ -0,0 +1,402 @@
.page_title {
margin-bottom: 0px;
}
#command_palette {
position: sticky;
top: 0;
padding-top: 1px;
height: 26px;
width: 100%;
z-index: 90;
margin-bottom: 16px;
background-color: #ffffff;
}
#btnSaveAll {
float: left;
}
#msgDisplay {
padding-left: 40px;
}
#lists_container {
display: flex;
gap: 24px;
align-items: flex-start;
}
.menu_list_panel {
flex: 1;
min-width: 320px;
border: 1px solid #ccc;
border-radius: 6px;
padding: 12px;
}
.menu_list_panel h2 {
margin-top: 0;
margin-bottom: 8px;
font-size: 1.1em;
}
.sub_tabs {
display: flex;
gap: 4px;
margin-bottom: 12px;
border-bottom: 1px solid #ccc;
padding-bottom: 0;
padding-left: 12px;
}
.sub_tab {
padding: 4px 14px;
border: 1px solid #ccc;
background: #f5f5f5;
cursor: pointer;
border-radius: 4px 4px 0 0;
font-size: 0.9em;
margin-bottom: -1px;
}
.sub_tab.active {
background: #fff;
border-bottom-color: #fff;
font-weight: bold;
}
.section_label {
font-size: 0.85em;
color: #666;
margin: 8px 0 4px 0;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.hidden_section_label {
border-top: 1px dashed #ccc;
padding-top: 8px;
margin-top: 12px;
}
.sortable_list,
.hidden_list {
list-style: none;
padding: 0;
margin: 0;
min-height: 40px;
}
.sortable_item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 8px;
margin: 3px 0;
border: 1px solid #ddd;
border-radius: 4px;
background: #fff;
user-select: none;
transition: background-color 0.15s;
}
.sortable_item:hover {
background: #f0f4ff;
}
.sortable_item.dragging {
opacity: 0.4;
border-style: dashed;
}
.sortable_item.drag-over {
border-top: 2px solid #409df3;
}
.drag_handle {
cursor: grab;
color: #999;
font-size: 1.1em;
padding: 0 4px;
flex-shrink: 0;
}
.drag_handle:active {
cursor: grabbing;
}
.item_toggle {
flex-shrink: 0;
width: 16px;
height: 16px;
cursor: pointer;
}
.item_name {
flex: 1;
font-size: 0.92em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.badge {
display: inline-block;
padding: 1px 6px;
border-radius: 3px;
font-size: 0.72em;
font-weight: 600;
text-transform: uppercase;
flex-shrink: 0;
}
.badge_default {
background: #e0e0e0;
color: #555;
}
.badge_special {
background: #d4eaff;
color: #1a5fa0;
}
.badge_custom {
background: #e8f5e9;
color: #2e7d32;
}
.badge_type {
background: #fff3e0;
color: #e65100;
}
.item_icon_preview {
width: 18px;
height: 18px;
object-fit: contain;
flex-shrink: 0;
cursor: pointer;
border: 1px solid transparent;
border-radius: 3px;
padding: 1px;
box-sizing: border-box;
}
.item_icon_preview:hover {
border-color: #409df3;
background: #eef5ff;
}
.item_icon_preview_empty {
display: inline-block;
background: transparent;
}
.item_icon_preview_editable {
border-color: #b0c4de;
}
.item_icon_preview_special {
cursor: default;
}
.item_icon_preview_special:hover {
border-color: transparent;
background: transparent;
}
.icon_picker_popover {
position: absolute;
z-index: 1000;
background: #fff;
border: 1px solid #bbb;
border-radius: 6px;
padding: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.18);
display: grid;
grid-template-columns: repeat(5, 28px);
gap: 4px;
max-width: 180px;
}
.icon_picker_cell {
width: 28px;
height: 28px;
padding: 2px;
border: 1px solid transparent;
background: transparent;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.icon_picker_cell img {
width: 20px;
height: 20px;
object-fit: contain;
}
.icon_picker_cell:hover {
background: #eef5ff;
border-color: #409df3;
}
.icon_picker_cell.selected {
background: #d4eaff;
border-color: #409df3;
}
.icon_picker_cell_none {
font-size: 1.2em;
color: #888;
}
/* Hidden list items */
.hidden_list .sortable_item {
opacity: 0.6;
background: #fafafa;
}
.hidden_list .sortable_item .drag_handle {
visibility: hidden;
}
.hidden_list .sortable_item:hover {
opacity: 0.8;
background: #f5f5f5;
}
/* Dark mode */
@media (prefers-color-scheme: dark) {
body {
background-color: #1c1b22;
color: rgb(251, 251, 254);
}
#command_palette {
background-color: inherit;
}
#msgDisplay {
color: #5cb85c;
}
.menu_list_panel {
border-color: #444;
background: #2a2a30;
}
.sub_tabs {
border-bottom-color: #555;
}
.sub_tab {
border-color: #555;
background: #333;
color: #ccc;
}
.sub_tab.active {
background: #2a2a30;
border-bottom-color: #2a2a30;
color: #fff;
}
.section_label {
color: #999;
}
.hidden_section_label {
border-top-color: #555;
}
.sortable_item {
border-color: #444;
background: #333;
color: #eee;
}
.sortable_item:hover {
background: #3a3a44;
}
.sortable_item.drag-over {
border-top-color: #409df3;
}
.drag_handle {
color: #777;
}
.badge_default {
background: #444;
color: #bbb;
}
.badge_special {
background: #1a3a5c;
color: #7ab8f5;
}
.badge_custom {
background: #1b3a1e;
color: #81c784;
}
.badge_type {
background: #3d2800;
color: #ffb74d;
}
.item_icon_preview_editable {
border-color: #556;
}
.item_icon_preview:hover {
background: #3a3a44;
border-color: #409df3;
}
.icon_picker_popover {
background: #2a2a30;
border-color: #555;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.6);
}
.icon_picker_cell:hover {
background: #3a3a44;
}
.icon_picker_cell.selected {
background: #1a3a5c;
}
.icon_picker_cell_none {
color: #aaa;
}
.hidden_list .sortable_item {
background: #2a2a2e;
}
.hidden_list .sortable_item:hover {
background: #333;
}
a:link { color: #409eff; }
a:visited { color: #409eff; }
a:hover { color: #66b1ff; }
a:active { color: #66b1ff; }
.item_icon_preview_empty {
filter: invert(1);
}
.icon_picker_cell_none img {
filter: invert(1);
}
}

View file

@ -0,0 +1,39 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>ThunderAI - __MSG_menu_order_title__</title>
<link rel="stylesheet" href="mzta-menu-order.css" />
<link rel="icon" href="../../images/icon-16px.png" />
</head>
<body>
<h1 class="page_title">__MSG_menu_order_title__</h1>
<p>__MSG_menu_order_info__</p>
<div id="command_palette">
<button id="btnSaveAll" disabled>__MSG_btnSaveAll_string__</button>
<span id="msgDisplay"></span>
</div>
<div id="lists_container">
<div class="menu_list_panel" id="popup_panel">
<h2>__MSG_menu_order_popup_list_title__</h2>
<div class="sub_tabs">
<button class="sub_tab active" data-view="display">__MSG_menu_order_tab_reading__</button>
<button class="sub_tab" data-view="compose">__MSG_menu_order_tab_composing__</button>
</div>
<h3 class="section_label">__MSG_menu_order_active_items__</h3>
<ul id="popup_list" class="sortable_list"></ul>
<h3 class="section_label hidden_section_label">__MSG_menu_order_hidden_items__</h3>
<ul id="popup_list_hidden" class="hidden_list"></ul>
</div>
<div class="menu_list_panel" id="context_panel">
<h2>__MSG_menu_order_context_list_title__</h2>
<h3 class="section_label">__MSG_menu_order_active_items__</h3>
<ul id="context_list" class="sortable_list"></ul>
<h3 class="section_label hidden_section_label">__MSG_menu_order_hidden_items__</h3>
<ul id="context_list_hidden" class="hidden_list"></ul>
</div>
</div>
<script src="mzta-menu-order.js" type="module"></script>
<script src="../../js/mzta-i18n.js"></script>
</body>
</html>

View file

@ -0,0 +1,523 @@
/*
* 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 {
getPrompts,
setDefaultPromptsProperties,
setCustomPrompts,
setSpecialPrompts,
getHiddenSpecialPromptIds
} from '../../js/mzta-prompts.js';
import {
i18nConditionalGet,
specialPromptToContextMenuID,
contextMenuIconsPath
} from '../../js/mzta-utils.js';
import {
customMenuIcons,
customMenuIconsPath
} from './mzta-custom-menu-icons.js';
// Convert "moz-extension:images/foo.png" to a relative path usable from this page
function resolveSpecialIconPath(promptId) {
const ctxId = specialPromptToContextMenuID[promptId];
if (!ctxId) return '';
const raw = contextMenuIconsPath[ctxId];
if (!raw) return '';
return '../../' + raw.replace(/^moz-extension:/, '');
}
let allPrompts = [];
let allExcludedSpecialPrompts = []; // special prompts excluded from UI (hidden + inactive features), preserved on save
let allDisabledPrompts = []; // default/custom prompts disabled (enabled=0), excluded from UI, preserved on save
let currentPopupView = 'display'; // 'display' or 'compose'
document.addEventListener('DOMContentLoaded', async () => {
await loadAndRender();
initSubTabs();
document.getElementById('btnSaveAll').addEventListener('click', saveAll);
// If prompts are modified elsewhere (e.g. custom prompts page saving), reload this page's data.
// Any unsaved changes on this page are discarded to avoid overwriting the other page's changes.
let reloadDebounce = null;
browser.storage.onChanged.addListener((changes, areaName) => {
if (areaName !== 'local') return;
if (!(changes._default_prompts_properties || changes._custom_prompt || changes._special_prompts)) return;
clearTimeout(reloadDebounce);
reloadDebounce = setTimeout(() => {
document.getElementById('btnSaveAll').disabled = true;
const msgDisplay = document.getElementById('msgDisplay');
msgDisplay.textContent = '';
msgDisplay.style.display = 'none';
loadAndRender();
}, 200);
});
i18n.updateDocument();
});
async function loadAndRender() {
allPrompts = await getPrompts(false, [], true);
// Exclude special prompts that are defined with show_in: "none" (internal prompts, not user-toggleable)
// and special prompts whose feature is not active (e.g. add_tags disabled, sparks not present)
const hiddenSpecialIds = getHiddenSpecialPromptIds();
const activeSpecialIds = await browser.runtime.sendMessage({ command: "get_active_special_ids" });
allExcludedSpecialPrompts = allPrompts.filter(p =>
hiddenSpecialIds.includes(p.id) ||
(String(p.is_special) === '1' && !activeSpecialIds.includes(p.id))
);
allPrompts = allPrompts.filter(p => !allExcludedSpecialPrompts.some(e => e.id === p.id));
// Exclude disabled prompts (enabled=0) from the UI, preserve them for save
allDisabledPrompts = allPrompts.filter(p => String(p.enabled) === '0');
allPrompts = allPrompts.filter(p => String(p.enabled) !== '0');
// Resolve i18n names and assign initial position_context if missing
let contextPos = 1;
const sortedForContext = [...allPrompts].sort((a, b) => {
const nameA = i18nConditionalGet(a.name);
const nameB = i18nConditionalGet(b.name);
return nameA.localeCompare(nameB);
});
sortedForContext.forEach(p => {
if (p.position_context === undefined || p.position_context === '' || p.position_context === 'undefined') {
p.position_context = contextPos;
}
contextPos++;
});
// Resolve display names
allPrompts.forEach(p => {
p._displayName = i18nConditionalGet(p.name);
});
renderPopupList();
renderContextList();
}
// ==================== Sub-tabs ====================
function initSubTabs() {
document.querySelectorAll('.sub_tab').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.sub_tab').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentPopupView = btn.dataset.view;
renderPopupList();
});
});
}
// ==================== Render Popup List ====================
function renderPopupList() {
const posKey = currentPopupView === 'display' ? 'position_display' : 'position_compose';
// Filter by type: reading view shows type 0+1, composing view shows type 0+2
const allowedTypes = currentPopupView === 'display' ? ['0', '1'] : ['0', '2'];
const typeFiltered = allPrompts.filter(p => allowedTypes.includes(String(p.type)));
const activeItems = typeFiltered.filter(p => {
const showIn = p.show_in || 'popup';
return showIn === 'popup' || showIn === 'both';
});
const hiddenItems = typeFiltered.filter(p => {
const showIn = p.show_in || 'popup';
return showIn !== 'popup' && showIn !== 'both';
});
activeItems.sort((a, b) => (a[posKey] || 9999) - (b[posKey] || 9999));
hiddenItems.sort((a, b) => a._displayName.localeCompare(b._displayName));
const activeList = document.getElementById('popup_list');
const hiddenList = document.getElementById('popup_list_hidden');
renderListItems(activeList, activeItems, 'popup', true);
renderListItems(hiddenList, hiddenItems, 'popup', false);
initDragAndDrop(activeList, posKey);
}
// ==================== Render Context List ====================
function renderContextList() {
// Only show items with type 0 or 1 (not composing-only)
const contextEligible = allPrompts.filter(p => String(p.type) === '0' || String(p.type) === '1');
const activeItems = contextEligible.filter(p => {
const showIn = p.show_in || 'popup';
return showIn === 'context' || showIn === 'both';
});
const hiddenItems = contextEligible.filter(p => {
const showIn = p.show_in || 'popup';
return showIn !== 'context' && showIn !== 'both';
});
activeItems.sort((a, b) => (a.position_context || 9999) - (b.position_context || 9999));
hiddenItems.sort((a, b) => a._displayName.localeCompare(b._displayName));
const activeList = document.getElementById('context_list');
const hiddenList = document.getElementById('context_list_hidden');
renderListItems(activeList, activeItems, 'context', true);
renderListItems(hiddenList, hiddenItems, 'context', false);
initDragAndDrop(activeList, 'position_context');
}
// ==================== Render List Items ====================
function renderListItems(listEl, items, menuType, isActive) {
listEl.innerHTML = '';
items.forEach(prompt => {
const li = document.createElement('li');
li.classList.add('sortable_item');
li.dataset.id = prompt.id;
if (isActive) {
li.draggable = true;
}
// Drag handle
const handle = document.createElement('span');
handle.classList.add('drag_handle');
handle.textContent = '\u2630';
li.appendChild(handle);
// Icon slot (context menu only) - between handle and toggle, to keep rows aligned
if (menuType === 'context') {
if (String(prompt.is_special) === '1') {
li.appendChild(buildSpecialIconDisplay(prompt));
} else {
li.appendChild(buildIconPicker(prompt));
}
}
// Toggle checkbox
const toggle = document.createElement('input');
toggle.type = 'checkbox';
toggle.classList.add('item_toggle');
toggle.checked = isActive;
toggle.addEventListener('change', () => {
toggleShowIn(prompt, menuType, toggle.checked);
});
li.appendChild(toggle);
// Name
const nameSpan = document.createElement('span');
nameSpan.classList.add('item_name');
nameSpan.textContent = prompt._displayName;
li.appendChild(nameSpan);
// Type badge
const typeBadge = document.createElement('span');
typeBadge.classList.add('badge', 'badge_type');
if (String(prompt.type) === '1') {
typeBadge.textContent = browser.i18n.getMessage('menu_order_type_reading');
} else if (String(prompt.type) === '2') {
typeBadge.textContent = browser.i18n.getMessage('menu_order_type_composing');
} else {
typeBadge.textContent = browser.i18n.getMessage('menu_order_type_always');
}
li.appendChild(typeBadge);
// Source badge
const sourceBadge = document.createElement('span');
sourceBadge.classList.add('badge');
if (String(prompt.is_special) === '1') {
sourceBadge.classList.add('badge_special');
sourceBadge.textContent = browser.i18n.getMessage('menu_order_badge_special');
} else if (String(prompt.is_default) === '1') {
sourceBadge.classList.add('badge_default');
sourceBadge.textContent = browser.i18n.getMessage('menu_order_badge_default');
} else {
sourceBadge.classList.add('badge_custom');
sourceBadge.textContent = browser.i18n.getMessage('menu_order_badge_custom');
}
li.appendChild(sourceBadge);
listEl.appendChild(li);
});
}
// ==================== Custom icon picker ====================
let activeIconPopover = null;
function closeIconPopover() {
if (activeIconPopover) {
activeIconPopover.remove();
activeIconPopover = null;
document.removeEventListener('mousedown', onDocMouseDownForPopover, true);
document.removeEventListener('keydown', onDocKeyDownForPopover, true);
}
}
function onDocMouseDownForPopover(e) {
if (activeIconPopover && !activeIconPopover.contains(e.target) && !e.target.classList.contains('item_icon_preview')) {
closeIconPopover();
}
}
function onDocKeyDownForPopover(e) {
if (e.key === 'Escape') closeIconPopover();
}
function applyIconToPreview(preview, filename) {
if (filename) {
preview.src = '../../' + customMenuIconsPath + filename;
preview.classList.remove('item_icon_preview_empty');
} else {
preview.src = '../../' + customMenuIconsPath + 'empty_icon.png';
preview.classList.add('item_icon_preview_empty');
}
}
function buildSpecialIconDisplay(prompt) {
const img = document.createElement('img');
img.classList.add('item_icon_preview', 'item_icon_preview_special');
img.alt = '';
const path = resolveSpecialIconPath(prompt.id);
if (path) {
img.src = path;
} else {
img.src = '../../' + customMenuIconsPath + 'empty_icon.png';
img.classList.add('item_icon_preview_empty');
}
return img;
}
function buildIconPicker(prompt) {
const preview = document.createElement('img');
preview.classList.add('item_icon_preview', 'item_icon_preview_editable');
preview.alt = '';
preview.title = browser.i18n.getMessage('menu_order_icon_label');
applyIconToPreview(preview, prompt.custom_icon || '');
preview.addEventListener('click', (e) => {
e.stopPropagation();
if (activeIconPopover && activeIconPopover.dataset.forId === prompt.id) {
closeIconPopover();
return;
}
closeIconPopover();
openIconPopover(preview, prompt);
});
return preview;
}
function openIconPopover(anchorEl, prompt) {
const popover = document.createElement('div');
popover.classList.add('icon_picker_popover');
popover.dataset.forId = prompt.id;
// "None" option
const noneBtn = document.createElement('button');
noneBtn.type = 'button';
noneBtn.classList.add('icon_picker_cell', 'icon_picker_cell_none');
noneBtn.title = browser.i18n.getMessage('menu_order_icon_none');
const noneImg = document.createElement('img');
noneImg.src = '../../' + customMenuIconsPath + 'empty_icon.png';
noneImg.alt = '';
noneBtn.appendChild(noneImg);
if (!prompt.custom_icon) noneBtn.classList.add('selected');
noneBtn.addEventListener('click', () => {
prompt.custom_icon = '';
applyIconToPreview(anchorEl, '');
markUnsaved();
closeIconPopover();
});
popover.appendChild(noneBtn);
customMenuIcons.forEach(filename => {
const btn = document.createElement('button');
btn.type = 'button';
btn.classList.add('icon_picker_cell');
btn.title = filename.replace(/\.[^.]+$/, '');
if (filename === prompt.custom_icon) btn.classList.add('selected');
const img = document.createElement('img');
img.src = '../../' + customMenuIconsPath + filename;
img.alt = '';
btn.appendChild(img);
btn.addEventListener('click', () => {
prompt.custom_icon = filename;
applyIconToPreview(anchorEl, filename);
markUnsaved();
closeIconPopover();
});
popover.appendChild(btn);
});
document.body.appendChild(popover);
activeIconPopover = popover;
// Position popover below the anchor
const rect = anchorEl.getBoundingClientRect();
const popRect = popover.getBoundingClientRect();
let left = rect.left + window.scrollX;
let top = rect.bottom + window.scrollY + 4;
if (left + popRect.width > window.scrollX + document.documentElement.clientWidth - 8) {
left = window.scrollX + document.documentElement.clientWidth - popRect.width - 8;
}
if (left < window.scrollX + 4) left = window.scrollX + 4;
popover.style.left = left + 'px';
popover.style.top = top + 'px';
document.addEventListener('mousedown', onDocMouseDownForPopover, true);
document.addEventListener('keydown', onDocKeyDownForPopover, true);
}
// ==================== Toggle show_in ====================
function toggleShowIn(prompt, menuType, isOn) {
const current = prompt.show_in || 'popup';
if (menuType === 'popup') {
if (isOn) {
prompt.show_in = (current === 'none') ? 'popup' : (current === 'context') ? 'both' : current;
} else {
prompt.show_in = (current === 'popup') ? 'none' : (current === 'both') ? 'context' : current;
}
} else { // context
if (isOn) {
prompt.show_in = (current === 'none') ? 'context' : (current === 'popup') ? 'both' : current;
} else {
prompt.show_in = (current === 'context') ? 'none' : (current === 'both') ? 'popup' : current;
}
}
markUnsaved();
renderPopupList();
renderContextList();
}
// ==================== Drag and Drop ====================
function initDragAndDrop(listEl, positionKey) {
let draggedItem = null;
listEl.addEventListener('dragstart', (e) => {
const li = e.target.closest('.sortable_item');
if (!li) return;
draggedItem = li;
draggedItem.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', li.dataset.id);
});
listEl.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (!draggedItem) return;
// Remove previous drag-over indicators
listEl.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
const afterElement = getDragAfterElement(listEl, e.clientY);
if (afterElement) {
afterElement.classList.add('drag-over');
listEl.insertBefore(draggedItem, afterElement);
} else {
listEl.appendChild(draggedItem);
}
});
listEl.addEventListener('dragleave', (e) => {
if (e.target.classList) {
e.target.classList.remove('drag-over');
}
});
listEl.addEventListener('drop', (e) => {
e.preventDefault();
listEl.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
});
listEl.addEventListener('dragend', () => {
if (draggedItem) {
draggedItem.classList.remove('dragging');
updatePositionsFromDOM(listEl, positionKey);
draggedItem = null;
markUnsaved();
}
});
}
function getDragAfterElement(container, y) {
const draggableElements = [...container.querySelectorAll('.sortable_item:not(.dragging)')];
return draggableElements.reduce((closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
}
return closest;
}, { offset: Number.NEGATIVE_INFINITY }).element;
}
function updatePositionsFromDOM(listEl, positionKey) {
const items = listEl.querySelectorAll('.sortable_item');
items.forEach((li, index) => {
const promptId = li.dataset.id;
const prompt = allPrompts.find(p => p.id === promptId);
if (prompt) {
prompt[positionKey] = index + 1;
}
});
}
// ==================== Save ====================
async function saveAll() {
const btnSaveAll = document.getElementById('btnSaveAll');
const msgDisplay = document.getElementById('msgDisplay');
btnSaveAll.disabled = true;
const disabledDefaults = allDisabledPrompts.filter(p => String(p.is_default) === '1' && String(p.is_special) !== '1');
const disabledCustoms = allDisabledPrompts.filter(p => String(p.is_default) === '0' && String(p.is_special) !== '1');
const disabledSpecials = allDisabledPrompts.filter(p => String(p.is_special) === '1');
const defaultPromptsToSave = allPrompts.filter(p => String(p.is_default) === '1' && String(p.is_special) !== '1').concat(disabledDefaults);
const customPromptsToSave = allPrompts.filter(p => String(p.is_default) === '0' && String(p.is_special) !== '1').concat(disabledCustoms);
const specialPromptsToSave = allPrompts.filter(p => String(p.is_special) === '1').concat(disabledSpecials).concat(allExcludedSpecialPrompts);
await setDefaultPromptsProperties(defaultPromptsToSave);
await setCustomPrompts(customPromptsToSave);
await setSpecialPrompts(specialPromptsToSave);
await browser.runtime.sendMessage({ command: "reload_menus" });
msgDisplay.textContent = browser.i18n.getMessage('menu_order_saved');
msgDisplay.style.display = 'inline';
msgDisplay.style.color = 'green';
setTimeout(() => {
msgDisplay.textContent = '';
msgDisplay.style.display = 'none';
}, 3000);
}
function markUnsaved() {
document.getElementById('btnSaveAll').disabled = false;
const msgDisplay = document.getElementById('msgDisplay');
msgDisplay.textContent = browser.i18n.getMessage('customPrompts_unsaved_changes');
msgDisplay.style.display = 'inline';
msgDisplay.style.color = 'red';
}

View file

@ -18,34 +18,15 @@
import { prefs_default } from "../options/mzta-options-default.js";
import { taLogger } from "../js/mzta-logger.js";
import {
checkSparksPresence,
checkAPIIntegration,
} from "../js/mzta-utils.js";
let menuSendImmediately = false;
let taLog = console;
let connection_type = 'chatgpt_web';
let add_tags = false;
let add_tags_use_specific_integration = false;
let add_tags_connection_type = '';
let get_calendar_event = false;
let get_calendar_event_from_clipboard = false;
let get_task = false;
let _ok_sparks = false;
let tabType;
let num_special_menu_items = 0;
document.addEventListener('DOMContentLoaded', async () => {
let prefs = await browser.storage.sync.get({
do_debug: prefs_default.do_debug,
dynamic_menu_force_enter: prefs_default.dynamic_menu_force_enter,
add_tags: prefs_default.add_tags,
add_tags_use_specific_integration: prefs_default.add_tags_use_specific_integration,
add_tags_connection_type: prefs_default.add_tags_connection_type,
get_calendar_event: prefs_default.get_calendar_event,
get_calendar_event_from_clipboard: prefs_default.get_calendar_event_from_clipboard,
get_task: prefs_default.get_task,
connection_type: prefs_default.connection_type
});
taLog = new taLogger("mzta-popup",prefs.do_debug);
@ -63,19 +44,7 @@ document.addEventListener('DOMContentLoaded', async () => {
});
taLog.log("active_prompts: " + JSON.stringify(active_prompts));
menuSendImmediately = prefs.dynamic_menu_force_enter;
connection_type = prefs.connection_type;
add_tags = prefs.add_tags;
add_tags_use_specific_integration = prefs.add_tags_use_specific_integration;
add_tags_connection_type = prefs.add_tags_connection_type;
get_calendar_event = prefs.get_calendar_event;
get_calendar_event_from_clipboard = prefs.get_calendar_event_from_clipboard;
get_task = prefs.get_task;
_ok_sparks = await checkSparksPresence() == 1;
// console.log(">>>>>>>>>>>>>>>>> add_tags: " + add_tags);
// console.log(">>>>>>>>>>>>>>>>> get_calendar_event: " + get_calendar_event);
// console.log(">>>>>>>>>>>>>>>>> get_task: " + get_task);
// console.log(">>>>>>>>>>>>>>>>> _ok_sparks: " + _ok_sparks);
searchPrompt(active_prompts, tabId, tabType);
searchPrompt(active_prompts, tabId, tabType, filtering);
i18n.updateDocument();
if(prefs.connection_type === 'chatgpt_web'){
@ -110,14 +79,12 @@ document.addEventListener('DOMContentLoaded', async () => {
}
}, { once: true });
async function searchPrompt(allPrompts, tabId, tabType){
async function searchPrompt(allPrompts, tabId, tabType, filtering){
taLog.log("tabType: " + tabType);
let prefs_order = await browser.storage.sync.get({dynamic_menu_order_alphabet: true});
if(prefs_order.dynamic_menu_order_alphabet){
allPrompts.sort((a, b) => a.label.localeCompare(b.label));
}
// Sort by position: use position_display for reading (filtering=1), position_compose for composing (filtering=2)
const posKey = filtering === 2 ? 'position_compose' : 'position_display';
allPrompts.sort((a, b) => (a[posKey] || 9999) - (b[posKey] || 9999));
// console.log(">>>>>>>>> allPrompts: " + JSON.stringify(allPrompts));
@ -161,68 +128,10 @@ async function searchPrompt(allPrompts, tabId, tabType){
// Prepend numbers to the first 10 items
// If add_tags is true and connection_type is not 'chatgpt_web' reserve 0 position for prompt_add_tags and 1 for prompt_get_calendar_event (0, if no prompt_add_tags is disabled)
let max_num_el = 10
let first_num_el = 0;
let do_add_tags = checkDoAddTags();
let do_get_calendar_event = checkDoCalendarEvent();
let do_get_calendar_event_from_clipboard = checkDoCalendarEventFromClipboard();
let do_get_task = checkDoTask();
// console.log(">>>>>>>>>>> do_add_tags: " + do_add_tags);
// console.log(">>>>>>>>>>> do_get_calendar_event: " + do_get_calendar_event);
// console.log(">>>>>>>>>>> do_get_task: " + do_get_task);
// console.log(">>>>>>>>>>> filteredData: " + JSON.stringify(filteredData));
num_special_menu_items = (do_add_tags ? 1 : 0) + (do_get_calendar_event ? 1 : 0) + (do_get_calendar_event_from_clipboard ? 1 : 0) + (do_get_task ? 1 : 0);
// console.log(">>>>>>>>>>>> num_special_menu_items: " + num_special_menu_items);
if(num_special_menu_items > 0){
max_num_el -= num_special_menu_items;
first_num_el = num_special_menu_items;
// console.log(">>>>>>>>>>>>> max_num_el: " + max_num_el);
// console.log(">>>>>>>>>>>>> first_num_el: " + first_num_el);
if(do_add_tags){
filteredData = ensurePromptAddTagsFirst(filteredData);
if (!filteredData[0].numberPrepended) {
filteredData[0].numberPrepended = 'true';
filteredData[0].label = '0. ' + filteredData[0].label;
}
}
if(do_get_calendar_event){
filteredData = ensurePromptGetCalendarEventFirst(filteredData, do_add_tags);
let gce_curr_pos = do_add_tags ? 1 : 0;
if (!filteredData[gce_curr_pos].numberPrepended) {
filteredData[gce_curr_pos].numberPrepended = 'true';
filteredData[gce_curr_pos].label = gce_curr_pos + '. ' + filteredData[gce_curr_pos].label;
}
}
if(do_get_calendar_event_from_clipboard){
filteredData = ensurePromptGetCalendarEventFromClipboardFirst(filteredData, do_add_tags, do_get_calendar_event);
let gcefc_curr_pos = (do_add_tags ? 1 : 0) + (do_get_calendar_event ? 1 : 0);
if (!filteredData[gcefc_curr_pos].numberPrepended) {
filteredData[gcefc_curr_pos].numberPrepended = 'true';
filteredData[gcefc_curr_pos].label = gcefc_curr_pos + '. ' + filteredData[gcefc_curr_pos].label;
}
}
if(do_get_task){
filteredData = ensurePromptGetTaskFirst(filteredData, do_add_tags, do_get_calendar_event, do_get_calendar_event_from_clipboard);
let gtask_curr_pos = (do_add_tags ? 1 : 0) + (do_get_calendar_event ? 1 : 0) + (do_get_calendar_event_from_clipboard ? 1 : 0);
if (!filteredData[gtask_curr_pos].numberPrepended) {
filteredData[gtask_curr_pos].numberPrepended = 'true';
filteredData[gtask_curr_pos].label = gtask_curr_pos + '. ' + filteredData[gtask_curr_pos].label;
}
}
}
// console.log(">>>>>>>>>>> filteredData after special items check: " + JSON.stringify(filteredData));
Array.from(filteredData).slice(first_num_el, max_num_el).forEach((item, index) => {
let number = (index + first_num_el).toString();
// Check if the number is already prepended to avoid duplication
Array.from(filteredData).slice(0, 10).forEach((item, index) => {
if (!item.numberPrepended) {
item.label = `${number}. ${item.label}`;
item.numberPrepended = 'true'; // Mark as prepended
item.label = `${index}. ${item.label}`;
item.numberPrepended = 'true';
}
});
@ -234,7 +143,7 @@ async function searchPrompt(allPrompts, tabId, tabType){
itemDiv.classList.add('mzta_autocomplete-item');
itemDiv.textContent = item.label;
itemDiv.setAttribute('data-id', item.id);
if((item.id === 'prompt_add_tags')||(item.id === 'prompt_get_calendar_event')||(item.id === 'prompt_get_calendar_event_from_clipboard')||(item.id === 'prompt_get_task')){
if(item.is_special == "1"){
itemDiv.className += ' special_prompt';
}
@ -367,9 +276,15 @@ async function sendPrompt(prompt_id, tabId){
}
function filterPromptsForTab(prompts_data, filtering){
// If filtering is 0, return the original array without any filters (btw it should not happen)
// Filter by show_in: only show prompts visible in the popup
let filtered = prompts_data.filter(prompt => {
const showIn = prompt.show_in || "popup";
return showIn === "popup" || showIn === "both";
});
// If filtering is 0, return without type filter (should not happen)
if (filtering === 0) {
return prompts_data;
return filtered;
}
// Define the types to include based on the value of filtering
@ -379,95 +294,10 @@ function filterPromptsForTab(prompts_data, filtering){
} else if (filtering === 2) {
allowedTypes = ["0", "2"];
} else {
// If filtering has an unexpected value, return the original data
return prompts_data;
return filtered;
}
// Filter the array based on the allowed types
return prompts_data.filter(prompt => allowedTypes.includes(prompt.type));
return filtered.filter(prompt => allowedTypes.includes(prompt.type));
}
function checkDoAddTags(){
return add_tags && checkAPIIntegration(connection_type, add_tags_use_specific_integration,add_tags_connection_type) && (tabType !== 'messageCompose');
}
function checkDoCalendarEvent(){
return get_calendar_event && (connection_type !== "chatgpt_web" && tabType !== 'messageCompose') && _ok_sparks;
}
function checkDoCalendarEventFromClipboard(){
return get_calendar_event_from_clipboard && (connection_type !== "chatgpt_web" && tabType !== 'messageCompose') && _ok_sparks;
}
function checkDoTask(){
return get_task && (connection_type !== "chatgpt_web" && tabType !== 'messageCompose') && _ok_sparks;
}
function ensurePromptAddTagsFirst(arr) {
// Find the index of the object with id "prompt_add_tags"
const index = arr.findIndex(item => item.id === "prompt_add_tags");
// If found and not already the first element
if (index !== -1 && index !== 0) {
// Remove it from its current position
const [promptAddTags] = arr.splice(index, 1);
// Add it to the beginning of the array
arr.unshift(promptAddTags);
}
return arr;
}
function ensurePromptGetCalendarEventFirst(arr, do_add_tags) {
// Find the index of the object with id "prompt_get_calendar_event"
const index = arr.findIndex(item => item.id === "prompt_get_calendar_event");
// If found and needs repositioning
if (index !== -1 && (do_add_tags ? index !== 1 : index !== 0)) {
// Remove it from its current position
const [promptAddTags] = arr.splice(index, 1);
// Add it to the specified position
const targetPosition = do_add_tags ? 1 : 0;
arr.splice(targetPosition, 0, promptAddTags);
}
return arr;
}
function ensurePromptGetCalendarEventFromClipboardFirst(arr, do_add_tags, do_get_calendar_event) {
// Find the index of the object with id "prompt_get_calendar_event_from_clipboard"
const index = arr.findIndex(item => item.id === "prompt_get_calendar_event_from_clipboard");
const targetPosition = (do_add_tags ? 1 : 0) + (do_get_calendar_event ? 1 : 0);
// If found and needs repositioning
if (index !== -1 && index !== targetPosition) {
// Remove it from its current position
const [promptAddTags] = arr.splice(index, 1);
// Add it to the specified position
arr.splice(targetPosition, 0, promptAddTags);
}
return arr;
}
function ensurePromptGetTaskFirst(arr, do_add_tags, do_get_calendar_event, do_get_calendar_event_from_clipboard) {
// Find the index of the object with id "prompt_get_task"
const index = arr.findIndex(item => item.id === "prompt_get_task");
// Determine the target position to insert "prompt_get_task" after calendar
const targetPosition = (do_add_tags ? 1 : 0) + (do_get_calendar_event ? 1 : 0) + (do_get_calendar_event_from_clipboard ? 1 : 0);
// If found and needs repositioning
if (index !== -1 && index !== targetPosition) {
// Remove it from its current position
const [promptGetTask] = arr.splice(index, 1);
// Add it to the specified position
arr.splice(targetPosition, 0, promptGetTask);
}
return arr;
}