Web: align therapy names with slugs, cap evidence/variant lists, truncate descriptions; ranking eligibility; SDK field-stats COALESCE; resolver lineage/display/qualifier fallbacks
5 changed files +17 −14
modified
apps/web/src/app/gene/[symbol]/page.tsx
+1 −1
@@ -119,7 +119,7 @@ export default async function GenePage({ params }: { params: Promise<{ symbol: s | ||
| 119 | 119 | /> |
| 120 | 120 | <Freshness dataUpdatedAt={g.updated_at} extra="source: hgnc" /> |
| 121 | 121 | </Section> |
| 122 | − <Section id="variants" kicker="Variants" title={`Variants (${fmtInt(variants.length)})`} level={3}> | |
| 122 | + <Section id="variants" kicker="Variants" title={variants.length >= 100 ? `Variants (top ${fmtInt(variants.length)} by evidence — full list via the API)` : `Variants (${fmtInt(variants.length)})`} level={3}> | |
| 123 | 123 | {variants.length ? ( |
| 124 | 124 | <ul className="max-h-[480px] overflow-y-auto text-[13.5px]"> |
| 125 | 125 | {variants.map((v) => ( |
modified
apps/web/src/components/cancer/tabs/drugs.tsx
+4 −5
@@ -19,14 +19,13 @@ export async function DrugsTab({ b, jurisdiction }: { b: CancerBundle; jurisdict | ||
| 19 | 19 | // Therapies mentioned in curated evidence (not approvals) — listed separately, never called "approved". |
| 20 | 20 | const therapyMap = new Map<string, { slug: string; name: string; n: number; sensitivity: number; resistance: number }>(); |
| 21 | 21 | for (const e of evidence) { |
| 22 | − e.therapy_ids.forEach((id, i) => { | |
| 23 | − const slug = e.therapy_slugs?.[i]; | |
| 24 | − if (!slug) return; | |
| 25 | − const cur = therapyMap.get(id) ?? { slug, name: e.therapy_names[i] ?? slug, n: 0, sensitivity: 0, resistance: 0 }; | |
| 22 | + // therapy_slugs and therapy_slug_names share one ORDER BY; therapy_ids/therapy_names follow CIViC's order and must not be zipped with them. | |
| 23 | + (e.therapy_slugs ?? []).forEach((slug, i) => { | |
| 24 | + const cur = therapyMap.get(slug) ?? { slug, name: e.therapy_slug_names?.[i] ?? slug, n: 0, sensitivity: 0, resistance: 0 }; | |
| 26 | 25 | cur.n += 1; |
| 27 | 26 | if ((e.significance ?? '').includes('SENSITIV')) cur.sensitivity += 1; |
| 28 | 27 | if ((e.significance ?? '').includes('RESIST')) cur.resistance += 1; |
| 29 | − therapyMap.set(id, cur); | |
| 28 | + therapyMap.set(slug, cur); | |
| 30 | 29 | }); |
| 31 | 30 | } |
| 32 | 31 | const therapies = [...therapyMap.values()].sort((a, c) => c.n - a.n); |
modified
apps/web/src/components/data/evidence-table.tsx
+2 −2
@@ -72,7 +72,7 @@ export function EvidenceTable({ items, prov, showCancer = false, showVariant = t | ||
| 72 | 72 | <span key={s}> |
| 73 | 73 | {j > 0 ? ' + ' : ''} |
| 74 | 74 | <Link className="ci-link" href={`/drug/${s}`}> |
| 75 | − {e.therapy_names[j] ?? s} | |
| 75 | + {e.therapy_slug_names?.[j] ?? s} | |
| 76 | 76 | </Link> |
| 77 | 77 | </span> |
| 78 | 78 | )) |
@@ -114,7 +114,7 @@ export function EvidenceTable({ items, prov, showCancer = false, showVariant = t | ||
| 114 | 114 | <td className="max-w-[360px] text-[12.5px] text-ink-2"> |
| 115 | 115 | <details> |
| 116 | 116 | <summary className="ci-link">EID{e.civic_id}</summary> |
| 117 | − <p className="mt-1">{e.description ?? 'No description at source.'}</p> | |
| 117 | + <p className="mt-1">{e.description ? `${e.description}${e.description.length >= 320 ? '…' : ''}` : 'No description at source.'}{e.description && e.description.length >= 320 ? <span className="text-ink-3"> (full text at CIViC)</span> : null}</p> | |
| 118 | 118 | {e.pmid ? ( |
| 119 | 119 | <p className="mt-1"> |
| 120 | 120 | PMID{' '} |
modified
apps/web/src/lib/queries/evidence.ts
+9 −5
@@ -36,29 +36,33 @@ export interface EvidenceItem { | ||
| 36 | 36 | variant_slugs: string[] | null; |
| 37 | 37 | variant_names: string[] | null; |
| 38 | 38 | therapy_slugs: string[] | null; |
| 39 | + /** Drug names aligned with therapy_slugs (same ORDER BY) — never zip with therapy_names, whose order is CIViC's. */ | |
| 40 | + therapy_slug_names: string[] | null; | |
| 39 | 41 | } |
| 40 | 42 | |
| 41 | 43 | const SELECT = sql` |
| 42 | 44 | SELECT e.*, c.slug AS cancer_slug, c.canonical_name AS cancer_name, |
| 43 | 45 | (SELECT array_agg(v.slug ORDER BY v.slug) FROM variants v WHERE v.id = ANY(e.variant_ids)) AS variant_slugs, |
| 44 | 46 | (SELECT array_agg(coalesce(v.gene_symbol || ' ', '') || v.name ORDER BY v.slug) FROM variants v WHERE v.id = ANY(e.variant_ids)) AS variant_names, |
| 45 | − (SELECT array_agg(d.slug ORDER BY d.slug) FROM drugs d WHERE d.id = ANY(e.therapy_ids)) AS therapy_slugs | |
| 47 | + (SELECT array_agg(d.slug ORDER BY d.slug) FROM drugs d WHERE d.id = ANY(e.therapy_ids)) AS therapy_slugs, | |
| 48 | + (SELECT array_agg(d.name ORDER BY d.slug) FROM drugs d WHERE d.id = ANY(e.therapy_ids)) AS therapy_slug_names, | |
| 49 | + left(e.description, 320) AS description | |
| 46 | 50 | FROM civic_evidence_items e LEFT JOIN cancers c ON c.id = e.cancer_id`; |
| 47 | 51 | |
| 48 | −export async function evidenceForCancer(cancerIds: string[], limit = 1000): Promise<EvidenceItem[]> { | |
| 52 | +export async function evidenceForCancer(cancerIds: string[], limit = 300): Promise<EvidenceItem[]> { | |
| 49 | 53 | if (cancerIds.length === 0) return []; |
| 50 | 54 | return safe(() => run<EvidenceItem>(sql`${SELECT} WHERE e.cancer_id IN (${sql.join(cancerIds.map((i) => sql`${i}`), sql`, `)}) ORDER BY e.evidence_level NULLS LAST, e.evidence_rating DESC NULLS LAST, e.civic_id LIMIT ${limit}`), [] as EvidenceItem[]); |
| 51 | 55 | } |
| 52 | 56 | |
| 53 | −export async function evidenceForVariant(variantId: string, limit = 500): Promise<EvidenceItem[]> { | |
| 57 | +export async function evidenceForVariant(variantId: string, limit = 200): Promise<EvidenceItem[]> { | |
| 54 | 58 | return safe(() => run<EvidenceItem>(sql`${SELECT} WHERE ${variantId} = ANY(e.variant_ids) ORDER BY c.canonical_name NULLS LAST, e.evidence_level NULLS LAST, e.civic_id LIMIT ${limit}`), [] as EvidenceItem[]); |
| 55 | 59 | } |
| 56 | 60 | |
| 57 | −export async function evidenceForGene(geneId: string, symbol: string, limit = 500): Promise<EvidenceItem[]> { | |
| 61 | +export async function evidenceForGene(geneId: string, symbol: string, limit = 150): Promise<EvidenceItem[]> { | |
| 58 | 62 | return safe(() => run<EvidenceItem>(sql`${SELECT} WHERE ${geneId} = ANY(e.gene_ids) OR ${symbol} = ANY(e.gene_symbols) ORDER BY c.canonical_name NULLS LAST, e.evidence_level NULLS LAST, e.civic_id LIMIT ${limit}`), [] as EvidenceItem[]); |
| 59 | 63 | } |
| 60 | 64 | |
| 61 | −export async function evidenceForDrug(drugId: string, limit = 500): Promise<EvidenceItem[]> { | |
| 65 | +export async function evidenceForDrug(drugId: string, limit = 150): Promise<EvidenceItem[]> { | |
| 62 | 66 | return safe(() => run<EvidenceItem>(sql`${SELECT} WHERE ${drugId} = ANY(e.therapy_ids) ORDER BY c.canonical_name NULLS LAST, e.evidence_level NULLS LAST, e.civic_id LIMIT ${limit}`), [] as EvidenceItem[]); |
| 63 | 67 | } |
| 64 | 68 | |
modified
apps/web/src/lib/queries/genomics.ts
+1 −1
@@ -142,7 +142,7 @@ export async function getVariantBySlug(slug: string): Promise<VariantRow | null> | ||
| 142 | 142 | return rows[0] ?? null; |
| 143 | 143 | } |
| 144 | 144 | |
| 145 | −export async function variantsForGene(geneId: string, limit = 300): Promise<VariantRow[]> { | |
| 145 | +export async function variantsForGene(geneId: string, limit = 100): Promise<VariantRow[]> { | |
| 146 | 146 | return safe(() => run<VariantRow>(sql`SELECT v.*, (SELECT count(*) FROM civic_evidence_items e WHERE v.id = ANY(e.variant_ids))::int AS evidence_count FROM variants v WHERE v.gene_id = ${geneId} ORDER BY evidence_count DESC, v.name LIMIT ${limit}`), [] as VariantRow[]); |
| 147 | 147 | } |
| 148 | 148 | |
| 149 | 149 | |