diff --git a/src/chat.js b/src/chat.js new file mode 100644 index 0000000..3f26b11 --- /dev/null +++ b/src/chat.js @@ -0,0 +1,156 @@ +// Chat functionality and message handling +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 { API_KEY } from './config.js'; +import { getSelectedModel, getSelectedModelEndpoint } from './models.js'; +import { speakText, generateTitleForTTS } from './tts.js'; +import { saveConversationHistory, initializeChatHistory } from './storage.js'; + +// Initialize chat history +export let chatHistory = initializeChatHistory(); + +// Generator function to send a message to the LLM and yield responses +export async function* sendMessage(message) { + chatHistory.push({ role: "user", content: message }); + + // Dynamically determine the API URL based on the selected model. + // (Assuming that the chat completions endpoint is at "/chat/completions") + const apiUrl = `${getSelectedModelEndpoint()}/chat/completions`; + + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${API_KEY}` + }, + body: JSON.stringify({ + model: getSelectedModel(), + messages: chatHistory, + temperature: 0.5, + max_tokens: 8192, + stream: true + }) + }); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + + for (let i = 0; i < lines.length - 1; i++) { + const line = lines[i].trim(); + if (line.startsWith('data: ')) { + const jsonData = line.slice(6); + if (jsonData === '[DONE]') continue; + + try { + const parsedData = JSON.parse(jsonData); + const content = parsedData.choices[0].delta.content; + if (content) { + yield content; + } + } catch (error) { + console.error('Error parsing JSON:', error); + } + } + } + + buffer = lines[lines.length - 1]; + } +} + +// Function to handle user input and display responses +export async function handleUserInput() { + const userInput = document.getElementById('user-input').value; + document.getElementById('user-input').value = ''; + + const chatBox = document.getElementById('chat-box'); + chatBox.innerHTML += `

You: ${userInput}

`; + + const aiResponseParagraph = document.createElement('p'); + aiResponseParagraph.innerHTML = 'AI: '; + chatBox.appendChild(aiResponseParagraph); + + const responseContent = document.createElement('span'); + aiResponseParagraph.appendChild(responseContent); + + let accumulatedContent = ''; + for await (const chunk of sendMessage(userInput)) { + accumulatedContent += chunk; + const parsedChunk = marked.parse(accumulatedContent); + responseContent.innerHTML = parsedChunk; + + responseContent.querySelectorAll('pre code').forEach((block) => { + hljs.highlightElement(block); + }); + } + + chatBox.scrollTop = chatBox.scrollHeight; + + // Add the response to chat history + chatHistory.push({ role: "assistant", content: accumulatedContent }); + saveConversationHistory(chatHistory); + + // Add play/pause button for TTS + const playPauseButton = document.createElement('button'); + playPauseButton.textContent = 'Generate TTS for AI Response'; + playPauseButton.style.margin = '5px'; + let aiAudio = null; + let aiBlob = null; + let isPaused = false; + + playPauseButton.onclick = async () => { + if (!aiAudio) { + playPauseButton.textContent = 'Processing...'; + playPauseButton.disabled = true; // Disable button while processing + const mainVoiceSelect = document.getElementById('read-page-voice'); + const selectedVoice = mainVoiceSelect ? mainVoiceSelect.value : 'alloy'; + const result = await speakText(accumulatedContent, selectedVoice, 0.9); + aiAudio = result.audio; + aiBlob = result.blob; + playPauseButton.textContent = 'Pause AI Response'; + playPauseButton.disabled = false; // Re-enable button after processing + aiAudio.play(); + + // Generate title for the MP3 file + const title = await generateTitleForTTS(accumulatedContent); + + // Add download button + const downloadButton = document.createElement('button'); + downloadButton.textContent = 'Download MP3'; + downloadButton.style.margin = '5px'; + downloadButton.onclick = () => { + const a = document.createElement('a'); + a.href = URL.createObjectURL(aiBlob); + a.download = `${title}.mp3`; + a.click(); + }; + chatBox.appendChild(downloadButton); + } else { + if (isPaused) { + aiAudio.play(); + playPauseButton.textContent = 'Pause AI Response'; + } else { + aiAudio.pause(); + playPauseButton.textContent = 'Play AI Response'; + } + isPaused = !isPaused; + } + }; + chatBox.appendChild(playPauseButton); +} + +// Update chat history reference (for external modules) +export function updateChatHistory(newHistory) { + chatHistory = newHistory; +} + +export function getChatHistory() { + return chatHistory; +} \ No newline at end of file diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..a720587 --- /dev/null +++ b/src/config.js @@ -0,0 +1,21 @@ +// Configuration and endpoints for uncloseai.js + +export const TTS_API_URL = "https://speech.ai.unturf.com/v1/audio/speech"; +export const MEGAPARCE_API_URL = "https://megaparce.ai.unturf.com/v1/file"; +export const API_KEY = "dummy-api-key"; +export const MODEL = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"; // default model (always available) + +// Dynamic Endpoints Configuration for Chat API +export const VLLM_ENDPOINTS = [ + { id: 'hermes.ai.unturf.com', url: 'https://hermes.ai.unturf.com/v1' }, + { id: 'hermes2.ai.unturf.com', url: 'https://hermes2.ai.unturf.com/v1' } +]; + +// Global state +export let lastTTSInput = ''; +export let lastTTSResult = null; + +export function setLastTTS(input, result) { + lastTTSInput = input; + lastTTSResult = result; +} \ No newline at end of file diff --git a/src/content.js b/src/content.js new file mode 100644 index 0000000..7a25e79 --- /dev/null +++ b/src/content.js @@ -0,0 +1,42 @@ +// Content extraction and processing functionality + +// Function to extract text content along with links and metadata from the webpage +export function extractWebpageContent() { + let content = ''; + + // Extract title + const title = document.title; + if (title) { + content += `**Page Title**: ${title}\n\n`; + } + + // Extract meta description + const metaDescription = document.querySelector('meta[name="description"]'); + if (metaDescription) { + content += `**Meta Description**: ${metaDescription.content}\n\n`; + } + + // Extract other metadata (if needed) + const metaKeywords = document.querySelector('meta[name="keywords"]'); + if (metaKeywords) { + content += `**Meta Keywords**: ${metaKeywords.content}\n\n`; + } + + // Recursively extract text and links from the body content + function getTextWithLinks(element) { + if (element.nodeType === Node.TEXT_NODE) { + content += element.textContent + ' '; + } else if (element.nodeType === Node.ELEMENT_NODE) { + if (element.tagName.toLowerCase() === 'a') { + // If it's a link, append the text and the href + content += `[${element.textContent}](${element.href}) `; + } else { + // Recursively process child nodes + element.childNodes.forEach(getTextWithLinks); + } + } + } + + getTextWithLinks(document.body); + return content.trim(); +} \ No newline at end of file diff --git a/src/file-upload.js b/src/file-upload.js new file mode 100644 index 0000000..628ee5e --- /dev/null +++ b/src/file-upload.js @@ -0,0 +1,146 @@ +// File upload and processing 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 { MEGAPARCE_API_URL } from './config.js'; +import { speakText } from './tts.js'; +import { chatHistory } from './chat.js'; + +// Progress indicator functions +export function showProgressIndicator(message) { + // Remove existing indicator if any + hideProgressIndicator(); + + const indicator = document.createElement('div'); + indicator.id = 'progress-indicator'; + indicator.style.cssText = ` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 20px; + border-radius: 5px; + z-index: 9999; + `; + indicator.textContent = message || 'Processing...'; + document.body.appendChild(indicator); +} + +export function hideProgressIndicator() { + const indicator = document.getElementById('progress-indicator'); + if (indicator) { + indicator.remove(); + } +} + +// Handle file upload from input element +export async function handleFileUpload() { + const fileInput = document.getElementById('file-input'); + if (!fileInput.files[0]) { + alert('Please select a file first.'); + return; + } + + try { + showProgressIndicator('Uploading file...'); + const response = await uploadFile(fileInput.files[0]); + hideProgressIndicator(); + + await integrateMegafarceResponse(response); + fileInput.value = ''; // Clear the input after upload + } catch (error) { + console.error('File upload error:', error); + alert('Failed to upload the file.'); + hideProgressIndicator(); + } +} + +// Upload File to MegaFarce with Progress Indicator +export async function uploadFile(file) { + const formData = new FormData(); + formData.append('file', file); + + const response = await fetch(MEGAPARCE_API_URL, { + method: 'POST', + body: formData + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Error uploading file: ${errorText}`); + } + + const responseData = await response.json(); + return responseData; +} + +// Integrate MegaFarce Response into Chat +export async function integrateMegafarceResponse(response) { + // Assuming the response contains a 'content' field with the parsed content + const content = response.content || response.result || "No content received."; + + // Add a system message with the uploaded content to provide context + chatHistory.push({ role: "system", content: `Uploaded Context: ${content}` }); + + // Display the uploaded content in the chat box + const chatBox = document.getElementById('chat-box'); + const systemMessage = document.createElement('p'); + systemMessage.innerHTML = `System: ${marked.parse(content)}`; + + // Apply syntax highlighting to any code blocks + systemMessage.querySelectorAll('pre code').forEach((block) => { + hljs.highlightElement(block); + }); + + chatBox.appendChild(systemMessage); + chatBox.scrollTop = chatBox.scrollHeight; + + // Add Generate TTS button for the uploaded content + const generateTTSButton = document.createElement('button'); + generateTTSButton.textContent = 'Generate TTS for Uploaded Content'; + let ttsAudio = null; + let ttsBlob = null; + let isPaused = false; + + generateTTSButton.onclick = async () => { + if (!ttsAudio) { + generateTTSButton.textContent = 'Processing...'; + generateTTSButton.disabled = true; // Disable button while processing + try { + const result = await speakText(content); + ttsAudio = result.audio; + ttsBlob = result.blob; + generateTTSButton.textContent = 'Pause TTS'; + generateTTSButton.disabled = false; // Re-enable button after processing + ttsAudio.play(); + + // Add download button for the TTS audio + const downloadButton = document.createElement('button'); + downloadButton.textContent = 'Download MP3'; + downloadButton.onclick = () => { + const a = document.createElement('a'); + a.href = URL.createObjectURL(ttsBlob); + a.download = 'uploaded-content.mp3'; + a.click(); + }; + chatBox.appendChild(downloadButton); + } catch (error) { + console.error('Error generating TTS:', error); + alert('Failed to generate TTS for the uploaded content.'); + generateTTSButton.textContent = 'Generate TTS for Uploaded Content'; + generateTTSButton.disabled = false; + } + } else { + if (isPaused) { + ttsAudio.play(); + generateTTSButton.textContent = 'Pause TTS'; + } else { + ttsAudio.pause(); + generateTTSButton.textContent = 'Play TTS'; + } + isPaused = !isPaused; + } + }; + chatBox.appendChild(generateTTSButton); +} \ No newline at end of file diff --git a/src/models.js b/src/models.js new file mode 100644 index 0000000..6a66a84 --- /dev/null +++ b/src/models.js @@ -0,0 +1,198 @@ +// Model registry and selection functionality +import { VLLM_ENDPOINTS } from './config.js'; + +// This registry maps a model's ID to the endpoint where it resides. +export const modelRegistry = {}; + +// Fetch models from each endpoint with caching. +// Cache is busted if the endpoint array changes or TTL. +export async function fetchModelsFromEndpoints() { + const cacheKey = 'modelRegistryCache'; + const endpointsKey = 'vllmEndpointsHash'; + const endpointsString = JSON.stringify(VLLM_ENDPOINTS); + const cachedEndpoints = localStorage.getItem(endpointsKey); + const cacheItem = localStorage.getItem(cacheKey); + const now = Date.now(); + const TTL = 300000; // 5 minutes in milliseconds + + if (cacheItem && cachedEndpoints === endpointsString) { + try { + const cachedData = JSON.parse(cacheItem); + if (now - cachedData.timestamp < TTL) { + // Restore cached modelRegistry + Object.assign(modelRegistry, cachedData.modelRegistry); + return cachedData.models; + } + } catch (e) { + console.error("Error reading model registry from cache", e); + } + } + + // If no valid cache, fetch models from all endpoints + const fetchPromises = VLLM_ENDPOINTS.map(async (endpoint) => { + try { + const res = await fetch(`${endpoint.url}/models`); + if (!res.ok) throw new Error(`HTTP error! status: ${res.status} from ${endpoint.url}`); + const jsonResponse = await res.json(); + // Expected JSON structure: { data: [ { id, ... }, ... ], object: "list" } + const models = jsonResponse.data || []; + // Map each model to include its endpoint ID, unique ID, and model name + return models.map((model) => ({ + ...model, + modelName: model.id, // Explicitly store model name + endpointId: endpoint.id, + uniqueId: `${endpoint.id}-${model.id}` // Unique ID with endpoint ID first + })); + } catch (error) { + console.error(`Error fetching models from ${endpoint.url}:`, error); + return []; + } + }); + const allModelsArrays = await Promise.all(fetchPromises); + const models = allModelsArrays.flat(); + + // Update modelRegistry with unique model instances + models.forEach((model) => { + modelRegistry[model.uniqueId] = { + url: VLLM_ENDPOINTS.find((e) => e.id === model.endpointId).url, + endpointId: model.endpointId + }; + }); + + // Cache the results + const cacheData = { + timestamp: now, + modelRegistry: modelRegistry, + models: models + }; + localStorage.setItem(cacheKey, JSON.stringify(cacheData)); + localStorage.setItem(endpointsKey, endpointsString); + + return models; +} + +// Create a dynamic drop-down for model selection +// This function creates a + + + `; + + // 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 uploadBtn = createButton('πŸ“ Upload File', () => document.querySelector('[data-uncloseai-file-input]')?.click()); + + controlsDiv.appendChild(readBtn); + controlsDiv.appendChild(ttsBtn); + 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 '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 = ` +

