import { ExternalLink } from 'lucide-react'; import Link from 'next/link'; import { hostOf, fmtInt, num, plural } from '@/lib/format'; import { routes } from '@/lib/site'; import type { TodayItem, TodaySection } from '@/lib/types'; import { ChangeRow } from './change-row'; /** Fixed order of the Today in AI 2.0 sections (API keys) — sections absent from the payload are listed as quiet. */ export const TODAY_ORDER = ['MAJOR_RELEASES', 'PRICE_MOVES', 'BENCHMARK_MOVES', 'MODEL_CHANGES', 'RESEARCH', 'OPEN_WEIGHT_RELEASES', 'DEPRECATIONS', 'PROVIDER_CHANGES', 'HARDWARE'] as const; export const TODAY_LABELS: Record = { MAJOR_RELEASES: 'Major releases', PRICE_MOVES: 'Price moves', BENCHMARK_MOVES: 'Benchmark moves', MODEL_CHANGES: 'Model changes', RESEARCH: 'Research', OPEN_WEIGHT_RELEASES: 'Open-weight releases', DEPRECATIONS: 'Deprecations', PROVIDER_CHANGES: 'Provider changes', HARDWARE: 'Hardware', }; /** Section key → the `/changes` filter that lists everything behind it. */ export const TODAY_FEED_HREF: Record string> = { MAJOR_RELEASES: (d) => `/changes?importance_min=3&since=${d}&until=${d}`, PRICE_MOVES: (d) => `/changes?category=price&since=${d}&until=${d}`, BENCHMARK_MOVES: (d) => `/changes?category=benchmark&since=${d}&until=${d}`, MODEL_CHANGES: (d) => `/changes?category=model&since=${d}&until=${d}`, RESEARCH: (d) => `/changes?category=paper&since=${d}&until=${d}`, OPEN_WEIGHT_RELEASES: (d) => `/changes?type=NEW_MODEL&since=${d}&until=${d}`, DEPRECATIONS: (d) => `/changes?type=DEPRECATION_ANNOUNCED&since=${d}&until=${d}`, PROVIDER_CHANGES: (d) => `/changes?category=provider&since=${d}&until=${d}`, HARDWARE: (d) => `/changes?category=hardware&since=${d}&until=${d}`, }; export function orderedSections(today: TodaySection[] | undefined): { present: TodaySection[]; quiet: string[] } { const byKey = new Map((today ?? []).map((s) => [s.key, s])); const present: TodaySection[] = []; const quiet: string[] = []; for (const k of TODAY_ORDER) { const s = byKey.get(k); if (s && s.items.length) present.push(s); else quiet.push(k); } for (const s of today ?? []) if (!(TODAY_ORDER as readonly string[]).includes(s.key) && s.items.length) present.push(s); return { present, quiet }; } /** "1 release · 4 documents" + expandable source list for grouped items (events sharing a group_key). */ export function GroupedSources({ it }: { it: TodayItem }) { const grouped = num(it.grouped_events) ?? 1; const docs = it.documents ?? []; const sources = num(it.sources) ?? docs.length; if (grouped <= 1 && docs.length <= 1) return null; const noun = it.event_type === 'RELEASE' || it.event_type === 'NEW_MODEL' ? 'release' : it.event_type === 'NEW_PAPER' ? 'paper' : 'event'; return (
1 {noun} · {fmtInt(grouped)} {plural(grouped, 'event')} · {fmtInt(docs.length || sources)} {plural(docs.length || sources, 'document')} {sources > 1 && docs.length !== sources ? ` · ${fmtInt(sources)} sources` : ''}
); } export function TodayItemRow({ it }: { it: TodayItem }) { return } />; } export function QuietLine({ quiet, labels }: { quiet: string[]; labels?: Record }) { if (!quiet.length) return null; return (

Quiet today: {quiet.map((k, i) => ( {i > 0 && ' · '} {labels?.[k] ?? TODAY_LABELS[k] ?? k} ))} . Timeline →

); }