Files
nexusAI/docs/services/chat-client.md
2026-04-19 02:09:12 -07:00

16 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
  • react-markdown — Markdown rendering in message bubbles and memory viewer
  • 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:

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',
      '/episodes': 'http://192.168.0.205:4000',
      '/settings': 'http://192.168.0.205:4000',
      '/health':   'http://192.168.0.205:4000',
    }
  }
});

When adding new top-level routes to the orchestration service, add a matching entry here and in the Caddy config.

Internal Structure

src/
├── api/
│   └── orchestration.js     # All fetch calls to the orchestration service
├── config/
│   └── constants.js         # FALLBACK_MODELS, DEFAULT_MODEL, API_DEFAULTS, CLIENT_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
│   ├── useSettings.js       # Settings fetch + saveSetting helper
│   └── useContextMenu.js    # Right-click context menu position and visibility
├── components/
│   ├── App.jsx              # Root component — layout, shared state, view routing
│   ├── Sidebar.jsx          # Left sidebar — projects, grouped recent chats, navigation
│   ├── HomeView.jsx         # Landing screen — greeting, centred input, quick actions
│   ├── ChatWindow.jsx       # Centre panel — message thread, back button, model pill
│   ├── MessageBubble.jsx    # Individual message bubble — renders markdown via react-markdown
│   ├── InfoPanel.jsx        # Right panel — model selector and session metadata (slide-in)
│   ├── SessionModal.jsx     # Modal for session rename, project assignment, delete
│   ├── ProjectModal.jsx     # Modal for project create, edit, delete
│   ├── AllChatsView.jsx     # Paginated session list with project indicator column
│   ├── AllProjectsView.jsx  # Project tile grid with create/edit/delete; tile click navigates to ProjectView
│   ├── ProjectView.jsx      # Individual project — conversations, new chat input, memory
│   │                        #   placeholder, user notes, ⋮ edit/delete menu
│   ├── MemoryView.jsx       # Paginated, searchable, expandable, deletable episode viewer
│   └── SettingsView.jsx     # Settings — Memory limits, Models (inference params, active
│                            #   model, context window), Service Health, Appearance placeholder
├── index.css                # Global reset, CSS variables, utility classes
└── main.jsx                 # React entry point

Layout

The app uses a view-based layout. App.jsx manages a view state string that controls which main panel is rendered. The left sidebar and right info panel are persistent across all views.

┌──────────────────┬──────────────────────────────┐
│     Sidebar      │   Main Area (view-dependent)  │
│  (collapsible)   │                               │
│                  │  home         → HomeView      │
│ + New Chat       │  chat         → ChatWindow    │
│ ⊞ View Projects  │  all-chats    → AllChatsView  │
│                  │  all-projects → AllProjectsView│
│ PROJECTS ▾       │  project      → ProjectView   │
│  [tile] [tile]   │  settings     → SettingsView  │
│  All Projects →  │  memory       → MemoryView    │
│                  │                               │
│ RECENT CHATS ▾   │                               │
│  ● Project A     │                               │
│    Session 1     │                               │
│    Session 2     │                               │
│  ● Project B     │                               │
│    Session 3     │                               │
│  Other           │                               │
│    Session 4     │                               │
│  All Chats →     │                               │
│                  │                               │
│ ⚙ Settings       │                               │
└──────────────────┴──────────────────────────────┘

The sidebar collapses to a 48px icon rail and starts collapsed on the home view. The right InfoPanel slides in from the right using transform: translateX() — hidden by default, toggled via the button in the ChatWindow header.

View Routing

View Component Trigger
'home' HomeView Initial load; going back from chat with no history
'chat' ChatWindow Selecting a session; new chat; sending from HomeView
'all-chats' AllChatsView "All Chats →" or ☰ icon in collapsed rail
'all-projects' AllProjectsView "View Projects" button or ⊞ icon
'project' ProjectView Clicking a project tile in sidebar or AllProjectsView
'settings' SettingsView Settings button or ⚙ icon
'memory' MemoryView "Open →" button in Settings → Memory section

activeProject state in App.jsx tracks which project ProjectView is displaying. Set via onSelectProject before navigating to 'project'.

View History Stack

App.jsx maintains a viewHistory array. Each navigate(view) call pushes the current view onto the stack. goBack() pops the last entry and restores it. All view components receive onBack={goBack} — no component hardcodes its own back destination. Navigating to 'home' collapses the sidebar; leaving 'home' expands it.

Home View

HomeView is the landing screen shown on initial load. It displays:

  • Time-based greeting ("Morning / Afternoon / Evening, Tim")
  • Currently loaded model name (from modelProps.modelAlias, stripped of .gguf)
  • Centred textarea input — sending creates a new session and navigates to chat
  • Quick action pills that populate the input without auto-sending

