// The checker: fills the form from an example, submits to the API named in , follows the
// job through RunPod's real stages and shows the verdict only after the model has answered. With no API set, unedited examples show the score the model gave
// them ahead of time. Cut-offs are the ones chosen on the development split.
const PASS = 0.88, BLOCK = 0.12, FLAG = 0.56;
const $ = (id) => document.getElementById(id);
const form = $("ck-form"), list = $("ck-list"), status = $("ck-status"), out = $("ck-out"), go = $("ck-go");
const fields = { source: $("ck-source"), request: $("ck-request"), answer: $("ck-answer") };
const API = (document.querySelector('meta[name="api-base"]')?.content || "").replace(/\/$/, "");
let current = null;
// Progress follows RunPod's real state. Each stage owns a stretch of the bar and eases across it while
// it lasts, so the bar always moves; a later stage jumps ahead the moment RunPod reports it, and the bar
// never goes back. While the page waits a soft sweep runs over the fill.
const bar = $("ck-progress"), fill = bar.querySelector("span");
const STAGES = {
sending: { lo: 0, hi: 12, tau: 0.6, text: () => "Sending the answer to the model." },
starting: { lo: 12, hi: 72, tau: 30, text: (s) => `Starting a model server · ${s} s. After a quiet spell this takes up to two minutes; your check keeps its place.` },
queued: { lo: 72, hi: 82, tau: 3, text: (s) => `The server is up. Your check is next · ${s} s.` },
checking: { lo: 82, hi: 96, tau: 0.8, text: () => "Reading the source and the answer." },
};
let stage = null, stageAt = 0, shown = 0, startedAt = 0, tick = null;
function set(p) {
shown = Math.max(shown, p);
fill.style.width = `${shown.toFixed(1)}%`;
bar.setAttribute("aria-valuenow", String(Math.round(shown)));
}
function render() {
const st = STAGES[stage]; if (!st) return;
const t = (performance.now() - stageAt) / 1000, secs = Math.round((performance.now() - startedAt) / 1000);
set(st.lo + (st.hi - st.lo) * (1 - Math.exp(-t / st.tau)));
status.textContent = st.text(secs);
}
function enter(next) {
if (!STAGES[next] || next === stage) return;
if (stage && STAGES[next].lo < STAGES[stage].lo) return; // RunPod's view can lag; never step back
stage = next; stageAt = performance.now();
set(STAGES[next].lo);
render();
}
function progressStart() {
shown = 0; stage = null; startedAt = performance.now();
bar.hidden = false; bar.classList.add("is-waiting"); fill.style.width = "0%";
enter("sending");
tick = setInterval(render, 200);
}
function progressEnd(done) {
clearInterval(tick); stage = null; bar.classList.remove("is-waiting");
if (done) set(100);
setTimeout(() => { bar.hidden = true; shown = 0; fill.style.width = "0%"; }, done ? 450 : 0);
}
function showExample(ex, btn) {
fields.source.value = ex.source; fields.request.value = ex.request; fields.answer.value = ex.answer;
list.querySelectorAll(".ck-item").forEach((b) => b.setAttribute("aria-pressed", String(b === btn)));
out.hidden = true; status.textContent = ""; current = ex;
}
async function loadExamples() {
let examples = [];
try { examples = await (await fetch("examples.json")).json(); } catch { return; }
for (const ex of examples) {
const li = document.createElement("li");
const b = document.createElement("button");
b.type = "button"; b.className = "ck-item"; b.setAttribute("aria-pressed", "false");
b.innerHTML = ``;
b.querySelector(".ck-item__t").textContent = ex.title;
b.querySelector(".ck-item__s").textContent = `${ex.task} · ${ex.label}`;
b.querySelector(".ck-item__l").textContent = ex.app;
b.addEventListener("click", () => showExample(ex, b));
li.append(b); list.append(li);
}
if (examples.length) showExample(examples[0], list.querySelector(".ck-item"));
}
function show(p) {
$("ck-verdict").textContent = p >= FLAG ? "Supported" : "Not supported";
$("ck-score").textContent = p.toFixed(3);
$("ck-route").textContent = p >= PASS ? "Pass" : p <= BLOCK ? "Block" : "Send to review";
out.hidden = false;
}
function invalid(el, on) {
const err = $(el.getAttribute("aria-describedby"));
el.setAttribute("aria-invalid", String(on)); if (err) err.hidden = !on;
}
form.addEventListener("submit", async (e) => {
e.preventDefault();
const missing = ["source", "answer"].filter((k) => !fields[k].value.trim());
["source", "answer"].forEach((k) => invalid(fields[k], missing.includes(k)));
if (missing.length) {
fields[missing[0]].focus();
status.textContent = missing.length === 1 ? "One field needs text." : "Two fields need text.";
return;
}
out.hidden = true;
if (!API) {
const same = current && ["source", "request", "answer"].every((k) => fields[k].value === current[k]);
if (same && typeof current.score === "number") {
show(current.score);
status.textContent = `Stored score from the model. Live checking of your own text is offline. Scores below ${FLAG} count as not supported.`;
} else status.textContent = "Live checking is offline right now. The examples on the left show stored scores.";
return;
}
go.disabled = true;
progressStart();
let ok = false;
try {
const noul = await runCheck();
const secs = ((performance.now() - startedAt) / 1000).toFixed(1);
ok = true; progressEnd(true);
show(noul);
status.textContent = `Checked in ${secs} s. Scores below ${FLAG} count as not supported.`;
} catch (err) {
status.textContent = `${err.message} Your text is still in the fields.`;
} finally { if (!ok) progressEnd(false); go.disabled = false; }
});
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// Submit, then follow the job until RunPod finishes it. A warm server answers in the first call; a cold
// start is followed for up to six minutes, and a lost poll is simply asked again.
async function runCheck() {
const r = await fetch(`${API}/api/check`, { method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ source: fields.source.value, request: fields.request.value, answer: fields.answer.value }) });
const j = await r.json().catch(() => ({}));
if (!r.ok) throw new Error(j.detail || "The check failed.");
if (j.stage === "done") { enter("checking"); return j.noul; }
enter(j.stage);
let misses = 0;
while ((performance.now() - startedAt) / 1000 < 360) {
await sleep((performance.now() - startedAt) / 1000 < 30 ? 1000 : 1500);
let s;
try {
const q = await fetch(`${API}/api/status?id=${encodeURIComponent(j.id)}`);
s = await q.json().catch(() => ({}));
if (!q.ok) throw Object.assign(new Error(s.detail || "The check failed."), { final: q.status !== 429 && q.status < 500 });
misses = 0;
} catch (err) {
if (err.final) throw err;
if (++misses >= 6) throw new Error("Lost contact with the model server.");
continue;
}
if (s.stage === "done") { enter("checking"); return s.noul; }
enter(s.stage);
}
throw new Error("The model server is taking unusually long. Try again in a minute.");
}
loadExamples();