import type { Metadata } from 'next'; import Link from 'next/link'; import { PageHeader, Note } from '@/components/ui/section'; import { EmptyState } from '@/components/ui/empty-state'; import { Freshness } from '@/components/ui/freshness'; import { ClaimBadge } from '@/components/ui/badge'; import { SourceBadge, TableProvenance } from '@/components/ui/source-badge'; import { listTrialIntelligence, type TrialIntelRow } from '@/lib/queries/trial-intelligence'; import { fmtInt, fmtNum, fmtPct, toDate } from '@/lib/format'; import { fmtGrowth, intelTotals, isIntelSortKey, sortIntel, type IntelSortKey } from '@/lib/trial-intel'; import { oneOf, str, withParams, type SP } from '@/lib/search-params'; export const metadata: Metadata = { title: 'Clinical trial intelligence', description: 'Per-cancer trial activity, growth, enrollment, sponsor and country concentration, termination share and burden-normalized intensity, computed from ClinicalTrials.gov with a versioned formula.' }; export const revalidate = 600; const ALL_LIMIT = 150; const COLUMNS: Array<{ key: IntelSortKey; label: string; title: string; num?: boolean }> = [ { key: 'name', label: 'Cancer', title: 'Cancer entity; figures aggregate the entity and its NCIt-hierarchy descendants' }, { key: 'total', label: 'Total', title: 'Interventional studies mapped to the entity or a descendant (any status)', num: true }, { key: 'active', label: 'Active', title: 'Recruiting, not yet recruiting, enrolling by invitation or active-not-recruiting interventional studies', num: true }, { key: 'recruiting', label: 'Recruiting', title: 'Interventional studies with overall status RECRUITING', num: true }, { key: 'phase3Recruiting', label: 'Ph III active / recruiting', title: 'Active (and recruiting) interventional studies with PHASE3 among their phases; a PHASE2|PHASE3 study counts in both phases', num: true }, { key: 'growth', label: 'Growth YoY', title: '(studies first posted in the last 12 months − studies first posted in the preceding 12 months) / preceding; null when the preceding window has fewer than 20 studies', num: true }, { key: 'avgEnrollment', label: 'Avg enrollment', title: 'Mean (median in title) enrollment_count over active interventional studies, as posted (anticipated or actual)', num: true }, { key: 'industryShare', label: 'Industry share', title: 'Share of active interventional studies whose lead sponsor class is INDUSTRY', num: true }, { key: 'sponsorHhi', label: 'Sponsor HHI · top sponsor', title: 'Herfindahl–Hirschman index of lead sponsors over active interventional studies (Σ share², 1 = one sponsor); null under 10 active studies', num: true }, { key: 'distinctCountries', label: 'Countries · US share', title: 'Distinct countries with a site among active interventional studies; share of active studies with at least one US site', num: true }, { key: 'terminationShare', label: 'Termination share', title: '(terminated + withdrawn) / (completed + terminated + withdrawn) over interventional studies first posted since 2010-01-01; null under 30 terminal studies', num: true }, { key: 'trialsPer1000Deaths', label: 'Trials / 1,000 deaths', title: 'Active interventional studies per 1,000 annual deaths (US, latest year with both incidence and mortality counts from one source; deaths ≥ 100). Top-level cancers only', num: true }, ]; export default async function TrialIntelligencePage({ searchParams }: { searchParams: Promise }) { const sp = await searchParams; const level = oneOf(sp, 'level', ['top', 'all'] as const, 'top'); const sortRaw = str(sp, 'sort', 'active'); const sort: IntelSortKey = isIntelSortKey(sortRaw) ? sortRaw : 'active'; const order = oneOf(sp, 'order', ['asc', 'desc'] as const, sort === 'name' ? 'asc' : 'desc'); const fetched = await listTrialIntelligence(level, level === 'all' ? ALL_LIMIT : 10_000); const rows = sortIntel(fetched, sort, order); const totals = intelTotals(rows); const current = { level, sort, order }; const href = (o: Record) => `/trials/intelligence${withParams(current, o)}`; const computedAt = rows.reduce((m, r) => { const d = toDate(r.computed_at); return d && (!m || d > m) ? d : m; }, null); const first = rows[0]; const th = (first?.inputs.thresholds ?? {}) as Record; const windows = (first?.inputs.windows ?? null) as { new12m: { from: string; to: string }; prior12m: { from: string; to: string } } | null; const burdenSlugs = [...new Set(rows.map((r) => r.burden_source_slug).filter((s): s is string => !!s))]; const sortLink = (key: IntelSortKey, label: string, title: string, num?: boolean) => { const active = sort === key; const nextOrder = active ? (order === 'desc' ? 'asc' : 'desc') : key === 'name' ? 'asc' : 'desc'; return ( {label} {active ? {order === 'asc' ? '↑' : '↓'} : null} ); }; return (
{rows.length === 0 ? ( The trial-intelligence layer is recomputed from the ClinicalTrials.gov records after each ingest (pnpm cix intel). Nothing is shown until it has run on this environment. ) : ( <> {level === 'top' ? (
{[ ['Top-level cancers', fmtInt(totals.entities), 'Mutually exclusive registry set; sums below do not double count'], ['Interventional studies', fmtInt(totals.total), 'Sum of total interventional studies over the top-level set'], ['Active', fmtInt(totals.active), 'Sum of active interventional studies'], ['Recruiting', fmtInt(totals.recruiting), 'Sum of recruiting interventional studies'], ['Phase III active', fmtInt(totals.phase3Active), 'Sum of active studies with PHASE3 among their phases'], ['Phase III recruiting', fmtInt(totals.phase3Recruiting), 'Sum of recruiting studies with PHASE3 among their phases'], ].map(([k, v, t]) => (
{k}
{v}
))}
) : ( Entities at this level overlap (a subtype and its parent both count the same study), so column totals are not shown. The {ALL_LIMIT} entities with the most active studies are listed; use the API (/api/v1/trials/intelligence?level=all) or the CSV export for the full set. )}
}> {fmtInt(rows.length)} entities · sorted by {COLUMNS.find((c) => c.key === sort)?.label ?? sort} ({order}) · click a header to sort
{COLUMNS.map((c) => sortLink(c.key, c.label, c.title, c.num))} {rows.map((r) => ( ))}

formula {first?.formula_version} · interventional studies only · aggregation over NCIt descendants (depth ≤ {String(th.maxHierarchyDepth ?? 12)}) · active = {(first?.inputs.activeStatuses as string[] | undefined)?.join(', ')}

Thresholds: growth requires ≥ {String(th.growthMinPriorTrials ?? 20)} studies in the prior window{windows ? ` (windows ${windows.new12m.from} → ${windows.new12m.to} vs ${windows.prior12m.from} → ${windows.prior12m.to})` : ''}; sponsor HHI requires ≥ {String(th.hhiMinActiveTrials ?? 10)} active studies; termination share requires ≥ {String(th.terminationMinTerminalTrials ?? 30)} terminal studies first posted since {String(th.terminationSince ?? '2010-01-01')}; burden ratios require ≥ {String(th.burdenMinDeaths ?? 100)} annual deaths ({String(th.burdenGeography ?? 'USA')}, latest year with both counts from one source{burdenSlugs.length ? `: ${burdenSlugs.join(', ')}` : ''}). A multinational study contributes to every country it lists, so country shares can sum above 100%.{' '} Methodology

)}
); } function IntelRow({ r }: { r: TrialIntelRow }) { return ( {r.cancer_name} {fmtInt(r.total_trials)} {fmtInt(r.active_trials)} {fmtInt(r.recruiting_trials)} {fmtInt(r.phase3_active)} / {fmtInt(r.phase3_recruiting)} 0 ? 'text-ok' : r.trial_growth_yoy != null && r.trial_growth_yoy < 0 ? 'text-danger' : ''}`} title={`${fmtInt(r.new_trials_12m)} first posted in the last 12 months vs ${fmtInt(r.new_trials_prior_12m)} in the preceding 12 months${r.trial_growth_yoy == null ? ' — below the 20-study threshold, not computed' : ''}`}> {fmtGrowth(r.trial_growth_yoy)} {fmtNum(r.avg_enrollment, 0)} {fmtPct(r.industry_share, 0)} {fmtNum(r.sponsor_hhi, 3)} {r.top_sponsor ? {r.top_sponsor} : null} {fmtInt(r.distinct_countries)} · {fmtPct(r.us_share, 0)} {fmtPct(r.termination_share, 1)} {r.trials_per_1000_deaths != null ? ( {fmtNum(r.trials_per_1000_deaths, 1)} {r.burden_geography} {r.burden_year} {r.burden_source_slug ? : null} ) : ( — )} ); }