rulechef-live / app.js
adaamko's picture
Restore the static page (no cpu-basic quota for Gradio)
27e064c verified
Raw History Blame Contribute Delete
17.5 kB
// RuleChef Live: replays data/replay.json (written by sim.py) and runs the rules in the browser (engine.js).
(function () {
"use strict";
const E = window.RCEngine;
const $ = (s) => document.querySelector(s);
const fmt = (n) => n.toLocaleString("en-US");
const pct = (a, b) => (b ? (100 * a / b).toFixed(1) + " %" : "–");
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
const NAMES = { DATETIME: "date/time", CODE: "code", QUANTITY: "quantity" };
let R, REQ = {}, EV, VERS, GUARD, META, N, AT, S = {}, LAB = [], HOLD = [];
let k = 0, timer = null;
function cum(xs) { const out = [0]; let s = 0; for (const x of xs) { s += x; out.push(s); } return out; }
function versionAt(i) { let lo = 0; for (let j = 0; j < AT.length; j++) if (AT[j] <= i) lo = j; return VERS[lo]; }
function blockedAt(i) { return new Set(GUARD.filter((g) => g.at < i).map((g) => g.key)); }
function activeRules(v, i) { const off = blockedAt(i); return v.rules.filter((r) => r.trusted && !off.has(r.key)); }
function card(t, v, s) { return `<div class="kpi"><div class="kt">${t}</div><div class="kv">${v}</div><div class="ks">${s}</div></div>`; }
function renderKpis() {
const v = versionAt(k);
const nr = S.rules[k], ns = S.spans[k], nc = S.check[k], na = S.agree[k], calls = S.call[k];
const lat = k ? S.lat[k] / k : 0, tlat = k ? S.tlat[k] / k : 0;
const rm = EV.slice(0, k).filter((e) => e.by === "rules").map((e) => e.rule_ms).sort((a, b) => a - b);
const med = rm.length ? rm[rm.length >> 1].toFixed(2) + " ms (median)" : "–";
$("#kpis").innerHTML = [
card("Requests", `${fmt(k)} / ${fmt(N)}`, `rule set v${v.v}`),
card("Answered by rules", pct(nr, k), `${fmt(nr)}: ${fmt(ns)} with spans, ${fmt(nr - ns)} with nothing to extract`),
card("Model calls", fmt(calls), `${fmt(k - calls)} saved (${pct(k - calls, k)}); spot checks included`),
card("Mean latency", `${Math.round(lat)} ms`, `teacher on every request: ${Math.round(tlat)} ms; a rule answer: ${med}`),
card("Spot-check agreement", pct(na, nc), `${na} of ${nc} checked rule answers equal the teacher's`),
card("Rules answering", String(activeRules(v, k).length), `of ${v.rules.length} in v${v.v}; ${blockedAt(k).size} switched off by the guard`),
].join("");
}
// ------------------------------------------------------------------ charts (SVG, two series each, crosshair tooltip)
const W = 360, H = 190, P = { l: 40, r: 10, t: 8, b: 22 };
function lineChart(el, cfg) {
const { series, yMax, yTicks, yFmt, markers } = cfg;
const x = (v) => P.l + (W - P.l - P.r) * v / N, y = (v) => H - P.b - (H - P.t - P.b) * v / yMax;
let g = `<svg viewBox="0 0 ${W} ${H}" role="img" aria-label="${esc(cfg.label)}">`;
for (const t of yTicks) g += `<line x1="${P.l}" x2="${W - P.r}" y1="${y(t)}" y2="${y(t)}" stroke="var(--grid)"/><g class="axis"><text x="${P.l - 5}" y="${y(t) + 3}" text-anchor="end">${yFmt(t)}</text></g>`;
for (let t = 0; t <= N; t += 1000) g += `<g class="axis"><text x="${x(t)}" y="${H - 6}" text-anchor="middle">${fmt(t)}</text></g>`;
for (const m of markers || []) if (m.at <= k && m.at > 0) g += `<line x1="${x(m.at)}" x2="${x(m.at)}" y1="${P.t}" y2="${H - P.b}" stroke="var(--line)" stroke-dasharray="2 3"/>`;
g += `<line x1="${P.l}" x2="${W - P.r}" y1="${H - P.b}" y2="${H - P.b}" stroke="var(--line)"/>`;
for (const s of series) {
if (s.pts.length < 2) continue;
g += `<polyline fill="none" stroke="${s.color}" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" points="${s.pts.map((p) => x(p[0]).toFixed(1) + "," + y(p[1]).toFixed(1)).join(" ")}"/>`;
}
g += `<line class="xh" x1="0" x2="0" y1="${P.t}" y2="${H - P.b}" stroke="var(--muted)" visibility="hidden"/>`;
g += `<rect x="${P.l}" y="${P.t}" width="${W - P.l - P.r}" height="${H - P.t - P.b}" fill="transparent"/></svg>`;
g += `<div class="legend">${series.map((s) => `<span><i style="background:${s.color}"></i>${esc(s.name)}</span>`).join("")}</div>`;
el.innerHTML = g;
const svg = el.querySelector("svg"), xh = el.querySelector(".xh"), tip = $("#tip");
svg.addEventListener("mousemove", (ev) => {
const b = svg.getBoundingClientRect();
const vx = (ev.clientX - b.left) / b.width * W;
const req = Math.max(0, Math.min(k, Math.round((vx - P.l) / (W - P.l - P.r) * N)));
const rows = series.map((s) => { let best = null; for (const p of s.pts) if (!best || Math.abs(p[0] - req) < Math.abs(best[0] - req)) best = p; return [s, best]; }).filter((r) => r[1]);
if (!rows.length) return;
xh.setAttribute("x1", x(rows[0][1][0])); xh.setAttribute("x2", x(rows[0][1][0])); xh.setAttribute("visibility", "visible");
tip.innerHTML = `request ${fmt(rows[0][1][0])}<br>` + rows.map(([s, p]) => `<span style="color:${s.color}">&#9644;</span> ${esc(s.name)}: <b>${cfg.tipFmt(p[1])}</b>`).join("<br>");
tip.hidden = false; tip.style.left = Math.min(ev.clientX + 12, window.innerWidth - 230) + "px"; tip.style.top = (ev.clientY + 12) + "px";
});
svg.addEventListener("mouseleave", () => { tip.hidden = true; xh.setAttribute("visibility", "hidden"); });
}
function windowPts(arr, w = 200, step = 20) {
const pts = [];
for (let i = step; i <= k; i += step) { const lo = Math.max(0, i - w); pts.push([i, (arr[i] - arr[lo]) / (i - lo)]); }
return pts;
}
const C1 = () => getComputedStyle(document.documentElement).getPropertyValue("--s1").trim();
const C2 = () => getComputedStyle(document.documentElement).getPropertyValue("--s2").trim();
function renderCharts() {
const markers = VERS.slice(1);
lineChart($("#c-cov"), { label: "Share of requests answered by rules", yMax: 1, yTicks: [0, 0.25, 0.5, 0.75, 1], yFmt: (t) => Math.round(t * 100) + "%",
tipFmt: (v) => (100 * v).toFixed(1) + " %", markers,
series: [{ name: "answered by rules", color: C1(), pts: windowPts(S.rules) }, { name: "rules found spans", color: C2(), pts: windowPts(S.spans) }] });
const step = 20, a = [], b = [];
for (let i = 0; i <= k; i += step) { a.push([i, S.call[i]]); b.push([i, i]); }
lineChart($("#c-calls"), { label: "Model calls", yMax: N, yTicks: [0, 1000, 2000, 3000], yFmt: (t) => fmt(t), tipFmt: (v) => fmt(v), markers,
series: [{ name: "with rules", color: C1(), pts: a }, { name: "teacher on every request", color: C2(), pts: b }] });
lineChart($("#c-lat"), { label: "Latency per request", yMax: 500, yTicks: [0, 100, 200, 300, 400, 500], yFmt: (t) => String(t), tipFmt: (v) => Math.round(v) + " ms", markers,
series: [{ name: "cascade", color: C1(), pts: windowPts(S.lat) }, { name: "teacher on every request", color: C2(), pts: windowPts(S.tlat) }] });
}
// ------------------------------------------------------------------ tables
function renderVersions() {
const cur = versionAt(k).v;
const head = `<tr><th>version</th><th class="num">from request</th><th class="num">labelled examples</th><th class="num">rules answering / learned</th>
<th class="num">synthesis s</th><th class="num">holdout answered by rules</th><th class="num">same answer as teacher</th>
<th class="num">rule spans P / R vs teacher</th><th class="num">cascade F1 vs teacher</th><th class="num">TAB annotation F1: teacher / cascade</th></tr>`;
const rows = VERS.filter((v) => v.at <= k).map((v) => {
const h = v.holdout, ra = h.rules_answer;
return `<tr class="${v.v === cur ? "current" : ""}"><td>v${v.v}</td><td class="num">${fmt(v.at)}</td><td class="num">${fmt(v.n_labelled)}</td>
<td class="num">${v.rules.filter((r) => r.trusted).length} / ${v.rules.length}</td><td class="num">${v.synth_s.toFixed(1)}</td>
<td class="num">${pct(ra, h.n)}</td><td class="num">${ra ? pct(h.exact_agree, ra) : "–"}</td>
<td class="num">${ra ? h.routed_t.P.toFixed(2) + " / " + h.routed_t.R.toFixed(2) : "–"}</td>
<td class="num">${h.casc_t.F1.toFixed(3)}</td><td class="num">${h.teach_g.F1.toFixed(3)} / ${h.casc_g.F1.toFixed(3)}</td></tr>`;
});
$("#t-vers").innerHTML = `<thead>${head}</thead><tbody>${rows.join("")}</tbody>`;
}
function renderRules() {
const v = versionAt(k), off = blockedAt(k), used = {};
for (let i = 0; i < k; i++) if (EV[i].by === "rules") for (const key of EV[i].fired) used[key] = (used[key] || 0) + 1;
const rows = v.rules.map((r) => {
const n = r.tp + r.fp;
const st = off.has(r.key) ? ["cut", "switched off"] : r.trusted ? ["on", "answering"] : ["off", "not trusted"];
return { r, st, n, used: used[r.key] || 0 };
}).sort((a, b) => (a.st[0] !== "on") - (b.st[0] !== "on") || b.used - a.used || b.n - a.n);
$("#rules-help").textContent = v.v === 0
? "No rule set yet: every request goes to the teacher until 50 examples are labelled."
: `v${v.v}, learned from ${fmt(v.n_labelled)} labelled examples in ${v.synth_s.toFixed(1)} s. "Answering" rules matched at least ${META.trust_min} times on the labelled data with precision at least ${META.trust_prec}; the others are kept but do not answer. The last column counts requests this exact rule answered so far.`;
const head = `<thead><tr><th>status</th><th>type</th><th>regex</th><th>output</th><th class="num">labelled matches (precision)</th><th class="num">requests answered</th></tr></thead>`;
const tr = ({ r, st, n, used: u }) => `<tr><td><span class="st ${st[0]}">${st[1]}</span></td><td>${r.type}</td><td class="rx">${esc(r.regex)}</td>
<td class="mono">${esc(r.template)}</td><td class="num">${n ? `${n} (${(r.tp / n).toFixed(2)})` : "0"}</td><td class="num">${u}</td></tr>`;
const on = rows.filter((x) => x.st[0] !== "off"), rest = rows.filter((x) => x.st[0] === "off");
$("#t-rules").innerHTML = head + "<tbody>" + (on.map(tr).join("") || '<tr><td colspan="6">No answering rules.</td></tr>') + "</tbody>";
const det = $("#rules-rest");
const open = det.open;
det.innerHTML = rest.length ? `<summary>${rest.length} learned rules that do not answer (precision below ${META.trust_prec} or too few matches)</summary><div class="tablewrap"><table class="rules">${head}<tbody>${rest.map(tr).join("")}</tbody></table></div>` : "";
det.open = open;
const g = v.gate;
let html = "";
if (g) {
html += `<p><b>Gate of v${v.v}.</b> A request goes to the rules only if every trigger token is covered by an answering rule or sits in a skip context.
Trigger tokens: anything with a digit, plus ${g.trigger_words.length} learned words: ${g.trigger_words.map((w) => `<code>${esc(w)}</code>`).join(", ")}.</p>
<p>Skip contexts (previous word + digit shape, <code>9</code> = digits) that the teacher labels at most ${Math.round(META.skip_ratio * 100)} % of the time:
${g.skip.map(([p, s]) => `<code>${esc(p || "(start)")} ${esc(s)}</code>`).join(", ") || "none"}.</p>`;
}
const gl = GUARD.filter((x) => x.at < k);
html += gl.length
? `<p><b>Drift guard.</b> ${gl.map((x) => `at request ${x.at}: <code>${esc(x.key.split("\t")[1])}</code> switched off (spot checks ${x.tp} right, ${x.fp} wrong)`).join("; ")}.</p>`
: `<p><b>Drift guard.</b> No rule switched off so far. A rule is switched off when its spot-check precision drops below ${META.guard_prec} after ${META.guard_min} checked spans; in this run none did.</p>`;
$("#gate").innerHTML = html;
}
function highlight(text, spans) {
const ss = spans.slice().sort((a, b) => a.start - b.start || b.end - a.end);
let out = "", pos = 0;
for (const s of ss) {
if (s.start < pos) continue;
out += esc(text.slice(pos, s.start)) + `<mark class="${s.type}">${esc(text.slice(s.start, s.end))}<sup>${NAMES[s.type] || s.type}</sup></mark>`;
pos = s.end;
}
return out + esc(text.slice(pos));
}
function renderRecent() {
const items = [];
for (let i = k - 1; i >= Math.max(0, k - 6); i--) {
const e = EV[i], q = REQ[e.id];
let spans, by;
if (e.by === "rules") {
spans = E.applyRules(activeRules(versionAt(i), i).map((r) => r.dict), q.text);
by = `<span class="by-rules">rules</span> (v${e.v}, ${e.rule_ms.toFixed(2)} ms)` + (e.checked ? `, spot-checked: ${e.agree ? "same as the teacher" : "differs from the teacher"}` : "");
} else {
spans = q.teacher;
by = `<span class="by-teacher">teacher</span> (${Math.round(e.teacher_ms)} ms), answer added to the labelled examples`;
}
items.push(`<li value="${i + 1}"><div class="meta">#${fmt(i + 1)} answered by ${by}</div><div class="hl-inline">${highlight(q.text, spans)}${spans.length ? "" : ' <span class="meta">(nothing to extract)</span>'}</div></li>`);
}
$("#recent").innerHTML = items.join("") || "<li>No requests yet. Press Play.</li>";
}
function render() {
$("#pos").value = k;
$("#posout").textContent = `${fmt(k)} requests`;
renderKpis(); renderCharts(); renderVersions(); renderRules(); renderRecent();
}
// ------------------------------------------------------------------ playback
function stop() { if (timer) clearTimeout(timer); timer = null; $("#play").textContent = "Play"; }
function tick() {
k = Math.min(N, k + parseInt($("#speed").value, 10));
render();
if (k >= N) { stop(); return; }
timer = setTimeout(tick, 280);
}
$("#play").addEventListener("click", () => {
if (timer) { stop(); return; }
if (k >= N) k = 0;
$("#play").textContent = "Pause";
tick();
});
$("#pos").addEventListener("input", (ev) => { stop(); k = parseInt(ev.target.value, 10); render(); });
// ------------------------------------------------------------------ try your own text
function tryText(text, recorded) {
text = (text || "").trim().slice(0, 2000);
if (!text) { $("#status").textContent = "Type or paste a sentence."; return; }
let v = versionAt(k);
if (!v.gate) v = VERS[VERS.length - 1];
const rules = activeRules(v, Math.max(k, v.at)).map((r) => r.dict);
const t0 = performance.now();
const spans = E.applyRules(rules, text);
const un = E.unexplained(v.gate, text, spans);
const ms = performance.now() - t0;
const rec = recorded || HOLD.find((q) => q.text === text);
if (!un.length) {
$("#status").innerHTML = `<b>Answered by rules</b> (v${v.v}, ${rules.length} answering rules) in ${ms.toFixed(2)} ms in your browser. No model call.`;
$("#out").innerHTML = `<span class="lab">Answer (rules)</span>${highlight(text, spans)}`;
$("#out-teacher").innerHTML = rec ? `<span class="lab">Teacher's recorded answer for this holdout sentence, for comparison</span>${highlight(rec.text, rec.teacher)}` : "";
} else {
$("#status").innerHTML = `<b>The gate sends this request to the teacher</b>: uncovered trigger tokens ${un.slice(0, 12).map((u) => `<code>${esc(u)}</code>`).join(", ")}. ` +
`Rules took ${ms.toFixed(2)} ms. The teacher does not run on this static page` + (rec ? "; its recorded answer for this holdout sentence is shown." : ".");
$("#out").innerHTML = rec ? `<span class="lab">Answer (teacher, recorded offline)</span>${highlight(rec.text, rec.teacher)}` : "";
$("#out-teacher").innerHTML = `<span class="lab">What the rules found</span>${highlight(text, spans)}`;
}
}
$("#go").addEventListener("click", () => tryText($("#inp").value));
$("#pick").addEventListener("click", () => { const q = HOLD[Math.floor(Math.random() * HOLD.length)]; $("#inp").value = q.text; tryText(q.text, q); });
document.querySelectorAll("button.ex").forEach((b) => b.addEventListener("click", () => { $("#inp").value = b.dataset.ex; tryText(b.dataset.ex); }));
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => R && renderCharts());
// ------------------------------------------------------------------ load
Promise.all([fetch("data/replay.json").then((r) => r.json()), fetch("data/requests.jsonl").then((r) => r.text())]).then(([rep, txt]) => {
R = rep; META = R.meta; VERS = R.versions; EV = R.events; GUARD = R.guard; N = EV.length; AT = VERS.map((v) => v.at);
for (const line of txt.trim().split("\n")) { const q = JSON.parse(line); REQ[q.id] = q; if (q.part === "holdout") HOLD.push(q); }
S.rules = cum(EV.map((e) => +(e.by === "rules")));
S.spans = cum(EV.map((e) => +(e.by === "rules" && e.fired.length > 0)));
S.check = cum(EV.map((e) => +!!e.checked));
S.agree = cum(EV.map((e) => +!!e.agree));
S.call = cum(EV.map((e) => +(e.by === "teacher" || !!e.checked)));
S.lat = cum(EV.map((e) => (e.by === "rules" ? e.rule_ms : e.rule_ms + e.teacher_ms)));
S.tlat = cum(EV.map((e) => e.teacher_ms));
document.querySelectorAll("[data-n-stream]").forEach((x) => (x.textContent = fmt(META.n_stream)));
document.querySelectorAll("[data-n-holdout]").forEach((x) => (x.textContent = fmt(META.n_holdout)));
const set = (sel, v) => document.querySelectorAll(sel).forEach((x) => (x.textContent = v));
set("[data-trust-min]", META.trust_min); set("[data-trust-prec]", META.trust_prec); set("[data-spot]", META.spot_every);
set("[data-guard-prec]", META.guard_prec); set("[data-guard-min]", META.guard_min);
$("#pos").max = N; k = N;
render();
tryText($("#inp").value);
}).catch((err) => { $("#kpis").textContent = "Could not load the replay data: " + err; });
})();