contimp-app / app /static /app.js
lefft's picture
Trajectory reveal/download (jsonl) + Cmd+Enter submit
d6217aa verified
Raw
History Blame Contribute Delete
15.4 kB
/* 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) =>
({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[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();
}