Core ML
legal
citation-verification
hallucination-detection
apple-neural-engine
hand-written-weights
citenet-coreml / demo /live.html
alexwengg's picture
CiteNet v1.5: legal-citation checker with hand-written weights (Core ML)
42b4a6a verified
Raw History Blame Contribute Delete
8.86 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CiteNet Live</title>
<style>
:root {
--bg: #f7f6f2; --panel: #ffffff; --ink: #1c1b19; --muted: #77746b; --line: #e6e3da;
--ok: #1f7a4d; --ok-bg: #e2f2e8; --bad: #c2321f; --bad-bg: #fbe3de;
--accent: #2f4fd6; --gpu: #1f7a4d;
--mono: ui-monospace, "SF Mono", Menlo, monospace;
--serif: "Iowan Old Style", Palatino, Georgia, serif;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #141412; --panel: #1c1b19; --ink: #ecebe6; --muted: #918e85; --line: #2d2c28;
--ok: #4fc487; --ok-bg: #1c3326; --bad: #ff7a66; --bad-bg: #42201a;
--accent: #8ea2ff; --gpu: #4fc487;
}
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--bg); color: var(--ink); font: 15px/1.5 -apple-system, BlinkMacSystemFont, system-ui, sans-serif; }
.wrap { max-width: 980px; margin: 0 auto; padding: 36px 20px 48px; }
h1 { margin: 0; font-size: 30px; letter-spacing: -0.02em; }
.lede { color: var(--muted); margin: 4px 0 20px; }
.lede a { color: inherit; }
.controls { display: flex; gap: 14px; align-items: center; flex-wrap: wrap; }
.controls button { font: 600 15px -apple-system, system-ui, sans-serif; border: 0; background: var(--ink); color: var(--bg); padding: 11px 18px; border-radius: 10px; cursor: pointer; }
.controls label { color: var(--muted); font-size: 14px; display: flex; gap: 6px; align-items: center; cursor: pointer; }
.nums { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin: 26px 0 10px; }
.num .k { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .07em; }
.num .v { font-size: 38px; font-weight: 700; letter-spacing: -0.03em; font-variant-numeric: tabular-nums; line-height: 1.15; }
.num .r { color: var(--muted); font-size: 14px; font-variant-numeric: tabular-nums; }
.num .r b { color: var(--ink); font-weight: 600; }
.bar { display: flex; height: 6px; border-radius: 3px; overflow: hidden; background: var(--line); }
.bar div { background: var(--ink); width: 0; transition: width .1s linear; }
.meta { color: var(--muted); font-size: 13px; margin-top: 8px; font-variant-numeric: tabular-nums; min-height: 1.5em; }
.cols { display: grid; grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); gap: 24px; margin-top: 26px; }
h2 { font-size: 12px; text-transform: uppercase; letter-spacing: .07em; color: var(--muted); margin: 0 0 8px; font-weight: 600; }
.tape { height: 420px; overflow: hidden; border: 1px solid var(--line); border-radius: 12px; background: var(--panel); padding: 8px 14px;
-webkit-mask-image: linear-gradient(to bottom, #000 70%, transparent); mask-image: linear-gradient(to bottom, #000 70%, transparent); }
.tape div { font: 14px/1.55 var(--serif); padding: 4px 0; border-bottom: 1px solid var(--line); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--muted); }
.tape mark { background: var(--ok-bg); color: var(--ink); border-radius: 3px; padding: 0 2px; }
.tape mark.bad { background: var(--bad-bg); box-shadow: inset 0 -2px 0 var(--bad); }
.caught { list-style: none; margin: 0; padding: 0; max-height: 420px; overflow: hidden; }
.caught li { padding: 8px 0; border-top: 1px solid var(--line); }
.caught li:first-child { border-top: 0; }
.caught .c { font: 13px var(--mono); color: var(--bad); overflow-wrap: anywhere; }
.caught .w { color: var(--muted); font-size: 13.5px; }
.note { color: var(--muted); font-size: 13px; margin-top: 10px; }
.empty { color: var(--muted); font-size: 14px; }
@media (max-width: 760px) {
.wrap { padding: 24px 16px 40px; }
.nums { grid-template-columns: minmax(0, 1fr); gap: 10px; }
.num .v { font-size: 30px; }
.cols { grid-template-columns: minmax(0, 1fr); }
.tape { height: 300px; }
}
</style>
</head>
<body>
<div class="wrap">
<h1>CiteNet Live</h1>
<div class="lede" id="lede">Real US court opinions, scanned for impossible citations as fast as the Mac can read them. <a href="/">Back</a></div>
<div class="controls">
<button id="go">Scan</button>
<label><input type="checkbox" id="loop"> keep going (loop the corpus)</label>
</div>
<div class="nums">
<div class="num"><div class="k">Text read</div><div class="v" id="n-bytes">0 GB</div><div class="r" id="r-bytes">&nbsp;</div></div>
<div class="num"><div class="k">Citations checked</div><div class="v" id="n-cites">0</div><div class="r" id="r-cites">&nbsp;</div></div>
<div class="num"><div class="k">Flagged</div><div class="v" id="n-flag">0</div><div class="r" id="r-flag">&nbsp;</div></div>
</div>
<div class="bar"><div id="prog"></div></div>
<div class="meta" id="meta"></div>
<div class="cols">
<div>
<h2>Citations going by</h2>
<div class="tape" id="tape"><div class="empty" style="border:0">Press Scan.</div></div>
</div>
<div>
<h2>Flagged</h2>
<ul class="caught" id="caught"></ul>
<div class="note">Unverified. Real opinions contain typos and OCR errors, and CiteNet's volume timelines are approximate, so some flags are its own mistakes.</div>
</div>
</div>
</div>
<script>
const $ = (id) => document.getElementById(id);
const esc = (s) => s.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(+n)).replace(/&amp;/g, "&")
.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
const fmtB = (b) => b >= 1e9 ? (b / 1e9).toFixed(2) + " GB" : (b / 1e6).toFixed(0) + " MB";
const fmtR = (n) => n >= 1e6 ? (n / 1e6).toFixed(2) + "M" : n >= 1e3 ? Math.round(n / 1e3) + "k" : String(n);
let info = null, es = null, seen = new Set();
function reset() {
["n-bytes", "n-cites", "n-flag"].forEach(id => $(id).textContent = "0");
$("n-bytes").textContent = "0 GB";
["r-bytes", "r-cites", "r-flag"].forEach(id => $(id).innerHTML = "&nbsp;");
$("tape").innerHTML = ""; $("caught").innerHTML = ""; $("meta").textContent = ""; $("prog").style.width = "0";
seen = new Set();
}
function stop() { if (es) { es.close(); es = null; } $("go").textContent = info ? `Scan ${fmtB(info.bytes)}` : "Scan"; }
function onTick(ev) {
if (ev.start) return;
const loopRun = $("loop").checked;
$("n-bytes").textContent = fmtB(ev.bytes);
$("r-bytes").innerHTML = ev.done ? `in <b>${ev.t.toFixed(2)} s</b>` : `<b>${(ev.mbps / 1000).toFixed(2)} GB/s</b>`;
$("n-cites").textContent = ev.checked.toLocaleString();
$("r-cites").innerHTML = ev.done ? `<b>${fmtR(Math.round(ev.checked / ev.t))}/s</b> average` : `<b>${fmtR(ev.cps)}/s</b>`;
$("n-flag").textContent = ev.flagged.toLocaleString();
$("r-flag").innerHTML = ev.checked ? `${(100 * ev.flagged / ev.checked).toFixed(3)}% of citations` : "&nbsp;";
const total = info ? info.bytes : ev.bytes;
$("prog").style.width = loopRun ? "100%" : Math.min(100, 100 * ev.bytes / total) + "%";
const lanes = ev.ane + ev.gpu;
$("meta").textContent = (lanes ? `Neural Engine ${Math.round(100 * ev.ane / lanes)}% · GPU ${Math.round(100 * ev.gpu / lanes)}% · ` : "") +
`4 CPU scan threads · ${ev.t.toFixed(1)} s` + (loopRun ? ` · pass ${ev.pass + 1}` : "") +
(ev.done ? ` · done: ${info ? info.decisions.toLocaleString() + " decisions, " : ""}${ev.checked.toLocaleString()} citations` : "");
const tape = $("tape");
for (const s of ev.ticker) {
const d = document.createElement("div");
const before = s.before.slice(-34), after = s.after.slice(0, 40);
d.innerHTML = `…${esc(before)}<mark class="${s.flag ? "bad" : ""}">${esc(s.cite)}</mark>${esc(after)}…`;
tape.prepend(d);
}
while (tape.children.length > 40) tape.lastChild.remove();
const ul = $("caught");
for (const c of ev.caught) {
if (seen.has(c.cite)) continue;
seen.add(c.cite);
const li = document.createElement("li");
li.innerHTML = `<div class="c">${esc(c.cite)}</div><div class="w">${esc(c.why || c.kind)}</div>`;
ul.prepend(li);
}
while (ul.children.length > 14) ul.lastChild.remove();
if (ev.done) stop();
}
$("go").onclick = () => {
if (es) return stop();
reset();
$("go").textContent = "Stop";
es = new EventSource("/live/stream?loop=" + ($("loop").checked ? 1 : 0));
es.onmessage = (m) => onTick(JSON.parse(m.data));
es.onerror = () => stop();
};
fetch("/live/info").then(r => r.json()).then(j => {
if (j.error) { $("lede").innerHTML = "No corpus found (data/cold/corpus.txt). <a href='/'>Back</a>"; return; }
info = j;
$("lede").innerHTML = `${j.decisions.toLocaleString()} real US court decisions (${fmtB(j.bytes)}, COLD Cases, CC0), ` +
`scanned by a native scanner on the CPU and checked by CiteNet on the Neural Engine + GPU. <a href="/wall">Watch it read</a> · <a href="/">Back</a>`;
$("go").textContent = `Scan ${fmtB(j.bytes)}`;
if (new URLSearchParams(location.search).get("auto")) $("go").click();
});
</script>
</body>
</html>