Merge pull request #121 from micz/2.1.0

merge 2.1.0
This commit is contained in:
Mic 2024-08-28 21:49:05 +02:00 committed by GitHub
commit 5a9f31e5a0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1093 additions and 210 deletions

View file

@ -3,7 +3,16 @@
<h2>Version 2.1.0 - ??/??/2024</h2>
<ul>
<li>Improved error messages for connection errors.</li>
<li><i>[Ollama API]</i> It's now possible to use a local LMM using <a href="https://ollama.com/" target="_blank">Ollama</a> [<a href="https://github.com/micz/ThunderAI/issues/79">#79</a>].</li>
<li><i>[ChatGPT API][Ollama API]</i> Added a "Stop" button to stop the current elaboration [<a href="https://github.com/micz/ThunderAI/issues/113">#113</a>].
<li><i>[ChatGPT API]</i> If the configuration is missing the model or the API key, an error message is now provided, and there is no more fallback to the ChatGPT web interface [<a href="https://github.com/micz/ThunderAI/issues/111">#111</a>].</li>
<li><i>[ChatGPT API][Ollama API]</i> Improved chat colors scheme for darkmode.</li>
<li><i>[ChatGPT API][Ollama API]</i> Added a status message to give feedback to the user about the current operation [<a href="https://github.com/micz/ThunderAI/issues/119">#119</a>].</li>
<li>Improved the internal messaging system between the background script and the chat windows [<a href="https://github.com/micz/ThunderAI/issues/117">#117</a>].</li>
<li>...</li>
<h2>Version 2.0.7 - 21/08/2024</h2>
<ul>
<li><i>[ChatGPT Web]</i> Fixed an issue with automatically sending the prompt.</li>

View file

@ -1,6 +1,6 @@
# ![ThunderAI icon](images/icon-32px.png "ThunderAI") ThunderAI
ThunderAI is a Thunderbird Addon that uses the capabilities of ChatGPT to enhance email management.
ThunderAI is a Thunderbird Addon that uses the capabilities of ChatGPT or Ollama to enhance email management.
It enables users to analyse, write, correct, and optimize their emails, facilitating more effective and professional communication.
@ -11,10 +11,20 @@ Find out how [here](https://micz.it/thunderbird-addon-thunderai/custom-prompts/)
> [!TIP]
> There is no need for an API key and is possibile to use this extension even with a free account!
> **Using ChatGPT**
>
> There is no need for an API key and is possibile to use this extension even with a free account, using the web interface!
>
> Starting from version 2.0.0, if you want to connect with the OpenAI API integration, now you can use an API Key!
<br>
> [!TIP]
> **Using Ollama**
>
> From version 2.1.0 is possible to use a local Ollama server!
>
> Just remember to add `OLLAMA_ORIGINS = moz-extension://*` to the Ollama server environment variables.
<br>

View file

@ -1,6 +1,6 @@
{
"extensionDescription": {
"message": "Verwenden Sie ChatGPT, um Ihre E-Mails zu verbessern!",
"message": "Verwende ChatGPT oder Ollama, um deine E-Mails zu verbessern!",
"description": "Beschreibung der Erweiterung."
},
"menu_title": {
@ -284,7 +284,7 @@
"description": ""
},
"prefs_OptionText_default_chatgpt_lang": {
"message": "Standardsprache für ChatGPT-Antworten",
"message": "Standardsprache für die Antworten",
"description": ""
},
"prefs_OptionText_reply_all": {
@ -320,11 +320,11 @@
"description": ""
},
"prefsInfoDesc_2": {
"message": "Um die ChatGPT-API zu verwenden, benötigen Sie einen OpenAI ChatGPT-API-Schlüssel und müssen ein Modell auswählen. Falls Sie dies nicht tun, wird ThunderAI auf die Verwendung der ChatGPT-Webschnittstelle zurückgreifen.",
"message": "Um die ChatGPT-API zu verwenden, benötigen Sie einen OpenAI ChatGPT-API-Schlüssel und müssen ein Modell auswählen.",
"description": ""
},
"prefsInfoDesc_3": {
"message": " ",
"message": "Um die Integration mit Ollama zu nutzen, müssen Sie einen lokalen Ollama-Server einrichten. Sobald der Server läuft, geben Sie dessen Adresse in das dafür vorgesehene Feld in der Anwendung ein. Um eine ordnungsgemäße Kommunikation zwischen der App und dem Ollama-Server sicherzustellen, denken Sie daran, OLLAMA_ORIGINS=moz-extension://* festzulegen.",
"description": ""
},
"prefsDonation_1": {
@ -399,7 +399,7 @@
"message": "ChatGPT API-Schlüssel",
"description": ""
},
"ChatGPT_Models": {
"ChatGPT_Models": {
"message": "ChatGPT-Modelle",
"description": ""
},
@ -411,7 +411,7 @@
"message": "Fehler beim Abrufen der ChatGPT-Modelle",
"description": ""
},
"ChatGPT_Models_Loading": {
"Loading": {
"message": "Laden...",
"description": ""
},
@ -431,8 +431,72 @@
"message": "Modell wird verwendet",
"description": ""
},
"chagtp_api_connecting": {
"message": "Versucht, eine Verbindung zu OpenAI ChatGPT mit dem angegebenen API-Schlüssel herzustellen...",
"chagpt_api_connecting": {
"message": "Versuch, eine Verbindung zu OpenAI ChatGPT mit dem bereitgestellten API-Schlüssel herzustellen",
"description": ""
},
"Debug": {
"message": "Debuggen",
"description": ""
},
"prefs_OptionText_do_debug_info": {
"message": "Aktiviere das Debug-System",
"description": ""
},
"prefs_Connection_type_Ollama_API": {
"message": "Ollama API (Lokales LLM)",
"description": ""
},
"prefs_Ollama_Host": {
"message": "Host-Adresse",
"description": ""
},
"Ollama_Models": {
"message": "Ollama-Modelle",
"description": ""
},
"Ollama_Models_Fetch": {
"message": "Liste der Ollama-Modelle aktualisieren",
"description": ""
},
"Ollama_Models_Error_fetching": {
"message": "Fehler beim Abrufen der Ollama-Modelle",
"description": ""
},
"Ollama_Models_Error_NoModels": {
"message": "Keine Modelle gefunden",
"description": ""
},
"ollama_empty_host": {
"message": "Sie haben keine Host-Adresse für die Ollama API hinzugefügt. Bitte fügen Sie eine auf der Optionsseite ein.",
"description": ""
},
"ollama_empty_model": {
"message": "Sie haben kein Modell für die Ollama API ausgewählt. Bitte wählen Sie eines auf der Optionsseite aus.",
"description": ""
},
"ollama_api_connecting": {
"message": "Versuch, eine Verbindung zum Ollama-Lokalserver über den Host herzustellen",
"description": ""
},
"andModel": {
"message": "und Modell",
"description": ""
},
"error_connection_interrupted": {
"message": "Die Verbindung zum Server wurde unerwartet unterbrochen.",
"description": ""
},
"ollama_api_request_failed": {
"message": "Anfrage an die Ollama API fehlgeschlagen.",
"description": ""
},
"WaitingServerReponse": {
"message": "Warten auf die Serverantwort.",
"description": ""
},
"prefs_Ollama_Host_Info": {
"message": "Etwas wie",
"description": ""
}
}

View file

@ -1,6 +1,6 @@
{
"extensionDescription": {
"message": "Use ChatGPT to enhance you emails!",
"message": "Use ChatGPT or Ollama to enhance you emails!",
"description": "Description of the extension."
},
"menu_title": {
@ -284,7 +284,7 @@
"description": ""
},
"prefs_OptionText_default_chatgpt_lang": {
"message": "Default language for ChatGPT responses",
"message": "Default language for the responses",
"description": ""
},
"prefs_OptionText_reply_all": {
@ -320,11 +320,11 @@
"description": ""
},
"prefsInfoDesc_2": {
"message": "To use the ChatGPT API, you need an OpenAI ChatGPT API Key and must choose a model. If you fail to do so, ThunderAI will fall back to using the ChatGPT web interface.",
"message": "To use the ChatGPT API, you need an OpenAI ChatGPT API Key and must choose a model.",
"description": ""
},
"prefsInfoDesc_3": {
"message": " ",
"message": "To use the integration with Ollama, you need to set up a local Ollama server. Once the server is running, enter its address in the designated field within the application. To ensure proper communication between the app and the Ollama server remember to set OLLAMA_ORIGINS=moz-extension://*.",
"description": ""
},
"prefsDonation_1": {
@ -411,7 +411,7 @@
"message": "Error trying to fetch ChatGPT models",
"description": ""
},
"ChatGPT_Models_Loading": {
"Loading": {
"message": "Loading...",
"description": ""
},
@ -431,8 +431,72 @@
"message": "Using model",
"description": ""
},
"chagtp_api_connecting": {
"message": "Will attempt to connect to OpenAI ChatGPT using the API key provided...",
"chagpt_api_connecting": {
"message": "Attempting to connect to OpenAI ChatGPT using the API key provided",
"description": ""
},
"Debug": {
"message": "Debug",
"description": ""
},
"prefs_OptionText_do_debug_info": {
"message": "Activate the debug system",
"description": ""
},
"prefs_Connection_type_Ollama_API": {
"message": "Ollama API (Local LLM)",
"description": ""
},
"prefs_Ollama_Host": {
"message": "Host Address",
"description": ""
},
"Ollama_Models": {
"message": "Ollama Models",
"description": ""
},
"Ollama_Models_Fetch": {
"message": "Update Ollama Models list",
"description": ""
},
"Ollama_Models_Error_fetching": {
"message": "Error trying to fetch Ollama models",
"description": ""
},
"Ollama_Models_Error_NoModels": {
"message": "No models found",
"description": ""
},
"ollama_empty_host": {
"message": "You've not added an Host Address for the Ollama API. Please insert one in the options page.",
"description": ""
},
"ollama_empty_model": {
"message": "You've not choosen a model for the Ollama API. Please choose one in the options page.",
"description": ""
},
"ollama_api_connecting": {
"message": "Attempting to connect to the Ollama Local Server using the host",
"description": ""
},
"andModel": {
"message": "and model",
"description": ""
},
"error_connection_interrupted": {
"message": "The connection to the server was unexpectedly interrupted",
"description": ""
},
"ollama_api_request_failed": {
"message": "Ollama API request failed",
"description": ""
},
"WaitingServerReponse": {
"message": "Waiting for the server response",
"description": ""
},
"prefs_Ollama_Host_Info": {
"message": "Something like",
"description": ""
}
}

View file

@ -1,6 +1,6 @@
{
"extensionDescription": {
"message": "Utilisez ChatGPT pour améliorer vos emails !",
"message": "Utilise ChatGPT ou Ollama pour améliorer tes e-mails !",
"description": "Description de l'extension."
},
"menu_title": {
@ -284,7 +284,7 @@
"description": ""
},
"prefs_OptionText_default_chatgpt_lang": {
"message": "Langue par défaut pour les réponses de ChatGPT",
"message": "Langue par défaut pour les réponses",
"description": ""
},
"prefs_OptionText_reply_all": {
@ -320,11 +320,11 @@
"description": ""
},
"prefsInfoDesc_2": {
"message": "Pour utiliser l'API ChatGPT, vous avez besoin d'une clé API OpenAI ChatGPT et devez choisir un modèle. Si vous ne le faites pas, ThunderAI utilisera l'interface web de ChatGPT par défaut.",
"message": "Pour utiliser l'API ChatGPT, vous avez besoin d'une clé API OpenAI ChatGPT et devez choisir un modèle.",
"description": ""
},
"prefsInfoDesc_3": {
"message": " ",
"message": "Pour utiliser l'intégration avec Ollama, vous devez configurer un serveur Ollama local. Une fois que le serveur est en marche, entrez son adresse dans le champ prévu à cet effet dans l'application. Pour garantir une communication adéquate entre l'application et le serveur Ollama, n'oubliez pas de définir OLLAMA_ORIGINS=moz-extension://*.",
"description": ""
},
"prefsDonation_1": {
@ -399,7 +399,7 @@
"message": "Clé API ChatGPT",
"description": ""
},
"ChatGPT_Models": {
"ChatGPT_Models": {
"message": "Modèles ChatGPT",
"description": ""
},
@ -411,7 +411,7 @@
"message": "Erreur lors de la récupération des modèles ChatGPT",
"description": ""
},
"ChatGPT_Models_Loading": {
"Loading": {
"message": "Chargement...",
"description": ""
},
@ -431,8 +431,72 @@
"message": "Utilisation du modèle",
"description": ""
},
"chagtp_api_connecting": {
"message": "Tentative de connexion à OpenAI ChatGPT en utilisant la clé API fournie...",
"chagpt_api_connecting": {
"message": "Tentative de connexion à OpenAI ChatGPT en utilisant la clé API fournie",
"description": ""
},
"Debug": {
"message": "Debug",
"description": ""
},
"prefs_OptionText_do_debug_info": {
"message": "Activer le système de débogage",
"description": ""
},
"prefs_Connection_type_Ollama_API": {
"message": "API Ollama (LLM local)",
"description": ""
},
"prefs_Ollama_Host": {
"message": "Adresse de l'hôte",
"description": ""
},
"Ollama_Models": {
"message": "Modèles Ollama",
"description": ""
},
"Ollama_Models_Fetch": {
"message": "Mettre à jour la liste des modèles Ollama",
"description": ""
},
"Ollama_Models_Error_fetching": {
"message": "Erreur lors de la tentative de récupération des modèles Ollama",
"description": ""
},
"Ollama_Models_Error_NoModels": {
"message": "Aucun modèle trouvé",
"description": ""
},
"ollama_empty_host": {
"message": "Vous n'avez pas ajouté d'adresse d'hôte pour l'API Ollama. Veuillez en insérer une sur la page des options.",
"description": ""
},
"ollama_empty_model": {
"message": "Vous n'avez pas choisi de modèle pour l'API Ollama. Veuillez en choisir un sur la page des options.",
"description": ""
},
"ollama_api_connecting": {
"message": "Tentative de connexion au serveur local Ollama en utilisant l'hôte",
"description": ""
},
"andModel": {
"message": "et le modèle",
"description": ""
},
"error_connection_interrupted": {
"message": "La connexion au serveur a été interrompue de manière inattendue",
"description": ""
},
"ollama_api_request_failed": {
"message": "La demande d'API Ollama a échoué",
"description": ""
},
"WaitingServerReponse": {
"message": "En attente de la réponse du serveur",
"description": ""
},
"prefs_Ollama_Host_Info": {
"message": "Quelque chose comme",
"description": ""
}
}

View file

@ -1,6 +1,6 @@
{
"extensionDescription": {
"message": "Usa ChatGPT per migliorare le tue email!",
"message": "Usa ChatGPT o Ollama per migliorare le tue email!",
"description": "Description of the extension."
},
"menu_title": {
@ -284,7 +284,7 @@
"description": ""
},
"prefs_OptionText_default_chatgpt_lang": {
"message": "Lingua predefinita per le risposte di ChatGPT",
"message": "Lingua predefinita per le risposte",
"description": ""
},
"prefs_OptionText_reply_all": {
@ -320,11 +320,11 @@
"description": ""
},
"prefsInfoDesc_2": {
"message": "Per utilizzare l'API di ChatGPT, è necessaria una OpenAI ChatGPT API Key e devi scegliere un modello. Se non lo fai, ThunderAI utilizzerà l'interfaccia web di ChatGPT.",
"message": "Per utilizzare l'API di ChatGPT, è necessaria una OpenAI ChatGPT API Key e devi scegliere un modello.",
"description": ""
},
"prefsInfoDesc_3": {
"message": " ",
"message": "Per utilizzare l'integrazione con Ollama, è necessario configurare un server Ollama locale. Una volta che il server è attivo, inserisci il suo indirizzo nel campo designato all'interno dell'applicazione. Per garantire una corretta comunicazione tra l'app e il server Ollama, ricorda di impostare OLLAMA_ORIGINS=moz-extension://*.",
"description": ""
},
"prefsDonation_1": {
@ -384,8 +384,8 @@
"description": ""
},
"prefs_Connection_type": {
"message": "Tipo di connessione",
"description": ""
"message": "Tipo di connessione",
"description": ""
},
"prefs_Connection_type_ChatGTP_Web": {
"message": "Interfaccia Web ChatGTP",
@ -399,7 +399,7 @@
"message": "Chiave API ChatGPT",
"description": ""
},
"ChatGPT_Models": {
"ChatGPT_Models": {
"message": "Modelli ChatGPT",
"description": ""
},
@ -408,10 +408,10 @@
"description": ""
},
"ChatGPT_Models_Error_fetching": {
"message": "Errore durante il recupero dei modelli di ChatGPT",
"description": ""
"message": "Errore durante il recupero dei modelli di ChatGPT",
"description": ""
},
"ChatGPT_Models_Loading": {
"Loading": {
"message": "Caricamento in corso...",
"description": ""
},
@ -431,8 +431,72 @@
"message": "Modello utilizzato",
"description": ""
},
"chagtp_api_connecting": {
"message": "Tentativo di connessione a OpenAI ChatGPT utilizzando la chiave API fornita...",
"chagpt_api_connecting": {
"message": "Tentativo di connessione a OpenAI ChatGPT utilizzando la chiave API fornita",
"description": ""
},
"Debug": {
"message": "Debug",
"description": ""
},
"prefs_OptionText_do_debug_info": {
"message": "Attiva il sistema di debug",
"description": ""
},
"prefs_Connection_type_Ollama_API": {
"message": "API Ollama (LLM Locale)",
"description": ""
},
"prefs_Ollama_Host": {
"message": "Indirizzo dell'host",
"description": ""
},
"Ollama_Models": {
"message": "Modelli Ollama",
"description": ""
},
"Ollama_Models_Fetch": {
"message": "Aggiorna la lista dei modelli Ollama",
"description": ""
},
"Ollama_Models_Error_fetching": {
"message": "Errore nel tentativo di recuperare i modelli Ollama",
"description": ""
},
"Ollama_Models_Error_NoModels": {
"message": "Nessun modello trovato",
"description": ""
},
"ollama_empty_host": {
"message": "Non hai aggiunto un indirizzo host per l'API Ollama. Per favore, inseriscine uno nella pagina delle opzioni.",
"description": ""
},
"ollama_empty_model": {
"message": "Non hai scelto un modello per l'API Ollama. Per favore, scegline uno nella pagina delle opzioni.",
"description": ""
},
"ollama_api_connecting": {
"message": "Tentativo di connettersi al server locale Ollama usando l'host",
"description": ""
},
"andModel": {
"message": "e il modello",
"description": ""
},
"error_connection_interrupted": {
"message": "La connessione al server è stata interrotta inaspettatamente",
"description": ""
},
"ollama_api_request_failed": {
"message": "La richiesta all'API Ollama è fallita",
"description": ""
},
"WaitingServerReponse": {
"message": "In attesa della risposta del server",
"description": ""
},
"prefs_Ollama_Host_Info": {
"message": "Qualcosa come",
"description": ""
}
}

View file

@ -20,11 +20,28 @@
* The original code has been released under the Apache License, Version 2.0.
*/
// Get the LLM to be used
const urlParams = new URLSearchParams(window.location.search);
const llm = urlParams.get('llm');
const call_id = urlParams.get('call_id');
//console.log(">>>>>>>>>>> llm: " + llm);
// The controller wires up all the components and workers together,
// managing the dependencies. A kind of "DI" class.
const worker = new Worker('model-worker.js', { type: 'module' });
let worker = null;
switch (llm) {
case "chatgpt_api":
browser.runtime.sendMessage({command: "openai_api_ready_" + call_id, window_id: (await browser.windows.getCurrent()).id});
worker = new Worker('model-worker-openai.js', { type: 'module' });
break;
case "ollama_api": {
browser.runtime.sendMessage({command: "ollama_api_ready_" + call_id, window_id: (await browser.windows.getCurrent()).id});
worker = new Worker('model-worker-ollama.js', { type: 'module' });
break;
}
}
const messagesArea = document.querySelector('messages-area');
messagesArea.init(worker);
@ -54,15 +71,31 @@ let promptData = null;
// }
// ============================== TESTING - END
const params = new URLSearchParams(window.location.search);
let prefs_api = await browser.storage.sync.get({chatgpt_api_key: '', chatgpt_model: ''});
// const openaiApiKey = params.get('openapi-key');
//console.log(">>>>>>>>>>> chatgpt_api_key: " + prefs_api_key.chatgpt_api_key);
worker.postMessage({ type: 'init', chatgpt_api_key: prefs_api.chatgpt_api_key, chatgpt_model: prefs_api.chatgpt_model});
messagesArea.appendUserMessage(browser.i18n.getMessage("chagtp_api_connecting"), "info");
switch (llm) {
case "chatgpt_api":
let prefs_api = await browser.storage.sync.get({chatgpt_api_key: '', chatgpt_model: ''});
//console.log(">>>>>>>>>>> chatgpt_api_key: " + prefs_api_key.chatgpt_api_key);
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});
messagesArea.appendUserMessage(browser.i18n.getMessage("chagpt_api_connecting") + " " +browser.i18n.getMessage("AndModel") + " " + prefs_api.chatgpt_model + " ...", "info");
break;
case "ollama_api": {
let prefs_api = await browser.storage.sync.get({ollama_host: '', ollama_model: ''});
let i18nStrings = {};
i18nStrings["ollama_api_request_failed"] = browser.i18n.getMessage('ollama_api_request_failed');
i18nStrings["error_connection_interrupted"] = browser.i18n.getMessage('error_connection_interrupted');
//console.log(">>>>>>>>>>> ollama_host: " + prefs_api_key.ollama_host);
messageInput.setModel(prefs_api.ollama_model);
messagesArea.setLLMName("Ollama Local");
worker.postMessage({ type: 'init', ollama_host: prefs_api.ollama_host, ollama_model: prefs_api.ollama_model, i18nStrings: i18nStrings});
messagesArea.appendUserMessage(browser.i18n.getMessage("ollama_api_connecting") + " " + prefs_api.ollama_host + " " +browser.i18n.getMessage("AndModel") + " " + prefs_api.ollama_model + " ...", "info");
break;
}
}
// Event listeners for worker messages
// TODO I'm sure there's a better way to do this
worker.onmessage = function(event) {
const { type, payload } = event.data;
switch (type) {
@ -71,6 +104,7 @@ worker.onmessage = function(event) {
break;
case 'newToken':
messagesArea.handleNewToken(payload.token);
messageInput.setStatusMessage('Receiving data...');
break;
case 'tokensDone':
messagesArea.handleTokensDone(promptData);
@ -78,6 +112,7 @@ worker.onmessage = function(event) {
break;
case 'error':
messagesArea.appendBotMessage(payload,'error');
messageInput.enableInput();
break;
default:
console.error('[ThunderAI] Unknown event type from API worker:', type);
@ -87,16 +122,22 @@ worker.onmessage = function(event) {
// handling commands from the backgound page
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
//console.log(">>>>>>>>>>>>> controller.js onMessage: " + JSON.stringify(message));
if (message.command === "chatgpt_send") {
//send the received prompt to the chatgpt api
if(message.do_custom_text=="1") {
let userInput = prompt(browser.i18n.getMessage("chatgpt_win_custom_text"));
if(userInput !== null) {
message.prompt += " " + userInput;
switch (message.command) {
case "api_send":
//send the received prompt to the llm api
if(message.do_custom_text=="1") {
let userInput = prompt(browser.i18n.getMessage("chatgpt_win_custom_text"));
if(userInput !== null) {
message.prompt += " " + userInput;
}
}
}
promptData = message;
messageInput._setMessageInputValue(message.prompt);
messageInput._handleNewChatMessage();
promptData = message;
messageInput._setMessageInputValue(message.prompt);
messageInput._handleNewChatMessage();
break;
case "api_error":
messagesArea.appendBotMessage(message.error,'error');
messageInput.enableInput();
break;
}
});

View file

@ -1,3 +1,5 @@
<!DOCTYPE html>
<html>
<head>
<!-- Other meta tags and stylesheets -->
<link rel="stylesheet" type="text/css" href="styles.css">
@ -7,7 +9,7 @@
<!-- Use the custom tags directly -->
<div id="appContainer">
<messages-area></messages-area>
<message-input></message-input>
<message-input style="position:relative;"></message-input>
</div>
<div id="content">
</div>
@ -18,3 +20,4 @@
<script src="messagesArea.js" type="module" defer></script>
<script src="controller.js" type="module" defer></script>
</body>
</html>

View file

@ -50,6 +50,22 @@ messagesInputStyle .textContent = `
cursor: pointer;
border-radius: 10px;
}
#stopButton {
width: 44px;
height: 36px;
cursor: pointer;
border-radius: 10px;
}
#statusLogger{
font-size: 0.8rem;
position: absolute;
bottom: 1.5em;
right: 5em;
border: 1px solid lightgrey;
border-radius: 5px;
padding: 5px;
background: #212121;
}
@media (prefers-color-scheme: dark) {
#messageInputField {
background-color: #303030;
@ -87,7 +103,38 @@ sendIcon.appendChild(sendPath);
sendButton.appendChild(sendIcon);
messageInputTemplate.content.appendChild(sendButton);
const stopButton = document.createElement('button');
stopButton.id = 'stopButton';
stopButton.style.display = 'none';
const stopIcon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
stopIcon.setAttribute('width', '24');
stopIcon.setAttribute('height', '24');
stopIcon.setAttribute('viewBox', '0 0 24 24');
stopIcon.setAttribute('fill', 'none');
stopIcon.classList.add('text-white', 'dark:text-black');
const stopRect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
stopRect.setAttribute('x', '6');
stopRect.setAttribute('y', '6');
stopRect.setAttribute('width', '12');
stopRect.setAttribute('height', '12');
stopRect.setAttribute('fill', 'currentColor');
stopIcon.appendChild(stopRect);
stopButton.appendChild(stopIcon);
messageInputTemplate.content.appendChild(stopButton);
const statusLogger = document.createElement('div');
statusLogger.id = 'statusLogger';
statusLogger.textContent = '';
statusLogger.style.display = 'none';
messageInputTemplate.content.appendChild(statusLogger);
class MessageInput extends HTMLElement {
model = '';
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
@ -95,9 +142,12 @@ class MessageInput extends HTMLElement {
this._messageInputField = shadowRoot.querySelector('#messageInputField');
this._sendButton = shadowRoot.querySelector('#sendButton');
this._stopButton = shadowRoot.querySelector('#stopButton');
this._statusLogger = shadowRoot.querySelector('#statusLogger');
this._messageInputField.addEventListener('keydown', this._handleKeyDown.bind(this));
this._sendButton.addEventListener('click', this._handleClick.bind(this));
this._stopButton.addEventListener('click', this._handleStopClick.bind(this));
}
connectedCallback() {
@ -107,14 +157,18 @@ class MessageInput extends HTMLElement {
async init(worker) {
this.worker = worker;
let prefs_api = await browser.storage.sync.get({chatgpt_model: ''});
this._sendButton.title = await browser.i18n.getMessage("chagtp_api_send_button") + ": " + prefs_api.chatgpt_model;
}
setMessagesArea(messagesAreaComponent) {
this.messagesAreaComponent = messagesAreaComponent;
}
setModel(model){
this.model = model;
this._sendButton.title = browser.i18n.getMessage("chagtp_api_send_button") + ": " + this.model;
this._stopButton.title = browser.i18n.getMessage("chagtp_api_send_button") + ": " + this.model;
}
handleMessageSent() {
// console.log("[ThunderAI] handleMessageSent");
this._messageInputField.value = '';
@ -123,8 +177,26 @@ class MessageInput extends HTMLElement {
enableInput() {
// console.log("[ThunderAI] enableInput");
this._messageInputField.value = '';
this._sendButton.removeAttribute('disabled');
this._messageInputField.removeAttribute('disabled');
this._sendButton.removeAttribute('disabled');
this._sendButton.style.display = 'block';
this._stopButton.setAttribute('disabled', 'disabled');
this._stopButton.style.display = 'none';
this._stopButton.title = browser.i18n.getMessage("chagtp_api_send_button") + ": " + this.model;
this.hideStatusMessage();
this.setStatusMessage('');
}
setStatusMessage(message) {
this._statusLogger.textContent = message;
}
showStatusMessage() {
this._statusLogger.style.display = 'block';
}
hideStatusMessage() {
this._statusLogger.style.display = 'none';
}
_handleKeyDown(event) {
@ -137,6 +209,12 @@ class MessageInput extends HTMLElement {
this._handleNewChatMessage();
}
_handleStopClick() {
this.worker.postMessage({ type: 'stop' });
this._stopButton.setAttribute('disabled', 'disabled');
this._stopButton.title = 'Stopping...';
}
_handleNewChatMessage() {
//do nothing if input is empty
if ((!this._messageInputField.value)||(this._messageInputField.value.trim().length === 0)) {
@ -144,12 +222,17 @@ class MessageInput extends HTMLElement {
}
// prevent user from interacting while we're waiting
this._sendButton.setAttribute('disabled', 'disabled');
this._sendButton.style.display = 'none';
this._stopButton.removeAttribute('disabled');
this._stopButton.style.display = 'block';
this._messageInputField.setAttribute('disabled', 'disabled');
let messageContent = this._messageInputField.value;
this._messageInputField.value = '';
if (this.messagesAreaComponent) {
this.messagesAreaComponent.appendUserMessage(messageContent);
}
this.setStatusMessage(browser.i18n.getMessage('WaitingServerReponse') + '...');
this.showStatusMessage();
this.worker.postMessage({ type: 'chatMessage', message: messageContent });
}

View file

@ -100,6 +100,7 @@ messagesAreaTemplate.content.appendChild(messagesDiv);
class MessagesArea extends HTMLElement {
fullTextHTML = "";
llmName = "LLM";
constructor() {
super();
@ -117,7 +118,7 @@ class MessagesArea extends HTMLElement {
if (isLastMessageFromUser) {
const header = document.createElement('h2');
header.textContent = "ChatGTP";
header.textContent = this.llmName;
this.messages.appendChild(header);
}
@ -130,6 +131,10 @@ class MessagesArea extends HTMLElement {
this.worker = worker;
}
setLLMName(llmName) {
this.llmName = llmName;
}
handleTokensDone(promptData = null) {
this.flushAccumulatingMessage();
this.addActionButtons(promptData);
@ -231,27 +236,25 @@ class MessagesArea extends HTMLElement {
//actionButton.textContent = browser.i18n.getMessage("chatgpt_win_get_answer");
const fullTextHTMLAtAssignment = this.fullTextHTML.trim().replace(/^"|"$/g, '').replace(/^<p>&quot;/, '<p>').replace(/&quot;<\/p>$/, '</p>'); // strip quotation marks
//console.log(">>>>>>>>>>>> fullTextHTMLAtAssignment: " + fullTextHTMLAtAssignment);
actionButton.addEventListener('click', () => {
actionButton.addEventListener('click', async () => {
switch(promptData.action) {
case "1": // do reply
// console.log("[ThunderAI] (do reply) fullTextHTMLAtAssignment: " + fullTextHTMLAtAssignment);
browser.runtime.sendMessage({command: "chatgpt_replyMessage", text: fullTextHTMLAtAssignment, tabId: promptData.tabId, mailMessageId: promptData.mailMessageId}).then(() => {
browser.runtime.sendMessage({command: "chatgpt_close"});
});
await browser.runtime.sendMessage({command: "chatgpt_replyMessage", text: fullTextHTMLAtAssignment, tabId: promptData.tabId, mailMessageId: promptData.mailMessageId});
browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id});
break;
case "2": // replace text
// console.log("[ThunderAI] (replace text) fullTextHTMLAtAssignment: " + fullTextHTMLAtAssignment);
browser.runtime.sendMessage({command: "chatgpt_replaceSelectedText", text: fullTextHTMLAtAssignment, tabId: promptData.tabId, mailMessageId: promptData.mailMessageId}).then(() => {
browser.runtime.sendMessage({command: "chatgpt_close"});
});
await browser.runtime.sendMessage({command: "chatgpt_replaceSelectedText", text: fullTextHTMLAtAssignment, tabId: promptData.tabId, mailMessageId: promptData.mailMessageId});
browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id});
break;
}
});
const closeButton = document.createElement('button');
closeButton.textContent = browser.i18n.getMessage("chatgpt_win_close");
closeButton.addEventListener('click', () => {
closeButton.addEventListener('click', async () => {
// console.log("[ThunderAI] (close) fullTextHTMLAtAssignment: " + fullTextHTMLAtAssignment);
browser.runtime.sendMessage({command: "chatgpt_close"}); // close window
browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id}); // close window
});
if(promptData.action != 0) { actionButtons.appendChild(actionButton); }
actionButtons.appendChild(closeButton);

View file

@ -0,0 +1,118 @@
/*
* 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 { Ollama } from '../js/api/ollama.js';
let ollama_host = null;
let ollama_model = '';
let ollama = null;
let stopStreaming = false;
let i18nStrings = null;
let conversationHistory = [];
let assistantResponseAccumulator = '';
self.onmessage = async function(event) {
switch (event.data.type) {
case 'init':
ollama_host = event.data.ollama_host;
ollama_model = event.data.ollama_model;
//console.log(">>>>>>>>>>> ollama_host: " + ollama_host);
ollama = new Ollama(ollama_host, ollama_model, true);
i18nStrings = event.data.i18nStrings;
break; // init
case 'chatMessage':
conversationHistory.push({ role: 'user', content: event.data.message });
//console.log(">>>>>>>>>>> conversationHistory: " + JSON.stringify(conversationHistory));
const response = await ollama.fetchResponse(conversationHistory); //4096);
postMessage({ type: 'messageSent' });
if (!response.ok) {
let error_message = '';
let errorDetail = '';
if(response.is_exception === true){
error_message = response.error;
}else{
const errorJSON = await response.json();
errorDetail = JSON.stringify(errorJSON);
error_message = errorJSON.error;
//console.log(">>>>>>>>>>>>> errorJSON.error.message: " + JSON.stringify(errorJSON.error.message));
}
postMessage({ type: 'error', payload: i18nStrings["ollama_api_request_failed"] + ": " + error_message });
throw new Error("[ThunderAI] Ollama API request failed: " + response.status + " " + response.statusText + ", Detail: " + errorDetail);
}
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
try {
while (true) {
if (stopStreaming) {
stopStreaming = false;
reader.cancel();
conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator });
assistantResponseAccumulator = '';
postMessage({ type: 'tokensDone' });
break;
}
const { done, value } = await reader.read();
if (done) {
conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator });
assistantResponseAccumulator = '';
postMessage({ type: 'tokensDone' });
break;
}
// lots of low-level Ollama response parsing stuff
const chunk = decoder.decode(value);
//console.log(">>>>>>>>>>>>> chunk: " + chunk);
const lines = chunk.split("\n");
const parsedLines = lines
.map((line) => line.replace(/^chunk: /, "").trim()) // Remove the "chunk: " prefix
.filter((line) => line !== "" && line !== "[DONE]") // Remove empty lines and "[DONE]"
.map((line) => JSON.parse(line)); // Parse the JSON string
for (const parsedLine of parsedLines) {
const { message } = parsedLine;
const { content } = message;
// Update the UI with the new content
if (content) {
assistantResponseAccumulator += content;
postMessage({ type: 'newToken', payload: { token: content } });
}
}
}
} catch (error) {
if (error instanceof TypeError && error.message.includes('Error in input stream')) {
console.error('[ThudenderAI] The connection to the server was unexpectedly interrupted:', error);
postMessage({ type: 'error', payload: i18nStrings['error_connection_interrupted'] + ": " + error.message });
} else {
console.error('[ThudenderAI] Ollama API request failed:', error);
postMessage({ type: 'error', payload: i18nStrings["ollama_api_request_failed"] + ": " + error.message });
}
}
break; //chatMessage
case 'stop':
stopStreaming = true;
break; //stop
}
};

View file

@ -42,6 +42,7 @@ import { OpenAI } from '../js/api/openai.js';
let chatgpt_api_key = null;
let chatgpt_model = '';
let openai = null;
let stopStreaming = false;
let conversationHistory = [];
let assistantResponseAccumulator = '';
@ -89,10 +90,17 @@ self.onmessage = async function(event) {
postMessage({ type: 'messageSent' });
if (!response.ok) {
const errorJSON = await response.json();
const errorDetail = JSON.stringify(errorJSON);
//console.log(">>>>>>>>>>>>> errorJSON.error.message: " + JSON.stringify(errorJSON.error.message));
postMessage({ type: 'error', payload: errorJSON.error.message });
let error_message = '';
let errorDetail = '';
if(response.is_exception === true){
error_message = response.error;
}else{
const errorJSON = await response.json();
errorDetail = JSON.stringify(errorJSON);
error_message = errorJSON.error.message;
//console.log(">>>>>>>>>>>>> errorJSON.error.message: " + JSON.stringify(errorJSON.error.message));
}
postMessage({ type: 'error', payload: error_message });
throw new Error("[ThunderAI] OpenAI API request failed: " + response.status + " " + response.statusText + ", Detail: " + errorDetail);
}
@ -100,6 +108,14 @@ self.onmessage = async function(event) {
const decoder = new TextDecoder("utf-8");
while (true) {
if (stopStreaming) {
stopStreaming = false;
reader.cancel();
conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator });
assistantResponseAccumulator = '';
postMessage({ type: 'tokensDone' });
break;
}
const { done, value } = await reader.read();
if (done) {
conversationHistory.push({ role: 'assistant', content: assistantResponseAccumulator });
@ -126,5 +142,7 @@ self.onmessage = async function(event) {
}
}
}
} else if (event.data.type === 'stop') {
stopStreaming = true;
}
};

View file

@ -43,7 +43,7 @@ p {
/* Dark mode styles */
@media (prefers-color-scheme: dark) {
body {
background-color: #000000;
color: #ffffff;
background-color: rgb(33, 33, 33);
color: rgb(236, 236, 236);
}
}

111
js/api/ollama.js Normal file
View file

@ -0,0 +1,111 @@
/*
* 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 Ollama {
host = '';
model = '';
stream = false;
constructor(host, model, stream = false) {
this.host = host.trim().replace(/\/+$/, "");
this.model = model;
this.stream = stream;
}
fetchModels = async () => {
const response = await fetch(this.host + "/api/tags", {
method: "GET",
headers: {
"Content-Type": "application/json"
},
});
if (!response.ok) {
const errorDetail = await response.text();
let err_msg = "[ThunderAI] Ollama 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();
output.response = output_response;
//console.log(">>>>>>>>>> output_response: " + JSON.stringify(output_response));
return output;
}
fetchResponse = async (messages) => {
try {
//console.log(">>>>>>>>>> messages: " +JSON.stringify(messages));
const response = await fetch(this.host + "/api/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
model: this.model,
messages: messages,
stream: this.stream,
}),
});
return response;
}catch (error) {
console.error("[ThunderAI] Ollama API request failed: " + error);
let output = {};
output.is_exception = true;
output.ok = false;
output.error = "Ollama API request failed: " + error;
return output;
}
}
// fetchResponse = async (messages) => {
// try {
// let sending = messages.join(' ');
// console.log(">>>>>>>>>> sending: " + sending);
// const response = await fetch(this.host + "/api/generate", {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// },
// body: JSON.stringify({
// model: this.model,
// prompt: sending,
// stream: this.stream,
// }),
// });
// return response;
// }catch (error) {
// console.error("[ThunderAI] Ollama API request failed: " + error);
// let output = {};
// output.is_exception = true;
// output.ok = false;
// output.error = "Ollama API request failed: " + error;
// return output;
// }
// }
}

View file

@ -60,21 +60,29 @@ export class OpenAI {
}
fetchResponse = async (messages, maxTokens = 0) => {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer "+ this.apiKey
},
body: JSON.stringify({
model: this.model,
messages: messages,
stream: this.stream,
...(maxTokens > 0 ? { 'max_tokens': parseInt(maxTokens) } : {})
}),
});
return response;
try {
const response = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer "+ this.apiKey
},
body: JSON.stringify({
model: this.model,
messages: messages,
stream: this.stream,
...(maxTokens > 0 ? { 'max_tokens': parseInt(maxTokens) } : {})
}),
});
return response;
}catch (error) {
console.error("[ThunderAI] OpenAI API request failed: " + error);
let output = {};
output.is_exception = true;
output.ok = false;
output.error = "OpenAI API request failed: " + error;
return output;
}
}
async countTokensUsingAPI(model, text) {

View file

@ -164,7 +164,7 @@ function addCustomDiv(prompt_action,tabId,mailMessageId) {
case "0": // close window
btn_ok.textContent = browser.i18n.getMessage("chatgpt_win_close");
btn_ok.onclick = async function() {
browser.runtime.sendMessage({command: "chatgpt_close"});
browser.runtime.sendMessage({command: "chatgpt_close", window_id: mztaWinId});
};
break;
case "1": // do reply
@ -172,9 +172,8 @@ function addCustomDiv(prompt_action,tabId,mailMessageId) {
btn_ok.textContent = browser.i18n.getMessage("chatgpt_win_get_answer");
btn_ok.onclick = async function() {
const response = getSelectedHtml();
browser.runtime.sendMessage({command: "chatgpt_replyMessage", text: response, tabId: tabId, mailMessageId: mailMessageId}).then(() => {
browser.runtime.sendMessage({command: "chatgpt_close"});
});
await browser.runtime.sendMessage({command: "chatgpt_replyMessage", text: response, tabId: tabId, mailMessageId: mailMessageId});
browser.runtime.sendMessage({command: "chatgpt_close"});
};
break;
case "2": // replace text
@ -183,9 +182,8 @@ function addCustomDiv(prompt_action,tabId,mailMessageId) {
btn_ok.onclick = async function() {
const response = getSelectedHtml();
//console.log('replace text: '+tabId)
browser.runtime.sendMessage({command: "chatgpt_replaceSelectedText", text: response, tabId: tabId, mailMessageId: mailMessageId}).then(() => {
browser.runtime.sendMessage({command: "chatgpt_close"});
});
await browser.runtime.sendMessage({command: "chatgpt_replaceSelectedText", text: response, tabId: tabId, mailMessageId: mailMessageId});
browser.runtime.sendMessage({command: "chatgpt_close"});
};
break;
}

45
js/mzta-logger.js Normal file
View file

@ -0,0 +1,45 @@
/*
* 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 taLogger {
do_debug = false;
prefix = "";
constructor(prefix, do_debug) {
this.do_debug = do_debug;
this.prefix = "[ThunderAI Logger | " + prefix + "] ";
}
changeDebug(do_debug) {
this.do_debug = do_debug;
}
log(msg, do_debug = -1) {
if(do_debug !== -1) this.do_debug = do_debug;
if(this.do_debug === true) console.log(this.prefix + msg);
}
error(msg) {
console.error(this.prefix + msg);
}
warn(msg) {
console.warn(this.prefix + msg);
}
};

View file

@ -123,6 +123,16 @@ function compareThunderbirdVersions(v1, v2) {
return 0;
}
export function generateCallID(length) {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
// The following methods are a modified version derived from https://github.com/ali-raheem/Aify/blob/13ff87583bc520fb80f555ab90a90c5c9df797a7/plugin/content_scripts/compose.js

View file

@ -2,7 +2,7 @@
"manifest_version": 2,
"name": "ThunderAI",
"description": "__MSG_extensionDescription__",
"version": "2.0.7",
"version": "2.1.0pre2",
"author": "Mic (m@micz.it)",
"homepage_url": "https://micz.it/thunderbird-addon-thunderai/",
"browser_specific_settings": {
@ -31,7 +31,6 @@
"activeTab",
"accountsRead",
"downloads",
"notifications",
"https://*.chatgpt.com/*"
],
"background": {

View file

@ -19,12 +19,15 @@
import { mzta_script } from './js/mzta-chatgpt.js';
import { prefs_default } from './options/mzta-options-default.js';
import { mzta_Menus } from './js/mzta-menus.js';
import { getCurrentIdentity, getOriginalBody, replaceBody, setBody, i18nConditionalGet, isThunderbird128OrGreater } from './js/mzta-utils.js';
import { taLogger } from './js/mzta-logger.js';
import { getCurrentIdentity, getOriginalBody, replaceBody, setBody, i18nConditionalGet, generateCallID } from './js/mzta-utils.js';
var createdWindowID = null;
var original_html = '';
var modified_html = '';
let prefs_debug = await browser.storage.sync.get({do_debug: false});
let taLog = new taLogger("mzta-background",prefs_debug.do_debug);
browser.composeScripts.register({
js: [{file: "/js/mzta-compose-script.js"}]
});
@ -62,11 +65,11 @@ messenger.runtime.onMessage.addListener(async (message, sender, sendResponse) =>
// openChatGPT(message.prompt,message.action,message.tabId);
// return true;
case 'chatgpt_close':
browser.windows.remove(createdWindowID).then(() => {
// console.log("[ThunderAI] ChatGPT window closed successfully.");
browser.windows.remove(message.window_id).then(() => {
taLog.log("ChatGPT window closed successfully.");
return true;
}).catch((error) => {
console.error("[ThunderAI] Error closing ChatGPT window:", error);
taLog.error("Error closing ChatGPT window:", error);
return false;
});
break;
@ -133,7 +136,7 @@ messenger.runtime.onMessage.addListener(async (message, sender, sendResponse) =>
break;
case 'reload_menus':
await menus.reload();
//console.log("[ThunderAI] Reloaded menus");
taLog.log("[ThunderAI] Reloaded menus");
break;
default:
break;
@ -146,27 +149,16 @@ messenger.runtime.onMessage.addListener(async (message, sender, sendResponse) =>
async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_custom_text = 0) {
let prefs = await browser.storage.sync.get(prefs_default);
taLog.changeDebug(prefs.do_debug);
prefs = checkScreenDimensions(prefs);
//console.log(">>>>>>>>>>>>>>>> prefs: " + JSON.stringify(prefs));
//console.log("[ThunderAI] Prompt length: " + promptText.length);
taLog.log("[ThunderAI] Prompt length: " + promptText.length);
if(promptText.length > 30000 ){
// Prompt too long for ChatGPT
browser.tabs.sendMessage(curr_tabId, { command: "promptTooLong" });
return;
}
if(prefs.connection_type === 'chatgpt_api'){
// check if the API is present, otherwise open the web interface
if (prefs.chatgpt_api_key == '') {
showNotification(browser.i18n.getMessage('error'),browser.i18n.getMessage('chatgpt_empty_apikey'));
prefs.connection_type = 'chatgpt_web';
}
if (prefs.chatgpt_model == '') {
showNotification(browser.i18n.getMessage('error'),browser.i18n.getMessage('chatgpt_empty_model'));
prefs.connection_type = 'chatgpt_web';
}
}
switch(prefs.connection_type){
case 'chatgpt_web':
// We are using the ChatGPT web interface
@ -177,87 +169,125 @@ async function openChatGPT(promptText, action, curr_tabId, prompt_name = '', do_
height: prefs.chatgpt_win_height
});
// console.log("[ThunderAI] ChatGPT web interface script started...");
createdWindowID = newWindow.id;
taLog.log("[ThunderAI] ChatGPT web interface script started...");
let createdTab = newWindow.tabs[0];
let newWindowId = newWindow.id;
// Wait for tab loaded.
await new Promise(resolve => {
const tabIsLoaded = tab => {
return tab.status == "complete" && tab.url != "about:blank";
};
const listener = (tabId, changeInfo, updatedTab) => {
if (tabIsLoaded(updatedTab)) {
browser.tabs.onUpdated.removeListener(listener);
resolve();
}
}
// Early exit if loaded already
if (tabIsLoaded(createdTab)) {
resolve();
} else {
browser.tabs.onUpdated.addListener(listener);
}
});
let pre_script = `let mztaStatusPageDesc="`+ browser.i18n.getMessage("prefs_status_page") +`";
let pre_script = `let mztaWinId = `+ newWindowId +`;
let mztaStatusPageDesc="`+ browser.i18n.getMessage("prefs_status_page") +`";
let mztaForceCompletionDesc="`+ browser.i18n.getMessage("chatgpt_force_completion") +`";
let mztaForceCompletionTitle="`+ browser.i18n.getMessage("chatgpt_force_completion_title") +`";
let mztaDoCustomText=`+ do_custom_text +`;
let mztaPromptName="[`+ i18nConditionalGet(prompt_name) +`]";
`;
browser.tabs.executeScript(createdTab.id, { code: pre_script + mzta_script, matchAboutBlank: false })
.then(async () => {
// console.log("[ThunderAI] ChatGPT web interface script injected successfully");
let done1 = false;
while(!done1){
try {
await browser.tabs.executeScript(createdTab.id, { code: pre_script + mzta_script, matchAboutBlank: false });
let mailMessage = await browser.messageDisplay.getDisplayedMessage(curr_tabId);
let mailMessageId = -1;
if(mailMessage) mailMessageId = mailMessage.id;
browser.tabs.sendMessage(createdTab.id, { command: "chatgpt_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId});
})
.catch(err => {
console.error("[ThunderAI] ChatGPT web interface error injecting the script: ", err);
});
taLog.log('[ChatGPT Web] Connection succeded!');
taLog.log("[ThunderAI] ChatGPT Web script injected successfully");
done1 = true;
} catch (error) {
taLog.warn('[ChatGPT Web] Error connecting to the window chat: ', error);
taLog.warn('[ChatGPT Web] Trying again...');
done1 = false;
}
await new Promise(resolve => setTimeout(resolve, 500));
}
break; // chatgpt_web
case 'chatgpt_api':
// We are using the ChatGPT API
let newWindow2 = await browser.windows.create({
url: browser.runtime.getURL('api_webchat/index.html'),
let rand_call_id = '_openai_' + generateCallID();
await browser.windows.create({
url: browser.runtime.getURL('api_webchat/index.html?llm='+prefs.connection_type+'&call_id='+rand_call_id),
type: "popup",
width: prefs.chatgpt_win_width,
height: prefs.chatgpt_win_height
});
createdWindowID = newWindow2.id;
let createdTab2 = newWindow2.tabs[0];
browser.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
if(await isThunderbird128OrGreater()){
// Wait for tab loaded.
await new Promise(resolve => {
const tabIsLoaded2 = tab => {
return tab.status == "complete" && tab.url != "about:blank";
};
const listener2 = (tabId, changeInfo, updatedTab) => {
if (tabIsLoaded2(updatedTab)) {
browser.tabs.onUpdated.removeListener(listener2);
resolve();
}
if (message.command === "openai_api_ready_"+rand_call_id) {
let newWindow2 = await browser.windows.get(message.window_id, {populate: true});
let createdTab2 = newWindow2.tabs[0];
let mailMessage = await browser.messageDisplay.getDisplayedMessage(curr_tabId);
let mailMessageId2 = -1;
if(mailMessage) mailMessageId2 = mailMessage.id;
// check if the config is present, or give a message error
if (prefs.chatgpt_api_key == '') {
browser.tabs.sendMessage(createdTab2.id, { command: "api_error", error: browser.i18n.getMessage('chatgpt_empty_apikey')});
return;
}
// Early exit if loaded already
if (tabIsLoaded2(createdTab2)) {
resolve();
} else {
browser.tabs.onUpdated.addListener(listener2);
if (prefs.chatgpt_model == '') {
browser.tabs.sendMessage(createdTab2.id, { command: "api_error", error: browser.i18n.getMessage('chatgpt_empty_model')});
return;
}
});
}
let mailMessage = await browser.messageDisplay.getDisplayedMessage(curr_tabId);
let mailMessageId = -1;
if(mailMessage) mailMessageId = mailMessage.id;
browser.tabs.sendMessage(createdTab2.id, { command: "chatgpt_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId, do_custom_text: do_custom_text});
browser.tabs.sendMessage(createdTab2.id, { command: "api_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId2, do_custom_text: do_custom_text});
taLog.log('[OpenAI ChatGPI] Connection succeded!');
browser.runtime.onMessage.removeListener(arguments.callee);
}
});
break; // chatgpt_api
case 'ollama_api':
// We are using the Ollama API
taLog.log("Ollama API window opening...");
let rand_call_id2 = '_ollama_' + generateCallID();
await browser.windows.create({
url: browser.runtime.getURL('api_webchat/index.html?llm='+prefs.connection_type+'&call_id='+rand_call_id2),
type: "popup",
width: prefs.chatgpt_win_width,
height: prefs.chatgpt_win_height
});
const listener3 = async (message, sender, sendResponse) => {
if (message.command === "ollama_api_ready_"+rand_call_id2) {
taLog.log("Ollama API window ready.");
taLog.log("message.window_id: " + message.window_id)
let newWindow3 = await browser.windows.get(message.window_id, {populate: true});
let createdTab3 = newWindow3.tabs[0];
taLog.log(">>>>>> createdTab3.id: " + createdTab3.id)
let mailMessage3 = await browser.messageDisplay.getDisplayedMessage(curr_tabId);
let mailMessageId3 = -1;
if(mailMessage3) mailMessageId3 = mailMessage3.id;
taLog.log(">>>>>> mailMessageId3: " + mailMessageId3)
// check if the config is present, or give a message error
if (prefs.ollama_host == '') {
await browser.tabs.sendMessage(createdTab3.id, { command: "api_error", error: browser.i18n.getMessage('ollama_empty_host')});
return;
}
if (prefs.ollama_model == '') {
await browser.tabs.sendMessage(createdTab3.id, { command: "api_error", error: browser.i18n.getMessage('ollama_empty_model')});
return;
}
await browser.tabs.sendMessage(createdTab3.id, { command: "api_send", prompt: promptText, action: action, tabId: curr_tabId, mailMessageId: mailMessageId3, do_custom_text: do_custom_text});
taLog.log('[Ollama API] Connection succeded!');
browser.runtime.onMessage.removeListener(listener3);
}
}
browser.runtime.onMessage.addListener(listener3);
break; // ollama_api
}
}
@ -271,15 +301,6 @@ function checkScreenDimensions(prefs){
return prefs;
}
function showNotification(title, message) {
browser.notifications.create({
"type": "basic",
"title": title,
"iconUrl": browser.extension.getURL("images/icon.png"),
"message": message
});
}
// Menus handling
const menus = new mzta_Menus(openChatGPT);
menus.loadMenus();

View file

@ -17,11 +17,14 @@
*/
export const prefs_default = {
do_debug: false,
chatgpt_win_height: 800,
chatgpt_win_width: 700,
chatpgt_use_gpt4: false,
default_chatgpt_lang: '',
connection_type: 'chatgpt_web', //Other values: 'chatgpt_api'
connection_type: 'chatgpt_web', //Other values: 'chatgpt_api', 'ollama_api'
chatgpt_api_key: '',
chatgpt_model: '',
ollama_host: '',
ollama_model: '',
}

View file

@ -102,7 +102,12 @@ tr.conntype_chatgpt_web, tr.conntype_chatgpt_web2{
}
*/
#model_fetch_loading{
#chatgpt_model_fetch_loading{
display: none;
font-style: italic;
}
#ollama_model_fetch_loading{
display: none;
font-style: italic;
}
@ -126,6 +131,10 @@ tr.conntype_chatgpt_web, tr.conntype_chatgpt_web2{
background-color: rgb(39, 11, 0);
}
tr.conntype_ollama_api, tr.conntype_ollama_api2{
background-color: rgb(7, 58, 0);
}
/* color to be used for a local LLM
.conntype_chatgpt_web,.conntype_local_llm{
background-color: rgb(0, 39, 0);

View file

@ -70,6 +70,7 @@
<select id="connection_type" name="connection_type" class="option-input">
<option value="chatgpt_web">__MSG_prefs_Connection_type_ChatGTP_Web__</option>
<option value="chatgpt_api">__MSG_prefs_Connection_type_ChatGTP_API__</option>
<option value="ollama_api">__MSG_prefs_Connection_type_Ollama_API__</option>
</select>
</label>
</td>
@ -104,13 +105,50 @@
</label>
</td>
<td>
<button id="btnUpdateChatGPTModels">__MSG_ChatGPT_Models_Fetch__</button> <span id="model_fetch_loading">__MSG_ChatGPT_Models_Loading__</span><br>
<button id="btnUpdateChatGPTModels">__MSG_ChatGPT_Models_Fetch__</button> <span id="chatgpt_model_fetch_loading">__MSG_Loading__</span><br>
<label>
<select id="chatgpt_model" name="chatgpt_model" class="option-input">
</select>
</label>
</td>
</tr>
<tr class="conntype_ollama_api">
<td><label>
<span>__MSG_prefs_Ollama_Host__</span>
<br><i style="font-size:smaller;">__MSG_prefs_Ollama_Host_Info__ http://127.0.0.1:11434</i>
</label></td>
<td>
<label>
<input type="text" id="ollama_host" name="ollama_host" class="option-input" style="width:28em" />
&nbsp;<span></span>
</label>
</td>
</tr>
<tr class="conntype_ollama_api">
<td>
<label>
<span>__MSG_Ollama_Models__</span>
</label>
</td>
<td>
<button id="btnUpdateOllamaModels">__MSG_Ollama_Models_Fetch__</button> <span id="ollama_model_fetch_loading">__MSG_Loading__</span><br>
<label>
<select id="ollama_model" name="ollama_model" class="option-input">
</select>
</label>
</td>
</tr>
<tr>
<td><label>
<span>__MSG_Debug__</span>
</label></td>
<td>
<label>
<input type="checkbox" id="do_debug" name="do_debug" class="option-input" />
&nbsp;<span>__MSG_prefs_OptionText_do_debug_info__</span>
</label>
</td>
</tr>
</table>
<div id="btn_custom_prompts">
<button id="btnManagePrompts">__MSG_prefs_OptionText_btnManagePrompts__</button>
@ -118,8 +156,8 @@
<div id="miczDescription">
<h1>__MSG_prefsInfoTitle__</h1>
<p><span class="conntype_chatgpt_web">__MSG_prefsInfoDesc_1__<br/></span>
<span class="conntype_chatgpt_api">__MSG_prefsInfoDesc_2__<br/>
__MSG_prefsInfoDesc_3__</span></p>
<span class="conntype_chatgpt_api">__MSG_prefsInfoDesc_2__<br/></span>
<span class="conntype_ollama_api">__MSG_prefsInfoDesc_3__</span></p>
</div>
<div id="miczTranslate">__MSG_TranslateText__ <a href="https://micz.it/thunderbird-addon-thunderai/translate/">__MSG_TranslateLink__</a></div>
<div id="miczDonation">__MSG_prefsDonation_1__<br/><a href="http://micz.it/thunderbird-addon-thunderai/donate/">__MSG_prefsDonation_2__</a></div>

View file

@ -18,7 +18,11 @@
import { prefs_default } from './mzta-options-default.js';
import { taLogger } from '../js/mzta-logger.js';
import { OpenAI } from '../js/api/openai.js';
import { Ollama } from '../js/api/ollama.js';
let taLog = new taLogger("mzta-options",true);
function saveOptions(e) {
e.preventDefault();
@ -49,7 +53,7 @@ function saveOptions(e) {
async function restoreOptions() {
function setCurrentChoice(result) {
document.querySelectorAll(".option-input").forEach(element => {
//console.log("[ThunderAI] Options restoring " + element.id + " = " + result[element.id]);
taLog.log("Options restoring " + element.id + " = " + result[element.id]);
switch (element.type) {
case 'checkbox':
element.checked = result[element.id] || false;
@ -88,10 +92,12 @@ async function restoreOptions() {
function showConnectionOptions() {
let chatgpt_web_display = 'table-row';
let chatgpt_api_display = 'none';
let ollama_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_chatgpt_web", (conntype_select.value === "chatgpt_web"));
parent.classList.toggle("conntype_ollama_api", (conntype_select.value === "ollama_api"));
if (conntype_select.value === "chatgpt_web") {
chatgpt_web_display = 'table-row';
}else{
@ -102,27 +108,35 @@ function showConnectionOptions() {
}else{
chatgpt_api_display = 'none';
}
if (conntype_select.value === "ollama_api") {
ollama_api_display = 'table-row';
}else{
ollama_api_display = 'none';
}
document.querySelectorAll(".conntype_chatgpt_web").forEach(element => {
element.style.display = chatgpt_web_display;
});
document.querySelectorAll(".conntype_chatgpt_api").forEach(element => {
element.style.display = chatgpt_api_display;
});
document.querySelectorAll(".conntype_ollama_api").forEach(element => {
element.style.display = ollama_api_display;
});
}
function warnAPIKeyEmpty() {
function warn_ChatGPT_APIKeyEmpty() {
let apiKeyInput = document.getElementById('chatgpt_api_key');
let btnFetchModels = document.getElementById('btnUpdateChatGPTModels');
let btnFetchChatGPTModels = document.getElementById('btnUpdateChatGPTModels');
let modelChatGPT = document.getElementById('chatgpt_model');
if(apiKeyInput.value === ''){
apiKeyInput.style.border = '2px solid red';
btnFetchModels.disabled = true;
btnFetchChatGPTModels.disabled = true;
modelChatGPT.disabled = true;
modelChatGPT.selectedIndex = -1;
modelChatGPT.style.border = 'none';
}else{
apiKeyInput.style.border = 'none';
btnFetchModels.disabled = false;
btnFetchChatGPTModels.disabled = false;
modelChatGPT.disabled = false;
if((modelChatGPT.selectedIndex === -1)||(modelChatGPT.value === '')){
modelChatGPT.style.border = '2px solid red';
@ -132,6 +146,28 @@ function warnAPIKeyEmpty() {
}
}
function warn_Ollama_HostEmpty() {
let hostInput = document.getElementById('ollama_host');
let btnFetchOllamaModels = document.getElementById('btnUpdateOllamaModels');
let modelOllama = document.getElementById('ollama_model');
if(hostInput.value === ''){
hostInput.style.border = '2px solid red';
btnFetchOllamaModels.disabled = true;
modelOllama.disabled = true;
modelOllama.selectedIndex = -1;
modelOllama.style.border = 'none';
}else{
hostInput.style.border = 'none';
btnFetchOllamaModels.disabled = false;
modelOllama.disabled = false;
if((modelOllama.selectedIndex === -1)||(modelOllama.value === '')){
modelOllama.style.border = '2px solid red';
}else{
modelOllama.style.border = 'none';
}
}
}
document.addEventListener('DOMContentLoaded', async () => {
await restoreOptions();
i18n.updateDocument();
@ -156,30 +192,33 @@ document.addEventListener('DOMContentLoaded', async () => {
let conntype_select = document.getElementById("connection_type");
conntype_select.addEventListener("change", showConnectionOptions);
conntype_select.addEventListener("change", warnAPIKeyEmpty);
document.getElementById("chatgpt_api_key").addEventListener("change", warnAPIKeyEmpty);
conntype_select.addEventListener("change", warn_ChatGPT_APIKeyEmpty);
conntype_select.addEventListener("change", warn_Ollama_HostEmpty);
document.getElementById("chatgpt_api_key").addEventListener("change", warn_ChatGPT_APIKeyEmpty);
document.getElementById("ollama_host").addEventListener("change", warn_Ollama_HostEmpty);
let prefs = await browser.storage.sync.get({chatgpt_api_key: '', chatgpt_model: ''});
let prefs = await browser.storage.sync.get({chatgpt_model: '', ollama_model: ''});
// OpenAI API ChatGPT model fetching
let select_chatgpt_model = document.getElementById('chatgpt_model');
const option = document.createElement('option');
option.value = prefs.chatgpt_model;
option.text = prefs.chatgpt_model;
select_chatgpt_model.appendChild(option);
select_chatgpt_model.addEventListener("change", warnAPIKeyEmpty);
const chatgpt_option = document.createElement('option');
chatgpt_option.value = prefs.chatgpt_model;
chatgpt_option.text = prefs.chatgpt_model;
select_chatgpt_model.appendChild(chatgpt_option);
select_chatgpt_model.addEventListener("change", warn_ChatGPT_APIKeyEmpty);
document.getElementById('btnUpdateChatGPTModels').addEventListener('click', async () => {
document.getElementById('model_fetch_loading').style.display = 'inline';
let openai = new OpenAI(document.getElementById("chatgpt_api_key").value, true);
document.getElementById('chatgpt_model_fetch_loading').style.display = 'inline';
let openai = new OpenAI(document.getElementById("chatgpt_api_key").value, '', true);
openai.fetchModels().then((data) => {
if(!data.ok){
let errorDetail = JSON.parse(data.error);
document.getElementById('model_fetch_loading').style.display = 'none';
document.getElementById('chatgpt_model_fetch_loading').style.display = 'none';
console.error("[ThunderAI] " + browser.i18n.getMessage("ChatGPT_Models_Error_fetching"));
alert(browser.i18n.getMessage("ChatGPT_Models_Error_fetching")+": " + errorDetail.error.message);
return;
}
//console.log(">>>>>>>>> ChatGPT models: " + JSON.stringify(data));
taLog.log("ChatGPT models: " + JSON.stringify(data));
data.response.forEach(model => {
if (!Array.from(select_chatgpt_model.options).some(option => option.value === model.id)) {
const option = document.createElement('option');
@ -188,9 +227,60 @@ document.addEventListener('DOMContentLoaded', async () => {
select_chatgpt_model.appendChild(option);
}
});
document.getElementById('model_fetch_loading').style.display = 'none';
document.getElementById('chatgpt_model_fetch_loading').style.display = 'none';
})
warnAPIKeyEmpty();
warn_ChatGPT_APIKeyEmpty();
});
// Ollama API Model fetching
let select_ollama_model = document.getElementById('ollama_model');
const ollama_option = document.createElement('option');
ollama_option.value = prefs.ollama_model;
ollama_option.text = prefs.ollama_model;
select_ollama_model.appendChild(ollama_option);
select_ollama_model.addEventListener("change", warn_Ollama_HostEmpty);
document.getElementById('btnUpdateOllamaModels').addEventListener('click', async () => {
document.getElementById('ollama_model_fetch_loading').style.display = 'inline';
let ollama = new Ollama(document.getElementById("ollama_host").value, true);
try {
let data = await ollama.fetchModels();
if(!data){
document.getElementById('ollama_model_fetch_loading').style.display = 'none';
console.error("[ThunderAI] " + browser.i18n.getMessage("Ollama_Models_Error_fetching"));
alert(browser.i18n.getMessage("Ollama_Models_Error_fetching"));
return;
}
if(!data.ok){
let errorDetail = JSON.parse(data.error);
document.getElementById('ollama_model_fetch_loading').style.display = 'none';
console.error("[ThunderAI] " + browser.i18n.getMessage("Ollama_Models_Error_fetching"));
alert(browser.i18n.getMessage("Ollama_Models_Error_fetching")+": " + errorDetail.error.message);
return;
}
if(data.response.models.length == 0){
document.getElementById('ollama_model_fetch_loading').style.display = 'none';
console.error("[ThunderAI] " + browser.i18n.getMessage("Ollama_Models_Error_fetching"));
alert(browser.i18n.getMessage("Ollama_Models_Error_fetching")+": " + browser.i18n.getMessage("Ollama_Models_Error_NoModels"));
return;
}
taLog.log("Ollama models: " + JSON.stringify(data));
data.response.models.forEach(model => {
if (!Array.from(select_ollama_model.options).some(option => option.value === model.model)) {
const option = document.createElement('option');
option.value = model.model;
option.text = model.name + " (" + model.model + ")";
select_ollama_model.appendChild(option);
}
});
document.getElementById('ollama_model_fetch_loading').style.display = 'none';
} catch (error) {
document.getElementById('ollama_model_fetch_loading').style.display = 'none';
taLog.error(browser.i18n.getMessage("Ollama_Models_Error_fetching"));
alert(browser.i18n.getMessage("Ollama_Models_Error_fetching")+": " + error.message);
}
warn_Ollama_HostEmpty();
});
}, { once: true });

View file

@ -7,6 +7,16 @@
<body>
<div id="miczBackPrefs"><a href="mzta-options.html">__MSG_backToOptionsText__</a></div>
<div id="miczRelNotes"><h1>ThunderAI Release Notes</h1>
<h2>Version 2.1.0 - ??/??/2024</h2>
<ul>
<li>Improved error messages for connection errors.</li>
<li><i>[Ollama API]</i> It's now possible to use a local LMM using <a href="https://ollama.com/" target="_blank">Ollama</a> [<a href="https://github.com/micz/ThunderAI/issues/79">#79</a>].</li>
<li><i>[ChatGPT API][Ollama API]</i> Added a "Stop" button to stop the current elaboration [<a href="https://github.com/micz/ThunderAI/issues/113">#113</a>].
<li><i>[ChatGPT API]</i> If the configuration is missing the model or the API key, an error message is now provided, and there is no more fallback to the ChatGPT web interface [<a href="https://github.com/micz/ThunderAI/issues/111">#111</a>].</li>
<li><i>[ChatGPT API][Ollama API]</i> Improved chat colors scheme for darkmode.</li>
<li><i>[ChatGPT API][Ollama API]</i> Added a status message to give feedback to the user about the current operation [<a href="https://github.com/micz/ThunderAI/issues/119">#119</a>].</li>
<li>Improved the internal messaging system between the background script and the chat windows [<a href="https://github.com/micz/ThunderAI/issues/117">#117</a>].</li>
<li>...</li>
<h2>Version 2.0.7 - 21/08/2024</h2>
<ul>
<li><i>[ChatGPT Web]</i> Fixed an issue with automatically sending the prompt.</li>