import type { ReactNode } from 'react'; type Tone = 'neutral' | 'accent' | 'warn' | 'danger' | 'ok' | 'outline'; // Tones map to short CSS classes (globals.css `.ci-badge--*`) instead of long Tailwind strings: dense // tables repeat badges hundreds of times and every className is shipped twice (HTML + RSC payload). const TONES: Record = { neutral: 'ci-badge--neutral', accent: 'ci-badge--accent', warn: 'ci-badge--warn', danger: 'ci-badge--danger', ok: 'ci-badge--ok', outline: 'ci-badge--outline', }; export function Badge({ children, tone = 'neutral', title, className = '', mono = false }: { children: ReactNode; tone?: Tone; title?: string; className?: string; mono?: boolean }) { // Only spread `title` when set: an undefined prop is serialized as "$undefined" in the RSC payload. const extra = title ? { title } : {}; return ( {children} ); } /** * Scientific safety label (CLAUDE.md §3). The seven categories are never merged. */ export type ClaimKind = 'observed' | 'published' | 'curated' | 'regulatory' | 'guideline' | 'computed' | 'ai'; const CLAIM: Record = { observed: { label: 'Observed', title: 'Observed data — registry or measured counts reported by the source', tone: 'neutral' }, published: { label: 'Published', title: 'Published evidence — peer-reviewed literature or trial registration', tone: 'neutral' }, curated: { label: 'Curated', title: 'Curated evidence — expert-curated knowledge base (e.g. CIViC, NCIt)', tone: 'neutral' }, regulatory: { label: 'Regulatory', title: 'Regulatory status — approval or label decision by a named authority and jurisdiction', tone: 'neutral' }, guideline: { label: 'Guideline', title: 'Clinical guideline statement', tone: 'neutral' }, computed: { label: 'Computed', title: 'Computed metric — derived by CancerIndex with a versioned formula', tone: 'accent' }, ai: { label: 'AI-generated', title: 'AI-generated synthesis — not enabled in Phase 1', tone: 'warn' }, }; export function ClaimBadge({ kind, className = '' }: { kind: ClaimKind; className?: string }) { const c = CLAIM[kind]; return ( {c.label} ); } export function claimKindFromCategory(cat: string | null | undefined): ClaimKind { switch (cat) { case 'observed_data': return 'observed'; case 'published_evidence': return 'published'; case 'regulatory_status': return 'regulatory'; case 'clinical_guideline': return 'guideline'; case 'computed_metric': return 'computed'; case 'ai_generated_synthesis': return 'ai'; default: return 'curated'; } } export type Confidence = 'HIGH' | 'MEDIUM' | 'LOW' | 'INSUFFICIENT_DATA'; export function ConfidenceBadge({ level, className = '' }: { level: string | null | undefined; className?: string }) { const l = (level ?? 'INSUFFICIENT_DATA').toUpperCase() as Confidence; const map: Record = { HIGH: { tone: 'ok', label: 'High confidence', title: 'Observed data from a primary source with complete coverage for the scope' }, MEDIUM: { tone: 'neutral', label: 'Medium confidence', title: 'Estimated, modelled or partially covered data' }, LOW: { tone: 'warn', label: 'Low confidence', title: 'Small denominators, heterogeneous definitions or indirect mapping' }, INSUFFICIENT_DATA: { tone: 'outline', label: 'Insufficient data', title: 'Not enough data to assign a confidence level' }, }; const m = map[l] ?? map.INSUFFICIENT_DATA; return ( {m.label} ); } /** True for mappings made through a shared identifier or a curated/ontology one-to-one match. */ export function isExactMatch(matchType: string | null | undefined): boolean { return matchType === 'EXACT_IDENTIFIER' || matchType === 'CURATED_EXACT' || matchType === 'ONTOLOGY_EXACT'; } /** Entity-mapping confidence (CLAUDE.md §221). */ export function MatchBadge({ matchType, className = '' }: { matchType: string | null | undefined; className?: string }) { const m = matchType ?? 'UNRESOLVED'; const tone: Tone = m === 'EXACT_IDENTIFIER' || m === 'CURATED_EXACT' || m === 'ONTOLOGY_EXACT' ? 'ok' : m === 'UNRESOLVED' ? 'danger' : m === 'PROBABILISTIC' ? 'warn' : 'neutral'; const titles: Record = { EXACT_IDENTIFIER: 'Mapped through a shared identifier (e.g. NCIt code)', CURATED_EXACT: 'Curated one-to-one mapping', ONTOLOGY_EXACT: 'Exact match through an ontology cross-reference', CURATED_BROADER: 'Mapped to a broader concept by a curator', CURATED_NARROWER: 'Mapped to a narrower concept by a curator', ALIAS: 'Matched on a known alias after normalization', PROBABILISTIC: 'Probabilistic string match — treat with caution', UNRESOLVED: 'Not mapped to a CancerIndex entity', }; return ( {m} ); } export function StatusBadge({ status, className = '' }: { status: string | null | undefined; className?: string }) { const s = (status ?? 'unknown').toLowerCase(); const tone: Tone = ['healthy', 'succeeded', 'active', 'approved', 'recruiting', 'accepted', 'validated'].includes(s) ? 'ok' : ['failed', 'failing', 'blocked', 'retracted', 'withdrawn', 'rejected', 'aborted'].includes(s) ? 'danger' : ['degraded', 'partial', 'review', 'awaiting_credentials', 'restricted', 'paused', 'candidate', 'submitted'].includes(s) ? 'warn' : 'neutral'; return ( {s.replace(/_/g, ' ')} ); }