AI Chat

+
+ + + `; + container.appendChild(chatDiv); +} + +export function createTTSFeature(container) { + const ttsDiv = document.createElement('div'); + ttsDiv.innerHTML = ` +

Text to Speech

+ + +
+ `; + container.appendChild(ttsDiv); +} + +export function createUploadFeature(container) { + const uploadDiv = document.createElement('div'); + uploadDiv.innerHTML = ` +

File Upload

+ + +
+ `; + container.appendChild(uploadDiv); +} + +export function createReadFeature(container) { + const readDiv = document.createElement('div'); + readDiv.innerHTML = ` +

Page Reader

+

Read this page with AI voice

+ + `; + 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 = 'Converting to speech...'; + + try { + const result = await speakText(text, 'alloy', 0.9); + resultDiv.innerHTML = ` +
+ + +
+ `; + resultDiv.insertBefore(result.audio, resultDiv.firstChild); + } catch (error) { + resultDiv.innerHTML = 'Error: ' + 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 = 'Uploading and analyzing file...'; + + try { + showProgressIndicator('Processing file...'); + const response = await uploadFile(fileInput.files[0]); + hideProgressIndicator(); + + resultDiv.innerHTML = `Analysis Result:
${response}`; + fileInput.value = ''; + } catch (error) { + hideProgressIndicator(); + resultDiv.innerHTML = 'Error: ' + 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 = () => toggleHermesModal(); + + document.body.appendChild(floatingButton); + console.log('uncloseai.js: Appended floating button to document.body.'); +} + +// Function to toggle Hermes modal +export function toggleHermesModal() { + const existingModal = document.getElementById('hermes-modal'); + if (existingModal) { + document.body.removeChild(existingModal); + hermesModalOpen = false; + } else { + openHermesModal(); + hermesModalOpen = true; + } +} + +// Function to open Hermes modal +export function openHermesModal() { + // Create modal using dialog element + 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; + `; + } + + // Create the modal content with full interface + createFullInterface(article); + + modal.appendChild(article); + document.body.appendChild(modal); +} + +// TTS Modal functionality +export function openTTSModal() { + const modal = document.createElement('dialog'); + modal.id = 'tts-modal'; + modal.open = true; + + // Modal styling + modal.style.cssText = ` + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 90%; + max-width: 500px; + border: none; + border-radius: 8px; + padding: 20px; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + z-index: 3000; + `; + + const article = document.createElement('article'); + article.innerHTML = ` +
+

Text to Speech

+ +
+
+ +
+ + +
+ +
+
+ `; + + modal.appendChild(article); + + // Add generate button functionality + const generateBtn = modal.querySelector('#tts-generate-btn'); + generateBtn.onclick = async () => { + const text = modal.querySelector('#tts-modal-text').value.trim(); + const voice = modal.querySelector('#tts-modal-voice').value; + const resultDiv = modal.querySelector('#tts-modal-result'); + + if (!text) { + alert('Please enter some text first!'); + return; + } + + generateBtn.disabled = true; + generateBtn.textContent = 'Generating...'; + resultDiv.innerHTML = 'Converting to speech...'; + + try { + const result = await speakText(text, voice, 1.0); + resultDiv.innerHTML = ` +
+ + +
+ `; + resultDiv.insertBefore(result.audio, resultDiv.firstChild); + } catch (error) { + resultDiv.innerHTML = 'Error: ' + error.message; + } finally { + generateBtn.disabled = false; + generateBtn.textContent = 'Generate Speech'; + } + }; + + 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(); +} \ No newline at end of file diff --git a/uncloseai.js b/uncloseai.js index 545d1a0..fa32e4a 100644 --- a/uncloseai.js +++ b/uncloseai.js @@ -1,2711 +1,150 @@ -/* how to use in HTML */ - -/* - - - - -
-
-
-
-
- - -
-*/ +/* + * uncloseai.js - Modular AI Integration Library + * + * Main entry point that imports all modules and exports the public API + * This maintains backward compatibility while enabling better code organization + */ +// External dependencies 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'; -// ------------------------- -// Configuration and Endpoints -// ------------------------- - -// Original API endpoints for other functionalities -const TTS_API_URL = "https://speech.ai.unturf.com/v1/audio/speech"; -const MEGAPARCE_API_URL = "https://megaparce.ai.unturf.com/v1/file"; -const API_KEY = "dummy-api-key"; -const MODEL = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"; // default model (always available) - -let lastTTSInput = ''; -let lastTTSResult = null; - -// Dynamic Endpoints Configuration for Chat API -// You can list as many endpoints as you need. -// { id: 'hermes.ai.unturf.com', url: 'https://hermes.ai.unturf.com/v1' }, - -const VLLM_ENDPOINTS = [ - { id: 'hermes.ai.unturf.com', url: 'https://hermes.ai.unturf.com/v1' }, - { id: 'hermes2.ai.unturf.com', url: 'https://hermes2.ai.unturf.com/v1' } -]; +// Internal modules +import * as Config from './src/config.js'; +import * as Models from './src/models.js'; +import * as Content from './src/content.js'; +import * as TTS from './src/tts.js'; +import * as Chat from './src/chat.js'; +import * as FileUpload from './src/file-upload.js'; +import * as Storage from './src/storage.js'; +import * as PageReader from './src/page-reader.js'; +import * as UI from './src/ui.js'; // ------------------------- -// Dynamic Endpoints and Model Registry +// Public API - Main Functions // ------------------------- -// This registry maps a model's ID to the endpoint where it resides. -const modelRegistry = {}; +// Chat functionality +export const sendMessage = Chat.sendMessage; +export const handleUserInput = Chat.handleUserInput; -// Fetch models from each endpoint with caching. -// Cache is busted if the endpoint array changes or TTL. -async function fetchModelsFromEndpoints() { - const cacheKey = 'modelRegistryCache'; - const endpointsKey = 'vllmEndpointsHash'; - const endpointsString = JSON.stringify(VLLM_ENDPOINTS); - const cachedEndpoints = localStorage.getItem(endpointsKey); - const cacheItem = localStorage.getItem(cacheKey); - const now = Date.now(); - const TTL = 300000; // 5 minutes in milliseconds +// TTS functionality +export const speakText = TTS.speakText; +export const processContentWithHermes = TTS.processContentWithHermes; +export const extractSpokenTokens = TTS.extractSpokenTokens; +export const generateTitleForTTS = TTS.generateTitleForTTS; - if (cacheItem && cachedEndpoints === endpointsString) { - try { - const cachedData = JSON.parse(cacheItem); - if (now - cachedData.timestamp < TTL) { - // Restore cached modelRegistry - Object.assign(modelRegistry, cachedData.modelRegistry); - return cachedData.models; - } - } catch (e) { - console.error("Error reading model registry from cache", e); - } - } +// File upload +export const uploadFile = FileUpload.uploadFile; +export const integrateMegafarceResponse = FileUpload.integrateMegafarceResponse; +export const showProgressIndicator = FileUpload.showProgressIndicator; +export const hideProgressIndicator = FileUpload.hideProgressIndicator; - // If no valid cache, fetch models from all endpoints - const fetchPromises = VLLM_ENDPOINTS.map(async (endpoint) => { - try { - const res = await fetch(`${endpoint.url}/models`); - if (!res.ok) throw new Error(`HTTP error! status: ${res.status} from ${endpoint.url}`); - const jsonResponse = await res.json(); - // Expected JSON structure: { data: [ { id, ... }, ... ], object: "list" } - const models = jsonResponse.data || []; - // Map each model to include its endpoint ID, unique ID, and model name - return models.map((model) => ({ - ...model, - modelName: model.id, // Explicitly store model name - endpointId: endpoint.id, - uniqueId: `${endpoint.id}-${model.id}` // Unique ID with endpoint ID first - })); - } catch (error) { - console.error(`Error fetching models from ${endpoint.url}:`, error); - return []; - } - }); - const allModelsArrays = await Promise.all(fetchPromises); - const models = allModelsArrays.flat(); +// Page reading +export const readPageWithHermes = PageReader.readPageWithHermes; +export const extractWebpageContent = Content.extractWebpageContent; - // Update modelRegistry with unique model instances - models.forEach((model) => { - modelRegistry[model.uniqueId] = { - url: VLLM_ENDPOINTS.find((e) => e.id === model.endpointId).url, - endpointId: model.endpointId - }; - }); +// Model management +export const getSelectedModel = Models.getSelectedModel; +export const getSelectedModelEndpoint = Models.getSelectedModelEndpoint; +export const fetchModelsFromEndpoints = Models.fetchModelsFromEndpoints; +export const createModelSelectionDropdown = Models.createModelSelectionDropdown; +export const addRefreshModelsButton = Models.addRefreshModelsButton; - // Cache the results - const cacheData = { - timestamp: now, - modelRegistry: modelRegistry, - models: models - }; - localStorage.setItem(cacheKey, JSON.stringify(cacheData)); - localStorage.setItem(endpointsKey, endpointsString); +// Storage +export const saveConversationHistory = Storage.saveConversationHistory; +export const loadConversationHistory = Storage.loadConversationHistory; +export const clearConversationHistory = Storage.clearConversationHistory; +export const getPageSpecificKey = Storage.getPageSpecificKey; - return models; -} - -// Create a dynamic drop-down for model selection -// This function creates a - - - `; - - // 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 uploadBtn = createButton('πŸ“ Upload File', () => document.querySelector('[data-uncloseai-file-input]')?.click()); - - controlsDiv.appendChild(readBtn); - controlsDiv.appendChild(ttsBtn); - 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 = async (e) => { - if (e.target.files[0]) { - try { - showProgressIndicator('Processing file...'); - const response = await uploadFile(e.target.files[0]); - await integrateMegafarceResponse(response); - hideProgressIndicator(); - e.target.value = ''; - } catch (error) { - hideProgressIndicator(); - alert('File upload failed: ' + error.message); - } - } - }; - - container.appendChild(controlsDiv); - container.appendChild(chatContainer); - container.appendChild(fileInput); -} - -// Create custom interface based on specific features -function createCustomInterface(container, features) { - features.forEach(feature => { - const featureDiv = document.createElement('div'); - featureDiv.style.margin = '10px 0'; - - switch (feature.trim()) { - case 'chat': - createChatFeature(featureDiv); - break; - case 'tts': - createTTSFeature(featureDiv); - break; - case 'upload': - createUploadFeature(featureDiv); - break; - case 'read': - createReadFeature(featureDiv); - break; - } - - container.appendChild(featureDiv); - }); -} - -// Individual feature creators -function createChatFeature(container) { - container.innerHTML = ` -
-

