- Add complete UI translations for all 19 supported languages - Add language preference dropdown in modal settings - Store language preference in localStorage - Inject language preference into Hermes system prompts - Add smart translation dropdown with AI-powered language detection - Keep both original translation modal and new smart dropdown - Remove non-functional upload button from modal - Add biome.json config to ignore third-party CSS files
5562 lines
173 KiB
JavaScript
5562 lines
173 KiB
JavaScript
// 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";
|
||
|
||
// Initialize ChunkFive font with absolute path
|
||
function initializeChunkFiveFont() {
|
||
const fontFaceStyle = document.createElement("style");
|
||
fontFaceStyle.textContent = `
|
||
@font-face {
|
||
font-family: "ChunkFiveRegular";
|
||
src: url("https://uncloseai.com/css/chunkfive/chunkfive-regular-webfont.woff2") format("woff2"),
|
||
url("https://uncloseai.com/css/chunkfive/chunkfive-regular-webfont.woff") format("woff");
|
||
font-weight: normal;
|
||
font-style: normal;
|
||
font-display: swap;
|
||
}
|
||
`;
|
||
document.head.appendChild(fontFaceStyle);
|
||
}
|
||
|
||
// Fast AI-powered language detection using Hermes
|
||
async function detectPageLanguage() {
|
||
try {
|
||
// 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 apiUrl = `${getSelectedModelEndpoint()}/chat/completions`;
|
||
|
||
const response = await fetch(apiUrl, {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Authorization: `Bearer ${API_KEY}`,
|
||
},
|
||
body: JSON.stringify({
|
||
model: getSelectedModel(),
|
||
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.",
|
||
},
|
||
{
|
||
role: "user",
|
||
content: `Detect the language of this text:\n\n${sample}`,
|
||
},
|
||
],
|
||
temperature: 0.1,
|
||
max_tokens: 10,
|
||
stream: false,
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
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);
|
||
return detectedLang;
|
||
} else {
|
||
console.warn("Invalid language code from AI:", detectedLang);
|
||
return fallbackLanguageDetection();
|
||
}
|
||
} catch (error) {
|
||
console.warn("Language detection error:", error);
|
||
return fallbackLanguageDetection();
|
||
}
|
||
}
|
||
|
||
// Fallback language detection using DOM attributes
|
||
function fallbackLanguageDetection() {
|
||
// Check html lang attribute first
|
||
const htmlLang = document.documentElement.lang;
|
||
if (htmlLang) {
|
||
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();
|
||
return langCode;
|
||
}
|
||
|
||
// Default to English
|
||
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
|
||
function detectCurrentTheme() {
|
||
const isDark =
|
||
document.documentElement.getAttribute("data-theme") === "dark" ||
|
||
(window.matchMedia &&
|
||
window.matchMedia("(prefers-color-scheme: dark)").matches &&
|
||
!document.documentElement.getAttribute("data-theme"));
|
||
|
||
return isDark ? "dark" : "light";
|
||
}
|
||
|
||
function getThemeColors(theme = detectCurrentTheme()) {
|
||
if (theme === "dark") {
|
||
return {
|
||
// Main backgrounds
|
||
modalBackground: "#1a1a1a",
|
||
contentBackground: "#2d2d2d",
|
||
panelBackground: "#262626",
|
||
inputBackground: "#363636",
|
||
|
||
// Text colors
|
||
primaryText: "#ffffff",
|
||
secondaryText: "#b3b3b3",
|
||
mutedText: "#808080",
|
||
|
||
// Borders and dividers
|
||
borderColor: "#404040",
|
||
dividerColor: "#333333",
|
||
|
||
// Interactive elements
|
||
buttonBackground: "#404040",
|
||
buttonHover: "#4a4a4a",
|
||
buttonBorder: "#555555",
|
||
|
||
// Message bubbles
|
||
userMessageBg: "#4a5568",
|
||
userMessageText: "#ffffff",
|
||
aiMessageBg: "#363636",
|
||
aiMessageText: "#ffffff",
|
||
|
||
// Input focus
|
||
focusBorder: "#667eea",
|
||
|
||
// Shadows (adjusted for dark theme)
|
||
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)",
|
||
};
|
||
} else {
|
||
return {
|
||
// Main backgrounds
|
||
modalBackground: "#ffffff",
|
||
contentBackground: "#f8f9fa",
|
||
panelBackground: "#ffffff",
|
||
inputBackground: "#ffffff",
|
||
|
||
// Text colors
|
||
primaryText: "#212529",
|
||
secondaryText: "#495057",
|
||
mutedText: "#6c757d",
|
||
|
||
// Borders and dividers
|
||
borderColor: "#dee2e6",
|
||
dividerColor: "#e9ecef",
|
||
|
||
// Interactive elements
|
||
buttonBackground: "#f8f9fa",
|
||
buttonHover: "#e9ecef",
|
||
buttonBorder: "#dee2e6",
|
||
|
||
// Message bubbles
|
||
userMessageBg: "#667eea",
|
||
userMessageText: "#ffffff",
|
||
aiMessageBg: "#ffffff",
|
||
aiMessageText: "#212529",
|
||
|
||
// Input focus
|
||
focusBorder: "#667eea",
|
||
|
||
// Shadows
|
||
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)",
|
||
};
|
||
}
|
||
}
|
||
|
||
// Ensure font is loaded when module is imported
|
||
if (typeof document !== "undefined") {
|
||
initializeChunkFiveFont();
|
||
}
|
||
|
||
// Helper function to add copy buttons to code blocks
|
||
function addCodeBlockCopyButtons(element) {
|
||
const codeBlocks = element.querySelectorAll("pre code");
|
||
codeBlocks.forEach((codeBlock) => {
|
||
const pre = codeBlock.parentElement;
|
||
if (pre.tagName.toLowerCase() === "pre") {
|
||
// Make the pre element relative for positioning
|
||
pre.style.position = "relative";
|
||
|
||
// Create copy button
|
||
const copyBtn = document.createElement("button");
|
||
copyBtn.textContent = "📋";
|
||
copyBtn.title = "Copy code";
|
||
copyBtn.style.cssText = `
|
||
position: absolute;
|
||
top: 8px;
|
||
right: 8px;
|
||
background: rgba(0, 0, 0, 0.7);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
opacity: 0.8;
|
||
transition: opacity 0.2s;
|
||
z-index: 10;
|
||
`;
|
||
|
||
copyBtn.onmouseenter = () => {
|
||
copyBtn.style.opacity = "1";
|
||
};
|
||
copyBtn.onmouseleave = () => {
|
||
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 = "✓";
|
||
setTimeout(() => {
|
||
copyBtn.textContent = originalText;
|
||
}, 1000);
|
||
} catch (error) {
|
||
alert("Failed to copy code: " + error.message);
|
||
}
|
||
};
|
||
|
||
pre.appendChild(copyBtn);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Send message with custom history (for intro generation)
|
||
async function* sendMessageWithCustomHistory(messageHistory) {
|
||
const { getSelectedModel, getSelectedModelEndpoint } = await import(
|
||
"./models.js"
|
||
);
|
||
const apiUrl = `${getSelectedModelEndpoint()}/chat/completions`;
|
||
|
||
const response = await fetch(apiUrl, {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Authorization: `Bearer ${API_KEY}`,
|
||
},
|
||
body: JSON.stringify({
|
||
model: getSelectedModel(),
|
||
messages: messageHistory,
|
||
temperature: 0.3,
|
||
max_tokens: 8192,
|
||
stream: true,
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder();
|
||
let buffer = "";
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
buffer += decoder.decode(value, { stream: true });
|
||
const lines = buffer.split("\n");
|
||
|
||
for (let i = 0; i < lines.length - 1; i++) {
|
||
const line = lines[i].trim();
|
||
if (line.startsWith("data: ")) {
|
||
const jsonData = line.slice(6);
|
||
if (jsonData === "[DONE]") continue;
|
||
|
||
try {
|
||
const parsedData = JSON.parse(jsonData);
|
||
const content = parsedData.choices[0].delta.content;
|
||
if (content) {
|
||
yield content;
|
||
}
|
||
} catch (error) {
|
||
console.error("Error parsing JSON:", error);
|
||
}
|
||
}
|
||
}
|
||
|
||
buffer = lines[lines.length - 1];
|
||
}
|
||
}
|
||
|
||
// Simple TTS function for chat messages (no preprocessing needed)
|
||
async function speakChatText(text, voice = "alloy", rate = 1.0) {
|
||
try {
|
||
const response = await fetch(TTS_API_URL, {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Authorization: `Bearer ${API_KEY}`,
|
||
},
|
||
body: JSON.stringify({
|
||
model: "tts-1",
|
||
voice: voice,
|
||
input: text,
|
||
}),
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
|
||
const audioBlob = await response.blob();
|
||
const audioUrl = URL.createObjectURL(audioBlob);
|
||
const audio = new Audio(audioUrl);
|
||
audio.playbackRate = rate;
|
||
|
||
return { audio, blob: audioBlob };
|
||
} catch (error) {
|
||
console.error("Error in chat TTS:", error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
import { chatHistory, handleUserInput, sendMessage } from "./chat.js";
|
||
import {
|
||
handleFileUpload,
|
||
hideProgressIndicator,
|
||
showProgressIndicator,
|
||
uploadFile,
|
||
} from "./file-upload.js";
|
||
import {
|
||
fetchModelsFromEndpoints,
|
||
getSelectedModel,
|
||
getSelectedModelEndpoint,
|
||
modelRegistry,
|
||
} from "./models.js";
|
||
import { readPageWithHermes } from "./page-reader.js";
|
||
import {
|
||
clearConversationHistory,
|
||
getPageSpecificKey,
|
||
loadConversationHistory,
|
||
saveConversationHistory,
|
||
} from "./storage.js";
|
||
import {
|
||
SUPPORTED_LANGUAGES,
|
||
translateCurrentPage,
|
||
translateText,
|
||
} from "./translation.js";
|
||
|
||
// Configuration flags
|
||
const USE_CUSTOM_STYLING = window.UNCLOSEAI_CUSTOM_STYLING !== false;
|
||
const SHOW_FLOATING_BUTTON = window.UNCLOSEAI_FLOATING_BUTTON !== false;
|
||
|
||
// Global variable to track modal state
|
||
let uncloseaiEmbeddedModalOpen = false;
|
||
|
||
// Initialize uncloseai elements based on class
|
||
export function initializeUncloseaiElements() {
|
||
const uncloseaiElements = document.querySelectorAll(".uncloseai");
|
||
|
||
uncloseaiElements.forEach((element) => {
|
||
const features = element.dataset.features || "full";
|
||
const type = element.dataset.type || "standard";
|
||
|
||
// Create container for this uncloseai instance
|
||
const container = document.createElement("div");
|
||
container.className = "uncloseai-container";
|
||
container.style.cssText = "width: 100%; margin: 10px 0;";
|
||
|
||
if (features === "full" || type === "full") {
|
||
createFullInterface(container);
|
||
} else {
|
||
createCustomInterface(container, features.split(","));
|
||
}
|
||
|
||
element.appendChild(container);
|
||
});
|
||
}
|
||
|
||
// Create full chat interface
|
||
export function createFullInterface(container) {
|
||
// Chat area
|
||
const chatContainer = document.createElement("div");
|
||
chatContainer.innerHTML = `
|
||
<div id="chat-box" style="min-height: 200px; border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; overflow-y: auto; border-radius: 4px;"></div>
|
||
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
|
||
<input type="text" id="user-input" placeholder="Ask about this page..." style="flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px;">
|
||
<button onclick="handleUserInput()" style="padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">Send</button>
|
||
</div>
|
||
`;
|
||
|
||
// Control buttons
|
||
const controlsDiv = document.createElement("div");
|
||
controlsDiv.style.cssText =
|
||
"display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; margin-bottom: 10px;";
|
||
|
||
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);
|
||
|
||
// Hidden file input
|
||
const fileInput = document.createElement("input");
|
||
fileInput.type = "file";
|
||
fileInput.setAttribute("data-uncloseai-file-input", "");
|
||
fileInput.style.display = "none";
|
||
fileInput.onchange = handleFileUpload;
|
||
|
||
container.appendChild(chatContainer);
|
||
container.appendChild(controlsDiv);
|
||
container.appendChild(fileInput);
|
||
}
|
||
|
||
// Create custom interface with specific features
|
||
export function createCustomInterface(container, features) {
|
||
const div = document.createElement("div");
|
||
div.style.cssText =
|
||
"display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; padding: 15px; border: 1px solid #ddd; border-radius: 8px;";
|
||
|
||
features.forEach((feature) => {
|
||
switch (feature.trim()) {
|
||
case "chat":
|
||
createChatFeature(div);
|
||
break;
|
||
case "tts":
|
||
createTTSFeature(div);
|
||
break;
|
||
case "translate":
|
||
createTranslateFeature(div);
|
||
break;
|
||
case "smart-translate":
|
||
createSmartTranslateFeature(div);
|
||
break;
|
||
case "upload":
|
||
createUploadFeature(div);
|
||
break;
|
||
case "read":
|
||
createReadFeature(div);
|
||
break;
|
||
}
|
||
});
|
||
|
||
container.appendChild(div);
|
||
}
|
||
|
||
// Individual feature creators
|
||
export function createChatFeature(container) {
|
||
const chatDiv = document.createElement("div");
|
||
chatDiv.innerHTML = `
|
||
<h4>AI Chat</h4>
|
||
<div style="border: 1px solid #ccc; height: 150px; padding: 8px; margin: 5px 0; overflow-y: auto;" data-chat-box></div>
|
||
<input type="text" placeholder="Ask anything..." style="width: 100%; margin: 2px 0;" data-chat-input>
|
||
<button onclick="handleCustomChat(this)" style="width: 100%; padding: 6px;">Send</button>
|
||
`;
|
||
container.appendChild(chatDiv);
|
||
}
|
||
|
||
export function createTTSFeature(container) {
|
||
const ttsDiv = document.createElement("div");
|
||
ttsDiv.innerHTML = `
|
||
<h4>Text to Speech</h4>
|
||
<textarea placeholder="Enter text to speak..." style="width: 100%; height: 80px; margin: 5px 0;" data-tts-input></textarea>
|
||
<button onclick="handleTTSFromElement(this)" style="width: 100%; padding: 6px;">🔊 Convert to Speech</button>
|
||
<div data-tts-result style="margin: 5px 0;"></div>
|
||
`;
|
||
container.appendChild(ttsDiv);
|
||
}
|
||
|
||
export function createUploadFeature(container) {
|
||
const uploadDiv = document.createElement("div");
|
||
uploadDiv.innerHTML = `
|
||
<h4>File Upload</h4>
|
||
<input type="file" style="width: 100%; margin: 5px 0;" data-upload-input>
|
||
<button onclick="handleUploadFromElement(this)" style="width: 100%; padding: 6px;">📁 Upload & Analyze</button>
|
||
<div data-upload-result style="margin: 5px 0; display: none;"></div>
|
||
`;
|
||
container.appendChild(uploadDiv);
|
||
}
|
||
|
||
export function createTranslateFeature(container) {
|
||
const translateDiv = document.createElement("div");
|
||
translateDiv.innerHTML = `
|
||
<h4>Translation Modal</h4>
|
||
<button onclick="openTranslateModal()" style="width: 100%; padding: 6px;">🌐 Translation Modal</button>
|
||
`;
|
||
container.appendChild(translateDiv);
|
||
}
|
||
|
||
export function createSmartTranslateFeature(container) {
|
||
const smartTranslateDiv = document.createElement("div");
|
||
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);
|
||
}
|
||
|
||
export function createReadFeature(container) {
|
||
const readDiv = document.createElement("div");
|
||
readDiv.innerHTML = `
|
||
<h4>Page Reader</h4>
|
||
<p style="font-size: 0.9em; margin: 5px 0;">Read this page with AI voice</p>
|
||
<button onclick="readPageWithHermes()" style="width: 100%; padding: 8px 16px; background: #6f42c1; color: white; border: none; border-radius: 4px; cursor: pointer;">📖 Read Page with AI</button>
|
||
`;
|
||
container.appendChild(readDiv);
|
||
}
|
||
|
||
// Helper functions for custom features
|
||
export function createButton(text, onclick) {
|
||
const btn = document.createElement("button");
|
||
btn.textContent = text;
|
||
btn.onclick = onclick;
|
||
btn.style.cssText =
|
||
"padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;";
|
||
return btn;
|
||
}
|
||
|
||
export async function handleTTSFromElement(button) {
|
||
const container =
|
||
button.closest("[data-tts-result]")?.parentElement || button.parentElement;
|
||
const textarea = container.querySelector("[data-tts-input]");
|
||
const resultDiv = container.querySelector("[data-tts-result]");
|
||
const text = textarea?.value?.trim();
|
||
|
||
if (!text) {
|
||
alert("Please enter some text first!");
|
||
return;
|
||
}
|
||
|
||
button.disabled = true;
|
||
button.textContent = "Converting...";
|
||
resultDiv.innerHTML = "<em>Converting to speech...</em>";
|
||
|
||
try {
|
||
const result = await speakText(text, "alloy", 0.9);
|
||
const audioControls = document.createElement("div");
|
||
audioControls.style.cssText = "margin: 10px 0;";
|
||
|
||
const playButton = document.createElement("button");
|
||
playButton.textContent = "▶️ Play";
|
||
playButton.style.cssText = "margin: 2px; padding: 4px 8px;";
|
||
playButton.onclick = () => result.audio.play();
|
||
|
||
const pauseButton = document.createElement("button");
|
||
pauseButton.textContent = "⏸️ Pause";
|
||
pauseButton.style.cssText = "margin: 2px; padding: 4px 8px;";
|
||
pauseButton.onclick = () => result.audio.pause();
|
||
|
||
audioControls.appendChild(playButton);
|
||
audioControls.appendChild(pauseButton);
|
||
|
||
resultDiv.innerHTML = "";
|
||
resultDiv.appendChild(result.audio);
|
||
resultDiv.appendChild(audioControls);
|
||
} catch (error) {
|
||
resultDiv.innerHTML = "<strong>Error:</strong> " + error.message;
|
||
} finally {
|
||
button.disabled = false;
|
||
button.textContent = "🔊 Convert to Speech";
|
||
}
|
||
}
|
||
|
||
export async function handleUploadFromElement(button) {
|
||
const container = button.parentElement;
|
||
const fileInput = container.querySelector("[data-upload-input]");
|
||
const resultDiv = container.querySelector("[data-upload-result]");
|
||
|
||
if (!fileInput.files[0]) {
|
||
alert("Please select a file first!");
|
||
return;
|
||
}
|
||
|
||
button.disabled = true;
|
||
button.textContent = "Processing...";
|
||
resultDiv.style.display = "block";
|
||
resultDiv.innerHTML = "<em>Uploading and analyzing file...</em>";
|
||
|
||
try {
|
||
showProgressIndicator("Processing file...");
|
||
const response = await uploadFile(fileInput.files[0]);
|
||
hideProgressIndicator();
|
||
|
||
resultDiv.innerHTML = `<strong>Analysis Result:</strong><br>${response}`;
|
||
fileInput.value = "";
|
||
} catch (error) {
|
||
hideProgressIndicator();
|
||
resultDiv.innerHTML = "<strong>Error:</strong> " + error.message;
|
||
} finally {
|
||
button.disabled = false;
|
||
button.textContent = "📁 Upload & Analyze";
|
||
}
|
||
}
|
||
|
||
export async function handleSmartTranslate(button) {
|
||
console.log("🤖 Smart translate clicked!", button);
|
||
|
||
// Check if dropdown already exists
|
||
const existingDropdown = button.parentElement.querySelector(
|
||
".translate-dropdown",
|
||
);
|
||
if (existingDropdown) {
|
||
existingDropdown.remove();
|
||
return;
|
||
}
|
||
|
||
// Detect page language
|
||
button.textContent = getUIText("detectingLanguage");
|
||
button.disabled = true;
|
||
|
||
try {
|
||
const currentLang = await detectPageLanguage();
|
||
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 colors = getThemeColors();
|
||
|
||
// Create dropdown
|
||
const dropdown = document.createElement("div");
|
||
dropdown.className = "translate-dropdown";
|
||
dropdown.style.cssText = `
|
||
position: absolute;
|
||
top: 100%;
|
||
left: 0;
|
||
background: ${colors.panelBackground};
|
||
border: 1px solid ${colors.borderColor};
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px ${colors.shadowColor};
|
||
z-index: 1000;
|
||
min-width: 200px;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
margin-top: 4px;
|
||
`;
|
||
|
||
// 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 =
|
||
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");
|
||
option.style.cssText = `
|
||
padding: 8px 12px;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s;
|
||
color: ${colors.primaryText};
|
||
font-size: 14px;
|
||
line-height: 1.4;
|
||
`;
|
||
|
||
// Format: "es • Spanish • Español"
|
||
option.innerHTML = `
|
||
<div style="display: flex; align-items: center; gap: 8px;">
|
||
<span style="font-family: monospace; color: ${colors.mutedText}; font-size: 12px; min-width: 24px;">${code}</span>
|
||
<span style="color: ${colors.mutedText};">•</span>
|
||
<span style="color: ${colors.secondaryText};">${englishName}</span>
|
||
<span style="color: ${colors.mutedText};">•</span>
|
||
<span style="font-weight: 500;">${nativeName}</span>
|
||
</div>
|
||
`;
|
||
|
||
option.onmouseenter = () => {
|
||
option.style.background = colors.buttonHover;
|
||
};
|
||
option.onmouseleave = () => {
|
||
option.style.background = "transparent";
|
||
};
|
||
|
||
option.onclick = async () => {
|
||
try {
|
||
// Show loading
|
||
option.innerHTML = `
|
||
<div style="display: flex; align-items: center; gap: 8px;">
|
||
<span style="font-family: monospace; color: ${colors.mutedText}; font-size: 12px; min-width: 24px;">${code}</span>
|
||
<span style="color: ${colors.mutedText};">•</span>
|
||
<span style="color: ${colors.secondaryText};">${getUIText("translatingTo")}</span>
|
||
</div>
|
||
`;
|
||
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}`);
|
||
// Restore original format
|
||
option.innerHTML = `
|
||
<div style="display: flex; align-items: center; gap: 8px;">
|
||
<span style="font-family: monospace; color: ${colors.mutedText}; font-size: 12px; min-width: 24px;">${code}</span>
|
||
<span style="color: ${colors.mutedText};">•</span>
|
||
<span style="color: ${colors.secondaryText};">${englishName}</span>
|
||
<span style="color: ${colors.mutedText};">•</span>
|
||
<span style="font-weight: 500;">${nativeName}</span>
|
||
</div>
|
||
`;
|
||
option.style.pointerEvents = "auto";
|
||
}
|
||
};
|
||
|
||
dropdown.appendChild(option);
|
||
});
|
||
|
||
// Position dropdown relative to button
|
||
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);
|
||
}
|
||
};
|
||
setTimeout(() => document.addEventListener("click", closeDropdown), 100);
|
||
} catch (error) {
|
||
button.textContent = getUIText("smartTranslate");
|
||
button.disabled = false;
|
||
alert(`Language detection failed: ${error.message}`);
|
||
}
|
||
}
|
||
|
||
// Initialize the legacy chat interface (for backward compatibility)
|
||
export function initializeChatInterface() {
|
||
// Only initialize if there are legacy elements (chat-container, user-input, etc.)
|
||
const legacyElements = document.querySelector(
|
||
"#chat-container, #user-input, #chat-box",
|
||
);
|
||
if (!legacyElements) return;
|
||
|
||
const pageContent = extractWebpageContent();
|
||
chatHistory.push({
|
||
role: "system",
|
||
content: `Here's the content of the webpage: ${pageContent}`,
|
||
});
|
||
|
||
marked.setOptions({
|
||
highlight: (code, lang) => {
|
||
const language = hljs.getLanguage(lang) ? lang : "plaintext";
|
||
return hljs.highlight(code, { language }).value;
|
||
},
|
||
});
|
||
|
||
// Add the Read Page button
|
||
addReadPageButton();
|
||
|
||
// Add the File Upload button and picker
|
||
addFileUploadButton();
|
||
|
||
// Event listeners
|
||
document
|
||
.getElementById("user-input")
|
||
?.addEventListener("keydown", (event) => {
|
||
if (event.key === "Enter" && !event.shiftKey) {
|
||
event.preventDefault();
|
||
handleUserInput();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Add button to page
|
||
export function addReadPageButton() {
|
||
const button = document.createElement("button");
|
||
button.textContent = "Read Page";
|
||
button.onclick = readPageWithHermes;
|
||
button.style.margin = "10px";
|
||
|
||
const ttsButton = document.createElement("button");
|
||
ttsButton.textContent = "TTS Anything";
|
||
ttsButton.onclick = openTTSModal;
|
||
ttsButton.style.margin = "10px";
|
||
|
||
document.body.appendChild(button);
|
||
document.body.appendChild(ttsButton);
|
||
}
|
||
|
||
// Add file upload button
|
||
export function addFileUploadButton() {
|
||
const button = document.createElement("button");
|
||
button.textContent = "Upload File";
|
||
button.style.margin = "10px";
|
||
|
||
const fileInput = document.createElement("input");
|
||
fileInput.type = "file";
|
||
fileInput.id = "file-input";
|
||
fileInput.style.display = "none";
|
||
|
||
button.onclick = () => fileInput.click();
|
||
fileInput.onchange = handleFileUpload;
|
||
|
||
document.body.appendChild(button);
|
||
document.body.appendChild(fileInput);
|
||
}
|
||
|
||
// 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) {
|
||
console.log("uncloseai.js: Found existing floating button, removing it.");
|
||
existingButton.remove();
|
||
}
|
||
|
||
// Create the main floating button
|
||
const floatingButton = document.createElement("button");
|
||
floatingButton.id = "floating-ai-button";
|
||
floatingButton.textContent = "uncloseai.";
|
||
console.log("uncloseai.js: Created new floating button element.");
|
||
|
||
// Function to update button theme
|
||
function updateButtonTheme() {
|
||
const isDark =
|
||
document.documentElement.getAttribute("data-theme") === "dark" ||
|
||
(window.matchMedia &&
|
||
window.matchMedia("(prefers-color-scheme: dark)").matches &&
|
||
!document.documentElement.getAttribute("data-theme"));
|
||
|
||
floatingButton.style.cssText = `
|
||
position: fixed;
|
||
bottom: 20px;
|
||
right: 10px;
|
||
width: 110px;
|
||
height: 55px;
|
||
border-radius: 22px;
|
||
background: ${isDark ? "#ffffff" : "#000000"};
|
||
border: 2px solid ${isDark ? "#000000" : "#ffffff"};
|
||
color: ${isDark ? "#000000" : "#ffffff"};
|
||
font-family: 'ChunkFiveRegular', monospace;
|
||
font-size: 13px;
|
||
cursor: pointer;
|
||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||
z-index: 1000;
|
||
transition: all 0.3s ease;
|
||
max-width: calc(100vw - 20px);
|
||
box-sizing: border-box;
|
||
`;
|
||
}
|
||
|
||
// Initial theme setup
|
||
updateButtonTheme();
|
||
|
||
// Watch for theme changes
|
||
const observer = new MutationObserver(updateButtonTheme);
|
||
observer.observe(document.documentElement, {
|
||
attributes: true,
|
||
attributeFilter: ["data-theme"],
|
||
});
|
||
|
||
// Watch for system theme changes
|
||
if (window.matchMedia) {
|
||
window
|
||
.matchMedia("(prefers-color-scheme: dark)")
|
||
.addEventListener("change", updateButtonTheme);
|
||
}
|
||
|
||
// Hover effects
|
||
floatingButton.onmouseenter = () => {
|
||
floatingButton.style.transform = "scale(1.1)";
|
||
floatingButton.style.boxShadow = "0 6px 16px rgba(0,0,0,0.4)";
|
||
};
|
||
floatingButton.onmouseleave = () => {
|
||
floatingButton.style.transform = "scale(1)";
|
||
floatingButton.style.boxShadow = "0 4px 12px rgba(0,0,0,0.3)";
|
||
};
|
||
|
||
// Set up self-contained click handler
|
||
setupFloatingButtonHandler(floatingButton);
|
||
|
||
document.body.appendChild(floatingButton);
|
||
console.log("uncloseai.js: Appended floating button to document.body.");
|
||
}
|
||
|
||
// Set up click handler for floating button (self-contained)
|
||
function setupFloatingButtonHandler(floatingButton) {
|
||
floatingButton.onclick = async () => {
|
||
console.log("Floating button clicked!");
|
||
|
||
// Show loading state
|
||
const originalText = floatingButton.textContent;
|
||
floatingButton.textContent = "loading...";
|
||
floatingButton.disabled = true;
|
||
floatingButton.style.opacity = "0.7";
|
||
floatingButton.style.cursor = "wait";
|
||
|
||
try {
|
||
console.log("Trying to toggle Hermes modal...");
|
||
|
||
// Try multiple ways to access the modal function
|
||
let modalFunction = null;
|
||
|
||
if (typeof toggleUncloseaiEmbeddedModal === "function") {
|
||
console.log("Found toggleUncloseaiEmbeddedModal in local scope");
|
||
modalFunction = toggleUncloseaiEmbeddedModal;
|
||
} else if (typeof window.toggleUncloseaiEmbeddedModal === "function") {
|
||
console.log("Found toggleUncloseaiEmbeddedModal on window");
|
||
modalFunction = window.toggleUncloseaiEmbeddedModal;
|
||
} else {
|
||
console.log(
|
||
"toggleUncloseaiEmbeddedModal not found, trying dynamic import",
|
||
);
|
||
// Try to import it dynamically as a fallback
|
||
try {
|
||
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,
|
||
);
|
||
}
|
||
}
|
||
|
||
if (modalFunction) {
|
||
console.log("Calling modal function");
|
||
await modalFunction();
|
||
} else {
|
||
console.error(
|
||
"toggleUncloseaiEmbeddedModal function not available in any form",
|
||
);
|
||
alert("AI modal not available in this context");
|
||
}
|
||
} catch (error) {
|
||
console.error("Error opening modal:", error);
|
||
alert("Error opening AI modal: " + error.message);
|
||
} finally {
|
||
// Restore button state
|
||
floatingButton.textContent = originalText;
|
||
floatingButton.disabled = false;
|
||
floatingButton.style.opacity = "1";
|
||
floatingButton.style.cursor = "pointer";
|
||
}
|
||
};
|
||
}
|
||
|
||
// New mobile-first Hermes modal
|
||
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;
|
||
padding: 0;
|
||
border: none;
|
||
background: ${colors.modalBackground};
|
||
color: ${colors.primaryText};
|
||
z-index: 2000;
|
||
${
|
||
isMobile
|
||
? `
|
||
top: 0;
|
||
left: 0;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
max-width: 100vw;
|
||
max-height: 100vh;
|
||
border-radius: 0;
|
||
`
|
||
: `
|
||
width: 90vw;
|
||
max-width: 800px;
|
||
height: 90vh;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
border-radius: 16px;
|
||
box-shadow: 0 20px 40px ${colors.shadowColor};
|
||
`
|
||
}
|
||
`;
|
||
|
||
// Simple container with flex layout
|
||
const container = document.createElement("div");
|
||
container.style.cssText = `
|
||
width: 100%;
|
||
height: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
`;
|
||
|
||
// Header
|
||
const header = document.createElement("header");
|
||
header.style.cssText = `
|
||
flex-shrink: 0;
|
||
padding: 16px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
box-shadow: 0 2px 4px ${colors.lightShadow};
|
||
`;
|
||
|
||
const titleContainer = document.createElement("div");
|
||
titleContainer.style.cssText = `flex: 1; display: flex; flex-direction: column; gap: 4px;`;
|
||
|
||
const title = document.createElement("h2");
|
||
title.innerHTML =
|
||
"🤖 <span style=\"font-family: 'ChunkFiveRegular', monospace;\">uncloseai.</span>";
|
||
title.style.cssText = `
|
||
margin: 0;
|
||
font-size: ${isMobile ? "18px" : "20px"};
|
||
font-weight: 600;
|
||
`;
|
||
|
||
const subtitle = document.createElement("div");
|
||
const pageTitle = document.title || window.location.hostname;
|
||
subtitle.innerHTML = `
|
||
<div style="font-size: 12px; opacity: 0.9; font-weight: normal; line-height: 1.3;">
|
||
<span style="font-family: 'ChunkFiveRegular', monospace;">uncloseai.</span> presents nous research's <a href="https://nousresearch.com/hermes3/" target="_blank" style="color: rgba(255,255,255,0.9); text-decoration: underline;">hermes</a> large language model<br>
|
||
<span style="opacity: 0.8;">You are discussing: ${pageTitle}</span>
|
||
</div>
|
||
`;
|
||
|
||
titleContainer.appendChild(title);
|
||
titleContainer.appendChild(subtitle);
|
||
|
||
const closeBtn = document.createElement("button");
|
||
closeBtn.textContent = "✕";
|
||
closeBtn.style.cssText = `
|
||
background: rgba(255,255,255,0.2);
|
||
border: none;
|
||
color: white;
|
||
font-size: 20px;
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
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.onclick = () => {
|
||
modal.close();
|
||
document.body.removeChild(modal);
|
||
uncloseaiEmbeddedModalOpen = false;
|
||
};
|
||
|
||
// Add settings/menu button
|
||
const menuBtn = document.createElement("button");
|
||
menuBtn.textContent = "⚙️";
|
||
menuBtn.style.cssText = `
|
||
background: rgba(255,255,255,0.2);
|
||
border: none;
|
||
color: white;
|
||
font-size: 18px;
|
||
width: 36px;
|
||
height: 36px;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
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)");
|
||
|
||
const headerRight = document.createElement("div");
|
||
headerRight.style.cssText = `display: flex; gap: 8px; align-items: center;`;
|
||
headerRight.appendChild(menuBtn);
|
||
headerRight.appendChild(closeBtn);
|
||
|
||
header.appendChild(titleContainer);
|
||
header.appendChild(headerRight);
|
||
|
||
// Chat area
|
||
const chatArea = document.createElement("div");
|
||
chatArea.style.cssText = `
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 16px;
|
||
background: ${colors.contentBackground};
|
||
-webkit-overflow-scrolling: touch;
|
||
`;
|
||
|
||
const chatBox = document.createElement("div");
|
||
chatBox.id = "modal-chat-box";
|
||
chatBox.style.cssText = `
|
||
max-width: 100%;
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
`;
|
||
chatArea.appendChild(chatBox);
|
||
|
||
// Settings panel (collapsible)
|
||
const settingsPanel = document.createElement("div");
|
||
settingsPanel.style.cssText = `
|
||
flex-shrink: 0;
|
||
background: ${colors.panelBackground};
|
||
border-top: 1px solid ${colors.dividerColor};
|
||
border-bottom: 1px solid ${colors.dividerColor};
|
||
padding: 16px;
|
||
display: none;
|
||
max-height: 40vh;
|
||
overflow-y: auto;
|
||
`;
|
||
|
||
// Model selection
|
||
const modelSection = document.createElement("div");
|
||
modelSection.style.cssText = `margin-bottom: 16px;`;
|
||
|
||
const modelLabel = document.createElement("label");
|
||
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%;
|
||
padding: 8px 12px;
|
||
border: 1px solid ${colors.borderColor};
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
background: ${colors.inputBackground};
|
||
color: ${colors.primaryText};
|
||
`;
|
||
|
||
// Store models for selection handler
|
||
let loadedModels = [];
|
||
|
||
// Load models
|
||
const loadModels = async () => {
|
||
try {
|
||
console.log("Loading models for Hermes modal...");
|
||
const models = await fetchModelsFromEndpoints();
|
||
console.log("Models loaded:", models);
|
||
loadedModels = models; // Store for selection handler
|
||
modelSelect.innerHTML = "";
|
||
|
||
if (models && models.length > 0) {
|
||
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 currentModel = getSelectedModel();
|
||
|
||
if (savedModel && 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,
|
||
);
|
||
}
|
||
} else if (currentModel) {
|
||
const currentOption = Array.from(modelSelect.options).find((opt) =>
|
||
opt.textContent.includes(currentModel),
|
||
);
|
||
if (currentOption) {
|
||
modelSelect.value = currentOption.value;
|
||
}
|
||
}
|
||
} else {
|
||
const option = document.createElement("option");
|
||
option.textContent = "No models available";
|
||
option.disabled = true;
|
||
modelSelect.appendChild(option);
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to load models:", error);
|
||
const option = document.createElement("option");
|
||
option.textContent = "Error loading models";
|
||
option.disabled = true;
|
||
modelSelect.appendChild(option);
|
||
}
|
||
};
|
||
|
||
// Handle model selection changes
|
||
modelSelect.onchange = () => {
|
||
const selectedUniqueId = modelSelect.value;
|
||
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);
|
||
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 = 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 = `
|
||
width: 100%;
|
||
padding: 8px 12px;
|
||
border: 1px solid ${colors.borderColor};
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
background: ${colors.inputBackground};
|
||
color: ${colors.primaryText};
|
||
`;
|
||
|
||
const voices = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"];
|
||
voices.forEach((voice, index) => {
|
||
const option = document.createElement("option");
|
||
option.value = voice;
|
||
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";
|
||
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);
|
||
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 = getUIText("quickActions");
|
||
actionsLabel.style.cssText = `margin-bottom: 8px; font-weight: 600; color: #495057;`;
|
||
|
||
const actionsGrid = document.createElement("div");
|
||
actionsGrid.style.cssText = `
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||
gap: 8px;
|
||
`;
|
||
|
||
const actions = [
|
||
{ 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 }) => {
|
||
const btn = document.createElement("button");
|
||
btn.textContent = text;
|
||
btn.style.cssText = `
|
||
padding: 8px 12px;
|
||
background: ${colors.buttonBackground};
|
||
border: 1px solid ${colors.buttonBorder};
|
||
border-radius: 6px;
|
||
cursor: pointer;
|
||
font-size: 13px;
|
||
color: ${colors.primaryText};
|
||
transition: all 0.2s;
|
||
white-space: nowrap;
|
||
`;
|
||
btn.onmouseover = () => {
|
||
btn.style.background = colors.buttonHover;
|
||
btn.style.borderColor = colors.buttonBorder;
|
||
};
|
||
btn.onmouseout = () => {
|
||
btn.style.background = colors.buttonBackground;
|
||
btn.style.borderColor = colors.buttonBorder;
|
||
};
|
||
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)";
|
||
|
||
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());
|
||
};
|
||
|
||
// Quick action buttons
|
||
const controls = document.createElement("div");
|
||
controls.style.cssText = `
|
||
flex-shrink: 0;
|
||
padding: 8px 16px;
|
||
background: ${colors.panelBackground};
|
||
border-top: 1px solid ${colors.dividerColor};
|
||
border-bottom: 1px solid ${colors.dividerColor};
|
||
display: flex;
|
||
gap: 8px;
|
||
overflow-x: auto;
|
||
-webkit-overflow-scrolling: touch;
|
||
`;
|
||
|
||
// HTML to Markdown converter function
|
||
function htmlToMarkdown(html) {
|
||
// Create a temporary element to parse HTML
|
||
const temp = document.createElement("div");
|
||
temp.innerHTML = html;
|
||
|
||
// Remove script and style elements
|
||
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 "";
|
||
|
||
const tag = node.tagName.toLowerCase();
|
||
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");
|
||
return href ? `[${children}](${href})` : children;
|
||
}
|
||
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;
|
||
}
|
||
}
|
||
|
||
return processNode(temp).trim();
|
||
}
|
||
|
||
let readPageAudio = null;
|
||
let isReadingPage = false;
|
||
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 = () => {
|
||
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 = `
|
||
padding: 8px 16px;
|
||
background: ${colors.buttonBackground};
|
||
border: 1px solid ${colors.buttonBorder};
|
||
border-radius: 20px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
white-space: nowrap;
|
||
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) {
|
||
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;
|
||
}
|
||
},
|
||
},
|
||
{ 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 = `
|
||
position: absolute;
|
||
top: 100%;
|
||
left: 0;
|
||
background: ${colors.panelBackground};
|
||
border: 1px solid ${colors.borderColor};
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px ${colors.shadowColor};
|
||
z-index: 1000;
|
||
min-width: 200px;
|
||
max-height: 300px;
|
||
overflow-y: auto;
|
||
margin-top: 4px;
|
||
`;
|
||
|
||
// 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 = `
|
||
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
|
||
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();
|
||
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 =
|
||
'<div style="font-family: system-ui, -apple-system, sans-serif;">';
|
||
history.forEach((msg) => {
|
||
if (msg.role === "user") {
|
||
htmlContent += `<div style="margin-bottom: 16px;"><strong>You:</strong><br>${marked.parse(msg.content)}</div>`;
|
||
} else if (msg.role === "assistant") {
|
||
htmlContent += `<div style="margin-bottom: 16px;"><strong>AI:</strong><br>${marked.parse(msg.content)}</div>`;
|
||
}
|
||
});
|
||
htmlContent += "</div>";
|
||
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 }) => {
|
||
const btn = document.createElement("button");
|
||
btn.textContent = text;
|
||
btn.style.cssText = `
|
||
padding: 8px 16px;
|
||
background: ${colors.buttonBackground};
|
||
border: 1px solid ${colors.buttonBorder};
|
||
border-radius: 20px;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
font-size: 14px;
|
||
color: ${colors.primaryText} !important;
|
||
transition: all 0.2s;
|
||
`;
|
||
btn.onmouseover = () => {
|
||
btn.style.background = colors.buttonHover;
|
||
btn.style.borderColor = colors.buttonBorder;
|
||
};
|
||
btn.onmouseout = () => {
|
||
btn.style.background = colors.buttonBackground;
|
||
btn.style.borderColor = colors.buttonBorder;
|
||
};
|
||
btn.onclick = () => action(btn);
|
||
controls.appendChild(btn);
|
||
});
|
||
|
||
// Assemble modal structure
|
||
container.appendChild(header);
|
||
container.appendChild(settingsPanel);
|
||
container.appendChild(chatArea);
|
||
container.appendChild(controls);
|
||
|
||
// Input area
|
||
const inputArea = document.createElement("div");
|
||
inputArea.style.cssText = `
|
||
flex-shrink: 0;
|
||
padding: 16px;
|
||
background: ${colors.panelBackground};
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: flex-end;
|
||
box-shadow: 0 -2px 10px ${colors.lightShadow};
|
||
`;
|
||
|
||
const input = document.createElement("textarea");
|
||
input.id = "modal-user-input";
|
||
input.placeholder = "Ask about this page...";
|
||
input.style.cssText = `
|
||
flex: 1;
|
||
min-height: 44px;
|
||
max-height: 120px;
|
||
padding: 12px;
|
||
border: 1px solid ${colors.borderColor};
|
||
border-radius: 24px;
|
||
resize: none;
|
||
font-size: 16px;
|
||
font-family: inherit;
|
||
line-height: 1.4;
|
||
outline: none;
|
||
background: ${colors.inputBackground};
|
||
color: ${colors.primaryText};
|
||
transition: border-color 0.2s;
|
||
`;
|
||
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";
|
||
};
|
||
|
||
const sendBtn = document.createElement("button");
|
||
sendBtn.textContent = "→";
|
||
sendBtn.style.cssText = `
|
||
width: 44px;
|
||
height: 44px;
|
||
padding: 0;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
font-size: 20px;
|
||
font-weight: bold;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: transform 0.2s;
|
||
flex-shrink: 0;
|
||
`;
|
||
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";
|
||
|
||
// Add user message
|
||
const userMsg = document.createElement("div");
|
||
userMsg.style.cssText = `
|
||
align-self: flex-end;
|
||
background: ${colors.userMessageBg};
|
||
color: ${colors.userMessageText};
|
||
padding: 12px 16px;
|
||
border-radius: 18px 18px 4px 18px;
|
||
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;
|
||
gap: 8px;
|
||
opacity: 0.8;
|
||
justify-content: flex-end;
|
||
`;
|
||
|
||
const userTtsBtn = document.createElement("button");
|
||
userTtsBtn.textContent = "🔊";
|
||
userTtsBtn.title = "Read aloud";
|
||
userTtsBtn.style.cssText = `
|
||
background: ${colors.actionBg};
|
||
border: 1px solid ${colors.actionBorder};
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
color: ${colors.userMessageText};
|
||
transition: all 0.2s;
|
||
`;
|
||
userTtsBtn.onmouseenter = () => {
|
||
userTtsBtn.style.background = colors.actionBgHover;
|
||
};
|
||
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";
|
||
|
||
// If audio is already playing, pause it
|
||
if (userAudio && !userAudio.paused) {
|
||
userAudio.pause();
|
||
userTtsBtn.textContent = "🔊";
|
||
isPlaying = false;
|
||
return;
|
||
}
|
||
|
||
// If audio exists and is paused, resume it
|
||
if (userAudio && userAudio.paused && userAudio.currentTime > 0) {
|
||
userAudio.play();
|
||
userTtsBtn.textContent = "⏸️";
|
||
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();
|
||
} catch (playError) {
|
||
userTtsBtn.textContent = "🔊";
|
||
}
|
||
} catch (error) {
|
||
alert("User TTS failed: " + error.message);
|
||
userTtsBtn.textContent = originalText;
|
||
} finally {
|
||
userTtsBtn.disabled = false;
|
||
}
|
||
};
|
||
|
||
const userDeleteBtn = document.createElement("button");
|
||
userDeleteBtn.textContent = "🗑️";
|
||
userDeleteBtn.title = "Delete message";
|
||
userDeleteBtn.style.cssText = `
|
||
background: ${colors.actionBg};
|
||
border: 1px solid ${colors.actionBorder};
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
color: ${colors.userMessageText};
|
||
transition: all 0.2s;
|
||
`;
|
||
userDeleteBtn.onmouseenter = () => {
|
||
userDeleteBtn.style.background = colors.actionBgHover;
|
||
};
|
||
userDeleteBtn.onmouseleave = () => {
|
||
userDeleteBtn.style.background = colors.actionBg;
|
||
};
|
||
userDeleteBtn.onclick = () => {
|
||
// Remove from chat history
|
||
const currentHistory = getChatHistory();
|
||
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);
|
||
userMsg.appendChild(userMessageActions);
|
||
chatBox.appendChild(userMsg);
|
||
|
||
// Add AI response placeholder
|
||
const aiMsg = document.createElement("div");
|
||
aiMsg.style.cssText = `
|
||
align-self: flex-start;
|
||
background: ${colors.aiMessageBg};
|
||
color: ${colors.aiMessageText};
|
||
padding: 12px 16px;
|
||
border-radius: 18px 18px 18px 4px;
|
||
max-width: 80%;
|
||
box-shadow: 0 1px 2px ${colors.lightShadow};
|
||
`;
|
||
aiMsg.innerHTML = `<em style="color: ${colors.mutedText};">Thinking...</em>`;
|
||
chatBox.appendChild(aiMsg);
|
||
|
||
chatArea.scrollTop = chatArea.scrollHeight;
|
||
|
||
try {
|
||
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 currentHistory = getChatHistory();
|
||
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) + "...",
|
||
})),
|
||
);
|
||
|
||
// Add TTS and delete buttons to AI message
|
||
const messageActions = document.createElement("div");
|
||
messageActions.style.cssText = `
|
||
margin-top: 8px;
|
||
display: flex;
|
||
gap: 8px;
|
||
opacity: 0.7;
|
||
`;
|
||
|
||
const ttsBtn = document.createElement("button");
|
||
ttsBtn.textContent = "🔊";
|
||
ttsBtn.title = "Read aloud";
|
||
ttsBtn.style.cssText = `
|
||
background: ${colors.buttonBackground};
|
||
border: 1px solid ${colors.borderColor};
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
color: ${colors.primaryText};
|
||
transition: all 0.2s;
|
||
`;
|
||
// 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";
|
||
|
||
// If audio is already playing, pause it
|
||
if (currentAudio && !currentAudio.paused) {
|
||
currentAudio.pause();
|
||
ttsBtn.textContent = "🔊";
|
||
isPlaying = false;
|
||
console.log("🔊 CHAT TTS: Paused");
|
||
return;
|
||
}
|
||
|
||
// If audio exists and is paused, resume it
|
||
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();
|
||
console.log("🔊 CHAT TTS: Playing new audio");
|
||
} catch (playError) {
|
||
console.log("🔊 CHAT TTS: Auto-play blocked:", playError.message);
|
||
ttsBtn.textContent = "🔊";
|
||
}
|
||
} catch (error) {
|
||
console.error("🔊 CHAT TTS: Failed with error:", error);
|
||
alert("Chat TTS failed: " + error.message);
|
||
ttsBtn.textContent = originalText;
|
||
} finally {
|
||
ttsBtn.disabled = false;
|
||
}
|
||
};
|
||
|
||
const deleteBtn = document.createElement("button");
|
||
deleteBtn.textContent = "🗑️";
|
||
deleteBtn.title = "Delete message";
|
||
deleteBtn.style.cssText = `
|
||
background: ${colors.buttonBackground};
|
||
border: 1px solid ${colors.borderColor};
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
color: ${colors.primaryText};
|
||
transition: all 0.2s;
|
||
`;
|
||
deleteBtn.onclick = () => {
|
||
userMsg.remove();
|
||
aiMsg.remove();
|
||
};
|
||
|
||
// Copy Raw (Markdown) button
|
||
const copyRawBtn = document.createElement("button");
|
||
copyRawBtn.textContent = "📋";
|
||
copyRawBtn.title = "Copy raw (Markdown)";
|
||
copyRawBtn.style.cssText = `
|
||
background: ${colors.buttonBackground};
|
||
border: 1px solid ${colors.borderColor};
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
color: ${colors.primaryText};
|
||
transition: all 0.2s;
|
||
`;
|
||
copyRawBtn.onclick = async () => {
|
||
try {
|
||
await navigator.clipboard.writeText(response);
|
||
const originalText = copyRawBtn.textContent;
|
||
copyRawBtn.textContent = "✓";
|
||
setTimeout(() => {
|
||
copyRawBtn.textContent = originalText;
|
||
}, 1000);
|
||
} catch (error) {
|
||
alert("Failed to copy: " + error.message);
|
||
}
|
||
};
|
||
|
||
// Copy HTML button
|
||
const copyHtmlBtn = document.createElement("button");
|
||
copyHtmlBtn.textContent = "📄";
|
||
copyHtmlBtn.title = "Copy HTML";
|
||
copyHtmlBtn.style.cssText = `
|
||
background: ${colors.buttonBackground};
|
||
border: 1px solid ${colors.borderColor};
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
color: ${colors.primaryText};
|
||
transition: all 0.2s;
|
||
`;
|
||
copyHtmlBtn.onclick = async () => {
|
||
try {
|
||
const htmlContent = marked.parse(response);
|
||
await navigator.clipboard.writeText(htmlContent);
|
||
const originalText = copyHtmlBtn.textContent;
|
||
copyHtmlBtn.textContent = "✓";
|
||
setTimeout(() => {
|
||
copyHtmlBtn.textContent = originalText;
|
||
}, 1000);
|
||
} catch (error) {
|
||
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 = `<span style="color: #dc3545;">Error: ${error.message}</span>`;
|
||
}
|
||
|
||
chatArea.scrollTop = chatArea.scrollHeight;
|
||
};
|
||
|
||
sendBtn.onclick = sendMessageHandler;
|
||
input.onkeydown = (e) => {
|
||
if (e.key === "Enter" && !e.shiftKey) {
|
||
e.preventDefault();
|
||
sendMessageHandler();
|
||
}
|
||
};
|
||
|
||
inputArea.appendChild(input);
|
||
inputArea.appendChild(sendBtn);
|
||
container.appendChild(inputArea);
|
||
modal.appendChild(container);
|
||
|
||
document.body.appendChild(modal);
|
||
modal.showModal();
|
||
|
||
modal.addEventListener("click", (e) => {
|
||
if (e.target === modal) {
|
||
modal.close();
|
||
document.body.removeChild(modal);
|
||
uncloseaiEmbeddedModalOpen = false;
|
||
}
|
||
});
|
||
|
||
// Theme change monitoring
|
||
const updateModalTheme = () => {
|
||
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) => {
|
||
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;
|
||
btn.style.borderColor = newColors.buttonBorder;
|
||
};
|
||
btn.onmouseout = () => {
|
||
btn.style.background = newColors.buttonBackground;
|
||
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}`;
|
||
});
|
||
};
|
||
|
||
// Set up theme change listeners
|
||
const themeObserver = new MutationObserver(updateModalTheme);
|
||
themeObserver.observe(document.documentElement, {
|
||
attributes: true,
|
||
attributeFilter: ["data-theme"],
|
||
});
|
||
|
||
if (window.matchMedia) {
|
||
window
|
||
.matchMedia("(prefers-color-scheme: dark)")
|
||
.addEventListener("change", updateModalTheme);
|
||
}
|
||
|
||
// Clean up listeners when modal is closed
|
||
const originalClose = modal.close.bind(modal);
|
||
modal.close = () => {
|
||
themeObserver.disconnect();
|
||
if (window.matchMedia) {
|
||
window
|
||
.matchMedia("(prefers-color-scheme: dark)")
|
||
.removeEventListener("change", updateModalTheme);
|
||
}
|
||
originalClose();
|
||
};
|
||
|
||
input.focus();
|
||
|
||
// Load models on startup
|
||
loadModels();
|
||
|
||
// Load conversation history
|
||
const loadHistory = async () => {
|
||
try {
|
||
const history = loadConversationHistory();
|
||
console.log("Raw history from localStorage:", history);
|
||
console.log("History length:", history ? history.length : 0);
|
||
if (history && history.length > 0) {
|
||
// Set up page context for existing conversation
|
||
const pageContent = extractWebpageContent();
|
||
const pageTitle = document.title || window.location.hostname;
|
||
const conversationContextAppend = `
|
||
|
||
PAGE CONTEXT FOR THIS CONVERSATION:
|
||
You are embedded on the webpage: "${pageTitle}"
|
||
URL: ${window.location.href}
|
||
|
||
FULL PAGE CONTENT:
|
||
${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");
|
||
|
||
// Create new history with updated system message and loaded conversation
|
||
const newHistory = [
|
||
{ 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("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") {
|
||
const userMsg = document.createElement("div");
|
||
userMsg.style.cssText = `
|
||
align-self: flex-end;
|
||
background: ${colors.userMessageBg};
|
||
color: ${colors.userMessageText};
|
||
padding: 12px 16px;
|
||
border-radius: 18px 18px 4px 18px;
|
||
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;
|
||
gap: 8px;
|
||
opacity: 0.8;
|
||
justify-content: flex-end;
|
||
`;
|
||
|
||
const userTtsBtn = document.createElement("button");
|
||
userTtsBtn.textContent = "🔊";
|
||
userTtsBtn.title = "Read aloud";
|
||
userTtsBtn.style.cssText = `
|
||
background: ${colors.actionBg};
|
||
border: 1px solid ${colors.actionBorder};
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
color: ${colors.userMessageText};
|
||
transition: all 0.2s;
|
||
`;
|
||
userTtsBtn.onmouseenter = () => {
|
||
userTtsBtn.style.background = "rgba(255, 255, 255, 0.3)";
|
||
};
|
||
userTtsBtn.onmouseleave = () => {
|
||
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";
|
||
|
||
// 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
|
||
) {
|
||
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,
|
||
);
|
||
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();
|
||
} catch (playError) {
|
||
userTtsBtn.textContent = "🔊";
|
||
}
|
||
} catch (error) {
|
||
alert("User TTS failed: " + error.message);
|
||
userTtsBtn.textContent = originalText;
|
||
} finally {
|
||
userTtsBtn.disabled = false;
|
||
}
|
||
};
|
||
|
||
const userDeleteBtn = document.createElement("button");
|
||
userDeleteBtn.textContent = "🗑️";
|
||
userDeleteBtn.title = "Delete message";
|
||
userDeleteBtn.style.cssText = `
|
||
background: rgba(255, 255, 255, 0.2);
|
||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
color: ${colors.userMessageText};
|
||
transition: all 0.2s;
|
||
`;
|
||
userDeleteBtn.onmouseenter = () => {
|
||
userDeleteBtn.style.background = "rgba(255, 255, 255, 0.3)";
|
||
};
|
||
userDeleteBtn.onmouseleave = () => {
|
||
userDeleteBtn.style.background = "rgba(255, 255, 255, 0.2)";
|
||
};
|
||
userDeleteBtn.onclick = async () => {
|
||
// Remove from chat history by index
|
||
const currentHistory = loadConversationHistory();
|
||
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 newHistory = [
|
||
{ 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") {
|
||
const aiMsg = document.createElement("div");
|
||
aiMsg.style.cssText = `
|
||
align-self: flex-start;
|
||
background: ${colors.aiMessageBg};
|
||
color: ${colors.aiMessageText};
|
||
padding: 12px 16px;
|
||
border-radius: 18px 18px 18px 4px;
|
||
max-width: 80%;
|
||
box-shadow: 0 1px 2px ${colors.lightShadow};
|
||
`;
|
||
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 = `
|
||
margin-top: 8px;
|
||
display: flex;
|
||
gap: 8px;
|
||
opacity: 0.7;
|
||
`;
|
||
|
||
const ttsBtn = document.createElement("button");
|
||
ttsBtn.textContent = "🔊";
|
||
ttsBtn.title = "Read aloud";
|
||
ttsBtn.style.cssText = `
|
||
background: ${colors.buttonBackground};
|
||
border: 1px solid ${colors.borderColor};
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
color: ${colors.primaryText};
|
||
transition: all 0.2s;
|
||
`;
|
||
// Store audio for pause/resume functionality
|
||
let historicalAudio = null;
|
||
|
||
ttsBtn.onclick = async () => {
|
||
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();
|
||
ttsBtn.textContent = "🔊";
|
||
console.log("🔊 HISTORICAL TTS: Paused");
|
||
return;
|
||
}
|
||
|
||
// If audio exists and is paused, resume it
|
||
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,
|
||
);
|
||
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,
|
||
);
|
||
ttsBtn.textContent = "🔊";
|
||
}
|
||
} catch (error) {
|
||
console.error("🔊 HISTORICAL TTS: Failed with error:", error);
|
||
alert("Historical TTS failed: " + error.message);
|
||
ttsBtn.textContent = originalText;
|
||
} finally {
|
||
ttsBtn.disabled = false;
|
||
}
|
||
};
|
||
|
||
const deleteBtn = document.createElement("button");
|
||
deleteBtn.textContent = "🗑️";
|
||
deleteBtn.title = "Delete message";
|
||
deleteBtn.style.cssText = `
|
||
background: none;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
transition: all 0.2s;
|
||
`;
|
||
deleteBtn.onclick = () => {
|
||
aiMsg.remove();
|
||
// TODO: Remove from chat history
|
||
};
|
||
|
||
// Copy Raw (Markdown) button
|
||
const copyRawBtn = document.createElement("button");
|
||
copyRawBtn.textContent = "📋";
|
||
copyRawBtn.title = "Copy raw (Markdown)";
|
||
copyRawBtn.style.cssText = `
|
||
background: none;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
transition: all 0.2s;
|
||
`;
|
||
copyRawBtn.onclick = async () => {
|
||
try {
|
||
await navigator.clipboard.writeText(msg.content);
|
||
const originalText = copyRawBtn.textContent;
|
||
copyRawBtn.textContent = "✓";
|
||
setTimeout(() => {
|
||
copyRawBtn.textContent = originalText;
|
||
}, 1000);
|
||
} catch (error) {
|
||
alert("Failed to copy: " + error.message);
|
||
}
|
||
};
|
||
|
||
// Copy HTML button
|
||
const copyHtmlBtn = document.createElement("button");
|
||
copyHtmlBtn.textContent = "📄";
|
||
copyHtmlBtn.title = "Copy HTML";
|
||
copyHtmlBtn.style.cssText = `
|
||
background: none;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
transition: all 0.2s;
|
||
`;
|
||
copyHtmlBtn.onclick = async () => {
|
||
try {
|
||
const htmlContent = marked.parse(msg.content);
|
||
await navigator.clipboard.writeText(htmlContent);
|
||
const originalText = copyHtmlBtn.textContent;
|
||
copyHtmlBtn.textContent = "✓";
|
||
setTimeout(() => {
|
||
copyHtmlBtn.textContent = originalText;
|
||
}, 1000);
|
||
} catch (error) {
|
||
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");
|
||
}
|
||
});
|
||
chatArea.scrollTop = chatArea.scrollHeight;
|
||
} else {
|
||
// Add intro message if no history
|
||
await addIntroMessage();
|
||
}
|
||
} catch (error) {
|
||
console.error("Failed to load conversation history:", error);
|
||
await addIntroMessage();
|
||
}
|
||
};
|
||
|
||
const addIntroMessage = async () => {
|
||
const introMsg = document.createElement("div");
|
||
introMsg.style.cssText = `
|
||
align-self: flex-start;
|
||
background: white;
|
||
padding: 12px 16px;
|
||
border-radius: 18px 18px 18px 4px;
|
||
max-width: 80%;
|
||
box-shadow: 0 1px 2px rgba(0,0,0,0.1);
|
||
color: #495057;
|
||
`;
|
||
|
||
// Show loading message first
|
||
introMsg.innerHTML =
|
||
'<em style="color: #6c757d;">Analyzing page and generating welcome message...</em>';
|
||
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.
|
||
|
||
PAGE INFORMATION:
|
||
Title: "${pageTitle}"
|
||
URL: ${window.location.href}
|
||
|
||
FULL PAGE CONTENT:
|
||
${pageContent}
|
||
|
||
Generate a 3-paragraph introduction that:
|
||
1. Introduces yourself as Hermes AI and acknowledges the specific page/content
|
||
2. Explains how you can help with this page content and related topics
|
||
3. Invites questions and explains your capabilities
|
||
|
||
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 },
|
||
];
|
||
|
||
// Generate intro with specialized system prompt
|
||
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 = `
|
||
|
||
PAGE CONTEXT FOR THIS CONVERSATION:
|
||
You are embedded on the webpage: "${pageTitle}"
|
||
URL: ${window.location.href}
|
||
|
||
FULL PAGE CONTENT:
|
||
${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");
|
||
|
||
// Create new history with system message and intro
|
||
const newHistory = [
|
||
{ 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 = `
|
||
margin-top: 8px;
|
||
display: flex;
|
||
gap: 8px;
|
||
opacity: 0.7;
|
||
`;
|
||
|
||
const ttsBtn = document.createElement("button");
|
||
ttsBtn.textContent = "🔊";
|
||
ttsBtn.title = "Read aloud";
|
||
ttsBtn.style.cssText = `
|
||
background: none;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
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";
|
||
|
||
// If audio is already playing, pause it
|
||
if (introAudio && !introAudio.paused) {
|
||
introAudio.pause();
|
||
ttsBtn.textContent = "🔊";
|
||
console.log("🔊 INTRO TTS: Paused");
|
||
return;
|
||
}
|
||
|
||
// If audio exists and is paused, resume it
|
||
if (introAudio && introAudio.paused && introAudio.currentTime > 0) {
|
||
introAudio.play();
|
||
ttsBtn.textContent = "⏸️";
|
||
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();
|
||
console.log("🔊 INTRO TTS: Playing new audio");
|
||
} catch (playError) {
|
||
console.log("🔊 INTRO TTS: Auto-play blocked:", playError.message);
|
||
ttsBtn.textContent = "🔊";
|
||
}
|
||
} catch (error) {
|
||
console.error("🔊 INTRO TTS: Failed with error:", error);
|
||
alert("Intro TTS failed: " + error.message);
|
||
ttsBtn.textContent = originalText;
|
||
} finally {
|
||
ttsBtn.disabled = false;
|
||
}
|
||
};
|
||
|
||
const deleteBtn = document.createElement("button");
|
||
deleteBtn.textContent = "🗑️";
|
||
deleteBtn.title = "Delete message";
|
||
deleteBtn.style.cssText = `
|
||
background: none;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
transition: all 0.2s;
|
||
`;
|
||
deleteBtn.onclick = () => {
|
||
introMsg.remove();
|
||
};
|
||
|
||
// Copy Raw (Markdown) button
|
||
const copyRawBtn = document.createElement("button");
|
||
copyRawBtn.textContent = "📋";
|
||
copyRawBtn.title = "Copy raw (Markdown)";
|
||
copyRawBtn.style.cssText = `
|
||
background: ${colors.buttonBackground};
|
||
border: 1px solid ${colors.borderColor};
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
color: ${colors.primaryText};
|
||
transition: all 0.2s;
|
||
`;
|
||
copyRawBtn.onclick = async () => {
|
||
try {
|
||
await navigator.clipboard.writeText(response);
|
||
const originalText = copyRawBtn.textContent;
|
||
copyRawBtn.textContent = "✓";
|
||
setTimeout(() => {
|
||
copyRawBtn.textContent = originalText;
|
||
}, 1000);
|
||
} catch (error) {
|
||
alert("Failed to copy: " + error.message);
|
||
}
|
||
};
|
||
|
||
// Copy HTML button
|
||
const copyHtmlBtn = document.createElement("button");
|
||
copyHtmlBtn.textContent = "📄";
|
||
copyHtmlBtn.title = "Copy HTML";
|
||
copyHtmlBtn.style.cssText = `
|
||
background: ${colors.buttonBackground};
|
||
border: 1px solid ${colors.borderColor};
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
color: ${colors.primaryText};
|
||
transition: all 0.2s;
|
||
`;
|
||
copyHtmlBtn.onclick = async () => {
|
||
try {
|
||
const htmlContent = marked.parse(response);
|
||
await navigator.clipboard.writeText(htmlContent);
|
||
const originalText = copyHtmlBtn.textContent;
|
||
copyHtmlBtn.textContent = "✓";
|
||
setTimeout(() => {
|
||
copyHtmlBtn.textContent = originalText;
|
||
}, 1000);
|
||
} catch (error) {
|
||
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();
|
||
}
|
||
|
||
// Function to toggle Hermes modal
|
||
export async function toggleUncloseaiEmbeddedModal() {
|
||
const existingModal = document.getElementById("uncloseai-embedded-modal");
|
||
if (existingModal) {
|
||
document.body.removeChild(existingModal);
|
||
uncloseaiEmbeddedModalOpen = false;
|
||
} else {
|
||
await openUncloseaiEmbeddedModalNew();
|
||
uncloseaiEmbeddedModalOpen = true;
|
||
}
|
||
}
|
||
|
||
// Function to open Hermes modal - Mobile-first design
|
||
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;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100vw;
|
||
height: 100vh;
|
||
border: none;
|
||
border-radius: 0;
|
||
background: white;
|
||
margin: 0;
|
||
padding: 0;
|
||
z-index: 2000;
|
||
overflow: hidden;
|
||
`;
|
||
} else {
|
||
modal.style.cssText = `
|
||
position: fixed;
|
||
width: 90vw;
|
||
max-width: 800px;
|
||
height: 90vh;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
border: none;
|
||
border-radius: 16px;
|
||
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
|
||
background: white;
|
||
margin: 0;
|
||
padding: 0;
|
||
z-index: 2000;
|
||
overflow: hidden;
|
||
`;
|
||
}
|
||
|
||
const article = document.createElement("article");
|
||
if (USE_CUSTOM_STYLING) {
|
||
article.style.cssText = `
|
||
width: 100%;
|
||
max-width: 100%;
|
||
box-sizing: border-box;
|
||
height: 100%;
|
||
display: grid;
|
||
grid-template-rows: auto auto 1fr auto;
|
||
margin: 0;
|
||
padding: 0;
|
||
`;
|
||
} else {
|
||
// Responsive scaling based on screen width
|
||
const screenWidth = window.innerWidth;
|
||
const scale = screenWidth < 480 ? 0.9 : screenWidth < 768 ? 0.8 : 0.75;
|
||
|
||
article.style.cssText = `
|
||
height: 100%;
|
||
display: grid;
|
||
grid-template-rows: auto auto 1fr auto;
|
||
transform: scale(${scale});
|
||
transform-origin: top center;
|
||
margin: 0;
|
||
padding: 0;
|
||
min-width: 0;
|
||
max-width: none;
|
||
box-sizing: border-box;
|
||
`;
|
||
}
|
||
modal.appendChild(article);
|
||
|
||
// Handle viewport changes (onscreen keyboard)
|
||
if (!USE_CUSTOM_STYLING) {
|
||
const handleViewportChange = () => {
|
||
// Use dvh (dynamic viewport height) for better mobile keyboard handling
|
||
modal.style.height = "100dvh";
|
||
// Fallback for browsers that don't support dvh
|
||
if (window.visualViewport) {
|
||
modal.style.height = `${window.visualViewport.height}px`;
|
||
}
|
||
};
|
||
|
||
// Listen for visual viewport changes (keyboard open/close)
|
||
if (window.visualViewport) {
|
||
window.visualViewport.addEventListener("resize", handleViewportChange);
|
||
}
|
||
|
||
// Also listen for window resize as fallback
|
||
window.addEventListener("resize", handleViewportChange);
|
||
|
||
// Initial call
|
||
handleViewportChange();
|
||
}
|
||
|
||
// Create modal header
|
||
const header = document.createElement("div");
|
||
if (USE_CUSTOM_STYLING) {
|
||
header.style.cssText = `
|
||
background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 16px 20px;
|
||
display: grid;
|
||
grid-template-columns: 1fr auto;
|
||
align-items: center;
|
||
`;
|
||
} else {
|
||
header.style.cssText = `
|
||
padding: 16px 20px;
|
||
display: grid;
|
||
grid-template-columns: 1fr auto;
|
||
align-items: center;
|
||
border-bottom: 1px solid #ccc;
|
||
`;
|
||
}
|
||
|
||
const titleContainer = document.createElement("div");
|
||
|
||
const title = document.createElement("h2");
|
||
title.innerHTML =
|
||
'<span style="font-family: \'ChunkFiveRegular\', monospace;">uncloseai.</span> presents nous research\'s <a href="https://nousresearch.com/hermes3/" target="_blank" style="color: inherit; text-decoration: underline;">hermes</a> large language model';
|
||
if (USE_CUSTOM_STYLING) {
|
||
title.style.cssText = `
|
||
margin: 0;
|
||
font-family: 'ChunkFiveRegular', monospace;
|
||
font-size: 16px;
|
||
line-height: 1.2;
|
||
`;
|
||
} else {
|
||
title.style.cssText = `
|
||
margin: 0;
|
||
font-size: 0.9em;
|
||
line-height: 1.2;
|
||
`;
|
||
}
|
||
|
||
const pageTitle = document.createElement("div");
|
||
pageTitle.textContent = `You are discussing: ${document.title}`;
|
||
if (USE_CUSTOM_STYLING) {
|
||
pageTitle.style.cssText = `
|
||
font-size: 12px;
|
||
opacity: 0.8;
|
||
margin-top: 4px;
|
||
`;
|
||
} else {
|
||
pageTitle.style.cssText = `
|
||
font-size: 0.75em;
|
||
opacity: 0.7;
|
||
margin-top: 4px;
|
||
`;
|
||
}
|
||
|
||
titleContainer.appendChild(title);
|
||
titleContainer.appendChild(pageTitle);
|
||
|
||
const closeButton = document.createElement("button");
|
||
closeButton.textContent = "×";
|
||
if (USE_CUSTOM_STYLING) {
|
||
closeButton.style.cssText = `
|
||
background: none;
|
||
border: none;
|
||
color: white;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
width: 30px;
|
||
height: 30px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
`;
|
||
} else {
|
||
closeButton.style.cssText = `
|
||
background: var(--background-color);
|
||
border: 1px solid var(--border-color);
|
||
color: var(--color);
|
||
font-size: 18px;
|
||
cursor: pointer;
|
||
padding: 4px;
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: opacity 0.2s;
|
||
`;
|
||
closeButton.onmouseenter = () => (closeButton.style.opacity = "0.7");
|
||
closeButton.onmouseleave = () => (closeButton.style.opacity = "1");
|
||
}
|
||
closeButton.onclick = () => {
|
||
document.body.removeChild(modal);
|
||
uncloseaiEmbeddedModalOpen = false;
|
||
};
|
||
|
||
header.appendChild(titleContainer);
|
||
header.appendChild(closeButton);
|
||
|
||
// Create controls section
|
||
const controls = document.createElement("div");
|
||
if (USE_CUSTOM_STYLING) {
|
||
controls.style.cssText = `
|
||
padding: 16px 20px;
|
||
border-bottom: 1px solid #e0e0e0;
|
||
display: grid;
|
||
grid-template-columns: auto 1fr auto 1fr;
|
||
gap: 12px;
|
||
align-items: center;
|
||
`;
|
||
} else {
|
||
controls.className = "uncloseai-controls";
|
||
controls.style.cssText = `
|
||
display: grid;
|
||
grid-template-columns: auto 1fr auto 1fr;
|
||
gap: 12px;
|
||
padding: 0.5rem;
|
||
align-items: center;
|
||
`;
|
||
}
|
||
|
||
// Add model selection dropdown
|
||
const modelLabel = document.createElement("label");
|
||
modelLabel.textContent = "Model: ";
|
||
if (USE_CUSTOM_STYLING) {
|
||
modelLabel.style.fontWeight = "bold";
|
||
}
|
||
|
||
const modelSelect = document.createElement("select");
|
||
modelSelect.id = "modal-model-selection";
|
||
if (USE_CUSTOM_STYLING) {
|
||
modelSelect.style.cssText = `
|
||
padding: 6px 12px;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
background: white;
|
||
`;
|
||
}
|
||
|
||
// Add loading placeholder
|
||
const loadingOption = document.createElement("option");
|
||
loadingOption.textContent = "Loading models...";
|
||
loadingOption.disabled = true;
|
||
modelSelect.appendChild(loadingOption);
|
||
|
||
// Populate model dropdown dynamically (async)
|
||
fetchModelsFromEndpoints()
|
||
.then((models) => {
|
||
modelSelect.innerHTML = ""; // Clear loading option
|
||
models.forEach((model) => {
|
||
const option = document.createElement("option");
|
||
option.value = model.uniqueId;
|
||
option.textContent = `${model.endpointId} | ${model.modelName}`;
|
||
modelSelect.appendChild(option);
|
||
});
|
||
|
||
// Restore saved model selection after loading
|
||
const savedModel = localStorage.getItem("hermes-selected-model");
|
||
if (
|
||
savedModel &&
|
||
modelSelect.querySelector(`option[value="${savedModel}"]`)
|
||
) {
|
||
modelSelect.value = savedModel;
|
||
}
|
||
})
|
||
.catch((error) => {
|
||
console.error("Error loading models:", error);
|
||
modelSelect.innerHTML = "";
|
||
const errorOption = document.createElement("option");
|
||
errorOption.textContent = "Error loading models";
|
||
errorOption.disabled = true;
|
||
modelSelect.appendChild(errorOption);
|
||
});
|
||
|
||
// Save model selection on change
|
||
modelSelect.addEventListener("change", () => {
|
||
localStorage.setItem("hermes-selected-model", modelSelect.value);
|
||
});
|
||
|
||
// Add voice selection with newline
|
||
const voiceBreak = document.createElement("div");
|
||
voiceBreak.style.width = "100%";
|
||
|
||
const voiceLabel = document.createElement("label");
|
||
voiceLabel.textContent = "Voice: ";
|
||
if (USE_CUSTOM_STYLING) {
|
||
voiceLabel.style.fontWeight = "bold";
|
||
}
|
||
|
||
const voiceSelect = document.createElement("select");
|
||
voiceSelect.id = "modal-voice-selection";
|
||
if (USE_CUSTOM_STYLING) {
|
||
voiceSelect.style.cssText = `
|
||
padding: 6px 12px;
|
||
border: 1px solid #ccc;
|
||
border-radius: 4px;
|
||
background: white;
|
||
`;
|
||
}
|
||
|
||
const voices = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"];
|
||
voices.forEach((voice) => {
|
||
const option = document.createElement("option");
|
||
option.value = voice;
|
||
option.textContent = voice;
|
||
if (voice === "alloy") option.selected = true;
|
||
voiceSelect.appendChild(option);
|
||
});
|
||
|
||
// Restore saved voice selection
|
||
const savedVoice = localStorage.getItem("hermes-selected-voice");
|
||
if (savedVoice && voices.includes(savedVoice)) {
|
||
voiceSelect.value = savedVoice;
|
||
}
|
||
|
||
// Save voice selection on change
|
||
voiceSelect.addEventListener("change", () => {
|
||
localStorage.setItem("hermes-selected-voice", voiceSelect.value);
|
||
});
|
||
|
||
// Add action buttons
|
||
const actionButtons = document.createElement("div");
|
||
if (USE_CUSTOM_STYLING) {
|
||
actionButtons.style.cssText = `
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(60px, 1fr));
|
||
gap: 4px;
|
||
`;
|
||
} else {
|
||
actionButtons.className = "uncloseai-button-group";
|
||
actionButtons.style.cssText = `
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(60px, 1fr));
|
||
gap: 4px;
|
||
`;
|
||
}
|
||
|
||
const readPageBtn = document.createElement("button");
|
||
readPageBtn.textContent = "📖 Read Page";
|
||
if (USE_CUSTOM_STYLING) {
|
||
readPageBtn.style.cssText =
|
||
"padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;";
|
||
}
|
||
readPageBtn.onclick = readPageWithHermes;
|
||
|
||
const ttsBtn = document.createElement("button");
|
||
ttsBtn.textContent = "🔊 TTS Anything";
|
||
if (USE_CUSTOM_STYLING) {
|
||
ttsBtn.style.cssText =
|
||
"padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;";
|
||
}
|
||
ttsBtn.onclick = openTTSModal;
|
||
|
||
const refreshBtn = document.createElement("button");
|
||
refreshBtn.textContent = "🔄 Refresh";
|
||
if (USE_CUSTOM_STYLING) {
|
||
refreshBtn.style.cssText =
|
||
"padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;";
|
||
}
|
||
refreshBtn.onclick = async () => {
|
||
// Clear cache and refresh models
|
||
localStorage.removeItem("modelRegistryCache");
|
||
localStorage.removeItem("vllmEndpointsHash");
|
||
const models = await fetchModelsFromEndpoints();
|
||
|
||
// Update modal dropdown
|
||
modelSelect.innerHTML = "";
|
||
models.forEach((model) => {
|
||
const option = document.createElement("option");
|
||
option.value = model.uniqueId;
|
||
option.textContent = `${model.endpointId} | ${model.modelName}`;
|
||
modelSelect.appendChild(option);
|
||
});
|
||
|
||
// Update main page dropdown if it exists
|
||
const mainDropdown = document.getElementById("model-selection");
|
||
if (mainDropdown) {
|
||
mainDropdown.innerHTML = "";
|
||
models.forEach((model) => {
|
||
const option = document.createElement("option");
|
||
option.value = model.uniqueId;
|
||
option.textContent = `${model.endpointId} | ${model.modelName}`;
|
||
mainDropdown.appendChild(option);
|
||
});
|
||
}
|
||
};
|
||
|
||
const clearBtn = document.createElement("button");
|
||
clearBtn.textContent = "🗑️ Clear";
|
||
if (USE_CUSTOM_STYLING) {
|
||
clearBtn.style.cssText =
|
||
"padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;";
|
||
}
|
||
clearBtn.onclick = async () => {
|
||
if (confirm("Clear all conversation history?")) {
|
||
clearConversationHistory();
|
||
chatBox.innerHTML = "";
|
||
await addHermesIntroduction(); // Call addHermesIntroduction after clearing
|
||
}
|
||
};
|
||
|
||
actionButtons.appendChild(readPageBtn);
|
||
actionButtons.appendChild(ttsBtn);
|
||
actionButtons.appendChild(refreshBtn);
|
||
actionButtons.appendChild(clearBtn);
|
||
|
||
// Make action buttons span all columns
|
||
actionButtons.style.gridColumn = "span 4";
|
||
|
||
controls.appendChild(actionButtons);
|
||
controls.appendChild(modelLabel);
|
||
controls.appendChild(modelSelect);
|
||
controls.appendChild(voiceLabel);
|
||
controls.appendChild(voiceSelect);
|
||
|
||
// Create chat area
|
||
const chatArea = document.createElement("div");
|
||
if (USE_CUSTOM_STYLING) {
|
||
chatArea.style.cssText = `
|
||
grid-row: 3;
|
||
padding: 20px;
|
||
overflow-y: auto;
|
||
border-bottom: 1px solid #e0e0e0;
|
||
`;
|
||
} else {
|
||
chatArea.style.cssText = `
|
||
grid-row: 3;
|
||
padding: 1rem;
|
||
overflow-y: auto;
|
||
border-bottom: 1px solid #e0e0e0;
|
||
min-height: 200px;
|
||
`;
|
||
}
|
||
|
||
const chatBox = document.createElement("div");
|
||
chatBox.id = "modal-chat-box";
|
||
if (USE_CUSTOM_STYLING) {
|
||
chatBox.style.cssText = `
|
||
height: 100%;
|
||
overflow-y: auto;
|
||
`;
|
||
} else {
|
||
chatBox.style.cssText = `
|
||
min-height: 150px;
|
||
overflow-y: auto;
|
||
`;
|
||
}
|
||
|
||
chatArea.appendChild(chatBox);
|
||
|
||
// Import necessary functions from other modules
|
||
const {
|
||
loadConversationHistory,
|
||
saveConversationHistory,
|
||
clearConversationHistory,
|
||
} = await import("./storage.js");
|
||
const { extractWebpageContent } = await import("./content.js");
|
||
const { sendMessage } = await import("./chat.js");
|
||
const { getSystemMessage } = await import("./config.js");
|
||
|
||
// Restore conversation history in modal
|
||
function restoreConversationHistory() {
|
||
const history = loadConversationHistory();
|
||
chatBox.innerHTML = "";
|
||
// Filter out system messages before displaying
|
||
const displayHistory = history.filter((msg) => msg.role !== "system");
|
||
displayHistory.forEach((msg, index) => {
|
||
const messageDiv = document.createElement("div");
|
||
messageDiv.style.cssText =
|
||
"position: relative; margin-bottom: 10px; padding: 5px; border-radius: 5px; background: rgba(0,0,0,0.05);";
|
||
|
||
const deleteBtn = document.createElement("button");
|
||
deleteBtn.textContent = "×";
|
||
deleteBtn.style.cssText =
|
||
"position: absolute; top: 2px; right: 2px; width: 20px; height: 20px; font-size: 12px; background: rgba(255,0,0,0.7); color: white; border: none; border-radius: 50%; cursor: pointer;";
|
||
deleteBtn.onclick = ((messageIndex) => {
|
||
return () => {
|
||
const savedHistory = loadConversationHistory();
|
||
if (messageIndex >= 0 && messageIndex < savedHistory.length) {
|
||
savedHistory.splice(messageIndex, 1);
|
||
localStorage.setItem(
|
||
getPageSpecificKey("hermes-conversation-history"),
|
||
JSON.stringify(savedHistory),
|
||
);
|
||
// Update global chat history
|
||
chatHistory = [
|
||
{
|
||
role: "system",
|
||
content: getSystemMessage(),
|
||
},
|
||
...savedHistory,
|
||
];
|
||
restoreConversationHistory();
|
||
}
|
||
messageDiv.remove();
|
||
};
|
||
})(index);
|
||
|
||
if (msg.role === "user") {
|
||
messageDiv.innerHTML = `<p><strong>You:</strong> ${msg.content}</p>`;
|
||
} else if (msg.role === "assistant") {
|
||
const parsedContent = marked.parse(msg.content);
|
||
messageDiv.innerHTML = `<p><strong>AI:</strong> ${parsedContent}</p>`;
|
||
addCodeBlockCopyButtons(messageDiv);
|
||
// Add TTS button to AI messages in history
|
||
addTTSButtonToMessage(messageDiv, msg.content);
|
||
}
|
||
|
||
messageDiv.appendChild(deleteBtn);
|
||
chatBox.appendChild(messageDiv);
|
||
});
|
||
chatBox.scrollTop = chatBox.scrollHeight;
|
||
}
|
||
restoreConversationHistory();
|
||
|
||
// Generate dynamic Hermes introduction using LLM
|
||
async function addHermesIntroduction() {
|
||
const history = loadConversationHistory();
|
||
if (history.length === 0) {
|
||
const pageContent = extractWebpageContent();
|
||
const pageTitle = document.title;
|
||
const pageUrl = window.location.href;
|
||
|
||
// Create cache key based on page content hash (handle Unicode safely)
|
||
const contentForHash = pageContent.substring(0, 1000);
|
||
let contentHash;
|
||
try {
|
||
contentHash = btoa(unescape(encodeURIComponent(contentForHash)))
|
||
.replace(/[^a-zA-Z0-9]/g, "")
|
||
.substring(0, 32);
|
||
} catch (e) {
|
||
// Fallback: use simple string hash if btoa fails
|
||
let hash = 0;
|
||
for (let i = 0; i < contentForHash.length; i++) {
|
||
const char = contentForHash.charCodeAt(i);
|
||
hash = (hash << 5) - hash + char;
|
||
hash = hash & hash; // Convert to 32bit integer
|
||
}
|
||
contentHash = Math.abs(hash).toString(36).substring(0, 32);
|
||
}
|
||
const cacheKey = `hermes-intro-${contentHash}`;
|
||
|
||
// Check if we have a cached introduction for this page content
|
||
const cachedIntro = localStorage.getItem(cacheKey);
|
||
if (cachedIntro) {
|
||
displayIntroduction(cachedIntro);
|
||
// Add cached intro to chat history
|
||
chatHistory.push({ role: "assistant", content: cachedIntro });
|
||
saveConversationHistory(chatHistory);
|
||
return;
|
||
}
|
||
|
||
// Generate new introduction using LLM
|
||
const introDiv = document.createElement("div");
|
||
introDiv.style.cssText =
|
||
"position: relative; margin-bottom: 10px; padding: 10px; border-radius: 5px; background: rgba(0,100,200,0.1); border-left: 4px solid #0066cc;";
|
||
introDiv.innerHTML =
|
||
"<p><strong>🤖 Hermes:</strong> <em>✨ Analyzing this page and crafting a personalized introduction... This may take a moment.</em></p>";
|
||
chatBox.appendChild(introDiv);
|
||
chatBox.scrollTop = chatBox.scrollHeight;
|
||
|
||
try {
|
||
const prompt = `You are Hermes, a large language model from Nous Research. Write a friendly 3-paragraph introduction for yourself when embedded on this webpage. Be specific about this page's content and identify 2-3 key takeaways. Keep it conversational and helpful.
|
||
|
||
Page Title: ${pageTitle}
|
||
Page URL: ${pageUrl}
|
||
Page Content: ${pageContent.substring(0, 2000)}
|
||
|
||
Format: Start with "Greetings! I'm Hermes..." and make it sound natural and engaging. Write 3 full paragraphs that showcase your capabilities and how you can help with THIS specific page.`;
|
||
|
||
let generatedIntro = "";
|
||
for await (const chunk of sendMessage(prompt)) {
|
||
generatedIntro += chunk;
|
||
// Update display in real-time
|
||
introDiv.innerHTML = `<p><strong>🤖 Hermes:</strong> ${generatedIntro}</p>`;
|
||
// Scroll to bottom as content updates
|
||
chatBox.scrollTop = chatBox.scrollHeight;
|
||
}
|
||
|
||
// Add to chat history and save
|
||
chatHistory.push({ role: "assistant", content: generatedIntro });
|
||
saveConversationHistory(chatHistory);
|
||
|
||
// Cache the generated introduction
|
||
localStorage.setItem(cacheKey, generatedIntro);
|
||
|
||
// Add TTS button to the generated introduction
|
||
addTTSButtonToMessage(introDiv, generatedIntro);
|
||
|
||
chatBox.scrollTop = chatBox.scrollHeight;
|
||
} catch (error) {
|
||
console.error("Error generating introduction:", error);
|
||
const fallbackIntro =
|
||
"Greetings! I'm Hermes, a large language model from Nous Research. I'm here to help you understand this page and assist with any questions, coding, or creative tasks you might have. Feel free to ask me anything!";
|
||
introDiv.innerHTML = `<p><strong>🤖 Hermes:</strong> ${fallbackIntro}</p>`;
|
||
// Add fallback to chat history too
|
||
chatHistory.push({ role: "assistant", content: fallbackIntro });
|
||
saveConversationHistory(chatHistory);
|
||
}
|
||
}
|
||
}
|
||
|
||
function displayIntroduction(introText) {
|
||
const introDiv = document.createElement("div");
|
||
introDiv.style.cssText =
|
||
"position: relative; margin-bottom: 10px; padding: 10px; border-radius: 5px; background: rgba(0,100,200,0.1); border-left: 4px solid #0066cc;";
|
||
introDiv.innerHTML = `<p><strong>🤖 Hermes:</strong> ${introText}</p>`;
|
||
chatBox.appendChild(introDiv);
|
||
|
||
// Add TTS button to introduction
|
||
addTTSButtonToMessage(introDiv, introText);
|
||
|
||
chatBox.scrollTop = chatBox.scrollHeight;
|
||
}
|
||
|
||
// Add TTS button to any message
|
||
function addTTSButtonToMessage(messageDiv, messageText) {
|
||
const ttsContainer = document.createElement("div");
|
||
ttsContainer.style.cssText =
|
||
"display: flex; gap: 8px; margin: 8px 0; align-items: center;";
|
||
|
||
const ttsBtn = document.createElement("button");
|
||
ttsBtn.textContent = "🔊 Play Response";
|
||
if (USE_CUSTOM_STYLING) {
|
||
ttsBtn.style.cssText =
|
||
"padding: 4px 8px; border: 1px solid #0066cc; border-radius: 4px; background: #f0f8ff; cursor: pointer; color: #0066cc; font-size: 12px;";
|
||
} else {
|
||
ttsBtn.style.cssText =
|
||
"padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px;";
|
||
}
|
||
|
||
const downloadBtn = document.createElement("button");
|
||
downloadBtn.textContent = "💾 Download";
|
||
downloadBtn.style.display = "none";
|
||
if (USE_CUSTOM_STYLING) {
|
||
downloadBtn.style.cssText =
|
||
"padding: 4px 8px; border: 1px solid #0066cc; border-radius: 4px; background: #f0f8ff; cursor: pointer; color: #0066cc; font-size: 12px;";
|
||
} else {
|
||
downloadBtn.style.cssText =
|
||
"padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px;";
|
||
}
|
||
|
||
let messageAudio = null;
|
||
let messageBlob = null;
|
||
|
||
ttsBtn.onclick = async () => {
|
||
if (!messageAudio) {
|
||
ttsBtn.textContent = "Processing...";
|
||
ttsBtn.disabled = true;
|
||
|
||
try {
|
||
const selectedVoice = voiceSelect.value;
|
||
const result = await speakText(messageText, selectedVoice, 0.9);
|
||
messageAudio = result.audio;
|
||
messageBlob = result.blob;
|
||
|
||
ttsBtn.textContent = "⏸️ Pause";
|
||
ttsBtn.disabled = false;
|
||
downloadBtn.style.display = "inline-block";
|
||
|
||
messageAudio.play();
|
||
|
||
messageAudio.onended = () => {
|
||
ttsBtn.textContent = "🔊 Play Response";
|
||
};
|
||
} catch (error) {
|
||
console.error("Error generating TTS:", error);
|
||
ttsBtn.textContent = "🔊 Play Response";
|
||
ttsBtn.disabled = false;
|
||
}
|
||
} else {
|
||
if (messageAudio.paused) {
|
||
messageAudio.play();
|
||
ttsBtn.textContent = "⏸️ Pause";
|
||
} else {
|
||
messageAudio.pause();
|
||
ttsBtn.textContent = "▶️ Resume";
|
||
}
|
||
}
|
||
};
|
||
|
||
downloadBtn.onclick = () => {
|
||
if (messageBlob) {
|
||
const a = document.createElement("a");
|
||
a.href = URL.createObjectURL(messageBlob);
|
||
a.download = `hermes-response-${Date.now()}.mp3`;
|
||
a.click();
|
||
}
|
||
};
|
||
|
||
ttsContainer.appendChild(ttsBtn);
|
||
ttsContainer.appendChild(downloadBtn);
|
||
messageDiv.appendChild(ttsContainer);
|
||
}
|
||
|
||
// 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) => {
|
||
if (e.target === modal) {
|
||
modal.close();
|
||
document.body.removeChild(modal);
|
||
uncloseaiEmbeddedModalOpen = false;
|
||
}
|
||
});
|
||
|
||
// Load Hermes introduction asynchronously after modal is shown
|
||
addHermesIntroduction().catch((error) => {
|
||
console.error("Error generating Hermes introduction:", error);
|
||
});
|
||
|
||
// Create input area
|
||
const inputArea = document.createElement("div");
|
||
if (USE_CUSTOM_STYLING) {
|
||
inputArea.style.cssText = `
|
||
grid-row: 4;
|
||
padding: 20px;
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: flex-end;
|
||
`;
|
||
} else {
|
||
inputArea.style.cssText = `
|
||
grid-row: 4;
|
||
padding: 1rem;
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: flex-end;
|
||
`;
|
||
}
|
||
|
||
const userInput = document.createElement("textarea");
|
||
userInput.id = "modal-user-input";
|
||
userInput.placeholder = "Ask about this page...";
|
||
if (USE_CUSTOM_STYLING) {
|
||
userInput.style.cssText = `
|
||
flex: 1;
|
||
padding: 12px;
|
||
border: 2px solid #e0e0e0;
|
||
border-radius: 8px;
|
||
font-family: inherit;
|
||
font-size: 14px;
|
||
resize: vertical;
|
||
min-height: 44px;
|
||
max-height: 120px;
|
||
`;
|
||
} else {
|
||
userInput.style.cssText = `
|
||
flex: 1;
|
||
padding: 0.5rem;
|
||
min-height: 44px;
|
||
max-height: 120px;
|
||
resize: vertical;
|
||
`;
|
||
}
|
||
|
||
const sendButton = document.createElement("button");
|
||
sendButton.textContent = "Send";
|
||
if (USE_CUSTOM_STYLING) {
|
||
sendButton.style.cssText = `
|
||
padding: 12px 24px;
|
||
background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
min-height: 44px;
|
||
`;
|
||
} else {
|
||
sendButton.style.cssText = `
|
||
padding: 0.75rem 1.5rem;
|
||
min-height: 44px;
|
||
`;
|
||
}
|
||
|
||
// Handle message sending
|
||
const handleModalInput = async () => {
|
||
const message = userInput.value.trim();
|
||
if (!message) return;
|
||
|
||
userInput.value = "";
|
||
userInput.style.height = "auto"; // Reset height
|
||
|
||
// Add user message to chat
|
||
const userDiv = document.createElement("div");
|
||
userDiv.style.cssText =
|
||
"position: relative; margin-bottom: 10px; padding: 5px; border-radius: 5px; background: rgba(0,0,0,0.05);";
|
||
userDiv.innerHTML = `<p><strong>You:</strong> ${message}</p>`;
|
||
chatBox.appendChild(userDiv);
|
||
|
||
// Add AI response placeholder
|
||
const aiDiv = document.createElement("div");
|
||
aiDiv.style.cssText =
|
||
"position: relative; margin-bottom: 10px; padding: 5px; border-radius: 5px; background: rgba(0,0,0,0.05);";
|
||
aiDiv.innerHTML = "<p><strong>AI:</strong> <em>thinking...</em></p>";
|
||
chatBox.appendChild(aiDiv);
|
||
chatBox.scrollTop = chatBox.scrollHeight;
|
||
|
||
try {
|
||
let response = "";
|
||
for await (const chunk of sendMessage(message)) {
|
||
response += chunk;
|
||
const parsedResponse = marked.parse(response);
|
||
aiDiv.innerHTML = `<p><strong>AI:</strong> ${parsedResponse}</p>`;
|
||
addCodeBlockCopyButtons(aiDiv);
|
||
chatBox.scrollTop = chatBox.scrollHeight;
|
||
}
|
||
|
||
// Add AI response to chat history and save
|
||
const { getChatHistory } = await import("./chat.js");
|
||
const currentHistory = getChatHistory();
|
||
currentHistory.push({ role: "assistant", content: response });
|
||
saveConversationHistory(currentHistory);
|
||
|
||
// Add delete button and TTS button to messages
|
||
[userDiv, aiDiv].forEach((div, index) => {
|
||
const deleteBtn = document.createElement("button");
|
||
deleteBtn.textContent = "×";
|
||
deleteBtn.style.cssText =
|
||
"position: absolute; top: 2px; right: 2px; width: 20px; height: 20px; font-size: 12px; background: rgba(255,0,0,0.7); color: white; border: none; border-radius: 50%; cursor: pointer;";
|
||
deleteBtn.onclick = () => {
|
||
div.remove();
|
||
// Update stored history
|
||
restoreConversationHistory();
|
||
};
|
||
div.appendChild(deleteBtn);
|
||
|
||
// Add TTS button to AI response
|
||
if (index === 1) {
|
||
// aiDiv
|
||
addTTSButtonToMessage(div, response);
|
||
}
|
||
});
|
||
} catch (error) {
|
||
aiDiv.innerHTML = `<p><strong>Error:</strong> ${error.message}</p>`;
|
||
}
|
||
};
|
||
|
||
sendButton.onclick = handleModalInput;
|
||
|
||
// Handle Enter key (Shift+Enter for new line)
|
||
userInput.addEventListener("keydown", (e) => {
|
||
if (e.key === "Enter" && !e.shiftKey) {
|
||
e.preventDefault();
|
||
handleModalInput();
|
||
}
|
||
});
|
||
|
||
inputArea.appendChild(userInput);
|
||
inputArea.appendChild(sendButton);
|
||
|
||
article.appendChild(header);
|
||
article.appendChild(controls);
|
||
article.appendChild(chatArea);
|
||
article.appendChild(inputArea);
|
||
}
|
||
|
||
// TTS Modal functionality
|
||
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";
|
||
|
||
const modal = document.createElement("dialog");
|
||
modal.style.zIndex = zIndex;
|
||
modal.style.position = "fixed";
|
||
|
||
if (USE_CUSTOM_STYLING) {
|
||
modal.style.cssText += `
|
||
width: 95vw;
|
||
max-width: 800px;
|
||
max-height: 95vh;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
border: none;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||
background: white;
|
||
margin: 0;
|
||
overflow: auto;
|
||
`;
|
||
} else {
|
||
modal.style.cssText += `
|
||
width: 95vw;
|
||
max-width: 800px;
|
||
max-height: 95vh;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
margin: 0;
|
||
overflow: auto;
|
||
`;
|
||
}
|
||
|
||
const article = document.createElement("article");
|
||
modal.appendChild(article);
|
||
|
||
const header = document.createElement("header");
|
||
if (USE_CUSTOM_STYLING) {
|
||
header.style.cssText = `
|
||
background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 16px 20px;
|
||
margin: -1em -1em 1em -1em;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
`;
|
||
} else {
|
||
header.style.cssText = `
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16px 20px;
|
||
margin: -1em -1em 1em -1em;
|
||
`;
|
||
}
|
||
article.appendChild(header);
|
||
|
||
const h1 = document.createElement("h1");
|
||
h1.innerHTML =
|
||
"<span style=\"font-family: 'ChunkFiveRegular', monospace;\">uncloseai.</span> TTS Anything!";
|
||
if (USE_CUSTOM_STYLING) {
|
||
h1.style.cssText = `
|
||
margin: 0;
|
||
font-size: 20px;
|
||
`;
|
||
}
|
||
header.appendChild(h1);
|
||
|
||
const closeButton = document.createElement("button");
|
||
closeButton.textContent = "X";
|
||
if (USE_CUSTOM_STYLING) {
|
||
closeButton.style.cssText = `
|
||
background: none;
|
||
border: none;
|
||
color: white;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
width: 30px;
|
||
height: 30px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
`;
|
||
} else {
|
||
closeButton.style.cssText = `
|
||
background: var(--background-color);
|
||
border: 1px solid var(--border-color);
|
||
color: var(--color);
|
||
font-size: 18px;
|
||
cursor: pointer;
|
||
padding: 4px;
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: opacity 0.2s;
|
||
`;
|
||
closeButton.onmouseenter = () => (closeButton.style.opacity = "0.7");
|
||
closeButton.onmouseleave = () => (closeButton.style.opacity = "1");
|
||
}
|
||
closeButton.onclick = () => {
|
||
modal.close();
|
||
document.body.removeChild(modal);
|
||
};
|
||
header.appendChild(closeButton);
|
||
|
||
const textArea = document.createElement("textarea");
|
||
textArea.style.width = "100%";
|
||
textArea.style.height = "240px";
|
||
textArea.placeholder =
|
||
"text-to-speech: write any message & have the artificial intelligence speak it!";
|
||
article.appendChild(textArea);
|
||
|
||
// Voice selection
|
||
const voices = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"];
|
||
const voiceSelection = document.createElement("div");
|
||
if (USE_CUSTOM_STYLING) {
|
||
voiceSelection.style.cssText = `
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
margin: 16px 0;
|
||
`;
|
||
} else {
|
||
voiceSelection.style.cssText = `
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 10px;
|
||
margin: 16px 0;
|
||
`;
|
||
}
|
||
voices.forEach((voice) => {
|
||
const label = document.createElement("label");
|
||
if (USE_CUSTOM_STYLING) {
|
||
label.style.cssText = `
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
cursor: pointer;
|
||
`;
|
||
} else {
|
||
label.style.display = "inline-block";
|
||
label.style.marginRight = "10px";
|
||
}
|
||
const radio = document.createElement("input");
|
||
radio.type = "radio";
|
||
radio.name = "tts-voice";
|
||
radio.value = voice;
|
||
if (voice === "alloy") radio.checked = true;
|
||
label.appendChild(radio);
|
||
label.appendChild(document.createTextNode(voice));
|
||
voiceSelection.appendChild(label);
|
||
});
|
||
article.appendChild(voiceSelection);
|
||
|
||
// Speed selection
|
||
const speedLabel = document.createElement("label");
|
||
const speedValue = document.createElement("span");
|
||
speedValue.textContent = "0.9";
|
||
|
||
speedLabel.textContent = `Speed: ${speedValue.textContent}`;
|
||
const speedSlider = document.createElement("input");
|
||
speedSlider.type = "range";
|
||
speedSlider.min = "0.25";
|
||
speedSlider.max = "4.0";
|
||
speedSlider.step = "0.05";
|
||
speedSlider.value = "0.9";
|
||
|
||
speedSlider.oninput = () => {
|
||
speedValue.textContent = speedSlider.value;
|
||
speedLabel.textContent = `Speed: ${speedSlider.value}`;
|
||
};
|
||
|
||
article.appendChild(speedLabel);
|
||
article.appendChild(speedSlider);
|
||
|
||
const playButton = document.createElement("button");
|
||
playButton.textContent = "Play Text";
|
||
if (USE_CUSTOM_STYLING) {
|
||
playButton.style.cssText = `
|
||
padding: 12px 24px;
|
||
background: linear-gradient(45deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
margin: 16px 10px 0 0;
|
||
`;
|
||
}
|
||
|
||
let ttsAudio = null;
|
||
let ttsBlob = null;
|
||
let lastTTSInput = "";
|
||
|
||
playButton.onclick = async () => {
|
||
const currentText = textArea.value.trim();
|
||
const selectedVoice = document.querySelector(
|
||
'input[name="tts-voice"]:checked',
|
||
).value;
|
||
const selectedSpeed = parseFloat(speedSlider.value);
|
||
|
||
if (!currentText) return;
|
||
|
||
if (!ttsAudio || lastTTSInput !== currentText) {
|
||
if (ttsAudio) ttsAudio.pause();
|
||
playButton.textContent = "Processing...";
|
||
playButton.disabled = true;
|
||
lastTTSInput = currentText;
|
||
const result = await speakText(currentText, selectedVoice, selectedSpeed);
|
||
ttsAudio = result.audio;
|
||
ttsBlob = result.blob;
|
||
playButton.disabled = false;
|
||
|
||
// Auto-play the audio (handle browser auto-play policies)
|
||
console.log("TTS MP3 ready, attempting auto-play...");
|
||
try {
|
||
const playPromise = ttsAudio.play();
|
||
await playPromise;
|
||
console.log("✅ TTS audio auto-playing successfully");
|
||
playButton.textContent = "Pause Text";
|
||
} catch (error) {
|
||
console.log("❌ Auto-play prevented by browser policy:", error.message);
|
||
console.log("User will need to manually click play");
|
||
playButton.textContent = "Play Text";
|
||
// Don't throw error, just log it - user can manually click play
|
||
}
|
||
|
||
// Add download button if not exists
|
||
let downloadButton = article.querySelector(".download-btn");
|
||
if (!downloadButton) {
|
||
downloadButton = document.createElement("button");
|
||
downloadButton.className = "download-btn";
|
||
downloadButton.textContent = "Download MP3";
|
||
if (USE_CUSTOM_STYLING) {
|
||
downloadButton.style.cssText = `
|
||
padding: 12px 24px;
|
||
background: #f5f5f5;
|
||
border: 1px solid #ccc;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
margin: 16px 0 0 10px;
|
||
`;
|
||
}
|
||
downloadButton.onclick = () => {
|
||
const a = document.createElement("a");
|
||
a.href = URL.createObjectURL(ttsBlob);
|
||
a.download = `tts-${Date.now()}.mp3`;
|
||
a.click();
|
||
};
|
||
article.appendChild(downloadButton);
|
||
}
|
||
} else {
|
||
// Audio already exists, toggle play/pause
|
||
if (ttsAudio.paused) {
|
||
ttsAudio.play();
|
||
playButton.textContent = "Pause Text";
|
||
} else {
|
||
ttsAudio.pause();
|
||
playButton.textContent = "Play Text";
|
||
}
|
||
}
|
||
|
||
ttsAudio.onended = () => {
|
||
playButton.textContent = "Play Text";
|
||
};
|
||
};
|
||
|
||
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) => {
|
||
if (e.target === modal) {
|
||
modal.close();
|
||
document.body.removeChild(modal);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Open translation modal
|
||
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";
|
||
|
||
const modal = document.createElement("dialog");
|
||
modal.style.zIndex = zIndex;
|
||
modal.style.position = "fixed";
|
||
|
||
if (USE_CUSTOM_STYLING) {
|
||
modal.style.cssText += `
|
||
width: 95vw;
|
||
max-width: 800px;
|
||
max-height: 95vh;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
border: none;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
|
||
background: white;
|
||
margin: 0;
|
||
overflow: auto;
|
||
`;
|
||
} else {
|
||
modal.style.cssText += `
|
||
width: 95vw;
|
||
max-width: 800px;
|
||
max-height: 95vh;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
margin: 0;
|
||
overflow: auto;
|
||
`;
|
||
}
|
||
|
||
const article = document.createElement("article");
|
||
modal.appendChild(article);
|
||
|
||
const header = document.createElement("header");
|
||
if (USE_CUSTOM_STYLING) {
|
||
header.style.cssText = `
|
||
background: linear-gradient(45deg, #4CAF50 0%, #45a049 100%);
|
||
color: white;
|
||
padding: 16px 20px;
|
||
margin: -1em -1em 1em -1em;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
`;
|
||
} else {
|
||
header.style.cssText = `
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 16px 20px;
|
||
margin: -1em -1em 1em -1em;
|
||
`;
|
||
}
|
||
article.appendChild(header);
|
||
|
||
const h1 = document.createElement("h1");
|
||
h1.innerHTML =
|
||
"<span style=\"font-family: 'ChunkFiveRegular', monospace;\">uncloseai.</span> 🌐 Translation";
|
||
if (USE_CUSTOM_STYLING) {
|
||
h1.style.cssText = `
|
||
margin: 0;
|
||
font-size: 20px;
|
||
`;
|
||
}
|
||
header.appendChild(h1);
|
||
|
||
const closeButton = document.createElement("button");
|
||
closeButton.textContent = "X";
|
||
if (USE_CUSTOM_STYLING) {
|
||
closeButton.style.cssText = `
|
||
background: none;
|
||
border: none;
|
||
color: white;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
width: 30px;
|
||
height: 30px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
touch-action: manipulation;
|
||
`;
|
||
} else {
|
||
closeButton.style.cssText = `
|
||
background: var(--background-color);
|
||
border: 1px solid var(--border-color);
|
||
color: var(--color);
|
||
font-size: 18px;
|
||
cursor: pointer;
|
||
padding: 4px;
|
||
width: 28px;
|
||
height: 28px;
|
||
border-radius: 4px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: opacity 0.2s;
|
||
touch-action: manipulation;
|
||
`;
|
||
closeButton.onmouseenter = () => (closeButton.style.opacity = "0.7");
|
||
closeButton.onmouseleave = () => (closeButton.style.opacity = "1");
|
||
}
|
||
closeButton.onclick = () => {
|
||
modal.close();
|
||
document.body.removeChild(modal);
|
||
};
|
||
header.appendChild(closeButton);
|
||
|
||
// Translation mode tabs
|
||
const modeContainer = document.createElement("div");
|
||
modeContainer.style.cssText =
|
||
"display: flex; gap: 10px; margin-bottom: 20px;";
|
||
|
||
const pageTab = document.createElement("button");
|
||
pageTab.textContent = "📄 Translate Page";
|
||
pageTab.style.cssText =
|
||
"flex: 1; padding: 10px; border: 2px solid #4CAF50; border-radius: 6px; background: #4CAF50; color: white; cursor: pointer;";
|
||
|
||
const customTab = document.createElement("button");
|
||
customTab.textContent = "✏️ Custom Text";
|
||
customTab.style.cssText =
|
||
"flex: 1; padding: 10px; border: 2px solid #4CAF50; border-radius: 6px; background: white; color: #4CAF50; cursor: pointer;";
|
||
|
||
modeContainer.appendChild(pageTab);
|
||
modeContainer.appendChild(customTab);
|
||
article.appendChild(modeContainer);
|
||
|
||
// Content areas
|
||
const pageContent = document.createElement("div");
|
||
const customContent = document.createElement("div");
|
||
customContent.style.display = "none";
|
||
|
||
// Page translation content
|
||
const pageInfo = document.createElement("p");
|
||
pageInfo.textContent =
|
||
"Translate the current page content into your chosen language:";
|
||
pageContent.appendChild(pageInfo);
|
||
|
||
// Custom text content
|
||
const textArea = document.createElement("textarea");
|
||
textArea.style.width = "100%";
|
||
textArea.style.height = "150px";
|
||
textArea.placeholder = "Enter any text to translate...";
|
||
customContent.appendChild(textArea);
|
||
|
||
article.appendChild(pageContent);
|
||
article.appendChild(customContent);
|
||
|
||
// Language selection
|
||
const languageContainer = document.createElement("div");
|
||
languageContainer.style.cssText = "margin: 20px 0;";
|
||
|
||
const languageLabel = document.createElement("label");
|
||
languageLabel.textContent = "Translate to: ";
|
||
languageLabel.style.display = "block";
|
||
languageLabel.style.marginBottom = "8px";
|
||
|
||
const languageSelect = document.createElement("select");
|
||
languageSelect.style.cssText =
|
||
"width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px;";
|
||
|
||
Object.entries(SUPPORTED_LANGUAGES).forEach(([code, name]) => {
|
||
const option = document.createElement("option");
|
||
option.value = code;
|
||
option.textContent = name;
|
||
languageSelect.appendChild(option);
|
||
});
|
||
|
||
languageContainer.appendChild(languageLabel);
|
||
languageContainer.appendChild(languageSelect);
|
||
article.appendChild(languageContainer);
|
||
|
||
// Translate button
|
||
const translateButton = document.createElement("button");
|
||
translateButton.textContent = "🌐 Translate";
|
||
if (USE_CUSTOM_STYLING) {
|
||
translateButton.style.cssText = `
|
||
padding: 12px 24px;
|
||
background: linear-gradient(45deg, #4CAF50 0%, #45a049 100%);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-weight: bold;
|
||
margin: 16px 10px 0 0;
|
||
width: 100%;
|
||
`;
|
||
} else {
|
||
translateButton.style.cssText =
|
||
"width: 100%; padding: 12px; margin: 16px 0;";
|
||
}
|
||
|
||
// Result area
|
||
const resultContainer = document.createElement("div");
|
||
resultContainer.style.cssText = "margin-top: 20px; display: none;";
|
||
|
||
const resultLabel = document.createElement("h3");
|
||
resultLabel.textContent = "Translation:";
|
||
|
||
const resultArea = document.createElement("div");
|
||
resultArea.style.cssText =
|
||
"border: 1px solid #ccc; border-radius: 4px; padding: 15px; max-height: 400px; overflow-y: auto; background: #f9f9f9; white-space: pre-wrap;";
|
||
|
||
resultContainer.appendChild(resultLabel);
|
||
resultContainer.appendChild(resultArea);
|
||
article.appendChild(resultContainer);
|
||
|
||
article.appendChild(translateButton);
|
||
|
||
// Tab switching
|
||
let isPageMode = true;
|
||
|
||
pageTab.onclick = () => {
|
||
if (!isPageMode) {
|
||
isPageMode = true;
|
||
pageTab.style.background = "#4CAF50";
|
||
pageTab.style.color = "white";
|
||
customTab.style.background = "white";
|
||
customTab.style.color = "#4CAF50";
|
||
pageContent.style.display = "block";
|
||
customContent.style.display = "none";
|
||
translateButton.textContent = "🌐 Translate Page";
|
||
}
|
||
};
|
||
|
||
customTab.onclick = () => {
|
||
if (isPageMode) {
|
||
isPageMode = false;
|
||
customTab.style.background = "#4CAF50";
|
||
customTab.style.color = "white";
|
||
pageTab.style.background = "white";
|
||
pageTab.style.color = "#4CAF50";
|
||
customContent.style.display = "block";
|
||
pageContent.style.display = "none";
|
||
translateButton.textContent = "🌐 Translate Text";
|
||
}
|
||
};
|
||
|
||
// Translation logic
|
||
translateButton.onclick = async () => {
|
||
const targetLanguage = languageSelect.value;
|
||
const targetLanguageName = SUPPORTED_LANGUAGES[targetLanguage];
|
||
|
||
try {
|
||
translateButton.textContent = "Processing...";
|
||
translateButton.disabled = true;
|
||
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 tokenInfo = countTokens(textToAnalyze);
|
||
const estimatedSeconds = estimateProcessingTime(tokenInfo.totalTokens);
|
||
|
||
// Create progress indicator
|
||
const progressContainer = document.createElement("div");
|
||
progressContainer.style.cssText = `
|
||
margin: 20px 0;
|
||
padding: 20px;
|
||
border: 2px solid #4CAF50;
|
||
border-radius: 8px;
|
||
background: #f0fff0;
|
||
text-align: center;
|
||
`;
|
||
|
||
const progressText = document.createElement("div");
|
||
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%;
|
||
height: 20px;
|
||
background: #e0e0e0;
|
||
border-radius: 10px;
|
||
overflow: hidden;
|
||
margin-bottom: 10px;
|
||
`;
|
||
|
||
const progressFill = document.createElement("div");
|
||
progressFill.style.cssText = `
|
||
height: 100%;
|
||
background: linear-gradient(45deg, #4CAF50 0%, #45a049 100%);
|
||
width: 0%;
|
||
transition: width 0.5s ease;
|
||
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,
|
||
);
|
||
|
||
// Start progress animation
|
||
const startTime = Date.now();
|
||
const progressInterval = setInterval(() => {
|
||
const elapsed = Date.now() - startTime;
|
||
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...";
|
||
|
||
if (elapsed < 3000) statusText.textContent = "Analyzing content...";
|
||
else if (elapsed < estimatedSeconds * 500)
|
||
statusText.textContent = "Translating with Hermes AI...";
|
||
else statusText.textContent = "Finalizing translation...";
|
||
}, 500);
|
||
|
||
let translatedText;
|
||
|
||
if (isPageMode) {
|
||
translatedText = await translateCurrentPage(targetLanguage);
|
||
} else {
|
||
const customText = textArea.value.trim();
|
||
if (!customText) {
|
||
alert("Please enter some text to translate!");
|
||
return;
|
||
}
|
||
translatedText = await translateText(customText, targetLanguage);
|
||
}
|
||
|
||
resultLabel.textContent = `Translation (${targetLanguageName}):`;
|
||
|
||
// Check if the translated content looks like HTML
|
||
const isHTML =
|
||
translatedText.includes("<") &&
|
||
translatedText.includes(">") &&
|
||
(translatedText.includes("<h") ||
|
||
translatedText.includes("<p") ||
|
||
translatedText.includes("<div") ||
|
||
translatedText.includes("<section"));
|
||
|
||
if (isHTML) {
|
||
// Auto-open preview in new tab
|
||
const previewWindow = window.open("", "_blank");
|
||
if (previewWindow) {
|
||
// Inject base URL for relative resources to work
|
||
const baseUrl =
|
||
window.location.origin +
|
||
window.location.pathname.substring(
|
||
0,
|
||
window.location.pathname.lastIndexOf("/") + 1,
|
||
);
|
||
|
||
let htmlWithBase;
|
||
const scriptTag = `
|
||
<script>
|
||
window.UNCLOSEAI_SKIP_INIT = true;
|
||
</script>
|
||
<script src="https://uncloseai.com/uncloseai.js" type="module"></script>`;
|
||
|
||
if (translatedText.includes("<!DOCTYPE")) {
|
||
// Already has DOCTYPE, just add base and script
|
||
htmlWithBase = translatedText.includes("<head>")
|
||
? translatedText.replace(
|
||
"<head>",
|
||
`<head><base href="${baseUrl}">${scriptTag}`,
|
||
)
|
||
: translatedText.replace(
|
||
"<html>",
|
||
`<html><head><base href="${baseUrl}">${scriptTag}</head>`,
|
||
);
|
||
} else if (translatedText.includes("<head>")) {
|
||
// Has head but no DOCTYPE
|
||
htmlWithBase = `<!DOCTYPE html>\n${translatedText.replace("<head>", `<head><base href="${baseUrl}">${scriptTag}`)}`;
|
||
} else {
|
||
// No DOCTYPE or head
|
||
htmlWithBase = `<!DOCTYPE html>\n<html><head><base href="${baseUrl}">${scriptTag}</head><body>${translatedText}</body></html>`;
|
||
}
|
||
|
||
previewWindow.document.write(htmlWithBase);
|
||
previewWindow.document.close();
|
||
previewWindow.document.title = `Translation Preview (${targetLanguageName}) - AI Enabled`;
|
||
|
||
// 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
|
||
) {
|
||
console.log("Adding AI notice to preview window");
|
||
const notice = previewWindow.document.createElement("div");
|
||
notice.style.cssText =
|
||
"position: fixed; top: 10px; right: 10px; background: #4CAF50; color: white; padding: 8px 12px; border-radius: 4px; font-size: 12px; z-index: 10000; box-shadow: 0 2px 8px rgba(0,0,0,0.2);";
|
||
notice.textContent = `🤖 AI features should work in ${targetLanguageName}!`;
|
||
previewWindow.document.body.appendChild(notice);
|
||
|
||
// Auto-hide notice after 5 seconds
|
||
setTimeout(() => {
|
||
if (notice.parentNode) notice.parentNode.removeChild(notice);
|
||
}, 5000);
|
||
} else if (previewWindow) {
|
||
// Wait a bit and try again
|
||
setTimeout(addNotice, 100);
|
||
}
|
||
};
|
||
|
||
// Only add notice if we have a valid preview window
|
||
if (previewWindow) {
|
||
addNotice();
|
||
}
|
||
}
|
||
|
||
// Show raw HTML toggle in modal
|
||
resultArea.innerHTML = `
|
||
<div style="margin-bottom: 15px;">
|
||
<button onclick="this.nextElementSibling.style.display = this.nextElementSibling.style.display === 'none' ? 'block' : 'none'; this.textContent = this.textContent.includes('Raw') ? 'Show Raw HTML' : 'Hide Raw HTML'" style="padding: 8px 16px; background: #666; color: white; border: none; border-radius: 4px; cursor: pointer;">Show Raw HTML</button>
|
||
<div style="display: none; margin: 10px 0; padding: 15px; background: #f0f0f0; border-radius: 4px; white-space: pre-wrap; font-family: monospace; font-size: 0.85em; max-height: 300px; overflow-y: auto; border: 1px solid #ddd;">${translatedText.replace(/</g, "<").replace(/>/g, ">")}</div>
|
||
</div>
|
||
<div style="border: 2px solid #4CAF50; border-radius: 8px; background: #f0fff0; padding: 15px; text-align: center;">
|
||
<div style="color: #4CAF50; font-weight: bold; margin-bottom: 10px;">✅ Preview Opened in New Window</div>
|
||
<p style="margin: 0; color: #666;">The translated HTML page has opened in a new window with full AI functionality!</p>
|
||
${previewWindow ? "" : '<p style="margin: 10px 0 0 0; color: #d32f2f;"><strong>Note:</strong> Please allow popups for preview window</p>'}
|
||
</div>
|
||
`;
|
||
resultArea.style.whiteSpace = "normal";
|
||
} else {
|
||
resultArea.textContent = translatedText;
|
||
resultArea.style.whiteSpace = "pre-wrap";
|
||
}
|
||
|
||
resultContainer.style.display = "block";
|
||
|
||
// Complete progress bar
|
||
clearInterval(progressInterval);
|
||
progressFill.style.width = "100%";
|
||
etaText.textContent = "Translation completed!";
|
||
statusText.textContent = "✅ Ready to preview";
|
||
|
||
// Hide progress after a moment
|
||
setTimeout(() => {
|
||
if (progressContainer.parentNode) {
|
||
progressContainer.parentNode.removeChild(progressContainer);
|
||
}
|
||
}, 2000);
|
||
|
||
// Scroll to result
|
||
resultContainer.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
||
} catch (error) {
|
||
// Clean up progress on error
|
||
if (typeof progressInterval !== "undefined") {
|
||
clearInterval(progressInterval);
|
||
}
|
||
const progressContainer = document.querySelector(
|
||
'[style*="border: 2px solid #4CAF50"]',
|
||
);
|
||
if (progressContainer && progressContainer.parentNode) {
|
||
progressContainer.parentNode.removeChild(progressContainer);
|
||
}
|
||
alert(`Translation failed: ${error.message}`);
|
||
} finally {
|
||
translateButton.textContent = isPageMode
|
||
? "🌐 Translate Page"
|
||
: "🌐 Translate Text";
|
||
translateButton.disabled = false;
|
||
}
|
||
};
|
||
|
||
document.body.appendChild(modal);
|
||
|
||
// Use showModal() for proper mobile support and backdrop
|
||
modal.showModal();
|
||
|
||
// Close modal when clicking backdrop
|
||
modal.addEventListener("click", (e) => {
|
||
if (e.target === modal) {
|
||
modal.close();
|
||
document.body.removeChild(modal);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Initialize the system
|
||
export function initializeSystem() {
|
||
// Check for skip init flag - allow partial initialization for preview windows
|
||
if (window.UNCLOSEAI_SKIP_INIT === true) {
|
||
console.log("uncloseai.js: Partial initialization for preview window");
|
||
// Only create floating button in preview windows if not explicitly disabled
|
||
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);
|
||
} else {
|
||
createFloatingAIButton();
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
console.log("uncloseai.js: Full initialization");
|
||
initializeChatInterface();
|
||
|
||
// Only create floating button if not disabled
|
||
if (SHOW_FLOATING_BUTTON) {
|
||
console.log(
|
||
"uncloseai.js: SHOW_FLOATING_BUTTON is true, calling createFloatingAIButton()",
|
||
);
|
||
createFloatingAIButton();
|
||
} else {
|
||
console.log(
|
||
"uncloseai.js: SHOW_FLOATING_BUTTON is false, not creating floating button.",
|
||
);
|
||
}
|
||
|
||
// Initialize class-based elements
|
||
initializeUncloseaiElements();
|
||
}
|