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