AI Chat

-
-
- - -
-
- `; -} - -function createTTSFeature(container) { - container.innerHTML = ` -
-

Text to Speech

- - -
-
- `; -} - -function createUploadFeature(container) { - container.innerHTML = ` -
-

File Upload & Analysis

- - -
-
- `; -} - -function createReadFeature(container) { - container.innerHTML = ` -
-

Page Reading

-

Let AI read and analyze the current page content.

- -
- `; -} - -// Helper functions for custom features -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; -} - -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 = 'Converting to speech...'; - - try { - const result = await speakText(text, 'alloy', 0.9); - resultDiv.innerHTML = ` -
- - -
- `; - resultDiv.insertBefore(result.audio, resultDiv.firstChild); - } catch (error) { - resultDiv.innerHTML = 'Error: ' + error.message; - } finally { - button.disabled = false; - button.textContent = 'πŸ”Š Convert to Speech'; - } -} - -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 = 'Uploading and analyzing file...'; - - try { - showProgressIndicator('Processing file...'); - const response = await uploadFile(fileInput.files[0]); - hideProgressIndicator(); - - resultDiv.innerHTML = `Analysis Result:
${response}`; - fileInput.value = ''; - } catch (error) { - hideProgressIndicator(); - resultDiv.innerHTML = 'Error: ' + error.message; - } finally { - button.disabled = false; - button.textContent = 'πŸ“ Upload & Analyze'; - } -} - -// Initialize the legacy chat interface (for backward compatibility) -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(); - } - }); -} - - -// Function to create floating AI button -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 = () => toggleHermesModal(); - - document.body.appendChild(floatingButton); - console.log('uncloseai.js: Appended floating button to document.body.'); -} - -// Global variable to track modal state -let hermesModalOpen = false; - -// Function to toggle Hermes modal -function toggleHermesModal() { - const existingModal = document.getElementById('hermes-modal'); - if (existingModal) { - document.body.removeChild(existingModal); - hermesModalOpen = false; - } else { - openHermesModal(); - hermesModalOpen = true; - } -} - -// Function to open Hermes modal -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 hermes 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; - `; - } - - // Copy options from existing model dropdown - const existingDropdown = document.getElementById('model-selection'); - if (existingDropdown) { - Array.from(existingDropdown.options).forEach(option => { - const newOption = document.createElement('option'); - newOption.value = option.value; - newOption.textContent = option.textContent; - newOption.selected = option.selected; - modelSelect.appendChild(newOption); - }); - } - - // Restore saved model selection - const savedModel = localStorage.getItem('hermes-selected-model'); - if (savedModel && modelSelect.querySelector(`option[value="${savedModel}"]`)) { - modelSelect.value = savedModel; - } - - // 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 both dropdowns - [modelSelect, document.getElementById('model-selection')].forEach(dropdown => { - if (dropdown) { - dropdown.innerHTML = ''; - models.forEach(model => { - const option = document.createElement('option'); - option.value = model.uniqueId; - option.textContent = `${model.endpointId} | ${model.modelName}`; - dropdown.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); - - // Restore conversation history in modal - function restoreConversationHistory() { - const history = loadConversationHistory(); - chatBox.innerHTML = ''; - history.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('hermes-conversation-history', JSON.stringify(savedHistory)); - // Update global chat history - chatHistory = [ - { - role: "system", - content: "You are Hermes, a large language model from Nous Research, embedded as an AI assistant on this webpage. Your primary task is to help users understand and interact with the content of this specific page, while also being capable of assisting with any other topics, coding, creative tasks, or questions they may have. You should be conversational, helpful, and knowledgeable about the webpage content as well as general topics. Always strive to provide accurate, useful responses." - }, - ...savedHistory - ]; - restoreConversationHistory(); - } - } - }; - })(index); - - if (msg.role === 'user') { - messageDiv.innerHTML = `

You: ${msg.content}

`; - } else if (msg.role === 'assistant') { - const parsedContent = marked.parse(msg.content); - messageDiv.innerHTML = `

AI: ${parsedContent}

`; - } - - 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(); - 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 = '

πŸ€– Hermes: ✨ Analyzing this page and crafting a personalized introduction... This may take a moment.

'; - 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 sendMessageDirect(prompt)) { - generatedIntro += chunk; - // Update display in real-time - introDiv.innerHTML = `

πŸ€– Hermes: ${generatedIntro}

`; - // Scroll to bottom as content updates - chatBox.scrollTop = chatBox.scrollHeight; - } - - // Add to chat history and save - chatHistory.push({ role: "assistant", content: generatedIntro }); - saveConversationHistory(); - - // Cache the generated introduction - localStorage.setItem(cacheKey, generatedIntro); - - // Add TTS button for introduction - addTTSButtonToIntro(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 = `

πŸ€– Hermes: ${fallbackIntro}

`; - // Add fallback to chat history too - chatHistory.push({ role: "assistant", content: fallbackIntro }); - saveConversationHistory(); - } - } - } - - 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 = `

πŸ€– Hermes: ${introText}

