SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
5.1 KB · 117 lines tsx
Raw Blame History
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