memlnaut-nisps/playground/src/primitives/GradientFlow.tsx

70 lines
2 KiB
TypeScript
Raw Normal View History

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 14:38:55 +02:00
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'}
/>
);
};