| import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client/dist/index.min.js"; |
|
|
| |
| const STATE = { |
| reasoningEffort: "medium", |
| maxTokens: 2048, |
| temperature: 0.7, |
| uploadedFiles: [], |
| conversationHistory: [], |
| gradioClient: null, |
| isThinking: false |
| }; |
|
|
| |
| 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"), |
| |
| |
| sidebarRight: document.getElementById("sidebar-right"), |
| sidebarOverlay: document.getElementById("sidebar-overlay"), |
| btnToggleRight: document.getElementById("btn-toggle-right"), |
| btnCloseDrawer: document.getElementById("btn-close-drawer"), |
| |
| |
| studioDashboard: document.getElementById("studio-dashboard"), |
| chatThreadContainer: document.getElementById("chat-thread-container"), |
| chatMessagesFeed: document.getElementById("chat-messages-feed"), |
| |
| |
| 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"), |
| |
| |
| 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"), |
| |
| |
| fileUploader: document.getElementById("file-uploader"), |
| shelfList: document.getElementById("shelf-list"), |
| dropZone: document.getElementById("drop-zone"), |
| |
| |
| menuNewChat: document.getElementById("menu-new-chat"), |
| clearChatBtn: document.getElementById("clear-chat-button"), |
| |
| |
| recipeChips: document.querySelectorAll(".recipe-chip") |
| }; |
|
|
| |
| marked.setOptions({ |
| breaks: true, |
| highlight: function(code, lang) { |
| const language = hljs.getLanguage(lang) ? lang : 'plaintext'; |
| return hljs.highlight(code, { language }).value; |
| } |
| }); |
|
|
| |
| async function initializeApp() { |
| |
| 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); |
| }); |
|
|
| |
| 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)); |
|
|
| |
|
|
| 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); |
| }); |
| } |
|
|
| |
| 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); |
|
|
| |
| 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); |
| }); |
| } |
|
|
| |
| 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); |
| } |
| }); |
| } |
|
|
| |
| if (dom.menuNewChat) dom.menuNewChat.addEventListener("click", resetSandbox); |
| if (dom.clearChatBtn) dom.clearChatBtn.addEventListener("click", resetSandbox); |
|
|
| |
| if (dom.recipeChips) { |
| dom.recipeChips.forEach(chip => { |
| chip.addEventListener("click", () => { |
| const recipeType = chip.getAttribute("data-recipe"); |
| loadRecipe(recipeType); |
| }); |
| }); |
| } |
|
|
| |
| [dom.studioPromptInput, dom.miniPromptInput].forEach(textarea => { |
| if (textarea) { |
| textarea.addEventListener("input", () => { |
| textarea.style.height = "auto"; |
| textarea.style.height = (textarea.scrollHeight) + "px"; |
| }); |
| } |
| }); |
| } |
|
|
| |
| 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"); |
| } |
| } |
|
|
| |
| 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() { |
| 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 => { |
| |
| 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); |
|
|
| |
| const previewItemDash = createPreviewThumb(file); |
| if (dom.innerShelfPreview) dom.innerShelfPreview.appendChild(previewItemDash); |
|
|
| |
| 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(); |
| } |
|
|
| |
| 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 |
| }); |
| } |
|
|
| |
| dom.studioPromptInput.value = promptText; |
| dom.miniPromptInput.value = promptText; |
|
|
| updateShelfUI(); |
| dom.studioPromptInput.dispatchEvent(new Event("input")); |
| dom.miniPromptInput.dispatchEvent(new Event("input")); |
| |
| |
| if (dom.studioDashboard.style.display !== "none") { |
| dom.studioPromptInput.focus(); |
| } else { |
| dom.miniPromptInput.focus(); |
| } |
| } |
|
|
| |
| async function triggerPromptSubmission(inputElement) { |
| if (STATE.isThinking) return; |
|
|
| const promptText = inputElement.value.trim(); |
| if (!promptText && STATE.uploadedFiles.length === 0) return; |
|
|
| setLoadingState(true); |
|
|
| |
| 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 |
| }; |
|
|
| |
| if (dom.studioDashboard.style.display !== "none") { |
| dom.studioDashboard.style.display = "none"; |
| dom.chatThreadContainer.style.display = "flex"; |
| } |
|
|
| |
| appendUserBubble(promptText, STATE.uploadedFiles); |
|
|
| |
| STATE.conversationHistory.push(userMessage); |
|
|
| |
| dom.studioPromptInput.value = ""; |
| dom.miniPromptInput.value = ""; |
| dom.studioPromptInput.style.height = "auto"; |
| dom.miniPromptInput.style.height = "auto"; |
| |
| STATE.uploadedFiles = []; |
| updateShelfUI(); |
|
|
| |
| 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_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(); |
| } 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); |
| } |
|
|
| |
| 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; |
| } |
| } |
|
|
| |
| 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(); |
| } |
|
|
| |
| 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(); |
|
|
| |
| 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; |
| } |
|
|
| |
| 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(); |
| } |
|
|
| |
| function resetSandbox() { |
| STATE.conversationHistory = []; |
| STATE.uploadedFiles = []; |
| updateShelfUI(); |
|
|
| |
| dom.chatMessagesFeed.innerHTML = ""; |
|
|
| |
| 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; |
| } |
|
|
| |
| window.addEventListener("DOMContentLoaded", initializeApp); |
|
|