diff --git a/src/components/AccessibleTable.tsx b/src/components/AccessibleTable.tsx index b9e1a44fe..3afe28f49 100644 --- a/src/components/AccessibleTable.tsx +++ b/src/components/AccessibleTable.tsx @@ -288,6 +288,29 @@ function useMobileTableExpansion(enabledByDefault: boolean) { return enabledByDefault && isMobile; } +// Mirrors the component's `normalized` computation so callers can decide layout +// (e.g. whether to collapse a source image) using the exact same parse/normalize +// rules AccessibleTable renders with. Returns false when the table would render +// nothing — columns-only input, unparseable markdown, or an all-metadata grid. +export function hasRenderableAccessibleTable({ + markdown, + rows, + columns, + clinicalOnly = false, +}: { + markdown?: string | null; + rows?: string[][] | null; + columns?: string[] | null; + clinicalOnly?: boolean; +}): boolean { + const hasExplicitRows = Boolean(rows?.length); + const parsed = hasExplicitRows ? rows : parseMarkdownTable(markdown); + if (!parsed?.length) return false; + const table = normalizeAccessibleTable(parsed, hasExplicitRows ? columns : null); + if (!table) return false; + return Boolean(clinicalOnly ? clinicalOnlyTable(table) : table); +} + export function AccessibleTable({ caption, markdown, diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx index 28a40eafd..7c46aa2ad 100644 --- a/src/components/DocumentViewer.tsx +++ b/src/components/DocumentViewer.tsx @@ -17,7 +17,6 @@ import { FileText, Loader2, Maximize2, - Menu, Minimize2, Minus, Plus, @@ -33,8 +32,9 @@ import { X, } from "lucide-react"; import { type FormEvent, useEffect, useMemo, useRef, useState } from "react"; -import { AccessibleTable } from "@/components/AccessibleTable"; -import { documentDisplayTitle } from "@/components/DocumentOrganizationBadges"; +import { AccessibleTable, hasRenderableAccessibleTable } from "@/components/AccessibleTable"; +import { documentDisplayTitle, documentOrganizationProfile } from "@/components/DocumentOrganizationBadges"; +import { formatDocumentLabelDisplay } from "@/lib/document-tags"; import { DocumentActionAnchor, DocumentActionButton, @@ -437,7 +437,15 @@ function DocumentImage({ image }: { image: ImageRow }) { : looksLikeTableText(image.tableTextSnippet) ? image.tableTextSnippet : null; - const hasStructuredTable = Boolean(tableMarkdown || image.tableRows?.length || image.tableColumns?.length); + // Only let the table "lead" (collapsing the source image) when AccessibleTable + // will actually render a table. Columns-only input or unparseable markdown + // render nothing, so those route to the image-first branch instead of leaving + // an empty caption above a hidden source image. + const hasStructuredTable = hasRenderableAccessibleTable({ + markdown: tableMarkdown, + rows: image.tableRows, + columns: image.tableColumns, + }); const tableCaption = tableHeading || cleanCaption || "Document table"; const showImageCaptionLine = cleanCaption && cleanCaption !== tableCaption; const displayLabels = smartEvidenceTags( @@ -447,6 +455,65 @@ function DocumentImage({ image }: { image: ImageRow }) { .join(" "), ); + const clinicalUseReasonLine = + image.clinicalUseClass && image.clinicalUseClass !== "clinical_evidence" && image.clinicalUseReason ? ( +

{image.clinicalUseReason}

