SPB Git

spb/cancerindex

Public
5.4 KB · 119 lines tsx
1import type { ReactNode } from 'react';23type Tone = 'neutral' | 'accent' | 'warn' | 'danger' | 'ok' | 'outline';45const TONES: Record<Tone, string> = {6  neutral: 'bg-paper-3 text-ink-2 border-transparent',7  accent: 'bg-accent-soft text-accent-2 border-transparent',8  warn: 'bg-warn-soft text-warn border-transparent',9  danger: 'bg-danger-soft text-danger border-transparent',10  ok: 'bg-ok-soft text-ok border-transparent',11  outline: 'bg-transparent text-ink-2 border-rule-strong',12};1314export function Badge({ children, tone = 'neutral', title, className = '', mono = false }: { children: ReactNode; tone?: Tone; title?: string; className?: string; mono?: boolean }) {15  return (16    <span title={title} className={`inline-flex items-center gap-1 rounded-sm border px-1.5 py-[1px] text-[11px] font-medium leading-4 tracking-wide ${mono ? 'ci-mono' : ''} ${TONES[tone]} ${className}`}>17      {children}18    </span>19  );20}2122/**23 * Scientific safety label (CLAUDE.md §3). The seven categories are never merged.24 */25export type ClaimKind = 'observed' | 'published' | 'curated' | 'regulatory' | 'guideline' | 'computed' | 'ai';2627const CLAIM: Record<ClaimKind, { label: string; title: string; tone: Tone }> = {28  observed: { label: 'Observed', title: 'Observed data — registry or measured counts reported by the source', tone: 'neutral' },29  published: { label: 'Published', title: 'Published evidence — peer-reviewed literature or trial registration', tone: 'neutral' },30  curated: { label: 'Curated', title: 'Curated evidence — expert-curated knowledge base (e.g. CIViC, NCIt)', tone: 'neutral' },31  regulatory: { label: 'Regulatory', title: 'Regulatory status — approval or label decision by a named authority and jurisdiction', tone: 'neutral' },32  guideline: { label: 'Guideline', title: 'Clinical guideline statement', tone: 'neutral' },33  computed: { label: 'Computed', title: 'Computed metric — derived by CancerIndex with a versioned formula', tone: 'accent' },34  ai: { label: 'AI-generated', title: 'AI-generated synthesis — not enabled in Phase 1', tone: 'warn' },35};3637export function ClaimBadge({ kind, className = '' }: { kind: ClaimKind; className?: string }) {38  const c = CLAIM[kind];39  return (40    <Badge tone={c.tone} title={c.title} className={className}>41      {c.label}42    </Badge>43  );44}4546export function claimKindFromCategory(cat: string | null | undefined): ClaimKind {47  switch (cat) {48    case 'observed_data':49      return 'observed';50    case 'published_evidence':51      return 'published';52    case 'regulatory_status':53      return 'regulatory';54    case 'clinical_guideline':55      return 'guideline';56    case 'computed_metric':57      return 'computed';58    case 'ai_generated_synthesis':59      return 'ai';60    default:61      return 'curated';62  }63}6465export type Confidence = 'HIGH' | 'MEDIUM' | 'LOW' | 'INSUFFICIENT_DATA';6667export function ConfidenceBadge({ level, className = '' }: { level: string | null | undefined; className?: string }) {68  const l = (level ?? 'INSUFFICIENT_DATA').toUpperCase() as Confidence;69  const map: Record<Confidence, { tone: Tone; label: string; title: string }> = {70    HIGH: { tone: 'ok', label: 'High confidence', title: 'Observed data from a primary source with complete coverage for the scope' },71    MEDIUM: { tone: 'neutral', label: 'Medium confidence', title: 'Estimated, modelled or partially covered data' },72    LOW: { tone: 'warn', label: 'Low confidence', title: 'Small denominators, heterogeneous definitions or indirect mapping' },73    INSUFFICIENT_DATA: { tone: 'outline', label: 'Insufficient data', title: 'Not enough data to assign a confidence level' },74  };75  const m = map[l] ?? map.INSUFFICIENT_DATA;76  return (77    <Badge tone={m.tone} title={m.title} className={className}>78      {m.label}79    </Badge>80  );81}8283/** Entity-mapping confidence (CLAUDE.md §221). */84export function MatchBadge({ matchType, className = '' }: { matchType: string | null | undefined; className?: string }) {85  const m = matchType ?? 'UNRESOLVED';86  const tone: Tone = m === 'EXACT_IDENTIFIER' || m === 'CURATED_EXACT' || m === 'ONTOLOGY_EXACT' ? 'ok' : m === 'UNRESOLVED' ? 'danger' : m === 'PROBABILISTIC' ? 'warn' : 'neutral';87  const titles: Record<string, string> = {88    EXACT_IDENTIFIER: 'Mapped through a shared identifier (e.g. NCIt code)',89    CURATED_EXACT: 'Curated one-to-one mapping',90    ONTOLOGY_EXACT: 'Exact match through an ontology cross-reference',91    CURATED_BROADER: 'Mapped to a broader concept by a curator',92    CURATED_NARROWER: 'Mapped to a narrower concept by a curator',93    ALIAS: 'Matched on a known alias after normalization',94    PROBABILISTIC: 'Probabilistic string match — treat with caution',95    UNRESOLVED: 'Not mapped to a CancerIndex entity',96  };97  return (98    <Badge tone={tone} mono title={titles[m] ?? m} className={className}>99      {m}100    </Badge>101  );102}103104export function StatusBadge({ status, className = '' }: { status: string | null | undefined; className?: string }) {105  const s = (status ?? 'unknown').toLowerCase();106  const tone: Tone = ['healthy', 'succeeded', 'active', 'approved', 'recruiting', 'accepted', 'validated'].includes(s)107    ? 'ok'108    : ['failed', 'failing', 'blocked', 'retracted', 'withdrawn', 'rejected', 'aborted'].includes(s)109      ? 'danger'110      : ['degraded', 'partial', 'review', 'awaiting_credentials', 'restricted', 'paused', 'candidate', 'submitted'].includes(s)111        ? 'warn'112        : 'neutral';113  return (114    <Badge tone={tone} className={className}>115      {s.replace(/_/g, ' ')}116    </Badge>117  );118}119
Files · 36d464dcc8d9fe84217ecbfd89a225759f5f0c06
Menu

Theme