Spaces:
Running
Running
| 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 = `<div class="empty-shelf-text">No active attachments loaded. Upload images or video clips.</div>`; | |
| 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 = `<img src="${file.base64}" alt="${file.name}">`; | |
| } else if (file.type.startsWith("video/")) { | |
| previewHtml = `🎬`; | |
| } else { | |
| previewHtml = `📎`; | |
| } | |
| chip.innerHTML = ` | |
| <div class="media-chip-preview">${previewHtml}</div> | |
| <div class="media-chip-details"> | |
| <div class="media-chip-name">${file.name}</div> | |
| <div class="media-chip-meta"> | |
| <span>${file.type.split("/")[1].toUpperCase()}</span> | |
| <span>${file.size}</span> | |
| </div> | |
| </div> | |
| <button class="media-chip-remove" data-id="${file.id}"> | |
| <svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> | |
| </button> | |
| `; | |
| 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 = `<img src="${file.base64}"><div class="quick-preview-badge"></div>`; | |
| } else { | |
| previewItem.innerHTML = `<div class="media-chip-preview" style="width:100%;height:100%;">🎬</div><div class="quick-preview-badge"></div>`; | |
| } | |
| 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 = `<div class="bubble-attachments">`; | |
| files.forEach(file => { | |
| if (file.type.startsWith("image/")) { | |
| attachmentsHtml += ` | |
| <div class="bubble-attachment-card"> | |
| <img src="${file.base64}"> | |
| <div class="bubble-attachment-label">IMG</div> | |
| </div> | |
| `; | |
| } else { | |
| attachmentsHtml += ` | |
| <div class="bubble-attachment-card"> | |
| <div class="media-chip-preview" style="width:100%;height:100%;">🎬</div> | |
| <div class="bubble-attachment-label">VIDEO</div> | |
| </div> | |
| `; | |
| } | |
| }); | |
| attachmentsHtml += `</div>`; | |
| } | |
| bubble.innerHTML = ` | |
| <div class="message-meta">User</div> | |
| <div class="message-body"> | |
| <div class="message-text">${escapeHtml(text)}</div> | |
| ${attachmentsHtml} | |
| </div> | |
| `; | |
| 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 = ` | |
| <div class="message-meta">Step 3.7 Flash</div> | |
| <div class="message-body"> | |
| <div class="thought-container" id="${id}-thought-box"> | |
| <div class="thought-header"> | |
| <div class="thought-title-group"> | |
| <div class="spinner-light" style="width:12px;height:12px;border-width:1.5px; border-top-color:#111827; border-left-color:#e5e7eb;"></div> | |
| <span>Reasoning...</span> | |
| </div> | |
| <span class="thought-timer" id="${id}-timer">0.0s</span> | |
| </div> | |
| </div> | |
| <div class="message-text markdown-body" id="${id}-text-box"> | |
| <span class="text-muted">Analyzing context and constructing reasoning chain...</span> | |
| </div> | |
| </div> | |
| `; | |
| 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 = ` | |
| <div class="thought-header" id="${id}-thought-toggle"> | |
| <div class="thought-title-group"> | |
| <span>🧠 Thought Process</span> | |
| </div> | |
| <div style="display:flex;align-items:center;gap:10px;"> | |
| <span class="thought-timer">Thought for ${duration}s</span> | |
| <span class="thought-toggle-icon">▼</span> | |
| </div> | |
| </div> | |
| <div class="thought-content">${escapeHtml(reasoning)}</div> | |
| `; | |
| 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 = ` | |
| <div class="message-meta">System</div> | |
| <div class="message-body" style="background-color: ${isError ? '#fef2f2' : '#f0fdf4'}; border-color: ${isError ? '#fee2e2' : '#bbf7d0'};"> | |
| <div class="message-text" style="color: ${isError ? '#ef4444' : '#16a34a'}; font-size: 11.5px; font-weight: 500;"> | |
| ${isError ? '🛑' : 'ℹ️'} ${message} | |
| </div> | |
| </div> | |
| `; | |
| dom.chatMessagesFeed.appendChild(log); | |
| scrollToBottom(); | |
| } | |
| function escapeHtml(text) { | |
| if (!text) return ""; | |
| return text | |
| .replace(/&/g, "&") | |
| .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); | |