memlnaut-nisps/playground/js/ui/snapshot-stack.js

170 lines
4.6 KiB
JavaScript
Raw Normal View History

feat(playground): implement Phases 2-4 of control surface spec Phase 2 — Pinning + History: - snapshot-stack.js: ring buffer (20 max) with auto-snapshot on train/randomize/thumbs-down, multi-level undo, tagged entries - ab-compare.js: A/B weight state comparison with capture/toggle/accept/revert - region-pin.js: pin rectangular input-space regions (Approach A: example pinning), pinned examples always included in training - param-pin.js: per-output pin flags, pin mask skips pinned nodes in moveWeights - phase2-ui.js: undo button with history popup, A/B toggle, long-press region pin, double-tap param pin - Modified mlp.js/iml.js/nisps-wasm.js to accept outputPinMask in moveWeights Phase 3 — Input Refinement + Exploration: - pressure-feedback.js: touch force + hold duration → intensity multiplier - auto-explore.js: automated thumbs-down at configurable interval, zoom-scaled - input-heatmap.js: 16×16 MLP sampling, 3 color modes (luminance/variance/ divergence), zoom-aware resampling, offscreen canvas rendering - phase3-ui.js: auto-explore toggle with progress ring, heatmap eye icon, pressure indicators, settings drawer section - joy-map-enhanced.js: added setHeatmap() for background layer rendering Phase 4 — Output Pipeline + Visualization + Polish: - output-pipeline.js: global curve → smoothing → slew rate → freeze gate - weight-health.js: weight magnitude histogram, dead/saturating/healthy status - gradient-flow.js: per-layer weight-delta analysis, vanishing/exploding detection - session-presets.js: save/load full state, URL sharing via compact params - phase4-ui.js: freeze button, network health panel, session preset UI All phases merged into a-app.js with proper integration: auto-snapshots, pressure-modulated RL, heatmap triggers, output pipeline in routeOutputs, gradient capture around training, persistence for all new state.
2026-03-26 09:48:12 +01:00
// Snapshot Stack — Multi-level undo for weight states
//
// A ring buffer of weight snapshots tagged with context (e.g. "before thumbs-down",
// "before train"). Auto-snapshot triggers in the app push snapshots before destructive
// operations. Users can undo (pop), peek, list, or jump to any snapshot.
//
// Each snapshot stores:
// - weights: the full flat weight array (Array<number> or Float32Array)
// - noiseLevel: the RL noise level at time of snapshot
// - zoomLevel: the input pipeline zoom level (optional)
// - tag: human-readable label for UI display
// - timestamp: Date.now() when captured
//
// Usage:
// import { SnapshotStack } from './snapshot-stack.js';
// const stack = new SnapshotStack(20);
// stack.push('before thumbs-down', { weights, noiseLevel, zoomLevel });
// const prev = stack.pop(); // undo
export class SnapshotStack {
/**
* @param {number} maxSnapshots Ring buffer capacity. Oldest evicted when full.
*/
constructor(maxSnapshots = 20) {
this._max = maxSnapshots;
this._stack = []; // Array of { tag, timestamp, state: { weights, noiseLevel, zoomLevel } }
}
// ---- Core operations ----
/**
* Push a snapshot onto the stack.
* @param {string} tag Human-readable label (e.g. "before thumbs-down")
* @param {{ weights: Array|Float32Array, noiseLevel: number, zoomLevel?: number }} state
*/
push(tag, state) {
const snapshot = {
tag,
timestamp: Date.now(),
state: {
weights: Array.isArray(state.weights)
? [...state.weights]
: Array.from(state.weights),
noiseLevel: state.noiseLevel ?? 0,
zoomLevel: state.zoomLevel ?? 1.0,
},
};
this._stack.push(snapshot);
// Ring buffer eviction: drop oldest if over capacity
if (this._stack.length > this._max) {
this._stack.shift();
}
this._dispatch('snapshot:push', { tag, depth: this.depth });
}
/**
* Pop the most recent snapshot (undo).
* @returns {{ weights: Array, noiseLevel: number, zoomLevel: number }|null}
*/
pop() {
if (this._stack.length === 0) return null;
const snapshot = this._stack.pop();
this._dispatch('snapshot:pop', { tag: snapshot.tag, depth: this.depth });
return snapshot.state;
}
/**
* Peek at the most recent snapshot without removing it.
* @returns {{ weights: Array, noiseLevel: number, zoomLevel: number }|null}
*/
peek() {
if (this._stack.length === 0) return null;
return this._stack[this._stack.length - 1].state;
}
/**
* List all snapshots (newest last) for UI display.
* @returns {Array<{ tag: string, timestamp: number, index: number }>}
*/
list() {
return this._stack.map((snap, i) => ({
tag: snap.tag,
timestamp: snap.timestamp,
index: i,
}));
}
/**
* Jump to a specific snapshot by index. Removes everything above it.
* @param {number} index
* @returns {{ weights: Array, noiseLevel: number, zoomLevel: number }|null}
*/
jumpTo(index) {
if (index < 0 || index >= this._stack.length) return null;
const snapshot = this._stack[index];
// Truncate: keep entries 0..index (inclusive), remove the rest
this._stack = this._stack.slice(0, index + 1);
feat(playground): implement Phases 2-4 of control surface spec Phase 2 — Pinning + History: - snapshot-stack.js: ring buffer (20 max) with auto-snapshot on train/randomize/thumbs-down, multi-level undo, tagged entries - ab-compare.js: A/B weight state comparison with capture/toggle/accept/revert - region-pin.js: pin rectangular input-space regions (Approach A: example pinning), pinned examples always included in training - param-pin.js: per-output pin flags, pin mask skips pinned nodes in moveWeights - phase2-ui.js: undo button with history popup, A/B toggle, long-press region pin, double-tap param pin - Modified mlp.js/iml.js/nisps-wasm.js to accept outputPinMask in moveWeights Phase 3 — Input Refinement + Exploration: - pressure-feedback.js: touch force + hold duration → intensity multiplier - auto-explore.js: automated thumbs-down at configurable interval, zoom-scaled - input-heatmap.js: 16×16 MLP sampling, 3 color modes (luminance/variance/ divergence), zoom-aware resampling, offscreen canvas rendering - phase3-ui.js: auto-explore toggle with progress ring, heatmap eye icon, pressure indicators, settings drawer section - joy-map-enhanced.js: added setHeatmap() for background layer rendering Phase 4 — Output Pipeline + Visualization + Polish: - output-pipeline.js: global curve → smoothing → slew rate → freeze gate - weight-health.js: weight magnitude histogram, dead/saturating/healthy status - gradient-flow.js: per-layer weight-delta analysis, vanishing/exploding detection - session-presets.js: save/load full state, URL sharing via compact params - phase4-ui.js: freeze button, network health panel, session preset UI All phases merged into a-app.js with proper integration: auto-snapshots, pressure-modulated RL, heatmap triggers, output pipeline in routeOutputs, gradient capture around training, persistence for all new state.
2026-03-26 09:48:12 +01:00
this._dispatch('snapshot:jump', { tag: snapshot.tag, depth: this.depth });
return snapshot.state;
}
/**
* Current stack depth.
* @returns {number}
*/
get depth() {
return this._stack.length;
}
/**
* Clear all snapshots.
*/
clear() {
this._stack = [];
this._dispatch('snapshot:clear', { depth: 0 });
}
// ---- Serialization ----
/**
* Serialize for localStorage persistence.
*/
getState() {
return {
max: this._max,
snapshots: this._stack.map(s => ({
tag: s.tag,
timestamp: s.timestamp,
state: {
weights: s.state.weights,
noiseLevel: s.state.noiseLevel,
zoomLevel: s.state.zoomLevel,
},
})),
};
}
/**
* Restore from serialized state.
*/
setState(saved) {
if (!saved) return;
if (typeof saved.max === 'number') this._max = saved.max;
if (Array.isArray(saved.snapshots)) {
this._stack = saved.snapshots.map(s => ({
tag: s.tag || 'restored',
timestamp: s.timestamp || Date.now(),
state: {
weights: s.state?.weights || [],
noiseLevel: s.state?.noiseLevel ?? 0,
zoomLevel: s.state?.zoomLevel ?? 1.0,
},
}));
// Trim to capacity
while (this._stack.length > this._max) {
this._stack.shift();
}
}
}
// ---- Internal ----
_dispatch(type, detail) {
document.dispatchEvent(new CustomEvent(type, { detail }));
}
}