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 ? `` : '';
- case 'ul': case 'ol':
+ }
+ case "img": {
+ const src = node.getAttribute("src");
+ const alt = node.getAttribute("alt") || "";
+ return 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]");