diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..4f72d44 --- /dev/null +++ b/biome.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.0.6/schema.json", + "files": { + "includes": [ + "**", + "!**/css/pico.classless.min.css", + "!**/node_modules", + "!**/*.min.js", + "!**/*.min.css" + ] + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noAssignInExpressions": "off" + }, + "style": { + "useTemplate": "warn", + "noParameterAssign": "error", + "useAsConstAssertion": "error", + "useDefaultParameterLast": "error", + "useEnumInitializers": "error", + "useSelfClosingElements": "error", + "useSingleVarDeclarator": "error", + "noUnusedTemplateLiteral": "error", + "useNumberNamespace": "error", + "noInferrableTypes": "error", + "noUselessElse": "error" + }, + "complexity": { + "useOptionalChain": "warn" + } + } + }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "indentStyle": "tab", + "indentWidth": 2, + "lineWidth": 80, + "lineEnding": "lf", + "includes": ["**", "!**/css/pico.classless.min.css"] + }, + "assist": { "actions": { "source": { "organizeImports": "on" } } }, + "javascript": { + "formatter": { + "quoteStyle": "double", + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "trailingCommas": "all", + "semicolons": "always", + "arrowParentheses": "always", + "bracketSpacing": true, + "bracketSameLine": false + } + } +} diff --git a/src/chat.js b/src/chat.js index 732f06f..82ad1f6 100644 --- a/src/chat.js +++ b/src/chat.js @@ -3,8 +3,8 @@ import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js"; import hljs from "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/es/highlight.min.js"; import { API_KEY } from "./config.js"; import { getSelectedModel, getSelectedModelEndpoint } from "./models.js"; -import { speakText, generateTitleForTTS } from "./tts.js"; -import { saveConversationHistory, initializeChatHistory } from "./storage.js"; +import { initializeChatHistory, saveConversationHistory } from "./storage.js"; +import { generateTitleForTTS, speakText } from "./tts.js"; // Initialize chat history export let chatHistory = initializeChatHistory(); diff --git a/src/config.js b/src/config.js index fe3f7d3..5b5ced2 100644 --- a/src/config.js +++ b/src/config.js @@ -17,11 +17,50 @@ export function setSystemMessageAppend(appendText) { SYSTEM_MESSAGE_APPEND = appendText; } -// Function to get the complete system message +// Function to get the complete system message with language preference export function getSystemMessage() { - return SYSTEM_MESSAGE_APPEND + // Get user's language preference from localStorage + let userLang = "en"; + try { + userLang = localStorage.getItem("uncloseai_language") || "en"; + } catch (error) { + console.warn("Failed to read language preference:", error); + } + + // Add language instruction to the system message + let languageInstruction = ""; + if (userLang !== "en") { + // Map language codes to full names for clarity + const langNames = { + es: "Spanish", + zh: "Chinese (Simplified)", + hi: "Hindi", + fr: "French", + ar: "Arabic", + bn: "Bengali", + ru: "Russian", + pt: "Portuguese", + ur: "Urdu", + id: "Indonesian", + de: "German", + ja: "Japanese", + sw: "Swahili", + mr: "Marathi", + te: "Telugu", + tr: "Turkish", + "zh-tw": "Chinese (Traditional)", + ko: "Korean", + }; + + const langName = langNames[userLang] || userLang; + languageInstruction = `\n\nIMPORTANT: The user has set their language preference to ${langName}. Please respond in ${langName} unless the user explicitly asks for another language. Maintain natural, fluent communication in ${langName}.`; + } + + const baseMessage = SYSTEM_MESSAGE_APPEND ? `${SYSTEM_MESSAGE_BASE}\n\n${SYSTEM_MESSAGE_APPEND}` : SYSTEM_MESSAGE_BASE; + + return baseMessage + languageInstruction; } // Dynamic Endpoints Configuration for Chat API diff --git a/src/content.js b/src/content.js index 9cba9dc..f62881a 100644 --- a/src/content.js +++ b/src/content.js @@ -25,7 +25,7 @@ export function extractWebpageContent() { // Recursively extract text and links from the body content function getTextWithLinks(element) { if (element.nodeType === Node.TEXT_NODE) { - content += element.textContent + " "; + content += `${element.textContent} `; } else if (element.nodeType === Node.ELEMENT_NODE) { if (element.tagName.toLowerCase() === "a") { // If it's a link, append the text and the href diff --git a/src/file-upload.js b/src/file-upload.js index 6b6b2e4..5420df1 100644 --- a/src/file-upload.js +++ b/src/file-upload.js @@ -1,9 +1,6 @@ -// File upload and processing functionality -import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js"; -import hljs from "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/es/highlight.min.js"; +import { chatHistory } from "./chat.js"; import { MEGAPARCE_API_URL } from "./config.js"; import { speakText } from "./tts.js"; -import { chatHistory } from "./chat.js"; // Progress indicator functions export function showProgressIndicator(message) { diff --git a/src/models.js b/src/models.js index 059d812..654bf48 100644 --- a/src/models.js +++ b/src/models.js @@ -105,7 +105,7 @@ export async function createModelSelectionDropdown() { }); // Insert the dropdown directly above the chat input box. - if (userInput && userInput.parentNode) { + if (userInput?.parentNode) { userInput.parentNode.parentNode.insertBefore( dropdown, userInput.parentNode, @@ -135,7 +135,7 @@ export function addRefreshModelsButton() { container.id = "refresh-models-container"; // Position it near the model selection dropdown - if (userInput && userInput.parentNode) { + if (userInput?.parentNode) { userInput.parentNode.parentNode.insertBefore( container, userInput.parentNode, @@ -146,7 +146,7 @@ export function addRefreshModelsButton() { const refreshButton = document.createElement("button"); refreshButton.textContent = "Refresh Models"; refreshButton.style.margin = "10px"; - refreshButton.onclick = async function () { + refreshButton.onclick = async () => { refreshButton.textContent = "Refreshing..."; refreshButton.disabled = true; @@ -189,7 +189,7 @@ export function addRefreshModelsButton() { export function getSelectedModel() { // Check modal dropdown first const modalDropdown = document.getElementById("hermes-model-selection"); - if (modalDropdown && modalDropdown.value) { + if (modalDropdown?.value) { // Extract just the model name, not the unique ID const selectedId = modalDropdown.value; if (modelRegistry[selectedId]) { @@ -200,7 +200,7 @@ export function getSelectedModel() { // Fallback to main dropdown const dropdown = document.getElementById("model-selection"); - if (dropdown && dropdown.value) { + if (dropdown?.value) { // Extract just the model name, not the unique ID const selectedId = dropdown.value; if (modelRegistry[selectedId]) { @@ -223,13 +223,13 @@ export function getSelectedModel() { export function getSelectedModelEndpoint() { // Check modal dropdown first const modalDropdown = document.getElementById("modal-model-selection"); - if (modalDropdown && modalDropdown.value && modelRegistry[modalDropdown.value]) { + if (modalDropdown?.value && modelRegistry[modalDropdown.value]) { return modelRegistry[modalDropdown.value].url; } // Fallback to main dropdown const dropdown = document.getElementById("model-selection"); - if (dropdown && dropdown.value && modelRegistry[dropdown.value]) { + if (dropdown?.value && modelRegistry[dropdown.value]) { return modelRegistry[dropdown.value].url; } diff --git a/src/token_estimator.js b/src/token_estimator.js index aba628f..bd5f176 100644 --- a/src/token_estimator.js +++ b/src/token_estimator.js @@ -10,51 +10,132 @@ export class SimpleBPETokenizer { this.initializePatterns(); this.initializeCommonTokens(); } - + initializePatterns() { // Simplified tokenization patterns based on tiktoken's approach this.patterns = [ // Contractions (case insensitive) /'(?:s|t|m|d|ll|ve|re)/gi, - + // Letters (word boundaries) /[a-zA-Z]+/g, - + // Numbers (1-3 digits groups) /\d{1,3}/g, - + // Punctuation and symbols /[^\s\w]/g, - + // Whitespace (including newlines) - /\s+/g + /\s+/g, ]; - + // Compiled regex for faster processing this.mainPattern = /'(?:s|t|m|d|ll|ve|re)|[a-zA-Z]+|\d{1,3}|[^\s\w]|\s+/gi; } - + initializeCommonTokens() { // Common subword patterns from BPE training (simplified set) this.commonSubwords = new Set([ // Common prefixes - 'un', 're', 'in', 'dis', 'en', 'non', 'pre', 'over', 'mis', 'sub', - 'anti', 'auto', 'co', 'de', 'ex', 'inter', 'multi', 'out', 'under', - - // Common suffixes - 'ing', 'ed', 'er', 'est', 'ly', 'tion', 'ness', 'ment', 'able', 'ible', - 'ful', 'less', 'ship', 'ward', 'wise', 'like', 'ous', 'ive', 'ate', - + "un", + "re", + "in", + "dis", + "en", + "non", + "pre", + "over", + "mis", + "sub", + "anti", + "auto", + "co", + "de", + "ex", + "inter", + "multi", + "out", + "under", + + // Common suffixes + "ing", + "ed", + "er", + "est", + "ly", + "tion", + "ness", + "ment", + "able", + "ible", + "ful", + "less", + "ship", + "ward", + "wise", + "like", + "ous", + "ive", + "ate", + // Common words (high frequency) - 'the', 'and', 'for', 'are', 'but', 'not', 'you', 'all', 'can', 'had', - 'her', 'was', 'one', 'our', 'out', 'day', 'get', 'use', 'man', 'new', - 'now', 'way', 'may', 'say', 'see', 'him', 'two', 'how', 'its', 'who', - + "the", + "and", + "for", + "are", + "but", + "not", + "you", + "all", + "can", + "had", + "her", + "was", + "one", + "our", + "out", + "day", + "get", + "use", + "man", + "new", + "now", + "way", + "may", + "say", + "see", + "him", + "two", + "how", + "its", + "who", + // Common letter combinations - 'th', 'er', 'on', 'an', 're', 'he', 'in', 'ed', 'nd', 'ha', 'at', - 'en', 'es', 'of', 'or', 'nt', 'ea', 'ti', 'to', 'it', 'st', 'io' + "th", + "er", + "on", + "an", + "re", + "he", + "in", + "ed", + "nd", + "ha", + "at", + "en", + "es", + "of", + "or", + "nt", + "ea", + "ti", + "to", + "it", + "st", + "io", ]); - + // Token length estimates for common patterns this.tokenWeights = { word: 1, @@ -62,29 +143,29 @@ export class SimpleBPETokenizer { punctuation: 1, whitespace: 0.3, contraction: 0.5, - subword: 0.6 + subword: 0.6, }; } - + // Main tokenization method encode(text) { - if (!text || typeof text !== 'string') return []; - + if (!text || typeof text !== "string") return []; + // Split text using main pattern const rawTokens = text.match(this.mainPattern) || []; const processedTokens = []; - + for (const token of rawTokens) { processedTokens.push(...this.processToken(token)); } - + return processedTokens; } - + processToken(token) { const trimmed = token.trim(); if (!trimmed) return [token]; // Keep whitespace as-is - + // Check token type and split if needed if (this.isWord(trimmed)) { return this.splitWord(trimmed); @@ -94,25 +175,25 @@ export class SimpleBPETokenizer { return [token]; // Punctuation, symbols, etc. } } - + isWord(token) { return /^[a-zA-Z]+$/.test(token); } - + isNumber(token) { return /^\d+$/.test(token); } - + splitWord(word) { if (word.length <= 3) return [word]; - + const tokens = []; let remaining = word.toLowerCase(); - + // Try to find common subwords while (remaining.length > 0) { let found = false; - + // Look for longest matching subword first for (let len = Math.min(remaining.length, 6); len >= 2; len--) { const substr = remaining.substring(0, len); @@ -123,7 +204,7 @@ export class SimpleBPETokenizer { break; } } - + if (!found) { // Split remaining into chunks if (remaining.length <= 4) { @@ -137,10 +218,10 @@ export class SimpleBPETokenizer { } } } - + return tokens; } - + splitNumber(number) { // Split numbers into groups of 1-3 digits const chunks = []; @@ -149,71 +230,72 @@ export class SimpleBPETokenizer { } return chunks; } - + // Quick token count (main method for estimation) count(text) { - if (!text || typeof text !== 'string') return 0; - + if (!text || typeof text !== "string") return 0; + // Handle special content first const specialTokens = this.countSpecialContent(text); - + // Remove special content and count regular tokens const cleanText = this.removeSpecialContent(text); const regularTokens = this.encode(cleanText); - + return { totalTokens: specialTokens.count + regularTokens.length, regularTokens: regularTokens.length, specialTokens: specialTokens.count, breakdown: { ...specialTokens.breakdown, - words: regularTokens.filter(t => this.isWord(t.trim())).length, - numbers: regularTokens.filter(t => this.isNumber(t.trim())).length, - punctuation: regularTokens.filter(t => /^[^\s\w]+$/.test(t.trim())).length, - whitespace: regularTokens.filter(t => /^\s+$/.test(t)).length + words: regularTokens.filter((t) => this.isWord(t.trim())).length, + numbers: regularTokens.filter((t) => this.isNumber(t.trim())).length, + punctuation: regularTokens.filter((t) => /^[^\s\w]+$/.test(t.trim())) + .length, + whitespace: regularTokens.filter((t) => /^\s+$/.test(t)).length, }, - textLength: text.length + textLength: text.length, }; } - + countSpecialContent(text) { let count = 0; const breakdown = { htmlTags: 0, codeBlocks: 0, inlineCode: 0, urls: 0 }; - + // HTML tags const htmlMatches = text.match(/<[^>]+>/g) || []; breakdown.htmlTags = htmlMatches.length; count += htmlMatches.length * 3; // ~3 tokens per tag - + // Code blocks const codeBlockMatches = text.match(/```[\s\S]*?```/g) || []; breakdown.codeBlocks = codeBlockMatches.length; - codeBlockMatches.forEach(block => { - const content = block.replace(/```[\w]*\n?/g, '').replace(/```$/g, ''); + codeBlockMatches.forEach((block) => { + const content = block.replace(/```[\w]*\n?/g, "").replace(/```$/g, ""); count += Math.ceil(content.length / 2.5); // Code is denser }); - + // Inline code const inlineMatches = text.match(/`[^`]+`/g) || []; breakdown.inlineCode = inlineMatches.length; - inlineMatches.forEach(code => { + inlineMatches.forEach((code) => { count += Math.ceil(code.length / 3); }); - + // URLs - const urlMatches = text.match(/https?:\/\/[^\s<>"{}|\\^`\[\]]+/g) || []; + const urlMatches = text.match(/https?:\/\/[^\s<>"{}|\\^`[\]]+/g) || []; breakdown.urls = urlMatches.length; count += urlMatches.length * 4; // URLs are typically 2-6 tokens - + return { count, breakdown }; } - + removeSpecialContent(text) { return text - .replace(/```[\s\S]*?```/g, ' ') // Replace code blocks - .replace(/`[^`]+`/g, ' ') // Replace inline code - .replace(/<[^>]+>/g, ' ') // Replace HTML tags - .replace(/https?:\/\/[^\s<>"{}|\\^`\[\]]+/g, ' '); // Replace URLs + .replace(/```[\s\S]*?```/g, " ") // Replace code blocks + .replace(/`[^`]+`/g, " ") // Replace inline code + .replace(/<[^>]+>/g, " ") // Replace HTML tags + .replace(/https?:\/\/[^\s<>"{}|\\^`[\]]+/g, " "); // Replace URLs } } @@ -228,4 +310,4 @@ export function countTokens(text) { export function estimateTokens(text) { const result = tokenizer.count(text); return result.totalTokens; -} \ No newline at end of file +} diff --git a/src/translation.js b/src/translation.js index 56524a1..5755751 100644 --- a/src/translation.js +++ b/src/translation.js @@ -1,5 +1,3 @@ -// Translation functionality with code block preservation -import { sendMessage } from "./chat.js"; import { API_KEY } from "./config.js"; import { getSelectedModel, getSelectedModelEndpoint } from "./models.js"; import { countTokens } from "./token_estimator.js"; @@ -168,7 +166,7 @@ export function preserveSpecialContent(text) { // URLs and URIs preservedText = preservedText.replace( - /https?:\/\/[^\s<>"{}|\\^`\[\]]+/g, + /https?:\/\/[^\s<>"{}|\\^`[\]]+/g, (match) => { const index = preservations.length; const placeholder = `__URI_${index}__`; @@ -219,17 +217,17 @@ export function restoreSpecialContent(translatedText, preservations) { // Calculate processing time estimate based on real vLLM metrics export function estimateProcessingTime(tokenCount) { // Real vLLM performance metrics from production: - // - Prompt processing: ~3,144 tokens/second + // - Prompt processing: ~3,144 tokens/second // - Generation: ~121 tokens/second // - Translation typically generates 1-2x input tokens - + const promptProcessingTime = tokenCount / 3144; // seconds for input processing const estimatedOutputTokens = tokenCount * 1.2; // Translation usually 1.2x input length const generationTime = estimatedOutputTokens / 121; // seconds for generation - + // Add some buffer for network latency and processing overhead const totalTime = (promptProcessingTime + generationTime) * 1.3; - + return Math.ceil(totalTime); } @@ -238,17 +236,17 @@ export async function translateText(text, targetLanguage) { // Ensure models are loaded before translation const { fetchModelsFromEndpoints } = await import("./models.js"); await fetchModelsFromEndpoints(); - + // Get accurate token count and timing estimate const tokenInfo = countTokens(text); const estimatedSeconds = estimateProcessingTime(tokenInfo.totalTokens); - + console.log("=== TRANSLATION ANALYSIS ==="); console.log(`Input tokens: ${tokenInfo.totalTokens}`); console.log(`Text length: ${tokenInfo.textLength} characters`); console.log(`Estimated processing time: ${estimatedSeconds} seconds`); console.log(`Token breakdown:`, tokenInfo.breakdown); - + const { preservedText, preservations } = preserveSpecialContent(text); // Debug logging @@ -303,11 +301,13 @@ ${preservedText}`; // Validate response if (!translatedText || translatedText.length === 0) { - throw new Error("Translation API returned empty response. Please try again."); + throw new Error( + "Translation API returned empty response. Please try again.", + ); } // Check if placeholders are still in the response - preservations.forEach((item, index) => { + preservations.forEach((item, _index) => { const found = translatedText.includes(item.placeholder); console.log( ` Placeholder ${item.placeholder} found in response: ${found}`, @@ -338,11 +338,13 @@ ${preservedText}`; export function extractPageContent() { // Clone the document to avoid modifying the original const documentClone = document.cloneNode(true); - + // Remove any open modals/dialogs that shouldn't be in the translation - const modalsToRemove = documentClone.querySelectorAll('dialog[open], #uncloseai-embedded-modal, [id*="modal"], [class*="modal"]'); - modalsToRemove.forEach(modal => modal.remove()); - + const modalsToRemove = documentClone.querySelectorAll( + 'dialog[open], #uncloseai-embedded-modal, [id*="modal"], [class*="modal"]', + ); + modalsToRemove.forEach((modal) => modal.remove()); + // Return the entire HTML of the document to preserve head, styles, and scripts return documentClone.documentElement.outerHTML; } @@ -359,7 +361,7 @@ export async function translateCurrentPage(targetLanguage) { const maxLength = 50000; // Much higher limit for complete page translation const contentToTranslate = pageContent.length > maxLength - ? pageContent.substring(0, maxLength) + "..." + ? `${pageContent.substring(0, maxLength)}...` : pageContent; return await translateText(contentToTranslate, targetLanguage); diff --git a/src/tts.js b/src/tts.js index bba0e2e..985cd3b 100644 --- a/src/tts.js +++ b/src/tts.js @@ -1,5 +1,5 @@ // Text-to-speech functionality -import { TTS_API_URL, API_KEY, MODEL, setLastTTS } from "./config.js"; +import { API_KEY, MODEL, setLastTTS, TTS_API_URL } from "./config.js"; import { getSelectedModel, getSelectedModelEndpoint } from "./models.js"; // Function to read text using TTS diff --git a/src/ui.js b/src/ui.js index 2fb0097..28bffd8 100644 --- a/src/ui.js +++ b/src/ui.js @@ -1,13 +1,13 @@ // UI creation and management functionality import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js"; import hljs from "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/es/highlight.min.js"; +import { API_KEY, setSystemMessageAppend, TTS_API_URL } from "./config.js"; import { extractWebpageContent } from "./content.js"; import { speakText } from "./tts.js"; -import { TTS_API_URL, API_KEY, setSystemMessageAppend } from "./config.js"; // Initialize ChunkFive font with absolute path function initializeChunkFiveFont() { - const fontFaceStyle = document.createElement('style'); + const fontFaceStyle = document.createElement("style"); fontFaceStyle.textContent = ` @font-face { font-family: "ChunkFiveRegular"; @@ -27,14 +27,16 @@ async function detectPageLanguage() { // Get page content as markdown const pageHtml = document.documentElement.outerHTML; const pageMarkdown = htmlToMarkdown(pageHtml); - + // Take a sample (first 1000 characters) for speed const sample = pageMarkdown.substring(0, 1000); - + // Import required modules - const { getSelectedModel, getSelectedModelEndpoint } = await import('./models.js'); + const { getSelectedModel, getSelectedModelEndpoint } = await import( + "./models.js" + ); const apiUrl = `${getSelectedModelEndpoint()}/chat/completions`; - + const response = await fetch(apiUrl, { method: "POST", headers: { @@ -46,12 +48,13 @@ async function detectPageLanguage() { messages: [ { role: "system", - content: "You are a language classifier. Respond with ONLY the 2-letter ISO language code (en, es, fr, de, etc.) for the detected language. No explanations, just the code." + content: + "You are a language classifier. Respond with ONLY the 2-letter ISO language code (en, es, fr, de, etc.) for the detected language. No explanations, just the code.", }, { - role: "user", - content: `Detect the language of this text:\n\n${sample}` - } + role: "user", + content: `Detect the language of this text:\n\n${sample}`, + }, ], temperature: 0.1, max_tokens: 10, @@ -60,24 +63,25 @@ async function detectPageLanguage() { }); if (!response.ok) { - console.warn('Language detection failed, falling back to browser language'); + console.warn( + "Language detection failed, falling back to browser language", + ); return fallbackLanguageDetection(); } const data = await response.json(); const detectedLang = data.choices[0].message.content.trim().toLowerCase(); - + // Validate it's a reasonable language code (2-3 chars) if (/^[a-z]{2,3}$/.test(detectedLang)) { - console.log('AI detected language:', detectedLang); + console.log("AI detected language:", detectedLang); return detectedLang; } else { - console.warn('Invalid language code from AI:', detectedLang); + console.warn("Invalid language code from AI:", detectedLang); return fallbackLanguageDetection(); } - } catch (error) { - console.warn('Language detection error:', error); + console.warn("Language detection error:", error); return fallbackLanguageDetection(); } } @@ -87,19 +91,692 @@ function fallbackLanguageDetection() { // Check html lang attribute first const htmlLang = document.documentElement.lang; if (htmlLang) { - const langCode = htmlLang.split('-')[0].toLowerCase(); + const langCode = htmlLang.split("-")[0].toLowerCase(); return langCode; } - + // Fallback to browser language const browserLang = navigator.language || navigator.userLanguage; if (browserLang) { - const langCode = browserLang.split('-')[0].toLowerCase(); + const langCode = browserLang.split("-")[0].toLowerCase(); return langCode; } - + // Default to English - return 'en'; + return "en"; +} + +// Complete UI Translations for all supported languages +const UI_TRANSLATIONS = { + en: { + // Main buttons + readPage: "📖 Read Page", + ttsAnything: "🔊 TTS Anything", + translate: "🌐 Translate", + smartTranslate: "🤖 Smart Translate", + uploadFile: "📁 Upload File", + + // Settings + modelLabel: "AI Model:", + voiceLabel: "TTS Voice:", + languageLabel: "Interface Language:", + quickActions: "Quick Actions:", + + // Actions + refreshModels: "🔄 Refresh Models", + clearChat: "🗑️ Clear Chat", + fullChatRaw: "📋 Full Chat Raw", + downloadChat: "💾 Download Chat", + + // Chat UI + sendButton: "Send", + typePlaceholder: "Type your message...", + clearChatConfirm: "Clear all conversation history?", + + // System messages + pageContentPrefix: "Here's the content of the webpage:", + additionalContext: + "You are embedded on this page. When answering questions, consider the page content and context.", + + // Notifications + languageChanged: + "Language set to {lang}. Refresh the page to see all interface elements in your preferred language.", + translationFailed: "Translation failed: {error}", + ttsFailed: "TTS failed: {error}", + + // Dropdown labels + currentPage: "Current page: {lang}", + translatingTo: "Translating...", + detectingLanguage: "🔍 Detecting...", + }, + + // Chinese (Simplified) + zh: { + readPage: "📖 阅读页面", + ttsAnything: "🔊 语音合成", + translate: "🌐 翻译", + smartTranslate: "🤖 智能翻译", + uploadFile: "📁 上传文件", + modelLabel: "AI 模型:", + voiceLabel: "语音类型:", + languageLabel: "界面语言:", + quickActions: "快捷操作:", + refreshModels: "🔄 刷新模型", + clearChat: "🗑️ 清除聊天", + fullChatRaw: "📋 完整聊天记录", + downloadChat: "💾 下载聊天", + sendButton: "发送", + typePlaceholder: "输入您的消息...", + clearChatConfirm: "清除所有对话历史记录?", + pageContentPrefix: "以下是网页内容:", + additionalContext: "您已嵌入此页面。回答问题时,请考虑页面内容和上下文。", + languageChanged: + "语言设置为 {lang}。刷新页面以查看所有界面元素的首选语言。", + translationFailed: "翻译失败:{error}", + ttsFailed: "语音合成失败:{error}", + currentPage: "当前页面:{lang}", + translatingTo: "翻译中...", + detectingLanguage: "🔍 检测中...", + }, + + // Hindi + hi: { + readPage: "📖 पृष्ठ पढ़ें", + ttsAnything: "🔊 TTS कुछ भी", + translate: "🌐 अनुवाद करें", + smartTranslate: "🤖 स्मार्ट अनुवाद", + uploadFile: "📁 फ़ाइल अपलोड करें", + modelLabel: "AI मॉडल:", + voiceLabel: "TTS आवाज़:", + languageLabel: "इंटरफ़ेस भाषा:", + quickActions: "त्वरित क्रियाएं:", + refreshModels: "🔄 मॉडल रीफ्रेश करें", + clearChat: "🗑️ चैट साफ़ करें", + fullChatRaw: "📋 पूर्ण चैट रॉ", + downloadChat: "💾 चैट डाउनलोड करें", + sendButton: "भेजें", + typePlaceholder: "अपना संदेश टाइप करें...", + clearChatConfirm: "सभी वार्तालाप इतिहास साफ़ करें?", + pageContentPrefix: "यहाँ वेबपेज की सामग्री है:", + additionalContext: + "आप इस पृष्ठ पर एम्बेडेड हैं। प्रश्नों का उत्तर देते समय, पृष्ठ सामग्री और संदर्भ पर विचार करें।", + languageChanged: + "भाषा {lang} पर सेट की गई। अपनी पसंदीदा भाषा में सभी इंटरफ़ेस तत्वों को देखने के लिए पृष्ठ को रीफ्रेश करें।", + translationFailed: "अनुवाद विफल: {error}", + ttsFailed: "TTS विफल: {error}", + currentPage: "वर्तमान पृष्ठ: {lang}", + translatingTo: "अनुवाद हो रहा है...", + detectingLanguage: "🔍 पता लगाया जा रहा है...", + }, + + // Spanish + es: { + readPage: "📖 Leer Página", + ttsAnything: "🔊 TTS Todo", + translate: "🌐 Traducir", + smartTranslate: "🤖 Traducción Inteligente", + uploadFile: "📁 Subir Archivo", + modelLabel: "Modelo de IA:", + voiceLabel: "Voz TTS:", + languageLabel: "Idioma de la Interfaz:", + quickActions: "Acciones Rápidas:", + refreshModels: "🔄 Actualizar Modelos", + clearChat: "🗑️ Borrar Chat", + fullChatRaw: "📋 Chat Completo Raw", + downloadChat: "💾 Descargar Chat", + sendButton: "Enviar", + typePlaceholder: "Escribe tu mensaje...", + clearChatConfirm: "¿Borrar todo el historial de conversación?", + pageContentPrefix: "Aquí está el contenido de la página web:", + additionalContext: + "Estás integrado en esta página. Al responder preguntas, considera el contenido y contexto de la página.", + languageChanged: + "Idioma establecido en {lang}. Actualiza la página para ver todos los elementos en tu idioma preferido.", + translationFailed: "Error en traducción: {error}", + ttsFailed: "Error en TTS: {error}", + currentPage: "Página actual: {lang}", + translatingTo: "Traduciendo...", + detectingLanguage: "🔍 Detectando...", + }, + + // French + fr: { + readPage: "📖 Lire la Page", + ttsAnything: "🔊 TTS Tout", + translate: "🌐 Traduire", + smartTranslate: "🤖 Traduction Intelligente", + uploadFile: "📁 Télécharger Fichier", + modelLabel: "Modèle IA :", + voiceLabel: "Voix TTS :", + languageLabel: "Langue de l'Interface :", + quickActions: "Actions Rapides :", + refreshModels: "🔄 Actualiser les Modèles", + clearChat: "🗑️ Effacer le Chat", + fullChatRaw: "📋 Chat Complet Brut", + downloadChat: "💾 Télécharger le Chat", + sendButton: "Envoyer", + typePlaceholder: "Tapez votre message...", + clearChatConfirm: "Effacer tout l'historique de conversation ?", + pageContentPrefix: "Voici le contenu de la page web :", + additionalContext: + "Vous êtes intégré sur cette page. Lors de la réponse aux questions, considérez le contenu et le contexte de la page.", + languageChanged: + "Langue définie sur {lang}. Actualisez la page pour voir tous les éléments de l'interface dans votre langue préférée.", + translationFailed: "Échec de la traduction : {error}", + ttsFailed: "Échec du TTS : {error}", + currentPage: "Page actuelle : {lang}", + translatingTo: "Traduction en cours...", + detectingLanguage: "🔍 Détection...", + }, + + // Arabic + ar: { + readPage: "📖 قراءة الصفحة", + ttsAnything: "🔊 تحويل النص إلى كلام", + translate: "🌐 ترجمة", + smartTranslate: "🤖 ترجمة ذكية", + uploadFile: "📁 رفع ملف", + modelLabel: "نموذج الذكاء الاصطناعي:", + voiceLabel: "صوت TTS:", + languageLabel: "لغة الواجهة:", + quickActions: "إجراءات سريعة:", + refreshModels: "🔄 تحديث النماذج", + clearChat: "🗑️ مسح المحادثة", + fullChatRaw: "📋 المحادثة الكاملة", + downloadChat: "💾 تحميل المحادثة", + sendButton: "إرسال", + typePlaceholder: "اكتب رسالتك...", + clearChatConfirm: "مسح كل سجل المحادثة؟", + pageContentPrefix: "هنا محتوى صفحة الويب:", + additionalContext: + "أنت مضمن في هذه الصفحة. عند الإجابة على الأسئلة، ضع في اعتبارك محتوى الصفحة والسياق.", + languageChanged: + "تم تعيين اللغة إلى {lang}. قم بتحديث الصفحة لرؤية جميع عناصر الواجهة بلغتك المفضلة.", + translationFailed: "فشلت الترجمة: {error}", + ttsFailed: "فشل TTS: {error}", + currentPage: "الصفحة الحالية: {lang}", + translatingTo: "جارٍ الترجمة...", + detectingLanguage: "🔍 جارٍ الكشف...", + }, + + // Bengali + bn: { + readPage: "📖 পৃষ্ঠা পড়ুন", + ttsAnything: "🔊 TTS সবকিছু", + translate: "🌐 অনুবাদ", + smartTranslate: "🤖 স্মার্ট অনুবাদ", + uploadFile: "📁 ফাইল আপলোড", + modelLabel: "AI মডেল:", + voiceLabel: "TTS ভয়েস:", + languageLabel: "ইন্টারফেস ভাষা:", + quickActions: "দ্রুত ক্রিয়া:", + refreshModels: "🔄 মডেল রিফ্রেশ", + clearChat: "🗑️ চ্যাট মুছুন", + fullChatRaw: "📋 সম্পূর্ণ চ্যাট র", + downloadChat: "💾 চ্যাট ডাউনলোড", + sendButton: "পাঠান", + typePlaceholder: "আপনার বার্তা টাইপ করুন...", + clearChatConfirm: "সমস্ত কথোপকথনের ইতিহাস মুছে ফেলবেন?", + pageContentPrefix: "এখানে ওয়েবপেজের বিষয়বস্তু:", + additionalContext: + "আপনি এই পৃষ্ঠায় এম্বেড করা আছেন। প্রশ্নের উত্তর দেওয়ার সময়, পৃষ্ঠার বিষয়বস্তু এবং প্রসঙ্গ বিবেচনা করুন।", + languageChanged: + "ভাষা {lang} সেট করা হয়েছে। আপনার পছন্দের ভাষায় সমস্ত ইন্টারফেস উপাদান দেখতে পৃষ্ঠাটি রিফ্রেশ করুন।", + translationFailed: "অনুবাদ ব্যর্থ: {error}", + ttsFailed: "TTS ব্যর্থ: {error}", + currentPage: "বর্তমান পৃষ্ঠা: {lang}", + translatingTo: "অনুবাদ হচ্ছে...", + detectingLanguage: "🔍 সনাক্ত করা হচ্ছে...", + }, + + // Russian + ru: { + readPage: "📖 Читать Страницу", + ttsAnything: "🔊 TTS Всё", + translate: "🌐 Перевести", + smartTranslate: "🤖 Умный Перевод", + uploadFile: "📁 Загрузить Файл", + modelLabel: "Модель ИИ:", + voiceLabel: "Голос TTS:", + languageLabel: "Язык Интерфейса:", + quickActions: "Быстрые Действия:", + refreshModels: "🔄 Обновить Модели", + clearChat: "🗑️ Очистить Чат", + fullChatRaw: "📋 Полный Чат Raw", + downloadChat: "💾 Скачать Чат", + sendButton: "Отправить", + typePlaceholder: "Введите ваше сообщение...", + clearChatConfirm: "Очистить всю историю разговора?", + pageContentPrefix: "Вот содержимое веб-страницы:", + additionalContext: + "Вы встроены на эту страницу. При ответе на вопросы учитывайте содержание и контекст страницы.", + languageChanged: + "Язык установлен на {lang}. Обновите страницу, чтобы увидеть все элементы интерфейса на предпочитаемом языке.", + translationFailed: "Ошибка перевода: {error}", + ttsFailed: "Ошибка TTS: {error}", + currentPage: "Текущая страница: {lang}", + translatingTo: "Перевод...", + detectingLanguage: "🔍 Определение...", + }, + + // Portuguese + pt: { + readPage: "📖 Ler Página", + ttsAnything: "🔊 TTS Tudo", + translate: "🌐 Traduzir", + smartTranslate: "🤖 Tradução Inteligente", + uploadFile: "📁 Enviar Arquivo", + modelLabel: "Modelo de IA:", + voiceLabel: "Voz TTS:", + languageLabel: "Idioma da Interface:", + quickActions: "Ações Rápidas:", + refreshModels: "🔄 Atualizar Modelos", + clearChat: "🗑️ Limpar Chat", + fullChatRaw: "📋 Chat Completo Raw", + downloadChat: "💾 Baixar Chat", + sendButton: "Enviar", + typePlaceholder: "Digite sua mensagem...", + clearChatConfirm: "Limpar todo o histórico de conversas?", + pageContentPrefix: "Aqui está o conteúdo da página web:", + additionalContext: + "Você está incorporado nesta página. Ao responder perguntas, considere o conteúdo e contexto da página.", + languageChanged: + "Idioma definido para {lang}. Atualize a página para ver todos os elementos da interface no seu idioma preferido.", + translationFailed: "Falha na tradução: {error}", + ttsFailed: "Falha no TTS: {error}", + currentPage: "Página atual: {lang}", + translatingTo: "Traduzindo...", + detectingLanguage: "🔍 Detectando...", + }, + + // Urdu + ur: { + readPage: "📖 صفحہ پڑھیں", + ttsAnything: "🔊 TTS سب کچھ", + translate: "🌐 ترجمہ", + smartTranslate: "🤖 سمارٹ ترجمہ", + uploadFile: "📁 فائل اپ لوڈ", + modelLabel: "AI ماڈل:", + voiceLabel: "TTS آواز:", + languageLabel: "انٹرفیس زبان:", + quickActions: "فوری اعمال:", + refreshModels: "🔄 ماڈلز ریفریش", + clearChat: "🗑️ چیٹ صاف کریں", + fullChatRaw: "📋 مکمل چیٹ Raw", + downloadChat: "💾 چیٹ ڈاؤن لوڈ", + sendButton: "بھیجیں", + typePlaceholder: "اپنا پیغام ٹائپ کریں...", + clearChatConfirm: "تمام گفتگو کی تاریخ صاف کریں؟", + pageContentPrefix: "یہاں ویب پیج کا مواد ہے:", + additionalContext: + "آپ اس صفحے پر ایمبیڈ ہیں۔ سوالات کا جواب دیتے وقت، صفحے کے مواد اور سیاق و سباق پر غور کریں۔", + languageChanged: + "زبان {lang} پر سیٹ کی گئی۔ اپنی پسندیدہ زبان میں تمام انٹرفیس عناصر دیکھنے کے لیے صفحہ ریفریش کریں۔", + translationFailed: "ترجمہ ناکام: {error}", + ttsFailed: "TTS ناکام: {error}", + currentPage: "موجودہ صفحہ: {lang}", + translatingTo: "ترجمہ ہو رہا ہے...", + detectingLanguage: "🔍 تلاش کر رہے ہیں...", + }, + + // Indonesian + id: { + readPage: "📖 Baca Halaman", + ttsAnything: "🔊 TTS Semua", + translate: "🌐 Terjemahkan", + smartTranslate: "🤖 Terjemahan Cerdas", + uploadFile: "📁 Unggah Berkas", + modelLabel: "Model AI:", + voiceLabel: "Suara TTS:", + languageLabel: "Bahasa Antarmuka:", + quickActions: "Aksi Cepat:", + refreshModels: "🔄 Segarkan Model", + clearChat: "🗑️ Hapus Obrolan", + fullChatRaw: "📋 Obrolan Lengkap Raw", + downloadChat: "💾 Unduh Obrolan", + sendButton: "Kirim", + typePlaceholder: "Ketik pesan Anda...", + clearChatConfirm: "Hapus semua riwayat percakapan?", + pageContentPrefix: "Berikut adalah konten halaman web:", + additionalContext: + "Anda tertanam di halaman ini. Saat menjawab pertanyaan, pertimbangkan konten dan konteks halaman.", + languageChanged: + "Bahasa diatur ke {lang}. Segarkan halaman untuk melihat semua elemen antarmuka dalam bahasa pilihan Anda.", + translationFailed: "Terjemahan gagal: {error}", + ttsFailed: "TTS gagal: {error}", + currentPage: "Halaman saat ini: {lang}", + translatingTo: "Menerjemahkan...", + detectingLanguage: "🔍 Mendeteksi...", + }, + + // German + de: { + readPage: "📖 Seite Lesen", + ttsAnything: "🔊 TTS Alles", + translate: "🌐 Übersetzen", + smartTranslate: "🤖 Intelligente Übersetzung", + uploadFile: "📁 Datei Hochladen", + modelLabel: "KI-Modell:", + voiceLabel: "TTS-Stimme:", + languageLabel: "Oberflächensprache:", + quickActions: "Schnellaktionen:", + refreshModels: "🔄 Modelle Aktualisieren", + clearChat: "🗑️ Chat Löschen", + fullChatRaw: "📋 Vollständiger Chat Raw", + downloadChat: "💾 Chat Herunterladen", + sendButton: "Senden", + typePlaceholder: "Geben Sie Ihre Nachricht ein...", + clearChatConfirm: "Gesamten Gesprächsverlauf löschen?", + pageContentPrefix: "Hier ist der Inhalt der Webseite:", + additionalContext: + "Sie sind auf dieser Seite eingebettet. Berücksichtigen Sie bei der Beantwortung von Fragen den Inhalt und Kontext der Seite.", + languageChanged: + "Sprache auf {lang} eingestellt. Aktualisieren Sie die Seite, um alle Oberflächenelemente in Ihrer bevorzugten Sprache zu sehen.", + translationFailed: "Übersetzung fehlgeschlagen: {error}", + ttsFailed: "TTS fehlgeschlagen: {error}", + currentPage: "Aktuelle Seite: {lang}", + translatingTo: "Übersetze...", + detectingLanguage: "🔍 Erkenne...", + }, + + // Japanese + ja: { + readPage: "📖 ページを読む", + ttsAnything: "🔊 TTS すべて", + translate: "🌐 翻訳", + smartTranslate: "🤖 スマート翻訳", + uploadFile: "📁 ファイルをアップロード", + modelLabel: "AIモデル:", + voiceLabel: "TTS音声:", + languageLabel: "インターフェース言語:", + quickActions: "クイックアクション:", + refreshModels: "🔄 モデルを更新", + clearChat: "🗑️ チャットをクリア", + fullChatRaw: "📋 完全なチャットRaw", + downloadChat: "💾 チャットをダウンロード", + sendButton: "送信", + typePlaceholder: "メッセージを入力してください...", + clearChatConfirm: "すべての会話履歴をクリアしますか?", + pageContentPrefix: "ウェブページのコンテンツは次のとおりです:", + additionalContext: + "このページに埋め込まれています。質問に答える際は、ページのコンテンツとコンテキストを考慮してください。", + languageChanged: + "言語を{lang}に設定しました。すべてのインターフェース要素を希望の言語で表示するには、ページを更新してください。", + translationFailed: "翻訳失敗:{error}", + ttsFailed: "TTS失敗:{error}", + currentPage: "現在のページ:{lang}", + translatingTo: "翻訳中...", + detectingLanguage: "🔍 検出中...", + }, + + // Swahili + sw: { + readPage: "📖 Soma Ukurasa", + ttsAnything: "🔊 TTS Yote", + translate: "🌐 Tafsiri", + smartTranslate: "🤖 Tafsiri ya Akili", + uploadFile: "📁 Pakia Faili", + modelLabel: "Mfano wa AI:", + voiceLabel: "Sauti ya TTS:", + languageLabel: "Lugha ya Kiolesura:", + quickActions: "Vitendo vya Haraka:", + refreshModels: "🔄 Onyesha Upya Mifano", + clearChat: "🗑️ Futa Mazungumzo", + fullChatRaw: "📋 Mazungumzo Kamili Raw", + downloadChat: "💾 Pakua Mazungumzo", + sendButton: "Tuma", + typePlaceholder: "Andika ujumbe wako...", + clearChatConfirm: "Futa historia yote ya mazungumzo?", + pageContentPrefix: "Hapa kuna maudhui ya ukurasa wa wavuti:", + additionalContext: + "Umepachikwa kwenye ukurasa huu. Unapojibu maswali, zingatia maudhui na muktadha wa ukurasa.", + languageChanged: + "Lugha imewekwa kuwa {lang}. Onyesha upya ukurasa ili kuona vipengele vyote vya kiolesura katika lugha unayopendelea.", + translationFailed: "Tafsiri imeshindwa: {error}", + ttsFailed: "TTS imeshindwa: {error}", + currentPage: "Ukurasa wa sasa: {lang}", + translatingTo: "Inatafsiri...", + detectingLanguage: "🔍 Inatambua...", + }, + + // Marathi + mr: { + readPage: "📖 पृष्ठ वाचा", + ttsAnything: "🔊 TTS सर्व", + translate: "🌐 भाषांतर", + smartTranslate: "🤖 स्मार्ट भाषांतर", + uploadFile: "📁 फाईल अपलोड", + modelLabel: "AI मॉडेल:", + voiceLabel: "TTS आवाज:", + languageLabel: "इंटरफेस भाषा:", + quickActions: "जलद क्रिया:", + refreshModels: "🔄 मॉडेल रीफ्रेश", + clearChat: "🗑️ चॅट साफ करा", + fullChatRaw: "📋 संपूर्ण चॅट Raw", + downloadChat: "💾 चॅट डाउनलोड", + sendButton: "पाठवा", + typePlaceholder: "तुमचा संदेश टाइप करा...", + clearChatConfirm: "सर्व संभाषण इतिहास साफ करायचा?", + pageContentPrefix: "येथे वेबपेजची सामग्री आहे:", + additionalContext: + "तुम्ही या पृष्ठावर एम्बेड केलेले आहात. प्रश्नांची उत्तरे देताना, पृष्ठाची सामग्री आणि संदर्भ विचारात घ्या.", + languageChanged: + "भाषा {lang} वर सेट केली. तुमच्या पसंतीच्या भाषेत सर्व इंटरफेस घटक पाहण्यासाठी पृष्ठ रीफ्रेश करा.", + translationFailed: "भाषांतर अयशस्वी: {error}", + ttsFailed: "TTS अयशस्वी: {error}", + currentPage: "वर्तमान पृष्ठ: {lang}", + translatingTo: "भाषांतर करत आहे...", + detectingLanguage: "🔍 शोधत आहे...", + }, + + // Telugu + te: { + readPage: "📖 పేజీ చదవండి", + ttsAnything: "🔊 TTS అన్నీ", + translate: "🌐 అనువదించు", + smartTranslate: "🤖 స్మార్ట్ అనువాదం", + uploadFile: "📁 ఫైల్ అప్‌లోడ్", + modelLabel: "AI మోడల్:", + voiceLabel: "TTS వాయిస్:", + languageLabel: "ఇంటర్ఫేస్ భాష:", + quickActions: "త్వరిత చర్యలు:", + refreshModels: "🔄 మోడల్స్ రిఫ్రెష్", + clearChat: "🗑️ చాట్ క్లియర్", + fullChatRaw: "📋 పూర్తి చాట్ Raw", + downloadChat: "💾 చాట్ డౌన్‌లోడ్", + sendButton: "పంపు", + typePlaceholder: "మీ సందేశం టైప్ చేయండి...", + clearChatConfirm: "అన్ని సంభాషణ చరిత్రను క్లియర్ చేయాలా?", + pageContentPrefix: "వెబ్‌పేజీ కంటెంట్ ఇక్కడ ఉంది:", + additionalContext: + "మీరు ఈ పేజీలో ఎంబెడ్ చేయబడ్డారు. ప్రశ్నలకు సమాధానం ఇచ్చేటప్పుడు, పేజీ కంటెంట్ మరియు సందర్భాన్ని పరిగణించండి.", + languageChanged: + "భాష {lang}కి సెట్ చేయబడింది. మీ ప్రాధాన్య భాషలో అన్ని ఇంటర్ఫేస్ అంశాలను చూడటానికి పేజీని రిఫ్రెష్ చేయండి.", + translationFailed: "అనువాదం విఫలమైంది: {error}", + ttsFailed: "TTS విఫలమైంది: {error}", + currentPage: "ప్రస్తుత పేజీ: {lang}", + translatingTo: "అనువదిస్తోంది...", + detectingLanguage: "🔍 గుర్తిస్తోంది...", + }, + + // Turkish + tr: { + readPage: "📖 Sayfayı Oku", + ttsAnything: "🔊 TTS Her Şey", + translate: "🌐 Çevir", + smartTranslate: "🤖 Akıllı Çeviri", + uploadFile: "📁 Dosya Yükle", + modelLabel: "AI Modeli:", + voiceLabel: "TTS Sesi:", + languageLabel: "Arayüz Dili:", + quickActions: "Hızlı İşlemler:", + refreshModels: "🔄 Modelleri Yenile", + clearChat: "🗑️ Sohbeti Temizle", + fullChatRaw: "📋 Tam Sohbet Raw", + downloadChat: "💾 Sohbeti İndir", + sendButton: "Gönder", + typePlaceholder: "Mesajınızı yazın...", + clearChatConfirm: "Tüm konuşma geçmişi silinsin mi?", + pageContentPrefix: "Web sayfasının içeriği burada:", + additionalContext: + "Bu sayfaya gömülüsünüz. Soruları yanıtlarken, sayfa içeriğini ve bağlamını göz önünde bulundurun.", + languageChanged: + "Dil {lang} olarak ayarlandı. Tüm arayüz öğelerini tercih ettiğiniz dilde görmek için sayfayı yenileyin.", + translationFailed: "Çeviri başarısız: {error}", + ttsFailed: "TTS başarısız: {error}", + currentPage: "Geçerli sayfa: {lang}", + translatingTo: "Çevriliyor...", + detectingLanguage: "🔍 Algılanıyor...", + }, + + // Chinese (Traditional) + "zh-tw": { + readPage: "📖 閱讀頁面", + ttsAnything: "🔊 語音合成", + translate: "🌐 翻譯", + smartTranslate: "🤖 智慧翻譯", + uploadFile: "📁 上傳檔案", + modelLabel: "AI 模型:", + voiceLabel: "語音類型:", + languageLabel: "介面語言:", + quickActions: "快速操作:", + refreshModels: "🔄 重新整理模型", + clearChat: "🗑️ 清除聊天", + fullChatRaw: "📋 完整聊天記錄", + downloadChat: "💾 下載聊天", + sendButton: "傳送", + typePlaceholder: "輸入您的訊息...", + clearChatConfirm: "清除所有對話歷史記錄?", + pageContentPrefix: "以下是網頁內容:", + additionalContext: "您已嵌入此頁面。回答問題時,請考慮頁面內容和上下文。", + languageChanged: + "語言設定為 {lang}。重新整理頁面以查看所有介面元素的首選語言。", + translationFailed: "翻譯失敗:{error}", + ttsFailed: "語音合成失敗:{error}", + currentPage: "目前頁面:{lang}", + translatingTo: "翻譯中...", + detectingLanguage: "🔍 偵測中...", + }, + + // Korean + ko: { + readPage: "📖 페이지 읽기", + ttsAnything: "🔊 TTS 모두", + translate: "🌐 번역", + smartTranslate: "🤖 스마트 번역", + uploadFile: "📁 파일 업로드", + modelLabel: "AI 모델:", + voiceLabel: "TTS 음성:", + languageLabel: "인터페이스 언어:", + quickActions: "빠른 작업:", + refreshModels: "🔄 모델 새로고침", + clearChat: "🗑️ 채팅 지우기", + fullChatRaw: "📋 전체 채팅 Raw", + downloadChat: "💾 채팅 다운로드", + sendButton: "보내기", + typePlaceholder: "메시지를 입력하세요...", + clearChatConfirm: "모든 대화 기록을 지우시겠습니까?", + pageContentPrefix: "웹페이지 내용은 다음과 같습니다:", + additionalContext: + "이 페이지에 포함되어 있습니다. 질문에 답할 때 페이지 내용과 맥락을 고려하세요.", + languageChanged: + "언어가 {lang}(으)로 설정되었습니다. 모든 인터페이스 요소를 선호하는 언어로 보려면 페이지를 새로고침하세요.", + translationFailed: "번역 실패: {error}", + ttsFailed: "TTS 실패: {error}", + currentPage: "현재 페이지: {lang}", + translatingTo: "번역 중...", + detectingLanguage: "🔍 감지 중...", + }, +}; + +// Get UI text in user's preferred language +function getUIText(key, params = {}) { + const lang = getUserLanguagePreference(); + const translations = UI_TRANSLATIONS[lang] || UI_TRANSLATIONS.en; + let text = translations[key] || UI_TRANSLATIONS.en[key] || key; + + // Replace parameters like {lang} with actual values + Object.entries(params).forEach(([param, value]) => { + text = text.replace(`{${param}}`, value); + }); + + return text; +} + +// Language preference management +function getUserLanguagePreference() { + try { + return localStorage.getItem("uncloseai_language") || "en"; + } catch (error) { + console.warn("Failed to read language preference:", error); + return "en"; + } +} + +function setUserLanguagePreference(langCode) { + try { + localStorage.setItem("uncloseai_language", langCode); + console.log("Language preference saved:", langCode); + // Trigger UI refresh when language changes + refreshUILanguage(); + } catch (error) { + console.warn("Failed to save language preference:", error); + } +} + +// Translation function for UI text +async function translateUIText(text, targetLang) { + if (targetLang === "en" || !text) return text; + + try { + const { translateText } = await import("./translation.js"); + const translated = await translateText(text, targetLang); + return translated || text; + } catch (error) { + console.warn("UI translation failed:", error); + return text; + } +} + +// Refresh UI elements when language preference changes +function refreshUILanguage() { + console.log("UI language refresh triggered"); + + // Update existing button texts if modal is open + const readBtn = document.querySelector('[onclick*="readPageWithHermes"]'); + if (readBtn) readBtn.textContent = getUIText("readPage"); + + const ttsBtn = document.querySelector('[onclick*="openTTSModal"]'); + if (ttsBtn) ttsBtn.textContent = getUIText("ttsAnything"); + + const translateBtn = document.querySelector( + '[onclick*="openTranslateModal"]', + ); + if (translateBtn) translateBtn.textContent = getUIText("translate"); + + // Update smart translate buttons + const smartTranslateBtns = document.querySelectorAll( + '[onclick*="handleSmartTranslate"]', + ); + smartTranslateBtns.forEach( + (btn) => (btn.textContent = getUIText("smartTranslate")), + ); + + // Update labels if settings panel is open + const modelLabel = document.querySelector( + "#hermes-model-select", + )?.previousElementSibling; + if (modelLabel) modelLabel.textContent = getUIText("modelLabel"); + + const voiceLabel = document.querySelector( + "#hermes-voice-select", + )?.previousElementSibling; + if (voiceLabel) voiceLabel.textContent = getUIText("voiceLabel"); + + const langLabel = document.querySelector( + "#hermes-language-select", + )?.previousElementSibling; + if (langLabel) langLabel.textContent = getUIText("languageLabel"); } // Theme detection and styling functions @@ -109,112 +786,112 @@ function detectCurrentTheme() { (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches && !document.documentElement.getAttribute("data-theme")); - - return isDark ? 'dark' : 'light'; + + return isDark ? "dark" : "light"; } function getThemeColors(theme = detectCurrentTheme()) { - if (theme === 'dark') { + if (theme === "dark") { return { // Main backgrounds - modalBackground: '#1a1a1a', - contentBackground: '#2d2d2d', - panelBackground: '#262626', - inputBackground: '#363636', - + modalBackground: "#1a1a1a", + contentBackground: "#2d2d2d", + panelBackground: "#262626", + inputBackground: "#363636", + // Text colors - primaryText: '#ffffff', - secondaryText: '#b3b3b3', - mutedText: '#808080', - + primaryText: "#ffffff", + secondaryText: "#b3b3b3", + mutedText: "#808080", + // Borders and dividers - borderColor: '#404040', - dividerColor: '#333333', - + borderColor: "#404040", + dividerColor: "#333333", + // Interactive elements - buttonBackground: '#404040', - buttonHover: '#4a4a4a', - buttonBorder: '#555555', - + buttonBackground: "#404040", + buttonHover: "#4a4a4a", + buttonBorder: "#555555", + // Message bubbles - userMessageBg: '#4a5568', - userMessageText: '#ffffff', - aiMessageBg: '#363636', - aiMessageText: '#ffffff', - + userMessageBg: "#4a5568", + userMessageText: "#ffffff", + aiMessageBg: "#363636", + aiMessageText: "#ffffff", + // Input focus - focusBorder: '#667eea', - + focusBorder: "#667eea", + // Shadows (adjusted for dark theme) - shadowColor: 'rgba(0,0,0,0.5)', - lightShadow: 'rgba(0,0,0,0.3)', - + shadowColor: "rgba(0,0,0,0.5)", + lightShadow: "rgba(0,0,0,0.3)", + // Action buttons - actionBg: 'rgba(255, 255, 255, 0.1)', - actionBgHover: 'rgba(255, 255, 255, 0.2)', - actionBorder: 'rgba(255, 255, 255, 0.2)', + actionBg: "rgba(255, 255, 255, 0.1)", + actionBgHover: "rgba(255, 255, 255, 0.2)", + actionBorder: "rgba(255, 255, 255, 0.2)", }; } else { return { // Main backgrounds - modalBackground: '#ffffff', - contentBackground: '#f8f9fa', - panelBackground: '#ffffff', - inputBackground: '#ffffff', - + modalBackground: "#ffffff", + contentBackground: "#f8f9fa", + panelBackground: "#ffffff", + inputBackground: "#ffffff", + // Text colors - primaryText: '#212529', - secondaryText: '#495057', - mutedText: '#6c757d', - + primaryText: "#212529", + secondaryText: "#495057", + mutedText: "#6c757d", + // Borders and dividers - borderColor: '#dee2e6', - dividerColor: '#e9ecef', - + borderColor: "#dee2e6", + dividerColor: "#e9ecef", + // Interactive elements - buttonBackground: '#f8f9fa', - buttonHover: '#e9ecef', - buttonBorder: '#dee2e6', - + buttonBackground: "#f8f9fa", + buttonHover: "#e9ecef", + buttonBorder: "#dee2e6", + // Message bubbles - userMessageBg: '#667eea', - userMessageText: '#ffffff', - aiMessageBg: '#ffffff', - aiMessageText: '#212529', - + userMessageBg: "#667eea", + userMessageText: "#ffffff", + aiMessageBg: "#ffffff", + aiMessageText: "#212529", + // Input focus - focusBorder: '#667eea', - + focusBorder: "#667eea", + // Shadows - shadowColor: 'rgba(0,0,0,0.3)', - lightShadow: 'rgba(0,0,0,0.1)', - + shadowColor: "rgba(0,0,0,0.3)", + lightShadow: "rgba(0,0,0,0.1)", + // Action buttons - actionBg: 'rgba(255, 255, 255, 0.2)', - actionBgHover: 'rgba(255, 255, 255, 0.3)', - actionBorder: 'rgba(255, 255, 255, 0.3)', + actionBg: "rgba(255, 255, 255, 0.2)", + actionBgHover: "rgba(255, 255, 255, 0.3)", + actionBorder: "rgba(255, 255, 255, 0.3)", }; } } // Ensure font is loaded when module is imported -if (typeof document !== 'undefined') { +if (typeof document !== "undefined") { initializeChunkFiveFont(); } // Helper function to add copy buttons to code blocks function addCodeBlockCopyButtons(element) { - const codeBlocks = element.querySelectorAll('pre code'); + const codeBlocks = element.querySelectorAll("pre code"); codeBlocks.forEach((codeBlock) => { const pre = codeBlock.parentElement; - if (pre.tagName.toLowerCase() === 'pre') { + if (pre.tagName.toLowerCase() === "pre") { // Make the pre element relative for positioning - pre.style.position = 'relative'; - + pre.style.position = "relative"; + // Create copy button - const copyBtn = document.createElement('button'); - copyBtn.textContent = '📋'; - copyBtn.title = 'Copy code'; + const copyBtn = document.createElement("button"); + copyBtn.textContent = "📋"; + copyBtn.title = "Copy code"; copyBtn.style.cssText = ` position: absolute; top: 8px; @@ -230,21 +907,21 @@ function addCodeBlockCopyButtons(element) { transition: opacity 0.2s; z-index: 10; `; - + copyBtn.onmouseenter = () => { - copyBtn.style.opacity = '1'; + copyBtn.style.opacity = "1"; }; copyBtn.onmouseleave = () => { - copyBtn.style.opacity = '0.8'; + copyBtn.style.opacity = "0.8"; }; - + copyBtn.onclick = async (e) => { e.stopPropagation(); try { const codeText = codeBlock.textContent; await navigator.clipboard.writeText(codeText); const originalText = copyBtn.textContent; - copyBtn.textContent = '✓'; + copyBtn.textContent = "✓"; setTimeout(() => { copyBtn.textContent = originalText; }, 1000); @@ -252,7 +929,7 @@ function addCodeBlockCopyButtons(element) { alert("Failed to copy code: " + error.message); } }; - + pre.appendChild(copyBtn); } }); @@ -260,7 +937,9 @@ function addCodeBlockCopyButtons(element) { // Send message with custom history (for intro generation) async function* sendMessageWithCustomHistory(messageHistory) { - const { getSelectedModel, getSelectedModelEndpoint } = await import('./models.js'); + const { getSelectedModel, getSelectedModelEndpoint } = await import( + "./models.js" + ); const apiUrl = `${getSelectedModelEndpoint()}/chat/completions`; const response = await fetch(apiUrl, { @@ -346,30 +1025,31 @@ async function speakChatText(text, voice = "alloy", rate = 1.0) { throw error; } } + +import { chatHistory, handleUserInput, sendMessage } from "./chat.js"; import { handleFileUpload, - uploadFile, - showProgressIndicator, hideProgressIndicator, + showProgressIndicator, + uploadFile, } from "./file-upload.js"; -import { readPageWithHermes } from "./page-reader.js"; -import { handleUserInput, chatHistory, sendMessage } from "./chat.js"; -import { - getPageSpecificKey, - loadConversationHistory, - clearConversationHistory, - saveConversationHistory -} from "./storage.js"; -import { - fetchModelsFromEndpoints, - getSelectedModel, +import { + fetchModelsFromEndpoints, + getSelectedModel, getSelectedModelEndpoint, - modelRegistry + modelRegistry, } from "./models.js"; +import { readPageWithHermes } from "./page-reader.js"; +import { + clearConversationHistory, + getPageSpecificKey, + loadConversationHistory, + saveConversationHistory, +} from "./storage.js"; import { SUPPORTED_LANGUAGES, - translateText, translateCurrentPage, + translateText, } from "./translation.js"; // Configuration flags @@ -419,17 +1099,17 @@ export function createFullInterface(container) { controlsDiv.style.cssText = "display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; margin-bottom: 10px;"; - const readBtn = createButton("📖 Read Page", () => readPageWithHermes()); - const ttsBtn = createButton("🔊 TTS Anything", () => openTTSModal()); - const translateBtn = createButton("🌐 Translate", () => openTranslateModal()); - const uploadBtn = createButton("📁 Upload File", () => - document.querySelector("[data-uncloseai-file-input]")?.click(), + const readBtn = createButton(getUIText("readPage"), () => + readPageWithHermes(), + ); + const ttsBtn = createButton(getUIText("ttsAnything"), () => openTTSModal()); + const translateBtn = createButton(getUIText("translate"), () => + openTranslateModal(), ); controlsDiv.appendChild(readBtn); controlsDiv.appendChild(ttsBtn); controlsDiv.appendChild(translateBtn); - controlsDiv.appendChild(uploadBtn); // Hidden file input const fileInput = document.createElement("input"); @@ -520,10 +1200,16 @@ export function createTranslateFeature(container) { export function createSmartTranslateFeature(container) { const smartTranslateDiv = document.createElement("div"); - smartTranslateDiv.innerHTML = ` -

