'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.
);
}