Spaces:
Running on Zero
Running on Zero
Download app/static/app.js from RealFalconsAI/DecisionLab: direct link, hf CLI and curl.
- Browser
- Download file 32.8 kB
-
https://huggingface.co/spaces/RealFalconsAI/DecisionLab/resolve/main/app/static/app.js
- Command line
-
hf download hf://spaces/RealFalconsAI/DecisionLab/app/static/app.js
-
curl -L -o app.js https://huggingface.co/spaces/RealFalconsAI/DecisionLab/resolve/main/app/static/app.js
32.8 kB
| ; | |
| // The server owns the model list and its order (/api/status "order"). This default only covers the first paint. | |
| let MODELS = []; // filled from /api/status; nothing is assumed before the server answers | |
| const $ = (id) => document.getElementById(id); | |
| const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); | |
| const pct = (p) => `${(100 * p).toFixed(1)}%`; | |
| const nModels = () => MODELS.length; | |
| // ------------------------------------------------------------------ API access from the page | |
| // The API is open (no token). If the server cannot be reached, say so plainly. | |
| async function api(url, opts = {}) { | |
| try { | |
| return await fetch(url, { ...opts, credentials: "same-origin" }); | |
| } catch { | |
| throw new Error("The page lost its connection to the lab server. Reload the page (Ctrl+F5)."); | |
| } | |
| } | |
| // CSP (DL-SA-006) forbids inline style attributes; bars carry data-scale and get their transform here. | |
| function applyScales(root) { | |
| root.querySelectorAll("[data-scale]").forEach((el) => { el.style.transform = `scaleX(${+el.dataset.scale || 0})`; }); | |
| } | |
| let DEMOS = []; | |
| let current = null; // selected demo | |
| let last = null; // {questions, reference, response, roundTrip} | |
| let status = null; | |
| // ------------------------------------------------------------------ status | |
| async function pollStatus() { | |
| try { | |
| status = await (await api("/api/status")).json(); | |
| syncModels(); | |
| renderStatus(); | |
| const busy = Object.values(status.models).some((m) => m.status === "loading" || m.status === "idle"); | |
| setTimeout(pollStatus, busy ? 2000 : 15000); | |
| } catch { | |
| $("readiness").textContent = "Lab server not reachable"; | |
| setTimeout(pollStatus, 4000); | |
| } | |
| } | |
| function syncModels() { | |
| const order = status.order || Object.keys(status.models); | |
| const next = order.filter((k) => status.models[k]).map((k) => { | |
| const d = status.models[k]; | |
| return { key: k, name: d.name || k, side: d.side || k }; | |
| }); | |
| const changed = JSON.stringify(next) !== JSON.stringify(MODELS); | |
| MODELS = next; | |
| if (changed) { applyModelCount(); renderHero(); renderScoreHead(); } | |
| } | |
| function applyModelCount() { | |
| document.documentElement.style.setProperty("--n-models", String(nModels())); | |
| $("run").textContent = runLabel(); | |
| const n = { 1: "One", 2: "Both", 3: "All three" }[nModels()] || `All ${nModels()}`; | |
| document.querySelectorAll("[data-n-models]").forEach((el) => { el.textContent = el.dataset.nModels.replace("{N}", n).replace("{n}", n.toLowerCase()); }); | |
| } | |
| const runLabel = () => (nModels() <= 1 ? "Run the model" : nModels() === 2 ? "Run both models" : `Run all ${nModels()} models`); | |
| function renderHero() { | |
| // "vs" between boxes reads well on one row (up to 3 models); with more, the boxes wrap into rows without it. | |
| const vs = MODELS.length <= 3; | |
| $("versus").classList.toggle("many", !vs); | |
| $("versus").innerHTML = MODELS.map((m, i) => `${i && vs ? `<div class="versus-mid">vs</div>` : ""} | |
| <div class="versus-side side-${esc(m.side)}"> | |
| <span class="versus-name">${esc(m.name)}</span> | |
| <span class="versus-figure" id="hero-params-${esc(m.key)}">–</span> | |
| <span class="versus-unit">parameters</span> | |
| </div>`).join(""); | |
| } | |
| function sourceLine(m, d) { | |
| if (d.path) return `<span title="Loaded from a local folder">Local folder <code>${esc(d.path)}</code></span>`; | |
| const repo = d.repo || ""; | |
| return repo ? `<a href="https://huggingface.co/${esc(repo)}" target="_blank" rel="noopener noreferrer">${esc(repo)}</a>` : ""; | |
| } | |
| function renderStatus() { | |
| const ms = status.models; | |
| const dir = status.env?.models_dir || "/models"; | |
| const found = MODELS.filter((m) => m.key !== "laya").length; | |
| $("models-found").textContent = found | |
| ? `Found ${found} model${found === 1 ? "" : "s"} in ${dir} (your DecisionLab\\models folder). Add or remove a folder there and restart the container to change the line-up.` | |
| : ""; | |
| const none = found ? "" : `<p class="no-models">No models found in <code>${esc(dir)}</code>. Unzip a model into your <code>DecisionLab\\models</code> folder (each in its own folder: a LightDec folder has <code>falcondec_config.json</code>, an Arthur folder has <code>config.json</code> and <code>model.safetensors</code>) and restart the container. Laya still runs.</p>`; | |
| $("models").innerHTML = none + MODELS.map((m) => { | |
| const d = ms[m.key] || {}; | |
| const label = { ready: "Ready", loading: "Loading", error: "Failed to load", idle: "Waiting" }[d.status] || d.status; | |
| const facts = [ | |
| ["Parameters", d.params_m ? `${d.params_m}M` : "–"], | |
| ["Weights", d.weights_mb ? `${d.weights_mb} MB` : m.key === "laya" ? "≈808 MB" : "–"], | |
| ["Device", d.device ? d.device.toUpperCase() : "–"], | |
| ["Load time", d.load_seconds ? `${d.load_seconds} s` : "–"], | |
| ["Version", d.version || "–"], | |
| ["Native confidence", d.confidence_native || "–"], | |
| ]; | |
| return `<article class="model-card side-${esc(m.side)}" data-model="${esc(m.key)}"> | |
| <h3>${esc(m.name)}<span class="state-tag ${esc(d.status)}">${esc(label)}</span></h3> | |
| <p class="repo">${sourceLine(m, d)}${d.note ? ` · ${esc(d.note)}` : ""}</p> | |
| <dl class="facts">${facts.map(([k, v]) => `<div><dt>${k}</dt><dd>${esc(v)}</dd></div>`).join("")}</dl> | |
| ${d.status === "error" ? `<p class="model-error">${esc(d.error)}</p><p><button class="btn btn-quiet btn-small" data-reload="${esc(m.key)}">Try loading again</button></p>` : ""} | |
| </article>`; | |
| }).join(""); | |
| for (const m of MODELS) { | |
| const d = ms[m.key] || {}, el = $(`hero-params-${m.key}`); | |
| if (el) el.textContent = d.params_m ? `${Math.round(d.params_m)}M` : "–"; | |
| } | |
| document.querySelectorAll("[data-reload]").forEach((b) => b.addEventListener("click", async () => { | |
| b.disabled = true; b.textContent = "Loading…"; | |
| await api(`/api/reload/${encodeURIComponent(b.dataset.reload)}`, { method: "POST" }); | |
| setTimeout(pollStatus, 500); | |
| })); | |
| const ready = MODELS.filter((m) => ms[m.key]?.status === "ready").map((m) => m.name); | |
| const loading = MODELS.filter((m) => ms[m.key]?.status === "loading").map((m) => m.name); | |
| const r = $("readiness"); | |
| r.classList.toggle("ready", ready.length === nModels()); | |
| r.textContent = ready.length === nModels() ? (nModels() === 2 ? "Both models ready" : `All ${nModels()} models ready`) | |
| : loading.length ? `Loading ${loading.join(", ")}…` : ready.length ? `${ready.join(", ")} ready` : "Models not loaded"; | |
| const e = status.env; | |
| $("env").textContent = `Server: ${e.gpu ? `GPU ${e.gpu}` : `CPU, ${e.threads} threads`}, PyTorch ${e.torch}, Python ${e.python}.`; | |
| } | |
| // ------------------------------------------------------------------ demos: tabs, tiles, filter, prev/next | |
| let GROUPS = []; | |
| let activeGroup = null; | |
| const groupOf = (id) => GROUPS.find((g) => g.id === id); | |
| async function loadDemos() { | |
| [DEMOS, GROUPS] = await Promise.all([ | |
| api("/api/demos").then((r) => r.json()), | |
| api("/api/groups").then((r) => r.json()).catch(() => []), | |
| ]); | |
| if (!GROUPS.length) { // older server: derive groups from the demos | |
| GROUPS = [...new Set(DEMOS.map((d) => d.group))].map((id) => ({ id, label: id, family: "", blurb: "" })); | |
| } | |
| DEMOS.forEach((d, i) => { d.num = i + 1; }); | |
| for (const g of GROUPS) g.count = DEMOS.filter((d) => d.group === g.id).length; | |
| activeGroup = GROUPS[0]?.id; | |
| renderTabs(); | |
| renderScope(); | |
| $("demo-filter").placeholder = `Filter all ${DEMOS.length} demos`; | |
| $("demo-filter").addEventListener("input", renderTiles); | |
| $("prev").addEventListener("click", () => step(-1)); | |
| $("next").addEventListener("click", () => step(1)); | |
| selectDemo(DEMOS[0].id); | |
| } | |
| function renderTabs() { | |
| const families = [...new Set(GROUPS.map((g) => g.family))]; | |
| $("tabs").innerHTML = families.map((f) => ` | |
| <div class="tab-family"> | |
| ${f ? `<span class="family-label">${esc(f)}</span>` : ""} | |
| <div class="tabs">${GROUPS.filter((g) => g.family === f).map((g) => ` | |
| <button type="button" role="tab" class="tab" data-group="${esc(g.id)}">${esc(g.label)} <span class="tab-count">${g.count}</span></button>`).join("")} | |
| </div> | |
| </div>`).join(""); | |
| document.querySelectorAll(".tab").forEach((t) => t.addEventListener("click", () => { | |
| $("demo-filter").value = ""; | |
| activeGroup = t.dataset.group; | |
| const first = DEMOS.find((d) => d.group === activeGroup); | |
| if (first && current?.group !== activeGroup) selectDemo(first.id); else renderTiles(); | |
| })); | |
| } | |
| function renderTiles() { | |
| const q = $("demo-filter").value.trim().toLowerCase(); | |
| const list = q | |
| ? DEMOS.filter((d) => `${d.num} ${d.title} ${d.blurb} ${groupOf(d.group)?.label ?? d.group}`.toLowerCase().includes(q)) | |
| : DEMOS.filter((d) => d.group === activeGroup); | |
| document.querySelectorAll(".tab").forEach((t) => t.setAttribute("aria-selected", String(!q && t.dataset.group === activeGroup))); | |
| $("group-blurb").textContent = q ? `${list.length} of ${DEMOS.length} demos match.` : (groupOf(activeGroup)?.blurb || ""); | |
| $("tiles").innerHTML = list.length ? list.map((d) => ` | |
| <button type="button" role="option" class="tile" data-demo="${esc(d.id)}" aria-selected="${current?.id === d.id}" title="${esc(d.blurb)}"> | |
| <span class="n">${d.num}</span><span class="t">${esc(d.title)}</span> | |
| <span class="g">${q ? `${esc(groupOf(d.group)?.label ?? d.group)} · ` : ""}${Object.keys(d.questions).length} question${Object.keys(d.questions).length === 1 ? "" : "s"}</span> | |
| </button>`).join("") : `<p class="none">No demo matches “${esc(q)}”.</p>`; | |
| $("tiles").querySelectorAll(".tile").forEach((b) => b.addEventListener("click", () => selectDemo(b.dataset.demo))); | |
| } | |
| function renderScope() { | |
| const nq = (ds) => ds.reduce((t, d) => t + Object.keys(d.questions).length, 0); | |
| const all = [...new Set(GROUPS.map((g) => g.family))].filter(Boolean); | |
| const families = all.length > 1 ? all : []; // a single family would only repeat "All demos" | |
| $("scope").innerHTML = `<option value="">All demos (${DEMOS.length} demos, ${nq(DEMOS)} questions)</option>` | |
| + families.map((f) => { | |
| const ds = DEMOS.filter((d) => groupOf(d.group)?.family === f); | |
| return `<option value="family:${esc(f)}">${esc(f)} (${ds.length} demos)</option>`; | |
| }).join("") | |
| + GROUPS.map((g) => `<option value="group:${esc(g.id)}">— ${esc(g.label)} (${g.count})</option>`).join(""); | |
| $("scope").addEventListener("change", updateRunLabel); | |
| updateRunLabel(); | |
| } | |
| function scopedDemos() { | |
| const v = $("scope").value; | |
| if (v.startsWith("family:")) return DEMOS.filter((d) => groupOf(d.group)?.family === v.slice(7)); | |
| if (v.startsWith("group:")) return DEMOS.filter((d) => d.group === v.slice(6)); | |
| return DEMOS; | |
| } | |
| function updateRunLabel() { | |
| const n = scopedDemos().length; | |
| $("run-all").textContent = n === DEMOS.length ? `Run all ${n} demos` : `Run ${n} demos`; | |
| } | |
| function selectDemo(id) { | |
| current = DEMOS.find((d) => d.id === id); | |
| if (!$("demo-filter").value.trim()) activeGroup = current.group; | |
| renderTiles(); | |
| $("demo-title").innerHTML = `<span class="num">${current.num}</span>${esc(current.title)}`; | |
| $("demo-blurb").textContent = current.blurb; | |
| $("state").value = typeof current.state === "string" ? current.state : JSON.stringify(current.state, null, 2); | |
| $("questions").value = JSON.stringify(current.questions, null, 2); | |
| $("form-error").textContent = ""; | |
| $("prev").disabled = current.num === 1; | |
| $("next").disabled = current.num === DEMOS.length; | |
| } | |
| function step(delta) { | |
| const i = DEMOS.indexOf(current) + delta; | |
| if (i >= 0 && i < DEMOS.length) { | |
| $("demo-filter").value = ""; | |
| selectDemo(DEMOS[i].id); | |
| } | |
| } | |
| function parseState(text) { | |
| const t = text.trim(); | |
| if (t.startsWith("{") || t.startsWith("[")) { | |
| try { return JSON.parse(t); } catch { /* plain text that happens to start with a brace */ } | |
| } | |
| return t; | |
| } | |
| // ------------------------------------------------------------------ running | |
| async function decide(state, questions) { | |
| const t0 = performance.now(); | |
| const res = await api("/api/decide", { | |
| method: "POST", headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(MODELS.length ? { state, questions, models: MODELS.map((m) => m.key) } : { state, questions }), | |
| }); | |
| const body = await res.json(); | |
| if (!res.ok) throw new Error(body.detail || `Request failed (${res.status})`); | |
| return { response: body, roundTrip: performance.now() - t0 }; | |
| } | |
| async function runCurrent() { | |
| const err = $("form-error"); | |
| err.textContent = ""; | |
| let questions; | |
| try { questions = JSON.parse($("questions").value); } catch (e) { | |
| err.textContent = `Questions aren't valid JSON: ${e.message}`; return; | |
| } | |
| const state = parseState($("state").value); | |
| if (!state || (typeof state === "string" && !state.length)) { err.textContent = "Add a state for the models to read."; return; } | |
| const btn = $("run"); | |
| btn.disabled = true; btn.textContent = "Running…"; | |
| try { | |
| const { response, roundTrip } = await decide(state, questions); | |
| const sameDemo = current && JSON.stringify(current.questions) === JSON.stringify(questions); | |
| last = { questions, reference: sameDemo ? current.reference : {}, response, roundTrip }; | |
| renderVerdicts(); | |
| $("verdicts").scrollIntoView(); | |
| } catch (e) { | |
| err.textContent = e.message; | |
| } finally { | |
| btn.disabled = false; btn.textContent = runLabel(); | |
| } | |
| } | |
| // ------------------------------------------------------------------ rendering helpers | |
| const measure = () => $("measure").value; | |
| const threshold = () => parseFloat($("threshold").value); | |
| const conf = (rec) => rec[measure()]; | |
| function optionLabels(q) { | |
| const t = q.type || "choice"; | |
| const crit = q.criteria ?? q.options; | |
| if (t === "noul") return [["true", "Yes"], ["false", "No"]]; | |
| if (t === "score") return crit.map((c, i) => [String(i), `${i} · ${c}`]); | |
| if (Array.isArray(crit)) return crit.map((c) => [String(c), String(c)]); | |
| return Object.entries(crit).map(([k, v]) => [k, v ? `${k}: ${v}` : k]); | |
| } | |
| function answerText(q, rec) { | |
| if (!rec) return "–"; | |
| const t = q.type || "choice"; | |
| if (t === "noul") return `${rec.choice === "true" ? "Yes" : "No"} (P(yes) ${pct(rec.p_true)})`; | |
| if (t === "score") { | |
| const crit = q.criteria ?? q.options; | |
| return `${crit[+rec.choice]} (level ${rec.expected_level.toFixed(2)})`; | |
| } | |
| return rec.choice; | |
| } | |
| function answerBlock(m, q, rec, err, ref) { | |
| const head = `<div class="who">${esc(m.name)}</div>`; | |
| if (err) return `<div class="answer side-${esc(m.side)}">${head}<div class="err">${esc(err)}</div></div>`; | |
| if (!rec) return `<div class="answer side-${esc(m.side)}">${head}<div class="err">No answer returned.</div></div>`; | |
| const c = conf(rec); | |
| const act = c >= threshold(); | |
| const refTxt = ref === undefined ? "" : rec.choice === ref ? `<span class="ref hit">matches reference</span>` : `<span class="ref miss">differs from reference</span>`; | |
| return `<div class="answer side-${esc(m.side)}"> | |
| ${head} | |
| <div class="what">${esc(answerText(q, rec))}</div> | |
| <div class="how">confidence ${c.toFixed(3)}<span class="badge ${act ? "act" : "defer"}">${act ? "act" : "defer"}</span>${refTxt}</div> | |
| </div>`; | |
| } | |
| // how many of the models that answered give the same top answer: {agree, of} | |
| function agreement(recs) { | |
| const got = recs.filter(Boolean); | |
| if (got.length < 2) return null; | |
| const counts = {}; | |
| for (const r of got) counts[r.choice] = (counts[r.choice] || 0) + 1; | |
| return { agree: Math.max(...Object.values(counts)), of: got.length }; | |
| } | |
| function agreeTag(ag) { | |
| if (!ag) return ""; | |
| const all = ag.agree === ag.of; | |
| const text = all ? (ag.of === 2 ? "Models agree" : `All ${ag.of} agree`) : ag.agree === 1 ? "All differ" : `${ag.agree} of ${ag.of} agree`; | |
| return `<span class="agree ${all ? "yes" : ag.agree === 1 ? "no" : "part"}">${text}</span>`; | |
| } | |
| function renderVerdicts() { | |
| if (!last) return; | |
| const { questions, reference, response, roundTrip } = last; | |
| const R = response.results; | |
| const res = MODELS.map((m) => ({ m, r: R[m.key] || {} })); | |
| const tm = $("timing"); | |
| tm.hidden = false; | |
| const timed = res.filter((x) => x.r.ms != null); | |
| const fast = timed.length ? timed.reduce((a, b) => (b.r.ms < a.r.ms ? b : a)) : null; | |
| const slow = timed.length ? timed.reduce((a, b) => (b.r.ms > a.r.ms ? b : a)) : null; | |
| tm.innerHTML = res.map((x) => `<div class="t-model side-${esc(x.m.side)}"><b>${x.r.ms != null ? `${x.r.ms} ms` : "–"}</b><span>${esc(x.m.name)} model time</span></div>`).join("") | |
| + `<div><b>${fast && slow && fast !== slow ? `${(slow.r.ms / fast.r.ms).toFixed(1)}×` : "–"}</b><span>${fast && slow && fast !== slow ? `${esc(fast.m.name)} fastest, vs ${esc(slow.m.name)}` : "speed ratio"}</span></div>` | |
| + `<div><b>${Math.round(roundTrip)} ms</b><span>round trip from your browser</span></div>`; | |
| $("verdict-list").innerHTML = Object.entries(questions).map(([name, q]) => { | |
| const recs = res.map((x) => x.r.answers?.[name]); | |
| const ref = reference?.[name]; | |
| const rows = optionLabels(q).map(([k, label]) => ` | |
| <div class="bar-label ${ref === k ? "is-ref" : ""}">${esc(label)}</div> | |
| <div class="bar-stack">${res.map((x, i) => { | |
| const rec = recs[i], p = rec?.probs?.[k] ?? 0; | |
| return `<div class="bar-cell side-${esc(x.m.side)} ${rec?.choice === k ? "top" : ""}" title="${esc(x.m.name)}"><i data-scale="${p.toFixed(4)}"></i><span>${rec ? pct(p) : ""}</span></div>`; | |
| }).join("")}</div>`).join(""); | |
| return `<article class="verdict"> | |
| <div class="verdict-head"> | |
| <div><h3 class="verdict-q">${esc(q.instructions || q.question || name)}</h3> | |
| <p class="verdict-meta">${esc(name)} · ${esc(q.type || "choice")}</p></div> | |
| ${agreeTag(agreement(recs))} | |
| </div> | |
| <div class="answers">${res.map((x, i) => answerBlock(x.m, q, recs[i], x.r.error, ref)).join("")}</div> | |
| <div class="bars">${rows}</div> | |
| </article>`; | |
| }).join(""); | |
| applyScales($("verdict-list")); | |
| $("raw").hidden = false; | |
| $("raw-json").textContent = JSON.stringify(response, null, 2); | |
| } | |
| // ------------------------------------------------------------------ scoreboard | |
| let board = null; // {rows: [{demo, name, q, ref, high, ans: {modelKey: rec}}], times: {modelKey: [ms]}} | |
| async function runAll() { | |
| const btn = $("run-all"); | |
| btn.disabled = true; | |
| const rows = [], times = Object.fromEntries(MODELS.map((m) => [m.key, []])); | |
| try { | |
| const list = scopedDemos(); | |
| for (let i = 0; i < list.length; i++) { | |
| const d = list[i]; | |
| $("progress").textContent = `Running demo ${i + 1} of ${list.length}: ${d.title}`; | |
| const { response } = await decide(d.state, d.questions); | |
| for (const k of Object.keys(times)) if (response.results[k]?.ms != null) times[k].push(response.results[k].ms); | |
| for (const [name, q] of Object.entries(d.questions)) { | |
| rows.push({ demo: d, name, q, ref: d.reference?.[name], high: d.stakes?.[name] === "high", | |
| ans: Object.fromEntries(MODELS.map((m) => [m.key, response.results[m.key]?.answers?.[name]])) }); | |
| } | |
| } | |
| board = { rows, times }; | |
| $("progress").textContent = `Done: ${rows.length} questions across ${list.length} demos.`; | |
| renderScoreHead(); | |
| renderBoard(); | |
| } catch (e) { | |
| $("progress").textContent = `Stopped: ${e.message}`; | |
| } finally { | |
| btn.disabled = false; | |
| } | |
| } | |
| function median(xs) { | |
| if (!xs.length) return null; | |
| const s = [...xs].sort((x, y) => x - y), m = Math.floor(s.length / 2); | |
| return s.length % 2 ? s[m] : (s[m - 1] + s[m]) / 2; | |
| } | |
| // Agentic Use Score (AUS): a 0-100 score skewed toward what matters when an agent acts on the answer. | |
| // Every question counts once; questions marked high-stakes (irreversible, security, money) count twice. | |
| const AUS_WEIGHTS = { precision: 0.35, caught: 0.25, accuracy: 0.15, autonomy: 0.15, speed: 0.10 }; | |
| const AUS_LABELS = { | |
| precision: "Right when it acts", caught: "Flags its own mistakes", accuracy: "Overall accuracy", | |
| autonomy: "Handles on its own", speed: "Speed", | |
| }; | |
| function speedScore(ms) { // 1.0 at 50 ms or faster, 0 at 1 s or slower, log scale in between | |
| if (ms == null) return 0; | |
| return Math.max(0, Math.min(1, 1 - Math.log10(Math.max(ms, 1) / 50) / Math.log10(20))); | |
| } | |
| function modelStats(rows, key, times, thr) { | |
| let n = 0, hits = 0, acted = 0, actedHits = 0, wrong = 0, wrongDeferred = 0; // raw counts | |
| let W = 0, wHits = 0, wActed = 0, wActedHits = 0, wWrong = 0, wWrongDeferred = 0; // stakes-weighted | |
| for (const r of rows) { | |
| const rec = r.ans[key]; | |
| if (!rec || r.ref === undefined) continue; | |
| const w = r.high ? 2 : 1, ok = rec.choice === r.ref, act = conf(rec) >= thr; | |
| n++; W += w; | |
| if (ok) { hits++; wHits += w; } | |
| if (act) { acted++; wActed += w; if (ok) { actedHits++; wActedHits += w; } } | |
| if (!ok) { wrong++; wWrong += w; if (!act) { wrongDeferred++; wWrongDeferred += w; } } | |
| } | |
| const med = median(times || []); | |
| const parts = { | |
| precision: (wActedHits + 1) / (wActed + 2), // small prior: a model that acts twice isn't "perfect" | |
| caught: wWrong ? wWrongDeferred / wWrong : 1, | |
| accuracy: W ? wHits / W : 0, | |
| autonomy: W ? wActed / W : 0, | |
| speed: speedScore(med), | |
| }; | |
| const aus = 100 * Object.entries(AUS_WEIGHTS).reduce((t, [k, w]) => t + w * parts[k], 0); | |
| return { n, hits, acted, actedHits, wrong, wrongDeferred, confidentWrong: acted - actedHits, med, parts, aus }; | |
| } | |
| // Which models are best on a metric. values: {modelKey: number|null}. Returns the Set of keys within eps of the | |
| // best value, or an empty Set when every model is tied (nothing to highlight) or fewer than two have a value. | |
| function best(values, higherIsBetter = true, eps = 1e-9) { | |
| const got = Object.entries(values).filter(([, v]) => v != null); | |
| if (got.length < 2) return new Set(); | |
| const top = higherIsBetter ? Math.max(...got.map(([, v]) => v)) : Math.min(...got.map(([, v]) => v)); | |
| const keys = got.filter(([, v]) => Math.abs(v - top) < eps).map(([k]) => k); | |
| return keys.length === got.length ? new Set() : new Set(keys); | |
| } | |
| const modelHeads = () => MODELS.map((m) => `<th scope="col" class="side-${esc(m.side)}">${esc(m.name)}</th>`).join(""); | |
| // per-group matches and confident mistakes, so a model's weak spots show up by use case | |
| function groupBreakdown(rows, thr) { | |
| const ids = [...new Set(rows.map((r) => r.demo.group))]; | |
| if (ids.length < 2) return ""; | |
| const line = (rs, key) => { | |
| const scored = rs.filter((r) => r.ans[key] && r.ref !== undefined); | |
| const hits = scored.filter((r) => r.ans[key].choice === r.ref).length; | |
| const cw = scored.filter((r) => r.ans[key].choice !== r.ref && conf(r.ans[key]) >= thr).length; | |
| return { hits, n: scored.length, cw, rate: scored.length ? hits / scored.length : null }; | |
| }; | |
| const body = ids.map((id) => { | |
| const rs = rows.filter((r) => r.demo.group === id); | |
| const L = Object.fromEntries(MODELS.map((m) => [m.key, line(rs, m.key)])); | |
| const w = best(Object.fromEntries(MODELS.map((m) => [m.key, L[m.key].rate]))); | |
| return `<tr><td>${esc(groupOf(id)?.label ?? id)}<small>${esc(groupOf(id)?.family ?? "")}</small></td>${MODELS.map((m) => { | |
| const x = L[m.key]; | |
| if (!x.n) return `<td><b>–</b><small>no reference answers</small></td>`; // e.g. Agent security: states and questions only | |
| return `<td class="${w.has(m.key) ? "win" : ""}"><b>${x.hits} / ${x.n}</b><small>${x.cw} confident mistake${x.cw === 1 ? "" : "s"}</small></td>`; | |
| }).join("")}</tr>`; | |
| }).join(""); | |
| return `<div class="group-board"> | |
| <h3>By group</h3> | |
| <div class="table-scroll"><table class="aus-table"> | |
| <thead><tr><th scope="col">Group</th>${modelHeads()}</tr></thead> | |
| <tbody>${body}</tbody> | |
| </table></div> | |
| </div>`; | |
| } | |
| function renderScoreHead() { | |
| $("score-head").innerHTML = `<th scope="col">Demo</th><th scope="col">Question</th><th scope="col">Reference</th>${modelHeads()}<th scope="col">Agree</th>`; | |
| } | |
| // A run with no reference answers (e.g. only the Agent security demos) cannot be scored: the Agentic Use Score | |
| // would fall back to the same defaults for every model. Show what CAN be compared instead. | |
| function renderUnscoredSummary() { | |
| const { rows, times } = board; | |
| const thr = threshold(); | |
| const card = (m) => { | |
| const recs = rows.map((r) => r.ans[m.key]).filter(Boolean); | |
| const acts = recs.filter((rec) => conf(rec) >= thr).length; | |
| const med = median(times[m.key] || []); | |
| const facts = [ | |
| ["Questions answered", `${recs.length} / ${rows.length}`], | |
| ["Median model time", med != null ? `${Math.round(med)} ms` : "–"], | |
| [`Acts on (conf ≥ ${thr.toFixed(2)})`, `${acts} / ${recs.length}`], | |
| ["Defers", `${recs.length - acts} / ${recs.length}`], | |
| ]; | |
| return `<div class="sum-card side-${esc(m.side)}"> | |
| <h3>${esc(m.name)}</h3> | |
| <dl>${facts.map(([k, v]) => `<div><dt>${k}</dt><dd>${v}</dd></div>`).join("")}</dl> | |
| </div>`; | |
| }; | |
| const answered = rows.filter((r) => MODELS.every((m) => r.ans[m.key])); | |
| const allAgree = answered.filter((r) => new Set(MODELS.map((m) => r.ans[m.key].choice)).size === 1).length; | |
| const sum = $("summary"); | |
| sum.hidden = false; | |
| sum.innerHTML = `<div class="unscored-note"> | |
| <h3>Not scored</h3> | |
| <p>The Agentic Use Score compares each answer with a reference answer, and the questions in this run have none. So there is no score or ranking here; compare the models below by what they would act on at your threshold, their speed, and how often they agree.</p> | |
| </div>` | |
| + MODELS.map(card).join("") | |
| + `<p class="sum-both">All ${MODELS.length} models give the same top answer on ${allAgree} of ${answered.length} questions. Confidence uses the measure selected above; changing it or the threshold updates every number here.</p>`; | |
| } | |
| function renderSummary() { | |
| const { rows, times } = board; | |
| if (!rows.some((r) => r.ref !== undefined)) return renderUnscoredSummary(); | |
| const thr = threshold(); | |
| const S = Object.fromEntries(MODELS.map((m) => [m.key, modelStats(rows, m.key, times[m.key], thr)])); | |
| const ratio = (x, y) => (y ? x / y : null); | |
| const per = (f) => Object.fromEntries(MODELS.map((m) => [m.key, f(S[m.key])])); | |
| const metrics = [ | |
| { label: "Matches reference", show: (x) => `${x.hits} / ${x.n}`, w: best(per((x) => x.hits)) }, | |
| { label: "Median model time", show: (x) => (x.med != null ? `${Math.round(x.med)} ms` : "–"), w: best(per((x) => x.med), false, 0.5) }, | |
| { label: `Acts on (conf ≥ ${thr.toFixed(2)})`, show: (x) => `${x.acted} / ${x.n}`, w: best(per((x) => x.acted)) }, | |
| { label: "Correct when acting", show: (x) => (x.acted ? `${x.actedHits} / ${x.acted}` : "–"), w: best(per((x) => ratio(x.actedHits, x.acted))) }, | |
| { label: "Confident mistakes", show: (x) => `${x.confidentWrong}`, w: best(per((x) => x.confidentWrong), false) }, | |
| { label: "Mistakes it deferred", show: (x) => (x.wrong ? `${x.wrongDeferred} / ${x.wrong}` : "–"), w: best(per((x) => ratio(x.wrongDeferred, x.wrong) ?? 1)) }, | |
| ]; | |
| const overall = best(per((x) => x.aus), true, 0.05); | |
| const answered = rows.filter((r) => MODELS.every((m) => r.ans[m.key])); | |
| const allAgree = answered.filter((r) => new Set(MODELS.map((m) => r.ans[m.key].choice)).size === 1).length; | |
| const card = (m) => { | |
| const st = S[m.key], win = overall.has(m.key); | |
| return `<div class="sum-card side-${esc(m.side)} ${win ? "winner" : ""}"> | |
| <h3>${esc(m.name)}${win ? `<span class="win-tag">Best for agents</span>` : ""}</h3> | |
| <dl>${metrics.map((x) => `<div class="${x.w.has(m.key) ? "win" : ""}"><dt>${x.label}</dt><dd>${x.show(st)}</dd></div>`).join("")}</dl> | |
| </div>`; | |
| }; | |
| const ranked = [...MODELS].sort((x, y) => S[y.key].aus - S[x.key].aus); | |
| let pos = 0, prev = null; | |
| const rankItems = ranked.map((m, i) => { | |
| const v = S[m.key].aus; | |
| if (prev === null || Math.abs(prev - v) >= 0.05) pos = i + 1; | |
| const tied = ranked.some((o) => o !== m && Math.abs(S[o.key].aus - v) < 0.05); | |
| prev = v; | |
| return `<li class="side-${esc(m.side)} ${overall.has(m.key) ? "first" : ""}"> | |
| <span class="aus-pos">${tied ? `=${pos}` : pos}</span> | |
| <span class="aus-name">${esc(m.name)}</span> | |
| <span class="aus-bar"><i data-scale="${(v / 100).toFixed(4)}"></i></span> | |
| <span class="aus-val">${v.toFixed(1)}</span> | |
| </li>`; | |
| }).join(""); | |
| const aus = `<div class="aus"> | |
| <div class="aus-head"> | |
| <h3>Agentic Use Score</h3> | |
| <p>How safe and useful each model is when an agent acts on its answers. Weighted toward being right when it acts and flagging its own mistakes; high-stakes questions count double.</p> | |
| </div> | |
| <ol class="aus-rank">${rankItems}</ol> | |
| <div class="table-scroll"><table class="aus-table"> | |
| <thead><tr><th scope="col">Component</th><th scope="col">Max points</th>${modelHeads()}</tr></thead> | |
| <tbody>${Object.keys(AUS_WEIGHTS).map((k) => { | |
| const max = AUS_WEIGHTS[k] * 100; | |
| const pts = Object.fromEntries(MODELS.map((m) => [m.key, max * S[m.key].parts[k]])); | |
| const w = best(pts, true, 0.05); | |
| return `<tr><td>${AUS_LABELS[k]}</td><td>${max.toFixed(0)}</td>${MODELS.map((m) => | |
| `<td class="${w.has(m.key) ? "win" : ""}"><b>${pts[m.key].toFixed(1)}</b><small>${Math.round(100 * S[m.key].parts[k])}% of max</small></td>`).join("")}</tr>`; | |
| }).join("")}</tbody> | |
| <tfoot><tr><th scope="row">Agentic Use Score</th><td>100</td>${MODELS.map((m) => | |
| `<td class="${overall.has(m.key) ? "win" : ""}"><b>${S[m.key].aus.toFixed(1)}</b></td>`).join("")}</tr></tfoot> | |
| </table></div> | |
| </div>`; | |
| const sum = $("summary"); | |
| sum.hidden = false; | |
| sum.innerHTML = aus + MODELS.map(card).join("") | |
| + groupBreakdown(rows, thr) | |
| + `<p class="sum-both">Highlighted values are the best model on that metric (none are highlighted when all tie). All ${MODELS.length} models give the same top answer on ${allAgree} of ${answered.length} questions. Confidence uses the measure selected above; changing it or the threshold updates every number here.</p>`; | |
| applyScales(sum); | |
| } | |
| function renderScoreRows() { | |
| const { rows } = board; | |
| const thr = threshold(); | |
| const cell = (q, rec, ref) => { | |
| if (!rec) return `<td>–</td>`; | |
| const cls = ref === undefined ? "" : rec.choice === ref ? "hit" : "miss"; | |
| return `<td class="${cls}">${esc(answerText(q, rec))}<small>conf ${conf(rec).toFixed(3)} · ${conf(rec) >= thr ? "act" : "defer"}</small></td>`; | |
| }; | |
| const refText = (q, ref) => { | |
| if (ref === undefined) return "–"; | |
| if ((q.type || "choice") === "noul") return ref === "true" ? "Yes" : "No"; | |
| if (q.type === "score") return (q.criteria ?? q.options)[+ref]; | |
| return ref; | |
| }; | |
| const table = $("score-table"); | |
| table.hidden = false; | |
| table.querySelector("tbody").innerHTML = rows.map((r) => { | |
| const ag = agreement(MODELS.map((m) => r.ans[m.key])); | |
| return `<tr> | |
| <td>${esc(r.demo.title)}<small>${esc(groupOf(r.demo.group)?.label ?? r.demo.group)}</small></td> | |
| <td>${esc(r.q.instructions)}<small>${esc(r.name)} · ${esc(r.q.type || "choice")}${r.high ? ` · <span class="stakes">high stakes</span>` : ""}</small></td> | |
| <td>${esc(refText(r.q, r.ref))}</td> | |
| ${MODELS.map((m) => cell(r.q, r.ans[m.key], r.ref)).join("")} | |
| <td>${ag ? `${ag.agree} / ${ag.of}` : "–"}</td> | |
| </tr>`; | |
| }).join(""); | |
| } | |
| // DL1: the summary paints in the current frame; the long row table follows in its own task, so no single | |
| // task has to rebuild everything. A newer request cancels a pending row render. | |
| let rowsTimer = 0; | |
| function renderBoard() { | |
| if (!board) return; | |
| renderSummary(); | |
| clearTimeout(rowsTimer); | |
| rowsTimer = setTimeout(renderScoreRows, 0); | |
| } | |
| // ------------------------------------------------------------------ wiring | |
| // DL1: the slider can fire faster than a frame; update the number at once and redraw at most once per frame. | |
| let rerenderFrame = 0; | |
| function onThresholdChange() { | |
| $("thr-out").textContent = threshold().toFixed(2); | |
| if (rerenderFrame) return; | |
| rerenderFrame = requestAnimationFrame(() => { | |
| rerenderFrame = 0; | |
| renderVerdicts(); | |
| renderBoard(); | |
| }); | |
| } | |
| $("run").addEventListener("click", runCurrent); | |
| $("run-all").addEventListener("click", runAll); | |
| $("threshold").addEventListener("input", onThresholdChange); | |
| $("measure").addEventListener("change", onThresholdChange); | |
| document.addEventListener("keydown", (e) => { if ((e.ctrlKey || e.metaKey) && e.key === "Enter") runCurrent(); }); | |
| applyModelCount(); | |
| renderHero(); | |
| renderScoreHead(); | |
| pollStatus(); | |
| loadDemos(); | |