uncloseai.com/src/ui.js

1768 lines
No EOL
58 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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 { extractWebpageContent } from './content.js';
import { speakText } from './tts.js';
import { handleFileUpload, uploadFile, showProgressIndicator, hideProgressIndicator } from './file-upload.js';
import { readPageWithHermes } from './page-reader.js';
import { handleUserInput, chatHistory } from './chat.js';
import { getPageSpecificKey } from './storage.js';
import { fetchModelsFromEndpoints } from './models.js';
import { SUPPORTED_LANGUAGES, translateText, translateCurrentPage } 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 hermesModalOpen = 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('📖 Read Page', () => readPageWithHermes());
const ttsBtn = createButton('🔊 TTS Anything', () => openTTSModal());
const translateBtn = createButton('🌐 Translate', () => openTranslateModal());
const uploadBtn = createButton('📁 Upload File', () => document.querySelector('[data-uncloseai-file-input]')?.click());
controlsDiv.appendChild(readBtn);
controlsDiv.appendChild(ttsBtn);
controlsDiv.appendChild(translateBtn);
controlsDiv.appendChild(uploadBtn);
// 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 '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</h4>
<button onclick="openTranslateModal()" style="width: 100%; padding: 6px;">🌐 Translate</button>
`;
container.appendChild(translateDiv);
}
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';
}
}
// 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: function(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", function(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.');
// 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)';
};
// Toggle modal on click
floatingButton.onclick = async () => {
// Show loading state
const originalText = floatingButton.textContent;
floatingButton.textContent = 'loading...';
floatingButton.disabled = true;
floatingButton.style.opacity = '0.7';
floatingButton.style.cursor = 'wait';
try {
await toggleHermesModal();
} finally {
// Restore button state
floatingButton.textContent = originalText;
floatingButton.disabled = false;
floatingButton.style.opacity = '1';
floatingButton.style.cursor = 'pointer';
}
};
document.body.appendChild(floatingButton);
console.log('uncloseai.js: Appended floating button to document.body.');
}
// Function to toggle Hermes modal
export async function toggleHermesModal() {
const existingModal = document.getElementById('hermes-modal');
if (existingModal) {
document.body.removeChild(existingModal);
hermesModalOpen = false;
} else {
await openHermesModal();
hermesModalOpen = true;
}
}
// Function to open Hermes modal
export async function openHermesModal() {
// Create modal using dialog element like TTS modal
const modal = document.createElement('dialog');
modal.id = 'hermes-modal';
modal.open = true;
if (USE_CUSTOM_STYLING) {
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 90%;
max-width: 800px;
height: 90%;
max-height: 700px;
border: none;
border-radius: 16px;
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
background: white;
z-index: 2000;
margin: auto;
`;
} else {
modal.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
height: 100dvh;
min-width: 0;
border: none;
background: transparent;
z-index: 2000;
margin: 0;
padding: 0;
`;
}
const article = document.createElement('article');
if (USE_CUSTOM_STYLING) {
article.style.cssText = `
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 = 'uncloseai. 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);
hermesModalOpen = 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 () => {
if (confirm('Delete this message?')) {
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();
}
}
};
})(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>`;
// 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);
// 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>`;
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 = () => {
if (confirm('Delete this message?')) {
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() {
// Check if Hermes modal is open to set appropriate z-index
const hermesModal = document.getElementById('hermes-modal');
const zIndex = hermesModal ? '2001' : '1001';
const modal = document.createElement('dialog');
modal.open = true;
modal.style.zIndex = zIndex;
modal.style.position = 'fixed';
if (USE_CUSTOM_STYLING) {
modal.style.cssText += `
max-width: 720px;
border: none;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
background: white;
`;
}
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.textContent = 'TTS Anything!';
if (USE_CUSTOM_STYLING) {
h1.style.cssText = `
margin: 0;
font-family: 'ChunkFiveRegular', monospace;
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 = () => 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;
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.textContent = 'Pause Text';
playButton.disabled = false;
// Auto-play the audio
ttsAudio.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);
}
}
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);
}
// Open translation modal
export function openTranslateModal() {
// Check if Hermes modal is open to set appropriate z-index
const hermesModal = document.getElementById('hermes-modal');
const zIndex = hermesModal ? '2001' : '1001';
const modal = document.createElement('dialog');
modal.open = true;
modal.style.zIndex = zIndex;
modal.style.position = 'fixed';
if (USE_CUSTOM_STYLING) {
modal.style.cssText += `
max-width: 720px;
border: none;
border-radius: 8px;
box-shadow: 0 4px 8px rgba(0,0,0,0.2);
background: white;
`;
}
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.textContent = '🌐 Omni-Directional Translation';
if (USE_CUSTOM_STYLING) {
h1.style.cssText = `
margin: 0;
font-family: 'ChunkFiveRegular', monospace;
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 = () => 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';
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) {
// Create HTML preview with toggle
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: 6px 12px; 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: 200px; overflow-y: auto; border: 1px solid #ddd;">${translatedText.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>
</div>
<div style="border: 2px solid #4CAF50; border-radius: 8px; background: white; overflow: hidden;">
<div style="background: #4CAF50; color: white; padding: 10px; font-weight: bold; margin: 0;">📄 Rendered Preview</div>
<iframe srcdoc="${translatedText.replace(/"/g, '&quot;').replace(/'/g, '&#39;')}" style="width: 100%; height: 500px; border: none; display: block;" sandbox="allow-same-origin"></iframe>
</div>
`;
resultArea.style.whiteSpace = 'normal';
} else {
resultArea.textContent = translatedText;
resultArea.style.whiteSpace = 'pre-wrap';
}
resultContainer.style.display = 'block';
// Scroll to result
resultContainer.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} catch (error) {
alert(`Translation failed: ${error.message}`);
} finally {
translateButton.textContent = isPageMode ? '🌐 Translate Page' : '🌐 Translate Text';
translateButton.disabled = false;
}
};
document.body.appendChild(modal);
}
// Initialize the system
export function initializeSystem() {
console.log('uncloseai.js: Initializing system');
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();
}