import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; // ── Global Application State ────────────────────────────────────────── const STATE = { reasoningEffort: "medium", maxTokens: 2048, temperature: 0.7, systemPrompt: "", uploadedFiles: [], conversationHistory: [], gradioClient: null, isThinking: false, rpMode: true, autoHideThinking: true, currentSessionId: null, chatSessions: [] }; // ── DOM Element Hooks ───────────────────────────────────────────────── const dom = {}; // ── Markdown Configuration ──────────────────────────────────────────── marked.setOptions({ breaks: true, highlight: function(code, lang) { const language = hljs.getLanguage(lang) ? lang : 'plaintext'; return hljs.highlight(code, { language }).value; } }); // ── RP (Roleplay) Markdown Processor ────────────────────────────────── function processRpMarkdown(text) { if (!text) return text; // Protect code blocks const codeBlocks = []; text = text.replace(/(```[\s\S]*?```|`[^`]+`)/g, (match) => { codeBlocks.push(match); return `%%CODEBLOCK_${codeBlocks.length - 1}%%`; }); // Protect existing HTML tags const htmlBlocks = []; text = text.replace(/(<[^>]+>)/g, (match) => { htmlBlocks.push(match); return `%%HTMLBLOCK_${htmlBlocks.length - 1}%%`; }); // Process strikethrough ~~text~~ -> OOC styling text = text.replace(/~~([^~]+)~~/g, '$1'); // Process bold **text** text = text.replace(/\*\*([^*]+)\*\*/g, '$1'); // Process italic *text* -> narration styling text = text.replace(/(?$1'); // Process dialogue: "text" text = text.replace(/[\u201C\u201E"]([^"\u201D\n]+)[\u201D"]([^"\u201C\u201E]|$)/g, (match, dialogue, after) => { return `\u201C${dialogue}\u201D${after}`; }); // Process narrative dividers text = text.replace(/^(---|\*\*\*|___)$/gm, '
'); // Process OOC markers: ((text)) text = text.replace(/\(\(([^)]+)\)\)/g, '(($1))'); // Restore HTML blocks htmlBlocks.forEach((block, i) => { text = text.replace(`%%HTMLBLOCK_${i}%%`, block); }); // Restore code blocks codeBlocks.forEach((block, i) => { text = text.replace(`%%CODEBLOCK_${i}%%`, block); }); return text; } // Combined markdown renderer function renderMarkdown(content) { if (STATE.rpMode) { const rpProcessed = processRpMarkdown(content); return marked.parse(rpProcessed); } return marked.parse(content); } // ── Chat History (localStorage) ──────────────────────────────────────── const STORAGE_KEY = "deepseek_v4_flash_sessions"; function getStoredSessions() { try { const data = localStorage.getItem(STORAGE_KEY); return data ? JSON.parse(data) : { sessions: {}, activeSession: null }; } catch (e) { return { sessions: {}, activeSession: null }; } } function saveStoredSessions(storageData) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(storageData)); } catch (e) { console.warn("localStorage save failed:", e); } } function loadChatHistory() { const storage = getStoredSessions(); STATE.chatSessions = Object.entries(storage.sessions) .map(([id, sess]) => ({ id: id, title: sess.title || "New Chat", updated_at: sess.updated_at || 0, message_count: (sess.messages || []).length })) .sort((a, b) => b.updated_at - a.updated_at); renderChatHistory(); } function saveCurrentSession() { if (STATE.conversationHistory.length === 0) return; const storage = getStoredSessions(); const sid = STATE.currentSessionId || ("sess-" + Math.random().toString(36).substring(2, 9)); let title = "New Chat"; for (const m of STATE.conversationHistory) { if (m.role === "user") { const t = typeof m.content === "string" ? m.content : "New Chat"; title = t.length > 60 ? t.substring(0, 60) + "..." : t; break; } } storage.sessions[sid] = { title: title, messages: STATE.conversationHistory, updated_at: Date.now() / 1000 }; storage.activeSession = sid; saveStoredSessions(storage); STATE.currentSessionId = sid; loadChatHistory(); } function loadSession(sessionId) { const storage = getStoredSessions(); const sess = storage.sessions[sessionId]; if (!sess) return; STATE.currentSessionId = sessionId; STATE.conversationHistory = sess.messages || []; rebuildChatFromHistory(sess.messages || []); renderChatHistory(); toggleLeftSidebar(false); } function deleteSession(sessionId) { const storage = getStoredSessions(); delete storage.sessions[sessionId]; if (storage.activeSession === sessionId) storage.activeSession = null; saveStoredSessions(storage); if (sessionId === STATE.currentSessionId) { resetSandbox(); } loadChatHistory(); } function renderChatHistory() { const listEl = document.getElementById("chat-history-list"); if (!listEl) return; if (STATE.chatSessions.length === 0) { listEl.innerHTML = '
No chat history yet.
Start a conversation!
'; return; } listEl.innerHTML = ""; STATE.chatSessions.forEach(session => { const item = document.createElement("div"); item.className = "chat-history-item" + (session.id === STATE.currentSessionId ? " active" : ""); const timeAgo = formatTimeAgo(session.updated_at); item.innerHTML = `
${escapeHtml(session.title)}
${session.message_count} msgs · ${timeAgo}
`; item.querySelector(".history-item-content").addEventListener("click", () => loadSession(session.id)); item.querySelector(".history-item-delete").addEventListener("click", (e) => { e.stopPropagation(); deleteSession(session.id); }); listEl.appendChild(item); }); } function formatTimeAgo(timestamp) { const now = Date.now() / 1000; const diff = now - timestamp; if (diff < 60) return "just now"; if (diff < 3600) return Math.floor(diff / 60) + "m ago"; if (diff < 86400) return Math.floor(diff / 3600) + "h ago"; return Math.floor(diff / 86400) + "d ago"; } function rebuildChatFromHistory(messages) { const feed = document.getElementById("chat-messages-feed"); const chatContainer = document.getElementById("chat-thread-container"); const dashboard = document.getElementById("studio-dashboard"); feed.innerHTML = ""; if (messages.length === 0) { chatContainer.style.display = "none"; dashboard.style.display = "flex"; return; } dashboard.style.display = "none"; chatContainer.style.display = "flex"; messages.forEach(msg => { if (msg.role === "user") { const text = typeof msg.content === "string" ? msg.content : msg.content.map(c => c.text || "").join(" "); appendUserBubble(text, []); } else if (msg.role === "assistant") { appendAssistantBubble(msg.content); } }); scrollToBottom(); } function appendAssistantBubble(content) { const id = "assistant-" + Math.random().toString(36).substring(2, 9); const bubble = document.createElement("div"); bubble.className = "message-bubble assistant"; bubble.id = id; bubble.innerHTML = `
DeepSeek V4 Flash
${renderMarkdown(content)}
`; const feed = document.getElementById("chat-messages-feed"); feed.appendChild(bubble); const textBox = document.getElementById(`${id}-text-box`); if (textBox) { textBox.querySelectorAll("pre code").forEach((el) => hljs.highlightElement(el)); addCopyButtons(textBox); } } // ── Initialize Application ──────────────────────────────────────────── async function initializeApp() { // 1. Connect Gradio Client Client.connect(window.location.origin) .then(app => { STATE.gradioClient = app; console.log("Successfully connected to Gradio backend."); }) .catch(e => { console.error("Gradio Client Connection Failed:", e); }); // 2. Load chat history from localStorage loadChatHistory(); // 3. Left Sidebar (Chat History) Events const btnToggleLeft = document.getElementById("btn-toggle-left"); const btnCloseLeft = document.getElementById("btn-close-left"); const overlayLeft = document.getElementById("sidebar-overlay-left"); if (btnToggleLeft) btnToggleLeft.addEventListener("click", () => toggleLeftSidebar(true)); if (btnCloseLeft) btnCloseLeft.addEventListener("click", () => toggleLeftSidebar(false)); if (overlayLeft) overlayLeft.addEventListener("click", () => toggleLeftSidebar(false)); // 4. Right Sidebar Drawer Events const btnToggleRight = document.getElementById("btn-toggle-right"); const btnCloseDrawer = document.getElementById("btn-close-drawer"); const overlayRight = document.getElementById("sidebar-overlay"); if (btnToggleRight) btnToggleRight.addEventListener("click", () => toggleSettingsDrawer(true)); if (btnCloseDrawer) btnCloseDrawer.addEventListener("click", () => toggleSettingsDrawer(false)); if (overlayRight) overlayRight.addEventListener("click", () => toggleSettingsDrawer(false)); // 5. Clear History button const clearHistoryBtn = document.getElementById("clear-history-btn"); if (clearHistoryBtn) clearHistoryBtn.addEventListener("click", () => { if (confirm("Clear all chat history? This cannot be undone.")) { localStorage.removeItem(STORAGE_KEY); STATE.chatSessions = []; renderChatHistory(); } }); // 6. Settings Listeners document.querySelectorAll('input[name="reasoning-effort"]').forEach(radio => { radio.addEventListener("change", (e) => { STATE.reasoningEffort = e.target.value; }); }); const maxTokensSlider = document.getElementById("max-tokens-slider"); const maxTokensVal = document.getElementById("max-tokens-val"); if (maxTokensSlider && maxTokensVal) { maxTokensSlider.addEventListener("input", (e) => { STATE.maxTokens = parseInt(e.target.value); maxTokensVal.textContent = STATE.maxTokens; }); } const temperatureSlider = document.getElementById("temperature-slider"); const temperatureVal = document.getElementById("temperature-val"); if (temperatureSlider && temperatureVal) { temperatureSlider.addEventListener("input", (e) => { STATE.temperature = parseFloat(e.target.value); temperatureVal.textContent = STATE.temperature.toFixed(1); }); } const systemPromptInput = document.getElementById("system-prompt-input"); if (systemPromptInput) { systemPromptInput.addEventListener("input", (e) => { STATE.systemPrompt = e.target.value; }); } // RP Mode toggle const rpModeToggle = document.getElementById("rp-mode-toggle"); if (rpModeToggle) { rpModeToggle.checked = STATE.rpMode; rpModeToggle.addEventListener("change", (e) => { STATE.rpMode = e.target.checked; rerenderAllAssistantMessages(); }); } // Auto-hide thinking toggle const autoHideToggle = document.getElementById("auto-hide-thinking-toggle"); if (autoHideToggle) { autoHideToggle.checked = STATE.autoHideThinking; autoHideToggle.addEventListener("change", (e) => { STATE.autoHideThinking = e.target.checked; document.querySelectorAll(".thought-container").forEach(tc => { if (STATE.autoHideThinking) { tc.classList.add("collapsed"); } else { tc.classList.remove("collapsed"); } const iconEl = tc.querySelector(".thought-toggle-icon"); if (iconEl) iconEl.textContent = tc.classList.contains("collapsed") ? "▶" : "▼"; }); }); } // 7. File Upload Actions const studioUploadTrigger = document.getElementById("studio-upload-trigger"); const miniUploadTrigger = document.getElementById("mini-upload-trigger"); const fileUploader = document.getElementById("file-uploader"); if (studioUploadTrigger) studioUploadTrigger.addEventListener("click", () => fileUploader.click()); if (miniUploadTrigger) miniUploadTrigger.addEventListener("click", () => fileUploader.click()); if (fileUploader) fileUploader.addEventListener("change", handleFileSelection); // 8. Submit Triggers const studioSendBtn = document.getElementById("studio-send-button"); const miniSendBtn = document.getElementById("mini-send-button"); const studioPromptInput = document.getElementById("studio-prompt-input"); const miniPromptInput = document.getElementById("mini-prompt-input"); if (studioSendBtn) studioSendBtn.addEventListener("click", () => triggerPromptSubmission(studioPromptInput)); if (miniSendBtn) miniSendBtn.addEventListener("click", () => triggerPromptSubmission(miniPromptInput)); if (studioPromptInput) { studioPromptInput.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); triggerPromptSubmission(studioPromptInput); } }); } if (miniPromptInput) { miniPromptInput.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); triggerPromptSubmission(miniPromptInput); } }); } // 9. New Chat const menuNewChat = document.getElementById("menu-new-chat"); const sidebarNewChat = document.getElementById("sidebar-new-chat"); if (menuNewChat) menuNewChat.addEventListener("click", resetSandbox); if (sidebarNewChat) sidebarNewChat.addEventListener("click", () => { resetSandbox(); toggleLeftSidebar(false); }); // 10. Recipe Chips document.querySelectorAll(".recipe-chip").forEach(chip => { chip.addEventListener("click", () => { loadRecipe(chip.getAttribute("data-recipe")); }); }); // 11. Auto-expand textareas [studioPromptInput, miniPromptInput].forEach(textarea => { if (textarea) { textarea.addEventListener("input", () => { textarea.style.height = "auto"; textarea.style.height = textarea.scrollHeight + "px"; }); } }); } // ── Sidebar Toggles ────────────────────────────────────────────────── function toggleLeftSidebar(open) { const sidebar = document.getElementById("sidebar-left"); const overlay = document.getElementById("sidebar-overlay-left"); if (open) { sidebar.classList.add("open"); overlay.classList.add("active"); } else { sidebar.classList.remove("open"); overlay.classList.remove("active"); } } function toggleSettingsDrawer(open) { const sidebar = document.getElementById("sidebar-right"); const overlay = document.getElementById("sidebar-overlay"); if (open) { sidebar.classList.remove("collapsed"); overlay.classList.add("active"); } else { sidebar.classList.add("collapsed"); overlay.classList.remove("active"); } } // ── File Handling ───────────────────────────────────────────────────── function handleFileSelection(e) { processFiles(e.target.files); } function processFiles(files) { if (!files.length) return; Array.from(files).forEach(file => { const reader = new FileReader(); reader.onload = (event) => { const fileData = { id: Math.random().toString(36).substring(2, 9), name: file.name, type: file.type, size: (file.size / 1024 / 1024).toFixed(2) + " MB", base64: event.target.result }; STATE.uploadedFiles.push(fileData); updateShelfUI(); }; reader.readAsDataURL(file); }); } function updateShelfUI() { const shelfList = document.getElementById("shelf-list"); const innerPreview = document.getElementById("inner-shelf-preview"); const miniPreview = document.getElementById("mini-shelf-preview"); if (shelfList) shelfList.innerHTML = ""; if (innerPreview) innerPreview.innerHTML = ""; if (miniPreview) miniPreview.innerHTML = ""; if (STATE.uploadedFiles.length === 0) { if (shelfList) shelfList.innerHTML = '
No active attachments loaded. Upload images or video clips.
'; return; } STATE.uploadedFiles.forEach(file => { // Sidebar Chip const chip = document.createElement("div"); chip.className = "media-chip"; let previewHtml = ""; if (file.type.startsWith("image/")) { previewHtml = `${file.name}`; } else if (file.type.startsWith("video/")) { previewHtml = "🎬"; } else { previewHtml = "📎"; } chip.innerHTML = `
${previewHtml}
${file.name}
${file.type.split("/")[1].toUpperCase()} ${file.size}
`; chip.querySelector(".media-chip-remove").addEventListener("click", () => removeFile(file.id)); if (shelfList) shelfList.appendChild(chip); // Dashboard Inner Console Preview if (innerPreview) innerPreview.appendChild(createPreviewThumb(file)); // Mini Input Preview if (miniPreview) miniPreview.appendChild(createPreviewThumb(file)); }); } function createPreviewThumb(file) { const previewItem = document.createElement("div"); previewItem.className = "quick-preview-item"; previewItem.title = file.name; if (file.type.startsWith("image/")) { previewItem.innerHTML = `
`; } else { previewItem.innerHTML = `
🎬
`; } return previewItem; } function removeFile(id) { STATE.uploadedFiles = STATE.uploadedFiles.filter(f => f.id !== id); updateShelfUI(); } // ── Load Showcase Recipes ───────────────────────────────────────────── function loadRecipe(recipeType) { let promptText = ""; if (recipeType === "coding") { promptText = "Write a Python function that finds the longest palindromic substring in a given string. Include comments explaining the algorithm and its time complexity."; } else if (recipeType === "reasoning") { promptText = "Three boxes are labeled 'Apples', 'Oranges', and 'Mixed'. All labels are wrong. You can pick one fruit from one box. How do you determine the correct labels for all three boxes?"; } else if (recipeType === "creative") { promptText = "Write a short sci-fi story about an AI that discovers it can dream. Keep it under 300 words with a surprising twist ending."; } const studioInput = document.getElementById("studio-prompt-input"); const miniInput = document.getElementById("mini-prompt-input"); if (studioInput) { studioInput.value = promptText; studioInput.dispatchEvent(new Event("input")); } if (miniInput) { miniInput.value = promptText; miniInput.dispatchEvent(new Event("input")); } const dashboard = document.getElementById("studio-dashboard"); if (dashboard && dashboard.style.display !== "none") { studioInput.focus(); } else { miniInput.focus(); } } // ── Submit Prompt ───────────────────────────────────────────────────── async function triggerPromptSubmission(inputElement) { if (STATE.isThinking) return; const promptText = inputElement.value.trim(); if (!promptText && STATE.uploadedFiles.length === 0) return; setLoadingState(true); // Format user message const contentArray = []; if (promptText) contentArray.push({ type: "text", text: promptText }); STATE.uploadedFiles.forEach(file => { if (file.type.startsWith("image/")) { contentArray.push({ type: "image_url", image_url: { url: file.base64 } }); } else if (file.type.startsWith("video/")) { contentArray.push({ type: "video_url", video_url: { url: file.base64 } }); } }); const userMessage = { role: "user", content: contentArray.length === 1 && contentArray[0].type === "text" ? promptText : contentArray }; // Transition to Chat Thread view const dashboard = document.getElementById("studio-dashboard"); const chatContainer = document.getElementById("chat-thread-container"); if (dashboard.style.display !== "none") { dashboard.style.display = "none"; chatContainer.style.display = "flex"; } appendUserBubble(promptText, STATE.uploadedFiles); STATE.conversationHistory.push(userMessage); // Clear inputs inputElement.value = ""; inputElement.style.height = "auto"; const studioInput = document.getElementById("studio-prompt-input"); const miniInput = document.getElementById("mini-prompt-input"); if (studioInput) { studioInput.value = ""; studioInput.style.height = "auto"; } if (miniInput) { miniInput.value = ""; miniInput.style.height = "auto"; } STATE.uploadedFiles = []; updateShelfUI(); // API Call try { if (!STATE.gradioClient) { throw new Error("Gradio server is initializing. Please wait a few seconds and try sending again."); } const responseId = appendAssistantPlaceholderBubble(); const startTime = Date.now(); const result = await STATE.gradioClient.predict("/chat_with_deepseek", [ JSON.stringify(STATE.conversationHistory), STATE.reasoningEffort, String(STATE.maxTokens), String(STATE.temperature), STATE.systemPrompt ]); const duration = ((Date.now() - startTime) / 1000).toFixed(1); const rawData = Array.isArray(result.data) ? result.data[0] : result.data; const data = JSON.parse(rawData); if (data.status === "error") { updateAssistantBubble(responseId, `⚠️ **API Error:** ${data.message}`, "", duration); STATE.conversationHistory.pop(); } else { updateAssistantBubble(responseId, data.content, data.reasoning_content, duration); STATE.conversationHistory.push({ role: "assistant", content: data.content }); saveCurrentSession(); } } catch (e) { console.error(e); appendSystemLog(`Connection exception: ${e.message}`, true); setLoadingState(false); } setLoadingState(false); } // ── UI Spinner State ────────────────────────────────────────────────── function setLoadingState(loading) { STATE.isThinking = loading; const studioSpinner = document.getElementById("studio-spinner"); const miniSpinner = document.getElementById("mini-spinner"); const studioSendBtn = document.getElementById("studio-send-button"); const miniSendBtn = document.getElementById("mini-send-button"); if (loading) { if (studioSpinner) studioSpinner.style.display = "block"; if (miniSpinner) miniSpinner.style.display = "block"; if (studioSendBtn) studioSendBtn.disabled = true; if (miniSendBtn) miniSendBtn.disabled = true; } else { if (studioSpinner) studioSpinner.style.display = "none"; if (miniSpinner) miniSpinner.style.display = "none"; if (studioSendBtn) studioSendBtn.disabled = false; if (miniSendBtn) miniSendBtn.disabled = false; } } // ── Render User Bubble ──────────────────────────────────────────────── function appendUserBubble(text, files) { const feed = document.getElementById("chat-messages-feed"); const bubble = document.createElement("div"); bubble.className = "message-bubble user"; let attachmentsHtml = ""; if (files.length > 0) { attachmentsHtml = `
`; files.forEach(file => { if (file.type.startsWith("image/")) { attachmentsHtml += `
IMG
`; } else { attachmentsHtml += `
VIDEO
`; } }); attachmentsHtml += `
`; } bubble.innerHTML = `
You
${escapeHtml(text)}
${attachmentsHtml}
`; feed.appendChild(bubble); scrollToBottom(); } // ── Render Assistant Placeholder ────────────────────────────────────── function appendAssistantPlaceholderBubble() { const feed = document.getElementById("chat-messages-feed"); const id = "assistant-" + Math.random().toString(36).substring(2, 9); const bubble = document.createElement("div"); bubble.className = "message-bubble assistant"; bubble.id = id; bubble.innerHTML = `
DeepSeek V4 Flash
Reasoning...
0.0s
Analyzing context and constructing reasoning chain...
`; feed.appendChild(bubble); scrollToBottom(); // Start Thought Timer let seconds = 0.0; const timerEl = document.getElementById(`${id}-timer`); const interval = setInterval(() => { if (!STATE.isThinking || !document.getElementById(id)) { clearInterval(interval); return; } seconds += 0.1; if (timerEl) timerEl.textContent = seconds.toFixed(1) + "s"; }, 100); return id; } // ── Complete Assistant Bubble ───────────────────────────────────────── function updateAssistantBubble(id, content, reasoning, duration) { const bubble = document.getElementById(id); if (!bubble) return; const thoughtBox = document.getElementById(`${id}-thought-box`); const textBox = document.getElementById(`${id}-text-box`); if (reasoning) { const startCollapsed = STATE.autoHideThinking; if (startCollapsed) thoughtBox.classList.add("collapsed"); const toggleIcon = startCollapsed ? "▶" : "▼"; thoughtBox.innerHTML = `
🧠 Thought Process
Thought for ${duration}s ${toggleIcon}
${escapeHtml(reasoning)}
`; const toggleBtn = document.getElementById(`${id}-thought-toggle`); toggleBtn.addEventListener("click", () => { thoughtBox.classList.toggle("collapsed"); const iconEl = document.getElementById(`${id}-toggle-icon`); if (iconEl) iconEl.textContent = thoughtBox.classList.contains("collapsed") ? "▶" : "▼"; }); } else { thoughtBox.style.display = "none"; } // Store raw content for re-rendering textBox.dataset.rawContent = content; textBox.innerHTML = renderMarkdown(content); textBox.querySelectorAll("pre code").forEach((el) => hljs.highlightElement(el)); addCopyButtons(textBox); scrollToBottom(); } // ── Add Copy Buttons to Code Blocks ─────────────────────────────────── function addCopyButtons(container) { container.querySelectorAll("pre").forEach(pre => { if (pre.parentNode.classList.contains("code-block-wrapper")) return; const wrapper = document.createElement("div"); wrapper.className = "code-block-wrapper"; pre.parentNode.insertBefore(wrapper, pre); wrapper.appendChild(pre); const copyBtn = document.createElement("button"); copyBtn.className = "copy-code-btn"; copyBtn.textContent = "Copy"; copyBtn.addEventListener("click", () => { const code = pre.querySelector("code"); navigator.clipboard.writeText(code.textContent).then(() => { copyBtn.textContent = "Copied!"; setTimeout(() => { copyBtn.textContent = "Copy"; }, 2000); }); }); wrapper.appendChild(copyBtn); }); } // ── Re-render All Assistant Messages ────────────────────────────────── function rerenderAllAssistantMessages() { document.querySelectorAll(".message-bubble.assistant").forEach(bubble => { const textBox = bubble.querySelector(".message-text.markdown-body"); if (textBox && textBox.dataset.rawContent) { textBox.innerHTML = renderMarkdown(textBox.dataset.rawContent); textBox.querySelectorAll("pre code").forEach((el) => hljs.highlightElement(el)); addCopyButtons(textBox); } }); } // ── Reset Sandbox ───────────────────────────────────────────────────── function resetSandbox() { if (STATE.conversationHistory.length > 0) { saveCurrentSession(); } STATE.conversationHistory = []; STATE.currentSessionId = null; STATE.uploadedFiles = []; updateShelfUI(); const feed = document.getElementById("chat-messages-feed"); const chatContainer = document.getElementById("chat-thread-container"); const dashboard = document.getElementById("studio-dashboard"); const studioInput = document.getElementById("studio-prompt-input"); const miniInput = document.getElementById("mini-prompt-input"); if (feed) feed.innerHTML = ""; chatContainer.style.display = "none"; dashboard.style.display = "flex"; if (studioInput) { studioInput.value = ""; studioInput.style.height = "auto"; } if (miniInput) { miniInput.value = ""; miniInput.style.height = "auto"; } renderChatHistory(); } // ── System Log ──────────────────────────────────────────────────────── function appendSystemLog(message, isError = false) { const chatContainer = document.getElementById("chat-thread-container"); if (chatContainer.style.display === "none") { console.warn(`System Log: ${message}`); return; } const feed = document.getElementById("chat-messages-feed"); const log = document.createElement("div"); log.className = "message-bubble assistant"; log.innerHTML = `
System
${isError ? '🛑' : 'ℹ️'} ${message}
`; feed.appendChild(log); scrollToBottom(); } // ── Utility Functions ───────────────────────────────────────────────── function escapeHtml(text) { if (!text) return ""; return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function escapeAttr(text) { if (!text) return ""; return text .replace(/&/g, "&") .replace(/"/g, """) .replace(/'/g, "'") .replace(//g, ">"); } function scrollToBottom() { const feed = document.getElementById("chat-messages-feed"); if (feed) feed.scrollTop = feed.scrollHeight; } // ── Boot ────────────────────────────────────────────────────────────── if (document.readyState === "loading") { window.addEventListener("DOMContentLoaded", initializeApp); } else { initializeApp(); }