`; - chatBox.appendChild(introDiv); - - // Add TTS button for cached introduction - addTTSButtonToIntro(introDiv, introText); - - chatBox.scrollTop = chatBox.scrollHeight; - } - - // Add TTS button to introduction message - function addTTSButtonToIntro(introDiv, introText) { - const ttsIntroContainer = document.createElement('div'); - ttsIntroContainer.style.cssText = ` - display: grid; - grid-template-columns: auto auto auto; - gap: 8px; - margin: 10px 0; - align-items: center; - `; - - const ttsIntroBtn = document.createElement('button'); - ttsIntroBtn.textContent = 'πŸ”Š Play Response'; - if (USE_CUSTOM_STYLING) { - ttsIntroBtn.style.cssText = 'padding: 6px 12px; border: 1px solid #0066cc; border-radius: 4px; background: #f0f8ff; cursor: pointer; color: #0066cc;'; - } else { - ttsIntroBtn.style.cssText = 'padding: 6px 12px; border-radius: 4px; cursor: pointer;'; - } - - const regenerateIntroBtn = document.createElement('button'); - regenerateIntroBtn.textContent = 'πŸ”„ Regenerate'; - regenerateIntroBtn.style.display = 'none'; - if (USE_CUSTOM_STYLING) { - regenerateIntroBtn.style.cssText += 'padding: 6px 12px; border: 1px solid #0066cc; border-radius: 4px; background: #f0f8ff; cursor: pointer; color: #0066cc;'; - } else { - regenerateIntroBtn.style.cssText += 'padding: 6px 12px; border-radius: 4px; cursor: pointer;'; - } - - const downloadIntroBtn = document.createElement('button'); - downloadIntroBtn.textContent = 'πŸ’Ύ Download'; - downloadIntroBtn.style.display = 'none'; - if (USE_CUSTOM_STYLING) { - downloadIntroBtn.style.cssText += 'padding: 6px 12px; border: 1px solid #0066cc; border-radius: 4px; background: #f0f8ff; cursor: pointer; color: #0066cc;'; - } else { - downloadIntroBtn.style.cssText += 'padding: 6px 12px; border-radius: 4px; cursor: pointer;'; - } - - let introAudio = null; - let introBlob = null; - let currentIntroVoice = voiceSelect.value; - - // Function to generate/regenerate intro TTS - const generateIntroTTS = async () => { - ttsIntroBtn.textContent = 'Processing...'; - ttsIntroBtn.disabled = true; - regenerateIntroBtn.disabled = true; - - const selectedVoice = voiceSelect.value; - currentIntroVoice = selectedVoice; - const result = await speakText(introText, selectedVoice, 0.9); - introAudio = result.audio; - introBlob = result.blob; - - ttsIntroBtn.textContent = 'Pause'; - ttsIntroBtn.disabled = false; - regenerateIntroBtn.disabled = false; - regenerateIntroBtn.style.display = 'inline-block'; - downloadIntroBtn.style.display = 'inline-block'; - - introAudio.play(); - }; - - ttsIntroBtn.onclick = async () => { - if (!introAudio) { - await generateIntroTTS(); - } else { - if (introAudio.paused) { - introAudio.play(); - ttsIntroBtn.textContent = 'Pause'; - } else { - introAudio.pause(); - ttsIntroBtn.textContent = 'Play'; - } - } - }; - - regenerateIntroBtn.onclick = async () => { - if (introAudio) { - introAudio.pause(); - } - introAudio = null; - introBlob = null; - await generateIntroTTS(); - }; - - downloadIntroBtn.onclick = () => { - if (introBlob) { - const a = document.createElement('a'); - a.href = URL.createObjectURL(introBlob); - a.download = `hermes-introduction-${Date.now()}.mp3`; - a.click(); - } - }; - - // Watch for voice changes - voiceSelect.addEventListener('change', () => { - if (introAudio && currentIntroVoice !== voiceSelect.value) { - regenerateIntroBtn.style.display = 'inline-block'; - regenerateIntroBtn.style.background = '#fffacd'; // Light yellow to indicate change needed - } - }); - - ttsIntroContainer.appendChild(ttsIntroBtn); - ttsIntroContainer.appendChild(regenerateIntroBtn); - ttsIntroContainer.appendChild(downloadIntroBtn); - introDiv.appendChild(ttsIntroContainer); - } - - // Direct LLM call for introduction generation (doesn't add to chat history) - async function* sendMessageDirect(message) { - const selectedModel = modelSelect.value || MODEL; - const modelInfo = modelRegistry[selectedModel]; - const endpoint = modelInfo ? modelInfo.url : VLLM_ENDPOINTS[0].url; - const modelName = modelInfo ? modelInfo.modelName : MODEL; - - const response = await fetch(`${endpoint}/chat/completions`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - model: modelName, - messages: [{ role: "user", content: message }], - temperature: 0.7, - max_tokens: 300, - stream: true - }), - }); - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let buffer = ''; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n'); - - for (let i = 0; i < lines.length - 1; i++) { - const line = lines[i].trim(); - if (line.startsWith('data: ')) { - const jsonData = line.slice(6); - if (jsonData === '[DONE]') continue; - - try { - const parsedData = JSON.parse(jsonData); - const content = parsedData.choices[0].delta.content; - if (content) { - yield content; - } - } catch (error) { - console.error('Error parsing JSON:', error); - } - } - } - buffer = lines[lines.length - 1]; - } - } - // Call async introduction function - addHermesIntroduction().catch(error => { - console.error('Failed to add Hermes introduction:', error); - }); - - // Create input area - const inputArea = document.createElement('div'); - if (USE_CUSTOM_STYLING) { - inputArea.style.cssText = ` - padding: 16px 20px; - display: flex; - gap: 12px; - align-items: center; - `; - } else { - inputArea.style.cssText = ` - padding: 1rem; - display: flex; - gap: 0.5rem; - align-items: center; - `; - } - - const messageInput = document.createElement('input'); - messageInput.type = 'text'; - messageInput.id = 'modal-user-input'; - messageInput.placeholder = 'πŸ’¬ Chat with Hermes about this page, code, ideas, or anything! Your AI companion awaits...'; - messageInput.style.cssText = ` - flex: 1; - padding: 12px; - border: 1px solid #ccc; - border-radius: 8px; - font-size: 14px; - `; - - const sendButton = document.createElement('button'); - sendButton.textContent = 'Send'; - -// Expose functions to the global scope for direct HTML calls -window.handleUserInput = handleUserInput; -window.readPageWithHermes = readPageWithHermes; -window.openTTSModal = openTTSModal; -window.handleTTSFromElement = handleTTSFromElement; -window.handleUploadFromElement = handleUploadFromElement; -window.uploadFile = uploadFile; -window.showProgressIndicator = showProgressIndicator; -window.hideProgressIndicator = hideProgressIndicator; -window.sendMessage = sendMessage; -window.speakText = speakText; - 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; - `; - } - - // File upload - const fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.id = 'modal-file-input'; - fileInput.style.display = 'none'; - - const fileButton = document.createElement('button'); - fileButton.textContent = 'πŸ“'; - fileButton.style.cssText = ` - padding: 12px; - background: #f5f5f5; - border: 1px solid #ccc; - border-radius: 8px; - cursor: pointer; - `; - fileButton.onclick = () => fileInput.click(); - - // Handle file upload - fileInput.onchange = async () => { - if (fileInput.files.length > 0) { - const file = fileInput.files[0]; - try { - showProgressIndicator('Uploading & Processing file...'); - const response = await uploadFile(file); - await integrateMegafarceResponseToModal(response, chatBox); - hideProgressIndicator(); - fileInput.value = ''; - } catch (error) { - console.error('File upload error:', error); - alert('Failed to upload the file.'); - hideProgressIndicator(); - } - } - }; - - // Handle send message - const handleModalInput = async () => { - const userInput = messageInput.value.trim(); - if (!userInput) return; - - messageInput.value = ''; - - // Create user message with delete functionality - const userMessageDiv = document.createElement('div'); - userMessageDiv.style.cssText = 'position: relative; margin-bottom: 10px; padding: 5px; border-radius: 5px; background: rgba(0,0,0,0.05);'; - userMessageDiv.innerHTML = `

You: ${userInput}

