1 Commits

Author SHA1 Message Date
a5d732bd29 feat(web-ui): redesign built-in UI shell and theming
All checks were successful
Build and Push Docker Image / build (pull_request) Successful in 1m18s
2026-03-12 15:13:21 +01:00
2 changed files with 1498 additions and 386 deletions

View File

@@ -1,6 +1,6 @@
# Web UI: how it works # Web UI architecture
This document explains the built-in browser UI served by the gateway. This document explains the redesigned built-in browser UI served by the gateway.
Source file: Source file:
@@ -10,148 +10,173 @@ Source file:
## Overview ## Overview
The Web UI is a single HTML page returned by `GET /` (when enabled). The web UI is still served from `GET /`, but now uses a reusable app shell:
It is intentionally simple: - shared **header + sidebar** layout
- responsive navigation (desktop sidebar + mobile sheet)
- shadcn-style design tokens and component primitives
- light/dark theme toggle with persisted preference
- chat + conversations + API guide views
- plain HTML/CSS/JS (no framework) The gateway API contract is unchanged. Chat requests still use `/v1/chat/stream`.
- sends requests to `/v1/chat/stream`
- renders streamed assistant text in real time
- stores and reuses `conversationId` in `localStorage`
--- ---
## Availability ## App shell and routes
The UI route is controlled by `GATEWAY_ENABLE_WEB_UI`: The page is a small client-side multi-view app with three route panels:
- `true` (default): `GET /` returns UI - `#/chat` (default)
- `#/conversations`
- `#/api`
Routing is hash-based and keeps the shell consistent across views:
- **Sidebar**: primary navigation/actions
- **Top header**: current page title, status badge, theme toggle, mobile menu trigger
- **Main content**: route panel content
### Shared shell behavior
- Desktop (`>960px`): sidebar is always visible
- Mobile (`<=960px`): sidebar is replaced with a sheet/drawer opened from header
- Escape key and backdrop click close the mobile sheet
---
## UI primitives and design tokens
The redesigned UI follows shadcn-style structure and naming:
- card surfaces
- buttons (`default`, `secondary`, `outline`, `ghost`)
- inputs and textarea
- badge/status pill
- separator
- sheet/drawer-style mobile nav
Design tokens are centralized as CSS variables on `:root` and overridden for dark mode via `[data-theme="dark"]`.
Key token families:
- `--background`, `--foreground`
- `--card`, `--card-foreground`
- `--primary`, `--secondary`, `--accent`
- `--muted`, `--muted-foreground`
- `--border`, `--input`, `--ring`
- sidebar-specific tokens
---
## Theme system (light/dark)
### Behavior
- Initial theme defaults to system preference (`prefers-color-scheme`) if no saved preference exists.
- User can toggle theme from the header button.
- Selected theme is persisted in `localStorage` under:
- `pi_gateway_theme`
- Theme applies by setting:
- `document.documentElement.dataset.theme`
- `document.documentElement.style.colorScheme`
### Persistence and system changes
- If user has explicitly chosen a theme, that preference wins.
- If no explicit preference exists, system theme changes are applied live.
---
## Chat view
The Chat panel contains:
1. **Session settings card**
- Conversation ID input (`#conversationId`)
- optional bearer token input (`#token`)
- new session and refresh conversations actions
2. **Messages card**
- streaming message log (`#messages`)
- clear visible messages action
3. **Composer card**
- prompt textarea (`#message`)
- send action (`#send`)
- keyboard shortcut hint
### Streaming behavior
- Sends `POST /v1/chat/stream`
- Parses SSE incrementally
- Handles key events:
- `assistant_text_delta`
- `assistant_thinking_delta`
- `tool_start`
- `done`
- `error`
- Saves `conversationId` from `done` into:
- input field
- localStorage key `pi_gateway_conversation_id`
---
## Conversations view
The Conversations panel reads `GET /v1/conversations` and renders:
- conversation id
- created/updated timestamps
- loaded/streaming indicators
Per-conversation actions:
- **Use in chat**: sets `#conversationId`, persists it, routes back to chat
- **Delete**: calls `DELETE /v1/conversations/:id`
---
## API guide view
A lightweight route that summarizes core gateway endpoints used by adapters and the web UI:
- `/v1/chat/stream`
- `/v1/chat`
- `/v1/conversations`
- `/v1/adapters/chat/stream`
---
## Accessibility notes
The redesigned UI includes:
- keyboard-focus-visible styles using `--ring`
- semantic regions (`nav`, `header`, `main`, `section`)
- `aria-current` on active nav item
- mobile sheet dialog semantics (`role="dialog"`, `aria-modal="true"`)
- message log with `role="log"` + `aria-live="polite"`
- keyboard send shortcut (`Cmd/Ctrl + Enter`)
---
## Availability and auth
The route is controlled by `GATEWAY_ENABLE_WEB_UI`:
- `true` (default): `GET /` serves UI
- `false`: `GET /` returns `404` with `{ "error": "Web UI disabled" }` - `false`: `GET /` returns `404` with `{ "error": "Web UI disabled" }`
If `GATEWAY_AUTH_TOKEN` is enabled, `GET /` also requires an `Authorization` header, because auth is global in the gateway. If `GATEWAY_AUTH_TOKEN` is set, auth is still global, so `GET /` also requires bearer auth.
--- ---
## UI sections ## Storage keys used by the UI
### 1) Session/header card
- **Conversation ID input** (`#conversationId`)
- if empty, server auto-creates one during first message
- persisted locally under `pi_gateway_conversation_id`
- **Auth token input** (`#token`)
- optional bearer token included in API requests from the page
- this affects `fetch` calls only; it does not add auth headers to the initial page load
### 2) Messages card
- container `#messages`
- each message is appended as a `.msg.user` or `.msg.assistant` block
- text is rendered as plain text (`textContent`), not Markdown/HTML
### 3) Composer card
- textarea `#message`
- status text `#status`
- buttons:
- `Send`
- `New session`
---
## Local state
The page keeps only minimal browser-side state:
- `conversationId` in input + local storage
- rendered message list in DOM
- current request state via button disabled/enabled
Storage key:
- `pi_gateway_conversation_id` - `pi_gateway_conversation_id`
- `pi_gateway_theme`
On load, if this key exists, it pre-fills the conversation input.
--- ---
## Send flow ## Related docs
When user presses **Send** (or Cmd/Ctrl + Enter):
1. Trim textarea value; ignore empty input.
2. Disable `Send` and `New session` buttons.
3. Append user message bubble.
4. Append empty assistant bubble.
5. Build payload:
- required: `message`
- optional: `conversationId` (if input non-empty)
6. POST to `/v1/chat/stream` with JSON body.
7. Parse SSE stream incrementally.
8. Update assistant bubble and status based on events.
9. Re-enable buttons when request finishes/fails.
---
## SSE event handling in UI
Handled events:
- `assistant_text_delta`
- appends `data.delta` to assistant message bubble
- `done`
- reads `data.conversationId`
- updates conversation input
- writes `pi_gateway_conversation_id`
- status becomes `Done • conversation <id>`
- `error`
- status becomes error text
- writes fallback error into assistant bubble if empty
Other event types are currently ignored by the UI.
---
## New session button behavior
Clicking **New session**:
- does nothing if a request is currently streaming (`Send` disabled)
- clears conversation ID input
- removes `pi_gateway_conversation_id` from local storage
- clears rendered message list
- sets status to `New session ready`
- focuses the message textarea
This starts a fresh client-side chat thread. The next send will create a new conversation on the server.
---
## Keyboard shortcut
In the message textarea:
- `Cmd + Enter` (macOS) or `Ctrl + Enter` (Windows/Linux)
- triggers the same send flow as the Send button
---
## Limitations
Current UI is intentionally minimal:
- no server-side message history loading
- no cancel/abort button for in-flight response
- no rendering for tool events/thinking events
- no Markdown formatting
- no multi-conversation sidebar
It is best used as a lightweight test/debug interface.
---
## Related API docs
For full gateway/API details, see:
- `docs/gateway.md` - `docs/gateway.md`
- `docs/channels.md`

File diff suppressed because it is too large Load Diff