'use client'; import { ExternalLink, History, TriangleAlert } from 'lucide-react'; import Link from 'next/link'; import { useEffect, useState } from 'react'; import { ConfidenceBadge, TierBadge } from '@/components/ui/badges'; import { Sheet } from '@/components/ui/sheet'; import { ClientApiError, clientApi } from '@/lib/client-api'; import { cn } from '@/lib/cn'; import { DASH, fmtAgo, fmtDateTime, fmtValue, hostOf } from '@/lib/format'; import { propertyLabel, routes, TIER_LABELS } from '@/lib/site'; import type { Claim, ProvenanceDetail } from '@/lib/types'; import { type EvidenceTarget, useEvidence } from './evidence-context'; type State = { status: 'loading' | 'ready' | 'none'; detail: ProvenanceDetail | null; source: 'api' | 'history' | 'fallback' | null }; /** * Right-side panel (≥ lg) / bottom sheet (< lg) explaining one displayed value: VALUE · SOURCE · TIER · OBSERVED · EXTRACTOR · * CONFIDENCE · VALID SINCE · links (View source · View history · View conflicts · Claim id). * Data: `GET /entities/{slug}/provenance/{property}` (1.1) → else the property's claim history (exists today) → else the * inline `fallback` the trigger passed. Mount once in the root layout, inside `EvidenceProvider`. */ export function EvidenceDrawer() { const { target, close } = useEvidence(); const [state, setState] = useState({ status: 'none', detail: null, source: null }); useEffect(() => { if (!target) return; const ctrl = new AbortController(); setState({ status: 'loading', detail: fromFallback(target), source: target.fallback ? 'fallback' : null }); (async () => { try { const d = await clientApi.provenance(target.slug, target.property, ctrl.signal); setState({ status: 'ready', detail: { ...fromFallback(target), ...d, property: d.property ?? target.property }, source: 'api' }); return; } catch (e) { if ((e as Error).name === 'AbortError') return; if (!(e instanceof ClientApiError) || (e.status !== 404 && e.status !== 405)) { setState((s) => ({ ...s, status: 'ready' })); return; } } try { const h = await clientApi.history(target.slug, target.property, ctrl.signal); setState({ status: 'ready', detail: fromHistory(target, h.items ?? []), source: 'history' }); } catch (e) { if ((e as Error).name === 'AbortError') return; setState((s) => ({ ...s, status: 'ready' })); } })(); return () => ctrl.abort(); }, [target]); const d = state.detail; const open = target !== null; const entityRef = target ? { entity_type: target.entity?.entity_type ?? 'model', slug: target.slug } : null; const conflicts = d?.conflicts ?? []; const sourceLabel = d?.source_name ?? d?.domain ?? hostOf(d?.url) ?? null; return ( {target && (

Value

{target.display ?? fmtValue(d?.value ?? target.value, target.property)} {(d?.unit ?? target.unit) && !/tokens|GB|W|\$|%/.test(target.display ?? '') && {d?.unit ?? target.unit}}

{state.status === 'loading' && !d &&

Loading evidence…

} {state.status === 'ready' && !d && (

No provenance recorded for this value. Every fact on AI Atlas should carry one — this is a data gap, not a hidden source.

)} {d && (
{d.url ? ( {sourceLabel ?? d.url} ) : ( {sourceLabel ?? DASH} )} {d.domain && sourceLabel !== d.domain && {d.domain}} {d.tier ? {tierHint(d.tier)} : null} {d.observed_at ? `${fmtAgo(d.observed_at)} · ${fmtDateTime(d.observed_at)}` : DASH} {d.extractor ?? DASH} {d.extractor?.startsWith('llm') && local LLM factory · claims one tier lower} {!d.confidence && DASH} {d.valid_from ? fmtDateTime(d.valid_from) : DASH} {d.valid_to && until {fmtDateTime(d.valid_to)}} {d.status && d.status !== 'current' && ( {d.status} )}
)} {conflicts.length > 0 && (

{conflicts.length} conflicting {conflicts.length === 1 ? 'claim' : 'claims'} — stored side by side, never averaged

    {conflicts.slice(0, 5).map((c) => (
  • {fmtValue(c.value, c.property)} · ·{' '} {c.source_url ? ( {c.source_name ?? hostOf(c.source_url)} ) : ( c.source_name ?? 'source' )}{' '} · {fmtAgo(c.observed_at)}
  • ))}
)}
{d?.url && ( View source )} {entityRef && ( View history )} {conflicts.length > 0 && entityRef && ( View conflicts )} Methodology
{d?.claim_id && (

claim {d.claim_id} {d.snapshot_id && <> · snapshot {d.snapshot_id}}

)} {state.source && state.source !== 'api' &&

{state.source === 'history' ? 'From the claim history endpoint (the field-level provenance endpoint is not live yet).' : 'From the inline provenance shipped with the page.'}

}
)}
); } function Row({ k, children }: { k: string; children: React.ReactNode }) { return (
{k}
{children}
); } function tierHint(t: number): string { const n = Math.min(4, Math.max(1, Math.round(t))); return TIER_LABELS[n] ? `— ${TIER_LABELS[n]?.toLowerCase()} source` : ''; } function fromFallback(t: EvidenceTarget): ProvenanceDetail | null { const f = t.fallback; if (!f) return null; return { slug: t.slug, property: t.property, value: t.value, unit: f.unit ?? t.unit ?? null, source_id: f.source_id, source_name: f.source_name ?? null, domain: hostOf(f.url), url: f.url, tier: f.tier, confidence: f.confidence, extractor: f.extractor, observed_at: f.observed_at, valid_from: null, conflicts: [], }; } function fromHistory(t: EvidenceTarget, items: Claim[]): ProvenanceDetail | null { const current = items.find((c) => c.status === 'current') ?? items[0]; const base = fromFallback(t); if (!current) return base; return { ...(base ?? { slug: t.slug, property: t.property, value: t.value, tier: null }), value: current.value ?? t.value, unit: current.unit ?? base?.unit ?? null, claim_id: current.id, source_name: current.source_name ?? base?.source_name ?? null, domain: hostOf(current.source_url) ?? base?.domain ?? null, url: current.source_url ?? base?.url ?? null, tier: current.tier, confidence: current.confidence, extractor: current.extractor, observed_at: current.observed_at, valid_from: current.valid_from, valid_to: current.valid_to, status: current.status, conflicts: items.filter((c) => c.status === 'conflicting'), history_count: items.length, }; }