import { ExternalLink } from 'lucide-react'; import Link from 'next/link'; import { EntityBadge, ImportanceMark } from '@/components/ui/badges'; import { LiveAgo } from '@/components/ui/live'; import { cn } from '@/lib/cn'; import { fmtDate, fmtDateTime, fmtUsdPerM, fmtValue } from '@/lib/format'; import { eventLabel, eventTone, propertyLabel, routes } from '@/lib/site'; import type { ChangeEvent } from '@/lib/types'; const TONE: Record, string> = { new: 'text-positive', price: 'text-accent-2', warn: 'text-warning', danger: 'text-danger', bench: 'text-type-benchmark', neutral: 'text-ink-2', }; function host(url: string): string { try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return 'source'; } } /** Price payloads (`PRICE_CHANGED`, `PROVIDER_LISTED`) are `{ input_per_mtok, output_per_mtok }` objects — never JSON-dump them. */ function fmtSide(v: unknown, key?: string): string { if (v && typeof v === 'object' && !Array.isArray(v)) { const o = v as Record; if ('input_per_mtok' in o || 'output_per_mtok' in o) return `${fmtUsdPerM(o.input_per_mtok)} in / ${fmtUsdPerM(o.output_per_mtok)} out`; const parts = Object.entries(o) .filter(([, x]) => x !== null && x !== undefined && typeof x !== 'object') .slice(0, 3) .map(([k, x]) => `${propertyLabel(k).toLowerCase()} ${fmtValue(x, k)}`); return parts.length ? parts.join(' · ') : fmtValue(v, key); } return fmtValue(v, key); } /** Old → new rendering for property changes and prices. */ export function Delta({ e }: { e: ChangeEvent }) { if (e.old_value === undefined || e.old_value === null || e.new_value === undefined || e.new_value === null) return null; const key = e.property ?? undefined; return ( {fmtSide(e.old_value, key)} → {fmtSide(e.new_value, key)} ); } /** * One event in a feed: importance meter · time · entity badge + name · summary · property delta · source. * `dense` (homepage) hides the delta and connector; `showDate` prints the date instead of relative time. */ export function ChangeRow({ e, dense = false, showDate = false, className, live = true, trailing }: { e: ChangeEvent; dense?: boolean; showDate?: boolean; className?: string; live?: boolean; /** Extra node after the summary (D3: grouped sources, evidence links). */ trailing?: React.ReactNode }) { const tone = eventTone(e.event_type); // 1.1: occurred_at = coalesce(effective_at, observed_at); the title always tells when AI Atlas observed it. const when = e.occurred_at ?? e.effective_at ?? e.observed_at; const observedTitle = `observed ${fmtDateTime(e.observed_at)}${e.effective_at ? ` · effective ${fmtDate(e.effective_at)}` : ''}`; return (
  • {showDate ? : live ? : } {e.is_backfill && backfill}
    {eventLabel(e.event_type)} {e.entity && ( <> {e.entity.name} {e.entity.organization && {e.entity.organization.name}} )}

    {e.summary}

    {!dense && (
    {e.property && {propertyLabel(e.property)}} {e.connector_name && {e.connector_name}}
    )} {trailing}
    {e.source_url ? ( {host(e.source_url)} ) : ( — )}
  • ); } /** Group events by UTC day (YYYY-MM-DD of `occurred_at` when present — 1.1 feeds — else observed_at). */ export function groupByDay(events: ChangeEvent[], field: 'occurred' | 'observed' = 'occurred'): { day: string; items: ChangeEvent[] }[] { const map = new Map(); for (const e of events) { const day = (field === 'occurred' ? e.occurred_at ?? e.effective_at ?? e.observed_at : e.observed_at).slice(0, 10); const arr = map.get(day); if (arr) arr.push(e); else map.set(day, [e]); } return [...map.entries()].map(([day, items]) => ({ day, items })); }