Gapless

Frontend

What the frontend of an AI assistant is responsible for, how it is structured, and how it differs from a normal web app frontend.

The frontend is where users decide whether to trust the assistant. A correct answer that arrives as a frozen spinner for eight seconds, then dumps a wall of text with no sources, feels worse than a slightly weaker answer that streams in immediately with clickable citations. This section covers how to build a frontend that makes AI output fast, verifiable, and safe to display.

What is different about an AI frontend

A normal CRUD app sends a request and renders a response. An AI frontend has to handle things most web apps never meet:

ChallengeWhy it is hardPage
Responses arrive as a stream over many secondsYou render incomplete markdown, keep scroll position sensible, and allow stopping midwayStreaming
Output is untrusted text written by a modelModel output can contain HTML, scripts, or malicious links copied from a poisoned documentRendering output
Answers must be verifiableUsers need to see which source supports which sentenceChat UI
State changes mid-messageA message goes through pending, streaming, tool confirmation, done, or failedState management
Failures are frequent and variedRate limits, provider outages, blocked content, and dropped streams all need distinct handlingError and empty states
Screen readers and live text conflictAnnouncing every streamed token makes the app unusable with assistive technologyAccessibility and i18n
Perceived speed is everythingTime to first token is the metric users feelPerformance

The design

Three kinds of state live in the browser, and keeping them separate is the main structural decision:

  • Server state: chats, past messages, user settings, documents. Fetched over REST and cached. A library like TanStack Query or SWR handles caching, refetching, and invalidation.
  • Stream state: the message currently being generated. It changes many times per second and must not trigger refetches or re-render the whole app.
  • UI state: the draft in the composer, which panel is open, which citation is highlighted.

A typical stack

These docs are framework-neutral, but most teams land on something close to this:

ConcernCommon choiceWhy
FrameworkReact with Next.js, or Vue with NuxtLarge ecosystem of chat and markdown components
Server stateTanStack Query or SWRCaching and invalidation without hand-written code
Stream stateA small store such as Zustand, or useReducerFrequent updates without context re-render storms
Streaming transportNative fetch with a stream reader, or EventSourceNo extra dependency. See Streaming
Markdownreact-markdown with remark-gfm, plus a sanitizerRenders tables and lists, with no raw HTML
Code highlightingShiki or Prism, loaded lazilyHighlighting libraries are large
StylingTailwind CSS or a design systemConsistent spacing and typography for long text

Folder structure

Group code by feature, not by file type. An AI app has a few large features, and each has its own components, hooks, and types.

src/
  features/
    chat/
      components/      MessageList, Message, Composer, CitationChip, ToolCard
      hooks/           useChatStream, useSendMessage, useAutoScroll
      stream/          parseSSE.ts, events.ts (typed event definitions)
      state/           chatStore.ts (stream state)
    documents/         upload, list, status of ingestion jobs
    admin/             usage, connectors, settings
  shared/
    markdown/          SafeMarkdown, CodeBlock
    api/               fetch client, auth token refresh
    ui/                buttons, dialogs, toasts

Put the event types in one file and share them with the backend if you can, for example from a shared package or generated from a schema. When the backend adds a new event type, the frontend should fail to compile, not silently ignore it.

What the frontend must never do

  • Never call model providers directly from the browser. API keys would be exposed, and you lose rate limiting, guardrails, logging, and permission checks.
  • Never render model output as raw HTML. Treat it like user input from a stranger.
  • Never decide permissions. Hiding a button is a convenience. The backend enforces access.
  • Never build prompts. The frontend sends the user's message and settings. Prompt assembly belongs to the orchestrator, where it is versioned and logged.

Pages in this section

PageCovers
Chat UIMessage layout, citations, feedback, stop and regenerate, attachments, tool confirmations
StreamingSSE protocol, parsing, reconnecting, cancelling, auto-scroll
State managementMessage lifecycle, optimistic updates, server state vs stream state
Rendering outputSafe markdown, partial markdown, code blocks, links, citations
Accessibility and i18nScreen readers and streaming, keyboard use, languages and RTL
Error and empty statesEvery failure the user can see, and what to show for each
PerformanceTime to first token, long conversations, bundle size

On this page