new file: src/chat.js
new file: src/config.js new file: src/content.js new file: src/file-upload.js new file: src/models.js new file: src/page-reader.js new file: src/storage.js new file: src/tts.js new file: src/ui.js modified: uncloseai.js new file: uncloseai.js.orig
This commit is contained in:
parent
2fed53f74b
commit
01df4d1e77
11 changed files with 4228 additions and 2683 deletions
156
src/chat.js
Normal file
156
src/chat.js
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
// Chat functionality and message handling
|
||||
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
|
||||
import hljs from 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/es/highlight.min.js';
|
||||
import { API_KEY } from './config.js';
|
||||
import { getSelectedModel, getSelectedModelEndpoint } from './models.js';
|
||||
import { speakText, generateTitleForTTS } from './tts.js';
|
||||
import { saveConversationHistory, initializeChatHistory } from './storage.js';
|
||||
|
||||
// Initialize chat history
|
||||
export let chatHistory = initializeChatHistory();
|
||||
|
||||
// Generator function to send a message to the LLM and yield responses
|
||||
export async function* sendMessage(message) {
|
||||
chatHistory.push({ role: "user", content: message });
|
||||
|
||||
// Dynamically determine the API URL based on the selected model.
|
||||
// (Assuming that the chat completions endpoint is at "/chat/completions")
|
||||
const apiUrl = `${getSelectedModelEndpoint()}/chat/completions`;
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${API_KEY}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: getSelectedModel(),
|
||||
messages: chatHistory,
|
||||
temperature: 0.5,
|
||||
max_tokens: 8192,
|
||||
stream: true
|
||||
})
|
||||
});
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length - 1; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (line.startsWith('data: ')) {
|
||||
const jsonData = line.slice(6);
|
||||
if (jsonData === '[DONE]') continue;
|
||||
|
||||
try {
|
||||
const parsedData = JSON.parse(jsonData);
|
||||
const content = parsedData.choices[0].delta.content;
|
||||
if (content) {
|
||||
yield content;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing JSON:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buffer = lines[lines.length - 1];
|
||||
}
|
||||
}
|
||||
|
||||
// Function to handle user input and display responses
|
||||
export async function handleUserInput() {
|
||||
const userInput = document.getElementById('user-input').value;
|
||||
document.getElementById('user-input').value = '';
|
||||
|
||||
const chatBox = document.getElementById('chat-box');
|
||||
chatBox.innerHTML += `<p><strong>You:</strong> ${userInput}</p>`;
|
||||
|
||||
const aiResponseParagraph = document.createElement('p');
|
||||
aiResponseParagraph.innerHTML = '<strong>AI:</strong> ';
|
||||
chatBox.appendChild(aiResponseParagraph);
|
||||
|
||||
const responseContent = document.createElement('span');
|
||||
aiResponseParagraph.appendChild(responseContent);
|
||||
|
||||
let accumulatedContent = '';
|
||||
for await (const chunk of sendMessage(userInput)) {
|
||||
accumulatedContent += chunk;
|
||||
const parsedChunk = marked.parse(accumulatedContent);
|
||||
responseContent.innerHTML = parsedChunk;
|
||||
|
||||
responseContent.querySelectorAll('pre code').forEach((block) => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
}
|
||||
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
|
||||
// Add the response to chat history
|
||||
chatHistory.push({ role: "assistant", content: accumulatedContent });
|
||||
saveConversationHistory(chatHistory);
|
||||
|
||||
// Add play/pause button for TTS
|
||||
const playPauseButton = document.createElement('button');
|
||||
playPauseButton.textContent = 'Generate TTS for AI Response';
|
||||
playPauseButton.style.margin = '5px';
|
||||
let aiAudio = null;
|
||||
let aiBlob = null;
|
||||
let isPaused = false;
|
||||
|
||||
playPauseButton.onclick = async () => {
|
||||
if (!aiAudio) {
|
||||
playPauseButton.textContent = 'Processing...';
|
||||
playPauseButton.disabled = true; // Disable button while processing
|
||||
const mainVoiceSelect = document.getElementById('read-page-voice');
|
||||
const selectedVoice = mainVoiceSelect ? mainVoiceSelect.value : 'alloy';
|
||||
const result = await speakText(accumulatedContent, selectedVoice, 0.9);
|
||||
aiAudio = result.audio;
|
||||
aiBlob = result.blob;
|
||||
playPauseButton.textContent = 'Pause AI Response';
|
||||
playPauseButton.disabled = false; // Re-enable button after processing
|
||||
aiAudio.play();
|
||||
|
||||
// Generate title for the MP3 file
|
||||
const title = await generateTitleForTTS(accumulatedContent);
|
||||
|
||||
// Add download button
|
||||
const downloadButton = document.createElement('button');
|
||||
downloadButton.textContent = 'Download MP3';
|
||||
downloadButton.style.margin = '5px';
|
||||
downloadButton.onclick = () => {
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(aiBlob);
|
||||
a.download = `${title}.mp3`;
|
||||
a.click();
|
||||
};
|
||||
chatBox.appendChild(downloadButton);
|
||||
} else {
|
||||
if (isPaused) {
|
||||
aiAudio.play();
|
||||
playPauseButton.textContent = 'Pause AI Response';
|
||||
} else {
|
||||
aiAudio.pause();
|
||||
playPauseButton.textContent = 'Play AI Response';
|
||||
}
|
||||
isPaused = !isPaused;
|
||||
}
|
||||
};
|
||||
chatBox.appendChild(playPauseButton);
|
||||
}
|
||||
|
||||
// Update chat history reference (for external modules)
|
||||
export function updateChatHistory(newHistory) {
|
||||
chatHistory = newHistory;
|
||||
}
|
||||
|
||||
export function getChatHistory() {
|
||||
return chatHistory;
|
||||
}
|
||||
21
src/config.js
Normal file
21
src/config.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Configuration and endpoints for uncloseai.js
|
||||
|
||||
export const TTS_API_URL = "https://speech.ai.unturf.com/v1/audio/speech";
|
||||
export const MEGAPARCE_API_URL = "https://megaparce.ai.unturf.com/v1/file";
|
||||
export const API_KEY = "dummy-api-key";
|
||||
export const MODEL = "adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic"; // default model (always available)
|
||||
|
||||
// Dynamic Endpoints Configuration for Chat API
|
||||
export const VLLM_ENDPOINTS = [
|
||||
{ id: 'hermes.ai.unturf.com', url: 'https://hermes.ai.unturf.com/v1' },
|
||||
{ id: 'hermes2.ai.unturf.com', url: 'https://hermes2.ai.unturf.com/v1' }
|
||||
];
|
||||
|
||||
// Global state
|
||||
export let lastTTSInput = '';
|
||||
export let lastTTSResult = null;
|
||||
|
||||
export function setLastTTS(input, result) {
|
||||
lastTTSInput = input;
|
||||
lastTTSResult = result;
|
||||
}
|
||||
42
src/content.js
Normal file
42
src/content.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
// Content extraction and processing functionality
|
||||
|
||||
// Function to extract text content along with links and metadata from the webpage
|
||||
export function extractWebpageContent() {
|
||||
let content = '';
|
||||
|
||||
// Extract title
|
||||
const title = document.title;
|
||||
if (title) {
|
||||
content += `**Page Title**: ${title}\n\n`;
|
||||
}
|
||||
|
||||
// Extract meta description
|
||||
const metaDescription = document.querySelector('meta[name="description"]');
|
||||
if (metaDescription) {
|
||||
content += `**Meta Description**: ${metaDescription.content}\n\n`;
|
||||
}
|
||||
|
||||
// Extract other metadata (if needed)
|
||||
const metaKeywords = document.querySelector('meta[name="keywords"]');
|
||||
if (metaKeywords) {
|
||||
content += `**Meta Keywords**: ${metaKeywords.content}\n\n`;
|
||||
}
|
||||
|
||||
// Recursively extract text and links from the body content
|
||||
function getTextWithLinks(element) {
|
||||
if (element.nodeType === Node.TEXT_NODE) {
|
||||
content += element.textContent + ' ';
|
||||
} else if (element.nodeType === Node.ELEMENT_NODE) {
|
||||
if (element.tagName.toLowerCase() === 'a') {
|
||||
// If it's a link, append the text and the href
|
||||
content += `[${element.textContent}](${element.href}) `;
|
||||
} else {
|
||||
// Recursively process child nodes
|
||||
element.childNodes.forEach(getTextWithLinks);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getTextWithLinks(document.body);
|
||||
return content.trim();
|
||||
}
|
||||
146
src/file-upload.js
Normal file
146
src/file-upload.js
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
// File upload and processing functionality
|
||||
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
|
||||
import hljs from 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/es/highlight.min.js';
|
||||
import { MEGAPARCE_API_URL } from './config.js';
|
||||
import { speakText } from './tts.js';
|
||||
import { chatHistory } from './chat.js';
|
||||
|
||||
// Progress indicator functions
|
||||
export function showProgressIndicator(message) {
|
||||
// Remove existing indicator if any
|
||||
hideProgressIndicator();
|
||||
|
||||
const indicator = document.createElement('div');
|
||||
indicator.id = 'progress-indicator';
|
||||
indicator.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
z-index: 9999;
|
||||
`;
|
||||
indicator.textContent = message || 'Processing...';
|
||||
document.body.appendChild(indicator);
|
||||
}
|
||||
|
||||
export function hideProgressIndicator() {
|
||||
const indicator = document.getElementById('progress-indicator');
|
||||
if (indicator) {
|
||||
indicator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle file upload from input element
|
||||
export async function handleFileUpload() {
|
||||
const fileInput = document.getElementById('file-input');
|
||||
if (!fileInput.files[0]) {
|
||||
alert('Please select a file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showProgressIndicator('Uploading file...');
|
||||
const response = await uploadFile(fileInput.files[0]);
|
||||
hideProgressIndicator();
|
||||
|
||||
await integrateMegafarceResponse(response);
|
||||
fileInput.value = ''; // Clear the input after upload
|
||||
} catch (error) {
|
||||
console.error('File upload error:', error);
|
||||
alert('Failed to upload the file.');
|
||||
hideProgressIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
// Upload File to MegaFarce with Progress Indicator
|
||||
export async function uploadFile(file) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const response = await fetch(MEGAPARCE_API_URL, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Error uploading file: ${errorText}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
return responseData;
|
||||
}
|
||||
|
||||
// Integrate MegaFarce Response into Chat
|
||||
export async function integrateMegafarceResponse(response) {
|
||||
// Assuming the response contains a 'content' field with the parsed content
|
||||
const content = response.content || response.result || "No content received.";
|
||||
|
||||
// Add a system message with the uploaded content to provide context
|
||||
chatHistory.push({ role: "system", content: `Uploaded Context: ${content}` });
|
||||
|
||||
// Display the uploaded content in the chat box
|
||||
const chatBox = document.getElementById('chat-box');
|
||||
const systemMessage = document.createElement('p');
|
||||
systemMessage.innerHTML = `<strong>System:</strong> ${marked.parse(content)}`;
|
||||
|
||||
// Apply syntax highlighting to any code blocks
|
||||
systemMessage.querySelectorAll('pre code').forEach((block) => {
|
||||
hljs.highlightElement(block);
|
||||
});
|
||||
|
||||
chatBox.appendChild(systemMessage);
|
||||
chatBox.scrollTop = chatBox.scrollHeight;
|
||||
|
||||
// Add Generate TTS button for the uploaded content
|
||||
const generateTTSButton = document.createElement('button');
|
||||
generateTTSButton.textContent = 'Generate TTS for Uploaded Content';
|
||||
let ttsAudio = null;
|
||||
let ttsBlob = null;
|
||||
let isPaused = false;
|
||||
|
||||
generateTTSButton.onclick = async () => {
|
||||
if (!ttsAudio) {
|
||||
generateTTSButton.textContent = 'Processing...';
|
||||
generateTTSButton.disabled = true; // Disable button while processing
|
||||
try {
|
||||
const result = await speakText(content);
|
||||
ttsAudio = result.audio;
|
||||
ttsBlob = result.blob;
|
||||
generateTTSButton.textContent = 'Pause TTS';
|
||||
generateTTSButton.disabled = false; // Re-enable button after processing
|
||||
ttsAudio.play();
|
||||
|
||||
// Add download button for the TTS audio
|
||||
const downloadButton = document.createElement('button');
|
||||
downloadButton.textContent = 'Download MP3';
|
||||
downloadButton.onclick = () => {
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(ttsBlob);
|
||||
a.download = 'uploaded-content.mp3';
|
||||
a.click();
|
||||
};
|
||||
chatBox.appendChild(downloadButton);
|
||||
} catch (error) {
|
||||
console.error('Error generating TTS:', error);
|
||||
alert('Failed to generate TTS for the uploaded content.');
|
||||
generateTTSButton.textContent = 'Generate TTS for Uploaded Content';
|
||||
generateTTSButton.disabled = false;
|
||||
}
|
||||
} else {
|
||||
if (isPaused) {
|
||||
ttsAudio.play();
|
||||
generateTTSButton.textContent = 'Pause TTS';
|
||||
} else {
|
||||
ttsAudio.pause();
|
||||
generateTTSButton.textContent = 'Play TTS';
|
||||
}
|
||||
isPaused = !isPaused;
|
||||
}
|
||||
};
|
||||
chatBox.appendChild(generateTTSButton);
|
||||
}
|
||||
198
src/models.js
Normal file
198
src/models.js
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
// Model registry and selection functionality
|
||||
import { VLLM_ENDPOINTS } from './config.js';
|
||||
|
||||
// This registry maps a model's ID to the endpoint where it resides.
|
||||
export const modelRegistry = {};
|
||||
|
||||
// Fetch models from each endpoint with caching.
|
||||
// Cache is busted if the endpoint array changes or TTL.
|
||||
export async function fetchModelsFromEndpoints() {
|
||||
const cacheKey = 'modelRegistryCache';
|
||||
const endpointsKey = 'vllmEndpointsHash';
|
||||
const endpointsString = JSON.stringify(VLLM_ENDPOINTS);
|
||||
const cachedEndpoints = localStorage.getItem(endpointsKey);
|
||||
const cacheItem = localStorage.getItem(cacheKey);
|
||||
const now = Date.now();
|
||||
const TTL = 300000; // 5 minutes in milliseconds
|
||||
|
||||
if (cacheItem && cachedEndpoints === endpointsString) {
|
||||
try {
|
||||
const cachedData = JSON.parse(cacheItem);
|
||||
if (now - cachedData.timestamp < TTL) {
|
||||
// Restore cached modelRegistry
|
||||
Object.assign(modelRegistry, cachedData.modelRegistry);
|
||||
return cachedData.models;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error reading model registry from cache", e);
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid cache, fetch models from all endpoints
|
||||
const fetchPromises = VLLM_ENDPOINTS.map(async (endpoint) => {
|
||||
try {
|
||||
const res = await fetch(`${endpoint.url}/models`);
|
||||
if (!res.ok) throw new Error(`HTTP error! status: ${res.status} from ${endpoint.url}`);
|
||||
const jsonResponse = await res.json();
|
||||
// Expected JSON structure: { data: [ { id, ... }, ... ], object: "list" }
|
||||
const models = jsonResponse.data || [];
|
||||
// Map each model to include its endpoint ID, unique ID, and model name
|
||||
return models.map((model) => ({
|
||||
...model,
|
||||
modelName: model.id, // Explicitly store model name
|
||||
endpointId: endpoint.id,
|
||||
uniqueId: `${endpoint.id}-${model.id}` // Unique ID with endpoint ID first
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(`Error fetching models from ${endpoint.url}:`, error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
const allModelsArrays = await Promise.all(fetchPromises);
|
||||
const models = allModelsArrays.flat();
|
||||
|
||||
// Update modelRegistry with unique model instances
|
||||
models.forEach((model) => {
|
||||
modelRegistry[model.uniqueId] = {
|
||||
url: VLLM_ENDPOINTS.find((e) => e.id === model.endpointId).url,
|
||||
endpointId: model.endpointId
|
||||
};
|
||||
});
|
||||
|
||||
// Cache the results
|
||||
const cacheData = {
|
||||
timestamp: now,
|
||||
modelRegistry: modelRegistry,
|
||||
models: models
|
||||
};
|
||||
localStorage.setItem(cacheKey, JSON.stringify(cacheData));
|
||||
localStorage.setItem(endpointsKey, endpointsString);
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
// Create a dynamic drop-down for model selection
|
||||
// This function creates a <select> element and fills it with unique model instances.
|
||||
export async function createModelSelectionDropdown() {
|
||||
const models = await fetchModelsFromEndpoints();
|
||||
if (!models.length) {
|
||||
console.warn("No models returned from any endpoint.");
|
||||
return;
|
||||
}
|
||||
|
||||
const dropdown = document.createElement('select');
|
||||
dropdown.id = 'model-selection';
|
||||
dropdown.style.margin = '10px';
|
||||
|
||||
models.forEach((model) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = model.uniqueId; // Use unique ID for selection
|
||||
option.textContent = `${model.endpointId} | ${model.modelName}`;
|
||||
dropdown.appendChild(option);
|
||||
});
|
||||
|
||||
// Insert the dropdown directly above the chat input box.
|
||||
const userInput = document.getElementById("user-input");
|
||||
if (userInput && userInput.parentNode) {
|
||||
userInput.parentNode.parentNode.insertBefore(dropdown, userInput.parentNode);
|
||||
} else {
|
||||
// Fallback: insert at the top of the body.
|
||||
document.body.insertBefore(dropdown, document.body.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to add the Refresh Models button
|
||||
export function addRefreshModelsButton() {
|
||||
// Create a container for the button
|
||||
const container = document.createElement('div');
|
||||
container.id = 'refresh-models-container';
|
||||
|
||||
// Position it near the model selection dropdown
|
||||
const userInput = document.getElementById("user-input");
|
||||
if (userInput && userInput.parentNode) {
|
||||
userInput.parentNode.parentNode.insertBefore(container, userInput.parentNode);
|
||||
}
|
||||
|
||||
// Create a button element
|
||||
const refreshButton = document.createElement('button');
|
||||
refreshButton.textContent = 'Refresh Models';
|
||||
refreshButton.style.margin = '10px';
|
||||
refreshButton.onclick = async function() {
|
||||
refreshButton.textContent = 'Refreshing...';
|
||||
refreshButton.disabled = true;
|
||||
|
||||
try {
|
||||
// Clear the model registry cache
|
||||
localStorage.removeItem('modelRegistryCache');
|
||||
localStorage.removeItem('vllmEndpointsHash');
|
||||
|
||||
// Fetch models again
|
||||
const models = await fetchModelsFromEndpoints();
|
||||
|
||||
// Update the dropdown
|
||||
const dropdown = document.getElementById('model-selection');
|
||||
if (dropdown) {
|
||||
// Clear existing options
|
||||
dropdown.innerHTML = '';
|
||||
|
||||
// Add options using models array
|
||||
models.forEach((model) => {
|
||||
const option = document.createElement('option');
|
||||
option.value = model.uniqueId;
|
||||
option.textContent = `${model.endpointId} | ${model.modelName}`;
|
||||
dropdown.appendChild(option);
|
||||
});
|
||||
}
|
||||
|
||||
alert('Models refreshed successfully!');
|
||||
} catch (error) {
|
||||
console.error('Error refreshing models:', error);
|
||||
alert('Failed to refresh models. Please try again.');
|
||||
} finally {
|
||||
refreshButton.textContent = 'Refresh Models';
|
||||
refreshButton.disabled = false;
|
||||
}
|
||||
};
|
||||
container.appendChild(refreshButton);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return modalDropdown.value;
|
||||
}
|
||||
|
||||
// Fallback to main dropdown
|
||||
const dropdown = document.getElementById("model-selection");
|
||||
if (dropdown && dropdown.value) {
|
||||
return dropdown.value;
|
||||
}
|
||||
|
||||
// Return default model if nothing selected
|
||||
const defaultModel = Object.keys(modelRegistry)[0];
|
||||
if (defaultModel) {
|
||||
return defaultModel;
|
||||
}
|
||||
|
||||
// Fallback to hardcoded model
|
||||
return "hermes.ai.unturf.com-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";
|
||||
}
|
||||
68
src/page-reader.js
Normal file
68
src/page-reader.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
// Page reading functionality
|
||||
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();
|
||||
|
||||
// Create status indicator
|
||||
const statusDiv = document.createElement('div');
|
||||
statusDiv.style.position = 'fixed';
|
||||
statusDiv.style.top = '10px';
|
||||
statusDiv.style.right = '10px';
|
||||
statusDiv.style.padding = '10px';
|
||||
statusDiv.style.background = 'rgba(0,0,0,0.8)';
|
||||
statusDiv.style.color = 'white';
|
||||
statusDiv.style.borderRadius = '5px';
|
||||
statusDiv.style.zIndex = '1000';
|
||||
document.body.appendChild(statusDiv);
|
||||
|
||||
statusDiv.textContent = 'Processing content and generating speech...';
|
||||
|
||||
const processedContent = await processContentWithHermes(content);
|
||||
|
||||
// Generate TTS with default voice and 90% speed immediately
|
||||
const { audio, blob } = await speakText(processedContent, 'alloy', 0.9);
|
||||
|
||||
statusDiv.textContent = 'Reading page... ';
|
||||
|
||||
let isPaused = false;
|
||||
|
||||
// Add pause/resume button
|
||||
const pauseButton = document.createElement('button');
|
||||
pauseButton.textContent = 'Pause Reading';
|
||||
pauseButton.style.marginLeft = '10px';
|
||||
statusDiv.appendChild(pauseButton);
|
||||
|
||||
// Add download button
|
||||
const downloadButton = document.createElement('button');
|
||||
downloadButton.textContent = 'Download MP3';
|
||||
downloadButton.style.marginLeft = '10px';
|
||||
downloadButton.onclick = () => {
|
||||
const a = document.createElement('a');
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `${document.title.replace(/\s+/g, '-').toLowerCase()}.mp3`;
|
||||
a.click();
|
||||
};
|
||||
statusDiv.appendChild(downloadButton);
|
||||
|
||||
pauseButton.onclick = () => {
|
||||
if (isPaused) {
|
||||
audio.play();
|
||||
pauseButton.textContent = 'Pause Reading';
|
||||
} else {
|
||||
audio.pause();
|
||||
pauseButton.textContent = 'Resume Reading';
|
||||
}
|
||||
isPaused = !isPaused;
|
||||
};
|
||||
|
||||
// Set up audio end handler
|
||||
audio.onended = () => {
|
||||
statusDiv.remove();
|
||||
};
|
||||
|
||||
// Start playing immediately
|
||||
audio.play();
|
||||
}
|
||||
49
src/storage.js
Normal file
49
src/storage.js
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// Conversation history and localStorage functionality
|
||||
|
||||
// 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}`;
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
export function loadConversationHistory() {
|
||||
const saved = localStorage.getItem(getPageSpecificKey('hermes-conversation-history'));
|
||||
if (saved) {
|
||||
try {
|
||||
const parsedHistory = JSON.parse(saved);
|
||||
return parsedHistory;
|
||||
} catch (e) {
|
||||
console.error('Error parsing conversation history:', e);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function clearConversationHistory() {
|
||||
localStorage.removeItem(getPageSpecificKey('hermes-conversation-history'));
|
||||
return [
|
||||
{
|
||||
role: "system",
|
||||
content: "You are Hermes, a large language model from Nous Research, embedded as an AI assistant on this webpage. Your primary task is to help users understand and interact with the content of this specific page, while also being capable of assisting with any other topics, coding, creative tasks, or questions they may have. You should be conversational, helpful, and knowledgeable about the webpage content as well as general topics. Always strive to provide accurate, useful responses."
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Initialize chat history
|
||||
export function initializeChatHistory() {
|
||||
return [
|
||||
{
|
||||
role: "system",
|
||||
content: "You are Hermes, a large language model from Nous Research, embedded as an AI assistant on this webpage. Your primary task is to help users understand and interact with the content of this specific page, while also being capable of assisting with any other topics, coding, creative tasks, or questions they may have. You should be conversational, helpful, and knowledgeable about the webpage content as well as general topics. Always strive to provide accurate, useful responses."
|
||||
},
|
||||
...loadConversationHistory()
|
||||
];
|
||||
}
|
||||
176
src/tts.js
Normal file
176
src/tts.js
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
// Text-to-speech functionality
|
||||
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);
|
||||
|
||||
const response = await fetch(TTS_API_URL, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${API_KEY}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: 'tts-1',
|
||||
voice: voice,
|
||||
input: spokenText
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const audioBlob = await response.blob();
|
||||
const audioUrl = URL.createObjectURL(audioBlob);
|
||||
const audio = new Audio(audioUrl);
|
||||
audio.playbackRate = rate;
|
||||
|
||||
// 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.
|
||||
|
||||
Avoid reading ads.
|
||||
|
||||
Do not change or summarize content. Return the same words verbatim.
|
||||
|
||||
When you encounter a list or bullets, add . . . to make the TTS pause between items.
|
||||
|
||||
Try to stay as close to the truth of the original version as possible. Start with the title of the post and then jump into it.
|
||||
|
||||
Do not mention the TTS stream just do the work!
|
||||
|
||||
Remember your only goal is to extract the entire & exact main content of the article.
|
||||
|
||||
Ready? Breath and then return a stream of tokens to be used in a TTS system.
|
||||
`
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: content
|
||||
}
|
||||
],
|
||||
temperature: 0,
|
||||
max_tokens: 76000
|
||||
};
|
||||
|
||||
console.log("Sending payload to LLM for spoken tokens:", JSON.stringify(payload, null, 2));
|
||||
|
||||
try {
|
||||
const response = await fetch(`${getSelectedModelEndpoint()}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${API_KEY}`
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(`HTTP error! status: ${response.status}`, errorText);
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.choices[0].message.content;
|
||||
} catch (error) {
|
||||
console.error('Error in processContentWithHermes:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
console.log("Sending payload to Hermes for spoken tokens:", JSON.stringify(payload, null, 2));
|
||||
|
||||
try {
|
||||
const response = await fetch(`${getSelectedModelEndpoint()}/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${API_KEY}`
|
||||
},
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error(`HTTP error! status: ${response.status}`, errorText);
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data.choices[0].message.content;
|
||||
} catch (error) {
|
||||
console.error('Error in extractSpokenTokens:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to generate a title for TTS
|
||||
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
|
||||
})
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
539
src/ui.js
Normal file
539
src/ui.js
Normal file
|
|
@ -0,0 +1,539 @@
|
|||
// UI creation and management functionality
|
||||
import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
|
||||
import hljs from 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.8.0/es/highlight.min.js';
|
||||
import { extractWebpageContent } from './content.js';
|
||||
import { speakText } from './tts.js';
|
||||
import { handleFileUpload, uploadFile, showProgressIndicator, hideProgressIndicator } from './file-upload.js';
|
||||
import { readPageWithHermes } from './page-reader.js';
|
||||
import { handleUserInput, chatHistory } from './chat.js';
|
||||
|
||||
// Configuration flags
|
||||
const USE_CUSTOM_STYLING = window.UNCLOSEAI_CUSTOM_STYLING !== false;
|
||||
const SHOW_FLOATING_BUTTON = window.UNCLOSEAI_FLOATING_BUTTON !== false;
|
||||
|
||||
// Global variable to track modal state
|
||||
let hermesModalOpen = false;
|
||||
|
||||
// Initialize uncloseai elements based on class
|
||||
export function initializeUncloseaiElements() {
|
||||
const uncloseaiElements = document.querySelectorAll('.uncloseai');
|
||||
|
||||
uncloseaiElements.forEach(element => {
|
||||
const features = element.dataset.features || 'full';
|
||||
const type = element.dataset.type || 'standard';
|
||||
|
||||
// Create container for this uncloseai instance
|
||||
const container = document.createElement('div');
|
||||
container.className = 'uncloseai-container';
|
||||
container.style.cssText = 'width: 100%; margin: 10px 0;';
|
||||
|
||||
if (features === 'full' || type === 'full') {
|
||||
createFullInterface(container);
|
||||
} else {
|
||||
createCustomInterface(container, features.split(','));
|
||||
}
|
||||
|
||||
element.appendChild(container);
|
||||
});
|
||||
}
|
||||
|
||||
// Create full chat interface
|
||||
export function createFullInterface(container) {
|
||||
// Chat area
|
||||
const chatContainer = document.createElement('div');
|
||||
chatContainer.innerHTML = `
|
||||
<div id="chat-box" style="min-height: 200px; border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; overflow-y: auto; border-radius: 4px;"></div>
|
||||
<div style="display: flex; gap: 10px; margin-bottom: 10px;">
|
||||
<input type="text" id="user-input" placeholder="Ask about this page..." style="flex: 1; padding: 8px; border: 1px solid #ccc; border-radius: 4px;">
|
||||
<button onclick="handleUserInput()" style="padding: 8px 16px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">Send</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Control buttons
|
||||
const controlsDiv = document.createElement('div');
|
||||
controlsDiv.style.cssText = 'display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; margin-bottom: 10px;';
|
||||
|
||||
const readBtn = createButton('📖 Read Page', () => readPageWithHermes());
|
||||
const ttsBtn = createButton('🔊 TTS Anything', () => openTTSModal());
|
||||
const uploadBtn = createButton('📁 Upload File', () => document.querySelector('[data-uncloseai-file-input]')?.click());
|
||||
|
||||
controlsDiv.appendChild(readBtn);
|
||||
controlsDiv.appendChild(ttsBtn);
|
||||
controlsDiv.appendChild(uploadBtn);
|
||||
|
||||
// Hidden file input
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.setAttribute('data-uncloseai-file-input', '');
|
||||
fileInput.style.display = 'none';
|
||||
fileInput.onchange = handleFileUpload;
|
||||
|
||||
container.appendChild(chatContainer);
|
||||
container.appendChild(controlsDiv);
|
||||
container.appendChild(fileInput);
|
||||
}
|
||||
|
||||
// Create custom interface with specific features
|
||||
export function createCustomInterface(container, features) {
|
||||
const div = document.createElement('div');
|
||||
div.style.cssText = 'display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; padding: 15px; border: 1px solid #ddd; border-radius: 8px;';
|
||||
|
||||
features.forEach(feature => {
|
||||
switch(feature.trim()) {
|
||||
case 'chat':
|
||||
createChatFeature(div);
|
||||
break;
|
||||
case 'tts':
|
||||
createTTSFeature(div);
|
||||
break;
|
||||
case 'upload':
|
||||
createUploadFeature(div);
|
||||
break;
|
||||
case 'read':
|
||||
createReadFeature(div);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
container.appendChild(div);
|
||||
}
|
||||
|
||||
// Individual feature creators
|
||||
export function createChatFeature(container) {
|
||||
const chatDiv = document.createElement('div');
|
||||
chatDiv.innerHTML = `
|
||||
<h4>AI Chat</h4>
|
||||
<div style="border: 1px solid #ccc; height: 150px; padding: 8px; margin: 5px 0; overflow-y: auto;" data-chat-box></div>
|
||||
<input type="text" placeholder="Ask anything..." style="width: 100%; margin: 2px 0;" data-chat-input>
|
||||
<button onclick="handleCustomChat(this)" style="width: 100%; padding: 6px;">Send</button>
|
||||
`;
|
||||
container.appendChild(chatDiv);
|
||||
}
|
||||
|
||||
export function createTTSFeature(container) {
|
||||
const ttsDiv = document.createElement('div');
|
||||
ttsDiv.innerHTML = `
|
||||
<h4>Text to Speech</h4>
|
||||
<textarea placeholder="Enter text to speak..." style="width: 100%; height: 80px; margin: 5px 0;" data-tts-input></textarea>
|
||||
<button onclick="handleTTSFromElement(this)" style="width: 100%; padding: 6px;">🔊 Convert to Speech</button>
|
||||
<div data-tts-result style="margin: 5px 0;"></div>
|
||||
`;
|
||||
container.appendChild(ttsDiv);
|
||||
}
|
||||
|
||||
export function createUploadFeature(container) {
|
||||
const uploadDiv = document.createElement('div');
|
||||
uploadDiv.innerHTML = `
|
||||
<h4>File Upload</h4>
|
||||
<input type="file" style="width: 100%; margin: 5px 0;" data-upload-input>
|
||||
<button onclick="handleUploadFromElement(this)" style="width: 100%; padding: 6px;">📁 Upload & Analyze</button>
|
||||
<div data-upload-result style="margin: 5px 0; display: none;"></div>
|
||||
`;
|
||||
container.appendChild(uploadDiv);
|
||||
}
|
||||
|
||||
export function createReadFeature(container) {
|
||||
const readDiv = document.createElement('div');
|
||||
readDiv.innerHTML = `
|
||||
<h4>Page Reader</h4>
|
||||
<p style="font-size: 0.9em; margin: 5px 0;">Read this page with AI voice</p>
|
||||
<button onclick="readPageWithHermes()" style="width: 100%; padding: 8px 16px; background: #6f42c1; color: white; border: none; border-radius: 4px; cursor: pointer;">📖 Read Page with AI</button>
|
||||
`;
|
||||
container.appendChild(readDiv);
|
||||
}
|
||||
|
||||
// Helper functions for custom features
|
||||
export function createButton(text, onclick) {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = text;
|
||||
btn.onclick = onclick;
|
||||
btn.style.cssText = 'padding: 8px 12px; border: 1px solid #ccc; border-radius: 4px; background: white; cursor: pointer;';
|
||||
return btn;
|
||||
}
|
||||
|
||||
export async function handleTTSFromElement(button) {
|
||||
const container = button.closest('[data-tts-result]')?.parentElement || button.parentElement;
|
||||
const textarea = container.querySelector('[data-tts-input]');
|
||||
const resultDiv = container.querySelector('[data-tts-result]');
|
||||
const text = textarea?.value?.trim();
|
||||
|
||||
if (!text) {
|
||||
alert('Please enter some text first!');
|
||||
return;
|
||||
}
|
||||
|
||||
button.disabled = true;
|
||||
button.textContent = 'Converting...';
|
||||
resultDiv.innerHTML = '<em>Converting to speech...</em>';
|
||||
|
||||
try {
|
||||
const result = await speakText(text, 'alloy', 0.9);
|
||||
resultDiv.innerHTML = `
|
||||
<div style="margin: 10px 0;">
|
||||
<button onclick="this.previousElementSibling.play()" style="margin: 2px; padding: 4px 8px;">▶️ Play</button>
|
||||
<button onclick="this.previousElementSibling.previousElementSibling.pause()" style="margin: 2px; padding: 4px 8px;">⏸️ Pause</button>
|
||||
</div>
|
||||
`;
|
||||
resultDiv.insertBefore(result.audio, resultDiv.firstChild);
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = '<strong>Error:</strong> ' + error.message;
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = '🔊 Convert to Speech';
|
||||
}
|
||||
}
|
||||
|
||||
export async function handleUploadFromElement(button) {
|
||||
const container = button.parentElement;
|
||||
const fileInput = container.querySelector('[data-upload-input]');
|
||||
const resultDiv = container.querySelector('[data-upload-result]');
|
||||
|
||||
if (!fileInput.files[0]) {
|
||||
alert('Please select a file first!');
|
||||
return;
|
||||
}
|
||||
|
||||
button.disabled = true;
|
||||
button.textContent = 'Processing...';
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.innerHTML = '<em>Uploading and analyzing file...</em>';
|
||||
|
||||
try {
|
||||
showProgressIndicator('Processing file...');
|
||||
const response = await uploadFile(fileInput.files[0]);
|
||||
hideProgressIndicator();
|
||||
|
||||
resultDiv.innerHTML = `<strong>Analysis Result:</strong><br>${response}`;
|
||||
fileInput.value = '';
|
||||
} catch (error) {
|
||||
hideProgressIndicator();
|
||||
resultDiv.innerHTML = '<strong>Error:</strong> ' + error.message;
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = '📁 Upload & Analyze';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the legacy chat interface (for backward compatibility)
|
||||
export function initializeChatInterface() {
|
||||
// Only initialize if there are legacy elements (chat-container, user-input, etc.)
|
||||
const legacyElements = document.querySelector('#chat-container, #user-input, #chat-box');
|
||||
if (!legacyElements) return;
|
||||
|
||||
const pageContent = extractWebpageContent();
|
||||
chatHistory.push({
|
||||
role: "system",
|
||||
content: `Here's the content of the webpage: ${pageContent}`
|
||||
});
|
||||
|
||||
marked.setOptions({
|
||||
highlight: function(code, lang) {
|
||||
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
|
||||
return hljs.highlight(code, { language }).value;
|
||||
}
|
||||
});
|
||||
|
||||
// Add the Read Page button
|
||||
addReadPageButton();
|
||||
|
||||
// Add the File Upload button and picker
|
||||
addFileUploadButton();
|
||||
|
||||
// Event listeners
|
||||
document.getElementById("user-input")?.addEventListener("keydown", function(event) {
|
||||
if (event.key === "Enter" && !event.shiftKey) {
|
||||
event.preventDefault();
|
||||
handleUserInput();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add button to page
|
||||
export function addReadPageButton() {
|
||||
const button = document.createElement('button');
|
||||
button.textContent = 'Read Page';
|
||||
button.onclick = readPageWithHermes;
|
||||
button.style.margin = '10px';
|
||||
|
||||
const ttsButton = document.createElement('button');
|
||||
ttsButton.textContent = 'TTS Anything';
|
||||
ttsButton.onclick = openTTSModal;
|
||||
ttsButton.style.margin = '10px';
|
||||
|
||||
document.body.appendChild(button);
|
||||
document.body.appendChild(ttsButton);
|
||||
}
|
||||
|
||||
// Add file upload button
|
||||
export function addFileUploadButton() {
|
||||
const button = document.createElement('button');
|
||||
button.textContent = 'Upload File';
|
||||
button.style.margin = '10px';
|
||||
|
||||
const fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.id = 'file-input';
|
||||
fileInput.style.display = 'none';
|
||||
|
||||
button.onclick = () => fileInput.click();
|
||||
fileInput.onchange = handleFileUpload;
|
||||
|
||||
document.body.appendChild(button);
|
||||
document.body.appendChild(fileInput);
|
||||
}
|
||||
|
||||
// Function to create floating AI button
|
||||
export function createFloatingAIButton() {
|
||||
console.log('uncloseai.js: createFloatingAIButton() called.');
|
||||
// Remove any existing floating button first
|
||||
const existingButton = document.getElementById('floating-ai-button');
|
||||
if (existingButton) {
|
||||
console.log('uncloseai.js: Found existing floating button, removing it.');
|
||||
existingButton.remove();
|
||||
}
|
||||
|
||||
// Create the main floating button
|
||||
const floatingButton = document.createElement('button');
|
||||
floatingButton.id = 'floating-ai-button';
|
||||
floatingButton.textContent = 'uncloseai.';
|
||||
console.log('uncloseai.js: Created new floating button element.');
|
||||
|
||||
// Function to update button theme
|
||||
function updateButtonTheme() {
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark' ||
|
||||
(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches &&
|
||||
!document.documentElement.getAttribute('data-theme'));
|
||||
|
||||
floatingButton.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 10px;
|
||||
width: 110px;
|
||||
height: 55px;
|
||||
border-radius: 22px;
|
||||
background: ${isDark ? '#ffffff' : '#000000'};
|
||||
border: 2px solid ${isDark ? '#000000' : '#ffffff'};
|
||||
color: ${isDark ? '#000000' : '#ffffff'};
|
||||
font-family: 'ChunkFiveRegular', monospace;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
||||
z-index: 1000;
|
||||
transition: all 0.3s ease;
|
||||
max-width: calc(100vw - 20px);
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
}
|
||||
|
||||
// Initial theme setup
|
||||
updateButtonTheme();
|
||||
|
||||
// Watch for theme changes
|
||||
const observer = new MutationObserver(updateButtonTheme);
|
||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
||||
|
||||
// Watch for system theme changes
|
||||
if (window.matchMedia) {
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateButtonTheme);
|
||||
}
|
||||
|
||||
// Hover effects
|
||||
floatingButton.onmouseenter = () => {
|
||||
floatingButton.style.transform = 'scale(1.1)';
|
||||
floatingButton.style.boxShadow = '0 6px 16px rgba(0,0,0,0.4)';
|
||||
};
|
||||
floatingButton.onmouseleave = () => {
|
||||
floatingButton.style.transform = 'scale(1)';
|
||||
floatingButton.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)';
|
||||
};
|
||||
|
||||
// Toggle modal on click
|
||||
floatingButton.onclick = () => toggleHermesModal();
|
||||
|
||||
document.body.appendChild(floatingButton);
|
||||
console.log('uncloseai.js: Appended floating button to document.body.');
|
||||
}
|
||||
|
||||
// Function to toggle Hermes modal
|
||||
export function toggleHermesModal() {
|
||||
const existingModal = document.getElementById('hermes-modal');
|
||||
if (existingModal) {
|
||||
document.body.removeChild(existingModal);
|
||||
hermesModalOpen = false;
|
||||
} else {
|
||||
openHermesModal();
|
||||
hermesModalOpen = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to open Hermes modal
|
||||
export function openHermesModal() {
|
||||
// Create modal using dialog element
|
||||
const modal = document.createElement('dialog');
|
||||
modal.id = 'hermes-modal';
|
||||
modal.open = true;
|
||||
|
||||
if (USE_CUSTOM_STYLING) {
|
||||
modal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 90%;
|
||||
max-width: 800px;
|
||||
height: 90%;
|
||||
max-height: 700px;
|
||||
border: none;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
|
||||
background: white;
|
||||
z-index: 2000;
|
||||
margin: auto;
|
||||
`;
|
||||
} else {
|
||||
modal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
height: 100dvh;
|
||||
min-width: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
z-index: 2000;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
`;
|
||||
}
|
||||
|
||||
const article = document.createElement('article');
|
||||
if (USE_CUSTOM_STYLING) {
|
||||
article.style.cssText = `
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
`;
|
||||
} else {
|
||||
// Responsive scaling based on screen width
|
||||
const screenWidth = window.innerWidth;
|
||||
const scale = screenWidth < 480 ? 0.9 : screenWidth < 768 ? 0.8 : 0.75;
|
||||
|
||||
article.style.cssText = `
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: auto auto 1fr auto;
|
||||
transform: scale(${scale});
|
||||
transform-origin: top center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
`;
|
||||
}
|
||||
|
||||
// Create the modal content with full interface
|
||||
createFullInterface(article);
|
||||
|
||||
modal.appendChild(article);
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
// TTS Modal functionality
|
||||
export function openTTSModal() {
|
||||
const modal = document.createElement('dialog');
|
||||
modal.id = 'tts-modal';
|
||||
modal.open = true;
|
||||
|
||||
// Modal styling
|
||||
modal.style.cssText = `
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||
z-index: 3000;
|
||||
`;
|
||||
|
||||
const article = document.createElement('article');
|
||||
article.innerHTML = `
|
||||
<header>
|
||||
<h3>Text to Speech</h3>
|
||||
<button onclick="this.closest('dialog').remove()" style="float: right; background: none; border: none; font-size: 20px; cursor: pointer;">×</button>
|
||||
</header>
|
||||
<div>
|
||||
<textarea id="tts-modal-text" placeholder="Enter text to convert to speech..." style="width: 100%; height: 120px; margin: 10px 0; padding: 8px; border: 1px solid #ccc; border-radius: 4px;"></textarea>
|
||||
<div style="display: flex; gap: 10px; align-items: center; margin: 10px 0;">
|
||||
<label>Voice:</label>
|
||||
<select id="tts-modal-voice" style="flex: 1; padding: 4px;">
|
||||
<option value="alloy">Alloy</option>
|
||||
<option value="echo">Echo</option>
|
||||
<option value="fable">Fable</option>
|
||||
<option value="onyx">Onyx</option>
|
||||
<option value="nova">Nova</option>
|
||||
<option value="shimmer">Shimmer</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="tts-generate-btn" style="width: 100%; padding: 10px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer;">Generate Speech</button>
|
||||
<div id="tts-modal-result" style="margin: 15px 0;"></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.appendChild(article);
|
||||
|
||||
// Add generate button functionality
|
||||
const generateBtn = modal.querySelector('#tts-generate-btn');
|
||||
generateBtn.onclick = async () => {
|
||||
const text = modal.querySelector('#tts-modal-text').value.trim();
|
||||
const voice = modal.querySelector('#tts-modal-voice').value;
|
||||
const resultDiv = modal.querySelector('#tts-modal-result');
|
||||
|
||||
if (!text) {
|
||||
alert('Please enter some text first!');
|
||||
return;
|
||||
}
|
||||
|
||||
generateBtn.disabled = true;
|
||||
generateBtn.textContent = 'Generating...';
|
||||
resultDiv.innerHTML = '<em>Converting to speech...</em>';
|
||||
|
||||
try {
|
||||
const result = await speakText(text, voice, 1.0);
|
||||
resultDiv.innerHTML = `
|
||||
<div style="margin: 10px 0;">
|
||||
<button onclick="this.previousElementSibling.play()" style="margin: 2px; padding: 6px 12px;">▶️ Play</button>
|
||||
<button onclick="this.previousElementSibling.previousElementSibling.pause()" style="margin: 2px; padding: 6px 12px;">⏸️ Pause</button>
|
||||
</div>
|
||||
`;
|
||||
resultDiv.insertBefore(result.audio, resultDiv.firstChild);
|
||||
} catch (error) {
|
||||
resultDiv.innerHTML = '<strong>Error:</strong> ' + error.message;
|
||||
} finally {
|
||||
generateBtn.disabled = false;
|
||||
generateBtn.textContent = 'Generate Speech';
|
||||
}
|
||||
};
|
||||
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
// Initialize the system
|
||||
export function initializeSystem() {
|
||||
console.log('uncloseai.js: Initializing system');
|
||||
initializeChatInterface();
|
||||
|
||||
// Only create floating button if not disabled
|
||||
if (SHOW_FLOATING_BUTTON) {
|
||||
console.log('uncloseai.js: SHOW_FLOATING_BUTTON is true, calling createFloatingAIButton()');
|
||||
createFloatingAIButton();
|
||||
} else {
|
||||
console.log('uncloseai.js: SHOW_FLOATING_BUTTON is false, not creating floating button.');
|
||||
}
|
||||
|
||||
// Initialize class-based elements
|
||||
initializeUncloseaiElements();
|
||||
}
|
||||
2805
uncloseai.js
2805
uncloseai.js
File diff suppressed because it is too large
Load diff
2711
uncloseai.js.orig
Normal file
2711
uncloseai.js.orig
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue