# Navi Webclient — Technical Specification

## 1. Stack

| Concern | Solution |
|---|---|
| Framework | Vue 3 (Composition API) |
| Build | Vite |
| Language | JavaScript (ES modules) |
| State | Pinia |
| UI kit | gnexus-ui-kit (SCSS source + JS utilities) |
| Markdown | marked.js |
| Syntax highlight | highlight.js + `summerfruit-dark` theme (from UI kit) |
| Icons | Phosphor Icons (font, from UI kit) |
| Virtual scroll | `vue-virtual-scroller` |
| Deployment | Vite builds to `dist/`, served via FastAPI `/static/` |

---

## 2. Project structure

```
navi-webclient/
├── index.html
├── vite.config.js
├── package.json
├── public/
│   └── assets/          # copied from gnexus-ui-kit/public/assets (fonts, icons)
├── src/
│   ├── main.js           # app entry: Vue, Pinia, global styles
│   ├── App.vue           # root: layout shell, sidebar + chat area
│   ├── components/
│   │   ├── sidebar/
│   │   │   ├── AppSidebar.vue
│   │   │   ├── SessionList.vue
│   │   │   └── SessionItem.vue
│   │   ├── chat/
│   │   │   ├── ChatArea.vue
│   │   │   ├── ChatHeader.vue
│   │   │   ├── MessageList.vue
│   │   │   ├── InputBar.vue
│   │   │   └── FilePreviewStrip.vue
│   │   ├── messages/
│   │   │   ├── UserMessage.vue
│   │   │   ├── AssistantMessage.vue
│   │   │   ├── ThinkingCard.vue
│   │   │   ├── ToolCard.vue
│   │   │   ├── SubagentStep.vue
│   │   │   ├── SummaryCard.vue
│   │   │   └── CompressionNotice.vue
│   │   └── ui/
│   │       ├── WelcomeScreen.vue
│   │       ├── ContextBar.vue     # token indicator
│   │       └── ProfileBadge.vue
│   ├── stores/
│   │   ├── sessions.js
│   │   ├── profiles.js
│   │   └── chat.js
│   ├── composables/
│   │   ├── useWebSocket.js        # WS connection lifecycle
│   │   └── useFileUpload.js       # drag-drop + file/image handling
│   ├── api/
│   │   └── index.js               # REST API wrapper (fetch-based)
│   └── styles/
│       ├── main.scss              # imports kit + overrides
│       └── overrides.scss         # project-specific deviations from kit
```

---

## 3. Routing

Hash-based, no router library needed:

- `/#` or `/#<session_id>` — load session by ID
- On app init: read `location.hash` → load session or show welcome
- On session switch: `location.hash = session_id` (no page reload)
- `hashchange` event → switch active session

---

## 4. Pinia stores

### `useSessionsStore`

```js
state: {
  sessions: [],          // Session[] — full list from GET /sessions
  currentId: null,       // active session_id
}
actions:
  fetchSessions()        // GET /sessions
  createSession(profileId)  // POST /sessions
  deleteSession(id)      // DELETE /sessions/{id}
  pinSession(id, pinned) // PATCH /sessions/{id}/pin
  setCurrentId(id)
```

Designed to support pagination later: `sessions` will become a partial list when the API adds pagination. Keep fetch logic isolated here.

### `useProfilesStore`

```js
state: {
  profiles: [],          // AgentProfile[]
  selectedProfileId: null
}
actions:
  fetchProfiles()        // GET /agents/profiles
```

### `useChatStore`

```js
state: {
  messages: [],          // rendered message list for current session
  streaming: false,
  streamingMessageId: null,
  pendingImages: [],     // base64 strings
  pendingFiles: [],      // uploaded file objects {name, path, size}
  contextTokens: 0,
  maxContextTokens: 0,
}
actions:
  loadSession(id)        // GET /sessions/{id} → populate messages
  clearChat()
  appendMessage(msg)
  updateStreamingMessage(delta)
  finalizeStreaming(data)
  setContext(tokens, max)
```

---

## 5. WebSocket — `useWebSocket.js`

Composable (not a store) — manages one connection per active session.

### Connection lifecycle

```
connect(sessionId)
  → open ws://localhost:8000/ws/sessions/{id}
  → on open: clear reconnect state
  → on message: dispatch to chat store
  → on close/error: start reconnect sequence
```

### Reconnect strategy

