spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import Link from 'next/link';2import { Fragment, type ReactNode } from 'react';3import { Badge, ClaimBadge, MatchBadge, StatusBadge, isExactMatch } from '@/components/ui/badge';4import { SourceBadge, TableProvenance } from '@/components/ui/source-badge';5import { EVIDENCE_LEVEL_LABEL, EVIDENCE_DESCRIPTION_CHARS, groupEvidence, therapyKey, type EvidenceItem, type EvidenceGroupBy } from '@/lib/queries/evidence';6import type { ProvRow } from '@/lib/queries/provenance';7import { toInfo } from '@/lib/queries/provenance';8import { humanize } from '@/lib/format';910const CIVIC = { sourceSlug: 'civic', sourceName: 'CIViC' };1112function Direction({ e }: { e: EvidenceItem }) {13 const dir = e.evidence_direction ?? '—';14 const sig = e.significance ?? '';15 const tone = dir === 'DOES_NOT_SUPPORT' ? 'warn' : sig.includes('RESIST') ? 'danger' : sig.includes('SENSITIV') ? 'ok' : 'neutral';16 return (17 <>18 <Badge tone={tone}>{humanize(dir)}</Badge>19 {sig ? (20 <>21 {' '}22 <Badge tone="outline">{humanize(sig.replace('SENSITIVITYRESPONSE', 'SENSITIVITY_RESPONSE'))}</Badge>23 </>24 ) : null}25 </>26 );27}2829/**30 * CIViC evidence in ONE dense table. Rows are grouped (by molecular profile → therapy, or by cancer31 * on variant pages) with a `<tbody>` per group and a row-group header, instead of one table per32 * group: a table header is rendered once, which halves the markup for pages with many small groups33 * (every element ships twice — HTML + RSC payload). Levels, directions and significance are shown34 * in their native form (§3, never collapsed to works/doesn't). One provenance popover for the whole35 * table (all rows come from one CIViC release); rows carry a compact badge.36 *37 * `summary` replaces the default "N evidence items" sentence — pass the page window when the caller38 * paginates ("Showing 1–50 of 405 evidence items").39 */40export function EvidenceTable({ items, prov, showCancer = false, groupBy = 'variant', summary }: { items: EvidenceItem[]; prov: Map<number, ProvRow>; showCancer?: boolean; groupBy?: EvidenceGroupBy; summary?: ReactNode }) {41 const groups = groupEvidence(items, groupBy);42 const first = items[0] ? toInfo(prov.get(items[0].provenance_id)) : null;43 const cancerCol = showCancer && groupBy !== 'cancer';44 const profileCol = groupBy === 'cancer';45 const cols = 7 + (cancerCol ? 1 : 0) + (profileCol ? 1 : 0);46 return (47 <div>48 <TableProvenance p={first ?? CIVIC} claim={<ClaimBadge kind="curated" />}>49 {summary ?? `${items.length} evidence item${items.length === 1 ? '' : 's'}`} · levels, directions and significance as curated at the source; each row links to its CIViC record.50 </TableProvenance>51 <div className="ci-table-wrap">52 <table className="ci-table ci-evidence">53 <thead>54 <tr>55 {profileCol ? <th scope="col">Molecular profile</th> : null}56 <th scope="col">Therapy</th>57 {cancerCol ? <th scope="col">Cancer</th> : null}58 <th scope="col">Type</th>59 <th scope="col" title="CIViC evidence level: A validated · B clinical · C case study · D preclinical · E inferential">60 Level61 </th>62 <th scope="col" title="Evidence direction and clinical significance as curated at the source">63 Direction · significance64 </th>65 <th scope="col" className="num">66 Rating (1–5)67 </th>68 <th scope="col">Status</th>69 <th scope="col">Evidence</th>70 <th scope="col">Source</th>71 </tr>72 </thead>73 {groups.map((g) => (74 <tbody key={g.key}>75 {groupBy !== 'none' ? (76 <tr className="ci-group">77 <th scope="rowgroup" colSpan={cols}>78 {g.href ? (79 <Link href={g.href}>80 {g.label}81 </Link>82 ) : (83 <span>{g.label}</span>84 )}85 {g.genes.length > 186 ? g.genes.map((s) => (87 <Link key={s} href={`/gene/${s}`} className="ci-mono ml-2 text-[12px] text-ink-3 hover:text-accent">88 {s}89 </Link>90 ))91 : null}92 <span className="ci-num ml-2 text-[12px] text-ink-3">{g.items.length}</span>93 {g.unmapped ? (94 <Badge tone="danger" className="ml-2">95 unmapped disease96 </Badge>97 ) : null}98 </th>99 </tr>100 ) : null}101 {g.items.map((e, i) => {102 const prevKey = i > 0 ? therapyKey(g.items[i - 1]!) : null;103 const sameTherapy = prevKey === therapyKey(e) && groupBy === 'variant';104 const desc = e.description ?? '';105 const cut = desc.length >= EVIDENCE_DESCRIPTION_CHARS;106 return (107 <tr key={e.id}>108 {profileCol ? <td className="w-t text-[12.5px]">{e.molecular_profile_name ?? e.name ?? '—'}</td> : null}109 <td className="w-t">110 {sameTherapy ? (111 <span className="text-ink-4" aria-label="same therapy as the row above">112 〃113 </span>114 ) : e.therapy_slugs?.length ? (115 e.therapy_slugs.map((s, j) => (116 <Fragment key={s}>117 {j > 0 ? ' + ' : ''}118 <Link href={`/drug/${s}`}>119 {e.therapy_slug_names?.[j] ?? s}120 </Link>121 </Fragment>122 ))123 ) : (124 <span className="text-ink-3">{therapyLabel(e)}</span>125 )}126 {e.therapy_interaction_type && !sameTherapy ? <span className="block text-[11px] text-ink-3">{humanize(e.therapy_interaction_type)}</span> : null}127 </td>128 {cancerCol ? (129 <td className="w-t">130 {e.cancer_slug ? (131 <Link href={`/cancer/${e.cancer_slug}`}>132 {e.cancer_name}133 </Link>134 ) : (135 <span className="text-ink-3">{e.disease_name ?? '—'}</span>136 )}137 {/* Exact mappings are the norm; only non-exact ones (alias, broader, probabilistic, unresolved) carry the caveat badge. */}138 {!isExactMatch(e.cancer_match_type) ? <MatchBadge matchType={e.cancer_match_type} className="ml-1" /> : null}139 </td>140 ) : null}141 <td>142 <Badge>{humanize(e.evidence_type)}</Badge>143 </td>144 <td>145 <abbr {...(e.evidence_level && EVIDENCE_LEVEL_LABEL[e.evidence_level] ? { title: EVIDENCE_LEVEL_LABEL[e.evidence_level] } : {})}>146 {e.evidence_level ?? '—'}147 </abbr>148 </td>149 <td>150 <Direction e={e} />151 </td>152 <td className="num">{e.evidence_rating ?? '—'}</td>153 <td>154 <StatusBadge status={e.status} />155 </td>156 <td className="ev">157 <details>158 <summary>EID{e.civic_id}</summary>159 <p>160 {desc ? `${desc}${cut ? '…' : ''}` : 'No description at source.'}161 {cut ? <span className="text-ink-3"> (full text at CIViC)</span> : null}162 </p>163 <p>164 {e.pmid ? (165 <>166 PMID{' '}167 <a href={`https://pubmed.ncbi.nlm.nih.gov/${e.pmid}/`} target="_blank">168 {e.pmid}169 </a>170 {e.source_citation ? <span className="text-ink-3"> · {e.source_citation}</span> : null}171 {' · '}172 </>173 ) : null}174 <a href={`https://civicdb.org/evidence/${e.civic_id}/summary`} target="_blank">175 Open in CIViC176 </a>177 </p>178 </details>179 </td>180 <td>181 <SourceBadge compact title={null} p={toInfo(prov.get(e.provenance_id)) ?? CIVIC} />182 </td>183 </tr>184 );185 })}186 </tbody>187 ))}188 </table>189 </div>190 </div>191 );192}193194function therapyLabel(e: EvidenceItem): string {195 if (e.therapy_names.length) return e.therapy_names.join(' + ');196 return e.evidence_type === 'PREDICTIVE' ? 'Unspecified therapy' : `(${(e.evidence_type ?? 'evidence').toLowerCase()})`;197}198