(function () { var canvas, ctx, t = 0; var TILE = 26, GAP = 1; var rafId = null; var cachedRgb = "10,10,10"; var lastDraw = 0; var FRAME_MS = 1000 / 24; function rgb() { var th = document.documentElement.getAttribute("data-theme") || "white"; if (th === "dark") return "220,210,175"; if (th === "yellow") return "120,85,20"; if (th === "blue") return "38,88,155"; return "10,10,10"; } function frame(ts) { rafId = requestAnimationFrame(frame); if (ts - lastDraw < FRAME_MS) return; lastDraw = ts; var w = canvas.width, h = canvas.height; var cols = Math.ceil(w / TILE) + 1; var rows = Math.ceil(h / TILE) + 1; var pre = "rgba(" + cachedRgb + ","; ctx.clearRect(0, 0, w, h); for (var r = 0; r < rows; r++) { for (var c = 0; c < cols; c++) { var wave = 0.6 * Math.sin(c * 0.21 + t * 0.36) * Math.sin(r * 0.17 + t * 0.28) + 0.4 * Math.sin(c * 0.11 - r * 0.13 + t * 0.19); var norm = (wave + 1) * 0.5; var v = norm * norm * norm; var a = Math.round((0.004 + v * 0.186) * 100) / 100; if (a < 0.02) continue; ctx.fillStyle = pre + a + ")"; ctx.fillRect(c * TILE + GAP, r * TILE + GAP, TILE - GAP, TILE - GAP); } } t += 0.007; } var resizeTimer; function resize() { clearTimeout(resizeTimer); resizeTimer = setTimeout(function () { var newW = window.innerWidth; var newH = window.innerHeight; if (newW === canvas.width && Math.abs(newH - canvas.height) <= 90) return; canvas.width = newW; canvas.height = newH; }, 120); } function onVisibilityChange() { if (document.hidden) { if (rafId) { cancelAnimationFrame(rafId); rafId = null; } } else if (!rafId) { rafId = requestAnimationFrame(frame); } } document.addEventListener("DOMContentLoaded", function () { canvas = document.createElement("canvas"); canvas.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;" + "z-index:0;pointer-events:none;will-change:transform;" + "-webkit-backface-visibility:hidden;backface-visibility:hidden;"; document.body.insertBefore(canvas, document.body.firstChild); ctx = canvas.getContext("2d"); canvas.width = window.innerWidth; canvas.height = window.innerHeight; cachedRgb = rgb(); window.addEventListener("resize", resize); document.addEventListener("visibilitychange", onVisibilityChange); rafId = requestAnimationFrame(frame); }); new MutationObserver(function () { cachedRgb = rgb(); }) .observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"] }); })(); (function () { var THEMES = ["white", "yellow", "blue", "dark"]; var LABELS = { white: "Pure White", yellow: "Warm Yellow", blue: "Cool Blue", dark: "Dark" }; function applyTheme(theme) { if (theme === "white") { document.documentElement.removeAttribute("data-theme"); } else { document.documentElement.setAttribute("data-theme", theme); } try { localStorage.setItem("rh-ui-theme", theme); } catch (e) {} document.querySelectorAll(".theme-dot").forEach(function (dot) { dot.classList.toggle("active", dot.dataset.theme === theme); }); } var saved = "white"; try { saved = localStorage.getItem("rh-ui-theme") || "white"; } catch (e) {} applyTheme(saved); document.addEventListener("DOMContentLoaded", function () { var switcher = document.createElement("div"); switcher.id = "theme-switcher"; switcher.setAttribute("aria-label", "Choose colour theme"); THEMES.forEach(function (theme) { var btn = document.createElement("button"); btn.className = "theme-dot"; btn.dataset.theme = theme; btn.title = LABELS[theme]; btn.setAttribute("aria-label", LABELS[theme]); btn.addEventListener("click", function () { applyTheme(theme); }); switcher.appendChild(btn); }); document.body.appendChild(switcher); applyTheme(saved); }); })(); (function () { var ws; var running = false; var interrupting = false; var pendingAskId = ""; var keepSubmittedMessageOnReset = false; var autoFollowTimeline = true; var conversationStarted = false; var downloadToken = ""; var fileToken = ""; var downloadingWorkspace = false; var images = []; var COLLAPSED_STEP_HEIGHT = 220; var promptInput = document.getElementById("promptInput"); var runBtn = document.getElementById("runBtn"); var newBtn = document.getElementById("newBtn"); var modelSelect = document.getElementById("modelSelect"); var modelDropdown = document.getElementById("modelDropdown"); var modelDropdownButton = document.getElementById("modelDropdownButton"); var modelSelectLabel = document.getElementById("modelSelectLabel"); var modelOptions = document.getElementById("modelOptions"); var attachBtn = document.getElementById("attachBtn"); var imageInput = document.getElementById("imageInput"); var imagePreview = document.getElementById("imagePreview"); var dropZone = document.getElementById("dropZone"); var timeline = document.getElementById("timeline"); var statusPill = document.getElementById("statusPill"); var workspaceStrip = document.getElementById("workspaceStrip"); var workspaceMeta = document.getElementById("workspaceMeta"); var downloadWorkspaceBtn = document.getElementById("downloadWorkspaceBtn"); var defaultPromptPlaceholder = promptInput.getAttribute("placeholder") || "Message ResearchHarness"; var mermaidCounter = 0; function escapeHtml(value) { return String(value || "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function protectMathSegments(text) { var segments = []; var protectedText = String(text || "").replace(/(\$\$[\s\S]+?\$\$|\\\[[\s\S]+?\\\]|\\\([\s\S]+?\\\))/g, function (match) { var token = "@@RH_MATH_" + segments.length + "@@"; segments.push({ token: token, text: match }); return token; }); return { text: protectedText, segments: segments }; } function restoreMathSegments(html, segments) { var restored = String(html || ""); (segments || []).forEach(function (segment) { restored = restored.split(segment.token).join(escapeHtml(segment.text)); }); return restored; } function isRemoteOrInlineImageSrc(src) { return /^(https?:|data:|blob:|#|\/api\/|\/static\/)/i.test(String(src || "").trim()); } function rewriteWorkspaceImageSources(html) { var token = fileToken || downloadToken; if (!token) return html; var template = document.createElement("template"); template.innerHTML = html; template.content.querySelectorAll("img").forEach(function (img) { var src = img.getAttribute("src") || ""; if (!src || isRemoteOrInlineImageSrc(src)) return; img.setAttribute("src", "/api/workspace-file?token=" + encodeURIComponent(token) + "&path=" + encodeURIComponent(src)); }); return template.innerHTML; } function normalizeMarkdownImageDestinations(text) { return String(text || "").replace(/!\[([^\]\n]*)\]\(([^)\n]+)\)/g, function (match, alt, target) { var src = String(target || "").trim(); if (!src || src[0] === "<" || !/\s/.test(src) || isRemoteOrInlineImageSrc(src)) return match; if (/[<>]/.test(src)) return match; return "![" + alt + "](<" + src + ">)"; }); } function unwrapFullMarkdownFence(text) { var source = String(text || "").trim(); var match = /^(```|~~~)[ \t]*(markdown|md|gfm)[^\n]*\n([\s\S]*?)\n\1[ \t]*$/i.exec(source); return match ? match[3] : text; } function renderMathInMarkdown(container) { if (!window.renderMathInElement) return; container.querySelectorAll(".markdown-body").forEach(function (body) { try { window.renderMathInElement(body, { delimiters: [ { left: "$$", right: "$$", display: true }, { left: "\\[", right: "\\]", display: true }, { left: "\\(", right: "\\)", display: false } ], ignoredTags: ["script", "noscript", "style", "textarea", "pre", "code"], throwOnError: false }); } catch (e) { console.warn("Math rendering failed.", e); } }); } function renderMermaidInMarkdown(container) { if (!window.mermaid) return; try { window.mermaid.initialize({ startOnLoad: false, securityLevel: "strict" }); } catch (e) { console.warn("Mermaid initialization failed.", e); return; } container.querySelectorAll(".markdown-body pre code").forEach(function (code) { var className = String(code.className || "").toLowerCase(); if (!className.split(/\s+/).some(function (name) { return name === "language-mermaid" || name === "lang-mermaid" || name === "language-mmd" || name === "lang-mmd"; })) return; var pre = code.closest("pre"); if (!pre) return; var source = code.textContent || ""; var target = document.createElement("div"); var id = "rh-mermaid-" + (++mermaidCounter); target.className = "mermaid-chart"; window.mermaid.render(id, source).then(function (result) { target.innerHTML = result.svg || ""; pre.replaceWith(target); }).catch(function (e) { console.warn("Mermaid rendering failed.", e); }); }); } function renderMarkdown(text) { if (!window.marked || !window.DOMPurify) { console.warn("Markdown renderer unavailable; falling back to plain text."); return "
" + escapeHtml(text) + "
"; } try { var normalizedText = normalizeMarkdownImageDestinations(unwrapFullMarkdownFence(text)); var protectedMath = protectMathSegments(normalizedText); var rawHtml = window.marked.parse(protectedMath.text, { gfm: true, breaks: false, async: false }); rawHtml = rewriteWorkspaceImageSources(rawHtml); var safeHtml = window.DOMPurify.sanitize(rawHtml, { USE_PROFILES: { html: true } }); safeHtml = restoreMathSegments(safeHtml, protectedMath.segments); return '
' + safeHtml + "
"; } catch (e) { console.warn("Markdown rendering failed; falling back to plain text.", e); return "
" + escapeHtml(text) + "
"; } } function setStatus(text, kind) { statusPill.textContent = text; statusPill.className = "status " + (kind || "idle"); } function updateDownloadWorkspaceButton() { if (!downloadWorkspaceBtn) return; downloadWorkspaceBtn.disabled = !downloadToken || downloadingWorkspace; downloadWorkspaceBtn.title = downloadingWorkspace ? "Preparing workspace.zip..." : downloadToken ? "Download files created or handled by the agent in this chat." : "Run the agent first, then download the generated workspace files."; } function setWorkspaceMessage(text, kind) { if (!workspaceMeta) return; workspaceMeta.textContent = text; if (!workspaceStrip) return; workspaceStrip.classList.toggle("empty", kind === "empty"); workspaceStrip.classList.toggle("error", kind === "error"); workspaceStrip.classList.toggle("busy", kind === "busy"); } function setDownloadToken(token) { downloadToken = String(token || ""); updateDownloadWorkspaceButton(); } function updateWorkspaceHint(hasWorkspace) { if (!workspaceMeta) return; setWorkspaceMessage( hasWorkspace ? "Agent files are ready to download as a zip." : "Temporary workspace. Download agent files as a zip.", "" ); } function filenameFromContentDisposition(headerValue) { var match = /filename="?([^";]+)"?/i.exec(String(headerValue || "")); return match ? match[1] : "workspace.zip"; } function workspaceDownloadMessage(response, fallback) { return response.json().then(function (payload) { return payload && payload.detail ? String(payload.detail) : fallback; }).catch(function () { return fallback; }); } function downloadWorkspaceZip() { if (!downloadToken || downloadingWorkspace) return; downloadingWorkspace = true; updateDownloadWorkspaceButton(); setWorkspaceMessage("Preparing workspace.zip...", "busy"); fetch("/api/workspace.zip?token=" + encodeURIComponent(downloadToken), { headers: { "Accept": "application/zip, application/json" } }).then(function (response) { if (!response.ok) { return workspaceDownloadMessage(response, "Workspace download is not available.").then(function (message) { if (response.status === 404 && message.indexOf("no downloadable files") >= 0) { setWorkspaceMessage("Workspace is empty. Create or handle a file with the agent, then download again.", "empty"); return; } setWorkspaceMessage(message, "error"); }); } var filename = filenameFromContentDisposition(response.headers.get("content-disposition")); return response.blob().then(function (blob) { var url = URL.createObjectURL(blob); var link = document.createElement("a"); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); link.remove(); setTimeout(function () { URL.revokeObjectURL(url); }, 1000); setWorkspaceMessage("Downloading workspace.zip.", ""); }); }).catch(function (error) { console.warn("Workspace download failed.", error); setWorkspaceMessage("Workspace download failed. Please try again.", "error"); }).finally(function () { downloadingWorkspace = false; updateDownloadWorkspaceButton(); }); } function closeModelDropdown() { if (!modelDropdown || !modelDropdownButton) return; modelDropdown.classList.remove("open"); if (modelOptions) modelOptions.classList.remove("open"); modelDropdownButton.setAttribute("aria-expanded", "false"); } function positionModelOptions() { if (!modelDropdownButton || !modelOptions) return; var rect = modelDropdownButton.getBoundingClientRect(); modelOptions.style.setProperty("--model-options-top", Math.round(rect.bottom + 8) + "px"); modelOptions.style.setProperty("--model-options-right", Math.max(12, Math.round(window.innerWidth - rect.right)) + "px"); } function setModelValue(value) { if (!modelSelect || !value) return; modelSelect.value = value; if (modelSelectLabel) modelSelectLabel.textContent = value; if (!modelOptions) return; modelOptions.querySelectorAll(".model-option").forEach(function (option) { var selected = option.getAttribute("data-model-value") === value; option.classList.toggle("selected", selected); option.setAttribute("aria-selected", selected ? "true" : "false"); }); } function setModelControlDisabled(disabled) { if (modelSelect) modelSelect.disabled = disabled; if (modelDropdownButton) modelDropdownButton.disabled = disabled; if (disabled) closeModelDropdown(); } function setupModelDropdown() { if (!modelDropdown || !modelDropdownButton || !modelOptions || !modelSelect) return; if (modelOptions.parentElement !== document.body) document.body.appendChild(modelOptions); setModelValue(modelSelect.value || "gpt-5.5"); modelDropdownButton.addEventListener("click", function () { if (modelDropdownButton.disabled) return; var shouldOpen = !modelDropdown.classList.contains("open"); if (shouldOpen) positionModelOptions(); modelDropdown.classList.toggle("open", shouldOpen); modelOptions.classList.toggle("open", shouldOpen); modelDropdownButton.setAttribute("aria-expanded", shouldOpen ? "true" : "false"); }); modelOptions.querySelectorAll(".model-option").forEach(function (option) { option.addEventListener("click", function () { setModelValue(option.getAttribute("data-model-value")); closeModelDropdown(); }); }); document.addEventListener("click", function (event) { if (!modelDropdown.contains(event.target) && !modelOptions.contains(event.target)) closeModelDropdown(); }); document.addEventListener("keydown", function (event) { if (event.key === "Escape") closeModelDropdown(); }); window.addEventListener("resize", function () { if (modelDropdown.classList.contains("open")) positionModelOptions(); }); } function updateComposerMode() { if (pendingAskId) { runBtn.disabled = false; runBtn.classList.remove("is-running"); runBtn.textContent = "Reply"; promptInput.placeholder = defaultPromptPlaceholder; setModelControlDisabled(true); return; } runBtn.disabled = running && interrupting; runBtn.classList.toggle("is-running", running); runBtn.textContent = running ? (interrupting ? "Stopping" : "Stop") : "Run"; promptInput.placeholder = defaultPromptPlaceholder; setModelControlDisabled(running); } function setRunning(active, statusText) { running = active; if (!active) interrupting = false; updateComposerMode(); setStatus(statusText || (active ? "Running" : "Idle"), active ? "running" : "idle"); } function clearTimeline() { autoFollowTimeline = true; timeline.innerHTML = '' + '
' + '

What should the agent do?

' + '

Ask a question, attach images, and watch tool calls stream from an isolated temporary workspace.

' + '
'; } function ensureTimelineReady() { var welcome = timeline.querySelector(".welcome"); if (welcome) welcome.remove(); } function isNearBottom() { return timeline.scrollHeight - timeline.scrollTop - timeline.clientHeight < 80; } function scrollTimeline(force) { if (!force && !autoFollowTimeline) return; requestAnimationFrame(function () { timeline.scrollTop = timeline.scrollHeight; requestAnimationFrame(function () { timeline.scrollTop = timeline.scrollHeight; autoFollowTimeline = isNearBottom(); }); }); } function syncTimelineFollowMode() { autoFollowTimeline = isNearBottom(); } function updateEventToggle(node) { var toggle = node.querySelector(".event-toggle"); if (!toggle) return; toggle.setAttribute("aria-expanded", node.classList.contains("collapsed") ? "false" : "true"); } function eventBody(node) { return node.querySelector(".event-body"); } function eventCanCollapse(node) { return node.classList.contains("can-collapse"); } function refreshEventCollapseCapability(node) { var body = eventBody(node); var toggle = node.querySelector(".event-toggle"); if (!body) return; var shouldCollapse = !node.classList.contains("event-ask-user") && body.scrollHeight > COLLAPSED_STEP_HEIGHT + 8; node.classList.toggle("can-collapse", shouldCollapse); if (toggle) toggle.hidden = !shouldCollapse; if (!shouldCollapse) { node.classList.remove("collapsed"); body.style.maxHeight = "none"; } updateEventToggle(node); } function setEventExpanded(node, expanded, animate) { var body = eventBody(node); if (!body) { node.classList.toggle("collapsed", !expanded); updateEventToggle(node); return; } refreshEventCollapseCapability(node); if (!eventCanCollapse(node)) return; if (expanded) { node.classList.remove("collapsed"); body.style.maxHeight = body.scrollHeight + "px"; if (!animate) { body.style.maxHeight = "none"; } else { body.addEventListener("transitionend", function onEnd(event) { if (event.propertyName !== "max-height") return; body.removeEventListener("transitionend", onEnd); if (!node.classList.contains("collapsed")) { body.style.maxHeight = "none"; } }); } } else { if (body.style.maxHeight === "none" || !body.style.maxHeight) { body.style.maxHeight = body.scrollHeight + "px"; } body.offsetHeight; node.classList.add("collapsed"); body.style.maxHeight = COLLAPSED_STEP_HEIGHT + "px"; } updateEventToggle(node); } function toggleEvent(node) { if (node.classList.contains("latest") || !eventCanCollapse(node)) return; setEventExpanded(node, node.classList.contains("collapsed"), true); } function addEvent(kind, title, bodyHtml, badges) { var shouldFollow = autoFollowTimeline || isNearBottom(); ensureTimelineReady(); timeline.querySelectorAll(".event.latest").forEach(function (eventNode) { eventNode.classList.remove("latest"); setEventExpanded(eventNode, false, true); updateEventToggle(eventNode); }); var badgeHtml = (badges || []).map(function (badge) { return '' + escapeHtml(badge) + ""; }).join(""); var node = document.createElement("article"); node.className = "event event-" + kind + " latest"; node.innerHTML = '' + '
' + '
' + escapeHtml(title) + badgeHtml + '
' + '' + '
' + '
' + bodyHtml + '
'; node.querySelector(".event-toggle").addEventListener("click", function (event) { event.stopPropagation(); toggleEvent(node); }); node.addEventListener("click", function () { toggleEvent(node); }); timeline.appendChild(node); renderMathInMarkdown(node); renderMermaidInMarkdown(node); setEventExpanded(node, true, false); scrollTimeline(shouldFollow); } function addMessage(kind, text, attachedImages) { autoFollowTimeline = true; ensureTimelineReady(); var node = document.createElement("article"); node.className = "message " + kind; var imageHtml = ""; (attachedImages || []).forEach(function (image) { imageHtml += ''; }); node.innerHTML = '
' + (imageHtml ? '
' + imageHtml + '
' : '') + '
' + escapeHtml(text) + '
' + '
'; timeline.appendChild(node); scrollTimeline(true); } function formatJson(value) { try { return JSON.stringify(value, null, 2); } catch (e) { return String(value); } } function renderTrace(row) { if (!row || row.capture_type === "llm_call" || row.capture_type === "compaction") return; var role = row.role || ""; var turn = row.turn_index || 0; var text = row.text || ""; if (role === "system") return; if (role === "user" && turn === 0) return; if (role === "assistant") { var tools = Array.isArray(row.tool_names) ? row.tool_names : []; var args = Array.isArray(row.tool_arguments) ? row.tool_arguments : []; var body = ""; if (text.trim()) { body += (!tools.length && row.termination === "result") ? renderMarkdown(text) : "
" + escapeHtml(text) + "
"; } if (tools.length) { body += '
'; tools.forEach(function (name, idx) { body += '
' + escapeHtml(name) + '
' + escapeHtml(formatJson(args[idx] || {})) + '
'; }); body += "
"; } if (!body) body = '
(empty assistant output)
'; if (row.error) body += '
' + escapeHtml(row.error) + "
"; addEvent("assistant", "Assistant", body, ["round " + turn]); return; } if (role === "tool") { var toolName = Array.isArray(row.tool_names) && row.tool_names.length ? row.tool_names[0] : "Tool"; var toolBody = "
" + escapeHtml(text) + "
"; if (row.error) toolBody += '
' + escapeHtml(row.error) + "
"; addEvent("tool", toolName + " result", toolBody, ["round " + turn]); return; } if (role === "runtime") { if (!text.trim() && !row.error && !row.termination) return; var runtimeBody = "
" + escapeHtml(text || row.termination || "") + "
"; if (row.error) runtimeBody += '
' + escapeHtml(row.error) + "
"; addEvent("runtime", "Runtime", runtimeBody, turn ? ["round " + turn] : []); return; } if (role === "user") { addEvent("runtime", "Runtime message", "
" + escapeHtml(text) + "
", ["round " + turn]); } } function connect() { var protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; ws = new WebSocket(protocol + "//" + window.location.host + "/ws"); ws.onopen = function () { setStatus("Connected", "idle"); }; ws.onclose = function () { fileToken = ""; setDownloadToken(""); updateWorkspaceHint(false); clearAskRequest(); setRunning(false, "Disconnected"); setStatus("Disconnected", "error"); }; ws.onmessage = function (event) { var message = JSON.parse(event.data); if (message.type === "ready") { setStatus("Connected", "idle"); } else if (message.type === "conversation_reset") { fileToken = ""; setDownloadToken(""); updateWorkspaceHint(false); if (keepSubmittedMessageOnReset) { keepSubmittedMessageOnReset = false; ensureTimelineReady(); } else { clearTimeline(); } conversationStarted = false; clearAskRequest(); } else if (message.type === "uploaded_images") { addEvent("runtime", "Uploaded images saved", "
" + escapeHtml((message.paths || []).join("\n")) + "
", []); } else if (message.type === "run_started") { fileToken = message.download_token || ""; setDownloadToken(message.download_token || ""); updateWorkspaceHint(Boolean(message.download_token)); setRunning(true, "Running"); } else if (message.type === "interrupt_requested") { interrupting = true; updateComposerMode(); setStatus("Interrupting", "running"); } else if (message.type === "trace") { renderTrace(message.row); } else if (message.type === "ask_user") { showAskRequest(message); } else if (message.type === "run_finished") { conversationStarted = true; setRunning(false, "Done"); clearAskRequest(); setStatus("Done", "done"); } else if (message.type === "run_error") { keepSubmittedMessageOnReset = false; clearAskRequest(); setRunning(false, "Error"); setStatus("Error", "error"); addEvent("runtime", "Error", '
' + escapeHtml(message.error || "unknown error") + "
", []); } }; } function showAskRequest(message) { pendingAskId = message.request_id || ""; var question = message.question || "Question"; var context = message.context || ""; var body = "
" + escapeHtml(question) + "
"; if (context) body += '
' + escapeHtml(context) + "
"; addEvent("ask-user", "Agent question", body, ["AskUser"]); setStatus("Waiting for input", "running"); updateComposerMode(); promptInput.focus(); } function clearAskRequest() { pendingAskId = ""; updateComposerMode(); } function sendStart() { if (pendingAskId) { sendAskUserAnswer(); return; } if (!ws || ws.readyState !== WebSocket.OPEN) { setStatus("Disconnected", "error"); return; } if (running) { sendInterrupt(); return; } var prompt = promptInput.value.trim(); if (!prompt) return; var sentImages = images.slice(); var continueConversation = conversationStarted; if (!continueConversation) clearTimeline(); addMessage("user", prompt, sentImages); keepSubmittedMessageOnReset = !continueConversation; setRunning(true, "Starting"); ws.send(JSON.stringify({ type: "start", prompt: prompt, model_name: modelSelect ? modelSelect.value : "", images: sentImages, continue_conversation: continueConversation })); promptInput.value = ""; promptInput.style.height = "auto"; images = []; renderImages(); } function sendInterrupt() { if (!running || interrupting || !ws || ws.readyState !== WebSocket.OPEN) return; interrupting = true; updateComposerMode(); setStatus("Interrupting", "running"); ws.send(JSON.stringify({ type: "interrupt" })); } function sendAskUserAnswer() { if (!pendingAskId || !ws || ws.readyState !== WebSocket.OPEN) return; var answer = promptInput.value.trim(); if (!answer) return; var requestId = pendingAskId; addMessage("user", answer, []); ws.send(JSON.stringify({ type: "ask_user_answer", request_id: requestId, answer: answer })); pendingAskId = ""; promptInput.value = ""; promptInput.style.height = "auto"; updateComposerMode(); setStatus("Running", "running"); } function addImageFiles(fileList) { Array.from(fileList || []).forEach(function (file) { if (!file.type || !file.type.startsWith("image/")) return; var reader = new FileReader(); reader.onload = function () { images.push({ name: file.name, data_url: String(reader.result || "") }); renderImages(); }; reader.readAsDataURL(file); }); } function renderImages() { imagePreview.innerHTML = ""; images.forEach(function (image, idx) { var chip = document.createElement("button"); chip.type = "button"; chip.className = "image-chip"; chip.title = "Remove image"; chip.innerHTML = '' + escapeHtml(image.name || "image") + ""; chip.addEventListener("click", function () { images.splice(idx, 1); renderImages(); }); imagePreview.appendChild(chip); }); } setupModelDropdown(); runBtn.addEventListener("click", sendStart); timeline.addEventListener("scroll", syncTimelineFollowMode); timeline.addEventListener("wheel", function (event) { if (event.deltaY < 0) autoFollowTimeline = false; }, { passive: true }); timeline.addEventListener("touchmove", function () { autoFollowTimeline = false; }, { passive: true }); promptInput.addEventListener("keydown", function (event) { if (event.isComposing) return; if (event.key === "Enter" && !event.shiftKey && !event.ctrlKey && !event.metaKey) { event.preventDefault(); sendStart(); } }); promptInput.addEventListener("input", function () { promptInput.style.height = "auto"; promptInput.style.height = Math.min(promptInput.scrollHeight, 180) + "px"; }); newBtn.addEventListener("click", function () { if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "new" })); if (!running) { promptInput.value = ""; images = []; renderImages(); clearTimeline(); clearAskRequest(); conversationStarted = false; setDownloadToken(""); updateWorkspaceHint(false); setRunning(false, "Idle"); } }); if (downloadWorkspaceBtn) { downloadWorkspaceBtn.addEventListener("click", function () { downloadWorkspaceZip(); }); updateDownloadWorkspaceButton(); } attachBtn.addEventListener("click", function () { imageInput.click(); }); imageInput.addEventListener("change", function (event) { addImageFiles(event.target.files); }); ["dragenter", "dragover"].forEach(function (name) { dropZone.addEventListener(name, function (event) { event.preventDefault(); dropZone.classList.add("dragover"); }); }); ["dragleave", "drop"].forEach(function (name) { dropZone.addEventListener(name, function (event) { event.preventDefault(); dropZone.classList.remove("dragover"); }); }); dropZone.addEventListener("drop", function (event) { addImageFiles(event.dataTransfer.files); }); document.addEventListener("paste", function (event) { var files = []; Array.from(event.clipboardData ? event.clipboardData.items : []).forEach(function (item) { if (item.kind === "file") { var file = item.getAsFile(); if (file) files.push(file); } }); if (files.length) addImageFiles(files); }); connect(); })();