fix(translation): correct syntax and logic in translation modal

- Reverted the broken openTranslateModal function to a working state.
- Re-implemented the logic to correctly inject the <base> tag for relative paths and to ensure the translation notice is displayed correctly.
- Ran biome to format the code and ensure there are no syntax errors.
This commit is contained in:
Russell Ballestrini 2025-07-01 18:23:02 -04:00
parent 206731c589
commit 17e3883bff
13 changed files with 5029 additions and 2270 deletions

View file

@ -1,12 +1,10 @@
/*! Generated by Font Squirrel (https://www.fontsquirrel.com) on October 4, 2021 */
@font-face {
font-family: 'chunkfiveregular';
src: url('chunkfive-regular-webfont.woff2') format('woff2'),
url('chunkfive-regular-webfont.woff') format('woff');
font-weight: normal;
font-style: normal;
}
font-family: "chunkfiveregular";
src:
url("chunkfive-regular-webfont.woff2") format("woff2"),
url("chunkfive-regular-webfont.woff") format("woff");
font-weight: normal;
font-style: normal;
}

File diff suppressed because one or more lines are too long

View file

@ -1,156 +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';
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 });
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`;
// 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 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 = '';
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
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;
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);
}
}
}
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];
}
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 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 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 aiResponseParagraph = document.createElement("p");
aiResponseParagraph.innerHTML = "<strong>AI:</strong> ";
chatBox.appendChild(aiResponseParagraph);
const responseContent = document.createElement('span');
aiResponseParagraph.appendChild(responseContent);
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;
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);
});
}
responseContent.querySelectorAll("pre code").forEach((block) => {
hljs.highlightElement(block);
});
}
chatBox.scrollTop = chatBox.scrollHeight;
chatBox.scrollTop = chatBox.scrollHeight;
// Add the response to chat history
chatHistory.push({ role: "assistant", content: accumulatedContent });
saveConversationHistory(chatHistory);
// 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;
// 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();
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);
// 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 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;
chatHistory = newHistory;
}
export function getChatHistory() {
return chatHistory;
}
return chatHistory;
}

View file

@ -6,32 +6,35 @@ export const API_KEY = "dummy-api-key";
export const MODEL = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"; // default model (always available)
// System message configuration - implementors can customize this
export const SYSTEM_MESSAGE_BASE = "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.";
export const SYSTEM_MESSAGE_BASE =
"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.";
// Optional additional system message content - can be set by implementor
export let SYSTEM_MESSAGE_APPEND = "";
// Function to set additional system message content
export function setSystemMessageAppend(appendText) {
SYSTEM_MESSAGE_APPEND = appendText;
SYSTEM_MESSAGE_APPEND = appendText;
}
// Function to get the complete system message
export function getSystemMessage() {
return SYSTEM_MESSAGE_APPEND ? `${SYSTEM_MESSAGE_BASE}\n\n${SYSTEM_MESSAGE_APPEND}` : SYSTEM_MESSAGE_BASE;
return SYSTEM_MESSAGE_APPEND
? `${SYSTEM_MESSAGE_BASE}\n\n${SYSTEM_MESSAGE_APPEND}`
: SYSTEM_MESSAGE_BASE;
}
// 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' }
{ 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 lastTTSInput = "";
export let lastTTSResult = null;
export function setLastTTS(input, result) {
lastTTSInput = input;
lastTTSResult = result;
}
lastTTSInput = input;
lastTTSResult = result;
}

View file

@ -2,41 +2,41 @@
// Function to extract text content along with links and metadata from the webpage
export function extractWebpageContent() {
let content = '';
let content = "";
// Extract title
const title = document.title;
if (title) {
content += `**Page Title**: ${title}\n\n`;
}
// 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 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`;
}
// 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);
}
}
}
// 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();
}
getTextWithLinks(document.body);
return content.trim();
}

View file

