HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import Link from 'next/link';2import { resolutionAction } from '@/lib/admin/actions';3import type { ResolutionDecision, ResolutionItem, ResolutionSide } from '@/lib/admin/types';4import { cn } from '@/lib/cn';5import { fmtDate, fmtInt, fmtParams, fmtPct, fmtTokens, num } from '@/lib/format';6import { routes } from '@/lib/site';7import { EntityBadge } from '@/components/ui/badges';8import { ActionButton, KindChip, Mono } from './ui';910/*11 One candidate pair, side by side (server component): every comparable field on one row, equal values dimmed, differing12 values highlighted; signals; six decisions as server-action forms. Direction matters for merge / variant_of /13 family_member: "A → B" means A is merged into (or becomes a variant / member of) B.14*/1516type Row = { label: string; a: React.ReactNode; b: React.ReactNode; same: boolean };1718function val(v: unknown): string {19 if (v === null || v === undefined || v === '') return '—';20 if (Array.isArray(v)) return v.length ? v.map(String).join(', ') : '—';21 return String(v);22}23function rowsFor(a: ResolutionSide, b: ResolutionSide): Row[] {24 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) });25 const ids = (s: ResolutionSide) => s.identifiers?.map((i) => `${i.scheme}:${i.value}`) ?? [];26 const na = (s: ResolutionSide) => (s.name_analysis ?? {}) as Record<string, unknown>;27 return [28 r('Name', a.name, b.name),29 r('Slug', a.slug, b.slug, (v) => <Mono>{val(v)}</Mono>),30 r('Organization', a.organization, b.organization),31 r('Family', a.family, b.family),32 r('Parameters', a.parameter_count, b.parameter_count, (v) => fmtParams(v)),33 r('Active params', a.active_parameter_count, b.active_parameter_count, (v) => fmtParams(v)),34 r('Release date', a.release_date, b.release_date, (v) => (typeof v === 'string' ? fmtDate(v) : '—')),35 r('Architecture', a.architecture ?? a.model_type, b.architecture ?? b.model_type),36 r('Openness', a.openness, b.openness),37 r('Context', a.context_length, b.context_length, (v) => (num(v) === null ? '—' : fmtTokens(v))),38 r('HF repo', a.hf_repo, b.hf_repo, (v) => <Mono>{val(v)}</Mono>),39 r('Identifiers', ids(a), ids(b), (v) => (Array.isArray(v) && v.length ? <span className="flex flex-col">{v.map((x) => <Mono key={String(x)}>{String(x)}</Mono>)}</span> : '—')),40 r('Aliases', a.aliases, b.aliases, (v) => (Array.isArray(v) && v.length ? <span className="text-xs">{v.map(String).join(' · ')}</span> : '—')),41 r('Variant key', a.variant_key, b.variant_key, (v) => <Mono>{val(v)}</Mono>),42 r('Base key (name analysis)', na(a).base_key, na(b).base_key, (v) => <Mono>{val(v)}</Mono>),43 r('Is artifact (name analysis)', na(a).is_artifact, na(b).is_artifact, (v) => (v === true ? 'yes' : v === false ? 'no' : '—')),44 r('Identity confidence', a.identity_confidence, b.identity_confidence),45 r('Relations · sources · claims', [a.relations, a.sources, a.claims], [b.relations, b.sources, b.claims], (v) => (Array.isArray(v) ? <span className="tnum">{v.map((x) => fmtInt(x)).join(' · ')}</span> : '—')),46 r('Prices · results', [a.prices, a.results], [b.prices, b.results], (v) => (Array.isArray(v) ? <span className="tnum">{v.map((x) => fmtInt(x)).join(' · ')}</span> : '—')),47 r('First seen', a.first_seen_at, b.first_seen_at, (v) => (typeof v === 'string' ? fmtDate(v) : '—')),48 ];49}5051const DECISION_LABEL: Record<ResolutionDecision, { label: string; tone: 'neutral' | 'accent' | 'danger' | 'positive'; title: string }> = {52 merge: { label: 'Merge', tone: 'danger', title: 'Merge A into B: aliases, identifiers, claims, relations and events move to B; A keeps resolving to B.' },53 alias: { label: 'Alias', tone: 'accent', title: 'Record A’s name as an alias of B without merging the rows.' },54 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.' },55 family_member: { label: 'Family member', tone: 'accent', title: 'A belongs to family B (B must be a model_family).' },56 keep_separate: { label: 'Keep separate', tone: 'positive', title: 'Two different entities: the pair stops being proposed.' },57 defer: { label: 'Defer', tone: 'neutral', title: 'Not now — keep the pair pending.' },58};59const DIRECTIONAL = new Set<ResolutionDecision>(['merge', 'alias', 'variant_of', 'family_member']);6061function Side({ s, letter }: { s: ResolutionSide; letter: 'A' | 'B' }) {62 return (63 <div className="min-w-0">64 <p className="eyebrow">{letter}</p>65 <p className="flex flex-wrap items-center gap-1.5">66 <EntityBadge type={s.entity_type} small />67 <Link href={routes.entity(s)} className="truncate text-sm font-medium text-ink hover:text-accent hover:underline">68 {s.name}69 </Link>70 </p>71 <Mono title={s.id}>{s.id}</Mono>72 </div>73 );74}7576export function ResolutionPair({ item, decisions, returnTo }: { item: ResolutionItem; decisions: ResolutionDecision[]; returnTo: string }) {77 const rows = rowsFor(item.a, item.b);78 const decided = item.decision ? (typeof item.decision === 'string' ? item.decision : item.decision.decision) : null;79 const sim = num(item.similarity);80 return (81 <article className="border border-rule" data-resolution-pair>82 <header className="flex flex-col gap-2 border-b border-rule bg-surface-2/50 px-3 py-2 md:flex-row md:items-center md:justify-between">83 <div className="flex flex-wrap items-center gap-2 text-xs">84 {item.signals.map((s, i) => (85 <KindChip key={i} value={s.kind} />86 ))}87 {sim !== null && <span className="tnum text-ink-2">similarity {fmtPct(sim * 100, 0)}</span>}88 {item.same_organization && <span className="text-positive">same organization</span>}89 {item.same_variant_key && <span className="text-accent">same variant key</span>}90 {item.hint && <span className="text-ink-3">· {item.hint}</span>}91 </div>92 {decided && (93 <span className="text-xs text-ink-2">94 decided: <Mono>{decided}</Mono>95 </span>96 )}97 </header>98 <div className="grid gap-4 p-3 md:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">99 <Side s={item.a} letter="A" />100 <Side s={item.b} letter="B" />101 </div>102 <div className="overflow-x-auto">103 <table className="data-table compact w-full table-fixed" style={{ minWidth: '36rem' }}>104 <caption className="sr-only">Side by side</caption>105 <colgroup>106 <col style={{ width: '11rem' }} />107 <col />108 <col />109 </colgroup>110 <thead>111 <tr>112 <th scope="col">Field</th>113 <th scope="col">A</th>114 <th scope="col">B</th>115 </tr>116 </thead>117 <tbody>118 {rows.map((r) => (119 <tr key={r.label} className={cn(r.same && 'text-ink-3')}>120 <td className="text-xs text-ink-2">{r.label}</td>121 <td className={cn('break-words text-xs [overflow-wrap:anywhere]', !r.same && 'bg-warning-soft/40 text-ink')} style={{ whiteSpace: 'normal' }}>{r.a}</td>122 <td className={cn('break-words text-xs [overflow-wrap:anywhere]', !r.same && 'bg-warning-soft/40 text-ink')} style={{ whiteSpace: 'normal' }}>{r.b}</td>123 </tr>124 ))}125 </tbody>126 </table>127 </div>128 {!decided && (129 <footer className="border-t border-rule px-3 py-2">130 <p className="mb-2 text-[11px] text-ink-3">Directional decisions read A → B (A merged into / variant of / member of B). Use the second row to apply them the other way round.</p>131 <div className="flex flex-wrap items-center gap-1.5">132 {decisions.map((d) => (133 <form key={d} action={resolutionAction} className="inline">134 <input type="hidden" name="a" value={item.a.id} />135 <input type="hidden" name="b" value={item.b.id} />136 <input type="hidden" name="decision" value={d} />137 <input type="hidden" name="return" value={returnTo} />138 <ActionButton tone={DECISION_LABEL[d]?.tone ?? 'neutral'} title={DECISION_LABEL[d]?.title}>139 {DECISION_LABEL[d]?.label ?? d}140 {DIRECTIONAL.has(d) ? ' A → B' : ''}141 </ActionButton>142 </form>143 ))}144 </div>145 <div className="mt-1.5 flex flex-wrap items-center gap-1.5">146 {decisions.filter((d) => DIRECTIONAL.has(d)).map((d) => (147 <form key={`${d}-rev`} action={resolutionAction} className="inline">148 <input type="hidden" name="a" value={item.b.id} />149 <input type="hidden" name="b" value={item.a.id} />150 <input type="hidden" name="decision" value={d} />151 <input type="hidden" name="return" value={returnTo} />152 <ActionButton tone="neutral" title={`${DECISION_LABEL[d]?.title} (reversed)`}>153 {DECISION_LABEL[d]?.label ?? d} B → A154 </ActionButton>155 </form>156 ))}157 </div>158 </footer>159 )}160 </article>161 );162}163