328 lines
14 KiB
Markdown
328 lines
14 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
|
|
- `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
|
|
// vite.config.js
|
|
import { defineConfig } from 'vite';
|
|
import react from '@vitejs/plugin-react';
|
|
|
|
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',
|
|
}
|
|
}
|
|
});
|
|
```
|
|
|
|
If new routes are added to the orchestration service, add them here too.
|
|
|
|
## Internal Structure
|
|
|
|
```
|
|
src/
|
|
├── api/
|
|
│ └── orchestration.js # All fetch calls to the orchestration service
|
|
├── config/
|
|
│ └── constants.js # FALLBACK_MODELS, DEFAULT_MODEL, API_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
|
|
│ └── useContextMenu.js # Right-click context menu position and visibility
|
|
├── components/
|
|
│ ├── App.jsx # Root component — layout, shared state, view routing
|
|
│ ├── Sidebar.jsx # Left sidebar — projects, recent chats, navigation
|
|
│ ├── ChatWindow.jsx # Centre panel — message thread and input bar
|
|
│ ├── MessageBubble.jsx # Individual message bubble (user or assistant)
|
|
│ ├── InfoPanel.jsx # Right panel — model selector and session metadata
|
|
│ ├── SessionModal.jsx # Modal for session rename and delete confirmation
|
|
│ ├── ProjectModal.jsx # Modal for project create, edit, and delete confirmation
|
|
│ ├── AllChatsView.jsx # Full paginated session list with multi-select bulk delete
|
|
│ ├── AllProjectsView.jsx # Project tile grid with create/edit/delete
|
|
│ └── SettingsView.jsx # Settings placeholder (sections: Appearance, Memory, Models, About)
|
|
├── index.css # Global reset, CSS variables, utility classes
|
|
└── main.jsx # React entry point
|
|
```
|
|
|
|
> `SessionList.jsx` is superseded by `Sidebar.jsx` and kept only as a reference.
|
|
|
|
## Layout
|
|
|
|
The app uses a view-based layout. `App.jsx` manages a `view` state
|
|
(`'chat' | 'all-chats' | 'all-projects' | 'settings'`) that controls which
|
|
main panel is rendered. The left sidebar and right info panel are always present.
|
|
|
|
```
|
|
┌──────────────────┬──────────────────────────────┐
|
|
│ Sidebar │ Main Area (view-dependent) │
|
|
│ (collapsible) │ │
|
|
│ │ chat → ChatWindow │
|
|
│ + New Chat │ all-chats → AllChatsView │
|
|
│ ⊞ New Project │ all-projects → AllProjectsView│
|
|
│ │ settings → SettingsView │
|
|
│ PROJECTS ▾ │ │
|
|
│ [tile] [tile] │ │
|
|
│ All Projects → │ │
|
|
│ │ │
|
|
│ RECENT CHATS ▾ │ │
|
|
│ Session 1 │ │
|
|
│ Session 2 │ │
|
|
│ All Chats → │ │
|
|
│ │ │
|
|
│ ⚙ Settings │ │
|
|
└──────────────────┴──────────────────────────────┘
|
|
```
|
|
|
|
The sidebar collapses to a 48px icon rail. The right info panel (`InfoPanel`)
|
|
slides in from the right over the main area using `transform: translateX()` —
|
|
it is hidden by default (`rightOpen` starts `false`) and toggled via a button
|
|
in the `ChatWindow` header.
|
|
|
|
## 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 |
|
|
|
|
## API Layer
|
|
|
|
All orchestration calls are centralised in `src/api/orchestration.js`:
|
|
|
|
| Function | Method | Path | Description |
|
|
|---|---|---|---|
|
|
| `fetchSessions` | GET | /sessions | Load session list for sidebar |
|
|
| `fetchSessionHistory` | GET | /sessions/:id/history | Load episode history on session select |
|
|
| `sendMessage` | POST | /chat | Send message, await full response |
|
|
| `streamMessage` | POST | /chat/stream | Send message, receive SSE token stream |
|
|
| `fetchModels` | GET | /models | Load available models from manifest |
|
|
| `renameSession` | PATCH | /sessions/:id | Rename a session |
|
|
| `deleteSession` | DELETE | /sessions/:id | Delete a session |
|
|
| `fetchProjects` | GET | /projects | Load project list |
|
|
| `createProject` | POST | /projects | Create a new project |
|
|
| `updateProject` | PATCH | /projects/:id | Update a project |
|
|
| `deleteProject` | DELETE | /projects/:id | Delete a project |
|
|
|
|
`streamMessage` returns an abort function — call it to cancel a stream mid-flight.
|
|
Uses a buffer pattern to handle SSE chunks that may span multiple network packets.
|
|
|
|
## Streaming
|
|
|
|
The chat input sends messages via `POST /chat/stream`. Tokens arrive as SSE events:
|
|
|
|
```
|
|
data: {"text":"Hello"}
|
|
data: {"text":" Tim"}
|
|
data: {"done":true,"model":"gemma-4-26B-A4B-Claude-Distill-APEX-I-Mini.gguf","tokenCount":87}
|
|
```
|
|
|
|
An empty assistant bubble is appended immediately when the stream opens, then
|
|
updated token by token using `updateLastMessage`. The blinking cursor in
|
|
`MessageBubble` is shown while `message.streaming === true` and disappears
|
|
when the done event is received. Model name and token count from the done
|
|
event are stored in `useChat` state and displayed in the InfoPanel.
|
|
|
|
## Dynamic Model Selector
|
|
|
|
Available models are fetched from `GET /models` on mount via the `useModels` hook.
|
|
The hook initialises with `FALLBACK_MODELS` from `constants.js` and replaces them
|
|
with the server response on success. If the fetch fails, the fallback list is used
|
|
silently — a warning is logged to the console.
|
|
|
|
To add a model, update `models.json` on the main PC — no client rebuild needed.
|
|
|
|
`FALLBACK_MODELS` in `constants.js` should be kept in sync with `models.json`
|
|
as a reasonable last-resort list in case the endpoint is unreachable.
|
|
|
|
## 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.
|
|
|
|
### Session Name Display
|
|
|
|
The chat header and session rows both display `session.name` if set, falling back
|
|
to `session.external_id` if no name has been assigned:
|
|
|
|
```js
|
|
activeSession.name || activeSession.external_id
|
|
```
|
|
|
|
### Session Actions
|
|
|
|
Session rows in the sidebar support rename and delete via two entry points:
|
|
|
|
- **Hover** — reveals ✎ (rename) and ✕ (delete) icon buttons alongside the row
|
|
- **Right-click** — opens a context menu with the same actions
|
|
|
|
Both trigger `SessionModal` — a shared modal component with two modes:
|
|
|
|
| Mode | Trigger | Behaviour |
|
|
|---|---|---|
|
|
| `settings` | Rename button / context menu rename | Shows name input, saves on Enter or Save button |
|
|
| `confirm-delete` | Delete button / context menu delete | Shows confirmation dialog, requires explicit Delete click |
|
|
|
|
Actions are disabled on unsaved (new) sessions that haven't had a first message sent yet.
|
|
|
|
### Active Session Clearing on Delete
|
|
|
|
When the deleted session is the currently active one, `App.jsx` detects the match
|
|
and calls `selectSession(null)` to clear the chat window before refreshing the list:
|
|
|
|
```js
|
|
function handleSessionsChange(deletedSession) {
|
|
if (deletedSession?.external_id === activeSession?.external_id) {
|
|
selectSession(null);
|
|
}
|
|
refreshSessions();
|
|
}
|
|
```
|
|
|
|
### Context Menu
|
|
|
|
Implemented via `useContextMenu` hook — tracks `{ x, y, session }` state and
|
|
attaches a `window` click listener to dismiss on any outside click. Rendered
|
|
outside the sidebar div via a React fragment to avoid being clipped by
|
|
`overflow: hidden`.
|
|
|
|
### Button Nesting
|
|
|
|
Session row action icons (✎ ✕) are rendered as siblings of the session
|
|
`<button>`, not children — HTML does not allow `<button>` inside `<button>`.
|
|
The outer `<div>` owns hover state and context menu; the inner `<button>` handles
|
|
session selection; action icon buttons sit alongside it in the same flex row.
|
|
|
|
## Project Management
|
|
|
|
Projects are a first-class concept in the UI. The `useProjects` hook fetches
|
|
the project list from `GET /projects` on mount and exposes a `refreshProjects`
|
|
callback for keeping the sidebar in sync after mutations.
|
|
|
|
### Project Actions
|
|
|
|
Projects are managed from `AllProjectsView` via `ProjectModal`:
|
|
|
|
| Mode | Behaviour |
|
|
|---|---|
|
|
| `create` | Name (required), description (optional), colour picker |
|
|
| `edit` | Same fields as create, pre-populated |
|
|
| `confirm-delete` | Confirmation dialog — sessions in the project are not deleted |
|
|
|
|
The sidebar Projects section shows up to 6 project tiles as coloured badge buttons.
|
|
Clicking any tile navigates to `AllProjectsView`. The "All Projects →" link is
|
|
always shown below the tiles.
|
|
|
|
After any create, edit, or delete in `AllProjectsView`, `onProjectsChange` is called
|
|
to trigger `refreshProjects` in `App.jsx`, keeping the sidebar tiles in sync.
|
|
|
|
## View Routing
|
|
|
|
`App.jsx` manages a `view` state string that controls which main panel renders:
|
|
|
|
| View | Component | Trigger |
|
|
|---|---|---|
|
|
| `'chat'` | `ChatWindow` | Default; selecting a session from sidebar or AllChatsView |
|
|
| `'all-chats'` | `AllChatsView` | "All Chats →" link or ☰ icon in collapsed rail |
|
|
| `'all-projects'` | `AllProjectsView` | "All Projects →" link, ⊞ icon, or New Project button |
|
|
| `'settings'` | `SettingsView` | Settings button or ⚙ icon in collapsed rail |
|
|
|
|
`AllChatsView` navigates back to `'chat'` on session row click, passing the selected
|
|
session to `selectSession` so history loads immediately. |