// DesignGym 2.0 — minimal demo client. // Talks only to the backend /demo/* endpoints; all policy logic lives server-side. const $ = (id) => document.getElementById(id); const taskSel = $("task"); const runBtn = $("run"); const stepBtn = $("step"); const resetBtn = $("reset"); const statusBox = $("status"); const scrollDetailsBtn = $("scroll-details"); const canvas = $("canvas"); const scoreFinal = $("score-final"); const scoreInstr = $("score-instr"); const scoreSteps = $("score-steps"); const scoreReward = $("score-reward"); const scoreValid = $("score-valid"); const logBody = $("log-body"); const logCount = $("log-count"); const rawSummary = $("raw-summary"); let llmActive = false; let lastTrajectory = []; window.addEventListener("error", (e) => setStatus(`JS error: ${e.message}`, "error")); window.addEventListener("unhandledrejection", (e) => setStatus(`Promise: ${e.reason}`, "error")); function setStatus(text, kind) { statusBox.textContent = text; statusBox.className = "status " + (kind || "muted"); } function selectedPolicy() { const r = document.querySelector('input[name="policy"]:checked'); return r ? r.value : "heuristic"; } function policyLabel(id) { return id === "sft" ? "SFT-LLM Picker" : "Heuristic Planner"; } function rectColor(role, type) { const key = role || type || "default"; const colors = { title: "#bfdbfe", subtitle: "#dbeafe", image: "#bbf7d0", cta: "#fecaca", logo: "#fde68a", badge: "#ddd6fe", body: "#e2e8f0", caption: "#fef3c7", shape: "#ddd6fe", masthead: "#fed7aa", headline: "#a7f3d0", default: "#e5e7eb" }; return colors[key] || colors.default; } function drawState(state) { canvas.innerHTML = ""; const bg = document.createElementNS("http://www.w3.org/2000/svg", "rect"); bg.setAttribute("x", 0); bg.setAttribute("y", 0); bg.setAttribute("width", 800); bg.setAttribute("height", 1000); bg.setAttribute("fill", "#f8fafc"); canvas.appendChild(bg); const elements = state?.elements || []; if (!elements.length) { const t = document.createElementNS("http://www.w3.org/2000/svg", "text"); t.setAttribute("x", 40); t.setAttribute("y", 60); t.setAttribute("fill", "#0f172a"); t.setAttribute("font-size", "22"); t.textContent = "Reset to load a layout."; canvas.appendChild(t); return; } for (const el of elements) { const b = el.bbox || el.box; if (!b || b.length < 4) continue; const x = Number(b[0]) * 800; const y = Number(b[1]) * 1000; const w = Number(b[2]) * 800; const h = Number(b[3]) * 1000; const r = document.createElementNS("http://www.w3.org/2000/svg", "rect"); r.setAttribute("x", x); r.setAttribute("y", y); r.setAttribute("width", w); r.setAttribute("height", h); r.setAttribute("rx", 8); r.setAttribute("fill", rectColor(el.role, el.type)); r.setAttribute("stroke", "#0f172a"); r.setAttribute("stroke-width", 2); canvas.appendChild(r); const label = document.createElementNS("http://www.w3.org/2000/svg", "text"); label.setAttribute("x", x + 8); label.setAttribute("y", y + 22); label.setAttribute("fill", "#0f172a"); label.setAttribute("font-size", "15"); label.setAttribute("font-family", "ui-monospace, monospace"); label.textContent = el.id || "element"; canvas.appendChild(label); const sub = document.createElementNS("http://www.w3.org/2000/svg", "text"); sub.setAttribute("x", x + 8); sub.setAttribute("y", y + 42); sub.setAttribute("fill", "#334155"); sub.setAttribute("font-size", "12"); sub.setAttribute("font-family", "ui-monospace, monospace"); sub.textContent = el.role || el.type || ""; canvas.appendChild(sub); } } function fmt(n, digits = 3) { if (n === null || n === undefined || Number.isNaN(Number(n))) return "—"; return Number(n).toFixed(digits); } function fmtPct(n) { if (n === null || n === undefined) return "—"; return `${(Number(n) * 100).toFixed(0)}%`; } function renderScores(summary, state) { const finalScore = summary?.final_score ?? state?.current_score; const instrScore = summary?.instruction_score ?? state?.instruction_score; scoreFinal.textContent = fmt(finalScore, 3); scoreInstr.textContent = fmt(instrScore, 3); scoreSteps.textContent = (summary?.steps_taken ?? state?.step_count ?? 0).toString(); scoreReward.textContent = fmt(summary?.total_reward ?? 0, 2); scoreValid.textContent = summary?.valid_action_rate !== undefined ? fmtPct(summary.valid_action_rate) : "—"; } function renderLog(trajectory) { logBody.innerHTML = ""; for (const t of trajectory) { const tr = document.createElement("tr"); if (t.error) tr.className = "error-row"; const deltaCls = t.delta_score > 0 ? "delta-pos" : (t.delta_score < 0 ? "delta-neg" : ""); const status = t.error ? `❌ ${t.error}` : (t.action_type === "finalize" ? "🏁 finalize" : "✓"); tr.innerHTML = ` ${t.step} ${t.action} ${fmt(t.reward, 3)} ${(t.delta_score >= 0 ? "+" : "") + fmt(t.delta_score, 3)} ${fmt(t.score, 3)} ${(t.worst_metrics || []).slice(0, 2).join(", ") || "—"} ${status} `; logBody.appendChild(tr); } logCount.textContent = trajectory.length ? `· ${trajectory.length} step${trajectory.length === 1 ? "" : "s"}` : "— no steps yet"; } function setRawSummary(payload) { rawSummary.textContent = JSON.stringify(payload, null, 2); } async function fetchPolicies() { try { const res = await fetch("/demo/policies"); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); llmActive = !!data.llm_active; } catch (err) { llmActive = false; } } async function resetEnv() { setStatus("Resetting environment…", "running"); try { const res = await fetch("/demo/reset", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ task_id: taskSel.value, seed: 0 }), }); if (!res.ok) throw new Error(`reset failed (${res.status})`); const payload = await res.json(); drawState(payload.state); lastTrajectory = []; renderLog([]); renderScores({ steps_taken: 0, total_reward: 0, valid_action_rate: 0 }, payload.state); setStatus(`Ready: ${taskSel.value}. Click Run to start.`, "muted"); setRawSummary({ task_id: taskSel.value, brief: payload.observation?.brief }); } catch (err) { setStatus(`Reset error: ${err.message}`, "error"); } } async function stepOnce() { setStatus(`Stepping (policy=${selectedPolicy()})…`, "running"); try { const res = await fetch("/demo/policy_step", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ policy: selectedPolicy() }), }); if (res.status === 409) { await resetEnv(); return stepOnce(); } if (!res.ok) throw new Error(`step failed (${res.status})`); const data = await res.json(); drawState(data.state); lastTrajectory.push(data.step_record); renderLog(lastTrajectory); const liveSummary = { final_score: data.state.current_score, instruction_score: data.state.instruction_score, steps_taken: lastTrajectory.length, total_reward: lastTrajectory.reduce((a, r) => a + (r.reward || 0), 0), valid_action_rate: lastTrajectory.filter((r) => !r.error).length / lastTrajectory.length, }; renderScores(liveSummary, data.state); setRawSummary({ step_record: data.step_record, summary: liveSummary }); if (data.done) { setStatus("Episode complete.", "success"); } else { setStatus(`Step ${data.step_record.step} done. Click Step or Run.`, "muted"); } } catch (err) { setStatus(`Step error: ${err.message}`, "error"); } } async function runEpisode() { const policy = selectedPolicy(); setStatus(`Running episode with ${policyLabel(policy)}… (this may take a few seconds for SFT)`, "running"); runBtn.disabled = true; stepBtn.disabled = true; resetBtn.disabled = true; try { const res = await fetch("/demo/run_episode", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ policy, task_id: taskSel.value, seed: 0 }), }); if (!res.ok) { const txt = await res.text(); throw new Error(`HTTP ${res.status} — ${txt.slice(0, 240)}`); } const data = await res.json(); if (data.error) throw new Error(data.error); drawState(data.final_state); lastTrajectory = data.trajectory || []; renderLog(lastTrajectory); renderScores(data.summary, data.final_state); setRawSummary(data.summary); const tag = data.summary.llm_available ? "live LLM" : "local-best fallback"; setStatus( `Done in ${data.summary.wall_time_sec}s — ${data.summary.steps_taken} steps · final score ${fmt(data.summary.final_score, 3)} (${tag})`, "success", ); } catch (err) { setStatus(`Run error: ${err.message}`, "error"); } finally { runBtn.disabled = false; stepBtn.disabled = false; resetBtn.disabled = false; } } runBtn.addEventListener("click", runEpisode); stepBtn.addEventListener("click", stepOnce); resetBtn.addEventListener("click", resetEnv); taskSel.addEventListener("change", resetEnv); if (scrollDetailsBtn) { scrollDetailsBtn.addEventListener("click", () => { const target = document.querySelector(".log-panel"); if (target) target.scrollIntoView({ behavior: "smooth", block: "start" }); }); } (async function init() { await fetchPolicies(); await resetEnv(); })();