Spaces:
Sleeping
Sleeping
| /* contimp-app frontend: task nav, deal/run flow, per-task result rendering. */ | |
| const $ = (id) => document.getElementById(id); | |
| const state = { | |
| tasks: [], | |
| task: null, | |
| inputId: null, // cleared when the user edits the dealt text | |
| traceId: null, | |
| sessionId: sessionStorage.getItem("sid") || crypto.randomUUID(), | |
| }; | |
| sessionStorage.setItem("sid", state.sessionId); | |
| const store = { | |
| get name() { return localStorage.getItem("name") || ""; }, | |
| set name(v) { localStorage.setItem("name", v); }, | |
| get passcode() { return localStorage.getItem("passcode") || ""; }, | |
| set passcode(v) { localStorage.setItem("passcode", v); }, | |
| }; | |
| async function api(path, options = {}) { | |
| const res = await fetch(path, { | |
| ...options, | |
| headers: { | |
| "Content-Type": "application/json", | |
| "X-Contimp-Passcode": store.passcode, | |
| ...(options.headers || {}), | |
| }, | |
| }); | |
| if (res.status === 401) { | |
| showGate("That passcode didn't work β ask in Slack for the current one."); | |
| throw new Error("unauthorized"); | |
| } | |
| if (!res.ok) throw new Error((await res.json().catch(() => ({}))).detail || res.statusText); | |
| return res.json(); | |
| } | |
| /* ---- gate ---- */ | |
| function showGate(message = "") { | |
| $("gate-error").textContent = message; | |
| $("gate-name").value = store.name; | |
| $("app").classList.add("hidden"); | |
| $("gate").classList.remove("hidden"); | |
| } | |
| async function enter() { | |
| const name = $("gate-name").value.trim(); | |
| if (!name) return ($("gate-error").textContent = "Tell us who you are :)"); | |
| store.name = name; | |
| store.passcode = $("gate-passcode").value.trim(); | |
| try { | |
| await boot(); | |
| } catch (e) { | |
| if (e.message !== "unauthorized") $("gate-error").textContent = e.message; | |
| } | |
| } | |
| /* ---- boot + nav ---- */ | |
| async function boot() { | |
| state.tasks = await api("/api/tasks"); | |
| $("gate").classList.add("hidden"); | |
| $("app").classList.remove("hidden"); | |
| $("user-chip").textContent = store.name; | |
| api("/api/health", { headers: {} }).then((h) => ($("model-name").textContent = h.model)); | |
| const nav = $("task-nav"); | |
| nav.innerHTML = ""; | |
| for (const task of state.tasks) { | |
| const btn = document.createElement("button"); | |
| btn.textContent = task.title; | |
| btn.onclick = () => selectTask(task.id); | |
| btn.dataset.task = task.id; | |
| nav.appendChild(btn); | |
| } | |
| selectTask(state.tasks[0].id); | |
| } | |
| function selectTask(taskId) { | |
| state.task = state.tasks.find((t) => t.id === taskId); | |
| state.inputId = null; | |
| document.querySelectorAll("nav button").forEach((b) => | |
| b.classList.toggle("active", b.dataset.task === taskId)); | |
| $("tagline").textContent = state.task.tagline; | |
| renderPanels(); | |
| $("input-label").textContent = state.task.ui.input_label; | |
| $("deal").textContent = "π² " + state.task.ui.deal_label; | |
| $("input-text").value = ""; | |
| $("input-text").placeholder = state.task.ui.placeholder; | |
| $("truth-hint").textContent = ""; | |
| $("output-card").classList.add("hidden"); | |
| $("error").classList.add("hidden"); | |
| } | |
| // Optional per-task explainer panels (ui.panels = [{title, html}], html is trusted), | |
| // each a collapsed-by-default box above the input. Built lazily so index.html is untouched. | |
| function renderPanels() { | |
| let host = $("task-panels"); | |
| if (!host) { | |
| host = document.createElement("div"); | |
| host.id = "task-panels"; | |
| $("tagline").insertAdjacentElement("afterend", host); | |
| } | |
| const panels = state.task.ui.panels || []; | |
| host.innerHTML = panels.map((p) => | |
| `<details class="panel"><summary>${esc(p.title)}</summary>` + | |
| `<div class="panel-body">${p.html}</div></details>` | |
| ).join(""); | |
| } | |
| /* ---- deal + run ---- */ | |
| async function deal() { | |
| const s = await api(`/api/tasks/${state.task.id}/sample`, { method: "POST" }); | |
| state.inputId = s.input_id; | |
| $("input-text").value = s.text; | |
| $("truth-hint").textContent = "dealt input β result will be auto-scored"; | |
| $("output-card").classList.add("hidden"); | |
| $("error").classList.add("hidden"); | |
| } | |
| async function run() { | |
| const text = $("input-text").value.trim(); | |
| if (!text) return; | |
| const btn = $("run"); | |
| btn.disabled = true; | |
| btn.innerHTML = '<span class="spin">π</span> Running'; | |
| $("error").classList.add("hidden"); | |
| try { | |
| const result = await api(`/api/tasks/${state.task.id}/run`, { | |
| method: "POST", | |
| body: JSON.stringify({ | |
| text, | |
| input_id: state.inputId, | |
| user: store.name, | |
| session_id: state.sessionId, | |
| }), | |
| }); | |
| render(result); | |
| } catch (e) { | |
| if (e.message !== "unauthorized") { | |
| $("error").textContent = "Run failed: " + e.message; | |
| $("error").classList.remove("hidden"); | |
| } | |
| } finally { | |
| btn.disabled = false; | |
| btn.textContent = "Run βΈ"; | |
| } | |
| } | |
| /* ---- rendering ---- */ | |
| const esc = (s) => String(s ?? "β").replace(/[&<>"]/g, (c) => | |
| ({ "&": "&", "<": "<", ">": ">", '"': """ }[c])); | |
| function render(result) { | |
| state.traceId = result.trace_id; | |
| state.lastResult = result; | |
| const out = $("output"); | |
| out.innerHTML = | |
| state.task.ui.output === "label" ? renderLabel(result) | |
| : state.task.ui.output === "comms" ? renderComms(result) | |
| : renderConfig(result); | |
| $("run-meta").textContent = result.truth ? "auto-scored against ground truth" : "no ground truth (free input)"; | |
| $("feedback").classList.remove("hidden"); | |
| $("feedback-done").textContent = ""; | |
| $("note-text").value = ""; | |
| document.querySelectorAll(".thumb").forEach((b) => b.classList.remove("chosen")); | |
| renderTrajectory(result); | |
| $("output-card").classList.remove("hidden"); | |
| $("output-card").scrollIntoView({ behavior: "smooth", block: "nearest" }); | |
| } | |
| // Complete OpenAI-format conversation (system β tool calls β tool results β final answer). | |
| function renderTrajectory(result) { | |
| const el = $("trajectory"); | |
| if (!result.messages || !result.messages.length) { | |
| el.classList.add("hidden"); | |
| return; | |
| } | |
| const nTools = (result.transcript || []).length; | |
| $("trajectory-meta").textContent = `${result.messages.length} messages Β· ${nTools} tool call${ | |
| nTools === 1 ? "" : "s"}`; | |
| $("trajectory-json").textContent = JSON.stringify({ messages: result.messages }, null, 2); | |
| el.open = false; // collapsed until the user reveals it | |
| el.classList.remove("hidden"); | |
| } | |
| // A single standard chat record β one line of {"messages":[...]} β the copy/download artifact. | |
| const trajectoryRecord = (messages) => JSON.stringify({ messages }); | |
| function downloadTrajectory() { | |
| const r = state.lastResult; | |
| if (!r || !r.messages) return; | |
| const blob = new Blob([trajectoryRecord(r.messages)], { type: "application/x-ndjson" }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement("a"); | |
| a.href = url; | |
| a.download = `trajectory-${state.inputId || r.trace_id || "run"}.jsonl`; | |
| document.body.appendChild(a); | |
| a.click(); | |
| a.remove(); | |
| URL.revokeObjectURL(url); | |
| } | |
| async function copyTrajectory() { | |
| const r = state.lastResult; | |
| if (!r || !r.messages) return; | |
| const text = trajectoryRecord(r.messages); | |
| try { | |
| await navigator.clipboard.writeText(text); | |
| } catch { | |
| const ta = document.createElement("textarea"); | |
| ta.value = text; | |
| document.body.appendChild(ta); | |
| ta.select(); | |
| try { document.execCommand("copy"); } catch (_) {} | |
| ta.remove(); | |
| } | |
| const btn = $("copy-trajectory"); | |
| const orig = btn.textContent; | |
| btn.textContent = "Copied!"; | |
| setTimeout(() => (btn.textContent = orig), 1200); | |
| } | |
| function renderLabel(result) { | |
| const predicted = result.output.area; | |
| const truth = result.truth?.area; | |
| let html = '<div class="label-result">'; | |
| html += `<span><span class="chip-label">model says</span><span class="chip ${ | |
| truth ? (predicted === truth ? "good" : "bad") : ""}">${esc(predicted ?? "unparseable")}</span></span>`; | |
| if (truth) { | |
| html += `<span><span class="chip-label">ground truth</span><span class="chip">${esc(truth)}</span></span>`; | |
| html += `<span class="verdict">${predicted === truth ? "β " : "β"}</span>`; | |
| } | |
| html += "</div>"; | |
| if (!predicted) html += `<pre class="yaml">${esc(result.raw_output)}</pre>`; | |
| return html; | |
| } | |
| function renderConfig(result) { | |
| const o = result.output; | |
| let html = '<div class="badges">'; | |
| html += `<span class="badge ${o.yaml_valid ? "good" : "bad"}">${o.yaml_valid ? "β" : "β"} YAML</span>`; | |
| html += `<span class="badge ${o.schema_valid ? "good" : "bad"}">${o.schema_valid ? "β" : "β"} schema</span>`; | |
| if (o.fields) { | |
| const hits = o.fields.filter((f) => f.match).length; | |
| const cls = hits === o.fields.length ? "good" : "bad"; | |
| html += `<span class="badge ${cls}">${hits}/${o.fields.length} requested fields</span>`; | |
| } | |
| html += "</div>"; | |
| if (result.transcript.length) { | |
| html += `<details class="transcript"><summary>π§ ${result.transcript.length} tool call${ | |
| result.transcript.length > 1 ? "s" : ""}</summary>`; | |
| for (const step of result.transcript) { | |
| html += `<div class="tool-step"><span class="tool-name">${esc(step.tool)}(${ | |
| esc(JSON.stringify(step.args)).slice(0, 120)})</span><pre>${esc(step.result).slice(0, 600)}</pre></div>`; | |
| } | |
| html += "</details>"; | |
| } | |
| html += `<pre class="yaml">${esc(o.yaml)}</pre>`; | |
| if (!o.schema_valid && o.errors?.length) { | |
| html += `<ul class="errors">${o.errors.map((e) => `<li>${esc(e)}</li>`).join("")}</ul>`; | |
| } | |
| if (o.fields) { | |
| html += `<table class="fields"><tr><th>requested</th><th>expected</th><th>got</th><th></th></tr>`; | |
| for (const f of o.fields) { | |
| html += `<tr class="${f.match ? "" : "miss"}"><td class="path">${esc(f.path)}</td><td>${ | |
| esc(JSON.stringify(f.expected))}</td><td>${esc(JSON.stringify(f.got))}</td><td>${ | |
| f.match ? "β " : "β"}</td></tr>`; | |
| } | |
| html += "</table>"; | |
| } | |
| return html; | |
| } | |
| function chip(value, ok) { | |
| const cls = ok === null || ok === undefined ? "" : ok ? "good" : "bad"; | |
| return `<span class="chip ${cls}">${esc(value ?? "β")}</span>`; | |
| } | |
| function renderComms(result) { | |
| const o = result.output; | |
| const s = result.scores || {}; | |
| const gold = o.gold; | |
| const has = result.truth != null; | |
| let html = ""; | |
| /* --- Task 1: classification --- */ | |
| html += '<div><div class="chip-label" style="margin:0 0 6px;font-size:12.5px">Intent classification</div>'; | |
| html += '<div class="label-result">'; | |
| html += `<span><span class="chip-label">model says</span>${ | |
| chip(o.classification.category, has ? !!s.category_match : null)} ${ | |
| chip(o.classification.subcategory, has ? !!s.subcategory_match : null)}</span>`; | |
| if (has && gold) { | |
| html += `<span><span class="chip-label">ground truth</span>${ | |
| chip(gold.classification.category)} ${chip(gold.classification.subcategory)}</span>`; | |
| html += `<span class="verdict">${s.intent_exact ? "β " : "β"}</span>`; | |
| } | |
| html += "</div></div>"; | |
| /* --- Task 2: entity tagging --- */ | |
| html += '<div style="margin-top:18px"><div class="chip-label" style="margin:0 0 8px;font-size:12.5px">Entity tagging</div>'; | |
| html += '<div class="badges">'; | |
| html += `<span class="badge ${o.tags.length && o.search_summary ? "good" : "bad"}">${ | |
| o.tags.length && o.search_summary ? "β" : "β"} format</span>`; | |
| if (has) { | |
| html += `<span class="badge ${s.primary_found ? "good" : "bad"}">${ | |
| s.primary_found ? "β" : "β"} primary found</span>`; | |
| html += `<span class="badge ${s.f1 >= 0.999 ? "good" : "bad"}">F1 ${(s.f1 ?? 0).toFixed(2)}</span>`; | |
| if (!s.no_hallucinated_id) html += `<span class="badge bad">β hallucinated id</span>`; | |
| } | |
| html += `<span class="badge">${result.transcript.length} tool call${ | |
| result.transcript.length === 1 ? "" : "s"}</span>`; | |
| html += "</div>"; | |
| if (result.transcript.length) { | |
| html += `<details class="transcript"><summary>π§ tool timeline</summary>`; | |
| for (const step of result.transcript) { | |
| html += `<div class="tool-step"><span class="tool-name">${esc(step.tool)}(${ | |
| esc(JSON.stringify(step.args)).slice(0, 160)})</span><pre>${ | |
| esc(step.result).slice(0, 700)}</pre></div>`; | |
| } | |
| html += "</details>"; | |
| } | |
| if (o.tags.length) { | |
| html += `<table class="fields"><tr><th></th><th>type</th><th>friendly id</th><th>entity id</th><th>conf</th>${ | |
| has ? "<th></th>" : ""}</tr>`; | |
| for (const t of o.tags) { | |
| const mark = t.is_primary ? "β " : ""; | |
| const eid = t.entity_id ? `<span title="${esc(t.entity_id)}">${esc(String(t.entity_id).slice(0, 8))}β¦</span>` : "β"; | |
| html += `<tr class="${has && t.in_gold === false ? "miss" : ""}"><td>${mark}</td><td>${ | |
| esc(t.entity_type)}</td><td class="path">${esc(t.friendly_id)}</td><td>${eid}</td><td>${ | |
| esc(t.confidence)}</td>${has ? `<td>${t.in_gold ? "β " : "β"}</td>` : ""}</tr>`; | |
| } | |
| html += "</table>"; | |
| } else { | |
| html += '<p class="hint">no entities tagged</p>'; | |
| } | |
| if (has && gold) { | |
| const goldList = [gold.primary, ...gold.required, ...gold.optional]; | |
| const seen = new Set(); | |
| const items = goldList.filter((g) => { | |
| const k = (g.friendly_id || g.entity_id); | |
| return seen.has(k) ? false : seen.add(k); | |
| }).map((g) => `${g.entity_type} ${esc(g.friendly_id || g.entity_id.slice(0, 8) + "β¦")}${ | |
| g.friendly_id === gold.primary.friendly_id && g.entity_id === gold.primary.entity_id ? " β " : ""}`); | |
| html += `<details class="transcript"><summary>π― ground-truth entities (${gold.lead} lead)</summary><pre>${ | |
| items.join("\n")}</pre></details>`; | |
| } | |
| html += "</div>"; | |
| if (o.search_summary) html += `<p class="hint"><strong>summary:</strong> ${esc(o.search_summary)}</p>`; | |
| if (!o.parsed_ok) html += `<pre class="yaml">${esc(o.raw)}</pre>`; | |
| return html; | |
| } | |
| /* ---- feedback ---- */ | |
| async function thumb(event) { | |
| const value = Number(event.target.dataset.value); | |
| await api("/api/feedback", { | |
| method: "POST", | |
| body: JSON.stringify({ trace_id: state.traceId, value }), | |
| }); | |
| document.querySelectorAll(".thumb").forEach((b) => b.classList.remove("chosen")); | |
| event.target.classList.add("chosen"); | |
| $("feedback-done").textContent = "thanks β recorded!"; | |
| } | |
| async function sendNote() { | |
| const comment = $("note-text").value.trim(); | |
| if (!comment) return; | |
| await api("/api/feedback", { | |
| method: "POST", | |
| body: JSON.stringify({ trace_id: state.traceId, comment }), | |
| }); | |
| $("note-text").value = ""; | |
| $("feedback-done").textContent = "note recorded β thanks!"; | |
| } | |
| /* ---- wire up ---- */ | |
| $("gate-enter").onclick = enter; | |
| $("gate-passcode").addEventListener("keydown", (e) => e.key === "Enter" && enter()); | |
| $("deal").onclick = () => deal().catch(() => {}); | |
| $("run").onclick = run; | |
| $("input-text").addEventListener("input", () => { | |
| state.inputId = null; | |
| $("truth-hint").textContent = ""; | |
| }); | |
| // Cmd/Ctrl+Enter submits (plain Enter stays a newline β the input is multi-line). | |
| $("input-text").addEventListener("keydown", (e) => { | |
| if ((e.metaKey || e.ctrlKey) && e.key === "Enter" && !$("run").disabled) { | |
| e.preventDefault(); | |
| run(); | |
| } | |
| }); | |
| document.querySelectorAll(".thumb").forEach((b) => (b.onclick = thumb)); | |
| $("note-send").onclick = () => sendNote().catch(() => {}); | |
| $("download-trajectory").onclick = downloadTrajectory; | |
| $("copy-trajectory").onclick = copyTrajectory; | |
| $("note-text").addEventListener("keydown", (e) => e.key === "Enter" && sendNote().catch(() => {})); | |
| $("user-chip").onclick = () => showGate(); | |
| if (store.name) { | |
| boot().catch(() => showGate()); | |
| } else { | |
| showGate(); | |
| } | |