HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { ActionButton, AdminFilters, AdminTitle, Mono, Notice } from '@/components/admin/ui';4import { EntityBadge } from '@/components/ui/badges';5import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';6import { Unavailable } from '@/components/ui/unavailable';7import { mergeAction } from '@/lib/admin/actions';8import { adminApi, load, requireAdmin } from '@/lib/admin/admin-api';9import type { DuplicateSide } from '@/lib/admin/types';10import { fmtDate, fmtInt, fmtPct, num } from '@/lib/format';11import { routes } from '@/lib/site';1213export const metadata: Metadata = { title: 'Duplicates', robots: { index: false, follow: false } };14export const dynamic = 'force-dynamic';1516const TYPES = ['model', 'company', 'organization', 'lab', 'paper', 'provider', 'benchmark', 'hardware', 'framework', 'library', 'dataset', 'tool', 'repository', 'researcher'];1718function Side({ s, type }: { s: DuplicateSide; type: string }) {19 return (20 <div className="min-w-0">21 <Link href={routes.entity({ entity_type: type, slug: s.slug })} className="block truncate text-sm font-medium text-ink hover:text-accent" title={s.name}>22 {s.name}23 </Link>24 <p className="truncate text-[11px] text-ink-3">25 <Mono>{s.slug}</Mono>26 {s.organization && <span> · {s.organization}</span>}27 </p>28 <p className="tnum text-[11px] text-ink-3">29 {fmtInt(s.claims)} claims · first seen {fmtDate(s.first_seen_at)}30 </p>31 </div>32 );33}3435export default async function AdminDuplicatesPage({ searchParams }: { searchParams: Promise<Record<string, string | undefined>> }) {36 await requireAdmin();37 const sp = await searchParams;38 const type = sp.type ?? 'model';39 const ret = `/admin/entities/duplicates?type=${encodeURIComponent(type)}`;40 const res = await load(adminApi.duplicates({ type, limit: 200 }));41 return (42 <>43 <AdminTitle title="Duplicate candidates" count={res.ok ? fmtInt(res.data.items.length) : undefined} lede={res.ok ? `pg_trgm similarity ≥ ${fmtPct((num(res.data.threshold) ?? 0) * 100, 0)} on normalized names, within one type. Merging moves aliases, identifiers, claims, relations and events into the target and marks the source merged_into.` : undefined} />44 <Notice notice={sp.notice} level={sp.level} />45 <AdminFilters action="/admin/entities/duplicates" className="mb-4" fields={[{ kind: 'select', name: 'type', label: 'Type', value: type, any: 'model', options: TYPES.map((t) => ({ value: t, label: t })) }]} />46 {!res.ok ? (47 <Unavailable what="Duplicates" reason={res.error} />48 ) : (49 <DataTable compact caption="Duplicate candidates">50 <thead>51 <tr>52 <Th>A</Th>53 <Th>B</Th>54 <Th num>Similarity</Th>55 <Th>Merge</Th>56 </tr>57 </thead>58 <tbody>59 {res.data.items.length === 0 && <EmptyRow cols={4}>No candidate pairs for this type.</EmptyRow>}60 {res.data.items.map((d) => {61 const aClaims = num(d.a.claims) ?? 0;62 const bClaims = num(d.b.claims) ?? 0;63 // Default suggestion: the record with fewer claims (or the newer one) is the source, the richer one the target.64 const preferAB = aClaims < bClaims || (aClaims === bClaims && (d.a.first_seen_at ?? '') > (d.b.first_seen_at ?? ''));65 return (66 <tr key={`${d.a.id}-${d.b.id}`}>67 <Td label="A" primary>68 <div className="flex items-start gap-2">69 <EntityBadge type={d.entity_type} small className="mt-0.5" />70 <Side s={d.a} type={d.entity_type} />71 </div>72 </Td>73 <Td label="B" primary>74 <div className="flex items-start gap-2">75 <EntityBadge type={d.entity_type} small className="mt-0.5" />76 <Side s={d.b} type={d.entity_type} />77 </div>78 </Td>79 <Td num label="Similarity" className="tnum text-xs">{fmtPct((num(d.similarity) ?? 0) * 100, 0)}</Td>80 <Td label="Merge" wide>81 <div className="flex flex-wrap items-center gap-1.5">82 <form action={mergeAction}>83 <input type="hidden" name="source_id" value={d.a.id} />84 <input type="hidden" name="target_id" value={d.b.id} />85 <input type="hidden" name="return" value={ret} />86 <ActionButton tone={preferAB ? 'accent' : 'neutral'} title={`Merge ${d.a.slug} into ${d.b.slug} (A disappears, B keeps everything)`}>87 Merge A → B{preferAB ? ' · suggested' : ''}88 </ActionButton>89 </form>90 <form action={mergeAction}>91 <input type="hidden" name="source_id" value={d.b.id} />92 <input type="hidden" name="target_id" value={d.a.id} />93 <input type="hidden" name="return" value={ret} />94 <ActionButton tone={preferAB ? 'neutral' : 'accent'} title={`Merge ${d.b.slug} into ${d.a.slug} (B disappears, A keeps everything)`}>95 Merge B → A{preferAB ? '' : ' · suggested'}96 </ActionButton>97 </form>98 </div>99 </Td>100 </tr>101 );102 })}103 </tbody>104 </DataTable>105 )}106 </>107 );108}109