/* CareRules page: runs fully in the browser on a free static Space. The engine (engine.js) reads the handbook, picks the rule pages, builds the prompt and checks every quote; the CareRules API Worker only gates and forwards the call to Beam (access code, spending limits, cache). */ const $ = (s, el = document) => el.querySelector(s); const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c])); // Same origin on care.backpr.com (the Worker serves page and API); the Hugging Face copy and local tests call it there. const API = /(^|\.)backpr\.com$/.test(location.hostname) ? "" : "https://care.backpr.com"; const REQUEST_EMAIL = "hi@backpr.com"; const LIBS = { pdfjs: "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.10.38/pdf.min.mjs", pdfWorker: "https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.10.38/pdf.worker.min.mjs", mammoth: "https://cdnjs.cloudflare.com/ajax/libs/mammoth/1.8.0/mammoth.browser.min.js", mammothSri: "sha512-wuWo/cLB9W5BsZeyTYLuiTwr+FDlvjQC7C6atr+To7Jk92XHWI7WsImJZiruw7C9bnc8Zg7N0ncQI2Q/B4PQYw==", }; const MAX_FILE = 12 * 1024 * 1024, MAX_TEXT = 240000; const COLORS = CR.COLORS; const LABELS = { conflict: "Breaks a rule", missing: "Missing", partial: "Incomplete", covered: "Covered" }; const SCOPE = { AR: "out-of-school-time programs" }; // a state whose rules cover only one kind of program const US = { AL: "Alabama", AK: "Alaska", AZ: "Arizona", AR: "Arkansas", CA: "California", CO: "Colorado", CT: "Connecticut", DE: "Delaware", DC: "District of Columbia", FL: "Florida", GA: "Georgia", HI: "Hawaii", ID: "Idaho", IL: "Illinois", IN: "Indiana", IA: "Iowa", KS: "Kansas", KY: "Kentucky", LA: "Louisiana", ME: "Maine", MD: "Maryland", MA: "Massachusetts", MI: "Michigan", MN: "Minnesota", MS: "Mississippi", MO: "Missouri", MT: "Montana", NE: "Nebraska", NV: "Nevada", NH: "New Hampshire", NJ: "New Jersey", NM: "New Mexico", NY: "New York", NC: "North Carolina", ND: "North Dakota", OH: "Ohio", OK: "Oklahoma", OR: "Oregon", PA: "Pennsylvania", RI: "Rhode Island", SC: "South Carolina", SD: "South Dakota", TN: "Tennessee", TX: "Texas", UT: "Utah", VT: "Vermont", VA: "Virginia", WA: "Washington", WV: "West Virginia", WI: "Wisconsin", WY: "Wyoming" }; /* "Is your state covered?" in the hero: answers at once, and picks the state for the check below */ function heroStates(sources) { const sel = $("#hero-state"), msg = $("#hero-state-msg"); sel.innerHTML += Object.entries(US).sort((a, b) => a[1].localeCompare(b[1])).map(([c, n]) => ``).join(""); sel.addEventListener("change", () => { const c = sel.value; msg.className = ""; if (!c) { msg.textContent = ""; return; } if (sources[c]) { msg.className = "yes"; msg.textContent = `✓ ${US[c]} is covered${SCOPE[c] ? ` (${SCOPE[c]} only)` : ""}. Check your handbook against its rules below.`; if (!state.demo) { state.chosen = new Set([c]); renderChips(); requestLink(); } // the state you picked, ready below } else { msg.className = "no"; msg.textContent = `${US[c]} isn't covered yet. Email ${REQUEST_EMAIL} to ask about it.`; } }); } const params = new URLSearchParams(location.search); const state = { states: [], chosen: new Set(), effort: "low", file: null, pages: null, fixes: "", fixesName: "handbook-fixes", maxStates: 3, last: null, readSeq: 0 }; const getJson = async (path) => { const r = await fetch(path); if (!r.ok) throw new Error(`Couldn't load ${path}`); return r.json(); }; const stateName = (code) => CR.NAMES[code] || code; /* The rulebook scan: each state's whole rulebook as a bar from page 1 to the last page, with each finding at its page. */ function scan(el, tracks, { loop = false, dur = 4.2 } = {}) { el.classList.toggle("loop", loop); el.style.setProperty("--dur", `${dur}s`); el.innerHTML = tracks.map((t, i) => { const pages = Math.max(2, t.pages || 2); const marks = (t.marks || []).filter((m) => m[1] !== "not_mentioned").map(([page, st]) => { const x = ((page - 1) / (pages - 1)) * 100; return ``; }).join(""); return `
${esc(t.name)} licensing rulespage 1 → ${pages}
${marks}
`; }).join("") + (tracks.some((t) => t.marks && t.marks.length) ? `
${["conflict", "missing", "partial", "covered"].map((k) => `${LABELS[k]}`).join("")}
` : ""); } /* Hero card: the sample report's own finding for one sentence, state by state (verbatim from the report). */ function demo(rows, scores) { const states = Object.keys(scores); const row = rows.find((r) => states.every((s) => r.verdict[s] === "conflict") && /asleep|nap/i.test(r.quote)) || rows.find((r) => states.every((s) => r.verdict[s] === "conflict")) || rows[0]; if (!row) { $("#demo").hidden = true; return; } const order = states.filter((s) => row.detail[s]); const cut = (t, n) => (t.length > n ? t.slice(0, n - 1).replace(/\s+\S*$/, "") + "…" : t); const sentences = (t, n) => { // whole sentences up to n characters (at least the first one) const parts = String(t || "").match(/[^.!?]+[.!?]+(\s|$)/g) || [String(t || "")]; let out = parts[0].trim(); for (const p of parts.slice(1)) { if ((out + " " + p.trim()).length > n) break; out += " " + p.trim(); } return out; }; $("#demo-tabs").innerHTML = order.map((s) => ``).join(""); const still = matchMedia("(prefers-reduced-motion: reduce)").matches; let i = 0, timer = null; const show = (s) => { const d = row.detail[s]; document.querySelectorAll("#demo-tabs button").forEach((t) => { t.classList.toggle("on", t.dataset.st === s); t.setAttribute("aria-pressed", String(t.dataset.st === s)); }); $("#demo-page").textContent = `page ${row.page}`; $("#demo-hit").textContent = `“${cut(d.quote || row.quote, 150)}”`; ["#demo-hit", "#demo-rule", "#demo-fix"].forEach((k) => $(k).classList.remove("flag", "in")); $("#demo-rule").innerHTML = `✕ Breaks ${esc(scores[s].name)} ${esc(d.section)}, page ${esc(d.page)} of the rules${esc(cut(d.rule, 190))}✓ Quote checked word for word against the official rules`; $("#demo-fix").innerHTML = `Suggested wording (AI draft)${esc(sentences(d.fix || d.why, 190))}`; setTimeout(() => $("#demo-hit").classList.add("flag"), still ? 0 : 500); setTimeout(() => $("#demo-rule").classList.add("in"), still ? 0 : 1300); setTimeout(() => $("#demo-fix").classList.add("in"), still ? 0 : 2300); }; show(order[0]); const stop = () => { clearInterval(timer); timer = null; const p = $("#demo-pause"); if (p) p.remove(); }; if (!still) { timer = setInterval(() => show(order[++i % order.length]), 9000); $("#demo-tabs").insertAdjacentHTML("beforeend", ''); } $("#demo-tabs").addEventListener("click", (ev) => { // a state button, or pause, stops the rotation if (ev.target.closest("#demo-pause")) { stop(); return; } const b = ev.target.closest("button[data-st]"); if (!b) return; stop(); show(b.dataset.st); }); } const PHOTO = [[/asleep|nap|crib|sleep|bouncer|swing|car seat/i, "nap-room.jpg"], [/lunch|dessert|food|meal|snack|eat/i, "kids-table.jpg"], [/trip|walk|park|library|bus|outside|outdoor/i, "park.jpg"], [/screen|tv|television|tablet|video/i, "screen-time.jpg"], [/pet|animal|visitor/i, "visiting-dog.jpg"], [/medic|medicine/i, "teachers.jpg"], [/ratio|teacher cares|class|Pre-K|years? 1 to/i, "classroom.jpg"]]; // Photos used nowhere else on the page come first, so the same photo never shows twice. const POOL = ["park.jpg", "screen-time.jpg", "visiting-dog.jpg", "nap-room.jpg", "kids-table.jpg"]; const photoFor = (text, used) => { const fit = PHOTO.filter(([re]) => re.test(text)).map(([, f]) => f); const pick = [...fit, ...POOL].find((f) => !used.has(f)) || POOL[0]; used.add(pick); return pick; }; /* "One handbook, every state": sentences where the states disagree. Abuse reporting is left out: other state laws govern it, so "no conflict in the licensing rules" there could read as "this is fine". */ function differ(rows, scores) { const states = Object.keys(scores); const mixed = (r) => new Set(states.map((s) => r.verdict[s])).size > 1; const usable = (r) => r.quote.split(/\s+/).length >= 8 && !CR.isTableRow(r.quote) && !/abus|neglect|maltreat/i.test(r.quote); const picked = rows.filter(usable).sort((a, b) => mixed(b) - mixed(a)).slice(0, 3); const used = new Set([...document.querySelectorAll("img")].map((i) => i.getAttribute("src").split("/").pop())); $("#differ-rows").innerHTML = picked.map((r) => `
${esc(r.quote.length > 170 ? r.quote.slice(0, 167) + "…" : r.quote)}Sample handbook, page ${esc(r.page)}
${states.map((s) => r.verdict[s] === "conflict" ? `
${esc(scores[s].name)}
Breaks
${esc(r.sections[s] || "")}
` : `
${esc(scores[s].name)}
No broken rule found
licensing rules only
`).join("")}
`).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 `

