Skip to content

Auto-itemize page: PDF preview viewport behavior + LLM document-level metadata extraction #1581

@steilerDev

Description

@steilerDev

Bug: Auto-itemize page — PDF preview viewport behavior + LLM document-level metadata extraction

Parent: #1547
Priority: Should Have
Type: Bug (two related defects)

The auto-itemize page (/budget/invoices/:id/auto-itemize/:documentId) has shipped in beta but exhibits two related defects that this bug bundles together because they touch the same page and are best tested as a single regression sweep.

Defect A — PDF preview viewport behavior

  • The user wants the PDF preview to span the full height of the viewport.
  • The PDF must remain visible while scrolling through the line-items list — truly anchored, not scrolling away.

Current state: .previewColumn is position: sticky and .pdfIframe has height: calc(100vh - header - spacing-12). Static analysis suggests this should work, but the user is reporting it doesn't behave as expected. Either the layout is mis-implemented or stronger behavior is required (literal viewport-fill with position: fixed rather than sticky bounded by content).

Defect B — LLM doesn't extract document-level metadata that we expect

The system prompt at server/src/services/budgetExtraction/prompts.ts already asks the LLM to extract invoiceDate and dueDate at the top level. However, the strict JSON-schema enforcement used for Anthropic at server/src/services/budgetExtraction/providerProfiles.ts:44-83 (EXTRACTED_LINES_SCHEMA) does not include invoiceDate and dueDate at the top level. With strict: true + additionalProperties: false, Anthropic's structured-output mode REJECTS any fields not in the schema. Result: the LLM is forbidden from outputting the very fields the prompt asks for.

Additionally, the user wants the LLM to extract two more document-level fields:

  • invoiceNumber (string | null) — the vendor's invoice identifier (e.g., "INV-2024-0123")
  • notes (string | null) — a very short description summarizing the invoice in plain language (e.g., "Bathroom tile installation, March 2024"). Keep brief — 1 sentence, ≤120 chars.

All four LLM-extracted document-level fields should appear as SuggestionBadges on the metadata form whenever the LLM's value differs from the stored value.

Also stale: EXTRACTED_LINES_SCHEMA still has vatRate in the required array — this was supposed to be removed when VAT was locked at 19% in #1576. Leftover.

Acceptance Criteria

Group A — PDF preview viewport behavior (Defect A)

  • 1. Given the auto-itemize page is open in a desktop viewport (≥ 860 px), when the page renders, then the PDF iframe fills the available viewport height between the page header and the bottom of the viewport (effectively 100vh - header_height).
  • 2. Given the page is open, when the user scrolls through the line-item list in the form column, then the PDF preview remains fully visible and anchored on the screen — no part of it scrolls off-screen.
  • 3. Given the PDF document is multi-page or long, when the user is viewing the PDF, then the browser's native PDF viewer controls (scroll within the PDF, page nav) work independently from the page-level scroll.
  • 4. Given a mobile viewport (< 860 px), when the page renders, then the layout remains stacked (form first, PDF below) per the existing @media (max-width: 860px) rules. The full-height/sticky behavior is desktop-only.
  • 5. Given the user resizes the viewport across the breakpoint, when the layout changes, then the desktop sticky/full-height behavior engages or disengages without visual glitches.

Group B — LLM schema includes all four document-level fields (Defect B)

  • 6. Given the LLM is called for a dry-run, when the response is parsed, then all four document-level fields are accepted by the validator: invoiceDate, dueDate, invoiceNumber, notes.
  • 7. Given Anthropic's strict-mode JSON schema is in use, when the request is constructed, then the EXTRACTED_LINES_SCHEMA declares the four document-level fields (each string | null) at the top level under properties AND lists them in required (strict mode requires all properties to be in required — nullability is the optional signal).
  • 8. Given the user prompt and system prompt, when the LLM is invoked, then the prompts ask for all four fields (currently they ask for invoiceDate/dueDate only — extend to also ask for invoiceNumber and a short notes description).
  • 9. Given the dry-run response, when the frontend receives it, then all four extracted values are surfaced to the page state.
  • 10. Given any of the four extracted values differ from the stored invoice value, when the page renders the corresponding metadata input, then a SuggestionBadge appears with the LLM's suggested value and an Apply button. Same conditional render logic as the existing extractedInvoiceDate/extractedDueDate flow.
  • 11. Given the notes field is suggested, when the user applies it, then the existing notes field accepts the value (multi-line/textarea behavior preserved).
  • 12. Given the stale vatRate field in EXTRACTED_LINES_SCHEMA.required, when the schema is rebuilt for this story, then vatRate is removed from required AND removed from properties (the LLM should NOT output a vatRate per the existing system prompt at line 29 of prompts.ts).

Group C — Backward compatibility

  • 13. Given the existing ExtractionResult type, when the new fields are added, then the type gains invoiceNumber?: string and notes?: string alongside the existing invoiceDate? / dueDate?. All optional.
  • 14. Given a provider returns the OLD response shape (no top-level invoiceNumber or notes), when validation runs, then the validator continues to work (those fields are simply undefined in the result). No regression.
  • 15. Given the AutoItemizeDryRunResponse shape, when the dry-run is committed, then the response includes extractedInvoiceNumber? and extractedNotes? alongside the existing extractedInvoiceDate? / extractedDueDate?. Frontend treats all four uniformly.

Out of scope

  • Changing the existing LLM provider abstraction or supporting new providers.
  • Allowing the user to edit which fields are extracted.
  • Adding vendor-name extraction (already a per-line field; not promoted to document-level here).
  • PDF.js integration (browser-native PDF viewer remains the renderer).

Files involved

  • server/src/services/budgetExtraction/prompts.ts — extend system prompt + user prompt with invoiceNumber and notes
  • server/src/services/budgetExtraction/providerProfiles.ts — fix EXTRACTED_LINES_SCHEMA (add top-level fields; remove stale vatRate)
  • server/src/services/budgetExtraction/openAICompatibleProvider.ts — extend validateExtractedLines to capture invoiceNumber and notes
  • shared/src/types/budgetExtraction.ts — extend ExtractionResult with optional invoiceNumber and notes
  • shared/src/types/invoiceAutoItemize.ts — extend AutoItemizeDryRunResponse with extractedInvoiceNumber? and extractedNotes?
  • server/src/services/invoiceAutoItemizeService.ts — propagate new fields to the response
  • client/src/pages/AutoItemizePage/AutoItemizePage.tsx — wire SuggestionBadge for invoiceNumber and notes; also: layout fix for sticky/full-height PDF
  • client/src/pages/AutoItemizePage/AutoItemizePage.module.css — layout adjustments for full-viewport PDF + sticky

Notes

  • Two related defects bundled into a single bug because they share the same page and warrant a single regression sweep.
  • Defect B has a known root cause (strict JSON schema mismatch with prompt) — implementation should verify the fix end-to-end with the Anthropic provider profile, not just unit tests.
  • Stale vatRate cleanup is in-scope (AC EPIC-11: CI/CD Infrastructure with Semantic Release #12) to avoid leaving dead schema state once the schema is being edited anyway.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Status

    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions