(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 ""; }); } 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 '
" + 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 = '' + '
Ask a question, attach images, and watch tool calls stream from an isolated temporary workspace.
' + '" + escapeHtml(text) + ""; } if (tools.length) { body += '
' + escapeHtml(formatJson(args[idx] || {})) + '(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 = '