| import { useEffect, useMemo, useRef, useState } from "react"; |
|
|
| import Weights from "./Weights.jsx"; |
|
|
| import { fmtParams, latticeAxisOf } from "../spec.js"; |
|
|
| |
| |
| |
| |
|
|
| const FAMILY = { |
| scan: { color: "#ffc061", glyph: "→" }, |
| kernel: { color: "#c39bd8", glyph: "⊗" }, |
| flatten: { color: "#5eead4", glyph: "⊕" }, |
| }; |
|
|
| const S = { |
| wrap: { |
| width: "44%", |
| minWidth: 300, |
| display: "flex", |
| flexDirection: "column", |
| borderLeft: "1px solid #1e2635", |
| background: "linear-gradient(180deg, #0e1320 0%, #0b0f18 100%)", |
| }, |
| head: { padding: "12px 16px 10px", borderBottom: "1px solid #1a2233" }, |
| title: { |
| color: "#5d6b84", |
| fontSize: 10.5, |
| fontWeight: 700, |
| letterSpacing: 1.4, |
| textTransform: "uppercase", |
| }, |
| io: { |
| display: "flex", |
| alignItems: "center", |
| gap: 8, |
| marginTop: 7, |
| fontSize: 12, |
| color: "#9fb0cc", |
| fontVariantNumeric: "tabular-nums", |
| flexWrap: "wrap", |
| }, |
| pill: { |
| background: "#151d2e", |
| border: "1px solid #253048", |
| borderRadius: 6, |
| padding: "3px 8px", |
| }, |
| body: { flex: 1, overflowY: "auto", padding: "10px 16px 16px" }, |
| node: (active, color) => ({ |
| position: "relative", |
| display: "flex", |
| alignItems: "center", |
| gap: 9, |
| padding: "7px 10px", |
| borderRadius: 8, |
| fontSize: 12.5, |
| cursor: "pointer", |
| background: active ? "#1a2438" : "#121926", |
| border: `1px solid ${active ? color : "#1f2a3d"}`, |
| boxShadow: active ? `0 0 0 1px ${color}55, inset 2px 0 0 ${color}` : "none", |
| }), |
| bar: { |
| position: "relative", |
| height: 4, |
| borderRadius: 99, |
| background: "#0c1018", |
| overflow: "hidden", |
| marginTop: 5, |
| }, |
| rail: { |
| width: 1, |
| height: 12, |
| background: "#243049", |
| margin: "0 auto", |
| }, |
| chip: { |
| fontSize: 10.5, |
| padding: "1px 6px", |
| borderRadius: 99, |
| background: "#1c2740", |
| color: "#9fb3d9", |
| whiteSpace: "nowrap", |
| }, |
| mech: { |
| display: "flex", |
| gap: 6, |
| flexWrap: "wrap", |
| marginTop: 8, |
| }, |
| tabs: { display: "flex", gap: 2, marginTop: 10 }, |
| tab: (active) => ({ |
| background: "none", |
| border: "none", |
| borderBottom: active ? "2px solid #ffc061" : "2px solid transparent", |
| color: active ? "#e8eef8" : "#7d8aa3", |
| fontSize: 11.5, |
| fontWeight: active ? 650 : 500, |
| padding: "5px 10px", |
| cursor: "pointer", |
| letterSpacing: 0.3, |
| }), |
| }; |
|
|
| function Node({ layer, i, spec, active, maxParams, onSelect }) { |
| const ref = useRef(); |
| const kind = layer.kind ?? "scan"; |
| const fam = FAMILY[kind] ?? FAMILY.scan; |
| const isTime = kind === "scan" && latticeAxisOf(layer, spec) === null; |
|
|
| |
| |
| |
| useEffect(() => { |
| if (active && ref.current) { |
| const el = ref.current; |
| const box = el.getBoundingClientRect(); |
| const par = el.parentElement.parentElement.getBoundingClientRect(); |
| if (box.top < par.top || box.bottom > par.bottom) { |
| el.scrollIntoView({ block: "nearest", behavior: "smooth" }); |
| } |
| } |
| }, [active]); |
|
|
| const label = |
| kind === "flatten" |
| ? (layer.axes ?? []).join(" ⊕ ") |
| : kind === "kernel" |
| ? (layer.contracted ?? []).join(" ⊗ ") |
| : layer.axis; |
| const detail = |
| kind === "flatten" |
| ? layer.tokens |
| ? `${layer.tokens} tokens` |
| : "" |
| : kind === "kernel" |
| ? layer.axis |
| ? `+ ${layer.axis} →` |
| : "" |
| : isTime |
| ? "→ causal" |
| : layer.reverse |
| ? "←" |
| : "→"; |
|
|
| const pct = maxParams ? (100 * (layer.n_params ?? 0)) / maxParams : 0; |
|
|
| return ( |
| <div> |
| <div |
| ref={ref} |
| className="td-row" |
| style={S.node(active, fam.color)} |
| onClick={() => onSelect(i)} |
| > |
| <span style={{ color: "#6b7a96", width: 26, flexShrink: 0 }}>L{i}</span> |
| <span style={{ color: fam.color, width: 14, flexShrink: 0 }}> |
| {fam.glyph} |
| </span> |
| <div style={{ flex: 1, minWidth: 0 }}> |
| <div |
| style={{ |
| display: "flex", |
| alignItems: "baseline", |
| gap: 6, |
| whiteSpace: "nowrap", |
| overflow: "hidden", |
| }} |
| > |
| <b style={{ color: active ? "#e8eef8" : "#c3cdde" }}>{label}</b> |
| <span style={{ color: "#6b7a96", fontSize: 11.5 }}>{detail}</span> |
| </div> |
| <div style={S.bar}> |
| <div |
| style={{ |
| height: "100%", |
| width: `${pct}%`, |
| background: fam.color, |
| opacity: active ? 0.95 : 0.45, |
| transition: "opacity 160ms ease", |
| }} |
| /> |
| </div> |
| </div> |
| <div style={{ textAlign: "right", flexShrink: 0 }}> |
| {layer.mixer && <div style={S.chip}>{layer.mixer}</div>} |
| <div |
| style={{ |
| color: "#6b7a96", |
| fontSize: 11, |
| marginTop: 3, |
| fontVariantNumeric: "tabular-nums", |
| }} |
| > |
| {fmtParams(layer.n_params ?? 0)} |
| </div> |
| </div> |
| </div> |
| <div style={S.rail} /> |
| </div> |
| ); |
| } |
|
|
| export default function ModelView({ |
| parsed, |
| layerIndex, |
| onSelectLayer, |
| weights, |
| }) { |
| const [view, setView] = useState("flow"); |
| const { spec } = parsed; |
| const m = spec.model; |
| const io = spec.io ?? {}; |
|
|
| const maxParams = useMemo( |
| () => Math.max(1, ...spec.layers.map((l) => l.n_params ?? 0)), |
| [spec], |
| ); |
| |
| |
| const mechanisms = useMemo(() => { |
| const mixers = [ |
| ...new Set(spec.layers.map((l) => l.mixer).filter(Boolean)), |
| ]; |
| const kinds = [...new Set(spec.layers.map((l) => l.kind ?? "scan"))]; |
| return { mixers, kinds }; |
| }, [spec]); |
|
|
| const swept = spec.layers.reduce((a, l) => a + (l.n_params ?? 0), 0); |
|
|
| return ( |
| <div style={S.wrap}> |
| <div style={S.head}> |
| <div style={S.title}>model</div> |
| <div style={S.io}> |
| <span style={S.pill}>{(io.input ?? []).join(" × ") || "—"}</span> |
| <span style={{ color: "#41568a" }}>▶</span> |
| <b style={{ color: "#e8eef8" }}>{m.kind}</b> |
| <span style={{ color: "#41568a" }}>▶</span> |
| <span style={S.pill}>{(io.output ?? []).join(" × ") || "—"}</span> |
| </div> |
| <div style={S.mech}> |
| <span style={S.chip}>{spec.nd_method.name}</span> |
| {mechanisms.mixers.map((x) => ( |
| <span key={x} style={{ ...S.chip, color: "#ffc9a0" }}> |
| {x} |
| </span> |
| ))} |
| {mechanisms.kinds.map((k) => ( |
| <span |
| key={k} |
| style={{ ...S.chip, color: FAMILY[k]?.color ?? "#9fb3d9" }} |
| > |
| {FAMILY[k]?.glyph} {k} |
| </span> |
| ))} |
| <span style={S.chip}>{fmtParams(m.n_params)} total</span> |
| {swept !== m.n_params && ( |
| // Layer weights rarely add up to the whole model: projections, |
| // norms and the head live outside the swept stack. Saying so beats |
| // letting the two numbers quietly disagree. |
| <span style={{ ...S.chip, color: "#7d8aa3" }}> |
| {fmtParams(m.n_params - swept)} outside layers |
| </span> |
| )} |
| </div> |
| <div style={S.tabs}> |
| {["flow", "weights"].map((v) => ( |
| <button |
| key={v} |
| style={S.tab(view === v)} |
| onClick={() => setView(v)} |
| > |
| {v === "flow" ? "layer flow" : `weights · L${layerIndex}`} |
| </button> |
| ))} |
| </div> |
| </div> |
| |
| <div style={S.body} className="td-panel"> |
| {view === "weights" && ( |
| <Weights weights={weights} layerIndex={layerIndex} /> |
| )} |
| {view === "flow" && |
| spec.layers.map((l, i) => ( |
| <Node |
| key={i} |
| layer={l} |
| i={i} |
| spec={spec} |
| active={i === layerIndex} |
| maxParams={maxParams} |
| onSelect={onSelectLayer} |
| /> |
| ))} |
| </div> |
| </div> |
| ); |
| } |
|
|