import Link from 'next/link'; import { fmtInt, fmtParams, num } from '@/lib/format'; import { routes } from '@/lib/site'; import type { EntitySummary } from '@/lib/types'; /* Compact server-rendered SVG lineage tree: ancestors → this model → descendants, artifacts collapsed by kind with counts. Not the force graph (components/graph is not ours); "Open in Graph" links to the interactive explorer in lineage mode. */ type Node = { id: string; label: string; sub?: string; href?: string; kind: 'model' | 'this' | 'artifact' }; const W = 720; const ROW = 34; const BOX_W = 200; const BOX_H = 28; const PAD = 8; function trunc(s: string, n: number): string { return s.length > n ? `${s.slice(0, n - 1)}…` : s; } function toNode(e: EntitySummary): Node { const p = num(e.attributes?.parameter_count); return { id: e.id, label: e.name, sub: p !== null ? fmtParams(p) : e.organization?.name ?? undefined, href: routes.entity(e), kind: 'model' }; } export function LineageTree({ self, ancestors, descendants, artifactKinds, className }: { self: EntitySummary; ancestors: EntitySummary[]; descendants: EntitySummary[]; artifactKinds: { kind: string; count: number }[]; className?: string }) { const left = ancestors.slice(0, 6).map(toNode); const rightModels = descendants.slice(0, 6).map(toNode); const rightArtifacts: Node[] = artifactKinds.filter((k) => k.count > 0).map((k) => ({ id: `kind-${k.kind}`, label: `${fmtInt(k.count)} ${k.kind}${k.count === 1 ? '' : 's'}`, sub: 'artifacts · collapsed', href: `${routes.entity(self)}#versions-artifacts`, kind: 'artifact' })); const right = [...rightModels, ...rightArtifacts]; const moreL = ancestors.length - left.length; const moreR = descendants.length - rightModels.length; const rows = Math.max(1, left.length + (moreL > 0 ? 1 : 0), right.length + (moreR > 0 ? 1 : 0)); const H = PAD * 2 + rows * ROW; const colX = [PAD, W / 2 - BOX_W / 2, W - PAD - BOX_W]; const yOf = (i: number, n: number) => PAD + ((H - PAD * 2) / Math.max(1, n)) * (i + 0.5) - BOX_H / 2; const selfY = H / 2 - BOX_H / 2; const p = num(self.attributes?.parameter_count); const Box = ({ n, x, y }: { n: Node; x: number; y: number }) => { const fill = n.kind === 'this' ? 'var(--accent-soft)' : n.kind === 'artifact' ? 'var(--surface-2)' : 'var(--surface)'; const stroke = n.kind === 'this' ? 'var(--accent)' : 'var(--rule-strong)'; const body = ( {trunc(n.label, 30)} {n.sub && ( {trunc(n.sub, 34)} )} {`${n.label}${n.sub ? ` — ${n.sub}` : ''}`} ); return n.href && n.kind !== 'this' ? ( {body} ) : ( body ); }; const edge = (x1: number, y1: number, x2: number, y2: number, dashed = false) => ; return (
n + k.count, 0)} artifacts`}> {left.length ? `ANCESTORS ${ancestors.length}` : ''} {right.length ? `DESCENDANTS ${descendants.length}${rightArtifacts.length ? ' · ARTIFACTS' : ''}` : ''} {left.map((n, i) => ( {edge((colX[0] as number) + BOX_W, yOf(i, rows) + BOX_H / 2, colX[1] as number, selfY + BOX_H / 2)} ))} {moreL > 0 && ( +{moreL} more ancestors )} {right.map((n, i) => ( {edge((colX[1] as number) + BOX_W, selfY + BOX_H / 2, colX[2] as number, yOf(i, rows) + BOX_H / 2, n.kind === 'artifact')} ))} {moreR > 0 && ( +{moreR} more descendants )}
); }