Skip to content

Commit 5cf9398

Browse files
DylanMerigaudclaude
andcommitted
Polish the PDF preview: full-size, framed, no bogus scroll
- Preview shows the document larger (max-w 680) so the important top of the invoice reads clearly, framed with a real border (the canvas no longer paints over an inset ring) and no inner padding gap. - Auto-scroll and the "more ↓" affordance only apply during a run, so selecting an invoice shows the top of the document instead of jumping to the bottom. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 798e540 commit 5cf9398

3 files changed

Lines changed: 70 additions & 47 deletions

File tree

components/dashboard.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ export function Dashboard({ queue }: { queue: QueueItem[] }) {
8888
useEffect(() => {
8989
const el = traceScrollRef.current;
9090
if (!el) return;
91+
// In the idle PDF preview there's no trace to follow — leave the scroll at the
92+
// top (show the top of the document), and don't flag "more".
93+
if (state.status === "idle") {
94+
el.scrollTop = 0;
95+
setTraceMore(false);
96+
autoFollowRef.current = true; // re-arm for the next run
97+
return;
98+
}
9199
// A fresh run (trace just reset) re-arms auto-follow, even if the user had
92100
// scrolled up during the previous run.
93101
if (state.trace.length <= 1) autoFollowRef.current = true;
@@ -355,7 +363,9 @@ export function Dashboard({ queue }: { queue: QueueItem[] }) {
355363
/>
356364
)}
357365
</div>
358-
{traceMore && (
366+
{/* "more ↓" is a trace affordance — only meaningful once a run is
367+
underway, never on the static PDF preview. */}
368+
{traceMore && state.status !== "idle" && (
359369
<>
360370
<div
361371
aria-hidden

components/extraction-reveal.tsx

Lines changed: 51 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -71,18 +71,33 @@ export function ExtractionReveal({
7171
// with the Extracted panel. (A small reflow at Run is fine; a preview that looks
7272
// like it's mid-extraction is not.)
7373
const running = mode === "running";
74+
75+
// Preview: the document alone, centered and a comfortable size (wider than the
76+
// run split's column so it isn't lost in whitespace). Width-driven so it never
77+
// overflows the pane; the card border/shadow stay visible around it.
78+
if (mode === "preview") {
79+
return (
80+
<div
81+
data-testid="extraction-reveal"
82+
data-status="preview"
83+
// `border` (not an inset ring) so the frame sits OUTSIDE the canvas and
84+
// isn't painted over by it. No padding — the page meets the frame cleanly.
85+
className="mx-auto w-full max-w-[680px] overflow-hidden rounded-lg border border-line bg-white shadow-card"
86+
>
87+
<PdfDocument src={pdfSrc} dim={false} />
88+
</div>
89+
);
90+
}
91+
7492
return (
7593
<div
7694
data-testid="extraction-reveal"
7795
data-status={mode}
78-
className={
79-
mode === "preview"
80-
? "mx-auto max-w-[440px]"
81-
: "grid grid-cols-1 gap-3 sm:grid-cols-[1.1fr_1fr]"
82-
}
96+
className="grid grid-cols-1 gap-3 sm:grid-cols-[1.1fr_1fr]"
8397
>
84-
{/* The real PDF the model reads */}
85-
<div className="relative overflow-hidden rounded-lg bg-white shadow-card ring-1 ring-inset ring-line">
98+
{/* The real PDF the model reads. `border` (not inset ring) so the canvas
99+
doesn't paint over the frame. */}
100+
<div className="relative overflow-hidden rounded-lg border border-line bg-white shadow-card">
86101
{/* scan sweep only while the model is actually reading */}
87102
{running && (
88103
<div
@@ -93,40 +108,37 @@ export function ExtractionReveal({
93108
<PdfDocument src={pdfSrc} dim={running} />
94109
</div>
95110

96-
{/* Extracted structure — only once a run is underway (absent in preview, so
97-
the preview is just the document, not a mid-extraction-looking state). */}
98-
{mode !== "preview" && (
99-
<div className="rounded-lg bg-surface p-3 shadow-card ring-1 ring-inset ring-line">
100-
<div className="mb-2 flex items-center justify-between">
101-
<span className="text-[11px] font-medium uppercase tracking-wide text-muted">
102-
Extracted
111+
{/* Extracted structure (the run share-the-row panel). */}
112+
<div className="rounded-lg bg-surface p-3 shadow-card ring-1 ring-inset ring-line">
113+
<div className="mb-2 flex items-center justify-between">
114+
<span className="text-[11px] font-medium uppercase tracking-wide text-muted">
115+
Extracted
116+
</span>
117+
{done && state?.matches != null && (
118+
<span
119+
className={`rounded-full px-2 py-0.5 text-[10px] font-medium ${
120+
state.matches
121+
? "bg-ok-soft text-ok ring-1 ring-inset ring-ok-line"
122+
: "bg-warn-soft text-warn ring-1 ring-inset ring-warn-line"
123+
}`}
124+
>
125+
{state.matches
126+
? "reconciled with PO record"
127+
: "differs from record"}
103128
</span>
104-
{done && state?.matches != null && (
105-
<span
106-
className={`rounded-full px-2 py-0.5 text-[10px] font-medium ${
107-
state.matches
108-
? "bg-ok-soft text-ok ring-1 ring-inset ring-ok-line"
109-
: "bg-warn-soft text-warn ring-1 ring-inset ring-warn-line"
110-
}`}
111-
>
112-
{state.matches
113-
? "reconciled with PO record"
114-
: "differs from record"}
115-
</span>
116-
)}
117-
</div>
118-
<dl className="space-y-1.5">
119-
{rows.map((label, i) => (
120-
<FieldRow
121-
key={label}
122-
label={label}
123-
value={fields[i]?.value ?? ""}
124-
state={!done ? "reading" : i < revealed ? "shown" : "pending"}
125-
/>
126-
))}
127-
</dl>
129+
)}
128130
</div>
129-
)}
131+
<dl className="space-y-1.5">
132+
{rows.map((label, i) => (
133+
<FieldRow
134+
key={label}
135+
label={label}
136+
value={fields[i]?.value ?? ""}
137+
state={!done ? "reading" : i < revealed ? "shown" : "pending"}
138+
/>
139+
))}
140+
</dl>
141+
</div>
130142
</div>
131143
);
132144
}

components/pdf-document.tsx

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ import type { PDFPageProxy } from "pdfjs-dist";
88
* canvas with pdf.js — the document shown in the right pane's extraction reveal.
99
* An actual PDF, not an HTML mock.
1010
*
11-
* pdf.js is loaded dynamically (client-only) and its worker is resolved through
12-
* the bundler via `new URL(..., import.meta.url)`, so there's no CDN dependency.
13-
* Until the page paints we show an A4-ratio skeleton (not a text spinner) so the
14-
* layout doesn't jump. Any failure falls back to a short message.
11+
* The canvas fills its parent's width and keeps A4 ratio; size the document by
12+
* constraining the parent's width. pdf.js is loaded dynamically (client-only) and
13+
* its worker is resolved through the bundler via `new URL(..., import.meta.url)`,
14+
* so there's no CDN dependency. Until the page paints we show an A4-ratio skeleton
15+
* (not a text spinner) so the layout doesn't jump. Failure falls back to a note.
1516
*/
1617

17-
// A4 portrait aspect ratio (height / width) — used to size the skeleton so it
18-
// matches the rendered page and the layout never shifts.
18+
// A4 portrait aspect ratio (height / width) — sizes the skeleton to match the
19+
// rendered page so the layout never shifts.
1920
const A4_RATIO = 841.89 / 595.28;
2021

2122
export function PdfDocument({ src, dim }: { src: string; dim: boolean }) {
@@ -110,7 +111,7 @@ export function PdfDocument({ src, dim }: { src: string; dim: boolean }) {
110111
)}
111112
<canvas
112113
ref={canvasRef}
113-
className={`absolute inset-0 block h-full w-full rounded ring-1 ring-inset ring-line transition-opacity duration-300 ${
114+
className={`absolute inset-0 block h-full w-full transition-opacity duration-300 ${
114115
ready ? (dim ? "opacity-80" : "opacity-100") : "opacity-0"
115116
}`}
116117
/>

0 commit comments

Comments
 (0)