1. Attempt 1–3: retry with 1s / 2s / 4s backoff (exponential)
2. After 3 failures: show toast "Connection lost. Reconnecting in background…"
3. Continue retrying every 15s silently
4. On success: dismiss toast, continue normally

### Auth extension point

```js
// composables/useWebSocket.js
function getAuthHeaders() {
  // TODO: return auth token when multi-user is ready
  return {};
}
```

The WS URL and any future auth token injection go through this single function.

### WS event dispatch (mirrors existing client)

| Event | Action |
|---|---|
| `stream_start` | `chat.streaming = true`, create streaming message slot |
| `thinking_delta` | append to thinking card text |
| `thinking_end` | collapse thinking card |
| `turn_thinking` | render complete thinking block (collapsed) |
| `plan_ready` | render plan card (collapsed) |
| `tool_started` | create pending tool card with spinner |
| `tool_call` | finalize tool card (result, success/failure color) |
| `stream_delta` | append to streaming bubble, re-render markdown |
| `stream_end` | finalize bubble, `streaming = false`, update context tokens |
| `stream_stopped` | `streaming = false`, re-enable input |
| `profile_switched` | update active profile in header |
| `context_compressed` | append compression notice |
| `error` | append inline error message to chat |

---

## 6. Component details

### `AppSidebar.vue`

- **Desktop:** fixed panel, 280px wide
- **Mobile:** full-screen drawer (uses gnexus-ui-kit `Drawer` component)
- Toggle button in `ChatHeader` on mobile
- Contains: logo + app name, profile `<select>`, "+ New Chat" button, `SessionList`

### `SessionList.vue`

Uses `vue-virtual-scroller` (`RecycleScroller`) for performance. Each item is a `SessionItem`.

- Pinned sessions rendered first (server sorts them)
- Active session highlighted
- Per-item: session preview text, pin icon, delete button
- Draft saved to `localStorage` keyed by `session_id` (restored on session switch)

### `MessageList.vue`

Uses `vue-virtual-scroller` with variable item height (`DynamicScroller`).

- Auto-scroll to bottom during streaming
- Maintains scroll position when switching sessions
- Scroll anchor: when user scrolls up manually, stop auto-scroll; resume on new message if within 100px of bottom

Message type routing:
```js
// each item in messages[] has a `role` and optional `type`
// role: 'user' | 'assistant' | 'system'
// type: 'summary' | 'compression_notice' | undefined
```

### `ThinkingCard.vue`

- `<details>` element
- Auto-closes on `thinking_end`
- Italic text style, distinct background from tool cards
- Re-openable

### `ToolCard.vue`

- Two phases: pending (spinner) → finalized (result)
- Left border accent: kit's `$color-secondary` (pending) → `$color-success` / `$color-error`
- Collapsed by default, expand to see args (JSON, monospace) + result
- Tool icon from `TOOL_ICONS` map; unknown → `ph-wrench`
- Subagent steps (`SubagentStep.vue`) nested inside `spawn_agent` cards

### `InputBar.vue`

- Auto-resize textarea (1–6 rows)
- Enter → send, Shift+Enter → newline
- Disabled during streaming (show stop button instead of send)
- File attach button → hidden `<input type="file" multiple>`
- **Drag-and-drop:** `dragover`/`drop` on the entire `ChatArea` — highlight drop zone on drag enter
- Image paste: intercept `paste` event → base64
- Stop button → `POST /sessions/{id}/stop`
- Upload progress bar (shown during file upload)

### `ContextBar.vue`

Shown in `ChatHeader`. Visible once first `stream_end` received.

```
[████████░░░░] 12,450 / 32,000 tokens (38%)
```

- Bar fill: color transitions: green → yellow (>60%) → orange (>80%)
- Compression fires at ≥80% (server-side), show visual warning
- Tooltip on hover with exact numbers

### `WelcomeScreen.vue`

Shown when no sessions exist (first launch). Contains:

- Navi logo + name
- Tagline (e.g. "Your personal AI assistant")
- Profile cards: one per active profile, icon + name + short description
- "Start a conversation" button → creates session with selected profile → loads chat

---

## 7. gnexus-ui-kit integration

### SCSS

Import kit source directly (not compiled) so updates propagate:

```scss
// src/styles/main.scss
@use '../../node_modules/gnexus-ui-kit/src/scss/kit' as *;
@use './overrides';
```

