UI guide
The interface has two halves: a developer-facing settings panel (lil-gui)
and a product-facing dashboard (src/brain-ui.ts).
Settings panel (lil-gui)
src/gui.ts mounts a lil-gui panel inside the #guiHost card (falling back to a
floating panel if the host is missing). It has two folders.
Info: live, read-only counts, polled via .listen():
- Neurons
- Axons
- Signals
Settings: every control routes through Brain.updateSettings():
| Control | Range | Effect |
|---|---|---|
| Max Signals | 0..limitSignals | Live active-signal cap (currentMaxSignals). |
| Signal Size | 0.2..2 | Particle point size. |
| Signal Min Speed | 0..8 | Lower bound of signal speed. |
| Signal Max Speed | 0..8 | Upper bound (clamped to ≥ min). |
| Neuron Size Mult | 0..2 | Neuron point-size multiplier. |
| Neuron Opacity | 0..1 | Neuron shader opacity. |
| Axon Opacity Mult | 0..5 | Axon line opacity multiplier. |
| Signal / Neuron / Axon Color | color | Recolors the respective elements. |
| Background | color | Stage background (sceneSettings.bgColor). |
| Floor Platform | toggle | Show/hide the floor. |
The "Signal Max Speed" control guards min <= max: if you drag max below min,
it snaps max back up to min.
Dashboard (brain-ui.ts)
The product UI ports the original app's panels.
Mode tabs
Two modes (.mode-tab): a Virality Study mode and an EXEPERT Brain
mode. Switching to the brain mode sets the stage label to "EXEPERT Human Brain
Online". (The brain mode is keyed internally as 'meta' in the UI state: a
residual name from before the rebrand.)
Stimulus upload and Analyze
You can upload an image or video as stimulus. The UI previews it, reports its
size ("… stimulus loaded. Analyze injects a controlled pulse into the live brain
simulation."), and the Analyze button injects a pulse. Under the hood this
calls Brain.injectStimulus, which releases a burst of signals scaled by
intensity: see Regions & Stimulus.
Telemetry readouts
Once a stimulus is loaded, the UI polls telemetry every 100 ms:
telemetryTimer = setInterval(updateFromTelemetry, 100)
It maps getTelemetrySnapshot() into the cortical response index (x/100),
per-region bars, and the rolling activity windows.
The activity bars normalize against the configured signal cap
(settings.limitSignals, default 10000 in the UI fallback), so they read as a
percentage of capacity rather than an absolute count:
const cap = (neuralNet.settings && neuralNet.settings.limitSignals) || 10000
const v = Math.max(0, value / cap) * 100
If typical activity is a few hundred signals, the bars will sit low on this scale: that is expected given the normalization.
Brain-assistant chat
The Chat tab swaps the left region's study content for an inline chat tile
(#leftChat) that streams real responses through the Supabase run-chat Edge
Function and 9Router. The browser keeps the visible transcript in
localStorage, and thumbs feedback on saved assistant messages calls
chat-feedback.
The header holds the custom model picker, New chat button, and close button.
The picker loads through GET /functions/v1/run-chat?models=1 and renders as an
app-owned dark popover instead of a native browser select. It shows the provider
icon, readable model label, raw model id, provider groups, search filtering,
selected/active row states, and variant badges such as review, mini, high,
low, none, spark, and xHigh.
The picker supports click, outside-click dismissal, Escape, ArrowUp/ArrowDown,
Enter, and search filtering. It closes and disables while a response is
streaming, preserving the selected model under
exepert.brainChat.model.v1. If a model route fails because 9Router reports it
as unavailable or deprecated, the chat renders a compact .chat-error-card,
offers retry/default-model/model-picker actions, and marks that model as
unavailable in the picker for the current browser session. The default-model
action switches to cx/gpt-5.5 and restores the prompt without auto-sending, so
the user stays in control of the next request.
Tile structure
The inline chat tile (#leftChat) is a flex-direction: column container whose
five regions are direct siblings: they must not be nested inside one
another:
<div class="left-chat" id="leftChat" role="dialog" aria-label="Neural Brain chat" aria-hidden="true">
<div class="brain-chat-header">searchable model menu / new chat / close</div>
<div class="chat-context">Brain Mode / Response / Signal / Model pills</div>
<div class="brain-chat-messages" id="brainMessages">…</div>
<div class="quick-prompts">…seed buttons…</div>
<div class="brain-chat-input">…input · send…</div>
</div>
The column relies on .brain-chat-messages { flex: 1 } to absorb free space, so
the header pins to the top and .brain-chat-input pins to the bottom.
.brain-chat-header is a horizontal row (display: flex; align-items: center).
If its closing </div> is missing: or the opening tag is accidentally
duplicated: the context pills, messages, quick-prompts, and input get absorbed
into the header and collapse into that narrow centered row, breaking the layout.
The sibling structure above is the contract.
Observability activity page
Trace Ingestion no longer lives below the chat tile. The observability panel
(#obs-panel, built in src/ui/observability.ts) mounts into
#observabilitySlot inside the right-panel data-view="observability"
workbench view. The existing activity-bar Observability button switches
workspace.dataset.workbenchView to observability, hides the dashboard
right-panel view, hides the center brain stage, and shows the wider Trace
Ingestion page.
src/ui/observability.ts still falls back to the old #obsSlot only for older
shells or alternate boot pages. The collapse chevron remains available for small
screens, but exepert.obsCollapsed.v2 defaults the activity-page panel to
expanded because it no longer competes with chat for space.