@ -1,18 +1,18 @@
// 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';
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 = `
// Remove existing indicator if any
hideProgressIndicator();
const indicator = document.createElement("div");
indicator.id = "progress-indicator";
indicator.style.cssText = `
position: fixed;
top: 50%;
left: 50%;
@ -23,114 +23,114 @@ export function showProgressIndicator(message) {
border-radius: 5px;
z-index: 9999;
`;
indicator.textContent = message || 'Processing...';
document.body.appendChild(indicator);
indicator.textContent = message || "Processing...";
document.body.appendChild(indicator);
}
export function hideProgressIndicator() {
const indicator = document.getElementById('progress-indicator');
if (indicator) {
indicator.remove();
}
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;
}
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();
}
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 formData = new FormData();
formData.append("file", file);
const response = await fetch(MEGAPARCE_API_URL, {
method: 'POST',
body: formData
});
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}`);
}
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Error uploading file: ${errorText}`);
}
const responseData = await response.json();
return responseData;
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.";
// 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}` });
// Add a system message with the uploaded content to provide context
chatHistory.push({ role: "system", content: `Uploaded Context: ${content}` });
// Note: System message is added to chat history for context but not displayed to user
// The uploaded content context is available to the AI without showing it in the chat
// Note: System message is added to chat history for context but not displayed to user
// The uploaded content context is available to the AI without showing it in the chat
// 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;
// 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();
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);
}
// 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);
}

View file

