diff --git a/docs/process-hardening.md b/docs/process-hardening.md
index 58fdb831b6..4f798c3b87 100644
--- a/docs/process-hardening.md
+++ b/docs/process-hardening.md
@@ -36,12 +36,13 @@ This document turns the current process review into phased, durable repo practic
- **2026-07-03:** extracted `output-panel.tsx` — `ClinicalOutputPanel` (block 487–688, moved verbatim). Monolith 6084 → 5882 lines. Clean move: empty danger set (no monolith-internal deps) and empty reverse set (the monolith never rendered it — its only consumer is `evidence-panels`'s `ClinicalNotesChecklistPanel`). This **replaced** the evidence-panels↔monolith cycle with an evidence-panels↔output-panel one: `evidence-panels` now imports `ClinicalOutputPanel` from `clinical-dashboard/output-panel` (still imports `clinicalQueryModeOptions` + `type AnswerFeedbackType` from the monolith); `output-panel` imports the detail-helper family + `AnswerViewModeControl`/`EvidenceMapTable` from `evidence-panels`. Retargeted the AST pin (`ClinicalOutputPanel` now resolves via the `scannedFiles` array's `output-panel.tsx` entry) and added `output-panel.tsx` to the `rendered-text-formatting.test.ts` corpus. Stripped 8 now-orphaned monolith imports.
+- **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)
-The approved move map (`docs/redesign/04-deferred.md` §2) has 2 modules left. Recommended order:
+The approved move map (`docs/redesign/04-deferred.md` §2) has 1 module left:
-1. `visual-evidence.tsx` — `VisualEvidenceStrip`, `InlineTableCard`, `MobileEvidenceSheetContent`, `MobileEvidenceTabPanel`, `UnifiedEvidenceDrawerContent` (+ `supportDotClass`/`supportLabel`/`EvidenceClaimsList`/`EvidenceGapsPanel`/`compactClinicalTableCaption`/`visualEvidenceHeader` helpers, currently just after `WhyThisMatchedPanel`).
-2. `document-results.tsx` — `WhyThisMatchedPanel`, `RelatedDocumentsPanel`, `StagedAnswerResultSurface`.
+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`.
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.
diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx
index 8e16a0ceb4..03c49b48e8 100644
--- a/src/components/ClinicalDashboard.tsx
+++ b/src/components/ClinicalDashboard.tsx
@@ -7,13 +7,10 @@ import {
AlertCircle,
Bell,
BookOpen,
- CheckCircle2,
ChevronDown,
CircleUserRound,
ClipboardCheck,
- Copy,
ExternalLink,
- FileImage,
FileText,
Globe2,
HelpCircle,
@@ -27,7 +24,6 @@ import {
Palette,
PanelTop,
Plus,
- Quote,
RefreshCw,
Search,
Settings as SettingsIcon,
@@ -44,7 +40,6 @@ import {
X,
} from "lucide-react";
import { type CSSProperties, type RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { AccessibleTable } from "@/components/AccessibleTable";
import {
DocumentOrganizationBadges,
documentDisplayTitle,
@@ -53,15 +48,12 @@ import {
import { DocumentTagCloud } from "@/components/DocumentTagCloud";
import { DocumentManagementActions, type DocumentDeleteResult } from "@/components/DocumentManagementActions";
import { useDismissableLayer } from "@/components/use-dismissable-layer";
-import { formatCompactCitationLabel } from "@/lib/citations";
import { extractSafetyFindings } from "@/lib/clinical-safety";
import { readLocalProjectIdentity, unsafeLocalProjectMessage } from "@/lib/local-project-identity";
import { isLocalNoAuthMode } from "@/lib/env";
import {
appBackdrop,
answerSurface,
- chatMicroAction,
- clinicalDivider,
clinicalNotesRow,
cn,
evidenceRow,
@@ -78,9 +70,6 @@ import {
SourceStatusBadge,
sourceCard,
subtleStatusPill,
- tableCard,
- tableCardHeader,
- tableMicroActionRow,
textMuted,
toneDanger,
toneInfo,
@@ -113,12 +102,7 @@ import {
type SetupCheck,
type IngestionQualityReviewItem,
} from "@/components/clinical-dashboard/DocumentManagerPanel";
-import {
- GuideDialog,
- GuideTrigger,
- SectionHeading,
- UtilityDrawer,
-} from "@/components/clinical-dashboard/dashboard-shell";
+import { GuideDialog, GuideTrigger, UtilityDrawer } from "@/components/clinical-dashboard/dashboard-shell";
import {
cleanDisplayTitle,
sanitizeAnswerDisplayText,
@@ -127,12 +111,9 @@ import {
import {
NaturalLanguageAnswer,
ScopeAndGovernanceNotice,
- SourceImage,
UserQuestionBubble,
} from "@/components/clinical-dashboard/answer-content";
import {
- AnswerFeedbackPanel,
- AnswerSafetyNotice,
AnswerSupportSummaryCard,
answerHasCentralTable,
answerSupportPriority,
@@ -141,17 +122,13 @@ import {
clinicalNotesDisplayCountForAnswer,
compactEvidenceSummary,
type EvidenceTabName,
- simpleClinicalTableProps,
evidenceMapRowsFromRenderModel,
- evidenceTabCount,
- evidenceTabOrder,
formatQuoteCardsForClipboard,
primaryVisualTable,
- QuoteCards,
SafetyFindingsPanel,
- VerificationWorkspace,
} 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 { MasterSearchHeader } from "@/components/clinical-dashboard/master-search-header";
import { emptyStates, errorCopy } from "@/lib/ui-copy";
import { applicationsLauncherItemCount } from "@/components/applications-launcher-page";
@@ -211,13 +188,11 @@ 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 { sourceTextForCompactDisplay } from "@/lib/source-text-sanitizer";
import {
frontendSourceGovernanceWarnings,
groupSourceGovernanceWarnings,
type SourceGovernanceWarning,
} from "@/lib/source-governance";
-import { smartEvidenceTags } from "@/lib/evidence-tags";
import {
documentLabelReviewStatus,
documentLabelTier,
@@ -544,719 +519,6 @@ function WhyThisMatchedPanel({ sources }: { sources: SearchResult[] }) {
);
}
-function compactClinicalTableCaption(item: VisualEvidenceCard) {
- const raw = item.tableTitle || item.tableLabel || item.caption || "Clinical table";
- const cleaned = sourceTextForCompactDisplay(raw)
- .replace(/\btable\s+\d+\s*[:.-]?\s*/i, "")
- .replace(/\b(?:page|p\.)\s*\d+\b/gi, "")
- .replace(/\s{2,}/g, " ")
- .trim();
- const caption = cleaned || "Clinical table";
- return caption.length <= 72 ? caption : `${caption.slice(0, 69).trim()}...`;
-}
-
-function visualEvidenceHeader(item: VisualEvidenceCard) {
- const titleSource = [item.tableLabel, item.tableTitle].filter(Boolean).join(" · ");
- const titleText = sourceTextForCompactDisplay(titleSource).trim();
- const captionText = sourceTextForCompactDisplay(item.caption ?? "").trim();
- const normalizedTitle = titleText.toLowerCase();
- const normalizedCaption = captionText.toLowerCase();
- const isDuplicateCaption =
- Boolean(normalizedCaption) &&
- (normalizedCaption.startsWith(normalizedTitle) || normalizedCaption === normalizedTitle);
- return {
- title: titleText || captionText || "Visual evidence",
- caption: isDuplicateCaption ? null : captionText,
- };
-}
-
-function VisualEvidenceStrip({
- evidence,
- collapsed = false,
- embedded = false,
-}: {
- evidence: VisualEvidenceCard[];
- collapsed?: boolean;
- embedded?: boolean;
-}) {
- function looksLikeTableText(value?: string | null) {
- return Boolean(value?.includes("|") && value.split("|").filter((cell) => cell.trim()).length >= 3);
- }
-
- if (collapsed) {
- return (
-
- );
- }
-
- const content = (
- <>
-
- {evidence.length === 0 ? (
-
- ) : (
-
- {evidence.map((item) => {
- const tableMarkdown = item.accessibleTableMarkdown?.trim()
- ? item.accessibleTableMarkdown
- : looksLikeTableText(item.tableTextSnippet)
- ? item.tableTextSnippet
- : null;
- const hasStructuredTable = Boolean(tableMarkdown || item.tableRows?.length || item.tableColumns?.length);
- const tableCaption = compactClinicalTableCaption(item);
- const sourceHeader = visualEvidenceHeader(item);
- const displayLabels = smartEvidenceTags(
- item.labels,
- [[item.tableLabel, item.tableTitle].filter(Boolean).join(": "), item.caption, item.tableTextSnippet]
- .filter(Boolean)
- .join(" "),
- );
- return (
-
-
-
-
-
- {!hasStructuredTable ? {sourceHeader.title}
: null}
- {!hasStructuredTable && sourceHeader.caption ? {sourceHeader.caption}
: null}
-
- {!hasStructuredTable && item.tableTextSnippet ? (
-
- {sourceTextForCompactDisplay(item.tableTextSnippet)}
-
- ) : null}
- {displayLabels.length ? (
-
- {displayLabels.map((label) => (
-
- {label}
-
- ))}
-
- ) : null}
-
-
-
- {formatCompactCitationLabel(item)}
-
-
- {cleanDisplayTitle(item.title)}, page {item.page_number ?? "n/a"}
-
- {item.image_type && (
-
- {item.image_type.replaceAll("_", " ")}
-
- )}
- {!hasStructuredTable ? : null}
-
-
- Open source
-
-
-
- );
- })}
-
- )}
- >
- );
-
- if (embedded) return {content}
;
-
- return ;
-}
-
-function InlineTableCard({ item }: { item: VisualEvidenceCard }) {
- const tableMarkdown = item.accessibleTableMarkdown?.trim() ? item.accessibleTableMarkdown : null;
- const title = compactClinicalTableCaption(item);
-
- return (
-
-
-
{title}
-
{title}
-
-
-
-
-
-
-
-
-
- Expand
-
-
- Source
-
-
-
- );
-}
-
-const evidenceTabIconMap: Record = {
- Claims: CheckCircle2,
- Quotes: Quote,
- Tables: ListChecks,
- Images: FileImage,
- Gaps: AlertCircle,
-};
-
-function supportDotClass(supportLevel: string) {
- const normalized = supportLevel.toLowerCase();
- if (normalized.includes("unsupported") || normalized.includes("none")) return "bg-[color:var(--danger)]";
- if (normalized.includes("partial") || normalized.includes("limited") || normalized.includes("nearby")) {
- return "bg-[color:var(--warning)]";
- }
- return "bg-[color:var(--clinical-accent)]";
-}
-
-function supportLabel(supportLevel: string) {
- const normalized = supportLevel.toLowerCase();
- if (normalized.includes("unsupported") || normalized.includes("none")) return "Unsupported";
- if (normalized.includes("partial") || normalized.includes("limited") || normalized.includes("nearby"))
- return "Partial";
- return "Direct";
-}
-
-function claimRowsForEvidencePanel(rows: AnswerEvidenceMapRow[], renderModel: AnswerRenderModel) {
- if (rows.length) return rows.slice(0, 6);
- return renderModel.primarySources.slice(0, 6).map((source, index) => ({
- id: source.id,
- section: source.label || cleanDisplayTitle(source.title || source.file_name) || `Source ${index + 1}`,
- detail: source.snippet || source.reason || "Open source passage to review the cited evidence.",
- supportLevel: source.sourceStrength === "none" ? "partial" : source.sourceStrength,
- citationCount: 1,
- sourceStatus:
- source.sourceStrength === "none" ? "Source requires review" : `${source.sourceStrength} source support`,
- bestSourceLabel: source.label,
- bestLinkedPassage: source.snippet || source.reason,
- href: source.href,
- }));
-}
-
-function EvidenceClaimsList({ rows, renderModel }: { rows: AnswerEvidenceMapRow[]; renderModel: AnswerRenderModel }) {
- const claimRows = claimRowsForEvidencePanel(rows, renderModel);
- const directCount = claimRows.filter((row) => supportLabel(row.supportLevel) === "Direct").length;
- const partialCount = claimRows.filter((row) => supportLabel(row.supportLevel) === "Partial").length;
-
- if (!claimRows.length) {
- return ;
- }
-
- return (
-
-
-
-
Claims checked
-
-
-
- Direct
-
-
-
- Partial
-
-
-
- Unsupported
-
-
-
-
- {directCount} direct · {partialCount} partial
-
-
-
-
- {claimRows.map((row, index) => (
-
-
-
-
- {row.section}
-
- {row.detail || row.bestLinkedPassage || row.bestSourceLabel}
-
-
-
-
- ))}
-
-
- );
-}
-
-function EvidenceGapsPanel({ warnings }: { warnings: string[] }) {
- if (!warnings.length) {
- return (
-
- );
- }
-
- return (
-
- {warnings.map((warning, index) => (
-
-
-
-
Gap {index + 1}
-
{warning}
-
-
- ))}
-
- );
-}
-
-function MobileEvidenceSheetContent({
- answer,
- sources,
- renderModel,
- query,
- visualEvidence,
- answerEvidenceMapRows,
- sourceGovernanceWarnings,
- demoMode,
- initialTab,
- pendingFeedback,
- copiedQuotes,
- onCopyQuotes,
- onSubmitFeedback,
- onFollowUpQuote,
- onScopeDocument,
-}: {
- answer: RagAnswer;
- sources: SearchResult[];
- renderModel: AnswerRenderModel;
- query: string;
- visualEvidence: VisualEvidenceCard[];
- answerEvidenceMapRows: AnswerEvidenceMapRow[];
- sourceGovernanceWarnings: SourceGovernanceWarning[];
- demoMode: boolean;
- initialTab?: EvidenceTabName | null;
- pendingFeedback: AnswerFeedbackType | null;
- copiedQuotes: boolean;
- onCopyQuotes: () => void;
- onSubmitFeedback: (feedbackType: AnswerFeedbackType) => void;
- onFollowUpQuote?: (quote: QuoteCard) => void;
- onScopeDocument: (documentId: string) => void;
-}) {
- const order = evidenceTabOrder(answer, renderModel);
- const [selectedTab, setSelectedTab] = useState(() => initialTab ?? null);
- const activeTab = selectedTab && order.includes(selectedTab) ? selectedTab : order[0];
- const panelIdFor = (tab: EvidenceTabName) => `mobile-evidence-panel-${tab.toLowerCase()}`;
- const [added, setAdded] = useState(false);
- const primarySourceHref = renderModel.primarySources[0]?.href;
- async function copyEvidence() {
- if (renderModel.quoteCards.length) {
- onCopyQuotes();
- return;
- }
- try {
- await navigator.clipboard.writeText(renderModel.copyText);
- } catch {
- // Clipboard writes can fail in locked-down browsers; keep the panel usable.
- }
- }
-
- return (
-
-
-
- {order.map((tab) => {
- const selected = tab === activeTab;
- const Icon = evidenceTabIconMap[tab];
- const count = evidenceTabCount({
- tab,
- sources,
- visualEvidence,
- answerEvidenceMapRows,
- renderModel,
- });
- return (
-
- );
- })}
-
-
-
-
- {order.map((tab) => {
- const selected = tab === activeTab;
- return (
-
- {selected ? (
-
- ) : null}
-
- );
- })}
-
-
-
-
-
-
- {primarySourceHref ? (
-
-
- Source
-
- ) : (
-
-
- Source
-
- )}
-
-
-
-
-
- );
-}
-
-function MobileEvidenceTabPanel({
- tab,
- renderModel,
- query,
- visualEvidence,
- answerEvidenceMapRows,
- copiedQuotes,
- onCopyQuotes,
- onFollowUpQuote,
- onScopeDocument,
-}: {
- tab: EvidenceTabName;
- renderModel: AnswerRenderModel;
- query: string;
- visualEvidence: VisualEvidenceCard[];
- answerEvidenceMapRows: AnswerEvidenceMapRow[];
- copiedQuotes: boolean;
- onCopyQuotes: () => void;
- onFollowUpQuote?: (quote: QuoteCard) => void;
- onScopeDocument: (documentId: string) => void;
-}) {
- if (tab === "Claims") {
- return ;
- }
-
- if (tab === "Tables") {
- const tableEvidence = visualEvidence.filter((item) => item.accessibleTableMarkdown || item.tableRows?.length);
- return tableEvidence.length ? (
-
- {tableEvidence.slice(0, 4).map((item, index) => (
-
-
-
-
-
-
- {compactClinicalTableCaption(item)}
-
-
- Table {index + 1} · p.{item.page_number ?? "n/a"}
-
-
-
-
-
-
- ))}
-
- ) : (
-
- );
- }
-
- if (tab === "Images") {
- return visualEvidence.length ? (
-
- ) : (
-
- );
- }
-
- if (tab === "Quotes") {
- return (
-
- );
- }
-
- return ;
-}
-
-function UnifiedEvidenceDrawerContent({
- answer,
- renderModel,
- query,
- visualEvidence,
- answerEvidenceMapRows,
- pendingFeedback,
- copiedQuotes,
- onCopyQuotes,
- onSubmitFeedback,
- onFollowUpQuote,
- onScopeDocument,
-}: {
- answer: RagAnswer;
- renderModel: AnswerRenderModel;
- query: string;
- visualEvidence: VisualEvidenceCard[];
- answerEvidenceMapRows: AnswerEvidenceMapRow[];
- pendingFeedback: AnswerFeedbackType | null;
- copiedQuotes: boolean;
- onCopyQuotes: () => void;
- onSubmitFeedback: (feedbackType: AnswerFeedbackType) => void;
- onFollowUpQuote?: (quote: QuoteCard) => void;
- onScopeDocument: (documentId: string) => void;
-}) {
- const order = evidenceTabOrder(answer, renderModel);
-
- return (
-
-
-
-
- {order.map((item) => (
-
- {item}
-
- ))}
-
-
- {order.map((section) => {
- if (section === "Claims") {
- return (
-
- );
- }
-
- if (section === "Tables") {
- return (
-
- Tables
- {visualEvidence.some((item) => item.accessibleTableMarkdown || item.tableRows?.length) ? (
-
- {visualEvidence
- .filter((item) => item.accessibleTableMarkdown || item.tableRows?.length)
- .slice(0, 3)
- .map((item) => (
-
-
-
- {compactClinicalTableCaption(item)}
-
-
p.{item.page_number ?? "n/a"}
-
-
-
- Expand
-
-
- Source
-
-
-
- ))}
-
- ) : (
-
- )}
-
- );
- }
-
- if (section === "Images") {
- return (
-
- );
- }
-
- if (section === "Quotes") {
- return (
-
- );
- }
-
- return (
-
- );
- })}
-
- );
-}
-
function RelatedDocumentsPanel({
documents,
onScopeDocument,
diff --git a/src/components/clinical-dashboard/visual-evidence.tsx b/src/components/clinical-dashboard/visual-evidence.tsx
new file mode 100644
index 0000000000..286429be5b
--- /dev/null
+++ b/src/components/clinical-dashboard/visual-evidence.tsx
@@ -0,0 +1,769 @@
+"use client";
+
+import Link from "next/link";
+import { useState } from "react";
+import {
+ AlertCircle,
+ BookOpen,
+ CheckCircle2,
+ ChevronDown,
+ Copy,
+ ExternalLink,
+ FileImage,
+ Layers,
+ ListChecks,
+ Plus,
+ Quote,
+} from "lucide-react";
+
+import { AccessibleTable } from "@/components/AccessibleTable";
+import { type AnswerFeedbackType } from "@/components/ClinicalDashboard";
+import { ScopeAndGovernanceNotice, SourceImage } from "@/components/clinical-dashboard/answer-content";
+import { SectionHeading, UtilityDrawer } from "@/components/clinical-dashboard/dashboard-shell";
+import { cleanDisplayTitle } from "@/components/clinical-dashboard/display-text";
+import {
+ AnswerFeedbackPanel,
+ AnswerSafetyNotice,
+ type EvidenceTabName,
+ evidenceTabCount,
+ evidenceTabOrder,
+ QuoteCards,
+ simpleClinicalTableProps,
+ VerificationWorkspace,
+} from "@/components/clinical-dashboard/evidence-panels";
+import { QueryCoverageChips } from "@/components/clinical-dashboard/relevance";
+import {
+ chatMicroAction,
+ clinicalDivider,
+ cn,
+ EmptyState,
+ floatingControl,
+ iconTilePremium,
+ metadataPill,
+ sourceCard,
+ tableCard,
+ tableCardHeader,
+ tableMicroActionRow,
+ textMuted,
+} from "@/components/ui-primitives";
+import { type AnswerRenderModel } from "@/lib/answer-render-policy";
+import { formatCompactCitationLabel } from "@/lib/citations";
+import { smartEvidenceTags } from "@/lib/evidence-tags";
+import { type SourceGovernanceWarning } from "@/lib/source-governance";
+import { sourceTextForCompactDisplay } from "@/lib/source-text-sanitizer";
+import type { QuoteCard, RagAnswer, SearchResult, VisualEvidenceCard } from "@/lib/types";
+import { emptyStates } from "@/lib/ui-copy";
+import { type AnswerEvidenceMapRow } from "@/lib/ward-output";
+
+function compactClinicalTableCaption(item: VisualEvidenceCard) {
+ const raw = item.tableTitle || item.tableLabel || item.caption || "Clinical table";
+ const cleaned = sourceTextForCompactDisplay(raw)
+ .replace(/\btable\s+\d+\s*[:.-]?\s*/i, "")
+ .replace(/\b(?:page|p\.)\s*\d+\b/gi, "")
+ .replace(/\s{2,}/g, " ")
+ .trim();
+ const caption = cleaned || "Clinical table";
+ return caption.length <= 72 ? caption : `${caption.slice(0, 69).trim()}...`;
+}
+
+function visualEvidenceHeader(item: VisualEvidenceCard) {
+ const titleSource = [item.tableLabel, item.tableTitle].filter(Boolean).join(" · ");
+ const titleText = sourceTextForCompactDisplay(titleSource).trim();
+ const captionText = sourceTextForCompactDisplay(item.caption ?? "").trim();
+ const normalizedTitle = titleText.toLowerCase();
+ const normalizedCaption = captionText.toLowerCase();
+ const isDuplicateCaption =
+ Boolean(normalizedCaption) &&
+ (normalizedCaption.startsWith(normalizedTitle) || normalizedCaption === normalizedTitle);
+ return {
+ title: titleText || captionText || "Visual evidence",
+ caption: isDuplicateCaption ? null : captionText,
+ };
+}
+
+function VisualEvidenceStrip({
+ evidence,
+ collapsed = false,
+ embedded = false,
+}: {
+ evidence: VisualEvidenceCard[];
+ collapsed?: boolean;
+ embedded?: boolean;
+}) {
+ function looksLikeTableText(value?: string | null) {
+ return Boolean(value?.includes("|") && value.split("|").filter((cell) => cell.trim()).length >= 3);
+ }
+
+ if (collapsed) {
+ return (
+
+ );
+ }
+
+ const content = (
+ <>
+
+ {evidence.length === 0 ? (
+
+ ) : (
+
+ {evidence.map((item) => {
+ const tableMarkdown = item.accessibleTableMarkdown?.trim()
+ ? item.accessibleTableMarkdown
+ : looksLikeTableText(item.tableTextSnippet)
+ ? item.tableTextSnippet
+ : null;
+ const hasStructuredTable = Boolean(tableMarkdown || item.tableRows?.length || item.tableColumns?.length);
+ const tableCaption = compactClinicalTableCaption(item);
+ const sourceHeader = visualEvidenceHeader(item);
+ const displayLabels = smartEvidenceTags(
+ item.labels,
+ [[item.tableLabel, item.tableTitle].filter(Boolean).join(": "), item.caption, item.tableTextSnippet]
+ .filter(Boolean)
+ .join(" "),
+ );
+ return (
+
+
+
+
+
+ {!hasStructuredTable ? {sourceHeader.title}
: null}
+ {!hasStructuredTable && sourceHeader.caption ? {sourceHeader.caption}
: null}
+
+ {!hasStructuredTable && item.tableTextSnippet ? (
+
+ {sourceTextForCompactDisplay(item.tableTextSnippet)}
+
+ ) : null}
+ {displayLabels.length ? (
+
+ {displayLabels.map((label) => (
+
+ {label}
+
+ ))}
+
+ ) : null}
+
+
+
+ {formatCompactCitationLabel(item)}
+
+
+ {cleanDisplayTitle(item.title)}, page {item.page_number ?? "n/a"}
+
+ {item.image_type && (
+
+ {item.image_type.replaceAll("_", " ")}
+
+ )}
+ {!hasStructuredTable ? : null}
+
+
+ Open source
+
+
+
+ );
+ })}
+
+ )}
+ >
+ );
+
+ if (embedded) return {content}
;
+
+ return ;
+}
+
+export function InlineTableCard({ item }: { item: VisualEvidenceCard }) {
+ const tableMarkdown = item.accessibleTableMarkdown?.trim() ? item.accessibleTableMarkdown : null;
+ const title = compactClinicalTableCaption(item);
+
+ return (
+
+
+
{title}
+
{title}
+
+
+
+
+
+
+
+
+
+ Expand
+
+
+ Source
+
+
+
+ );
+}
+
+const evidenceTabIconMap: Record = {
+ Claims: CheckCircle2,
+ Quotes: Quote,
+ Tables: ListChecks,
+ Images: FileImage,
+ Gaps: AlertCircle,
+};
+
+function supportDotClass(supportLevel: string) {
+ const normalized = supportLevel.toLowerCase();
+ if (normalized.includes("unsupported") || normalized.includes("none")) return "bg-[color:var(--danger)]";
+ if (normalized.includes("partial") || normalized.includes("limited") || normalized.includes("nearby")) {
+ return "bg-[color:var(--warning)]";
+ }
+ return "bg-[color:var(--clinical-accent)]";
+}
+
+function supportLabel(supportLevel: string) {
+ const normalized = supportLevel.toLowerCase();
+ if (normalized.includes("unsupported") || normalized.includes("none")) return "Unsupported";
+ if (normalized.includes("partial") || normalized.includes("limited") || normalized.includes("nearby"))
+ return "Partial";
+ return "Direct";
+}
+
+function claimRowsForEvidencePanel(rows: AnswerEvidenceMapRow[], renderModel: AnswerRenderModel) {
+ if (rows.length) return rows.slice(0, 6);
+ return renderModel.primarySources.slice(0, 6).map((source, index) => ({
+ id: source.id,
+ section: source.label || cleanDisplayTitle(source.title || source.file_name) || `Source ${index + 1}`,
+ detail: source.snippet || source.reason || "Open source passage to review the cited evidence.",
+ supportLevel: source.sourceStrength === "none" ? "partial" : source.sourceStrength,
+ citationCount: 1,
+ sourceStatus:
+ source.sourceStrength === "none" ? "Source requires review" : `${source.sourceStrength} source support`,
+ bestSourceLabel: source.label,
+ bestLinkedPassage: source.snippet || source.reason,
+ href: source.href,
+ }));
+}
+
+function EvidenceClaimsList({ rows, renderModel }: { rows: AnswerEvidenceMapRow[]; renderModel: AnswerRenderModel }) {
+ const claimRows = claimRowsForEvidencePanel(rows, renderModel);
+ const directCount = claimRows.filter((row) => supportLabel(row.supportLevel) === "Direct").length;
+ const partialCount = claimRows.filter((row) => supportLabel(row.supportLevel) === "Partial").length;
+
+ if (!claimRows.length) {
+ return ;
+ }
+
+ return (
+
+
+
+
Claims checked
+
+
+
+ Direct
+
+
+
+ Partial
+
+
+
+ Unsupported
+
+
+
+
+ {directCount} direct · {partialCount} partial
+
+
+
+
+ {claimRows.map((row, index) => (
+
+
+
+
+ {row.section}
+
+ {row.detail || row.bestLinkedPassage || row.bestSourceLabel}
+
+
+
+
+ ))}
+
+
+ );
+}
+
+function EvidenceGapsPanel({ warnings }: { warnings: string[] }) {
+ if (!warnings.length) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {warnings.map((warning, index) => (
+
+
+
+
Gap {index + 1}
+
{warning}
+
+
+ ))}
+
+ );
+}
+
+export function MobileEvidenceSheetContent({
+ answer,
+ sources,
+ renderModel,
+ query,
+ visualEvidence,
+ answerEvidenceMapRows,
+ sourceGovernanceWarnings,
+ demoMode,
+ initialTab,
+ pendingFeedback,
+ copiedQuotes,
+ onCopyQuotes,
+ onSubmitFeedback,
+ onFollowUpQuote,
+ onScopeDocument,
+}: {
+ answer: RagAnswer;
+ sources: SearchResult[];
+ renderModel: AnswerRenderModel;
+ query: string;
+ visualEvidence: VisualEvidenceCard[];
+ answerEvidenceMapRows: AnswerEvidenceMapRow[];
+ sourceGovernanceWarnings: SourceGovernanceWarning[];
+ demoMode: boolean;
+ initialTab?: EvidenceTabName | null;
+ pendingFeedback: AnswerFeedbackType | null;
+ copiedQuotes: boolean;
+ onCopyQuotes: () => void;
+ onSubmitFeedback: (feedbackType: AnswerFeedbackType) => void;
+ onFollowUpQuote?: (quote: QuoteCard) => void;
+ onScopeDocument: (documentId: string) => void;
+}) {
+ const order = evidenceTabOrder(answer, renderModel);
+ const [selectedTab, setSelectedTab] = useState(() => initialTab ?? null);
+ const activeTab = selectedTab && order.includes(selectedTab) ? selectedTab : order[0];
+ const panelIdFor = (tab: EvidenceTabName) => `mobile-evidence-panel-${tab.toLowerCase()}`;
+ const [added, setAdded] = useState(false);
+ const primarySourceHref = renderModel.primarySources[0]?.href;
+ async function copyEvidence() {
+ if (renderModel.quoteCards.length) {
+ onCopyQuotes();
+ return;
+ }
+ try {
+ await navigator.clipboard.writeText(renderModel.copyText);
+ } catch {
+ // Clipboard writes can fail in locked-down browsers; keep the panel usable.
+ }
+ }
+
+ return (
+
+
+
+ {order.map((tab) => {
+ const selected = tab === activeTab;
+ const Icon = evidenceTabIconMap[tab];
+ const count = evidenceTabCount({
+ tab,
+ sources,
+ visualEvidence,
+ answerEvidenceMapRows,
+ renderModel,
+ });
+ return (
+
+ );
+ })}
+
+
+
+
+ {order.map((tab) => {
+ const selected = tab === activeTab;
+ return (
+
+ {selected ? (
+
+ ) : null}
+
+ );
+ })}
+
+
+
+
+
+
+ {primarySourceHref ? (
+
+
+ Source
+
+ ) : (
+
+
+ Source
+
+ )}
+
+
+
+
+
+ );
+}
+
+function MobileEvidenceTabPanel({
+ tab,
+ renderModel,
+ query,
+ visualEvidence,
+ answerEvidenceMapRows,
+ copiedQuotes,
+ onCopyQuotes,
+ onFollowUpQuote,
+ onScopeDocument,
+}: {
+ tab: EvidenceTabName;
+ renderModel: AnswerRenderModel;
+ query: string;
+ visualEvidence: VisualEvidenceCard[];
+ answerEvidenceMapRows: AnswerEvidenceMapRow[];
+ copiedQuotes: boolean;
+ onCopyQuotes: () => void;
+ onFollowUpQuote?: (quote: QuoteCard) => void;
+ onScopeDocument: (documentId: string) => void;
+}) {
+ if (tab === "Claims") {
+ return ;
+ }
+
+ if (tab === "Tables") {
+ const tableEvidence = visualEvidence.filter((item) => item.accessibleTableMarkdown || item.tableRows?.length);
+ return tableEvidence.length ? (
+
+ {tableEvidence.slice(0, 4).map((item, index) => (
+
+
+
+
+
+
+ {compactClinicalTableCaption(item)}
+
+
+ Table {index + 1} · p.{item.page_number ?? "n/a"}
+
+
+
+
+
+
+ ))}
+
+ ) : (
+
+ );
+ }
+
+ if (tab === "Images") {
+ return visualEvidence.length ? (
+
+ ) : (
+
+ );
+ }
+
+ if (tab === "Quotes") {
+ return (
+
+ );
+ }
+
+ return ;
+}
+
+function UnifiedEvidenceDrawerContent({
+ answer,
+ renderModel,
+ query,
+ visualEvidence,
+ answerEvidenceMapRows,
+ pendingFeedback,
+ copiedQuotes,
+ onCopyQuotes,
+ onSubmitFeedback,
+ onFollowUpQuote,
+ onScopeDocument,
+}: {
+ answer: RagAnswer;
+ renderModel: AnswerRenderModel;
+ query: string;
+ visualEvidence: VisualEvidenceCard[];
+ answerEvidenceMapRows: AnswerEvidenceMapRow[];
+ pendingFeedback: AnswerFeedbackType | null;
+ copiedQuotes: boolean;
+ onCopyQuotes: () => void;
+ onSubmitFeedback: (feedbackType: AnswerFeedbackType) => void;
+ onFollowUpQuote?: (quote: QuoteCard) => void;
+ onScopeDocument: (documentId: string) => void;
+}) {
+ const order = evidenceTabOrder(answer, renderModel);
+
+ return (
+
+
+
+
+ {order.map((item) => (
+
+ {item}
+
+ ))}
+
+
+ {order.map((section) => {
+ if (section === "Claims") {
+ return (
+
+ );
+ }
+
+ if (section === "Tables") {
+ return (
+
+ Tables
+ {visualEvidence.some((item) => item.accessibleTableMarkdown || item.tableRows?.length) ? (
+
+ {visualEvidence
+ .filter((item) => item.accessibleTableMarkdown || item.tableRows?.length)
+ .slice(0, 3)
+ .map((item) => (
+
+
+
+ {compactClinicalTableCaption(item)}
+
+
p.{item.page_number ?? "n/a"}
+
+
+
+ Expand
+
+
+ Source
+
+
+
+ ))}
+
+ ) : (
+
+ )}
+
+ );
+ }
+
+ if (section === "Images") {
+ return (
+
+ );
+ }
+
+ if (section === "Quotes") {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+ })}
+
+ );
+}
diff --git a/tests/rendered-text-formatting.test.ts b/tests/rendered-text-formatting.test.ts
index 075f8503d4..89fc328480 100644
--- a/tests/rendered-text-formatting.test.ts
+++ b/tests/rendered-text-formatting.test.ts
@@ -21,7 +21,8 @@ describe("document-derived text must route through a formatter", () => {
const answerContent = componentSource("clinical-dashboard/answer-content.tsx");
const evidenceContent = componentSource("clinical-dashboard/evidence-panels.tsx");
const outputPanel = componentSource("clinical-dashboard/output-panel.tsx");
- const dashboardSurfaces = `${dashboard}\n${answerContent}\n${evidenceContent}\n${outputPanel}`;
+ const visualEvidence = componentSource("clinical-dashboard/visual-evidence.tsx");
+ const dashboardSurfaces = `${dashboard}\n${answerContent}\n${evidenceContent}\n${outputPanel}\n${visualEvidence}`;
it("renders exact quotes through the verbatim cleaner, never raw", () => {
// Allow `${quote.quote}` inside template literals (React keys, clipboard text);