Skip to main content
Version: Next

UI guide

The interface has two halves: a developer-facing settings panel (lil-gui) and a product-facing dashboard (src/brain-ui.ts).

For the visual language behind these surfaces: tokens, palette, typography, and radii: see the Design system.

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():

ControlRangeEffect
Max Signals0..limitSignalsLive active-signal cap (currentMaxSignals).
Signal Size0.2..2Particle point size.
Signal Min Speed0..8Lower bound of signal speed.
Signal Max Speed0..8Upper bound (clamped to ≥ min).
Neuron Size Mult0..2Neuron point-size multiplier.
Neuron Opacity0..1Neuron shader opacity.
Axon Opacity Mult0..5Axon line opacity multiplier.
Signal / Neuron / Axon ColorcolorRecolors the respective elements.
BackgroundcolorStage background (sceneSettings.bgColor).
Floor PlatformtoggleShow/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, arranged as a bento layout (see Bento layout below).

Mode tabs

Three tabs (.mode-tab) sit as a persistent header at the top of the left region, above the scrolling content:

  • Virality Study and EXEPERT Brain are the two simulation modes. They call applyMode(...) and own the active highlight. 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.)
  • Chat is an action, not a mode: it carries data-action="chat" (no data-mode) and opens the inline chat (see Brain-assistant chat). bindModes() branches on data-action === 'chat'; the three tabs stay mutually exclusive: selecting a mode tab closes the chat, and selecting Chat clears the mode highlight.

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:

src/brain-ui.ts
telemetryTimer = setInterval(updateFromTelemetry, 100)

It maps getTelemetrySnapshot() into the cortical response index (x/100), per-region bars, and the rolling activity windows.

Activity-bar scaling

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:

src/brain-ui.ts
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 (.study-content) for an inline chat tile (#leftChat) that answers questions about the readout: it is not a separate full-screen overlay. openChat() toggles .chat-active on the left panel and .active on #leftChat; the close button and Escape both route through applyMode(state.mode), which restores the previously active mode and its tab highlight.

Responses stream from the Supabase run-chat Edge Function, which routes to 9Router with server-only credentials and saves each turn as a trace/span. The browser keeps the visible transcript in localStorage; thumbs feedback on a saved assistant message calls chat-feedback and writes a user_feedback annotation. Quick-prompt buttons seed common questions.

The header holds the model picker, New chat button, and close button. The picker is loaded through GET /functions/v1/run-chat?models=1 and renders as an app-owned dark popover instead of a native browser select. The trigger shows the provider icon, readable model label, and raw model id. Opening it reveals a search box, provider groups, 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 same selected-model persistence key: exepert.brainChat.model.v1. The "Brain Mode" context pill describes the simulation mode; the separate "Model" pill and assistant message footer show which AI provider/model handled the response. If 9Router reports the selected model as unavailable or deprecated, 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.

The Affective Field below the context pills is a synthetic research layer for emotion-reactive canvas behavior. User text is classified locally before the chat request, assistant output is classified when streaming completes, and chat route failures become assistant_error affect events when the failed span is available. The readout shows dominant affect, intensity, valence, arousal, and toxicity; message chips show the dominant affect per turn; the brain canvas pulses with the corresponding emotion color. These are conversational tone signals for EXEPERT research, not clinical or literal emotion detection.

Tile structure

#leftChat is a flex-direction: column tile whose five regions are direct siblings: they must not be nested inside one another:

index.html
<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">messages</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 the free space, so the header pins to the top and .brain-chat-input pins to the bottom.

Keep the header self-closed

.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.

Bento layout

The workspace (.workspace) is a three-column bento grid: a left region, a dominant center brain-canvas cell, and a right metrics stack, with every section sharing a flat tile chrome (.bento-tile: 1px --line border, 2px radius, --panel background). The grid collapses to fewer columns at the 1240 / 960 / 520px breakpoints. See the Design system for the tokens.

The left region stacks the persistent mode tabs and one active content tile: study content or chat. Trace Ingestion no longer lives below the chat tile, so the chat area keeps the full vertical space in Chat mode.

Observability activity page

Current implementation: 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 the new storage key exepert.obsCollapsed.v2 defaults the activity-page panel to expanded because it no longer competes with chat for space.

The Observability page header is static HTML in index.html; the live panel body remains generated by mountObservabilityPanel(), preserving the project switcher, Supabase configured status, import drop zone, sample loader, trace viewer, live toggle, and recent activity summary.

Historical note: before the Observability activity page, the panel was mounted as a collapsible left-region tile:

Historically, the observability panel (#obs-panel, built in src/ui/observability.ts) mounts into the static #obsSlot in the left region: falling back to document.body only if the slot is absent (other entry pages / boot order). It flows in document order as an in-flow bento tile rather than a floating position: fixed overlay, so it no longer overlaps the panel content.

A chevron toggle in the panel header (#obsCollapse) collapses the tile down to just its header, hiding .obs-body so the chat tile reclaims the vertical space (the slot is flex: 0 0 auto while #leftChat is flex: 1). It defaults to collapsed and the choice is persisted in localStorage under exepert.obsCollapsed (wrapped in try/catch for private-mode safety).

Cost & token summary

Below the ingestion controls the panel renders a compact cost / health summary for the current project: total cost, tokens, traces, error rate, and p50 / p95 latency. It refreshes on mount and after each import, and hides itself when Supabase is unconfigured or the project is empty.

The figures come from a pure cost model in src/data/cost.ts. A price book (generative_models + token_prices, seeded with an OpenAI/Anthropic manifest) matches each span's llm_model to a per-token rate via an anchored, case-insensitive regex: match_priority breaks ties so specific patterns beat the catch-all, and a leading provider/ prefix (e.g. openai/gpt-4o-mini) is stripped before matching. computeSpanCost multiplies prompt/completion tokens by their rates; fillSpanCost backfills spans.cost_usd on import only when a trace didn't already report a cost (it never overwrites). rollupCost aggregates a window of spans + traces into the summary.

Recent-activity window

The summary is computed over the most-recent 1000 spans and 1000 traces (two independent bounded windows), so it is labeled "Recent activity (last 1000)" and a value shows a trailing + when its window hits the cap. It is a recent-activity readout, not a project-lifetime total; a server-side aggregate can replace it later.

FPS panel

The Stats.js FPS panel is relocated into the top nav, just left of the search box. src/scene.ts appends stats.dom to #navStats (falling back to #canvas-container) and neutralizes the library's inline position: fixed; top: 0; left: 0 so it flows inline; .nav-stats scales the 80×48 panel down to fit the 38px nav row.

When the Workbench Settings view is open, it is a fixed app-level overlay above the top nav. The app shell also hides #navStats for that state so the FPS canvas cannot bleed through the Settings panel.

Observability panel (Plans 007 / 008)

src/ui/observability.ts builds the observability panel that replaces the trace-ingestion placeholder with a full troubleshooting surface. It mounts the following features together in one panel:

Auth gate

src/auth/gate.ts renders a compact 32×32 avatar button in #authSlot in the top nav bar. Clicking the avatar toggles a dropdown card containing either the sign-in form (when unauthenticated) or the user's profile panel with name, email, avatar image, and sign-out button (when authenticated). The dropdown dismisses on outside click or via a × close button in the card header. When Supabase is unconfigured (offline dev) the gate is bypassed entirely and the observability panel mounts immediately. On sign-in the nav avatar updates to the user's OAuth avatar image or email initial; on sign-out it reverts to a generic person placeholder.

Project switcher

src/ui/project-switcher.ts renders a small dropdown (ARIA listbox) next to the panel header, listing every project the signed-in user has access to via listUserProjects(). Selecting an item calls setActiveProject() which publishes a exepert:project-changed event; the trace list, summary tile, and brain all re-render reactively. Defaults to the demo project when offline.

Session grouping

The trace list now groups traces by session_id using collapsible <details>-style headers (.obs-session-group). Traces with no session render in a flat "no-session" group. Clicking a session header opens renderSessionDetail showing rollups (traces, tokens, cost, error rate, p95) with a Replay session button that sequentially replays all traces through the brain via replaySession() in src/map/session-rollup.ts.

Toolbar

The panel toolbar (.obs-toolbar) has two toggle buttons:

  • Error focus: filters the trace list to traces containing ERROR or slow (> 5 s) spans. Opening one drops into a dedicated error waterfall (renderErrorSpans) with danger-colored bars; clicking back returns to the full trace trace detail.
  • Compare: entering compare mode lets you select two traces; the panel renders a side-by-side region-activity diff grid with language / visual / attention / auditory rows colored green/red by delta. Each trace can be replayed independently.

Human annotation form

src/ui/annotation-form.ts mounts below the span-detail waterfall. It loads the project's annotation_configs (categorical / continuous / freeform) and shows the form alongside already-annotated types. Submissions go through submitHumanAnnotation() with annotator_kind='HUMAN' and feed into the same annotationsToRegionScores() path as LLM/CODE evals, tinting the brain in real time.

Offline degradation

All observability features degrade gracefully to a typed supabase.not_configured error when Supabase is offline. The brain simulation always boots; the observability panel stays empty (or shows a "not configured" caption) rather than crashing.