@ -1,5 +1,5 @@
// Model registry and selection functionality
import { VLLM_ENDPOINTS } from './config.js';
import { VLLM_ENDPOINTS } from "./config.js";
// This registry maps a model's ID to the endpoint where it resides.
export const modelRegistry = {};
@ -7,222 +7,231 @@ 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
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 (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();
// 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
};
});
// 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);
// Cache the results
const cacheData = {
timestamp: now,
modelRegistry: modelRegistry,
models: models,
};
localStorage.setItem(cacheKey, JSON.stringify(cacheData));
localStorage.setItem(endpointsKey, endpointsString);
return models;
return models;
}
// Create a dynamic drop-down for model selection
// This function creates a <select> element and fills it with unique model instances.
export async function createModelSelectionDropdown() {
// Only create dropdown if there's a chat interface or user input
const userInput = document.getElementById("user-input");
const chatBox = document.getElementById("chat-box");
const uncloseaiElements = document.querySelectorAll('.uncloseai');
if (!userInput && !chatBox && uncloseaiElements.length === 0) {
console.log("No chat interface found, skipping model dropdown creation");
return;
}
// Only create dropdown if there's a chat interface or user input
const userInput = document.getElementById("user-input");
const chatBox = document.getElementById("chat-box");
const uncloseaiElements = document.querySelectorAll(".uncloseai");
const models = await fetchModelsFromEndpoints();
if (!models.length) {
console.warn("No models returned from any endpoint.");
return;
}
if (!userInput && !chatBox && uncloseaiElements.length === 0) {
console.log("No chat interface found, skipping model dropdown creation");
return;
}
const dropdown = document.createElement('select');
dropdown.id = 'model-selection';
dropdown.style.margin = '10px';
const models = await fetchModelsFromEndpoints();
if (!models.length) {
console.warn("No models returned from any endpoint.");
return;
}
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);
});
const dropdown = document.createElement("select");
dropdown.id = "model-selection";
dropdown.style.margin = "10px";
// Insert the dropdown directly above the chat input box.
if (userInput && userInput.parentNode) {
userInput.parentNode.parentNode.insertBefore(dropdown, userInput.parentNode);
} else {
// Only insert at top of body if there are uncloseai elements
if (uncloseaiElements.length > 0) {
document.body.insertBefore(dropdown, document.body.firstChild);
}
}
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.
if (userInput && userInput.parentNode) {
userInput.parentNode.parentNode.insertBefore(
dropdown,
userInput.parentNode,
);
} else {
// Only insert at top of body if there are uncloseai elements
if (uncloseaiElements.length > 0) {
document.body.insertBefore(dropdown, document.body.firstChild);
}
}
}
// Function to add the Refresh Models button
export function addRefreshModelsButton() {
// Only create button if there's a chat interface
const userInput = document.getElementById("user-input");
const chatBox = document.getElementById("chat-box");
const uncloseaiElements = document.querySelectorAll('.uncloseai');
if (!userInput && !chatBox && uncloseaiElements.length === 0) {
console.log("No chat interface found, skipping refresh button creation");
return;
}
// Only create button if there's a chat interface
const userInput = document.getElementById("user-input");
const chatBox = document.getElementById("chat-box");
const uncloseaiElements = document.querySelectorAll(".uncloseai");
// Create a container for the button
const container = document.createElement('div');
container.id = 'refresh-models-container';
// Position it near the model selection dropdown
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);
if (!userInput && !chatBox && uncloseaiElements.length === 0) {
console.log("No chat interface found, skipping refresh button creation");
return;
}
// Create a container for the button
const container = document.createElement("div");
container.id = "refresh-models-container";
// Position it near the model selection dropdown
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);
}
// Helper function to get the selected model from the dropdown.
export function getSelectedModel() {
// Check modal dropdown first
const modalDropdown = document.getElementById("hermes-model-selection");
if (modalDropdown && modalDropdown.value) {
// Extract just the model name, not the unique ID
const selectedId = modalDropdown.value;
if (modelRegistry[selectedId]) {
return selectedId.split('-').slice(1).join('-'); // Remove endpoint prefix
}
return modalDropdown.value;
}
// Check modal dropdown first
const modalDropdown = document.getElementById("hermes-model-selection");
if (modalDropdown && modalDropdown.value) {
// Extract just the model name, not the unique ID
const selectedId = modalDropdown.value;
if (modelRegistry[selectedId]) {
return selectedId.split("-").slice(1).join("-"); // Remove endpoint prefix
}
return modalDropdown.value;
}
// Fallback to main dropdown
const dropdown = document.getElementById("model-selection");
if (dropdown && dropdown.value) {
// Extract just the model name, not the unique ID
const selectedId = dropdown.value;
if (modelRegistry[selectedId]) {
return selectedId.split('-').slice(1).join('-'); // Remove endpoint prefix
}
return dropdown.value;
}
// Fallback to main dropdown
const dropdown = document.getElementById("model-selection");
if (dropdown && dropdown.value) {
// Extract just the model name, not the unique ID
const selectedId = dropdown.value;
if (modelRegistry[selectedId]) {
return selectedId.split("-").slice(1).join("-"); // Remove endpoint prefix
}
return dropdown.value;
}
// Return default model if nothing selected
const defaultModel = Object.keys(modelRegistry)[0];
if (defaultModel) {
return defaultModel.split('-').slice(1).join('-'); // Remove endpoint prefix
}
// Return default model if nothing selected
const defaultModel = Object.keys(modelRegistry)[0];
if (defaultModel) {
return defaultModel.split("-").slice(1).join("-"); // Remove endpoint prefix
}
// Fallback to hardcoded model
return "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic";
// Fallback to hardcoded model
return "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic";
}
// Helper function to get the API endpoint for the selected model.
export function getSelectedModelEndpoint() {
const selectedModel = getSelectedModel();
if (modelRegistry[selectedModel]) {
return modelRegistry[selectedModel].url;
}
// Fallback to the first endpoint if model not found
if (VLLM_ENDPOINTS.length > 0) {
return VLLM_ENDPOINTS[0].url;
}
// Final fallback
return "https://hermes.ai.unturf.com/v1";
}
const selectedModel = getSelectedModel();
if (modelRegistry[selectedModel]) {
return modelRegistry[selectedModel].url;
}
// Fallback to the first endpoint if model not found
if (VLLM_ENDPOINTS.length > 0) {
return VLLM_ENDPOINTS[0].url;
}
// Final fallback
return "https://hermes.ai.unturf.com/v1";
}

View file

@ -1,68 +1,68 @@
// Page reading functionality
import { extractWebpageContent } from './content.js';
import { processContentWithHermes, speakText } from './tts.js';
import { extractWebpageContent } from "./content.js";
import { processContentWithHermes, speakText } from "./tts.js";
// Function to read the entire page using Hermes
export async function readPageWithHermes() {
const content = extractWebpageContent();
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...';
// 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);
const processedContent = await processContentWithHermes(content);
statusDiv.textContent = "Processing content and generating speech...";
// 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();
}
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();
}

View file

