HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import Link from 'next/link';2import { fmtInt, fmtParams, num } from '@/lib/format';3import { routes } from '@/lib/site';4import type { EntitySummary } from '@/lib/types';56/*7 Compact server-rendered SVG lineage tree: ancestors → this model → descendants, artifacts collapsed by kind with counts.8 Not the force graph (components/graph is not ours); "Open in Graph" links to the interactive explorer in lineage mode.9*/1011type Node = { id: string; label: string; sub?: string; href?: string; kind: 'model' | 'this' | 'artifact' };1213const W = 720;14const ROW = 34;15const BOX_W = 200;16const BOX_H = 28;17const PAD = 8;1819function trunc(s: string, n: number): string {20 return s.length > n ? `${s.slice(0, n - 1)}…` : s;21}2223function toNode(e: EntitySummary): Node {24 const p = num(e.attributes?.parameter_count);25 return { id: e.id, label: e.name, sub: p !== null ? fmtParams(p) : e.organization?.name ?? undefined, href: routes.entity(e), kind: 'model' };26}2728export function LineageTree({ self, ancestors, descendants, artifactKinds, className }: { self: EntitySummary; ancestors: EntitySummary[]; descendants: EntitySummary[]; artifactKinds: { kind: string; count: number }[]; className?: string }) {29 const left = ancestors.slice(0, 6).map(toNode);30 const rightModels = descendants.slice(0, 6).map(toNode);31 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' }));32 const right = [...rightModels, ...rightArtifacts];33 const moreL = ancestors.length - left.length;34 const moreR = descendants.length - rightModels.length;35 const rows = Math.max(1, left.length + (moreL > 0 ? 1 : 0), right.length + (moreR > 0 ? 1 : 0));36 const H = PAD * 2 + rows * ROW;37 const colX = [PAD, W / 2 - BOX_W / 2, W - PAD - BOX_W];38 const yOf = (i: number, n: number) => PAD + ((H - PAD * 2) / Math.max(1, n)) * (i + 0.5) - BOX_H / 2;39 const selfY = H / 2 - BOX_H / 2;40 const p = num(self.attributes?.parameter_count);4142 const Box = ({ n, x, y }: { n: Node; x: number; y: number }) => {43 const fill = n.kind === 'this' ? 'var(--accent-soft)' : n.kind === 'artifact' ? 'var(--surface-2)' : 'var(--surface)';44 const stroke = n.kind === 'this' ? 'var(--accent)' : 'var(--rule-strong)';45 const body = (46 <g>47 <rect x={x} y={y} width={BOX_W} height={BOX_H} rx={3} fill={fill} stroke={stroke} strokeDasharray={n.kind === 'artifact' ? '3 2' : undefined} />48 <text x={x + 8} y={y + 12} fontSize={11} fontWeight={n.kind === 'this' ? 600 : 500} fill="var(--ink)">49 {trunc(n.label, 30)}50 </text>51 {n.sub && (52 <text x={x + 8} y={y + 23} fontSize={9.5} fill="var(--ink-3)">53 {trunc(n.sub, 34)}54 </text>55 )}56 <title>{`${n.label}${n.sub ? ` — ${n.sub}` : ''}`}</title>57 </g>58 );59 return n.href && n.kind !== 'this' ? (60 <a href={n.href} className="hover:opacity-80">61 {body}62 </a>63 ) : (64 body65 );66 };67 const edge = (x1: number, y1: number, x2: number, y2: number, dashed = false) => <path d={`M${x1},${y1} C${(x1 + x2) / 2},${y1} ${(x1 + x2) / 2},${y2} ${x2},${y2}`} fill="none" stroke="var(--rule-strong)" strokeWidth={1.2} strokeDasharray={dashed ? '3 3' : undefined} />;6869 return (70 <div className={className} data-lineage-tree>71 <svg viewBox={`0 0 ${W} ${H}`} className="block w-full" role="img" aria-label={`Lineage of ${self.name}: ${ancestors.length} ancestors, ${descendants.length} descendants, ${artifactKinds.reduce((n, k) => n + k.count, 0)} artifacts`}>72 <text x={colX[0]} y={PAD - 1} fontSize={9} fill="var(--ink-3)" className="eyebrow">73 {left.length ? `ANCESTORS ${ancestors.length}` : ''}74 </text>75 <text x={W - PAD} y={PAD - 1} fontSize={9} fill="var(--ink-3)" textAnchor="end">76 {right.length ? `DESCENDANTS ${descendants.length}${rightArtifacts.length ? ' · ARTIFACTS' : ''}` : ''}77 </text>78 {left.map((n, i) => (79 <g key={n.id}>80 {edge((colX[0] as number) + BOX_W, yOf(i, rows) + BOX_H / 2, colX[1] as number, selfY + BOX_H / 2)}81 <Box n={n} x={colX[0] as number} y={yOf(i, rows)} />82 </g>83 ))}84 {moreL > 0 && (85 <text x={(colX[0] as number) + 8} y={yOf(left.length, rows) + 16} fontSize={10} fill="var(--ink-3)">86 +{moreL} more ancestors87 </text>88 )}89 {right.map((n, i) => (90 <g key={n.id}>91 {edge((colX[1] as number) + BOX_W, selfY + BOX_H / 2, colX[2] as number, yOf(i, rows) + BOX_H / 2, n.kind === 'artifact')}92 <Box n={n} x={colX[2] as number} y={yOf(i, rows)} />93 </g>94 ))}95 {moreR > 0 && (96 <text x={(colX[2] as number) + 8} y={yOf(right.length, rows) + 16} fontSize={10} fill="var(--ink-3)">97 +{moreR} more descendants98 </text>99 )}100 <Box n={{ id: self.id, label: self.name, sub: p !== null ? `${fmtParams(p)} params · this model` : 'this model', kind: 'this' }} x={colX[1] as number} y={selfY} />101 </svg>102 <ul className="sr-only">103 {ancestors.map((e) => (104 <li key={e.id}>105 ancestor: <Link href={routes.entity(e)}>{e.name}</Link>106 </li>107 ))}108 {descendants.map((e) => (109 <li key={e.id}>110 descendant: <Link href={routes.entity(e)}>{e.name}</Link>111 </li>112 ))}113 </ul>114 </div>115 );116}117