1768 lines
No EOL
58 KiB
JavaScript
1768 lines
No EOL
58 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 { 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, '<').replace(/>/g, '>')}</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, '"').replace(/'/g, ''')}" 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();
|
||
} |