The kit is added as a git submodule or npm-linked local package. Do **not** copy-paste kit SCSS into this project.

Override only via `src/styles/overrides.scss` — never edit kit files directly.

### JS utilities

Called imperatively where needed, not wrapped in Vue components:

```js
// example: show toast from any composable or component
import { Toasts } from 'gnexus-ui-kit'
Toasts.createError('Connection lost', 'Reconnecting…').show()
```

Components that require DOM init (Accordion, etc.) call `.init()` in `onMounted`.

### Icons

```html
<i class="ph ph-paper-plane-tilt"></i>  <!-- send -->
<i class="ph ph-sidebar"></i>            <!-- toggle sidebar -->
<i class="ph ph-stop-circle"></i>        <!-- stop generation -->
```

---

## 8. Responsive layout

### Desktop (≥768px)

```
┌─────────────┬──────────────────────────────┐
│  Sidebar    │  Chat area                   │
│  280px      │  flex: 1                     │
│  fixed      │                              │
└─────────────┴──────────────────────────────┘
```

### Mobile (<768px)

```
┌──────────────────────────────────────────┐
│  [☰]  Session title          [tokens]   │  ← ChatHeader
├──────────────────────────────────────────┤
│                                          │
│  Messages                                │
│                                          │
├──────────────────────────────────────────┤
│  [📎]  [textarea...]          [send ↑]  │  ← InputBar
└──────────────────────────────────────────┘

Sidebar: full-screen drawer, opens over content
```

---

## 9. Animations

Using kit motion tokens (`$motion-fast: 0.15s`, `$motion-base: 0.2s`, `$motion-slow: 0.28s`).

| Element | Animation |
|---|---|
| Session switch | Chat area fade-out → fade-in (`0.15s`) |
| New message appear | Slide up + fade in (`0.2s`) |
| Thinking/tool card expand | Height transition via `<details>` + CSS (`0.2s`) |
| Sidebar drawer (mobile) | Slide in from left (`0.28s ease`) |
| Toast | Kit's built-in `a-show` / `a-hide` classes |
| Streaming text | No animation — raw append for performance |

Vue `<Transition>` components used for session switch and message appearance.

---

## 10. Error handling

| Error type | Display |
|---|---|
| Network / WS disconnect | Toast (kit `Toasts.createError`) |
| WS reconnect failed (3 attempts) | Persistent toast with status |
| Agent error event from server | Inline in chat (red error bubble) |
| REST API failure (create session, delete, etc.) | Toast |
| File upload failure | Toast + remove file from preview strip |

---

## 11. File and image attachment

### Images
1. Paste (`paste` event) or file picker (image types)
2. `FileReader.readAsDataURL()` → base64 → `pendingImages[]`
3. Preview thumbnail in `FilePreviewStrip` with remove button
4. On send: included in WS message `{ images: [...base64] }`

### Files
1. File picker (any type) or **drag-and-drop onto ChatArea**
2. Upload via `POST /sessions/{id}/files` (multipart)
3. Response `{ name, path, size, content_type }` → `pendingFiles[]`
4. Preview pill in `FilePreviewStrip`
5. On send: included in WS message `{ files: [...] }`

Drag-and-drop: visual highlight on `ChatArea` when dragging over, handled in `useFileUpload.js` composable shared between drag events and the attach button.

---

## 12. Build and deployment

```js
// vite.config.js
export default {
  build: {
    outDir: '../navi-1/client',   // outputs directly into FastAPI's static dir
    emptyOutDir: true
  }
}
```

FastAPI serves the built files from `GET /static/**`. `index.html` is served from `GET /` by the existing catch-all route.

---

## 13. Initialization sequence

```
1. main.js → mount App, init Pinia
2. App.vue onMounted:
   a. profilesStore.fetchProfiles()    → populate sidebar profile selector
   b. sessionsStore.fetchSessions()    → populate session list
   c. Read location.hash
      ├─ valid session_id → chatStore.loadSession(id) → connect WS
      └─ no sessions at all → show WelcomeScreen
3. loadSession(id):
   a. GET /sessions/{id} → render message history
   b. Update location.hash
   c. useWebSocket.connect(id)
   d. Restore draft from localStorage
```

---

## 14. Out of scope (this version)

- Keyboard shortcuts (planned for later)
- Session search/filter (server-side feature, planned)
- Authentication UI (extension point in code only)
- Light theme
- Session rename / export
- Message search