+ ) : null; + + // The image preview and its extracted table are the same content; when a + // structured table is available it leads and the raw image collapses behind a + // disclosure so the card is not doubled in height. + const imageBlock = ( +
+ {failed ? ( +
+
+ + Image preview failed. + +
+
+ ) : ( + // Fixed-aspect frame: placeholder and image share one reserved box so + // the loaded image never resizes the layout (no content shift on load). +
+ {url ? ( + {cleanCaption setLoaded(true)} + onError={handleImageError} + className={cn( + "absolute inset-0 h-full w-full rounded-lg object-contain transition-opacity duration-300 motion-reduce:transition-none", + loaded ? "opacity-100" : "opacity-0", + )} + /> + ) : null} + {!url || !loaded ? ( +
+ {shouldLoad ? ( + <> + + Loading image + + ) : ( + "Image preview will load when visible" + )} +
+ ) : null} +
+ )} +
+ ); + return (

@@ -457,73 +524,44 @@ function DocumentImage({ image }: { image: ImageRow }) { ? ` · ${image.clinicalUseClass.replaceAll("_", " ")}` : ""}

-
- {failed ? ( -
-
- - Image preview failed. - -
-
- ) : ( - // Fixed-aspect frame: placeholder and image share one reserved box so - // the loaded image never resizes the layout (no content shift on load). -
- {url ? ( - {cleanCaption setLoaded(true)} - onError={handleImageError} - className={cn( - "absolute inset-0 h-full w-full rounded-lg object-contain transition-opacity duration-300 motion-reduce:transition-none", - loaded ? "opacity-100" : "opacity-0", - )} - /> - ) : null} - {!url || !loaded ? ( -
- {shouldLoad ? ( - <> - - Loading image - - ) : ( - "Image preview will load when visible" - )} -
+ {hasStructuredTable ? ( + <> +
+ {tableHeading ?

{tableHeading}

: null} + {showImageCaptionLine ?

{cleanCaption}

: null} + + {clinicalUseReasonLine} +
+
+ + + Show source image + +
{imageBlock}
+
+ + ) : ( + <> +
{imageBlock}
+
+ {tableHeading ?

{tableHeading}

: null} + {showImageCaptionLine ?

{cleanCaption}

: null} + {image.tableTextSnippet ? ( +

{image.tableTextSnippet}

) : null} -
- )} -
-
- {tableHeading ?

{tableHeading}

: null} - {showImageCaptionLine ?

{cleanCaption}

: null} - - {!hasStructuredTable && image.tableTextSnippet ? ( -

{image.tableTextSnippet}

- ) : null} - {image.clinicalUseClass && image.clinicalUseClass !== "clinical_evidence" && image.clinicalUseReason ? ( -

{image.clinicalUseReason}

- ) : null} -
+ {clinicalUseReasonLine} + + + )} {displayLabels.length ? (
{displayLabels.map((label) => ( @@ -670,26 +708,14 @@ function PinnedSourceEvidence({ : ""; if (!loading && !chunk) { + // Direct visits (not arrived via a cited answer passage) have nothing to + // pin, so keep this a slim, muted note rather than a prominent empty card. return ( -
-
- - - -
-

Source evidence

-

- Open a cited answer passage to pin the exact indexed excerpt here. -

-
-
+
+

+ + Open a cited answer passage to pin its exact indexed excerpt here. +

); } @@ -1315,49 +1341,51 @@ function PdfCanvasViewer({ url, title, initialPage }: { url: string; title: stri >
- - {pagesReady ? ( - - ) : ( -
- - {error ? "Page unavailable" : "Loading pages"} - {error ? "Unavailable" : "Loading"} -
- )} - -
+
+ + {pagesReady ? ( + + ) : ( +
+ + {error ? "Page unavailable" : "Loading pages"} + {error ? "Unavailable" : "Loading"} +
+ )} + +
+
-

{headerTitle}

{readyDocument ? ( @@ -2626,7 +2616,7 @@ export function DocumentViewer({ className={cn(secondaryButton, "min-h-12 justify-start text-xs")} > - Scope + Add to scope
@@ -2767,23 +2757,22 @@ export function DocumentViewer({
) : signedUrl && document?.file_type === "application/pdf" ? ( <> -
-

- Browser PDF mode keeps heavy-zoom pages crisp and is recommended when zoom quality looks soft. -

+
{useNativePdfViewer ? ( @@ -2866,7 +2855,7 @@ export function DocumentViewer({
-