`; - chatBox.appendChild(userMessageDiv); - - // Scroll to bottom when user message is added - chatBox.scrollTop = chatBox.scrollHeight; - - // Create AI response container - const aiMessageDiv = document.createElement('div'); - aiMessageDiv.style.cssText = 'position: relative; margin-bottom: 10px; padding: 5px; border-radius: 5px; background: rgba(0,0,0,0.05);'; - const aiResponseParagraph = document.createElement('p'); - aiResponseParagraph.innerHTML = 'AI: '; - const responseContent = document.createElement('span'); - aiResponseParagraph.appendChild(responseContent); - aiMessageDiv.appendChild(aiResponseParagraph); - chatBox.appendChild(aiMessageDiv); - - let accumulatedContent = ''; - for await (const chunk of sendMessage(userInput)) { - accumulatedContent += chunk; - const parsedChunk = marked.parse(accumulatedContent); - responseContent.innerHTML = parsedChunk; - - responseContent.querySelectorAll('pre code').forEach((block) => { - hljs.highlightElement(block); - }); - - // Auto-scroll to bottom as response streams in - chatBox.scrollTop = chatBox.scrollHeight; - } - - // Add assistant response to chat history and save - chatHistory.push({ role: "assistant", content: accumulatedContent }); - saveConversationHistory(); - - // Add delete buttons to the new messages - const userDeleteBtn = createStyledButton('Γ—', 'close', 'position: absolute; top: 2px; right: 2px; width: 20px; height: 20px; font-size: 12px; background: rgba(255,0,0,0.7); color: white; border-radius: 4px;'); - userDeleteBtn.onclick = () => { - if (confirm('Delete this conversation pair?')) { - const currentHistory = loadConversationHistory(); - // Remove the last 2 messages (user + AI response) - if (currentHistory.length >= 2) { - currentHistory.splice(-2, 2); - localStorage.setItem('hermes-conversation-history', JSON.stringify(currentHistory)); - chatHistory = [ - { - role: "system", - content: "You are Hermes, a large language model from Nous Research, embedded as an AI assistant on this webpage. Your primary task is to help users understand and interact with the content of this specific page, while also being capable of assisting with any other topics, coding, creative tasks, or questions they may have. You should be conversational, helpful, and knowledgeable about the webpage content as well as general topics. Always strive to provide accurate, useful responses." - }, - ...currentHistory - ]; - restoreConversationHistory(); - } - } - }; - - const aiDeleteBtn = createStyledButton('Γ—', 'close', 'position: absolute; top: 2px; right: 2px; width: 20px; height: 20px; font-size: 12px; background: rgba(255,0,0,0.7); color: white; border-radius: 4px;'); - aiDeleteBtn.onclick = userDeleteBtn.onclick; // Same functionality - delete the pair - - userMessageDiv.appendChild(userDeleteBtn); - aiMessageDiv.appendChild(aiDeleteBtn); - - chatBox.scrollTop = chatBox.scrollHeight; - - // Add TTS button container with grid layout - const ttsContainer = document.createElement('div'); - ttsContainer.style.cssText = ` - display: grid; - grid-template-columns: auto auto auto; - gap: 8px; - margin: 10px 0; - align-items: center; - `; - - const ttsResponseBtn = document.createElement('button'); - ttsResponseBtn.textContent = 'πŸ”Š Play Response'; - if (USE_CUSTOM_STYLING) { - ttsResponseBtn.style.cssText = 'padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;'; - } - - const regenerateBtn = document.createElement('button'); - regenerateBtn.textContent = 'πŸ”„ Regenerate'; - regenerateBtn.style.display = 'none'; - if (USE_CUSTOM_STYLING) { - regenerateBtn.style.cssText += 'padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;'; - } - - const downloadBtn = document.createElement('button'); - downloadBtn.textContent = 'πŸ’Ύ Download'; - downloadBtn.style.display = 'none'; - if (USE_CUSTOM_STYLING) { - downloadBtn.style.cssText += 'padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;'; - } - - let responseAudio = null; - let responseBlob = null; - let currentVoice = voiceSelect.value; - - // Function to generate/regenerate TTS - const generateTTS = async () => { - ttsResponseBtn.textContent = 'Processing...'; - ttsResponseBtn.disabled = true; - regenerateBtn.disabled = true; - - const selectedVoice = voiceSelect.value; - currentVoice = selectedVoice; - const result = await speakText(accumulatedContent, selectedVoice, 0.9); - responseAudio = result.audio; - responseBlob = result.blob; - - ttsResponseBtn.textContent = 'Pause'; - ttsResponseBtn.disabled = false; - regenerateBtn.disabled = false; - regenerateBtn.style.display = 'inline-block'; - downloadBtn.style.display = 'inline-block'; - - responseAudio.play(); - }; - - ttsResponseBtn.onclick = async () => { - if (!responseAudio) { - await generateTTS(); - } else { - if (responseAudio.paused) { - responseAudio.play(); - ttsResponseBtn.textContent = 'Pause'; - } else { - responseAudio.pause(); - ttsResponseBtn.textContent = 'Play'; - } - } - }; - - regenerateBtn.onclick = async () => { - if (responseAudio) { - responseAudio.pause(); - } - responseAudio = null; - responseBlob = null; - await generateTTS(); - }; - - downloadBtn.onclick = () => { - if (responseBlob) { - const a = document.createElement('a'); - a.href = URL.createObjectURL(responseBlob); - a.download = `hermes-response-${Date.now()}.mp3`; - a.click(); - } - }; - - // Watch for voice changes - voiceSelect.addEventListener('change', () => { - if (responseAudio && currentVoice !== voiceSelect.value) { - regenerateBtn.style.display = 'inline-block'; - regenerateBtn.style.background = '#fffacd'; // Light yellow to indicate change needed - } - }); - - ttsContainer.appendChild(ttsResponseBtn); - ttsContainer.appendChild(regenerateBtn); - ttsContainer.appendChild(downloadBtn); - chatBox.appendChild(ttsContainer); - }; - - sendButton.onclick = handleModalInput; - messageInput.addEventListener('keydown', (e) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleModalInput(); - } - }); - - inputArea.appendChild(fileButton); - inputArea.appendChild(fileInput); - inputArea.appendChild(messageInput); - inputArea.appendChild(sendButton); - - // Assemble modal - article.appendChild(header); - article.appendChild(controls); - article.appendChild(chatArea); - article.appendChild(inputArea); - - document.body.appendChild(modal); - messageInput.focus(); -} - -// Configuration for styling - set to false to disable custom styling -const USE_CUSTOM_STYLING = window.UNCLOSEAI_CUSTOM_STYLING !== false; +// Export configuration constants +export const TTS_API_URL = Config.TTS_API_URL; +export const MEGAPARCE_API_URL = Config.MEGAPARCE_API_URL; +export const API_KEY = Config.API_KEY; +export const MODEL = Config.MODEL; +export const VLLM_ENDPOINTS = Config.VLLM_ENDPOINTS; + +// Export model registry +export const modelRegistry = Models.modelRegistry; + +// Export chat history (for external access) +export { chatHistory } from './src/chat.js'; // ------------------------- -// Shared Utility Functions for DRY Code +// Initialization // ------------------------- -// Create standardized button with consistent styling -function createStyledButton(text, type = 'primary', additionalStyles = '') { - const button = document.createElement('button'); - button.textContent = text; - - const baseStyles = 'padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px;'; - - let typeStyles = ''; - switch (type) { - case 'primary': - typeStyles = USE_CUSTOM_STYLING - ? 'background: #007bff; color: white;' - : 'background: var(--primary, #007bff); color: white;'; - break; - case 'success': - typeStyles = USE_CUSTOM_STYLING - ? 'background: #28a745; color: white;' - : 'background: var(--success, #28a745); color: white;'; - break; - case 'warning': - typeStyles = USE_CUSTOM_STYLING - ? 'background: #ffc107; color: black;' - : 'background: var(--warning, #ffc107); color: black;'; - break; - case 'info': - typeStyles = USE_CUSTOM_STYLING - ? 'background: #17a2b8; color: white;' - : 'background: var(--info, #17a2b8); color: white;'; - break; - case 'close': - typeStyles = USE_CUSTOM_STYLING - ? 'background: none; border: none; color: currentColor; font-size: 24px; padding: 4px 8px;' - : 'float: right; background: none; border: none; font-size: 1.2em; padding: 4px 8px; border-radius: 4px;'; - break; - default: - typeStyles = USE_CUSTOM_STYLING - ? 'background: #6c757d; color: white;' - : ''; - } - - button.style.cssText = baseStyles + typeStyles + additionalStyles; - return button; -} - -// Create standardized chat message element -function createChatMessage(content, sender = 'user', isHTML = false) { - const messageDiv = document.createElement('div'); - const bgColor = sender === 'user' ? '#e3f2fd' : '#f3e5f5'; - const label = sender === 'user' ? 'You' : 'AI'; - - messageDiv.style.cssText = `margin-bottom: 10px; padding: 8px; background: ${bgColor}; border-radius: 4px;`; - - if (isHTML) { - messageDiv.innerHTML = `${label}: ${content}`; - } else { - messageDiv.innerHTML = `${label}: ${content}`; - } - - return messageDiv; -} - -// Create standardized TTS control set (play/pause, regenerate, download) -function createTTSControls(textContent, voiceSelectElement, onAudioGenerated = null) { - const controlContainer = document.createElement('div'); - controlContainer.style.cssText = 'display: grid; grid-template-columns: auto auto auto; gap: 8px; margin: 10px 0; align-items: center;'; - - const playBtn = createStyledButton('πŸ”Š Play', 'primary'); - const regenerateBtn = createStyledButton('πŸ”„ Regenerate', 'secondary'); - const downloadBtn = createStyledButton('πŸ’Ύ Download', 'secondary'); - - regenerateBtn.style.display = 'none'; - downloadBtn.style.display = 'none'; - - let audio = null; - let audioBlob = null; - let currentVoice = voiceSelectElement.value; - - const generateTTS = async () => { - playBtn.textContent = 'Processing...'; - playBtn.disabled = true; - regenerateBtn.disabled = true; - - const selectedVoice = voiceSelectElement.value; - currentVoice = selectedVoice; - const result = await speakText(textContent, selectedVoice, 0.9); - audio = result.audio; - audioBlob = result.blob; - - playBtn.textContent = 'Pause'; - playBtn.disabled = false; - regenerateBtn.disabled = false; - regenerateBtn.style.display = 'inline-block'; - downloadBtn.style.display = 'inline-block'; - - if (onAudioGenerated) onAudioGenerated(audio, audioBlob); - audio.play(); - }; - - playBtn.onclick = async () => { - if (!audio) { - await generateTTS(); - } else { - if (audio.paused) { - audio.play(); - playBtn.textContent = 'Pause'; - } else { - audio.pause(); - playBtn.textContent = 'Play'; - } - } - }; - - regenerateBtn.onclick = async () => { - if (audio) audio.pause(); - audio = null; - audioBlob = null; - await generateTTS(); - }; - - downloadBtn.onclick = () => { - if (audioBlob) { - const a = document.createElement('a'); - a.href = URL.createObjectURL(audioBlob); - a.download = `tts-audio-${Date.now()}.mp3`; - a.click(); - } - }; - - // Watch for voice changes - voiceSelectElement.addEventListener('change', () => { - if (audio && currentVoice !== voiceSelectElement.value) { - regenerateBtn.style.display = 'inline-block'; - regenerateBtn.style.background = '#fffacd'; - } - }); - - controlContainer.appendChild(playBtn); - controlContainer.appendChild(regenerateBtn); - controlContainer.appendChild(downloadBtn); - - return { container: controlContainer, playBtn, regenerateBtn, downloadBtn }; -} - -// Create standardized voice selection dropdown -function createVoiceSelect(id = '', selectedVoice = 'alloy') { - const select = document.createElement('select'); - if (id) select.id = id; - - const voices = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer']; - voices.forEach(voice => { - const option = document.createElement('option'); - option.value = voice; - option.textContent = voice.charAt(0).toUpperCase() + voice.slice(1); - if (voice === selectedVoice) option.selected = true; - select.appendChild(option); - }); - - return select; -} - -// Create standardized modal header with close button -function createModalHeader(title, onClose) { - 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; - `; - } - - const h1 = document.createElement('h1'); - h1.textContent = title; - if (USE_CUSTOM_STYLING) { - h1.style.cssText = 'margin: 0; font-family: "ChunkFiveRegular", monospace; font-size: 20px;'; - } - - const closeButton = createStyledButton('X', 'close'); - closeButton.onclick = onClose; - - header.appendChild(h1); - header.appendChild(closeButton); - - return header; -} - -// Function to open TTS modal -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 = createStyledButton('X', 'close'); - 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; - - 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; - - // 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); -} - -// Helper function to integrate file upload response to modal -async function integrateMegafarceResponseToModal(response, chatBox) { - const content = response.content || response.result || "No content received."; - chatHistory.push({ role: "system", content: `Uploaded Context: ${content}` }); - - const systemMessage = document.createElement('p'); - systemMessage.innerHTML = `System: ${marked.parse(content)}`; - - systemMessage.querySelectorAll('pre code').forEach((block) => { - hljs.highlightElement(block); - }); - - chatBox.appendChild(systemMessage); - chatBox.scrollTop = chatBox.scrollHeight; -} - -// Helper function to scroll to chat box -function scrollToChatBox() { - const chatContainer = document.getElementById('chat-container'); - if (chatContainer) { - chatContainer.scrollIntoView({ behavior: 'smooth' }); - document.getElementById('user-input')?.focus(); - } -} - -// ------------------------- -// Class-based Initialization System -// ------------------------- - - - -// Configuration flag for showing floating button -const SHOW_FLOATING_BUTTON = window.UNCLOSEAI_SHOW_BUTTON !== false; - // Initialize on page load -window.onload = () => { +window.addEventListener('load', () => { console.log('uncloseai.js: window.onload event fired.'); - initializeChatInterface(); - createModelSelectionDropdown(); - addRefreshModelsButton(); + UI.initializeSystem(); + Models.createModelSelectionDropdown(); + Models.addRefreshModelsButton(); +}); + +// ------------------------- +// Global Scope Exports (for backward compatibility) +// ------------------------- + +// Export functions to global scope for HTML onclick handlers +window.handleUserInput = Chat.handleUserInput; +window.readPageWithHermes = PageReader.readPageWithHermes; +window.sendMessage = Chat.sendMessage; +window.speakText = TTS.speakText; +window.uploadFile = FileUpload.uploadFile; +window.openTTSModal = UI.openTTSModal; +window.toggleHermesModal = UI.toggleHermesModal; +window.extractWebpageContent = Content.extractWebpageContent; +window.getSelectedModel = Models.getSelectedModel; +window.getSelectedModelEndpoint = Models.getSelectedModelEndpoint; +window.showProgressIndicator = FileUpload.showProgressIndicator; +window.hideProgressIndicator = FileUpload.hideProgressIndicator; +window.handleTTSFromElement = UI.handleTTSFromElement; +window.handleUploadFromElement = UI.handleUploadFromElement; + +// Export helper functions for class-based integrations +window.handleCustomChat = async function(button) { + const container = button.parentElement; + const input = container.querySelector('[data-chat-input]'); + const chatBox = container.querySelector('[data-chat-box]'); + const message = input.value.trim(); - // 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.'); + if (!message) return; + + // Add user message + chatBox.innerHTML += `
You: ${message}
`; + input.value = ''; + + // Add AI thinking indicator + const thinkingDiv = document.createElement('div'); + thinkingDiv.style.cssText = 'margin: 2px 0; padding: 2px; background: #f5f5f5; border-radius: 2px; font-size: 0.9em;'; + thinkingDiv.innerHTML = 'AI: thinking...'; + chatBox.appendChild(thinkingDiv); + chatBox.scrollTop = chatBox.scrollHeight; + + try { + let response = ''; + for await (const chunk of Chat.sendMessage(message)) { + response += chunk; + thinkingDiv.innerHTML = `AI: ${response}`; + chatBox.scrollTop = chatBox.scrollHeight; + } + } catch (error) { + thinkingDiv.innerHTML = `Error: ${error.message}`; } - - // Initialize class-based elements - initializeUncloseaiElements(); }; -// Export functions to global scope -window.handleUserInput = handleUserInput; -window.readPage = readPageWithHermes; +console.log('uncloseai.js: Modular version loaded successfully'); \ No newline at end of file diff --git a/uncloseai.js.orig b/uncloseai.js.orig new file mode 100644 index 0000000..545d1a0 --- /dev/null +++ b/uncloseai.js.orig @@ -0,0 +1,2711 @@ +/* how to use in HTML */ + +/* + + + + +
+
+
+
+
+ + +
+*/ + +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'; + +// ------------------------- +// Configuration and Endpoints +// ------------------------- + +// Original API endpoints for other functionalities +const TTS_API_URL = "https://speech.ai.unturf.com/v1/audio/speech"; +const MEGAPARCE_API_URL = "https://megaparce.ai.unturf.com/v1/file"; +const API_KEY = "dummy-api-key"; +const MODEL = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"; // default model (always available) + +let lastTTSInput = ''; +let lastTTSResult = null; + +// Dynamic Endpoints Configuration for Chat API +// You can list as many endpoints as you need. +// { id: 'hermes.ai.unturf.com', url: 'https://hermes.ai.unturf.com/v1' }, + +const VLLM_ENDPOINTS = [ + { id: 'hermes.ai.unturf.com', url: 'https://hermes.ai.unturf.com/v1' }, + { id: 'hermes2.ai.unturf.com', url: 'https://hermes2.ai.unturf.com/v1' } +]; + +// ------------------------- +// Dynamic Endpoints and Model Registry +// ------------------------- + +// This registry maps a model's ID to the endpoint where it resides. +const modelRegistry = {}; + +// Fetch models from each endpoint with caching. +// Cache is busted if the endpoint array changes or TTL. +async function fetchModelsFromEndpoints() { + const cacheKey = 'modelRegistryCache'; + const endpointsKey = 'vllmEndpointsHash'; + const endpointsString = JSON.stringify(VLLM_ENDPOINTS); + const cachedEndpoints = localStorage.getItem(endpointsKey); + const cacheItem = localStorage.getItem(cacheKey); + const now = Date.now(); + const TTL = 300000; // 5 minutes in milliseconds + + if (cacheItem && cachedEndpoints === endpointsString) { + try { + const cachedData = JSON.parse(cacheItem); + if (now - cachedData.timestamp < TTL) { + // Restore cached modelRegistry + Object.assign(modelRegistry, cachedData.modelRegistry); + return cachedData.models; + } + } catch (e) { + console.error("Error reading model registry from cache", e); + } + } + + // If no valid cache, fetch models from all endpoints + const fetchPromises = VLLM_ENDPOINTS.map(async (endpoint) => { + try { + const res = await fetch(`${endpoint.url}/models`); + if (!res.ok) throw new Error(`HTTP error! status: ${res.status} from ${endpoint.url}`); + const jsonResponse = await res.json(); + // Expected JSON structure: { data: [ { id, ... }, ... ], object: "list" } + const models = jsonResponse.data || []; + // Map each model to include its endpoint ID, unique ID, and model name + return models.map((model) => ({ + ...model, + modelName: model.id, // Explicitly store model name + endpointId: endpoint.id, + uniqueId: `${endpoint.id}-${model.id}` // Unique ID with endpoint ID first + })); + } catch (error) { + console.error(`Error fetching models from ${endpoint.url}:`, error); + return []; + } + }); + const allModelsArrays = await Promise.all(fetchPromises); + const models = allModelsArrays.flat(); + + // Update modelRegistry with unique model instances + models.forEach((model) => { + modelRegistry[model.uniqueId] = { + url: VLLM_ENDPOINTS.find((e) => e.id === model.endpointId).url, + endpointId: model.endpointId + }; + }); + + // Cache the results + const cacheData = { + timestamp: now, + modelRegistry: modelRegistry, + models: models + }; + localStorage.setItem(cacheKey, JSON.stringify(cacheData)); + localStorage.setItem(endpointsKey, endpointsString); + + return models; +} + +// Create a dynamic drop-down for model selection +// This function creates a + + + `; + + // 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 uploadBtn = createButton('πŸ“ Upload File', () => document.querySelector('[data-uncloseai-file-input]')?.click()); + + controlsDiv.appendChild(readBtn); + controlsDiv.appendChild(ttsBtn); + 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 = async (e) => { + if (e.target.files[0]) { + try { + showProgressIndicator('Processing file...'); + const response = await uploadFile(e.target.files[0]); + await integrateMegafarceResponse(response); + hideProgressIndicator(); + e.target.value = ''; + } catch (error) { + hideProgressIndicator(); + alert('File upload failed: ' + error.message); + } + } + }; + + container.appendChild(controlsDiv); + container.appendChild(chatContainer); + container.appendChild(fileInput); +} + +// Create custom interface based on specific features +function createCustomInterface(container, features) { + features.forEach(feature => { + const featureDiv = document.createElement('div'); + featureDiv.style.margin = '10px 0'; + + switch (feature.trim()) { + case 'chat': + createChatFeature(featureDiv); + break; + case 'tts': + createTTSFeature(featureDiv); + break; + case 'upload': + createUploadFeature(featureDiv); + break; + case 'read': + createReadFeature(featureDiv); + break; + } + + container.appendChild(featureDiv); + }); +} + +// Individual feature creators +function createChatFeature(container) { + container.innerHTML = ` +
+

