From a160b722957740c45775e6ad0e8f566647ac4242 Mon Sep 17 00:00:00 2001 From: w1n5t0n Date: Wed, 29 Apr 2026 15:37:23 +0300 Subject: [PATCH] feat(playground): scaffold vite + solid + ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fresh playground/ now sits on: - Vite 5 + vite-plugin-solid + Solid.js 1.9 - TypeScript strict (jsx preserve, target ES2022) - index.html → main.tsx → App.tsx with a tiny pushState/popstate router - Design tokens (dark immersive palette, JetBrains Mono) at src/styles/tokens.css - COOP/COEP headers in vite.config.ts (server + preview) so SharedArrayBuffer is available for the C15 + AudioWorklet wiring stream 7 will land. - Debug-probe stub at src/debug/probe.ts: window.__nisps with placeholder methods + __ready=false marker. Stream 10 fills it in. Keeping the install point stable from day one means Playwright tests can rely on the global existing. Stores, pipelines, and primitives ride in the next commits. Stream 8 of the rewrite (meml-911). --- playground/.gitignore | 5 ++ playground/index.html | 13 ++++ playground/package.json | 23 ++++++ playground/src/App.module.css | 84 +++++++++++++++++++++ playground/src/App.tsx | 71 ++++++++++++++++++ playground/src/debug/probe.ts | 118 +++++++++++++++++++++++++++++ playground/src/main.tsx | 17 +++++ playground/src/styles/tokens.css | 123 +++++++++++++++++++++++++++++++ playground/src/types.d.ts | 8 ++ playground/tsconfig.json | 32 ++++++++ playground/vite.config.ts | 25 +++++++ 11 files changed, 519 insertions(+) create mode 100644 playground/.gitignore create mode 100644 playground/index.html create mode 100644 playground/package.json create mode 100644 playground/src/App.module.css create mode 100644 playground/src/App.tsx create mode 100644 playground/src/debug/probe.ts create mode 100644 playground/src/main.tsx create mode 100644 playground/src/styles/tokens.css create mode 100644 playground/src/types.d.ts create mode 100644 playground/tsconfig.json create mode 100644 playground/vite.config.ts diff --git a/playground/.gitignore b/playground/.gitignore new file mode 100644 index 0000000..dfa9518 --- /dev/null +++ b/playground/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +.vite +*.log +.DS_Store diff --git a/playground/index.html b/playground/index.html new file mode 100644 index 0000000..60b50c7 --- /dev/null +++ b/playground/index.html @@ -0,0 +1,13 @@ + + + + + + + MEMLNaut Playground + + +
+ + + diff --git a/playground/package.json b/playground/package.json new file mode 100644 index 0000000..df92492 --- /dev/null +++ b/playground/package.json @@ -0,0 +1,23 @@ +{ + "name": "memlnaut-playground", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview --port 4173", + "typecheck": "tsc --noEmit", + "test:e2e": "playwright test" + }, + "dependencies": { + "solid-js": "^1.8.22" + }, + "devDependencies": { + "@playwright/test": "^1.48.0", + "@types/node": "^22.7.0", + "typescript": "^5.5.4", + "vite": "^5.4.10", + "vite-plugin-solid": "^2.10.2" + } +} diff --git a/playground/src/App.module.css b/playground/src/App.module.css new file mode 100644 index 0000000..7659dd0 --- /dev/null +++ b/playground/src/App.module.css @@ -0,0 +1,84 @@ +.app { + display: flex; + flex-direction: column; + min-height: 100%; +} + +.header { + display: flex; + align-items: center; + gap: var(--sp-3); + padding: var(--sp-3) var(--sp-4); + border-bottom: 1px solid var(--line); + background: var(--bg-1); +} + +.dim { + color: var(--fg-mute); + font-size: var(--fs-sm); +} + +.nav { + margin-left: auto; + display: flex; + gap: var(--sp-2); +} + +.nav button { + padding: var(--sp-1) var(--sp-3); + font-size: var(--fs-sm); +} + +.active { + border-color: var(--accent) !important; + color: var(--accent); +} + +.main { + flex: 1; + padding: var(--sp-5); + overflow: auto; +} + +.home { + max-width: 720px; + margin: 0 auto; + display: flex; + flex-direction: column; + gap: var(--sp-4); +} + +.title { + font-size: var(--fs-xl); + margin: 0; + color: var(--accent); +} + +.tagline { + color: var(--fg-mute); + margin: 0; +} + +.linkList { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: var(--sp-2); +} + +.note { + color: var(--fg-dim); + font-size: var(--fs-sm); + margin-top: var(--sp-4); +} + +.notFound { + max-width: 540px; + margin: var(--sp-6) auto; +} + +.notFound h1 { + color: var(--accent); +} diff --git a/playground/src/App.tsx b/playground/src/App.tsx new file mode 100644 index 0000000..cb35056 --- /dev/null +++ b/playground/src/App.tsx @@ -0,0 +1,71 @@ +import { Component, createSignal, onCleanup, Show } from 'solid-js'; +import styles from './App.module.css'; + +type Route = 'home' | 'unknown'; + +function parseRoute(path: string): Route { + if (path === '' || path === '/' || path === '/index.html') return 'home'; + return 'unknown'; +} + +const App: Component = () => { + const [route, setRoute] = createSignal(parseRoute(window.location.pathname)); + + const onPop = () => setRoute(parseRoute(window.location.pathname)); + window.addEventListener('popstate', onPop); + onCleanup(() => window.removeEventListener('popstate', onPop)); + + const navigate = (path: string) => { + if (window.location.pathname === path) return; + window.history.pushState({}, '', path); + setRoute(parseRoute(path)); + }; + + return ( +
+
+ MEMLNaut + playground + +
+
+ + + + +
+

404

+

No route at {window.location.pathname}.

+

+ { e.preventDefault(); navigate('/'); }}>back to home +

