# 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 ```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 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 — name, description, colour, │ │ # system prompt override; delete confirmation │ ├── 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, Models, Behaviour (system prompt), │ # About, Appearance ├── 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 | | `'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 `handleHomeSend` in `App.jsx` calls `createSession()` (which returns the new session object), then immediately calls `sendMessage` with the session passed directly — avoiding the React state settling race condition. ## 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 `