Tasks 5-7: eval harness, FastAPI backend, Paper & Ink UI- src/eval/ β precision/recall/F1 harness with type-aware comparators,micro/macro F1, CSV + markdown reports, --model benchmark flag- src/api/ β FastAPI backend with /extract, /schemas, /health,request-ID middleware, typed error envelope, injectable extractor- ui/ β Vite + React + TS + Tailwind + Motion + React Three FiberPaper & Ink editorial UI with 3D paper hero, dark/light mode,confidence inkwell, wax-stamp metrics, kinetic typography- 95 passing tests (up from 54); UI is a separate npm workspace
557ab38 | /** | |
| * JsonView β pretty-printed, syntax-highlighted JSON with a copy button. | |
| * No dependencies β a small tokenizer that hits the keys/strings/numbers/ | |
| * booleans we care about. Style-tunable via CSS variables so the palette | |
| * shifts cleanly between light and dark. | |
| */ | |
| import { useMemo, useState } from "react"; | |
| interface Props { | |
| data: unknown; | |
| maxHeight?: string; | |
| } | |
| export function JsonView({ data, maxHeight = "420px" }: Props) { | |
| const pretty = useMemo(() => JSON.stringify(data, null, 2), [data]); | |
| const [copied, setCopied] = useState(false); | |
| const onCopy = async () => { | |
| try { | |
| await navigator.clipboard.writeText(pretty); | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 1400); | |
| } catch { | |
| /* clipboard denied */ | |
| } | |
| }; | |
| return ( | |
| <div className="relative overflow-hidden rounded-[6px] border border-[var(--rule)] bg-[var(--surface-2)]"> | |
| <div className="flex items-center justify-between border-b border-[var(--rule)] px-4 py-2.5"> | |
| <span className="eyebrow">extraction_result.json</span> | |
| <button | |
| type="button" | |
| onClick={onCopy} | |
| className="rounded-full border border-[var(--rule)] px-3 py-1 text-[11px] tracking-wide text-[var(--ink-soft)] transition-colors duration-300 hover:border-[var(--ink)] hover:text-[var(--ink)]" | |
| > | |
| {copied ? "Copied" : "Copy"} | |
| </button> | |
| </div> | |
| <pre | |
| style={{ maxHeight }} | |
| className="overflow-auto px-5 py-4 font-mono text-[12.5px] leading-[1.7] text-[var(--ink)]" | |
| > | |
| <code dangerouslySetInnerHTML={{ __html: highlight(pretty) }} /> | |
| </pre> | |
| </div> | |
| ); | |
| } | |
| /* ------------------------------------------------------------------------ */ | |
| // Minimal JSON syntax highlighter β enough to distinguish keys, strings, | |
| // numbers, booleans, and null. Uses spans with CSS variable colors. | |
| function highlight(json: string): string { | |
| const esc = json | |
| .replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">"); | |
| return esc.replace( | |
| /("(?:\\.|[^"\\])*"(?:\s*:)?)|\b(true|false|null)\b|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g, | |
| (m) => { | |
| if (/^"/.test(m)) { | |
| if (/:$/.test(m)) { | |
| return `<span style="color:var(--accent)">${m.replace(/:$/, "")}</span>:`; | |
| } | |
| return `<span style="color:var(--sage)">${m}</span>`; | |
| } | |
| if (/true|false/.test(m)) { | |
| return `<span style="color:var(--mustard)">${m}</span>`; | |
| } | |
| if (/null/.test(m)) { | |
| return `<span style="color:var(--ink-mute)">${m}</span>`; | |
| } | |
| // number | |
| return `<span style="color:var(--ink-strong)">${m}</span>`; | |
| } | |
| ); | |
| } | |