// 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 { API_KEY, setSystemMessageAppend, TTS_API_URL, getAPIConfig } from "./config.js"; import { extractWebpageContent } from "./content.js"; import { detectPageLanguage } from "./language-detection.js"; import { speakText } from "./tts.js"; import { detectCurrentTheme, getThemeColors, initializeChunkFiveFont, } from "./ui-themes.js"; import { getUIText, getUserLanguagePreference, setUserLanguagePreference, } from "./ui-translations.js"; import { NATIVE_LANGUAGE_NAMES } from "./translation.js"; import { openTTSModal } from "./tts-modal.js"; import { openTranslateModal } from "./translate-modal.js"; import { createFullInterface, createCustomInterface, createButton, handleTTSFromElement, handleUploadFromElement, handleSmartTranslate, } from "./widget-library.js"; import { openUncloseaiEmbeddedModal, openUncloseaiEmbeddedModalNew, } from "./uncloseai-embed-modal.js"; // Refresh UI elements when language preference changes function refreshUILanguage() { console.log("UI language refresh triggered"); // Update existing button texts if modal is open const readBtn = document.querySelector('[onclick*="readPageWithHermes"]'); if (readBtn) readBtn.textContent = getUIText("readPage"); const ttsBtn = document.querySelector('[onclick*="openTTSModal"]'); if (ttsBtn) ttsBtn.textContent = getUIText("ttsAnything"); const translateBtn = document.querySelector( '[onclick*="openTranslateModal"]', ); if (translateBtn) translateBtn.textContent = getUIText("translate"); // Update smart translate buttons const smartTranslateBtns = document.querySelectorAll( '[onclick*="handleSmartTranslate"]', ); smartTranslateBtns.forEach( (btn) => (btn.textContent = getUIText("smartTranslate")), ); // Update labels if settings panel is open const modelLabel = document.querySelector( "#hermes-model-select", )?.previousElementSibling; if (modelLabel) modelLabel.textContent = getUIText("modelLabel"); const voiceLabel = document.querySelector( "#hermes-voice-select", )?.previousElementSibling; if (voiceLabel) voiceLabel.textContent = getUIText("voiceLabel"); const langLabel = document.querySelector( "#hermes-language-select", )?.previousElementSibling; if (langLabel) langLabel.textContent = getUIText("languageLabel"); } // Ensure font is loaded when module is imported if (typeof document !== "undefined") { initializeChunkFiveFont(); } // Helper function to add copy buttons to code blocks function addCodeBlockCopyButtons(element) { const codeBlocks = element.querySelectorAll("pre code"); codeBlocks.forEach((codeBlock) => { const pre = codeBlock.parentElement; if (pre.tagName.toLowerCase() === "pre") { // Make the pre element relative for positioning pre.style.position = "relative"; // Create copy button const copyBtn = document.createElement("button"); copyBtn.textContent = "📋"; copyBtn.title = "Copy code"; copyBtn.className = "uncloseai-code-copy-btn"; copyBtn.onclick = async (e) => { e.stopPropagation(); try { const codeText = codeBlock.textContent; await navigator.clipboard.writeText(codeText); const originalText = copyBtn.textContent; copyBtn.textContent = "✓"; setTimeout(() => { copyBtn.textContent = originalText; }, 1000); } catch (error) { alert(getUIText("failedToCopyCode", { error: error.message })); } }; pre.appendChild(copyBtn); } }); } // Send message with custom history (for intro generation) async function* sendMessageWithCustomHistory(messageHistory) { // Get API configuration (custom or default) const apiConfig = await getAPIConfig(); let apiUrl, headers, model; if (apiConfig.isCustom) { // Use custom API configuration apiUrl = `${apiConfig.endpoint}/chat/completions`; headers = apiConfig.headers; model = apiConfig.model; } else { // Use default Hermes configuration const { getSelectedModel, getSelectedModelEndpoint } = await import( "./models.js" ); apiUrl = `${getSelectedModelEndpoint()}/chat/completions`; headers = { "Content-Type": "application/json", Authorization: `Bearer ${API_KEY}`, }; model = getSelectedModel(); } // Import getSelectedModelMaxTokens to get dynamic max tokens const { getSelectedModelMaxTokens } = await import("./models.js"); const maxTokens = getSelectedModelMaxTokens(); console.log("UI (intro generation) using max_tokens:", maxTokens); const response = await fetch(apiUrl, { method: "POST", headers: headers, body: JSON.stringify({ model: model, messages: messageHistory, temperature: 0.3, max_tokens: maxTokens, stream: true, }), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } 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]; } } // Simple TTS function for chat messages (no preprocessing needed) async function speakChatText(text, voice = "alloy", rate = 1.0) { try { 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: text, }), }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const audioBlob = await response.blob(); const audioUrl = URL.createObjectURL(audioBlob); const audio = new Audio(audioUrl); audio.playbackRate = rate; return { audio, blob: audioBlob }; } catch (error) { console.error("Error in chat TTS:", error); throw error; } } import { chatHistory, handleUserInput, sendMessage } from "./chat.js"; import { addFileUploadButton, handleFileUpload, hideProgressIndicator, showProgressIndicator, uploadFile, } from "./file-upload.js"; import { fetchModelsFromEndpoints, getSelectedModel, getSelectedModelEndpoint, modelRegistry, } from "./models.js"; import { readPageWithHermes } from "./page-reader.js"; import { clearConversationHistory, getPageSpecificKey, loadConversationHistory, saveConversationHistory, } from "./storage.js"; import { SUPPORTED_LANGUAGES, translateCurrentPage, translateText, } from "./translation.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 uncloseaiEmbeddedModalOpen = 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 uncloseai-ui-container"; if (features === "full" || type === "full") { createFullInterface(container); } else { createCustomInterface(container, features.split(",")); } element.appendChild(container); }); } // Widget creation functions imported from widget-library.js // 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: (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", (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 = (event) => readPageWithHermes(event.target); button.className = "uncloseai-ui-button-margin"; const ttsButton = document.createElement("button"); ttsButton.textContent = "TTS Anything"; ttsButton.onclick = openTTSModal; ttsButton.className = "uncloseai-ui-button-margin"; document.body.appendChild(button); document.body.appendChild(ttsButton); } // File upload functionality moved to file-upload.js // Helper function to create and append the floating button function createAndAppendFloatingButton() { console.log("uncloseai.js: createAndAppendFloatingButton() called."); // Ensure ChunkFive font is loaded initializeChunkFiveFont(); // 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?.("(prefers-color-scheme: dark)").matches && !document.documentElement.getAttribute("data-theme")); // Set floating button theme colors via CSS custom properties const root = document.documentElement; root.style.setProperty('--floating-button-bg', isDark ? "#ffffff" : "#000000"); root.style.setProperty('--floating-button-border', isDark ? "#000000" : "#ffffff"); root.style.setProperty('--floating-button-text', isDark ? "#000000" : "#ffffff"); floatingButton.className = "uncloseai-floating-button"; } // 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 handled by CSS // Set up self-contained click handler setupFloatingButtonHandler(floatingButton); document.body.appendChild(floatingButton); console.log("uncloseai.js: Appended floating button to document.body."); } // Function to create floating AI button export function createFloatingAIButton() { console.log("uncloseai.js: createFloatingAIButton() called."); // Load CSS for floating button if not already loaded const hasPicoCSS = document.querySelector('link[href*="pico"]') !== null; const cssFile = hasPicoCSS ? 'uncloseai-modal-pico.css' : 'uncloseai-modal-builtin.css'; if (!document.querySelector(`link[href*="${cssFile}"]`)) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = `https://uncloseai.com/src/${cssFile}`; // Create button only after CSS loads link.onload = () => { console.log(`uncloseai.js: ${cssFile} loaded, creating floating button.`); createAndAppendFloatingButton(); }; link.onerror = () => { console.error(`uncloseai.js: Failed to load ${cssFile}, creating button anyway.`); createAndAppendFloatingButton(); }; document.head.appendChild(link); return; // Exit early, button will be created after CSS loads } // CSS already loaded, create button immediately createAndAppendFloatingButton(); } // Set up click handler for floating button (self-contained) function setupFloatingButtonHandler(floatingButton) { floatingButton.onclick = async () => { console.log("Floating button clicked!"); // Show loading state const originalText = floatingButton.textContent; floatingButton.textContent = "loading..."; floatingButton.disabled = true; floatingButton.classList.add("loading"); try { console.log("Trying to toggle Hermes modal..."); // Try multiple ways to access the modal function let modalFunction = null; if (typeof toggleUncloseaiEmbeddedModal === "function") { console.log("Found toggleUncloseaiEmbeddedModal in local scope"); modalFunction = toggleUncloseaiEmbeddedModal; } else if (typeof window.toggleUncloseaiEmbeddedModal === "function") { console.log("Found toggleUncloseaiEmbeddedModal on window"); modalFunction = window.toggleUncloseaiEmbeddedModal; } else { console.log( "toggleUncloseaiEmbeddedModal not found, trying dynamic import", ); // Try to import it dynamically as a fallback try { const uiModule = await import("./ui.js"); if (uiModule.toggleUncloseaiEmbeddedModal) { console.log("Successfully imported toggleUncloseaiEmbeddedModal"); modalFunction = uiModule.toggleUncloseaiEmbeddedModal; } } catch (importError) { console.error( "Could not import toggleUncloseaiEmbeddedModal:", importError, ); } } if (modalFunction) { console.log("Calling modal function"); await modalFunction(); } else { // Fallback: directly open the modal console.log("Using fallback to directly open modal"); await openUncloseaiEmbeddedModal(); } } catch (error) { console.error("Error opening modal:", error); alert(getUIText("errorOpeningModal", { error: error.message })); } finally { // Restore button state floatingButton.textContent = originalText; floatingButton.disabled = false; floatingButton.classList.remove("loading"); } }; } // New mobile-first Hermes modal // Modal functions extracted to uncloseai-embed-modal.js export function initializeSystem() { // Check for skip init flag - allow partial initialization for preview windows if (window.UNCLOSEAI_SKIP_INIT === true) { console.log("uncloseai.js: Partial initialization for preview window"); // Only create floating button in preview windows if not explicitly disabled if (window.UNCLOSEAI_FLOATING_BUTTON !== false) { console.log("uncloseai.js: Creating floating button in preview window"); // Ensure DOM is ready before creating button if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", createFloatingAIButton); } else { createFloatingAIButton(); } } return; } console.log("uncloseai.js: Full initialization"); 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(); } // Toggle function for floating button export async function toggleUncloseaiEmbeddedModal() { console.log("toggleUncloseaiEmbeddedModal called"); // Check if modal is already open const existingModal = document.getElementById("uncloseai-embedded-modal"); if (existingModal && window.uncloseaiEmbeddedModalOpen) { console.log("Modal is open, closing it"); existingModal.close(); document.body.removeChild(existingModal); window.uncloseaiEmbeddedModalOpen = false; } else { console.log("Modal is closed, opening it"); await openUncloseaiEmbeddedModal(); } } // Export modal functions globally for onclick handlers window.openTTSModal = openTTSModal; window.openTranslateModal = openTranslateModal; window.toggleUncloseaiEmbeddedModal = toggleUncloseaiEmbeddedModal;