@ -1,51 +1,56 @@
// Conversation history and localStorage functionality
import { getSystemMessage } from './config.js';
import { getSystemMessage } from "./config.js";
// Helper function to generate a page-specific key for localStorage
export 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}`;
// 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}`;
}
// Functions for conversation history persistence
export function saveConversationHistory(chatHistory) {
const historyToSave = chatHistory.filter(msg => msg.role !== 'system');
localStorage.setItem(getPageSpecificKey('hermes-conversation-history'), JSON.stringify(historyToSave));
const historyToSave = chatHistory.filter((msg) => msg.role !== "system");
localStorage.setItem(
getPageSpecificKey("hermes-conversation-history"),
JSON.stringify(historyToSave),
);
}
export function loadConversationHistory() {
const saved = localStorage.getItem(getPageSpecificKey('hermes-conversation-history'));
if (saved) {
try {
const parsedHistory = JSON.parse(saved);
// Filter out any system messages that might have been saved before our fix
return parsedHistory.filter(msg => msg.role !== 'system');
} catch (e) {
console.error('Error parsing conversation history:', e);
}
}
return [];
const saved = localStorage.getItem(
getPageSpecificKey("hermes-conversation-history"),
);
if (saved) {
try {
const parsedHistory = JSON.parse(saved);
// Filter out any system messages that might have been saved before our fix
return parsedHistory.filter((msg) => msg.role !== "system");
} catch (e) {
console.error("Error parsing conversation history:", e);
}
}
return [];
}
export function clearConversationHistory() {
localStorage.removeItem(getPageSpecificKey('hermes-conversation-history'));
return [
{
role: "system",
content: getSystemMessage()
}
];
localStorage.removeItem(getPageSpecificKey("hermes-conversation-history"));
return [
{
role: "system",
content: getSystemMessage(),
},
];
}
// Initialize chat history
export function initializeChatHistory() {
return [
{
role: "system",
content: getSystemMessage()
},
...loadConversationHistory()
];
}
return [
{
role: "system",
content: getSystemMessage(),
},
...loadConversationHistory(),
];
}

View file

