uncloseai.com/uncloseai.js.orig
Russell Ballestrini 01df4d1e77 new file: src/chat.js
new file:   src/config.js
	new file:   src/content.js
	new file:   src/file-upload.js
	new file:   src/models.js
	new file:   src/page-reader.js
	new file:   src/storage.js
	new file:   src/tts.js
	new file:   src/ui.js
	modified:   uncloseai.js
	new file:   uncloseai.js.orig
2025-06-30 20:28:42 -04:00

2711 lines
90 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

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

/* how to use in HTML */
/*
<!-- Highlight.js for syntax highlighting -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.10.0/styles/a11y-dark.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.6.0/highlight.min.js"></script>
<div id="chat-container">
<div id="chat-box"></div>
<div></div>
</div>
<div>
<input type="text" id="user-input" placeholder="Ask about this page...">
<button onclick="handleUserInput()">Send</button>
</div>
*/
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 <select> element and fills it with unique model instances.
async function createModelSelectionDropdown() {
const models = await fetchModelsFromEndpoints();
if (!models.length) {
console.warn("No models returned from any endpoint.");
return;
}
const dropdown = document.createElement('select');
dropdown.id = 'model-selection';
dropdown.style.margin = '10px';
models.forEach((model) => {
const option = document.createElement('option');
option.value = model.uniqueId; // Use unique ID for selection
option.textContent = `${model.endpointId} | ${model.modelName}`;
dropdown.appendChild(option);
});
// Insert the dropdown directly above the chat input box.
const userInput = document.getElementById("user-input");
if (userInput && userInput.parentNode) {
userInput.parentNode.parentNode.insertBefore(dropdown, userInput.parentNode);
} else {
// Fallback: insert at the top of the body.
document.body.insertBefore(dropdown, document.body.firstChild);
}
}
// Function to add the Refresh Models button
function addRefreshModelsButton() {
// Create a container for the button
const container = document.createElement('div');
container.id = 'refresh-models-container';
// Position it near the model selection dropdown
const userInput = document.getElementById("user-input");
if (userInput && userInput.parentNode) {
userInput.parentNode.parentNode.insertBefore(container, userInput.parentNode);
}
// Create a button element
const refreshButton = document.createElement('button');
refreshButton.textContent = 'Refresh Models';
refreshButton.style.margin = '10px';
refreshButton.onclick = async function() {
refreshButton.textContent = 'Refreshing...';
refreshButton.disabled = true;
try {
// Clear the model registry cache
localStorage.removeItem('modelRegistryCache');
localStorage.removeItem('vllmEndpointsHash');
// Fetch models again
const models = await fetchModelsFromEndpoints();
// Update the dropdown
const dropdown = document.getElementById('model-selection');
if (dropdown) {
// Clear existing options
dropdown.innerHTML = '';
// Add options using models array
models.forEach((model) => {
const option = document.createElement('option');
option.value = model.uniqueId;
option.textContent = `${model.endpointId} | ${model.modelName}`;
dropdown.appendChild(option);
});
}
alert('Models refreshed successfully!');
} catch (error) {
console.error('Error refreshing models:', error);
alert('Failed to refresh models. Please try again.');
} finally {
refreshButton.textContent = 'Refresh Models';
refreshButton.disabled = false;
}
};
container.appendChild(refreshButton);
}
// NEW: Helper function to get the selected model from the dropdown.
function getSelectedModel() {
// Check modal dropdown first
const modalDropdown = document.getElementById('modal-model-selection');
const dropdown = modalDropdown || document.getElementById('model-selection');
if (!dropdown) return MODEL;
const selectedUniqueId = dropdown.value;
const cacheItem = localStorage.getItem('modelRegistryCache');
if (cacheItem) {
try {
const cachedData = JSON.parse(cacheItem);
const selectedModel = cachedData.models.find(model => model.uniqueId === selectedUniqueId);
if (selectedModel) return selectedModel.modelName;
} catch (e) {
console.error("Error reading model from cache", e);
}
}
// Fallback: parse uniqueId to extract modelName
return selectedUniqueId.split('-').slice(1).join('-') || MODEL;
}
// Utility to determine which endpoint to use based on the selected model
function getSelectedModelEndpoint() {
// Check modal dropdown first
const modalDropdown = document.getElementById('modal-model-selection');
const dropdown = modalDropdown || document.getElementById('model-selection');
if (!dropdown) {
console.warn("Model selection dropdown not found, falling back to default endpoint.");
return VLLM_ENDPOINTS[0].url;
}
const selectedModelUniqueId = dropdown.value;
return modelRegistry[selectedModelUniqueId]?.url || VLLM_ENDPOINTS[0].url;
}
// -------------------------
// Existing Functionality
// -------------------------
// Function to extract text content along with links and metadata from the webpage
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();
}
// Function to read text using TTS
async function speakText(text, voice = 'alloy', rate = 0.9) {
try {
// Preprocess the text using the new function to get spoken tokens
const spokenText = await extractSpokenTokens(text);
const response = await fetch(TTS_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
model: 'tts-1',
voice: voice,
input: spokenText
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const audioBlob = await response.blob();
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
audio.playbackRate = rate;
return { audio, blob: audioBlob };
} catch (error) {
console.error('Error in TTS:', error);
throw error;
}
}
// process page content using Hermes
async function processContentWithHermes(content) {
const payload = {
model: getSelectedModel(),
messages: [
{
role: "system",
content: `Extract the exact main content of the article.
Avoid reading ads.
Do not change or summarize content. Return the same words verbatim.
When you encounter a list or bullets, add . . . to make the TTS pause between items.
Try to stay as close to the truth of the original version as possible. Start with the title of the post and then jump into it.
Do not mention the TTS stream just do the work!
Remember your only goal is to extract the entire & exact main content of the article.
Ready? Breath and then return a stream of tokens to be used in a TTS system.
`
},
{
role: "user",
content: content
}
],
temperature: 0,
max_tokens: 76000
};
console.log("Sending payload to LLM for spoken tokens:", JSON.stringify(payload, null, 2));
try {
const response = await fetch(`${getSelectedModelEndpoint()}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorText = await response.text();
console.error(`HTTP error! status: ${response.status}`, errorText);
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
console.error('Error in processContentWithHermes:', error);
throw error;
}
}
async function extractSpokenTokens(content) {
const payload = {
model: MODEL,
messages: [
{
role: "system",
content: `Extract the spoken tokens from the content without altering or summarizing it. Ensure that the spoken tokens match the content displayed on the screen. Skip any embedded advertisements. Remove asterisks, markdown symbols, and other characters that should not be spoken, such as **, *, #, or []. Preserve the original text's meaning and structure.`
},
{
role: "user",
content: content
}
],
temperature: 0,
max_tokens: 76000
};
console.log("Sending payload to Hermes for spoken tokens:", JSON.stringify(payload, null, 2));
try {
const response = await fetch(`${getSelectedModelEndpoint()}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorText = await response.text();
console.error(`HTTP error! status: ${response.status}`, errorText);
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
console.error('Error in extractSpokenTokens:', error);
throw error;
}
}
// Function to generate a title for TTS
async function generateTitleForTTS(text) {
const response = await fetch(`${getSelectedModelEndpoint()}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({
model: MODEL,
messages: [
{
role: "system",
content: "Generate a concise title for the following text."
},
{
role: "user",
content: text
}
],
temperature: 0.5,
max_tokens: 60
})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data.choices[0].message.content.trim().replace(/\s+/g, '-').toLowerCase();
}
// Function to read the entire page using Hermes
async function readPageWithHermes() {
const content = extractWebpageContent();
// Create status indicator
const statusDiv = document.createElement('div');
statusDiv.style.position = 'fixed';
statusDiv.style.top = '10px';
statusDiv.style.right = '10px';
statusDiv.style.padding = '10px';
statusDiv.style.background = 'rgba(0,0,0,0.8)';
statusDiv.style.color = 'white';
statusDiv.style.borderRadius = '5px';
statusDiv.style.zIndex = '1000';
document.body.appendChild(statusDiv);
statusDiv.textContent = 'Processing content and generating speech...';
const processedContent = await processContentWithHermes(content);
// Generate TTS with default voice and 90% speed immediately
const { audio, blob } = await speakText(processedContent, 'alloy', 0.9);
statusDiv.textContent = 'Reading page... ';
let isPaused = false;
// Add pause/resume button
const pauseButton = document.createElement('button');
pauseButton.textContent = 'Pause Reading';
pauseButton.style.marginLeft = '10px';
statusDiv.appendChild(pauseButton);
// Add download button
const downloadButton = document.createElement('button');
downloadButton.textContent = 'Download MP3';
downloadButton.style.marginLeft = '10px';
downloadButton.onclick = () => {
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `${document.title.replace(/\s+/g, '-').toLowerCase()}.mp3`;
a.click();
};
statusDiv.appendChild(downloadButton);
pauseButton.onclick = () => {
if (isPaused) {
audio.play();
pauseButton.textContent = 'Pause Reading';
} else {
audio.pause();
pauseButton.textContent = 'Resume Reading';
}
isPaused = !isPaused;
};
// Set up audio end handler
audio.onended = () => {
statusDiv.remove();
};
// Start playing immediately
audio.play();
}
// Generator function to send a message to the LLM and yield responses
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
async function handleUserInput() {
const userInput = document.getElementById('user-input').value;
document.getElementById('user-input').value = '';
const chatBox = document.getElementById('chat-box');
chatBox.innerHTML += `<p><strong>You:</strong> ${userInput}</p>`;
const aiResponseParagraph = document.createElement('p');
aiResponseParagraph.innerHTML = '<strong>AI:</strong> ';
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 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);
}
// Add button to page
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';
// Add main voice selection dropdown for AI responses
const voiceSelect = document.createElement('select');
voiceSelect.id = 'read-page-voice';
voiceSelect.style.margin = '10px';
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);
});
const voiceLabel = document.createElement('label');
voiceLabel.textContent = 'Voice: ';
voiceLabel.style.margin = '10px';
voiceLabel.appendChild(voiceSelect);
// Insert before the chat container
const chatContainer = document.getElementById('chat-container');
chatContainer.parentNode.insertBefore(voiceLabel, chatContainer);
chatContainer.parentNode.insertBefore(button, chatContainer);
chatContainer.parentNode.insertBefore(ttsButton, chatContainer);
}
// New Function: Add File Picker and Upload Button
function addFileUploadButton() {
const userInputContainer = document.getElementById('user-input').parentNode;
// Create a container for the upload elements
const uploadContainer = document.createElement('div');
uploadContainer.style.margin = '10px 0';
// Create file input
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.id = 'file-input';
fileInput.style.marginRight = '10px';
// Create upload button
const uploadButton = document.createElement('button');
uploadButton.textContent = 'Upload File';
uploadButton.onclick = handleFileUpload;
// Append elements to the upload container
uploadContainer.appendChild(fileInput);
uploadContainer.appendChild(uploadButton);
// Append the upload container below the user input
userInputContainer.parentNode.insertBefore(uploadContainer, userInputContainer.nextSibling);
}
// Function to show progress indicator
function showProgressIndicator(message) {
let progressIndicator = document.getElementById('progress-indicator');
if (!progressIndicator) {
progressIndicator = document.createElement('div');
progressIndicator.id = 'progress-indicator';
progressIndicator.style.position = 'fixed';
progressIndicator.style.top = '10px';
progressIndicator.style.right = '10px';
progressIndicator.style.padding = '10px';
progressIndicator.style.background = 'rgba(0,0,0,0.8)';
progressIndicator.style.color = 'white';
progressIndicator.style.borderRadius = '5px';
progressIndicator.style.zIndex = '2002';
document.body.appendChild(progressIndicator);
}
progressIndicator.textContent = message;
}
// Function to hide progress indicator
function hideProgressIndicator() {
const progressIndicator = document.getElementById('progress-indicator');
if (progressIndicator) {
progressIndicator.remove();
}
}
// Updated Function: Handle File Upload with Progress Indicator
async function handleFileUpload() {
const fileInput = document.getElementById('file-input');
const files = fileInput.files;
if (files.length === 0) {
alert('Please select a file to upload.');
return;
}
const file = files[0];
try {
// Show progress indicator for upload
showProgressIndicator('Uploading & Processing file...');
// Send file to MegaFarce endpoint
const response = await uploadFile(file);
// Show progress indicator for processing
showProgressIndicator('Processing file...');
// Integrate response into Chat DOM and chatHistory
await integrateMegafarceResponse(response);
// Hide progress indicator after completion
hideProgressIndicator();
// Clear the file input after successful upload
fileInput.value = '';
} catch (error) {
console.error('File upload error:', error);
alert('Failed to upload the file.');
hideProgressIndicator();
}
}
// New Function: Upload File to MegaFarce with Progress Indicator
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;
}
// New Function: Integrate MegaFarce Response into Chat
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 = `<strong>System:</strong> ${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);
}
// Helper function to generate a page-specific key for localStorage
function getPageSpecificKey(baseKey) {
// Use the current page's URL to create a unique key
// Replace non-alphanumeric characters to make it a valid key
const pageIdentifier = window.location.href.replace(/[^a-zA-Z0-9]/g, '_');
return `${baseKey}-${pageIdentifier}`;
}
// Initialize chat history
// Functions for conversation history persistence
function saveConversationHistory() {
const historyToSave = chatHistory.filter(msg => msg.role !== 'system');
localStorage.setItem(getPageSpecificKey('hermes-conversation-history'), JSON.stringify(historyToSave));
}
function loadConversationHistory() {
const saved = localStorage.getItem(getPageSpecificKey('hermes-conversation-history'));
if (saved) {
try {
const parsedHistory = JSON.parse(saved);
return parsedHistory;
} catch (e) {
console.error('Error parsing conversation history:', e);
}
}
return [];
}
function clearConversationHistory() {
localStorage.removeItem(getPageSpecificKey('hermes-conversation-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."
}
];
}
let 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."
},
...loadConversationHistory()
];
// Initialize uncloseai elements based on class
function initializeUncloseaiElements() {
const uncloseaiElements = document.querySelectorAll('.uncloseai');
uncloseaiElements.forEach(element => {
const features = element.dataset.features || 'full';
const type = element.dataset.type || 'standard';
// Create container for this uncloseai instance
const container = document.createElement('div');
container.className = 'uncloseai-container';
container.style.cssText = 'width: 100%; margin: 10px 0;';
if (features === 'full' || type === 'full') {
createFullInterface(container);
} else {
createCustomInterface(container, features.split(','));
}
element.appendChild(container);
});
}
// Create full chat interface
function createFullInterface(container) {
// Chat area
const chatContainer = document.createElement('div');
chatContainer.innerHTML = `
<div id="chat-box" style="min-height: 200px; border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; overflow-y: auto; border-radius: 4px;"></div>
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
<input type="text" id="user-input" placeholder="Ask about this page..." style="flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px;">
<button onclick="handleUserInput()" style="padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">Send</button>
</div>
`;
// Control buttons
const controlsDiv = document.createElement('div');
controlsDiv.style.cssText = 'display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; margin-bottom: 10px;';
const readBtn = createButton('📖 Read Page', () => readPageWithHermes());
const ttsBtn = createButton('🔊 TTS Anything', () => openTTSModal());
const 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 = `
<div style="border: 1px solid #ccc; border-radius: 4px; padding: 10px;">
<h4 style="margin-top: 0;">AI Chat</h4>
<div id="chat-box" style="min-height: 150px; border: 1px solid #eee; padding: 8px; margin: 10px 0; overflow-y: auto; border-radius: 4px;"></div>
<div style="display: flex; gap: 8px;">
<input type="text" id="user-input" placeholder="Chat with AI..." style="flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px;">
<button onclick="handleUserInput()" style="padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">Send</button>
</div>
</div>
`;
}
function createTTSFeature(container) {
container.innerHTML = `
<div style="border: 1px solid #ccc; border-radius: 4px; padding: 10px;">
<h4 style="margin-top: 0;">Text to Speech</h4>
<textarea placeholder="Enter text to convert to speech..." style="width: 100%; height: 80px; margin: 10px 0; padding: 8px; border: 1px solid #ccc; border-radius: 4px;" data-tts-input></textarea>
<button onclick="handleTTSFromElement(this)" style="padding: 8px 16px; background: #28a745; color: white; border: none; border-radius: 4px; cursor: pointer;">🔊 Convert to Speech</button>
<div data-tts-result style="margin-top: 10px;"></div>
</div>
`;
}
function createUploadFeature(container) {
container.innerHTML = `
<div style="border: 1px solid #ccc; border-radius: 4px; padding: 10px;">
<h4 style="margin-top: 0;">File Upload & Analysis</h4>
<input type="file" style="margin: 10px 0; width: 100%;" data-upload-input>
<button onclick="handleUploadFromElement(this)" style="padding: 8px 16px; background: #ffc107; color: black; border: none; border-radius: 4px; cursor: pointer;">📁 Upload & Analyze</button>
<div data-upload-result style="margin-top: 10px; padding: 8px; border: 1px solid #eee; border-radius: 4px; display: none;"></div>
</div>
`;
}
function createReadFeature(container) {
container.innerHTML = `
<div style="border: 1px solid #ccc; border-radius: 4px; padding: 10px;">
<h4 style="margin-top: 0;">Page Reading</h4>
<p>Let AI read and analyze the current page content.</p>
<button onclick="readPageWithHermes()" style="padding: 8px 16px; background: #6f42c1; color: white; border: none; border-radius: 4px; cursor: pointer;">📖 Read Page with AI</button>
</div>
`;
}
// 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 = '<em>Converting to speech...</em>';
try {
const result = await speakText(text, 'alloy', 0.9);
resultDiv.innerHTML = `
<div style="margin: 10px 0;">
<button onclick="this.previousElementSibling.play()" style="margin: 2px; padding: 4px 8px;">▶️ Play</button>
<button onclick="this.previousElementSibling.previousElementSibling.pause()" style="margin: 2px; padding: 4px 8px;">⏸️ Pause</button>
</div>
`;
resultDiv.insertBefore(result.audio, resultDiv.firstChild);
} catch (error) {
resultDiv.innerHTML = '<strong>Error:</strong> ' + 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 = '<em>Uploading and analyzing file...</em>';
try {
showProgressIndicator('Processing file...');
const response = await uploadFile(fileInput.files[0]);
hideProgressIndicator();
resultDiv.innerHTML = `<strong>Analysis Result:</strong><br>${response}`;
fileInput.value = '';
} catch (error) {
hideProgressIndicator();
resultDiv.innerHTML = '<strong>Error:</strong> ' + error.message;
} finally {
button.disabled = false;
button.textContent = '📁 Upload & Analyze';
}
}
// Initialize the legacy chat interface (for backward compatibility)
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 <a href="https://nousresearch.com/hermes3/" target="_blank" style="color: inherit; text-decoration: underline;">hermes</a> large language model';
if (USE_CUSTOM_STYLING) {
title.style.cssText = `
margin: 0;
font-family: 'ChunkFiveRegular', monospace;
font-size: 16px;
line-height: 1.2;
`;
} else {
title.style.cssText = `
margin: 0;
font-size: 0.9em;
line-height: 1.2;
`;
}
const pageTitle = document.createElement('div');
pageTitle.textContent = `You are discussing: ${document.title}`;
if (USE_CUSTOM_STYLING) {
pageTitle.style.cssText = `
font-size: 12px;
opacity: 0.8;
margin-top: 4px;
`;
} else {
pageTitle.style.cssText = `
font-size: 0.75em;
opacity: 0.7;
margin-top: 4px;
`;
}
titleContainer.appendChild(title);
titleContainer.appendChild(pageTitle);
const closeButton = document.createElement('button');
closeButton.textContent = '×';
if (USE_CUSTOM_STYLING) {
closeButton.style.cssText = `
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
`;
} else {
closeButton.style.cssText = `
background: var(--background-color);
border: 1px solid var(--border-color);
color: var(--color);
font-size: 18px;
cursor: pointer;
padding: 4px;
width: 28px;
height: 28px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.2s;
`;
closeButton.onmouseenter = () => closeButton.style.opacity = '0.7';
closeButton.onmouseleave = () => closeButton.style.opacity = '1';
}
closeButton.onclick = () => {
document.body.removeChild(modal);
hermesModalOpen = false;
};
header.appendChild(titleContainer);
header.appendChild(closeButton);
// Create controls section
const controls = document.createElement('div');
if (USE_CUSTOM_STYLING) {
controls.style.cssText = `
padding: 16px 20px;
border-bottom: 1px solid #e0e0e0;
display: grid;
grid-template-columns: auto 1fr auto 1fr;
gap: 12px;
align-items: center;
`;
} else {
controls.className = 'uncloseai-controls';
controls.style.cssText = `
display: grid;
grid-template-columns: auto 1fr auto 1fr;
gap: 12px;
padding: 0.5rem;
align-items: center;
`;
}
// Add model selection dropdown
const modelLabel = document.createElement('label');
modelLabel.textContent = 'Model: ';
if (USE_CUSTOM_STYLING) {
modelLabel.style.fontWeight = 'bold';
}
const modelSelect = document.createElement('select');
modelSelect.id = 'modal-model-selection';
if (USE_CUSTOM_STYLING) {
modelSelect.style.cssText = `
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: white;
`;
}
// 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 = `<p><strong>You:</strong> ${msg.content}</p>`;
} else if (msg.role === 'assistant') {
const parsedContent = marked.parse(msg.content);
messageDiv.innerHTML = `<p><strong>AI:</strong> ${parsedContent}</p>`;
}
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 = '<p><strong>🤖 Hermes:</strong> <em>✨ Analyzing this page and crafting a personalized introduction... This may take a moment.</em></p>';
chatBox.appendChild(introDiv);
chatBox.scrollTop = chatBox.scrollHeight;
try {
const prompt = `You are Hermes, a large language model from Nous Research. Write a friendly 3-paragraph introduction for yourself when embedded on this webpage. Be specific about this page's content and identify 2-3 key takeaways. Keep it conversational and helpful.
Page Title: ${pageTitle}
Page URL: ${pageUrl}
Page Content: ${pageContent.substring(0, 2000)}
Format: Start with "Greetings! I'm Hermes..." and make it sound natural and engaging. Write 3 full paragraphs that showcase your capabilities and how you can help with THIS specific page.`;
let generatedIntro = '';
for await (const chunk of sendMessageDirect(prompt)) {
generatedIntro += chunk;
// Update display in real-time
introDiv.innerHTML = `<p><strong>🤖 Hermes:</strong> ${generatedIntro}</p>`;
// Scroll to bottom as content updates
chatBox.scrollTop = chatBox.scrollHeight;
}
// Add to chat history and save
chatHistory.push({ role: "assistant", content: generatedIntro });
saveConversationHistory();
// 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 = `<p><strong>🤖 Hermes:</strong> ${fallbackIntro}</p>`;
// 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 = `<p><strong>🤖 Hermes:</strong> ${introText}</p>`;
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 = `<p><strong>You:</strong> ${userInput}</p>`;
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 = '<strong>AI:</strong> ';
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 = `<strong>${label}:</strong> ${content}`;
} else {
messageDiv.innerHTML = `<strong>${label}:</strong> ${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 = `<strong>System:</strong> ${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;