Skip to content
Merged
21 changes: 11 additions & 10 deletions docs/branch-review-ledger.md

Large diffs are not rendered by default.

61 changes: 45 additions & 16 deletions src/components/DocumentViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ const secondaryButton = floatingControl;
*
* @param documentId - The identifier of the document to load.
* @param initialPage - The page to display initially in the source preview.
* @param chunkId - An optional indexed passage to pin and scroll into view.
* @param chunkId - An optional indexed passage to pin as a cited excerpt above the PDF.
* @returns The document viewer interface.
*/
export function DocumentViewer({
Expand Down Expand Up @@ -164,6 +164,18 @@ export function DocumentViewer({
const [mobileActionsOpen, setMobileActionsOpen] = useState(false);
const [sectionSheetOpen, setSectionSheetOpen] = useState(false);
const [compactView, setCompactView] = useDocumentViewDensity();
// Explicit inspect intent keyed to the current document+citation. A stale
// reveal from a prior deep-link must not survive into the next landing.
const citationLandingKey = `${documentId}::${activeChunkId ?? ""}`;
const [inspectRevealKey, setInspectRevealKey] = useState<string | null>(null);
const [prevCitationLandingKey, setPrevCitationLandingKey] = useState(citationLandingKey);
if (citationLandingKey !== prevCitationLandingKey) {
setPrevCitationLandingKey(citationLandingKey);
// Chunk/document identity changed: drop any prior inspect latch so a
// revisit to the same citation does not auto-reopen the indexed dump.
if (inspectRevealKey !== null) setInspectRevealKey(null);
}
const inspectIndexedText = inspectRevealKey === citationLandingKey;
Comment thread
BigSimmo marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const [composerChromeFocused, setComposerChromeFocused] = useState(false);
const [shellScrollContainer, setShellScrollContainer] = useState<HTMLElement | null>(null);
useEffect(() => {
Expand Down Expand Up @@ -871,10 +883,18 @@ export function DocumentViewer({
}, []);
const jumpToSection = useCallback(
(id: string) => {
// Condensed view keeps IndexedTextPanel React-controlled. A raw
// `details.open = true` from the section jump would lose on the next
// render unless we also raise the inspect reveal for this citation.
// Jumping anywhere else must lower it again — otherwise IndexedTextPanel's
// controlled `open` stays true and the exclusive accordion group (native
// `name="document-viewer-section"`) closes the section this jump targets
// right back on the next render.
setInspectRevealKey(id === "source-text" ? `${documentId}::${activeChunkId ?? ""}` : null);
selectSection(id);
jumpToDocumentSection(id);
},
[selectSection],
[activeChunkId, documentId, selectSection],
);
const generatedSummaryText = summary ? cleanClinicalSummaryText(summary.answer) : "";
const generatedAnswerIsSummary = summaryQuery === documentSummaryQuestion;
Expand All @@ -892,10 +912,19 @@ export function DocumentViewer({
: [];
useEffect(() => {
if (!activeChunkId || loadingDocument) return;
window.document
.querySelector<HTMLElement>(`[data-source-chunk-id="${CSS.escape(activeChunkId)}"]`)
?.scrollIntoView({ block: "center", behavior: resolveScrollBehavior() });
}, [activeChunkId, loadingDocument, chunks.length]);
// Citation landing: keep the PDF as the first reading surface. Do not dump
// the viewport into the indexed source-passages list.
globalThis.document.getElementById("pdf-preview-section")?.scrollIntoView({
block: "start",
behavior: resolveScrollBehavior(),
});
}, [activeChunkId, loadingDocument]);
const inspectIndexedTextSection = useCallback(() => {
setInspectRevealKey(`${documentId}::${activeChunkId ?? ""}`);
window.requestAnimationFrame(() => {
jumpToDocumentSection("source-text");
});
}, [activeChunkId, documentId]);
const retryPreview = () => {
setViewerError(null);
setPreviewError(null);
Expand Down Expand Up @@ -944,6 +973,7 @@ export function DocumentViewer({
};
const submitSourceSearch = () => {
if (normalizedSourceSearch.length < 2) return;
setInspectRevealKey(`${documentId}::${activeChunkId ?? ""}`);
globalThis.document.getElementById("source-text")?.scrollIntoView({
block: "start",
behavior: resolveScrollBehavior(),
Expand Down Expand Up @@ -1410,15 +1440,14 @@ export function DocumentViewer({
</div>
</div>

<div className="grid gap-4 sm:gap-5 md:grid-cols-2 md:items-start lg:block">
<div className="lg:hidden">
<PinnedSourceEvidence
loading={effectiveLoadingDocument}
chunk={selectedChunk}
compact
sectionId="source-evidence"
/>
</div>
<div className="grid gap-4 sm:gap-5">
<PinnedSourceEvidence
loading={effectiveLoadingDocument}
chunk={selectedChunk}
compact
sectionId="source-evidence"
onInspectIndexedText={inspectIndexedTextSection}
/>
<IndexedTextPanel
loading={effectiveLoadingDocument}
selectedPage={selectedPage}
Expand All @@ -1432,6 +1461,7 @@ export function DocumentViewer({
selectedChunkId={activeChunkId}
onSearchChange={setSourceSearch}
compact={compactView}
revealRequest={inspectIndexedText || normalizedSourceSearch.length >= 2}
/>
</div>
</div>
Expand All @@ -1445,7 +1475,6 @@ export function DocumentViewer({
onCompactChange={setCompactView}
indexWarnings={indexWarnings}
effectiveLoadingDocument={effectiveLoadingDocument}
selectedChunk={selectedChunk}
document={document}
summaryBadges={summaryBadges}
formattedStoredSummary={formattedStoredSummary}
Expand Down
79 changes: 68 additions & 11 deletions src/components/document-viewer/document-overview-landing.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Overview landing for the document viewer: the compact header, quick actions,
// and high-yield clinical summary. Extracted from DocumentViewer.tsx (maturity
// X3) as a pure move.
import { Download, Loader2, Sparkles, Target } from "lucide-react";
import { Download, Loader2, MoreHorizontal, Sparkles, Target } from "lucide-react";
import { documentDisplayTitle, documentOrganizationProfile } from "@/components/DocumentOrganizationBadges";
import { DocumentClinicalSummary } from "@/components/document-viewer/document-clinical-summary";
import { formatDocumentLabelDisplay } from "@/lib/document-tags";
Expand Down Expand Up @@ -69,18 +69,18 @@ export function DocumentOverviewLanding({

return (
<section className="grid gap-4 lg:grid-cols-3 lg:items-start">
<article className={cn(panel, "p-4 sm:p-5 lg:col-span-3")}>
<div className="grid grid-cols-[auto_minmax(0,1fr)] gap-4">
<article className={cn(panel, "p-3 sm:p-5 lg:col-span-3")}>
<div className="grid grid-cols-[auto_minmax(0,1fr)] gap-3 sm:gap-4">
<DocumentFileTile
kind={documentType}
tone={documentTileTone(documentType)}
className="h-20 w-20 rounded-xl text-sm sm:h-24 sm:w-24"
className="h-14 w-14 rounded-xl text-sm sm:h-24 sm:w-24"
/>
<div className="min-w-0">
<p className="text-xs font-semibold uppercase tracking-label text-[color:var(--text-muted)]">
{documentTypeEyebrow(document)}
</p>
<h2 className="line-clamp-2 text-xl font-semibold leading-7 text-[color:var(--text-heading)]">
<h2 className="line-clamp-2 text-lg font-semibold leading-6 text-[color:var(--text-heading)] sm:text-xl sm:leading-7">
{documentDisplayTitle(document)}
</h2>
<DocumentMetaRow
Expand All @@ -90,20 +90,77 @@ export function DocumentOverviewLanding({
{/* Search relevance badges are rendered in document search results; the viewer has no ranking context. */}
</div>
</div>
<div className="mt-4 grid grid-cols-2 gap-2 sm:grid-cols-4">
{/* Phone: primary reading actions only. Download / scope stay behind More
so the first viewport reaches the PDF and clinical summary faster. */}
<div className="mt-3 grid grid-cols-2 gap-2 sm:hidden">
{signedUrl ? (
<DocumentActionAnchor
href={signedUrl}
target="_blank"
rel="noreferrer"
className={cn(primaryButton, "w-full min-h-12 px-2 text-xs sm:text-sm")}
className={cn(primaryButton, "w-full min-h-12 px-2 text-xs")}
>
Open PDF
</DocumentActionAnchor>
) : (
<DocumentActionAnchor
href="#pdf-preview-section"
className={cn(primaryButton, "w-full min-h-12 px-2 text-xs sm:text-sm")}
className={cn(primaryButton, "w-full min-h-12 px-2 text-xs")}
>
Open preview
</DocumentActionAnchor>
)}
<DocumentActionButton
onClick={onAskFromDocument}
disabled={!canSummarizeDocument}
icon={Sparkles}
className={cn(secondaryButton, "w-full min-h-12 whitespace-nowrap px-2 text-xs")}
>
Answer from this
</DocumentActionButton>
</div>
<details className="group mt-2 sm:hidden" data-testid="document-overview-more-actions">
<summary
className={cn(
secondaryButton,
"w-full min-h-12 cursor-pointer list-none justify-center gap-2 px-2 text-xs [&::-webkit-details-marker]:hidden",
)}
>
<MoreHorizontal aria-hidden="true" className="h-4 w-4" />
More actions
</summary>
<div className="mt-2 grid grid-cols-2 gap-2">
<DocumentActionButton
onClick={onDownload}
disabled={downloading}
icon={downloading ? Loader2 : Download}
className={cn(secondaryButton, "w-full min-h-12 px-2 text-xs")}
>
{downloading ? "Preparing" : "Download"}
</DocumentActionButton>
<DocumentActionButton
onClick={onAddToScope}
icon={Target}
className={cn(secondaryButton, "w-full min-h-12 px-2 text-xs")}
>
Add to scope
</DocumentActionButton>
</div>
</details>
<div className="mt-4 hidden grid-cols-2 gap-2 sm:grid sm:grid-cols-4">
{signedUrl ? (
<DocumentActionAnchor
href={signedUrl}
target="_blank"
rel="noreferrer"
className={cn(primaryButton, "w-full min-h-12 px-2 text-sm")}
>
Open PDF
</DocumentActionAnchor>
) : (
<DocumentActionAnchor
href="#pdf-preview-section"
className={cn(primaryButton, "w-full min-h-12 px-2 text-sm")}
>
Open preview
</DocumentActionAnchor>
Expand All @@ -112,22 +169,22 @@ export function DocumentOverviewLanding({
onClick={onDownload}
disabled={downloading}
icon={downloading ? Loader2 : Download}
className={cn(secondaryButton, "w-full min-h-12 px-2 text-xs sm:text-sm")}
className={cn(secondaryButton, "w-full min-h-12 px-2 text-sm")}
>
{downloading ? "Preparing" : "Download"}
</DocumentActionButton>
<DocumentActionButton
onClick={onAddToScope}
icon={Target}
className={cn(secondaryButton, "w-full min-h-12 px-2 text-xs sm:text-sm")}
className={cn(secondaryButton, "w-full min-h-12 px-2 text-sm")}
>
Add to scope
</DocumentActionButton>
<DocumentActionButton
onClick={onAskFromDocument}
disabled={!canSummarizeDocument}
icon={Sparkles}
className={cn(secondaryButton, "w-full min-h-12 whitespace-nowrap px-2 text-xs sm:text-sm")}
className={cn(secondaryButton, "w-full min-h-12 whitespace-nowrap px-2 text-sm")}
>
Answer from this
</DocumentActionButton>
Expand Down
14 changes: 1 addition & 13 deletions src/components/document-viewer/document-rail-panels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@ import {
DocumentImage,
DocumentSectionSummary,
FormattedHighYieldSummary,
PinnedSourceEvidence,
TableReviewPanel,
} from "@/components/document-viewer/source-panels";
import { DocumentImageFilmstrip } from "@/components/document-viewer/document-image-filmstrip";
import type { ChunkRow, DocumentIndexHealth, ImageRow, TableFactRow } from "@/components/document-viewer/types";
import type { DocumentIndexHealth, ImageRow, TableFactRow } from "@/components/document-viewer/types";
import type { DocumentSection } from "@/components/document-viewer/section-index";
import { BadgeCluster } from "@/components/clinical-dashboard/clinical-badge";
import {
Expand Down Expand Up @@ -43,7 +42,6 @@ export function DocumentViewerRail({
onCompactChange,
indexWarnings,
effectiveLoadingDocument,
selectedChunk,
document,
summaryBadges,
formattedStoredSummary,
Expand All @@ -70,7 +68,6 @@ export function DocumentViewerRail({
onCompactChange: (compact: boolean) => void;
indexWarnings: string[];
effectiveLoadingDocument: boolean;
selectedChunk: ChunkRow | undefined;
document: ClinicalDocument | null;
summaryBadges: DocumentSummaryBadge[];
formattedStoredSummary: FormattedDocumentSummary;
Expand Down Expand Up @@ -121,15 +118,6 @@ export function DocumentViewerRail({
</InlineNotice>
) : null}

<div className="hidden lg:block">
<PinnedSourceEvidence
loading={effectiveLoadingDocument}
chunk={selectedChunk}
compact
sectionId="source-evidence-rail"
/>
</div>

{document ? (
<details
id="source-summary"
Expand Down
2 changes: 1 addition & 1 deletion src/components/document-viewer/section-index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export function buildDocumentSectionIndex(input: DocumentSectionIndexInput): Doc
},
{
id: "source-evidence",
label: "Pinned evidence",
label: "Cited excerpt",
icon: Quote,
detail: input.pinnedPage ? `Page ${input.pinnedPage}` : "No passage pinned",
collapsible: false,
Expand Down
Loading
Loading