HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1'use client';2import { InteractiveLineChart, TimelineLanes } from '@/components/charts';3import { Note } from '@/components/ui/section';4import { fmtDate, fmtInt } from '@/lib/format';5import { routes } from '@/lib/site';6import type { FamilyMember } from '@/lib/types';78/*9 Client blocks of the family page: release timeline (TimelineLanes by status) and benchmark progress10 (best rank of any member by member release date; ranks go down = better, so the axis is inverted).11*/1213const STATUS_LANES: { key: string; label: string; color: string }[] = [14 { key: 'active', label: 'Active', color: 'var(--positive)' },15 { key: 'preview', label: 'Preview', color: 'var(--accent)' },16 { key: 'announced', label: 'Announced', color: 'var(--accent)' },17 { key: 'deprecated', label: 'Deprecated', color: 'var(--warning)' },18 { key: 'retired', label: 'Retired', color: 'var(--danger)' },19 { key: 'other', label: 'Other', color: 'var(--ink-3)' },20];2122export function FamilyReleaseLanes({ members }: { members: FamilyMember[] }) {23 const dated = members.filter((m) => typeof m.key_facts?.release_date === 'string' || typeof m.model.attributes?.release_date === 'string');24 const undated = members.length - dated.length;25 if (dated.length === 0) return <Note>No member has a sourced release date yet — the timeline needs at least one.</Note>;26 const laneOf = (m: FamilyMember) => {27 const s = String(m.key_facts?.status ?? m.model.status ?? 'other');28 return STATUS_LANES.some((l) => l.key === s) ? s : 'other';29 };30 const used = new Set(dated.map(laneOf));31 const lanes = STATUS_LANES.filter((l) => used.has(l.key));32 const events = dated.map((m) => {33 const at = String(m.key_facts?.release_date ?? m.model.attributes?.release_date);34 const ranks = Object.values(m.benchmark_ranks ?? {});35 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)}` : ''}` };36 });37 return (38 <div data-family-timeline>39 <TimelineLanes lanes={lanes} events={events} title="Family releases by status" laneWidth={88} />40 <p className="mt-1 text-[11px] text-ink-3">41 {fmtInt(dated.length)} dated releases · dot size = best benchmark rank of the member42 {undated > 0 ? ` · ${fmtInt(undated)} member${undated === 1 ? '' : 's'} without a sourced release date not shown` : ''}43 </p>44 </div>45 );46}4748export function FamilyBenchmarkProgress({ members, benchNames }: { members: FamilyMember[]; benchNames: Record<string, string> }) {49 // per benchmark: (release date, best rank so far among members released up to that date)50 const perBench = new Map<string, { x: Date; y: number; model: string }[]>();51 const dated = members52 .map((m) => ({ m, at: typeof m.key_facts?.release_date === 'string' ? new Date(m.key_facts.release_date) : null }))53 .filter((x): x is { m: FamilyMember; at: Date } => !!x.at && !Number.isNaN(x.at.getTime()))54 .sort((a, b) => a.at.getTime() - b.at.getTime());55 for (const { m, at } of dated) {56 for (const [slug, rank] of Object.entries(m.benchmark_ranks ?? {})) {57 const arr = perBench.get(slug) ?? [];58 const best = arr.length ? Math.min(arr[arr.length - 1]!.y, rank) : rank;59 arr.push({ x: at, y: best, model: m.model.name });60 perBench.set(slug, arr);61 }62 }63 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);64 if (!charts.length) return <Note>Benchmark progress needs at least two dated members with a rank on the same benchmark. Ranks (not scores) are what the family endpoint provides.</Note>;65 return (66 <div className="grid gap-6 md:grid-cols-2 xl:grid-cols-3" data-family-progress>67 {charts.map((c) => {68 const maxRank = Math.max(...c.pts.map((p) => p.y));69 return (70 <div key={c.slug}>71 <p className="mb-1 flex items-baseline justify-between gap-2 text-xs">72 <a href={routes.benchmark(c.slug)} className="truncate font-medium text-ink hover:text-accent">73 {benchNames[c.slug] ?? c.slug}74 </a>75 <span className="tnum text-ink-3">best #{c.pts[c.pts.length - 1]!.y}</span>76 </p>77 <InteractiveLineChart series={[{ name: 'Best rank so far', color: 'var(--type-benchmark)', points: c.pts.map((p) => ({ 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}`} />78 </div>79 );80 })}81 <p className="text-[11px] text-ink-3 md:col-span-2 xl:col-span-3">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.</p>82 </div>83 );84}85