Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,720 changes: 1,699 additions & 21 deletions ui/studio/package-lock.json

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions ui/studio/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
"start": "next start",
"test": "vitest run",
"test:watch": "vitest"
},
"dependencies": {
"@react-three/drei": "^9.96.0",
Expand All @@ -23,6 +25,7 @@
"autoprefixer": "^10.5.0",
"postcss": "^8.5.15",
"tailwindcss": "^3.4.0",
"typescript": "^5.4.0"
"typescript": "^5.4.0",
"vitest": "^1.6.0"
}
}
9 changes: 7 additions & 2 deletions ui/studio/src/app/ClientViews.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
"use client";

import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { emitIntentPulse } from '../lib/intentPulseStore';

export function IntentForm({ targetPath }: { targetPath: string }) {
const [name, setName] = useState('');
const [metric, setMetric] = useState('');

const router = useRouter();

const submit = async (e: any) => {
e.preventDefault();
await fetch('http://localhost:8000/intent', {
Expand All @@ -14,7 +17,9 @@ export function IntentForm({ targetPath }: { targetPath: string }) {
body: JSON.stringify({ target: targetPath, name, metric })
});
setName(''); setMetric('');
window.location.reload(); // Quick refresh for MVP
emitIntentPulse(name, metric, Date.now());
// Soft-refresh the server component so the Active Intent Vectors list reflects the new intent, without a full reload (keeps the 3D Canvas alive).
router.refresh();
};

return (
Expand Down
14 changes: 14 additions & 0 deletions ui/studio/src/components/MatrixScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,28 @@ import { useFrame } from '@react-three/fiber';
import * as THREE from 'three';
import { AlphaRig } from './AlphaRig';
import { TelemetryMonitor } from './TelemetryMonitor';
import { useIntentPulse } from '../lib/useIntentPulse';

export function MatrixScene() {
const coreRef = useRef<THREE.Mesh>(null);
const pulse = useIntentPulse();
const lastPulseId = useRef(0);
const kick = useRef(0); // decays 1->0, scales the core on each new intent

useFrame((state, delta) => {
if (pulse && pulse.id !== lastPulseId.current) {
lastPulseId.current = pulse.id;
kick.current = 1;
}
kick.current = Math.max(0, kick.current - delta * 1.5);
// The pop is animated; if the Canvas is ever switched to frameloop="demand",
// keep requesting frames while the kick decays (no-op under the default "always").
if (kick.current > 0) state.invalidate();
if (coreRef.current) {
coreRef.current.rotation.x += delta * 0.2;
coreRef.current.rotation.y += delta * 0.3;
const s = 1 + kick.current * 0.4; // visible pop on new intent
coreRef.current.scale.setScalar(s);
}
});

Expand Down
58 changes: 58 additions & 0 deletions ui/studio/src/lib/__tests__/intentPulseStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { describe, it, expect, beforeEach } from 'vitest';
import {
emitIntentPulse,
getIntentPulse,
subscribeIntentPulse,
__resetIntentPulseStore,
} from '../intentPulseStore';

describe('intentPulseStore', () => {
beforeEach(() => __resetIntentPulseStore());

it('starts empty', () => {
expect(getIntentPulse()).toBeNull();
});

it('emits a pulse with a monotonically increasing id', () => {
const a = emitIntentPulse('Cyberpunk Button', 'O(1)', 1000);
const b = emitIntentPulse('Neon Slider', 'Contrast 4.5:1', 2000);
expect(a.id).toBe(1);
expect(b.id).toBe(2);
expect(getIntentPulse()).toEqual(b);
expect(b).toEqual({ id: 2, name: 'Neon Slider', metric: 'Contrast 4.5:1', at: 2000 });
});

it('notifies subscribers on emit and stops after unsubscribe', () => {
let calls = 0;
const unsub = subscribeIntentPulse(() => { calls += 1; });
emitIntentPulse('A', 'm', 1);
emitIntentPulse('B', 'm', 2);
expect(calls).toBe(2);
unsub();
emitIntentPulse('C', 'm', 3);
expect(calls).toBe(2);
});

it('returns a stable reference between emits and a new one after emit', () => {
emitIntentPulse('A', 'm', 1);
const first = getIntentPulse();
expect(getIntentPulse()).toBe(first); // same ref between emits -> no tearing
emitIntentPulse('B', 'm', 2);
expect(getIntentPulse()).not.toBe(first); // new ref after emit -> re-render fires
});

it('supports multiple subscribers and unsubscribes them independently', () => {
let a = 0;
let b = 0;
const unsubA = subscribeIntentPulse(() => { a += 1; });
const unsubB = subscribeIntentPulse(() => { b += 1; });
emitIntentPulse('x', 'm', 1);
expect(a).toBe(1);
expect(b).toBe(1);
unsubA();
emitIntentPulse('y', 'm', 2);
expect(a).toBe(1); // A no longer notified
expect(b).toBe(2); // B still notified
unsubB();
});
});
42 changes: 42 additions & 0 deletions ui/studio/src/lib/intentPulseStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Framework-agnostic external store for "intent pulses". Pure and synchronous so it
// can be unit-tested in plain node, then bridged into React via useSyncExternalStore.
export type PulseListener = () => void;

export interface IntentPulse {
id: number;
name: string;
metric: string;
at: number; // epoch ms, supplied by caller (keeps the store deterministic/testable)
}

// Client-side singleton: the module is re-evaluated per server request, but the
// server snapshot is always null (emits only happen client-side), so per-request
// reset is harmless.
let current: IntentPulse | null = null;
let count = 0;
const listeners = new Set<PulseListener>();

export function emitIntentPulse(name: string, metric: string, at: number): IntentPulse {
count += 1;
current = { id: count, name, metric, at };
listeners.forEach((listener) => listener());
return current;
}

export function getIntentPulse(): IntentPulse | null {
return current;
}

export function subscribeIntentPulse(listener: PulseListener): () => void {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
}

// Test-only helper.
export function __resetIntentPulseStore(): void {
current = null;
count = 0;
listeners.clear();
}
12 changes: 12 additions & 0 deletions ui/studio/src/lib/useIntentPulse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
"use client";

import { useSyncExternalStore } from 'react';
import { getIntentPulse, subscribeIntentPulse, type IntentPulse } from './intentPulseStore';

export function useIntentPulse(): IntentPulse | null {
return useSyncExternalStore(
subscribeIntentPulse,
getIntentPulse,
() => null, // server snapshot
);
}
8 changes: 8 additions & 0 deletions ui/studio/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
environment: 'node',
include: ['src/lib/**/*.test.ts'],
},
});
Loading