Smart Translate

- - `; + const button = document.createElement("button"); + button.textContent = getUIText("smartTranslate"); + button.style.cssText = "width: 100%; padding: 6px;"; + button.onclick = () => handleSmartTranslate(button); + + const heading = document.createElement("h4"); + heading.textContent = "Smart Translate"; + + smartTranslateDiv.appendChild(heading); + smartTranslateDiv.appendChild(button); container.appendChild(smartTranslateDiv); } @@ -624,29 +1310,34 @@ export async function handleUploadFromElement(button) { } export async function handleSmartTranslate(button) { + console.log("🤖 Smart translate clicked!", button); + // Check if dropdown already exists - const existingDropdown = button.parentElement.querySelector('.translate-dropdown'); + const existingDropdown = button.parentElement.querySelector( + ".translate-dropdown", + ); if (existingDropdown) { existingDropdown.remove(); return; } - + // Detect page language - button.textContent = "🔍 Detecting..."; + button.textContent = getUIText("detectingLanguage"); button.disabled = true; - + try { const currentLang = await detectPageLanguage(); - button.textContent = "🤖 Smart Translate"; + button.textContent = getUIText("smartTranslate"); button.disabled = false; - + // Import supported languages and native names - const { SUPPORTED_LANGUAGES, NATIVE_LANGUAGE_NAMES, translateCurrentPage } = await import('./translation.js'); + const { SUPPORTED_LANGUAGES, NATIVE_LANGUAGE_NAMES, translateCurrentPage } = + await import("./translation.js"); const colors = getThemeColors(); - + // Create dropdown - const dropdown = document.createElement('div'); - dropdown.className = 'translate-dropdown'; + const dropdown = document.createElement("div"); + dropdown.className = "translate-dropdown"; dropdown.style.cssText = ` position: absolute; top: 100%; @@ -661,9 +1352,9 @@ export async function handleSmartTranslate(button) { overflow-y: auto; margin-top: 4px; `; - + // Add current language indicator - const currentLangDiv = document.createElement('div'); + const currentLangDiv = document.createElement("div"); currentLangDiv.style.cssText = ` padding: 8px 12px; font-size: 12px; @@ -671,18 +1362,23 @@ export async function handleSmartTranslate(button) { border-bottom: 1px solid ${colors.dividerColor}; background: ${colors.contentBackground}; `; - const currentLangName = NATIVE_LANGUAGE_NAMES[currentLang] || SUPPORTED_LANGUAGES[currentLang] || 'Unknown'; - currentLangDiv.textContent = `Current page: ${currentLangName}`; + const currentLangName = + NATIVE_LANGUAGE_NAMES[currentLang] || + SUPPORTED_LANGUAGES[currentLang] || + "Unknown"; + currentLangDiv.textContent = getUIText("currentPage", { + lang: currentLangName, + }); dropdown.appendChild(currentLangDiv); - + // Add translation options with code, English, and native names Object.entries(NATIVE_LANGUAGE_NAMES).forEach(([code, nativeName]) => { // Skip current language if (code === currentLang) return; - + const englishName = SUPPORTED_LANGUAGES[code]; - - const option = document.createElement('div'); + + const option = document.createElement("div"); option.style.cssText = ` padding: 8px 12px; cursor: pointer; @@ -691,7 +1387,7 @@ export async function handleSmartTranslate(button) { font-size: 14px; line-height: 1.4; `; - + // Format: "es • Spanish • Español" option.innerHTML = `
@@ -702,14 +1398,14 @@ export async function handleSmartTranslate(button) { ${nativeName}
`; - + option.onmouseenter = () => { option.style.background = colors.buttonHover; }; option.onmouseleave = () => { - option.style.background = 'transparent'; + option.style.background = "transparent"; }; - + option.onclick = async () => { try { // Show loading @@ -717,23 +1413,22 @@ export async function handleSmartTranslate(button) {
${code} - Translating... + ${getUIText("translatingTo")}
`; - option.style.pointerEvents = 'none'; - + option.style.pointerEvents = "none"; + // Translate the page const translatedHtml = await translateCurrentPage(code); - + // Open in new tab - const newWindow = window.open('', '_blank'); + const newWindow = window.open("", "_blank"); newWindow.document.write(translatedHtml); newWindow.document.close(); newWindow.document.title = `${document.title} (${name})`; - + // Close dropdown dropdown.remove(); - } catch (error) { alert(`Translation failed: ${error.message}`); // Restore original format @@ -746,28 +1441,27 @@ export async function handleSmartTranslate(button) { ${nativeName} `; - option.style.pointerEvents = 'auto'; + option.style.pointerEvents = "auto"; } }; - + dropdown.appendChild(option); }); - + // Position dropdown relative to button - button.style.position = 'relative'; + button.style.position = "relative"; button.parentElement.appendChild(dropdown); - + // Close dropdown when clicking outside const closeDropdown = (e) => { if (!dropdown.contains(e.target) && e.target !== button) { dropdown.remove(); - document.removeEventListener('click', closeDropdown); + document.removeEventListener("click", closeDropdown); } }; - setTimeout(() => document.addEventListener('click', closeDropdown), 100); - + setTimeout(() => document.addEventListener("click", closeDropdown), 100); } catch (error) { - button.textContent = "🤖 Smart Translate"; + button.textContent = getUIText("smartTranslate"); button.disabled = false; alert(`Language detection failed: ${error.message}`); } @@ -788,7 +1482,7 @@ export function initializeChatInterface() { }); marked.setOptions({ - highlight: function (code, lang) { + highlight: (code, lang) => { const language = hljs.getLanguage(lang) ? lang : "plaintext"; return hljs.highlight(code, { language }).value; }, @@ -803,7 +1497,7 @@ export function initializeChatInterface() { // Event listeners document .getElementById("user-input") - ?.addEventListener("keydown", function (event) { + ?.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.shiftKey) { event.preventDefault(); handleUserInput(); @@ -848,10 +1542,10 @@ export function addFileUploadButton() { // Function to create floating AI button export function createFloatingAIButton() { console.log("uncloseai.js: createFloatingAIButton() called."); - + // Ensure ChunkFive font is loaded initializeChunkFiveFont(); - + // Remove any existing floating button first const existingButton = document.getElementById("floating-ai-button"); if (existingButton) { @@ -932,7 +1626,7 @@ export function createFloatingAIButton() { function setupFloatingButtonHandler(floatingButton) { floatingButton.onclick = async () => { console.log("Floating button clicked!"); - + // Show loading state const originalText = floatingButton.textContent; floatingButton.textContent = "loading..."; @@ -942,35 +1636,42 @@ function setupFloatingButtonHandler(floatingButton) { try { console.log("Trying to toggle Hermes modal..."); - + // Try multiple ways to access the modal function let modalFunction = null; - - if (typeof toggleUncloseaiEmbeddedModal === 'function') { + + if (typeof toggleUncloseaiEmbeddedModal === "function") { console.log("Found toggleUncloseaiEmbeddedModal in local scope"); modalFunction = toggleUncloseaiEmbeddedModal; - } else if (typeof window.toggleUncloseaiEmbeddedModal === 'function') { + } else if (typeof window.toggleUncloseaiEmbeddedModal === "function") { console.log("Found toggleUncloseaiEmbeddedModal on window"); modalFunction = window.toggleUncloseaiEmbeddedModal; } else { - console.log("toggleUncloseaiEmbeddedModal not found, trying dynamic import"); + console.log( + "toggleUncloseaiEmbeddedModal not found, trying dynamic import", + ); // Try to import it dynamically as a fallback try { - const uiModule = await import('./ui.js'); + const uiModule = await import("./ui.js"); if (uiModule.toggleUncloseaiEmbeddedModal) { console.log("Successfully imported toggleUncloseaiEmbeddedModal"); modalFunction = uiModule.toggleUncloseaiEmbeddedModal; } } catch (importError) { - console.error("Could not import toggleUncloseaiEmbeddedModal:", importError); + console.error( + "Could not import toggleUncloseaiEmbeddedModal:", + importError, + ); } } - + if (modalFunction) { console.log("Calling modal function"); await modalFunction(); } else { - console.error("toggleUncloseaiEmbeddedModal function not available in any form"); + console.error( + "toggleUncloseaiEmbeddedModal function not available in any form", + ); alert("AI modal not available in this context"); } } catch (error) { @@ -990,18 +1691,18 @@ function setupFloatingButtonHandler(floatingButton) { async function openUncloseaiEmbeddedModalNew() { // Ensure ChunkFive font is loaded initializeChunkFiveFont(); - + // Get theme colors const theme = detectCurrentTheme(); const colors = getThemeColors(theme); - + const modal = document.createElement("dialog"); modal.id = "uncloseai-embedded-modal"; modal.setAttribute("data-theme", theme); - + // Mobile-first: full screen on mobile, centered on desktop const isMobile = window.innerWidth <= 768; - + modal.style.cssText = ` position: fixed; margin: 0; @@ -1010,7 +1711,9 @@ async function openUncloseaiEmbeddedModalNew() { background: ${colors.modalBackground}; color: ${colors.primaryText}; z-index: 2000; - ${isMobile ? ` + ${ + isMobile + ? ` top: 0; left: 0; width: 100vw; @@ -1018,7 +1721,8 @@ async function openUncloseaiEmbeddedModalNew() { max-width: 100vw; max-height: 100vh; border-radius: 0; - ` : ` + ` + : ` width: 90vw; max-width: 800px; height: 90vh; @@ -1027,7 +1731,8 @@ async function openUncloseaiEmbeddedModalNew() { transform: translate(-50%, -50%); border-radius: 16px; box-shadow: 0 20px 40px ${colors.shadowColor}; - `} + ` + } `; // Simple container with flex layout @@ -1055,15 +1760,16 @@ async function openUncloseaiEmbeddedModalNew() { const titleContainer = document.createElement("div"); titleContainer.style.cssText = `flex: 1; display: flex; flex-direction: column; gap: 4px;`; - + const title = document.createElement("h2"); - title.innerHTML = '🤖 uncloseai.'; + title.innerHTML = + "🤖 uncloseai."; title.style.cssText = ` margin: 0; - font-size: ${isMobile ? '18px' : '20px'}; + font-size: ${isMobile ? "18px" : "20px"}; font-weight: 600; `; - + const subtitle = document.createElement("div"); const pageTitle = document.title || window.location.hostname; subtitle.innerHTML = ` @@ -1072,7 +1778,7 @@ async function openUncloseaiEmbeddedModalNew() { You are discussing: ${pageTitle} `; - + titleContainer.appendChild(title); titleContainer.appendChild(subtitle); @@ -1092,8 +1798,10 @@ async function openUncloseaiEmbeddedModalNew() { justify-content: center; transition: background 0.2s; `; - closeBtn.onmouseover = () => closeBtn.style.background = 'rgba(255,255,255,0.3)'; - closeBtn.onmouseout = () => closeBtn.style.background = 'rgba(255,255,255,0.2)'; + closeBtn.onmouseover = () => + (closeBtn.style.background = "rgba(255,255,255,0.3)"); + closeBtn.onmouseout = () => + (closeBtn.style.background = "rgba(255,255,255,0.2)"); closeBtn.onclick = () => { modal.close(); document.body.removeChild(modal); @@ -1118,9 +1826,11 @@ async function openUncloseaiEmbeddedModalNew() { transition: background 0.2s; margin-right: 8px; `; - menuBtn.onmouseover = () => menuBtn.style.background = 'rgba(255,255,255,0.3)'; - menuBtn.onmouseout = () => menuBtn.style.background = 'rgba(255,255,255,0.2)'; - + menuBtn.onmouseover = () => + (menuBtn.style.background = "rgba(255,255,255,0.3)"); + menuBtn.onmouseout = () => + (menuBtn.style.background = "rgba(255,255,255,0.2)"); + const headerRight = document.createElement("div"); headerRight.style.cssText = `display: flex; gap: 8px; align-items: center;`; headerRight.appendChild(menuBtn); @@ -1165,11 +1875,11 @@ async function openUncloseaiEmbeddedModalNew() { // Model selection const modelSection = document.createElement("div"); modelSection.style.cssText = `margin-bottom: 16px;`; - + const modelLabel = document.createElement("label"); - modelLabel.textContent = "AI Model:"; + modelLabel.textContent = getUIText("modelLabel"); modelLabel.style.cssText = `display: block; margin-bottom: 8px; font-weight: 600; color: ${colors.primaryText};`; - + const modelSelect = document.createElement("select"); modelSelect.style.cssText = ` width: 100%; @@ -1180,10 +1890,10 @@ async function openUncloseaiEmbeddedModalNew() { background: ${colors.inputBackground}; color: ${colors.primaryText}; `; - + // Store models for selection handler let loadedModels = []; - + // Load models const loadModels = async () => { try { @@ -1191,32 +1901,39 @@ async function openUncloseaiEmbeddedModalNew() { const models = await fetchModelsFromEndpoints(); console.log("Models loaded:", models); loadedModels = models; // Store for selection handler - modelSelect.innerHTML = ''; - + modelSelect.innerHTML = ""; + if (models && models.length > 0) { - models.forEach(model => { + models.forEach((model) => { const option = document.createElement("option"); option.value = model.uniqueId; option.textContent = `${model.endpointId} | ${model.modelName}`; modelSelect.appendChild(option); }); - + // Load saved model from localStorage or use current selection - const savedModel = localStorage.getItem('selectedModel'); - const savedEndpoint = localStorage.getItem('selectedEndpoint'); + const savedModel = localStorage.getItem("selectedModel"); + const savedEndpoint = localStorage.getItem("selectedEndpoint"); const currentModel = getSelectedModel(); - + if (savedModel && savedEndpoint) { - const savedOption = Array.from(modelSelect.options).find(opt => - opt.textContent.includes(savedModel) && opt.textContent.includes(savedEndpoint) + const savedOption = Array.from(modelSelect.options).find( + (opt) => + opt.textContent.includes(savedModel) && + opt.textContent.includes(savedEndpoint), ); if (savedOption) { modelSelect.value = savedOption.value; - console.log("Restored saved model:", savedModel, "from endpoint:", savedEndpoint); + console.log( + "Restored saved model:", + savedModel, + "from endpoint:", + savedEndpoint, + ); } } else if (currentModel) { - const currentOption = Array.from(modelSelect.options).find(opt => - opt.textContent.includes(currentModel) + const currentOption = Array.from(modelSelect.options).find((opt) => + opt.textContent.includes(currentModel), ); if (currentOption) { modelSelect.value = currentOption.value; @@ -1236,30 +1953,32 @@ async function openUncloseaiEmbeddedModalNew() { modelSelect.appendChild(option); } }; - + // Handle model selection changes modelSelect.onchange = () => { const selectedUniqueId = modelSelect.value; - const selectedModel = loadedModels.find(model => model.uniqueId === selectedUniqueId); + const selectedModel = loadedModels.find( + (model) => model.uniqueId === selectedUniqueId, + ); if (selectedModel) { // Update the global model selection - localStorage.setItem('selectedModel', selectedModel.modelName); - localStorage.setItem('selectedEndpoint', selectedModel.endpointId); + localStorage.setItem("selectedModel", selectedModel.modelName); + localStorage.setItem("selectedEndpoint", selectedModel.endpointId); console.log("Model changed to:", selectedModel); } }; - + modelSection.appendChild(modelLabel); modelSection.appendChild(modelSelect); // Voice selection for TTS const voiceSection = document.createElement("div"); voiceSection.style.cssText = `margin-bottom: 16px;`; - + const voiceLabel = document.createElement("label"); - voiceLabel.textContent = "TTS Voice:"; + voiceLabel.textContent = getUIText("voiceLabel"); voiceLabel.style.cssText = `display: block; margin-bottom: 8px; font-weight: 600; color: ${colors.primaryText};`; - + const voiceSelect = document.createElement("select"); voiceSelect.id = "hermes-voice-select"; voiceSelect.style.cssText = ` @@ -1271,7 +1990,7 @@ async function openUncloseaiEmbeddedModalNew() { background: ${colors.inputBackground}; color: ${colors.primaryText}; `; - + const voices = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"]; voices.forEach((voice, index) => { const option = document.createElement("option"); @@ -1279,29 +1998,88 @@ async function openUncloseaiEmbeddedModalNew() { option.textContent = voice.charAt(0).toUpperCase() + voice.slice(1); voiceSelect.appendChild(option); }); - + // Load saved voice from localStorage or default to alloy - const savedVoice = localStorage.getItem('selectedVoice') || 'alloy'; + const savedVoice = localStorage.getItem("selectedVoice") || "alloy"; voiceSelect.value = savedVoice; console.log("Voices loaded:", voices.length, "voices, selected:", savedVoice); - + // Save voice selection to localStorage when changed voiceSelect.onchange = () => { - localStorage.setItem('selectedVoice', voiceSelect.value); + localStorage.setItem("selectedVoice", voiceSelect.value); console.log("Voice changed to:", voiceSelect.value); }; - + voiceSection.appendChild(voiceLabel); voiceSection.appendChild(voiceSelect); + // Language preference section + const languageSection = document.createElement("div"); + languageSection.style.cssText = `margin-bottom: 16px;`; + + const languageLabel = document.createElement("label"); + languageLabel.textContent = getUIText("languageLabel"); + languageLabel.style.cssText = `display: block; margin-bottom: 8px; font-weight: 600; color: ${colors.primaryText};`; + + // Create language selector + const { SUPPORTED_LANGUAGES, NATIVE_LANGUAGE_NAMES } = await import( + "./translation.js" + ); + const currentLang = getUserLanguagePreference(); + + const languageSelect = document.createElement("select"); + languageSelect.id = "hermes-language-select"; + languageSelect.style.cssText = ` + width: 100%; + padding: 8px 12px; + border: 1px solid ${colors.borderColor}; + border-radius: 6px; + font-size: 14px; + background: ${colors.inputBackground}; + color: ${colors.primaryText}; + `; + + // Add options for each supported language + Object.entries(SUPPORTED_LANGUAGES).forEach(([code, englishName]) => { + const option = document.createElement("option"); + option.value = code; + const nativeName = NATIVE_LANGUAGE_NAMES[code]; + option.textContent = `${nativeName} (${englishName})`; + + if (code === currentLang) { + option.selected = true; + } + + languageSelect.appendChild(option); + }); + + // Handle language change + languageSelect.onchange = () => { + const newLang = languageSelect.value; + setUserLanguagePreference(newLang); + + // Show confirmation + const selectedOption = languageSelect.options[languageSelect.selectedIndex]; + console.log(`Language changed to: ${selectedOption.textContent}`); + + // Notify user to refresh for full effect + setTimeout(() => { + const langName = SUPPORTED_LANGUAGES[newLang]; + alert(getUIText("languageChanged", { lang: langName })); + }, 100); + }; + + languageSection.appendChild(languageLabel); + languageSection.appendChild(languageSelect); + // Action buttons const actionsSection = document.createElement("div"); actionsSection.style.cssText = `margin-bottom: 16px;`; - + const actionsLabel = document.createElement("div"); - actionsLabel.textContent = "Quick Actions:"; + actionsLabel.textContent = getUIText("quickActions"); actionsLabel.style.cssText = `margin-bottom: 8px; font-weight: 600; color: #495057;`; - + const actionsGrid = document.createElement("div"); actionsGrid.style.cssText = ` display: grid; @@ -1310,22 +2088,25 @@ async function openUncloseaiEmbeddedModalNew() { `; const actions = [ - { text: "🔄 Refresh Models", action: loadModels }, - { text: "🗑️ Clear Chat", action: async () => { - if (confirm("Clear all conversation history?")) { - chatBox.innerHTML = ''; - clearConversationHistory(); - - // Also clear the chat history in the chat.js module - const systemMsg = chatHistory.find(msg => msg.role === 'system'); - chatHistory.length = 0; // Clear array - if (systemMsg) chatHistory.push(systemMsg); - console.log("Chat history cleared, reset to system message only"); - - // Re-add intro message - await addIntroMessage(); - } - }} + { text: getUIText("refreshModels"), action: loadModels }, + { + text: getUIText("clearChat"), + action: async () => { + if (confirm(getUIText("clearChatConfirm"))) { + chatBox.innerHTML = ""; + clearConversationHistory(); + + // Also clear the chat history in the chat.js module + const systemMsg = chatHistory.find((msg) => msg.role === "system"); + chatHistory.length = 0; // Clear array + if (systemMsg) chatHistory.push(systemMsg); + console.log("Chat history cleared, reset to system message only"); + + // Re-add intro message + await addIntroMessage(); + } + }, + }, ]; actions.forEach(({ text, action }) => { @@ -1353,27 +2134,32 @@ async function openUncloseaiEmbeddedModalNew() { btn.onclick = action; actionsGrid.appendChild(btn); }); - + actionsSection.appendChild(actionsLabel); actionsSection.appendChild(actionsGrid); settingsPanel.appendChild(modelSection); settingsPanel.appendChild(voiceSection); + settingsPanel.appendChild(languageSection); settingsPanel.appendChild(actionsSection); // Load and save settings panel state - let settingsOpen = localStorage.getItem('hermesSettingsOpen') === 'true'; - settingsPanel.style.display = settingsOpen ? 'block' : 'none'; - menuBtn.style.background = settingsOpen ? 'rgba(255,255,255,0.3)' : 'rgba(255,255,255,0.2)'; - + let settingsOpen = localStorage.getItem("hermesSettingsOpen") === "true"; + settingsPanel.style.display = settingsOpen ? "block" : "none"; + menuBtn.style.background = settingsOpen + ? "rgba(255,255,255,0.3)" + : "rgba(255,255,255,0.2)"; + menuBtn.onclick = () => { settingsOpen = !settingsOpen; - settingsPanel.style.display = settingsOpen ? 'block' : 'none'; - menuBtn.style.background = settingsOpen ? 'rgba(255,255,255,0.3)' : 'rgba(255,255,255,0.2)'; - localStorage.setItem('hermesSettingsOpen', settingsOpen.toString()); + settingsPanel.style.display = settingsOpen ? "block" : "none"; + menuBtn.style.background = settingsOpen + ? "rgba(255,255,255,0.3)" + : "rgba(255,255,255,0.2)"; + localStorage.setItem("hermesSettingsOpen", settingsOpen.toString()); }; - // Quick action buttons + // Quick action buttons const controls = document.createElement("div"); controls.style.cssText = ` flex-shrink: 0; @@ -1390,52 +2176,74 @@ async function openUncloseaiEmbeddedModalNew() { // HTML to Markdown converter function function htmlToMarkdown(html) { // Create a temporary element to parse HTML - const temp = document.createElement('div'); + const temp = document.createElement("div"); temp.innerHTML = html; - + // Remove script and style elements - temp.querySelectorAll('script, style, noscript').forEach(el => el.remove()); - - let markdown = ''; - + temp + .querySelectorAll("script, style, noscript") + .forEach((el) => el.remove()); + + const markdown = ""; + function processNode(node) { if (node.nodeType === Node.TEXT_NODE) { return node.textContent.trim(); } - - if (node.nodeType !== Node.ELEMENT_NODE) return ''; - + + if (node.nodeType !== Node.ELEMENT_NODE) return ""; + const tag = node.tagName.toLowerCase(); - const children = Array.from(node.childNodes).map(processNode).join(''); - + const children = Array.from(node.childNodes).map(processNode).join(""); + switch (tag) { - case 'h1': return `# ${children}\n\n`; - case 'h2': return `## ${children}\n\n`; - case 'h3': return `### ${children}\n\n`; - case 'h4': return `#### ${children}\n\n`; - case 'h5': return `##### ${children}\n\n`; - case 'h6': return `###### ${children}\n\n`; - case 'p': return `${children}\n\n`; - case 'br': return '\n'; - case 'strong': case 'b': return `**${children}**`; - case 'em': case 'i': return `*${children}*`; - case 'code': return `\`${children}\``; - case 'pre': return `\`\`\`\n${children}\n\`\`\`\n\n`; - case 'a': - const href = node.getAttribute('href'); + case "h1": + return `# ${children}\n\n`; + case "h2": + return `## ${children}\n\n`; + case "h3": + return `### ${children}\n\n`; + case "h4": + return `#### ${children}\n\n`; + case "h5": + return `##### ${children}\n\n`; + case "h6": + return `###### ${children}\n\n`; + case "p": + return `${children}\n\n`; + case "br": + return "\n"; + case "strong": + case "b": + return `**${children}**`; + case "em": + case "i": + return `*${children}*`; + case "code": + return `\`${children}\``; + case "pre": + return `\`\`\`\n${children}\n\`\`\`\n\n`; + case "a": { + const href = node.getAttribute("href"); return href ? `[${children}](${href})` : children; - case 'img': - const src = node.getAttribute('src'); - const alt = node.getAttribute('alt') || ''; - return src ? `![${alt}](${src})` : ''; - case 'ul': case 'ol': + } + case "img": { + const src = node.getAttribute("src"); + const alt = node.getAttribute("alt") || ""; + return src ? `![${alt}](${src})` : ""; + } + case "ul": + case "ol": return `${children}\n`; - case 'li': return `- ${children}\n`; - case 'blockquote': return `> ${children}\n\n`; - default: return children; + case "li": + return `- ${children}\n`; + case "blockquote": + return `> ${children}\n\n`; + default: + return children; } } - + return processNode(temp).trim(); } @@ -1444,81 +2252,87 @@ async function openUncloseaiEmbeddedModalNew() { let readPageContainer = null; const controlActions = [ - { text: "📖 Read Page", action: async (button) => { - // If already playing, pause - if (readPageAudio && !readPageAudio.paused) { - readPageAudio.pause(); - button.textContent = "📖 Read Page"; - isReadingPage = false; - return; - } - - // If paused, resume - if (readPageAudio && readPageAudio.paused && readPageAudio.currentTime > 0) { - readPageAudio.play(); - button.textContent = "⏸️ Pause"; - isReadingPage = true; - return; - } - - // Prevent multiple simultaneous requests - if (isReadingPage) return; - - // Start new reading - button.textContent = "⏳ Processing..."; - button.disabled = true; - isReadingPage = true; - - try { - // Get page content - const { extractWebpageContent } = await import('./content.js'); - const pageContent = extractWebpageContent(); - - // Generate TTS - const { speakText } = await import('./tts.js'); - const result = await speakText(pageContent, 'alloy', 1.0); - readPageAudio = result.audio; - - // Set up audio event handlers - readPageAudio.onplay = () => { - button.textContent = "⏸️ Pause"; - isReadingPage = true; - }; - - readPageAudio.onpause = () => { - button.textContent = "▶️ Resume"; - }; - - readPageAudio.onended = () => { + { + text: "📖 Read Page", + action: async (button) => { + // If already playing, pause + if (readPageAudio && !readPageAudio.paused) { + readPageAudio.pause(); button.textContent = "📖 Read Page"; isReadingPage = false; - readPageAudio = null; - // Remove download button container if it exists - if (readPageContainer) { - readPageContainer.remove(); - readPageContainer = null; - } - }; - - // Create download button next to the read button - if (!readPageContainer) { - readPageContainer = document.createElement('div'); - readPageContainer.style.cssText = ` + return; + } + + // If paused, resume + if ( + readPageAudio && + readPageAudio.paused && + readPageAudio.currentTime > 0 + ) { + readPageAudio.play(); + button.textContent = "⏸️ Pause"; + isReadingPage = true; + return; + } + + // Prevent multiple simultaneous requests + if (isReadingPage) return; + + // Start new reading + button.textContent = "⏳ Processing..."; + button.disabled = true; + isReadingPage = true; + + try { + // Get page content + const { extractWebpageContent } = await import("./content.js"); + const pageContent = extractWebpageContent(); + + // Generate TTS + const { speakText } = await import("./tts.js"); + const result = await speakText(pageContent, "alloy", 1.0); + readPageAudio = result.audio; + + // Set up audio event handlers + readPageAudio.onplay = () => { + button.textContent = "⏸️ Pause"; + isReadingPage = true; + }; + + readPageAudio.onpause = () => { + button.textContent = "▶️ Resume"; + }; + + readPageAudio.onended = () => { + button.textContent = "📖 Read Page"; + isReadingPage = false; + readPageAudio = null; + // Remove download button container if it exists + if (readPageContainer) { + readPageContainer.remove(); + readPageContainer = null; + } + }; + + // Create download button next to the read button + if (!readPageContainer) { + readPageContainer = document.createElement("div"); + readPageContainer.style.cssText = ` display: inline-flex; gap: 8px; align-items: center; `; - - // Move the read button into the container - const parent = button.parentElement; - parent.insertBefore(readPageContainer, button); - readPageContainer.appendChild(button); - - // Add download button - const downloadBtn = document.createElement('button'); - downloadBtn.textContent = '💾'; - downloadBtn.title = 'Download audio'; - downloadBtn.style.cssText = ` + + // Move the read button into the container + const parent = button.parentElement; + parent.insertBefore(readPageContainer, button); + readPageContainer.appendChild(button); + + // Add download button + const downloadBtn = document.createElement("button"); + downloadBtn.textContent = "💾"; + downloadBtn.title = "Download audio"; + downloadBtn.style.cssText = ` padding: 8px 16px; background: ${colors.buttonBackground}; border: 1px solid ${colors.buttonBorder}; @@ -1529,70 +2343,81 @@ async function openUncloseaiEmbeddedModalNew() { color: ${colors.primaryText} !important; transition: all 0.2s; `; - downloadBtn.onmouseover = () => { - downloadBtn.style.background = colors.buttonHover; - downloadBtn.style.borderColor = colors.buttonBorder; - }; - downloadBtn.onmouseout = () => { - downloadBtn.style.background = colors.buttonBackground; - downloadBtn.style.borderColor = colors.buttonBorder; - }; - downloadBtn.onclick = () => { - if (readPageAudio) { - const url = readPageAudio.src; - const a = document.createElement('a'); - a.href = url; - a.download = 'page-audio.mp3'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - } - }; - - readPageContainer.appendChild(downloadBtn); - } - - // Auto-play the generated audio - try { - await readPageAudio.play(); - } catch (playError) { + downloadBtn.onmouseover = () => { + downloadBtn.style.background = colors.buttonHover; + downloadBtn.style.borderColor = colors.buttonBorder; + }; + downloadBtn.onmouseout = () => { + downloadBtn.style.background = colors.buttonBackground; + downloadBtn.style.borderColor = colors.buttonBorder; + }; + downloadBtn.onclick = () => { + if (readPageAudio) { + const url = readPageAudio.src; + const a = document.createElement("a"); + a.href = url; + a.download = "page-audio.mp3"; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } + }; + + readPageContainer.appendChild(downloadBtn); + } + + // Auto-play the generated audio + try { + await readPageAudio.play(); + } catch (playError) { + button.textContent = "📖 Read Page"; + isReadingPage = false; + } + } catch (error) { + alert("Failed to read page: " + error.message); button.textContent = "📖 Read Page"; isReadingPage = false; + } finally { + button.disabled = false; } - } catch (error) { - alert("Failed to read page: " + error.message); - button.textContent = "📖 Read Page"; - isReadingPage = false; - } finally { - button.disabled = false; - } - }}, + }, + }, { text: "🔊 TTS Anything", action: (btn) => window.openTTSModal() }, - { text: "🌐 Translation Modal", action: (btn) => window.openTranslateModal() }, - { text: "🤖 Smart Translate", action: async (btn) => { - // Create dropdown instead of opening modal - if (btn.nextElementSibling && btn.nextElementSibling.classList.contains('translate-dropdown')) { - // Toggle existing dropdown - btn.nextElementSibling.remove(); - return; - } - - // Detect page language - btn.textContent = "🔍 Detecting..."; - btn.disabled = true; - - try { - const currentLang = await detectPageLanguage(); - btn.textContent = "🤖 Smart Translate"; - btn.disabled = false; - - // Import supported languages - const { SUPPORTED_LANGUAGES, translateCurrentPage } = await import('./translation.js'); - - // Create dropdown - const dropdown = document.createElement('div'); - dropdown.className = 'translate-dropdown'; - dropdown.style.cssText = ` + { + text: "🌐 Translation Modal", + action: (btn) => window.openTranslateModal(), + }, + { + text: "🤖 Smart Translate", + action: async (btn) => { + // Create dropdown instead of opening modal + if ( + btn.nextElementSibling && + btn.nextElementSibling.classList.contains("translate-dropdown") + ) { + // Toggle existing dropdown + btn.nextElementSibling.remove(); + return; + } + + // Detect page language + btn.textContent = "🔍 Detecting..."; + btn.disabled = true; + + try { + const currentLang = await detectPageLanguage(); + btn.textContent = "🤖 Smart Translate"; + btn.disabled = false; + + // Import supported languages + const { SUPPORTED_LANGUAGES, translateCurrentPage } = await import( + "./translation.js" + ); + + // Create dropdown + const dropdown = document.createElement("div"); + dropdown.className = "translate-dropdown"; + dropdown.style.cssText = ` position: absolute; top: 100%; left: 0; @@ -1606,138 +2431,150 @@ async function openUncloseaiEmbeddedModalNew() { overflow-y: auto; margin-top: 4px; `; - - // Add current language indicator - const currentLangDiv = document.createElement('div'); - currentLangDiv.style.cssText = ` + + // Add current language indicator + const currentLangDiv = document.createElement("div"); + currentLangDiv.style.cssText = ` padding: 8px 12px; font-size: 12px; color: ${colors.mutedText}; border-bottom: 1px solid ${colors.dividerColor}; background: ${colors.contentBackground}; `; - const currentLangName = SUPPORTED_LANGUAGES[currentLang] || 'Unknown'; - currentLangDiv.textContent = `Current: ${currentLangName} (${currentLang})`; - dropdown.appendChild(currentLangDiv); - - // Add translation options - Object.entries(SUPPORTED_LANGUAGES).forEach(([code, name]) => { - // Skip current language - if (code === currentLang) return; - - const option = document.createElement('div'); - option.style.cssText = ` + const currentLangName = SUPPORTED_LANGUAGES[currentLang] || "Unknown"; + currentLangDiv.textContent = `Current: ${currentLangName} (${currentLang})`; + dropdown.appendChild(currentLangDiv); + + // Add translation options + Object.entries(SUPPORTED_LANGUAGES).forEach(([code, name]) => { + // Skip current language + if (code === currentLang) return; + + const option = document.createElement("div"); + option.style.cssText = ` padding: 8px 12px; cursor: pointer; transition: background-color 0.2s; color: ${colors.primaryText}; font-size: 14px; `; - option.textContent = `${name} (${code})`; - - option.onmouseenter = () => { - option.style.background = colors.buttonHover; - }; - option.onmouseleave = () => { - option.style.background = 'transparent'; - }; - - option.onclick = async () => { - try { - // Show loading - option.textContent = `Translating to ${name}...`; - option.style.pointerEvents = 'none'; - - // Translate the page - const translatedHtml = await translateCurrentPage(code); - - // Open in new tab - const newWindow = window.open('', '_blank'); - newWindow.document.write(translatedHtml); - newWindow.document.close(); - newWindow.document.title = `${document.title} (${name})`; - - // Close dropdown + option.textContent = `${name} (${code})`; + + option.onmouseenter = () => { + option.style.background = colors.buttonHover; + }; + option.onmouseleave = () => { + option.style.background = "transparent"; + }; + + option.onclick = async () => { + try { + // Show loading + option.textContent = `Translating to ${name}...`; + option.style.pointerEvents = "none"; + + // Translate the page + const translatedHtml = await translateCurrentPage(code); + + // Open in new tab + const newWindow = window.open("", "_blank"); + newWindow.document.write(translatedHtml); + newWindow.document.close(); + newWindow.document.title = `${document.title} (${name})`; + + // Close dropdown + dropdown.remove(); + } catch (error) { + alert(`Translation failed: ${error.message}`); + option.textContent = `${name} (${code})`; + option.style.pointerEvents = "auto"; + } + }; + + dropdown.appendChild(option); + }); + + // Position dropdown relative to button + btn.style.position = "relative"; + btn.parentElement.appendChild(dropdown); + + // Close dropdown when clicking outside + const closeDropdown = (e) => { + if (!dropdown.contains(e.target) && e.target !== btn) { dropdown.remove(); - - } catch (error) { - alert(`Translation failed: ${error.message}`); - option.textContent = `${name} (${code})`; - option.style.pointerEvents = 'auto'; + document.removeEventListener("click", closeDropdown); } }; - - dropdown.appendChild(option); - }); - - // Position dropdown relative to button - btn.style.position = 'relative'; - btn.parentElement.appendChild(dropdown); - - // Close dropdown when clicking outside - const closeDropdown = (e) => { - if (!dropdown.contains(e.target) && e.target !== btn) { - dropdown.remove(); - document.removeEventListener('click', closeDropdown); - } - }; - setTimeout(() => document.addEventListener('click', closeDropdown), 100); - - } catch (error) { - btn.textContent = "🤖 Smart Translate"; - btn.disabled = false; - alert(`Language detection failed: ${error.message}`); - } - }}, - { text: "📋 Full Chat Raw", action: async () => { - try { - const { getChatHistory } = await import('./chat.js'); - const history = getChatHistory(); - let rawContent = ''; - history.forEach(msg => { - if (msg.role === 'user') { - rawContent += `**You:** ${msg.content}\n\n`; - } else if (msg.role === 'assistant') { - rawContent += `**AI:** ${msg.content}\n\n`; - } - }); - await navigator.clipboard.writeText(rawContent); - alert("Full chat copied as raw markdown!"); - } catch (error) { - alert("Failed to copy: " + error.message); - } - }}, - { text: "📄 Full Chat HTML", action: async () => { - try { - const { getChatHistory } = await import('./chat.js'); - const history = getChatHistory(); - let htmlContent = '
'; - history.forEach(msg => { - if (msg.role === 'user') { - htmlContent += `
You:
${marked.parse(msg.content)}
`; - } else if (msg.role === 'assistant') { - htmlContent += `
AI:
${marked.parse(msg.content)}
`; - } - }); - htmlContent += '
'; - await navigator.clipboard.writeText(htmlContent); - alert("Full chat copied as HTML!"); - } catch (error) { - alert("Failed to copy: " + error.message); - } - }}, - { text: "📝 Full Page Raw", action: async () => { - try { - // Get the entire page HTML - const pageHtml = document.documentElement.outerHTML; - // Convert to markdown - const markdown = htmlToMarkdown(pageHtml); - await navigator.clipboard.writeText(markdown); - alert("Full page copied as markdown!"); - } catch (error) { - alert("Failed to copy page: " + error.message); - } - }} + setTimeout( + () => document.addEventListener("click", closeDropdown), + 100, + ); + } catch (error) { + btn.textContent = "🤖 Smart Translate"; + btn.disabled = false; + alert(`Language detection failed: ${error.message}`); + } + }, + }, + { + text: "📋 Full Chat Raw", + action: async () => { + try { + const { getChatHistory } = await import("./chat.js"); + const history = getChatHistory(); + let rawContent = ""; + history.forEach((msg) => { + if (msg.role === "user") { + rawContent += `**You:** ${msg.content}\n\n`; + } else if (msg.role === "assistant") { + rawContent += `**AI:** ${msg.content}\n\n`; + } + }); + await navigator.clipboard.writeText(rawContent); + alert("Full chat copied as raw markdown!"); + } catch (error) { + alert("Failed to copy: " + error.message); + } + }, + }, + { + text: "📄 Full Chat HTML", + action: async () => { + try { + const { getChatHistory } = await import("./chat.js"); + const history = getChatHistory(); + let htmlContent = + '
'; + history.forEach((msg) => { + if (msg.role === "user") { + htmlContent += `
You:
${marked.parse(msg.content)}
`; + } else if (msg.role === "assistant") { + htmlContent += `
AI:
${marked.parse(msg.content)}
`; + } + }); + htmlContent += "
"; + await navigator.clipboard.writeText(htmlContent); + alert("Full chat copied as HTML!"); + } catch (error) { + alert("Failed to copy: " + error.message); + } + }, + }, + { + text: "📝 Full Page Raw", + action: async () => { + try { + // Get the entire page HTML + const pageHtml = document.documentElement.outerHTML; + // Convert to markdown + const markdown = htmlToMarkdown(pageHtml); + await navigator.clipboard.writeText(markdown); + alert("Full page copied as markdown!"); + } catch (error) { + alert("Failed to copy page: " + error.message); + } + }, + }, ]; controlActions.forEach(({ text, action }) => { @@ -1803,13 +2640,13 @@ async function openUncloseaiEmbeddedModalNew() { color: ${colors.primaryText}; transition: border-color 0.2s; `; - input.onfocus = () => input.style.borderColor = colors.focusBorder; - input.onblur = () => input.style.borderColor = colors.borderColor; - + input.onfocus = () => (input.style.borderColor = colors.focusBorder); + input.onblur = () => (input.style.borderColor = colors.borderColor); + // Auto-resize textarea input.oninput = () => { - input.style.height = 'auto'; - input.style.height = Math.min(input.scrollHeight, 120) + 'px'; + input.style.height = "auto"; + input.style.height = Math.min(input.scrollHeight, 120) + "px"; }; const sendBtn = document.createElement("button"); @@ -1831,16 +2668,16 @@ async function openUncloseaiEmbeddedModalNew() { transition: transform 0.2s; flex-shrink: 0; `; - sendBtn.onmouseover = () => sendBtn.style.transform = 'scale(1.05)'; - sendBtn.onmouseout = () => sendBtn.style.transform = 'scale(1)'; + sendBtn.onmouseover = () => (sendBtn.style.transform = "scale(1.05)"); + sendBtn.onmouseout = () => (sendBtn.style.transform = "scale(1)"); // Handle message sending const sendMessageHandler = async () => { const message = input.value.trim(); if (!message) return; - input.value = ''; - input.style.height = '44px'; + input.value = ""; + input.style.height = "44px"; // Add user message const userMsg = document.createElement("div"); @@ -1853,12 +2690,12 @@ async function openUncloseaiEmbeddedModalNew() { max-width: 80%; word-wrap: break-word; `; - + // Add message text and actions inside the bubble const messageText = document.createElement("div"); messageText.textContent = message; messageText.style.cssText = `margin-bottom: 8px;`; - + const userMessageActions = document.createElement("div"); userMessageActions.style.cssText = ` display: flex; @@ -1866,7 +2703,7 @@ async function openUncloseaiEmbeddedModalNew() { opacity: 0.8; justify-content: flex-end; `; - + const userTtsBtn = document.createElement("button"); userTtsBtn.textContent = "🔊"; userTtsBtn.title = "Read aloud"; @@ -1886,15 +2723,15 @@ async function openUncloseaiEmbeddedModalNew() { userTtsBtn.onmouseleave = () => { userTtsBtn.style.background = colors.actionBg; }; - + // Store audio for pause/resume functionality let userAudio = null; let isPlaying = false; - + userTtsBtn.onclick = async () => { - const modalVoiceSelect = document.getElementById('hermes-voice-select'); - const selectedVoice = modalVoiceSelect?.value || 'alloy'; - + const modalVoiceSelect = document.getElementById("hermes-voice-select"); + const selectedVoice = modalVoiceSelect?.value || "alloy"; + // If audio is already playing, pause it if (userAudio && !userAudio.paused) { userAudio.pause(); @@ -1902,7 +2739,7 @@ async function openUncloseaiEmbeddedModalNew() { isPlaying = false; return; } - + // If audio exists and is paused, resume it if (userAudio && userAudio.paused && userAudio.currentTime > 0) { userAudio.play(); @@ -1910,33 +2747,33 @@ async function openUncloseaiEmbeddedModalNew() { isPlaying = true; return; } - + // Generate new audio const originalText = userTtsBtn.textContent; userTtsBtn.textContent = "⏳"; userTtsBtn.disabled = true; - + try { const result = await speakChatText(message, selectedVoice, 1.0); userAudio = result.audio; - + // Set up audio event handlers userAudio.onplay = () => { userTtsBtn.textContent = "⏸️"; isPlaying = true; }; - + userAudio.onpause = () => { userTtsBtn.textContent = "🔊"; isPlaying = false; }; - + userAudio.onended = () => { userTtsBtn.textContent = "🔊"; isPlaying = false; userAudio = null; }; - + // Auto-play the generated audio try { await userAudio.play(); @@ -1973,13 +2810,15 @@ async function openUncloseaiEmbeddedModalNew() { userDeleteBtn.onclick = () => { // Remove from chat history const currentHistory = getChatHistory(); - const updatedHistory = currentHistory.filter(msg => !(msg.role === 'user' && msg.content === message)); + const updatedHistory = currentHistory.filter( + (msg) => !(msg.role === "user" && msg.content === message), + ); updateChatHistory(updatedHistory); saveConversationHistory(updatedHistory); - + userMsg.remove(); }; - + userMessageActions.appendChild(userTtsBtn); userMessageActions.appendChild(userDeleteBtn); userMsg.appendChild(messageText); @@ -1999,28 +2838,38 @@ async function openUncloseaiEmbeddedModalNew() { `; aiMsg.innerHTML = `Thinking...`; chatBox.appendChild(aiMsg); - + chatArea.scrollTop = chatArea.scrollHeight; try { - let response = ''; + let response = ""; for await (const chunk of sendMessage(message)) { response += chunk; aiMsg.innerHTML = marked.parse(response); addCodeBlockCopyButtons(aiMsg); } - + // Add the AI response to chat history manually since sendMessage generator doesn't do it - const { getChatHistory, updateChatHistory } = await import('./chat.js'); + const { getChatHistory, updateChatHistory } = await import("./chat.js"); const currentHistory = getChatHistory(); - currentHistory.push({ role: 'assistant', content: response }); + currentHistory.push({ role: "assistant", content: response }); updateChatHistory(currentHistory); - + // Save conversation history after successful response saveConversationHistory(currentHistory); - console.log("Conversation history saved:", currentHistory.length, "messages"); - console.log("Saved history content:", currentHistory.map(msg => ({ role: msg.role, content: msg.content.substring(0, 50) + '...' }))); - + console.log( + "Conversation history saved:", + currentHistory.length, + "messages", + ); + console.log( + "Saved history content:", + currentHistory.map((msg) => ({ + role: msg.role, + content: msg.content.substring(0, 50) + "...", + })), + ); + // Add TTS and delete buttons to AI message const messageActions = document.createElement("div"); messageActions.style.cssText = ` @@ -2029,7 +2878,7 @@ async function openUncloseaiEmbeddedModalNew() { gap: 8px; opacity: 0.7; `; - + const ttsBtn = document.createElement("button"); ttsBtn.textContent = "🔊"; ttsBtn.title = "Read aloud"; @@ -2046,12 +2895,12 @@ async function openUncloseaiEmbeddedModalNew() { // Store audio for pause/resume functionality let currentAudio = null; let isPlaying = false; - + ttsBtn.onclick = async () => { // Find voice selection from the modal by ID - const modalVoiceSelect = document.getElementById('hermes-voice-select'); - const selectedVoice = modalVoiceSelect?.value || 'alloy'; - + const modalVoiceSelect = document.getElementById("hermes-voice-select"); + const selectedVoice = modalVoiceSelect?.value || "alloy"; + // If audio is already playing, pause it if (currentAudio && !currentAudio.paused) { currentAudio.pause(); @@ -2060,43 +2909,47 @@ async function openUncloseaiEmbeddedModalNew() { console.log("🔊 CHAT TTS: Paused"); return; } - + // If audio exists and is paused, resume it - if (currentAudio && currentAudio.paused && currentAudio.currentTime > 0) { + if ( + currentAudio && + currentAudio.paused && + currentAudio.currentTime > 0 + ) { currentAudio.play(); ttsBtn.textContent = "⏸️"; isPlaying = true; console.log("🔊 CHAT TTS: Resumed"); return; } - + // Generate new audio console.log("🔊 CHAT TTS: Generating new audio"); const originalText = ttsBtn.textContent; ttsBtn.textContent = "⏳"; ttsBtn.disabled = true; - + try { const result = await speakChatText(response, selectedVoice, 1.0); currentAudio = result.audio; - + // Set up audio event handlers currentAudio.onplay = () => { ttsBtn.textContent = "⏸️"; isPlaying = true; }; - + currentAudio.onpause = () => { ttsBtn.textContent = "🔊"; isPlaying = false; }; - + currentAudio.onended = () => { ttsBtn.textContent = "🔊"; isPlaying = false; currentAudio = null; }; - + // Auto-play the generated audio try { await currentAudio.play(); @@ -2113,7 +2966,7 @@ async function openUncloseaiEmbeddedModalNew() { ttsBtn.disabled = false; } }; - + const deleteBtn = document.createElement("button"); deleteBtn.textContent = "🗑️"; deleteBtn.title = "Delete message"; @@ -2131,7 +2984,7 @@ async function openUncloseaiEmbeddedModalNew() { userMsg.remove(); aiMsg.remove(); }; - + // Copy Raw (Markdown) button const copyRawBtn = document.createElement("button"); copyRawBtn.textContent = "📋"; @@ -2158,7 +3011,7 @@ async function openUncloseaiEmbeddedModalNew() { alert("Failed to copy: " + error.message); } }; - + // Copy HTML button const copyHtmlBtn = document.createElement("button"); copyHtmlBtn.textContent = "📄"; @@ -2186,23 +3039,22 @@ async function openUncloseaiEmbeddedModalNew() { alert("Failed to copy: " + error.message); } }; - + messageActions.appendChild(ttsBtn); messageActions.appendChild(copyRawBtn); messageActions.appendChild(copyHtmlBtn); messageActions.appendChild(deleteBtn); aiMsg.appendChild(messageActions); - } catch (error) { aiMsg.innerHTML = `Error: ${error.message}`; } - + chatArea.scrollTop = chatArea.scrollHeight; }; sendBtn.onclick = sendMessageHandler; input.onkeydown = (e) => { - if (e.key === 'Enter' && !e.shiftKey) { + if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessageHandler(); } @@ -2215,8 +3067,8 @@ async function openUncloseaiEmbeddedModalNew() { document.body.appendChild(modal); modal.showModal(); - - modal.addEventListener('click', (e) => { + + modal.addEventListener("click", (e) => { if (e.target === modal) { modal.close(); document.body.removeChild(modal); @@ -2229,36 +3081,36 @@ async function openUncloseaiEmbeddedModalNew() { const newTheme = detectCurrentTheme(); const newColors = getThemeColors(newTheme); modal.setAttribute("data-theme", newTheme); - + // Update modal background and main color modal.style.background = newColors.modalBackground; modal.style.color = newColors.primaryText; - + // Update chat area background chatArea.style.background = newColors.contentBackground; - + // Update settings panel background settingsPanel.style.background = newColors.panelBackground; settingsPanel.style.borderTopColor = newColors.dividerColor; settingsPanel.style.borderBottomColor = newColors.dividerColor; - + // Update input area inputArea.style.background = newColors.panelBackground; input.style.background = newColors.inputBackground; input.style.color = newColors.primaryText; input.style.borderColor = newColors.borderColor; - + // Update controls panel controls.style.background = newColors.panelBackground; controls.style.borderTopColor = newColors.dividerColor; controls.style.borderBottomColor = newColors.dividerColor; - + // Update all control buttons - controls.querySelectorAll('button').forEach(btn => { + controls.querySelectorAll("button").forEach((btn) => { btn.style.background = newColors.buttonBackground; btn.style.borderColor = newColors.buttonBorder; btn.style.color = `${newColors.primaryText} !important`; - + // Re-attach hover handlers with new colors btn.onmouseover = () => { btn.style.background = newColors.buttonHover; @@ -2269,18 +3121,22 @@ async function openUncloseaiEmbeddedModalNew() { btn.style.borderColor = newColors.buttonBorder; }; }); - + // Update all message bubbles - chatBox.querySelectorAll('div[style*="align-self: flex-end"]').forEach(userMsg => { - userMsg.style.background = newColors.userMessageBg; - userMsg.style.color = newColors.userMessageText; - }); - - chatBox.querySelectorAll('div[style*="align-self: flex-start"]').forEach(aiMsg => { - aiMsg.style.background = newColors.aiMessageBg; - aiMsg.style.color = newColors.aiMessageText; - aiMsg.style.boxShadow = `0 1px 2px ${newColors.lightShadow}`; - }); + chatBox + .querySelectorAll('div[style*="align-self: flex-end"]') + .forEach((userMsg) => { + userMsg.style.background = newColors.userMessageBg; + userMsg.style.color = newColors.userMessageText; + }); + + chatBox + .querySelectorAll('div[style*="align-self: flex-start"]') + .forEach((aiMsg) => { + aiMsg.style.background = newColors.aiMessageBg; + aiMsg.style.color = newColors.aiMessageText; + aiMsg.style.boxShadow = `0 1px 2px ${newColors.lightShadow}`; + }); }; // Set up theme change listeners @@ -2291,7 +3147,9 @@ async function openUncloseaiEmbeddedModalNew() { }); if (window.matchMedia) { - window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", updateModalTheme); + window + .matchMedia("(prefers-color-scheme: dark)") + .addEventListener("change", updateModalTheme); } // Clean up listeners when modal is closed @@ -2299,16 +3157,18 @@ async function openUncloseaiEmbeddedModalNew() { modal.close = () => { themeObserver.disconnect(); if (window.matchMedia) { - window.matchMedia("(prefers-color-scheme: dark)").removeEventListener("change", updateModalTheme); + window + .matchMedia("(prefers-color-scheme: dark)") + .removeEventListener("change", updateModalTheme); } originalClose(); }; input.focus(); - + // Load models on startup loadModels(); - + // Load conversation history const loadHistory = async () => { try { @@ -2331,25 +3191,33 @@ ${pageContent} You have complete knowledge of this page content and can reference any details, names, topics, or information mentioned on this page. Answer questions about the page content accurately and helpfully.`; setSystemMessageAppend(conversationContextAppend); - + // Sync the loaded history with the chat.js module - const { updateChatHistory } = await import('./chat.js'); - const { getSystemMessage } = await import('./config.js'); - + const { updateChatHistory } = await import("./chat.js"); + const { getSystemMessage } = await import("./config.js"); + // Create new history with updated system message and loaded conversation const newHistory = [ - { role: 'system', content: getSystemMessage() }, - ...history + { role: "system", content: getSystemMessage() }, + ...history, ]; - + // Update the chat.js module's history updateChatHistory(newHistory); - console.log("Chat history synced with loaded data:", newHistory.length, "messages"); + console.log( + "Chat history synced with loaded data:", + newHistory.length, + "messages", + ); console.log("History to display:", history); // Display previous conversation history.forEach((msg, index) => { - console.log(`Displaying message ${index}:`, msg.role, msg.content.substring(0, 50)); - if (msg.role === 'user') { + console.log( + `Displaying message ${index}:`, + msg.role, + msg.content.substring(0, 50), + ); + if (msg.role === "user") { const userMsg = document.createElement("div"); userMsg.style.cssText = ` align-self: flex-end; @@ -2360,12 +3228,12 @@ You have complete knowledge of this page content and can reference any details, max-width: 80%; word-wrap: break-word; `; - + // Add message text and actions inside the bubble const messageText = document.createElement("div"); messageText.textContent = msg.content; messageText.style.cssText = `margin-bottom: 8px;`; - + const userMessageActions = document.createElement("div"); userMessageActions.style.cssText = ` display: flex; @@ -2373,7 +3241,7 @@ You have complete knowledge of this page content and can reference any details, opacity: 0.8; justify-content: flex-end; `; - + const userTtsBtn = document.createElement("button"); userTtsBtn.textContent = "🔊"; userTtsBtn.title = "Read aloud"; @@ -2388,56 +3256,66 @@ You have complete knowledge of this page content and can reference any details, transition: all 0.2s; `; userTtsBtn.onmouseenter = () => { - userTtsBtn.style.background = 'rgba(255, 255, 255, 0.3)'; + userTtsBtn.style.background = "rgba(255, 255, 255, 0.3)"; }; userTtsBtn.onmouseleave = () => { - userTtsBtn.style.background = 'rgba(255, 255, 255, 0.2)'; + userTtsBtn.style.background = "rgba(255, 255, 255, 0.2)"; }; - + // Store audio for pause/resume functionality let historicalUserAudio = null; - + userTtsBtn.onclick = async () => { - const modalVoiceSelect = document.getElementById('hermes-voice-select'); - const selectedVoice = modalVoiceSelect?.value || 'alloy'; - + const modalVoiceSelect = document.getElementById( + "hermes-voice-select", + ); + const selectedVoice = modalVoiceSelect?.value || "alloy"; + // If audio is already playing, pause it if (historicalUserAudio && !historicalUserAudio.paused) { historicalUserAudio.pause(); userTtsBtn.textContent = "🔊"; return; } - + // If audio exists and is paused, resume it - if (historicalUserAudio && historicalUserAudio.paused && historicalUserAudio.currentTime > 0) { + if ( + historicalUserAudio && + historicalUserAudio.paused && + historicalUserAudio.currentTime > 0 + ) { historicalUserAudio.play(); userTtsBtn.textContent = "⏸️"; return; } - + // Generate new audio const originalText = userTtsBtn.textContent; userTtsBtn.textContent = "⏳"; userTtsBtn.disabled = true; - + try { - const result = await speakChatText(msg.content, selectedVoice, 1.0); + const result = await speakChatText( + msg.content, + selectedVoice, + 1.0, + ); historicalUserAudio = result.audio; - + // Set up audio event handlers historicalUserAudio.onplay = () => { userTtsBtn.textContent = "⏸️"; }; - + historicalUserAudio.onpause = () => { userTtsBtn.textContent = "🔊"; }; - + historicalUserAudio.onended = () => { userTtsBtn.textContent = "🔊"; historicalUserAudio = null; }; - + // Auto-play the generated audio try { await historicalUserAudio.play(); @@ -2466,10 +3344,10 @@ You have complete knowledge of this page content and can reference any details, transition: all 0.2s; `; userDeleteBtn.onmouseenter = () => { - userDeleteBtn.style.background = 'rgba(255, 255, 255, 0.3)'; + userDeleteBtn.style.background = "rgba(255, 255, 255, 0.3)"; }; userDeleteBtn.onmouseleave = () => { - userDeleteBtn.style.background = 'rgba(255, 255, 255, 0.2)'; + userDeleteBtn.style.background = "rgba(255, 255, 255, 0.2)"; }; userDeleteBtn.onclick = async () => { // Remove from chat history by index @@ -2477,27 +3355,27 @@ You have complete knowledge of this page content and can reference any details, if (index >= 0 && index < currentHistory.length) { currentHistory.splice(index, 1); saveConversationHistory(currentHistory); - + // Update the chat.js module history to match localStorage - const { updateChatHistory } = await import('./chat.js'); - const { getSystemMessage } = await import('./config.js'); + const { updateChatHistory } = await import("./chat.js"); + const { getSystemMessage } = await import("./config.js"); const newHistory = [ - { role: 'system', content: getSystemMessage() }, - ...currentHistory + { role: "system", content: getSystemMessage() }, + ...currentHistory, ]; updateChatHistory(newHistory); } - + userMsg.remove(); }; - + userMessageActions.appendChild(userTtsBtn); userMessageActions.appendChild(userDeleteBtn); userMsg.appendChild(messageText); userMsg.appendChild(userMessageActions); chatBox.appendChild(userMsg); console.log("Added user message to chatBox"); - } else if (msg.role === 'assistant') { + } else if (msg.role === "assistant") { const aiMsg = document.createElement("div"); aiMsg.style.cssText = ` align-self: flex-start; @@ -2510,7 +3388,7 @@ You have complete knowledge of this page content and can reference any details, `; aiMsg.innerHTML = marked.parse(msg.content); addCodeBlockCopyButtons(aiMsg); - + // Add TTS and delete buttons to historical AI messages const messageActions = document.createElement("div"); messageActions.style.cssText = ` @@ -2519,7 +3397,7 @@ You have complete knowledge of this page content and can reference any details, gap: 8px; opacity: 0.7; `; - + const ttsBtn = document.createElement("button"); ttsBtn.textContent = "🔊"; ttsBtn.title = "Read aloud"; @@ -2535,11 +3413,13 @@ You have complete knowledge of this page content and can reference any details, `; // Store audio for pause/resume functionality let historicalAudio = null; - + ttsBtn.onclick = async () => { - const modalVoiceSelect = document.getElementById('hermes-voice-select'); - const selectedVoice = modalVoiceSelect?.value || 'alloy'; - + const modalVoiceSelect = document.getElementById( + "hermes-voice-select", + ); + const selectedVoice = modalVoiceSelect?.value || "alloy"; + // If audio is already playing, pause it if (historicalAudio && !historicalAudio.paused) { historicalAudio.pause(); @@ -2547,45 +3427,56 @@ You have complete knowledge of this page content and can reference any details, console.log("🔊 HISTORICAL TTS: Paused"); return; } - + // If audio exists and is paused, resume it - if (historicalAudio && historicalAudio.paused && historicalAudio.currentTime > 0) { + if ( + historicalAudio && + historicalAudio.paused && + historicalAudio.currentTime > 0 + ) { historicalAudio.play(); ttsBtn.textContent = "⏸️"; console.log("🔊 HISTORICAL TTS: Resumed"); return; } - + // Generate new audio console.log("🔊 HISTORICAL TTS: Generating new audio"); const originalText = ttsBtn.textContent; ttsBtn.textContent = "⏳"; ttsBtn.disabled = true; - + try { - const result = await speakChatText(msg.content, selectedVoice, 1.0); + const result = await speakChatText( + msg.content, + selectedVoice, + 1.0, + ); historicalAudio = result.audio; - + // Set up audio event handlers historicalAudio.onplay = () => { ttsBtn.textContent = "⏸️"; }; - + historicalAudio.onpause = () => { ttsBtn.textContent = "🔊"; }; - + historicalAudio.onended = () => { ttsBtn.textContent = "🔊"; historicalAudio = null; }; - + // Auto-play the generated audio try { await historicalAudio.play(); console.log("🔊 HISTORICAL TTS: Playing new audio"); } catch (playError) { - console.log("🔊 HISTORICAL TTS: Auto-play blocked:", playError.message); + console.log( + "🔊 HISTORICAL TTS: Auto-play blocked:", + playError.message, + ); ttsBtn.textContent = "🔊"; } } catch (error) { @@ -2596,7 +3487,7 @@ You have complete knowledge of this page content and can reference any details, ttsBtn.disabled = false; } }; - + const deleteBtn = document.createElement("button"); deleteBtn.textContent = "🗑️"; deleteBtn.title = "Delete message"; @@ -2613,7 +3504,7 @@ You have complete knowledge of this page content and can reference any details, aiMsg.remove(); // TODO: Remove from chat history }; - + // Copy Raw (Markdown) button const copyRawBtn = document.createElement("button"); copyRawBtn.textContent = "📋"; @@ -2639,7 +3530,7 @@ You have complete knowledge of this page content and can reference any details, alert("Failed to copy: " + error.message); } }; - + // Copy HTML button const copyHtmlBtn = document.createElement("button"); copyHtmlBtn.textContent = "📄"; @@ -2666,13 +3557,13 @@ You have complete knowledge of this page content and can reference any details, alert("Failed to copy: " + error.message); } }; - + messageActions.appendChild(ttsBtn); messageActions.appendChild(copyRawBtn); messageActions.appendChild(copyHtmlBtn); messageActions.appendChild(deleteBtn); aiMsg.appendChild(messageActions); - + chatBox.appendChild(aiMsg); console.log("Added AI message to chatBox with TTS buttons"); } @@ -2687,7 +3578,7 @@ You have complete knowledge of this page content and can reference any details, await addIntroMessage(); } }; - + const addIntroMessage = async () => { const introMsg = document.createElement("div"); introMsg.style.cssText = ` @@ -2699,16 +3590,17 @@ You have complete knowledge of this page content and can reference any details, box-shadow: 0 1px 2px rgba(0,0,0,0.1); color: #495057; `; - + // Show loading message first - introMsg.innerHTML = 'Analyzing page and generating welcome message...'; + introMsg.innerHTML = + 'Analyzing page and generating welcome message...'; chatBox.appendChild(introMsg); - + try { // Get page content for context const pageContent = extractWebpageContent(); const pageTitle = document.title || window.location.hostname; - + // FIRST: Generate intro message with specialized intro system prompt const introSystemPrompt = `You are Hermes AI, powered by Nous Research, embedded on a webpage. Your task is to generate a warm, welcoming 3-paragraph introduction that shows you understand the specific page the user is viewing. @@ -2727,21 +3619,21 @@ Generate a 3-paragraph introduction that: Reference specific details from the page content to show understanding.`; const introPrompt = `Generate the welcoming introduction message now.`; - + // Create temporary chat history for intro generation const introHistory = [ { role: "system", content: introSystemPrompt }, - { role: "user", content: introPrompt } + { role: "user", content: introPrompt }, ]; - + // Generate intro with specialized system prompt - let response = ''; + let response = ""; for await (const chunk of sendMessageWithCustomHistory(introHistory)) { response += chunk; introMsg.innerHTML = marked.parse(response); addCodeBlockCopyButtons(introMsg); } - + // SECOND: Set up conversation system prompt for follow-up messages const conversationContextAppend = ` @@ -2755,21 +3647,21 @@ ${pageContent} You have complete knowledge of this page content and can reference any details, names, topics, or information mentioned on this page. Answer questions about the page content accurately and helpfully.`; setSystemMessageAppend(conversationContextAppend); - + // Update the chat.js module with the intro message - const { updateChatHistory, getChatHistory } = await import('./chat.js'); - const { getSystemMessage } = await import('./config.js'); - + const { updateChatHistory, getChatHistory } = await import("./chat.js"); + const { getSystemMessage } = await import("./config.js"); + // Create new history with system message and intro const newHistory = [ - { role: 'system', content: getSystemMessage() }, - { role: 'assistant', content: response } + { role: "system", content: getSystemMessage() }, + { role: "assistant", content: response }, ]; - + // Update the chat.js module's history updateChatHistory(newHistory); console.log("Added intro message to chat history"); - + // Add TTS and delete buttons to intro message const messageActions = document.createElement("div"); messageActions.style.cssText = ` @@ -2778,7 +3670,7 @@ You have complete knowledge of this page content and can reference any details, gap: 8px; opacity: 0.7; `; - + const ttsBtn = document.createElement("button"); ttsBtn.textContent = "🔊"; ttsBtn.title = "Read aloud"; @@ -2791,14 +3683,14 @@ You have complete knowledge of this page content and can reference any details, font-size: 12px; transition: all 0.2s; `; - + // Store audio for pause/resume functionality let introAudio = null; - + ttsBtn.onclick = async () => { - const modalVoiceSelect = document.getElementById('hermes-voice-select'); - const selectedVoice = modalVoiceSelect?.value || 'alloy'; - + const modalVoiceSelect = document.getElementById("hermes-voice-select"); + const selectedVoice = modalVoiceSelect?.value || "alloy"; + // If audio is already playing, pause it if (introAudio && !introAudio.paused) { introAudio.pause(); @@ -2806,7 +3698,7 @@ You have complete knowledge of this page content and can reference any details, console.log("🔊 INTRO TTS: Paused"); return; } - + // If audio exists and is paused, resume it if (introAudio && introAudio.paused && introAudio.currentTime > 0) { introAudio.play(); @@ -2814,31 +3706,31 @@ You have complete knowledge of this page content and can reference any details, console.log("🔊 INTRO TTS: Resumed"); return; } - + // Generate new audio console.log("🔊 INTRO TTS: Generating new audio"); const originalText = ttsBtn.textContent; ttsBtn.textContent = "⏳"; ttsBtn.disabled = true; - + try { const result = await speakChatText(response, selectedVoice, 1.0); introAudio = result.audio; - + // Set up audio event handlers introAudio.onplay = () => { ttsBtn.textContent = "⏸️"; }; - + introAudio.onpause = () => { ttsBtn.textContent = "🔊"; }; - + introAudio.onended = () => { ttsBtn.textContent = "🔊"; introAudio = null; }; - + // Auto-play the generated audio try { await introAudio.play(); @@ -2855,7 +3747,7 @@ You have complete knowledge of this page content and can reference any details, ttsBtn.disabled = false; } }; - + const deleteBtn = document.createElement("button"); deleteBtn.textContent = "🗑️"; deleteBtn.title = "Delete message"; @@ -2871,7 +3763,7 @@ You have complete knowledge of this page content and can reference any details, deleteBtn.onclick = () => { introMsg.remove(); }; - + // Copy Raw (Markdown) button const copyRawBtn = document.createElement("button"); copyRawBtn.textContent = "📋"; @@ -2898,7 +3790,7 @@ You have complete knowledge of this page content and can reference any details, alert("Failed to copy: " + error.message); } }; - + // Copy HTML button const copyHtmlBtn = document.createElement("button"); copyHtmlBtn.textContent = "📄"; @@ -2926,23 +3818,22 @@ You have complete knowledge of this page content and can reference any details, alert("Failed to copy: " + error.message); } }; - + messageActions.appendChild(ttsBtn); messageActions.appendChild(copyRawBtn); messageActions.appendChild(copyHtmlBtn); messageActions.appendChild(deleteBtn); introMsg.appendChild(messageActions); - + // Save this intro as part of the conversation saveConversationHistory(getChatHistory()); - } catch (error) { console.error("Failed to generate contextual intro:", error); // Fallback to basic intro introMsg.innerHTML = `👋 Hi! I'm Hermes AI. I can help you understand this page, answer questions, or assist with various tasks. How can I help you today?`; } }; - + await loadHistory(); } @@ -2962,10 +3853,10 @@ export async function toggleUncloseaiEmbeddedModal() { export async function openUncloseaiEmbeddedModal() { const modal = document.createElement("dialog"); modal.id = "uncloseai-embedded-modal"; - + // Mobile-first: full screen on mobile, centered on desktop const isMobile = window.innerWidth <= 768; - + if (isMobile) { modal.style.cssText = ` position: fixed; @@ -3657,12 +4548,12 @@ Format: Start with "Greetings! I'm Hermes..." and make it sound natural and enga // Show the modal first, then load intro asynchronously document.body.appendChild(modal); - + // Use showModal() for proper mobile support and backdrop modal.showModal(); - + // Close modal when clicking backdrop - modal.addEventListener('click', (e) => { + modal.addEventListener("click", (e) => { if (e.target === modal) { modal.close(); document.body.removeChild(modal); @@ -3826,7 +4717,7 @@ Format: Start with "Greetings! I'm Hermes..." and make it sound natural and enga export function openTTSModal() { // Ensure ChunkFive font is loaded initializeChunkFiveFont(); - + // Check if Hermes modal is open to set appropriate z-index const hermesModal = document.getElementById("uncloseai-embedded-modal"); const zIndex = hermesModal ? "2001" : "1001"; @@ -3889,7 +4780,8 @@ export function openTTSModal() { article.appendChild(header); const h1 = document.createElement("h1"); - h1.innerHTML = 'uncloseai. TTS Anything!'; + h1.innerHTML = + "uncloseai. TTS Anything!"; if (USE_CUSTOM_STYLING) { h1.style.cssText = ` margin: 0; @@ -4103,14 +4995,14 @@ export function openTTSModal() { }; article.appendChild(playButton); - + document.body.appendChild(modal); - + // Use showModal() for proper mobile support and backdrop modal.showModal(); - + // Close modal when clicking backdrop - modal.addEventListener('click', (e) => { + modal.addEventListener("click", (e) => { if (e.target === modal) { modal.close(); document.body.removeChild(modal); @@ -4122,7 +5014,7 @@ export function openTTSModal() { export function openTranslateModal() { // Ensure ChunkFive font is loaded initializeChunkFiveFont(); - + // Check if Hermes modal is open to set appropriate z-index const hermesModal = document.getElementById("uncloseai-embedded-modal"); const zIndex = hermesModal ? "2001" : "1001"; @@ -4185,7 +5077,8 @@ export function openTranslateModal() { article.appendChild(header); const h1 = document.createElement("h1"); - h1.innerHTML = 'uncloseai. 🌐 Translation'; + h1.innerHTML = + "uncloseai. 🌐 Translation"; if (USE_CUSTOM_STYLING) { h1.style.cssText = ` margin: 0; @@ -4378,9 +5271,11 @@ export function openTranslateModal() { resultContainer.style.display = "none"; // Get token count and estimated time for progress - const { countTokens } = await import('./token_estimator.js'); - const { estimateProcessingTime } = await import('./translation.js'); - const textToAnalyze = isPageMode ? document.documentElement.outerHTML : textArea.value.trim(); + const { countTokens } = await import("./token_estimator.js"); + const { estimateProcessingTime } = await import("./translation.js"); + const textToAnalyze = isPageMode + ? document.documentElement.outerHTML + : textArea.value.trim(); const tokenInfo = countTokens(textToAnalyze); const estimatedSeconds = estimateProcessingTime(tokenInfo.totalTokens); @@ -4394,15 +5289,16 @@ export function openTranslateModal() { background: #f0fff0; text-align: center; `; - + const progressText = document.createElement("div"); - progressText.style.cssText = "font-weight: bold; color: #4CAF50; margin-bottom: 10px;"; + progressText.style.cssText = + "font-weight: bold; color: #4CAF50; margin-bottom: 10px;"; progressText.textContent = `🤖 Processing ${tokenInfo.totalTokens} tokens...`; - + const etaText = document.createElement("div"); etaText.style.cssText = "color: #666; margin-bottom: 15px;"; etaText.textContent = `Estimated time: ${estimatedSeconds} seconds`; - + const progressBar = document.createElement("div"); progressBar.style.cssText = ` width: 100%; @@ -4412,7 +5308,7 @@ export function openTranslateModal() { overflow: hidden; margin-bottom: 10px; `; - + const progressFill = document.createElement("div"); progressFill.style.cssText = ` height: 100%; @@ -4422,31 +5318,44 @@ export function openTranslateModal() { border-radius: 10px; `; progressBar.appendChild(progressFill); - + const statusText = document.createElement("div"); statusText.style.cssText = "color: #777; font-size: 0.9em;"; statusText.textContent = "Initializing translation..."; - + progressContainer.appendChild(progressText); progressContainer.appendChild(etaText); progressContainer.appendChild(progressBar); progressContainer.appendChild(statusText); - + // Insert progress before result container - resultContainer.parentNode.insertBefore(progressContainer, resultContainer); - + resultContainer.parentNode.insertBefore( + progressContainer, + resultContainer, + ); + // Start progress animation const startTime = Date.now(); - let progressInterval = setInterval(() => { + const progressInterval = setInterval(() => { const elapsed = Date.now() - startTime; - const progress = Math.min((elapsed / (estimatedSeconds * 1000)) * 100, 95); + const progress = Math.min( + (elapsed / (estimatedSeconds * 1000)) * 100, + 95, + ); progressFill.style.width = `${progress}%`; - - const remainingTime = Math.max(0, estimatedSeconds - Math.floor(elapsed / 1000)); - etaText.textContent = remainingTime > 0 ? `Time remaining: ${remainingTime}s` : "Almost done..."; - + + const remainingTime = Math.max( + 0, + estimatedSeconds - Math.floor(elapsed / 1000), + ); + etaText.textContent = + remainingTime > 0 + ? `Time remaining: ${remainingTime}s` + : "Almost done..."; + if (elapsed < 3000) statusText.textContent = "Analyzing content..."; - else if (elapsed < estimatedSeconds * 500) statusText.textContent = "Translating with Hermes AI..."; + else if (elapsed < estimatedSeconds * 500) + statusText.textContent = "Translating with Hermes AI..."; else statusText.textContent = "Finalizing translation..."; }, 500); @@ -4518,7 +5427,11 @@ export function openTranslateModal() { // Add a notice about AI functionality only in preview window (wait for body to be available) const addNotice = () => { - if (previewWindow && previewWindow.document && previewWindow.document.body) { + if ( + previewWindow && + previewWindow.document && + previewWindow.document.body + ) { console.log("Adding AI notice to preview window"); const notice = previewWindow.document.createElement("div"); notice.style.cssText = @@ -4535,7 +5448,7 @@ export function openTranslateModal() { setTimeout(addNotice, 100); } }; - + // Only add notice if we have a valid preview window if (previewWindow) { addNotice(); @@ -4567,7 +5480,7 @@ export function openTranslateModal() { progressFill.style.width = "100%"; etaText.textContent = "Translation completed!"; statusText.textContent = "✅ Ready to preview"; - + // Hide progress after a moment setTimeout(() => { if (progressContainer.parentNode) { @@ -4579,10 +5492,12 @@ export function openTranslateModal() { resultContainer.scrollIntoView({ behavior: "smooth", block: "nearest" }); } catch (error) { // Clean up progress on error - if (typeof progressInterval !== 'undefined') { + if (typeof progressInterval !== "undefined") { clearInterval(progressInterval); } - const progressContainer = document.querySelector('[style*="border: 2px solid #4CAF50"]'); + const progressContainer = document.querySelector( + '[style*="border: 2px solid #4CAF50"]', + ); if (progressContainer && progressContainer.parentNode) { progressContainer.parentNode.removeChild(progressContainer); } @@ -4596,12 +5511,12 @@ export function openTranslateModal() { }; document.body.appendChild(modal); - + // Use showModal() for proper mobile support and backdrop modal.showModal(); - + // Close modal when clicking backdrop - modal.addEventListener('click', (e) => { + modal.addEventListener("click", (e) => { if (e.target === modal) { modal.close(); document.body.removeChild(modal); @@ -4618,8 +5533,8 @@ export function initializeSystem() { if (window.UNCLOSEAI_FLOATING_BUTTON !== false) { console.log("uncloseai.js: Creating floating button in preview window"); // Ensure DOM is ready before creating button - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', createFloatingAIButton); + if (document.readyState === "loading") { + document.addEventListener("DOMContentLoaded", createFloatingAIButton); } else { createFloatingAIButton(); } diff --git a/uncloseai.js b/uncloseai.js index dbe9ffc..21a713c 100644 --- a/uncloseai.js +++ b/uncloseai.js @@ -5,21 +5,17 @@ * This maintains backward compatibility while enabling better code organization */ -// External dependencies -import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js"; -import hljs from "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/es/highlight.min.js"; - +import * as Chat from "./src/chat.js"; // Internal modules import * as Config from "./src/config.js"; -import * as Models from "./src/models.js"; import * as Content from "./src/content.js"; -import * as TTS from "./src/tts.js"; -import * as Chat from "./src/chat.js"; import * as FileUpload from "./src/file-upload.js"; -import * as Storage from "./src/storage.js"; +import * as Models from "./src/models.js"; import * as PageReader from "./src/page-reader.js"; -import * as UI from "./src/ui.js"; +import * as Storage from "./src/storage.js"; import * as Translation from "./src/translation.js"; +import * as TTS from "./src/tts.js"; +import * as UI from "./src/ui.js"; // ------------------------- // Public API - Main Functions @@ -105,7 +101,8 @@ window.speakText = TTS.speakText; window.uploadFile = FileUpload.uploadFile; window.openTTSModal = UI.openTTSModal; window.openTranslateModal = UI.openTranslateModal; -window.toggleUncloseaiEmbeddedModal = async () => await UI.toggleUncloseaiEmbeddedModal(); +window.toggleUncloseaiEmbeddedModal = async () => + await UI.toggleUncloseaiEmbeddedModal(); window.extractWebpageContent = Content.extractWebpageContent; window.getSelectedModel = Models.getSelectedModel; window.getSelectedModelEndpoint = Models.getSelectedModelEndpoint; @@ -118,12 +115,16 @@ window.handleSmartTranslate = UI.handleSmartTranslate; // Initialize on page load window.addEventListener("load", () => { console.log("uncloseai.js: window.onload event fired."); - + // Check skip init flag first if (window.UNCLOSEAI_SKIP_INIT === true) { - console.log("uncloseai.js: Skipping full initialization as requested by flag."); - console.log("uncloseai.js: Creating floating button only for preview windows."); - + console.log( + "uncloseai.js: Skipping full initialization as requested by flag.", + ); + console.log( + "uncloseai.js: Creating floating button only for preview windows.", + ); + // Still create floating button for preview windows, but skip other initialization const SHOW_FLOATING_BUTTON = window.UNCLOSEAI_FLOATING_BUTTON !== false; if (SHOW_FLOATING_BUTTON) { @@ -131,12 +132,12 @@ window.addEventListener("load", () => { } return; } - + UI.initializeSystem(); }); // Export helper functions for class-based integrations -window.handleCustomChat = async function (button) { +window.handleCustomChat = async (button) => { const container = button.parentElement; const input = container.querySelector("[data-chat-input]"); const chatBox = container.querySelector("[data-chat-box]");