diff --git a/src/components/clinical-dashboard/medication-prescribing-workspace.tsx b/src/components/clinical-dashboard/medication-prescribing-workspace.tsx index 5e6404c0c..14023d501 100644 --- a/src/components/clinical-dashboard/medication-prescribing-workspace.tsx +++ b/src/components/clinical-dashboard/medication-prescribing-workspace.tsx @@ -2,12 +2,18 @@ import { Activity, + Ban, CalendarDays, CheckCircle2, ChevronRight, Lock, Pill, + SearchX, + ShieldAlert, ShieldCheck, + Sparkles, + Target, + TriangleAlert, UserRound, type LucideIcon, } from "lucide-react"; @@ -18,9 +24,17 @@ import { ModeHomeTemplate, ModeHomeVerificationFooter } from "@/components/mode- import { SearchResultsHeaderBand } from "@/components/clinical-dashboard/search-results-header-band"; import { useSearchCommand } from "@/components/clinical-dashboard/search-command-context"; import { useMedicationCatalog } from "@/components/clinical-dashboard/use-medication-catalog"; +import { + BadgeCluster, + ClinicalBadge, + type ClinicalBadgeItem, + type ClinicalBadgeTone, +} from "@/components/clinical-dashboard/clinical-badge"; +import { medicationIdentityBadges, type MedicationRecord } from "@/lib/medications"; import { medicationMatchesCommandScopes } from "@/lib/search-command-surface"; +import { SEMANTIC_TONE_META } from "@/lib/semantic-tone"; import { isDeployedClinicalKb } from "@/lib/deployed-app"; -import { cn } from "@/components/ui-primitives"; +import { cn, EmptyState } from "@/components/ui-primitives"; type MedicationPrescribingWorkspaceProps = { query: string; @@ -46,17 +60,23 @@ type MedicationResult = { dose: string; ceiling: string; action: string; + actionTone: "danger" | "warning" | "neutral"; tone: "teal" | "blue" | "slate"; href?: string; }; +type MedicationRow = { + result: MedicationResult; + badges: ClinicalBadgeItem[]; +}; + type MedicationResultFilter = "best" | "indication" | "safety" | "monitoring"; -const medicationResultFilters: Array<{ id: MedicationResultFilter; label: string }> = [ - { id: "best", label: "Best" }, - { id: "indication", label: "Indication" }, - { id: "safety", label: "Safety" }, - { id: "monitoring", label: "Monitor" }, +const medicationResultFilters: Array<{ id: MedicationResultFilter; label: string; icon: LucideIcon }> = [ + { id: "best", label: "Best", icon: Sparkles }, + { id: "indication", label: "Indication", icon: Target }, + { id: "safety", label: "Safety", icon: ShieldAlert }, + { id: "monitoring", label: "Monitor", icon: Activity }, ]; const medicationCapabilities: Capability[] = [ @@ -199,15 +219,26 @@ function MedicationHome({ function resultMatchesFilter(result: MedicationResult, filter: MedicationResultFilter) { if (filter === "best") return true; if (filter === "indication") return result.match !== "Related match"; - if (filter === "safety") return /check|avoid|caution|ceiling|max/i.test(result.action); - return /monitor|level|review|renal|hepatic/i.test(`${result.action} ${result.dose} ${result.ceiling}`); + // actionTone is source-derived (contraindication vs caution vs monitoring content), + // so it is a stronger signal than the text heuristics — any row whose action shows + // a safety icon (danger or warning) must be reachable through the Safety chip. The + // chips are lenses, not partitions, so warning rows may also appear under Monitor. + if (filter === "safety") { + return result.actionTone !== "neutral" || /check|avoid|caution|ceiling|max/i.test(result.action); + } + return ( + result.actionTone === "warning" || + /monitor|level|review|renal|hepatic/i.test(`${result.action} ${result.dose} ${result.ceiling}`) + ); } function FilterStrip({ activeFilter, + counts, onFilterChange, }: { activeFilter: MedicationResultFilter; + counts: Record; onFilterChange: (filter: MedicationResultFilter) => void; }) { return ( @@ -215,22 +246,32 @@ function FilterStrip({ className="flex gap-1.5 overflow-x-auto pb-1 [-webkit-overflow-scrolling:touch]" aria-label="Medication result filters" > - {medicationResultFilters.map((filter) => ( - - ))} + {medicationResultFilters.map((filter) => { + const active = activeFilter === filter.id; + const Icon = filter.icon; + return ( + + ); + })} ); } @@ -240,21 +281,53 @@ function ResultToneIcon({ result }: { result: MedicationResult }) { return ; } +const matchBadgeTone: Record = { + teal: "clinical", + blue: "info", + slate: "neutral", +}; + function ResultMatchBadge({ result }: { result: MedicationResult }) { + return ; +} + +// Small leading icon for the prescribing-action text: contraindication content is +// a quiet stop signal (Ban, danger colour), monitoring content a check-first +// caution (TriangleAlert, warning colour). The text itself stays heading-coloured +// so red remains reserved and readable per the badge governance guide. +function ActionToneIcon({ tone, className }: { tone: MedicationResult["actionTone"]; className?: string }) { + if (tone === "neutral") return null; + const Icon = tone === "danger" ? Ban : TriangleAlert; return ( - - + <> + {SEMANTIC_TONE_META[tone].ariaPrefix}: +