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 = `
`;
} else if (file.type.startsWith("video/")) {
previewHtml = "🎬";
} else {
previewHtml = "📎";
}
chip.innerHTML = `
${previewHtml}
`;
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 += `
`;
}
});
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
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 = `
${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();
}