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%
4.8 KB · 103 lines tsx
Raw Blame History
1import Link from 'next/link';2import { predicateLabel } from '@/lib/site';34/*5  Compact layered SVG graph for lineage edges among a known set of nodes (family members): roots left, derived models to the right.6  Server-rendered, links on nodes, accessible edge list. For the interactive explorer use /graph.7*/89export type MiniNode = { id: string; label: string; href?: string; sub?: string };10export type MiniEdge = { source: string; target: string; predicate: string };1112const W = 720;13const BOX_W = 168;14const BOX_H = 26;15const ROW = 32;16const PAD = 8;17const trunc = (s: string, n: number) => (s.length > n ? `${s.slice(0, n - 1)}…` : s);1819export function MiniGraph({ nodes, edges, className, title = 'Lineage' }: { nodes: MiniNode[]; edges: MiniEdge[]; className?: string; title?: string }) {20  const ids = new Set(nodes.map((n) => n.id));21  const E = edges.filter((e) => ids.has(e.source) && ids.has(e.target));22  if (!E.length) return <p className="text-sm text-ink-3">No lineage relation recorded among these members.</p>;23  const involved = new Set(E.flatMap((e) => [e.source, e.target]));24  const N = nodes.filter((n) => involved.has(n.id));25  // depth = longest chain from a root; edges point source (derived) → target (base): the base is upstream, so depth(source) = depth(target) + 126  const depth = new Map<string, number>();27  const upstream = new Map<string, string[]>();28  for (const e of E) upstream.set(e.source, [...(upstream.get(e.source) ?? []), e.target]);29  const visit = (id: string, seen: Set<string>): number => {30    if (depth.has(id)) return depth.get(id) as number;31    if (seen.has(id)) return 0;32    seen.add(id);33    const ups = upstream.get(id) ?? [];34    const d = ups.length ? 1 + Math.max(...ups.map((u) => visit(u, seen))) : 0;35    depth.set(id, d);36    return d;37  };38  for (const n of N) visit(n.id, new Set());39  const maxD = Math.max(...N.map((n) => depth.get(n.id) ?? 0));40  const cols = Array.from({ length: maxD + 1 }, () => [] as MiniNode[]);41  for (const n of N) cols[depth.get(n.id) ?? 0]!.push(n);42  const rows = Math.max(...cols.map((c) => c.length));43  const H = PAD * 2 + rows * ROW;44  const colX = (d: number) => (maxD === 0 ? W / 2 - BOX_W / 2 : PAD + (d * (W - PAD * 2 - BOX_W)) / maxD);45  const pos = new Map<string, { x: number; y: number }>();46  cols.forEach((c, d) => c.forEach((n, i) => pos.set(n.id, { x: colX(d), y: PAD + ((H - PAD * 2) / Math.max(1, c.length)) * (i + 0.5) - BOX_H / 2 })));47  const tooMany = N.length > 40;48  return (49    <div className={className} data-mini-graph>50      <svg viewBox={`0 0 ${W} ${Math.min(H, 640)}`} className="block w-full" role="img" aria-label={`${title}: ${N.length} models, ${E.length} relations`}>51        <title>{title}</title>52        {E.map((e, i) => {53          const a = pos.get(e.target);54          const b = pos.get(e.source);55          if (!a || !b) return null;56          const x1 = a.x + BOX_W;57          const y1 = a.y + BOX_H / 2;58          const x2 = b.x;59          const y2 = b.y + BOX_H / 2;60          return (61            <g key={i}>62              <path d={`M${x1},${y1} C${(x1 + x2) / 2},${y1} ${(x1 + x2) / 2},${y2} ${x2},${y2}`} fill="none" stroke="var(--rule-strong)" strokeWidth={1.1} strokeDasharray={e.predicate === 'quantized_from' ? '3 2' : undefined} />63              <title>{`${nodes.find((n) => n.id === e.source)?.label} ${predicateLabel(e.predicate, 'out').toLowerCase()} ${nodes.find((n) => n.id === e.target)?.label}`}</title>64            </g>65          );66        })}67        {N.map((n) => {68          const p = pos.get(n.id)!;69          const body = (70            <g>71              <rect x={p.x} y={p.y} width={BOX_W} height={BOX_H} rx={3} fill="var(--surface)" stroke="var(--rule-strong)" />72              <text x={p.x + 7} y={p.y + 11} fontSize={10.5} fontWeight={500} fill="var(--ink)">73                {trunc(n.label, 26)}74              </text>75              {n.sub && (76                <text x={p.x + 7} y={p.y + 21} fontSize={8.5} fill="var(--ink-3)">77                  {trunc(n.sub, 30)}78                </text>79              )}80              <title>{n.label}</title>81            </g>82          );83          return n.href ? (84            <a key={n.id} href={n.href} className="hover:opacity-80">85              {body}86            </a>87          ) : (88            <g key={n.id}>{body}</g>89          );90        })}91      </svg>92      {tooMany && <p className="text-[11px] text-ink-3">Large family — open the interactive graph for the full picture.</p>}93      <ul className="sr-only">94        {E.map((e, i) => (95          <li key={i}>96            <Link href={nodes.find((n) => n.id === e.source)?.href ?? '#'}>{nodes.find((n) => n.id === e.source)?.label}</Link> {predicateLabel(e.predicate, 'out').toLowerCase()} <Link href={nodes.find((n) => n.id === e.target)?.href ?? '#'}>{nodes.find((n) => n.id === e.target)?.label}</Link>97          </li>98        ))}99      </ul>100    </div>101  );102}103