Chat App
A chat interface renders a running list of messages and lets the user send new ones. Each
message is a {"role": ..., "content": ...} dictionary held in state, and the component
loops over that list to draw bubbles. This recipe covers a basic chat UI and the
streaming patterns that keep the loading experience clean.
Basic Chat UI
Store messages in state, append the user's message on submit, and produce a reply. The example below echoes the input so it runs on its own; in a real app you would call your model provider instead.
ExpandCollapse
Streaming Responses
When a model streams its reply token by token, show a loading indicator immediately and append the assistant message only once the stream actually starts producing content.
The key rule: do not pre-append an empty assistant message before the response begins. Doing so renders a blank bubble alongside the loading dots, producing a broken double-indicator experience.
Both snippets below use the package and read
the API key from the OPENAI_API_KEY environment variable. They also run as
background events, so all state mutations happen inside
async with self: blocks with deltas being sent to the client after exiting the block.
Implementation
The loading dots show first, and the assistant bubble appears only on the first streamed token. Two more details make this safe:
- The streaming LLM call is wrapped in a
try/finallysois_streamingalways resets even if the API call raises - An
is_generatingguard serializes requests while the assistant message is updated by a captured index rather thanself.messages[-1], which would otherwise point at whichever message is currently last and let overlapping streams write to the wrong bubble:
ExpandCollapse
Key Details
- Build the API request from
self.messagesdirectly — there is no trailing empty message to slice off with[:-1]. - Append the assistant message only on the first token from the stream.
- Hide the loading indicator after the first token, not at the end of the stream.
- Wrap the stream in
try/finallysois_streamingresets even when the API call fails. - Guard overlapping submits with
is_generatingand update the assistant message by its captured index (notself.messages[-1]) — background events run concurrently, so a second stream could otherwise append tokens to the wrong message.