Merge pull request #220 from micz/google_gemini

Google Gemini API
This commit is contained in:
Mic 2025-01-08 20:47:58 +01:00 committed by GitHub
commit 340964adfb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 555 additions and 15 deletions

View file

@ -7,6 +7,7 @@
<h2>Version 3.1.0 - ??/??/2025</h2>
<ul>
<li>Added Google Gemini API support [<a href="https://github.com/micz/ThunderAI/issues/204">#204</a>, <a href="https://github.com/micz/ThunderAI/issues/217">#2174</a>].</li>
<li>Added an info text about using the new <i>{%tags_full_list%}</i> placeholder in the "Add Tags Prompt" page [<a href="https://github.com/micz/ThunderAI/issues/215">#215</a>].</li>
<li>...</li>
</ul>

View file

@ -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": ""

View file

@ -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 = {};

View file

@ -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);
}

109
js/api/google_gemini.js Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}
}

View file

@ -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",

View file

@ -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: ''});

View file

@ -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 <http://www.gnu.org/licenses/>.
*
*
* 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;
}
};

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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,

View file

@ -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);
}

View file

@ -109,6 +109,7 @@
<select id="connection_type" name="connection_type" class="option-input">
<option value="chatgpt_web">__MSG_prefs_Connection_type_ChatGPT_Web__</option>
<option value="chatgpt_api">__MSG_prefs_Connection_type_ChatGPT_API__</option>
<option value="google_gemini_api">__MSG_prefs_Connection_type_Google_Gemini_API__</option>
<option value="ollama_api">__MSG_prefs_Connection_type_Ollama_API__</option>
<option value="openai_comp_api">__MSG_prefs_Connection_type_OpenAI_Comp_API__</option>
</select>
@ -174,6 +175,59 @@
</label>
</td>
</tr>
<tr class="conntype_chatgpt_api">
<td>
<label>
<span>__MSG_ChatGPT_Developer_Messages__</span>
</label>
</td>
<td>
<label>
<textarea id="chatgpt_developer_messages" name="chatgpt_developer_messages" class="option-input option-textarea"></textarea>
<br>__MSG_ChatGPT_Developer_Messages_Info__
</label>
</td>
</tr>
<tr class="conntype_google_gemini_api">
<td><label>
<span>__MSG_prefs_GoogleGemini_API_Key__</span>
</label></td>
<td>
<div class="api_key-container">
<label>
<input type="password" id="google_gemini_api_key" name="google_gemini_api_key" class="option-input"/>
</label>
<span class="toggle-icon" id="toggle_google_gemini_api_key"><img src="../images/pwd-show.png" id="pwd-icon_google_gemini_api_key"></span>
</div>
</td>
</tr>
<tr class="conntype_google_gemini_api">
<td>
<label>
<span>__MSG_GoogleGemini_Models__</span>
</label>
</td>
<td>
<button id="btnUpdateGoogleGeminiModels">__MSG_GoogleGemini_Models_Fetch__</button> <span id="google_gemini_model_fetch_loading">__MSG_Loading__</span><br>
<label>
<select id="google_gemini_model" name="google_gemini_model" class="option-input">
</select>
</label>
</td>
</tr>
<tr class="conntype_google_gemini_api">
<td>
<label>
<span>__MSG_GoogleGemini_SystemInstruction__</span>
</label>
</td>
<td>
<label>
<textarea id="google_gemini_system_instruction" name="google_gemini_system_instruction" class="option-input option-textarea"></textarea>
<br>__MSG_GoogleGemini_SystemInstruction_Info__
</label>
</td>
</tr>
<tr class="conntype_ollama_api">
<td><label>
<span>__MSG_prefs_API_Host__</span>

View file

