spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { PageHeader, Note } from '@/components/ui/section';4import { EmptyState } from '@/components/ui/empty-state';5import { Badge, ClaimBadge } from '@/components/ui/badge';6import { SourceBadge } from '@/components/ui/source-badge';7import { Freshness } from '@/components/ui/freshness';8import { listBiomarkers, biomarkerKindFacets, kindLabel, BIOMARKER_KINDS, BIOMARKER_LINKS_FORMULA } from '@/lib/queries/biomarkers';9import { fmtInt } from '@/lib/format';10import { str, oneOf, withParams, type SP } from '@/lib/search-params';1112export const metadata: Metadata = {13 title: 'Biomarkers',14 description: 'Canonical oncology biomarkers with verified NCIt concepts, anchor genes, assay conventions, and links derived from curated evidence, approvals and trials.',15 alternates: { canonical: '/biomarkers' },16};17// Filtered list: rendered per request (searchParams); 600 s is the CDN/ISR hint shared by all list pages.18export const revalidate = 600;1920const KIND_FILTER = ['', ...BIOMARKER_KINDS] as const;2122export default async function BiomarkersPage({ searchParams }: { searchParams: Promise<SP> }) {23 const sp = await searchParams;24 const q = str(sp, 'q');25 const kind = oneOf(sp, 'kind', KIND_FILTER, '');26 const [rows, facets] = await Promise.all([listBiomarkers({ kind: kind || undefined, q: q || undefined }), biomarkerKindFacets()]);27 const href = (o: Record<string, string | number | null | undefined>) => `/biomarkers${withParams({ q, kind }, o)}`;28 const total = facets.reduce((s, f) => s + f.n, 0);29 const latest = rows.reduce<Date | string | null>((m, r) => (m == null || String(r.updated_at) > String(m) ? r.updated_at : m), null);30 const verification = rows[0]?.measurement.verification;3132 return (33 <div>34 <PageHeader35 kicker="Biomarkers"36 title="Biomarkers"37 lede="A biomarker entry is an identity record — what is measured, by which assay family, the NCIt concept it maps to and the gene it is anchored to. Every link to cancers, drugs, approvals and trials is derived at query time from curated evidence and registries; nothing on this page grades clinical utility or states a threshold of its own."38 />3940 <form method="get" action="/biomarkers" className="flex flex-wrap items-end gap-2 border-y border-rule py-3 text-[13.5px]">41 <label className="flex flex-col gap-1">42 <span className="ci-kicker">Name, alias, gene or NCIt code</span>43 <input name="q" defaultValue={q} placeholder="e.g. HER2, dMMR, CD274, C98342" className="border border-rule-strong bg-white px-2 py-1.5 outline-none focus:border-accent" />44 </label>45 {kind ? <input type="hidden" name="kind" value={kind} /> : null}46 <button type="submit" className="border border-ink bg-ink px-3 py-1.5 text-paper hover:bg-ink-2">47 Apply48 </button>49 </form>5051 <nav aria-label="Kind" className="mt-3 flex flex-wrap gap-1.5 text-[12.5px]">52 <Link href={href({ kind: '' })} aria-current={!kind ? 'page' : undefined} className="ci-chip">53 All <span className="ci-num text-ink-3">{fmtInt(total)}</span>54 </Link>55 {facets.map((f) => (56 <Link key={f.kind} href={href({ kind: f.kind })} aria-current={kind === f.kind ? 'page' : undefined} className="ci-chip">57 {kindLabel(f.kind)} <span className="ci-num text-ink-3">{f.n}</span>58 </Link>59 ))}60 </nav>6162 <div className="mt-3 flex flex-wrap items-center gap-2 text-[13px] text-ink-2">63 <span>64 <span className="ci-num font-medium text-ink">{fmtInt(rows.length)}</span> biomarker{rows.length === 1 ? '' : 's'}65 </span>66 <SourceBadge p={{ sourceSlug: 'ncit-evs', sourceName: 'NCI Thesaurus (NCIt)', dataset: verification ? `EVS REST API, NCIt ${verification.ncitVersion}` : 'EVS REST API', retrievedAt: verification?.verifiedAt ?? null, layer: 'canonical', note: 'Identity metadata is curated; every NCIt code was fetched from the EVS REST API and recorded with its version and date.' }} />67 <ClaimBadge kind="curated" />68 <span className="text-ink-3">identity ·</span>69 <ClaimBadge kind="computed" />70 <span className="text-ink-3">71 counts, rule <span className="ci-mono">{BIOMARKER_LINKS_FORMULA}</span> ·{' '}72 <Link className="ci-link" href="/methodology#biomarkers">73 methodology74 </Link>75 </span>76 </div>7778 {rows.length === 0 ? (79 <div className="mt-3">80 <EmptyState title={total === 0 ? 'Biomarkers not yet available' : 'No biomarker matches'} knows={[{ label: 'Genes', href: '/genes' }, { label: 'Drugs', href: '/drugs' }, { label: 'Approvals', href: '/approvals' }]}>81 {total === 0 ? 'The biomarker catalogue has not been seeded on this environment (pnpm db:seed).' : 'Try a different name, alias, gene symbol or NCIt code, or clear the kind filter.'}82 </EmptyState>83 </div>84 ) : (85 <>86 <div className="ci-table-wrap mt-3">87 <table className="ci-table">88 <thead>89 <tr>90 <th scope="col">Biomarker</th>91 <th scope="col">Kind</th>92 <th scope="col">Gene(s)</th>93 <th scope="col">NCIt</th>94 <th scope="col" className="num" title="Distinct cancers mapped in ACCEPTED CIViC predictive / prognostic / diagnostic items in the biomarker's scope (computed)">95 Cancers96 </th>97 <th scope="col" className="num" title="Drugs with predictive evidence: CIViC therapies in scope ∪ PREDICTS_RESPONSE_TO knowledge-edge targets (computed)">98 Drugs99 </th>100 <th scope="col" className="num" title="Approval records of those drugs, or whose indication text names the biomarker (computed)">101 Approvals102 </th>103 <th scope="col" title="Tumor-agnostic = at least one approval row with the source flag tumor_agnostic = true among those approvals; the curated flag alone is never shown as a badge">104 Tissue-agnostic105 </th>106 <th scope="col" className="num" title="Trials with an active ClinicalTrials.gov status, an intervention mapped to a scope drug and a condition mapped to a scope cancer (computed)">107 Active trials108 </th>109 </tr>110 </thead>111 <tbody>112 {rows.map((b) => {113 const noScope = b.gene_ids.length === 0 && b.variant_ids.length === 0;114 return (115 <tr key={b.id}>116 <td className="min-w-[220px]">117 <Link className="ci-link font-medium" href={`/biomarker/${b.slug}`}>118 {b.name}119 </Link>120 {b.measurement.aliases?.length ? <span className="block text-[11.5px] text-ink-3">{b.measurement.aliases.slice(0, 4).join(' · ')}</span> : null}121 </td>122 <td>123 <Badge tone="outline">{kindLabel(b.kind)}</Badge>124 </td>125 <td className="ci-mono text-[12px]">126 {b.gene_symbols.length ? (127 b.gene_symbols.map((s, i) => (128 <span key={s}>129 {i > 0 ? ', ' : ''}130 <Link className="ci-link" href={`/gene/${s}`}>131 {s}132 </Link>133 </span>134 ))135 ) : (136 <span className="font-sans text-ink-3" title={b.measurement.notes ?? 'No anchor gene'}>137 none138 </span>139 )}140 {b.variant_ids.length ? <span className="block font-sans text-[11px] text-ink-3">{b.variant_ids.length} variant anchor{b.variant_ids.length === 1 ? '' : 's'}</span> : null}141 </td>142 <td className="ci-mono text-[12px]">143 {b.ncit_code ? (144 <a className="ci-link" href={`https://evsexplore.semantics.cancer.gov/evsexplore/concept/ncit/${b.ncit_code}`} target="_blank" rel="noopener noreferrer" title={b.measurement.ncit?.name ?? undefined}>145 {b.ncit_code}146 </a>147 ) : (148 '—'149 )}150 </td>151 <td className="num">{noScope ? <span className="text-ink-4" title="No gene or variant anchor: not computed">—</span> : fmtInt(b.cancers_n)}</td>152 <td className="num">{noScope ? <span className="text-ink-4">—</span> : fmtInt(b.drugs_n)}</td>153 <td className="num">{fmtInt(b.approvals_n)}</td>154 <td>155 {b.tumor_agnostic_n > 0 ? (156 <Badge tone="accent" title={`${b.tumor_agnostic_n} approval row${b.tumor_agnostic_n === 1 ? '' : 's'} flagged tumor_agnostic by the source`}>157 Tumor-agnostic · {b.tumor_agnostic_n}158 </Badge>159 ) : b.measurement.tumorAgnostic ? (160 <span className="text-[11.5px] text-ink-3" title="Curated flag set, but no approval row with tumor_agnostic = true is ingested for this marker yet">161 curated · no row yet162 </span>163 ) : (164 <span className="text-ink-4">—</span>165 )}166 </td>167 <td className="num">{noScope ? <span className="text-ink-4">—</span> : fmtInt(b.active_trials_n)}</td>168 </tr>169 );170 })}171 </tbody>172 </table>173 </div>174 <Freshness dataUpdatedAt={latest} sourceVersion={verification ? `NCIt ${verification.ncitVersion}` : null} extra={verification ? `codes verified ${verification.verifiedAt}` : undefined} />175 <div className="mt-4">176 <Note>177 Counts are computed over ingested sources only (CIViC, ClinicalTrials.gov, openFDA, Health Canada, PubMed) and say nothing about biomarkers or links that are not yet ingested. A trial counted here tests a linked drug in a linked cancer; it does not necessarily select patients on the biomarker.178 </Note>179 </div>180 </>181 )}182 </div>183 );184}185