diff --git a/_locales/de/messages.json b/_locales/de/messages.json index e6fd3cfa..b614523f 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -432,7 +432,7 @@ "description": "" }, "chagpt_api_connecting": { - "message": "Versucht, eine Verbindung zu OpenAI ChatGPT mit dem angegebenen API-Schlüssel herzustellen...", + "message": "Versucht, eine Verbindung zu OpenAI ChatGPT mit dem angegebenen API-Schlüssel herzustellen", "description": "" } } \ No newline at end of file diff --git a/_locales/en/messages.json b/_locales/en/messages.json index c38b896b..5e3add3b 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -432,7 +432,7 @@ "description": "" }, "chagpt_api_connecting": { - "message": "Will attempt to connect to OpenAI ChatGPT using the API key provided...", + "message": "Will attempt to connect to OpenAI ChatGPT using the API key provided", "description": "" }, "Debug": { @@ -478,5 +478,9 @@ "ollama_api_connecting": { "message": "Will attempt to connect to the Ollama Local Server using the host", "description": "" + }, + "andModel": { + "message": "and model", + "description": "" } } \ No newline at end of file diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 7793311c..7d03a42b 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -432,7 +432,7 @@ "description": "" }, "chagpt_api_connecting": { - "message": "Tentative de connexion à OpenAI ChatGPT en utilisant la clé API fournie...", + "message": "Tentative de connexion à OpenAI ChatGPT en utilisant la clé API fournie", "description": "" } } \ No newline at end of file diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 02ab8679..596e5869 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -432,7 +432,7 @@ "description": "" }, "chagpt_api_connecting": { - "message": "Tentativo di connessione a OpenAI ChatGPT utilizzando la chiave API fornita...", + "message": "Tentativo di connessione a OpenAI ChatGPT utilizzando la chiave API fornita", "description": "" } } \ No newline at end of file diff --git a/api_webchat/controller.js b/api_webchat/controller.js index 1619fc65..e80919d6 100644 --- a/api_webchat/controller.js +++ b/api_webchat/controller.js @@ -68,26 +68,27 @@ let promptData = null; // } // ============================== TESTING - END -const params = new URLSearchParams(window.location.search); - 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"), "info"); + 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: ''}); //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}); - messagesArea.appendUserMessage(browser.i18n.getMessage("ollama_api_connecting") + " " + prefs_api.ollama_host + " ...", "info"); + 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 worker.onmessage = function(event) { const { type, payload } = event.data; diff --git a/api_webchat/messageInput.js b/api_webchat/messageInput.js index 51d79a4f..c9cbcce9 100644 --- a/api_webchat/messageInput.js +++ b/api_webchat/messageInput.js @@ -50,6 +50,12 @@ messagesInputStyle .textContent = ` cursor: pointer; border-radius: 10px; } + #stopButton { + width: 44px; + height: 36px; + cursor: pointer; + border-radius: 10px; + } @media (prefers-color-scheme: dark) { #messageInputField { background-color: #303030; @@ -87,7 +93,32 @@ 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); + class MessageInput extends HTMLElement { + + model = ''; + constructor() { super(); const shadowRoot = this.attachShadow({ mode: 'open' }); @@ -95,9 +126,11 @@ class MessageInput extends HTMLElement { this._messageInputField = shadowRoot.querySelector('#messageInputField'); this._sendButton = shadowRoot.querySelector('#sendButton'); + this._stopButton = shadowRoot.querySelector('#stopButton'); 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 +140,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 +160,12 @@ 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; } _handleKeyDown(event) { @@ -137,6 +178,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,6 +191,9 @@ 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 = ''; diff --git a/api_webchat/messagesArea.js b/api_webchat/messagesArea.js index a75e37c2..23707b2d 100644 --- a/api_webchat/messagesArea.js +++ b/api_webchat/messagesArea.js @@ -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); diff --git a/api_webchat/model-worker-ollama.js b/api_webchat/model-worker-ollama.js index 2c11612b..f8c0c0fb 100644 --- a/api_webchat/model-worker-ollama.js +++ b/api_webchat/model-worker-ollama.js @@ -25,66 +25,80 @@ import { Ollama } from '../js/api/ollama.js'; let ollama_host = null; let ollama_model = ''; let ollama = null; +let stopStreaming = false; let conversationHistory = []; let assistantResponseAccumulator = ''; self.onmessage = async function(event) { - if (event.data.type === '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); - } else if (event.data.type === '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' }); + 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); + 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)); + 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: "Ollama API request failed: " + error_message }); + throw new Error("[ThunderAI] Ollama API request failed: " + response.status + " " + response.statusText + ", Detail: " + errorDetail); } - postMessage({ type: 'error', payload: "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"); - - while (true) { - 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 } }); + const reader = response.body.getReader(); + const decoder = new TextDecoder("utf-8"); + + while (true) { + if (stopStreaming) { + stopStreaming = false; + 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 } }); + } } } - } - } + break; //chatMessage + case 'stop': + stopStreaming = true; + break; //stop + } }; diff --git a/api_webchat/model-worker-openai.js b/api_webchat/model-worker-openai.js index 5ebb3e4e..a8f041d5 100644 --- a/api_webchat/model-worker-openai.js +++ b/api_webchat/model-worker-openai.js @@ -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 = ''; @@ -107,6 +108,13 @@ self.onmessage = async function(event) { const decoder = new TextDecoder("utf-8"); while (true) { + if (stopStreaming) { + stopStreaming = false; + 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 }); @@ -133,5 +141,7 @@ self.onmessage = async function(event) { } } } + } else if (event.data.type === 'stop') { + stopStreaming = true; } };