HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import { ExternalLink } from 'lucide-react';2import Link from 'next/link';3import { EntityBadge, ImportanceMark } from '@/components/ui/badges';4import { LiveAgo } from '@/components/ui/live';5import { cn } from '@/lib/cn';6import { fmtDate, fmtDateTime, fmtUsdPerM, fmtValue } from '@/lib/format';7import { eventLabel, eventTone, propertyLabel, routes } from '@/lib/site';8import type { ChangeEvent } from '@/lib/types';910const TONE: Record<ReturnType<typeof eventTone>, string> = {11 new: 'text-positive',12 price: 'text-accent-2',13 warn: 'text-warning',14 danger: 'text-danger',15 bench: 'text-type-benchmark',16 neutral: 'text-ink-2',17};1819function host(url: string): string {20 try {21 return new URL(url).hostname.replace(/^www\./, '');22 } catch {23 return 'source';24 }25}2627/** Price payloads (`PRICE_CHANGED`, `PROVIDER_LISTED`) are `{ input_per_mtok, output_per_mtok }` objects — never JSON-dump them. */28function fmtSide(v: unknown, key?: string): string {29 if (v && typeof v === 'object' && !Array.isArray(v)) {30 const o = v as Record<string, unknown>;31 if ('input_per_mtok' in o || 'output_per_mtok' in o) return `${fmtUsdPerM(o.input_per_mtok)} in / ${fmtUsdPerM(o.output_per_mtok)} out`;32 const parts = Object.entries(o)33 .filter(([, x]) => x !== null && x !== undefined && typeof x !== 'object')34 .slice(0, 3)35 .map(([k, x]) => `${propertyLabel(k).toLowerCase()} ${fmtValue(x, k)}`);36 return parts.length ? parts.join(' · ') : fmtValue(v, key);37 }38 return fmtValue(v, key);39}4041/** Old → new rendering for property changes and prices. */42export function Delta({ e }: { e: ChangeEvent }) {43 if (e.old_value === undefined || e.old_value === null || e.new_value === undefined || e.new_value === null) return null;44 const key = e.property ?? undefined;45 return (46 <span className="tnum inline-flex min-w-0 flex-wrap items-center gap-1 break-words text-xs">47 <span className="text-ink-3 line-through decoration-ink-3/60">{fmtSide(e.old_value, key)}</span>48 <span className="text-ink-3">→</span>49 <span className="font-medium text-ink">{fmtSide(e.new_value, key)}</span>50 </span>51 );52}5354/**55 * One event in a feed: importance meter · time · entity badge + name · summary · property delta · source.56 * `dense` (homepage) hides the delta and connector; `showDate` prints the date instead of relative time.57 */58export 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 }) {59 const tone = eventTone(e.event_type);60 // 1.1: occurred_at = coalesce(effective_at, observed_at); the title always tells when AI Atlas observed it.61 const when = e.occurred_at ?? e.effective_at ?? e.observed_at;62 const observedTitle = `observed ${fmtDateTime(e.observed_at)}${e.effective_at ? ` · effective ${fmtDate(e.effective_at)}` : ''}`;63 return (64 <li className={cn('row-y grid grid-cols-[auto_minmax(0,1fr)] gap-x-3 border-b border-rule md:grid-cols-[5.5rem_auto_minmax(0,1fr)_auto] md:items-baseline', className)} data-event-id={e.id}>65 <div className="col-span-2 flex items-center gap-2 text-xs text-ink-3 md:col-span-1 md:block">66 {showDate ? <time dateTime={when} title={observedTitle}>{fmtDate(when)}</time> : live ? <LiveAgo at={e.observed_at} /> : <time dateTime={e.observed_at} title={observedTitle}>{fmtDate(e.observed_at)}</time>}67 {e.is_backfill && <span className="ml-1 rounded-[3px] bg-surface-2 px-1 text-[10px] uppercase tracking-wide text-ink-3" title="Historical backfill: imported when the source was first crawled, not observed live">backfill</span>}68 <ImportanceMark importance={e.importance} className="md:hidden" />69 </div>70 <div className="hidden md:flex md:items-center md:self-center">71 <ImportanceMark importance={e.importance} />72 </div>73 <div className="col-span-2 min-w-0 md:col-span-1">74 <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5">75 <span className={cn('text-[11px] font-medium uppercase tracking-wide', TONE[tone])}>{eventLabel(e.event_type)}</span>76 {e.entity && (77 <>78 <EntityBadge type={e.entity.entity_type} small />79 <Link href={routes.entity(e.entity)} className="max-w-full truncate text-[15px] font-medium text-ink hover:text-accent hover:underline">80 {e.entity.name}81 </Link>82 {e.entity.organization && <span className="max-w-full truncate text-xs text-ink-3">{e.entity.organization.name}</span>}83 </>84 )}85 </div>86 <p className={cn('mt-0.5 break-words text-sm text-ink-2 [overflow-wrap:anywhere]', dense && 'line-clamp-2')}>{e.summary}</p>87 {!dense && (88 <div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-0.5">89 {e.property && <span className="text-xs text-ink-3">{propertyLabel(e.property)}</span>}90 <Delta e={e} />91 {e.connector_name && <span className="mono text-[11px] text-ink-3">{e.connector_name}</span>}92 </div>93 )}94 {trailing}95 </div>96 <div className="col-span-2 mt-1 text-xs md:col-span-1 md:mt-0 md:text-right">97 {e.source_url ? (98 <a href={e.source_url} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-ink-3 hover:text-accent">99 {host(e.source_url)} <ExternalLink className="size-3" aria-hidden />100 </a>101 ) : (102 <span className="text-ink-3">—</span>103 )}104 </div>105 </li>106 );107}108109/** Group events by UTC day (YYYY-MM-DD of `occurred_at` when present — 1.1 feeds — else observed_at). */110export function groupByDay(events: ChangeEvent[], field: 'occurred' | 'observed' = 'occurred'): { day: string; items: ChangeEvent[] }[] {111 const map = new Map<string, ChangeEvent[]>();112 for (const e of events) {113 const day = (field === 'occurred' ? e.occurred_at ?? e.effective_at ?? e.observed_at : e.observed_at).slice(0, 10);114 const arr = map.get(day);115 if (arr) arr.push(e);116 else map.set(day, [e]);117 }118 return [...map.entries()].map(([day, items]) => ({ day, items }));119}120