AI Chat

+
+
+ + +
+
+ `; +} + +function createTTSFeature(container) { + container.innerHTML = ` +
+

Text to Speech

+ + +
+
+ `; +} + +function createUploadFeature(container) { + container.innerHTML = ` +
+

File Upload & Analysis

+ + +
+
+ `; +} + +function createReadFeature(container) { + container.innerHTML = ` +
+

Page Reading

+

Let AI read and analyze the current page content.

+ +
+ `; +} + +// Helper functions for custom features +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; +} + +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 = 'Converting to speech...'; + + try { + const result = await speakText(text, 'alloy', 0.9); + resultDiv.innerHTML = ` +
+ + +
+ `; + resultDiv.insertBefore(result.audio, resultDiv.firstChild); + } catch (error) { + resultDiv.innerHTML = 'Error: ' + error.message; + } finally { + button.disabled = false; + button.textContent = 'πŸ”Š Convert to Speech'; + } +} + +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 = 'Uploading and analyzing file...'; + + try { + showProgressIndicator('Processing file...'); + const response = await uploadFile(fileInput.files[0]); + hideProgressIndicator(); + + resultDiv.innerHTML = `Analysis Result:
${response}`; + fileInput.value = ''; + } catch (error) { + hideProgressIndicator(); + resultDiv.innerHTML = 'Error: ' + error.message; + } finally { + button.disabled = false; + button.textContent = 'πŸ“ Upload & Analyze'; + } +} + +// Initialize the legacy chat interface (for backward compatibility) +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(); + } + }); +} + + +// Function to create floating AI button +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 = () => toggleHermesModal(); + + document.body.appendChild(floatingButton); + console.log('uncloseai.js: Appended floating button to document.body.'); +} + +// Global variable to track modal state +let hermesModalOpen = false; + +// Function to toggle Hermes modal +function toggleHermesModal() { + const existingModal = document.getElementById('hermes-modal'); + if (existingModal) { + document.body.removeChild(existingModal); + hermesModalOpen = false; + } else { + openHermesModal(); + hermesModalOpen = true; + } +} + +// Function to open Hermes modal +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 hermes 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; + `; + } + + // Copy options from existing model dropdown + const existingDropdown = document.getElementById('model-selection'); + if (existingDropdown) { + Array.from(existingDropdown.options).forEach(option => { + const newOption = document.createElement('option'); + newOption.value = option.value; + newOption.textContent = option.textContent; + newOption.selected = option.selected; + modelSelect.appendChild(newOption); + }); + } + + // Restore saved model selection + const savedModel = localStorage.getItem('hermes-selected-model'); + if (savedModel && modelSelect.querySelector(`option[value="${savedModel}"]`)) { + modelSelect.value = savedModel; + } + + // 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 both dropdowns + [modelSelect, document.getElementById('model-selection')].forEach(dropdown => { + if (dropdown) { + dropdown.innerHTML = ''; + models.forEach(model => { + const option = document.createElement('option'); + option.value = model.uniqueId; + option.textContent = `${model.endpointId} | ${model.modelName}`; + dropdown.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); + + // Restore conversation history in modal + function restoreConversationHistory() { + const history = loadConversationHistory(); + chatBox.innerHTML = ''; + history.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('hermes-conversation-history', JSON.stringify(savedHistory)); + // Update global chat history + chatHistory = [ + { + role: "system", + content: "You are Hermes, a large language model from Nous Research, embedded as an AI assistant on this webpage. Your primary task is to help users understand and interact with the content of this specific page, while also being capable of assisting with any other topics, coding, creative tasks, or questions they may have. You should be conversational, helpful, and knowledgeable about the webpage content as well as general topics. Always strive to provide accurate, useful responses." + }, + ...savedHistory + ]; + restoreConversationHistory(); + } + } + }; + })(index); + + if (msg.role === 'user') { + messageDiv.innerHTML = `

You: ${msg.content}

`; + } else if (msg.role === 'assistant') { + const parsedContent = marked.parse(msg.content); + messageDiv.innerHTML = `

AI: ${parsedContent}

`; + } + + 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(); + 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 = '

πŸ€– Hermes: ✨ Analyzing this page and crafting a personalized introduction... This may take a moment.

'; + 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 sendMessageDirect(prompt)) { + generatedIntro += chunk; + // Update display in real-time + introDiv.innerHTML = `

πŸ€– Hermes: ${generatedIntro}

`; + // Scroll to bottom as content updates + chatBox.scrollTop = chatBox.scrollHeight; + } + + // Add to chat history and save + chatHistory.push({ role: "assistant", content: generatedIntro }); + saveConversationHistory(); + + // Cache the generated introduction + localStorage.setItem(cacheKey, generatedIntro); + + // Add TTS button for introduction + addTTSButtonToIntro(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 = `

πŸ€– Hermes: ${fallbackIntro}

`; + // Add fallback to chat history too + chatHistory.push({ role: "assistant", content: fallbackIntro }); + saveConversationHistory(); + } + } + } + + 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 = `

πŸ€– Hermes: ${introText}