Sending from HomeView uses handleHomeSend in App.jsx, which calls createSession() (returns the new session object), then immediately calls sendMessage with the session passed directly as a parameter — avoiding the React state settling race condition that would cause the message to be dropped.

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

Streaming

Messages are sent via POST /chat/stream. Tokens arrive as SSE events and are written into the active assistant bubble token by token via updateLastMessage. The blinking cursor in MessageBubble is shown while message.streaming === true.

useChat.sendMessage accepts an optional session parameter (4th arg) that overrides the closed-over activeSession. This is used by handleHomeSend and handleNewProjectChat in App.jsx to pass the newly created session object directly, avoiding React state settling races.

useChat accepts an optional projectId parameter in sendMessage. After the first message completes in a new session, if projectId is set, updateSession is called to write the project assignment to the backend.

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.

useSession.createSession returns the new session object — callers can pass it directly to sendMessage rather than waiting for React state to update.

useSession.selectSession skips the history fetch for new (isNew: true) sessions — fetching history for an unsaved session would 404 since it doesn't exist in the backend yet.

Auto-naming

After the first exchange completes, orchestration fires a secondary inference call with a short naming prompt (max 20 tokens, temperature 0.3). The result is written back as session.name. The client fires a second refreshSessions after a 3-second delay to pick up the name once written.

Manually renamed sessions are never overwritten — the !session.name guard in chat/index.js prevents this.

Session Actions

Session rows support rename, project assignment, and delete via:

  • Hover — reveals ✎ and ✕ icon buttons alongside the row
  • Right-click — context menu with the same actions

SessionModal handles rename and project assignment together in settings mode, and delete confirmation in confirm-delete mode.

Key Patterns

  • Button nesting: action icons are siblings of row buttons, not children — HTML forbids <button> inside <button>
  • Context menu rendered outside sidebar via React fragment to avoid overflow: hidden clipping
  • useContextMenu dismisses on a window click listener
  • Dynamic updateSession SQL builds SET clause from only the fields passed — prevents accidental overwrites
  • AllChatsView pagination uses CLIENT_DEFAULTS.PAGE_SIZE (not API_DEFAULTS.PAGE_SIZE which doesn't exist)

Sidebar — Session Grouping

Recent sessions in the sidebar are grouped by project under a colour dot + project name label. Unassigned sessions appear under "Other" if any project groups are present. The grouping is computed client-side from the sessions array and projects list already available in App.jsx — no extra API call.

AllChatsView receives projects as a prop from App.jsx and displays a project indicator column (colour dot + truncated name) in each session row.

Project Management

All projects are isolated by default (isolated: 1 hardcoded on create). The isolated toggle has been removed from ProjectModal.

useProjects fetches the project list from GET /projects on mount and exposes refreshProjects for keeping the sidebar in sync after mutations.

ProjectModal handles create, edit, and delete confirmation. Fields: name (required), description (optional), colour picker.

Clicking a project tile in AllProjectsView calls onSelectProject then navigates to 'project'.

ProjectView

ProjectView is a full project workspace with:

  • Colour accent bar + project title + description
  • ⋮ dropdown menu for edit (opens ProjectModal pre-filled) and delete
  • Conversations list — each session is a clickable row navigating to 'chat'
  • ChatInput component below the list (or centred when no sessions exist) for starting new project-tied conversations without a separate button
  • Project Memory — placeholder section explaining upcoming auto-summary feature
  • Project Notes — textarea with Save button; notes saved to projects.notes column in SQLite; save button only appears when content has changed from last saved value (savedNotes state tracks the baseline, not initialNotes)

updateProject in orchestration.js uses a passthrough pattern — spreads all fields directly into the request body, only transforming isolated if present. This allows partial updates like { notes } without clobbering other fields.

For memory isolation behaviour, see memory-isolation.md.

Settings

useSettings fetches from GET /settings on mount and exposes a saveSetting(key, value) helper that issues a PATCH /settings with a single key-value pair. The saving boolean is exposed for disabling save buttons during in-flight requests.

SettingsView receives settings/saveSetting/saving from a single useSettings() call at the top level and passes them as props to ModelsSection and ModelsFolderSetting — avoiding triple fetch on mount. modelProps (context window, loaded model) is fetched once in App.jsx and passed down as a prop, eliminating a duplicate fetch on every settings open.

SettingsView is organised into sections:

  • Memory — recent episode limit, semantic limit, score threshold, link to MemoryView
  • Models — models folder path, temperature, repeat penalty, Top-P, Top-K, active model dropdown, read-only model info panel (file, size, context window, loaded model from llama-server)
  • About — service health check panel, version
  • Appearance — theme (coming soon)

An error boundary (SettingsSectionErrorBoundary) wraps the Models section — if the models fetch fails, only that section shows an error with a Retry button rather than blanking the entire settings view.