Files
nexusAI/docs/services/chat-client.md

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 framework
  • uuid — session ID generation
  • vite + @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_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:

// 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.