App Developer Docs
Features
Chat
The chat interface is a single-page, in-memory conversation. Nothing is stored — the conversation starts fresh on every session.
How it works
- The user types a message and sends (Enter or the send button).
- The frontend appends the user message to the local
messagesarray. - A
POST /api/chatrequest is sent with the full conversation history. - The backend prepends the system prompt and calls the OpenAI completions API.
- The reply is appended to the messages array and rendered.
Because the full history is sent on every request, the AI has context for the whole conversation. Because nothing is persisted, there’s no privacy exposure and no cleanup logic needed.
Shift+Enter
Pressing Enter sends the message. Shift+Enter inserts a newline. The textarea auto-scrolls to the bottom after each message.
State Check integration
When the user attaches a state check result from the State Check tab, it prepends a formatted block to the input:
[State Check]
Arousal: Mostly agree (4/5)
Valence: Neutral (3/5)
Approach: Mostly agree (4/5)
Agency: Neutral (3/5)
Bandwidth: Mostly disagree (2/5)
Social: Neutral (3/5)
Temporal: Mostly agree (4/5)
Meaning: Neutral (3/5)
The user then types their question below the state check block and sends. The AI receives both as a single user message and uses the dimensional context to shape its response.
System prompt
The system prompt is defined in backend/src/routes/chat.js. It:
- Describes the app’s purpose (conscious habit choice → neural change)
- Names and defines the 8 dimensions with their 1–5 scale ends
- Instructs the model to use state context when present and keep responses concise
Update the system prompt there to change how the AI interprets and responds.
Component
frontend/src/components/ChatView.vue — all state is local (ref). No Pinia store. Listening for sulci:attach-state is set up in onMounted and torn down implicitly when the component unmounts.
State Check
Eight questions, one per nervous system dimension. The user answers on a 1–5 scale and taps “Attach to chat →” to inject the results into the chat input.
The 8 dimensions
| Label | Question |
|---|---|
| Arousal | I feel physically activated or energized right now. |
| Valence | My emotional state feels predominantly positive. |
| Approach | I feel drawn toward engaging with my environment. |
| Agency | I feel in control of what happens next. |
| Bandwidth | I have mental space to think clearly right now. |
| Social | I feel open to connecting with other people. |
| Temporal | I can think ahead and imagine future possibilities. |
| Meaning | My current experience feels meaningful or purposeful. |
The 5-point scale
| Value | Label |
|---|---|
| 1 | Totally disagree |
| 2 | Mostly disagree |
| 3 | Neutral |
| 4 | Mostly agree |
| 5 | Totally agree |
This is a 1–5 self-report scale. It maps roughly to the 1–7 scale used in the platform’s dimensional model (the SpiderChart on the science pages), but is simpler for everyday use. The mapping is intentionally loose — the goal is directional context for the AI, not precise measurement.
Attach flow
The “Attach to chat →” button is disabled until all 8 questions are answered. When tapped:
- Formats results as a text block (see above).
- Dispatches
window.dispatchEvent(new CustomEvent('sulci:attach-state', { detail: text })). - Navigates to
#/chat.
The Chat view receives the event and prepends the block to the current input. If the user already has text in the input, the state check is prepended with a blank line separator.
The Reset button clears all 8 answers. State check results are not persisted.
Component
frontend/src/components/StateCheckView.vue — all state is local. Dimensions and answer labels are defined as constants at the top of the component’s <script setup> block. To add, remove, or reword a dimension, edit those constants.
Panic
The Panic view (frontend/src/components/PanicView.vue) guides users through a panic moment — naming what they’re feeling, recognizing the cycle it belongs to, and choosing to stay in the cycle rather than exit.
Flow
- Entry gate — User taps PANIC MOMENT. A full-screen breathing animation runs (4 press-and-hold rounds, inhale 3s / exhale 6s). A Skip button in the top-right bypasses it.
- Chat — After the animation, the user describes what they’re feeling. The AI responds via a streaming connection to
POST /api/panic/:id/message. - Recognition panel — After the first AI reply, a collapsible panel opens below the messages (capped at 50vh so the response stays visible). The user can select:
- Panic type — one of 4 types from
lib/panicTypes.js - Goal — an existing goal or a new one created inline
- Cycle phase — a phase from the selected goal’s generated cycles
- Panic type — one of 4 types from
- Reply — The “Reply” button in the panel sends the current selections as a plain-text context message to the AI and gets another response. After Reply the panel collapses and stays collapsed for all future AI turns (
recognitionUserCollapsedflag). The user can still expand it manually via the “Recognition (click to expand)” toggle. - Stay in the cycle — A persistent button outside the recognition panel. Always visible once the AI has responded. Resolves the panic moment in the backend and plays the ascend animation.
- Resolved — “Cycle recognized” card with option to start a new panic moment.
Key state
| Ref | Purpose |
|---|---|
activeMoment | Current open panic moment from the backend (null until first message sent) |
recognitionOpen | Whether the recognition panel is expanded |
recognitionUserCollapsed | Set true when user hits Reply; stops future AI replies from auto-reopening the panel. Reset on new moment. |
entryMode | True until user completes or skips the breathing animation |
Component
frontend/src/components/PanicView.vue — uses useBrainScene (Three.js brain in the header) and usePanicAnimations (breathing and ascend overlays). Both are mocked in tests. Panic moment state (messages, selections) is in-memory only; the backend persists the moment record but not the full message history beyond the session.