Falsafa
FrontendHigh-Level Design

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.

ActionEndpointWhat happens
CreatePOST /api/chat/sessionsCreates a new session row in chat_sessions for a character, checks library ownership.
Create-or-returnPOST /api/chat/sessionReturns an existing session for the character if one exists, otherwise creates a new one.
List recentGET /api/chat/recentFetches the authenticated user's recent conversations, joined with character and book data.
FetchGET /api/chat/session/[sessionId]Returns full session data plus all messages. Validates user ownership.
Add messagePOST /api/chat/session/[sessionId]/messageInserts 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:

  1. The function POSTs to /api/chat with { session_id, message }.
  2. 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, and user_message.
  3. It forwards the request to POST /character/chat on the Python backend and returns the SSE response as a stream.
  4. The client reads the SSE stream using the ReadableStream API, parsing each data: line through parseSSEStream().

SSE Event Types

Client typeBackend payloadWhat 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 preferences

Preference dimensions

DimensionOptionsEffect
Relationship modestranger, friend, sibling, parent, child, partner, customControls the relationship framing in the system prompt
Custom relationshipfree textUsed when relationship mode is custom
Speech modifierstalk_less, no_analogies, use_simple_words, be_formal, be_informalTone and style adjustments
Behavioral modifiersbe_funny, be_serious, be_flirty, be_protective, be_warm, be_coldCharacter behavior adjustments
Preference summaryfree textFree-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:

  1. 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.
  2. 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 state
  • streamChat() 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.

On this page