@ -22,6 +22,7 @@ import { taLogger } from '../js/mzta-logger.js';
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';
let taLog = new taLogger("mzta-options",true);
@ -44,7 +45,9 @@ function saveOptions(e) {
default:
if (element.tagName === 'SELECT') {
options[element.id] = element.value;
}else{
} else if (element.tagName === 'TEXTAREA') {
options[element.id] = element.value.trim();
} else {
console.error("[ThunderAI] Unhandled input type:", element.type);
}
}
@ -55,7 +58,7 @@ function saveOptions(e) {
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]));
taLog.log("Options restoring " + element.id + " = " + (element.id=="chatgpt_api_key" || element.id=="openai_comp_api_key" || element.id=="google_gemini_api_key" ? "****************" : result[element.id]));
switch (element.type) {
case 'checkbox':
element.checked = result[element.id] || false;
@ -81,6 +84,8 @@ async function restoreOptions() {
if (element.value === '') {
element.selectedIndex = -1;
}
} else if (element.tagName === 'TEXTAREA') {
element.value = result[element.id];
}else{
console.error("[ThunderAI] Unhandled input type:", element.type);
}
@ -99,12 +104,14 @@ function showConnectionOptions() {
let chatgpt_api_display = 'none';
let ollama_api_display = 'none';
let openai_comp_api_display = 'none';
let google_gemini_api_display = 'none';
let conntype_select = document.getElementById("connection_type");
let parent = conntype_select.parentElement.parentElement.parentElement;
parent.classList.toggle("conntype_chatgpt_web", (conntype_select.value === "chatgpt_web"));
parent.classList.toggle("conntype_chatgpt_api", (conntype_select.value === "chatgpt_api"));
parent.classList.toggle("conntype_ollama_api", (conntype_select.value === "ollama_api"));
parent.classList.toggle("conntype_openai_comp_api", (conntype_select.value === "openai_comp_api"));
parent.classList.toggle("conntype_google_gemini_api", (conntype_select.value === "google_gemini_api"));
if (conntype_select.value === "chatgpt_web") {
chatgpt_web_display = 'table-row';
}else{
@ -125,6 +132,11 @@ function showConnectionOptions() {
}else{
openai_comp_api_display = 'none';
}
if (conntype_select.value === "google_gemini_api") {
google_gemini_api_display = 'table-row';
}else{
google_gemini_api_display = 'none';
}
document.querySelectorAll(".conntype_chatgpt_web").forEach(element => {
element.style.display = chatgpt_web_display;
});
@ -137,6 +149,9 @@ function showConnectionOptions() {
document.querySelectorAll(".conntype_openai_comp_api").forEach(element => {
element.style.display = openai_comp_api_display;
});
document.querySelectorAll(".conntype_google_gemini_api").forEach(element => {
element.style.display = google_gemini_api_display;
});
}
function warn_ChatGPT_APIKeyEmpty() {
@ -161,6 +176,28 @@ function warn_ChatGPT_APIKeyEmpty() {
}
}
function warn_GoogleGemini_APIKeyEmpty() {
let apiKeyInput = document.getElementById('google_gemini_api_key');
let btnFetchGoogleGeminiModels = document.getElementById('btnUpdateGoogleGeminiModels');
let modelGoogleGemini = document.getElementById('google_gemini_model');
if(apiKeyInput.value === ''){
apiKeyInput.style.border = '2px solid red';
btnFetchGoogleGeminiModels.disabled = true;
modelGoogleGemini.disabled = true;
modelGoogleGemini.selectedIndex = -1;
modelGoogleGemini.style.border = '';
}else{
apiKeyInput.style.border = '';
btnFetchGoogleGeminiModels.disabled = false;
modelGoogleGemini.disabled = false;
if((modelGoogleGemini.selectedIndex === -1)||(modelGoogleGemini.value === '')){
modelGoogleGemini.style.border = '2px solid red';
}else{
modelGoogleGemini.style.border = '';
}
}
}
function warn_Ollama_HostEmpty() {
let hostInput = document.getElementById('ollama_host');
let btnFetchOllamaModels = document.getElementById('btnUpdateOllamaModels');
@ -294,11 +331,13 @@ document.addEventListener('DOMContentLoaded', async () => {
conntype_select.addEventListener("change", warn_ChatGPT_APIKeyEmpty);
conntype_select.addEventListener("change", warn_Ollama_HostEmpty);
conntype_select.addEventListener("change", warn_OpenAIComp_HostEmpty);
conntype_select.addEventListener("change", warn_GoogleGemini_APIKeyEmpty);
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);
document.getElementById("google_gemini_api_key").addEventListener("change", warn_GoogleGemini_APIKeyEmpty);
let prefs = await browser.storage.sync.get({chatgpt_model: '', ollama_model: '', openai_comp_model: ''});
let prefs = await browser.storage.sync.get({chatgpt_model: '', ollama_model: '', openai_comp_model: '', google_gemini_model: ''});
// OpenAI API ChatGPT model fetching
let select_chatgpt_model = document.getElementById('chatgpt_model');
@ -340,6 +379,46 @@ document.addEventListener('DOMContentLoaded', async () => {
warn_ChatGPT_APIKeyEmpty();
});
// Google Gemini API ChatGPT model fetching
let select_google_gemini_model = document.getElementById('google_gemini_model');
const google_gemini_option = document.createElement('option');
google_gemini_option.value = prefs.google_gemini_model;
google_gemini_option.text = prefs.google_gemini_model;
select_google_gemini_model.appendChild(google_gemini_option);
select_google_gemini_model.addEventListener("change", warn_GoogleGemini_APIKeyEmpty);
document.getElementById('btnUpdateGoogleGeminiModels').addEventListener('click', async () => {
document.getElementById('google_gemini_model_fetch_loading').style.display = 'inline';
let google_gemini = new GoogleGemini(document.getElementById("google_gemini_api_key").value, '', true);
google_gemini.fetchModels().then((data) => {
if(!data.ok){
let errorDetail;
try {
errorDetail = JSON.parse(data.error);
errorDetail = errorDetail.error.message;
} catch (e) {
errorDetail = data.error;
}
document.getElementById('google_gemini_model_fetch_loading').style.display = 'none';
console.error("[ThunderAI] " + browser.i18n.getMessage("GoogleGemini_Models_Error_fetching"));
alert(browser.i18n.getMessage("GoogleGemini_Models_Error_fetching")+": " + errorDetail);
return;
}
taLog.log("GoogleGemini models: " + JSON.stringify(data));
data.response.forEach(model => {
if (!Array.from(select_google_gemini_model.options).some(option => option.value === model.id)) {
const option = document.createElement('option');
option.value = model.name.substring(model.name.lastIndexOf("/") + 1);;
option.text = model.displayName;
select_google_gemini_model.appendChild(option);
}
});
document.getElementById('google_gemini_model_fetch_loading').style.display = 'none';
});
warn_GoogleGemini_APIKeyEmpty();
});
// Ollama API Model fetching
let select_ollama_model = document.getElementById('ollama_model');
const ollama_option = document.createElement('option');
@ -442,6 +521,7 @@ select_openai_comp_model.addEventListener("change", warn_OpenAIComp_HostEmpty);
warn_ChatGPT_APIKeyEmpty();
warn_Ollama_HostEmpty();
warn_OpenAIComp_HostEmpty();
warn_GoogleGemini_APIKeyEmpty();
disable_MaxPromptLength();
disable_AddTags();
@ -456,6 +536,17 @@ select_openai_comp_model.addEventListener("change", warn_OpenAIComp_HostEmpty);
icon_img_chatgpt_api_key.src = type === 'password' ? "../images/pwd-show.png" : "../images/pwd-hide.png";
});
const passwordField_google_gemini_api_key = document.getElementById('google_gemini_api_key');
const toggleIcon_google_gemini_api_key = document.getElementById('toggle_google_gemini_api_key');
const icon_img_google_gemini_api_key = document.getElementById('pwd-icon_google_gemini_api_key');
toggleIcon_google_gemini_api_key.addEventListener('click', () => {
const type = passwordField_google_gemini_api_key.getAttribute('type') === 'password' ? 'text' : 'password';
passwordField_google_gemini_api_key.setAttribute('type', type);
icon_img_google_gemini_api_key.src = type === 'password' ? "../images/pwd-show.png" : "../images/pwd-hide.png";
});
const passwordField_openai_comp_api_key = document.getElementById('openai_comp_api_key');
const toggleIcon_openai_comp_api_key = document.getElementById('toggle_openai_comp_api_key');
const icon_img_openai_comp_api_key = document.getElementById('pwd-icon_openai_comp_api_key');

View file

@ -9,6 +9,7 @@
<div id="miczRelNotes"><h1>ThunderAI Release Notes</h1>
<h2>Version 3.1.0 - ??/??/2025</h2>
<ul>
<li>Added Google Gemini API support [<a href="https://github.com/micz/ThunderAI/issues/204">#204</a>, <a href="https://github.com/micz/ThunderAI/issues/217">#2174</a>].</li>
<li>Added an info text about using the new <i>{%tags_full_list%}</i> placeholder in the "Add Tags Prompt" page [<a href="https://github.com/micz/ThunderAI/issues/215">#215</a>].</li>
<li>...</li>
</ul>

View file

@ -262,7 +262,7 @@ async function sendPrompt(prompt_id, tabId){
document.getElementById('mzta_search_input').style.display = 'none';
document.getElementById('mzta_sending_prompt').style.display = 'block';
let response = await browser.runtime.sendMessage({command: "shortcut_do_prompt", tabId: tabId, promptId: prompt_id});
console.log(">>>>>>>>>>>>>>>>> response: " + JSON.stringify(response));
// console.log(">>>>>>>>>>>>>>>>> response: " + JSON.stringify(response));
if(response.ok == '1'){
window.close();
}