${out + text.slice(at)}

`; }).join(""); const margin = conflicts.filter((n) => n.num).sort((a, b) => a.num - b.num).map((n) => `
${n.num}. ${esc(n.why || "Breaks a rule")}
${Object.entries(n.states).map(([s, sec]) => `${esc(stateName(s))} ${esc(sec)}`).join("")}
`).join(""); const legend = `
Red breaks a ruleYellow covers a required policy only in part
`; $("#markup-page").innerHTML = `
Little Acorns Family Handbookpage ${pageNo}
${html}${legend}
${margin || '

No conflicts on this page.

'}
`; } /* From finding to fix: the handbook sentence and the first sentence of the suggested wording. */ function pairs(rows, scores) { const first = (t) => (String(t || "").match(/^.*?[.!?](\s|$)/) || [String(t || "")])[0].trim(); const picked = rows.filter((r) => Object.values(r.detail).some((d) => d.fix) && !CR.isTableRow(r.quote)).slice(0, 3); $("#pairs").innerHTML = picked.map((r) => { const s = Object.keys(r.detail).find((k) => r.detail[k].fix); return `
“${esc(r.quote.length > 120 ? r.quote.slice(0, 117) + "…" : r.quote)}”
${esc(first(r.detail[s].fix))}
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) => `
  • ${esc(stateName(s.state))}: checked against ${esc(s.title)}, downloaded ${esc(new Date(`${s.retrieved}T12:00:00`).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }))}.
  • `).join(""); } function renderChips() { $("#state-chips").innerHTML = state.states.map((s) => { const on = state.chosen.has(s.state); return ``; }).join(""); ruleSources(); } async function init() { const sources = await CR.sources(); state.states = Object.values(sources).sort((a, b) => stateName(a.state).localeCompare(stateName(b.state))); const wanted = (params.get("state") || "").toUpperCase().split(",").filter((s) => sources[s]); wanted.forEach((s) => state.chosen.add(s)); renderChips(); requestLink(); heroStates(sources); const fromUrl = params.get("code"); let saved = ""; try { saved = localStorage.getItem("carerules-code") || ""; } catch (e) { /* private mode */ } if (fromUrl || saved) { $("#code").value = fromUrl || saved; checkCode().then((s) => { if (!s || !s.valid) return; // a valid code: say what to do next instead of how to get one $("#code-first").innerHTML = `

    Your access code is ready. Add your parent handbook below, pick your state, and press Check. No handbook handy? Try a sample handbook.

    `; $("#code-first").classList.add("welcome"); }); } if (location.hash === "#states") $("#states").open = true; if (location.hash === "#test-handbook") $("#test-handbook").open = true; const [sample, pages] = await Promise.all([getJson("samples/little-acorns.audit-high.json"), getJson("samples/little-acorns-pages.json")]); const light = await CR.light(sample.results); scan($("#hero-scan"), Object.values(light.scores).map((s) => ({ name: s.name, pages: s.pages, marks: s.marks })), { dur: 5 }); demo(light.rows, light.scores); differ(light.rows, light.scores); markup(sample.results, pages, 4); pairs(light.rows, light.scores); $("#scoreboard").innerHTML = await CR.scoreCards(sample.results); state.sample = [await CR.report(sample.results, sample.name, sample.pages), CR.fixesMarkdown(sample.results, sample.name), `Sample report

    Little Acorns Early Learning

    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})

    Required policies removed (${key.removals.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", ``); } finally { clearInterval(timer); state.running = false; input.disabled = false; drop.classList.remove("busy"); $("#progress").hidden = true; $("#go").disabled = false; checkCode(); } } $("#go").addEventListener("click", runCheck); document.addEventListener("click", (ev) => { if (ev.target.id === "retry") $("#go").click(); }); const save = (blob, name) => { const a = Object.assign(document.createElement("a"), { href: URL.createObjectURL(blob), download: name }); document.body.append(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 1000); }; $("#dl").addEventListener("click", () => save(new Blob([state.fixes.replace(/^#+ /gm, "")], { type: "text/plain" }), `${state.fixesName}.txt`)); $("#dl-word").addEventListener("click", () => { // Word opens simple HTML saved as .doc const html = state.fixes.split("\n").map((l) => (l.startsWith("# ") ? `

    ${esc(l.slice(2))}

    ` : l.startsWith("## ") ? `

    ${esc(l.slice(3))}

    ` : l.startsWith("### ") ? `

    ${esc(l.slice(4))}

    ` : l === "---" ? "
    " : l ? `

    ${esc(l)}

    ` : "")).join("\n"); save(new Blob([`${html}`], { type: "application/msword" }), `${state.fixesName}.doc`); }); $("#print").addEventListener("click", () => { $("#report-box").open = true; document.querySelectorAll("#report details").forEach((d) => (d.open = true)); window.print(); }); init().catch((err) => { $("#report").innerHTML = ``; });