/** * ResultsPanel — the right column when we have a result. Composes: * - ConfidenceInkwell (top) * - MetricsStrip (cost / latency / tokens / model) * - JsonView (extracted data) * - WarningsList * * Empty and error states are handled inline — no separate components needed * for something this small. */ import { AnimatePresence, motion } from "motion/react"; import type { APIError } from "@/lib/api"; import type { ExtractResponse } from "@/types"; import { ConfidenceInkwell } from "./ConfidenceInkwell"; import { JsonView } from "./JsonView"; import { MetricsStrip } from "./MetricsStrip"; import { WarningsList } from "./WarningsList"; interface Props { status: "idle" | "loading" | "success" | "error"; response: ExtractResponse | null; error: APIError | null; } const ITEM = { hidden: { y: 24, opacity: 0 }, visible: { y: 0, opacity: 1, transition: { duration: 0.7, ease: [0.16, 1, 0.3, 1] }, }, }; export function ResultsPanel({ status, response, error }: Props) { return ( {status === "idle" && } {status === "loading" && } {status === "error" && error && } {status === "success" && response && (

Extracted data

Warnings

)}
); } /* ------------------------------------------------------------------------ */ function Empty() { return (

Awaiting document

The result will land here —

Confidence, cost, latency, and the extracted JSON. Every field cross-checked against a Pydantic schema before it reaches you.

Try one of the samples on the left
); } function Loading() { return (

Extracting…

Reading the sheet.

{[0, 1, 2, 3].map((i) => ( ))}
); } function Skeleton({ delay }: { delay: number }) { return ( ); } function ErrorView({ error }: { error: APIError }) { return (

Extraction failed

{error.message}

code: {error.code} {error.requestId && ` · request_id: ${error.requestId}`}

); }