diff --git a/CHANGELOG.md b/CHANGELOG.md index 88750066..ea003c7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@

Version 3.1.0 - ??/??/2025

diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 5435b6ca..56ad5422 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -895,6 +895,56 @@ "message": "The tags must be written in", "description": "" }, + "prefs_Connection_type_Google_Gemini_API": { + "message": "Google Gemini API", + "description": "" + }, + "prefs_GoogleGemini_API_Key": { + "message": "API Key", + "description": "" + }, + "GoogleGemini_Models": { + "message": "Google Gemini API Models", + "description": "" + }, + "GoogleGemini_Models_Fetch": { + "message": "Update Google Gemini Models list", + "description": "" + }, + "GoogleGemini_Models_Error_fetching": { + "message": "Error trying to fetch Google Gemini models", + "description": "" + }, + "google_gemini_api_request_failed": { + "message": "Google Gemini API request failed", + "description": "" + }, + "google_gemini_api_connecting": { + "message": "Attempting to connect to Google Gemini using the API key provided", + "description": "" + }, + "google_gemini_empty_apikey": { + "message": "You've not added an API Key for the Google Gemini API. Please insert one in the options page.", + "description": "" + }, + "google_gemini_empty_model": { + "message": "You've not choosen a model for the Google Gemini API. Please choose one in the options page.", + "description": "" + }, + "GoogleGemini_SystemInstruction": { + "message": "System Instruction", + "description": "" + }, + "GoogleGemini_SystemInstruction_Info": { + "message": "When you set a system instruction, you give the model additional context to understand the task, provide more customized responses, and adhere to specific guidelines over the prompt that will be sent.", + "description": "" + }, + "ChatGPT_Developer_Messages": { + "message": "Developer Messages", + "description": "" + }, + "ChatGPT_Developer_Messages_Info": { + "message": "When you set the developer messages, you give the model additional context to understand the task, provide more customized responses, and adhere to specific guidelines over the prompt that will be sent.", "prefs_OptionText_btnManagePrompts_infoline3": { "message": "You can use the {%tags_full_list%} data placeholder in the prompt to list the available tags. With an appropriate prompt, you could then force the tags to be chosen only from the list of those already existing.", "description": "" diff --git a/api_webchat/controller.js b/api_webchat/controller.js index aaed0d59..cb315ea8 100644 --- a/api_webchat/controller.js +++ b/api_webchat/controller.js @@ -47,6 +47,9 @@ switch (llm) { case "chatgpt_api": worker = new Worker('../js/workers/model-worker-openai.js', { type: 'module' }); break; + case "google_gemini_api": + worker = new Worker('../js/workers/model-worker-google_gemini.js', { type: 'module' }); + break; case "ollama_api": worker = new Worker('../js/workers/model-worker-ollama.js', { type: 'module' }); break; @@ -62,17 +65,30 @@ messageInput.init(worker); messageInput.setMessagesArea(messagesArea); switch (llm) { - case "chatgpt_api": - let prefs_api = await browser.storage.sync.get({chatgpt_api_key: '', chatgpt_model: '', do_debug: false}); + case "chatgpt_api": { + let prefs_api = await browser.storage.sync.get({chatgpt_api_key: '', chatgpt_model: '', chatgpt_developer_messages:'', do_debug: false}); let i18nStrings = {}; i18nStrings["chatgpt_api_request_failed"] = browser.i18n.getMessage('chatgpt_api_request_failed'); i18nStrings["error_connection_interrupted"] = browser.i18n.getMessage('error_connection_interrupted'); messageInput.setModel(prefs_api.chatgpt_model); messagesArea.setLLMName("ChatGPT"); - worker.postMessage({ type: 'init', chatgpt_api_key: prefs_api.chatgpt_api_key, chatgpt_model: prefs_api.chatgpt_model, do_debug: prefs_api.do_debug, i18nStrings: i18nStrings}); + worker.postMessage({ type: 'init', chatgpt_api_key: prefs_api.chatgpt_api_key, chatgpt_model: prefs_api.chatgpt_model, chatgpt_developer_messages: prefs_api.chatgpt_developer_messages, do_debug: prefs_api.do_debug, i18nStrings: i18nStrings}); messagesArea.appendUserMessage(browser.i18n.getMessage("chagpt_api_connecting") + " " +browser.i18n.getMessage("AndModel") + " \"" + prefs_api.chatgpt_model + "\"...", "info"); browser.runtime.sendMessage({command: "openai_api_ready_" + call_id, window_id: (await browser.windows.getCurrent()).id}); break; + } + case "google_gemini_api": { + let prefs_api = await browser.storage.sync.get({google_gemini_api_key: '', google_gemini_model: '', google_gemini_system_instruction: '', do_debug: false}); + let i18nStrings = {}; + i18nStrings["google_gemini_api_request_failed"] = browser.i18n.getMessage('google_gemini_api_request_failed'); + i18nStrings["error_connection_interrupted"] = browser.i18n.getMessage('error_connection_interrupted'); + messageInput.setModel(prefs_api.google_gemini_model); + messagesArea.setLLMName("Google Gemini"); + worker.postMessage({ type: 'init', google_gemini_api_key: prefs_api.google_gemini_api_key, google_gemini_model: prefs_api.google_gemini_model, google_gemini_system_instruction: prefs_api.google_gemini_system_instruction, do_debug: prefs_api.do_debug, i18nStrings: i18nStrings}); + messagesArea.appendUserMessage(browser.i18n.getMessage("google_gemini_api_connecting") + " " +browser.i18n.getMessage("AndModel") + " \"" + prefs_api.google_gemini_model + "\"...", "info"); + browser.runtime.sendMessage({command: "google_gemini_api_ready_" + call_id, window_id: (await browser.windows.getCurrent()).id}); + break; + } case "ollama_api": { let prefs_api = await browser.storage.sync.get({ollama_host: '', ollama_model: '', do_debug: false}); let i18nStrings = {}; diff --git a/api_webchat/messagesArea.js b/api_webchat/messagesArea.js index f2257f24..5e87f57b 100644 --- a/api_webchat/messagesArea.js +++ b/api_webchat/messagesArea.js @@ -174,7 +174,7 @@ class MessagesArea extends HTMLElement { if (isLastMessageFromUser) { const header = document.createElement('h2'); - header.textContent = "Chat GPT" + (type=='error' ? " - Error" : ""); + header.textContent = this.llmName + (type=='error' ? " - Error" : ""); this.messages.appendChild(header); } diff --git a/js/api/google_gemini.js b/js/api/google_gemini.js new file mode 100644 index 00000000..59624ea7 --- /dev/null +++ b/js/api/google_gemini.js @@ -0,0 +1,109 @@ +/* + * ThunderAI [https://micz.it/thunderbird-addon-thunderai/] + * Copyright (C) 2024 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 . + */ + + + +export class GoogleGemini { + + apiKey = ''; + model = ''; + system_instruction = ''; + stream = false; + + constructor(apiKey, model, system_instruction, stream) { + this.apiKey = apiKey; + this.model = model; + this.system_instruction = system_instruction; + this.stream = stream; + } + + + fetchModels = async () => { + try{ + const response = await fetch("https://generativelanguage.googleapis.com/v1beta/models?key=" + this.apiKey, { + method: "GET", + headers: { + "Content-Type": "application/json" + }, + }); + + if (!response.ok) { + const errorDetail = await response.text(); + let err_msg = "[ThunderAI] Google Gemini API request failed: " + response.status + " " + response.statusText + ", Detail: " + errorDetail; + console.error(err_msg); + let output = {}; + output.ok = false; + output.error = errorDetail; + return output; + } + + let output = {}; + output.ok = true; + let output_response = await response.json(); + console.log("[ThunderAI] Google Gemini API response: " + JSON.stringify(output_response)); + output.response = output_response.models; + + return output; + }catch (error) { + console.error("[ThunderAI] Google Gemini API request failed: " + error); + let output = {}; + output.is_exception = true; + output.ok = false; + output.error = "Google Gemini API request failed: " + error; + return output; + } + } + + fetchResponse = async (messages) => { + try { + + let google_gemini_body = { + contents:messages + }; + + console.log("[ThunderAI] Google Gemini API system_instruction: " + JSON.stringify(this.system_instruction)); + + if(this.system_instruction !== '') { + google_gemini_body.system_instruction = { + parts:{ + text: this.system_instruction + } + } + } + + console.log("[ThunderAI] Google Gemini API request: " + JSON.stringify(google_gemini_body)); + + const response = await fetch("https://generativelanguage.googleapis.com/v1beta/models/" + this.model + ":" + (this.stream ? 'streamGenerateContent?alt=sse&' : 'generateContent?') + "key=" + this.apiKey, { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify(google_gemini_body), + }); + return response; + }catch (error) { + console.error("[ThunderAI] Google Gemini API request failed: " + error); + let output = {}; + output.is_exception = true; + output.ok = false; + output.error = "Google Gemini API request failed: " + error; + return output; + } + } + +} \ No newline at end of file diff --git a/js/api/openai.js b/js/api/openai.js index 11d4398d..2bcf86a8 100644 --- a/js/api/openai.js +++ b/js/api/openai.js @@ -23,11 +23,13 @@ export class OpenAI { apiKey = ''; model = ''; + developer_messages = ''; stream = false; - constructor(apiKey, model, stream) { + constructor(apiKey, model, developer_messages, stream) { this.apiKey = apiKey; this.model = model; + this.developer_messages = developer_messages; this.stream = stream; } @@ -69,6 +71,13 @@ export class OpenAI { } fetchResponse = async (messages, maxTokens = 0) => { + + if(this.developer_messages !== ''){ + messages.push({role: "developer", content: [{"type": "text", "text": this.developer_messages}]}); + } + + // console.log(">>>>>>>>>>> OpenAI API request: " + JSON.stringify(messages)); + try { const response = await fetch("https://api.openai.com/v1/chat/completions", { method: "POST", diff --git a/js/special_commands/mzta-add-tags.js b/js/special_commands/mzta-add-tags.js index 7cfa422d..3902b00e 100644 --- a/js/special_commands/mzta-add-tags.js +++ b/js/special_commands/mzta-add-tags.js @@ -38,6 +38,9 @@ case "chatgpt_api": this.worker = new Worker(new URL('../workers/model-worker-openai.js', import.meta.url), { type: 'module' }); break; + case "google_gemini_api": + this.worker = new Worker(new URL('../workers/model-worker-google_gemini.js', import.meta.url), { type: 'module' }); + break; case "ollama_api": this.worker = new Worker(new URL('../workers/model-worker-ollama.js', import.meta.url), { type: 'module' }); break; @@ -49,10 +52,16 @@ async initWorker() { switch (this.llm) { - case "chatgpt_api": - let prefs_api = await browser.storage.sync.get({chatgpt_api_key: '', chatgpt_model: ''}); - this.worker.postMessage({ type: 'init', chatgpt_api_key: prefs_api.chatgpt_api_key, chatgpt_model: prefs_api.chatgpt_model, do_debug: this.do_debug, i18nStrings: ''}); + case "chatgpt_api": { + let prefs_api = await browser.storage.sync.get({chatgpt_api_key: '', chatgpt_model: '', chatgpt_developer_messages: ''}); + this.worker.postMessage({ type: 'init', chatgpt_api_key: prefs_api.chatgpt_api_key, chatgpt_model: prefs_api.chatgpt_model, chatgpt_developer_messages: prefs_api.chatgpt_developer_messages, do_debug: this.do_debug, i18nStrings: ''}); break; + } + case "google_gemini_api": { + let prefs_api = await browser.storage.sync.get({google_gemini_api_key: '', google_gemini_model: '', google_gemini_system_instruction: ''}); + this.worker.postMessage({ type: 'init', google_gemini_api_key: prefs_api.google_gemini_api_key, google_gemini_model: prefs_api.google_gemini_model, google_gemini_system_instruction: prefs_api.google_gemini_system_instruction, do_debug: this.do_debug, i18nStrings: ''}); + break; + } case "ollama_api": { let prefs_api = await browser.storage.sync.get({ollama_host: '', ollama_model: ''}); this.worker.postMessage({ type: 'init', ollama_host: prefs_api.ollama_host, ollama_model: prefs_api.ollama_model, do_debug: this.do_debug, i18nStrings: ''}); diff --git a/js/workers/model-worker-google_gemini.js b/js/workers/model-worker-google_gemini.js new file mode 100644 index 00000000..514bddde --- /dev/null +++ b/js/workers/model-worker-google_gemini.js @@ -0,0 +1,125 @@ +/* + * ThunderAI [https://micz.it/thunderbird-addon-thunderai/] + * Copyright (C) 2024 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 . + * + * + * This file contains a modified version of the code from the project at https://github.com/boxabirds/chatgpt-frontend-nobuild + * The original code has been released under the Apache License, Version 2.0. + */ + +import { GoogleGemini } from '../api/google_gemini.js'; +import { taLogger } from '../mzta-logger.js'; + +let google_gemini_api_key = null; +let google_gemini_model = ''; +let google_gemini = null; +let stopStreaming = false; +let i18nStrings = null; +let do_debug = false; +let taLog = null; + +let conversationHistory = []; +let assistantResponseAccumulator = ''; + +self.onmessage = async function(event) { + if (event.data.type === 'init') { + google_gemini_api_key = event.data.google_gemini_api_key; + google_gemini_model = event.data.google_gemini_model; + google_gemini = new GoogleGemini(google_gemini_api_key, google_gemini_model, event.data.google_gemini_system_instruction, true); + do_debug = event.data.do_debug; + i18nStrings = event.data.i18nStrings; + taLog = new taLogger('model-worker-google_gemini', do_debug); + } else if (event.data.type === 'chatMessage') { + conversationHistory.push({ role: 'user', parts: [{"text": event.data.message}] }); + + const response = await google_gemini.fetchResponse(conversationHistory); + postMessage({ type: 'messageSent' }); + + if (!response.ok) { + let error_message = ''; + let errorDetail = ''; + if(response.is_exception === true){ + error_message = response.error; + }else{ + try{ + const errorJSON = await response.json(); + errorDetail = JSON.stringify(errorJSON); + error_message = errorJSON.error.message; + }catch(e){ + error_message = response.statusText; + } + taLog.log("error_message: " + JSON.stringify(error_message)); + } + postMessage({ type: 'error', payload: i18nStrings["google_gemini_api_request_failed"] + ": " + response.status + " " + response.statusText + ", Detail: " + error_message + " " + errorDetail }); + throw new Error("[ThunderAI] Google Gemini API request failed: " + response.status + " " + response.statusText + ", Detail: " + error_message + " " + errorDetail); + } + + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + let buffer = ''; + + while (true) { + if (stopStreaming) { + stopStreaming = false; + reader.cancel(); + conversationHistory.push({ role: 'model', parts: [{"text": assistantResponseAccumulator}] }); + assistantResponseAccumulator = ''; + postMessage({ type: 'tokensDone' }); + break; + } + const { done, value } = await reader.read(); + if (done) { + conversationHistory.push({ role: 'model', parts: [{"text": assistantResponseAccumulator}] }); + assistantResponseAccumulator = ''; + postMessage({ type: 'tokensDone' }); + break; + } + // lots of low-level Google Gemini response parsing stuff + const chunk = decoder.decode(value); + buffer += chunk; + taLog.log("buffer " + buffer); + const lines = buffer.split("\n"); + buffer = lines.pop(); + let parsedLines = []; + try{ + parsedLines = lines + .map((line) => line.replace(/^data: /, "").trim()) // Remove the "data: " prefix + .filter((line) => line !== "" ) // Remove empty lines + // .map((line) => JSON.parse(line)); // Parse the JSON string + .map((line) => { + taLog.log("line: " + JSON.stringify(line)); + return JSON.parse(line); + }); + }catch(e){ + taLog.error("Error parsing lines: " + e); + } + + for (const parsedLine of parsedLines) { + const { candidates } = parsedLine; + const { content } = candidates[0]; + const { parts } = content; + const { text } = parts[0]; + // Update the UI with the new content + if (text) { + assistantResponseAccumulator += text; + postMessage({ type: 'newToken', payload: { token: text } }); + } + } + } + } else if (event.data.type === 'stop') { + stopStreaming = true; + } +}; diff --git a/js/workers/model-worker-openai.js b/js/workers/model-worker-openai.js index 3f364682..536dc96d 100644 --- a/js/workers/model-worker-openai.js +++ b/js/workers/model-worker-openai.js @@ -38,7 +38,7 @@ self.onmessage = async function(event) { if (event.data.type === 'init') { chatgpt_api_key = event.data.chatgpt_api_key; chatgpt_model = event.data.chatgpt_model; - openai = new OpenAI(chatgpt_api_key, chatgpt_model, true); + openai = new OpenAI(chatgpt_api_key, chatgpt_model, event.data.chatgpt_developer_messages, true); do_debug = event.data.do_debug; i18nStrings = event.data.i18nStrings; taLog = new taLogger('model-worker-openai', do_debug); diff --git a/mzta-background.js b/mzta-background.js index d48a3d22..4d202899 100644 --- a/mzta-background.js +++ b/mzta-background.js @@ -400,6 +400,56 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_ break; // chatgpt_api - END + case 'google_gemini_api': + // We are using the Google Gemini API + + let rand_call_id5 = '_google_gemini_' + generateCallID(); + + const listener5 = (message, sender, sendResponse) => { + + function handleChatGptApi(createdTab) { + let mailMessageId5 = -1; + if(mailMessage) mailMessageId5 = mailMessage.id; + + // check if the config is present, or give a message error + if (prefs.chatgpt_api_key == '') { + browser.tabs.sendMessage(createdTab.id, { command: "api_error", error: browser.i18n.getMessage('google_gemini_empty_apikey')}); + return; + } + if (prefs.chatgpt_model == '') { + browser.tabs.sendMessage(createdTab.id, { command: "api_error", error: browser.i18n.getMessage('google_gemini_empty_model')}); + return; + } + //console.log(">>>>>>>>>> sender: " + JSON.stringify(sender)); + browser.tabs.sendMessage(createdTab.id, { command: "api_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId5, do_custom_text: do_custom_text}); + taLog.log('[Google Gemini] Connection succeded!'); + browser.runtime.onMessage.removeListener(listener5); + } + + if (message.command === "google_gemini_api_ready_"+rand_call_id5) { + return handleChatGptApi(sender.tab); + } + return false; + } + + browser.runtime.onMessage.addListener(listener5); + + let win_options5 = { + url: browser.runtime.getURL('api_webchat/index.html?llm='+prefs.connection_type+'&call_id='+rand_call_id5+'&ph_def_val='+(prefs.placeholders_use_default_value?'1':'0')), + type: "popup", + } + + taLog.log("[chatgpt_api] prefs.chatgpt_win_width: " + prefs.chatgpt_win_width + ", prefs.chatgpt_win_height: " + prefs.chatgpt_win_height); + + if((prefs.chatgpt_win_width != '') && (prefs.chatgpt_win_height != '') && (prefs.chatgpt_win_width != 0) && (prefs.chatgpt_win_height != 0)){ + win_options5.width = prefs.chatgpt_win_width, + win_options5.height = prefs.chatgpt_win_height + } + + await browser.windows.create(win_options5); + + break; // google_gemini_api - END + case 'ollama_api': // We are using the Ollama API @@ -506,7 +556,10 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_ await browser.windows.create(win_options4); - break; // openai_comp_api + break; // openai_comp_api - END + default: + taLog.error("Unknown API connection type: " + prefs.connection_type); + break; } } diff --git a/options/mzta-options-default.js b/options/mzta-options-default.js index da3703e7..7a2315bb 100644 --- a/options/mzta-options-default.js +++ b/options/mzta-options-default.js @@ -21,11 +21,12 @@ export const prefs_default = { chatgpt_win_height: 800, chatgpt_win_width: 700, default_chatgpt_lang: '', - connection_type: 'chatgpt_web', //Other values: 'chatgpt_api', 'ollama_api', 'openai_comp_api' + connection_type: 'chatgpt_web', //Other values: 'chatgpt_api', 'ollama_api', 'openai_comp_api', 'google_gemini_api' chatgpt_web_model: '', chatgpt_web_tempchat: false, chatgpt_api_key: '', chatgpt_model: '', + chatgpt_developer_messages: '', ollama_host: '', ollama_model: '', openai_comp_host: '', // For OpenAI Compatible API as LM-Studio @@ -33,6 +34,9 @@ export const prefs_default = { openai_comp_api_key: '', openai_comp_use_v1: true, openai_comp_chat_name: 'OpenAI Comp', + google_gemini_api_key: '', + google_gemini_model: '', + google_gemini_system_instruction: '', dynamic_menu_force_enter: false, dynamic_menu_order_alphabet: true, placeholders_use_default_value: false, diff --git a/options/mzta-options.css b/options/mzta-options.css index 77a05fef..e3680a08 100644 --- a/options/mzta-options.css +++ b/options/mzta-options.css @@ -104,11 +104,25 @@ tr.conntype_openai_comp_api, tr.conntype_openai_comp_api2{ background-color: rgb(213, 169, 238); } +tr.conntype_google_gemini_api, tr.conntype_google_gemini_api2{ + background-color: rgb(233, 238, 169); +} + #chatgpt_model_fetch_loading{ display: none; font-style: italic; } +#google_gemini_model_fetch_loading{ + display: none; + font-style: italic; +} + +textarea.option-textarea{ + width: -moz-available; + height: 10em; +} + .api_key-container { position: relative; width: 100%; @@ -218,6 +232,10 @@ input.option-input[type="text"]{ background-color: rgb(89, 20, 129); } + tr.conntype_google_gemini_api, tr.conntype_google_gemini_api2{ + background-color: rgb(72, 77, 3); + } + .api_key-container .toggle-icon img { filter: invert(1); } diff --git a/options/mzta-options.html b/options/mzta-options.html index dc54e2eb..76e99676 100644 --- a/options/mzta-options.html +++ b/options/mzta-options.html @@ -109,6 +109,7 @@ @@ -174,6 +175,59 @@ + + + + + + + + + + + +
+ + +
+ + + + + + + + __MSG_Loading__
+ + + + + + + + + + +