diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2401da7a..bff4fe01 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,8 +7,11 @@
__MSG_Debug__
diff --git a/options/mzta-options.js b/options/mzta-options.js
index 21f6a50c..81844d01 100644
--- a/options/mzta-options.js
+++ b/options/mzta-options.js
@@ -23,6 +23,7 @@ import { OpenAI } from '../js/api/openai.js';
import { Ollama } from '../js/api/ollama.js';
import { OpenAIComp } from '../js/api/openai_comp.js'
import { GoogleGemini } from '../js/api/google_gemini.js';
+import { checkSparksPresence } from '../js/mzta-utils.js';
let taLog = new taLogger("mzta-options",true);
@@ -100,6 +101,7 @@ async function restoreOptions() {
function showConnectionOptions() {
disable_MaxPromptLength();
disable_AddTags();
+ disable_GetCalendarEvent();
let chatgpt_web_display = 'table-row';
let chatgpt_api_display = 'none';
let ollama_api_display = 'none';
@@ -252,12 +254,27 @@ function disable_MaxPromptLength(){
function disable_AddTags(){
let add_tags = document.getElementById('add_tags');
+ let conntype_select = document.getElementById("connection_type");
+ add_tags.disabled = (conntype_select.value === "chatgpt_web");
let add_tags_tr_elements = document.querySelectorAll('.add_tags_tr');
add_tags_tr_elements.forEach(add_tags_tr => {
add_tags_tr.style.display = (add_tags.disabled) ? 'none' : 'table-row';
});
}
+async function disable_GetCalendarEvent(){
+ let get_calendar_event = document.getElementById('get_calendar_event');
+ let no_sparks_tr = document.getElementById('no_sparks');
+ let is_spark_present = await checkSparksPresence()
+ let conntype_select = document.getElementById("connection_type");
+ get_calendar_event.disabled = (conntype_select.value === "chatgpt_web") || !is_spark_present;
+ let get_calendar_event_tr_elements = document.querySelectorAll('.get_calendar_event_tr');
+ get_calendar_event_tr_elements.forEach(get_calendar_event_tr => {
+ get_calendar_event_tr.style.display = get_calendar_event.disabled ? 'none' : 'table-row';
+ });
+ no_sparks_tr.style.display = is_spark_present ? 'none' : 'table-row';
+}
+
document.addEventListener('DOMContentLoaded', async () => {
await restoreOptions();
@@ -286,6 +303,13 @@ document.addEventListener('DOMContentLoaded', async () => {
addtags_info_btn.disabled = event.target.checked ? '' : 'disabled';
});
addtags_info_btn.disabled = addtags_el.checked ? '' : 'disabled';
+
+ let get_calendar_event_el = document.getElementById('get_calendar_event');
+ let get_calendar_event_info_btn = document.getElementById('btnManageCalendarEventInfo');
+ get_calendar_event_el.addEventListener('click', (event) => {
+ get_calendar_event_info_btn.disabled = event.target.checked ? '' : 'disabled';
+ });
+ get_calendar_event_info_btn.disabled = get_calendar_event_el.checked ? '' : 'disabled';
document.getElementById('btnManagePrompts').addEventListener('click', () => {
// check if the tab is already there
@@ -313,6 +337,19 @@ document.addEventListener('DOMContentLoaded', async () => {
})
});
+ document.getElementById('btnManageCalendarEventInfo').addEventListener('click', () => {
+ // check if the tab is already there
+ browser.tabs.query({url: browser.runtime.getURL('../pages/get-calendar-event/mzta-get-calendar-event.html')}).then((tabs) => {
+ if (tabs.length > 0) {
+ // if the tab is already there, focus it
+ browser.tabs.update(tabs[0].id, {active: true});
+ } else {
+ // if the tab is not there, create it
+ browser.tabs.create({url: browser.runtime.getURL('../pages/get-calendar-event/mzta-get-calendar-event.html')});
+ }
+ })
+ });
+
document.getElementById('btnOpenAICompForceModel').addEventListener('click', () => {
let modelName = prompt(browser.i18n.getMessage('OpenAIComp_force_model_ask')).trim();
if ((modelName !== null) && (modelName !== undefined) && (modelName !== '')) {
@@ -332,6 +369,8 @@ document.addEventListener('DOMContentLoaded', async () => {
conntype_select.addEventListener("change", warn_Ollama_HostEmpty);
conntype_select.addEventListener("change", warn_OpenAIComp_HostEmpty);
conntype_select.addEventListener("change", warn_GoogleGemini_APIKeyEmpty);
+ conntype_select.addEventListener("change", disable_AddTags);
+ conntype_select.addEventListener("change", disable_GetCalendarEvent);
document.getElementById("chatgpt_api_key").addEventListener("change", warn_ChatGPT_APIKeyEmpty);
document.getElementById("ollama_host").addEventListener("change", warn_Ollama_HostEmpty);
document.getElementById("openai_comp_host").addEventListener("change", warn_OpenAIComp_HostEmpty);
@@ -524,6 +563,7 @@ select_openai_comp_model.addEventListener("change", warn_OpenAIComp_HostEmpty);
warn_GoogleGemini_APIKeyEmpty();
disable_MaxPromptLength();
disable_AddTags();
+ disable_GetCalendarEvent();
const passwordField_chatgpt_api_key = document.getElementById('chatgpt_api_key');
const toggleIcon_chatgpt_api_key = document.getElementById('toggle_chatgpt_api_key');
diff --git a/options/mzta-release-notes.html b/options/mzta-release-notes.html
index 40a9b6cd..76b3338b 100644
--- a/options/mzta-release-notes.html
+++ b/options/mzta-release-notes.html
@@ -9,8 +9,11 @@
ThunderAI Release Notes
Version 3.1.0 - ??/??/2025
- Added Google Gemini API support [#204 , #2174 ].
+ [ChatGPT API][Ollama API][OpenAI Comp API][Gemini API] Added a special prompt to get calendar events data from emails [#182 ]. To use this feature, you must install also the Sparks add-on.
+ Added Google Gemini API support [#204 , #217 ].
Added {%mail_typed_text%} data placeholder to get the text inserted before the quoted mail body when replying [#196 ].
+ Added {%mail_datetime%} data placeholder to get the date and time of the email [#223 ].
+ Added {%current_datetime%} data placeholder to get the current date and time [#224 ].
Added an info text about using the new {%tags_full_list%} placeholder in the "Add Tags Prompt" page [#215 ].
...
diff --git a/pages/get-calendar-event/mzta-get-calendar-event.css b/pages/get-calendar-event/mzta-get-calendar-event.css
new file mode 100644
index 00000000..75c89f0c
--- /dev/null
+++ b/pages/get-calendar-event/mzta-get-calendar-event.css
@@ -0,0 +1,87 @@
+#get_calendar_event_prompt_container{
+ width: 90%;
+ margin: 20px auto;
+}
+
+#get_calendar_event_prompt_text{
+ width: 100%;
+}
+
+.infoline{
+ font-size: 0.8em;
+ font-style: italic;
+}
+
+.btn_div{
+ width: 100%;
+ display: flex;
+ justify-content: space-between;
+}
+
+.autocomplete-container {
+ position: relative;
+}
+
+.autocomplete-list {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ background-color: white;
+ border: 1px solid #ccc;
+ z-index: 1000;
+ max-height: 200px;
+ overflow-y: auto;
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ font-size: small;
+}
+
+.autocomplete-list li {
+ padding: 8px;
+ cursor: pointer;
+}
+
+.autocomplete-list li:hover {
+ background-color: #f0f0f0;
+}
+
+.autocomplete-list li.active {
+ background-color: #ddd;
+}
+
+.hidden {
+ display: none;
+}
+
+.unsaved{
+ color: red;
+}
+
+
+@media (prefers-color-scheme: dark) {
+ body {
+ background-color: #1C1B22;
+ color: rgb(251, 251, 254);
+ }
+
+ a:link { color: #409EFF; }
+ a:visited { color: #409EFF; }
+ a:hover { color: #66B1FF; }
+ a:active { color: #66B1FF; }
+ a:active { color: #66B1FF; }
+
+ .autocomplete-list {
+ background-color: #2E2F36;
+ border: 1px solid #2E2F36;
+ }
+
+ .autocomplete-list li:hover {
+ background-color: #4c4e58;
+ }
+
+ .autocomplete-list li.active {
+ background-color: #4c4e58;
+ }
+}
\ No newline at end of file
diff --git a/pages/get-calendar-event/mzta-get-calendar-event.html b/pages/get-calendar-event/mzta-get-calendar-event.html
new file mode 100644
index 00000000..b2f5eb3b
--- /dev/null
+++ b/pages/get-calendar-event/mzta-get-calendar-event.html
@@ -0,0 +1,30 @@
+
+
+
+
+
ThunderAI - __MSG_GetCalendarEvent_PageTitle__
+
+
+
+
+
+
__MSG_GetCalendarEvent_PageTitle__
+
__MSG_GetCalendarEvent_info_default__
+
+
Work in progress...
+
+
__MSG_GetCalendarEvent_prompt_text_title__ __MSG_customPrompts_unsaved_changes__
+
__MSG_prefs_OptionText_btnManagePrompts_infoline__ __MSG_customPrompts_managePrompts_help__
+ __MSG_prefs_OptionText_GetCalendarEvent_infoline2__
+
+
+
+
__MSG_reset_default__ __MSG_save__
+
+
+
+
+
\ No newline at end of file
diff --git a/pages/get-calendar-event/mzta-get-calendar-event.js b/pages/get-calendar-event/mzta-get-calendar-event.js
new file mode 100644
index 00000000..a6654632
--- /dev/null
+++ b/pages/get-calendar-event/mzta-get-calendar-event.js
@@ -0,0 +1,149 @@
+/*
+ * ThunderAI [https://micz.it/thunderbird-addon-thunderai/]
+ * Copyright (C) 2024 - 2025 Mic (m@micz.it)
+
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see
.
+ */
+
+import { prefs_default } from '../../options/mzta-options-default.js';
+import { taLogger } from '../../js/mzta-logger.js';
+import { getSpecialPrompts, setSpecialPrompts } from "../../js/mzta-prompts.js";
+import { getPlaceholders } from "../../js/mzta-placeholders.js";
+import { textareaAutocomplete } from "../../js/mzta-placeholders-autocomplete.js";
+
+let autocompleteSuggestions = [];
+let taLog = new taLogger("mzta-get-calendar-event-page",true);
+
+document.addEventListener('DOMContentLoaded', async () => {
+
+ i18n.updateDocument();
+ await restoreOptions();
+
+ document.querySelectorAll(".option-input").forEach(element => {
+ element.addEventListener("change", saveOptions);
+ });
+
+ let get_calendar_event_textarea = document.getElementById('get_calendar_event_prompt_text');
+ let get_calendar_event_save_btn = document.getElementById('btn_save_prompt');
+ let get_calendar_event_reset_btn = document.getElementById('btn_reset_prompt');
+
+ let specialPrompts = await getSpecialPrompts();
+ let get_calendar_event_prompt = specialPrompts.find(prompt => prompt.id === 'prompt_get_calendar_event');
+
+ get_calendar_event_textarea.addEventListener('input', (event) => {
+ get_calendar_event_reset_btn.disabled = (event.target.value === browser.i18n.getMessage('prompt_get_calendar_event_full_text'));
+ get_calendar_event_save_btn.disabled = (event.target.value === get_calendar_event_prompt.text);
+ if(get_calendar_event_save_btn.disabled){
+ document.getElementById('get_calendar_event_prompt_unsaved').classList.add('hidden');
+ } else {
+ document.getElementById('get_calendar_event_prompt_unsaved').classList.remove('hidden');
+ }
+ });
+
+ get_calendar_event_reset_btn.addEventListener('click', () => {
+ get_calendar_event_textarea.value = browser.i18n.getMessage('prompt_get_calendar_event_full_text');
+ get_calendar_event_reset_btn.disabled = true;
+ let event = new Event('input', { bubbles: true, cancelable: true });
+ get_calendar_event_textarea.dispatchEvent(event);
+ });
+
+ get_calendar_event_save_btn.addEventListener('click', () => {
+ specialPrompts.find(prompt => prompt.id === 'prompt_get_calendar_event').text = get_calendar_event_textarea.value;
+ setSpecialPrompts(specialPrompts);
+ get_calendar_event_save_btn.disabled = true;
+ document.getElementById('get_calendar_event_prompt_unsaved').classList.add('hidden');
+ browser.runtime.sendMessage({command: "reload_menus"});
+ });
+
+ if(get_calendar_event_prompt.text === 'prompt_get_calendar_event_full_text'){
+ get_calendar_event_prompt.text = browser.i18n.getMessage(get_calendar_event_prompt.text);
+ }
+ get_calendar_event_textarea.value = get_calendar_event_prompt.text;
+ get_calendar_event_reset_btn.disabled = (get_calendar_event_textarea.value === browser.i18n.getMessage('prompt_get_calendar_event_full_text'));
+
+ autocompleteSuggestions = (await getPlaceholders(true)).filter(p => !(p.id === 'additional_text')).map(p => ({command: '{%'+p.id+'%}', type: p.type}));
+ textareaAutocomplete(get_calendar_event_textarea, autocompleteSuggestions, 1); // type_value = 1, only when reading an email
+
+});
+
+
+
+// Methods to manage options, derived from: /options/mzta-options.js
+
+function saveOptions(e) {
+ e.preventDefault();
+ let options = {};
+ let element = e.target;
+
+ switch (element.type) {
+ case 'checkbox':
+ options[element.id] = element.checked;
+ break;
+ case 'number':
+ options[element.id] = element.valueAsNumber;
+ break;
+ case 'text':
+ case 'password':
+ options[element.id] = element.value.trim();
+ break;
+ default:
+ if (element.tagName === 'SELECT') {
+ options[element.id] = element.value;
+ }else{
+ console.error("[ThunderAI] Unhandled input type:", element.type);
+ }
+ }
+
+ browser.storage.sync.set(options);
+}
+
+async function restoreOptions() {
+ function setCurrentChoice(result) {
+ document.querySelectorAll(".option-input").forEach(element => {
+ taLog.log("Options restoring " + element.id + " = " + (element.id=="chatgpt_api_key" || element.id=="openai_comp_api_key" ? "****************" : result[element.id]));
+ switch (element.type) {
+ case 'checkbox':
+ element.checked = result[element.id] || false;
+ break;
+ case 'number':
+ let default_number_value = 0;
+ if(element.id == 'chatgpt_win_height') default_number_value = prefs_default.chatgpt_win_height;
+ if(element.id == 'chatgpt_win_width') default_number_value = prefs_default.chatgpt_win_width;
+ element.value = result[element.id] ?? default_number_value;
+ break;
+ case 'text':
+ case 'password':
+ let default_text_value = '';
+ if(element.id == 'default_chatgpt_lang') default_text_value = prefs_default.default_chatgpt_lang;
+ element.value = result[element.id] || default_text_value;
+ break;
+ default:
+ if (element.tagName === 'SELECT') {
+ let default_select_value = '';
+ if(element.id == 'reply_type') default_select_value = 'reply_all';
+ if(element.id == 'connection_type') default_select_value = 'chatgpt_web';
+ element.value = result[element.id] || default_select_value;
+ if (element.value === '') {
+ element.selectedIndex = -1;
+ }
+ }else{
+ console.error("[ThunderAI] Unhandled input type:", element.type);
+ }
+ }
+ });
+ }
+
+ let getting = await browser.storage.sync.get(prefs_default);
+ setCurrentChoice(getting);
+}
\ No newline at end of file
diff --git a/popup/mzta-popup.css b/popup/mzta-popup.css
index 94c605d7..6052981f 100644
--- a/popup/mzta-popup.css
+++ b/popup/mzta-popup.css
@@ -64,7 +64,7 @@ body {
background-color: #e9e9e9;
}
- .prompt_add_tags{
+ .special_prompt{
background-color: #cfe9ff;
}
@@ -102,7 +102,7 @@ body {
background-color: #444444;
}
- .prompt_add_tags{
+ .special_prompt{
background-color: #353542;
}
}
\ No newline at end of file
diff --git a/popup/mzta-popup.js b/popup/mzta-popup.js
index 0fe95c0d..a7d1ce65 100644
--- a/popup/mzta-popup.js
+++ b/popup/mzta-popup.js
@@ -17,15 +17,18 @@
*/
import { taLogger } from "../js/mzta-logger.js";
+import { checkSparksPresence } from "../js/mzta-utils.js";
let menuSendImmediately = false;
let taLog = console;
let connection_type = 'chatgpt_web';
let add_tags = false;
+let get_calendar_event = false;
let tabType;
+let num_special_menu_items = 0;
document.addEventListener('DOMContentLoaded', async () => {
- let prefs = await browser.storage.sync.get({do_debug: false, dynamic_menu_force_enter: false, add_tags: false, connection_type: 'chatgpt_web'});
+ let prefs = await browser.storage.sync.get({do_debug: false, dynamic_menu_force_enter: false, add_tags: true, get_calendar_event: true, connection_type: 'chatgpt_web'});
taLog = new taLogger("mzta-popup",prefs.do_debug);
i18n.updateDocument();
let reponse = await browser.runtime.sendMessage({command: "popup_menu_ready"});
@@ -40,6 +43,7 @@ document.addEventListener('DOMContentLoaded', async () => {
menuSendImmediately = prefs.dynamic_menu_force_enter;
connection_type = prefs.connection_type;
add_tags = prefs.add_tags;
+ get_calendar_event = prefs.get_calendar_event;
searchPrompt(active_prompts, tabId, tabType);
i18n.updateDocument();
}, { once: true });
@@ -95,20 +99,56 @@ 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
+ // 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;
- if(checkDoAddTags()){
- max_num_el = 9;
- first_num_el = 1;
- filteredData = ensurePromptAddTagsFirst(filteredData);
- if (!filteredData[0].numberPrepended) {
- filteredData[0].numberPrepended = 'true';
- filteredData[0].label = '0. ' + filteredData[0].label;
+
+ let do_add_tags = checkDoAddTags();
+ let do_get_calendar_event = checkDoCalendarEvent();
+
+ // console.log(">>>>>>>>>>> do_add_tags: " + do_add_tags);
+ // console.log(">>>>>>>>>>> do_get_calendar_event: " + do_get_calendar_event);
+ // console.log(">>>>>>>>>>> filteredData: " + JSON.stringify(filteredData));
+
+ if(do_add_tags){
+ num_special_menu_items++;
+ }
+ if(do_get_calendar_event){
+ num_special_menu_items++;
+ }
+ // 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(checkDoAddTags()){
+ // max_num_el = 9;
+ // first_num_el = 1;
+ // filteredData = ensurePromptAddTagsFirst(filteredData);
+ // if (!filteredData[0].numberPrepended) {
+ // filteredData[0].numberPrepended = 'true';
+ // filteredData[0].label = '0. ' + filteredData[0].label;
+ // }
+ // }
Array.from(filteredData).slice(first_num_el, max_num_el).forEach((item, index) => {
- let number = (index < 9) ? (index + 1).toString() : '0';
+ let number = (index + first_num_el).toString();
// Check if the number is already prepended to avoid duplication
if (!item.numberPrepended) {
item.label = `${number}. ${item.label}`;
@@ -118,21 +158,14 @@ async function searchPrompt(allPrompts, tabId, tabType){
// console.log(">>>>>>>>>>>>> filteredData: " + JSON.stringify(filteredData));
- // add the prompt_add_tags if add_tags is true and connection_type is not 'chatgpt_web'
- // if(checkDoAddTags()){
- // let number = '0';
- // let item = {label: `${number}. ${browser.i18n.getMessage('prompt_add_tags')}`, id: 'prompt_add_tags', numberPrepended: 'true'};
- // filteredData.unshift(item);
- // }
-
// Create a div for each filtered result
filteredData.forEach(item => {
const itemDiv = document.createElement('div');
itemDiv.classList.add('mzta_autocomplete-item');
itemDiv.textContent = item.label;
itemDiv.setAttribute('data-id', item.id);
- if(item.id === 'prompt_add_tags'){
- itemDiv.className += ' prompt_add_tags';
+ if((item.id === 'prompt_add_tags')||(item.id === 'prompt_get_calendar_event')){
+ itemDiv.className += ' special_prompt';
}
// Add a mousedown event to select the item
@@ -293,6 +326,10 @@ function checkDoAddTags(){
return add_tags && (connection_type !== "chatgpt_web" && tabType !== 'messageCompose');
}
+function checkDoCalendarEvent(){
+ return get_calendar_event && (connection_type !== "chatgpt_web" && tabType !== 'messageCompose') && checkSparksPresence();
+}
+
function ensurePromptAddTagsFirst(arr) {
// Find the index of the object with id "prompt_add_tags"
const index = arr.findIndex(item => item.id === "prompt_add_tags");
@@ -305,5 +342,22 @@ function ensurePromptAddTagsFirst(arr) {
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;
}
\ No newline at end of file