From a6798c2fca05e4d09d58b71edf4cbf2abfead9a8 Mon Sep 17 00:00:00 2001 From: Sam Valladares Date: Sun, 21 Jun 2026 20:23:26 -0500 Subject: [PATCH] feat(dashboard): wire Memory Cinema UI into graph page - MemoryCinema.svelte: launch button + fullscreen overlay, typewriter caption stream, SpeechSynthesis voice toggle, opt-in lazy-loaded Local AI toggle, progress/beat indicators, replay. Director-driven master loop hardened so a WebGPU render failure drops to camera-only without stalling the tour. - sandbox: construct Scene/Camera from the three/webgpu module instance so all objects fed to the WebGPU renderer are instance-compatible (fixes 'multiple instances of Three.js' incompatibility). - graph page: Cinema button beside Dream, gated on having nodes. Verified live: button renders, overlay opens, WebGPU boots + reports active, 7-beat path plans, narration resolves to live captions, bundle code-splits (WebGPU 479K chunk loads on demand only). 926 tests + build green. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../src/lib/components/MemoryCinema.svelte | 422 ++++++++++++++++++ .../dashboard/src/lib/graph/cinema/sandbox.ts | 31 +- .../src/routes/(app)/graph/+page.svelte | 10 + 3 files changed, 452 insertions(+), 11 deletions(-) create mode 100644 apps/dashboard/src/lib/components/MemoryCinema.svelte diff --git a/apps/dashboard/src/lib/components/MemoryCinema.svelte b/apps/dashboard/src/lib/components/MemoryCinema.svelte new file mode 100644 index 0000000..47b6f5d --- /dev/null +++ b/apps/dashboard/src/lib/components/MemoryCinema.svelte @@ -0,0 +1,422 @@ + + + + +{#if open} + +{/if} + + diff --git a/apps/dashboard/src/lib/graph/cinema/sandbox.ts b/apps/dashboard/src/lib/graph/cinema/sandbox.ts index 2243898..3ede9f6 100644 --- a/apps/dashboard/src/lib/graph/cinema/sandbox.ts +++ b/apps/dashboard/src/lib/graph/cinema/sandbox.ts @@ -37,8 +37,13 @@ export class CinemaSandbox { private container: HTMLElement; private deps!: SandboxDeps; private renderer!: SandboxDeps['WebGPURenderer']['prototype']; - private scene = new THREE.Scene(); - private camera: THREE.PerspectiveCamera; + // Scene/camera are created in boot() from the three/webgpu module so every + // object handed to the WebGPU renderer comes from the SAME Three.js instance + // (avoids the "multiple instances of Three.js" incompatibility — the base + // three import is used only for the shared Vector3 math type the director + // mutates, which is identical across instances). + private scene!: THREE.Scene; + private camera!: THREE.PerspectiveCamera; private storm!: SemanticComputeStorm; private post: { renderAsync: () => Promise } | null = null; private booted = false; @@ -48,14 +53,6 @@ export class CinemaSandbox { constructor(container: HTMLElement) { this.container = container; - this.camera = new THREE.PerspectiveCamera( - 60, - container.clientWidth / Math.max(1, container.clientHeight), - 0.1, - 2000 - ); - this.camera.position.set(0, 18, 60); - this.scene.background = new THREE.Color(0x02020a); } get cameraRef(): THREE.PerspectiveCamera { @@ -74,6 +71,9 @@ export class CinemaSandbox { const webgpu = (await import('three/webgpu')) as unknown as { WebGPURenderer: SandboxDeps['WebGPURenderer']; PostProcessing: SandboxDeps['PostProcessing']; + Scene: new () => THREE.Scene; + PerspectiveCamera: new (fov: number, aspect: number, near: number, far: number) => THREE.PerspectiveCamera; + Color: new (hex: number) => THREE.Color; }; const tsl = (await import('three/tsl')) as typeof import('three/tsl'); // bloom() lives in the TSL display helpers; import the node module. @@ -90,9 +90,18 @@ export class CinemaSandbox { bloomMod, }; + // Build scene + camera from the SAME (webgpu) module instance the + // renderer + storm use, so all objects are instance-compatible. + const w = Math.max(1, this.container.clientWidth); + const h = Math.max(1, this.container.clientHeight); + this.scene = new webgpu.Scene(); + this.scene.background = new webgpu.Color(0x02020a); + this.camera = new webgpu.PerspectiveCamera(60, w / h, 0.1, 2000); + this.camera.position.set(0, 18, 60); + const renderer = new this.deps.WebGPURenderer({ antialias: true, alpha: false }); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); - renderer.setSize(this.container.clientWidth, this.container.clientHeight); + renderer.setSize(w, h); // CRITICAL FOOTGUN: WebGPU init is async. Must await before first render // or the canvas silently draws nothing. await renderer.init(); diff --git a/apps/dashboard/src/routes/(app)/graph/+page.svelte b/apps/dashboard/src/routes/(app)/graph/+page.svelte index a06dbe7..57c8d8f 100644 --- a/apps/dashboard/src/routes/(app)/graph/+page.svelte +++ b/apps/dashboard/src/routes/(app)/graph/+page.svelte @@ -5,6 +5,7 @@ import RetentionCurve from '$components/RetentionCurve.svelte'; import TimeSlider from '$components/TimeSlider.svelte'; import MemoryStateLegend from '$components/MemoryStateLegend.svelte'; + import MemoryCinema from '$components/MemoryCinema.svelte'; import { api } from '$stores/api'; import { eventFeed } from '$stores/websocket'; import { graphState } from '$stores/graph-state.svelte'; @@ -361,6 +362,15 @@ disown {isDreaming ? '◈ Dreaming...' : '◈ Dream'} + + {#if displayNodes.length > 0} + + {/if} +