memlnaut-nisps/playground/src/primitives/GradientFlow.tsx
w1n5t0n c3fb63a113 feat(playground): UI primitive library + /dev/primitives showcase
Sixteen primitives at src/primitives/, each with its own .module.css and a
.demo.tsx that wires the primitive to live local state. All primitives
are typed (strict), pointer-event based (touch + mouse), keyboard-accessible
where it makes sense, and free of business logic.

Primitives:
- Slider, SliderBank: value sliders with optional curve mapping;
  collapsible sections.
- VirtualJoystick, XYPad: 2D pointer input, returning [0,1]^2.
- Heatmap: NxN canvas rendering with three color modes
  (luminance/variance/divergence) and a configurable palette.
- OutputDisplay: bar chart of N output values.
- TrainingControls: thumbs-up/down/undo + train/randomise + status.
- Drawer: portal'd slide-in panel from left or right; ESC + scrim close.
- ControlAxis: compound-axis slider (Boldness/Memory/Precision shape)
  with active-preset hint, endpoint labels, double-tap-to-relink.
- ProgressRing: SVG circular progress with optional inline label.
- PillToggle: segmented radio control.
- ParamEditor: min/max/curve/mute/pin/fixedValue editor for a single param.
- JoyMap: zoom minimap with adaptive grid + zoom window, vanishing trail
  with Catmull-Rom spline + tap-to-return, dual concentric noise rings,
  region pin overlays, frozen overlay.
- WeightHealth: 10-bin histogram + dead/saturating/healthy status glow.
- GradientFlow: per-layer bar chart with vanishing/exploding/converged
  color coding.
- LossPlot: log-scale line chart of training loss history.

App.tsx now wires the /dev/primitives route to a lazy-loaded
PrimitivesShowcase that renders all sixteen demos in a responsive grid.
Lazy import keeps the home page bundle ~20 kB while the showcase ships
its own ~42 kB chunk.

Stream 8 of the rewrite (meml-911).
2026-04-29 15:38:55 +03:00

69 lines
2 KiB
TypeScript

import { Component, createEffect } from 'solid-js';
import styles from './GradientFlow.module.css';
export type GradientStatus = 'vanishing' | 'exploding' | 'converged' | 'healthy';
export interface GradientFlowProps {
/** Per-layer L2 norms of weight delta. */
layerNorms: () => ReadonlyArray<number>;
/** Per-layer status. Length must match layerNorms(). */
status: () => ReadonlyArray<GradientStatus>;
width?: number;
height?: number;
ariaLabel?: string;
}
const STATUS_COLOR: Record<GradientStatus, string> = {
vanishing: '#5b9eef',
exploding: '#ef5b5b',
converged: '#9a9a9a',
healthy: '#6bc26b',
};
export const GradientFlow: Component<GradientFlowProps> = (props) => {
let canvasEl: HTMLCanvasElement | undefined;
createEffect(() => {
if (!canvasEl) return;
const dpr = window.devicePixelRatio || 1;
const w = (props.width ?? 240) * dpr;
const h = (props.height ?? 80) * dpr;
if (canvasEl.width !== w || canvasEl.height !== h) {
canvasEl.width = w;
canvasEl.height = h;
}
const ctx = canvasEl.getContext('2d');
if (!ctx) return;
ctx.clearRect(0, 0, w, h);
const norms = props.layerNorms();
const status = props.status();
if (norms.length === 0) return;
let max = 0;
for (const n of norms) if (n > max) max = n;
if (max <= 0) max = 1;
const barW = w / norms.length;
for (let i = 0; i < norms.length; i++) {
const bh = (norms[i]! / max) * (h - 14 * dpr);
const s = status[i] ?? 'healthy';
ctx.fillStyle = STATUS_COLOR[s];
ctx.fillRect(i * barW + 2, h - bh - 2, barW - 4, bh);
ctx.fillStyle = '#5a5a5a';
ctx.font = `${10 * dpr}px var(--font-mono)`;
ctx.textAlign = 'center';
ctx.fillText(`L${i}`, i * barW + barW / 2, h - 2);
}
});
return (
<canvas
ref={canvasEl}
class={styles.canvas}
style={{ width: `${props.width ?? 240}px`, height: `${props.height ?? 80}px` }}
role="img"
aria-label={props.ariaLabel ?? 'Gradient flow per layer'}
/>
);
};