/*
* ThunderAI [https://micz.it/thunderbird-addon-thunderai/]
* Copyright (C) 2024 - 2026 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
for correct HTML display
if (type === "info") {
messageElement.appendChild(htmlStringToFragment(messageText));
} else {
messageElement.appendChild(textWithBrToFragment(messageText));
}
// messageElement.textContent = messageText;
// // Replace \n with
elements for correct HTML display
// messageElement.innerHTML = '';
// messageText.split('\n').forEach((line, idx, arr) => {
// messageElement.appendChild(document.createTextNode(line));
// if (idx < arr.length - 1) {
// messageElement.appendChild(document.createElement('br'));
// }
// });
this.messages.appendChild(messageElement);
this.scrollToBottom();
}
appendBotMessage(messageText, type="bot") {
// console.log("[ThunderAI] appendBotMessage: " + messageText);
this.fullTextHTML = messageText;
const lastMessage = this.messages.lastElementChild;
const isLastMessageFromUser = lastMessage && lastMessage.classList.contains('user');
if (isLastMessageFromUser) {
const header = document.createElement('h2');
header.textContent = this.llmName + (type=='error' ? " - " + browser.i18n.getMessage("apiwebchat_error") : "");
this.messages.appendChild(header);
}
const messageElement = document.createElement('div');
messageElement.classList.add('message', type);
messageElement.textContent = messageText;
this.messages.appendChild(messageElement);
this.scrollToBottom();
}
handleNewToken(token) {
if (!this.accumulatingMessageEl) {
this.createNewAccumulatingMessage();
}
const newTokenElement = document.createElement('span');
newTokenElement.classList.add('token');
newTokenElement.textContent = token;
this.accumulatingMessageEl.appendChild(newTokenElement);
this.scrollToBottom();
if (token === '\n') {
this.flushAccumulatingMessage();
}
}
scrollToBottom() {
this.messages.scrollTop = this.messages.scrollHeight;
}
// Helper to create dropdown options
createOption(label, callback) {
const btn = document.createElement('button');
btn.textContent = label;
btn.onclick = callback;
return btn;
}
// click callcback for the "use this answer" button
handleUseThisAnswerButtonClick(promptData, replyType, fullTextHTMLAtAssignment){
return async () => {
if(promptData.mailMessageId == -1) { // we are using the reply from the compose window!
promptData.action = "2"; // replace text
}
let finalText = removeAloneBRs(fullTextHTMLAtAssignment);
const selectedHTML = this.getCurrentSelectionHTML();
if(selectedHTML != "") {
finalText = removeAloneBRs(selectedHTML);
}
switch(promptData.action) {
case "1": // do reply
// console.log("[ThunderAI] (do reply) fullTextHTMLAtAssignment: " + fullTextHTMLAtAssignment);
await browser.runtime.sendMessage({command: "chatgpt_replyMessage", text: finalText, tabId: promptData.tabId, mailMessageId: promptData.mailMessageId, replyType: replyType});
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);
await browser.runtime.sendMessage({command: "chatgpt_replaceSelectedText", text: finalText, tabId: promptData.tabId, mailMessageId: promptData.mailMessageId});
browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id});
break;
}
}
}
async addActionButtons(promptData = null) {
if(promptData == null) { return; }
const actionButtons = document.createElement('div');
actionButtons.classList.add('action-buttons');
// Create the main container for the "use this answer" button when replying
const splitButton = document.createElement('div');
splitButton.className = 'split-button';
// selection info
const selectionInfo = document.createElement('p');
selectionInfo.textContent = browser.i18n.getMessage("apiwebchat_selection_info");
selectionInfo.classList.add('sel_info');
// main button
const actionButton = document.createElement('button');
actionButton.className = 'action_btn';
const actionButton_line1 = document.createElement('span');
actionButton_line1.textContent = browser.i18n.getMessage("apiwebchat_use_this_answer");
actionButton.appendChild(actionButton_line1);
splitButton.appendChild(actionButton);
const fullTextHTMLAtAssignment = this.fullTextHTML.trim().replace(/^"|"$/g, '').replace(/^
"/, '
').replace(/"<\/p>$/, '
'); // strip quotation marks //console.log(">>>>>>>>>>>> fullTextHTMLAtAssignment: " + fullTextHTMLAtAssignment); let reply_type_pref = await browser.storage.sync.get({ reply_type: prefs_default.reply_type }); if((promptData.action == "1") && (promptData.mailMessageId != -1)) { const actionButton_line2 = document.createElement('span'); actionButton_line2.classList.add('action_btn_info'); actionButton_line2.textContent = reply_type_pref.reply_type == 'reply_all' ? browser.i18n.getMessage("prefs_OptionText_reply_all") : browser.i18n.getMessage("prefs_OptionText_reply_sender"); actionButton.appendChild(document.createElement('br')); actionButton.appendChild(actionButton_line2); // Dropdown toggle button const toggleBtn = document.createElement('button'); toggleBtn.className = 'dropdown-toggle'; toggleBtn.setAttribute('aria-label', 'Show options'); // SVG icon const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('viewBox', '0 0 20 20'); svg.setAttribute('width', '16'); svg.setAttribute('height', '16'); svg.setAttribute('fill', 'currentColor'); svg.setAttribute('stroke-width', '2'); const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); path.setAttribute('d', 'M19 9l-7 7-7-7'); svg.appendChild(path); toggleBtn.appendChild(svg); splitButton.appendChild(toggleBtn); // Dropdown menu const dropdown = document.createElement('div'); dropdown.className = 'dropdown-menu'; dropdown.id = 'dropdown'; // Add options dropdown.appendChild(this.createOption( reply_type_pref.reply_type == 'reply_all' ? browser.i18n.getMessage("prefs_OptionText_reply_sender") : browser.i18n.getMessage("prefs_OptionText_reply_all"), this.handleUseThisAnswerButtonClick(promptData, reply_type_pref.reply_type == 'reply_all' ? 'reply_sender' : 'reply_all', fullTextHTMLAtAssignment)) ); splitButton.appendChild(dropdown); let dropdownJustOpened = false; // Toggle function toggleBtn.onclick = () => { dropdownJustOpened = true; dropdown.classList.toggle('show'); }; // Close on outside click window.addEventListener('click', (e) => { // Delay the execution to allow other handlers (like toggle) to run first if (dropdownJustOpened) { dropdownJustOpened = false; return; // Skip this click because it's the one that opened the menu } setTimeout(() => { if (!splitButton.contains(e.target)) { dropdown.classList.remove('show'); } }, 0); }); }else{ actionButton.style.paddingRight = "10px"; actionButton.style.borderTopRightRadius = "5px"; actionButton.style.borderBottomRightRadius = "5px"; actionButton.style.marginRight = "10px"; } actionButton.addEventListener('click', this.handleUseThisAnswerButtonClick(promptData,reply_type_pref.reply_type, fullTextHTMLAtAssignment)); const closeButton = document.createElement('button'); closeButton.textContent = browser.i18n.getMessage("chatgpt_win_close"); closeButton.classList.add('close_btn'); closeButton.addEventListener('click', async () => { browser.runtime.sendMessage({command: "chatgpt_close", window_id: (await browser.windows.getCurrent()).id}); // close window }); if(promptData.action != 0) { actionButtons.appendChild(splitButton); selectionInfo.style.display = "block"; // show selection info } // diff viewer button if(promptData.prompt_info?.use_diff_viewer == "1") { const diffvButton = document.createElement('button'); diffvButton.textContent = browser.i18n.getMessage("btn_show_differences"); diffvButton.classList.add('diffv_btn'); diffvButton.addEventListener('click', async () => { let strippedText = fullTextHTMLAtAssignment.replace(/<\/?[^>]+(>|$)/g, ""); let originalText = promptData.prompt_info?.selection_text; if((originalText == null) || (originalText == "")) { originalText = promptData.prompt_info?.body_text; } this.appendDiffViewer(originalText, strippedText); diffvButton.disabled = true; }); actionButtons.appendChild(diffvButton); } actionButtons.appendChild(closeButton); this.messages.appendChild(actionButtons); this.messages.appendChild(selectionInfo); this.scrollToBottom(); } addDivider() { const divider = document.createElement('hr'); this.messages.appendChild(divider); this.scrollToBottom(); } appendDiffViewer(originalText, newText) { const wordDiff = Diff.diffWords(originalText, newText); const messageElement = document.createElement('div'); messageElement.classList.add('message', 'bot'); // Iterate over each part of the diff to create the HTML output wordDiff.forEach(part => { // Split part.value by