Emotion-reactive brain canvas for EXEPERT Chat
EXEPERT Chat now drives a synthetic affect layer that colors and pulses the brain canvas while a user chats with an AI model. The feature is designed for AI emotion research UX: it visualizes conversational affect signals, not clinical or literal emotion detection.
What changed
src/affect/*adds the affect engine: shared types, the emotion palette, the deterministic local classifier, text hashing, affect-to-region mapping, and the browser client for saving affect annotations.src/brain-ui.tsclassifies user input immediately, classifies assistant output after streaming, renders message affect chips, updates the new Affective Field, and triggers emotion-colored neural pulses. Chat error states can also becomeassistant_erroraffect events when the failed span is available.index.htmladds the Affective Field readout inside the Chat tile and the canvas-stage affect overlay.css/app.cssstyles the Affective Field, message chips, stage overlay, and responsive narrow-panel behavior.rust/brain_sim/src/lib.rsextends the WASM signal pool with per-signal RGB color buffers. Stimuli can now pass a hex color, and signals propagate that color through neuron firing.src/brain.tsbinds the WASM particle color buffer to a Three.jscolorgeometry attribute so simultaneous signals can render different emotion colors in the same canvas.supabase/functions/chat-affect/index.tsadds an authenticated Edge Function that writesaffective_statespan annotations. It always stores the localCODEclassifier output and can also store an LLM-refined annotation through 9Router.supabase/functions/run-chat/index.tsnow includestrace_id,span_id,otel_trace_id, andotel_span_idon streaming error payloads when the error span was persisted successfully.
Affect model
The local classifier scores:
- anger
- fear
- sadness
- joy
- disgust
- surprise
- curiosity
- calm
- distress
- neutral
Profanity is context-aware. For example, "fuck yeah" routes toward high-arousal joy, while directed hostile profanity routes toward anger, distress, and higher toxicity. The UI can look dramatic, but docs and metadata call the output synthetic affect telemetry.
Persistence
chat-affect writes to existing span_annotations rather than adding a new
table:
name = "affective_state"annotator_kind = "CODE"for local analysisannotator_kind = "LLM"for optional refinementidentifier = chat-turn:<turn_id>:phase:<phase>:source:<source>
Metadata stores score vectors, region mix, color palette, classifier version, selected chat model, affect model, phase, and text hash. Raw message text is not duplicated in affect metadata.
Verification
Checked locally with:
pnpm run typecheck
pnpm test
pnpm build
pnpm --dir docs-site build
A browser smoke check opened Chat at http://127.0.0.1:9001/, verified the
Affective Field on desktop and narrow viewports, confirmed no Chat overflow,
and confirmed the WebGL brain canvas screenshot was nonblank.