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
This commit is contained in:
Russell Ballestrini 2025-07-02 18:18:40 -04:00
parent 90842ebaac
commit 6a4e9f9f72

202
src/ui.js
View file

@ -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');