Chat Pipeline
SSE streaming from the frontend: session lifecycle, message proxying, SSE parsing, user preferences per character, and how the frontend creates, lists, and manages chat sessions.
The chat feature is the most architecturally interesting part of the frontend. It manages session state, proxies SSE streams from the Python backend, and allows users to customize how each character responds.
Session Lifecycle
The frontend owns the session lifecycle entirely. The backend never creates, deletes, or lists sessions - it only reads them.
| Action | Endpoint | What happens |
|---|---|---|
| Create | POST /api/chat/sessions | Creates a new session row in chat_sessions for a character, checks library ownership. |
| Create-or-return | POST /api/chat/session | Returns an existing session for the character if one exists, otherwise creates a new one. |
| List recent | GET /api/chat/recent | Fetches the authenticated user's recent conversations, joined with character and book data. |
| Fetch | GET /api/chat/session/[sessionId] | Returns full session data plus all messages. Validates user ownership. |
| Add message | POST /api/chat/session/[sessionId]/message | Inserts a user message directly into the messages table (non-streaming path - used for edge cases). |
The session creation endpoints check library ownership - a user can only start a chat with a character from a book they own. The fetch endpoint validates that the session belongs to the requesting user.
Streaming a Message
When the user sends a message, the chat page calls streamChat() from lib/chat.ts:
- The function POSTs to
/api/chatwith{ session_id, message }. - The API route authenticates the user, loads the session and character context, and constructs the backend request with
user_id,character_id,book_id,session_id, anduser_message. - It forwards the request to
POST /character/chaton the Python backend and returns the SSE response as a stream. - The client reads the SSE stream using the
ReadableStreamAPI, parsing eachdata:line throughparseSSEStream().
SSE Event Types
| Client type | Backend payload | What the page does |
|---|---|---|
token | {"token": "..."} | Appends content to the current assistant message |
done | {"done": true, "full_response": "..."} | Finalizes the message, returns full response |
error | {"error": "...", "code": 502} | Shows error UI, aborts streaming |
The SSE parser handles multiple wire formats for backward compatibility: {token}, {done}, {error}, {type: "token"}, {type: "done"}, {type: "error"}, raw text, [DONE], and {"type":"done"}. This allows the frontend to work with different backend versions and LLM providers.
User Preferences
Each user can set preferences for each character they chat with. Preferences are stored in the user_preferences table and managed through /api/chat/preferences:
GET /api/chat/preferences?characterId=... - get preferences for a character
POST /api/chat/preferences - create or update preferences
DELETE /api/chat/preferences?characterId=... - delete preferencesPreference dimensions
| Dimension | Options | Effect |
|---|---|---|
| Relationship mode | stranger, friend, sibling, parent, child, partner, custom | Controls the relationship framing in the system prompt |
| Custom relationship | free text | Used when relationship mode is custom |
| Speech modifiers | talk_less, no_analogies, use_simple_words, be_formal, be_informal | Tone and style adjustments |
| Behavioral modifiers | be_funny, be_serious, be_flirty, be_protective, be_warm, be_cold | Character behavior adjustments |
| Preference summary | free text | Free-form override for all of the above |
The frontend compiles these preferences into a USER PREFERENCE MODIFIERS block via buildPreferenceModifierBlock(). This block is included in the system prompt sent to the LLM, allowing the backend to adjust character behavior without knowing about the preference model.
Chat UI Architecture
The chat page has two modes:
- Chat list (
/chat) - Shows recent conversations and a library book list with characters to start new chats. Supports creating a new session via a query parameter. - Conversation view (
/chat/[id]) - Dedicated view for a single session, fetches session data and messages, manages SSE streaming for AI responses.
Both modes share:
useAuth()for authentication statestreamChat()for SSE communication- Preference modifiers for character behavior
- Optimistic message updates (user messages appear immediately, then assistant tokens stream in)
The conversation page handles back-end errors gracefully: single-session lock contention (429) shows a "session busy" message, LLM failure (502) shows an error, and non-streaming errors (404) redirect to the chat list.