SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
5.5 KB · 117 lines tsx
Raw Blame History
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