11 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 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_URLto the HTTPS domain during local dev. Requests fromlocalhost:5173tonexus.jellystorm.comwill hit Authelia, which returns an HTML login page instead of JSON — causingUnexpected token '<'parse errors inuseModelsanduseSession.
Vite Dev Proxy
vite.config.js proxies API routes directly to the orchestration service
during local development, bypassing Caddy and Authelia entirely:
// 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:
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:
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.