355 lines
16 KiB
Markdown
355 lines
16 KiB
Markdown
# Chat Client
|
|
|
|
**Package:** `@nexusai/chat-client`
|
|
**Location:** `packages/chat-client`
|
|
**Deployed on:** Mini PC 2 (192.168.0.205)
|
|
**URL:** `https://nexus.jellystorm.com` (behind Authelia SSO)
|
|
|
|
## Purpose
|
|
|
|
Browser-based chat interface for NexusAI. Communicates exclusively with
|
|
the orchestration service — no direct access to memory, embedding, or
|
|
inference services. Served as static files by Caddy on Mini PC 2.
|
|
|
|
## Dependencies
|
|
|
|
- `react` + `react-dom` — UI framework
|
|
- `react-markdown` — Markdown rendering in message bubbles and memory viewer
|
|
- `uuid` — session ID generation
|
|
- `vite` + `@vitejs/plugin-react` — build tooling
|
|
|
|
## Build
|
|
|
|
```bash
|
|
cd packages/chat-client
|
|
npm run dev # local dev server on port 5173
|
|
npm run build # outputs to dist/ for production
|
|
```
|
|
|
|
After building, copy `dist/` contents to `/srv/nexusai` on Mini PC 2 for Caddy to serve.
|
|
|
|
Vite bakes environment variables into the bundle at build time. The `.env`
|
|
file is only needed on the machine running the build, not where files are served.
|
|
|
|
## Environment Variables
|
|
|
|
| Variable | Required | Default | Description |
|
|
|---|---|---|---|
|
|
| VITE_ORCHESTRATION_URL | No | `''` (empty) | Orchestration base URL. Leave empty in dev (Vite proxy handles routing). Set to HTTPS domain for production builds. |
|
|
|
|
**Development:** leave `VITE_ORCHESTRATION_URL` unset — the Vite proxy routes
|
|
API requests directly to orchestration, bypassing Caddy and Authelia.
|
|
|
|
**Production build:** set before running `npm run build`:
|
|
```
|
|
VITE_ORCHESTRATION_URL=https://nexus.jellystorm.com
|
|
```
|
|
|
|
> Do not set `VITE_ORCHESTRATION_URL` to the HTTPS domain during local dev.
|
|
> Requests from `localhost:5173` to `nexus.jellystorm.com` will hit Authelia,
|
|
> which returns an HTML login page instead of JSON — causing `Unexpected token '<'`
|
|
> parse errors in `useModels` and `useSession`.
|
|
|
|
## Vite Dev Proxy
|
|
|
|
`vite.config.js` proxies API routes directly to the orchestration service
|
|
during local development, bypassing Caddy and Authelia entirely:
|
|
|
|
```js
|
|
export default defineConfig({
|
|
plugins: [react()],
|
|
server: {
|
|
proxy: {
|
|
'/models': 'http://192.168.0.205:4000',
|
|
'/sessions': 'http://192.168.0.205:4000',
|
|
'/chat': 'http://192.168.0.205:4000',
|
|
'/projects': 'http://192.168.0.205:4000',
|
|
'/episodes': 'http://192.168.0.205:4000',
|
|
'/settings': 'http://192.168.0.205:4000',
|
|
'/health': 'http://192.168.0.205:4000',
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
When adding new top-level routes to the orchestration service, add a matching
|
|
entry here and in the Caddy config.
|
|
|
|
## Internal Structure
|
|
|
|
```
|
|
src/
|
|
├── api/
|
|
│ └── orchestration.js # All fetch calls to the orchestration service
|
|
├── config/
|
|
│ └── constants.js # FALLBACK_MODELS, DEFAULT_MODEL, API_DEFAULTS, CLIENT_DEFAULTS
|
|
├── hooks/
|
|
│ ├── useSession.js # Session list, history loading, active session state
|
|
│ ├── useChat.js # Message sending, SSE streaming, message state
|
|
│ ├── useModels.js # Dynamic model list fetched from /models endpoint
|
|
│ ├── useProjects.js # Project list fetched from /projects endpoint
|
|
│ ├── useSettings.js # Settings fetch + saveSetting helper
|
|
│ └── useContextMenu.js # Right-click context menu position and visibility
|
|
├── components/
|
|
│ ├── App.jsx # Root component — layout, shared state, view routing
|
|
│ ├── Sidebar.jsx # Left sidebar — projects, grouped recent chats, navigation
|
|
│ ├── HomeView.jsx # Landing screen — greeting, centred input, quick actions
|
|
│ ├── ChatWindow.jsx # Centre panel — message thread, back button, model pill
|
|
│ ├── MessageBubble.jsx # Individual message bubble — renders markdown via react-markdown
|
|
│ ├── InfoPanel.jsx # Right panel — model selector and session metadata (slide-in)
|
|
│ ├── SessionModal.jsx # Modal for session rename, project assignment, delete
|
|
│ ├── ProjectModal.jsx # Modal for project create, edit, delete
|
|
│ ├── AllChatsView.jsx # Paginated session list with project indicator column
|
|
│ ├── AllProjectsView.jsx # Project tile grid with create/edit/delete; tile click navigates to ProjectView
|
|
│ ├── ProjectView.jsx # Individual project — conversations, new chat input, memory
|
|
│ │ # placeholder, user notes, ⋮ edit/delete menu
|
|
│ ├── MemoryView.jsx # Paginated, searchable, expandable, deletable episode viewer
|
|
│ └── SettingsView.jsx # Settings — Memory limits, Models (inference params, active
|
|
│ # model, context window), Service Health, Appearance placeholder
|
|
├── index.css # Global reset, CSS variables, utility classes
|
|
└── main.jsx # React entry point
|
|
```
|
|
|
|
## Layout
|
|
|
|
The app uses a view-based layout. `App.jsx` manages a `view` state string
|
|
that controls which main panel is rendered. The left sidebar and right info
|
|
panel are persistent across all views.
|
|
|
|
```
|
|
┌──────────────────┬──────────────────────────────┐
|
|
│ Sidebar │ Main Area (view-dependent) │
|
|
│ (collapsible) │ │
|
|
│ │ home → HomeView │
|
|
│ + New Chat │ chat → ChatWindow │
|
|
│ ⊞ View Projects │ all-chats → AllChatsView │
|
|
│ │ all-projects → AllProjectsView│
|
|
│ PROJECTS ▾ │ project → ProjectView │
|
|
│ [tile] [tile] │ settings → SettingsView │
|
|
│ All Projects → │ memory → MemoryView │
|
|
│ │ │
|
|
│ RECENT CHATS ▾ │ │
|
|
│ ● Project A │ │
|
|
│ Session 1 │ │
|
|
│ Session 2 │ │
|
|
│ ● Project B │ │
|
|
│ Session 3 │ │
|
|
│ Other │ │
|
|
│ Session 4 │ │
|
|
│ All Chats → │ │
|
|
│ │ │
|
|
│ ⚙ Settings │ │
|
|
└──────────────────┴──────────────────────────────┘
|
|
```
|
|
|
|
The sidebar collapses to a 48px icon rail and starts collapsed on the home
|
|
view. The right `InfoPanel` slides in from the right using
|
|
`transform: translateX()` — hidden by default, toggled via the `⊹` button
|
|
in the `ChatWindow` header.
|
|
|
|
## View Routing
|
|
|
|
| View | Component | Trigger |
|
|
|---|---|---|
|
|
| `'home'` | `HomeView` | Initial load; going back from chat with no history |
|
|
| `'chat'` | `ChatWindow` | Selecting a session; new chat; sending from HomeView |
|
|
| `'all-chats'` | `AllChatsView` | "All Chats →" or ☰ icon in collapsed rail |
|
|
| `'all-projects'` | `AllProjectsView` | "View Projects" button or ⊞ icon |
|
|
| `'project'` | `ProjectView` | Clicking a project tile in sidebar or AllProjectsView |
|
|
| `'settings'` | `SettingsView` | Settings button or ⚙ icon |
|
|
| `'memory'` | `MemoryView` | "Open →" button in Settings → Memory section |
|
|
|
|
`activeProject` state in `App.jsx` tracks which project `ProjectView` is
|
|
displaying. Set via `onSelectProject` before navigating to `'project'`.
|
|
|
|
### View History Stack
|
|
|
|
`App.jsx` maintains a `viewHistory` array. Each `navigate(view)` call pushes
|
|
the current view onto the stack. `goBack()` pops the last entry and restores
|
|
it. All view components receive `onBack={goBack}` — no component hardcodes
|
|
its own back destination. Navigating to `'home'` collapses the sidebar;
|
|
leaving `'home'` expands it.
|
|
|
|
## Home View
|
|
|
|
`HomeView` is the landing screen shown on initial load. It displays:
|
|
- Time-based greeting ("Morning / Afternoon / Evening, Tim")
|
|
- Currently loaded model name (from `modelProps.modelAlias`, stripped of `.gguf`)
|
|
- Centred textarea input — sending creates a new session and navigates to chat
|
|
- Quick action pills that populate the input without auto-sending
|
|
|
|
Sending from HomeView uses `handleHomeSend` in `App.jsx`, which calls
|
|
`createSession()` (returns the new session object), then immediately calls
|
|
`sendMessage` with the session passed directly as a parameter — avoiding the
|
|
React state settling race condition that would cause the message to be dropped.
|
|
|
|
## CSS Architecture
|
|
|
|
Styles follow a hybrid approach — CSS utility classes for static reusable
|
|
rules, inline styles for dynamic prop-driven values.
|
|
|
|
### CSS Variables (`:root`)
|
|
|
|
| Variable | Value | Description |
|
|
|---|---|---|
|
|
| `--bg-base` | `#0f1117` | Page background |
|
|
| `--bg-surface` | `#0e0d0d` | Panel backgrounds |
|
|
| `--bg-elevated` | `#222536` | Elevated elements (inputs, cards) |
|
|
| `--border` | `#2e3150` | Border colour |
|
|
| `--accent` | `#3d3a79` | Primary accent (buttons, highlights) |
|
|
| `--accent-hover` | `#574fd6` | Accent hover state |
|
|
| `--text-primary` | `#e8e8f0` | Primary text |
|
|
| `--text-secondary` | `#8b8fa8` | Secondary text |
|
|
| `--text-muted` | `#555870` | Muted / placeholder text |
|
|
| `--bubble-user` | `#4742a8` | User message bubble background |
|
|
| `--bubble-ai` | `#20264d` | AI message bubble background |
|
|
| `--sidebar-width` | `180px` | Expanded sidebar width |
|
|
| `--panel-width` | `200px` | Expanded info panel width |
|
|
| `--header-height` | `40px` | Shared header height across all panels |
|
|
| `--radius-sm` | `6px` | Small border radius |
|
|
| `--radius-md` | `8px` | Medium border radius |
|
|
| `--radius-lg` | `12px` | Large border radius |
|
|
|
|
### Utility Classes
|
|
|
|
| Class | Description |
|
|
|---|---|
|
|
| `.panel-header` | Shared header row — used across all panels |
|
|
| `.btn-reset` | Resets button styles (no border, bg, cursor pointer) |
|
|
| `.btn-icon` | Icon button with hover state |
|
|
| `.btn-primary` | Accent-coloured action button with `:hover` and `:disabled` states |
|
|
| `.flex` / `.flex-col` | Flex layout helpers |
|
|
| `.flex-1` / `.flex-shrink` | Flex sizing helpers |
|
|
| `.items-center` / `.justify-center` / `.justify-between` | Alignment helpers |
|
|
| `.overflow-hidden` / `.scroll-y` | Overflow helpers |
|
|
| `.text-xs` / `.text-sm` / `.text-base` | Font size helpers |
|
|
| `.text-muted` / `.text-secondary` / `.text-accent` | Colour helpers |
|
|
| `.label-upper` | Uppercase section label style |
|
|
| `.truncate` | Text overflow ellipsis |
|
|
|
|
## Streaming
|
|
|
|
Messages are sent via `POST /chat/stream`. Tokens arrive as SSE events and
|
|
are written into the active assistant bubble token by token via
|
|
`updateLastMessage`. The blinking cursor in `MessageBubble` is shown while
|
|
`message.streaming === true`.
|
|
|
|
`useChat.sendMessage` accepts an optional `session` parameter (4th arg) that
|
|
overrides the closed-over `activeSession`. This is used by `handleHomeSend`
|
|
and `handleNewProjectChat` in `App.jsx` to pass the newly created session
|
|
object directly, avoiding React state settling races.
|
|
|
|
`useChat` accepts an optional `projectId` parameter in `sendMessage`. After
|
|
the first message completes in a new session, if `projectId` is set,
|
|
`updateSession` is called to write the project assignment to the backend.
|
|
|
|
## Session Management
|
|
|
|
Sessions are identified by `external_id` — a UUID generated client-side via
|
|
the `uuid` package. New sessions are created locally and auto-registered in
|
|
the memory service on the first message. The session list refreshes after
|
|
each completed response to surface newly created sessions.
|
|
|
|
`useSession.createSession` returns the new session object — callers can pass
|
|
it directly to `sendMessage` rather than waiting for React state to update.
|
|
|
|
`useSession.selectSession` skips the history fetch for new (`isNew: true`)
|
|
sessions — fetching history for an unsaved session would 404 since it doesn't
|
|
exist in the backend yet.
|
|
|
|
### Auto-naming
|
|
|
|
After the first exchange completes, orchestration fires a secondary inference
|
|
call with a short naming prompt (max 20 tokens, temperature 0.3). The result
|
|
is written back as `session.name`. The client fires a second `refreshSessions`
|
|
after a 3-second delay to pick up the name once written.
|
|
|
|
Manually renamed sessions are never overwritten — the `!session.name` guard
|
|
in `chat/index.js` prevents this.
|
|
|
|
### Session Actions
|
|
|
|
Session rows support rename, project assignment, and delete via:
|
|
- **Hover** — reveals ✎ and ✕ icon buttons alongside the row
|
|
- **Right-click** — context menu with the same actions
|
|
|
|
`SessionModal` handles rename and project assignment together in `settings`
|
|
mode, and delete confirmation in `confirm-delete` mode.
|
|
|
|
### Key Patterns
|
|
|
|
- Button nesting: action icons are siblings of row buttons, not children — HTML forbids `<button>` inside `<button>`
|
|
- Context menu rendered outside sidebar via React fragment to avoid `overflow: hidden` clipping
|
|
- `useContextMenu` dismisses on a `window` click listener
|
|
- Dynamic `updateSession` SQL builds `SET` clause from only the fields passed — prevents accidental overwrites
|
|
- `AllChatsView` pagination uses `CLIENT_DEFAULTS.PAGE_SIZE` (not `API_DEFAULTS.PAGE_SIZE` which doesn't exist)
|
|
|
|
## Sidebar — Session Grouping
|
|
|
|
Recent sessions in the sidebar are grouped by project under a colour dot +
|
|
project name label. Unassigned sessions appear under "Other" if any project
|
|
groups are present. The grouping is computed client-side from the `sessions`
|
|
array and `projects` list already available in `App.jsx` — no extra API call.
|
|
|
|
`AllChatsView` receives `projects` as a prop from `App.jsx` and displays a
|
|
project indicator column (colour dot + truncated name) in each session row.
|
|
|
|
## Project Management
|
|
|
|
All projects are isolated by default (`isolated: 1` hardcoded on create).
|
|
The isolated toggle has been removed from `ProjectModal`.
|
|
|
|
`useProjects` fetches the project list from `GET /projects` on mount and
|
|
exposes `refreshProjects` for keeping the sidebar in sync after mutations.
|
|
|
|
`ProjectModal` handles create, edit, and delete confirmation. Fields: name
|
|
(required), description (optional), colour picker.
|
|
|
|
Clicking a project tile in `AllProjectsView` calls `onSelectProject` then
|
|
navigates to `'project'`.
|
|
|
|
### ProjectView
|
|
|
|
`ProjectView` is a full project workspace with:
|
|
- Colour accent bar + project title + description
|
|
- ⋮ dropdown menu for edit (opens `ProjectModal` pre-filled) and delete
|
|
- Conversations list — each session is a clickable row navigating to `'chat'`
|
|
- `ChatInput` component below the list (or centred when no sessions exist) for
|
|
starting new project-tied conversations without a separate button
|
|
- **Project Memory** — placeholder section explaining upcoming auto-summary feature
|
|
- **Project Notes** — textarea with Save button; notes saved to `projects.notes`
|
|
column in SQLite; save button only appears when content has changed from last
|
|
saved value (`savedNotes` state tracks the baseline, not `initialNotes`)
|
|
|
|
`updateProject` in `orchestration.js` uses a passthrough pattern — spreads
|
|
all fields directly into the request body, only transforming `isolated` if
|
|
present. This allows partial updates like `{ notes }` without clobbering
|
|
other fields.
|
|
|
|
For memory isolation behaviour, see `memory-isolation.md`.
|
|
|
|
## Settings
|
|
|
|
`useSettings` fetches from `GET /settings` on mount and exposes a
|
|
`saveSetting(key, value)` helper that issues a `PATCH /settings` with a
|
|
single key-value pair. The `saving` boolean is exposed for disabling save
|
|
buttons during in-flight requests.
|
|
|
|
`SettingsView` receives `settings`/`saveSetting`/`saving` from a single
|
|
`useSettings()` call at the top level and passes them as props to
|
|
`ModelsSection` and `ModelsFolderSetting` — avoiding triple fetch on mount.
|
|
`modelProps` (context window, loaded model) is fetched once in `App.jsx` and
|
|
passed down as a prop, eliminating a duplicate fetch on every settings open.
|
|
|
|
`SettingsView` is organised into sections:
|
|
|
|
- **Memory** — recent episode limit, semantic limit, score threshold, link to MemoryView
|
|
- **Models** — models folder path, temperature, repeat penalty, Top-P, Top-K,
|
|
active model dropdown, read-only model info panel (file, size, context window,
|
|
loaded model from llama-server)
|
|
- **About** — service health check panel, version
|
|
- **Appearance** — theme (coming soon)
|
|
|
|
An error boundary (`SettingsSectionErrorBoundary`) wraps the Models section —
|
|
if the models fetch fails, only that section shows an error with a Retry
|
|
button rather than blanking the entire settings view. |