# 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. - `wouter` for routing, a router of about 2 KB. - `hono/client`'s `hc` with 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. - `EventSource` for reply streams. - One small stylesheet for layout only: a sidebar and main column with flexbox, and `white-space: pre-wrap` on 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 `
`. - Tool calls and results go in a `
` 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 `read` helper returns the body of the successful responses and throws an `ApiError` with 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`. ## Acceptance Criteria - [ ] Signing in with a handle starts OAuth, and the app returns to the chat list afterwards. - [ ] Signed-out users are sent to `/login`. - [ ] Creating a chat, sending a message, and watching the reply stream works end to end. - [ ] The chat list shows new titles once they are generated. - [ ] Sibling controls switch branches and show the matching replies. - [ ] Regenerating adds a sibling reply and selects it. - [ ] Editing a message adds a sibling user message with its own reply. - [ ] Stop cancels a generating reply, which then shows as stopped. - [ ] The effort select appears only for reasoning models. - [ ] Image attachment is disabled for models without vision. - [ ] A PDF attachment uploads, shows its name, and reaches the model as text. - [ ] Settings save preferences, add and delete API keys, and save plugin settings through generated forms. - [ ] A shared link shows the conversation read-only to a permitted viewer. - [ ] Reasoning and tool details are collapsed by default. - [ ] If the stream drops, the reply still appears through repeated refreshes. - [ ] The sync button syncs the conversation and shows changes written from another client. - [ ] The background sync switch appears only when the admin allows opting out, and saves the account setting.