+
+
+
+
+ ); +}; + +const Home: Component = () => { + return ( +
+

MEMLNaut Playground

+

+ Interactive ML control of audio. SolidJS scaffold — modes coming online in stream 9. +

+

+ This is a fresh scaffold. Primitives, stores, ML, WASM, and audio engines are added in subsequent commits. +

+
+ ); +}; + +export default App; diff --git a/playground/src/debug/probe.ts b/playground/src/debug/probe.ts new file mode 100644 index 0000000..f241afc --- /dev/null +++ b/playground/src/debug/probe.ts @@ -0,0 +1,118 @@ +/** + * Debug probe: window.__nisps + * + * Stream 8 (this stream) installs a stub that returns placeholder values. + * Stream 10 wires real ML calls. Keeping the install path stable here means + * Playwright tests can rely on `window.__nisps` existing from page load even + * before the ML engine boots. + * + * All methods MUST be synchronous (or return immediately-resolved promises). + * The probe deliberately bypasses Solid reactivity so tests get deterministic, + * imperative semantics. + */ + +export interface DebugProbe { + /** Current 126-element output vector (Float32Array). */ + getOutputs(): Float32Array; + /** Last training loss, or null if no training has occurred. */ + getLoss(): number | null; + /** Flat weight array (~13K floats once wired). */ + getWeights(): Float32Array; + /** Number of training examples currently in the dataset. */ + getExampleCount(): number; + /** Set joystick X/Y in [0,1] and run inference. */ + setInputs(x: number, y: number): void; + /** Trigger thumbs-up RL feedback (train + decay noise). */ + thumbsUp(): void; + /** Trigger thumbs-down RL feedback (move weights + grow noise). */ + thumbsDown(): void; + /** Synchronous training; returns final loss. */ + train(): number; + /** Async training; returns Promise. */ + trainAsync(): Promise; + /** Randomize weights with current spread. */ + randomise(): void; + /** Clear all training examples. */ + clearExamples(): void; + /** Force a save to localStorage now (no debounce). */ + saveState(): void; + /** Non-destructive loss query against current dataset. */ + evalLoss(): number | null; + /** Batch inference: input is Nx2 array of [x,y] pairs. Output: Float32Array of N*outputSize. */ + inferBatch(points: ReadonlyArray): Float32Array; + /** Per-layer weight statistics: Float32Array of layerCount * 4 (mean|w|, max|w|, dead%, sat%). */ + getLayerStats(): Float32Array; + /** Marker showing this is a stream-8 stub. Tests can read this to skip when not ready. */ + readonly __ready: boolean; +} + +declare global { + interface Window { + __nisps?: DebugProbe; + } +} + +const EMPTY_F32 = new Float32Array(0); + +const stubProbe: DebugProbe = { + getOutputs() { + return EMPTY_F32; + }, + getLoss() { + return null; + }, + getWeights() { + return EMPTY_F32; + }, + getExampleCount() { + return 0; + }, + setInputs(_x: number, _y: number) { + /* no-op until ML wired */ + }, + thumbsUp() { + /* no-op */ + }, + thumbsDown() { + /* no-op */ + }, + train() { + return 0; + }, + trainAsync() { + return Promise.resolve(0); + }, + randomise() { + /* no-op */ + }, + clearExamples() { + /* no-op */ + }, + saveState() { + /* no-op */ + }, + evalLoss() { + return null; + }, + inferBatch(points) { + // Return a zero array of the right size for at least the inputs. + return new Float32Array(points.length); + }, + getLayerStats() { + return EMPTY_F32; + }, + __ready: false, +}; + +/** + * Install the probe on window. Idempotent. + * + * Stream 10 will replace this with a fully-wired version. Until then the stub + * advertises `__ready === false`, letting tests skip ML-dependent assertions. + */ +export function installDebugProbe(): void { + if (typeof window === 'undefined') return; + // Always overwrite — later streams may replace it; the marker prevents stale + // probes from passing tests. + window.__nisps = stubProbe; +} diff --git a/playground/src/main.tsx b/playground/src/main.tsx new file mode 100644 index 0000000..3fdf670 --- /dev/null +++ b/playground/src/main.tsx @@ -0,0 +1,17 @@ +/* @refresh reload */ +import { render } from 'solid-js/web'; +import App from './App'; +import './styles/tokens.css'; +import { installDebugProbe } from './debug/probe'; + +const root = document.getElementById('root'); +if (!root) { + throw new Error('Root element #root not found'); +} + +// Install debug probe early. It is a stub for now; stream 10 fills it in +// once WASM ML is wired up. Keeping the install path stable from day one +// makes Playwright tests insensitive to ordering. +installDebugProbe(); + +render(() => , root); diff --git a/playground/src/styles/tokens.css b/playground/src/styles/tokens.css new file mode 100644 index 0000000..ccab3e0 --- /dev/null +++ b/playground/src/styles/tokens.css @@ -0,0 +1,123 @@ +/** + * Global design tokens for the MEMLNaut playground. + * Carries forward the immersive identity: dark canvas, monospace, warm + cool accents. + */ + +:root { + /* Colors */ + --bg: #0d0d0d; + --bg-1: #141414; + --bg-2: #1c1c1c; + --bg-3: #242424; + --fg: #e8e8e8; + --fg-mute: #9a9a9a; + --fg-dim: #5a5a5a; + --line: #2a2a2a; + --line-strong: #3a3a3a; + + --accent: #ff6a00; /* warm primary */ + --accent-2: #00ccff; /* cool secondary */ + --accent-3: #ffa860; /* hover warm */ + --good: #6bc26b; + --warn: #f5c45e; + --bad: #ef5b5b; + --info: #5b9eef; + + --pin-1: rgba(255, 106, 0, 0.25); + --pin-2: rgba(0, 204, 255, 0.25); + --pin-3: rgba(180, 100, 255, 0.25); + --pin-4: rgba(80, 200, 120, 0.25); + --pin-5: rgba(255, 200, 80, 0.25); + + /* Typography */ + --font-mono: 'JetBrains Mono', 'Fira Code', 'SF Mono', Menlo, Consolas, monospace; + --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; + + --fs-xs: 11px; + --fs-sm: 13px; + --fs-md: 15px; + --fs-lg: 18px; + --fs-xl: 24px; + + /* Spacing */ + --sp-0: 2px; + --sp-1: 4px; + --sp-2: 8px; + --sp-3: 12px; + --sp-4: 16px; + --sp-5: 24px; + --sp-6: 32px; + + /* Radius */ + --r-1: 4px; + --r-2: 8px; + --r-3: 14px; + --r-pill: 999px; + + /* Motion */ + --ease: cubic-bezier(.25, .8, .35, 1); + --dur-fast: 120ms; + --dur-med: 220ms; + --dur-slow: 360ms; + + /* Layers */ + --z-bg: 0; + --z-content: 10; + --z-overlay: 100; + --z-drawer: 200; + --z-modal: 1000; +} + +* { + box-sizing: border-box; +} + +html, body, #root { + margin: 0; + padding: 0; + height: 100%; + background: var(--bg); + color: var(--fg); + font-family: var(--font-mono); + font-size: var(--fs-md); + -webkit-tap-highlight-color: transparent; +} + +button { + font-family: inherit; + font-size: inherit; + background: var(--bg-2); + color: var(--fg); + border: 1px solid var(--line); + border-radius: var(--r-1); + padding: var(--sp-2) var(--sp-3); + cursor: pointer; + transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease); +} +button:hover:not(:disabled) { + background: var(--bg-3); + border-color: var(--line-strong); +} +button:disabled { + color: var(--fg-dim); + cursor: not-allowed; +} + +input, select { + font-family: inherit; + font-size: inherit; + color: var(--fg); +} + +a { + color: var(--accent-2); + text-decoration: none; +} +a:hover { + text-decoration: underline; +} + +::selection { + background: var(--accent); + color: var(--bg); +} diff --git a/playground/src/types.d.ts b/playground/src/types.d.ts new file mode 100644 index 0000000..cabcafe --- /dev/null +++ b/playground/src/types.d.ts @@ -0,0 +1,8 @@ +/// + +declare module '*.module.css' { + const classes: Record; + export default classes; +} + +declare module '*.css'; diff --git a/playground/tsconfig.json b/playground/tsconfig.json new file mode 100644 index 0000000..55233af --- /dev/null +++ b/playground/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "jsx": "preserve", + "jsxImportSource": "solid-js", + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "exactOptionalPropertyTypes": false, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "isolatedModules": true, + "useDefineForClassFields": true, + "verbatimModuleSyntax": false, + "forceConsistentCasingInFileNames": true, + "types": ["vite/client", "node"], + "baseUrl": ".", + "paths": { + "@/*": ["src/*"] + }, + "noEmit": true + }, + "include": ["src/**/*", "vite.config.ts"], + "exclude": ["node_modules", "dist", "tests/e2e"] +} diff --git a/playground/vite.config.ts b/playground/vite.config.ts new file mode 100644 index 0000000..1154fc9 --- /dev/null +++ b/playground/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite'; +import solid from 'vite-plugin-solid'; + +// COOP/COEP headers are required for SharedArrayBuffer (used by C15 + AudioWorklet). +// Set on both dev server and the production preview. +const crossOriginIsolationHeaders = { + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Cross-Origin-Embedder-Policy': 'require-corp', +}; + +export default defineConfig({ + plugins: [solid()], + server: { + port: 5173, + headers: crossOriginIsolationHeaders, + }, + preview: { + port: 4173, + headers: crossOriginIsolationHeaders, + }, + build: { + target: 'es2022', + sourcemap: true, + }, +});