diff --git a/docs/process-hardening.md b/docs/process-hardening.md index a26a65fb0..a4a678c7d 100644 --- a/docs/process-hardening.md +++ b/docs/process-hardening.md @@ -31,16 +31,16 @@ This document turns the current process review into phased, durable repo practic - `src/app/page.tsx` now imports `ClinicalDashboard` from the module path (`@/components/clinical-dashboard`) while preserving the legacy source declaration file for AST and merge-guard compatibility. - **2026-07-03:** extracted `AuthPanel` (+ its solely-consumed auth-email snapshot helpers) into `clinical-dashboard/auth-panel.tsx`. Monolith 7924 → 7800 lines. Per-module gate established: `npm run typecheck` + `npx vitest run tests/clinical-dashboard-merge-artifacts.test.ts tests/rendered-text-formatting.test.ts` + a `data-testid`/`aria-label` sha1 checksum over `ClinicalDashboard.tsx` + `clinical-dashboard/*.tsx` (must be byte-identical before/after each move) + lint + prettier. +- **2026-07-03:** extracted `answer-content.tsx` — `SourceImage`, `ScopeAndGovernanceNotice`, the answer/source formatters, `SourcePreviewContent`, `NaturalLanguageAnswer`, `UserQuestionBubble`, `KeyClinicalItems` (block 510–1249 against the post-drift monolith), moved verbatim (block diff empty; testid/aria checksum byte-identical). Two shared helpers went to clean homes instead of a monolith↔module cycle: `useMobilePreviewSheet` (+ its media-query snapshot helpers) → new `clinical-dashboard/use-mobile-preview-sheet.ts`, and `comparableAnswerText` → `clinical-dashboard/display-text.ts`. Retargeted the `NaturalLanguageAnswer` AST pin to scan `answer-content.tsx`, and widened `rendered-text-formatting.test.ts` negative scans to the monolith+module corpus (both strengthened, not weakened). Reusable finding: the answer/evidence families' helpers already live in `@/lib/*` and extracted sibling modules, so extractions re-import rather than needing wide monolith exports; only `comparableAnswerText`/`useMobilePreviewSheet` were monolith-internal. +- **2026-07-03:** extracted `evidence-panels.tsx` — the clinical-detail/notes helper family + `AnswerSupportSummaryCard`, `ClinicalNotesChecklistPanel`, `SafetyFindingsPanel`, `EvidenceGapPanel`, `EvidenceCounts`, `AnswerSourceStatus`, `EvidenceSummaryCard`, `AnswerInsightBar`, `EvidenceVerificationStrip`, `AnswerFeedbackPanel`, `RenderModelSourceList`, `VerificationWorkspace`, `AnswerViewModeControl`, `EvidenceMapTable`, `AnswerSafetyNotice`, `QuoteCards` (contiguous block 484–2311, moved verbatim). Monolith 7909 → 6084 lines. **This module needs a monolith↔module cycle** — `ClinicalNotesChecklistPanel` renders `` (staying in the monolith as B3), so evidence-panels imports `ClinicalOutputPanel` + `clinicalQueryModeOptions` + `type AnswerFeedbackType` back from `@/components/ClinicalDashboard`. This is the SAME pattern already used by `global-mockup-search-shell.tsx` (imports `SettingsDialog` back); the repo has no `import/no-cycle` rule and the refs are render/runtime-time so init is safe (the `ui-smoke` build is the definitive cycle check). The monolith imports 26 symbols back (incl. the exported detail-helper family for output-panel). Robust dependency finding: the danger/reverse regex missed `const X: Type =` and `const X = {…}` forms (caught `clinicalQueryModeOptions` + `simpleClinicalTableProps` only via typecheck) — use a name-only regex `^(export )?(async )?(function|const|type) NAME` next time. #### Remaining decomposition — hand-off (do on a stable `main`, one module per commit) -The approved move map (`docs/redesign/04-deferred.md` §2) has 5 modules left. Unlike `auth-panel`, these are **interdependent** — they share a clinical-detail/notes helper family, so order matters and cross-module `export`s are required. Recommended order and the key dependency to resolve first: +The approved move map (`docs/redesign/04-deferred.md` §2) has 3 modules left. Recommended order: -1. `answer-content.tsx` — `SourceImage`, `ScopeAndGovernanceNotice`, `SourcePreviewContent`, `NaturalLanguageAnswer` (**AST-pinned** — retarget `tests/clinical-dashboard-merge-artifacts.test.ts` to scan this file for `NaturalLanguageAnswer`), `UserQuestionBubble`, `KeyClinicalItems` + answer formatters. Widen `tests/rendered-text-formatting.test.ts` to also scan this file. -2. `evidence-panels.tsx` — the clinical-detail/notes helper family (`displayItemsForClinicalDetailSection`, `sortClinicalDetailSections`, `clinicalDetailSummaryItems`, and siblings) **plus** `ClinicalNotesChecklistPanel`, `SafetyFindingsPanel`, `EvidenceGapPanel`, `EvidenceCounts`, `AnswerSourceStatus`, `EvidenceSummaryCard`, `AnswerInsightBar`, `EvidenceVerificationStrip`, `AnswerFeedbackPanel`, `VerificationWorkspace`, `AnswerViewModeControl`, `EvidenceMapTable`, `AnswerSafetyNotice`, `QuoteCards`. **Export the helper family** so output-panel can import it. Must land before output-panel. -3. `output-panel.tsx` — `ClinicalOutputPanel` (**AST-pinned** — retarget `dashboardPath` in `tests/clinical-dashboard-merge-artifacts.test.ts` to resolve declarations across the monolith + this file). Imports the detail helpers from `evidence-panels`. -4. `visual-evidence.tsx` — `VisualEvidenceStrip`, `InlineTableCard`, `MobileEvidenceSheetContent`, `MobileEvidenceTabPanel`, `UnifiedEvidenceDrawerContent`. -5. `document-results.tsx` — `WhyThisMatchedPanel`, `RelatedDocumentsPanel`, `StagedAnswerResultSurface`. +1. `output-panel.tsx` — `ClinicalOutputPanel` (**AST-pinned** — retarget `dashboardPath` in `tests/clinical-dashboard-merge-artifacts.test.ts`; the test now scans a `scannedFiles` array — add this file to it). It already imports the detail helpers from `evidence-panels`; when it moves out of the monolith, update `evidence-panels`'s back-import of `ClinicalOutputPanel` to point at `output-panel.tsx` instead of `@/components/ClinicalDashboard` (and move `clinicalQueryModeOptions`/`AnswerFeedbackType` to a shared home or keep the back-import). +2. `visual-evidence.tsx` — `VisualEvidenceStrip`, `InlineTableCard`, `MobileEvidenceSheetContent`, `MobileEvidenceTabPanel`, `UnifiedEvidenceDrawerContent` (+ `supportDotClass`/`supportLabel`/`EvidenceClaimsList`/`EvidenceGapsPanel` helpers, currently just after `ClinicalOutputPanel`). +3. `document-results.tsx` — `WhyThisMatchedPanel`, `RelatedDocumentsPanel`, `StagedAnswerResultSurface`. 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 f5722ea76..e2f0295a4 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -1,12 +1,9 @@ "use client"; -/* eslint-disable @next/next/no-img-element */ - import Link from "next/link"; import { useRouter } from "next/navigation"; import dynamic from "next/dynamic"; import { - Activity, AlertCircle, Bell, BookOpen, @@ -18,7 +15,6 @@ import { ExternalLink, FileImage, FileText, - Filter, Globe2, HelpCircle, Heart, @@ -40,26 +36,14 @@ import { SlidersHorizontal, Sparkles, Stethoscope, - Table2, Tag, - Target, UploadCloud, UserRound, WifiOff, Wrench, X, } from "lucide-react"; -import { - type CSSProperties, - memo, - type RefObject, - useCallback, - useEffect, - useMemo, - useRef, - useState, - useSyncExternalStore, -} from "react"; +import { type CSSProperties, type RefObject, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { AccessibleTable } from "@/components/AccessibleTable"; import { DocumentOrganizationBadges, @@ -69,24 +53,18 @@ import { import { DocumentTagCloud } from "@/components/DocumentTagCloud"; import { DocumentManagementActions, type DocumentDeleteResult } from "@/components/DocumentManagementActions"; import { useDismissableLayer } from "@/components/use-dismissable-layer"; -import { documentCitationHref, formatCompactCitationLabel, formatCitationLabel } from "@/lib/citations"; -import { extractSafetyFindings, formatSafetyFindingLabel } from "@/lib/clinical-safety"; -import { clearCachedSignedUrl, getCachedSignedUrl, setCachedSignedUrl } from "@/lib/signed-url-cache"; +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 { normalizeSourceMetadata, sourceStatusLabel, validationStatusLabel } from "@/lib/source-metadata"; import { appBackdrop, answerSurface, - chatActionRow, - chatAnswerText, chatMicroAction, - codeText, clinicalDivider, clinicalNotesRow, cn, evidenceRow, - evidenceSurface, EmptyState, fieldControlPlain, fieldControlWithIcon, @@ -96,15 +74,9 @@ import { metadataPill, panelSubtle, primaryControl, - proseMeasure, - raisedCard, SourceProvenance, SourceStatusBadge, sourceCard, - sourceCapsule, - statusDotMuted, - statusDotReady, - statusDotReview, subtleStatusPill, tableCard, tableCardHeader, @@ -119,7 +91,7 @@ import { import { useAuthSession } from "@/lib/supabase/client"; import { SafeBoldText } from "@/components/SafeBoldText"; import { Sheet } from "@/components/ui/sheet"; -import { AnswerEmptyState, AnswerSkeleton, CopyButton } from "@/components/clinical-dashboard/answer-status"; +import { AnswerEmptyState, AnswerSkeleton } from "@/components/clinical-dashboard/answer-status"; 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"; @@ -149,10 +121,45 @@ import { } from "@/components/clinical-dashboard/dashboard-shell"; import { cleanDisplayTitle, - compactSourceSnippet, sanitizeAnswerDisplayText, sanitizeDisplayText, } from "@/components/clinical-dashboard/display-text"; +import { + NaturalLanguageAnswer, + plainAnswerText, + ScopeAndGovernanceNotice, + SourceImage, + UserQuestionBubble, +} from "@/components/clinical-dashboard/answer-content"; +import { + AnswerFeedbackPanel, + AnswerSafetyNotice, + AnswerSupportSummaryCard, + AnswerViewModeControl, + answerHasCentralTable, + answerSupportPriority, + ClinicalNotesChecklistPanel, + clinicalDetailContentCount, + clinicalDetailMeta, + clinicalDetailSummaryItems, + clinicalNotesCount, + clinicalNotesDisplayCountForAnswer, + compactEvidenceSummary, + displayItemsForClinicalDetailSection, + EvidenceMapTable, + type EvidenceTabName, + simpleClinicalTableProps, + evidenceMapRowsFromRenderModel, + evidenceTabCount, + evidenceTabOrder, + formatQuoteCardsForClipboard, + primaryVisualTable, + QuoteCards, + SafetyFindingsPanel, + sortClinicalDetailSections, + VerificationWorkspace, +} from "@/components/clinical-dashboard/evidence-panels"; +import { useMobilePreviewSheet } from "@/components/clinical-dashboard/use-mobile-preview-sheet"; import { MasterSearchHeader } from "@/components/clinical-dashboard/master-search-header"; import { emptyStates, errorCopy } from "@/lib/ui-copy"; import { applicationsLauncherItemCount } from "@/components/applications-launcher-page"; @@ -181,13 +188,7 @@ import { MatchExplanationChips, type SearchFacets, } from "@/components/clinical-dashboard/document-search-results"; -import { - hasStrongRelevanceIcon, - isWeakRelevance, - QueryCoverageChips, - relevanceChipLabel, - RelevanceBadge, -} from "@/components/clinical-dashboard/relevance"; +import { isWeakRelevance, QueryCoverageChips, RelevanceBadge } from "@/components/clinical-dashboard/relevance"; import { answerPayloadIsUsable, isRetryableError, @@ -216,14 +217,8 @@ import { } from "@/lib/app-modes"; import { searchFormRecords } from "@/lib/forms"; import { searchServiceRecords } from "@/lib/services"; -import { buildAnswerRenderModel, type AnswerRenderModel, type SourceLink } from "@/lib/answer-render-policy"; -import { SourceActionRow, sourceResultHref } from "@/components/clinical-dashboard/source-actions"; -import { - clinicalProseUsefulness, - normalizeExtractedGlyphs, - sourceTextForCompactDisplay, - sourceTextForVerbatimQuote, -} from "@/lib/source-text-sanitizer"; +import { buildAnswerRenderModel, type AnswerRenderModel } from "@/lib/answer-render-policy"; +import { sourceTextForCompactDisplay } from "@/lib/source-text-sanitizer"; import { frontendSourceGovernanceWarnings, groupSourceGovernanceWarnings, @@ -252,7 +247,6 @@ import type { QuoteCard, RagAnswer, AnswerSection, - AnswerSectionKind, ConflictOrGap, RelatedDocument, EvidenceSummary, @@ -276,23 +270,6 @@ import { const navigationHashes = ["#search", "#quotes", "#images", "#sources"] as const; const mobileSectionFabMediaQuery = "(max-width: 768px), ((max-width: 1023px) and (hover: none) and (pointer: coarse))"; -const sourcePreviewSheetMediaQuery = "(max-width: 1023px)"; - -function subscribeToMobilePreviewMedia(callback: () => void) { - if (typeof window === "undefined" || typeof window.matchMedia !== "function") return () => undefined; - const media = window.matchMedia(sourcePreviewSheetMediaQuery); - media.addEventListener("change", callback); - return () => media.removeEventListener("change", callback); -} - -function getMobilePreviewSnapshot() { - if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false; - return window.matchMedia(sourcePreviewSheetMediaQuery).matches; -} - -function useMobilePreviewSheet() { - return useSyncExternalStore(subscribeToMobilePreviewMedia, getMobilePreviewSnapshot, () => false); -} export const recentQueryStorageKey = "clinical-kb-recent-queries"; const documentPageSize = 150; @@ -346,7 +323,7 @@ type BatchesPayload = { hasActiveBatches?: boolean; pollAfterMs?: number | null; }; -type AnswerFeedbackType = +export type AnswerFeedbackType = | "verified" | "needs_correction" | "source_insufficient" @@ -360,7 +337,7 @@ type IngestionQualityPayload = { demoMode?: boolean; }; -const clinicalQueryModeOptions: Array<{ value: ClinicalQueryMode; label: string }> = [ +export const clinicalQueryModeOptions: Array<{ value: ClinicalQueryMode; label: string }> = [ { value: "auto", label: "Auto" }, { value: "monitoring_schedule", label: "Monitoring" }, { value: "dose_threshold_lookup", label: "Dose / thresholds" }, @@ -507,2585 +484,7 @@ function normalizeNavigationHash(hash: string) { return navigationHashes.includes(hash as (typeof navigationHashes)[number]) ? hash : "#search"; } -const SourceImage = memo(function SourceImage({ - endpoint, - caption, - className = "max-h-52", -}: { - endpoint: string; - caption: string; - className?: string; -}) { - const [url, setUrl] = useState(() => getCachedSignedUrl(endpoint)?.url ?? null); - const [failed, setFailed] = useState(false); - const [attempt, setAttempt] = useState(0); - const { authorizationHeader, markSessionExpired } = useAuthSession(); - - useEffect(() => { - const cached = getCachedSignedUrl(endpoint); - if (cached) return () => undefined; - - let active = true; - fetch(endpoint, { headers: authorizationHeader }) - .then((response) => { - if (response.status === 401) markSessionExpired(); - return response.ok ? response.json() : null; - }) - .then((data) => { - if (active && data?.url) { - setCachedSignedUrl(endpoint, data); - setUrl(data.url); - setFailed(false); - } else if (active) { - setFailed(true); - } - }) - .catch(() => { - if (active) setFailed(true); - }); - return () => { - active = false; - }; - }, [attempt, authorizationHeader, endpoint, markSessionExpired]); - - function retryImage() { - clearCachedSignedUrl(endpoint); - setUrl(null); - setFailed(false); - setAttempt((current) => current + 1); - } - - function handleImageError() { - clearCachedSignedUrl(endpoint); - setFailed(true); - } - - if (failed) { - return ( -
-
- - Image preview could not load. - -
-
- ); - } - - if (!url) { - return ( -
- - Loading image -
- ); - } - - return ( - {caption} - ); -}); - -function ScopeAndGovernanceNotice({ - scope, - warnings, -}: { - scope: SearchScopeSummary | null; - warnings: SourceGovernanceWarning[]; -}) { - const groupedWarnings = groupSourceGovernanceWarnings(frontendSourceGovernanceWarnings(warnings)).slice(0, 4); - const showScope = - Boolean(scope && scope.activeFilterCount > 0) || - Boolean(scope?.warnings?.length) || - scope?.matchedDocumentCount === 0; - if (!showScope && groupedWarnings.length === 0) return null; - return ( -
- {showScope && scope ? ( -

- Scope: {scope.summary} - {scope.queryMode && scope.queryMode !== "auto" ? ` · ${scope.queryMode.replaceAll("_", " ")}` : ""} -

- ) : null} - {scope?.warnings?.length ? ( - - ) : null} - {groupedWarnings.length ? ( - - ) : null} -
- ); -} - -function plainAnswerText(value: string) { - const useful = clinicalProseUsefulness(value); - return sanitizeAnswerDisplayText(useful.text || value, { minLength: 8, minTokens: 2 }) - .replace(/(?:\s*\n\s*)?Synthetic demo only:.*$/i, "") - .trim(); -} - -function primaryAnswerDisplayText(value: string) { - const cleaned = plainAnswerText(value); - const fragments = cleaned - .split(/\r?\n+/) - .flatMap((line: string) => - line.split(/(?<=[.!?])\s+(?=(?:[A-Z]|\*\*|If\b|When\b|Do\b|Use\b|Monitor\b|Escalate\b|Document\b))/), - ) - .map((fragment: string) => - fragment - .replace(/^(?:[-*•]|\d+[.)])\s+/, "") - .replace( - /^(?:\*\*)?(?:answer|summary|bottom line|direct answer|clinical point|key point|required actions?|monitoring(?:\/timing)?|thresholds?|dose detail|medication(?:\/dose details?)?|escalation(?:\/risk)?|risk|safety|documentation(?:\/forms)?|source gaps?)(?:\*\*)?:\s+/i, - "", - ) - .trim(), - ) - .map((fragment: string) => clinicalProseUsefulness(fragment).text || fragment) - .filter((fragment: string) => { - if (!fragment) return false; - const useful = clinicalProseUsefulness(fragment); - return useful.useful || fragment.split(/\s+/).length >= 8; - }); - const uniqueFragments = Array.from(new Set(fragments)); - const selected = uniqueFragments.slice(0, 3).join(" "); - const words = selected.split(/\s+/).filter(Boolean); - if (words.length <= 85) return selected || cleaned; - return `${words - .slice(0, 85) - .join(" ") - .replace(/[;,:-]\s*$/, "")}...`; -} - -function sourceCapsuleText({ - sourceCount, - weakEvidence, - grounded, -}: { - sourceCount: number; - weakEvidence: boolean; - grounded: boolean; -}) { - if (sourceCount <= 0) return "No direct source found"; - if (!grounded) return "Review nearby sources"; - if (weakEvidence) return "Review sources"; - return `${sourceCount} source${sourceCount === 1 ? "" : "s"}`; -} - -function sourceStatusDotClass(metadata: ReturnType | null | undefined) { - if (!metadata) return statusDotMuted; - if (metadata.document_status === "current") return statusDotReady; - if (metadata.document_status === "review_due" || metadata.document_status === "outdated") return statusDotReview; - return statusDotMuted; -} - -type CapsulePreviewSource = { - id: string; - title: string; - pageNumber: number | null; - metadata: ReturnType; - score: number; - href: string; - snippet?: string; - sourceStrength?: - SourceLink["sourceStrength"] | BestSourceRecommendation["source_strength"] | SearchResult["source_strength"]; -}; - -function sourceBadgeLabel(index: number) { - return `S${index + 1}`; -} - -function sourceBadgeToneClass(metadata: ReturnType, index: number) { - if (metadata.document_status === "review_due" || metadata.document_status === "outdated") { - return "border-[color:var(--warning-border)] bg-[color:var(--warning-soft)] text-[color:var(--warning)]"; - } - if (index === 0) { - return "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent)] text-[color:var(--clinical-accent-contrast)]"; - } - return "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]"; -} - -function sourceSupportLabel(source: CapsulePreviewSource, index: number) { - if (!source.sourceStrength || source.sourceStrength === "none") return "Unsupported"; - if (source.sourceStrength === "limited") return "Partial"; - if (source.sourceStrength === "moderate") return "Partial"; - if (index === 0 || source.sourceStrength === "strong") return "Direct"; - return "Partial"; -} - -function sourceStatusShortLabel(metadata: ReturnType) { - if (metadata.document_status === "review_due") return "Review due"; - if (metadata.document_status === "outdated") return "Outdated"; - if (metadata.document_status === "current") return "Current"; - return sourceStatusLabel(metadata); -} - -function sourcePreviewPageCountLabel(previewSources: CapsulePreviewSource[]) { - const uniquePages = new Set(previewSources.map((source) => source.pageNumber).filter((page) => page !== null)); - const count = uniquePages.size || previewSources.length; - return `${count} page${count === 1 ? "" : "s"}`; -} - -function capsulePreviewSources( - bestSource: BestSourceRecommendation | null, - sources: SearchResult[], - sourceLinks: SourceLink[] = [], -) { - const rows: CapsulePreviewSource[] = []; - const seen = new Set(); - const pushRow = (row: CapsulePreviewSource) => { - const key = `${row.id}:${row.title}:${row.pageNumber ?? "n/a"}`; - if (seen.has(key)) return; - seen.add(key); - rows.push(row); - }; - - sourceLinks.slice(0, 5).forEach((source) => { - pushRow({ - id: source.chunk_id, - title: source.title || source.file_name || "Source", - pageNumber: source.page_number, - metadata: normalizeSourceMetadata(source.sourceMetadata), - score: source.score ?? 0, - href: source.href, - snippet: source.snippet, - sourceStrength: source.sourceStrength, - }); - }); - - if (bestSource) { - pushRow({ - id: bestSource.chunk_id, - title: bestSource.title || bestSource.file_name || "Source", - pageNumber: bestSource.page_number, - metadata: normalizeSourceMetadata(bestSource.source_metadata), - score: bestSource.score, - href: bestSource.viewer_href, - sourceStrength: bestSource.source_strength, - }); - } - - sources.slice(0, 5).forEach((source) => { - pushRow({ - id: source.id, - title: source.title || source.file_name || "Source", - pageNumber: source.page_number, - metadata: normalizeSourceMetadata(source.source_metadata), - score: source.hybrid_score ?? source.similarity ?? source.lexical_score ?? 0, - href: sourceResultHref(source), - sourceStrength: source.source_strength, - }); - }); - - return rows.slice(0, 4); -} - -function SourcePreviewContent({ - previewSources, - quoteText, - copiedQuote, - onCopyQuote, - showHeader = true, -}: { - previewSources: CapsulePreviewSource[]; - quoteText?: string | null; - copiedQuote: boolean; - onCopyQuote: () => void; - showHeader?: boolean; -}) { - const primaryPreviewSource = previewSources[0] ?? null; - const reviewDueSource = previewSources.find( - (source) => source.metadata.document_status === "review_due" || source.metadata.document_status === "outdated", - ); - - return ( - <> - {showHeader ? ( -
-
-
-

Sources

- - {sourcePreviewPageCountLabel(previewSources)} - -
-

Open the original PDF page.

-
-
- ) : null} -
- {previewSources.map((source, index) => ( -
- {index === 0 ? ( -

- - Best match -

- ) : index === 1 ? ( -

Also used

- ) : null} -
- - {sourceBadgeLabel(index)} - - - - {cleanDisplayTitle(source.title)} - - - p. {source.pageNumber ?? "n/a"} - · - {sourceSupportLabel(source, index)} - - - - - {index === 0 ? Open : null} - -
-
- ))} -
- {quoteText ? ( -
- “{quoteText}” -
- ) : null} -
- {primaryPreviewSource ? ( - - - Open source page - - ) : null} - {quoteText ? ( - - ) : null} -
-
- - {reviewDueSource ? : } - {reviewDueSource - ? `${sourceBadgeLabel(previewSources.indexOf(reviewDueSource))} review due` - : "Sources current"} - - {primaryPreviewSource ? ( - - Evidence details - - - ) : null} -
- - ); -} - -function NaturalLanguageAnswer({ - text, - sourceCount, - weakEvidence, - grounded, - sourceOnly, - bestSource, - sources, - sourceLinks, - copied, - onCopy, -}: { - text: string; - sourceCount: number; - weakEvidence: boolean; - grounded: boolean; - sourceOnly: boolean; - bestSource: BestSourceRecommendation | null; - sources: SearchResult[]; - sourceLinks: SourceLink[]; - copied: boolean; - onCopy: () => void; -}) { - const [sourcePreviewOpen, setSourcePreviewOpen] = useState(false); - const [copiedSourceQuote, setCopiedSourceQuote] = useState(false); - const sourceCapsuleRef = useRef(null); - const copySourceQuoteTimerRef = useRef(null); - const usePreviewSheet = useMobilePreviewSheet(); - useEffect(() => { - return () => { - if (copySourceQuoteTimerRef.current !== null) window.clearTimeout(copySourceQuoteTimerRef.current); - }; - }, []); - const cleaned = primaryAnswerDisplayText(text); - if (!cleaned) return null; - const capsuleText = sourceCapsuleText({ sourceCount, weakEvidence, grounded }); - const previewSources = capsulePreviewSources(bestSource, sources, sourceLinks); - const quoteText = sourceLinks.find((source) => source.snippet)?.snippet || bestSource?.quote || bestSource?.snippet; - const canOpenSourcePreview = previewSources.length > 0; - async function copySourceQuote() { - if (!quoteText) return; - try { - await navigator.clipboard.writeText(quoteText); - setCopiedSourceQuote(true); - if (copySourceQuoteTimerRef.current !== null) window.clearTimeout(copySourceQuoteTimerRef.current); - copySourceQuoteTimerRef.current = window.setTimeout(() => setCopiedSourceQuote(false), 1600); - } catch { - setCopiedSourceQuote(false); - } - } - const sourceCapsuleButton = ( - - ); - - return ( -
- -
-

- - - -

- {sourceOnly ? ( -

- Source-only answer — assembled from your documents without the AI model, so it may be less complete. Verify - it against the cited passages below. -

- ) : null} - {sourceCapsuleButton} - {sourcePreviewOpen && canOpenSourcePreview && !usePreviewSheet ? ( -
- -
- ) : null} - setSourcePreviewOpen(false)} - title="Sources" - description="Open the original PDF page." - titleAccessory={ - - {sourcePreviewPageCountLabel(previewSources)} - - } - closeLabel="Close answer sources" - contentClassName="sm:max-w-xl" - returnFocusRef={sourceCapsuleRef} - portal - > -
- -
-
-
- -
-
-
- ); -} - -function UserQuestionBubble({ query }: { query: string }) { - const cleaned = query.trim(); - if (!cleaned) return null; - - return ( -
-
-

{cleaned}

-

9:14 AM

-
-
- ); -} - -type KeyClinicalItem = { - id: string; - label?: string; - detail: string; -}; - -function keyClinicalItemFromText(item: string): KeyClinicalItem | null { - const cleaned = item.replace(/^[-*•]\s*/, "").trim(); - if (cleaned.length < 24) return null; - const [labelCandidate, ...detailParts] = cleaned.split(/\s+(?:—|-)\s+/); - const label = labelCandidate?.trim(); - const detail = detailParts.join(" — ").trim(); - const id = comparableAnswerText(cleaned); - if (label && detail && label.length <= 64) return { id, label, detail }; - return { id, detail: cleaned }; -} - -function keyClinicalItemsFromSections( - sections: Array, -): KeyClinicalItem[] { - const usefulKinds = new Set([ - "required_actions", - "monitoring_timing", - "medication_dose", - "thresholds", - "escalation_risk", - "contraindications_cautions", - "comparison", - ]); - return sections - .filter((section) => usefulKinds.has(section.kind)) - .flatMap((section) => - section.body - .split(/\n+|(?<=\.)\s+(?=(?:Monitor|Check|Use|Avoid|Escalate|Withhold|Review|Document|Repeat|Consider)\b)/) - .map((item) => keyClinicalItemFromText(item)) - .filter((item): item is KeyClinicalItem => Boolean(item)), - ) - .filter((item, index, items) => items.findIndex((candidate) => candidate.id === item.id) === index) - .slice(0, 5); -} - -function keyClinicalItemsFromTable(item: VisualEvidenceCard | null): KeyClinicalItem[] { - const rows = item?.tableRows?.filter((row) => row.some((cell) => cell.trim())) ?? []; - if (rows.length < 2) return []; - - return rows - .slice(0, 3) - .map((row): KeyClinicalItem | null => { - const [domain, baseline] = row.map((cell) => cell.trim()).filter(Boolean); - if (!domain || !baseline) return null; - const detail = baseline; - return { - id: comparableAnswerText([domain, detail].join(" ")), - label: domain, - detail, - }; - }) - .filter((value): value is KeyClinicalItem => value !== null) - .slice(0, 5); -} - -function KeyClinicalItems({ - sections, - table, -}: { - sections: Array; - table: VisualEvidenceCard | null; -}) { - const sectionItems = keyClinicalItemsFromSections(sections); - const tableItems = keyClinicalItemsFromTable(table); - const items = sectionItems.length >= 2 ? sectionItems : tableItems; - if (items.length < 2) return null; - - return ( -
-

Key monitoring items

-
    - {items.map((item) => ( -
  • - {item.label ? ( - <> - {item.label} - - - - ) : ( - - )} -
  • - ))} -
-
- ); -} - -type AnswerSupportPriority = { - title: string; - detail: string; - sourceLabel?: string; - tone: "priority" | "caution"; -}; - -function answerSupportPriority( - answer: RagAnswer, - sections: Array, - table: VisualEvidenceCard | null, - safetyFindings: ReturnType, - options: { grounded: boolean; weakEvidence: boolean }, -): AnswerSupportPriority | null { - const firstSafetyFinding = safetyFindings[0]; - if (firstSafetyFinding) { - return { - title: "Priority", - detail: formatSafetyFindingLabel(firstSafetyFinding), - sourceLabel: "S1", - tone: "caution", - }; - } - - if (answer.answerQualityTier === "source_only" || !options.grounded || options.weakEvidence) { - return { - title: "Review source match", - detail: - "Verify cited passages before using clinical numbers, monitoring, dose, route, timing, or risk decisions.", - sourceLabel: "Review", - tone: "caution", - }; - } - - const sectionItems = keyClinicalItemsFromSections(sections); - const tableItems = keyClinicalItemsFromTable(table); - const item = sectionItems[0] ?? tableItems[0] ?? null; - if (!item) return null; - - return { - title: item.label ?? "Priority", - detail: item.detail, - sourceLabel: "S1", - tone: "priority", - }; -} - -function AnswerSupportSummaryCard({ - priority, - clinicalCount, - evidenceSummary, - clinicalAvailable, - evidenceAvailable, - clinicalTriggerRef, - evidenceTriggerRef, - onOpenClinicalNotes, - onOpenEvidence, -}: { - priority: AnswerSupportPriority | null; - clinicalCount: number; - evidenceSummary: string; - clinicalAvailable: boolean; - evidenceAvailable: boolean; - clinicalTriggerRef?: RefObject; - evidenceTriggerRef?: RefObject; - onOpenClinicalNotes: () => void; - onOpenEvidence: () => void; -}) { - const supportRowCount = Number(clinicalAvailable) + Number(evidenceAvailable); - const supportButtonClass = - "grid min-h-[72px] grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 px-3 py-3 text-left transition hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[color:var(--focus)]"; - - return ( -
- {priority ? ( -
- -
-

{priority.title}

-

{priority.detail}

-
- {priority.sourceLabel ? ( - {priority.sourceLabel} - ) : null} -
- ) : null} - - {supportRowCount > 0 ? ( -
- {clinicalAvailable ? ( - - ) : null} - {evidenceAvailable ? ( - - ) : null} -
- ) : null} -
- ); -} - -function comparableAnswerText(value: string) { - return value - .replace(/\*\*/g, "") - .replace(/\.\.\.$/, "") - .toLowerCase() - .replace(/[^a-z0-9]+/g, " ") - .trim(); -} - -function isRedundantStructuredItem(item: string, primaryAnswer: string) { - const itemText = comparableAnswerText(item); - const answerText = comparableAnswerText(primaryAnswer); - if (!itemText || !answerText) return false; - if (answerText.includes(itemText) || itemText.includes(answerText)) return true; - if (itemText.length < 40) return false; - const answerWords = new Set(answerText.split(" ").filter((word) => word.length > 3)); - const itemWords = itemText.split(" ").filter((word) => word.length > 3); - if (itemWords.length < 6) return false; - const sharedWords = itemWords.filter((word) => answerWords.has(word)).length; - if (sharedWords / itemWords.length >= 0.82) return true; - return answerText.includes(itemText.slice(0, Math.min(160, itemText.length))); -} - -type ClinicalDetailSection = ReturnType[number]; - -function displayItemsForClinicalDetailSection( - section: ClinicalDetailSection, - primaryAnswer: string, - showLead: boolean, -) { - if (showLead) return section.items; - const nonRedundantItems = section.items.filter((item) => !isRedundantStructuredItem(item, primaryAnswer)); - return nonRedundantItems.length > 0 || section.items.length === 0 ? nonRedundantItems : section.items; -} - -const clinicalDetailPriority: Record = { - action: 10, - escalation: 20, - thresholds: 30, - cautions: 40, - monitoring: 50, - medication: 60, - documentation: 70, - comparison: 80, - "support-map": 90, - "source-gap": 100, -}; - -function clinicalDetailContentCount(section: ClinicalDetailSection) { - if (section.items.length > 0) return section.items.length; - const tableRows = - section.tables?.reduce((total, table) => total + (table.rows?.length ?? (table.markdown ? 1 : 0)), 0) ?? 0; - return tableRows || section.tables?.length || 0; -} - -function sortClinicalDetailSections(sections: ClinicalDetailSection[]) { - return [...sections].sort((left, right) => { - const leftPriority = clinicalDetailPriority[left.id] ?? 75; - const rightPriority = clinicalDetailPriority[right.id] ?? 75; - if (leftPriority !== rightPriority) return leftPriority - rightPriority; - return left.title.localeCompare(right.title); - }); -} - -function clinicalDetailMeta(section: ClinicalDetailSection): { - icon: typeof Search; - eyebrow: string; - toneClassName: string; - accentClassName: string; -} { - if (section.id === "thresholds") { - return { - icon: Target, - eyebrow: "Thresholds", - toneClassName: toneWarning, - accentClassName: "bg-[color:var(--warning)]", - }; - } - if (section.id === "escalation" || section.id === "cautions" || section.id === "source-gap") { - return { - icon: ShieldAlert, - eyebrow: section.id === "source-gap" ? "Source gap" : "Risk", - toneClassName: toneDanger, - accentClassName: "bg-[color:var(--danger)]", - }; - } - if (section.id === "monitoring" || section.id === "medication") { - return { - icon: ClipboardCheck, - eyebrow: section.id === "monitoring" ? "Monitoring" : "Medication", - toneClassName: toneWarning, - accentClassName: "bg-[color:var(--warning)]", - }; - } - if (section.id === "support-map" || section.id === "comparison") { - return { - icon: BookOpen, - eyebrow: section.id === "support-map" ? "Evidence support" : "Comparison", - toneClassName: toneInfo, - accentClassName: "bg-[color:var(--info)]", - }; - } - if (section.id === "documentation") { - return { - icon: FileText, - eyebrow: "Documentation", - toneClassName: toneNeutral, - accentClassName: "bg-[color:var(--border-strong)]", - }; - } - return { - icon: ListChecks, - eyebrow: "Clinical action", - toneClassName: toneSuccess, - accentClassName: "bg-[color:var(--success)]", - }; -} - -function clinicalDetailSummaryItems(sections: ClinicalDetailSection[]) { - const countById = (ids: string[]) => - sections - .filter((section) => ids.includes(section.id)) - .reduce((total, section) => total + clinicalDetailContentCount(section), 0); - const tableCount = sections.reduce((total, section) => total + (section.tables?.length ?? 0), 0); - const items = [ - { label: "Actions", value: countById(["action", "escalation", "documentation"]) }, - { label: "Monitoring", value: countById(["monitoring", "medication"]) }, - { label: "Tables", value: tableCount }, - { label: "Cautions", value: countById(["cautions", "source-gap"]) }, - { label: "Evidence", value: countById(["support-map", "comparison"]) }, - ]; - return items.filter((item) => item.value > 0); -} - -type ClinicalNotesTabId = "essentials" | "actions" | "safety"; - -type ClinicalNotesRow = { - id: string; - title: string; - detail: string; - sourceIndex: number; - tone: "safe" | "warn"; -}; - -const clinicalNotesTabMeta: Record< - ClinicalNotesTabId, - { label: string; icon: typeof ShieldCheck; sectionIds: string[] } -> = { - essentials: { - label: "Essentials", - icon: ClipboardCheck, - sectionIds: ["thresholds", "monitoring", "medication", "support-map", "comparison"], - }, - actions: { - label: "Actions", - icon: Activity, - sectionIds: ["action", "documentation", "monitoring", "medication"], - }, - safety: { - label: "Safety", - icon: ShieldCheck, - sectionIds: ["escalation", "cautions", "source-gap", "thresholds"], - }, -}; - -function compactClinicalNoteText(value: string) { - return normalizeExtractedGlyphs(value) - .replace(/\*\*/g, "") - .replace(/\s*\[\d+(?:,\s*\d+)*\]\s*/g, " ") - .replace(/\s+/g, " ") - .trim(); -} - -function stripClinicalNoteLeadIn(value: string) { - let text = compactClinicalNoteText(value); - let previous = ""; - while (text !== previous) { - previous = text; - text = text - .replace(/^(the\s+same\s+)?synthetic\s+source\s+says\s+/i, "") - .replace(/^the\s+(indexed\s+)?source\s+says\s+/i, "") - .replace(/^source\s+text\s+says\s+/i, "") - .replace(/^according\s+to\s+[^,]+,\s*/i, "") - .trim(); - } - return text; -} - -function titleCaseClinicalNote(value: string) { - return value - .replace(/\b\w[\w/-]*/g, (word) => { - if (/[A-Z]{2,}|\/|\d/.test(word)) return word; - return `${word.charAt(0).toUpperCase()}${word.slice(1).toLowerCase()}`; - }) - .replace(/\bAnd\b/g, "and") - .replace(/\bOr\b/g, "or") - .replace(/\bTo\b/g, "to"); -} - -function sentenceCaseClinicalNoteDetail(value: string) { - const text = stripClinicalNoteLeadIn(value); - return text ? `${text.charAt(0).toUpperCase()}${text.slice(1)}` : text; -} - -function clinicalNoteHeuristicTitle(value: string) { - const text = stripClinicalNoteLeadIn(value); - const lower = text.toLowerCase(); - - if (/\bbaseline checklist\b/.test(lower) && /\bconfirm indication\b/.test(lower)) return "Indication"; - if (/\b(vomiting|diarrhoea|diarrhea|dehydration|acute kidney injury|tremor|confusion|ataxia)\b/.test(lower)) { - return "Toxicity review triggers"; - } - if ( - /\b(escalate|urgent review|urgent|red flag|seizures?|severe constipation|chest pain|dyspnoea|tachycardia)\b/.test( - lower, - ) - ) { - return "Escalation triggers"; - } - if (/\blithium levels?\b/.test(lower) && /\b(5\s*(?:to|-|–)\s*7|dose change|stable|days?)\b/.test(lower)) { - return "Lithium level timing"; - } - if (/\b(lithium level|serum lithium|trough level)\b/.test(lower)) return "Lithium level check"; - if (/\b(fbc|anc)\b/.test(lower)) return "FBC/ANC monitoring"; - if (/\bmyocarditis\b/.test(lower)) return "Myocarditis screening"; - if (/\b(metabolic|weight|lipids?|glucose|hba1c|waist)\b/.test(lower)) return "Metabolic monitoring"; - if (/\b(constipation|bowel)\b/.test(lower)) return "Constipation prevention"; - if (/\b(shared-care|shared care|communication|handover)\b/.test(lower)) return "Shared-care communication"; - if (/\b(renal|kidney|creatinine|egfr)\b/.test(lower)) return "Renal function"; - if (/\b(thyroid|tsh)\b/.test(lower)) return "Thyroid monitoring"; - if (/\bcalcium\b/.test(lower)) return "Calcium monitoring"; - if (/\b(nsaid|ace inhibitor|diuretic|interacting medicine|medicine reconciliation)\b/.test(lower)) { - return "Interacting medicines"; - } - - return null; -} - -function clinicalNoteTitleFromItem(item: string, section: ClinicalDetailSection, index: number) { - const text = stripClinicalNoteLeadIn(item); - const heuristicTitle = clinicalNoteHeuristicTitle(text); - if (heuristicTitle) return heuristicTitle; - const colonIndex = text.indexOf(":"); - if (colonIndex > 8 && colonIndex < 54) { - const title = text.slice(0, colonIndex).trim(); - const detailStart = text - .slice(colonIndex + 1) - .split(/[,;]/)[0] - ?.trim(); - if (/\b(checklist|checkpoint|points?)\b/i.test(title) && detailStart) { - return clinicalNoteTitleFromFragment(detailStart); - } - return title; - } - const dashIndex = text.search(/\s[-–]\s/); - if (dashIndex > 8 && dashIndex < 54) return text.slice(0, dashIndex).trim(); - if (section.items.length === 1 && section.title.length <= 42) return section.title; - const words = text - .replace(/^(confirm|check|review|record|document)\s+/i, "") - .split(" ") - .filter(Boolean); - return words.slice(0, Math.min(words.length, index === 0 ? 5 : 4)).join(" ") || section.title; -} - -function clinicalNoteDetailFromItem(item: string, title: string) { - const text = stripClinicalNoteLeadIn(item); - const normalizedTitle = title.toLowerCase(); - const lowerText = text.toLowerCase(); - const colonIndex = text.indexOf(":"); - if (colonIndex > 8 && colonIndex < 64) { - const beforeColon = text.slice(0, colonIndex); - const afterColon = text.slice(colonIndex + 1).trim(); - if (/\b(checklist|checkpoint|points?)\b/i.test(beforeColon) && afterColon) { - return sentenceCaseClinicalNoteDetail(afterColon); - } - } - if (lowerText.startsWith(`${normalizedTitle}:`)) { - return sentenceCaseClinicalNoteDetail(text.slice(title.length + 1).trim()); - } - if (lowerText.startsWith(`${normalizedTitle} -`) || lowerText.startsWith(`${normalizedTitle} –`)) { - return sentenceCaseClinicalNoteDetail(text.slice(title.length + 2).trim()); - } - if (text === title) return "Review linked source context before using this note."; - return sentenceCaseClinicalNoteDetail(text); -} - -function clinicalNoteTitleFromFragment(fragment: string) { - const text = stripClinicalNoteLeadIn(fragment) - .replace(/^(and|or)\s+/i, "") - .replace(/^(confirm|check|review|record|document)\s+/i, "") - .replace(/[.;:,]+$/g, ""); - if (!text) return "Clinical note"; - return clinicalNoteHeuristicTitle(text) ?? titleCaseClinicalNote(text); -} - -function splitClinicalNoteFragments(item: string, section: ClinicalDetailSection, title: string) { - const detail = clinicalNoteDetailFromItem(item, title); - const titleLooksGeneric = /\b(checkpoint|checklist|item|point|monitoring|safety)\b/i.test(title); - const itemLooksGeneric = /\b(checkpoint|checklist|item|point|monitoring|safety)\b/i.test( - stripClinicalNoteLeadIn(item), - ); - if (!titleLooksGeneric && !itemLooksGeneric && section.items.length > 1) return null; - - const fragments = detail - .replace(/\band\s+/gi, "") - .split(/[,;]\s+/) - .map((fragment) => compactClinicalNoteText(fragment).replace(/[.;:,]+$/g, "")) - .filter((fragment) => fragment.length > 5); - - return fragments.length >= 3 ? fragments.slice(0, 5) : null; -} - -function clinicalNoteToneForText(text: string, fallback: ClinicalNotesRow["tone"]) { - if (/\b(toxicity|toxic|warning|caution|urgent|red flag|adverse|confusion|ataxia|tremor)\b/i.test(text)) { - return "warn"; - } - return fallback; -} - -function clinicalNoteHasDistinctDetail(row: ClinicalNotesRow) { - const title = compactClinicalNoteText(row.title).toLowerCase(); - const detail = compactClinicalNoteText(row.detail).toLowerCase(); - return Boolean(detail) && detail !== title; -} - -function clinicalNoteDetailLabel(row: ClinicalNotesRow) { - const text = `${row.title} ${row.detail}`.toLowerCase(); - if (/\b(timing|level|schedule|dose change|stable|days?)\b/.test(text)) return "Timing"; - if (/\b(escalation|escalate|urgent|toxicity|trigger|vomiting|confusion|ataxia|tremor)\b/.test(text)) { - return "Escalate"; - } - if (/\b(baseline|confirm|record|document|check|review)\b/.test(text)) return "Action"; - return "Note"; -} - -function ClinicalNoteDetailCard({ row }: { row: ClinicalNotesRow }) { - const detail = sentenceCaseClinicalNoteDetail(row.detail); - return ( -
-

- {clinicalNoteDetailLabel(row)}: - {detail} -

-
- ); -} - -function clinicalNotesTableEvidenceCount(answer: RagAnswer) { - return (answer.visualEvidence ?? answer.smartPanel?.visualEvidence ?? []).filter( - (item) => item.accessibleTableMarkdown || item.tableRows?.length, - ).length; -} - -function clinicalNotesRowsForTab(sections: ClinicalDetailSection[], tab: ClinicalNotesTabId) { - const meta = clinicalNotesTabMeta[tab]; - const rows: ClinicalNotesRow[] = []; - let sourceIndex = 1; - - for (const section of sections) { - const sectionText = `${section.title} ${section.items.join(" ")}`.toLowerCase(); - const hasMonitoringText = - (tab === "actions" || tab === "essentials") && - /\b(monitor|screen|level|fbc|anc|metabolic|renal|thyroid|function)\b/i.test(sectionText); - const hasSafetyText = - tab === "safety" && - /\b(toxicity|toxic|urgent|caution|contraindication|red flag|escalat|warning|review due)\b/i.test(sectionText); - if (!meta.sectionIds.includes(section.id) && !hasMonitoringText) { - if (!hasSafetyText) continue; - } - if (tab === "essentials" && section.id === "action" && rows.length >= 2) { - continue; - } - const tone: ClinicalNotesRow["tone"] = section.id === "escalation" || section.id === "cautions" ? "warn" : "safe"; - - for (const item of section.items.slice(0, 4)) { - if (section.tables?.length && /\b(table|showing domains|table showing)\b/i.test(item)) continue; - const title = clinicalNoteTitleFromItem(item, section, rows.length); - const fragments = splitClinicalNoteFragments(item, section, title); - if (fragments) { - for (const fragment of fragments) { - const fragmentTitle = clinicalNoteTitleFromFragment(fragment); - rows.push({ - id: `${tab}:${section.id}:${rows.length}:${fragmentTitle}`, - title: fragmentTitle, - detail: fragment, - sourceIndex: sourceIndex++, - tone: clinicalNoteToneForText(fragment, tone), - }); - } - } else { - rows.push({ - id: `${tab}:${section.id}:${rows.length}:${title}`, - title, - detail: clinicalNoteDetailFromItem(item, title), - sourceIndex: sourceIndex++, - tone: clinicalNoteToneForText(item, tone), - }); - } - } - } - - return rows.slice(0, 6); -} - -function clinicalNotesAvailableTabs(sections: ClinicalDetailSection[]) { - return (Object.keys(clinicalNotesTabMeta) as ClinicalNotesTabId[]) - .map((id) => ({ id, ...clinicalNotesTabMeta[id], count: clinicalNotesRowsForTab(sections, id).length })) - .filter((tab) => tab.count > 0); -} - -function clinicalNotesDetailSectionsForAnswer(answer: RagAnswer, viewMode: AnswerViewMode) { - const sections = - viewMode === "high_yield" ? buildHighYieldClinicalOutputSections(answer) : buildClinicalOutputSections(answer); - const primaryAnswer = plainAnswerText(answer.answer); - return sortClinicalDetailSections( - sections - .filter((section) => section.id !== "verify-source" && section.id !== "bottom-line") - .map((section) => ({ - ...section, - items: displayItemsForClinicalDetailSection(section, primaryAnswer, false), - })) - .filter((section) => section.items.length > 0), - ); -} - -function clinicalNotesDisplayCountForAnswer(answer: RagAnswer, viewMode: AnswerViewMode, fallback: number) { - const tabs = clinicalNotesAvailableTabs(clinicalNotesDetailSectionsForAnswer(answer, viewMode)); - const largestTabCount = tabs.reduce((largest, tab) => Math.max(largest, tab.count), 0); - return Math.max(1, largestTabCount || fallback); -} - -function ClinicalNotesChecklistPanel({ - answer, - viewMode, - evidenceMapRows, - bestSource, - copied, - onCopy, - onOpenTables, -}: { - answer: RagAnswer; - viewMode: AnswerViewMode; - evidenceMapRows: AnswerEvidenceMapRow[]; - bestSource: BestSourceRecommendation | null; - copied: boolean; - onCopy: () => void; - onOpenTables?: () => void; -}) { - const detailSections = clinicalNotesDetailSectionsForAnswer(answer, viewMode); - const tabs = clinicalNotesAvailableTabs(detailSections); - const defaultTab = tabs.find((tab) => tab.id === "actions")?.id ?? tabs[0]?.id ?? "actions"; - const [requestedTab, setRequestedTab] = useState(defaultTab); - const activeTab = tabs.some((tab) => tab.id === requestedTab) ? requestedTab : defaultTab; - const rows = clinicalNotesRowsForTab(detailSections, activeTab); - const tableEvidenceCount = clinicalNotesTableEvidenceCount(answer); - const [added, setAdded] = useState(false); - const warningRows = clinicalNotesRowsForTab(detailSections, "safety"); - const warningCount = warningRows.filter((row) => row.tone === "warn").length || warningRows.length; - - if (!tabs.length || rows.length === 0) { - return ( - - ); - } - - const activeMeta = clinicalNotesTabMeta[activeTab]; - - return ( -
-
-
- {tabs.map((tab) => { - const selected = tab.id === activeTab; - return ( - - ); - })} -
-
- -
-

- {activeMeta.label} ({rows.length}) -

- {tableEvidenceCount > 0 && onOpenTables ? ( - - ) : null} -
- -
- {rows.map((row) => { - const hasDistinctDetail = clinicalNoteHasDistinctDetail(row); - const RowIcon = row.tone === "warn" ? AlertCircle : activeTab === "actions" ? Activity : CheckCircle2; - return ( -
- -
-
-

- {row.title} -

- - {row.tone === "warn" ? "Review" : activeTab === "actions" ? "Action" : "Source"} - -
- {hasDistinctDetail ? ( -

{row.detail}

- ) : null} -
-
- - S{row.sourceIndex} - - -
-
- ); - })} -
- - {warningCount > 0 && activeTab !== "safety" ? ( - - ) : null} - -
-
- {bestSource ? ( - - - Source - - ) : ( - - - Source - - )} - - -
-
-
- ); -} - -function SafetyFindingsPanel({ findings }: { findings: ReturnType }) { - if (findings.length === 0) return null; - - return ( -
- -
- {findings.map((finding, index) => ( -
-
- - {finding.label} - - - - Source - -
-

{finding.text}

-

- {formatCitationLabel(finding.citation)} -

-
- ))} -
-
- ); -} - -function EvidenceGapPanel({ - relevance, - sources, - query, -}: { - relevance?: EvidenceRelevance | null; - sources: SearchResult[]; - query: string; -}) { - if (!relevance || relevance.isSourceBacked) return null; - const closestSources = sources.slice(0, 3); - const found = relevance.matchedTerms.length - ? relevance.matchedTerms.slice(0, 6).join(", ") - : "Only weak neighboring passages were retrieved."; - const missing = relevance.missingTerms.length - ? relevance.missingTerms.slice(0, 6).join(", ") - : "No direct indexed passage covered the full question."; - - return ( -
- } - /> -
-
-

What was found

-

{found}

-
-
-

- What was not found -

-

{missing}

-
-
-

Closest sources

- {closestSources.length ? ( -
- {closestSources.map((source) => ( - - - {cleanDisplayTitle(source.title)} - - ))} -
- ) : ( -

No nearby indexed sources were returned.

- )} -
-
-

- Suggested next search/upload -

-

- Try a narrower query using the missing terms, scope to a likely document, or upload/index the guideline that - directly covers "{query.trim()}". -

-
-
-
- ); -} - -function EvidenceCounts({ - answer, - sourceSummary, - sourceCount, -}: { - answer: RagAnswer; - sourceSummary?: EvidenceSummary; - sourceCount: number; -}) { - const counts = [ - { - label: "Citations", - value: answer.citations.length, - }, - { - label: "Quotes", - value: answer.quoteCards?.length ?? sourceSummary?.quote_count ?? 0, - }, - { - label: "Images", - value: - answer.visualEvidence?.length ?? answer.smartPanel?.visualEvidence?.length ?? sourceSummary?.image_count ?? 0, - }, - { - label: "Passages", - value: sourceCount || sourceSummary?.total_sources || 0, - }, - ]; - - return ( -
- {counts.map((item) => ( -
-

{item.value}

-

{item.label}

-
- ))} -
- ); -} - -function AnswerSourceStatus({ - source, - weakEvidence, -}: { - source: BestSourceRecommendation | null | undefined; - weakEvidence: boolean; -}) { - const metadata = source?.source_metadata; - return ( -
-
-

Source status

- -
- - {weakEvidence ? ( -

- Evidence support is limited. Treat this as a source-finding result until the linked passage is verified. -

- ) : null} -
- ); -} - -function EvidenceSummaryCard({ - answer, - bestSource, - grounded, - relevance, - sourceSummary, - weakEvidence, - sources, - gaps, - onScopeDocument, - compact = false, - supporting = false, -}: { - answer: RagAnswer; - bestSource: BestSourceRecommendation | null; - grounded: boolean; - relevance?: EvidenceRelevance | null; - sourceSummary?: EvidenceSummary; - weakEvidence: boolean; - sources: SearchResult[]; - gaps: ConflictOrGap[]; - onScopeDocument: (documentId: string) => void; - compact?: boolean; - supporting?: boolean; -}) { - const sourceLabel = relevance && !relevance.isSourceBacked ? "Closest source" : "Top source"; - const supportLabel = relevanceChipLabel(relevance, grounded); - const sourceStrength = bestSource?.source_strength ?? sourceSummary?.source_strength ?? "none"; - const gapMessage = - gaps[0]?.message ?? - (!relevance?.isSourceBacked && relevance?.supportReason - ? relevance.supportReason - : (sourceSummary?.summary ?? null)); - - return ( - - ); -} - -function compactEvidenceSummary( - answer: RagAnswer, - sources: SearchResult[], - sourceSummary?: EvidenceSummary, - renderModel?: AnswerRenderModel, -) { - const support = - renderModel?.trust === "high" - ? "Strong support" - : renderModel?.trust === "medium" - ? "Supported" - : renderModel?.trust === "low" - ? "Limited support" - : "Review support"; - const claimCount = renderModel?.evidenceRows.length || answer.answerSections?.length || answer.citations.length; - const quoteCount = renderModel?.quoteCards.length ?? answer.quoteCards?.length ?? sourceSummary?.quote_count ?? 0; - const tableCount = (renderModel?.visualEvidence ?? answer.visualEvidence ?? []).filter( - (item) => item.accessibleTableMarkdown || item.tableRows?.length, - ).length; - const sourceCount = renderModel?.primarySources.length || sourceSummary?.total_sources || sources.length; - const countParts = [ - claimCount > 0 ? `${claimCount} claim${claimCount === 1 ? "" : "s"}` : null, - quoteCount > 0 ? `${quoteCount} quote${quoteCount === 1 ? "" : "s"}` : null, - tableCount > 0 ? `${tableCount} table${tableCount === 1 ? "" : "s"}` : null, - ].filter((part): part is string => Boolean(part)); - - if (countParts.length === 0 && sourceCount > 0) { - countParts.push(`${sourceCount} source${sourceCount === 1 ? "" : "s"}`); - } - - return [support, ...countParts].join(" · "); -} - -type EvidenceTabName = "Claims" | "Quotes" | "Tables" | "Images" | "Gaps"; - -function renderModelAllows(renderModel: AnswerRenderModel, block: AnswerRenderModel["allowedBlocks"][number]) { - return renderModel.allowedBlocks.includes(block); -} - -function evidenceTabOrder(_answer: RagAnswer, renderModel: AnswerRenderModel): EvidenceTabName[] { - const order: EvidenceTabName[] = ["Claims", "Quotes", "Tables", "Images", "Gaps"]; - return order.filter((tab) => { - if (tab === "Tables") { - return ( - renderModelAllows(renderModel, "visualEvidence") && - renderModel.visualEvidence.some((item) => item.accessibleTableMarkdown || item.tableRows?.length) - ); - } - if (tab === "Images") return renderModelAllows(renderModel, "visualEvidence"); - if (tab === "Quotes") return renderModelAllows(renderModel, "quoteCards"); - if (tab === "Gaps") return renderModel.warnings.length > 0; - return renderModelAllows(renderModel, "evidenceMap") || renderModelAllows(renderModel, "reviewSources"); - }); -} - -function evidenceTabCount({ - tab, - sources, - visualEvidence, - answerEvidenceMapRows, - renderModel, -}: { - tab: EvidenceTabName; - sources: SearchResult[]; - visualEvidence: VisualEvidenceCard[]; - answerEvidenceMapRows: AnswerEvidenceMapRow[]; - renderModel: AnswerRenderModel; -}) { - if (tab === "Tables") { - return visualEvidence.filter((item) => item.accessibleTableMarkdown || item.tableRows?.length).length; - } - if (tab === "Claims") - return ( - answerEvidenceMapRows.length || - renderModel.evidenceRows.length || - sources.length || - renderModel.primarySources.length - ); - if (tab === "Images") return visualEvidence.length; - if (tab === "Quotes") return renderModel.quoteCards.length; - return renderModel.warnings.length; -} - -function clinicalNotesCount(answer: RagAnswer) { - return buildHighYieldClinicalOutputSections(answer).filter((section) => - ["action", "escalation", "thresholds", "cautions", "monitoring", "medication", "source-gap"].includes(section.id), - ).length; -} - -function answerHasCentralTable(answer: RagAnswer) { - return ( - answer.queryClass === "table_threshold" || - answer.responseMode === "threshold_table" || - Boolean(answer.visualEvidence?.some((item) => item.accessibleTableMarkdown || item.tableRows?.length)) - ); -} - -function primaryVisualTable(answer: RagAnswer) { - return answer.visualEvidence?.find((item) => item.accessibleTableMarkdown || item.tableRows?.length) ?? null; -} - -type RenderModelPdfSource = { - document_id: string; - title: string; - file_name: string; - page_number: number | null; - chunk_id: string | null; -}; - -function uniquePdfSourcesForRenderModel(renderModel: AnswerRenderModel): RenderModelPdfSource[] { - return renderModel.primarySources.map((source) => ({ - document_id: source.document_id, - title: source.title, - file_name: source.file_name, - page_number: source.page_number, - chunk_id: source.chunk_id, - })); -} - -function queryModeLabel(mode: ClinicalQueryMode) { - return clinicalQueryModeOptions.find((option) => option.value === mode)?.label ?? mode.replaceAll("_", " "); -} - -function AnswerInsightBar({ - answer, - bestSource, - relevance, - queryMode, - sourceGovernanceWarnings, -}: { - answer: RagAnswer; - bestSource: BestSourceRecommendation | null; - relevance?: EvidenceRelevance | null; - queryMode: ClinicalQueryMode; - sourceGovernanceWarnings: SourceGovernanceWarning[]; -}) { - const frontendGovernanceWarnings = frontendSourceGovernanceWarnings(sourceGovernanceWarnings); - const metadata = normalizeSourceMetadata( - bestSource?.source_metadata ?? answer.sources?.[0]?.source_metadata ?? answer.citations?.[0]?.source_metadata, - ); - const modeLabel = - answer.smartApiPlan?.displayMode?.replaceAll("_", " ") ?? - answer.responseMode?.replaceAll("_", " ") ?? - queryModeLabel(queryMode); - const sourceCount = answer.evidenceSummary?.total_sources ?? answer.sources?.length ?? answer.citations.length; - const support = relevanceChipLabel(relevance ?? answer.relevance, answer.grounded); - const sourceStatus = frontendGovernanceWarnings.length - ? `${frontendGovernanceWarnings.length} source status note${frontendGovernanceWarnings.length === 1 ? "" : "s"}` - : sourceStatusLabel(metadata); - const retrievalGate = answer.retrievalDiagnostics?.gateStatus; - const items = [ - { label: "Mode", value: modeLabel, icon: SlidersHorizontal }, - { - label: "Support", - value: support, - icon: hasStrongRelevanceIcon(relevance ?? answer.relevance, answer.grounded) ? CheckCircle2 : AlertCircle, - }, - { label: "Sources", value: String(sourceCount), icon: FileText }, - { label: "Confidence", value: answer.confidence, icon: Target }, - { - label: "Retrieval", - value: retrievalGate ? `${retrievalGate} gate` : "Not logged", - icon: retrievalGate === "blocked" ? ShieldAlert : CheckCircle2, - }, - { label: "Status", value: `${sourceStatus} / ${validationStatusLabel(metadata)}`, icon: BookOpen }, - ]; - - return ( -
- {items.map((item) => { - const Icon = item.icon; - return ( - - - - {item.label} - - {item.value} - - ); - })} -
- ); -} - -function EvidenceVerificationStrip({ - answer, - bestSource, - sourceSummary, - weakEvidence, - governanceWarningCount, -}: { - answer: RagAnswer; - bestSource: BestSourceRecommendation | null; - sourceSummary?: EvidenceSummary | null; - weakEvidence: boolean; - governanceWarningCount: number; -}) { - const metadata = normalizeSourceMetadata( - bestSource?.source_metadata ?? answer.sources?.[0]?.source_metadata ?? answer.citations?.[0]?.source_metadata, - ); - const sourceCount = sourceSummary?.total_sources ?? answer.sources?.length ?? answer.citations.length; - const citationCount = answer.citations.length; - const gapCount = answer.conflictsOrGaps?.length ?? answer.smartPanel?.conflictsOrGaps?.length ?? 0; - const retrievalGateBlocked = answer.retrievalDiagnostics?.gateStatus === "blocked"; - const checks = [ - { - label: "Citations", - value: citationCount ? `${citationCount} citation${citationCount === 1 ? "" : "s"}` : "None", - ready: citationCount > 0, - }, - { - label: "Sources", - value: `${sourceCount} source${sourceCount === 1 ? "" : "s"}`, - ready: sourceCount > 0, - }, - { - label: "Source status", - value: sourceStatusLabel(metadata), - ready: metadata.document_status === "current" && !governanceWarningCount, - }, - { - label: "Retrieval gate", - value: retrievalGateBlocked ? "Blocked for low signal" : answer.retrievalDiagnostics ? "Passed" : "Not available", - ready: !retrievalGateBlocked, - }, - { - label: "Gaps", - value: governanceWarningCount - ? `${governanceWarningCount} status note${governanceWarningCount === 1 ? "" : "s"}` - : gapCount - ? `${gapCount} gap${gapCount === 1 ? "" : "s"}` - : "None", - ready: !weakEvidence && !gapCount && !governanceWarningCount, - }, - ]; - - return ( -
-
- {checks.map((check) => ( -
-
- {check.ready ? ( - - ) : ( - - )} -

- {check.label} -

-
-

{check.value}

-
- ))} -
-
- - Pinned source - - {bestSource ? ( - <> - {bestSource.title} - - - ) : ( - No pinned source yet - )} -
-
- ); -} - -const answerFeedbackOptions: Array<{ - type: AnswerFeedbackType; - label: string; - icon: typeof CheckCircle2; - tone: "success" | "warning" | "danger" | "neutral"; -}> = [ - { type: "verified", label: "Verified", icon: CheckCircle2, tone: "success" }, - { type: "needs_correction", label: "Needs correction", icon: AlertCircle, tone: "warning" }, - { type: "source_insufficient", label: "Source insufficient", icon: ShieldAlert, tone: "warning" }, - { type: "wrong_source", label: "Wrong source", icon: FileText, tone: "danger" }, - { type: "missing_source", label: "Missing source", icon: Search, tone: "warning" }, - { type: "unsupported_answer", label: "Unsupported answer", icon: ShieldAlert, tone: "danger" }, - { type: "numeric_error", label: "Numeric error", icon: Target, tone: "danger" }, - { type: "outdated_guidance", label: "Outdated guidance", icon: RefreshCw, tone: "warning" }, -]; - -function feedbackToneClass(tone: "success" | "warning" | "danger" | "neutral") { - if (tone === "success") return toneSuccess; - if (tone === "warning") return toneWarning; - if (tone === "danger") return toneDanger; - return toneNeutral; -} - -function AnswerFeedbackPanel({ - pending, - onSubmit, -}: { - pending: AnswerFeedbackType | null; - onSubmit: (feedbackType: AnswerFeedbackType) => void; -}) { - return ( -
-
-
-

Answer review

-

- Capture misses for retrieval and RAG evals without changing the answer. -

-
- {pending ? ( - - - Saving - - ) : null} -
-
- {answerFeedbackOptions.map((item) => { - const Icon = item.icon; - return ( - - ); - })} -
-
- ); -} - -function RenderModelSourceList({ - sources, - query, - onScopeDocument, -}: { - sources: SourceLink[]; - query: string; - onScopeDocument: (documentId: string) => void; -}) { - if (sources.length === 0) { - return ( - - ); - } - - return ( -
- {sources.map((source, index) => { - const metadata = normalizeSourceMetadata(source.sourceMetadata); - const snippet = compactSourceSnippet(source.snippet ?? "", { dropTitle: source.title }); - const openLabel = `Open source ${index + 1}: ${cleanDisplayTitle(source.title)}${query ? ` for ${query}` : ""}`; - return ( -
- -
-
- {snippet ?

{snippet}

: null} - -
- -
-
- ); - })} -
- ); -} - -function VerificationWorkspace({ - renderModel, - query, - answerEvidenceMapRows, - pendingFeedback, - onSubmitFeedback, - onScopeDocument, -}: { - renderModel: AnswerRenderModel; - query: string; - answerEvidenceMapRows: AnswerEvidenceMapRow[]; - pendingFeedback: AnswerFeedbackType | null; - onSubmitFeedback: (feedbackType: AnswerFeedbackType) => void; - onScopeDocument: (documentId: string) => void; -}) { - const verificationSources = renderModel.primarySources.slice(0, renderModel.trust === "unsupported" ? 3 : 6); - return ( -
-
- -
-

Section support map

-

- Each answer section should resolve back to a linked cited passage before clinical use. -

-
- -
-
-
-
-
-

Cited source excerpts

-

- Open the document to inspect the PDF page and highlighted indexed passage. -

-
- -
-
- ); -} - -function AnswerViewModeControl({ - value, - onChange, -}: { - value: AnswerViewMode; - onChange: (mode: AnswerViewMode) => void; -}) { - const modes: Array<{ value: AnswerViewMode; label: string; shortLabel: string; icon: typeof Search }> = [ - { value: "standard", label: "Standard", shortLabel: "All", icon: ListChecks }, - { value: "high_yield", label: "High-yield", shortLabel: "Key", icon: Target }, - { value: "evidence_map", label: "Evidence map", shortLabel: "Map", icon: BookOpen }, - ]; - - return ( -
- {modes.map((mode) => { - const Icon = mode.icon; - const active = value === mode.value; - return ( - - ); - })} -
- ); -} - -const simpleClinicalTableProps = { - compact: false, - expandOnMobile: true, -} as const; - -function compactEvidenceCell(value: string | null | undefined, max = 140) { - const text = value ? value.replace(/\s+/g, " ").trim() : ""; - return text.length > max ? `${text.slice(0, max - 1).trim()}…` : text; -} - -function evidenceMapRowsFromRenderModel(renderModel: AnswerRenderModel): AnswerEvidenceMapRow[] { - return renderModel.evidenceRows.map((row, index) => ({ - id: row.id || `${row.source.chunk_id}:${index}`, - section: row.section || "Source evidence", - detail: - sourceTextForCompactDisplay(row.quote || row.source.snippet || row.source.reason || "") || - cleanDisplayTitle(row.source.title), - supportLevel: row.supportLevel || row.source.sourceStrength, - citationCount: 1, - sourceStatus: - row.source.sourceStrength === "none" ? "Source requires review" : `${row.source.sourceStrength} source support`, - bestSourceLabel: row.source.label, - bestLinkedPassage: row.quote || row.source.snippet || row.source.reason, - href: row.source.href, - })); -} - -function EvidenceMapTable({ rows }: { rows: AnswerEvidenceMapRow[] }) { - if (rows.length === 0) { - return ; - } - - const tableRows = rows.map((row) => [ - compactEvidenceCell(row.section), - row.supportLevel, - String(row.citationCount), - compactEvidenceCell(row.sourceStatus), - compactEvidenceCell(row.bestSourceLabel, 72), - row.bestLinkedPassage || "Open source passage.", - ]); - const linkedRows = rows.filter((row) => row.href); - - return ( -
- - {linkedRows.length ? ( -
- {linkedRows.map((row) => ( - - - {row.section} - {row.bestSourceLabel} - - - Open source - - - - ))} -
- ) : null} -
- ); -} - -function AnswerSafetyNotice({ - demoMode, - weakEvidence = false, - retrievalDiagnostics, -}: { - demoMode: boolean; - weakEvidence?: boolean; - retrievalDiagnostics?: RagAnswer["retrievalDiagnostics"]; -}) { - const retrievalGateBlocked = retrievalDiagnostics?.gateStatus === "blocked"; - return ( -
-

- {weakEvidence - ? "Weak source support; verify the linked source before relying on this answer." - : "Draft only; verify source first before pasting into the medical record."} -

- {retrievalGateBlocked ? ( -

- Retrieval confidence gate was triggered (low-confidence retrieval signal). Expand evidence details before - using this result. -

- ) : null} - {demoMode ? ( -

- Synthetic demo only: this is not clinical guidance. -

- ) : null} -
- ); -} - -function QuoteCards({ - quotes, - copiedQuotes, - onCopyQuotes, - onFollowUp, - onScopeDocument, -}: { - quotes: QuoteCard[]; - copiedQuotes: boolean; - onCopyQuotes: () => void; - onFollowUp?: (quote: QuoteCard) => void; - onScopeDocument: (documentId: string) => void; -}) { - return ( -
- 0 ? ( - - ) : null - } - /> - {quotes.length === 0 ? ( - - ) : ( -
- {quotes.map((quote, index) => { - const quoteText = sourceTextForVerbatimQuote(quote.quote); - const quoteTitle = cleanDisplayTitle(quote.title); - return ( -
-
- - {index + 1} - - -
-
- “{quoteText}” -
-
- - {formatCompactCitationLabel(quote)} - - - {quoteTitle}, page {quote.page_number ?? "n/a"} - -
- onFollowUp(quote) : undefined} - divider={false} - /> -
-
-
- ); - })} -
- )} -
- ); -} - -function formatQuoteCardsForClipboard(quotes: QuoteCard[]) { - return quotes - .map((quote, index) => - [ - // Clean the copied text the same way the card displays it, so clipboard - // output never contains internal image-data blocks or glyph artifacts. - `${index + 1}. "${sourceTextForVerbatimQuote(quote.quote)}"`, - `Source: ${formatCitationLabel(quote)}`, - `Link: ${documentCitationHref(quote)}`, - ].join("\n"), - ) - .join("\n\n"); -} - -function ClinicalOutputPanel({ +export function ClinicalOutputPanel({ answer, collapsed = false, showLead = true, diff --git a/src/components/clinical-dashboard/answer-content.tsx b/src/components/clinical-dashboard/answer-content.tsx new file mode 100644 index 000000000..9140030b2 --- /dev/null +++ b/src/components/clinical-dashboard/answer-content.tsx @@ -0,0 +1,797 @@ +"use client"; + +/* eslint-disable @next/next/no-img-element */ + +import Link from "next/link"; +import { memo, useEffect, useRef, useState } from "react"; +import { + AlertCircle, + CheckCircle2, + ChevronDown, + Copy, + ExternalLink, + Loader2, + ShieldCheck, + Sparkles, +} from "lucide-react"; + +import { SafeBoldText } from "@/components/SafeBoldText"; +import { Sheet } from "@/components/ui/sheet"; +import { + chatActionRow, + chatAnswerText, + chatMicroAction, + cn, + sourceCapsule, + statusDotMuted, + statusDotReady, + statusDotReview, + subtleStatusPill, + textMuted, +} from "@/components/ui-primitives"; +import { sourceResultHref } from "@/components/clinical-dashboard/source-actions"; +import { + cleanDisplayTitle, + comparableAnswerText, + sanitizeAnswerDisplayText, +} from "@/components/clinical-dashboard/display-text"; +import { useMobilePreviewSheet } from "@/components/clinical-dashboard/use-mobile-preview-sheet"; +import { clearCachedSignedUrl, getCachedSignedUrl, setCachedSignedUrl } from "@/lib/signed-url-cache"; +import { normalizeSourceMetadata, sourceStatusLabel } from "@/lib/source-metadata"; +import { clinicalProseUsefulness } from "@/lib/source-text-sanitizer"; +import { + frontendSourceGovernanceWarnings, + groupSourceGovernanceWarnings, + type SourceGovernanceWarning, +} from "@/lib/source-governance"; +import { type SourceLink } from "@/lib/answer-render-policy"; +import { useAuthSession } from "@/lib/supabase/client"; +import type { + AnswerSection, + AnswerSectionKind, + BestSourceRecommendation, + SearchResult, + SearchScopeSummary, + VisualEvidenceCard, +} from "@/lib/types"; + +export const SourceImage = memo(function SourceImage({ + endpoint, + caption, + className = "max-h-52", +}: { + endpoint: string; + caption: string; + className?: string; +}) { + const [url, setUrl] = useState(() => getCachedSignedUrl(endpoint)?.url ?? null); + const [failed, setFailed] = useState(false); + const [attempt, setAttempt] = useState(0); + const { authorizationHeader, markSessionExpired } = useAuthSession(); + + useEffect(() => { + const cached = getCachedSignedUrl(endpoint); + if (cached) return () => undefined; + + let active = true; + fetch(endpoint, { headers: authorizationHeader }) + .then((response) => { + if (response.status === 401) markSessionExpired(); + return response.ok ? response.json() : null; + }) + .then((data) => { + if (active && data?.url) { + setCachedSignedUrl(endpoint, data); + setUrl(data.url); + setFailed(false); + } else if (active) { + setFailed(true); + } + }) + .catch(() => { + if (active) setFailed(true); + }); + return () => { + active = false; + }; + }, [attempt, authorizationHeader, endpoint, markSessionExpired]); + + function retryImage() { + clearCachedSignedUrl(endpoint); + setUrl(null); + setFailed(false); + setAttempt((current) => current + 1); + } + + function handleImageError() { + clearCachedSignedUrl(endpoint); + setFailed(true); + } + + if (failed) { + return ( +
+
+ + Image preview could not load. + +
+
+ ); + } + + if (!url) { + return ( +
+ + Loading image +
+ ); + } + + return ( + {caption} + ); +}); + +export function ScopeAndGovernanceNotice({ + scope, + warnings, +}: { + scope: SearchScopeSummary | null; + warnings: SourceGovernanceWarning[]; +}) { + const groupedWarnings = groupSourceGovernanceWarnings(frontendSourceGovernanceWarnings(warnings)).slice(0, 4); + const showScope = + Boolean(scope && scope.activeFilterCount > 0) || + Boolean(scope?.warnings?.length) || + scope?.matchedDocumentCount === 0; + if (!showScope && groupedWarnings.length === 0) return null; + return ( +
+ {showScope && scope ? ( +

+ Scope: {scope.summary} + {scope.queryMode && scope.queryMode !== "auto" ? ` · ${scope.queryMode.replaceAll("_", " ")}` : ""} +

+ ) : null} + {scope?.warnings?.length ? ( +
    + {scope.warnings.slice(0, 3).map((warning) => ( +
  • {warning}
  • + ))} +
+ ) : null} + {groupedWarnings.length ? ( +
    + {groupedWarnings.map((warning) => ( +
  • + {warning.message} + {warning.titles.length ? ( +
    + Sources affected + {warning.titles.slice(0, 5).join(", ")} +
    + ) : null} +
  • + ))} +
+ ) : null} +
+ ); +} + +export function plainAnswerText(value: string) { + const useful = clinicalProseUsefulness(value); + return sanitizeAnswerDisplayText(useful.text || value, { minLength: 8, minTokens: 2 }) + .replace(/(?:\s*\n\s*)?Synthetic demo only:.*$/i, "") + .trim(); +} + +function primaryAnswerDisplayText(value: string) { + const cleaned = plainAnswerText(value); + const fragments = cleaned + .split(/\r?\n+/) + .flatMap((line: string) => + line.split(/(?<=[.!?])\s+(?=(?:[A-Z]|\*\*|If\b|When\b|Do\b|Use\b|Monitor\b|Escalate\b|Document\b))/), + ) + .map((fragment: string) => + fragment + .replace(/^(?:[-*•]|\d+[.)])\s+/, "") + .replace( + /^(?:\*\*)?(?:answer|summary|bottom line|direct answer|clinical point|key point|required actions?|monitoring(?:\/timing)?|thresholds?|dose detail|medication(?:\/dose details?)?|escalation(?:\/risk)?|risk|safety|documentation(?:\/forms)?|source gaps?)(?:\*\*)?:\s+/i, + "", + ) + .trim(), + ) + .map((fragment: string) => clinicalProseUsefulness(fragment).text || fragment) + .filter((fragment: string) => { + if (!fragment) return false; + const useful = clinicalProseUsefulness(fragment); + return useful.useful || fragment.split(/\s+/).length >= 8; + }); + const uniqueFragments = Array.from(new Set(fragments)); + const selected = uniqueFragments.slice(0, 3).join(" "); + const words = selected.split(/\s+/).filter(Boolean); + if (words.length <= 85) return selected || cleaned; + return `${words + .slice(0, 85) + .join(" ") + .replace(/[;,:-]\s*$/, "")}...`; +} + +function sourceCapsuleText({ + sourceCount, + weakEvidence, + grounded, +}: { + sourceCount: number; + weakEvidence: boolean; + grounded: boolean; +}) { + if (sourceCount <= 0) return "No direct source found"; + if (!grounded) return "Review nearby sources"; + if (weakEvidence) return "Review sources"; + return `${sourceCount} source${sourceCount === 1 ? "" : "s"}`; +} + +export function sourceStatusDotClass(metadata: ReturnType | null | undefined) { + if (!metadata) return statusDotMuted; + if (metadata.document_status === "current") return statusDotReady; + if (metadata.document_status === "review_due" || metadata.document_status === "outdated") return statusDotReview; + return statusDotMuted; +} + +type CapsulePreviewSource = { + id: string; + title: string; + pageNumber: number | null; + metadata: ReturnType; + score: number; + href: string; + snippet?: string; + sourceStrength?: + SourceLink["sourceStrength"] | BestSourceRecommendation["source_strength"] | SearchResult["source_strength"]; +}; + +function sourceBadgeLabel(index: number) { + return `S${index + 1}`; +} + +function sourceBadgeToneClass(metadata: ReturnType, index: number) { + if (metadata.document_status === "review_due" || metadata.document_status === "outdated") { + return "border-[color:var(--warning-border)] bg-[color:var(--warning-soft)] text-[color:var(--warning)]"; + } + if (index === 0) { + return "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent)] text-[color:var(--clinical-accent-contrast)]"; + } + return "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]"; +} + +function sourceSupportLabel(source: CapsulePreviewSource, index: number) { + if (!source.sourceStrength || source.sourceStrength === "none") return "Unsupported"; + if (source.sourceStrength === "limited") return "Partial"; + if (source.sourceStrength === "moderate") return "Partial"; + if (index === 0 || source.sourceStrength === "strong") return "Direct"; + return "Partial"; +} + +function sourceStatusShortLabel(metadata: ReturnType) { + if (metadata.document_status === "review_due") return "Review due"; + if (metadata.document_status === "outdated") return "Outdated"; + if (metadata.document_status === "current") return "Current"; + return sourceStatusLabel(metadata); +} + +function sourcePreviewPageCountLabel(previewSources: CapsulePreviewSource[]) { + const uniquePages = new Set(previewSources.map((source) => source.pageNumber).filter((page) => page !== null)); + const count = uniquePages.size || previewSources.length; + return `${count} page${count === 1 ? "" : "s"}`; +} + +function capsulePreviewSources( + bestSource: BestSourceRecommendation | null, + sources: SearchResult[], + sourceLinks: SourceLink[] = [], +) { + const rows: CapsulePreviewSource[] = []; + const seen = new Set(); + const pushRow = (row: CapsulePreviewSource) => { + const key = `${row.id}:${row.title}:${row.pageNumber ?? "n/a"}`; + if (seen.has(key)) return; + seen.add(key); + rows.push(row); + }; + + sourceLinks.slice(0, 5).forEach((source) => { + pushRow({ + id: source.chunk_id, + title: source.title || source.file_name || "Source", + pageNumber: source.page_number, + metadata: normalizeSourceMetadata(source.sourceMetadata), + score: source.score ?? 0, + href: source.href, + snippet: source.snippet, + sourceStrength: source.sourceStrength, + }); + }); + + if (bestSource) { + pushRow({ + id: bestSource.chunk_id, + title: bestSource.title || bestSource.file_name || "Source", + pageNumber: bestSource.page_number, + metadata: normalizeSourceMetadata(bestSource.source_metadata), + score: bestSource.score, + href: bestSource.viewer_href, + sourceStrength: bestSource.source_strength, + }); + } + + sources.slice(0, 5).forEach((source) => { + pushRow({ + id: source.id, + title: source.title || source.file_name || "Source", + pageNumber: source.page_number, + metadata: normalizeSourceMetadata(source.source_metadata), + score: source.hybrid_score ?? source.similarity ?? source.lexical_score ?? 0, + href: sourceResultHref(source), + sourceStrength: source.source_strength, + }); + }); + + return rows.slice(0, 4); +} + +function SourcePreviewContent({ + previewSources, + quoteText, + copiedQuote, + onCopyQuote, + showHeader = true, +}: { + previewSources: CapsulePreviewSource[]; + quoteText?: string | null; + copiedQuote: boolean; + onCopyQuote: () => void; + showHeader?: boolean; +}) { + const primaryPreviewSource = previewSources[0] ?? null; + const reviewDueSource = previewSources.find( + (source) => source.metadata.document_status === "review_due" || source.metadata.document_status === "outdated", + ); + + return ( + <> + {showHeader ? ( +
+
+
+

Sources

+ + {sourcePreviewPageCountLabel(previewSources)} + +
+

Open the original PDF page.

+
+
+ ) : null} +
+ {previewSources.map((source, index) => ( +
+ {index === 0 ? ( +

+ + Best match +

+ ) : index === 1 ? ( +

Also used

+ ) : null} +
+ + {sourceBadgeLabel(index)} + + + + {cleanDisplayTitle(source.title)} + + + p. {source.pageNumber ?? "n/a"} + · + {sourceSupportLabel(source, index)} + + + + + {index === 0 ? Open : null} + +
+
+ ))} +
+ {quoteText ? ( +
+ “{quoteText}” +
+ ) : null} +
+ {primaryPreviewSource ? ( + + + Open source page + + ) : null} + {quoteText ? ( + + ) : null} +
+
+ + {reviewDueSource ? : } + {reviewDueSource + ? `${sourceBadgeLabel(previewSources.indexOf(reviewDueSource))} review due` + : "Sources current"} + + {primaryPreviewSource ? ( + + Evidence details + + + ) : null} +
+ + ); +} + +export function NaturalLanguageAnswer({ + text, + sourceCount, + weakEvidence, + grounded, + sourceOnly, + bestSource, + sources, + sourceLinks, + copied, + onCopy, +}: { + text: string; + sourceCount: number; + weakEvidence: boolean; + grounded: boolean; + sourceOnly: boolean; + bestSource: BestSourceRecommendation | null; + sources: SearchResult[]; + sourceLinks: SourceLink[]; + copied: boolean; + onCopy: () => void; +}) { + const [sourcePreviewOpen, setSourcePreviewOpen] = useState(false); + const [copiedSourceQuote, setCopiedSourceQuote] = useState(false); + const sourceCapsuleRef = useRef(null); + const copySourceQuoteTimerRef = useRef(null); + const usePreviewSheet = useMobilePreviewSheet(); + useEffect(() => { + return () => { + if (copySourceQuoteTimerRef.current !== null) window.clearTimeout(copySourceQuoteTimerRef.current); + }; + }, []); + const cleaned = primaryAnswerDisplayText(text); + if (!cleaned) return null; + const capsuleText = sourceCapsuleText({ sourceCount, weakEvidence, grounded }); + const previewSources = capsulePreviewSources(bestSource, sources, sourceLinks); + const quoteText = sourceLinks.find((source) => source.snippet)?.snippet || bestSource?.quote || bestSource?.snippet; + const canOpenSourcePreview = previewSources.length > 0; + async function copySourceQuote() { + if (!quoteText) return; + try { + await navigator.clipboard.writeText(quoteText); + setCopiedSourceQuote(true); + if (copySourceQuoteTimerRef.current !== null) window.clearTimeout(copySourceQuoteTimerRef.current); + copySourceQuoteTimerRef.current = window.setTimeout(() => setCopiedSourceQuote(false), 1600); + } catch { + setCopiedSourceQuote(false); + } + } + const sourceCapsuleButton = ( + + ); + + return ( +
+ +
+

+ + + +

+ {sourceOnly ? ( +

+ Source-only answer — assembled from your documents without the AI model, so it may be less complete. Verify + it against the cited passages below. +

+ ) : null} + {sourceCapsuleButton} + {sourcePreviewOpen && canOpenSourcePreview && !usePreviewSheet ? ( +
+ +
+ ) : null} + setSourcePreviewOpen(false)} + title="Sources" + description="Open the original PDF page." + titleAccessory={ + + {sourcePreviewPageCountLabel(previewSources)} + + } + closeLabel="Close answer sources" + contentClassName="sm:max-w-xl" + returnFocusRef={sourceCapsuleRef} + portal + > +
+ +
+
+
+ +
+
+
+ ); +} + +export function UserQuestionBubble({ query }: { query: string }) { + const cleaned = query.trim(); + if (!cleaned) return null; + + return ( +
+
+

{cleaned}

+

9:14 AM

+
+
+ ); +} + +type KeyClinicalItem = { + id: string; + label?: string; + detail: string; +}; + +function keyClinicalItemFromText(item: string): KeyClinicalItem | null { + const cleaned = item.replace(/^[-*•]\s*/, "").trim(); + if (cleaned.length < 24) return null; + const [labelCandidate, ...detailParts] = cleaned.split(/\s+(?:—|-)\s+/); + const label = labelCandidate?.trim(); + const detail = detailParts.join(" — ").trim(); + const id = comparableAnswerText(cleaned); + if (label && detail && label.length <= 64) return { id, label, detail }; + return { id, detail: cleaned }; +} + +export function keyClinicalItemsFromSections( + sections: Array, +): KeyClinicalItem[] { + const usefulKinds = new Set([ + "required_actions", + "monitoring_timing", + "medication_dose", + "thresholds", + "escalation_risk", + "contraindications_cautions", + "comparison", + ]); + return sections + .filter((section) => usefulKinds.has(section.kind)) + .flatMap((section) => + section.body + .split(/\n+|(?<=\.)\s+(?=(?:Monitor|Check|Use|Avoid|Escalate|Withhold|Review|Document|Repeat|Consider)\b)/) + .map((item) => keyClinicalItemFromText(item)) + .filter((item): item is KeyClinicalItem => Boolean(item)), + ) + .filter((item, index, items) => items.findIndex((candidate) => candidate.id === item.id) === index) + .slice(0, 5); +} + +export function keyClinicalItemsFromTable(item: VisualEvidenceCard | null): KeyClinicalItem[] { + const rows = item?.tableRows?.filter((row) => row.some((cell) => cell.trim())) ?? []; + if (rows.length < 2) return []; + + return rows + .slice(0, 3) + .map((row): KeyClinicalItem | null => { + const [domain, baseline] = row.map((cell) => cell.trim()).filter(Boolean); + if (!domain || !baseline) return null; + const detail = baseline; + return { + id: comparableAnswerText([domain, detail].join(" ")), + label: domain, + detail, + }; + }) + .filter((value): value is KeyClinicalItem => value !== null) + .slice(0, 5); +} + +function KeyClinicalItems({ + sections, + table, +}: { + sections: Array; + table: VisualEvidenceCard | null; +}) { + const sectionItems = keyClinicalItemsFromSections(sections); + const tableItems = keyClinicalItemsFromTable(table); + const items = sectionItems.length >= 2 ? sectionItems : tableItems; + if (items.length < 2) return null; + + return ( +
+

Key monitoring items

+
    + {items.map((item) => ( +
  • + {item.label ? ( + <> + {item.label} + + + + ) : ( + + )} +
  • + ))} +
+
+ ); +} diff --git a/src/components/clinical-dashboard/display-text.ts b/src/components/clinical-dashboard/display-text.ts index c599b295b..a21b08f88 100644 --- a/src/components/clinical-dashboard/display-text.ts +++ b/src/components/clinical-dashboard/display-text.ts @@ -241,3 +241,12 @@ export function sourceDisplayMeta(source: SearchResult, title: string) { Boolean(fileBase) && fileBaseNormalized !== titleBase && !fileBaseNormalized.startsWith(titleBase); return [includeFile ? source.file_name : null, `page ${source.page_number ?? "n/a"}`].filter(Boolean).join(" · "); } + +export function comparableAnswerText(value: string) { + return value + .replace(/\*\*/g, "") + .replace(/\.\.\.$/, "") + .toLowerCase() + .replace(/[^a-z0-9]+/g, " ") + .trim(); +} diff --git a/src/components/clinical-dashboard/evidence-panels.tsx b/src/components/clinical-dashboard/evidence-panels.tsx new file mode 100644 index 000000000..88e1138aa --- /dev/null +++ b/src/components/clinical-dashboard/evidence-panels.tsx @@ -0,0 +1,1935 @@ +"use client"; + +import Link from "next/link"; +import { type RefObject, useState } from "react"; +import { + Activity, + AlertCircle, + BookOpen, + CheckCircle2, + ChevronDown, + ClipboardCheck, + Copy, + ExternalLink, + FileText, + Filter, + Layers, + ListChecks, + Loader2, + Plus, + Quote, + RefreshCw, + Search, + ShieldAlert, + ShieldCheck, + SlidersHorizontal, + Table2, + Target, +} from "lucide-react"; + +import { AccessibleTable } from "@/components/AccessibleTable"; +import { ClinicalOutputPanel, clinicalQueryModeOptions, type AnswerFeedbackType } from "@/components/ClinicalDashboard"; +import { + keyClinicalItemsFromSections, + keyClinicalItemsFromTable, + plainAnswerText, + sourceStatusDotClass, +} from "@/components/clinical-dashboard/answer-content"; +import { CopyButton } from "@/components/clinical-dashboard/answer-status"; +import { StrengthBadge } from "@/components/clinical-dashboard/badges"; +import { SectionHeading } from "@/components/clinical-dashboard/dashboard-shell"; +import { + cleanDisplayTitle, + compactSourceSnippet, + comparableAnswerText, +} from "@/components/clinical-dashboard/display-text"; +import { + hasStrongRelevanceIcon, + QueryCoverageChips, + relevanceChipLabel, + RelevanceBadge, +} from "@/components/clinical-dashboard/relevance"; +import { SourceActionRow, sourceResultHref } from "@/components/clinical-dashboard/source-actions"; +import { + chatMicroAction, + clinicalDivider, + cn, + codeText, + EmptyState, + evidenceSurface, + floatingControl, + iconTilePremium, + metadataPill, + panelSubtle, + primaryControl, + proseMeasure, + raisedCard, + sourceCard, + SourceProvenance, + SourceStatusBadge, + subtleStatusPill, + tableMicroActionRow, + textMuted, + toneDanger, + toneInfo, + toneNeutral, + toneSuccess, + toneWarning, +} from "@/components/ui-primitives"; +import { type AnswerRenderModel, type SourceLink } from "@/lib/answer-render-policy"; +import { documentCitationHref, formatCitationLabel, formatCompactCitationLabel } from "@/lib/citations"; +import { extractSafetyFindings, formatSafetyFindingLabel } from "@/lib/clinical-safety"; +import { frontendSourceGovernanceWarnings, type SourceGovernanceWarning } from "@/lib/source-governance"; +import { normalizeSourceMetadata, sourceStatusLabel, validationStatusLabel } from "@/lib/source-metadata"; +import { + normalizeExtractedGlyphs, + sourceTextForCompactDisplay, + sourceTextForVerbatimQuote, +} from "@/lib/source-text-sanitizer"; +import type { + AnswerSection, + BestSourceRecommendation, + ClinicalQueryMode, + ConflictOrGap, + EvidenceRelevance, + EvidenceSummary, + QuoteCard, + RagAnswer, + SearchResult, + VisualEvidenceCard, +} from "@/lib/types"; +import { emptyStates } from "@/lib/ui-copy"; +import { + type AnswerEvidenceMapRow, + type AnswerViewMode, + buildClinicalOutputSections, + buildHighYieldClinicalOutputSections, +} from "@/lib/ward-output"; + +type AnswerSupportPriority = { + title: string; + detail: string; + sourceLabel?: string; + tone: "priority" | "caution"; +}; + +export function answerSupportPriority( + answer: RagAnswer, + sections: Array, + table: VisualEvidenceCard | null, + safetyFindings: ReturnType, + options: { grounded: boolean; weakEvidence: boolean }, +): AnswerSupportPriority | null { + const firstSafetyFinding = safetyFindings[0]; + if (firstSafetyFinding) { + return { + title: "Priority", + detail: formatSafetyFindingLabel(firstSafetyFinding), + sourceLabel: "S1", + tone: "caution", + }; + } + + if (answer.answerQualityTier === "source_only" || !options.grounded || options.weakEvidence) { + return { + title: "Review source match", + detail: + "Verify cited passages before using clinical numbers, monitoring, dose, route, timing, or risk decisions.", + sourceLabel: "Review", + tone: "caution", + }; + } + + const sectionItems = keyClinicalItemsFromSections(sections); + const tableItems = keyClinicalItemsFromTable(table); + const item = sectionItems[0] ?? tableItems[0] ?? null; + if (!item) return null; + + return { + title: item.label ?? "Priority", + detail: item.detail, + sourceLabel: "S1", + tone: "priority", + }; +} + +export function AnswerSupportSummaryCard({ + priority, + clinicalCount, + evidenceSummary, + clinicalAvailable, + evidenceAvailable, + clinicalTriggerRef, + evidenceTriggerRef, + onOpenClinicalNotes, + onOpenEvidence, +}: { + priority: AnswerSupportPriority | null; + clinicalCount: number; + evidenceSummary: string; + clinicalAvailable: boolean; + evidenceAvailable: boolean; + clinicalTriggerRef?: RefObject; + evidenceTriggerRef?: RefObject; + onOpenClinicalNotes: () => void; + onOpenEvidence: () => void; +}) { + const supportRowCount = Number(clinicalAvailable) + Number(evidenceAvailable); + const supportButtonClass = + "grid min-h-[72px] grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 px-3 py-3 text-left transition hover:bg-[color:var(--surface-subtle)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-[color:var(--focus)]"; + + return ( +
+ {priority ? ( +
+ +
+

{priority.title}

+

{priority.detail}

+
+ {priority.sourceLabel ? ( + {priority.sourceLabel} + ) : null} +
+ ) : null} + + {supportRowCount > 0 ? ( +
+ {clinicalAvailable ? ( + + ) : null} + {evidenceAvailable ? ( + + ) : null} +
+ ) : null} +
+ ); +} + +function isRedundantStructuredItem(item: string, primaryAnswer: string) { + const itemText = comparableAnswerText(item); + const answerText = comparableAnswerText(primaryAnswer); + if (!itemText || !answerText) return false; + if (answerText.includes(itemText) || itemText.includes(answerText)) return true; + if (itemText.length < 40) return false; + const answerWords = new Set(answerText.split(" ").filter((word) => word.length > 3)); + const itemWords = itemText.split(" ").filter((word) => word.length > 3); + if (itemWords.length < 6) return false; + const sharedWords = itemWords.filter((word) => answerWords.has(word)).length; + if (sharedWords / itemWords.length >= 0.82) return true; + return answerText.includes(itemText.slice(0, Math.min(160, itemText.length))); +} + +type ClinicalDetailSection = ReturnType[number]; + +export function displayItemsForClinicalDetailSection( + section: ClinicalDetailSection, + primaryAnswer: string, + showLead: boolean, +) { + if (showLead) return section.items; + const nonRedundantItems = section.items.filter((item) => !isRedundantStructuredItem(item, primaryAnswer)); + return nonRedundantItems.length > 0 || section.items.length === 0 ? nonRedundantItems : section.items; +} + +const clinicalDetailPriority: Record = { + action: 10, + escalation: 20, + thresholds: 30, + cautions: 40, + monitoring: 50, + medication: 60, + documentation: 70, + comparison: 80, + "support-map": 90, + "source-gap": 100, +}; + +export function clinicalDetailContentCount(section: ClinicalDetailSection) { + if (section.items.length > 0) return section.items.length; + const tableRows = + section.tables?.reduce((total, table) => total + (table.rows?.length ?? (table.markdown ? 1 : 0)), 0) ?? 0; + return tableRows || section.tables?.length || 0; +} + +export function sortClinicalDetailSections(sections: ClinicalDetailSection[]) { + return [...sections].sort((left, right) => { + const leftPriority = clinicalDetailPriority[left.id] ?? 75; + const rightPriority = clinicalDetailPriority[right.id] ?? 75; + if (leftPriority !== rightPriority) return leftPriority - rightPriority; + return left.title.localeCompare(right.title); + }); +} + +export function clinicalDetailMeta(section: ClinicalDetailSection): { + icon: typeof Search; + eyebrow: string; + toneClassName: string; + accentClassName: string; +} { + if (section.id === "thresholds") { + return { + icon: Target, + eyebrow: "Thresholds", + toneClassName: toneWarning, + accentClassName: "bg-[color:var(--warning)]", + }; + } + if (section.id === "escalation" || section.id === "cautions" || section.id === "source-gap") { + return { + icon: ShieldAlert, + eyebrow: section.id === "source-gap" ? "Source gap" : "Risk", + toneClassName: toneDanger, + accentClassName: "bg-[color:var(--danger)]", + }; + } + if (section.id === "monitoring" || section.id === "medication") { + return { + icon: ClipboardCheck, + eyebrow: section.id === "monitoring" ? "Monitoring" : "Medication", + toneClassName: toneWarning, + accentClassName: "bg-[color:var(--warning)]", + }; + } + if (section.id === "support-map" || section.id === "comparison") { + return { + icon: BookOpen, + eyebrow: section.id === "support-map" ? "Evidence support" : "Comparison", + toneClassName: toneInfo, + accentClassName: "bg-[color:var(--info)]", + }; + } + if (section.id === "documentation") { + return { + icon: FileText, + eyebrow: "Documentation", + toneClassName: toneNeutral, + accentClassName: "bg-[color:var(--border-strong)]", + }; + } + return { + icon: ListChecks, + eyebrow: "Clinical action", + toneClassName: toneSuccess, + accentClassName: "bg-[color:var(--success)]", + }; +} + +export function clinicalDetailSummaryItems(sections: ClinicalDetailSection[]) { + const countById = (ids: string[]) => + sections + .filter((section) => ids.includes(section.id)) + .reduce((total, section) => total + clinicalDetailContentCount(section), 0); + const tableCount = sections.reduce((total, section) => total + (section.tables?.length ?? 0), 0); + const items = [ + { label: "Actions", value: countById(["action", "escalation", "documentation"]) }, + { label: "Monitoring", value: countById(["monitoring", "medication"]) }, + { label: "Tables", value: tableCount }, + { label: "Cautions", value: countById(["cautions", "source-gap"]) }, + { label: "Evidence", value: countById(["support-map", "comparison"]) }, + ]; + return items.filter((item) => item.value > 0); +} + +type ClinicalNotesTabId = "essentials" | "actions" | "safety"; + +type ClinicalNotesRow = { + id: string; + title: string; + detail: string; + sourceIndex: number; + tone: "safe" | "warn"; +}; + +const clinicalNotesTabMeta: Record< + ClinicalNotesTabId, + { label: string; icon: typeof ShieldCheck; sectionIds: string[] } +> = { + essentials: { + label: "Essentials", + icon: ClipboardCheck, + sectionIds: ["thresholds", "monitoring", "medication", "support-map", "comparison"], + }, + actions: { + label: "Actions", + icon: Activity, + sectionIds: ["action", "documentation", "monitoring", "medication"], + }, + safety: { + label: "Safety", + icon: ShieldCheck, + sectionIds: ["escalation", "cautions", "source-gap", "thresholds"], + }, +}; + +function compactClinicalNoteText(value: string) { + return normalizeExtractedGlyphs(value) + .replace(/\*\*/g, "") + .replace(/\s*\[\d+(?:,\s*\d+)*\]\s*/g, " ") + .replace(/\s+/g, " ") + .trim(); +} + +function stripClinicalNoteLeadIn(value: string) { + let text = compactClinicalNoteText(value); + let previous = ""; + while (text !== previous) { + previous = text; + text = text + .replace(/^(the\s+same\s+)?synthetic\s+source\s+says\s+/i, "") + .replace(/^the\s+(indexed\s+)?source\s+says\s+/i, "") + .replace(/^source\s+text\s+says\s+/i, "") + .replace(/^according\s+to\s+[^,]+,\s*/i, "") + .trim(); + } + return text; +} + +function titleCaseClinicalNote(value: string) { + return value + .replace(/\b\w[\w/-]*/g, (word) => { + if (/[A-Z]{2,}|\/|\d/.test(word)) return word; + return `${word.charAt(0).toUpperCase()}${word.slice(1).toLowerCase()}`; + }) + .replace(/\bAnd\b/g, "and") + .replace(/\bOr\b/g, "or") + .replace(/\bTo\b/g, "to"); +} + +function sentenceCaseClinicalNoteDetail(value: string) { + const text = stripClinicalNoteLeadIn(value); + return text ? `${text.charAt(0).toUpperCase()}${text.slice(1)}` : text; +} + +function clinicalNoteHeuristicTitle(value: string) { + const text = stripClinicalNoteLeadIn(value); + const lower = text.toLowerCase(); + + if (/\bbaseline checklist\b/.test(lower) && /\bconfirm indication\b/.test(lower)) return "Indication"; + if (/\b(vomiting|diarrhoea|diarrhea|dehydration|acute kidney injury|tremor|confusion|ataxia)\b/.test(lower)) { + return "Toxicity review triggers"; + } + if ( + /\b(escalate|urgent review|urgent|red flag|seizures?|severe constipation|chest pain|dyspnoea|tachycardia)\b/.test( + lower, + ) + ) { + return "Escalation triggers"; + } + if (/\blithium levels?\b/.test(lower) && /\b(5\s*(?:to|-|–)\s*7|dose change|stable|days?)\b/.test(lower)) { + return "Lithium level timing"; + } + if (/\b(lithium level|serum lithium|trough level)\b/.test(lower)) return "Lithium level check"; + if (/\b(fbc|anc)\b/.test(lower)) return "FBC/ANC monitoring"; + if (/\bmyocarditis\b/.test(lower)) return "Myocarditis screening"; + if (/\b(metabolic|weight|lipids?|glucose|hba1c|waist)\b/.test(lower)) return "Metabolic monitoring"; + if (/\b(constipation|bowel)\b/.test(lower)) return "Constipation prevention"; + if (/\b(shared-care|shared care|communication|handover)\b/.test(lower)) return "Shared-care communication"; + if (/\b(renal|kidney|creatinine|egfr)\b/.test(lower)) return "Renal function"; + if (/\b(thyroid|tsh)\b/.test(lower)) return "Thyroid monitoring"; + if (/\bcalcium\b/.test(lower)) return "Calcium monitoring"; + if (/\b(nsaid|ace inhibitor|diuretic|interacting medicine|medicine reconciliation)\b/.test(lower)) { + return "Interacting medicines"; + } + + return null; +} + +function clinicalNoteTitleFromItem(item: string, section: ClinicalDetailSection, index: number) { + const text = stripClinicalNoteLeadIn(item); + const heuristicTitle = clinicalNoteHeuristicTitle(text); + if (heuristicTitle) return heuristicTitle; + const colonIndex = text.indexOf(":"); + if (colonIndex > 8 && colonIndex < 54) { + const title = text.slice(0, colonIndex).trim(); + const detailStart = text + .slice(colonIndex + 1) + .split(/[,;]/)[0] + ?.trim(); + if (/\b(checklist|checkpoint|points?)\b/i.test(title) && detailStart) { + return clinicalNoteTitleFromFragment(detailStart); + } + return title; + } + const dashIndex = text.search(/\s[-–]\s/); + if (dashIndex > 8 && dashIndex < 54) return text.slice(0, dashIndex).trim(); + if (section.items.length === 1 && section.title.length <= 42) return section.title; + const words = text + .replace(/^(confirm|check|review|record|document)\s+/i, "") + .split(" ") + .filter(Boolean); + return words.slice(0, Math.min(words.length, index === 0 ? 5 : 4)).join(" ") || section.title; +} + +function clinicalNoteDetailFromItem(item: string, title: string) { + const text = stripClinicalNoteLeadIn(item); + const normalizedTitle = title.toLowerCase(); + const lowerText = text.toLowerCase(); + const colonIndex = text.indexOf(":"); + if (colonIndex > 8 && colonIndex < 64) { + const beforeColon = text.slice(0, colonIndex); + const afterColon = text.slice(colonIndex + 1).trim(); + if (/\b(checklist|checkpoint|points?)\b/i.test(beforeColon) && afterColon) { + return sentenceCaseClinicalNoteDetail(afterColon); + } + } + if (lowerText.startsWith(`${normalizedTitle}:`)) { + return sentenceCaseClinicalNoteDetail(text.slice(title.length + 1).trim()); + } + if (lowerText.startsWith(`${normalizedTitle} -`) || lowerText.startsWith(`${normalizedTitle} –`)) { + return sentenceCaseClinicalNoteDetail(text.slice(title.length + 2).trim()); + } + if (text === title) return "Review linked source context before using this note."; + return sentenceCaseClinicalNoteDetail(text); +} + +function clinicalNoteTitleFromFragment(fragment: string) { + const text = stripClinicalNoteLeadIn(fragment) + .replace(/^(and|or)\s+/i, "") + .replace(/^(confirm|check|review|record|document)\s+/i, "") + .replace(/[.;:,]+$/g, ""); + if (!text) return "Clinical note"; + return clinicalNoteHeuristicTitle(text) ?? titleCaseClinicalNote(text); +} + +function splitClinicalNoteFragments(item: string, section: ClinicalDetailSection, title: string) { + const detail = clinicalNoteDetailFromItem(item, title); + const titleLooksGeneric = /\b(checkpoint|checklist|item|point|monitoring|safety)\b/i.test(title); + const itemLooksGeneric = /\b(checkpoint|checklist|item|point|monitoring|safety)\b/i.test( + stripClinicalNoteLeadIn(item), + ); + if (!titleLooksGeneric && !itemLooksGeneric && section.items.length > 1) return null; + + const fragments = detail + .replace(/\band\s+/gi, "") + .split(/[,;]\s+/) + .map((fragment) => compactClinicalNoteText(fragment).replace(/[.;:,]+$/g, "")) + .filter((fragment) => fragment.length > 5); + + return fragments.length >= 3 ? fragments.slice(0, 5) : null; +} + +function clinicalNoteToneForText(text: string, fallback: ClinicalNotesRow["tone"]) { + if (/\b(toxicity|toxic|warning|caution|urgent|red flag|adverse|confusion|ataxia|tremor)\b/i.test(text)) { + return "warn"; + } + return fallback; +} + +function clinicalNoteHasDistinctDetail(row: ClinicalNotesRow) { + const title = compactClinicalNoteText(row.title).toLowerCase(); + const detail = compactClinicalNoteText(row.detail).toLowerCase(); + return Boolean(detail) && detail !== title; +} + +function clinicalNoteDetailLabel(row: ClinicalNotesRow) { + const text = `${row.title} ${row.detail}`.toLowerCase(); + if (/\b(timing|level|schedule|dose change|stable|days?)\b/.test(text)) return "Timing"; + if (/\b(escalation|escalate|urgent|toxicity|trigger|vomiting|confusion|ataxia|tremor)\b/.test(text)) { + return "Escalate"; + } + if (/\b(baseline|confirm|record|document|check|review)\b/.test(text)) return "Action"; + return "Note"; +} + +function ClinicalNoteDetailCard({ row }: { row: ClinicalNotesRow }) { + const detail = sentenceCaseClinicalNoteDetail(row.detail); + return ( +
+

+ {clinicalNoteDetailLabel(row)}: + {detail} +

+
+ ); +} + +function clinicalNotesTableEvidenceCount(answer: RagAnswer) { + return (answer.visualEvidence ?? answer.smartPanel?.visualEvidence ?? []).filter( + (item) => item.accessibleTableMarkdown || item.tableRows?.length, + ).length; +} + +function clinicalNotesRowsForTab(sections: ClinicalDetailSection[], tab: ClinicalNotesTabId) { + const meta = clinicalNotesTabMeta[tab]; + const rows: ClinicalNotesRow[] = []; + let sourceIndex = 1; + + for (const section of sections) { + const sectionText = `${section.title} ${section.items.join(" ")}`.toLowerCase(); + const hasMonitoringText = + (tab === "actions" || tab === "essentials") && + /\b(monitor|screen|level|fbc|anc|metabolic|renal|thyroid|function)\b/i.test(sectionText); + const hasSafetyText = + tab === "safety" && + /\b(toxicity|toxic|urgent|caution|contraindication|red flag|escalat|warning|review due)\b/i.test(sectionText); + if (!meta.sectionIds.includes(section.id) && !hasMonitoringText) { + if (!hasSafetyText) continue; + } + if (tab === "essentials" && section.id === "action" && rows.length >= 2) { + continue; + } + const tone: ClinicalNotesRow["tone"] = section.id === "escalation" || section.id === "cautions" ? "warn" : "safe"; + + for (const item of section.items.slice(0, 4)) { + if (section.tables?.length && /\b(table|showing domains|table showing)\b/i.test(item)) continue; + const title = clinicalNoteTitleFromItem(item, section, rows.length); + const fragments = splitClinicalNoteFragments(item, section, title); + if (fragments) { + for (const fragment of fragments) { + const fragmentTitle = clinicalNoteTitleFromFragment(fragment); + rows.push({ + id: `${tab}:${section.id}:${rows.length}:${fragmentTitle}`, + title: fragmentTitle, + detail: fragment, + sourceIndex: sourceIndex++, + tone: clinicalNoteToneForText(fragment, tone), + }); + } + } else { + rows.push({ + id: `${tab}:${section.id}:${rows.length}:${title}`, + title, + detail: clinicalNoteDetailFromItem(item, title), + sourceIndex: sourceIndex++, + tone: clinicalNoteToneForText(item, tone), + }); + } + } + } + + return rows.slice(0, 6); +} + +function clinicalNotesAvailableTabs(sections: ClinicalDetailSection[]) { + return (Object.keys(clinicalNotesTabMeta) as ClinicalNotesTabId[]) + .map((id) => ({ id, ...clinicalNotesTabMeta[id], count: clinicalNotesRowsForTab(sections, id).length })) + .filter((tab) => tab.count > 0); +} + +function clinicalNotesDetailSectionsForAnswer(answer: RagAnswer, viewMode: AnswerViewMode) { + const sections = + viewMode === "high_yield" ? buildHighYieldClinicalOutputSections(answer) : buildClinicalOutputSections(answer); + const primaryAnswer = plainAnswerText(answer.answer); + return sortClinicalDetailSections( + sections + .filter((section) => section.id !== "verify-source" && section.id !== "bottom-line") + .map((section) => ({ + ...section, + items: displayItemsForClinicalDetailSection(section, primaryAnswer, false), + })) + .filter((section) => section.items.length > 0), + ); +} + +export function clinicalNotesDisplayCountForAnswer(answer: RagAnswer, viewMode: AnswerViewMode, fallback: number) { + const tabs = clinicalNotesAvailableTabs(clinicalNotesDetailSectionsForAnswer(answer, viewMode)); + const largestTabCount = tabs.reduce((largest, tab) => Math.max(largest, tab.count), 0); + return Math.max(1, largestTabCount || fallback); +} + +export function ClinicalNotesChecklistPanel({ + answer, + viewMode, + evidenceMapRows, + bestSource, + copied, + onCopy, + onOpenTables, +}: { + answer: RagAnswer; + viewMode: AnswerViewMode; + evidenceMapRows: AnswerEvidenceMapRow[]; + bestSource: BestSourceRecommendation | null; + copied: boolean; + onCopy: () => void; + onOpenTables?: () => void; +}) { + const detailSections = clinicalNotesDetailSectionsForAnswer(answer, viewMode); + const tabs = clinicalNotesAvailableTabs(detailSections); + const defaultTab = tabs.find((tab) => tab.id === "actions")?.id ?? tabs[0]?.id ?? "actions"; + const [requestedTab, setRequestedTab] = useState(defaultTab); + const activeTab = tabs.some((tab) => tab.id === requestedTab) ? requestedTab : defaultTab; + const rows = clinicalNotesRowsForTab(detailSections, activeTab); + const tableEvidenceCount = clinicalNotesTableEvidenceCount(answer); + const [added, setAdded] = useState(false); + const warningRows = clinicalNotesRowsForTab(detailSections, "safety"); + const warningCount = warningRows.filter((row) => row.tone === "warn").length || warningRows.length; + + if (!tabs.length || rows.length === 0) { + return ( + + ); + } + + const activeMeta = clinicalNotesTabMeta[activeTab]; + + return ( +
+
+
+ {tabs.map((tab) => { + const selected = tab.id === activeTab; + return ( + + ); + })} +
+
+ +
+

+ {activeMeta.label} ({rows.length}) +

+ {tableEvidenceCount > 0 && onOpenTables ? ( + + ) : null} +
+ +
+ {rows.map((row) => { + const hasDistinctDetail = clinicalNoteHasDistinctDetail(row); + const RowIcon = row.tone === "warn" ? AlertCircle : activeTab === "actions" ? Activity : CheckCircle2; + return ( +
+ +
+
+

+ {row.title} +

+ + {row.tone === "warn" ? "Review" : activeTab === "actions" ? "Action" : "Source"} + +
+ {hasDistinctDetail ? ( +

{row.detail}

+ ) : null} +
+
+ + S{row.sourceIndex} + + +
+
+ ); + })} +
+ + {warningCount > 0 && activeTab !== "safety" ? ( + + ) : null} + +
+
+ {bestSource ? ( + + + Source + + ) : ( + + + Source + + )} + + +
+
+
+ ); +} + +export function SafetyFindingsPanel({ findings }: { findings: ReturnType }) { + if (findings.length === 0) return null; + + return ( +
+ +
+ {findings.map((finding, index) => ( +
+
+ + {finding.label} + + + + Source + +
+

{finding.text}

+

+ {formatCitationLabel(finding.citation)} +

+
+ ))} +
+
+ ); +} + +function EvidenceGapPanel({ + relevance, + sources, + query, +}: { + relevance?: EvidenceRelevance | null; + sources: SearchResult[]; + query: string; +}) { + if (!relevance || relevance.isSourceBacked) return null; + const closestSources = sources.slice(0, 3); + const found = relevance.matchedTerms.length + ? relevance.matchedTerms.slice(0, 6).join(", ") + : "Only weak neighboring passages were retrieved."; + const missing = relevance.missingTerms.length + ? relevance.missingTerms.slice(0, 6).join(", ") + : "No direct indexed passage covered the full question."; + + return ( +
+ } + /> +
+
+

What was found

+

{found}

+
+
+

+ What was not found +

+

{missing}

+
+
+

Closest sources

+ {closestSources.length ? ( +
+ {closestSources.map((source) => ( + + + {cleanDisplayTitle(source.title)} + + ))} +
+ ) : ( +

No nearby indexed sources were returned.

+ )} +
+
+

+ Suggested next search/upload +

+

+ Try a narrower query using the missing terms, scope to a likely document, or upload/index the guideline that + directly covers "{query.trim()}". +

+
+
+
+ ); +} + +function EvidenceCounts({ + answer, + sourceSummary, + sourceCount, +}: { + answer: RagAnswer; + sourceSummary?: EvidenceSummary; + sourceCount: number; +}) { + const counts = [ + { + label: "Citations", + value: answer.citations.length, + }, + { + label: "Quotes", + value: answer.quoteCards?.length ?? sourceSummary?.quote_count ?? 0, + }, + { + label: "Images", + value: + answer.visualEvidence?.length ?? answer.smartPanel?.visualEvidence?.length ?? sourceSummary?.image_count ?? 0, + }, + { + label: "Passages", + value: sourceCount || sourceSummary?.total_sources || 0, + }, + ]; + + return ( +
+ {counts.map((item) => ( +
+

{item.value}

+

{item.label}

+
+ ))} +
+ ); +} + +function AnswerSourceStatus({ + source, + weakEvidence, +}: { + source: BestSourceRecommendation | null | undefined; + weakEvidence: boolean; +}) { + const metadata = source?.source_metadata; + return ( +
+
+

Source status

+ +
+ + {weakEvidence ? ( +

+ Evidence support is limited. Treat this as a source-finding result until the linked passage is verified. +

+ ) : null} +
+ ); +} + +function EvidenceSummaryCard({ + answer, + bestSource, + grounded, + relevance, + sourceSummary, + weakEvidence, + sources, + gaps, + onScopeDocument, + compact = false, + supporting = false, +}: { + answer: RagAnswer; + bestSource: BestSourceRecommendation | null; + grounded: boolean; + relevance?: EvidenceRelevance | null; + sourceSummary?: EvidenceSummary; + weakEvidence: boolean; + sources: SearchResult[]; + gaps: ConflictOrGap[]; + onScopeDocument: (documentId: string) => void; + compact?: boolean; + supporting?: boolean; +}) { + const sourceLabel = relevance && !relevance.isSourceBacked ? "Closest source" : "Top source"; + const supportLabel = relevanceChipLabel(relevance, grounded); + const sourceStrength = bestSource?.source_strength ?? sourceSummary?.source_strength ?? "none"; + const gapMessage = + gaps[0]?.message ?? + (!relevance?.isSourceBacked && relevance?.supportReason + ? relevance.supportReason + : (sourceSummary?.summary ?? null)); + + return ( + + ); +} + +export function compactEvidenceSummary( + answer: RagAnswer, + sources: SearchResult[], + sourceSummary?: EvidenceSummary, + renderModel?: AnswerRenderModel, +) { + const support = + renderModel?.trust === "high" + ? "Strong support" + : renderModel?.trust === "medium" + ? "Supported" + : renderModel?.trust === "low" + ? "Limited support" + : "Review support"; + const claimCount = renderModel?.evidenceRows.length || answer.answerSections?.length || answer.citations.length; + const quoteCount = renderModel?.quoteCards.length ?? answer.quoteCards?.length ?? sourceSummary?.quote_count ?? 0; + const tableCount = (renderModel?.visualEvidence ?? answer.visualEvidence ?? []).filter( + (item) => item.accessibleTableMarkdown || item.tableRows?.length, + ).length; + const sourceCount = renderModel?.primarySources.length || sourceSummary?.total_sources || sources.length; + const countParts = [ + claimCount > 0 ? `${claimCount} claim${claimCount === 1 ? "" : "s"}` : null, + quoteCount > 0 ? `${quoteCount} quote${quoteCount === 1 ? "" : "s"}` : null, + tableCount > 0 ? `${tableCount} table${tableCount === 1 ? "" : "s"}` : null, + ].filter((part): part is string => Boolean(part)); + + if (countParts.length === 0 && sourceCount > 0) { + countParts.push(`${sourceCount} source${sourceCount === 1 ? "" : "s"}`); + } + + return [support, ...countParts].join(" · "); +} + +export type EvidenceTabName = "Claims" | "Quotes" | "Tables" | "Images" | "Gaps"; + +function renderModelAllows(renderModel: AnswerRenderModel, block: AnswerRenderModel["allowedBlocks"][number]) { + return renderModel.allowedBlocks.includes(block); +} + +export function evidenceTabOrder(_answer: RagAnswer, renderModel: AnswerRenderModel): EvidenceTabName[] { + const order: EvidenceTabName[] = ["Claims", "Quotes", "Tables", "Images", "Gaps"]; + return order.filter((tab) => { + if (tab === "Tables") { + return ( + renderModelAllows(renderModel, "visualEvidence") && + renderModel.visualEvidence.some((item) => item.accessibleTableMarkdown || item.tableRows?.length) + ); + } + if (tab === "Images") return renderModelAllows(renderModel, "visualEvidence"); + if (tab === "Quotes") return renderModelAllows(renderModel, "quoteCards"); + if (tab === "Gaps") return renderModel.warnings.length > 0; + return renderModelAllows(renderModel, "evidenceMap") || renderModelAllows(renderModel, "reviewSources"); + }); +} + +export function evidenceTabCount({ + tab, + sources, + visualEvidence, + answerEvidenceMapRows, + renderModel, +}: { + tab: EvidenceTabName; + sources: SearchResult[]; + visualEvidence: VisualEvidenceCard[]; + answerEvidenceMapRows: AnswerEvidenceMapRow[]; + renderModel: AnswerRenderModel; +}) { + if (tab === "Tables") { + return visualEvidence.filter((item) => item.accessibleTableMarkdown || item.tableRows?.length).length; + } + if (tab === "Claims") + return ( + answerEvidenceMapRows.length || + renderModel.evidenceRows.length || + sources.length || + renderModel.primarySources.length + ); + if (tab === "Images") return visualEvidence.length; + if (tab === "Quotes") return renderModel.quoteCards.length; + return renderModel.warnings.length; +} + +export function clinicalNotesCount(answer: RagAnswer) { + return buildHighYieldClinicalOutputSections(answer).filter((section) => + ["action", "escalation", "thresholds", "cautions", "monitoring", "medication", "source-gap"].includes(section.id), + ).length; +} + +export function answerHasCentralTable(answer: RagAnswer) { + return ( + answer.queryClass === "table_threshold" || + answer.responseMode === "threshold_table" || + Boolean(answer.visualEvidence?.some((item) => item.accessibleTableMarkdown || item.tableRows?.length)) + ); +} + +export function primaryVisualTable(answer: RagAnswer) { + return answer.visualEvidence?.find((item) => item.accessibleTableMarkdown || item.tableRows?.length) ?? null; +} + +type RenderModelPdfSource = { + document_id: string; + title: string; + file_name: string; + page_number: number | null; + chunk_id: string | null; +}; + +function uniquePdfSourcesForRenderModel(renderModel: AnswerRenderModel): RenderModelPdfSource[] { + return renderModel.primarySources.map((source) => ({ + document_id: source.document_id, + title: source.title, + file_name: source.file_name, + page_number: source.page_number, + chunk_id: source.chunk_id, + })); +} + +function queryModeLabel(mode: ClinicalQueryMode) { + return clinicalQueryModeOptions.find((option) => option.value === mode)?.label ?? mode.replaceAll("_", " "); +} + +function AnswerInsightBar({ + answer, + bestSource, + relevance, + queryMode, + sourceGovernanceWarnings, +}: { + answer: RagAnswer; + bestSource: BestSourceRecommendation | null; + relevance?: EvidenceRelevance | null; + queryMode: ClinicalQueryMode; + sourceGovernanceWarnings: SourceGovernanceWarning[]; +}) { + const frontendGovernanceWarnings = frontendSourceGovernanceWarnings(sourceGovernanceWarnings); + const metadata = normalizeSourceMetadata( + bestSource?.source_metadata ?? answer.sources?.[0]?.source_metadata ?? answer.citations?.[0]?.source_metadata, + ); + const modeLabel = + answer.smartApiPlan?.displayMode?.replaceAll("_", " ") ?? + answer.responseMode?.replaceAll("_", " ") ?? + queryModeLabel(queryMode); + const sourceCount = answer.evidenceSummary?.total_sources ?? answer.sources?.length ?? answer.citations.length; + const support = relevanceChipLabel(relevance ?? answer.relevance, answer.grounded); + const sourceStatus = frontendGovernanceWarnings.length + ? `${frontendGovernanceWarnings.length} source status note${frontendGovernanceWarnings.length === 1 ? "" : "s"}` + : sourceStatusLabel(metadata); + const retrievalGate = answer.retrievalDiagnostics?.gateStatus; + const items = [ + { label: "Mode", value: modeLabel, icon: SlidersHorizontal }, + { + label: "Support", + value: support, + icon: hasStrongRelevanceIcon(relevance ?? answer.relevance, answer.grounded) ? CheckCircle2 : AlertCircle, + }, + { label: "Sources", value: String(sourceCount), icon: FileText }, + { label: "Confidence", value: answer.confidence, icon: Target }, + { + label: "Retrieval", + value: retrievalGate ? `${retrievalGate} gate` : "Not logged", + icon: retrievalGate === "blocked" ? ShieldAlert : CheckCircle2, + }, + { label: "Status", value: `${sourceStatus} / ${validationStatusLabel(metadata)}`, icon: BookOpen }, + ]; + + return ( +
+ {items.map((item) => { + const Icon = item.icon; + return ( + + + + {item.label} + + {item.value} + + ); + })} +
+ ); +} + +function EvidenceVerificationStrip({ + answer, + bestSource, + sourceSummary, + weakEvidence, + governanceWarningCount, +}: { + answer: RagAnswer; + bestSource: BestSourceRecommendation | null; + sourceSummary?: EvidenceSummary | null; + weakEvidence: boolean; + governanceWarningCount: number; +}) { + const metadata = normalizeSourceMetadata( + bestSource?.source_metadata ?? answer.sources?.[0]?.source_metadata ?? answer.citations?.[0]?.source_metadata, + ); + const sourceCount = sourceSummary?.total_sources ?? answer.sources?.length ?? answer.citations.length; + const citationCount = answer.citations.length; + const gapCount = answer.conflictsOrGaps?.length ?? answer.smartPanel?.conflictsOrGaps?.length ?? 0; + const retrievalGateBlocked = answer.retrievalDiagnostics?.gateStatus === "blocked"; + const checks = [ + { + label: "Citations", + value: citationCount ? `${citationCount} citation${citationCount === 1 ? "" : "s"}` : "None", + ready: citationCount > 0, + }, + { + label: "Sources", + value: `${sourceCount} source${sourceCount === 1 ? "" : "s"}`, + ready: sourceCount > 0, + }, + { + label: "Source status", + value: sourceStatusLabel(metadata), + ready: metadata.document_status === "current" && !governanceWarningCount, + }, + { + label: "Retrieval gate", + value: retrievalGateBlocked ? "Blocked for low signal" : answer.retrievalDiagnostics ? "Passed" : "Not available", + ready: !retrievalGateBlocked, + }, + { + label: "Gaps", + value: governanceWarningCount + ? `${governanceWarningCount} status note${governanceWarningCount === 1 ? "" : "s"}` + : gapCount + ? `${gapCount} gap${gapCount === 1 ? "" : "s"}` + : "None", + ready: !weakEvidence && !gapCount && !governanceWarningCount, + }, + ]; + + return ( +
+
+ {checks.map((check) => ( +
+
+ {check.ready ? ( + + ) : ( + + )} +

+ {check.label} +

+
+

{check.value}

+
+ ))} +
+
+ + Pinned source + + {bestSource ? ( + <> + {bestSource.title} + + + ) : ( + No pinned source yet + )} +
+
+ ); +} + +const answerFeedbackOptions: Array<{ + type: AnswerFeedbackType; + label: string; + icon: typeof CheckCircle2; + tone: "success" | "warning" | "danger" | "neutral"; +}> = [ + { type: "verified", label: "Verified", icon: CheckCircle2, tone: "success" }, + { type: "needs_correction", label: "Needs correction", icon: AlertCircle, tone: "warning" }, + { type: "source_insufficient", label: "Source insufficient", icon: ShieldAlert, tone: "warning" }, + { type: "wrong_source", label: "Wrong source", icon: FileText, tone: "danger" }, + { type: "missing_source", label: "Missing source", icon: Search, tone: "warning" }, + { type: "unsupported_answer", label: "Unsupported answer", icon: ShieldAlert, tone: "danger" }, + { type: "numeric_error", label: "Numeric error", icon: Target, tone: "danger" }, + { type: "outdated_guidance", label: "Outdated guidance", icon: RefreshCw, tone: "warning" }, +]; + +function feedbackToneClass(tone: "success" | "warning" | "danger" | "neutral") { + if (tone === "success") return toneSuccess; + if (tone === "warning") return toneWarning; + if (tone === "danger") return toneDanger; + return toneNeutral; +} + +export function AnswerFeedbackPanel({ + pending, + onSubmit, +}: { + pending: AnswerFeedbackType | null; + onSubmit: (feedbackType: AnswerFeedbackType) => void; +}) { + return ( +
+
+
+

Answer review

+

+ Capture misses for retrieval and RAG evals without changing the answer. +

+
+ {pending ? ( + + + Saving + + ) : null} +
+
+ {answerFeedbackOptions.map((item) => { + const Icon = item.icon; + return ( + + ); + })} +
+
+ ); +} + +function RenderModelSourceList({ + sources, + query, + onScopeDocument, +}: { + sources: SourceLink[]; + query: string; + onScopeDocument: (documentId: string) => void; +}) { + if (sources.length === 0) { + return ( + + ); + } + + return ( +
+ {sources.map((source, index) => { + const metadata = normalizeSourceMetadata(source.sourceMetadata); + const snippet = compactSourceSnippet(source.snippet ?? "", { dropTitle: source.title }); + const openLabel = `Open source ${index + 1}: ${cleanDisplayTitle(source.title)}${query ? ` for ${query}` : ""}`; + return ( +
+ +
+
+ {snippet ?

{snippet}

: null} + +
+ +
+
+ ); + })} +
+ ); +} + +export function VerificationWorkspace({ + renderModel, + query, + answerEvidenceMapRows, + pendingFeedback, + onSubmitFeedback, + onScopeDocument, +}: { + renderModel: AnswerRenderModel; + query: string; + answerEvidenceMapRows: AnswerEvidenceMapRow[]; + pendingFeedback: AnswerFeedbackType | null; + onSubmitFeedback: (feedbackType: AnswerFeedbackType) => void; + onScopeDocument: (documentId: string) => void; +}) { + const verificationSources = renderModel.primarySources.slice(0, renderModel.trust === "unsupported" ? 3 : 6); + return ( +
+
+ +
+

Section support map

+

+ Each answer section should resolve back to a linked cited passage before clinical use. +

+
+ +
+
+
+
+
+

Cited source excerpts

+

+ Open the document to inspect the PDF page and highlighted indexed passage. +

+
+ +
+
+ ); +} + +export function AnswerViewModeControl({ + value, + onChange, +}: { + value: AnswerViewMode; + onChange: (mode: AnswerViewMode) => void; +}) { + const modes: Array<{ value: AnswerViewMode; label: string; shortLabel: string; icon: typeof Search }> = [ + { value: "standard", label: "Standard", shortLabel: "All", icon: ListChecks }, + { value: "high_yield", label: "High-yield", shortLabel: "Key", icon: Target }, + { value: "evidence_map", label: "Evidence map", shortLabel: "Map", icon: BookOpen }, + ]; + + return ( +
+ {modes.map((mode) => { + const Icon = mode.icon; + const active = value === mode.value; + return ( + + ); + })} +
+ ); +} + +export const simpleClinicalTableProps = { + compact: false, + expandOnMobile: true, +} as const; + +function compactEvidenceCell(value: string | null | undefined, max = 140) { + const text = value ? value.replace(/\s+/g, " ").trim() : ""; + return text.length > max ? `${text.slice(0, max - 1).trim()}…` : text; +} + +export function evidenceMapRowsFromRenderModel(renderModel: AnswerRenderModel): AnswerEvidenceMapRow[] { + return renderModel.evidenceRows.map((row, index) => ({ + id: row.id || `${row.source.chunk_id}:${index}`, + section: row.section || "Source evidence", + detail: + sourceTextForCompactDisplay(row.quote || row.source.snippet || row.source.reason || "") || + cleanDisplayTitle(row.source.title), + supportLevel: row.supportLevel || row.source.sourceStrength, + citationCount: 1, + sourceStatus: + row.source.sourceStrength === "none" ? "Source requires review" : `${row.source.sourceStrength} source support`, + bestSourceLabel: row.source.label, + bestLinkedPassage: row.quote || row.source.snippet || row.source.reason, + href: row.source.href, + })); +} + +export function EvidenceMapTable({ rows }: { rows: AnswerEvidenceMapRow[] }) { + if (rows.length === 0) { + return ; + } + + const tableRows = rows.map((row) => [ + compactEvidenceCell(row.section), + row.supportLevel, + String(row.citationCount), + compactEvidenceCell(row.sourceStatus), + compactEvidenceCell(row.bestSourceLabel, 72), + row.bestLinkedPassage || "Open source passage.", + ]); + const linkedRows = rows.filter((row) => row.href); + + return ( +
+ + {linkedRows.length ? ( +
+ {linkedRows.map((row) => ( + + + {row.section} + {row.bestSourceLabel} + + + Open source + + + + ))} +
+ ) : null} +
+ ); +} + +export function AnswerSafetyNotice({ + demoMode, + weakEvidence = false, + retrievalDiagnostics, +}: { + demoMode: boolean; + weakEvidence?: boolean; + retrievalDiagnostics?: RagAnswer["retrievalDiagnostics"]; +}) { + const retrievalGateBlocked = retrievalDiagnostics?.gateStatus === "blocked"; + return ( +
+

+ {weakEvidence + ? "Weak source support; verify the linked source before relying on this answer." + : "Draft only; verify source first before pasting into the medical record."} +

+ {retrievalGateBlocked ? ( +

+ Retrieval confidence gate was triggered (low-confidence retrieval signal). Expand evidence details before + using this result. +

+ ) : null} + {demoMode ? ( +

+ Synthetic demo only: this is not clinical guidance. +

+ ) : null} +
+ ); +} + +export function QuoteCards({ + quotes, + copiedQuotes, + onCopyQuotes, + onFollowUp, + onScopeDocument, +}: { + quotes: QuoteCard[]; + copiedQuotes: boolean; + onCopyQuotes: () => void; + onFollowUp?: (quote: QuoteCard) => void; + onScopeDocument: (documentId: string) => void; +}) { + return ( +
+ 0 ? ( + + ) : null + } + /> + {quotes.length === 0 ? ( + + ) : ( +
+ {quotes.map((quote, index) => { + const quoteText = sourceTextForVerbatimQuote(quote.quote); + const quoteTitle = cleanDisplayTitle(quote.title); + return ( +
+
+ + {index + 1} + + +
+
+ “{quoteText}” +
+
+ + {formatCompactCitationLabel(quote)} + + + {quoteTitle}, page {quote.page_number ?? "n/a"} + +
+ onFollowUp(quote) : undefined} + divider={false} + /> +
+
+
+ ); + })} +
+ )} +
+ ); +} + +export function formatQuoteCardsForClipboard(quotes: QuoteCard[]) { + return quotes + .map((quote, index) => + [ + // Clean the copied text the same way the card displays it, so clipboard + // output never contains internal image-data blocks or glyph artifacts. + `${index + 1}. "${sourceTextForVerbatimQuote(quote.quote)}"`, + `Source: ${formatCitationLabel(quote)}`, + `Link: ${documentCitationHref(quote)}`, + ].join("\n"), + ) + .join("\n\n"); +} diff --git a/src/components/clinical-dashboard/use-mobile-preview-sheet.ts b/src/components/clinical-dashboard/use-mobile-preview-sheet.ts new file mode 100644 index 000000000..88976d083 --- /dev/null +++ b/src/components/clinical-dashboard/use-mobile-preview-sheet.ts @@ -0,0 +1,21 @@ +"use client"; + +import { useSyncExternalStore } from "react"; + +const sourcePreviewSheetMediaQuery = "(max-width: 1023px)"; + +function subscribeToMobilePreviewMedia(callback: () => void) { + if (typeof window === "undefined" || typeof window.matchMedia !== "function") return () => undefined; + const media = window.matchMedia(sourcePreviewSheetMediaQuery); + media.addEventListener("change", callback); + return () => media.removeEventListener("change", callback); +} + +function getMobilePreviewSnapshot() { + if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false; + return window.matchMedia(sourcePreviewSheetMediaQuery).matches; +} + +export function useMobilePreviewSheet() { + return useSyncExternalStore(subscribeToMobilePreviewMedia, getMobilePreviewSnapshot, () => false); +} diff --git a/tests/clinical-dashboard-merge-artifacts.test.ts b/tests/clinical-dashboard-merge-artifacts.test.ts index 08308d664..a9e0ed411 100644 --- a/tests/clinical-dashboard-merge-artifacts.test.ts +++ b/tests/clinical-dashboard-merge-artifacts.test.ts @@ -3,29 +3,42 @@ import { resolve } from "node:path"; import * as ts from "typescript"; import { describe, expect, it } from "vitest"; -const dashboardPath = resolve(process.cwd(), "src/components/ClinicalDashboard.tsx"); -const dashboardSource = readFileSync(dashboardPath, "utf8"); -const dashboardAst = ts.createSourceFile( - dashboardPath, - dashboardSource, - ts.ScriptTarget.Latest, - true, - ts.ScriptKind.TSX, -); +// The dashboard render surfaces are progressively being extracted from the +// monolith into src/components/clinical-dashboard/*. Scan every file that now +// owns a pinned declaration so the guards travel with the code and the +// absence checks strengthen across the whole set. +const scannedFiles = [ + "src/components/ClinicalDashboard.tsx", + "src/components/clinical-dashboard/answer-content.tsx", +].map((relativePath) => { + const path = resolve(process.cwd(), relativePath); + const source = readFileSync(path, "utf8"); + return { + path, + source, + ast: ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX), + }; +}); + +type FoundDeclaration = { node: ts.FunctionDeclaration; ast: ts.SourceFile }; -function findFunctionDeclaration(name: string): ts.FunctionDeclaration | null { - let found: ts.FunctionDeclaration | null = null; +function findFunctionDeclaration(name: string): FoundDeclaration | null { + for (const file of scannedFiles) { + let found: ts.FunctionDeclaration | null = null; - function visit(node: ts.Node) { - if (ts.isFunctionDeclaration(node) && node.name?.text === name) { - found = node; - return; + function visit(node: ts.Node) { + if (found) return; + if (ts.isFunctionDeclaration(node) && node.name?.text === name) { + found = node; + return; + } + ts.forEachChild(node, visit); } - ts.forEachChild(node, visit); - } - visit(dashboardAst); - return found; + visit(file.ast); + if (found) return { node: found, ast: file.ast }; + } + return null; } function descendantIdentifiers(node: ts.Node) { @@ -50,8 +63,8 @@ describe("ClinicalDashboard merge-artifact guards", () => { expect(panel, "ClinicalOutputPanel should remain a local function declaration").not.toBeNull(); if (!panel) throw new Error("ClinicalOutputPanel should remain a local function declaration"); - const panelSource = panel.getText(dashboardAst); - const panelIdentifiers = descendantIdentifiers(panel); + const panelSource = panel.node.getText(panel.ast); + const panelIdentifiers = descendantIdentifiers(panel.node); expect(panelIdentifiers.has("copiedWardNote")).toBe(false); expect(panelIdentifiers.has("onCopyWardNote")).toBe(false); @@ -67,7 +80,7 @@ describe("ClinicalDashboard merge-artifact guards", () => { expect(answer, "NaturalLanguageAnswer should remain a local function declaration").not.toBeNull(); if (!answer) throw new Error("NaturalLanguageAnswer should remain a local function declaration"); - const answerSource = answer.getText(dashboardAst); + const answerSource = answer.node.getText(answer.ast); expect(answerSource).toContain("plain-answer-prose"); expect(answerSource).not.toContain("parseAnswerDisplayContent"); expect(answerSource).not.toContain("AnswerSymbolTile"); diff --git a/tests/rendered-text-formatting.test.ts b/tests/rendered-text-formatting.test.ts index f123d6d87..7b1779c85 100644 --- a/tests/rendered-text-formatting.test.ts +++ b/tests/rendered-text-formatting.test.ts @@ -14,31 +14,38 @@ function componentSource(relativePath: string) { describe("document-derived text must route through a formatter", () => { const dashboard = componentSource("ClinicalDashboard.tsx"); const documentViewer = componentSource("DocumentViewer.tsx"); + // Answer/evidence render surfaces (SourceImage, SourcePreviewContent, + // NaturalLanguageAnswer, QuoteCards, EvidenceMapTable, …) now live in extracted + // modules. Scan them alongside the monolith so the raw-render guards travel with + // the code as it moves out — assertions check the combined dashboard surfaces. + const answerContent = componentSource("clinical-dashboard/answer-content.tsx"); + const evidenceContent = componentSource("clinical-dashboard/evidence-panels.tsx"); + const dashboardSurfaces = `${dashboard}\n${answerContent}\n${evidenceContent}`; it("renders exact quotes through the verbatim cleaner, never raw", () => { // Allow `${quote.quote}` inside template literals (React keys, clipboard text); // only a bare JSX child `{quote.quote}` is a raw-render regression. - expect(dashboard).not.toMatch(/(? { - expect(dashboard).not.toMatch(/\{source\.title\}/); - expect(dashboard).toContain("cleanDisplayTitle("); + expect(dashboardSurfaces).not.toMatch(/\{source\.title\}/); + expect(dashboardSurfaces).toContain("cleanDisplayTitle("); }); it("renders extracted table snippets through a compact formatter, never raw", () => { - expect(dashboard).not.toMatch(/>\s*\{item\.tableTextSnippet\}\s*\s*\{item\.tableTextSnippet\}\s* { - expect(dashboard).not.toMatch(/(? { - expect(dashboard).toContain("sourceTextForCompactDisplay(row.quote || row.source.snippet"); + expect(dashboardSurfaces).toContain("sourceTextForCompactDisplay(row.quote || row.source.snippet"); }); it("renders document-viewer image captions through a formatter, never raw", () => { @@ -47,13 +54,13 @@ describe("document-derived text must route through a formatter", () => { }); it("renders visual-evidence titles and alt text through formatters, never raw", () => { - expect(dashboard).not.toMatch(/(? { // One call for the rendered blockquote, one for the copy-to-clipboard text. - const cleanerCalls = dashboard.match(/sourceTextForVerbatimQuote\(quote\.quote\)/g) ?? []; + const cleanerCalls = dashboardSurfaces.match(/sourceTextForVerbatimQuote\(quote\.quote\)/g) ?? []; expect(cleanerCalls.length).toBeGreaterThanOrEqual(2); }); });