| |
|
|
| import { IRModel, EDGE_KINDS, patternColor } from "./ir.js"; |
| import { Layout } from "./layout.js"; |
| import { Graph, styleForKind } from "./graph.js"; |
| import { compareIR, mergeIR } from "./compare.js"; |
|
|
| const state = { |
| ir: null, |
| rawIR: null, |
| specLevel: 3, |
| showTP: true, |
| showKernels: true, |
| expanded: new Set(), |
| fields: {}, |
| showInfo: true, |
| edgeKinds: new Set(EDGE_KINDS), |
| showProvenance: true, |
| selectedId: null, |
| manifest: [], |
| irCache: new Map(), |
| compare: false, |
| compareMerged: true, |
| }; |
|
|
| let graph; |
|
|
| |
| function h(tag, attrs = {}, ...children) { |
| const node = document.createElement(tag); |
| for (const [k, v] of Object.entries(attrs)) { |
| if (v == null) continue; |
| if (k === "class") node.className = v; |
| else if (k === "html") node.innerHTML = v; |
| else if (k.startsWith("on") && typeof v === "function") node.addEventListener(k.slice(2), v); |
| else node.setAttribute(k, v); |
| } |
| for (const c of children.flat()) { |
| if (c == null) continue; |
| node.appendChild(typeof c === "string" ? document.createTextNode(c) : c); |
| } |
| return node; |
| } |
|
|
| const $ = (sel) => document.querySelector(sel); |
|
|
| |
|
|
| |
| const IR_BASE = "ir/"; |
|
|
| |
| |
| function fetchIR(url) { |
| return fetch(url, { cache: "no-store" }); |
| } |
|
|
| |
| |
| function applySpec(raw, level) { |
| const r = { ...raw }; |
| if (level < 2) delete r.capabilities; |
| if (level < 3) { |
| delete r.extends; |
| delete r.patches; |
| } |
| return r; |
| } |
|
|
| |
| const avatarCache = new Map(); |
|
|
| |
| function kernelOrgs(ir) { |
| const set = new Set(); |
| const map = ir && ir.capabilities && ir.capabilities.kernels; |
| if (map) for (const repos of Object.values(map)) (repos || []).forEach((r) => set.add(String(r).split("/")[0])); |
| return [...set]; |
| } |
|
|
| |
| async function ensureAvatars(orgs, onReady) { |
| const missing = orgs.filter((o) => o && !avatarCache.has(o)); |
| if (!missing.length) return; |
| await Promise.all( |
| missing.map(async (org) => { |
| for (const kind of ["organizations", "users"]) { |
| try { |
| const res = await fetch(`https://huggingface.co/api/${kind}/${org}/avatar`); |
| if (res.ok) { |
| const j = await res.json(); |
| if (j && j.avatarUrl) return void avatarCache.set(org, j.avatarUrl); |
| } |
| } catch (_) {} |
| } |
| avatarCache.set(org, null); |
| }) |
| ); |
| onReady && onReady(); |
| } |
|
|
| async function loadManifest() { |
| const sel = $("#arch-select"); |
| sel.replaceChildren(); |
| try { |
| const res = await fetchIR(IR_BASE + "manifest.json"); |
| if (!res.ok) throw new Error(res.status); |
| const manifest = await res.json(); |
| state.manifest = manifest.architectures || []; |
| state.manifest.forEach((a) => { |
| const label = `${a.model_type} — ${a.model_class || ""}`.trim(); |
| sel.appendChild(h("option", { value: a.artifact }, label)); |
| }); |
| populateCompareSelectors(); |
| $("#manifest-note").textContent = `${state.manifest.length} architectures · schema ${manifest.artifact_schema_version || "?"}`; |
| if (sel.options.length) { |
| await loadArtifact(sel.value); |
| } |
| } catch (err) { |
| $("#manifest-note").textContent = "No manifest found — upload or paste an IR artifact to begin."; |
| } |
| } |
|
|
| |
| |
| async function loadArtifact(path) { |
| const url = IR_BASE + path; |
| try { |
| const res = await fetchIR(url); |
| if (!res.ok) throw new Error(res.status); |
| const raw = await res.json(); |
| setIR(raw); |
| } catch (err) { |
| setStatus(`Failed to load ${url}: ${err.message}`, true); |
| } |
| } |
|
|
| |
| |
| async function getIR(path) { |
| const key = `${path}@${state.specLevel}`; |
| if (state.irCache.has(key)) return state.irCache.get(key); |
| const res = await fetchIR(IR_BASE + path); |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); |
| const ir = new IRModel(applySpec(await res.json(), state.specLevel)); |
| state.irCache.set(key, ir); |
| return ir; |
| } |
|
|
| function setIR(raw) { |
| state.rawIR = raw; |
| try { |
| state.ir = new IRModel(applySpec(raw, state.specLevel)); |
| } catch (err) { |
| setStatus(`Invalid IR: ${err.message}`, true); |
| return; |
| } |
| graph.clearOverrides(); |
| state.edgeKinds = new Set(state.ir.edgeKinds); |
| renderArchFacts(); |
| renderCapabilityChips(); |
| renderModularity(); |
| state.expanded = new Set(state.ir.repeatsById.keys()); |
| state.fields = { ...state.ir.configFields }; |
| state.selectedId = state.ir.rootId; |
| $("#config-editor").value = JSON.stringify(state.fields, null, 2); |
| if (!state.ir.rootId || state.ir.nodes.size === 0) { |
| setStatus(`${state.ir.modelType}: empty artifact — no components emitted`, true); |
| } else { |
| setStatus(`Loaded ${state.ir.modelType} · ${state.ir.nodes.size} components · ${state.ir.edges.length} edges`); |
| } |
| rebuild(true); |
| loadHubModels(state.ir.modelType); |
| |
| ensureAvatars(kernelOrgs(state.ir), () => { |
| rebuild(false); |
| if (state.compare) renderCompare(); |
| }); |
| } |
|
|
| |
| function updateSpecUI() { |
| const canCompare = state.specLevel >= 3; |
| const btn = $("#compare-toggle"); |
| if (btn) btn.style.display = canCompare ? "" : "none"; |
| if (!canCompare && state.compare) toggleCompare(); |
| } |
|
|
| |
| function reapplySpec() { |
| if (!state.rawIR) return; |
| updateSpecUI(); |
| state.ir = new IRModel(applySpec(state.rawIR, state.specLevel)); |
| state.edgeKinds = new Set(state.ir.edgeKinds); |
| renderArchFacts(); |
| renderCapabilityChips(); |
| renderModularity(); |
| state.expanded = new Set([...state.expanded].filter((id) => state.ir.repeatsById.has(id))); |
| if (!state.ir.node(state.selectedId)) state.selectedId = state.ir.rootId; |
| rebuild(false); |
| ensureAvatars(kernelOrgs(state.ir), () => rebuild(false)); |
| if (state.compare) renderCompare(); |
| } |
|
|
| |
|
|
| function fmtDownloads(n) { |
| if (n == null) return ""; |
| if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`; |
| if (n >= 1e3) return `${(n / 1e3).toFixed(1)}k`; |
| return String(n); |
| } |
|
|
| |
| async function loadHubModels(modelType) { |
| const sel = $("#hub-select"); |
| const note = $("#hub-note"); |
| sel.replaceChildren(h("option", { value: "" }, "— loading checkpoints… —")); |
| try { |
| const url = `https://huggingface.co/api/models?filter=${encodeURIComponent( |
| modelType |
| )}&sort=downloads&direction=-1&limit=30`; |
| const res = await fetch(url); |
| if (!res.ok) throw new Error(`HTTP ${res.status}`); |
| const models = await res.json(); |
| sel.replaceChildren( |
| h("option", { value: "" }, `— pick a ${modelType} checkpoint —`) |
| ); |
| models.forEach((m) => { |
| const dl = fmtDownloads(m.downloads); |
| sel.appendChild(h("option", { value: m.id }, dl ? `${m.id} · ${dl} ↓` : m.id)); |
| }); |
| note.innerHTML = `${models.length} public <code>${modelType}</code> checkpoints · loads its <code>config.json</code>`; |
| } catch (err) { |
| sel.replaceChildren(h("option", { value: "" }, "— Hub unavailable —")); |
| note.textContent = `Could not reach the Hub (${err.message}). Edit the config manually below.`; |
| } |
| } |
|
|
| |
| async function loadHubConfig(modelId) { |
| const note = $("#hub-note"); |
| note.textContent = `Fetching config.json for ${modelId}…`; |
| try { |
| const res = await fetch(`https://huggingface.co/${modelId}/resolve/main/config.json`); |
| if (!res.ok) throw new Error(res.status === 401 ? "gated/private" : `HTTP ${res.status}`); |
| const cfg = await res.json(); |
| state.fields = cfg; |
| $("#config-editor").value = JSON.stringify(cfg, null, 2); |
| note.innerHTML = `Resolved against <code>${modelId}</code>`; |
| setStatus(`Resolved template against ${modelId}`); |
| renderCapabilityChips(); |
| rebuild(false); |
| } catch (err) { |
| note.textContent = `Could not load config for ${modelId} (${err.message}).`; |
| setStatus(`Failed to load config for ${modelId}: ${err.message}`, true); |
| } |
| } |
|
|
| function setStatus(msg, isError = false) { |
| const el = $("#status"); |
| el.textContent = msg; |
| el.classList.toggle("error", isError); |
| } |
|
|
| |
|
|
| function rebuild(fit = false) { |
| if (!state.ir) return; |
| const tied = state.ir.tiedEmbeddings(state.fields); |
| const layout = new Layout(state.ir, { |
| expanded: state.expanded, |
| fields: state.fields, |
| showInfo: state.showInfo, |
| tied, |
| }); |
| graph.render(layout, { |
| ir: state.ir, |
| fields: state.fields, |
| edgeKinds: state.edgeKinds, |
| avatars: avatarCache, |
| tied, |
| |
| showTP: state.showTP && state.specLevel >= 2, |
| showKernels: state.showKernels && state.specLevel >= 2, |
| }); |
| graph.setSelected(state.selectedId); |
| if (fit) graph.fit(); |
| renderInspector(); |
| } |
|
|
| |
|
|
| function row(label, value, opts = {}) { |
| if (value == null || value === "") return null; |
| return h( |
| "div", |
| { class: "insp-row" }, |
| h("div", { class: "insp-key" }, label), |
| h("div", { class: `insp-val ${opts.mono ? "mono" : ""}` }, String(value)) |
| ); |
| } |
|
|
| function section(title, ...rows) { |
| const kept = rows.flat().filter(Boolean); |
| if (!kept.length) return null; |
| return h("div", { class: "insp-section" }, h("div", { class: "insp-h" }, title), ...kept); |
| } |
|
|
| |
| function chipsRow(label, items) { |
| if (!items || !items.length) return null; |
| return h( |
| "div", |
| { class: "insp-row" }, |
| h("div", { class: "insp-key" }, label), |
| h("div", { class: "cap-chips" }, ...items.map((t) => h("span", { class: "cap-chip" }, String(t)))) |
| ); |
| } |
|
|
| |
| function scheduleSection(sched, summary) { |
| const grid = h( |
| "div", |
| { class: "sched-grid" }, |
| ...sched.map((p, i) => |
| h("span", { |
| class: "pat-cell", |
| title: `layer ${i}: ${p}`, |
| style: `background:${patternColor(p)}`, |
| }) |
| ) |
| ); |
| const distinct = [...new Set(sched)]; |
| const legend = h( |
| "div", |
| { class: "sched-legend" }, |
| ...distinct.map((p) => |
| h( |
| "span", |
| { class: "sched-leg-item" }, |
| h("span", { class: "pat-cell", style: `background:${patternColor(p)}` }), |
| p.replace(/_attention$/, "") |
| ) |
| ) |
| ); |
| return h( |
| "div", |
| { class: "insp-section" }, |
| h("div", { class: "insp-h" }, `Attention schedule · ${sched.length} layers`), |
| h("div", { class: "sched-summary" }, summary || ""), |
| grid, |
| legend |
| ); |
| } |
|
|
| function edgePill(edge, endpointKey) { |
| const otherId = endpointKey === "source" ? edge.target : edge.source; |
| const style = styleForKind(edge.kind); |
| const label = state.ir.label(otherId, state.fields); |
| const dir = endpointKey === "source" ? "→" : "←"; |
| return h( |
| "div", |
| { |
| class: "edge-item", |
| title: `${edge.kind}: ${edge.source} → ${edge.target}`, |
| onclick: () => select(otherId), |
| }, |
| h("span", { class: "edge-dot", style: `background:${style.color}` }), |
| h("span", { class: "edge-dir" }, dir), |
| h("span", { class: "edge-name" }, label), |
| h("span", { class: "edge-kind" }, edge.kind) |
| ); |
| } |
|
|
| function renderInspector() { |
| const panel = $("#inspector-body"); |
| panel.replaceChildren(); |
| const id = state.selectedId; |
| if (!id || !state.ir) { |
| panel.appendChild(h("div", { class: "insp-empty" }, "Select a component to inspect it.")); |
| return; |
| } |
|
|
| const isInput = typeof id === "string" && id.startsWith("input:"); |
| const n = state.ir.node(id); |
| const label = state.ir.label(id, state.fields); |
|
|
| const add = (elm) => elm && panel.appendChild(elm); |
|
|
| panel.appendChild( |
| h( |
| "div", |
| { class: "insp-title" }, |
| h("div", { class: "insp-label" }, label), |
| h("div", { class: "insp-id mono" }, id) |
| ) |
| ); |
|
|
| if (isInput) { |
| add(section("Overview", row("Kind", "graph input"), row("ID", id, { mono: true }))); |
| } else if (n) { |
| const kind = state.ir.kindLabel(n.kind); |
| add( |
| section( |
| "Overview", |
| row("Kind", `${kind} (${n.kind})`), |
| row("Class", n.className, { mono: true }), |
| row("Component ID", n.id, { mono: true }), |
| row("Node type", n.nodeType) |
| ) |
| ); |
|
|
| if (n.nodeType === "repeat") { |
| const r = n.repeat; |
| const resolved = state.ir.resolveCount(n, state.fields); |
| add( |
| section( |
| "Symbolic repeat", |
| row("Repeated class", r.repeated_class_name, { mono: true }), |
| row("Count expr", r.count_expr, { mono: true }), |
| row("Resolved count", resolved), |
| row("Baked count", r.count), |
| row("Index symbol", r.index_symbol, { mono: true }), |
| row("Container path", r.container_path_pattern, { mono: true }), |
| row("Item path", r.item_path_pattern, { mono: true }) |
| ) |
| ); |
| } |
|
|
| add(section("Source", row("Path pattern", n.pathPattern, { mono: true }))); |
|
|
| |
| if (n.attributes && typeof n.attributes === "object") { |
| const attrRows = Object.entries(n.attributes).map(([k, v]) => |
| row(k, typeof v === "object" ? JSON.stringify(v) : String(v), { mono: true }) |
| ); |
| add(section("Attributes", ...attrRows)); |
| } |
|
|
| |
| |
| if (n.kind === "projection" && n.attributes && n.attributes.tp) { |
| const col = n.attributes.tp === "colwise"; |
| add( |
| section( |
| "Tensor parallel", |
| row("Shard", col ? "column-parallel" : "row-parallel"), |
| row( |
| "Meaning", |
| col |
| ? "Output features split across TP ranks — computed locally, no collective." |
| : "Input features split across TP ranks — all-reduce combines the partial sums." |
| ) |
| ) |
| ); |
| } else { |
| const plan = state.ir.tpPlan(id); |
| if (plan) { |
| add( |
| section( |
| "Tensor-parallel plan", |
| row("Column-parallel", plan.colwise.join(", "), { mono: true }), |
| row("Row-parallel", plan.rowwise.join(", "), { mono: true }), |
| row("Collective", `${plan.rowwise.length} all-reduce${plan.rowwise.length === 1 ? "" : "s"} per block`) |
| ) |
| ); |
| } |
| } |
|
|
| |
| const kern = state.ir.nodeKernel(n); |
| if (kern) { |
| const orgs = [...new Set(kern.repos.map((r) => r.split("/")[0]))]; |
| const avatarRow = orgs.length |
| ? h( |
| "div", |
| { class: "insp-row" }, |
| h("div", { class: "insp-key" }, "Orgs"), |
| h( |
| "div", |
| { class: "org-avatars" }, |
| ...orgs.map((org) => { |
| const url = avatarCache.get(org); |
| const a = h("span", { |
| class: "org-avatar-chip", |
| title: org, |
| onclick: () => window.open(`https://huggingface.co/${org}`, "_blank", "noopener"), |
| }); |
| if (url) a.appendChild(h("img", { src: url, alt: org })); |
| a.appendChild(h("span", {}, org)); |
| return a; |
| }) |
| ) |
| ) |
| : null; |
| const repoRows = kern.repos.map((repo) => |
| h( |
| "div", |
| { |
| class: "edge-item", |
| title: `open ${repo} on the Hub`, |
| onclick: () => window.open(`https://huggingface.co/${repo}`, "_blank", "noopener"), |
| }, |
| h("span", { class: "kernel-bolt" }, "⚡"), |
| h("span", { class: "mono" }, repo) |
| ) |
| ); |
| add(section("Hub kernel", row("Kernel", kern.name, { mono: true }), avatarRow, ...repoRows)); |
| } |
|
|
| |
| if (n.kind === "model" && state.ir.architecture && typeof state.ir.architecture === "object") { |
| const archRows = Object.entries(state.ir.architecture).map(([k, v]) => |
| row(k, typeof v === "object" ? JSON.stringify(v) : String(v), { mono: true }) |
| ); |
| add(section("Architecture facts", ...archRows)); |
| } |
|
|
| |
| if (n.kind === "model" && state.ir.capabilities) { |
| const c = state.ir.capabilities; |
| const capRows = []; |
| const classes = state.ir.architectureClasses(state.fields); |
| if (classes.length) capRows.push(chipsRow("Architecture", classes)); |
| if (c.task_heads && c.task_heads.length) capRows.push(chipsRow("Task heads", c.task_heads)); |
| if (c.attention_backends && c.attention_backends.length) |
| capRows.push(chipsRow("Attn backends", c.attention_backends.map((b) => b.replace(/_attention$/, "")))); |
| if (c.attention_patterns && c.attention_patterns.length) |
| capRows.push(chipsRow("Attn patterns", c.attention_patterns.map((p) => p.replace(/_attention$/, "")))); |
| if (c.kernels && Object.keys(c.kernels).length) |
| capRows.push(chipsRow("Hub kernels", Object.keys(c.kernels))); |
| capRows.push(row("Tensor parallel", c.tensor_parallel ? "yes" : "no")); |
| add(section("Capabilities", ...capRows)); |
| } |
|
|
| |
| |
| const sched = |
| n.kind === "model" |
| ? state.ir.capabilities && state.ir.capabilities.attention_schedule |
| : n.nodeType === "repeat" |
| ? state.ir.scheduleForRepeat(n, state.fields) |
| : null; |
| if (Array.isArray(sched) && sched.length) { |
| add(scheduleSection(sched, state.ir.scheduleSummary(sched))); |
| } else if (n.kind === "model" && state.ir.capabilities && state.ir.capabilities.attention_patterns) { |
| add(section("Attention schedule", row("Uniform", state.ir.capabilities.attention_patterns.join(", ")))); |
| } |
|
|
| |
| |
| if (n.kind === "model" && state.ir.dataflow) { |
| const df = state.ir.dataflow; |
| add( |
| section( |
| "Observed dataflow", |
| row("Input", df.input ? `${df.input.name || ""} ${fmtShape(df.input.shape) || ""}`.trim() : null, { mono: true }), |
| row("Output", df.output ? fmtShape(df.output.shape) : null, { mono: true }), |
| row("Source", df.source, { mono: true }) |
| ) |
| ); |
| } |
| const shapes = state.ir.nodeShapes(id); |
| if (shapes) { |
| add( |
| section( |
| "Observed shape", |
| row("In", fmtShape(shapes.in), { mono: true }), |
| row("Out", fmtShape(shapes.out), { mono: true }) |
| ) |
| ); |
| } |
|
|
| |
| const cfgRows = configHints(n).map(([k, v]) => row(k, JSON.stringify(v), { mono: true })); |
| add(section("Config-derived", ...cfgRows)); |
|
|
| if (state.showProvenance) { |
| const p = state.ir.provenance || {}; |
| add( |
| section( |
| "Provenance", |
| row("Class name", n.className, { mono: true }), |
| row("Path pattern", n.pathPattern, { mono: true }), |
| row("Model class", p.model_class, { mono: true }), |
| row("Model module", p.model_module, { mono: true }) |
| ) |
| ); |
| } |
| } |
|
|
| |
| const { incoming, outgoing } = state.ir.edgesFor(id); |
| const edgesBox = []; |
| if (incoming.length) |
| edgesBox.push(h("div", { class: "edge-group-h" }, `Incoming (${incoming.length})`)); |
| incoming.forEach((e) => edgesBox.push(edgePill(e, "target"))); |
| if (outgoing.length) |
| edgesBox.push(h("div", { class: "edge-group-h" }, `Outgoing (${outgoing.length})`)); |
| outgoing.forEach((e) => edgesBox.push(edgePill(e, "source"))); |
| if (edgesBox.length) { |
| panel.appendChild(h("div", { class: "insp-section" }, h("div", { class: "insp-h" }, "Edges"), ...edgesBox)); |
| } |
| } |
|
|
| |
| function configHints(n) { |
| const f = state.fields || {}; |
| const wanted = { |
| attention: ["num_attention_heads", "num_heads", "num_key_value_heads", "head_dim", "attention_dropout"], |
| cross_attention: ["num_attention_heads", "num_heads", "d_kv"], |
| feed_forward: ["intermediate_size", "d_ff", "hidden_act", "dense_act_fn", "is_gated_act"], |
| embedding: ["vocab_size", "hidden_size", "d_model", "max_position_embeddings", "type_vocab_size"], |
| position: ["max_position_embeddings", "rope_parameters", "relative_attention_num_buckets"], |
| normalization: ["rms_norm_eps", "layer_norm_eps", "layer_norm_epsilon"], |
| model: ["hidden_size", "d_model", "num_hidden_layers", "num_layers", "vocab_size"], |
| }; |
| const keys = wanted[n.kind] || []; |
| return keys.filter((k) => f[k] !== undefined).map((k) => [k, f[k]]); |
| } |
|
|
| |
|
|
| function select(id) { |
| state.selectedId = id; |
| graph.setSelected(id); |
| renderInspector(); |
| } |
|
|
| function toggle(id) { |
| if (state.expanded.has(id)) state.expanded.delete(id); |
| else state.expanded.add(id); |
| rebuild(false); |
| } |
|
|
| function setAllRepeats(expand) { |
| state.expanded = new Set(); |
| if (expand) { |
| for (const r of state.ir.repeatsById.keys()) state.expanded.add(r); |
| } |
| rebuild(false); |
| } |
|
|
| function toggleTheme() { |
| const light = document.body.classList.toggle("light"); |
| try { |
| localStorage.setItem("tv-theme", light ? "light" : "dark"); |
| } catch (_) {} |
| } |
|
|
| |
| function renderArchFacts() { |
| const note = $("#arch-facts"); |
| const a = state.ir && state.ir.architecture; |
| if (!a || typeof a !== "object") { |
| note.textContent = ""; |
| return; |
| } |
| const parts = ["family", "view", "positional", "attention_variant"] |
| .map((k) => a[k]) |
| .filter((v) => v != null); |
| if (a.is_moe) parts.push("MoE"); |
| if (a.tie_word_embeddings !== undefined) parts.push(a.tie_word_embeddings ? "tied emb" : "untied emb"); |
| note.textContent = parts.join(" · "); |
| } |
|
|
| |
| |
| function renderCapabilityChips() { |
| const box = $("#capability-chips"); |
| box.replaceChildren(); |
| if (!state.ir) return; |
| const c = state.ir.capabilities; |
| const chip = (t, cls) => h("span", { class: `cap-chip ${cls || ""}` }, t); |
| |
| state.ir.architectureClasses(state.fields).forEach((cn) => box.appendChild(chip(cn, "arch"))); |
| if (c && c.tensor_parallel) box.appendChild(chip("TP", "on")); |
| if (state.ir.architecture && state.ir.architecture.is_moe) box.appendChild(chip("MoE", "on")); |
| } |
|
|
| |
| |
| function renderModularity() { |
| const note = $("#modularity-note"); |
| const ir = state.ir; |
| if (!ir || !ir.extends) { |
| note.textContent = ""; |
| return; |
| } |
| const bits = [`extends <code>${ir.extends}</code>`]; |
| if (ir.patches.length) bits.push(`${ir.patches.length} patches`); |
| const diff = ir.diffSize(); |
| if (diff) bits.push(`diff ${diff}`); |
| note.innerHTML = `Modular: ${bits.join(" · ")}`; |
| } |
|
|
| function fmtShape(s) { |
| return Array.isArray(s) ? `[${s.join(", ")}]` : s == null ? null : String(s); |
| } |
|
|
| |
|
|
| function modelTypeOf(path) { |
| const a = state.manifest.find((m) => m.artifact === path); |
| return a ? a.model_type : path; |
| } |
|
|
| |
| |
| function populateCompareSelectors() { |
| const sel = $("#cmp-b"); |
| if (!sel) return; |
| sel.replaceChildren(); |
| state.manifest.forEach((a) => |
| sel.appendChild(h("option", { value: a.artifact }, a.model_type)) |
| ); |
| const cur = $("#arch-select").value; |
| const other = state.manifest.find((a) => a.artifact !== cur) || state.manifest[0]; |
| if (other) sel.value = other.artifact; |
| } |
|
|
| |
| let cmpPaneA = null; |
| let cmpPaneB = null; |
| let cmpMerge = null; |
|
|
| function makePane(svgSelector) { |
| const pane = { ir: null, expanded: new Set(), shared: new Set(), graph: null }; |
| pane.graph = new Graph($(svgSelector), { |
| onSelect: (id) => pane.graph.setSelected(id), |
| onToggle: (id) => { |
| if (pane.expanded.has(id)) pane.expanded.delete(id); |
| else pane.expanded.add(id); |
| renderPane(pane); |
| }, |
| }); |
| return pane; |
| } |
|
|
| function renderPane(pane, fit = false) { |
| const tied = pane.ir.tiedEmbeddings(pane.ir.configFields); |
| const layout = new Layout(pane.ir, { |
| expanded: pane.expanded, |
| fields: pane.ir.configFields, |
| showInfo: state.showInfo, |
| tied, |
| }); |
| pane.graph.render(layout, { |
| ir: pane.ir, |
| fields: pane.ir.configFields, |
| edgeKinds: new Set(pane.ir.edgeKinds), |
| sharedIds: pane.shared, |
| avatars: avatarCache, |
| tied, |
| showTP: state.showTP && state.specLevel >= 2, |
| showKernels: state.showKernels && state.specLevel >= 2, |
| }); |
| if (fit) pane.graph.fit(); |
| } |
|
|
| |
| function sharedIdSet(a, b) { |
| const s = new Set(); |
| for (const id of a.nodes.keys()) if (b.nodes.has(id)) s.add(id); |
| return s; |
| } |
|
|
| |
| function makeMergePane() { |
| const pane = { ir: null, expanded: new Set(), origin: null, edgeOrigin: null, graph: null }; |
| pane.graph = new Graph($("#cmp-graph-merged"), { |
| onSelect: (id) => pane.graph.setSelected(id), |
| onToggle: (id) => { |
| if (pane.expanded.has(id)) pane.expanded.delete(id); |
| else pane.expanded.add(id); |
| renderMergePane(pane); |
| }, |
| }); |
| return pane; |
| } |
|
|
| function renderMergePane(pane, fit = false) { |
| const tied = pane.ir.tiedEmbeddings(pane.ir.configFields); |
| const layout = new Layout(pane.ir, { |
| expanded: pane.expanded, |
| fields: pane.ir.configFields, |
| showInfo: state.showInfo, |
| tied, |
| }); |
| pane.graph.render(layout, { |
| ir: pane.ir, |
| fields: pane.ir.configFields, |
| edgeKinds: new Set(pane.ir.edgeKinds), |
| avatars: avatarCache, |
| origin: pane.origin, |
| edgeOrigin: pane.edgeOrigin, |
| tied, |
| showTP: state.showTP && state.specLevel >= 2, |
| showKernels: state.showKernels && state.specLevel >= 2, |
| }); |
| if (fit) pane.graph.fit(); |
| } |
|
|
| function toggleCompare() { |
| state.compare = !state.compare; |
| document.body.classList.toggle("compare", state.compare); |
| $("#compare-toggle").classList.toggle("active", state.compare); |
| if (state.compare) renderCompare(); |
| else graph.fit(); |
| } |
|
|
| async function renderCompare() { |
| const body = $("#compare-body"); |
| const av = $("#arch-select").value; |
| |
| if ($("#cmp-b").value === av) { |
| const other = state.manifest.find((a) => a.artifact !== av); |
| if (other) $("#cmp-b").value = other.artifact; |
| } |
| const bv = $("#cmp-b").value; |
| $("#cmp-a-label").textContent = modelTypeOf(av); |
| body.replaceChildren(h("div", { class: "cmp-msg" }, "Comparing…")); |
| try { |
| const [a, b] = await Promise.all([getIR(av), getIR(bv)]); |
| const diff = compareIR(a, b); |
| |
| const useMerged = state.compareMerged && diff.regime.basis === "id"; |
| document.body.classList.toggle("cmp-merged", useMerged); |
| $("#cmp-mode").classList.toggle("active", useMerged); |
| $("#cmp-mode").disabled = diff.regime.basis !== "id"; |
|
|
| if (useMerged) { |
| if (!cmpMerge) cmpMerge = makeMergePane(); |
| const m = mergeIR(a, b); |
| cmpMerge.ir = new IRModel(m.raw); |
| cmpMerge.origin = m.origin; |
| cmpMerge.edgeOrigin = m.edgeOrigin; |
| cmpMerge.expanded = new Set(cmpMerge.ir.repeatsById.keys()); |
| $("#cmp-merge-legend").innerHTML = |
| `<span class="mrg-both">■ shared</span><span class="mrg-a">■ only ${a.modelType}</span><span class="mrg-b">■ only ${b.modelType}</span>`; |
| const onlyA = [...m.origin.values()].filter((o) => o === "a").length; |
| const onlyB = [...m.origin.values()].filter((o) => o === "b").length; |
| $("#cmp-shared-note").innerHTML = `<span class="cmp-shared-dot"></span>${onlyA} only ${a.modelType} · ${onlyB} only ${b.modelType}`; |
| renderMergePane(cmpMerge, true); |
| ensureAvatars([...kernelOrgs(a), ...kernelOrgs(b)], () => renderMergePane(cmpMerge)); |
| } else { |
| |
| if (!cmpPaneA) { |
| cmpPaneA = makePane("#cmp-graph-a"); |
| cmpPaneB = makePane("#cmp-graph-b"); |
| cmpPaneA.graph.cb.onView = (v) => cmpPaneB.graph.setView(v); |
| cmpPaneB.graph.cb.onView = (v) => cmpPaneA.graph.setView(v); |
| } |
| const shared = sharedIdSet(a, b); |
| cmpPaneA.ir = a; |
| cmpPaneA.expanded = new Set(a.repeatsById.keys()); |
| cmpPaneA.shared = shared; |
| cmpPaneB.ir = b; |
| cmpPaneB.expanded = new Set(b.repeatsById.keys()); |
| cmpPaneB.shared = shared; |
| $("#cmp-a-title").textContent = a.modelType; |
| $("#cmp-b-title").textContent = b.modelType; |
| $("#cmp-shared-note").innerHTML = shared.size |
| ? `<span class="cmp-shared-dot"></span>${shared.size} shared component${shared.size === 1 ? "" : "s"} (by stable id)` |
| : "no shared components — different lineages"; |
| renderPane(cmpPaneA, true); |
| renderPane(cmpPaneB, true); |
| ensureAvatars([...kernelOrgs(a), ...kernelOrgs(b)], () => { |
| renderPane(cmpPaneA); |
| renderPane(cmpPaneB); |
| }); |
| } |
|
|
| body.replaceChildren(...renderDiff(diff)); |
| } catch (err) { |
| body.replaceChildren(h("div", { class: "cmp-msg error" }, `Compare failed: ${err.message}`)); |
| } |
| } |
|
|
| |
| |
| function diffCard(title, rows, cellA, cellB) { |
| const ca = cellA || ((r) => String(r.a)); |
| const cb = cellB || ((r) => String(r.b)); |
| const trs = rows.map((r) => |
| h( |
| "div", |
| { class: `cmp-row ${r.same ? "" : "cmp-diff"}` }, |
| h("div", { class: "cmp-k" }, r.field || r.kind || r.label || r.item), |
| h("div", { class: "cmp-v cmp-va" }, ca(r)), |
| h("div", { class: "cmp-v cmp-vb" }, cb(r)) |
| ) |
| ); |
| return h("section", { class: "cmp-card" }, h("h3", {}, title), ...trs); |
| } |
|
|
| function renderDiff(d) { |
| const out = []; |
|
|
| |
| const regimeLabel = { lineage: "same lineage", shared_ids: "shared ids", cross_lineage: "cross-lineage" }[d.regime.kind] || d.regime.kind; |
| out.push( |
| h( |
| "div", |
| { class: "cmp-banner" }, |
| h("div", { class: "cmp-title" }, `${d.a.modelType} vs ${d.b.modelType}`), |
| h("div", { class: "cmp-regime" }, h("span", { class: `cmp-badge regime-${d.regime.kind}` }, regimeLabel), d.regime.note) |
| ) |
| ); |
|
|
| |
| out.push( |
| h( |
| "div", |
| { class: "cmp-row cmp-head" }, |
| h("div", { class: "cmp-k" }, ""), |
| h("div", { class: "cmp-v cmp-va" }, d.a.modelType), |
| h("div", { class: "cmp-v cmp-vb" }, d.b.modelType) |
| ) |
| ); |
|
|
| |
| const chk = (on) => h("span", { class: on ? "cmp-yes" : "cmp-no" }, on ? "✓" : "·"); |
|
|
| if (d.headline.length) out.push(diffCard("Architecture", d.headline)); |
| if (d.scale.length) out.push(diffCard("Scale", d.scale)); |
|
|
| |
| if (d.capabilities) { |
| const cap = d.capabilities; |
| out.push(diffCard("Capabilities · attention backends", cap.backends, (r) => chk(r.inA), (r) => chk(r.inB))); |
| out.push(diffCard("Capabilities · task heads", cap.taskHeads, (r) => chk(r.inA), (r) => chk(r.inB))); |
| out.push(diffCard("Capabilities · attention", cap.rows)); |
| } |
|
|
| out.push( |
| diffCard( |
| "Topology (edge kinds)", |
| d.topology, |
| (r) => chk(r.inA), |
| (r) => chk(r.inB) |
| ) |
| ); |
|
|
| |
| if (d.nodes.basis === "kind") { |
| out.push(renderKindLanes(d.nodes.lanes)); |
| } else { |
| out.push(renderIdAlign(d.nodes)); |
| } |
|
|
| return out; |
| } |
|
|
| function renderKindLanes(lanes) { |
| const rows = lanes.map((l) => { |
| const tag = l.onlyA ? h("span", { class: "cmp-tag only-a" }, "A only") : l.onlyB ? h("span", { class: "cmp-tag only-b" }, "B only") : null; |
| const deltas = l.attrDeltas.length |
| ? h("span", { class: "cmp-deltas" }, "Δ " + l.attrDeltas.map((x) => x.key).join(", ")) |
| : null; |
| return h( |
| "div", |
| { class: `cmp-row ${l.onlyA || l.onlyB || l.attrDeltas.length || l.countA !== l.countB ? "cmp-diff" : ""}` }, |
| h("div", { class: "cmp-k" }, l.kind), |
| h("div", { class: "cmp-v cmp-va" }, `×${l.countA}`), |
| h("div", { class: "cmp-v cmp-vb" }, `×${l.countB}`, tag, deltas) |
| ); |
| }); |
| return h("section", { class: "cmp-card" }, h("h3", {}, "Components by kind (aligned into lanes)"), ...rows); |
| } |
|
|
| function renderIdAlign(nodes) { |
| const kids = []; |
| const changed = nodes.matched.filter((m) => m.changed); |
| changed.forEach((m) => { |
| const deltas = m.attrDeltas.map((dd) => |
| h("div", { class: "cmp-attr" }, h("span", { class: "cmp-attr-k mono" }, dd.key), h("span", {}, `${dd.a} → ${dd.b}`)) |
| ); |
| kids.push( |
| h( |
| "div", |
| { class: "cmp-node cmp-changed" }, |
| h("div", { class: "cmp-node-h" }, h("span", { class: "mono" }, m.id), m.kindChanged ? h("span", { class: "cmp-tag" }, `${m.kindA}→${m.kindB}`) : null), |
| ...deltas |
| ) |
| ); |
| }); |
| nodes.added.forEach((n) => kids.push(h("div", { class: "cmp-node cmp-added" }, h("span", { class: "cmp-tag add" }, "added"), h("span", { class: "mono" }, n.id)))); |
| nodes.removed.forEach((n) => kids.push(h("div", { class: "cmp-node cmp-removed" }, h("span", { class: "cmp-tag del" }, "removed"), h("span", { class: "mono" }, n.id)))); |
| if (!kids.length) kids.push(h("div", { class: "cmp-msg" }, "No component-level differences.")); |
| return h("section", { class: "cmp-card" }, h("h3", {}, `Component deltas (${changed.length} changed · ${nodes.added.length} added · ${nodes.removed.length} removed)`), ...kids); |
| } |
|
|
| |
|
|
| function bindControls() { |
| $("#arch-select").addEventListener("change", (e) => { |
| loadArtifact(e.target.value); |
| if (state.compare) renderCompare(); |
| }); |
|
|
| $("#fit-btn").addEventListener("click", () => graph.fit()); |
| $("#expand-all").addEventListener("click", () => setAllRepeats(true)); |
| $("#collapse-all").addEventListener("click", () => setAllRepeats(false)); |
| $("#reset-layout").addEventListener("click", () => graph.resetPositions()); |
| $("#theme-toggle").addEventListener("click", toggleTheme); |
| $("#compare-toggle").addEventListener("click", toggleCompare); |
| $("#cmp-b").addEventListener("change", renderCompare); |
| $("#cmp-mode").addEventListener("click", () => { |
| state.compareMerged = !state.compareMerged; |
| renderCompare(); |
| }); |
| $("#cmp-fit").addEventListener("click", () => { |
| if (state.compareMerged && cmpMerge && document.body.classList.contains("cmp-merged")) cmpMerge.graph.fit(); |
| if (cmpPaneA) cmpPaneA.graph.fit(); |
| if (cmpPaneB) cmpPaneB.graph.fit(); |
| }); |
|
|
| $("#hub-select").addEventListener("change", (e) => { |
| if (e.target.value) loadHubConfig(e.target.value); |
| }); |
|
|
| |
| $("#spec-seg").addEventListener("click", (e) => { |
| const btn = e.target.closest("[data-spec]"); |
| if (!btn) return; |
| state.specLevel = Number(btn.getAttribute("data-spec")); |
| [...$("#spec-seg").children].forEach((b) => b.classList.toggle("active", b === btn)); |
| state.irCache.clear(); |
| reapplySpec(); |
| }); |
| |
| $("#opt-tp").addEventListener("change", (e) => { |
| state.showTP = e.target.checked; |
| rebuild(false); |
| if (state.compare) renderCompare(); |
| }); |
| $("#opt-kernels").addEventListener("change", (e) => { |
| state.showKernels = e.target.checked; |
| rebuild(false); |
| if (state.compare) renderCompare(); |
| }); |
|
|
| |
| $("#config-apply").addEventListener("click", () => { |
| try { |
| const parsed = JSON.parse($("#config-editor").value); |
| state.fields = parsed; |
| setStatus("Config applied · repeat counts re-resolved"); |
| renderCapabilityChips(); |
| rebuild(false); |
| } catch (err) { |
| setStatus(`Invalid config JSON: ${err.message}`, true); |
| } |
| }); |
| $("#config-reset").addEventListener("click", () => { |
| if (!state.ir) return; |
| state.fields = { ...state.ir.configFields }; |
| $("#config-editor").value = JSON.stringify(state.fields, null, 2); |
| setStatus("Config reset to artifact defaults"); |
| renderCapabilityChips(); |
| rebuild(false); |
| }); |
| } |
|
|
| |
|
|
| function boot() { |
| |
| try { |
| if (localStorage.getItem("tv-theme") === "light") document.body.classList.add("light"); |
| } catch (_) {} |
| graph = new Graph($("#graph"), { onSelect: select, onToggle: toggle }); |
| bindControls(); |
| updateSpecUI(); |
| window.addEventListener("resize", () => graph.fit()); |
| loadManifest(); |
| } |
|
|
| boot(); |
|
|
| export { renderDiff, chipsRow, scheduleSection }; |
|
|