- Data source: {modeLabel}. Switch modes to toggle between synthetic simulation, TRIBE v2 neural predictions, and live EEG input.
- Press ? for keyboard shortcuts.
-
diff --git a/brainsnn-r3f-app/src/components/CalendarHeatmapPanel.jsx b/brainsnn-r3f-app/src/components/CalendarHeatmapPanel.jsx
index 458f8d1..c5f5bcf 100644
--- a/brainsnn-r3f-app/src/components/CalendarHeatmapPanel.jsx
+++ b/brainsnn-r3f-app/src/components/CalendarHeatmapPanel.jsx
@@ -73,9 +73,9 @@ export default function CalendarHeatmapPanel() {
Less
-
+
-
+
More / higher-pressure
diff --git a/brainsnn-r3f-app/src/components/CapabilitiesPanel.jsx b/brainsnn-r3f-app/src/components/CapabilitiesPanel.jsx
new file mode 100644
index 0000000..eeabc11
--- /dev/null
+++ b/brainsnn-r3f-app/src/components/CapabilitiesPanel.jsx
@@ -0,0 +1,72 @@
+import React, { useEffect, useState } from 'react';
+import { capabilitySnapshot, hasWebGPU } from '../utils/capabilities';
+
+/**
+ * CapabilitiesPanel — surfaces what's available under the hood.
+ *
+ * Companion to Privacy Budget. Lets the user (and the user's IT
+ * sysadmin, in enterprise) see which Beast-mode features are active
+ * and which the current browser doesn't support.
+ */
+export default function CapabilitiesPanel() {
+ const [snap, setSnap] = useState(() => capabilitySnapshot());
+ const [webgpu, setWebgpu] = useState(null);
+
+ useEffect(() => {
+ hasWebGPU().then(setWebgpu);
+ }, []);
+
+ const refresh = () => setSnap(capabilitySnapshot());
+
+ const rows = [
+ { label: 'CPU cores', value: snap.cores, kind: 'info' },
+ { label: 'Web Worker', value: snap.worker },
+ { label: 'IndexedDB', value: snap.indexedDB },
+ { label: 'BroadcastChannel', value: snap.broadcastChannel, hint: 'multi-tab brain sync' },
+ { label: 'Web Locks', value: snap.webLocks, hint: 'atomic snapshot writes' },
+ { label: 'Background Sync', value: snap.backgroundSync, hint: 'offline write queue' },
+ { label: 'OffscreenCanvas', value: snap.offscreenCanvas, hint: 'unblocks renderer (future)' },
+ { label: 'File System Access', value: snap.fileSystemAccess, hint: 'skip download dialogs' },
+ { label: 'WebGPU', value: webgpu, hint: 'experimental renderer (future)' }
+ ];
+
+ return (
+
+
Layer 103 · Capabilities
+
What's available under the hood
+
+ Beast-mode features depend on browser support. This is what your current browser
+ actually offers. Refresh after switching browsers or updating to re-probe.
+
diff --git a/brainsnn-r3f-app/src/components/FeedbackPanel.jsx b/brainsnn-r3f-app/src/components/FeedbackPanel.jsx
index ccd1ace..afd9133 100644
--- a/brainsnn-r3f-app/src/components/FeedbackPanel.jsx
+++ b/brainsnn-r3f-app/src/components/FeedbackPanel.jsx
@@ -1,18 +1,23 @@
-import React, { useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import { calibrationReport, listFeedback, clearFeedback } from '../utils/feedback';
+import { subscribe as subscribeMultiTab } from '../utils/multiTab';
export default function FeedbackPanel() {
const [report, setReport] = useState(() => calibrationReport());
const [rows, setRows] = useState(() => listFeedback());
- useEffect(() => {
- const id = setInterval(() => {
- setReport(calibrationReport());
- setRows(listFeedback());
- }, 3000);
- return () => clearInterval(id);
+ const refresh = useCallback(() => {
+ setReport(calibrationReport());
+ setRows(listFeedback());
}, []);
+ // feedback.js publishes 'feedback:changed' on each recordFeedback /
+ // clearFeedback. Locked writes are async, so this subscription
+ // patches the UI right after the write commits. Replaces the
+ // previous 3s polling interval — live updates make polling
+ // redundant (saves a setInterval on every panel mount).
+ useEffect(() => subscribeMultiTab('feedback:changed', refresh), [refresh]);
+
function wipe() {
if (!window.confirm('Clear all calibration feedback?')) return;
clearFeedback();
@@ -20,7 +25,7 @@ export default function FeedbackPanel() {
setRows(listFeedback());
}
- const tone = report.suggestedMul > 1.05 ? '#fdab43' : report.suggestedMul < 0.95 ? '#77dbe4' : '#5ee69a';
+ const tone = report.suggestedMul > 1.05 ? 'var(--severity-mid)' : report.suggestedMul < 0.95 ? 'var(--severity-info)' : 'var(--severity-ok)';
return (
@@ -41,17 +46,17 @@ export default function FeedbackPanel() {
- too cold {report.tooCold}
- accurate {report.accurate}
- too hot {report.tooHot}
- Clear
+ too cold {report.tooCold}
+ accurate {report.accurate}
+ too hot {report.tooHot}
+ Clear
+
+ );
+}
diff --git a/brainsnn-r3f-app/src/shell/Composer.jsx b/brainsnn-r3f-app/src/shell/Composer.jsx
new file mode 100644
index 0000000..b97bcc3
--- /dev/null
+++ b/brainsnn-r3f-app/src/shell/Composer.jsx
@@ -0,0 +1,68 @@
+import React, { useState } from 'react';
+import { bus } from './bus';
+
+/**
+ * Composer — persistent top input. Paste text or drop a file; the active
+ * workspace decides how to consume the payload. Emits `shell:compose`
+ * with { text, mode } when the user submits; workspaces subscribe.
+ *
+ * Designed as a single-line ambient input — for the heavy stuff users
+ * still go into the relevant panel's own textarea.
+ *
+ * Takes no props so re-renders are entirely state-internal — memoize
+ * the export so AppShell's simulation-tick re-renders skip past it.
+ */
+function ComposerImpl() {
+ const [text, setText] = useState('');
+ const [mode, setMode] = useState('scan');
+
+ // Each mode routes to the workspace that knows how to handle it.
+ const MODE_WORKSPACE = {
+ scan: 'defend',
+ autopsy: 'defend',
+ diff: 'connect',
+ rag: 'knowledge'
+ };
+
+ const submit = () => {
+ const trimmed = text.trim();
+ if (!trimmed) return;
+ const ws = MODE_WORKSPACE[mode];
+ if (ws) bus.emit('shell:goto', { workspace: ws });
+ // requestAnimationFrame so the target workspace mounts before
+ // the compose event arrives at its subscribers.
+ requestAnimationFrame(() => bus.emit('shell:compose', { text: trimmed, mode }));
+ // Don't clear — let the user iterate.
+ };
+
+ const onKey = (e) => {
+ if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
+ e.preventDefault();
+ submit();
+ }
+ };
+
+ return (
+
+
+ setText(e.target.value)}
+ onKeyDown={onKey}
+ placeholder="Paste text to scan, route through autopsy, diff, or ask the brain… (⌘+Enter)"
+ />
+
+ Send
+
+
+
+ );
+}
+
+// Topbar's props change only when the user runs a scan (firewallResult),
+// switches workspace (label), or earns immunity — not every simulation
+// tick. React.memo gates out the cascade. onShowHelp is the only
+// inline lambda; if it gets de-stabilized later, memoize at the caller.
+export default React.memo(TopbarImpl);
diff --git a/brainsnn-r3f-app/src/shell/UpdateBanner.jsx b/brainsnn-r3f-app/src/shell/UpdateBanner.jsx
new file mode 100644
index 0000000..1ede2ca
--- /dev/null
+++ b/brainsnn-r3f-app/src/shell/UpdateBanner.jsx
@@ -0,0 +1,41 @@
+import React, { useEffect, useState } from 'react';
+import { startSwUpdateWatch, subscribeSwUpdate, activateNewSw } from '../utils/swUpdate';
+
+/**
+ * UpdateBanner — small chip in the bottom-right that appears when a
+ * new service worker is waiting. Click "Reload" to skipWaiting + boot
+ * against the new chunks. Dismissible until the next update.
+ */
+export default function UpdateBanner() {
+ const [waiting, setWaiting] = useState(false);
+ const [dismissed, setDismissed] = useState(false);
+
+ useEffect(() => {
+ startSwUpdateWatch();
+ return subscribeSwUpdate((state) => {
+ if (state.status === 'waiting') {
+ setWaiting(true);
+ setDismissed(false); // re-show after each new update
+ }
+ });
+ }, []);
+
+ if (!waiting || dismissed) return null;
+
+ return (
+
+ A new version is ready.
+ activateNewSw()}
+ >
+ Reload
+
+ setDismissed(true)}
+ aria-label="Dismiss"
+ >×
+
+ );
+}
diff --git a/brainsnn-r3f-app/src/shell/WorkspaceTabs.jsx b/brainsnn-r3f-app/src/shell/WorkspaceTabs.jsx
new file mode 100644
index 0000000..c55f2cd
--- /dev/null
+++ b/brainsnn-r3f-app/src/shell/WorkspaceTabs.jsx
@@ -0,0 +1,69 @@
+import React, { useRef } from 'react';
+
+export const WORKSPACES = [
+ { id: 'home', label: 'Home', chord: 'gh', glyph: '◉' },
+ { id: 'analyze', label: 'Analyze', chord: 'ga', glyph: '◯' },
+ { id: 'defend', label: 'Defend', chord: 'gd', glyph: '◇' },
+ { id: 'brain', label: 'Brain', chord: 'gb', glyph: '◐' },
+ { id: 'knowledge', label: 'Knowledge', chord: 'gk', glyph: '◑' },
+ { id: 'training', label: 'Training', chord: 'gt', glyph: '◒' },
+ { id: 'connect', label: 'Connect', chord: 'gc', glyph: '◓' }
+];
+
+function WorkspaceTabsImpl({ active, onChange }) {
+ const ref = useRef(null);
+ const activeIdx = WORKSPACES.findIndex((w) => w.id === active);
+
+ function onKeyDown(e) {
+ const idx = WORKSPACES.findIndex((w) => w.id === active);
+ if (idx < 0) return;
+ let next = idx;
+ if (e.key === 'ArrowDown' || e.key === 'ArrowRight') next = (idx + 1) % WORKSPACES.length;
+ else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') next = (idx - 1 + WORKSPACES.length) % WORKSPACES.length;
+ else if (e.key === 'Home') next = 0;
+ else if (e.key === 'End') next = WORKSPACES.length - 1;
+ else return;
+ e.preventDefault();
+ onChange(WORKSPACES[next].id);
+ // Move focus to the newly active tab so the user can keep arrowing.
+ requestAnimationFrame(() => {
+ const btns = ref.current?.querySelectorAll('[role="tab"]');
+ btns?.[next]?.focus();
+ });
+ }
+
+ return (
+
+ );
+}
+
+// Tabs depend only on { active, onChange }. AppShell re-renders every
+// 180ms (simulation tick) but neither of these change with it.
+export default React.memo(WorkspaceTabsImpl);
diff --git a/brainsnn-r3f-app/src/shell/bus.js b/brainsnn-r3f-app/src/shell/bus.js
new file mode 100644
index 0000000..445c0de
--- /dev/null
+++ b/brainsnn-r3f-app/src/shell/bus.js
@@ -0,0 +1,85 @@
+/**
+ * bus — tiny global event bus for the AppShell.
+ *
+ * Used by Workspace navigation (`shell:goto`), command palette / hotkeys
+ * (`shell:flash`), onboarding (`shell:goto` before highlighting), and
+ * any panel that needs to communicate cross-workspace without prop
+ * drilling.
+ *
+ * Intentionally ~50 LOC and dependency-free. Replaces the scattered
+ * window.dispatchEvent / addEventListener patterns the legacy code uses.
+ */
+
+const _listeners = new Map();
+
+// Sticky-event buffer. The composer emits `shell:compose` before the
+// destination workspace's lazy chunk has finished loading; without
+// this, the event is fired into the void and the panel never sees
+// it (silent drop). Sticky events are stored with a timestamp and
+// replayed to late subscribers if still inside the TTL window.
+const _sticky = new Map();
+const STICKY_EVENTS = {
+ 'shell:compose': 2500 // ms — covers a cold lazy-chunk fetch
+};
+
+export const bus = {
+ on(event, handler) {
+ if (!_listeners.has(event)) _listeners.set(event, new Set());
+ _listeners.get(event).add(handler);
+
+ // Late-subscriber replay for sticky events.
+ const ttl = STICKY_EVENTS[event];
+ if (ttl != null) {
+ const buffered = _sticky.get(event);
+ if (buffered && Date.now() - buffered.ts <= ttl) {
+ // Defer one microtask so the subscriber's component has
+ // finished mounting before its handler runs.
+ Promise.resolve().then(() => {
+ try { handler(buffered.payload); } catch (err) {
+ console.error(`[bus] sticky replay for "${event}" threw:`, err);
+ }
+ });
+ }
+ }
+
+ return () => bus.off(event, handler);
+ },
+
+ off(event, handler) {
+ const set = _listeners.get(event);
+ if (!set) return;
+ set.delete(handler);
+ if (set.size === 0) _listeners.delete(event);
+ },
+
+ emit(event, payload) {
+ // Stash sticky events so late subscribers (lazy workspaces) can
+ // pick them up after the cold chunk fetch resolves.
+ if (event in STICKY_EVENTS) {
+ _sticky.set(event, { payload, ts: Date.now() });
+ }
+ const set = _listeners.get(event);
+ if (!set) return;
+ // Snapshot so handlers can off() themselves without breaking iteration.
+ for (const fn of Array.from(set)) {
+ try { fn(payload); } catch (err) {
+ console.error(`[bus] handler for "${event}" threw:`, err);
+ }
+ }
+ },
+
+ /**
+ * Clear a sticky event so a stale value doesn't replay to a
+ * subscriber that mounts much later. Used by the composer after
+ * its event has been seen (or after a workspace switch).
+ */
+ clearSticky(event) {
+ _sticky.delete(event);
+ }
+};
+
+// React hook helper — auto-cleanup on unmount.
+import { useEffect } from 'react';
+export function useBusEvent(event, handler) {
+ useEffect(() => bus.on(event, handler), [event, handler]);
+}
diff --git a/brainsnn-r3f-app/src/shell/workspaces/AnalyzeWorkspace.jsx b/brainsnn-r3f-app/src/shell/workspaces/AnalyzeWorkspace.jsx
new file mode 100644
index 0000000..8f28d9f
--- /dev/null
+++ b/brainsnn-r3f-app/src/shell/workspaces/AnalyzeWorkspace.jsx
@@ -0,0 +1,36 @@
+import React, { Suspense, lazy } from 'react';
+import ErrorBoundary from '../../components/ErrorBoundary';
+import AnalyticsDashboard from '../../components/AnalyticsDashboard';
+import NarrativePanel from '../../components/NarrativePanel';
+import ActivityCharts from '../../components/ActivityCharts';
+
+const TimeSeriesPanel = lazy(() => import('../../components/TimeSeriesPanel'));
+const CalendarHeatmapPanel = lazy(() => import('../../components/CalendarHeatmapPanel'));
+const HeatmapTimeline = lazy(() => import('../../components/HeatmapTimeline'));
+const ComparatorPanel = lazy(() => import('../../components/ComparatorPanel'));
+const DrillDownPanel = lazy(() => import('../../components/DrillDownPanel'));
+
+export default function AnalyzeWorkspace({ session }) {
+ const { state, trends, firewallResult } = session;
+ return (
+
+
+
Analyze
+
Read the brain.
+
Live signals, trends, anomalies, and how today's activity stacks up.
+ );
+}
diff --git a/brainsnn-r3f-app/src/shell/workspaces/HomeWorkspace.jsx b/brainsnn-r3f-app/src/shell/workspaces/HomeWorkspace.jsx
new file mode 100644
index 0000000..8a340a4
--- /dev/null
+++ b/brainsnn-r3f-app/src/shell/workspaces/HomeWorkspace.jsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import ControlsBar from '../../components/ControlsBar';
+import DemoTiles from '../../components/DemoTiles';
+
+/**
+ * Home — landing workspace. Run controls + demo tiles for first-time
+ * users. The brain itself lives in BrainViewport which is rendered
+ * by AppShell and visible from every workspace.
+ */
+export default function HomeWorkspace({ session }) {
+ const { state, mode, quality, isRecording, exportStatus, gifOptions, onControls, onDemo } = session;
+
+ return (
+
+
+
Welcome back
+
The brain is listening.
+
+ Drop a tweet, an email, a transcript, or a screenshot. Watch the brain react,
+ firewall score the manipulation pressure, and 100+ cognitive layers light up.
+ Pick a workspace on the left to go deeper.
+