`).join("");
observe();
}
/* The handbook page, marked up: conflicting sentences highlighted, with the rules beside them (sample page 4). */
function markup(results, pages, pageNo) {
const raw = (pages.find(([n]) => n === pageNo) || [0, ""])[1];
const blocks = [];
for (const line of raw.split("\n").map((l) => l.trim()).filter(Boolean)) {
const heading = line.length < 42 && !/[.,:;]$/.test(line) && /^[A-Z]/.test(line);
if (heading) blocks.push({ h: line });
else if (blocks.length && !blocks[blocks.length - 1].h) blocks[blocks.length - 1].p += " " + line;
else blocks.push({ p: line });
}
const notes = [];
for (const [st, r] of Object.entries(results)) {
for (const f of r.findings) {
if (f.handbook_page !== pageNo || !["conflict", "partial"].includes(f.status) || !f.handbook_quote) continue;
const key = f.handbook_quote.toLowerCase().split(/\W+/).slice(0, 7).join(" ");
let n = notes.find((x) => x.key === key);
if (!n) notes.push(n = { key, quote: f.handbook_quote, status: f.status, states: {}, why: f.explanation });
if (f.status === "conflict") { n.status = "conflict"; n.why = n.why || f.explanation; }
n.states[st] = n.states[st] || f.section;
}
}
const conflicts = notes.filter((n) => n.status === "conflict");
const pattern = (ws) => new RegExp(ws.map((w) => esc(w).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("\\W+"));
let num = 0;
const html = blocks.map((b) => {
if (b.h) return `
${esc(b.h)}
`;
const text = esc(b.p);
// Find every note in this paragraph (the whole verified quote, or its first 14 words if punctuation differs),
// then mark them in reading order so the footnote numbers run 1, 2, 3 down the page.
const hits = [];
for (const n of notes) {
if (n.placed) continue;
const all = n.quote.split(/\s+/).filter(Boolean);
const m = text.match(pattern(all)) || text.match(pattern(all.slice(0, 14)));
if (m) hits.push({ n, start: m.index, end: m.index + m[0].length });
}
hits.sort((a, b) => a.start - b.start);
let out = "", at = 0;
for (const h of hits) {
if (h.start < at) continue; // overlaps the previous mark
h.n.placed = true;
const label = h.n.status === "conflict" ? `${(h.n.num = ++num)}` : "";
out += text.slice(at, h.start) + `${text.slice(h.start, h.end)}${label}`;
at = h.end;
}
return `
Suggested for ${esc(scores[s].name)} · ${esc(r.detail[s].section)}
`;
}).join("");
observe();
}
/* scroll reveals, statement lines lit by scroll position, scans that start when seen */
let io;
function observe() {
io = io || new IntersectionObserver((entries) => entries.forEach((e) => {
if (!e.isIntersecting) return;
e.target.classList.add(e.target.classList.contains("scan") ? "go" : "in");
io.unobserve(e.target);
}), { threshold: 0.15 });
document.querySelectorAll(".reveal:not(.in), .scan:not(.go)").forEach((el) => io.observe(el));
}
function lightStatements() {
const mid = innerHeight * 0.62;
document.querySelectorAll(".lit").forEach((el) => el.classList.toggle("on", el.getBoundingClientRect().top < mid));
}
addEventListener("scroll", lightStatements, { passive: true });
/* the hero video: only on wide screens, never with reduced motion or data saver */
/* The 20-second demo: plays (muted) while it's on screen. Reduced motion keeps the poster; the controls are always there. */
function demoVideo() {
const v = $("#demo-video video");
if (!v || matchMedia("(prefers-reduced-motion: reduce)").matches) return;
new IntersectionObserver(([e]) => { if (e.isIntersecting) v.play().catch(() => {}); else v.pause(); }, { threshold: 0.4 }).observe(v);
}
function heroVideo() {
const v = $("#hero-video");
const save = navigator.connection && navigator.connection.saveData;
const site = /(^|\.)backpr\.com$/.test(location.hostname); // the clip is licensed for the website only
if (!v || !site || save || innerWidth < 900 || matchMedia("(prefers-reduced-motion: reduce)").matches) return;
v.src = "static/media/hero-paperwork.mp4";
v.addEventListener("canplay", () => { v.classList.add("on"); v.play().catch(() => {}); }, { once: true });
v.load();
}
function showReport(html, fixes, head, note = "", summary = "") {
if (summary) $("#report-summary").textContent = summary;
$("#report-head").innerHTML = head;
$("#report").innerHTML = (note ? `
${note}
` : "") + html;
state.fixes = fixes || "";
$("#actions").hidden = !state.fixes;
const box = $("#backpr-box"); // removed on the Space
if (box) box.hidden = !state.fixes;
}
/* the email that asks for a code: the address is always visible too, because mailto links fail silently on webmail */
function requestLink() {
const chosen = [...state.chosen].map(stateName).join(", ");
const body = `Hi BackPR team,\n\nI'd like a free CareRules access code.\n\nCenter name:\nState(s): ${chosen}\nMy role:\n\n` +
"Also send me information about BackPR's registration and tuition software (yes/no):\n\nThank you!";
const href = `mailto:${REQUEST_EMAIL}?subject=${encodeURIComponent("CareRules: free access code")}&body=${encodeURIComponent(body)}`;
$("#request").href = href;
$("#request-inline").href = href;
}
/* access codes: given out on request; a judge's link carries one as ?code= */
async function checkCode() {
const code = $("#code").value.trim();
if (!code) { $("#code").classList.remove("ok"); return null; }
try {
const r = await fetch(`${API}/code?code=${encodeURIComponent(code)}`);
let s = {};
try { s = await r.json(); } catch (e) { throw new Error("The checking service had a problem. Please try again in a minute."); }
if (!r.ok) throw new Error(s.error || "The checking service had a problem. Please try again in a minute.");
$("#code").classList.toggle("ok", s.valid);
if (s.valid) {
try { localStorage.setItem("carerules-code", code); } catch (e) { /* private mode */ }
state.maxStates = s.max_states;
$("#code-status").textContent = s.paused ? "Code accepted, but live checks are paused right now. The sample reports still work."
: `Code accepted. You have ${s.left_today} check${s.left_today === 1 ? "" : "s"} left today (up to ${s.max_states} state${s.max_states === 1 ? "" : "s"} per check).`;
$("#states-max").textContent = s.max_states;
} else {
$("#code-status").textContent = `That code isn't valid. Check for typos, or email ${REQUEST_EMAIL} for one.`;
}
return s;
} catch (e) {
$("#code-status").textContent = friendly(e, "Couldn't reach the checking service. Please try again in a moment.");
return null;
}
}
/* exactly which rules each chosen state is checked against */
function ruleSources() {
const list = state.states.filter((s) => state.chosen.has(s.state));
$("#rule-sources").hidden = !list.length;
$("#rule-sources").innerHTML = list.map((s) => `
A fictional nonprofit with centers in Texas, North Carolina, and Oregon, and one shared parent handbook with some mistakes planted in it. Read the handbook (PDF).
`,
"", "See the sample report: one handbook, checked for Texas, North Carolina and Oregon"];
showReport(...state.sample);
state.fixesName = "little-acorns-fixes";
if (/hf\.space$/.test(location.hostname)) document.querySelectorAll("[data-promo]").forEach((el) => el.remove()); // keep the Space about the project
observe();
lightStatements();
heroVideo();
demoVideo();
// A link to a part of the page (#test-handbook, #faq...): jump there now that the sections above it are drawn, and
// keep it in place for two seconds while the page settles, unless the visitor scrolls first.
const target = location.hash.length > 1 && document.getElementById(location.hash.slice(1));
if (target) (async () => { // not awaited: the rest of the page keeps loading meanwhile
let user = false;
["wheel", "touchstart", "keydown"].forEach((e) => addEventListener(e, () => { user = true; }, { once: true, passive: true }));
const pad = parseFloat(getComputedStyle(document.documentElement).scrollPaddingTop) || 0;
for (let i = 0; i < 10 && !user; i++) {
if (Math.abs(target.getBoundingClientRect().top - pad) > 4) target.scrollIntoView({ behavior: "instant" });
await new Promise((r) => setTimeout(r, 200));
}
})();
const examples = await getJson("samples/eval-results/index.json");
$("#example-states").innerHTML = examples.map((x) => ``).join("");
state.examples = Object.fromEntries(examples.map((x) => [x.code, x]));
$("#test-states").innerHTML = examples.map((x) => ``).join("");
}
/* the answer key under a sample report: what was planted, and whether the check found it */
function answerKey(key) {
const yes = (ok) => (ok ? '✓ found' : '✕ not found');
return `
Answer key
What we changed in this handbook before checking it, and whether the check caught it.
Sentences we changed to break a rule (${key.conflicts.length})
`;
}
/* reading the file as soon as it's chosen, so problems show before the check */
/* plain words for what file readers and networks throw */
function friendly(err, fallback) {
const m = String((err && err.message) || "");
if (/password/i.test(m)) return "This PDF is password-protected. Save a copy without a password and try again.";
if (/Invalid PDF|PDF structure|Missing PDF|bad XRef/i.test(m)) return "This PDF couldn't be read. Try saving it again, or use the Word version.";
if (/Failed to fetch|NetworkError|Load failed|network/i.test(m)) return "Couldn't reach the checking service. Check your internet connection and try again.";
if (/Unexpected token|JSON/i.test(m)) return "The checking service had a problem. Please try again in a minute.";
return m || fallback;
}
async function setFile(f, demo = null) {
const seq = ++state.readSeq; // a slower read of an earlier file must never replace a newer choice
state.file = null; state.pages = null; state.fileError = ""; state.demo = demo; // demo: a sample handbook's state
$("#file-info").hidden = false;
if (!f) return;
$("#drop-text").innerHTML = `${esc(f.name)} ${(f.size / 1024).toFixed(0)} KB · click to change`;
if (f.size > MAX_FILE) { $("#file-info").textContent = state.fileError = "That file is over 12 MB. Try a smaller copy, or the Word version."; return; }
$("#file-info").textContent = "Reading the file…";
try {
const pages = await CR.readFile(f, await loadLibs(f));
if (seq !== state.readSeq) return;
const words = pages.reduce((a, [, t]) => a + t.split(/\s+/).filter(Boolean).length, 0);
const empty = pages.filter(([, t]) => t.trim().split(/\s+/).length < 15).length;
const chars = pages.reduce((a, [, t]) => a + t.length, 0);
state.file = f; state.pages = pages;
$("#file-info").textContent = `Read ${pages.length} page${pages.length === 1 ? "" : "s"}, about ${words.toLocaleString("en-US")} words.` +
(empty ? ` ${empty} page${empty === 1 ? " has" : "s have"} little or no text (scanned pages can't be read).` : "") +
(chars >= MAX_TEXT ? " This handbook is long, so only about the first 60,000 words will be checked." : "");
} catch (err) {
if (seq !== state.readSeq) return;
$("#file-info").textContent = state.fileError = friendly(err, "Couldn't read this file. Try a PDF, Word (.docx) or text copy.");
}
}
async function loadLibs(file) {
const ext = (file.name.match(/\.[^.]+$/) || [""])[0].toLowerCase();
const libs = {};
if (ext === ".pdf") {
libs.pdfjs = await import(LIBS.pdfjs);
libs.pdfjs.GlobalWorkerOptions.workerSrc = LIBS.pdfWorker;
} else if (ext === ".docx") {
if (!window.mammoth) await new Promise((ok, fail) => { // pinned by hash: a changed file on the CDN is refused
const s = Object.assign(document.createElement("script"), { src: LIBS.mammoth, integrity: LIBS.mammothSri, crossOrigin: "anonymous", onload: ok, onerror: fail });
document.head.append(s);
});
libs.mammoth = window.mammoth;
}
return libs;
}
function copyText(text, btn) {
const done = () => { btn.textContent = "Copied"; setTimeout(() => (btn.textContent = "Copy"), 1500); };
const fallback = () => { // older browsers and pages without clipboard permission
const t = Object.assign(document.createElement("textarea"), { value: text });
document.body.append(t); t.select();
try { document.execCommand("copy"); done(); } catch (e) { btn.textContent = "Press Ctrl+C"; }
t.remove();
};
if (navigator.clipboard) navigator.clipboard.writeText(text).then(done, fallback); else fallback();
}
/* inputs */
document.addEventListener("click", async (ev) => {
const chip = ev.target.closest(".chip[data-code]");
if (chip) {
const c = chip.dataset.code;
if (state.chosen.has(c)) state.chosen.delete(c);
else if (state.chosen.size < state.maxStates) state.chosen.add(c);
else help(`You can check up to ${state.maxStates} states at once.`, false);
chip.classList.toggle("on", state.chosen.has(c));
chip.setAttribute("aria-pressed", String(state.chosen.has(c)));
requestLink();
ruleSources();
}
const seg = ev.target.closest("#effort button");
if (seg) {
state.effort = seg.dataset.v;
document.querySelectorAll("#effort button").forEach((b) => { b.classList.toggle("on", b === seg); b.setAttribute("aria-pressed", String(b === seg)); });
}
const copy = ev.target.closest("[data-copy]");
if (copy) copyText(copy.dataset.copy, copy);
if (ev.target.closest('a[href="#states"]')) $("#states").open = true;
if (ev.target.closest('a[href="#terms"]')) $("#terms").open = true;
if (ev.target.closest('a[href="#sample"]') && !state.running) $("#report-box").open = true;
if (ev.target.closest('a[href="#test-handbook"]')) $("#test-handbook").open = true;
const ex = ev.target.closest("[data-example]");
if (ex) {
document.querySelectorAll("[data-example]").forEach((b) => { b.classList.toggle("on", b === ex); b.setAttribute("aria-pressed", String(b === ex)); });
const x = state.examples[ex.dataset.example];
$("#example-report").innerHTML = '
Loading…
';
const [r, key] = await Promise.all([getJson(`samples/eval-results/${x.code}.json`), getJson(`samples/eval-results/${x.code}.key.json`)]);
$("#example-report").innerHTML = await CR.report({ [x.code]: r }, `${x.title}, made up with planted mistakes`, x.pages, "part") + answerKey(key);
}
});
/* "Plant your own mistake": a compliant test handbook to edit and check */
$("#test-load").addEventListener("click", async () => {
const code = $("#test-states").value;
if (!code) return; // the list is still loading
const text = await fetch(`samples/eval/${code}/clean.md`).then((r) => r.text());
$("#test-text").value = text;
$("#test-edit").hidden = false;
state.chosen = new Set([code]);
renderChips(); requestLink();
});
/* One click: read the edited sample handbook and start the live check at once (no code, its own state, Standard). */
$("#test-use").addEventListener("click", async () => {
if (state.running) return; // a running check keeps its file
const code = $("#test-states").value;
$("#test-use").disabled = true;
await setFile(new File([$("#test-text").value], `sample-handbook-${code}.md`, { type: "text/markdown" }), code);
$("#test-use").disabled = false;
state.chosen = new Set([code]); renderChips(); requestLink();
if (!state.pages) { help(state.fileError || "Couldn't read the edited handbook. Load it again and retry."); return; }
runCheck();
});
document.querySelectorAll(".choice").forEach((b) => b.addEventListener("click", () => {
if (b.getAttribute("aria-disabled") === "true") return;
document.querySelectorAll(".choice").forEach((c) => {
c.classList.toggle("right", c.dataset.k === "b"); c.classList.toggle("dim", c.dataset.k !== "b");
c.classList.toggle("picked", c === b); c.setAttribute("aria-disabled", "true");
});
$("#reveal-title").textContent = b.dataset.k === "b" ? "Right: it's B." : `You picked ${b.dataset.k.toUpperCase()}. It's B.`;
$("#reveal-note").textContent = b.dataset.k === "b" ? "" : "Easy to miss, because the limit for four-year-olds is different in each state.";
$("#reveal").hidden = false;
$("#reveal").focus();
}));
$("#code").addEventListener("change", checkCode);
$("#code").addEventListener("input", () => { // clearing the box clears the old verdict
if ($("#code").value.trim()) return;
$("#code").classList.remove("ok");
$("#code-status").textContent = "Paste the code from our email.";
});
const drop = $("#drop"), input = $("#file");
input.addEventListener("change", () => setFile(input.files[0]));
["dragenter", "dragover"].forEach((e) => drop.addEventListener(e, (ev) => { ev.preventDefault(); drop.classList.add("over"); }));
["dragleave", "drop"].forEach((e) => drop.addEventListener(e, (ev) => { ev.preventDefault(); drop.classList.remove("over"); }));
drop.addEventListener("drop", (ev) => { if (ev.dataTransfer.files[0]) setFile(ev.dataTransfer.files[0]); });
const help = (text, bad = true) => { $("#go-help").textContent = text; $("#go-help").classList.toggle("bad", !!text && bad); };
async function runCheck() {
if (state.running) return;
if (!state.pages) { help(state.fileError || "Add your parent handbook first."); drop.classList.add("over"); setTimeout(() => drop.classList.remove("over"), 900); return; }
if (!state.chosen.size) { help("Pick your state first."); return; }
let code = $("#code").value.trim(), status;
const demo = !!state.demo; // a sample handbook: no code needed (a typed code isn't spent on it), its own state, Standard
if (!code && !demo) { $("#code").focus(); help(`Live checks need a free access code: email ${REQUEST_EMAIL}. Or try a sample handbook, no code needed.`); return; }
if (demo) {
code = "DEMO"; status = { max_states: 1 };
} else {
status = await checkCode();
if (!status || !status.valid) { help($("#code-status").textContent); return; }
if (status.paused) { help("Live checks are paused right now. The sample reports still work."); return; }
}
help("");
const effort = demo ? "low" : state.effort;
const chosen = demo ? [state.demo] : [...state.chosen].slice(0, status.max_states);
const info = state.states.filter((s) => chosen.includes(s.state));
// A retry of the same file, states and effort keeps its check id, so it doesn't use up another check.
const file = state.file, pages = state.pages; // a file chosen during the run must not change this check
const same = `${file.name}|${file.size}|${file.lastModified}|${chosen.join(",")}|${effort}`;
if (!state.last || state.last.key !== same) state.last = { key: same, id: `${Date.now()}-${Math.random().toString(36).slice(2, 10)}` };
const check = state.last.id;
state.running = true;
$("#go").disabled = true;
$("#progress").hidden = false;
$("#report-head").innerHTML = ""; $("#report").innerHTML = ""; $("#actions").hidden = true;
if ($("#backpr-box")) $("#backpr-box").hidden = true;
$("#report-box").open = true;
$("#report-summary").textContent = `Your report: ${file.name}`;
input.disabled = true; drop.classList.add("busy");
$("#sample").scrollIntoView({ behavior: "smooth" });
scan($("#run-scan"), info.map((s) => ({ name: stateName(s.state), pages: s.pdf_pages, marks: [] })), { loop: true, dur: 3 });
$("#run-scan").classList.add("go");
const steps = ["Reading your handbook…", ...info.map((s) => `Reading the ${stateName(s.state)} rules that apply…`), "Comparing every policy with the rules…", "Checking every quote word for word…", "Writing suggested wording…"];
let i = 0;
$("#progress-text").textContent = steps[0];
const timer = setInterval(() => { i = Math.min(i + 1, steps.length - 1); $("#progress-text").textContent = steps[i]; }, 6000);
const callBeam = async (prompt, effort, retry = false) => {
let res;
try { res = await fetch(`${API}/beam`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code, check, effort, prompt, retry }) }); }
catch (e) { throw new Error(friendly(e, "Couldn't reach the checking service. Please try again.")); }
let data = {};
try { data = await res.json(); } catch (e) { /* not JSON */ }
if (!res.ok) throw new Error(data.error || "The check failed. Please try again; a failed try doesn't use up your check.");
return data;
};
try {
const { results, errors } = await CR.audit(pages, chosen, effort, callBeam);
if (!Object.keys(results).length) throw new Error(Object.values(errors)[0] || "The check failed. Please try again.");
const notes = [];
if (demo) notes.push(`A live check of a sample handbook against ${esc(stateName(chosen[0]))}'s rules. To check your own handbook, email ${REQUEST_EMAIL} for a free access code.`);
else if (chosen.length < state.chosen.size) notes.push(`Your code covers ${status.max_states} states per check, so only the first ${status.max_states} were checked.`);
for (const [s, msg] of Object.entries(errors)) notes.push(`${esc(stateName(s))} couldn't be checked: ${esc(msg)} Press Check again to retry; it won't use up another check.`);
if (CR.suspicious(pages)) notes.push("Your handbook contains text that reads like instructions to an AI. CareRules ignores it, but read these findings with extra care.");
const base = file.name.replace(/\.[^.]+$/, "");
state.fixesName = `${base}-fixes`;
const unit = /\.pdf$/i.test(file.name) ? "page" : "part"; // Word and text files have no fixed pages
showReport(await CR.report(results, file.name, pages.length, unit), CR.fixesMarkdown(results, file.name, unit),
`Your report
${esc(file.name)}
`, notes.join(" "), `Your report: ${file.name}`);
} catch (err) { // put the sample report back, with the error on top
showReport(...state.sample);
state.fixesName = "little-acorns-fixes";
$("#report").insertAdjacentHTML("afterbegin", `
${esc(friendly(err, "The check failed. Please try again."))}