8.6 KiB
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 frameworkuuid— session ID generationvite+@vitejs/plugin-react— build tooling
Build
cd packages/chat-client
npm run build # outputs to dist/
npm run dev # local dev server on port 5173
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.
After building, copy dist/ contents to /srv/nexusai on Mini PC 2 for Caddy to serve.
Environment Variables
| Variable | Required | Default | Description |
|---|---|---|---|
| VITE_ORCHESTRATION_URL | No | '' (empty) |
Orchestration base URL. Must be set to the HTTPS domain in production to avoid mixed content errors. |
Production value:
VITE_ORCHESTRATION_URL=https://nexus.jellystorm.com
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 (rename)
├── 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 |
#1a1d27 |
Panel backgrounds |
--bg-elevated |
#222536 |
Elevated elements (inputs, cards) |
--border |
#2e3150 |
Border colour |
--accent |
#6c63ff |
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 |
#6c63ff |
User message bubble background |
--bubble-ai |
#222536 |
AI message bubble background |
--sidebar-width |
280px |
Expanded sidebar width |
--panel-width |
260px |
Expanded info panel width |
--header-height |
56px |
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.
// constants.js
export const FALLBACK_MODELS = [
{ value: 'companion:latest', label: 'Companion' },
// ...
];
The selected model is passed with every chat request. To add a model, update
models.json on the main PC — no client rebuild needed.
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 Actions
The session list supports rename and delete:
- Hover — reveals ✎ (rename) and ✕ (delete) icon buttons on the session row
- Right-click — opens a context menu with the same actions
Rename opens a SessionModal dialog. The modal is designed to expand into a full
session settings panel in future — the title is already "Session Settings" to
reflect this intent.
Delete is immediate with no confirmation dialog (planned for a future update).
Actions are disabled on unsaved (new) sessions that haven't had a message sent yet.
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 React fragment) to avoid being clipped by
overflow: hidden.