@ -1,253 +1,287 @@
// Translation functionality with code block preservation
import { sendMessage } from './chat.js';
import { API_KEY } from './config.js';
import { getSelectedModel, getSelectedModelEndpoint } from './models.js';
import { sendMessage } from "./chat.js";
import { API_KEY } from "./config.js";
import { getSelectedModel, getSelectedModelEndpoint } from "./models.js";
// Languages supported by Hermes 3 model (from Russell's implementation)
export const SUPPORTED_LANGUAGES = {
'en': 'English',
'zh': 'Chinese (Simplified)',
'hi': 'Hindi',
'es': 'Spanish',
'fr': 'French',
'ar': 'Arabic',
'bn': 'Bengali',
'ru': 'Russian',
'pt': 'Portuguese',
'ur': 'Urdu',
'id': 'Indonesian',
'de': 'German',
'ja': 'Japanese',
'sw': 'Swahili',
'mr': 'Marathi',
'te': 'Telugu',
'tr': 'Turkish',
'zh-tw': 'Chinese (Traditional)',
'ko': 'Korean'
en: "English",
zh: "Chinese (Simplified)",
hi: "Hindi",
es: "Spanish",
fr: "French",
ar: "Arabic",
bn: "Bengali",
ru: "Russian",
pt: "Portuguese",
ur: "Urdu",
id: "Indonesian",
de: "German",
ja: "Japanese",
sw: "Swahili",
mr: "Marathi",
te: "Telugu",
tr: "Turkish",
"zh-tw": "Chinese (Traditional)",
ko: "Korean",
};
// Send message with custom history (for translation with minimal system prompt)
async function* sendMessageWithHistory(messageHistory) {
const apiUrl = `${getSelectedModelEndpoint()}/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: messageHistory,
temperature: 0.3, // Lower temperature for more consistent translation
max_tokens: 70000,
stream: true
})
});
const response = await fetch(apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
model: getSelectedModel(),
messages: messageHistory,
temperature: 0.3, // Lower temperature for more consistent translation
max_tokens: 70000,
stream: true,
}),
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
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;
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);
}
}
}
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];
}
buffer = lines[lines.length - 1];
}
}
// Preserve code blocks, URLs, and special formatting during translation
export function preserveSpecialContent(text) {
const preservations = [];
let preservedText = text;
// HTML code elements (most important in HTML context)
preservedText = preservedText.replace(/<script[\s\S]*?<\/script>/gi, (match) => {
const index = preservations.length;
const placeholder = `__CODE_${index}__`;
preservations.push({ type: 'CODE', content: match, placeholder });
return placeholder;
});
preservedText = preservedText.replace(/<pre[\s\S]*?<\/pre>/gi, (match) => {
const index = preservations.length;
const placeholder = `__CODE_${index}__`;
preservations.push({ type: 'CODE', content: match, placeholder });
return placeholder;
});
preservedText = preservedText.replace(/<code[\s\S]*?<\/code>/gi, (match) => {
const index = preservations.length;
const placeholder = `__CODE_${index}__`;
preservations.push({ type: 'CODE', content: match, placeholder });
return placeholder;
});
// Other code-related HTML elements
preservedText = preservedText.replace(/<(kbd|samp|var)[\s\S]*?<\/\1>/gi, (match) => {
const index = preservations.length;
const placeholder = `__CODE_${index}__`;
preservations.push({ type: 'CODE', content: match, placeholder });
return placeholder;
});
// Markdown code blocks (for mixed content)
preservedText = preservedText.replace(/```[\s\S]*?```/g, (match) => {
const index = preservations.length;
const placeholder = `__CODE_${index}__`;
preservations.push({ type: 'CODE', content: match, placeholder });
return placeholder;
});
// Inline markdown code
preservedText = preservedText.replace(/`[^`\n]+`/g, (match) => {
const index = preservations.length;
const placeholder = `__CODE_${index}__`;
preservations.push({ type: 'CODE', content: match, placeholder });
return placeholder;
});
// URLs and URIs
preservedText = preservedText.replace(/https?:\/\/[^\s<>"{}|\\^`\[\]]+/g, (match) => {
const index = preservations.length;
const placeholder = `__URI_${index}__`;
preservations.push({ type: 'URI', content: match, placeholder });
return placeholder;
});
// Email addresses
preservedText = preservedText.replace(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, (match) => {
const index = preservations.length;
const placeholder = `__URI_${index}__`;
preservations.push({ type: 'URI', content: match, placeholder });
return placeholder;
});
// Other HTML tags (after code elements are preserved)
preservedText = preservedText.replace(/<[^>]+>/g, (match) => {
const index = preservations.length;
const placeholder = `__HTML_${index}__`;
preservations.push({ type: 'HTML', content: match, placeholder });
return placeholder;
});
return { preservedText, preservations };
const preservations = [];
let preservedText = text;
// HTML code elements (most important in HTML context)
preservedText = preservedText.replace(
/<script[\s\S]*?<\/script>/gi,
(match) => {
const index = preservations.length;
const placeholder = `__CODE_${index}__`;
preservations.push({ type: "CODE", content: match, placeholder });
return placeholder;
},
);
preservedText = preservedText.replace(/<pre[\s\S]*?<\/pre>/gi, (match) => {
const index = preservations.length;
const placeholder = `__CODE_${index}__`;
preservations.push({ type: "CODE", content: match, placeholder });
return placeholder;
});
preservedText = preservedText.replace(/<code[\s\S]*?<\/code>/gi, (match) => {
const index = preservations.length;
const placeholder = `__CODE_${index}__`;
preservations.push({ type: "CODE", content: match, placeholder });
return placeholder;
});
// Other code-related HTML elements
preservedText = preservedText.replace(
/<(kbd|samp|var)[\s\S]*?<\/\1>/gi,
(match) => {
const index = preservations.length;
const placeholder = `__CODE_${index}__`;
preservations.push({ type: "CODE", content: match, placeholder });
return placeholder;
},
);
// Markdown code blocks (for mixed content)
preservedText = preservedText.replace(/```[\s\S]*?```/g, (match) => {
const index = preservations.length;
const placeholder = `__CODE_${index}__`;
preservations.push({ type: "CODE", content: match, placeholder });
return placeholder;
});
// Inline markdown code
preservedText = preservedText.replace(/`[^`\n]+`/g, (match) => {
const index = preservations.length;
const placeholder = `__CODE_${index}__`;
preservations.push({ type: "CODE", content: match, placeholder });
return placeholder;
});
// URLs and URIs
preservedText = preservedText.replace(
/https?:\/\/[^\s<>"{}|\\^`\[\]]+/g,
(match) => {
const index = preservations.length;
const placeholder = `__URI_${index}__`;
preservations.push({ type: "URI", content: match, placeholder });
return placeholder;
},
);
// Email addresses
preservedText = preservedText.replace(
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g,
(match) => {
const index = preservations.length;
const placeholder = `__URI_${index}__`;
preservations.push({ type: "URI", content: match, placeholder });
return placeholder;
},
);
// Other HTML tags (after code elements are preserved)
preservedText = preservedText.replace(/<[^>]+>/g, (match) => {
const index = preservations.length;
const placeholder = `__HTML_${index}__`;
preservations.push({ type: "HTML", content: match, placeholder });
return placeholder;
});
return { preservedText, preservations };
}
// Restore preserved content after translation
export function restoreSpecialContent(translatedText, preservations) {
let restoredText = translatedText;
// By reversing the array, we restore the outer elements (like HTML tags) first,
// which then reveals the inner placeholders (like URIs and code) for subsequent replacement.
for (let i = preservations.length - 1; i >= 0; i--) {
const item = preservations[i];
restoredText = restoredText.replace(new RegExp(item.placeholder, 'g'), item.content);
}
return restoredText;
let restoredText = translatedText;
// By reversing the array, we restore the outer elements (like HTML tags) first,
// which then reveals the inner placeholders (like URIs and code) for subsequent replacement.
for (let i = preservations.length - 1; i >= 0; i--) {
const item = preservations[i];
restoredText = restoredText.replace(
new RegExp(item.placeholder, "g"),
item.content,
);
}
return restoredText;
}
// Translate text using Hermes AI with minimal system context
export async function translateText(text, targetLanguage) {
const { preservedText, preservations } = preserveSpecialContent(text);
// Debug logging
console.log('=== TRANSLATION DEBUG ===');
console.log('Original text:', text.substring(0, 300) + (text.length > 300 ? '...' : ''));
console.log('Preserved text:', preservedText.substring(0, 300) + (preservedText.length > 300 ? '...' : ''));
console.log('Preservations found:', preservations.length);
preservations.forEach((item, index) => {
console.log(` ${index}: ${item.placeholder} -> ${item.content.substring(0, 50)}${item.content.length > 50 ? '...' : ''}`);
});
const prompt = `Translate the following HTML content into ${SUPPORTED_LANGUAGES[targetLanguage]}. The input is in English and formatted as HTML, which includes formatting, code blocks, and technical content. Ensure the output preserves the structure, syntax, and formatting of the original. Do not translate or modify placeholders like __CODE_0__, __URI_0__, __HTML_0__, etc., as they represent code blocks, URLs, or HTML elements that should remain unchanged. Only translate the surrounding text. Here is the text to translate:
const { preservedText, preservations } = preserveSpecialContent(text);
// Debug logging
console.log("=== TRANSLATION DEBUG ===");
console.log(
"Original text:",
text.substring(0, 300) + (text.length > 300 ? "..." : ""),
);
console.log(
"Preserved text:",
preservedText.substring(0, 300) + (preservedText.length > 300 ? "..." : ""),
);
console.log("Preservations found:", preservations.length);
preservations.forEach((item, index) => {
console.log(
` ${index}: ${item.placeholder} -> ${item.content.substring(0, 50)}${item.content.length > 50 ? "..." : ""}`,
);
});
const prompt = `Translate the following HTML content into ${SUPPORTED_LANGUAGES[targetLanguage]}. The input is in English and formatted as HTML, which includes formatting, code blocks, and technical content. Ensure the output preserves the structure, syntax, and formatting of the original. Do not translate or modify placeholders like __CODE_0__, __URI_0__, __HTML_0__, etc., as they represent code blocks, URLs, or HTML elements that should remain unchanged. Only translate the surrounding text. Here is the text to translate:
${preservedText}`;
// Use minimal system message for translation - focused purely on the task
const translationHistory = [
{
role: "system",
content: "You are a translation assistant. Translate text accurately while preserving all formatting and placeholders exactly as provided."
},
{
role: "user",
content: prompt
}
];
// Use minimal system message for translation - focused purely on the task
const translationHistory = [
{
role: "system",
content:
"You are a translation assistant. Translate text accurately while preserving all formatting and placeholders exactly as provided.",
},
{
role: "user",
content: prompt,
},
];
let translatedText = '';
try {
for await (const chunk of sendMessageWithHistory(translationHistory)) {
translatedText += chunk;
}
// Clean up the response (remove any extra explanations)
translatedText = translatedText.trim();
console.log('AI response length:', translatedText.length);
console.log('AI response:', translatedText.substring(0, 300) + (translatedText.length > 300 ? '...' : ''));
// Check if placeholders are still in the response
preservations.forEach((item, index) => {
const found = translatedText.includes(item.placeholder);
console.log(` Placeholder ${item.placeholder} found in response: ${found}`);
});
// Restore preserved content
const restored = restoreSpecialContent(translatedText, preservations);
console.log('Restored text length:', restored.length);
console.log('Restored text:', restored.substring(0, 300) + (restored.length > 300 ? '...' : ''));
return restored;
} catch (error) {
console.error('Translation error:', error);
throw new Error('Translation failed. Please try again.');
}
let translatedText = "";
try {
for await (const chunk of sendMessageWithHistory(translationHistory)) {
translatedText += chunk;
}
// Clean up the response (remove any extra explanations)
translatedText = translatedText.trim();
console.log("AI response length:", translatedText.length);
console.log(
"AI response:",
translatedText.substring(0, 300) +
(translatedText.length > 300 ? "..." : ""),
);
// Check if placeholders are still in the response
preservations.forEach((item, index) => {
const found = translatedText.includes(item.placeholder);
console.log(
` Placeholder ${item.placeholder} found in response: ${found}`,
);
});
// Restore preserved content
const restored = restoreSpecialContent(translatedText, preservations);
console.log("Restored text length:", restored.length);
console.log(
"Restored text:",
restored.substring(0, 300) + (restored.length > 300 ? "..." : ""),
);
return restored;
} catch (error) {
console.error("Translation error:", error);
throw new Error("Translation failed. Please try again.");
}
}
// Extract page content for translation
export function extractPageContent() {
// Return the entire HTML of the document to preserve head, styles, and scripts
return document.documentElement.outerHTML;
// Return the entire HTML of the document to preserve head, styles, and scripts
return document.documentElement.outerHTML;
}
// Translate current page content
export async function translateCurrentPage(targetLanguage) {
const pageContent = extractPageContent();
if (!pageContent || pageContent.length < 10) {
throw new Error('Unable to extract meaningful content from this page.');
}
// Limit content length to avoid overwhelming the AI
const maxLength = 50000; // Much higher limit for complete page translation
const contentToTranslate = pageContent.length > maxLength
? pageContent.substring(0, maxLength) + '...'
: pageContent;
return await translateText(contentToTranslate, targetLanguage);
}
const pageContent = extractPageContent();
if (!pageContent || pageContent.length < 10) {
throw new Error("Unable to extract meaningful content from this page.");
}
// Limit content length to avoid overwhelming the AI
const maxLength = 50000; // Much higher limit for complete page translation
const contentToTranslate =
pageContent.length > maxLength
? pageContent.substring(0, maxLength) + "..."
: pageContent;
return await translateText(contentToTranslate, targetLanguage);
}

View file

@ -1,53 +1,53 @@
// Text-to-speech functionality
import { TTS_API_URL, API_KEY, MODEL, setLastTTS } from './config.js';
import { getSelectedModel, getSelectedModelEndpoint } from './models.js';
import { TTS_API_URL, API_KEY, MODEL, setLastTTS } from "./config.js";
import { getSelectedModel, getSelectedModelEndpoint } from "./models.js";
// Function to read text using TTS
export 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);
export 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
})
});
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}`);
}
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;
// Store the last TTS result
setLastTTS(text, { audio, blob: audioBlob });
return { audio, blob: audioBlob };
} catch (error) {
console.error('Error in TTS:', error);
throw error;
}
const audioBlob = await response.blob();
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
audio.playbackRate = rate;
// Store the last TTS result
setLastTTS(text, { audio, blob: audioBlob });
return { audio, blob: audioBlob };
} catch (error) {
console.error("Error in TTS:", error);
throw error;
}
}
// Process page content using Hermes
export async function processContentWithHermes(content) {
const payload = {
model: getSelectedModel(),
messages: [
{
role: "system",
content: `Extract the exact main content of the article.
const payload = {
model: getSelectedModel(),
messages: [
{
role: "system",
content: `Extract the exact main content of the article.
Avoid reading ads.
@ -62,115 +62,133 @@ 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
};
`,
},
{
role: "user",
content: content,
},
],
temperature: 0,
max_tokens: 76000,
};
console.log("Sending payload to LLM for spoken tokens:", JSON.stringify(payload, null, 2));
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)
});
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}`);
}
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;
}
const data = await response.json();
return data.choices[0].message.content;
} catch (error) {
console.error("Error in processContentWithHermes:", error);
throw error;
}
}
export 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
};
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));
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)
});
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}`);
}
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;
}
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
export 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
})
});
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}`);
}
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();
}
const data = await response.json();
return data.choices[0].message.content
.trim()
.replace(/\s+/g, "-")
.toLowerCase();
}

