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:
parent
206731c589
commit
17e3883bff
13 changed files with 5029 additions and 2270 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
2611
css/pico.classless.min.css
vendored
2611
css/pico.classless.min.css
vendored
File diff suppressed because one or more lines are too long
236
src/chat.js
236
src/chat.js
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
389
src/models.js
389
src/models.js
|
|
@ -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";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
288
src/tts.js
288
src/tts.js
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
97
uncloseai.js
97
uncloseai.js
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue