Chat Lifecycle
Conversation Model
Every conversation is a tree of messages connected by parent_id. The client tracks the “active leaf” — the currently selected branch tip — per conversation in activeMessageId.
interface ChatMessage {
id: string
role: 'user' | 'assistant' | 'system'
content: string
sources?: SourceChunk[]
created_at: number
parent_id?: string // enables tree branching
}
interface SendMessageRequest {
message: string
config?: { temperature?, max_tokens?, use_rag?, model? }
parent_id?: string // parent in message tree
user_message_id?: string // pre-assigned ID for optimistic update
assistant_message_id?: string // pre-assigned ID for the response
regenerate_user_msg_id?: string // for regeneration flow
}Chat Store State Machine
Free Tier Limit
Unauthenticated users are limited to 5 free messages tracked client-side:
const FREE_MESSAGE_LIMIT = 5
// In sendMessage():
if (!isAuthenticated && freeMessageCount >= FREE_MESSAGE_LIMIT) {
openModal('signin') // triggers auth modal
return
}freeMessageCount is persisted in localStorage via Zustand’s persist middleware. This limit is enforced client-side only — there is no server-side enforcement for unauthenticated chat.
Streaming vs Non-Streaming
sendMessage(content, useStream?) selects the endpoint based on the useStream flag:
| Flow | Endpoint | Response format |
|---|---|---|
| Streaming | POST /chat/conversations/{id}/stream | SSE (text/event-stream) |
| Non-streaming | POST /chat/conversations/{id}/messages | JSON MessageResponse |
The streaming flow uses an AbortController stored in the Zustand store. stopGeneration() calls abortController.abort(), which closes the SSE connection.
SSE Chat Sequence
Message Branching (Edit & Regenerate)
editMessage(messageId, newContent): Creates a new conversation branch:
- Calls
createNewConversation()— creates a sibling fork - Copies messages up to
messageIdinto the new conversation - Sends
newContentas the new user message withparent_idset appropriately
regenerateLastResponse(): Re-sends the last user message:
- Identifies the last
userrole message - Calls
sendMessage()withregenerate_user_msg_idset to that message’s ID
switchBranch(messageId): Sets activeMessageId for the current conversation, causing the UI to render the message tree from that leaf node upward.
Title Generation
After a successful message exchange, if useAiTitleGeneration is true, the store calls:
POST /chat/conversations/{id}/generate-title
Body: { user_message: string, assistant_message: string }This triggers the Intelligence service to generate a concise title via the LLM. If useAiTitleGeneration is false, a simple title heuristic is used client-side (first N characters of the first user message).
Conversation List Pagination
fetchConversations(reset?) supports cursor-based pagination:
reset = truefetches page 1 (cursor = null)- Subsequent calls pass
nextCursorfrom the previous response - Conversations are sorted by
updated_at DESC totalConversationsCountis tracked for infinite scroll triggering