import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; // Global Application State Management const STATE = { reasoningEffort: "medium", maxTokens: 2048, temperature: 0.7, uploadedFiles: [], // Current prompt attachments conversationHistory: [], // Sent to StepFun API gradioClient: null, isThinking: false }; // DOM Elements hooks const dom = { effortRadioButtons: document.querySelectorAll('input[name="reasoning-effort"]'), maxTokensSlider: document.getElementById("max-tokens-slider"), maxTokensVal: document.getElementById("max-tokens-val"), temperatureSlider: document.getElementById("temperature-slider"), temperatureVal: document.getElementById("temperature-val"), // Sidebar Drawer and Overlay Elements (Mobile & Minimalist) sidebarRight: document.getElementById("sidebar-right"), sidebarOverlay: document.getElementById("sidebar-overlay"), btnToggleRight: document.getElementById("btn-toggle-right"), btnCloseDrawer: document.getElementById("btn-close-drawer"), // Viewports studioDashboard: document.getElementById("studio-dashboard"), chatThreadContainer: document.getElementById("chat-thread-container"), chatMessagesFeed: document.getElementById("chat-messages-feed"), // Main Console Box Elements (Dashboard view) studioPromptInput: document.getElementById("studio-prompt-input"), innerShelfPreview: document.getElementById("inner-shelf-preview"), studioUploadTrigger: document.getElementById("studio-upload-trigger"), studioSendBtn: document.getElementById("studio-send-button"), studioSpinner: document.getElementById("studio-spinner"), // Mini Console Box Elements (Chat thread view) miniPromptInput: document.getElementById("mini-prompt-input"), miniShelfPreview: document.getElementById("mini-shelf-preview"), miniUploadTrigger: document.getElementById("mini-upload-trigger"), miniSendBtn: document.getElementById("mini-send-button"), miniSpinner: document.getElementById("mini-spinner"), // Core file upload elements fileUploader: document.getElementById("file-uploader"), shelfList: document.getElementById("shelf-list"), dropZone: document.getElementById("drop-zone"), // Action Resets menuNewChat: document.getElementById("menu-new-chat"), clearChatBtn: document.getElementById("clear-chat-button"), // Showcase recipe chips recipeChips: document.querySelectorAll(".recipe-chip") }; // Markdown configuration marked.setOptions({ breaks: true, highlight: function(code, lang) { const language = hljs.getLanguage(lang) ? lang : 'plaintext'; return hljs.highlight(code, { language }).value; } }); // Setup Initial State & Event Handlers async function initializeApp() { // 1. Connect Gradio Client in background (Non-blocking) Client.connect(window.location.origin) .then(app => { STATE.gradioClient = app; console.log("Successfully connected to Gradio.Server backend."); }) .catch(e => { console.error("Gradio Client Connection Failed:", e); }); // 2. Register Sidebar Drawer Slide Events (Drawer + Overlay) if (dom.btnToggleRight) dom.btnToggleRight.addEventListener("click", () => toggleSettingsDrawer(true)); if (dom.btnCloseDrawer) dom.btnCloseDrawer.addEventListener("click", () => toggleSettingsDrawer(false)); if (dom.sidebarOverlay) dom.sidebarOverlay.addEventListener("click", () => toggleSettingsDrawer(false)); // 3. Register Settings Listeners if (dom.effortRadioButtons) { dom.effortRadioButtons.forEach(radio => { radio.addEventListener("change", (e) => { STATE.reasoningEffort = e.target.value; }); }); } if (dom.maxTokensSlider && dom.maxTokensVal) { dom.maxTokensSlider.addEventListener("input", (e) => { STATE.maxTokens = parseInt(e.target.value); dom.maxTokensVal.textContent = STATE.maxTokens; }); } if (dom.temperatureSlider && dom.temperatureVal) { dom.temperatureSlider.addEventListener("input", (e) => { STATE.temperature = parseFloat(e.target.value); dom.temperatureVal.textContent = STATE.temperature.toFixed(1); }); } // 5. Register File Upload Actions if (dom.studioUploadTrigger) dom.studioUploadTrigger.addEventListener("click", () => dom.fileUploader.click()); if (dom.miniUploadTrigger) dom.miniUploadTrigger.addEventListener("click", () => dom.fileUploader.click()); if (dom.dropZone) dom.dropZone.addEventListener("click", () => dom.fileUploader.click()); if (dom.fileUploader) dom.fileUploader.addEventListener("change", handleFileSelection); // Dropzone Drag-and-Drop animations if (dom.dropZone) { ["dragenter", "dragover"].forEach(eventName => { dom.dropZone.addEventListener(eventName, (e) => { e.preventDefault(); dom.dropZone.classList.add("drag-active"); }, false); }); ["dragleave", "drop"].forEach(eventName => { dom.dropZone.addEventListener(eventName, (e) => { e.preventDefault(); dom.dropZone.classList.remove("drag-active"); }, false); }); dom.dropZone.addEventListener("drop", (e) => { const dt = e.dataTransfer; const files = dt.files; processFiles(files); }); } // 6. Submit Triggers if (dom.studioSendBtn) { dom.studioSendBtn.addEventListener("click", () => triggerPromptSubmission(dom.studioPromptInput)); } if (dom.miniSendBtn) { dom.miniSendBtn.addEventListener("click", () => triggerPromptSubmission(dom.miniPromptInput)); } if (dom.studioPromptInput) { dom.studioPromptInput.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); triggerPromptSubmission(dom.studioPromptInput); } }); } if (dom.miniPromptInput) { dom.miniPromptInput.addEventListener("keydown", (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); triggerPromptSubmission(dom.miniPromptInput); } }); } // 7. Resets if (dom.menuNewChat) dom.menuNewChat.addEventListener("click", resetSandbox); if (dom.clearChatBtn) dom.clearChatBtn.addEventListener("click", resetSandbox); // 8. Recipe Chips Console Setup if (dom.recipeChips) { dom.recipeChips.forEach(chip => { chip.addEventListener("click", () => { const recipeType = chip.getAttribute("data-recipe"); loadRecipe(recipeType); }); }); } // Auto-expand input textareas [dom.studioPromptInput, dom.miniPromptInput].forEach(textarea => { if (textarea) { textarea.addEventListener("input", () => { textarea.style.height = "auto"; textarea.style.height = (textarea.scrollHeight) + "px"; }); } }); } // Drawer Toggler Action function toggleSettingsDrawer(open) { if (open) { if (dom.sidebarRight) dom.sidebarRight.classList.remove("collapsed"); if (dom.sidebarOverlay) dom.sidebarOverlay.classList.add("active"); } else { if (dom.sidebarRight) dom.sidebarRight.classList.add("collapsed"); if (dom.sidebarOverlay) dom.sidebarOverlay.classList.remove("active"); } } // Handle File Select & Base64 Encoder 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); }); } // Update UI Attachment Previews function updateShelfUI() { if (dom.shelfList) dom.shelfList.innerHTML = ""; if (dom.innerShelfPreview) dom.innerShelfPreview.innerHTML = ""; if (dom.miniShelfPreview) dom.miniShelfPreview.innerHTML = ""; if (STATE.uploadedFiles.length === 0) { if (dom.shelfList) dom.shelfList.innerHTML = `
No active attachments loaded. Upload images or video clips.
`; return; } STATE.uploadedFiles.forEach(file => { // 1. 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 (dom.shelfList) dom.shelfList.appendChild(chip); // 2. Dashboard Inner Console Preview const previewItemDash = createPreviewThumb(file); if (dom.innerShelfPreview) dom.innerShelfPreview.appendChild(previewItemDash); // 3. Mini Input Preview const previewItemMini = createPreviewThumb(file); if (dom.miniShelfPreview) dom.miniShelfPreview.appendChild(previewItemMini); }); } 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 Cookbook Showcase Recipes function loadRecipe(recipeType) { const mockImageBase64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="; STATE.uploadedFiles = []; let promptText = ""; if (recipeType === "whiteboard") { promptText = "Here is a snapshot of our whiteboard project plan sketch. Translate this visual sequence of tasks and boxes into a clean, itemized roadmap plan with a structured markdown table."; STATE.uploadedFiles.push({ id: "recipe-whiteboard", name: "whiteboard_gantt.png", type: "image/png", size: "0.02 MB", base64: mockImageBase64 }); } else if (recipeType === "diagnostic") { promptText = "This is a recorded visual sequence of steps leading to a runtime exception crash. Provide a detailed reconstruction of the event timeline, summarize the diagnostic signals, and suggest an engineering hotfix."; STATE.uploadedFiles.push({ id: "recipe-diagnostics", name: "console_bug.mp4", type: "video/mp4", size: "1.45 MB", base64: mockImageBase64 }); } // Set value in BOTH text areas dom.studioPromptInput.value = promptText; dom.miniPromptInput.value = promptText; updateShelfUI(); dom.studioPromptInput.dispatchEvent(new Event("input")); dom.miniPromptInput.dispatchEvent(new Event("input")); // Focus active textarea if (dom.studioDashboard.style.display !== "none") { dom.studioPromptInput.focus(); } else { dom.miniPromptInput.focus(); } } // Submit prompt values to Gradio.Server API async function triggerPromptSubmission(inputElement) { if (STATE.isThinking) return; const promptText = inputElement.value.trim(); if (!promptText && STATE.uploadedFiles.length === 0) return; setLoadingState(true); // 1. Format user message contents const contentArray = []; if (promptText) { contentArray.push({ type: "text", text: promptText }); } // Attachments 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 }; // 2. Transition dashboard to Chat Thread view if (dom.studioDashboard.style.display !== "none") { dom.studioDashboard.style.display = "none"; dom.chatThreadContainer.style.display = "flex"; } // Append to UI thread list appendUserBubble(promptText, STATE.uploadedFiles); // Append to backend log history STATE.conversationHistory.push(userMessage); // Clear active UI containers dom.studioPromptInput.value = ""; dom.miniPromptInput.value = ""; dom.studioPromptInput.style.height = "auto"; dom.miniPromptInput.style.height = "auto"; STATE.uploadedFiles = []; updateShelfUI(); // 3. Connect 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(); // Call our gradio.Server api endpoint using standard positional array arguments const result = await STATE.gradioClient.predict("/chat_with_step", [ JSON.stringify(STATE.conversationHistory), STATE.reasoningEffort, STATE.maxTokens, STATE.temperature ]); 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(); // Remove failed prompt } else { updateAssistantBubble(responseId, data.content, data.reasoning_content, duration); STATE.conversationHistory.push({ role: "assistant", content: data.content }); } } catch (e) { console.error(e); appendSystemLog(`Connection exception: ${e.message}`, true); setLoadingState(false); } setLoadingState(false); } // UI spinner state toggles function setLoadingState(loading) { STATE.isThinking = loading; if (loading) { dom.studioSpinner.style.display = "block"; dom.miniSpinner.style.display = "block"; dom.studioSendBtn.disabled = true; dom.miniSendBtn.disabled = true; } else { dom.studioSpinner.style.display = "none"; dom.miniSpinner.style.display = "none"; dom.studioSendBtn.disabled = false; dom.miniSendBtn.disabled = false; } } // Render User Bubble function appendUserBubble(text, files) { 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 = `
User
${escapeHtml(text)}
${attachmentsHtml}
`; dom.chatMessagesFeed.appendChild(bubble); scrollToBottom(); } // Render Assistant Placeholder function appendAssistantPlaceholderBubble() { 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 = `
Step 3.7 Flash
Reasoning...
0.0s
Analyzing context and constructing reasoning chain...
`; dom.chatMessagesFeed.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; 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) { thoughtBox.innerHTML = `
🧠 Thought Process
Thought for ${duration}s â–ŧ
${escapeHtml(reasoning)}
`; const toggleBtn = document.getElementById(`${id}-thought-toggle`); toggleBtn.addEventListener("click", () => { thoughtBox.classList.toggle("collapsed"); }); } else { thoughtBox.style.display = "none"; } textBox.innerHTML = marked.parse(content); textBox.querySelectorAll("pre code").forEach((el) => { hljs.highlightElement(el); }); scrollToBottom(); } // Reset Sandbox Chat Context Logs function resetSandbox() { STATE.conversationHistory = []; STATE.uploadedFiles = []; updateShelfUI(); // Clear feed dom.chatMessagesFeed.innerHTML = ""; // Show dashboard dom.chatThreadContainer.style.display = "none"; dom.studioDashboard.style.display = "flex"; dom.studioPromptInput.value = ""; dom.miniPromptInput.value = ""; dom.studioPromptInput.style.height = "auto"; dom.miniPromptInput.style.height = "auto"; appendSystemLog("Workspace sandbox reset successful."); } function appendSystemLog(message, isError = false) { if (dom.chatThreadContainer.style.display === "none") { console.warn(`System Log: ${message}`); return; } const log = document.createElement("div"); log.className = "message-bubble assistant"; log.innerHTML = `
System
${isError ? '🛑' : 'â„šī¸'} ${message}
`; dom.chatMessagesFeed.appendChild(log); scrollToBottom(); } function escapeHtml(text) { if (!text) return ""; return text .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function scrollToBottom() { dom.chatMessagesFeed.scrollTop = dom.chatMessagesFeed.scrollHeight; } // Initialise application when DOM is fully set up window.addEventListener("DOMContentLoaded", initializeApp);