I lead frontend at dmclounge.ai, where chat-style AI interfaces are core to the product. The model gets the attention, but the interface decides whether people trust it.
Model the state properly
type Message = | { id: string; role: "user"; content: string } | { id: string; role: "assistant"; status: "streaming"; content: string } | { id: string; role: "assistant"; status: "done"; content: string } | { id: string; role: "assistant"; status: "error"; content: string; error: string };
A discriminated union stops you rendering a "retry" button on a message that's still streaming.
Streaming
On the client, read the response body stream and append chunks:
async function streamReply(prompt: string, signal: AbortSignal, onChunk: (t: string) => void) { const res = await fetch("/api/chat", { method: "POST", body: JSON.stringify({ prompt }), signal, }); if (!res.ok || !res.body) throw new Error(`HTTP ${res.status}`); const reader = res.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; onChunk(decoder.decode(value, { stream: true })); } }
Libraries like the Vercel AI SDK handle this plumbing well if you'd rather not.
Stop and retry
- Stop: an
AbortControllerper request. Also stop the upstream model call on the server, or you keep paying for tokens nobody sees. - Retry: re-send the same user message, and replace the failed assistant message rather than appending a new one.
- Edit and resend: let users fix their prompt.
Rendering output safely
- Use a markdown renderer without raw HTML, or sanitise.
- Syntax-highlight code blocks and add a copy button.
- Don't auto-open links from model output without user action.
UX details that build trust
- Disclose it's AI. This is also an EU AI Act transparency duty.
- Auto-scroll only if the user is at the bottom. Don't yank them away while reading.
- Show sources when answers come from retrieval (RAG).
- Clear error messages with a retry button, not a silent failure.
- Accessible: announce new messages with an
aria-liveregion, keep everything keyboard-operable.