spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import Link from 'next/link';2import { Badge, ClaimBadge } from '@/components/ui/badge';3import { SourceBadge } from '@/components/ui/source-badge';4import { fmtInt, fmtPct } from '@/lib/format';5import type { PathChain } from '@/lib/graph-model';67const SOURCE_SLUG_BY_ID: Record<string, string> = { 'CI-SOURCE-00000006': 'civic', 'CI-SOURCE-00000015': 'chembl', 'CI-SOURCE-00000016': 'openfda', 'CI-SOURCE-00000004': 'clinicaltrials', 'CI-SOURCE-00000008': 'gdc', 'CI-SOURCE-00000017': 'cbioportal' };89function Arrow() {10 return (11 <span aria-hidden className="mx-1 text-ink-4">12 →13 </span>14 );15}1617/**18 * One cancer → gene → variant → drug → approval → trials chain, readable as a sentence. Each hop19 * keeps its own claim category: cohort frequency (observed), CIViC predictive edge (curated),20 * approval (regulatory), trial count (observed). Missing hops say so — nothing is filled in.21 */22export function PathChainView({ chain, compact = false, sourceSlugs }: { chain: PathChain; compact?: boolean; sourceSlugs?: Map<string, string> }) {23 const slugOf = (id: string) => sourceSlugs?.get(id) ?? SOURCE_SLUG_BY_ID[id] ?? id;24 const g = chain.gene;25 const freq = g.frequency != null && g.casesAffected != null && g.casesProfiled != null ? `${fmtPct(g.frequency, g.frequency >= 0.1 ? 0 : 1)} (${fmtInt(g.casesAffected)} / ${fmtInt(g.casesProfiled)} cases${g.cohorts > 1 ? `, largest of ${g.cohorts} cohorts` : ''})` : null;26 return (27 <li className={`ci-rule ${compact ? 'py-2' : 'py-3'} text-[13.5px] leading-relaxed`}>28 <p className="m-0 flex flex-wrap items-baseline">29 <Link href={`/gene/${g.symbol}`} className="ci-link ci-mono font-sans font-medium">30 {g.symbol}31 </Link>32 <Arrow />33 <Link href={`/variant/${chain.variant.slug}`} className="ci-link">34 {chain.variant.name}35 </Link>36 <Arrow />37 <Link href={`/drug/${chain.drug.slug}`} className="ci-link font-medium">38 {chain.drug.name}39 </Link>40 {chain.approval ? (41 <>42 <Arrow />43 <span>44 {chain.approval.authority} <Badge tone="outline" mono>{chain.approval.jurisdiction}</Badge>45 {chain.approval.approvalDate ? <span className="ci-num text-ink-2"> {chain.approval.approvalDate}</span> : <span className="text-ink-3"> (date not published)</span>}46 {chain.approval.tumorAgnostic && !chain.approval.cancerId ? <span className="text-ink-3"> · tumour-agnostic</span> : null}47 </span>48 </>49 ) : null}50 {chain.trials ? (51 <>52 <Arrow />53 <span>54 <span className="ci-num">{fmtInt(chain.trials.active)}</span> active / <span className="ci-num">{fmtInt(chain.trials.total)}</span> trials55 </span>56 </>57 ) : null}58 </p>59 <p className="m-0 mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-[12px] text-ink-3">60 <span className="inline-flex items-center gap-1">61 <ClaimBadge kind="curated" />62 {chain.edge.evidenceLevel ? (63 <Badge tone="accent" mono title="Source-native evidence level (CIViC A–E)">64 level {chain.edge.evidenceLevel}65 </Badge>66 ) : (67 <Badge tone="outline">level not stated</Badge>68 )}69 <span>{chain.edge.direction ?? 'direction unknown'}</span>70 <span>71 · support <span className="ci-num">{fmtInt(chain.edge.supportCount)}</span>72 </span>73 {chain.edge.sourceIds.map((s) => (74 <SourceBadge key={s} compact p={{ sourceSlug: slugOf(s) }} title={null} />75 ))}76 </span>77 {!compact ? (78 <>79 <span aria-hidden>·</span>80 <span className="inline-flex items-center gap-1">81 <ClaimBadge kind="observed" />82 {freq ? <span>{g.symbol} altered in {freq}</span> : <span>cohort frequency not yet available for {g.symbol} in this cancer</span>}83 </span>84 {chain.approval ? (85 <>86 <span aria-hidden>·</span>87 <span className="inline-flex items-center gap-1">88 <ClaimBadge kind="regulatory" />89 <span>90 {chain.approval.status}91 {chain.approval.cancerName ? ` for ${chain.approval.cancerName}` : ''}92 {chain.approval.total > 1 ? ` (${fmtInt(chain.approval.total)} approval records)` : ''}93 </span>94 </span>95 </>96 ) : (97 <>98 <span aria-hidden>·</span>99 <span>no approval record in this cancer</span>100 </>101 )}102 {chain.edge.contextNames.length ? (103 <>104 <span aria-hidden>·</span>105 <span>106 context: {chain.edge.contextNames.slice(0, 3).join(', ')}107 {chain.edge.contextNames.length > 3 ? ` +${chain.edge.contextNames.length - 3}` : ''}108 </span>109 </>110 ) : null}111 </>112 ) : null}113 </p>114 </li>115 );116}117