Web UI #
Summary #
The web UI is a deliberately bare React app. It exposes every phase 1 feature with plain HTML elements and almost no styling, so a designer can build the real interface on top without fighting existing design decisions. It covers signing in, the chat list, a conversation with branch navigation, the composer with model and effort choice and attachments, streaming replies, sharing, and settings for preferences, API keys, and plugin settings. It talks to the server through Hono's typed client, so API changes surface as type errors.
Motivation #
A separate designer will build the production interface. The phase 1 UI exists to prove the features work end to end, and to give the designer working components and data flow to restyle or replace. Every visual choice made here is one the designer might have to undo, so this UI makes as few as possible.
Design #
Stack #
- React with Vite, from the foundation spec.
wouterfor routing, a router of about 2 KB.hono/client'shcwith the server's exported route types, for a typed API client with no hand-written API types.- Chats are read from the browser store, through its worker API. Writes and other data go through the typed client with small hooks. No state management or data-fetching library.
EventSourcefor reply streams.- One small stylesheet for layout only: a sidebar and main column with flexbox, and
white-space: pre-wrapon message text. No colors, fonts, or component library.
Routes #
| Route | Screen |
|---|---|
/login |
A handle field and a sign-in button that goes to /oauth/login |
/ |
Chat list sidebar, and a new chat |
/c/:skey |
Chat list sidebar and a conversation |
/s/:ownerDid/:skey |
A shared conversation, read-only |
/settings |
Preferences, API keys, and plugin settings |
Any route except /login and a shared route redirects to /login when /api/me returns 401. A shared route shows a sign-in prompt instead.
Components #
- ChatList. Conversation titles from the browser store, newest first. Untitled conversations show "New chat". It has a button to start a new chat.
- Conversation. It shows the messages on the current branch, from the root to the selected leaf. A message with siblings shows "‹ 2 / 3 ›" controls that switch branches. By default the branch follows the newest sibling at each level. It also has the title, with rename, the share control from the sharing spec, and a sync button that calls the conversation's sync route and then refreshes it in the browser store.
- Message.
- Text parts are shown as plain text, with no Markdown rendering.
- Reasoning parts go in a closed
<details>. - Tool calls and results go in a
<details>with their JSON. - Sources are shown as a list of links.
- Images are shown inline, and files as their names.
- A pending reply shows streamed text as it arrives. An errored reply shows its error, and a cancelled reply is labeled as stopped.
- Message actions. On user messages, edit, which opens the composer prefilled and sends a sibling. On assistant replies, regenerate, with an optional model change. While generating, stop.
- Composer. A textarea, a model select from
/api/models, an effort select for models with the reasoning capability, and an attach button. The attach button accepts images only for models with vision, plus any type an ingester supports. Attachments upload when chosen and show a progress state. Enter sends, and Shift+Enter adds a newline. - Settings.
- Preferences: default model, default effort, custom instructions, and generate titles.
- API keys: add, list with the last four characters, and delete. For providers with user endpoints, a base URL field.
- Plugin settings: one form per plugin with user settings, generated from its JSON schema.
- Background sync: an on and off switch, shown only when the admin allows users to opt out.
- This device: a button that deletes and rebuilds the browser store.
Schema forms #
SchemaForm renders a JSON schema object as labeled inputs. It supports strings, as text inputs or password inputs for secret fields, plus numbers, booleans as checkboxes, and string enums as selects. Any other type is shown as unsupported, not guessed at.
Streaming #
After sending, the conversation subscribes to the reply's stream endpoint and appends deltas to the pending reply. On the status event, it asks the browser store to refresh the conversation, which fetches the final record. If the stream fails, it refreshes the conversation every two seconds until the reply leaves pending.
Scope Boundaries #
- No visual design, theming, dark mode, icons, or animations.
- No Markdown or code highlighting.
- No PWA, offline support, or notifications.
- No mobile-specific layout beyond what the browser does.
- No search box. That is the search spec.
- No admin screens.
Edge Cases and Decisions #
- Message text is plain text, so the designer chooses the Markdown renderer and its sanitizing.
- The typed Hono client comes from the server's route types, so the web app and server share one definition of the API.
- The default branch is the newest sibling at each level, matching how ChatGPT shows the latest regeneration.
- Write routes read JSON without validators, so the typed client sends bodies through its request options. Response types still come from the server's routes.
- The
readhelper returns the body of the successful responses and throws anApiErrorwith the server's message otherwise. - Shared links show a sign-in prompt when signed out, and every other route sends signed-out users to
/login.