/* 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)}
` +
`
${esc(result.raw_output)}`;
return html;
}
function renderConfig(result) {
const o = result.output;
let html = '${esc(step.result).slice(0, 600)}${esc(o.yaml)}`;
if (!o.schema_valid && o.errors?.length) {
html += `| requested | expected | got | |
|---|---|---|---|
| ${esc(f.path)} | ${ esc(JSON.stringify(f.expected))} | ${esc(JSON.stringify(f.got))} | ${ f.match ? "โ " : "โ"} |
${
esc(step.result).slice(0, 700)}| type | friendly id | entity id | conf | ${ has ? "" : ""} | |
|---|---|---|---|---|---|
| ${mark} | ${ esc(t.entity_type)} | ${esc(t.friendly_id)} | ${eid} | ${ esc(t.confidence)} | ${has ? `${t.in_gold ? "โ " : "โ"} | ` : ""}
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 += `${
items.join("\n")}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();
}