From 6a4e9f9f72dabfd3a0e9beaae2032786d254e854 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Wed, 2 Jul 2025 18:18:40 -0400 Subject: [PATCH] feat(translate): add smart AI-powered translation dropdown - Replace translation modal with intelligent dropdown interface - Add AI-powered language detection using Hermes classifier - Send page markdown sample to Hermes for accurate language detection - Create themed dropdown with current language indicator - Support all 18+ languages with quick selection - Open translations in new tab automatically - Add fallback language detection for reliability - Show translation progress with loading states - Auto-close dropdown on outside clicks --- src/ui.js | 202 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 201 insertions(+), 1 deletion(-) diff --git a/src/ui.js b/src/ui.js index 5d95ea5..6f0d5d7 100644 --- a/src/ui.js +++ b/src/ui.js @@ -21,6 +21,87 @@ function initializeChunkFiveFont() { 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'; +} + // Theme detection and styling functions function detectCurrentTheme() { const isDark = @@ -1325,7 +1406,126 @@ async function openUncloseaiEmbeddedModalNew() { } }}, { text: "🔊 TTS Anything", action: (btn) => window.openTTSModal() }, - { text: "🌐 Translate", action: (btn) => window.openTranslateModal() }, + { text: "🌐 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 = "🌐 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 = "🌐 Translate"; + btn.disabled = false; + alert(`Language detection failed: ${error.message}`); + } + }}, { text: "📋 Full Chat Raw", action: async () => { try { const { getChatHistory } = await import('./chat.js');