/* 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) => `
${esc(p.title)}` + `
${p.html}
` ).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 = '๐Ÿ” 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 = '
'; html += `model says${esc(predicted ?? "unparseable")}`; if (truth) { html += `ground truth${esc(truth)}`; html += `${predicted === truth ? "โœ…" : "โŒ"}`; } html += "
"; if (!predicted) html += `
${esc(result.raw_output)}
`; return html; } function renderConfig(result) { const o = result.output; let html = '
'; html += `${o.yaml_valid ? "โœ“" : "โœ—"} YAML`; html += `${o.schema_valid ? "โœ“" : "โœ—"} schema`; if (o.fields) { const hits = o.fields.filter((f) => f.match).length; const cls = hits === o.fields.length ? "good" : "bad"; html += `${hits}/${o.fields.length} requested fields`; } html += "
"; if (result.transcript.length) { html += `
๐Ÿ”ง ${result.transcript.length} tool call${ result.transcript.length > 1 ? "s" : ""}`; for (const step of result.transcript) { html += `
${esc(step.tool)}(${ esc(JSON.stringify(step.args)).slice(0, 120)})
${esc(step.result).slice(0, 600)}
`; } html += "
"; } html += `
${esc(o.yaml)}
`; if (!o.schema_valid && o.errors?.length) { html += ``; } if (o.fields) { html += ``; for (const f of o.fields) { html += ``; } html += "
requestedexpectedgot
${esc(f.path)}${ esc(JSON.stringify(f.expected))}${esc(JSON.stringify(f.got))}${ f.match ? "โœ…" : "โŒ"}
"; } return html; } function chip(value, ok) { const cls = ok === null || ok === undefined ? "" : ok ? "good" : "bad"; return `${esc(value ?? "โ€”")}`; } 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 += '
Intent classification
'; html += '
'; html += `model says${ chip(o.classification.category, has ? !!s.category_match : null)} ${ chip(o.classification.subcategory, has ? !!s.subcategory_match : null)}`; if (has && gold) { html += `ground truth${ chip(gold.classification.category)} ${chip(gold.classification.subcategory)}`; html += `${s.intent_exact ? "โœ…" : "โŒ"}`; } html += "
"; /* --- Task 2: entity tagging --- */ html += '
Entity tagging
'; html += '
'; html += `${ o.tags.length && o.search_summary ? "โœ“" : "โœ—"} format`; if (has) { html += `${ s.primary_found ? "โœ“" : "โœ—"} primary found`; html += `F1 ${(s.f1 ?? 0).toFixed(2)}`; if (!s.no_hallucinated_id) html += `โš  hallucinated id`; } html += `${result.transcript.length} tool call${ result.transcript.length === 1 ? "" : "s"}`; html += "
"; if (result.transcript.length) { html += `
๐Ÿ”ง tool timeline`; for (const step of result.transcript) { html += `
${esc(step.tool)}(${ esc(JSON.stringify(step.args)).slice(0, 160)})
${
        esc(step.result).slice(0, 700)}
`; } html += "
"; } if (o.tags.length) { html += `${ has ? "" : ""}`; for (const t of o.tags) { const mark = t.is_primary ? "โ˜…" : ""; const eid = t.entity_id ? `${esc(String(t.entity_id).slice(0, 8))}โ€ฆ` : "โ€”"; html += `${has ? `` : ""}`; } html += "
typefriendly identity idconf
${mark}${ esc(t.entity_type)}${esc(t.friendly_id)}${eid}${ esc(t.confidence)}${t.in_gold ? "โœ…" : "โŒ"}
"; } else { html += '

no entities tagged

'; } 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 += `
๐ŸŽฏ ground-truth entities (${gold.lead} lead)
${
      items.join("\n")}
`; } html += "
"; if (o.search_summary) html += `

summary: ${esc(o.search_summary)}

`; if (!o.parsed_ok) html += `
${esc(o.raw)}
`; 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(); }