// 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();