Core ML
legal
citation-verification
hallucination-detection
apple-neural-engine
hand-written-weights
citenet-coreml / demo /wall.html
alexwengg's picture
CiteNet v1.5: legal-citation checker with hand-written weights (Core ML)
42b4a6a verified
Raw History Blame Contribute Delete
18 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>CiteNet Wall</title>
<style>
:root {
--bg: #f7f6f2; --panel: #ffffff; --ink: #1c1b19; --muted: #77746b; --faint: #8f8b80; --line: #e6e3da;
--ok: #1f7a4d; --ok-bg: #dcefe3; --bad: #c2321f; --bad-bg: #f9dcd6; --warn: #a86a00; --warn-bg: #f8ebcc;
--mono: ui-monospace, "SF Mono", Menlo, monospace;
--serif: "Iowan Old Style", Palatino, Georgia, serif;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #121210; --panel: #1a1917; --ink: #ecebe6; --muted: #918e85; --faint: #8a877e; --line: #2b2a26;
--ok: #4fc487; --ok-bg: #1b3325; --bad: #ff7a66; --bad-bg: #4a221b; --warn: #f0b545; --warn-bg: #3b2f17;
}
}
* { box-sizing: border-box; }
html, body { height: 100%; }
body { margin: 0; background: var(--bg); color: var(--ink); font: 14px/1.5 -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
display: flex; flex-direction: column; overflow: hidden; }
header { padding: 14px 22px 10px; display: flex; gap: 10px 18px; align-items: center; flex-wrap: wrap; border-bottom: 1px solid var(--line); }
h1 { margin: 0; font-size: 20px; letter-spacing: -0.02em; }
.group { display: flex; gap: 4px; align-items: center; }
.group span { color: var(--muted); font-size: 12px; margin-right: 4px; }
button { font: inherit; font-size: 13px; border: 1px solid var(--line); background: var(--panel); color: var(--ink); padding: 4px 10px; border-radius: 999px; cursor: pointer; }
button.on { border-color: var(--ink); background: var(--ink); color: var(--bg); }
header a { color: var(--muted); font-size: 13px; }
.stats { padding: 8px 22px; display: flex; gap: 6px 22px; flex-wrap: wrap; color: var(--muted); font-size: 13px; font-variant-numeric: tabular-nums; border-bottom: 1px solid var(--line); }
.stats b { color: var(--ink); font-weight: 600; }
.stats .bad { color: var(--bad); }
main { flex: 1; display: flex; min-height: 0; }
.viewport { flex: 1; overflow: hidden; position: relative; cursor: pointer; min-width: 0;
-webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 6%, #000 88%, transparent 100%);
mask-image: linear-gradient(to bottom, transparent 0, #000 6%, #000 88%, transparent 100%); }
.wall { position: absolute; left: 0; right: 0; top: 0; will-change: transform; }
.chunk { max-width: 1180px; margin: 0 auto; padding: 0 22px; font: 14.5px/1.62 var(--serif); color: var(--faint); white-space: pre-wrap; overflow-wrap: break-word; }
.case { display: inline-block; color: var(--ink); font: 600 13px/1.4 -apple-system, system-ui, sans-serif; margin-top: 10px; }
mark { border-radius: 3px; padding: 0 1px; color: var(--ink); }
mark.ok { background: var(--ok-bg); }
mark.unknown { background: var(--warn-bg); }
mark.phantom { background: var(--bad-bg); box-shadow: inset 0 -2px 0 var(--bad); font-weight: 600; }
mark.missed { outline: 2px dashed var(--bad); }
.tag.miss { background: var(--muted); }
.q .b { font: 600 10px/1 -apple-system, system-ui, sans-serif; text-transform: uppercase; letter-spacing: .05em; color: var(--muted); margin-left: 6px; }
.tag { font: 600 11px/1 -apple-system, system-ui, sans-serif; color: #fff; background: var(--bad); border-radius: 4px; padding: 2px 6px; margin-left: 4px; white-space: nowrap; vertical-align: 1px; }
.readline { position: absolute; left: 0; right: 0; top: 55%; border-top: 1px dashed var(--line); pointer-events: none; }
.readline span { position: absolute; right: 10px; top: -9px; font-size: 11px; color: var(--muted); background: var(--bg); padding: 0 4px; }
mark.hit { animation: hit .6s ease-out; }
@keyframes hit { from { outline: 3px solid var(--ink); } to { outline: 3px solid transparent; } }
aside { width: 380px; border-left: 1px solid var(--line); display: flex; flex-direction: column; min-height: 0; background: var(--panel); }
aside h2 { margin: 0; padding: 10px 14px 6px; font-size: 11px; text-transform: uppercase; letter-spacing: .07em; color: var(--muted); font-weight: 600;
display: flex; justify-content: space-between; }
aside h2 b { color: var(--ink); font-variant-numeric: tabular-nums; }
.reds { max-height: 38%; overflow-y: auto; border-bottom: 1px solid var(--line); padding-bottom: 6px; }
.reds .empty { color: var(--muted); font-size: 13px; padding: 2px 14px 6px; }
.lights { flex: 1; overflow: hidden; -webkit-mask-image: linear-gradient(to bottom, #000 75%, transparent); mask-image: linear-gradient(to bottom, #000 75%, transparent); }
.q { display: grid; grid-template-columns: 9px minmax(0, 1fr); gap: 1px 9px; padding: 4px 14px; align-items: baseline; }
.q i { width: 9px; height: 9px; border-radius: 50%; background: var(--ok); display: inline-block; }
.q.unknown i { background: var(--warn); }
.q.phantom i { background: var(--bad); }
.q.missedrow i { background: transparent; border: 2px solid var(--bad); }
.q .c { font: 12.5px/1.35 var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.q.phantom .c { color: var(--bad); white-space: normal; overflow-wrap: anywhere; }
.q .w { grid-column: 2; color: var(--muted); font-size: 12.5px; line-height: 1.35; }
.q.new { animation: slide .35s ease-out; }
@keyframes slide { from { background: var(--line); } to { background: transparent; } }
button.play { font-weight: 600; border-color: var(--bad); color: var(--bad); }
button.play.on { background: var(--bad); color: #fff; border-color: var(--bad); }
.runbar { position: absolute; left: 0; top: 0; height: 3px; background: var(--bad); width: 0; z-index: 2; }
.summary { position: absolute; left: 50%; top: 38%; transform: translate(-50%, -50%); z-index: 3; display: none;
background: var(--panel); border: 1px solid var(--line); border-radius: 14px; padding: 18px 26px; text-align: center;
box-shadow: 0 10px 40px rgba(0,0,0,.35); min-width: 360px; cursor: pointer; }
.summary .t { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); }
.summary .big { font-size: 34px; font-weight: 700; letter-spacing: -0.02em; margin: 4px 0 8px; font-variant-numeric: tabular-nums; }
.summary .row { color: var(--muted); font-size: 14px; font-variant-numeric: tabular-nums; line-height: 1.7; }
.summary .row b { color: var(--ink); }
.summary .row .bad { color: var(--bad); }
.summary .hint { color: var(--faint); font-size: 12px; margin-top: 8px; }
.paused { position: absolute; right: 22px; top: 14px; font-size: 12px; color: var(--muted); background: var(--panel); border: 1px solid var(--line); border-radius: 6px; padding: 3px 8px; display: none; }
@media (max-width: 900px) {
main { flex-direction: column; }
aside { width: auto; height: 34vh; border-left: 0; border-top: 1px solid var(--line); }
header, .stats { padding-left: 16px; padding-right: 16px; } .chunk { padding: 0 16px; font-size: 14px; }
}
</style>
</head>
<body>
<header>
<h1>CiteNet Wall</h1>
<div class="group"><span>Text</span>
<button data-src="planted" class="on" title="real opinions with ~1 in 11 citations mutated into impossible ones">Opinions + planted fakes</button>
<button data-src="cold">Court opinions</button>
<button data-src="lepc">LePhantomCite briefs</button>
</div>
<div class="group"><span>Speed</span>
<button data-speed="45">Read</button>
<button data-speed="400" class="on">Fast</button>
<button data-speed="2500">Faster</button>
<button data-speed="9000">Max</button>
</div>
<button id="play" class="play" title="fresh run for exactly 30 seconds at the selected speed, then stop">▶ 30 s</button>
<button id="pause" title="pause / resume (space, or click the text)">❚❚ Pause</button>
<a href="/live">Full-speed scan</a> <a href="/">Back</a>
</header>
<div class="stats" id="stats">&nbsp;</div>
<main>
<div class="viewport" id="vp">
<div class="wall" id="wall"></div>
<div class="readline"><span>citations join the queue here</span></div>
<div class="paused" id="paused">paused</div>
<div class="runbar" id="runbar"></div>
<div class="summary" id="summary"></div>
</div>
<aside>
<h2><span>Phantoms</span><b id="n-red">0</b></h2>
<div class="reds" id="reds"><div class="empty">None yet.</div></div>
<h2><span>Queue</span><b id="n-lit">0</b></h2>
<div class="lights" id="lights"></div>
</aside>
</main>
<script>
const $ = (id) => document.getElementById(id);
const vp = $("vp"), wall = $("wall");
const ENT = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: "\u00a0", sect: "\u00a7", para: "\u00b6" };
const unent = (s) => s.replace(/&(#\d+|[a-z]+);/g, (m, e) => e[0] === "#" ? String.fromCharCode(+e.slice(1)) : (ENT[e] ?? m));
const esc = (s) => unent(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
let pending = [], lit = 0, reds = 0;
const RUN_S = 30;
let runLeft = 0; // seconds left in a 30 s run (0 = not in a run)
let src = "planted", speed = 400, y = 0, next = 0, inflight = false, paused = false, gen = 0;
let tot = { bytes: 0, cites: 0, flagged: 0, chunks: 0, planted: 0, caught: 0, missed: 0 }, recentMs = [], lastModel = "", px = 0, lastPx = 0, lastT = performance.now();
function render(c, first) {
const t = c.text; let h = "", pos = 0;
for (const [s, e, st, why, planted] of c.spans) {
const cls = st + (planted ? " planted" : "") + (planted && st !== "phantom" ? " missed" : "");
h += esc(t.slice(pos, s)) + `<mark class="${cls}" data-why="${esc(why)}"${why ? ` title="${esc(why)}"` : ""}>${esc(t.slice(s, e))}</mark>`;
if (st === "phantom") h += `<span class="tag">${planted ? "planted · " : ""}${esc(why)}</span>`;
else if (planted) h += `<span class="tag miss">missed planted fake</span>`;
pos = e;
}
h += esc(t.slice(pos));
// a decision starts after a triple line break with a "case name, court, date" line
h = h.replace(/\n\n\n([^\n<]{3,220})\n/g, "\n\n\n<span class=\"case\">$1</span>\n");
if (first && src === "cold") h = h.replace(/^([^\n<]{3,220})\n/, "<span class=\"case\">$1</span>\n");
const d = document.createElement("div");
d.className = "chunk"; d.innerHTML = h;
return d;
}
async function fetchMore(g) {
inflight = true;
const size = speed >= 2500 ? 65536 : 12288; // bigger chunks when scrolling fast
const c = await fetch(`/wall/chunk?src=${src}&off=${next}&size=${size}`).then(r => r.json());
inflight = false;
if (g !== gen) return;
next = c.next;
const d = render(c, tot.chunks === 0); d.dataset.bytes = c.bytes;
wall.appendChild(d);
pending.push(...d.querySelectorAll("mark"));
tot.bytes += c.bytes; tot.cites += c.n; tot.flagged += c.flagged; tot.chunks += 1;
tot.planted += c.planted || 0; tot.caught += c.caught || 0; tot.missed += c.missed || 0;
recentMs.push([c.model_ms, c.bytes]); if (recentMs.length > 25) recentMs.shift();
lastModel = c.model;
}
function stats() {
// scroll rate in bytes: pixels scrolled per second times bytes per pixel of the text on screen
const now = performance.now(), dt = (now - lastT) / 1000;
const pps = (px - lastPx) / Math.max(dt, 1e-3); lastPx = px; lastT = now;
let b = 0; for (const ch of wall.children) b += +ch.dataset.bytes || 0;
const kbs = pps * (b / Math.max(1, wall.scrollHeight)) / 1000;
const unit = lastModel.includes("ANE") ? "Neural Engine" : "CPU";
const sorted = [...recentMs].sort((a, b) => a[0] - b[0]), mid = sorted[sorted.length >> 1] || [0, 0];
const per = mid[0], chunkKB = Math.round(mid[1] / 1024);
$("stats").innerHTML =
`<span><b>${(tot.bytes / 1e6).toFixed(2)} MB</b> read</span>` +
`<span><b>${tot.cites.toLocaleString()}</b> citations highlighted</span>` +
(src === "planted"
? `<span><b>${tot.planted.toLocaleString()}</b> fakes planted</span><span class="bad"><b class="bad">${tot.caught.toLocaleString()}</b> caught</span>` +
`<span><b>${tot.missed.toLocaleString()}</b> missed</span><span><b>${(tot.flagged - tot.caught).toLocaleString()}</b> found in the source</span>`
: `<span class="bad"><b class="bad">${tot.flagged.toLocaleString()}</b> phantom</span>`) +
`<span>each ${chunkKB} KB chunk checked in <b>${per.toFixed(2)} ms</b> on the ${unit}</span>` +
`<span>scrolling <b>${kbs < 1 ? Math.round(kbs * 1000) + " B/s" : kbs < 1000 ? kbs.toFixed(0) + " KB/s" : (kbs / 1000).toFixed(1) + " MB/s"}</b></span>`;
}
let last = performance.now();
function frame(t) {
const dt = Math.min(0.1, (t - last) / 1000); last = t;
const vh = vp.clientHeight;
if (runLeft > 0 && !paused) {
runLeft -= dt;
$("runbar").style.width = (100 * (1 - Math.max(0, runLeft) / RUN_S)) + "%";
$("play").textContent = "0:" + String(Math.ceil(Math.max(0, runLeft))).padStart(2, "0");
if (runLeft <= 0) finishRun();
}
if (!paused) {
const room = wall.scrollHeight - y - vh;
const step = Math.min(speed * dt, Math.max(0, room)); // never scroll past what has been checked
y += step; px += step;
}
// drop chunks that have scrolled off the top
while (wall.firstChild && wall.firstChild.offsetHeight < y - vh * 0.2) {
y -= wall.firstChild.offsetHeight;
wall.removeChild(wall.firstChild);
}
wall.style.transform = `translate3d(0, ${-y}px, 0)`;
// citations that crossed the reading line join the queue, in reading order
const line = vp.getBoundingClientRect().top + vh * 0.55;
let moved = 0;
while (pending.length && moved < 40) {
const m = pending[0];
if (!m.isConnected) { pending.shift(); continue; }
if (m.getBoundingClientRect().top > line) break;
pending.shift(); enqueue(m); moved++;
}
if (!inflight && wall.scrollHeight - y < vh * 3 + speed * 1.5) fetchMore(gen); // keep ~1.5 s of text ahead
requestAnimationFrame(frame);
}
function enqueue(m) {
const st = m.className.split(" ")[0], why = m.dataset.why;
const planted = m.classList.contains("planted"), missed = m.classList.contains("missed");
m.classList.add("hit");
const row = document.createElement("div");
row.className = `q ${missed ? "phantom missedrow" : st} new`;
const badge = src === "planted" && st === "phantom" ? `<span class="b">${planted ? "planted" : "in source"}</span>` : missed ? `<span class="b">missed</span>` : "";
row.innerHTML = `<i></i><span class="c">${esc(m.textContent)}${badge}</span>` + (why && st !== "ok" ? `<span class="w">${esc(why)}</span>` : "");
const L = $("lights");
L.prepend(row);
while (L.children.length > 80) L.lastChild.remove();
$("n-lit").textContent = (++lit).toLocaleString();
if (st === "phantom" || missed) {
const R = $("reds");
if (!reds) R.innerHTML = "";
R.prepend(row.cloneNode(true));
while (R.children.length > 40) R.lastChild.remove();
$("n-red").textContent = (++reds).toLocaleString();
}
}
async function start(newSrc) {
src = newSrc; gen += 1; y = 0; wall.innerHTML = "";
pending = []; lit = 0; reds = 0;
$("lights").innerHTML = ""; $("reds").innerHTML = `<div class="empty">None yet.</div>`;
$("n-lit").textContent = "0"; $("n-red").textContent = "0";
tot = { bytes: 0, cites: 0, flagged: 0, chunks: 0, planted: 0, caught: 0, missed: 0 }; recentMs = [];
document.querySelectorAll("[data-src]").forEach(b => b.classList.toggle("on", b.dataset.src === src));
next = (await fetch(`/wall/random?src=${src}`).then(r => r.json())).off;
inflight = false;
}
document.querySelectorAll("[data-src]").forEach(b => b.onclick = () => start(b.dataset.src));
document.querySelectorAll("[data-speed]").forEach(b => b.onclick = () => {
speed = +b.dataset.speed;
document.querySelectorAll("[data-speed]").forEach(x => x.classList.toggle("on", x === b));
});
function syncPause() {
$("pause").textContent = paused ? "▶ Resume" : "❚❚ Pause";
$("pause").classList.toggle("on", paused);
$("paused").style.display = paused && $("summary").style.display !== "block" ? "block" : "none";
}
const toggle = () => {
if ($("summary").style.display === "block") { $("summary").style.display = "none"; }
paused = !paused;
syncPause();
};
function finishRun() {
runLeft = 0; paused = true;
$("play").classList.remove("on"); $("play").textContent = "↻ 30 s";
const planted = src === "planted";
const kb = tot.bytes / 1000;
$("summary").innerHTML =
`<div class="t">${RUN_S} seconds · ${{ planted: "opinions + planted fakes", cold: "court opinions", lepc: "LePhantomCite briefs" }[src]} · ${document.querySelector("[data-speed].on").textContent}</div>` +
`<div class="big">${tot.cites.toLocaleString()} citations checked</div>` +
`<div class="row"><b>${kb < 1000 ? kb.toFixed(0) + " KB" : (kb / 1000).toFixed(2) + " MB"}</b> of text read · ` +
`each chunk checked in <b>${$("stats").querySelector("span:nth-last-child(2) b")?.textContent || "–"}</b> on the Neural Engine</div>` +
`<div class="row">` + (planted
? `<b>${tot.planted}</b> fakes planted · <b class="bad">${tot.caught}</b> caught · <b>${tot.missed}</b> missed · <b>${tot.flagged - tot.caught}</b> found in the source`
: `<b class="bad">${tot.flagged}</b> phantom citations flagged`) + `</div>` +
`<div class="hint">click to keep reading</div>`;
$("summary").style.display = "block";
syncPause();
}
$("play").onclick = async (e) => {
e.stopPropagation();
$("summary").style.display = "none"; $("paused").style.display = "none";
await start(src);
paused = false; runLeft = RUN_S; syncPause();
$("play").classList.add("on"); $("runbar").style.width = "0";
};
vp.onclick = toggle;
$("pause").onclick = (e) => { e.stopPropagation(); toggle(); };
document.addEventListener("keydown", (e) => { if (e.code === "Space") { e.preventDefault(); toggle(); } });
const q = new URLSearchParams(location.search);
if (q.get("speed")) { const b = document.querySelector(`[data-speed="${q.get("speed")}"]`); if (b) b.click(); }
start(q.get("src") || "planted").then(() => {
requestAnimationFrame(frame);
if (q.get("play")) $("play").click();
});
setInterval(stats, 250);
</script>
</body>
</html>