`; + chatBox.appendChild(introDiv); + + // Add TTS button for cached introduction + addTTSButtonToIntro(introDiv, introText); + + chatBox.scrollTop = chatBox.scrollHeight; + } + + // Add TTS button to introduction message + function addTTSButtonToIntro(introDiv, introText) { + const ttsIntroContainer = document.createElement('div'); + ttsIntroContainer.style.cssText = ` + display: grid; + grid-template-columns: auto auto auto; + gap: 8px; + margin: 10px 0; + align-items: center; + `; + + const ttsIntroBtn = document.createElement('button'); + ttsIntroBtn.textContent = 'πŸ”Š Play Response'; + if (USE_CUSTOM_STYLING) { + ttsIntroBtn.style.cssText = 'padding: 6px 12px; border: 1px solid #0066cc; border-radius: 4px; background: #f0f8ff; cursor: pointer; color: #0066cc;'; + } else { + ttsIntroBtn.style.cssText = 'padding: 6px 12px; border-radius: 4px; cursor: pointer;'; + } + + const regenerateIntroBtn = document.createElement('button'); + regenerateIntroBtn.textContent = 'πŸ”„ Regenerate'; + regenerateIntroBtn.style.display = 'none'; + if (USE_CUSTOM_STYLING) { + regenerateIntroBtn.style.cssText += 'padding: 6px 12px; border: 1px solid #0066cc; border-radius: 4px; background: #f0f8ff; cursor: pointer; color: #0066cc;'; + } else { + regenerateIntroBtn.style.cssText += 'padding: 6px 12px; border-radius: 4px; cursor: pointer;'; + } + + const downloadIntroBtn = document.createElement('button'); + downloadIntroBtn.textContent = 'πŸ’Ύ Download'; + downloadIntroBtn.style.display = 'none'; + if (USE_CUSTOM_STYLING) { + downloadIntroBtn.style.cssText += 'padding: 6px 12px; border: 1px solid #0066cc; border-radius: 4px; background: #f0f8ff; cursor: pointer; color: #0066cc;'; + } else { + downloadIntroBtn.style.cssText += 'padding: 6px 12px; border-radius: 4px; cursor: pointer;'; + } + + let introAudio = null; + let introBlob = null; + let currentIntroVoice = voiceSelect.value; + + // Function to generate/regenerate intro TTS + const generateIntroTTS = async () => { + ttsIntroBtn.textContent = 'Processing...'; + ttsIntroBtn.disabled = true; + regenerateIntroBtn.disabled = true; + + const selectedVoice = voiceSelect.value; + currentIntroVoice = selectedVoice; + const result = await speakText(introText, selectedVoice, 0.9); + introAudio = result.audio; + introBlob = result.blob; + + ttsIntroBtn.textContent = 'Pause'; + ttsIntroBtn.disabled = false; + regenerateIntroBtn.disabled = false; + regenerateIntroBtn.style.display = 'inline-block'; + downloadIntroBtn.style.display = 'inline-block'; + + introAudio.play(); + }; + + ttsIntroBtn.onclick = async () => { + if (!introAudio) { + await generateIntroTTS(); + } else { + if (introAudio.paused) { + introAudio.play(); + ttsIntroBtn.textContent = 'Pause'; + } else { + introAudio.pause(); + ttsIntroBtn.textContent = 'Play'; + } + } + }; + + regenerateIntroBtn.onclick = async () => { + if (introAudio) { + introAudio.pause(); + } + introAudio = null; + introBlob = null; + await generateIntroTTS(); + }; + + downloadIntroBtn.onclick = () => { + if (introBlob) { + const a = document.createElement('a'); + a.href = URL.createObjectURL(introBlob); + a.download = `hermes-introduction-${Date.now()}.mp3`; + a.click(); + } + }; + + // Watch for voice changes + voiceSelect.addEventListener('change', () => { + if (introAudio && currentIntroVoice !== voiceSelect.value) { + regenerateIntroBtn.style.display = 'inline-block'; + regenerateIntroBtn.style.background = '#fffacd'; // Light yellow to indicate change needed + } + }); + + ttsIntroContainer.appendChild(ttsIntroBtn); + ttsIntroContainer.appendChild(regenerateIntroBtn); + ttsIntroContainer.appendChild(downloadIntroBtn); + introDiv.appendChild(ttsIntroContainer); + } + + // Direct LLM call for introduction generation (doesn't add to chat history) + async function* sendMessageDirect(message) { + const selectedModel = modelSelect.value || MODEL; + const modelInfo = modelRegistry[selectedModel]; + const endpoint = modelInfo ? modelInfo.url : VLLM_ENDPOINTS[0].url; + const modelName = modelInfo ? modelInfo.modelName : MODEL; + + const response = await fetch(`${endpoint}/chat/completions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + model: modelName, + messages: [{ role: "user", content: message }], + temperature: 0.7, + max_tokens: 300, + stream: true + }), + }); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + + for (let i = 0; i < lines.length - 1; i++) { + const line = lines[i].trim(); + if (line.startsWith('data: ')) { + const jsonData = line.slice(6); + if (jsonData === '[DONE]') continue; + + try { + const parsedData = JSON.parse(jsonData); + const content = parsedData.choices[0].delta.content; + if (content) { + yield content; + } + } catch (error) { + console.error('Error parsing JSON:', error); + } + } + } + buffer = lines[lines.length - 1]; + } + } + // Call async introduction function + addHermesIntroduction().catch(error => { + console.error('Failed to add Hermes introduction:', error); + }); + + // Create input area + const inputArea = document.createElement('div'); + if (USE_CUSTOM_STYLING) { + inputArea.style.cssText = ` + padding: 16px 20px; + display: flex; + gap: 12px; + align-items: center; + `; + } else { + inputArea.style.cssText = ` + padding: 1rem; + display: flex; + gap: 0.5rem; + align-items: center; + `; + } + + const messageInput = document.createElement('input'); + messageInput.type = 'text'; + messageInput.id = 'modal-user-input'; + messageInput.placeholder = 'πŸ’¬ Chat with Hermes about this page, code, ideas, or anything! Your AI companion awaits...'; + messageInput.style.cssText = ` + flex: 1; + padding: 12px; + border: 1px solid #ccc; + border-radius: 8px; + font-size: 14px; + `; + + const sendButton = document.createElement('button'); + sendButton.textContent = 'Send'; + +// Expose functions to the global scope for direct HTML calls +window.handleUserInput = handleUserInput; +window.readPageWithHermes = readPageWithHermes; +window.openTTSModal = openTTSModal; +window.handleTTSFromElement = handleTTSFromElement; +window.handleUploadFromElement = handleUploadFromElement; +window.uploadFile = uploadFile; +window.showProgressIndicator = showProgressIndicator; +window.hideProgressIndicator = hideProgressIndicator; +window.sendMessage = sendMessage; +window.speakText = speakText; + 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; + `; + } + + // File upload + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.id = 'modal-file-input'; + fileInput.style.display = 'none'; + + const fileButton = document.createElement('button'); + fileButton.textContent = 'πŸ“'; + fileButton.style.cssText = ` + padding: 12px; + background: #f5f5f5; + border: 1px solid #ccc; + border-radius: 8px; + cursor: pointer; + `; + fileButton.onclick = () => fileInput.click(); + + // Handle file upload + fileInput.onchange = async () => { + if (fileInput.files.length > 0) { + const file = fileInput.files[0]; + try { + showProgressIndicator('Uploading & Processing file...'); + const response = await uploadFile(file); + await integrateMegafarceResponseToModal(response, chatBox); + hideProgressIndicator(); + fileInput.value = ''; + } catch (error) { + console.error('File upload error:', error); + alert('Failed to upload the file.'); + hideProgressIndicator(); + } + } + }; + + // Handle send message + const handleModalInput = async () => { + const userInput = messageInput.value.trim(); + if (!userInput) return; + + messageInput.value = ''; + + // Create user message with delete functionality + const userMessageDiv = document.createElement('div'); + userMessageDiv.style.cssText = 'position: relative; margin-bottom: 10px; padding: 5px; border-radius: 5px; background: rgba(0,0,0,0.05);'; + userMessageDiv.innerHTML = `

You: ${userInput}

