structured-data-extractor / ui /src /components /ExtractSection.tsx
aditya0103's picture
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
Raw
History Blame Contribute Delete
3.65 kB
/**
* ExtractSection — the interactive core. Wires Dropzone <-> useExtract <->
* ResultsPanel. The parent (App) needs to know when we're extracting so the
* hero's 3D paper can respond, so we lift the state up via `onStateChange`.
*/
import { useEffect, useState } from "react";
import { motion } from "motion/react";
import { useExtract } from "@/hooks/useExtract";
import type { DocType } from "@/types";
import type { PaperState } from "./PaperScene";
import { Dropzone } from "./Dropzone";
import { ResultsPanel } from "./ResultsPanel";
interface Props {
onStateChange: (s: PaperState) => void;
bindExtract: (fn: () => void) => void;
}
export function ExtractSection({ onStateChange, bindExtract }: Props) {
const [file, setFile] = useState<File | null>(null);
const [docType, setDocType] = useState<DocType>("receipt");
const { status, response, error, run, reset } = useExtract();
const busy = status === "loading";
// Bubble status up so hero's 3D paper can react.
useEffect(() => {
if (status === "loading") onStateChange("extracting");
else if (status === "success") onStateChange("extracted");
else onStateChange("idle");
}, [status, onStateChange]);
const doExtract = () => {
if (!file) return;
run({ file, docType });
};
// The hero's "Try a sample" CTA scrolls here + focuses the browse. Expose
// that hook to the parent via bindExtract.
useEffect(() => {
bindExtract(() => {
const el = document.getElementById("extract");
el?.scrollIntoView({ behavior: "smooth", block: "start" });
});
}, [bindExtract]);
return (
<section
id="extract"
className="relative mx-auto w-full max-w-[1240px] border-t border-[var(--rule)] px-6 py-24 md:px-10"
>
<div className="mb-14 flex items-end justify-between gap-6">
<div>
<p className="eyebrow mb-4">The workbench</p>
<h2 className="font-display text-[clamp(38px,5vw,72px)] leading-[1] text-[var(--ink-strong)]">
Try it on a document.
</h2>
</div>
{response && (
<button
type="button"
onClick={() => {
reset();
setFile(null);
}}
className="text-[13px] tracking-wide text-[var(--ink-soft)] underline decoration-[var(--rule)] decoration-1 underline-offset-4 hover:text-[var(--ink)] hover:decoration-[var(--ink)]"
>
Start over
</button>
)}
</div>
<div className="grid grid-cols-1 gap-14 lg:grid-cols-[1fr_1fr] lg:gap-20">
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.8, ease: [0.16, 1, 0.3, 1] }}
>
<Dropzone
file={file}
docType={docType}
setFile={setFile}
setDocType={setDocType}
onExtract={doExtract}
onSample={() => {
// If a sample was picked, kick off extraction automatically.
setTimeout(doExtract, 200);
}}
busy={busy}
/>
</motion.div>
<motion.div
initial={{ opacity: 0, y: 24 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-100px" }}
transition={{
duration: 0.8,
delay: 0.15,
ease: [0.16, 1, 0.3, 1],
}}
>
<ResultsPanel status={status} response={response} error={error} />
</motion.div>
</div>
</section>
);
}