# 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', } } }); ``` 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 │ └── useContextMenu.js # Right-click context menu position and visibility ├── components/ │ ├── App.jsx # Root component — layout and shared state │ ├── SessionList.jsx # Left sidebar — session list, rename, delete │ ├── 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 dialog for session settings and delete confirmation ├── index.css # Global reset, CSS variables, utility classes └── main.jsx # React entry point ``` ## Layout Three-panel layout with collapsible sidebars: ``` ┌─────────────────┬──────────────────────────┬─────────────┐ │ Session List │ Chat Window │ Info Panel │ │ (collapsible) │ │ (collapsible)│ │ │ [message thread] │ │ │ + New Chat │ │ Model │ │ │ │ Session ID │ │ Session 1 │ │ Token count │ │ Session 2 │ │ │ │ │ [input bar] │ │ └─────────────────┴──────────────────────────┴─────────────┘ ``` Sidebars collapse to a 56px icon rail. The centre chat window always fills the remaining space. ## 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 in all three 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 | `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 list 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 The session list supports rename and delete via two entry points: - **Hover** — reveals ✎ (rename) and ✕ (delete) icon buttons alongside the session 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 with session name, requires explicit Delete button click | The modal is intentionally titled "Session Settings" and structured to expand into a full settings panel in future iterations. 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 `