2726
src/ui.js

File diff suppressed because it is too large Load diff

View file

@ -1,24 +1,24 @@
/*
/*
* 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';
import hljs from "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/es/highlight.min.js";
// 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';
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";
// -------------------------
// Public API - Main Functions
@ -81,18 +81,18 @@ export const VLLM_ENDPOINTS = Config.VLLM_ENDPOINTS;
export const modelRegistry = Models.modelRegistry;
// Export chat history (for external access)
export { chatHistory } from './src/chat.js';
export { chatHistory } from "./src/chat.js";
// -------------------------
// Initialization
// -------------------------
// Initialize on page load
window.addEventListener('load', () => {
console.log('uncloseai.js: window.onload event fired.');
UI.initializeSystem();
Models.createModelSelectionDropdown();
Models.addRefreshModelsButton();
window.addEventListener("load", () => {
console.log("uncloseai.js: window.onload event fired.");
UI.initializeSystem();
Models.createModelSelectionDropdown();
Models.addRefreshModelsButton();
});
// -------------------------
@ -117,35 +117,36 @@ 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();
if (!message) return;
// Add user message
chatBox.innerHTML += `<div style="margin: 2px 0; padding: 2px; background: #e3f2fd; border-radius: 2px; font-size: 0.9em;"><strong>You:</strong> ${message}</div>`;
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 = '<strong>AI:</strong> <em>thinking...</em>';
chatBox.appendChild(thinkingDiv);
chatBox.scrollTop = chatBox.scrollHeight;
try {
let response = '';
for await (const chunk of Chat.sendMessage(message)) {
response += chunk;
thinkingDiv.innerHTML = `<strong>AI:</strong> ${response}`;
chatBox.scrollTop = chatBox.scrollHeight;
}
} catch (error) {
thinkingDiv.innerHTML = `<strong>Error:</strong> ${error.message}`;
}
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();
if (!message) return;
// Add user message
chatBox.innerHTML += `<div style="margin: 2px 0; padding: 2px; background: #e3f2fd; border-radius: 2px; font-size: 0.9em;"><strong>You:</strong> ${message}</div>`;
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 = "<strong>AI:</strong> <em>thinking...</em>";
chatBox.appendChild(thinkingDiv);
chatBox.scrollTop = chatBox.scrollHeight;
try {
let response = "";
for await (const chunk of Chat.sendMessage(message)) {
response += chunk;
thinkingDiv.innerHTML = `<strong>AI:</strong> ${response}`;
chatBox.scrollTop = chatBox.scrollHeight;
}
} catch (error) {
thinkingDiv.innerHTML = `<strong>Error:</strong> ${error.message}`;
}
};
console.log('uncloseai.js: Modular version loaded successfully');
console.log("uncloseai.js: Modular version loaded successfully");