App Developer Docs
Features
Chat
The chat is a persisted, multi-session conversation with the Downshiftr Life Agent. Messages are saved server-side in chat_contexts; the user can start new sessions, switch between them, rename, and delete them.
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.
The full history is sent on every request, so the AI has context for the whole conversation. History is persisted per session and reloaded when the user reopens a session (see the chat history/session endpoints in the API reference).
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
- Injects the user’s current local time (weekday, date, wall-clock time, and UTC offset from their
timezone) so scheduling and snoozing — “remind me at 3”, “pause until 6pm” — resolve against the user’s clock, not the server’s UTC
Update the system prompt there to change how the AI interprets and responds.
Component
frontend/src/components/ChatView.vue — backed by stores/chat.js (messages, streaming, sessions). It still listens for the sulci:attach-state event (set up in onMounted) to receive State Check results from that tab.
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.
Coach notifications (Web Push)
The coach reaches out proactively via the browser’s Push API — no third-party service, no always-on connection. Everything runs off a once-a-minute cron that hits /api/push/tick.
Enabling
Settings → Coach notifications → Turn on requests notification permission, registers the service worker (frontend/public/sw.js, scope /app/), subscribes via PushManager, and POSTs the subscription to /api/push/subscribe. The VAPID public key is fetched at runtime from /api/push/vapid-public-key.
Send a test notification schedules one ~60 seconds out, so it arrives via the cron even with the app backgrounded — proving the whole pipeline, not just a foreground toast.
Morning nudges
In the same card, a daily morning check-in nudge toggle + time picker writes nudge_enabled / nudge_morning_time to the user. Each cron tick, for every nudge-enabled user whose local time has reached their set time (timezone-aware, deduped to once per day), the server generates a short message via the Life Agent (backend/src/nudge.js) and delivers it to all the user’s devices.
The copy follows an engage-don’t-guilt framework: assume the user probably already did the thing, invite them to log it and glance at what’s open — never shame a gap. It targets the downshifter habits the user flags with the “Nudge me about this one” toggle in a habit’s edit overlay (falling back to all downshifters). If OpenAI is unavailable, a static fallback message is used.
Why Web Push
It’s standards-based, lives entirely on the app’s own server, needs no daemon or polling, and the reply surface is the app itself — tapping a notification deep-links to the chat. (On iOS, push only works after the site is added to the Home Screen as a PWA.)