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