diff --git a/docs/process-hardening.md b/docs/process-hardening.md index 4f798c3b87..c7d7bf50c6 100644 --- a/docs/process-hardening.md +++ b/docs/process-hardening.md @@ -21,8 +21,8 @@ This document turns the current process review into phased, durable repo practic ## Phase 3 - Structural cleanup -- [ ] Decompose `src/components/ClinicalDashboard.tsx` into the planned `src/components/clinical-dashboard/` modules. -- Preserve `data-testid`, `aria-label`, and AST-pinned `ClinicalOutputPanel` contracts during the move. +- [x] Decompose `src/components/ClinicalDashboard.tsx` into the planned `src/components/clinical-dashboard/` modules. **DONE** — all 6 approved render-surface modules extracted (~8.8k → ~4.7k lines); see the progress log below. Only the deferred admin surfaces remain in the monolith. +- Preserved `data-testid`, `aria-label`, and AST-pinned `ClinicalOutputPanel`/`NaturalLanguageAnswer` contracts across every move (byte-identical sha1 checksum + retargeted AST guards). - After decomposition, run `npm run verify:cheap`, `npm run verify:ui`, and focused visual/browser checks against the dashboard and document viewer. ### Phase 3 progress (started) @@ -38,13 +38,13 @@ This document turns the current process review into phased, durable repo practic - **2026-07-03:** extracted `visual-evidence.tsx` — `compactClinicalTableCaption`, `visualEvidenceHeader`, `VisualEvidenceStrip`, `InlineTableCard`, `supportDotClass`, `supportLabel`, `claimRowsForEvidencePanel`, `EvidenceClaimsList`, `EvidenceGapsPanel`, `MobileEvidenceSheetContent`, `MobileEvidenceTabPanel`, `UnifiedEvidenceDrawerContent` (contiguous block 547–1259, moved verbatim). Monolith 5953 → 5241 lines. **No runtime cycle** — the only monolith import is `type AnswerFeedbackType` (erased at compile time); the module imports one-way from `evidence-panels`/`answer-content`/siblings, and the monolith imports `InlineTableCard` + `MobileEvidenceSheetContent` back. Added `visual-evidence.tsx` to the `rendered-text-formatting.test.ts` corpus (the `item.tableTextSnippet`/`item.title`/`item.caption` render surfaces moved here). Stripped 22 now-orphaned monolith imports. Done on a **dedicated worktree/branch** `claude/clinical-dashboard-decomp` (off `main`) instead of the shared claude branch, to avoid the squash-ancestry churn. -#### Remaining decomposition — hand-off (do on a stable `main`, one module per commit) +- **2026-07-03:** extracted `document-results.tsx` — `WhyThisMatchedPanel`, `RelatedDocumentsPanel`, `StagedAnswerResultSurface` (contiguous block 456–944, moved verbatim). Monolith 5216 → 4726 lines. **No runtime cycle** — the only monolith import is `type AnswerFeedbackType` (erased); `StagedAnswerResultSurface` is a leaf result-surface that imports one-way from every sibling module (answer-content, evidence-panels, visual-evidence, relevance, document-search-results, badges, dashboard-shell, display-text, use-mobile-preview-sheet) and the monolith imports `RelatedDocumentsPanel` + `StagedAnswerResultSurface` back. The monolith's `visual-evidence` import (`InlineTableCard`/`MobileEvidenceSheetContent`) moved into `document-results` as predicted. Added `document-results.tsx` to the `rendered-text-formatting.test.ts` corpus; stripped 35 now-orphaned monolith imports. -The approved move map (`docs/redesign/04-deferred.md` §2) has 1 module left: +#### Decomposition COMPLETE (approved move map) -1. `document-results.tsx` — `WhyThisMatchedPanel` (~line 481, before visual-evidence was extracted), `RelatedDocumentsPanel`, `StagedAnswerResultSurface`. These are the answer-results surfaces around the former visual-evidence block; check whether `InlineTableCard`/`MobileEvidenceSheetContent` (now imported from `visual-evidence`) are rendered by `StagedAnswerResultSurface` — if so the monolith's `visual-evidence` import moves into `document-results`. +All approved render-surface modules are extracted. `ClinicalDashboard.tsx` went from ~8.8k → ~4.7k lines and now holds the main `ClinicalDashboard` orchestrator, its data/state hooks, and the deferred admin surfaces only. The 6 extracted modules live in `src/components/clinical-dashboard/`: `auth-panel`, `answer-content`, `evidence-panels`, `output-panel`, `visual-evidence`, `document-results` (+ the shared `use-mobile-preview-sheet` hook and `display-text` helpers). The barrel `index.ts` was intentionally not extended. -For each: trace which module-scope helpers/icons/types it uses; move solely-consumed ones with it, import shared ones; strip newly-orphaned monolith imports (lint flags them); run the per-module gate above; commit immediately. Keep the main `ClinicalDashboard` export in `ClinicalDashboard.tsx` (the barrel/bridge stays). Admin surfaces (`DocumentDrawer`, `SettingsDialog`, `ToolsHub`, `MobileSectionFab`) are out of the approved map — a later pass. +**Out of scope (deferred admin surfaces, optional later pass):** `DocumentDrawer`, `SettingsDialog`, `ToolsHub`, `MobileSectionFab`, `DocumentLabelReviewPanel`/`DocumentTagQualityPanel`/`DocumentIndexRepairPanel` and their label helpers (`tagQualityTone`, `labelTierTone`, `documentLabelTypeOptions`) all remain in `ClinicalDashboard.tsx`. ## Phase 4 - Release maturity diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index 03c49b48e8..a54aed1fc0 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -16,7 +16,6 @@ import { HelpCircle, Heart, Keyboard, - Layers, ListChecks, Loader2, LogOut, @@ -39,7 +38,7 @@ import { Wrench, X, } from "lucide-react"; -import { type CSSProperties, type RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { type CSSProperties, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { DocumentOrganizationBadges, documentDisplayTitle, @@ -53,7 +52,6 @@ import { readLocalProjectIdentity, unsafeLocalProjectMessage } from "@/lib/local import { isLocalNoAuthMode } from "@/lib/env"; import { appBackdrop, - answerSurface, clinicalNotesRow, cn, evidenceRow, @@ -69,7 +67,6 @@ import { SourceProvenance, SourceStatusBadge, sourceCard, - subtleStatusPill, textMuted, toneDanger, toneInfo, @@ -84,7 +81,7 @@ import { AnswerEmptyState, AnswerSkeleton } from "@/components/clinical-dashboar import { AuthPanel } from "@/components/clinical-dashboard/auth-panel"; import { useSidebarCollapsed } from "@/components/clinical-dashboard/use-sidebar-collapsed"; import { useTheme } from "@/components/clinical-dashboard/use-theme"; -import { StatusBadge, StrengthBadge } from "@/components/clinical-dashboard/badges"; +import { StatusBadge } from "@/components/clinical-dashboard/badges"; import { type SidebarIdentity, deriveSidebarIdentity, @@ -103,32 +100,10 @@ import { type IngestionQualityReviewItem, } from "@/components/clinical-dashboard/DocumentManagerPanel"; import { GuideDialog, GuideTrigger, UtilityDrawer } from "@/components/clinical-dashboard/dashboard-shell"; -import { - cleanDisplayTitle, - sanitizeAnswerDisplayText, - sanitizeDisplayText, -} from "@/components/clinical-dashboard/display-text"; -import { - NaturalLanguageAnswer, - ScopeAndGovernanceNotice, - UserQuestionBubble, -} from "@/components/clinical-dashboard/answer-content"; -import { - AnswerSupportSummaryCard, - answerHasCentralTable, - answerSupportPriority, - ClinicalNotesChecklistPanel, - clinicalNotesCount, - clinicalNotesDisplayCountForAnswer, - compactEvidenceSummary, - type EvidenceTabName, - evidenceMapRowsFromRenderModel, - formatQuoteCardsForClipboard, - primaryVisualTable, - SafetyFindingsPanel, -} from "@/components/clinical-dashboard/evidence-panels"; -import { useMobilePreviewSheet } from "@/components/clinical-dashboard/use-mobile-preview-sheet"; -import { InlineTableCard, MobileEvidenceSheetContent } from "@/components/clinical-dashboard/visual-evidence"; +import { sanitizeAnswerDisplayText, sanitizeDisplayText } from "@/components/clinical-dashboard/display-text"; +import { ScopeAndGovernanceNotice } from "@/components/clinical-dashboard/answer-content"; +import { evidenceMapRowsFromRenderModel } from "@/components/clinical-dashboard/evidence-panels"; +import { RelatedDocumentsPanel, StagedAnswerResultSurface } from "@/components/clinical-dashboard/document-results"; import { MasterSearchHeader } from "@/components/clinical-dashboard/master-search-header"; import { emptyStates, errorCopy } from "@/lib/ui-copy"; import { applicationsLauncherItemCount } from "@/components/applications-launcher-page"; @@ -152,12 +127,8 @@ const ApplicationsLauncherWorkspace = dynamic( () => import("@/components/applications-launcher-page").then((m) => m.ApplicationsLauncherWorkspace), { ssr: false }, ); -import { - DocumentSearchResultsPanel, - MatchExplanationChips, - type SearchFacets, -} from "@/components/clinical-dashboard/document-search-results"; -import { isWeakRelevance, QueryCoverageChips, RelevanceBadge } from "@/components/clinical-dashboard/relevance"; +import { DocumentSearchResultsPanel, type SearchFacets } from "@/components/clinical-dashboard/document-search-results"; +import { isWeakRelevance } from "@/components/clinical-dashboard/relevance"; import { answerPayloadIsUsable, isRetryableError, @@ -187,7 +158,7 @@ import { import { rankFormRecords } from "@/lib/forms"; import { rankServiceRecords } from "@/lib/services"; import { useRegistryRecords } from "@/lib/use-registry-records"; -import { buildAnswerRenderModel, type AnswerRenderModel } from "@/lib/answer-render-policy"; +import { buildAnswerRenderModel } from "@/lib/answer-render-policy"; import { frontendSourceGovernanceWarnings, groupSourceGovernanceWarnings, @@ -207,34 +178,22 @@ import { } from "@/lib/document-tags"; import type { ClinicalDocument, - BestSourceRecommendation, DocumentMatch, EvidenceRelevance, ImportBatch, IngestionJob, - QuoteCard, RagAnswer, AnswerSection, - ConflictOrGap, RelatedDocument, - EvidenceSummary, SearchResult, SearchScopeSummary, - VisualEvidenceCard, ClinicalQueryMode, DocumentLabel, DocumentLabelType, } from "@/lib/types"; import type { SearchScopeFilters } from "@/lib/search-scope"; import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer"; -import { - type AnswerEvidenceMapRow, - type AnswerViewMode, - buildAnswerEvidenceMap, - buildClinicalOutputSections, - buildHighYieldClinicalOutputSections, - shouldPollForUpdates, -} from "@/lib/ward-output"; +import { type AnswerViewMode, shouldPollForUpdates } from "@/lib/ward-output"; const navigationHashes = ["#search", "#sources"] as const; const mobileSectionFabMediaQuery = "(max-width: 768px), ((max-width: 1023px) and (hover: none) and (pointer: coarse))"; @@ -453,496 +412,6 @@ function normalizeNavigationHash(hash: string) { return navigationHashes.includes(hash as (typeof navigationHashes)[number]) ? hash : "#search"; } -function WhyThisMatchedPanel({ sources }: { sources: SearchResult[] }) { - const visibleSources = sources.slice(0, 3); - if (visibleSources.length === 0) return null; - - return ( -
- - - - - - - Why this matched - - Match signals, source strength, and term coverage for top passages - - - - - -
- {visibleSources.map((source) => ( -
-
-
-

- {cleanDisplayTitle(source.title)} -

-

- page {source.page_number ?? "n/a"} ·{" "} - chunk {source.chunk_index} -

-
-
- - - -
-
- - {source.index_unit ? ( -

- - {source.index_unit.unit_type.replaceAll("_", " ")}: - {" "} - {source.index_unit.title} -

- ) : null} -
- -
-
- ))} -
-
- ); -} - -function RelatedDocumentsPanel({ - documents, - onScopeDocument, - onTagSearch, -}: { - documents: RelatedDocument[]; - onScopeDocument: (documentId: string) => void; - onTagSearch: (tag: SmartDocumentTag) => void; -}) { - if (documents.length === 0) return null; - - return ( - -
- {documents.map((document) => ( -
-
-
- - {documentDisplayTitle(document)} - - -

- {document.match_reason} · pages {document.best_pages.join(", ") || "n/a"} · {document.image_count}{" "} - images{document.table_count ? ` · ${document.table_count} tables` : ""} -

-
- -
- {document.summary && ( -

- -

- )} - -
- ))} -
-
- ); -} - -function StagedAnswerResultSurface({ - answer, - query, - safeAnswerText, - bestSource, - currentRelevance, - queryMode, - sourceGovernanceWarnings, - sourceSummary, - renderModel, - weakEvidence, - groupedGovernanceWarningCount, - answerViewMode, - answerEvidenceMapRows, - onScopeDocument, - answerGrounded, - sources, - gaps, - searchScope, - demoMode, - safeAnswerSections, - safetyFindings, - copiedAnswer, - pendingFeedback, - onCopyAnswer, - onSubmitFeedback, -}: { - answer: RagAnswer; - query: string; - safeAnswerText: string; - bestSource: BestSourceRecommendation | null; - currentRelevance: EvidenceRelevance | null | undefined; - queryMode: ClinicalQueryMode; - sourceGovernanceWarnings: SourceGovernanceWarning[]; - sourceSummary?: EvidenceSummary; - renderModel: AnswerRenderModel; - weakEvidence: boolean; - groupedGovernanceWarningCount: number; - answerViewMode: AnswerViewMode; - answerEvidenceMapRows: AnswerEvidenceMapRow[]; - onScopeDocument: (documentId: string) => void; - answerGrounded: boolean; - sources: SearchResult[]; - gaps: ConflictOrGap[]; - searchScope: SearchScopeSummary | null; - demoMode: boolean; - safeAnswerSections: Array; - safetyFindings: ReturnType; - copiedAnswer: boolean; - pendingFeedback: AnswerFeedbackType | null; - onCopyAnswer: () => void; - onSubmitFeedback: (feedbackType: AnswerFeedbackType) => void; -}) { - const noteCount = clinicalNotesCount(answer); - const showClinicalNotes = safetyFindings.length > 0 || noteCount > 0; - const clinicalNoteDisplayCount = clinicalNotesDisplayCountForAnswer( - answer, - answerViewMode, - noteCount || safetyFindings.length, - ); - const sourceCount = - renderModel.primarySources.length || - sourceSummary?.total_sources || - sources.length || - answer.sources?.length || - answer.citations.length; - const centralTable = answerHasCentralTable(answer) ? primaryVisualTable(answer) : null; - const showEvidenceDrawer = renderModel.allowedBlocks.some((block) => - ["sourceStatus", "reviewSources", "evidenceMap", "quoteCards", "visualEvidence", "warnings"].includes(block), - ); - const [clinicalNotesOpen, setClinicalNotesOpen] = useState(false); - const [evidenceOpen, setEvidenceOpen] = useState(false); - const [evidenceInitialTab, setEvidenceInitialTab] = useState(null); - const [activeReviewPanel, setActiveReviewPanel] = useState<"clinical" | "evidence" | null>(null); - const [copiedQuotes, setCopiedQuotes] = useState(false); - const clinicalNotesTriggerRef = useRef(null); - const evidenceTriggerRef = useRef(null); - const useReviewSheet = useMobilePreviewSheet(); - const copyQuotesTimerRef = useRef(null); - useEffect(() => { - return () => { - if (copyQuotesTimerRef.current !== null) window.clearTimeout(copyQuotesTimerRef.current); - }; - }, []); - function openClinicalNotes() { - setEvidenceOpen(false); - setEvidenceInitialTab(null); - if (useReviewSheet) { - setActiveReviewPanel(null); - setClinicalNotesOpen(true); - return; - } - setClinicalNotesOpen(false); - setActiveReviewPanel("clinical"); - } - function restoreFocusToTrigger(ref: RefObject) { - window.requestAnimationFrame(() => { - if (ref.current?.isConnected) ref.current.focus({ preventScroll: true }); - }); - } - function closeClinicalNotesReview() { - setClinicalNotesOpen(false); - restoreFocusToTrigger(clinicalNotesTriggerRef); - } - function openEvidence(initialTab: EvidenceTabName | null = null) { - setClinicalNotesOpen(false); - setEvidenceInitialTab(initialTab); - if (useReviewSheet) { - setActiveReviewPanel(null); - setEvidenceOpen(true); - return; - } - setEvidenceOpen(false); - setActiveReviewPanel("evidence"); - } - function closeEvidenceReview() { - setEvidenceOpen(false); - setEvidenceInitialTab(null); - restoreFocusToTrigger(evidenceTriggerRef); - } - function closeDesktopReviewPanel() { - const triggerRef = activeReviewPanel === "clinical" ? clinicalNotesTriggerRef : evidenceTriggerRef; - setActiveReviewPanel(null); - restoreFocusToTrigger(triggerRef); - } - function openTableEvidence() { - setClinicalNotesOpen(false); - openEvidence("Tables"); - } - const copyQuotes = useCallback(async () => { - const quoteText = formatQuoteCardsForClipboard(renderModel.quoteCards); - if (!quoteText) return; - try { - await navigator.clipboard.writeText(quoteText); - setCopiedQuotes(true); - if (copyQuotesTimerRef.current !== null) window.clearTimeout(copyQuotesTimerRef.current); - copyQuotesTimerRef.current = window.setTimeout(() => setCopiedQuotes(false), 1600); - } catch { - setCopiedQuotes(false); - } - }, [renderModel.quoteCards]); - const priority = answerSupportPriority(answer, safeAnswerSections, centralTable, safetyFindings, { - grounded: answerGrounded, - weakEvidence, - }); - const inlineEvidenceSummary = compactEvidenceSummary(answer, sources, sourceSummary, renderModel); - const evidenceTrustLabel = inlineEvidenceSummary.split(" · ")[0] || "Review support"; - const showInlineSupportCard = Boolean(priority || showClinicalNotes || showEvidenceDrawer); - const showLayoutAside = Boolean(activeReviewPanel || centralTable); - - return ( -
-
- - -
-
- - - {showInlineSupportCard ? ( - openEvidence(null)} - /> - ) : null} - - {centralTable && activeReviewPanel ? : null} -
- - {activeReviewPanel ? ( - - ) : centralTable ? ( -
- -
- ) : null} -
- - {showClinicalNotes ? ( - - - - } - titleAccessory={ - - {clinicalNoteDisplayCount} - - } - headerActions={ - bestSource ? ( - - - - ) : null - } - headerClassName="gap-2 p-2.5 sm:p-3" - titleClassName="text-[15px] leading-5" - closeButtonClassName="inline-flex h-8 w-8 items-center justify-center rounded-full text-[color:var(--text-muted)] transition hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]" - contentClassName="max-h-[92dvh] translate-y-0 bg-[color:var(--surface-raised)] motion-safe:animate-none sm:h-auto sm:max-h-[88dvh] sm:max-w-lg" - contentStyle={{ height: "80dvh" }} - bodyClassName="flex flex-col bg-[color:var(--surface-raised)] px-3 pb-0 pt-2 sm:p-3" - returnFocusRef={clinicalNotesTriggerRef} - portal - > - - - ) : null} - - {showEvidenceDrawer ? ( - {evidenceTrustLabel} - } - closeLabel="Close evidence" - headerLeading={ - - - - } - contentClassName="max-h-[92dvh] translate-y-0 bg-[color:var(--surface-raised)] motion-safe:animate-none sm:h-auto sm:max-h-[88dvh] sm:max-w-lg" - contentStyle={{ height: "80dvh" }} - bodyClassName="bg-[color:var(--surface-raised)] px-3 pb-0 pt-2 sm:p-3" - returnFocusRef={evidenceTriggerRef} - portal - > - - - ) : null} -
- - -
- ); -} - const tagQualityTone: Record = { noisy: toneDanger, duplicate: toneWarning, diff --git a/src/components/clinical-dashboard/document-results.tsx b/src/components/clinical-dashboard/document-results.tsx new file mode 100644 index 0000000000..e3863a08e2 --- /dev/null +++ b/src/components/clinical-dashboard/document-results.tsx @@ -0,0 +1,550 @@ +"use client"; + +import Link from "next/link"; +import { type RefObject, useCallback, useEffect, useRef, useState } from "react"; +import { BookOpen, ChevronDown, ClipboardCheck, ExternalLink, Layers, Search, X } from "lucide-react"; + +import { DocumentOrganizationBadges, documentDisplayTitle } from "@/components/DocumentOrganizationBadges"; +import { DocumentTagCloud } from "@/components/DocumentTagCloud"; +import { SafeBoldText } from "@/components/SafeBoldText"; +import { Sheet } from "@/components/ui/sheet"; +import { type AnswerFeedbackType } from "@/components/ClinicalDashboard"; +import { NaturalLanguageAnswer, UserQuestionBubble } from "@/components/clinical-dashboard/answer-content"; +import { StrengthBadge } from "@/components/clinical-dashboard/badges"; +import { UtilityDrawer } from "@/components/clinical-dashboard/dashboard-shell"; +import { cleanDisplayTitle } from "@/components/clinical-dashboard/display-text"; +import { MatchExplanationChips } from "@/components/clinical-dashboard/document-search-results"; +import { + AnswerSupportSummaryCard, + answerHasCentralTable, + answerSupportPriority, + ClinicalNotesChecklistPanel, + clinicalNotesCount, + clinicalNotesDisplayCountForAnswer, + compactEvidenceSummary, + type EvidenceTabName, + formatQuoteCardsForClipboard, + primaryVisualTable, + SafetyFindingsPanel, +} from "@/components/clinical-dashboard/evidence-panels"; +import { QueryCoverageChips, RelevanceBadge } from "@/components/clinical-dashboard/relevance"; +import { useMobilePreviewSheet } from "@/components/clinical-dashboard/use-mobile-preview-sheet"; +import { InlineTableCard, MobileEvidenceSheetContent } from "@/components/clinical-dashboard/visual-evidence"; +import { + answerSurface, + cn, + floatingControl, + iconTilePremium, + panelSubtle, + sourceCard, + SourceStatusBadge, + subtleStatusPill, + textMuted, +} from "@/components/ui-primitives"; +import { type AnswerRenderModel } from "@/lib/answer-render-policy"; +import { extractSafetyFindings } from "@/lib/clinical-safety"; +import { type SmartDocumentTag } from "@/lib/document-tags"; +import { type SourceGovernanceWarning } from "@/lib/source-governance"; +import type { + AnswerSection, + BestSourceRecommendation, + ClinicalQueryMode, + ConflictOrGap, + EvidenceRelevance, + EvidenceSummary, + RagAnswer, + RelatedDocument, + SearchResult, + SearchScopeSummary, +} from "@/lib/types"; +import { type AnswerEvidenceMapRow, type AnswerViewMode } from "@/lib/ward-output"; + +function WhyThisMatchedPanel({ sources }: { sources: SearchResult[] }) { + const visibleSources = sources.slice(0, 3); + if (visibleSources.length === 0) return null; + + return ( +
+ + + + + + + Why this matched + + Match signals, source strength, and term coverage for top passages + + + + + +
+ {visibleSources.map((source) => ( +
+
+
+

+ {cleanDisplayTitle(source.title)} +

+

+ page {source.page_number ?? "n/a"} ·{" "} + chunk {source.chunk_index} +

+
+
+ + + +
+
+ + {source.index_unit ? ( +

+ + {source.index_unit.unit_type.replaceAll("_", " ")}: + {" "} + {source.index_unit.title} +

+ ) : null} +
+ +
+
+ ))} +
+
+ ); +} + +export function RelatedDocumentsPanel({ + documents, + onScopeDocument, + onTagSearch, +}: { + documents: RelatedDocument[]; + onScopeDocument: (documentId: string) => void; + onTagSearch: (tag: SmartDocumentTag) => void; +}) { + if (documents.length === 0) return null; + + return ( + +
+ {documents.map((document) => ( +
+
+
+ + {documentDisplayTitle(document)} + + +

+ {document.match_reason} · pages {document.best_pages.join(", ") || "n/a"} · {document.image_count}{" "} + images{document.table_count ? ` · ${document.table_count} tables` : ""} +

+
+ +
+ {document.summary && ( +

+ +

+ )} + +
+ ))} +
+
+ ); +} + +export function StagedAnswerResultSurface({ + answer, + query, + safeAnswerText, + bestSource, + currentRelevance, + queryMode, + sourceGovernanceWarnings, + sourceSummary, + renderModel, + weakEvidence, + groupedGovernanceWarningCount, + answerViewMode, + answerEvidenceMapRows, + onScopeDocument, + answerGrounded, + sources, + gaps, + searchScope, + demoMode, + safeAnswerSections, + safetyFindings, + copiedAnswer, + pendingFeedback, + onCopyAnswer, + onSubmitFeedback, +}: { + answer: RagAnswer; + query: string; + safeAnswerText: string; + bestSource: BestSourceRecommendation | null; + currentRelevance: EvidenceRelevance | null | undefined; + queryMode: ClinicalQueryMode; + sourceGovernanceWarnings: SourceGovernanceWarning[]; + sourceSummary?: EvidenceSummary; + renderModel: AnswerRenderModel; + weakEvidence: boolean; + groupedGovernanceWarningCount: number; + answerViewMode: AnswerViewMode; + answerEvidenceMapRows: AnswerEvidenceMapRow[]; + onScopeDocument: (documentId: string) => void; + answerGrounded: boolean; + sources: SearchResult[]; + gaps: ConflictOrGap[]; + searchScope: SearchScopeSummary | null; + demoMode: boolean; + safeAnswerSections: Array; + safetyFindings: ReturnType; + copiedAnswer: boolean; + pendingFeedback: AnswerFeedbackType | null; + onCopyAnswer: () => void; + onSubmitFeedback: (feedbackType: AnswerFeedbackType) => void; +}) { + const noteCount = clinicalNotesCount(answer); + const showClinicalNotes = safetyFindings.length > 0 || noteCount > 0; + const clinicalNoteDisplayCount = clinicalNotesDisplayCountForAnswer( + answer, + answerViewMode, + noteCount || safetyFindings.length, + ); + const sourceCount = + renderModel.primarySources.length || + sourceSummary?.total_sources || + sources.length || + answer.sources?.length || + answer.citations.length; + const centralTable = answerHasCentralTable(answer) ? primaryVisualTable(answer) : null; + const showEvidenceDrawer = renderModel.allowedBlocks.some((block) => + ["sourceStatus", "reviewSources", "evidenceMap", "quoteCards", "visualEvidence", "warnings"].includes(block), + ); + const [clinicalNotesOpen, setClinicalNotesOpen] = useState(false); + const [evidenceOpen, setEvidenceOpen] = useState(false); + const [evidenceInitialTab, setEvidenceInitialTab] = useState(null); + const [activeReviewPanel, setActiveReviewPanel] = useState<"clinical" | "evidence" | null>(null); + const [copiedQuotes, setCopiedQuotes] = useState(false); + const clinicalNotesTriggerRef = useRef(null); + const evidenceTriggerRef = useRef(null); + const useReviewSheet = useMobilePreviewSheet(); + const copyQuotesTimerRef = useRef(null); + useEffect(() => { + return () => { + if (copyQuotesTimerRef.current !== null) window.clearTimeout(copyQuotesTimerRef.current); + }; + }, []); + function openClinicalNotes() { + setEvidenceOpen(false); + setEvidenceInitialTab(null); + if (useReviewSheet) { + setActiveReviewPanel(null); + setClinicalNotesOpen(true); + return; + } + setClinicalNotesOpen(false); + setActiveReviewPanel("clinical"); + } + function restoreFocusToTrigger(ref: RefObject) { + window.requestAnimationFrame(() => { + if (ref.current?.isConnected) ref.current.focus({ preventScroll: true }); + }); + } + function closeClinicalNotesReview() { + setClinicalNotesOpen(false); + restoreFocusToTrigger(clinicalNotesTriggerRef); + } + function openEvidence(initialTab: EvidenceTabName | null = null) { + setClinicalNotesOpen(false); + setEvidenceInitialTab(initialTab); + if (useReviewSheet) { + setActiveReviewPanel(null); + setEvidenceOpen(true); + return; + } + setEvidenceOpen(false); + setActiveReviewPanel("evidence"); + } + function closeEvidenceReview() { + setEvidenceOpen(false); + setEvidenceInitialTab(null); + restoreFocusToTrigger(evidenceTriggerRef); + } + function closeDesktopReviewPanel() { + const triggerRef = activeReviewPanel === "clinical" ? clinicalNotesTriggerRef : evidenceTriggerRef; + setActiveReviewPanel(null); + restoreFocusToTrigger(triggerRef); + } + function openTableEvidence() { + setClinicalNotesOpen(false); + openEvidence("Tables"); + } + const copyQuotes = useCallback(async () => { + const quoteText = formatQuoteCardsForClipboard(renderModel.quoteCards); + if (!quoteText) return; + try { + await navigator.clipboard.writeText(quoteText); + setCopiedQuotes(true); + if (copyQuotesTimerRef.current !== null) window.clearTimeout(copyQuotesTimerRef.current); + copyQuotesTimerRef.current = window.setTimeout(() => setCopiedQuotes(false), 1600); + } catch { + setCopiedQuotes(false); + } + }, [renderModel.quoteCards]); + const priority = answerSupportPriority(answer, safeAnswerSections, centralTable, safetyFindings, { + grounded: answerGrounded, + weakEvidence, + }); + const inlineEvidenceSummary = compactEvidenceSummary(answer, sources, sourceSummary, renderModel); + const evidenceTrustLabel = inlineEvidenceSummary.split(" · ")[0] || "Review support"; + const showInlineSupportCard = Boolean(priority || showClinicalNotes || showEvidenceDrawer); + const showLayoutAside = Boolean(activeReviewPanel || centralTable); + + return ( +
+
+ + +
+
+ + + {showInlineSupportCard ? ( + openEvidence(null)} + /> + ) : null} + + {centralTable && activeReviewPanel ? : null} +
+ + {activeReviewPanel ? ( + + ) : centralTable ? ( +
+ +
+ ) : null} +
+ + {showClinicalNotes ? ( + + + + } + titleAccessory={ + + {clinicalNoteDisplayCount} + + } + headerActions={ + bestSource ? ( + + + + ) : null + } + headerClassName="gap-2 p-2.5 sm:p-3" + titleClassName="text-[15px] leading-5" + closeButtonClassName="inline-flex h-8 w-8 items-center justify-center rounded-full text-[color:var(--text-muted)] transition hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]" + contentClassName="max-h-[92dvh] translate-y-0 bg-[color:var(--surface-raised)] motion-safe:animate-none sm:h-auto sm:max-h-[88dvh] sm:max-w-lg" + contentStyle={{ height: "80dvh" }} + bodyClassName="flex flex-col bg-[color:var(--surface-raised)] px-3 pb-0 pt-2 sm:p-3" + returnFocusRef={clinicalNotesTriggerRef} + portal + > + + + ) : null} + + {showEvidenceDrawer ? ( + {evidenceTrustLabel} + } + closeLabel="Close evidence" + headerLeading={ + + + + } + contentClassName="max-h-[92dvh] translate-y-0 bg-[color:var(--surface-raised)] motion-safe:animate-none sm:h-auto sm:max-h-[88dvh] sm:max-w-lg" + contentStyle={{ height: "80dvh" }} + bodyClassName="bg-[color:var(--surface-raised)] px-3 pb-0 pt-2 sm:p-3" + returnFocusRef={evidenceTriggerRef} + portal + > + + + ) : null} +
+ + +
+ ); +} diff --git a/tests/rendered-text-formatting.test.ts b/tests/rendered-text-formatting.test.ts index 89fc328480..13d5da9d51 100644 --- a/tests/rendered-text-formatting.test.ts +++ b/tests/rendered-text-formatting.test.ts @@ -22,7 +22,8 @@ describe("document-derived text must route through a formatter", () => { const evidenceContent = componentSource("clinical-dashboard/evidence-panels.tsx"); const outputPanel = componentSource("clinical-dashboard/output-panel.tsx"); const visualEvidence = componentSource("clinical-dashboard/visual-evidence.tsx"); - const dashboardSurfaces = `${dashboard}\n${answerContent}\n${evidenceContent}\n${outputPanel}\n${visualEvidence}`; + const documentResults = componentSource("clinical-dashboard/document-results.tsx"); + const dashboardSurfaces = `${dashboard}\n${answerContent}\n${evidenceContent}\n${outputPanel}\n${visualEvidence}\n${documentResults}`; it("renders exact quotes through the verbatim cleaner, never raw", () => { // Allow `${quote.quote}` inside template literals (React keys, clipboard text);