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:
parent
90842ebaac
commit
6a4e9f9f72
1 changed files with 201 additions and 1 deletions
202
src/ui.js
202
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');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue