diff --git a/dashboard/api-server.cjs b/dashboard/api-server.cjs index 209fb9fd..6ff9f4a9 100644 --- a/dashboard/api-server.cjs +++ b/dashboard/api-server.cjs @@ -41,6 +41,33 @@ const ALLOWED_ORIGINS = [ "http://localhost:18789", // Gateway (Swift wrapper loads dashboard here) "http://127.0.0.1:18789", // Gateway (alternate) ]; + +// Argent Lite: on a Pi the operator may access via hostname or LAN IP. +// Dynamically add the system hostname and all non-loopback IPs so CORS +// doesn't block saves from the same machine via a different address. +try { + const os = require("os"); + const hostname = os.hostname(); + const ifaces = os.networkInterfaces(); + const extraHosts = new Set([hostname]); + for (const name of Object.keys(ifaces)) { + for (const iface of ifaces[name] || []) { + if (!iface.internal && iface.family === "IPv4") { + extraHosts.add(iface.address); + } + } + } + for (const host of extraHosts) { + for (const port of [8080, 5173]) { + const origin = `http://${host}:${port}`; + if (!ALLOWED_ORIGINS.includes(origin)) { + ALLOWED_ORIGINS.push(origin); + } + } + } +} catch { + /* best-effort — loopback origins always work */ +} app.use( cors({ origin: (origin, callback) => { diff --git a/dashboard/docs/pi-profile.md b/dashboard/docs/pi-profile.md new file mode 100644 index 00000000..bdc618bf --- /dev/null +++ b/dashboard/docs/pi-profile.md @@ -0,0 +1,68 @@ +# Pi Profile — low-intensity AEVP mode + +The ArgentOS dashboard's AEVP (Argent Emotional Visualization Presence) +renders the orb and particles via WebGL at the browser's native refresh +rate. On a Raspberry Pi 5 with software rendering (or a Hailo-free +display), the 180-particle default + 60+ fps loop can drive the CPU +above 70% and saturate the GPU queue. + +**Pi Profile** is an opt-in rendering mode that reduces the per-frame +cost without changing any identity preset or personality modulation. +The orb still breathes, still colour-shifts, still emits particles on +tool events — it just costs ~60% less to draw. + +## What it changes + +| Lever | Default | Pi profile | Source of truth | +|---|---|---|---| +| Particle cap (runtime) | up to `180` | `60` | `aevp/colorMapping.ts` via `getMaxParticlesCap()` | +| Density scale | `1.0 ×` | `0.5 ×` | `aevp/colorMapping.ts` via `getDensityScale()` | +| Render tick interval | `0 ms` (rAF) | `33 ms` (~30 fps) | `aevp/renderer.ts` via `getFrameIntervalMs()` | + +The hard allocation (`MAX_PARTICLES = 180` in `aevp/particles.ts`) is +unchanged — the buffer still holds room for 180 — so the profile can +be flipped on and off without a reload or re-alloc. + +## How to enable + +Any one of these signals activates the profile. The check runs once +at module load. + +| Method | Where | When to use | +|---|---|---| +| `PI_PROFILE=1` env var | Node / SSR | Build-time or Electron wrappers | +| `VITE_PI_PROFILE=1` env var | Vite dev/build | `.env.local` in dashboard dev | +| `localStorage.setItem("argent.piProfile", "1")` | Browser console | Runtime toggle, survives reload | +| `?piProfile=1` URL param | Any | One-shot test without touching storage | + +To disable, unset / remove / set to `"0"`, and reload. + +## How to verify + +1. Open the dashboard with the profile active. +2. Open dev tools → Performance → record ~5 seconds. +3. Expect: ~30 fps main thread, GPU frame time below 8 ms, particle + count ≤60 in the `drawArrays(POINTS, ...)` call in `renderer.ts`. +4. Compare against a baseline tab without the profile. + +## What it does NOT change + +- Identity presets in `identityPresets.ts` — still emitted as-is. +- Personality modulation (warmth, energy, openness, formality) — still + applied in full. +- Morning particles and evening fireflies (separate components) — mount + them conditionally in app code if those also need to drop on Pi. +- Orb bloom / glow post-processing — future cut, currently always on. + +## Future cuts + +- Gate bloom FBO pass behind the profile (skip `bloomFBO` render). +- Lower the shader precision on the particle fragment shader to + `mediump` when the profile is active. +- Expose an in-dashboard toggle under a "Performance" tab. + +## References + +- `dashboard/src/aevp/pi-profile.ts` — single source of truth. +- `dashboard/src/aevp/colorMapping.ts:448` — cap + scale call sites. +- `dashboard/src/aevp/renderer.ts:269` — frame gate. diff --git a/dashboard/src/aevp/colorMapping.ts b/dashboard/src/aevp/colorMapping.ts index 4b449029..0198760c 100644 --- a/dashboard/src/aevp/colorMapping.ts +++ b/dashboard/src/aevp/colorMapping.ts @@ -10,6 +10,7 @@ import type { EmotionalState, ActivityStateName } from "../types/agentState"; import type { AgentVisualIdentity, AEVPRenderState } from "./types"; import { classifyTool, getResonanceTargets } from "./toolCategories"; +import { getMaxParticlesCap, getDensityScale } from "./pi-profile"; // ── Helpers ──────────────────────────────────────────────────────────────── @@ -445,7 +446,13 @@ export function computeRenderState( 2.5, ); - const maxParticles = Math.round(100 * presence.particleDensity); + // Pi profile: scale density + apply a hard cap on top of identity preset. + const piCap = getMaxParticlesCap(); + const piScale = getDensityScale(); + const maxParticles = Math.min( + piCap, + Math.round(100 * presence.particleDensity * piScale), + ); let particleCount = Math.round( clamp( maxParticles * (0.3 + moodVis.brightness * 0.4 + arousal * 0.3) * energyMul, diff --git a/dashboard/src/aevp/pi-profile.ts b/dashboard/src/aevp/pi-profile.ts new file mode 100644 index 00000000..71103979 --- /dev/null +++ b/dashboard/src/aevp/pi-profile.ts @@ -0,0 +1,95 @@ +/** + * Pi Profile — low-intensity rendering mode for the Raspberry Pi or any + * underpowered host running the ArgentOS dashboard. + * + * Activation: set any ONE of + * 1. env var PI_PROFILE=1 (SSR / build time via Vite) + * 2. env var VITE_PI_PROFILE=1 (Vite convention) + * 3. localStorage "argent.piProfile" = "1" (runtime toggle, no rebuild) + * 4. URL param ?piProfile=1 (one-shot try) + * + * Effect: the three AEVP perf levers all get tightened: + * - MAX particle count cap 180 → 60 (colorMapping runtime) + * - Runtime density multiplier 1.0 → 0.5 (colorMapping runtime) + * - Render tick frame interval 0 → 33ms (renderer ~30fps) + * + * This keeps identity presets and personality modulation intact — the + * character of the orb is preserved, only the per-frame cost drops. + * Expected reduction: ~65% fewer GPU particle updates, ~50% fewer draw + * calls on a display running at 60+ Hz. + */ + +function envFlag(name: string): boolean { + try { + // Node / SSR + const p = (globalThis as { process?: { env?: Record } }).process; + if (p?.env?.[name] && p.env[name] !== "0" && p.env[name] !== "") return true; + } catch { + /* ignore */ + } + try { + // Vite (import.meta.env is statically available only inside Vite-processed + // modules; protect with a typeof check so this file compiles under raw tsc) + const meta = (globalThis as { __ARGENT_VITE_ENV__?: Record }) + .__ARGENT_VITE_ENV__; + if (meta?.[name] && meta[name] !== "0" && meta[name] !== "") return true; + } catch { + /* ignore */ + } + return false; +} + +function localStorageFlag(key: string): boolean { + try { + const ls = (globalThis as { localStorage?: { getItem(k: string): string | null } }).localStorage; + const v = ls?.getItem(key); + return v === "1" || v === "true"; + } catch { + return false; + } +} + +function urlParamFlag(key: string): boolean { + try { + const loc = (globalThis as { location?: { search?: string } }).location; + if (!loc?.search) return false; + const params = new URLSearchParams(loc.search); + const v = params.get(key); + return v === "1" || v === "true"; + } catch { + return false; + } +} + +/** True if any Pi-profile signal is active. Computed once at module load. */ +export const PI_PROFILE_ACTIVE: boolean = + envFlag("PI_PROFILE") || + envFlag("VITE_PI_PROFILE") || + localStorageFlag("argent.piProfile") || + urlParamFlag("piProfile"); + +/** + * Hard runtime cap on active particle count. Colour mapping applies this + * in addition to identity preset density so low-end hardware gets a + * predictable ceiling regardless of mood. + */ +export function getMaxParticlesCap(): number { + return PI_PROFILE_ACTIVE ? 60 : 180; +} + +/** + * Multiplicative scale applied to presence.particleDensity at runtime. + * Identity presets still express themselves (relative density is preserved), + * but the absolute count lands lower. + */ +export function getDensityScale(): number { + return PI_PROFILE_ACTIVE ? 0.5 : 1.0; +} + +/** + * Minimum milliseconds between tick renders. Zero means no gate (use + * requestAnimationFrame cadence). 33 ≈ 30 fps, 50 = 20 fps. + */ +export function getFrameIntervalMs(): number { + return PI_PROFILE_ACTIVE ? 33 : 0; +} diff --git a/dashboard/src/aevp/renderer.ts b/dashboard/src/aevp/renderer.ts index cb00bf77..1c96c04c 100644 --- a/dashboard/src/aevp/renderer.ts +++ b/dashboard/src/aevp/renderer.ts @@ -6,6 +6,7 @@ */ import type { AEVPRenderState } from "./types"; +import { getFrameIntervalMs } from "./pi-profile"; import { ParticleSystem, type ParticleFormationRequest, @@ -272,6 +273,12 @@ export class AEVPRenderer { if (this.contextLost) return; + // Pi profile: throttle ticks to >= frameIntervalMs between renders. + const frameInterval = getFrameIntervalMs(); + if (frameInterval > 0 && now - this.lastFrameTime < frameInterval) { + return; + } + const dt = Math.min((now - this.lastFrameTime) / 1000, 0.1); // Cap at 100ms this.lastFrameTime = now; diff --git a/dashboard/src/components/SetupWizard.tsx b/dashboard/src/components/SetupWizard.tsx index ce5c9db9..8c4ba6dc 100644 --- a/dashboard/src/components/SetupWizard.tsx +++ b/dashboard/src/components/SetupWizard.tsx @@ -357,13 +357,12 @@ export function SetupWizard({ isOpen, onComplete }: SetupWizardProps) { className={`w-5 h-5 ${authType === "setup-token" ? "text-amber-400" : "text-white/40"}`} />
-
Claude Setup Token
+
Claude API Key
- From Anthropic Max subscription. Run{" "} + From{" "} - claude setup-token - {" "} - in terminal. + console.anthropic.com/settings/keys +
@@ -488,7 +487,7 @@ export function SetupWizard({ isOpen, onComplete }: SetupWizardProps) {