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 | /** | |
| * Theme hook β persists user choice in localStorage, respects prefers-color-scheme | |
| * on first visit, and reflects the current theme as data-theme on <html>. | |
| * The initial paint happens in index.html to avoid a flash. | |
| */ | |
| import { useCallback, useEffect, useState } from "react"; | |
| export type Theme = "light" | "dark"; | |
| const STORAGE_KEY = "theme"; | |
| function getInitial(): Theme { | |
| if (typeof window === "undefined") return "light"; | |
| const stored = window.localStorage.getItem(STORAGE_KEY) as Theme | null; | |
| if (stored === "light" || stored === "dark") return stored; | |
| const dark = window.matchMedia("(prefers-color-scheme: dark)").matches; | |
| return dark ? "dark" : "light"; | |
| } | |
| export function useTheme() { | |
| const [theme, setThemeState] = useState<Theme>(getInitial); | |
| useEffect(() => { | |
| document.documentElement.setAttribute("data-theme", theme); | |
| window.localStorage.setItem(STORAGE_KEY, theme); | |
| }, [theme]); | |
| const setTheme = useCallback((t: Theme) => setThemeState(t), []); | |
| const toggle = useCallback( | |
| () => setThemeState((t) => (t === "dark" ? "light" : "dark")), | |
| [] | |
| ); | |
| return { theme, setTheme, toggle }; | |
| } | |