`; + chatBox.appendChild(userMessageDiv); + + // Scroll to bottom when user message is added + chatBox.scrollTop = chatBox.scrollHeight; + + // Create AI response container + const aiMessageDiv = document.createElement('div'); + aiMessageDiv.style.cssText = 'position: relative; margin-bottom: 10px; padding: 5px; border-radius: 5px; background: rgba(0,0,0,0.05);'; + const aiResponseParagraph = document.createElement('p'); + aiResponseParagraph.innerHTML = 'AI: '; + const responseContent = document.createElement('span'); + aiResponseParagraph.appendChild(responseContent); + aiMessageDiv.appendChild(aiResponseParagraph); + chatBox.appendChild(aiMessageDiv); + + let accumulatedContent = ''; + for await (const chunk of sendMessage(userInput)) { + accumulatedContent += chunk; + const parsedChunk = marked.parse(accumulatedContent); + responseContent.innerHTML = parsedChunk; + + responseContent.querySelectorAll('pre code').forEach((block) => { + hljs.highlightElement(block); + }); + + // Auto-scroll to bottom as response streams in + chatBox.scrollTop = chatBox.scrollHeight; + } + + // Add assistant response to chat history and save + chatHistory.push({ role: "assistant", content: accumulatedContent }); + saveConversationHistory(); + + // Add delete buttons to the new messages + const userDeleteBtn = createStyledButton('Γ—', 'close', 'position: absolute; top: 2px; right: 2px; width: 20px; height: 20px; font-size: 12px; background: rgba(255,0,0,0.7); color: white; border-radius: 4px;'); + userDeleteBtn.onclick = () => { + if (confirm('Delete this conversation pair?')) { + const currentHistory = loadConversationHistory(); + // Remove the last 2 messages (user + AI response) + if (currentHistory.length >= 2) { + currentHistory.splice(-2, 2); + localStorage.setItem('hermes-conversation-history', JSON.stringify(currentHistory)); + chatHistory = [ + { + role: "system", + content: "You are Hermes, a large language model from Nous Research, embedded as an AI assistant on this webpage. Your primary task is to help users understand and interact with the content of this specific page, while also being capable of assisting with any other topics, coding, creative tasks, or questions they may have. You should be conversational, helpful, and knowledgeable about the webpage content as well as general topics. Always strive to provide accurate, useful responses." + }, + ...currentHistory + ]; + restoreConversationHistory(); + } + } + }; + + const aiDeleteBtn = createStyledButton('Γ—', 'close', 'position: absolute; top: 2px; right: 2px; width: 20px; height: 20px; font-size: 12px; background: rgba(255,0,0,0.7); color: white; border-radius: 4px;'); + aiDeleteBtn.onclick = userDeleteBtn.onclick; // Same functionality - delete the pair + + userMessageDiv.appendChild(userDeleteBtn); + aiMessageDiv.appendChild(aiDeleteBtn); + + chatBox.scrollTop = chatBox.scrollHeight; + + // Add TTS button container with grid layout + const ttsContainer = document.createElement('div'); + ttsContainer.style.cssText = ` + display: grid; + grid-template-columns: auto auto auto; + gap: 8px; + margin: 10px 0; + align-items: center; + `; + + const ttsResponseBtn = document.createElement('button'); + ttsResponseBtn.textContent = 'πŸ”Š Play Response'; + if (USE_CUSTOM_STYLING) { + ttsResponseBtn.style.cssText = 'padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;'; + } + + const regenerateBtn = document.createElement('button'); + regenerateBtn.textContent = 'πŸ”„ Regenerate'; + regenerateBtn.style.display = 'none'; + if (USE_CUSTOM_STYLING) { + regenerateBtn.style.cssText += 'padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;'; + } + + const downloadBtn = document.createElement('button'); + downloadBtn.textContent = 'πŸ’Ύ Download'; + downloadBtn.style.display = 'none'; + if (USE_CUSTOM_STYLING) { + downloadBtn.style.cssText += 'padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;'; + } + + let responseAudio = null; + let responseBlob = null; + let currentVoice = voiceSelect.value; + + // Function to generate/regenerate TTS + const generateTTS = async () => { + ttsResponseBtn.textContent = 'Processing...'; + ttsResponseBtn.disabled = true; + regenerateBtn.disabled = true; + + const selectedVoice = voiceSelect.value; + currentVoice = selectedVoice; + const result = await speakText(accumulatedContent, selectedVoice, 0.9); + responseAudio = result.audio; + responseBlob = result.blob; + + ttsResponseBtn.textContent = 'Pause'; + ttsResponseBtn.disabled = false; + regenerateBtn.disabled = false; + regenerateBtn.style.display = 'inline-block'; + downloadBtn.style.display = 'inline-block'; + + responseAudio.play(); + }; + + ttsResponseBtn.onclick = async () => { + if (!responseAudio) { + await generateTTS(); + } else { + if (responseAudio.paused) { + responseAudio.play(); + ttsResponseBtn.textContent = 'Pause'; + } else { + responseAudio.pause(); + ttsResponseBtn.textContent = 'Play'; + } + } + }; + + regenerateBtn.onclick = async () => { + if (responseAudio) { + responseAudio.pause(); + } + responseAudio = null; + responseBlob = null; + await generateTTS(); + }; + + downloadBtn.onclick = () => { + if (responseBlob) { + const a = document.createElement('a'); + a.href = URL.createObjectURL(responseBlob); + a.download = `hermes-response-${Date.now()}.mp3`; + a.click(); + } + }; + + // Watch for voice changes + voiceSelect.addEventListener('change', () => { + if (responseAudio && currentVoice !== voiceSelect.value) { + regenerateBtn.style.display = 'inline-block'; + regenerateBtn.style.background = '#fffacd'; // Light yellow to indicate change needed + } + }); + + ttsContainer.appendChild(ttsResponseBtn); + ttsContainer.appendChild(regenerateBtn); + ttsContainer.appendChild(downloadBtn); + chatBox.appendChild(ttsContainer); + }; + + sendButton.onclick = handleModalInput; + messageInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleModalInput(); + } + }); + + inputArea.appendChild(fileButton); + inputArea.appendChild(fileInput); + inputArea.appendChild(messageInput); + inputArea.appendChild(sendButton); + + // Assemble modal + article.appendChild(header); + article.appendChild(controls); + article.appendChild(chatArea); + article.appendChild(inputArea); + + document.body.appendChild(modal); + messageInput.focus(); +} + +// Configuration for styling - set to false to disable custom styling +const USE_CUSTOM_STYLING = window.UNCLOSEAI_CUSTOM_STYLING !== false; + +// ------------------------- +// Shared Utility Functions for DRY Code +// ------------------------- + +// Create standardized button with consistent styling +function createStyledButton(text, type = 'primary', additionalStyles = '') { + const button = document.createElement('button'); + button.textContent = text; + + const baseStyles = 'padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px;'; + + let typeStyles = ''; + switch (type) { + case 'primary': + typeStyles = USE_CUSTOM_STYLING + ? 'background: #007bff; color: white;' + : 'background: var(--primary, #007bff); color: white;'; + break; + case 'success': + typeStyles = USE_CUSTOM_STYLING + ? 'background: #28a745; color: white;' + : 'background: var(--success, #28a745); color: white;'; + break; + case 'warning': + typeStyles = USE_CUSTOM_STYLING + ? 'background: #ffc107; color: black;' + : 'background: var(--warning, #ffc107); color: black;'; + break; + case 'info': + typeStyles = USE_CUSTOM_STYLING + ? 'background: #17a2b8; color: white;' + : 'background: var(--info, #17a2b8); color: white;'; + break; + case 'close': + typeStyles = USE_CUSTOM_STYLING + ? 'background: none; border: none; color: currentColor; font-size: 24px; padding: 4px 8px;' + : 'float: right; background: none; border: none; font-size: 1.2em; padding: 4px 8px; border-radius: 4px;'; + break; + default: + typeStyles = USE_CUSTOM_STYLING + ? 'background: #6c757d; color: white;' + : ''; + } + + button.style.cssText = baseStyles + typeStyles + additionalStyles; + return button; +} + +// Create standardized chat message element +function createChatMessage(content, sender = 'user', isHTML = false) { + const messageDiv = document.createElement('div'); + const bgColor = sender === 'user' ? '#e3f2fd' : '#f3e5f5'; + const label = sender === 'user' ? 'You' : 'AI'; + + messageDiv.style.cssText = `margin-bottom: 10px; padding: 8px; background: ${bgColor}; border-radius: 4px;`; + + if (isHTML) { + messageDiv.innerHTML = `${label}: ${content}`; + } else { + messageDiv.innerHTML = `${label}: ${content}`; + } + + return messageDiv; +} + +// Create standardized TTS control set (play/pause, regenerate, download) +function createTTSControls(textContent, voiceSelectElement, onAudioGenerated = null) { + const controlContainer = document.createElement('div'); + controlContainer.style.cssText = 'display: grid; grid-template-columns: auto auto auto; gap: 8px; margin: 10px 0; align-items: center;'; + + const playBtn = createStyledButton('πŸ”Š Play', 'primary'); + const regenerateBtn = createStyledButton('πŸ”„ Regenerate', 'secondary'); + const downloadBtn = createStyledButton('πŸ’Ύ Download', 'secondary'); + + regenerateBtn.style.display = 'none'; + downloadBtn.style.display = 'none'; + + let audio = null; + let audioBlob = null; + let currentVoice = voiceSelectElement.value; + + const generateTTS = async () => { + playBtn.textContent = 'Processing...'; + playBtn.disabled = true; + regenerateBtn.disabled = true; + + const selectedVoice = voiceSelectElement.value; + currentVoice = selectedVoice; + const result = await speakText(textContent, selectedVoice, 0.9); + audio = result.audio; + audioBlob = result.blob; + + playBtn.textContent = 'Pause'; + playBtn.disabled = false; + regenerateBtn.disabled = false; + regenerateBtn.style.display = 'inline-block'; + downloadBtn.style.display = 'inline-block'; + + if (onAudioGenerated) onAudioGenerated(audio, audioBlob); + audio.play(); + }; + + playBtn.onclick = async () => { + if (!audio) { + await generateTTS(); + } else { + if (audio.paused) { + audio.play(); + playBtn.textContent = 'Pause'; + } else { + audio.pause(); + playBtn.textContent = 'Play'; + } + } + }; + + regenerateBtn.onclick = async () => { + if (audio) audio.pause(); + audio = null; + audioBlob = null; + await generateTTS(); + }; + + downloadBtn.onclick = () => { + if (audioBlob) { + const a = document.createElement('a'); + a.href = URL.createObjectURL(audioBlob); + a.download = `tts-audio-${Date.now()}.mp3`; + a.click(); + } + }; + + // Watch for voice changes + voiceSelectElement.addEventListener('change', () => { + if (audio && currentVoice !== voiceSelectElement.value) { + regenerateBtn.style.display = 'inline-block'; + regenerateBtn.style.background = '#fffacd'; + } + }); + + controlContainer.appendChild(playBtn); + controlContainer.appendChild(regenerateBtn); + controlContainer.appendChild(downloadBtn); + + return { container: controlContainer, playBtn, regenerateBtn, downloadBtn }; +} + +// Create standardized voice selection dropdown +function createVoiceSelect(id = '', selectedVoice = 'alloy') { + const select = document.createElement('select'); + if (id) select.id = id; + + const voices = ['alloy', 'echo', 'fable', 'onyx', 'nova', 'shimmer']; + voices.forEach(voice => { + const option = document.createElement('option'); + option.value = voice; + option.textContent = voice.charAt(0).toUpperCase() + voice.slice(1); + if (voice === selectedVoice) option.selected = true; + select.appendChild(option); + }); + + return select; +} + +// Create standardized modal header with close button +function createModalHeader(title, onClose) { + 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; + `; + } + + const h1 = document.createElement('h1'); + h1.textContent = title; + if (USE_CUSTOM_STYLING) { + h1.style.cssText = 'margin: 0; font-family: "ChunkFiveRegular", monospace; font-size: 20px;'; + } + + const closeButton = createStyledButton('X', 'close'); + closeButton.onclick = onClose; + + header.appendChild(h1); + header.appendChild(closeButton); + + return header; +} + +// Function to open TTS modal +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 = createStyledButton('X', 'close'); + 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; + + 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; + + // 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); +} + +// Helper function to integrate file upload response to modal +async function integrateMegafarceResponseToModal(response, chatBox) { + const content = response.content || response.result || "No content received."; + chatHistory.push({ role: "system", content: `Uploaded Context: ${content}` }); + + const systemMessage = document.createElement('p'); + systemMessage.innerHTML = `System: ${marked.parse(content)}`; + + systemMessage.querySelectorAll('pre code').forEach((block) => { + hljs.highlightElement(block); + }); + + chatBox.appendChild(systemMessage); + chatBox.scrollTop = chatBox.scrollHeight; +} + +// Helper function to scroll to chat box +function scrollToChatBox() { + const chatContainer = document.getElementById('chat-container'); + if (chatContainer) { + chatContainer.scrollIntoView({ behavior: 'smooth' }); + document.getElementById('user-input')?.focus(); + } +} + +// ------------------------- +// Class-based Initialization System +// ------------------------- + + + +// Configuration flag for showing floating button +const SHOW_FLOATING_BUTTON = window.UNCLOSEAI_SHOW_BUTTON !== false; + +// Initialize on page load +window.onload = () => { + console.log('uncloseai.js: window.onload event fired.'); + initializeChatInterface(); + createModelSelectionDropdown(); + addRefreshModelsButton(); + + // 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(); +}; + +// Export functions to global scope +window.handleUserInput = handleUserInput; +window.readPage = readPageWithHermes;