import type { Metadata } from 'next'; import Link from 'next/link'; import { PageHeader, Section, KV, Note } from '@/components/ui/section'; import { Badge, ClaimBadge, claimKindFromCategory } from '@/components/ui/badge'; import { EmptyState } from '@/components/ui/empty-state'; import { Freshness } from '@/components/ui/freshness'; import { SourceBadge } from '@/components/ui/source-badge'; import { RadialGraph } from '@/components/graph/radial-graph'; import { PathChainView } from '@/components/graph/path-chain'; import { defaultFocus, edgesFreshness, loadNeighborhood, loadPaths, resolveFocus, suggestedFoci, DEFAULT_GROUP_LIMIT, EXPANDED_GROUP_LIMIT, TRIAL_GROUP_LIMIT, FREQ_MIN, CASES_MIN, type FocusSuggestion } from '@/lib/queries/graph'; import { loadProvenance, toInfo } from '@/lib/queries/provenance'; import { type GraphEdge, type GraphNode, NODE_TYPE_LABEL, NODE_TYPE_ORDER, focusHref, nodeKey, parseFocus, relationshipLabel } from '@/lib/graph-model'; import { fmtInt } from '@/lib/format'; import { str, withParams, type SP } from '@/lib/search-params'; export const dynamic = 'force-dynamic'; const MAX_DRAWN = 60; export async function generateMetadata({ searchParams }: { searchParams: Promise }): Promise { const sp = await searchParams; const f = parseFocus(str(sp, 'focus')); const node = f ? await resolveFocus(f) : null; const title = node ? `${node.label} — knowledge graph` : 'Knowledge graph'; return { title, description: node ? `Contextual knowledge graph around ${node.label}: source-native cancer–gene–variant–drug edges with evidence level and cancer context, plus derived registry counts (trials, cohort frequencies, approvals).` : 'Cancer–gene–variant–drug–trial knowledge graph: every edge carries its cancer context, direction, evidence level and provenance.', robots: { index: !str(sp, 'more') }, alternates: { canonical: node ? `/graph?focus=${node.type}:${encodeURIComponent(node.ref)}` : '/graph' }, }; } function FocusForm({ focus, suggestions }: { focus: string; suggestions: FocusSuggestion[] }) { return (

Accepted: cancer:<slug>, gene:<symbol>, variant:<slug>, drug:<slug>, trial:<NCT id>, or a bare CI id. Find slugs with{' '} search .

{suggestions.length ? (

Most connected {suggestions.map((s) => ( {s.type === 'gene' ? {s.label} : s.label} {fmtInt(s.edges)} ))}

) : null}
); } function DirectionCell({ e }: { e: GraphEdge }) { if (!e.direction) return —; const tone = e.direction === 'resistance' ? 'warn' : e.direction === 'sensitivity' ? 'ok' : e.direction === 'mixed' ? 'warn' : 'neutral'; return {e.direction}; } function ContextCell({ e }: { e: GraphEdge }) { if (e.cancerContext.length === 0) return {e.derived ? 'not mapped' : '—'}; const shown = e.cancerContext.slice(0, 3); return ( {shown.map((c, i) => ( {c.slug ? ( {c.name} ) : ( {c.id} )} {i < shown.length - 1 ? ',' : ''} ))} {e.cancerContext.length > 3 ? +{e.cancerContext.length - 3} : null} ); } export default async function GraphPage({ searchParams }: { searchParams: Promise }) { const sp = await searchParams; const focusParam = str(sp, 'focus').slice(0, 200); const more = str(sp, 'more').slice(0, 40).toUpperCase() || null; const suggestions = await suggestedFoci(); const requested = parseFocus(focusParam); const focusRef = requested ?? (await defaultFocus()); const focus = focusRef ? await resolveFocus(focusRef) : null; if (!focus) { return (
({ label: `${s.label} (${s.type})`, href: `/graph?focus=${s.type}:${encodeURIComponent(s.ref)}` })), { label: 'Search entities', href: '/search' }]}> {requested ? 'The reference must be the entity slug (cancer, variant, drug), the HGNC symbol (gene) or the NCT id (trial). Use search to find it, or pick a suggested focus.' : 'No knowledge edges are loaded on this environment yet.'}
); } const nb = await loadNeighborhood(focus, { more }); const [paths, freshAt] = await Promise.all([focus.type === 'cancer' ? loadPaths(focus, nb.cancerIds) : Promise.resolve([]), edgesFreshness(focus)]); const allEdges = nb.groups.flatMap((g) => g.edges); const prov = await loadProvenance(allEdges.map((e) => e.provenanceIds[0]).filter((x): x is number => typeof x === 'number')); const nodeByKey = new Map(nb.nodes.map((n) => [nodeKey(n), n])); const totalEdges = nb.groups.reduce((a, g) => a + g.total, 0); const current = { focus: `${focus.type}:${focus.ref}`, more: more ?? '' }; const hrefMore = (rel: string | null) => `/graph${withParams(current, { more: rel ?? '' })}#edges`; const typeOrder = NODE_TYPE_ORDER.filter((t) => nb.degreeByType[t] > 0); return (
{focus.label} : focus.label} lede={focus.sublabel ? focus.sublabel.replace(/_/g, ' ') : undefined}>
{focus.id} }, { k: 'Entity page', v: ( {focus.href} ↗ ), }, { k: 'Edges', v: {fmtInt(totalEdges)} }, ]} /> {typeOrder.map((t) => ( {fmtInt(nb.degreeByType[t])} {NODE_TYPE_LABEL[t].toLowerCase()} ))} ) : ( 'none' ), }, { k: 'Drawn', v: `${fmtInt(Math.min(nb.nodes.length, MAX_DRAWN))} of ${fmtInt(nb.nodes.length)} neighbours (cap ${MAX_DRAWN}); table lists every fetched edge` }, ]} />
{nb.nodes.length === 0 ? ( ({ label: s.label, href: `/graph?focus=${s.type}:${encodeURIComponent(s.ref)}` }))]}> No knowledge edge or registry link touches this {focus.type} on this environment. Edges appear once a source (CIViC, ChEMBL, openFDA, ClinicalTrials.gov, GDC) states one — CancerIndex does not infer them. ) : (
{focus.type !== 'cancer' ? (

Pick a cancer in the graph or from the neighbour table to see gene → variant → drug → approval → trial chains. {nb.nodes.some((n) => n.type === 'cancer') ? ( <> {' '} Cancers here:{' '} {nb.nodes .filter((n) => n.type === 'cancer') .slice(0, 5) .map((n, i) => ( {i > 0 ? ', ' : ''} {n.label} ))} . ) : null}

) : paths.length === 0 ? ( No PREDICTS_RESPONSE_TO edge with direction “sensitivity” names this cancer (or a descendant) in its context, so no chain can be assembled. The neighbour table still lists what the sources state. ) : ( <>
    {paths.map((c) => ( ))}

Each hop keeps its own claim category: cohort alteration frequency (observed, cases ≥ {CASES_MIN}), CIViC predictive edge (curated, level as stated by CIViC), regulatory approval (authority, jurisdiction, date as published), trial count (registry). A missing hop is shown as missing — never filled in. Not treatment guidance.

)}
)} {nb.groups.length ? (
{nb.groups.map((g) => { const expanded = more === g.relationshipType; return ( {g.edges.map((e) => { const nnode = nodeByKey.get(e.neighborKey); if (!nnode) return null; const p = toInfo(prov.get(e.provenanceIds[0] ?? -1)); const expand = focusHref(nnode.type, nnode.ref); return ( ); })} ); })}
Relationship Neighbour Direction Evidence level Cancer context Support Claim Source Expand
{focus.label} {relationshipLabel(g.relationshipType)} … {g.relationshipType} {g.derived ? derived : null} {fmtInt(g.edges.length)} of {fmtInt(g.total)} {g.total > g.edges.length ? ( show all (up to {EXPANDED_GROUP_LIMIT}) ) : expanded ? ( show fewer ) : null}
{e.via ? ( in context · ) : ( {e.outgoing ? '→' : '←'} )} {relationshipLabel(e.relationshipType)} {nnode.type === 'gene' ? {nnode.label} : nnode.label} {nnode.sublabel && nnode.type !== 'gene' ? {nnode.sublabel} : null} {e.via ? ( {' '} → {relationshipLabel(e.relationshipType)}{' '} {e.via.label} ) : null} {e.detail ?
{e.detail}
: null}
{e.evidenceLevel ? {e.evidenceLevel} : —} {fmtInt(e.supportCount)} {e.sourceSlugs.map((s) => ( ))} {expand ? ( graph → ) : ( open ↗ )}
An edge is a relationship stated by a source (CIViC evidence item, ChEMBL indication or mechanism, openFDA approval) and kept with its native evidence level, direction and cancer context — CancerIndex never infers, merges or re-scales it. Rows marked derived are counts and measurements read from registries (ClinicalTrials.gov conditions and interventions, GDC/cBioPortal cohort frequencies ≥ {Math.round(FREQ_MIN * 100)} % with ≥ {CASES_MIN} cases affected, regulatory approval records): they say how often two entities co-occur in a registry, not that a source asserted a biological or clinical link. Trials are rolled up over the cancer and its descendants; approvals list authority, jurisdiction and date as published.
) : null}
); }