import Link from 'next/link'; import { resolutionAction } from '@/lib/admin/actions'; import type { ResolutionDecision, ResolutionItem, ResolutionSide } from '@/lib/admin/types'; import { cn } from '@/lib/cn'; import { fmtDate, fmtInt, fmtParams, fmtPct, fmtTokens, num } from '@/lib/format'; import { routes } from '@/lib/site'; import { EntityBadge } from '@/components/ui/badges'; import { ActionButton, KindChip, Mono } from './ui'; /* One candidate pair, side by side (server component): every comparable field on one row, equal values dimmed, differing values highlighted; signals; six decisions as server-action forms. Direction matters for merge / variant_of / family_member: "A → B" means A is merged into (or becomes a variant / member of) B. */ type Row = { label: string; a: React.ReactNode; b: React.ReactNode; same: boolean }; function val(v: unknown): string { if (v === null || v === undefined || v === '') return '—'; if (Array.isArray(v)) return v.length ? v.map(String).join(', ') : '—'; return String(v); } function rowsFor(a: ResolutionSide, b: ResolutionSide): Row[] { const r = (label: string, fa: unknown, fb: unknown, fmt: (v: unknown) => React.ReactNode = val) => ({ label, a: fmt(fa), b: fmt(fb), same: JSON.stringify(fa ?? null) === JSON.stringify(fb ?? null) }); const ids = (s: ResolutionSide) => s.identifiers?.map((i) => `${i.scheme}:${i.value}`) ?? []; const na = (s: ResolutionSide) => (s.name_analysis ?? {}) as Record; return [ r('Name', a.name, b.name), r('Slug', a.slug, b.slug, (v) => {val(v)}), r('Organization', a.organization, b.organization), r('Family', a.family, b.family), r('Parameters', a.parameter_count, b.parameter_count, (v) => fmtParams(v)), r('Active params', a.active_parameter_count, b.active_parameter_count, (v) => fmtParams(v)), r('Release date', a.release_date, b.release_date, (v) => (typeof v === 'string' ? fmtDate(v) : '—')), r('Architecture', a.architecture ?? a.model_type, b.architecture ?? b.model_type), r('Openness', a.openness, b.openness), r('Context', a.context_length, b.context_length, (v) => (num(v) === null ? '—' : fmtTokens(v))), r('HF repo', a.hf_repo, b.hf_repo, (v) => {val(v)}), r('Identifiers', ids(a), ids(b), (v) => (Array.isArray(v) && v.length ? {v.map((x) => {String(x)})} : '—')), r('Aliases', a.aliases, b.aliases, (v) => (Array.isArray(v) && v.length ? {v.map(String).join(' · ')} : '—')), r('Variant key', a.variant_key, b.variant_key, (v) => {val(v)}), r('Base key (name analysis)', na(a).base_key, na(b).base_key, (v) => {val(v)}), r('Is artifact (name analysis)', na(a).is_artifact, na(b).is_artifact, (v) => (v === true ? 'yes' : v === false ? 'no' : '—')), r('Identity confidence', a.identity_confidence, b.identity_confidence), r('Relations · sources · claims', [a.relations, a.sources, a.claims], [b.relations, b.sources, b.claims], (v) => (Array.isArray(v) ? {v.map((x) => fmtInt(x)).join(' · ')} : '—')), r('Prices · results', [a.prices, a.results], [b.prices, b.results], (v) => (Array.isArray(v) ? {v.map((x) => fmtInt(x)).join(' · ')} : '—')), r('First seen', a.first_seen_at, b.first_seen_at, (v) => (typeof v === 'string' ? fmtDate(v) : '—')), ]; } const DECISION_LABEL: Record = { merge: { label: 'Merge', tone: 'danger', title: 'Merge A into B: aliases, identifiers, claims, relations and events move to B; A keeps resolving to B.' }, alias: { label: 'Alias', tone: 'accent', title: 'Record A’s name as an alias of B without merging the rows.' }, variant_of: { label: 'Variant of', tone: 'accent', title: 'A is an evaluation / effort variant of B (canonical_id = B), folded into B in the model universe.' }, family_member: { label: 'Family member', tone: 'accent', title: 'A belongs to family B (B must be a model_family).' }, keep_separate: { label: 'Keep separate', tone: 'positive', title: 'Two different entities: the pair stops being proposed.' }, defer: { label: 'Defer', tone: 'neutral', title: 'Not now — keep the pair pending.' }, }; const DIRECTIONAL = new Set(['merge', 'alias', 'variant_of', 'family_member']); function Side({ s, letter }: { s: ResolutionSide; letter: 'A' | 'B' }) { return (

{letter}

{s.name}

{s.id}
); } export function ResolutionPair({ item, decisions, returnTo }: { item: ResolutionItem; decisions: ResolutionDecision[]; returnTo: string }) { const rows = rowsFor(item.a, item.b); const decided = item.decision ? (typeof item.decision === 'string' ? item.decision : item.decision.decision) : null; const sim = num(item.similarity); return (
{item.signals.map((s, i) => ( ))} {sim !== null && similarity {fmtPct(sim * 100, 0)}} {item.same_organization && same organization} {item.same_variant_key && same variant key} {item.hint && · {item.hint}}
{decided && ( decided: {decided} )}
{rows.map((r) => ( ))}
Side by side
Field A B
{r.label} {r.a} {r.b}
{!decided && ( )}
); }