'use client'; import { InteractiveLineChart, TimelineLanes } from '@/components/charts'; import { Note } from '@/components/ui/section'; import { fmtDate, fmtInt } from '@/lib/format'; import { routes } from '@/lib/site'; import type { FamilyMember } from '@/lib/types'; /* Client blocks of the family page: release timeline (TimelineLanes by status) and benchmark progress (best rank of any member by member release date; ranks go down = better, so the axis is inverted). */ const STATUS_LANES: { key: string; label: string; color: string }[] = [ { key: 'active', label: 'Active', color: 'var(--positive)' }, { key: 'preview', label: 'Preview', color: 'var(--accent)' }, { key: 'announced', label: 'Announced', color: 'var(--accent)' }, { key: 'deprecated', label: 'Deprecated', color: 'var(--warning)' }, { key: 'retired', label: 'Retired', color: 'var(--danger)' }, { key: 'other', label: 'Other', color: 'var(--ink-3)' }, ]; export function FamilyReleaseLanes({ members }: { members: FamilyMember[] }) { const dated = members.filter((m) => typeof m.key_facts?.release_date === 'string' || typeof m.model.attributes?.release_date === 'string'); const undated = members.length - dated.length; if (dated.length === 0) return No member has a sourced release date yet — the timeline needs at least one.; const laneOf = (m: FamilyMember) => { const s = String(m.key_facts?.status ?? m.model.status ?? 'other'); return STATUS_LANES.some((l) => l.key === s) ? s : 'other'; }; const used = new Set(dated.map(laneOf)); const lanes = STATUS_LANES.filter((l) => used.has(l.key)); const events = dated.map((m) => { const at = String(m.key_facts?.release_date ?? m.model.attributes?.release_date); const ranks = Object.values(m.benchmark_ranks ?? {}); return { id: m.model.id, lane: laneOf(m), at, importance: ranks.length ? (Math.min(...ranks) <= 10 ? 3 : Math.min(...ranks) <= 50 ? 2 : 1) : 1, label: m.model.name, href: routes.entity(m.model), sub: `${fmtDate(at)}${ranks.length ? ` · best rank #${Math.min(...ranks)}` : ''}` }; }); return (

{fmtInt(dated.length)} dated releases · dot size = best benchmark rank of the member {undated > 0 ? ` · ${fmtInt(undated)} member${undated === 1 ? '' : 's'} without a sourced release date not shown` : ''}

); } export function FamilyBenchmarkProgress({ members, benchNames }: { members: FamilyMember[]; benchNames: Record }) { // per benchmark: (release date, best rank so far among members released up to that date) const perBench = new Map(); const dated = members .map((m) => ({ m, at: typeof m.key_facts?.release_date === 'string' ? new Date(m.key_facts.release_date) : null })) .filter((x): x is { m: FamilyMember; at: Date } => !!x.at && !Number.isNaN(x.at.getTime())) .sort((a, b) => a.at.getTime() - b.at.getTime()); for (const { m, at } of dated) { for (const [slug, rank] of Object.entries(m.benchmark_ranks ?? {})) { const arr = perBench.get(slug) ?? []; const best = arr.length ? Math.min(arr[arr.length - 1]!.y, rank) : rank; arr.push({ x: at, y: best, model: m.model.name }); perBench.set(slug, arr); } } const charts = [...perBench.entries()].map(([slug, pts]) => ({ slug, pts: pts.filter((p, i, a) => i === 0 || p.x.getTime() !== a[i - 1]!.x.getTime() || p.y !== a[i - 1]!.y) })).filter((c) => new Set(c.pts.map((p) => p.x.toISOString().slice(0, 10))).size >= 2).sort((a, b) => a.pts[a.pts.length - 1]!.y - b.pts[b.pts.length - 1]!.y).slice(0, 6); if (!charts.length) return Benchmark progress needs at least two dated members with a rank on the same benchmark. Ranks (not scores) are what the family endpoint provides.; return (
{charts.map((c) => { const maxRank = Math.max(...c.pts.map((p) => p.y)); return (

{benchNames[c.slug] ?? c.slug} best #{c.pts[c.pts.length - 1]!.y}

({ x: p.x, y: p.y })) }]} height={150} step showDots yDomain={[Math.max(2, Math.ceil(maxRank * 1.15)), 1]} yFormat={(v) => `#${Math.round(v)}`} yLabel={`Best rank of the family on ${benchNames[c.slug] ?? c.slug}`} />
); })}

Best rank (primary comparability group, current leaderboard) reached by any member released up to each date — lower is better, axis inverted. Ranks are today's ranks, not the ranks at release time.

); }