kazutab commited on
Commit
ef6e870
·
verified ·
1 Parent(s): 0475af5

Upload folder using huggingface_hub (part 8)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +2 -0
  2. backend/llama.cpp/tools/ui/README.md +688 -0
  3. backend/llama.cpp/tools/ui/docs/architecture/high-level-architecture.md +373 -0
  4. backend/llama.cpp/tools/ui/docs/flows/chat-flow.md +228 -0
  5. backend/llama.cpp/tools/ui/docs/flows/conversations-flow.md +183 -0
  6. backend/llama.cpp/tools/ui/docs/flows/data-flow-simplified-model-mode.md +45 -0
  7. backend/llama.cpp/tools/ui/docs/flows/data-flow-simplified-router-mode.md +77 -0
  8. backend/llama.cpp/tools/ui/docs/flows/database-flow.md +174 -0
  9. backend/llama.cpp/tools/ui/docs/flows/mcp-flow.md +226 -0
  10. backend/llama.cpp/tools/ui/docs/flows/models-flow.md +181 -0
  11. backend/llama.cpp/tools/ui/docs/flows/server-flow.md +76 -0
  12. backend/llama.cpp/tools/ui/docs/flows/settings-flow.md +156 -0
  13. backend/llama.cpp/tools/ui/embed.cpp +306 -0
  14. backend/llama.cpp/tools/ui/eslint.config.js +59 -0
  15. backend/llama.cpp/tools/ui/package-lock.json +0 -0
  16. backend/llama.cpp/tools/ui/package.json +107 -0
  17. backend/llama.cpp/tools/ui/playwright.config.ts +31 -0
  18. backend/llama.cpp/tools/ui/pwa-assets-dark.config.ts +27 -0
  19. backend/llama.cpp/tools/ui/pwa-assets.config.ts +68 -0
  20. backend/llama.cpp/tools/ui/scripts/dev.sh +61 -0
  21. backend/llama.cpp/tools/ui/scripts/favicon-colorize.ts +107 -0
  22. backend/llama.cpp/tools/ui/scripts/git-hooks/install.sh +35 -0
  23. backend/llama.cpp/tools/ui/scripts/git-hooks/pre-commit.sh +57 -0
  24. backend/llama.cpp/tools/ui/scripts/git-hooks/pre-push.sh +67 -0
  25. backend/llama.cpp/tools/ui/scripts/make-icons-circular.js +137 -0
  26. backend/llama.cpp/tools/ui/scripts/vite-plugin-build-info.ts +41 -0
  27. backend/llama.cpp/tools/ui/scripts/vite-plugin-relativize-base.ts +61 -0
  28. backend/llama.cpp/tools/ui/scripts/vite-plugin-splash-screen.ts +115 -0
  29. backend/llama.cpp/tools/ui/sources.cmake +16 -0
  30. backend/llama.cpp/tools/ui/src/.gitignore +1 -0
  31. backend/llama.cpp/tools/ui/src/app.css +200 -0
  32. backend/llama.cpp/tools/ui/src/app.d.ts +146 -0
  33. backend/llama.cpp/tools/ui/src/app.html +22 -0
  34. backend/llama.cpp/tools/ui/src/lib/actions/fade-in-view.svelte.ts +49 -0
  35. backend/llama.cpp/tools/ui/src/lib/assets/logo.svg +7 -0
  36. backend/llama.cpp/tools/ui/src/lib/components/app/SKILL.md +11 -0
  37. backend/llama.cpp/tools/ui/src/lib/components/app/actions/ActionIcon.svelte +81 -0
  38. backend/llama.cpp/tools/ui/src/lib/components/app/actions/ActionIconCopyToClipboard.svelte +17 -0
  39. backend/llama.cpp/tools/ui/src/lib/components/app/actions/index.ts +13 -0
  40. backend/llama.cpp/tools/ui/src/lib/components/app/badges/BadgeInfo.svelte +26 -0
  41. backend/llama.cpp/tools/ui/src/lib/components/app/badges/BadgesModality.svelte +36 -0
  42. backend/llama.cpp/tools/ui/src/lib/components/app/badges/index.ts +13 -0
  43. backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsList.svelte +119 -0
  44. backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItem.svelte +132 -0
  45. backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItemMcpPrompt.svelte +41 -0
  46. backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItemMcpResource.svelte +89 -0
  47. backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItemThumbnailFile.svelte +186 -0
  48. backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItemThumbnailImage.svelte +65 -0
  49. backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview.svelte +212 -0
  50. backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreviewCurrentItem/ChatAttachmentsPreviewCurrentItem.svelte +74 -0
.gitattributes CHANGED
@@ -65,3 +65,5 @@ backend/llama.cpp/tools/mtmd/test-1.jpeg filter=lfs diff=lfs merge=lfs -text
65
  backend/llama.cpp/tools/mtmd/test-2.mp3 filter=lfs diff=lfs merge=lfs -text
66
  backend/llama.cpp/tools/mtmd/test-3.mp4 filter=lfs diff=lfs merge=lfs -text
67
  backend/llama.cpp/tools/mtmd/tests/test-1-positive.png filter=lfs diff=lfs merge=lfs -text
 
 
 
65
  backend/llama.cpp/tools/mtmd/test-2.mp3 filter=lfs diff=lfs merge=lfs -text
66
  backend/llama.cpp/tools/mtmd/test-3.mp4 filter=lfs diff=lfs merge=lfs -text
67
  backend/llama.cpp/tools/mtmd/tests/test-1-positive.png filter=lfs diff=lfs merge=lfs -text
68
+ backend/llama.cpp/tools/ui/tests/stories/fixtures/assets/beautiful-flowers-lotus.webp filter=lfs diff=lfs merge=lfs -text
69
+ backend/llama.cpp/tools/ui/tests/stories/fixtures/assets/example.pdf filter=lfs diff=lfs merge=lfs -text
backend/llama.cpp/tools/ui/README.md ADDED
@@ -0,0 +1,688 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # llama-ui
2
+
3
+ A modern, feature-rich web interface for llama-server built with SvelteKit. This UI provides an intuitive chat interface with advanced file handling, conversation management, and comprehensive model interaction capabilities.
4
+
5
+ Llama UI supports two server operation modes:
6
+
7
+ - **MODEL mode** - Single model operation (standard llama-server)
8
+ - **ROUTER mode** - Multi-model operation with dynamic model loading/unloading
9
+
10
+ ---
11
+
12
+ ## Table of Contents
13
+
14
+ - [Features](#features)
15
+ - [Getting Started](#getting-started)
16
+ - [Tech Stack](#tech-stack)
17
+ - [Build Pipeline](#build-pipeline)
18
+ - [Architecture](#architecture)
19
+ - [Data Flows](#data-flows)
20
+ - [Architectural Patterns](#architectural-patterns)
21
+ - [Testing](#testing)
22
+
23
+ ---
24
+
25
+ ## Features
26
+
27
+ ### Chat Interface
28
+
29
+ - **Streaming responses** with real-time updates
30
+ - **Reasoning content** - Support for models with thinking/reasoning blocks
31
+ - **Dark/light theme** with system preference detection
32
+ - **Responsive design** for desktop and mobile
33
+
34
+ ### File Attachments
35
+
36
+ - **Images** - JPEG, PNG, GIF, WebP, SVG (with PNG conversion)
37
+ - **Documents** - PDF (text extraction or image conversion for vision models)
38
+ - **Audio** - MP3, WAV for audio-capable models
39
+ - **Text files** - Source code, markdown, and other text formats
40
+ - **Drag-and-drop** and paste support with rich previews
41
+
42
+ ### Conversation Management
43
+
44
+ - **Branching** - Branch messages conversations at any point by editing messages or regenerating responses, navigate between branches
45
+ - **Regeneration** - Regenerate responses with optional model switching (ROUTER mode)
46
+ - **Import/Export** - JSON format for backup and sharing
47
+ - **Search** - Find conversations by title or content
48
+
49
+ ### Advanced Rendering
50
+
51
+ - **Syntax highlighting** - Code blocks with language detection
52
+ - **Math formulas** - KaTeX rendering for LaTeX expressions
53
+ - **Markdown** - Full GFM support with tables, lists, and more
54
+
55
+ ### Multi-Model Support (ROUTER mode)
56
+
57
+ - **Model selector** with Loaded/Available groups
58
+ - **Automatic loading** - Models load on selection
59
+ - **Modality validation** - Prevents sending images to non-vision models
60
+ - **LRU unloading** - Server auto-manages model cache
61
+
62
+ ### Keyboard Shortcuts
63
+
64
+ | Shortcut | Action |
65
+ | ------------------ | -------------------- |
66
+ | `Shift+Ctrl/Cmd+O` | New chat |
67
+ | `Shift+Ctrl/Cmd+E` | Edit conversation |
68
+ | `Shift+Ctrl/Cmd+D` | Delete conversation |
69
+ | `Ctrl/Cmd+K` | Search conversations |
70
+ | `Ctrl/Cmd+B` | Toggle sidebar |
71
+
72
+ ### Developer Experience
73
+
74
+ - **Request tracking** - Monitor token generation with `/slots` endpoint
75
+ - **Storybook** - Component library with visual testing
76
+ - **Hot reload** - Instant updates during development
77
+
78
+ ---
79
+
80
+ ## Getting Started
81
+
82
+ ### Prerequisites
83
+
84
+ - **Node.js** 18+ (20+ recommended)
85
+ - **npm** 9+
86
+ - **llama-server** running locally (for API access)
87
+
88
+ ### 1. Install Dependencies
89
+
90
+ ```bash
91
+ cd tools/ui
92
+ npm install
93
+ ```
94
+
95
+ ### 2. Start llama-server
96
+
97
+ In a separate terminal, start the backend server:
98
+
99
+ ```bash
100
+ # Single model (MODEL mode)
101
+ ./llama-server -m model.gguf
102
+
103
+ # Multi-model (ROUTER mode)
104
+ ./llama-server --models-dir /path/to/models
105
+ ```
106
+
107
+ ### 3. Start Development Servers
108
+
109
+ ```bash
110
+ npm run dev
111
+ ```
112
+
113
+ This starts:
114
+
115
+ - **Vite dev server** at `http://localhost:5173` - The main UI frontend app
116
+ - **Storybook** at `http://localhost:6006` - Component documentation
117
+
118
+ The Vite dev server proxies API requests to `SERVER_ORIGIN` (with fallback to default llama-server `8080` port):
119
+
120
+ ```typescript
121
+ // vite.config.ts proxy configuration
122
+ proxy: {
123
+ '/v1': SERVER_ORIGIN,
124
+ '/props': SERVER_ORIGIN,
125
+ '/models': SERVER_ORIGIN,
126
+ '/tools': SERVER_ORIGIN,
127
+ '/slots': SERVER_ORIGIN,
128
+ '/cors-proxy': SERVER_ORIGIN
129
+ },
130
+ ```
131
+
132
+ ### Development Workflow
133
+
134
+ 1. Open `http://localhost:5173` in your browser
135
+ 2. Make changes to `.svelte`, `.ts`, or `.css` files
136
+ 3. Changes hot-reload instantly
137
+ 4. Use Storybook at `http://localhost:6006` for isolated component development
138
+
139
+ ---
140
+
141
+ ## Tech Stack
142
+
143
+ | Layer | Technology | Purpose |
144
+ | ----------------- | ------------------------------- | -------------------------------------------------------- |
145
+ | **Framework** | SvelteKit + Svelte 5 | Reactive UI with runes (`$state`, `$derived`, `$effect`) |
146
+ | **UI Components** | shadcn-svelte + bits-ui | Accessible, customizable component library |
147
+ | **Styling** | TailwindCSS 4 | Utility-first CSS with design tokens |
148
+ | **Database** | IndexedDB (Dexie) | Client-side storage for conversations and messages |
149
+ | **Build** | Vite | Fast bundling with static adapter |
150
+ | **Testing** | Playwright + Vitest + Storybook | E2E, unit, and visual testing |
151
+ | **Markdown** | remark + rehype | Markdown processing with KaTeX and syntax highlighting |
152
+
153
+ ### Key Dependencies
154
+
155
+ ```json
156
+ {
157
+ "svelte": "^5.0.0",
158
+ "bits-ui": "^2.8.11",
159
+ "dexie": "^4.0.11",
160
+ "pdfjs-dist": "^5.4.54",
161
+ "highlight.js": "^11.11.1",
162
+ "rehype-katex": "^7.0.1"
163
+ }
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Build Pipeline
169
+
170
+ ### Development Build
171
+
172
+ ```bash
173
+ npm run dev
174
+ ```
175
+
176
+ Runs Vite in development mode with:
177
+
178
+ - Hot Module Replacement (HMR)
179
+ - Source maps
180
+ - Proxy to llama-server
181
+
182
+ ### Production Build
183
+
184
+ ```bash
185
+ npm run build
186
+ ```
187
+
188
+ The build process:
189
+
190
+ 1. **Vite Build** - Bundles all TypeScript, Svelte, and CSS
191
+ 2. **Static Adapter** - Outputs to `../../build/tools/ui/dist` (llama-server's static file directory)
192
+ 3. **Post-Build Script** - Cleans up intermediate files
193
+ 4. **Custom Plugin** - Creates `index.html` with:
194
+ - Inlined favicon as base64
195
+ - GZIP compression (level 9)
196
+ - Deterministic output (zeroed timestamps)
197
+
198
+ ```text
199
+ tools/ui/ → build → build/tools/ui/dist/
200
+ ├── src/ ├── index.html (served by llama-server)
201
+ ├── static/ └── (favicon inlined)
202
+ └── ...
203
+ ```
204
+
205
+ ### SvelteKit Configuration
206
+
207
+ ```javascript
208
+ // svelte.config.js
209
+ adapter: adapter({
210
+ pages: '../../build/tools/ui/dist', // Output directory
211
+ assets: '../../build/tools/ui/dist', // Static assets
212
+ fallback: 'index.html', // SPA fallback
213
+ strict: true
214
+ }),
215
+ output: {
216
+ bundleStrategy: 'inline' // Single-file bundle
217
+ }
218
+ ```
219
+
220
+ ### Integration with llama-server
221
+
222
+ llama-ui is embedded directly into the llama-server binary:
223
+
224
+ 1. `npm run build` outputs `index.html` to `build/tools/ui/dist/`
225
+ 2. llama-server compiles this into the binary at build time
226
+ 3. When accessing `/`, llama-server serves the bundled HTML
227
+
228
+ This results in a **single portable binary** with the full Llama UI included.
229
+
230
+ ---
231
+
232
+ ## Architecture
233
+
234
+ Llama UI follows a layered architecture with unidirectional data flow:
235
+
236
+ ```text
237
+ Routes → Components → Hooks → Stores → Services → Storage/API
238
+ ```
239
+
240
+ ### High-Level Architecture
241
+
242
+ See: [`docs/architecture/high-level-architecture-simplified.md`](docs/architecture/high-level-architecture-simplified.md)
243
+
244
+ ```mermaid
245
+ flowchart TB
246
+ subgraph Routes["📍 Routes"]
247
+ R1["/ (Welcome)"]
248
+ R2["/chat/[id]"]
249
+ RL["+layout.svelte"]
250
+ end
251
+
252
+ subgraph Components["🧩 Components"]
253
+ C_Sidebar["ChatSidebar"]
254
+ C_Screen["ChatScreen"]
255
+ C_Form["ChatForm"]
256
+ C_Messages["ChatMessages"]
257
+ C_ModelsSelector["ModelsSelector"]
258
+ C_Settings["ChatSettings"]
259
+ end
260
+
261
+ subgraph Stores["🗄️ Stores"]
262
+ S1["chatStore"]
263
+ S2["conversationsStore"]
264
+ S3["modelsStore"]
265
+ S4["serverStore"]
266
+ S5["settingsStore"]
267
+ end
268
+
269
+ subgraph Services["⚙️ Services"]
270
+ SV1["ChatService"]
271
+ SV2["ModelsService"]
272
+ SV3["PropsService"]
273
+ SV4["DatabaseService"]
274
+ end
275
+
276
+ subgraph Storage["💾 Storage"]
277
+ ST1["IndexedDB"]
278
+ ST2["LocalStorage"]
279
+ end
280
+
281
+ subgraph APIs["🌐 llama-server"]
282
+ API1["/v1/chat/completions"]
283
+ API2["/props"]
284
+ API3["/models/*"]
285
+ end
286
+
287
+ R1 & R2 --> C_Screen
288
+ RL --> C_Sidebar
289
+ C_Screen --> C_Form & C_Messages & C_Settings
290
+ C_Screen --> S1 & S2
291
+ C_ModelsSelector --> S3 & S4
292
+ S1 --> SV1 & SV4
293
+ S3 --> SV2 & SV3
294
+ SV4 --> ST1
295
+ SV1 --> API1
296
+ SV2 --> API3
297
+ SV3 --> API2
298
+ ```
299
+
300
+ ### Layer Breakdown
301
+
302
+ #### Routes (`src/routes/`)
303
+
304
+ - **`/`** - Welcome screen, creates new conversation
305
+ - **`/chat/[id]`** - Active chat interface
306
+ - **`+layout.svelte`** - Sidebar, navigation, global initialization
307
+
308
+ #### Components (`src/lib/components/`)
309
+
310
+ Components are organized in `app/` (application-specific) and `ui/` (shadcn-svelte primitives).
311
+
312
+ **Chat Components** (`app/chat/`):
313
+
314
+ | Component | Responsibility |
315
+ | ------------------ | --------------------------------------------------------------------------- |
316
+ | `ChatScreen/` | Main chat container, coordinates message list, input form, and attachments |
317
+ | `ChatForm/` | Message input textarea with file upload, paste handling, keyboard shortcuts |
318
+ | `ChatMessages/` | Message list with branch navigation, regenerate/continue/edit actions |
319
+ | `ChatAttachments/` | File attachment previews, drag-and-drop, PDF/image/audio handling |
320
+ | `ChatSettings/` | Parameter sliders (temperature, top-p, etc.) with server default sync |
321
+ | `ChatSidebar/` | Conversation list, search, import/export, navigation |
322
+
323
+ **Dialog Components** (`app/dialogs/`):
324
+
325
+ | Component | Responsibility |
326
+ | ------------------------------- | -------------------------------------------------------- |
327
+ | `DialogChatSettings` | Full-screen settings configuration |
328
+ | `DialogModelInformation` | Model details (context size, modalities, parallel slots) |
329
+ | `DialogChatAttachmentPreview` | Full preview for images, PDFs (text or page view), code |
330
+ | `DialogConfirmation` | Generic confirmation for destructive actions |
331
+ | `DialogConversationTitleUpdate` | Edit conversation title |
332
+
333
+ **Server/Model Components** (`app/server/`, `app/models/`):
334
+
335
+ | Component | Responsibility |
336
+ | ------------------- | --------------------------------------------------------- |
337
+ | `ServerErrorSplash` | Error display when server is unreachable |
338
+ | `ModelsSelector` | Model dropdown with Loaded/Available groups (ROUTER mode) |
339
+
340
+ **Shared UI Components** (`app/misc/`):
341
+
342
+ | Component | Responsibility |
343
+ | -------------------------------- | ---------------------------------------------------------------- |
344
+ | `MarkdownContent` | Markdown rendering with KaTeX, syntax highlighting, copy buttons |
345
+ | `SyntaxHighlightedCode` | Code blocks with language detection and highlighting |
346
+ | `ActionButton`, `ActionDropdown` | Reusable action buttons and menus |
347
+ | `BadgeModality`, `BadgeInfo` | Status and capability badges |
348
+
349
+ #### Hooks (`src/lib/hooks/`)
350
+
351
+ - **`useModelChangeValidation`** - Validates model switch against conversation modalities
352
+ - **`useProcessingState`** - Tracks streaming progress and token generation
353
+
354
+ #### Stores (`src/lib/stores/`)
355
+
356
+ | Store | Responsibility |
357
+ | -------------------- | --------------------------------------------------------- |
358
+ | `chatStore` | Message sending, streaming, abort control, error handling |
359
+ | `conversationsStore` | CRUD for conversations, message branching, navigation |
360
+ | `modelsStore` | Model list, selection, loading/unloading (ROUTER) |
361
+ | `serverStore` | Server properties, role detection, modalities |
362
+ | `settingsStore` | User preferences, parameter sync with server defaults |
363
+
364
+ #### Services (`src/lib/services/`)
365
+
366
+ | Service | Responsibility |
367
+ | ---------------------- | ----------------------------------------------- |
368
+ | `ChatService` | API calls to`/v1/chat/completions`, SSE parsing |
369
+ | `ModelsService` | `/models`, `/models/load`, `/models/unload` |
370
+ | `PropsService` | `/props`, `/props?model=` |
371
+ | `DatabaseService` | IndexedDB operations via Dexie |
372
+ | `ParameterSyncService` | Syncs settings with server defaults |
373
+
374
+ ---
375
+
376
+ ## Data Flows
377
+
378
+ ### MODEL Mode (Single Model)
379
+
380
+ See: [`docs/flows/data-flow-simplified-model-mode.md`](docs/flows/data-flow-simplified-model-mode.md)
381
+
382
+ ```mermaid
383
+ sequenceDiagram
384
+ participant User
385
+ participant UI
386
+ participant Stores
387
+ participant DB as IndexedDB
388
+ participant API as llama-server
389
+
390
+ Note over User,API: Initialization
391
+ UI->>Stores: initialize()
392
+ Stores->>DB: load conversations
393
+ Stores->>API: GET /props
394
+ API-->>Stores: server config
395
+ Stores->>API: GET /v1/models
396
+ API-->>Stores: single model (auto-selected)
397
+
398
+ Note over User,API: Chat Flow
399
+ User->>UI: send message
400
+ Stores->>DB: save user message
401
+ Stores->>API: POST /v1/chat/completions (stream)
402
+ loop streaming
403
+ API-->>Stores: SSE chunks
404
+ Stores-->>UI: reactive update
405
+ end
406
+ Stores->>DB: save assistant message
407
+ ```
408
+
409
+ ### ROUTER Mode (Multi-Model)
410
+
411
+ See: [`docs/flows/data-flow-simplified-router-mode.md`](docs/flows/data-flow-simplified-router-mode.md)
412
+
413
+ ```mermaid
414
+ sequenceDiagram
415
+ participant User
416
+ participant UI
417
+ participant Stores
418
+ participant API as llama-server
419
+
420
+ Note over User,API: Initialization
421
+ Stores->>API: GET /props
422
+ API-->>Stores: {role: "router"}
423
+ Stores->>API: GET /models
424
+ API-->>Stores: models[] with status
425
+
426
+ Note over User,API: Model Selection
427
+ User->>UI: select model
428
+ alt model not loaded
429
+ Stores->>API: POST /models/load
430
+ loop poll status
431
+ Stores->>API: GET /models
432
+ end
433
+ Stores->>API: GET /props?model=X
434
+ end
435
+ Stores->>Stores: validate modalities
436
+
437
+ Note over User,API: Chat Flow
438
+ Stores->>API: POST /v1/chat/completions {model: X}
439
+ loop streaming
440
+ API-->>Stores: SSE chunks + model info
441
+ end
442
+ ```
443
+
444
+ ### Detailed Flow Diagrams
445
+
446
+ | Flow | Description | File |
447
+ | ------------- | ------------------------------------------ | ----------------------------------------------------------- |
448
+ | Chat | Message lifecycle, streaming, regeneration | [`chat-flow.md`](docs/flows/chat-flow.md) |
449
+ | Models | Loading, unloading, modality caching | [`models-flow.md`](docs/flows/models-flow.md) |
450
+ | Server | Props fetching, role detection | [`server-flow.md`](docs/flows/server-flow.md) |
451
+ | Conversations | CRUD, branching, import/export | [`conversations-flow.md`](docs/flows/conversations-flow.md) |
452
+ | Database | IndexedDB schema, operations | [`database-flow.md`](docs/flows/database-flow.md) |
453
+ | Settings | Parameter sync, user overrides | [`settings-flow.md`](docs/flows/settings-flow.md) |
454
+
455
+ ---
456
+
457
+ ## Architectural Patterns
458
+
459
+ ### 1. Reactive State with Svelte 5 Runes
460
+
461
+ All stores use Svelte 5's fine-grained reactivity:
462
+
463
+ ```typescript
464
+ // Store with reactive state
465
+ class ChatStore {
466
+ #isLoading = $state(false);
467
+ #currentResponse = $state('');
468
+
469
+ // Derived values auto-update
470
+ get isStreaming() {
471
+ return $derived(this.#isLoading && this.#currentResponse.length > 0);
472
+ }
473
+ }
474
+
475
+ // Exported reactive accessors
476
+ export const isLoading = () => chatStore.isLoading;
477
+ export const currentResponse = () => chatStore.currentResponse;
478
+ ```
479
+
480
+ ### 2. Unidirectional Data Flow
481
+
482
+ Data flows in one direction, making state predictable:
483
+
484
+ ```mermaid
485
+ flowchart LR
486
+ subgraph UI["UI Layer"]
487
+ A[User Action] --> B[Component]
488
+ end
489
+
490
+ subgraph State["State Layer"]
491
+ B --> C[Store Method]
492
+ C --> D[State Update]
493
+ end
494
+
495
+ subgraph IO["I/O Layer"]
496
+ C --> E[Service]
497
+ E --> F[API / IndexedDB]
498
+ F -.->|Response| D
499
+ end
500
+
501
+ D -->|Reactive| B
502
+ ```
503
+
504
+ Components dispatch actions to stores, stores coordinate with services for I/O, and state updates reactively propagate back to the UI.
505
+
506
+ ### 3. Per-Conversation State
507
+
508
+ Enables concurrent streaming across multiple conversations:
509
+
510
+ ```typescript
511
+ class ChatStore {
512
+ chatLoadingStates = new Map<string, boolean>();
513
+ chatStreamingStates = new Map<string, { response: string; messageId: string }>();
514
+ abortControllers = new Map<string, AbortController>();
515
+ }
516
+ ```
517
+
518
+ ### 4. Message Branching with Tree Structure
519
+
520
+ Conversations are stored as a tree, not a linear list:
521
+
522
+ ```typescript
523
+ interface DatabaseMessage {
524
+ id: string;
525
+ parent: string | null; // Points to parent message
526
+ children: string[]; // List of child message IDs
527
+ // ...
528
+ }
529
+
530
+ interface DatabaseConversation {
531
+ currentNode: string; // Currently viewed branch tip
532
+ // ...
533
+ }
534
+ ```
535
+
536
+ Navigation between branches updates `currentNode` without losing history.
537
+
538
+ ### 5. Layered Service Architecture
539
+
540
+ Stores handle state; services handle I/O:
541
+
542
+ ```text
543
+ ┌─────────────────┐
544
+ │ Stores │ Business logic, state management
545
+ ├─────────────────┤
546
+ │ Services │ API calls, database operations
547
+ ├─────────────────┤
548
+ │ Storage/API │ IndexedDB, LocalStorage, HTTP
549
+ └─────────────────┘
550
+ ```
551
+
552
+ ### 6. Server Role Abstraction
553
+
554
+ Single codebase handles both MODEL and ROUTER modes:
555
+
556
+ ```typescript
557
+ // serverStore.ts
558
+ get isRouterMode() {
559
+ return this.role === ServerRole.ROUTER;
560
+ }
561
+
562
+ // Components conditionally render based on mode
563
+ {#if isRouterMode()}
564
+ <ModelsSelector />
565
+ {/if}
566
+ ```
567
+
568
+ ### 7. Modality Validation
569
+
570
+ Prevents sending attachments to incompatible models:
571
+
572
+ ```typescript
573
+ // useModelChangeValidation hook
574
+ const validate = (modelId: string) => {
575
+ const modelModalities = modelsStore.getModelModalities(modelId);
576
+ const conversationModalities = conversationsStore.usedModalities;
577
+
578
+ // Check if model supports all used modalities
579
+ if (conversationModalities.hasImages && !modelModalities.vision) {
580
+ return { valid: false, reason: 'Model does not support images' };
581
+ }
582
+ // ...
583
+ };
584
+ ```
585
+
586
+ ### 8. Persistent Storage Strategy
587
+
588
+ Data is persisted across sessions using two storage mechanisms:
589
+
590
+ ```mermaid
591
+ flowchart TB
592
+ subgraph Browser["Browser Storage"]
593
+ subgraph IDB["IndexedDB (Dexie)"]
594
+ C[Conversations]
595
+ M[Messages]
596
+ end
597
+ subgraph LS["LocalStorage"]
598
+ S[Settings Config]
599
+ O[User Overrides]
600
+ T[Theme Preference]
601
+ end
602
+ end
603
+
604
+ subgraph Stores["Svelte Stores"]
605
+ CS[conversationsStore] --> C
606
+ CS --> M
607
+ SS[settingsStore] --> S
608
+ SS --> O
609
+ SS --> T
610
+ end
611
+ ```
612
+
613
+ - **IndexedDB**: Conversations and messages (large, structured data)
614
+ - **LocalStorage**: Settings, user parameter overrides, theme (small key-value data)
615
+ - **Memory only**: Server props, model list (fetched fresh on each session)
616
+
617
+ ---
618
+
619
+ ## Testing
620
+
621
+ ### Test Types
622
+
623
+ | Type | Tool | Location | Command |
624
+ | ------------- | ------------------ | ---------------- | ------------------- |
625
+ | **Unit** | Vitest | `tests/unit/` | `npm run test:unit` |
626
+ | **UI/Visual** | Storybook + Vitest | `tests/stories/` | `npm run test:ui` |
627
+ | **E2E** | Playwright | `tests/e2e/` | `npm run test:e2e` |
628
+ | **Client** | Vitest | `tests/client/`. | `npm run test:unit` |
629
+
630
+ ### Running Tests
631
+
632
+ ```bash
633
+ # All tests
634
+ npm run test
635
+
636
+ # Individual test suites
637
+ npm run test:e2e # End-to-end (requires llama-server)
638
+ npm run test:client # Client-side unit tests
639
+ npm run test:server # Server-side unit tests
640
+ npm run test:ui # Storybook visual tests
641
+ ```
642
+
643
+ ### Storybook Development
644
+
645
+ ```bash
646
+ npm run storybook # Start Storybook dev server on :6006
647
+ npm run build-storybook # Build static Storybook
648
+ ```
649
+
650
+ ### Linting and Formatting
651
+
652
+ ```bash
653
+ npm run lint # Check code style
654
+ npm run format # Auto-format with Prettier
655
+ npm run check # TypeScript type checking
656
+ ```
657
+
658
+ ---
659
+
660
+ ## Project Structure
661
+
662
+ ```text
663
+ tools/ui/
664
+ ├── src/
665
+ │ ├── lib/
666
+ │ │ ├── components/ # UI components (app/, ui/)
667
+ │ │ ├── hooks/ # Svelte hooks
668
+ │ │ ├── stores/ # State management
669
+ │ │ ├── services/ # API and database services
670
+ │ │ ├── types/ # TypeScript interfaces
671
+ │ │ └── utils/ # Utility functions
672
+ │ ├── routes/ # SvelteKit routes
673
+ │ └── styles/ # Global styles
674
+ ├── static/ # Static assets
675
+ ├── tests/ # Test files
676
+ ├── docs/ # Architecture diagrams
677
+ │ ├── architecture/ # High-level architecture
678
+ │ └── flows/ # Feature-specific flows
679
+ └── .storybook/ # Storybook configuration
680
+ ```
681
+
682
+ ---
683
+
684
+ ## Related Documentation
685
+
686
+ - [llama.cpp Server README](../server/README.md) - Full server documentation
687
+ - [Multimodal Documentation](../../docs/multimodal.md) - Image and audio support
688
+ - [Function Calling](../../docs/function-calling.md) - Tool use capabilities
backend/llama.cpp/tools/ui/docs/architecture/high-level-architecture.md ADDED
@@ -0,0 +1,373 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```mermaid
2
+ flowchart TB
3
+ subgraph Routes["📍 Routes"]
4
+ R1["/ (+page.svelte)"]
5
+ R2["/chat/[id]"]
6
+ RL["+layout.svelte"]
7
+ end
8
+
9
+ subgraph Components["🧩 Components"]
10
+ direction TB
11
+ subgraph LayoutComponents["Layout"]
12
+ C_Sidebar["ChatSidebar"]
13
+ C_Screen["ChatScreen"]
14
+ end
15
+ subgraph ChatUIComponents["Chat UI"]
16
+ C_Form["ChatForm"]
17
+ C_Messages["ChatMessages"]
18
+ C_Message["ChatMessage"]
19
+ C_MessageUser["ChatMessageUser"]
20
+ C_MessageEditForm["ChatMessageEditForm"]
21
+ C_Attach["ChatAttachments"]
22
+ C_ModelsSelector["ModelsSelector"]
23
+ C_Settings["ChatSettings"]
24
+ end
25
+ subgraph MCPComponents["MCP UI"]
26
+ C_McpSettings["McpServersSettings"]
27
+ C_McpServerCard["McpServerCard"]
28
+ C_McpResourceBrowser["McpResourceBrowser"]
29
+ C_McpResourcePreview["McpResourcePreview"]
30
+ C_McpServersSelector["McpServersSelector"]
31
+ end
32
+ end
33
+
34
+ subgraph Hooks["🪝 Hooks"]
35
+ H1["useModelChangeValidation"]
36
+ H2["useProcessingState"]
37
+ H3["isMobile"]
38
+ end
39
+
40
+ subgraph Stores["🗄️ Stores"]
41
+ direction TB
42
+ subgraph S1["chatStore"]
43
+ S1State["<b>State:</b><br/>isLoading, currentResponse<br/>errorDialogState<br/>activeProcessingState<br/>chatLoadingStates<br/>chatStreamingStates<br/>abortControllers<br/>processingStates<br/>activeConversationId<br/>isStreamingActive"]
44
+ S1LoadState["<b>Loading State:</b><br/>setChatLoading()<br/>isChatLoading()<br/>syncLoadingStateForChat()<br/>clearUIState()<br/>isChatLoadingPublic()<br/>getAllLoadingChats()<br/>getAllStreamingChats()"]
45
+ S1ProcState["<b>Processing State:</b><br/>setActiveProcessingConversation()<br/>getProcessingState()<br/>clearProcessingState()<br/>getActiveProcessingState()<br/>updateProcessingStateFromTimings()<br/>getCurrentProcessingStateSync()<br/>restoreProcessingStateFromMessages()"]
46
+ S1Stream["<b>Streaming:</b><br/>streamChatCompletion()<br/>startStreaming()<br/>stopStreaming()<br/>stopGeneration()<br/>isStreaming()"]
47
+ S1Error["<b>Error Handling:</b><br/>showErrorDialog()<br/>dismissErrorDialog()<br/>isAbortError()"]
48
+ S1Msg["<b>Message Operations:</b><br/>addMessage()<br/>sendMessage()<br/>updateMessage()<br/>deleteMessage()<br/>getDeletionInfo()"]
49
+ S1Regen["<b>Regeneration:</b><br/>regenerateMessage()<br/>regenerateMessageWithBranching()<br/>continueAssistantMessage()"]
50
+ S1Edit["<b>Editing:</b><br/>editAssistantMessage()<br/>editUserMessagePreserveResponses()<br/>editMessageWithBranching()<br/>clearEditMode()<br/>isEditModeActive()<br/>getAddFilesHandler()<br/>setEditModeActive()"]
51
+ S1Utils["<b>Utilities:</b><br/>getApiOptions()<br/>parseTimingData()<br/>getOrCreateAbortController()<br/>getConversationModel()"]
52
+ end
53
+ subgraph SA["agenticStore"]
54
+ SAState["<b>State:</b><br/>sessions (Map)<br/>isAnyRunning"]
55
+ SASession["<b>Session Management:</b><br/>getSession()<br/>updateSession()<br/>clearSession()<br/>getActiveSessions()<br/>isRunning()<br/>currentTurn()<br/>totalToolCalls()<br/>lastError()<br/>streamingToolCall()"]
56
+ SAConfig["<b>Configuration:</b><br/>getConfig()<br/>maxTurns, maxToolPreviewLines"]
57
+ SAFlow["<b>Agentic Loop:</b><br/>runAgenticFlow()<br/>executeAgenticLoop()<br/>normalizeToolCalls()<br/>emitToolCallResult()<br/>extractBase64Attachments()"]
58
+ end
59
+ subgraph S2["conversationsStore"]
60
+ S2State["<b>State:</b><br/>conversations<br/>activeConversation<br/>activeMessages<br/>isInitialized<br/>pendingMcpServerOverrides<br/>titleUpdateConfirmationCallback"]
61
+ S2Lifecycle["<b>Lifecycle:</b><br/>initialize()<br/>loadConversations()<br/>clearActiveConversation()"]
62
+ S2ConvCRUD["<b>Conversation CRUD:</b><br/>createConversation()<br/>loadConversation()<br/>deleteConversation()<br/>deleteAll()<br/>updateConversationName()<br/>updateConversationTitleWithConfirmation()"]
63
+ S2MsgMgmt["<b>Message Management:</b><br/>refreshActiveMessages()<br/>addMessageToActive()<br/>updateMessageAtIndex()<br/>findMessageIndex()<br/>sliceActiveMessages()<br/>removeMessageAtIndex()<br/>getConversationMessages()"]
64
+ S2Nav["<b>Navigation:</b><br/>navigateToSibling()<br/>updateCurrentNode()<br/>updateConversationTimestamp()"]
65
+ S2McpOverrides["<b>MCP Per-Chat Overrides:</b><br/>getMcpServerOverride()<br/>getAllMcpServerOverrides()<br/>setMcpServerOverride()<br/>toggleMcpServerForChat()<br/>removeMcpServerOverride()<br/>isMcpServerEnabledForChat()<br/>clearPendingMcpServerOverrides()"]
66
+ S2Export["<b>Import/Export:</b><br/>downloadConversation()<br/>exportAllConversations()<br/>importConversations()<br/>importConversationsData()<br/>triggerDownload()"]
67
+ S2Utils["<b>Utilities:</b><br/>setTitleUpdateConfirmationCallback()"]
68
+ end
69
+ subgraph S3["modelsStore"]
70
+ S3State["<b>State:</b><br/>models, routerModels<br/>selectedModelId<br/>selectedModelName<br/>loading, updating, error<br/>modelLoadingStates<br/>modelPropsCache<br/>modelPropsFetching<br/>propsCacheVersion"]
71
+ S3Getters["<b>Computed Getters:</b><br/>selectedModel<br/>loadedModelIds<br/>loadingModelIds<br/>singleModelName"]
72
+ S3Modal["<b>Modalities:</b><br/>getModelModalities()<br/>modelSupportsVision()<br/>modelSupportsAudio()<br/>getModelModalitiesArray()<br/>getModelProps()<br/>updateModelModalities()"]
73
+ S3Status["<b>Status Queries:</b><br/>isModelLoaded()<br/>isModelOperationInProgress()<br/>getModelStatus()<br/>isModelPropsFetching()"]
74
+ S3Fetch["<b>Data Fetching:</b><br/>fetch()<br/>fetchRouterModels()<br/>fetchModelProps()<br/>fetchModalitiesForLoadedModels()"]
75
+ S3Select["<b>Model Selection:</b><br/>selectModelById()<br/>selectModelByName()<br/>clearSelection()<br/>findModelByName()<br/>findModelById()<br/>hasModel()"]
76
+ S3LoadUnload["<b>Loading/Unloading Models:</b><br/>loadModel()<br/>unloadModel()<br/>ensureModelLoaded()<br/>waitForModelStatus()<br/>pollForModelStatus()"]
77
+ S3Utils["<b>Utilities:</b><br/>toDisplayName()<br/>clear()"]
78
+ end
79
+ subgraph S4["serverStore"]
80
+ S4State["<b>State:</b><br/>props<br/>loading, error<br/>role<br/>fetchPromise"]
81
+ S4Getters["<b>Getters:</b><br/>defaultParams<br/>contextSize<br/>isRouterMode<br/>isModelMode"]
82
+ S4Data["<b>Data Handling:</b><br/>fetch()<br/>getErrorMessage()<br/>clear()"]
83
+ S4Utils["<b>Utilities:</b><br/>detectRole()"]
84
+ end
85
+ subgraph S5["settingsStore"]
86
+ S5State["<b>State:</b><br/>config<br/>theme<br/>isInitialized<br/>userOverrides"]
87
+ S5Lifecycle["<b>Lifecycle:</b><br/>initialize()<br/>loadConfig()<br/>saveConfig()<br/>loadTheme()<br/>saveTheme()"]
88
+ S5Update["<b>Config Updates:</b><br/>updateConfig()<br/>updateMultipleConfig()<br/>updateTheme()"]
89
+ S5Reset["<b>Reset:</b><br/>resetConfig()<br/>resetTheme()<br/>resetAll()<br/>resetParameterToServerDefault()"]
90
+ S5Sync["<b>Server Sync:</b><br/>syncWithServerDefaults()<br/>forceSyncWithServerDefaults()"]
91
+ S5Utils["<b>Utilities:</b><br/>getConfig()<br/>getAllConfig()<br/>getParameterInfo()<br/>getParameterDiff()<br/>getServerDefaults()<br/>clearAllUserOverrides()"]
92
+ end
93
+ subgraph S6["mcpStore"]
94
+ S6State["<b>State:</b><br/>isInitializing, error<br/>toolCount, connectedServers<br/>healthChecks (Map)<br/>connections (Map)<br/>toolsIndex (Map)"]
95
+ S6Lifecycle["<b>Lifecycle:</b><br/>ensureInitialized()<br/>initialize()<br/>shutdown()<br/>acquireConnection()<br/>releaseConnection()"]
96
+ S6Health["<b>Health Checks:</b><br/>runHealthCheck()<br/>runHealthChecksForServers()<br/>updateHealthCheck()<br/>getHealthCheckState()<br/>clearHealthCheck()"]
97
+ S6Servers["<b>Server Management:</b><br/>getServers()<br/>addServer()<br/>updateServer()<br/>removeServer()<br/>getServerById()<br/>getServerDisplayName()"]
98
+ S6Tools["<b>Tool Operations:</b><br/>getToolDefinitionsForLLM()<br/>getToolNames()<br/>hasTool()<br/>getToolServer()<br/>executeTool()<br/>executeToolByName()"]
99
+ S6Prompts["<b>Prompt Operations:</b><br/>getAllPrompts()<br/>getPrompt()<br/>hasPromptsCapability()<br/>getPromptCompletions()"]
100
+ end
101
+ subgraph S7["mcpResourceStore"]
102
+ S7State["<b>State:</b><br/>serverResources (Map)<br/>cachedResources (Map)<br/>subscriptions (Map)<br/>attachments[]<br/>isLoading"]
103
+ S7Resources["<b>Resource Discovery:</b><br/>setServerResources()<br/>getServerResources()<br/>getAllResourceInfos()<br/>getAllTemplateInfos()<br/>clearServerResources()"]
104
+ S7Cache["<b>Caching:</b><br/>cacheResourceContent()<br/>getCachedContent()<br/>invalidateCache()<br/>clearCache()"]
105
+ S7Subs["<b>Subscriptions:</b><br/>addSubscription()<br/>removeSubscription()<br/>isSubscribed()<br/>handleResourceUpdate()"]
106
+ S7Attach["<b>Attachments:</b><br/>addAttachment()<br/>updateAttachmentContent()<br/>removeAttachment()<br/>clearAttachments()<br/>toMessageExtras()"]
107
+ end
108
+
109
+ subgraph ReactiveExports["⚡ Reactive Exports"]
110
+ direction LR
111
+ subgraph ChatExports["chatStore"]
112
+ RE1["isLoading()"]
113
+ RE2["currentResponse()"]
114
+ RE3["errorDialog()"]
115
+ RE4["activeProcessingState()"]
116
+ RE5["isChatStreaming()"]
117
+ RE6["isChatLoading()"]
118
+ RE7["getChatStreaming()"]
119
+ RE8["getAllLoadingChats()"]
120
+ RE9["getAllStreamingChats()"]
121
+ RE9a["isEditModeActive()"]
122
+ RE9b["getAddFilesHandler()"]
123
+ RE9c["setEditModeActive()"]
124
+ RE9d["clearEditMode()"]
125
+ end
126
+ subgraph AgenticExports["agenticStore"]
127
+ REA1["agenticIsRunning()"]
128
+ REA2["agenticCurrentTurn()"]
129
+ REA3["agenticTotalToolCalls()"]
130
+ REA4["agenticLastError()"]
131
+ REA5["agenticStreamingToolCall()"]
132
+ REA6["agenticIsAnyRunning()"]
133
+ end
134
+ subgraph ConvExports["conversationsStore"]
135
+ RE10["conversations()"]
136
+ RE11["activeConversation()"]
137
+ RE12["activeMessages()"]
138
+ RE13["isConversationsInitialized()"]
139
+ end
140
+ subgraph ModelsExports["modelsStore"]
141
+ RE15["modelOptions()"]
142
+ RE16["routerModels()"]
143
+ RE17["modelsLoading()"]
144
+ RE18["modelsUpdating()"]
145
+ RE19["modelsError()"]
146
+ RE20["selectedModelId()"]
147
+ RE21["selectedModelName()"]
148
+ RE22["selectedModelOption()"]
149
+ RE23["loadedModelIds()"]
150
+ RE24["loadingModelIds()"]
151
+ RE25["propsCacheVersion()"]
152
+ RE26["singleModelName()"]
153
+ end
154
+ subgraph ServerExports["serverStore"]
155
+ RE27["serverProps()"]
156
+ RE28["serverLoading()"]
157
+ RE29["serverError()"]
158
+ RE30["serverRole()"]
159
+ RE31["defaultParams()"]
160
+ RE32["contextSize()"]
161
+ RE33["isRouterMode()"]
162
+ RE34["isModelMode()"]
163
+ end
164
+ subgraph SettingsExports["settingsStore"]
165
+ RE35["config()"]
166
+ RE36["theme()"]
167
+ RE37["isInitialized()"]
168
+ end
169
+ subgraph MCPExports["mcpStore / mcpResourceStore"]
170
+ RE38["mcpResources()"]
171
+ RE39["mcpResourceAttachments()"]
172
+ RE40["mcpHasResourceAttachments()"]
173
+ RE41["mcpTotalResourceCount()"]
174
+ RE42["mcpResourcesLoading()"]
175
+ end
176
+ end
177
+ end
178
+
179
+ subgraph Services["⚙️ Services"]
180
+ direction TB
181
+ subgraph SV1["ChatService"]
182
+ SV1Msg["<b>Messaging:</b><br/>sendMessage()"]
183
+ SV1Stream["<b>Streaming:</b><br/>handleStreamResponse()<br/>handleNonStreamResponse()"]
184
+ SV1Convert["<b>Conversion:</b><br/>convertDbMessageToApiChatMessageData()<br/>mergeToolCallDeltas()"]
185
+ SV1Utils["<b>Utilities:</b><br/>stripReasoningContent()<br/>extractModelName()<br/>parseErrorResponse()"]
186
+ end
187
+ subgraph SV2["ModelsService"]
188
+ SV2List["<b>Listing:</b><br/>list()<br/>listRouter()"]
189
+ SV2LoadUnload["<b>Load/Unload:</b><br/>load()<br/>unload()"]
190
+ SV2Status["<b>Status:</b><br/>isModelLoaded()<br/>isModelLoading()"]
191
+ end
192
+ subgraph SV3["PropsService"]
193
+ SV3Fetch["<b>Fetching:</b><br/>fetch()<br/>fetchForModel()"]
194
+ end
195
+ subgraph SV4["DatabaseService"]
196
+ SV4Conv["<b>Conversations:</b><br/>createConversation()<br/>getConversation()<br/>getAllConversations()<br/>updateConversation()<br/>deleteConversation()"]
197
+ SV4Msg["<b>Messages:</b><br/>createMessageBranch()<br/>createRootMessage()<br/>createSystemMessage()<br/>getConversationMessages()<br/>updateMessage()<br/>deleteMessage()<br/>deleteMessageCascading()"]
198
+ SV4Node["<b>Navigation:</b><br/>updateCurrentNode()"]
199
+ SV4Import["<b>Import:</b><br/>importConversations()"]
200
+ end
201
+ subgraph SV5["ParameterSyncService"]
202
+ SV5Extract["<b>Extraction:</b><br/>extractServerDefaults()"]
203
+ SV5Merge["<b>Merging:</b><br/>mergeWithServerDefaults()"]
204
+ SV5Info["<b>Info:</b><br/>getParameterInfo()<br/>canSyncParameter()<br/>getSyncableParameterKeys()<br/>validateServerParameter()"]
205
+ SV5Diff["<b>Diff:</b><br/>createParameterDiff()"]
206
+ end
207
+ subgraph SV6["MCPService"]
208
+ SV6Transport["<b>Transport:</b><br/>createTransport()<br/>WebSocket / StreamableHTTP / SSE"]
209
+ SV6Conn["<b>Connection:</b><br/>connect()<br/>disconnect()"]
210
+ SV6Tools["<b>Tools:</b><br/>listTools()<br/>callTool()"]
211
+ SV6Prompts["<b>Prompts:</b><br/>listPrompts()<br/>getPrompt()"]
212
+ SV6Resources["<b>Resources:</b><br/>listResources()<br/>listResourceTemplates()<br/>readResource()<br/>subscribeResource()<br/>unsubscribeResource()"]
213
+ SV6Complete["<b>Completions:</b><br/>complete()"]
214
+ end
215
+ end
216
+
217
+ subgraph ExternalMCP["🔌 External MCP Servers"]
218
+ EXT1["MCP Server 1<br/>(WebSocket/StreamableHTTP/SSE)"]
219
+ EXT2["MCP Server N"]
220
+ end
221
+
222
+ subgraph Storage["💾 Storage"]
223
+ ST1["IndexedDB"]
224
+ ST2["conversations"]
225
+ ST3["messages"]
226
+ ST5["LocalStorage"]
227
+ ST6["config"]
228
+ ST7["userOverrides"]
229
+ ST8["mcpServers"]
230
+ end
231
+
232
+ subgraph APIs["🌐 llama-server API"]
233
+ API1["/v1/chat/completions"]
234
+ API2["/props<br/>/props?model="]
235
+ API3["/models<br/>/models/load<br/>/models/unload"]
236
+ API4["/v1/models"]
237
+ end
238
+
239
+ %% Routes render Components
240
+ R1 --> C_Screen
241
+ R2 --> C_Screen
242
+ RL --> C_Sidebar
243
+
244
+ %% Layout runs MCP health checks on startup
245
+ RL --> S6
246
+
247
+ %% Component hierarchy
248
+ C_Screen --> C_Form & C_Messages & C_Settings
249
+ C_Messages --> C_Message
250
+ C_Message --> C_MessageUser
251
+ C_MessageUser --> C_MessageEditForm
252
+ C_MessageEditForm --> C_ModelsSelector
253
+ C_MessageEditForm --> C_Attach
254
+ C_Form --> C_ModelsSelector
255
+ C_Form --> C_Attach
256
+ C_Form --> C_McpServersSelector
257
+ C_Message --> C_Attach
258
+
259
+ %% MCP Components hierarchy
260
+ C_Settings --> C_McpSettings
261
+ C_McpSettings --> C_McpServerCard
262
+ C_McpServerCard --> C_McpResourceBrowser
263
+ C_McpResourceBrowser --> C_McpResourcePreview
264
+
265
+ %% Components use Hooks
266
+ C_Form --> H1
267
+ C_Message --> H1 & H2
268
+ C_MessageEditForm --> H1
269
+ C_Screen --> H2
270
+
271
+ %% Hooks use Stores
272
+ H1 --> S3 & S4
273
+ H2 --> S1 & S5
274
+
275
+ %% Components use Stores
276
+ C_Screen --> S1 & S2
277
+ C_Messages --> S2
278
+ C_Message --> S1 & S2 & S3
279
+ C_Form --> S1 & S3 & S6
280
+ C_Sidebar --> S2
281
+ C_ModelsSelector --> S3 & S4
282
+ C_Settings --> S5
283
+ C_McpSettings --> S6
284
+ C_McpServerCard --> S6
285
+ C_McpResourceBrowser --> S6 & S7
286
+ C_McpServersSelector --> S6
287
+
288
+ %% Stores export Reactive State
289
+ S1 -. exports .-> ChatExports
290
+ SA -. exports .-> AgenticExports
291
+ S2 -. exports .-> ConvExports
292
+ S3 -. exports .-> ModelsExports
293
+ S4 -. exports .-> ServerExports
294
+ S5 -. exports .-> SettingsExports
295
+ S6 -. exports .-> MCPExports
296
+ S7 -. exports .-> MCPExports
297
+
298
+ %% chatStore → agenticStore (agentic loop orchestration)
299
+ S1 --> SA
300
+ SA --> SV1
301
+ SA --> S6
302
+
303
+ %% Stores use Services
304
+ S1 --> SV1 & SV4
305
+ S2 --> SV4
306
+ S3 --> SV2 & SV3
307
+ S4 --> SV3
308
+ S5 --> SV5
309
+ S6 --> SV6
310
+ S7 --> SV6
311
+
312
+ %% Services to Storage
313
+ SV4 --> ST1
314
+ ST1 --> ST2 & ST3
315
+ SV5 --> ST5
316
+ ST5 --> ST6 & ST7 & ST8
317
+
318
+ %% Services to APIs
319
+ SV1 --> API1
320
+ SV2 --> API3 & API4
321
+ SV3 --> API2
322
+
323
+ %% MCP → External Servers
324
+ SV6 --> EXT1 & EXT2
325
+
326
+ %% Styling
327
+ classDef routeStyle fill:#e1f5fe,stroke:#01579b,stroke-width:2px
328
+ classDef componentStyle fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
329
+ classDef componentGroupStyle fill:#e1bee7,stroke:#7b1fa2,stroke-width:1px
330
+ classDef hookStyle fill:#fff8e1,stroke:#ff8f00,stroke-width:2px
331
+ classDef storeStyle fill:#fff3e0,stroke:#e65100,stroke-width:2px
332
+ classDef stateStyle fill:#ffe0b2,stroke:#e65100,stroke-width:1px
333
+ classDef methodStyle fill:#ffecb3,stroke:#e65100,stroke-width:1px
334
+ classDef reactiveStyle fill:#fffde7,stroke:#f9a825,stroke-width:1px
335
+ classDef serviceStyle fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px
336
+ classDef serviceMStyle fill:#c8e6c9,stroke:#2e7d32,stroke-width:1px
337
+ classDef externalStyle fill:#f3e5f5,stroke:#6a1b9a,stroke-width:2px,stroke-dasharray: 5 5
338
+ classDef storageStyle fill:#fce4ec,stroke:#c2185b,stroke-width:2px
339
+ classDef apiStyle fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
340
+
341
+ class R1,R2,RL routeStyle
342
+ class C_Sidebar,C_Screen,C_Form,C_Messages,C_Message,C_MessageUser,C_MessageEditForm componentStyle
343
+ class C_ModelsSelector,C_Settings componentStyle
344
+ class C_Attach componentStyle
345
+ class C_McpSettings,C_McpServerCard,C_McpResourceBrowser,C_McpResourcePreview,C_McpServersSelector componentStyle
346
+ class H1,H2,H3 hookStyle
347
+ class LayoutComponents,ChatUIComponents,MCPComponents componentGroupStyle
348
+ class Hooks hookStyle
349
+ classDef agenticStyle fill:#e8eaf6,stroke:#283593,stroke-width:2px
350
+ classDef agenticMethodStyle fill:#c5cae9,stroke:#283593,stroke-width:1px
351
+
352
+ class S1,S2,S3,S4,S5,SA,S6,S7 storeStyle
353
+ class S1State,S2State,S3State,S4State,S5State,SAState,S6State,S7State stateStyle
354
+ class S1Msg,S1Regen,S1Edit,S1Stream,S1LoadState,S1ProcState,S1Error,S1Utils methodStyle
355
+ class SASession,SAConfig,SAFlow methodStyle
356
+ class S2Lifecycle,S2ConvCRUD,S2MsgMgmt,S2Nav,S2McpOverrides,S2Export,S2Utils methodStyle
357
+ class S3Getters,S3Modal,S3Status,S3Fetch,S3Select,S3LoadUnload,S3Utils methodStyle
358
+ class S4Getters,S4Data,S4Utils methodStyle
359
+ class S5Lifecycle,S5Update,S5Reset,S5Sync,S5Utils methodStyle
360
+ class S6Lifecycle,S6Health,S6Servers,S6Tools,S6Prompts methodStyle
361
+ class S7Resources,S7Cache,S7Subs,S7Attach methodStyle
362
+ class ChatExports,AgenticExports,ConvExports,ModelsExports,ServerExports,SettingsExports,MCPExports reactiveStyle
363
+ class SV1,SV2,SV3,SV4,SV5,SV6 serviceStyle
364
+ class SV6Transport,SV6Conn,SV6Tools,SV6Prompts,SV6Resources,SV6Complete serviceMStyle
365
+ class EXT1,EXT2 externalStyle
366
+ class SV1Msg,SV1Stream,SV1Convert,SV1Utils serviceMStyle
367
+ class SV2List,SV2LoadUnload,SV2Status serviceMStyle
368
+ class SV3Fetch serviceMStyle
369
+ class SV4Conv,SV4Msg,SV4Node,SV4Import serviceMStyle
370
+ class SV5Extract,SV5Merge,SV5Info,SV5Diff serviceMStyle
371
+ class ST1,ST2,ST3,ST5,ST6,ST7,ST8 storageStyle
372
+ class API1,API2,API3,API4 apiStyle
373
+ ```
backend/llama.cpp/tools/ui/docs/flows/chat-flow.md ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```mermaid
2
+ sequenceDiagram
3
+ participant UI as 🧩 ChatForm / ChatMessage
4
+ participant chatStore as 🗄️ chatStore
5
+ participant agenticStore as 🗄️ agenticStore
6
+ participant convStore as 🗄️ conversationsStore
7
+ participant settingsStore as 🗄️ settingsStore
8
+ participant mcpStore as 🗄️ mcpStore
9
+ participant ChatSvc as ⚙️ ChatService
10
+ participant DbSvc as ⚙️ DatabaseService
11
+ participant API as 🌐 /v1/chat/completions
12
+
13
+ Note over chatStore: State:<br/>isLoading, currentResponse<br/>errorDialogState, activeProcessingState<br/>chatLoadingStates (Map)<br/>chatStreamingStates (Map)<br/>abortControllers (Map)<br/>processingStates (Map)
14
+
15
+ %% ═══════════════════════════════════════════════════════════════════════════
16
+ Note over UI,API: 💬 SEND MESSAGE
17
+ %% ═══════════════════════════════════════════════════════════════════════════
18
+
19
+ UI->>chatStore: sendMessage(content, extras)
20
+ activate chatStore
21
+
22
+ chatStore->>chatStore: setChatLoading(convId, true)
23
+ chatStore->>chatStore: clearChatStreaming(convId)
24
+
25
+ alt no active conversation
26
+ chatStore->>convStore: createConversation()
27
+ Note over convStore: → see conversations-flow.mmd
28
+ end
29
+
30
+ chatStore->>mcpStore: consumeResourceAttachmentsAsExtras()
31
+ Note right of mcpStore: Converts pending MCP resource<br/>attachments into message extras
32
+
33
+ chatStore->>chatStore: addMessage("user", content, extras)
34
+ chatStore->>DbSvc: createMessageBranch(userMsg, parentId)
35
+ chatStore->>convStore: addMessageToActive(userMsg)
36
+ chatStore->>convStore: updateCurrentNode(userMsg.id)
37
+
38
+ chatStore->>chatStore: createAssistantMessage(userMsg.id)
39
+ chatStore->>DbSvc: createMessageBranch(assistantMsg, userMsg.id)
40
+ chatStore->>convStore: addMessageToActive(assistantMsg)
41
+
42
+ chatStore->>chatStore: streamChatCompletion(messages, assistantMsg)
43
+ deactivate chatStore
44
+
45
+ %% ═══════════════════════════════════════════════════════════════════════════
46
+ Note over UI,API: 🌊 STREAMING (with agentic flow detection)
47
+ %% ═══════════════════════════════════════════════════════════════════════════
48
+
49
+ activate chatStore
50
+ chatStore->>chatStore: startStreaming()
51
+ Note right of chatStore: isStreamingActive = true
52
+
53
+ chatStore->>chatStore: setActiveProcessingConversation(convId)
54
+ chatStore->>chatStore: getOrCreateAbortController(convId)
55
+ Note right of chatStore: abortControllers.set(convId, new AbortController())
56
+
57
+ chatStore->>chatStore: getApiOptions()
58
+ Note right of chatStore: Merge from settingsStore.config:<br/>temperature, max_tokens, top_p, etc.
59
+
60
+ alt agenticConfig.enabled && mcpStore has connected servers
61
+ chatStore->>agenticStore: runAgenticFlow(convId, messages, assistantMsg, options, signal)
62
+ Note over agenticStore: Multi-turn agentic loop:<br/>1. Call ChatService.sendMessage()<br/>2. If response has tool_calls → execute via mcpStore<br/>3. Append tool results as messages<br/>4. Loop until no more tool_calls or maxTurns<br/>→ see agentic flow details below
63
+ agenticStore-->>chatStore: final response with timings
64
+ else standard (non-agentic) flow
65
+ chatStore->>ChatSvc: sendMessage(messages, options, signal)
66
+ end
67
+
68
+ activate ChatSvc
69
+
70
+ ChatSvc->>ChatSvc: convertDbMessageToApiChatMessageData(messages)
71
+ Note right of ChatSvc: DatabaseMessage[] → ApiChatMessageData[]<br/>Process attachments (images, PDFs, audio)
72
+
73
+ ChatSvc->>API: POST /v1/chat/completions
74
+ Note right of API: {messages, model?, stream: true, ...params}
75
+
76
+ loop SSE chunks
77
+ API-->>ChatSvc: data: {"choices":[{"delta":{...}}]}
78
+ ChatSvc->>ChatSvc: handleStreamResponse(response)
79
+
80
+ alt content chunk
81
+ ChatSvc-->>chatStore: onChunk(content)
82
+ chatStore->>chatStore: setChatStreaming(convId, response, msgId)
83
+ Note right of chatStore: currentResponse = $state(accumulated)
84
+ chatStore->>convStore: updateMessageAtIndex(idx, {content})
85
+ end
86
+
87
+ alt reasoning chunk
88
+ ChatSvc-->>chatStore: onReasoningChunk(reasoning)
89
+ chatStore->>convStore: updateMessageAtIndex(idx, {thinking})
90
+ end
91
+
92
+ alt tool_calls chunk
93
+ ChatSvc-->>chatStore: onToolCallChunk(toolCalls)
94
+ chatStore->>convStore: updateMessageAtIndex(idx, {toolCalls})
95
+ end
96
+
97
+ alt model info
98
+ ChatSvc-->>chatStore: onModel(modelName)
99
+ chatStore->>chatStore: recordModel(modelName)
100
+ chatStore->>DbSvc: updateMessage(msgId, {model})
101
+ end
102
+
103
+ alt timings (during stream)
104
+ ChatSvc-->>chatStore: onTimings(timings, promptProgress)
105
+ chatStore->>chatStore: updateProcessingStateFromTimings()
106
+ end
107
+
108
+ chatStore-->>UI: reactive $state update
109
+ end
110
+
111
+ API-->>ChatSvc: data: [DONE]
112
+ ChatSvc-->>chatStore: onComplete(content, reasoning, timings, toolCalls)
113
+ deactivate ChatSvc
114
+
115
+ chatStore->>chatStore: stopStreaming()
116
+ chatStore->>DbSvc: updateMessage(msgId, {content, timings, model})
117
+ chatStore->>convStore: updateCurrentNode(msgId)
118
+ chatStore->>chatStore: setChatLoading(convId, false)
119
+ chatStore->>chatStore: clearChatStreaming(convId)
120
+ chatStore->>chatStore: clearProcessingState(convId)
121
+ deactivate chatStore
122
+
123
+ %% ═══════════════════════════════════════════════════════════════════════════
124
+ Note over UI,API: ⏹️ STOP GENERATION
125
+ %% ═══════════════════════════════════════════════════════════════════════════
126
+
127
+ UI->>chatStore: stopGeneration()
128
+ activate chatStore
129
+ chatStore->>chatStore: savePartialResponseIfNeeded(convId)
130
+ Note right of chatStore: Save currentResponse to DB if non-empty
131
+ chatStore->>chatStore: abortControllers.get(convId).abort()
132
+ Note right of chatStore: fetch throws AbortError → caught by isAbortError()
133
+ chatStore->>chatStore: stopStreaming()
134
+ chatStore->>chatStore: setChatLoading(convId, false)
135
+ chatStore->>chatStore: clearChatStreaming(convId)
136
+ chatStore->>chatStore: clearProcessingState(convId)
137
+ deactivate chatStore
138
+
139
+ %% ═══════════════════════════════════════════════════════════════════════════
140
+ Note over UI,API: 🔁 REGENERATE
141
+ %% ═══════════════════════════════════════════════════════════════════════════
142
+
143
+ UI->>chatStore: regenerateMessageWithBranching(msgId, model?)
144
+ activate chatStore
145
+ chatStore->>convStore: findMessageIndex(msgId)
146
+ chatStore->>chatStore: Get parent of target message
147
+ chatStore->>chatStore: createAssistantMessage(parentId)
148
+ chatStore->>DbSvc: createMessageBranch(newAssistantMsg, parentId)
149
+ chatStore->>convStore: refreshActiveMessages()
150
+ Note right of chatStore: Same streaming flow
151
+ chatStore->>chatStore: streamChatCompletion(...)
152
+ deactivate chatStore
153
+
154
+ %% ═══════════════════════════════════════════════════════════════════════════
155
+ Note over UI,API: ➡️ CONTINUE
156
+ %% ═══════════════════════════════════════════════════════════════════════════
157
+
158
+ UI->>chatStore: continueAssistantMessage(msgId)
159
+ activate chatStore
160
+ chatStore->>chatStore: Get existing content from message
161
+ chatStore->>chatStore: streamChatCompletion(..., existingContent)
162
+ Note right of chatStore: Appends to existing message content
163
+ deactivate chatStore
164
+
165
+ %% ═══════════════════════════════════════════════════════════════════════════
166
+ Note over UI,API: ✏️ EDIT USER MESSAGE
167
+ %% ═══════════════════════════════════════════════════════════════════════════
168
+
169
+ UI->>chatStore: editMessageWithBranching(msgId, newContent, extras)
170
+ activate chatStore
171
+ chatStore->>chatStore: Get parent of target message
172
+ chatStore->>DbSvc: createMessageBranch(editedMsg, parentId)
173
+ chatStore->>convStore: refreshActiveMessages()
174
+ Note right of chatStore: Creates new branch, original preserved
175
+ chatStore->>chatStore: createAssistantMessage(editedMsg.id)
176
+ chatStore->>chatStore: streamChatCompletion(...)
177
+ Note right of chatStore: Automatically regenerates response
178
+ deactivate chatStore
179
+
180
+ %% ═════════════════════════════════════════════════���═════════════════════════
181
+ Note over UI,API: ❌ ERROR HANDLING
182
+ %% ═══════════════════════════════════════════════════════════════════════════
183
+
184
+ Note over chatStore: On stream error (non-abort):
185
+ chatStore->>chatStore: showErrorDialog(type, message)
186
+ Note right of chatStore: errorDialogState = {type: 'timeout'|'server', message}
187
+ chatStore->>convStore: removeMessageAtIndex(failedMsgIdx)
188
+ chatStore->>DbSvc: deleteMessage(failedMsgId)
189
+
190
+ %% ═══════════════════════════════════════════════════════════════════════════
191
+ Note over UI,API: 🤖 AGENTIC LOOP (when agenticConfig.enabled)
192
+ %% ═══════════════════════════════════════════════════════════════════════════
193
+
194
+ Note over agenticStore: agenticStore.runAgenticFlow(convId, messages, assistantMsg, options, signal)
195
+ activate agenticStore
196
+ agenticStore->>agenticStore: getSession(convId) or create new
197
+ agenticStore->>agenticStore: updateSession(turn: 0, running: true)
198
+
199
+ loop executeAgenticLoop (until no tool_calls or maxTurns)
200
+ agenticStore->>agenticStore: turn++
201
+ agenticStore->>ChatSvc: sendMessage(messages, options, signal)
202
+ ChatSvc->>API: POST /v1/chat/completions
203
+ API-->>ChatSvc: response with potential tool_calls
204
+ ChatSvc-->>agenticStore: onComplete(content, reasoning, timings, toolCalls)
205
+
206
+ alt response has tool_calls
207
+ agenticStore->>agenticStore: normalizeToolCalls(toolCalls)
208
+ loop for each tool_call
209
+ agenticStore->>agenticStore: updateSession(streamingToolCall)
210
+ agenticStore->>mcpStore: executeTool(mcpCall, signal)
211
+ mcpStore-->>agenticStore: tool result
212
+ agenticStore->>agenticStore: extractBase64Attachments(result)
213
+ agenticStore->>agenticStore: emitToolCallResult(convId, ...)
214
+ agenticStore->>convStore: addMessageToActive(toolResultMsg)
215
+ agenticStore->>DbSvc: createMessageBranch(toolResultMsg)
216
+ end
217
+ agenticStore->>agenticStore: Create new assistantMsg for next turn
218
+ Note right of agenticStore: Continue loop with updated messages
219
+ else no tool_calls (final response)
220
+ agenticStore->>agenticStore: buildFinalTimings(allTurns)
221
+ Note right of agenticStore: Break loop, return final response
222
+ end
223
+ end
224
+
225
+ agenticStore->>agenticStore: updateSession(running: false)
226
+ agenticStore-->>chatStore: final content, timings, model
227
+ deactivate agenticStore
228
+ ```
backend/llama.cpp/tools/ui/docs/flows/conversations-flow.md ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```mermaid
2
+ sequenceDiagram
3
+ participant UI as 🧩 ChatSidebar / ChatScreen
4
+ participant convStore as 🗄️ conversationsStore
5
+ participant chatStore as 🗄️ chatStore
6
+ participant DbSvc as ⚙️ DatabaseService
7
+ participant IDB as 💾 IndexedDB
8
+
9
+ Note over convStore: State:<br/>conversations: DatabaseConversation[]<br/>activeConversation: DatabaseConversation | null<br/>activeMessages: DatabaseMessage[]<br/>isInitialized: boolean<br/>pendingMcpServerOverrides: Map&lt;string, McpServerOverride&gt;
10
+
11
+ %% ═══════════════════════════════════════════════════════════════════════════
12
+ Note over UI,IDB: 🚀 INITIALIZATION
13
+ %% ═══════════════════════════════════════════════════════════════════════════
14
+
15
+ Note over convStore: Auto-initialized in constructor (browser only)
16
+ convStore->>convStore: initialize()
17
+ activate convStore
18
+ convStore->>convStore: loadConversations()
19
+ convStore->>DbSvc: getAllConversations()
20
+ DbSvc->>IDB: SELECT * FROM conversations ORDER BY lastModified DESC
21
+ IDB-->>DbSvc: Conversation[]
22
+ DbSvc-->>convStore: conversations
23
+ convStore->>convStore: conversations = $state(data)
24
+ convStore->>convStore: isInitialized = true
25
+ deactivate convStore
26
+
27
+ %% ═══════════════════════════════════════════════════════════════════════════
28
+ Note over UI,IDB: ➕ CREATE CONVERSATION
29
+ %% ═══════════════════════════════════════════════════════════════════════════
30
+
31
+ UI->>convStore: createConversation(name?)
32
+ activate convStore
33
+ convStore->>DbSvc: createConversation(name || "New Chat")
34
+ DbSvc->>IDB: INSERT INTO conversations
35
+ IDB-->>DbSvc: conversation {id, name, lastModified, currNode: ""}
36
+ DbSvc-->>convStore: conversation
37
+ convStore->>convStore: conversations.unshift(conversation)
38
+ convStore->>convStore: activeConversation = $state(conversation)
39
+ convStore->>convStore: activeMessages = $state([])
40
+
41
+ alt pendingMcpServerOverrides has entries
42
+ loop each pending override
43
+ convStore->>DbSvc: Store MCP server override for new conversation
44
+ end
45
+ convStore->>convStore: clearPendingMcpServerOverrides()
46
+ end
47
+ deactivate convStore
48
+
49
+ %% ═══════════════════════════════════════════════════════════════════════════
50
+ Note over UI,IDB: 📂 LOAD CONVERSATION
51
+ %% ═══════════════════════════════════════════════════════════════════════════
52
+
53
+ UI->>convStore: loadConversation(convId)
54
+ activate convStore
55
+ convStore->>DbSvc: getConversation(convId)
56
+ DbSvc->>IDB: SELECT * FROM conversations WHERE id = ?
57
+ IDB-->>DbSvc: conversation
58
+ convStore->>convStore: activeConversation = $state(conversation)
59
+
60
+ convStore->>convStore: refreshActiveMessages()
61
+ convStore->>DbSvc: getConversationMessages(convId)
62
+ DbSvc->>IDB: SELECT * FROM messages WHERE convId = ?
63
+ IDB-->>DbSvc: allMessages[]
64
+ convStore->>convStore: filterByLeafNodeId(allMessages, currNode)
65
+ Note right of convStore: Filter to show only current branch path
66
+ convStore->>convStore: activeMessages = $state(filtered)
67
+
68
+ Note right of convStore: Route (+page.svelte) then calls:<br/>chatStore.syncLoadingStateForChat(convId)
69
+ deactivate convStore
70
+
71
+ %% ═══════════════════════════════════════════════════════════════════════════
72
+ Note over UI,IDB: 🌳 MESSAGE BRANCHING MODEL
73
+ %% ═══════════════════════════════════════════════════════════════════════════
74
+
75
+ Note over IDB: Message Tree Structure:<br/>- Each message has parent (null for root)<br/>- Each message has children[] array<br/>- Conversation.currNode points to active leaf<br/>- filterByLeafNodeId() traverses from root to currNode
76
+
77
+ rect rgb(240, 240, 255)
78
+ Note over convStore: Example Branch Structure:
79
+ Note over convStore: root → user1 → assistant1 → user2 → assistant2a (currNode)<br/> ↘ assistant2b (alt branch)
80
+ end
81
+
82
+ %% ═══════════════════════════════════════════════════════════════════════════
83
+ Note over UI,IDB: ↔️ BRANCH NAVIGATION
84
+ %% ═══════════════════════════════════════════════════════════════════════════
85
+
86
+ UI->>convStore: navigateToSibling(msgId, direction)
87
+ activate convStore
88
+ convStore->>convStore: Find message in activeMessages
89
+ convStore->>convStore: Get parent message
90
+ convStore->>convStore: Find sibling in parent.children[]
91
+ convStore->>convStore: findLeafNode(siblingId, allMessages)
92
+ Note right of convStore: Navigate to leaf of sibling branch
93
+ convStore->>convStore: updateCurrentNode(leafId)
94
+ convStore->>DbSvc: updateCurrentNode(convId, leafId)
95
+ DbSvc->>IDB: UPDATE conversations SET currNode = ?
96
+ convStore->>convStore: refreshActiveMessages()
97
+ deactivate convStore
98
+
99
+ %% ═══════════════════════════════════════════════════════════════════════════
100
+ Note over UI,IDB: 📝 UPDATE CONVERSATION
101
+ %% ═══════════════════════════════════════════════════════════════════════════
102
+
103
+ UI->>convStore: updateConversationName(convId, newName)
104
+ activate convStore
105
+ convStore->>DbSvc: updateConversation(convId, {name: newName})
106
+ DbSvc->>IDB: UPDATE conversations SET name = ?
107
+ convStore->>convStore: Update in conversations array
108
+ deactivate convStore
109
+
110
+ Note over convStore: Auto-title update (after first response):
111
+ convStore->>convStore: updateConversationTitleWithConfirmation()
112
+ convStore->>convStore: titleUpdateConfirmationCallback?()
113
+ Note right of convStore: Shows dialog if title would change
114
+
115
+ %% ═══════════════════════════════════════════════════════════════════════════
116
+ Note over UI,IDB: 🗑️ DELETE CONVERSATION
117
+ %% ═══════════════════════════════════════════════════════════════════════════
118
+
119
+ UI->>convStore: deleteConversation(convId)
120
+ activate convStore
121
+ convStore->>DbSvc: deleteConversation(convId)
122
+ DbSvc->>IDB: DELETE FROM conversations WHERE id = ?
123
+ DbSvc->>IDB: DELETE FROM messages WHERE convId = ?
124
+ convStore->>convStore: conversations.filter(c => c.id !== convId)
125
+ alt deleted active conversation
126
+ convStore->>convStore: clearActiveConversation()
127
+ end
128
+ deactivate convStore
129
+
130
+ UI->>convStore: deleteAll()
131
+ activate convStore
132
+ convStore->>DbSvc: Delete all conversations and messages
133
+ convStore->>convStore: conversations = []
134
+ convStore->>convStore: clearActiveConversation()
135
+ deactivate convStore
136
+
137
+ %% ═══════════════════════════════════════════════════════════════════════════
138
+ Note over UI,IDB: � MCP SERVER PER-CHAT OVERRIDES
139
+ %% ═══════════════════════════════════════════════════════════════════════════
140
+
141
+ Note over convStore: Conversations can override which MCP servers are enabled.
142
+ Note over convStore: Uses pendingMcpServerOverrides before conversation<br/>is created, then persists to conversation metadata.
143
+
144
+ UI->>convStore: setMcpServerOverride(convId, serverName, override)
145
+ Note right of convStore: override = {enabled: boolean}
146
+
147
+ UI->>convStore: toggleMcpServerForChat(convId, serverName, enabled)
148
+ activate convStore
149
+ convStore->>convStore: setMcpServerOverride(convId, serverName, {enabled})
150
+ deactivate convStore
151
+
152
+ UI->>convStore: isMcpServerEnabledForChat(convId, serverName)
153
+ Note right of convStore: Check override → fall back to global MCP config
154
+
155
+ UI->>convStore: getAllMcpServerOverrides(convId)
156
+ Note right of convStore: Returns all overrides for a conversation
157
+
158
+ UI->>convStore: removeMcpServerOverride(convId, serverName)
159
+ UI->>convStore: getMcpServerOverride(convId, serverName)
160
+
161
+ %% ═══════════════════════════════��═══════════════════════════════════════════
162
+ Note over UI,IDB: 📤 EXPORT / 📥 IMPORT
163
+ %% ═══════════════════════════════════════════════════════════════════════════
164
+
165
+ UI->>convStore: exportAllConversations()
166
+ activate convStore
167
+ convStore->>DbSvc: getAllConversations()
168
+ loop each conversation
169
+ convStore->>DbSvc: getConversationMessages(convId)
170
+ end
171
+ convStore->>convStore: triggerDownload(JSON blob)
172
+ deactivate convStore
173
+
174
+ UI->>convStore: importConversations(file)
175
+ activate convStore
176
+ convStore->>convStore: Parse JSON file
177
+ convStore->>convStore: importConversationsData(parsed)
178
+ convStore->>DbSvc: importConversations(parsed)
179
+ Note right of DbSvc: Skips duplicate conversations<br/>(checks existing by ID)
180
+ DbSvc->>IDB: INSERT conversations + messages (skip existing)
181
+ convStore->>convStore: loadConversations()
182
+ deactivate convStore
183
+ ```
backend/llama.cpp/tools/ui/docs/flows/data-flow-simplified-model-mode.md ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```mermaid
2
+ %% MODEL Mode Data Flow (single model)
3
+ %% Detailed flows: ./flows/server-flow.mmd, ./flows/models-flow.mmd, ./flows/chat-flow.mmd
4
+
5
+ sequenceDiagram
6
+ participant User as 👤 User
7
+ participant UI as 🧩 UI
8
+ participant Stores as 🗄️ Stores
9
+ participant DB as 💾 IndexedDB
10
+ participant API as 🌐 llama-server
11
+
12
+ Note over User,API: 🚀 Initialization (see: server-flow.mmd, models-flow.mmd)
13
+
14
+ UI->>Stores: initialize()
15
+ Stores->>DB: load conversations
16
+ Stores->>API: GET /props
17
+ API-->>Stores: server config + modalities
18
+ Stores->>API: GET /v1/models
19
+ API-->>Stores: single model (auto-selected)
20
+
21
+ Note over User,API: 💬 Chat Flow (see: chat-flow.mmd)
22
+
23
+ User->>UI: send message
24
+ UI->>Stores: sendMessage()
25
+ Stores->>DB: save user message
26
+ Stores->>API: POST /v1/chat/completions (stream)
27
+ loop streaming
28
+ API-->>Stores: SSE chunks
29
+ Stores-->>UI: reactive update
30
+ end
31
+ API-->>Stores: done + timings
32
+ Stores->>DB: save assistant message
33
+
34
+ Note over User,API: 🔁 Regenerate
35
+
36
+ User->>UI: regenerate
37
+ Stores->>DB: create message branch
38
+ Note right of Stores: same streaming flow
39
+
40
+ Note over User,API: ⏹️ Stop
41
+
42
+ User->>UI: stop
43
+ Stores->>Stores: abort stream
44
+ Stores->>DB: save partial response
45
+ ```
backend/llama.cpp/tools/ui/docs/flows/data-flow-simplified-router-mode.md ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```mermaid
2
+ %% ROUTER Mode Data Flow (multi-model)
3
+ %% Detailed flows: ./flows/server-flow.mmd, ./flows/models-flow.mmd, ./flows/chat-flow.mmd
4
+
5
+ sequenceDiagram
6
+ participant User as 👤 User
7
+ participant UI as 🧩 UI
8
+ participant Stores as 🗄️ Stores
9
+ participant DB as 💾 IndexedDB
10
+ participant API as 🌐 llama-server
11
+
12
+ Note over User,API: 🚀 Initialization (see: server-flow.mmd, models-flow.mmd)
13
+
14
+ UI->>Stores: initialize()
15
+ Stores->>DB: load conversations
16
+ Stores->>API: GET /props
17
+ API-->>Stores: {role: "router"}
18
+ Stores->>API: GET /v1/models
19
+ API-->>Stores: models[] with status (loaded/available)
20
+ loop each loaded model
21
+ Stores->>API: GET /props?model=X
22
+ API-->>Stores: modalities (vision/audio)
23
+ end
24
+
25
+ Note over User,API: 🔄 Model Selection (see: models-flow.mmd)
26
+
27
+ User->>UI: select model
28
+ alt model not loaded
29
+ Stores->>API: POST /models/load
30
+ loop poll status
31
+ Stores->>API: GET /v1/models
32
+ API-->>Stores: check if loaded
33
+ end
34
+ Stores->>API: GET /props?model=X
35
+ API-->>Stores: cache modalities
36
+ end
37
+ Stores->>Stores: validate modalities vs conversation
38
+ alt valid
39
+ Stores->>Stores: select model
40
+ else invalid
41
+ Stores->>API: POST /models/unload
42
+ UI->>User: show error toast
43
+ end
44
+
45
+ Note over User,API: 💬 Chat Flow (see: chat-flow.mmd)
46
+
47
+ User->>UI: send message
48
+ UI->>Stores: sendMessage()
49
+ Stores->>DB: save user message
50
+ Stores->>API: POST /v1/chat/completions {model: X}
51
+ Note right of API: router forwards to model
52
+ loop streaming
53
+ API-->>Stores: SSE chunks + model info
54
+ Stores-->>UI: reactive update
55
+ end
56
+ API-->>Stores: done + timings
57
+ Stores->>DB: save assistant message + model used
58
+
59
+ Note over User,API: 🔁 Regenerate (optional: different model)
60
+
61
+ User->>UI: regenerate
62
+ Stores->>Stores: validate modalities up to this message
63
+ Stores->>DB: create message branch
64
+ Note right of Stores: same streaming flow
65
+
66
+ Note over User,API: ⏹️ Stop
67
+
68
+ User->>UI: stop
69
+ Stores->>Stores: abort stream
70
+ Stores->>DB: save partial response
71
+
72
+ Note over User,API: 🗑️ LRU Unloading
73
+
74
+ Note right of API: Server auto-unloads LRU models<br/>when cache full
75
+ User->>UI: select unloaded model
76
+ Note right of Stores: triggers load flow again
77
+ ```
backend/llama.cpp/tools/ui/docs/flows/database-flow.md ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```mermaid
2
+ sequenceDiagram
3
+ participant Store as 🗄️ Stores
4
+ participant DbSvc as ⚙️ DatabaseService
5
+ participant Dexie as 📦 Dexie ORM
6
+ participant IDB as 💾 IndexedDB
7
+
8
+ Note over DbSvc: Stateless service - all methods static<br/>Database: "LlamacppWebui"
9
+
10
+ %% ═══════════════════════════════════════════════════════════════════════════
11
+ Note over Store,IDB: 📊 SCHEMA
12
+ %% ═══════════════════════════════════════════════════════════════════════════
13
+
14
+ rect rgb(240, 248, 255)
15
+ Note over IDB: conversations table:<br/>id (PK), lastModified, currNode, name
16
+ end
17
+
18
+ rect rgb(255, 248, 240)
19
+ Note over IDB: messages table:<br/>id (PK), convId (FK), type, role, timestamp,<br/>parent, children[], content, thinking,<br/>toolCalls, extra[], model, timings
20
+ end
21
+
22
+ %% ═══════════════════════════════════════════════════════════════════════════
23
+ Note over Store,IDB: 💬 CONVERSATIONS CRUD
24
+ %% ═══════════════════════════════════════════════════════════════════════════
25
+
26
+ Store->>DbSvc: createConversation(name)
27
+ activate DbSvc
28
+ DbSvc->>DbSvc: Generate UUID
29
+ DbSvc->>Dexie: db.conversations.add({id, name, lastModified, currNode: ""})
30
+ Dexie->>IDB: INSERT
31
+ IDB-->>Dexie: success
32
+ DbSvc-->>Store: DatabaseConversation
33
+ deactivate DbSvc
34
+
35
+ Store->>DbSvc: getConversation(convId)
36
+ DbSvc->>Dexie: db.conversations.get(convId)
37
+ Dexie->>IDB: SELECT WHERE id = ?
38
+ IDB-->>DbSvc: DatabaseConversation
39
+
40
+ Store->>DbSvc: getAllConversations()
41
+ DbSvc->>Dexie: db.conversations.orderBy('lastModified').reverse().toArray()
42
+ Dexie->>IDB: SELECT ORDER BY lastModified DESC
43
+ IDB-->>DbSvc: DatabaseConversation[]
44
+
45
+ Store->>DbSvc: updateConversation(convId, updates)
46
+ DbSvc->>Dexie: db.conversations.update(convId, {...updates, lastModified})
47
+ Dexie->>IDB: UPDATE
48
+
49
+ Store->>DbSvc: deleteConversation(convId)
50
+ activate DbSvc
51
+ DbSvc->>Dexie: db.conversations.delete(convId)
52
+ Dexie->>IDB: DELETE FROM conversations
53
+ DbSvc->>Dexie: db.messages.where('convId').equals(convId).delete()
54
+ Dexie->>IDB: DELETE FROM messages WHERE convId = ?
55
+ deactivate DbSvc
56
+
57
+ %% ═══════════════════════════════════════════════════════════════════════════
58
+ Note over Store,IDB: 📝 MESSAGES CRUD
59
+ %% ═══════════════════════════════════════════════════════════════════════════
60
+
61
+ Store->>DbSvc: createRootMessage(convId)
62
+ activate DbSvc
63
+ DbSvc->>DbSvc: Create root message {type: "root", parent: null}
64
+ DbSvc->>Dexie: db.messages.add(rootMsg)
65
+ Dexie->>IDB: INSERT
66
+ DbSvc-->>Store: rootMessageId
67
+ deactivate DbSvc
68
+
69
+ Store->>DbSvc: createSystemMessage(convId, content, parentId)
70
+ activate DbSvc
71
+ DbSvc->>DbSvc: Create message {role: "system", parent: parentId}
72
+ DbSvc->>Dexie: db.messages.add(systemMsg)
73
+ Dexie->>IDB: INSERT
74
+ DbSvc-->>Store: DatabaseMessage
75
+ deactivate DbSvc
76
+
77
+ Store->>DbSvc: createMessageBranch(message, parentId)
78
+ activate DbSvc
79
+ DbSvc->>DbSvc: Generate UUID for new message
80
+ DbSvc->>Dexie: db.messages.add({...message, id, parent: parentId})
81
+ Dexie->>IDB: INSERT message
82
+
83
+ alt parentId exists
84
+ DbSvc->>Dexie: db.messages.get(parentId)
85
+ Dexie->>IDB: SELECT parent
86
+ DbSvc->>DbSvc: parent.children.push(newId)
87
+ DbSvc->>Dexie: db.messages.update(parentId, {children})
88
+ Dexie->>IDB: UPDATE parent.children
89
+ end
90
+
91
+ DbSvc->>Dexie: db.conversations.update(convId, {currNode: newId})
92
+ Dexie->>IDB: UPDATE conversation.currNode
93
+ DbSvc-->>Store: DatabaseMessage
94
+ deactivate DbSvc
95
+
96
+ Store->>DbSvc: getConversationMessages(convId)
97
+ DbSvc->>Dexie: db.messages.where('convId').equals(convId).toArray()
98
+ Dexie->>IDB: SELECT WHERE convId = ?
99
+ IDB-->>DbSvc: DatabaseMessage[]
100
+
101
+ Store->>DbSvc: updateMessage(msgId, updates)
102
+ DbSvc->>Dexie: db.messages.update(msgId, updates)
103
+ Dexie->>IDB: UPDATE
104
+
105
+ Store->>DbSvc: deleteMessage(msgId)
106
+ DbSvc->>Dexie: db.messages.delete(msgId)
107
+ Dexie->>IDB: DELETE
108
+
109
+ %% ═══════════════���═══════════════════════════════════════════════════════════
110
+ Note over Store,IDB: 🌳 BRANCHING OPERATIONS
111
+ %% ═══════════════════════════════════════════════════════════════════════════
112
+
113
+ Store->>DbSvc: updateCurrentNode(convId, nodeId)
114
+ DbSvc->>Dexie: db.conversations.update(convId, {currNode: nodeId, lastModified})
115
+ Dexie->>IDB: UPDATE
116
+
117
+ Store->>DbSvc: deleteMessageCascading(msgId)
118
+ activate DbSvc
119
+ DbSvc->>DbSvc: findDescendantMessages(msgId, allMessages)
120
+ Note right of DbSvc: Recursively find all children
121
+ loop each descendant
122
+ DbSvc->>Dexie: db.messages.delete(descendantId)
123
+ Dexie->>IDB: DELETE
124
+ end
125
+ DbSvc->>Dexie: db.messages.delete(msgId)
126
+ Dexie->>IDB: DELETE target message
127
+
128
+ alt target message has a parent
129
+ DbSvc->>Dexie: db.messages.get(parentId)
130
+ DbSvc->>DbSvc: parent.children.filter(id !== msgId)
131
+ DbSvc->>Dexie: db.messages.update(parentId, {children})
132
+ Note right of DbSvc: Remove deleted message from parent's children[]
133
+ end
134
+ deactivate DbSvc
135
+
136
+ %% ═══════════════════════════════════════════════════════════════════════════
137
+ Note over Store,IDB: 📥 IMPORT
138
+ %% ═══════════════════════════════════════════════════════════════════════════
139
+
140
+ Store->>DbSvc: importConversations(data)
141
+ activate DbSvc
142
+ loop each conversation in data
143
+ DbSvc->>Dexie: db.conversations.get(conv.id)
144
+ alt conversation already exists
145
+ Note right of DbSvc: Skip duplicate (keep existing)
146
+ else conversation is new
147
+ DbSvc->>Dexie: db.conversations.add(conversation)
148
+ Dexie->>IDB: INSERT conversation
149
+ loop each message
150
+ DbSvc->>Dexie: db.messages.add(message)
151
+ Dexie->>IDB: INSERT message
152
+ end
153
+ end
154
+ end
155
+ deactivate DbSvc
156
+
157
+ %% ═══════════════════════════════════════════════════════════════════════════
158
+ Note over Store,IDB: 🔗 MESSAGE TREE UTILITIES
159
+ %% ═══════════════════════════════════════════════════════════════════════════
160
+
161
+ Note over DbSvc: Used by stores (imported from utils):
162
+
163
+ rect rgb(240, 255, 240)
164
+ Note over DbSvc: filterByLeafNodeId(messages, leafId)<br/>→ Returns path from root to leaf<br/>→ Used to display current branch
165
+ end
166
+
167
+ rect rgb(240, 255, 240)
168
+ Note over DbSvc: findLeafNode(startId, messages)<br/>→ Traverse to deepest child<br/>→ Used for branch navigation
169
+ end
170
+
171
+ rect rgb(240, 255, 240)
172
+ Note over DbSvc: findDescendantMessages(msgId, messages)<br/>→ Find all children recursively<br/>→ Used for cascading deletes
173
+ end
174
+ ```
backend/llama.cpp/tools/ui/docs/flows/mcp-flow.md ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```mermaid
2
+ sequenceDiagram
3
+ participant UI as 🧩 McpServersSettings / ChatForm
4
+ participant chatStore as 🗄️ chatStore
5
+ participant mcpStore as 🗄️ mcpStore
6
+ participant mcpResStore as 🗄️ mcpResourceStore
7
+ participant convStore as 🗄️ conversationsStore
8
+ participant MCPSvc as ⚙️ MCPService
9
+ participant LS as 💾 LocalStorage
10
+ participant ExtMCP as 🔌 External MCP Server
11
+
12
+ Note over mcpStore: State:<br/>isInitializing, error<br/>toolCount, connectedServers<br/>healthChecks (Map)<br/>connections (Map)<br/>toolsIndex (Map)<br/>serverConfigs (Map)
13
+
14
+ Note over mcpResStore: State:<br/>serverResources (Map)<br/>cachedResources (Map)<br/>subscriptions (Map)<br/>attachments[]
15
+
16
+ %% ═══════════════════════════════════════════════════════════════════════════
17
+ Note over UI,ExtMCP: 🚀 INITIALIZATION (App Startup)
18
+ %% ═══════════════════════════════════════════════════════════════════════════
19
+
20
+ UI->>mcpStore: ensureInitialized()
21
+ activate mcpStore
22
+
23
+ mcpStore->>LS: get(MCP_SERVERS_LOCALSTORAGE_KEY)
24
+ LS-->>mcpStore: MCPServerSettingsEntry[]
25
+
26
+ mcpStore->>mcpStore: parseServerSettings(servers)
27
+ Note right of mcpStore: Filter enabled servers<br/>Build MCPServerConfig objects<br/>Per-chat overrides checked via convStore
28
+
29
+ loop For each enabled server
30
+ mcpStore->>mcpStore: runHealthCheck(serverId)
31
+ mcpStore->>mcpStore: updateHealthCheck(id, CONNECTING)
32
+
33
+ mcpStore->>MCPSvc: connect(serverName, config, clientInfo, capabilities, onPhase)
34
+ activate MCPSvc
35
+
36
+ MCPSvc->>MCPSvc: createTransport(config)
37
+ Note right of MCPSvc: WebSocket / StreamableHTTP / SSE<br/>with optional CORS proxy
38
+
39
+ MCPSvc->>ExtMCP: Transport handshake
40
+ ExtMCP-->>MCPSvc: Connection established
41
+
42
+ MCPSvc->>ExtMCP: Initialize request
43
+ Note right of ExtMCP: Exchange capabilities<br/>Server info, protocol version
44
+
45
+ ExtMCP-->>MCPSvc: InitializeResult (serverInfo, capabilities)
46
+
47
+ MCPSvc->>ExtMCP: listTools()
48
+ ExtMCP-->>MCPSvc: Tool[]
49
+
50
+ MCPSvc-->>mcpStore: MCPConnection
51
+ deactivate MCPSvc
52
+
53
+ mcpStore->>mcpStore: connections.set(serverName, connection)
54
+ mcpStore->>mcpStore: indexTools(connection.tools, serverName)
55
+ Note right of mcpStore: toolsIndex.set(toolName, serverName)<br/>Handle name conflicts with prefixes
56
+
57
+ mcpStore->>mcpStore: updateHealthCheck(id, SUCCESS)
58
+ mcpStore->>mcpStore: _connectedServers.push(serverName)
59
+
60
+ alt Server supports resources
61
+ mcpStore->>MCPSvc: listAllResources(connection)
62
+ MCPSvc->>ExtMCP: listResources()
63
+ ExtMCP-->>MCPSvc: MCPResource[]
64
+ MCPSvc-->>mcpStore: resources
65
+
66
+ mcpStore->>MCPSvc: listAllResourceTemplates(connection)
67
+ MCPSvc->>ExtMCP: listResourceTemplates()
68
+ ExtMCP-->>MCPSvc: MCPResourceTemplate[]
69
+ MCPSvc-->>mcpStore: templates
70
+
71
+ mcpStore->>mcpResStore: setServerResources(serverName, resources, templates)
72
+ end
73
+ end
74
+
75
+ mcpStore->>mcpStore: _isInitializing = false
76
+ deactivate mcpStore
77
+
78
+ %% ═══════════════════════════════════════════════════════════════════════════
79
+ Note over UI,ExtMCP: 🔧 TOOL EXECUTION (Chat with Tools)
80
+ %% ═══════════════════════════════════════════════════════════════════════════
81
+
82
+ UI->>mcpStore: executeTool(mcpCall: MCPToolCall, signal?)
83
+ activate mcpStore
84
+
85
+ mcpStore->>mcpStore: toolsIndex.get(mcpCall.function.name)
86
+ Note right of mcpStore: Resolve serverName from toolsIndex<br/>MCPToolCall = {id, type, function: {name, arguments}}
87
+
88
+ mcpStore->>mcpStore: acquireConnection()
89
+ Note right of mcpStore: activeFlowCount++<br/>Prevent shutdown during execution
90
+
91
+ mcpStore->>mcpStore: connection = connections.get(serverName)
92
+
93
+ mcpStore->>MCPSvc: callTool(connection, {name, arguments}, signal)
94
+ activate MCPSvc
95
+
96
+ MCPSvc->>MCPSvc: throwIfAborted(signal)
97
+ MCPSvc->>ExtMCP: callTool(name, arguments)
98
+
99
+ alt Tool execution success
100
+ ExtMCP-->>MCPSvc: ToolCallResult (content, isError)
101
+ MCPSvc->>MCPSvc: formatToolResult(result)
102
+ Note right of MCPSvc: Handle text, image (base64),<br/>embedded resource content
103
+ MCPSvc-->>mcpStore: ToolExecutionResult
104
+ else Tool execution error
105
+ ExtMCP-->>MCPSvc: Error
106
+ MCPSvc-->>mcpStore: throw Error
107
+ else Aborted
108
+ MCPSvc-->>mcpStore: throw AbortError
109
+ end
110
+
111
+ deactivate MCPSvc
112
+
113
+ mcpStore->>mcpStore: releaseConnection()
114
+ Note right of mcpStore: activeFlowCount--
115
+
116
+ mcpStore-->>UI: ToolExecutionResult
117
+ deactivate mcpStore
118
+
119
+ %% ═══════════════════════════════════════════════════════════════════════════
120
+ Note over UI,ExtMCP: � RESOURCE ATTACHMENT CONSUMPTION
121
+ %% ═══════════════════════════════════════════════════════════════════════════
122
+
123
+ chatStore->>mcpStore: consumeResourceAttachmentsAsExtras()
124
+ activate mcpStore
125
+ mcpStore->>mcpResStore: getAttachments()
126
+ mcpResStore-->>mcpStore: MCPResourceAttachment[]
127
+ mcpStore->>mcpStore: Convert attachments to message extras
128
+ mcpStore->>mcpResStore: clearAttachments()
129
+ mcpStore-->>chatStore: MessageExtra[] (for user message)
130
+ deactivate mcpStore
131
+
132
+ %% ═══════════════════════════════════════════════════════════════════════════
133
+ Note over UI,ExtMCP: �📝 PROMPT OPERATIONS
134
+ %% ═══════════════════════════════════════════════════════════════════════════
135
+
136
+ UI->>mcpStore: getAllPrompts()
137
+ activate mcpStore
138
+
139
+ loop For each connected server with prompts capability
140
+ mcpStore->>MCPSvc: listPrompts(connection)
141
+ MCPSvc->>ExtMCP: listPrompts()
142
+ ExtMCP-->>MCPSvc: Prompt[]
143
+ MCPSvc-->>mcpStore: prompts
144
+ end
145
+
146
+ mcpStore-->>UI: MCPPromptInfo[] (with serverName)
147
+ deactivate mcpStore
148
+
149
+ UI->>mcpStore: getPrompt(serverName, promptName, args?)
150
+ activate mcpStore
151
+
152
+ mcpStore->>MCPSvc: getPrompt(connection, name, args)
153
+ MCPSvc->>ExtMCP: getPrompt({name, arguments})
154
+ ExtMCP-->>MCPSvc: GetPromptResult (messages)
155
+ MCPSvc-->>mcpStore: GetPromptResult
156
+
157
+ mcpStore-->>UI: GetPromptResult
158
+ deactivate mcpStore
159
+
160
+ %% ═══════════════════════════════════════════════════════════════════════════
161
+ Note over UI,ExtMCP: 📁 RESOURCE OPERATIONS
162
+ %% ═══════════════════════════════════════════════════════════════════════════
163
+
164
+ UI->>mcpResStore: addAttachment(resourceInfo)
165
+ activate mcpResStore
166
+ mcpResStore->>mcpResStore: Create MCPResourceAttachment (loading: true)
167
+ mcpResStore-->>UI: attachment
168
+
169
+ UI->>mcpStore: readResource(serverName, uri)
170
+ activate mcpStore
171
+
172
+ mcpStore->>MCPSvc: readResource(connection, uri)
173
+ MCPSvc->>ExtMCP: readResource({uri})
174
+ ExtMCP-->>MCPSvc: MCPReadResourceResult (contents)
175
+ MCPSvc-->>mcpStore: contents
176
+
177
+ mcpStore-->>UI: MCPResourceContent[]
178
+ deactivate mcpStore
179
+
180
+ UI->>mcpResStore: updateAttachmentContent(attachmentId, content)
181
+ mcpResStore->>mcpResStore: cacheResourceContent(resource, content)
182
+ deactivate mcpResStore
183
+
184
+ %% ═══════════════════════════════════════════════════════════════════════════
185
+ Note over UI,ExtMCP: 🔄 AUTO-RECONNECTION
186
+ %% ═══════════════════════════════════════════════════════════════════════════
187
+
188
+ Note over mcpStore: On WebSocket close or connection error:
189
+ mcpStore->>mcpStore: autoReconnect(serverName, attempt)
190
+ activate mcpStore
191
+
192
+ mcpStore->>mcpStore: Calculate backoff delay
193
+ Note right of mcpStore: delay = min(30s, 1s * 2^attempt)
194
+
195
+ mcpStore->>mcpStore: Wait for delay
196
+ mcpStore->>mcpStore: reconnectServer(serverName)
197
+
198
+ alt Reconnection success
199
+ mcpStore->>mcpStore: updateHealthCheck(id, SUCCESS)
200
+ else Max attempts reached
201
+ mcpStore->>mcpStore: updateHealthCheck(id, ERROR)
202
+ end
203
+ deactivate mcpStore
204
+
205
+ %% ═══════════════════════════════════════════════════════════════════════════
206
+ Note over UI,ExtMCP: 🛑 SHUTDOWN
207
+ %% ═══════════════════════════════════════════════════════════════════════════
208
+
209
+ UI->>mcpStore: shutdown()
210
+ activate mcpStore
211
+
212
+ mcpStore->>mcpStore: Wait for activeFlowCount == 0
213
+
214
+ loop For each connection
215
+ mcpStore->>MCPSvc: disconnect(connection)
216
+ MCPSvc->>MCPSvc: transport.onclose = undefined
217
+ MCPSvc->>ExtMCP: close()
218
+ end
219
+
220
+ mcpStore->>mcpStore: connections.clear()
221
+ mcpStore->>mcpStore: toolsIndex.clear()
222
+ mcpStore->>mcpStore: _connectedServers = []
223
+
224
+ mcpStore->>mcpResStore: clear()
225
+ deactivate mcpStore
226
+ ```
backend/llama.cpp/tools/ui/docs/flows/models-flow.md ADDED
@@ -0,0 +1,181 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```mermaid
2
+ sequenceDiagram
3
+ participant UI as 🧩 ModelsSelector
4
+ participant Hooks as 🪝 useModelChangeValidation
5
+ participant modelsStore as 🗄️ modelsStore
6
+ participant serverStore as 🗄️ serverStore
7
+ participant convStore as 🗄️ conversationsStore
8
+ participant ModelsSvc as ⚙️ ModelsService
9
+ participant PropsSvc as ⚙️ PropsService
10
+ participant API as 🌐 llama-server
11
+
12
+ Note over modelsStore: State:<br/>models: ModelOption[]<br/>routerModels: ApiModelDataEntry[]<br/>selectedModelId, selectedModelName<br/>loading, updating, error<br/>modelLoadingStates (Map)<br/>modelPropsCache (Map)<br/>propsCacheVersion
13
+
14
+ %% ═══════════════════════════════════════════════════════════════════════════
15
+ Note over UI,API: 🚀 INITIALIZATION (MODEL mode)
16
+ %% ═══════════════════════════════════════════════════════════════════════════
17
+
18
+ UI->>modelsStore: fetch()
19
+ activate modelsStore
20
+ modelsStore->>modelsStore: loading = true
21
+
22
+ alt serverStore.props not loaded
23
+ modelsStore->>serverStore: fetch()
24
+ Note over serverStore: → see server-flow.mmd
25
+ end
26
+
27
+ modelsStore->>ModelsSvc: list()
28
+ ModelsSvc->>API: GET /v1/models
29
+ API-->>ModelsSvc: ApiModelListResponse {data: [model]}
30
+
31
+ modelsStore->>modelsStore: models = $state(mapped)
32
+ Note right of modelsStore: Map to ModelOption[]:<br/>{id, name, model, description, capabilities}
33
+
34
+ Note over modelsStore: MODEL mode: Get modalities from serverStore.props
35
+ modelsStore->>modelsStore: modelPropsCache.set(model.id, serverStore.props)
36
+ modelsStore->>modelsStore: models[0].modalities = props.modalities
37
+
38
+ modelsStore->>modelsStore: Auto-select single model
39
+ Note right of modelsStore: selectedModelId = models[0].id
40
+ modelsStore->>modelsStore: loading = false
41
+ deactivate modelsStore
42
+
43
+ %% ═══════════════════════════════════════════════════════════════════════════
44
+ Note over UI,API: 🚀 INITIALIZATION (ROUTER mode)
45
+ %% ═══════════════════════════════════════════════════════════════════════════
46
+
47
+ UI->>modelsStore: fetch()
48
+ activate modelsStore
49
+ modelsStore->>ModelsSvc: list()
50
+ ModelsSvc->>API: GET /v1/models
51
+ API-->>ModelsSvc: ApiModelListResponse
52
+ modelsStore->>modelsStore: models = $state(mapped)
53
+ deactivate modelsStore
54
+
55
+ Note over UI: After models loaded, layout triggers:
56
+ UI->>modelsStore: fetchRouterModels()
57
+ activate modelsStore
58
+ modelsStore->>ModelsSvc: listRouter()
59
+ ModelsSvc->>API: GET /v1/models
60
+ API-->>ModelsSvc: ApiRouterModelsListResponse
61
+ Note right of API: {data: [{id, status, path, in_cache}]}
62
+ modelsStore->>modelsStore: routerModels = $state(data)
63
+
64
+ modelsStore->>modelsStore: fetchModalitiesForLoadedModels()
65
+ loop each model where status === "loaded"
66
+ modelsStore->>PropsSvc: fetchForModel(modelId)
67
+ PropsSvc->>API: GET /props?model={modelId}
68
+ API-->>PropsSvc: ApiLlamaCppServerProps
69
+ modelsStore->>modelsStore: modelPropsCache.set(modelId, props)
70
+ end
71
+ modelsStore->>modelsStore: propsCacheVersion++
72
+ deactivate modelsStore
73
+
74
+ %% ═══════════════════════════════════════════════════════════════════════════
75
+ Note over UI,API: 🔄 MODEL SELECTION (ROUTER mode)
76
+ %% ═══════════════════════════════════════════════════════════════════════════
77
+
78
+ UI->>Hooks: useModelChangeValidation({getRequiredModalities, onSuccess?, onValidationFailure?})
79
+ Note over Hooks: Hook configured per-component:<br/>ChatForm: getRequiredModalities = usedModalities<br/>ChatMessage: getRequiredModalities = getModalitiesUpToMessage(msgId)
80
+
81
+ UI->>Hooks: handleModelChange(modelId, modelName)
82
+ activate Hooks
83
+ Hooks->>Hooks: previousSelectedModelId = modelsStore.selectedModelId
84
+ Hooks->>modelsStore: isModelLoaded(modelName)?
85
+
86
+ alt model NOT loaded
87
+ Hooks->>modelsStore: loadModel(modelName)
88
+ Note over modelsStore: → see LOAD MODEL section below
89
+ end
90
+
91
+ Note over Hooks: Always fetch props (from cache or API)
92
+ Hooks->>modelsStore: fetchModelProps(modelName)
93
+ modelsStore-->>Hooks: props
94
+
95
+ Hooks->>convStore: getRequiredModalities()
96
+ convStore-->>Hooks: {vision, audio}
97
+
98
+ Hooks->>Hooks: Validate: model.modalities ⊇ required?
99
+
100
+ alt validation PASSED
101
+ Hooks->>modelsStore: selectModelById(modelId)
102
+ Hooks-->>UI: return true
103
+ else validation FAILED
104
+ Hooks->>UI: toast.error("Model doesn't support required modalities")
105
+ alt model was just loaded
106
+ Hooks->>modelsStore: unloadModel(modelName)
107
+ end
108
+ alt onValidationFailure provided
109
+ Hooks->>modelsStore: selectModelById(previousSelectedModelId)
110
+ end
111
+ Hooks-->>UI: return false
112
+ end
113
+ deactivate Hooks
114
+
115
+ %% ═══════════════════════════════════════════════════════════════════════════
116
+ Note over UI,API: ⬆️ LOAD MODEL (ROUTER mode)
117
+ %% ═══════════════════════════════════════════════════════════════════════════
118
+
119
+ modelsStore->>modelsStore: loadModel(modelId)
120
+ activate modelsStore
121
+
122
+ alt already loaded
123
+ modelsStore-->>modelsStore: return (no-op)
124
+ end
125
+
126
+ modelsStore->>modelsStore: modelLoadingStates.set(modelId, true)
127
+ modelsStore->>ModelsSvc: load(modelId)
128
+ ModelsSvc->>API: POST /models/load {model: modelId}
129
+ API-->>ModelsSvc: {status: "loading"}
130
+
131
+ modelsStore->>modelsStore: pollForModelStatus(modelId, LOADED)
132
+ loop poll every 500ms (max 60 attempts)
133
+ modelsStore->>modelsStore: fetchRouterModels()
134
+ modelsStore->>ModelsSvc: listRouter()
135
+ ModelsSvc->>API: GET /v1/models
136
+ API-->>ModelsSvc: models[]
137
+ modelsStore->>modelsStore: getModelStatus(modelId)
138
+ alt status === LOADED
139
+ Note right of modelsStore: break loop
140
+ else status === LOADING
141
+ Note right of modelsStore: wait 500ms, continue
142
+ end
143
+ end
144
+
145
+ modelsStore->>modelsStore: updateModelModalities(modelId)
146
+ modelsStore->>PropsSvc: fetchForModel(modelId)
147
+ PropsSvc->>API: GET /props?model={modelId}
148
+ API-->>PropsSvc: props with modalities
149
+ modelsStore->>modelsStore: modelPropsCache.set(modelId, props)
150
+ modelsStore->>modelsStore: propsCacheVersion++
151
+
152
+ modelsStore->>modelsStore: modelLoadingStates.set(modelId, false)
153
+ deactivate modelsStore
154
+
155
+ %% ═══════════════════════════════════════════════════════════════════════════
156
+ Note over UI,API: ⬇️ UNLOAD MODEL (ROUTER mode)
157
+ %% ═══════════════════════════════════════════════════════════════════════════
158
+
159
+ modelsStore->>modelsStore: unloadModel(modelId)
160
+ activate modelsStore
161
+ modelsStore->>modelsStore: modelLoadingStates.set(modelId, true)
162
+ modelsStore->>ModelsSvc: unload(modelId)
163
+ ModelsSvc->>API: POST /models/unload {model: modelId}
164
+
165
+ modelsStore->>modelsStore: pollForModelStatus(modelId, UNLOADED)
166
+ loop poll until unloaded
167
+ modelsStore->>ModelsSvc: listRouter()
168
+ ModelsSvc->>API: GET /v1/models
169
+ end
170
+
171
+ modelsStore->>modelsStore: modelLoadingStates.set(modelId, false)
172
+ deactivate modelsStore
173
+
174
+ %% ═══════════════════════════════════════════════════════════════════════════
175
+ Note over UI,API: 📊 COMPUTED GETTERS
176
+ %% ═══════════════════════════════════════════════════════════════════════════
177
+
178
+ Note over modelsStore: Getters:<br/>- selectedModel: ModelOption | null<br/>- loadedModelIds: string[] (from routerModels)<br/>- loadingModelIds: string[] (from modelLoadingStates)<br/>- singleModelName: string | null (MODEL mode only)
179
+
180
+ Note over modelsStore: Modality helpers:<br/>- getModelModalities(modelId): {vision, audio}<br/>- modelSupportsVision(modelId): boolean<br/>- modelSupportsAudio(modelId): boolean
181
+ ```
backend/llama.cpp/tools/ui/docs/flows/server-flow.md ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```mermaid
2
+ sequenceDiagram
3
+ participant UI as 🧩 +layout.svelte
4
+ participant serverStore as 🗄️ serverStore
5
+ participant PropsSvc as ⚙️ PropsService
6
+ participant API as 🌐 llama-server
7
+
8
+ Note over serverStore: State:<br/>props: ApiLlamaCppServerProps | null<br/>loading, error<br/>role: ServerRole | null (MODEL | ROUTER)<br/>fetchPromise (deduplication)
9
+
10
+ %% ═══════════════════════════════════════════════════════════════════════════
11
+ Note over UI,API: 🚀 INITIALIZATION
12
+ %% ═══════════════════════════════════════════════════════════════════════════
13
+
14
+ UI->>serverStore: fetch()
15
+ activate serverStore
16
+
17
+ alt fetchPromise exists (already fetching)
18
+ serverStore-->>UI: return fetchPromise
19
+ Note right of serverStore: Deduplicate concurrent calls
20
+ end
21
+
22
+ serverStore->>serverStore: loading = true
23
+ serverStore->>serverStore: fetchPromise = new Promise()
24
+
25
+ serverStore->>PropsSvc: fetch()
26
+ PropsSvc->>API: GET /props
27
+ API-->>PropsSvc: ApiLlamaCppServerProps
28
+ Note right of API: {role, model_path, model_alias,<br/>modalities, default_generation_settings, ...}
29
+
30
+ PropsSvc-->>serverStore: props
31
+ serverStore->>serverStore: props = $state(data)
32
+
33
+ serverStore->>serverStore: detectRole(props)
34
+ Note right of serverStore: role = props.role === "router"<br/> ? ServerRole.ROUTER<br/> : ServerRole.MODEL
35
+
36
+ serverStore->>serverStore: loading = false
37
+ serverStore->>serverStore: fetchPromise = null
38
+ deactivate serverStore
39
+
40
+ %% ═══════════════════════════════════════════════════════════════════════════
41
+ Note over UI,API: 📊 COMPUTED GETTERS
42
+ %% ═══════════════════════════════════════════════════════════════════════════
43
+
44
+ Note over serverStore: Getters from props:
45
+
46
+ rect rgb(240, 255, 240)
47
+ Note over serverStore: defaultParams<br/>→ props.default_generation_settings.params<br/>(temperature, top_p, top_k, etc.)
48
+ end
49
+
50
+ rect rgb(240, 255, 240)
51
+ Note over serverStore: contextSize<br/>→ props.default_generation_settings.n_ctx
52
+ end
53
+
54
+ rect rgb(255, 240, 240)
55
+ Note over serverStore: isRouterMode<br/>→ role === ServerRole.ROUTER
56
+ end
57
+
58
+ rect rgb(255, 240, 240)
59
+ Note over serverStore: isModelMode<br/>→ role === ServerRole.MODEL
60
+ end
61
+
62
+ %% ═══════════════════════════════════════════════════════════════════════════
63
+ Note over UI,API: 🔗 RELATIONSHIPS
64
+ %% ═══════════════════════════════════════════════════════════════════════════
65
+
66
+ Note over serverStore: Used by:
67
+ Note right of serverStore: - modelsStore: role detection, MODEL mode modalities<br/>- settingsStore: syncWithServerDefaults (defaultParams)<br/>- chatStore: contextSize for processing state<br/>- UI components: isRouterMode for conditional rendering
68
+
69
+ %% ═══════════════════════════════════════════════════════════════════════════
70
+ Note over UI,API: ❌ ERROR HANDLING
71
+ %% ═══════════════════════════════════════════════════════════════════════════
72
+
73
+ Note over serverStore: getErrorMessage(): string | null<br/>Returns formatted error for UI display
74
+
75
+ Note over serverStore: clear(): void<br/>Resets all state (props, error, loading, role)
76
+ ```
backend/llama.cpp/tools/ui/docs/flows/settings-flow.md ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```mermaid
2
+ sequenceDiagram
3
+ participant UI as 🧩 ChatSettings
4
+ participant settingsStore as 🗄️ settingsStore
5
+ participant serverStore as 🗄️ serverStore
6
+ participant ParamSvc as ⚙️ ParameterSyncService
7
+ participant LS as 💾 LocalStorage
8
+
9
+ Note over settingsStore: State:<br/>config: SettingsConfigType<br/>theme: string ("auto" | "light" | "dark")<br/>isInitialized: boolean<br/>userOverrides: Set&lt;string&gt;
10
+
11
+ %% ═══════════════════════════════════════════════════════════════════════════
12
+ Note over UI,LS: 🚀 INITIALIZATION
13
+ %% ═══════════════════════════════════════════════════════════════════════════
14
+
15
+ Note over settingsStore: Auto-initialized in constructor (browser only)
16
+ settingsStore->>settingsStore: initialize()
17
+ activate settingsStore
18
+
19
+ settingsStore->>settingsStore: loadConfig()
20
+ settingsStore->>LS: get("llama-config")
21
+ LS-->>settingsStore: StoredConfig | null
22
+
23
+ alt config exists
24
+ settingsStore->>settingsStore: Merge with SETTING_CONFIG_DEFAULT
25
+ Note right of settingsStore: Fill missing keys with defaults
26
+ else no config
27
+ settingsStore->>settingsStore: config = SETTING_CONFIG_DEFAULT
28
+ end
29
+
30
+ settingsStore->>LS: get("llama-userOverrides")
31
+ LS-->>settingsStore: string[] | null
32
+ settingsStore->>settingsStore: userOverrides = new Set(data)
33
+
34
+ settingsStore->>settingsStore: loadTheme()
35
+ settingsStore->>LS: get("llama-theme")
36
+ LS-->>settingsStore: theme | "auto"
37
+
38
+ settingsStore->>settingsStore: isInitialized = true
39
+ deactivate settingsStore
40
+
41
+ %% ═══════════════════════════════════════════════════════════════════════════
42
+ Note over UI,LS: 🔄 SYNC WITH SERVER DEFAULTS
43
+ %% ═══════════════════════════════════════════════════════════════════════════
44
+
45
+ Note over UI: Triggered from +layout.svelte when serverStore.props loaded
46
+ UI->>settingsStore: syncWithServerDefaults()
47
+ activate settingsStore
48
+
49
+ settingsStore->>serverStore: defaultParams
50
+ serverStore-->>settingsStore: {temperature, top_p, top_k, ...}
51
+
52
+ loop each SYNCABLE_PARAMETER
53
+ alt key NOT in userOverrides
54
+ settingsStore->>settingsStore: config[key] = serverDefault[key]
55
+ Note right of settingsStore: Non-overridden params adopt server default
56
+ else key in userOverrides
57
+ Note right of settingsStore: Keep user value, skip server default
58
+ end
59
+ end
60
+
61
+ alt serverStore.props has uiSettings
62
+ settingsStore->>settingsStore: Apply uiSettings from server
63
+ Note right of settingsStore: Server-provided UI settings<br/>(e.g. showRawOutputSwitch)
64
+ end
65
+
66
+ settingsStore->>settingsStore: saveConfig()
67
+ deactivate settingsStore
68
+
69
+ %% ═══════════════════════════════════════════════════════════════════════════
70
+ Note over UI,LS: ⚙️ UPDATE CONFIG
71
+ %% ═══════════════════════════════════════════════════════════════════════════
72
+
73
+ UI->>settingsStore: updateConfig(key, value)
74
+ activate settingsStore
75
+ settingsStore->>settingsStore: config[key] = value
76
+
77
+ alt value matches server default for key
78
+ settingsStore->>settingsStore: userOverrides.delete(key)
79
+ Note right of settingsStore: Matches server default, remove override
80
+ else value differs from server default
81
+ settingsStore->>settingsStore: userOverrides.add(key)
82
+ Note right of settingsStore: Mark as user-modified (won't be overwritten)
83
+ end
84
+
85
+ settingsStore->>settingsStore: saveConfig()
86
+ settingsStore->>LS: set(CONFIG_LOCALSTORAGE_KEY, config)
87
+ settingsStore->>LS: set(USER_OVERRIDES_LOCALSTORAGE_KEY, [...userOverrides])
88
+ deactivate settingsStore
89
+
90
+ UI->>settingsStore: updateMultipleConfig({key1: val1, key2: val2})
91
+ activate settingsStore
92
+ Note right of settingsStore: Batch update, single save
93
+ settingsStore->>settingsStore: For each key: config[key] = value
94
+ settingsStore->>settingsStore: For each key: userOverrides.add(key)
95
+ settingsStore->>settingsStore: saveConfig()
96
+ deactivate settingsStore
97
+
98
+ %% ════════════════���══════════════════════════════════════════════════════════
99
+ Note over UI,LS: 🔄 RESET
100
+ %% ═══════════════════════════════════════════════════════════════════════════
101
+
102
+ UI->>settingsStore: resetConfig()
103
+ activate settingsStore
104
+ settingsStore->>settingsStore: config = {...SETTING_CONFIG_DEFAULT}
105
+ settingsStore->>settingsStore: userOverrides.clear()
106
+ Note right of settingsStore: All params reset to defaults<br/>Next syncWithServerDefaults will adopt server values
107
+ settingsStore->>settingsStore: saveConfig()
108
+ deactivate settingsStore
109
+
110
+ UI->>settingsStore: resetParameterToServerDefault(key)
111
+ activate settingsStore
112
+ settingsStore->>settingsStore: userOverrides.delete(key)
113
+ settingsStore->>serverStore: defaultParams[key]
114
+ settingsStore->>settingsStore: config[key] = serverDefault
115
+ settingsStore->>settingsStore: saveConfig()
116
+ deactivate settingsStore
117
+
118
+ %% ═══════════════════════════════════════════════════════════════════════════
119
+ Note over UI,LS: 🎨 THEME
120
+ %% ═══════════════════════════════════════════════════════════════════════════
121
+
122
+ UI->>settingsStore: updateTheme(newTheme)
123
+ activate settingsStore
124
+ settingsStore->>settingsStore: theme = newTheme
125
+ settingsStore->>settingsStore: saveTheme()
126
+ settingsStore->>LS: set("llama-theme", theme)
127
+ deactivate settingsStore
128
+
129
+ %% ═══════════════════════════════════════════════════════════════════════════
130
+ Note over UI,LS: 📊 PARAMETER INFO
131
+ %% ═══════════════════════════════════════════════════════════════════════════
132
+
133
+ UI->>settingsStore: getParameterInfo(key)
134
+ settingsStore->>ParamSvc: getParameterInfo(key, config, serverDefaults, userOverrides)
135
+ ParamSvc-->>settingsStore: ParameterInfo
136
+ Note right of ParamSvc: {<br/> currentValue,<br/> serverDefault,<br/> isUserOverride: boolean,<br/> canSync: boolean,<br/> isDifferentFromServer: boolean<br/>}
137
+
138
+ UI->>settingsStore: getParameterDiff()
139
+ settingsStore->>ParamSvc: createParameterDiff(config, serverDefaults, userOverrides)
140
+ ParamSvc-->>settingsStore: ParameterDiff[]
141
+ Note right of ParamSvc: Array of parameters where user != server
142
+
143
+ %% ═══════════════════════════════════════════════════════════════════════════
144
+ Note over UI,LS: 📋 CONFIG CATEGORIES
145
+ %% ═══════════════════════════════════════════════════════════════════════════
146
+
147
+ Note over settingsStore: Syncable with server (from /props):
148
+ rect rgb(240, 255, 240)
149
+ Note over settingsStore: temperature, top_p, top_k, min_p<br/>repeat_penalty, presence_penalty, frequency_penalty<br/>dynatemp_range, dynatemp_exponent<br/>typ_p, xtc_probability, xtc_threshold<br/>dry_multiplier, dry_base, dry_allowed_length, dry_penalty_last_n
150
+ end
151
+
152
+ Note over settingsStore: UI-only (not synced):
153
+ rect rgb(255, 240, 240)
154
+ Note over settingsStore: systemMessage, custom (JSON)<br/>showStatistics, enableContinueGeneration<br/>autoMicOnEmpty, disableAutoScroll<br/>apiKey, pdfAsImage, disableReasoningParsing, showRawOutputSwitch
155
+ end
156
+ ```
backend/llama.cpp/tools/ui/embed.cpp ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // llama-ui-embed: generate ui.cpp / ui.h that embed UI assets as C arrays.
2
+ //
3
+ // Usage:
4
+ // llama-ui-embed <out_cpp> <out_h> [<asset_dir>]
5
+ //
6
+ // Recursively embeds every regular file under <asset_dir>.
7
+ // Asset names are relative paths from <asset_dir> (e.g. "_app/immutable/bundle.HASH.js").
8
+ // Without <asset_dir>, emits an empty asset table.
9
+
10
+ #include <inttypes.h>
11
+ #include <stdarg.h>
12
+ #include <stdint.h>
13
+ #include <stdio.h>
14
+ #include <string.h>
15
+
16
+ #include <algorithm>
17
+ #include <filesystem>
18
+ #include <fstream>
19
+ #include <functional>
20
+ #include <string>
21
+ #include <vector>
22
+
23
+
24
+ static const char * mime_from_ext(const std::string & name) {
25
+ auto ext = name.rfind('.');
26
+ if (ext == std::string::npos) return "application/octet-stream";
27
+ std::string e = name.substr(ext + 1);
28
+ if (e == "html") return "text/html; charset=utf-8";
29
+ if (e == "css") return "text/css";
30
+ if (e == "js") return "application/javascript";
31
+ if (e == "json") return "application/json";
32
+ if (e == "webmanifest") return "application/manifest+json";
33
+ if (e == "svg") return "image/svg+xml";
34
+ if (e == "png") return "image/png";
35
+ if (e == "jpg" ||
36
+ e == "jpeg") return "image/jpeg";
37
+ if (e == "ico") return "image/x-icon";
38
+ if (e == "woff") return "font/woff";
39
+ if (e == "woff2") return "font/woff2";
40
+ return "application/octet-stream";
41
+ }
42
+
43
+ // Computes FNV-1a hash of the data
44
+ static uint64_t fnv_hash(const uint8_t * data, size_t len) {
45
+ const uint64_t fnv_prime = 0x100000001b3ULL;
46
+ uint64_t hash = 0xcbf29ce484222325ULL;
47
+
48
+ for (size_t i = 0; i < len; ++i) {
49
+ hash ^= data[i];
50
+ hash *= fnv_prime;
51
+ }
52
+ return hash;
53
+ }
54
+
55
+ static bool read_file(const std::filesystem::path & path, std::vector<unsigned char> & out) {
56
+ std::ifstream f(path, std::ios::binary | std::ios::ate);
57
+ if (!f) {
58
+ fprintf(stderr, "embed: cannot open %s\n", path.string().c_str());
59
+ return false;
60
+ }
61
+ const auto sz = f.tellg();
62
+ if (sz < 0) {
63
+ return false;
64
+ }
65
+ f.seekg(0);
66
+ out.resize(static_cast<size_t>(sz));
67
+ if (sz > 0 && !f.read(reinterpret_cast<char *>(out.data()), sz)) {
68
+ return false;
69
+ }
70
+ return true;
71
+ }
72
+
73
+ static void append_bytes_hex(std::string & out, const std::vector<unsigned char> & bytes) {
74
+ static const char hex[] = "0123456789abcdef";
75
+ out.reserve(out.size() + bytes.size() * 5);
76
+ for (unsigned char b : bytes) {
77
+ out += '0';
78
+ out += 'x';
79
+ out += hex[b >> 4];
80
+ out += hex[b & 0xf];
81
+ out += ',';
82
+ }
83
+ }
84
+
85
+ static bool write_if_different(const std::string & path, const std::string & content) {
86
+ std::ifstream f(path, std::ios::binary | std::ios::ate);
87
+ if (f) {
88
+ const auto sz = f.tellg();
89
+ if (sz >= 0 && static_cast<size_t>(sz) == content.size()) {
90
+ std::string existing(static_cast<size_t>(sz), '\0');
91
+ f.seekg(0);
92
+ if (sz == 0 || f.read(existing.data(), sz)) {
93
+ if (existing == content) {
94
+ return true;
95
+ }
96
+ }
97
+ }
98
+ }
99
+
100
+ std::ofstream out(path, std::ios::binary | std::ios::trunc);
101
+ if (!out) {
102
+ fprintf(stderr, "embed: cannot write %s\n", path.c_str());
103
+ return false;
104
+ }
105
+ if (!content.empty()) {
106
+ out.write(content.data(), static_cast<std::streamsize>(content.size()));
107
+ }
108
+ bool ok = out.good();
109
+ if (ok) {
110
+ printf("embed: write output file %s\n", path.c_str());
111
+ }
112
+ return ok;
113
+ }
114
+
115
+ static std::string path_basename(const std::string & name) {
116
+ const size_t p = name.rfind('/');
117
+ return p == std::string::npos ? name : name.substr(p + 1);
118
+ }
119
+ static bool str_starts_with(const std::string & s, const char * prefix) {
120
+ const size_t n = strlen(prefix);
121
+ return s.size() >= n && s.compare(0, n, prefix) == 0;
122
+ }
123
+ static bool str_ends_with(const std::string & s, const char * suffix) {
124
+ const size_t n = strlen(suffix);
125
+ return s.size() >= n && s.compare(s.size() - n, n, suffix) == 0;
126
+ }
127
+
128
+ static std::string fmt(const char * pattern, ...) {
129
+ char tmp[512];
130
+ va_list ap;
131
+ va_start(ap, pattern);
132
+ const int n = vsnprintf(tmp, sizeof(tmp), pattern, ap);
133
+ va_end(ap);
134
+ return (n > 0) ? std::string(tmp, static_cast<size_t>(n)) : std::string();
135
+ }
136
+
137
+ struct asset_entry {
138
+ std::string name;
139
+ std::filesystem::path path;
140
+ };
141
+
142
+ int main(int argc, char ** argv) {
143
+ if (argc < 3 || argc > 4) {
144
+ fprintf(stderr, "usage: %s <out_cpp> <out_h> [<asset_dir>]\n", argv[0]);
145
+ return 1;
146
+ }
147
+
148
+ const std::string out_cpp = argv[1];
149
+ const std::string out_h = argv[2];
150
+ const std::string asset_dir = (argc >= 4) ? argv[3] : std::string();
151
+
152
+ const bool use_gzip = !asset_dir.empty() && std::filesystem::exists(asset_dir + "/_gzip");
153
+ const std::string in_dir = use_gzip ? (asset_dir + "/_gzip") : asset_dir;
154
+
155
+ std::vector<asset_entry> assets;
156
+ if (!in_dir.empty()) {
157
+ const std::filesystem::path dir = in_dir;
158
+
159
+ std::error_code ec;
160
+ std::filesystem::recursive_directory_iterator it(dir, ec);
161
+ if (ec) {
162
+ fprintf(stderr, "embed: cannot iterate %s: %s\n", argv[3], ec.message().c_str());
163
+ return 1;
164
+ }
165
+ for (const auto & entry : it) {
166
+ if (!entry.is_regular_file()) {
167
+ continue;
168
+ }
169
+ // name is the relative path from dir, with forward slashes
170
+ const std::string name = entry.path().lexically_relative(dir).generic_string();
171
+ assets.push_back({ name, entry.path() });
172
+ }
173
+
174
+ // directory iteration order is unspecified; sort for reproducible output
175
+ std::sort(assets.begin(), assets.end(),
176
+ [](const asset_entry & a, const asset_entry & b) { return a.name < b.name; });
177
+ }
178
+
179
+ const int n_assets = static_cast<int>(assets.size());
180
+
181
+ if (n_assets > 0) {
182
+ using match_fn = std::function<bool(const std::string &)>;
183
+ auto exact = [](const char * name) -> match_fn {
184
+ return [name](const std::string & base) { return base == name; };
185
+ };
186
+
187
+ struct required_check { const char * label; match_fn match; bool found; };
188
+ required_check checks[] = {
189
+ { "index.html", exact("index.html"), false },
190
+ { "manifest.webmanifest", exact("manifest.webmanifest"), false },
191
+ { "sw.js", exact("sw.js"), false },
192
+ { "build.json", exact("build.json"), false },
193
+ { "version.json", exact("version.json"), false },
194
+ { "bundle[hash].js", [](const std::string & b) {
195
+ return str_starts_with(b, "bundle") && str_ends_with(b, ".js");
196
+ }, false },
197
+ { "bundle[hash].css", [](const std::string & b) {
198
+ return str_starts_with(b, "bundle") && str_ends_with(b, ".css");
199
+ }, false },
200
+ { "workbox[hash].js", [](const std::string & b) {
201
+ return str_starts_with(b, "workbox") && str_ends_with(b, ".js");
202
+ }, false },
203
+ };
204
+
205
+ for (const auto & a : assets) {
206
+ const std::string base = path_basename(a.name);
207
+ for (auto & c : checks) {
208
+ if (!c.found) { c.found = c.match(base); }
209
+ }
210
+ }
211
+
212
+ std::vector<const char *> missing;
213
+ for (const auto & c : checks) {
214
+ if (!c.found) { missing.push_back(c.label); }
215
+ }
216
+ if (!missing.empty()) {
217
+ fprintf(stderr, "\ncurrent asset files:\n");
218
+ for (const auto & a : assets) {
219
+ fprintf(stderr, " %s\n", a.name.c_str());
220
+ }
221
+ fprintf(stderr, "missing required asset(s):\n");
222
+ for (const char * m : missing) {
223
+ fprintf(stderr, " %s\n", m);
224
+ }
225
+ fprintf(stderr, "hint: try cleaning your build directory: %s\n", in_dir.c_str());
226
+ return 1;
227
+ }
228
+ }
229
+
230
+ std::string h;
231
+ h += "#pragma once\n\n#include <array>\n#include <string>\n\n";
232
+ if (n_assets > 0) {
233
+ h += "#define LLAMA_UI_HAS_ASSETS 1\n\n";
234
+ }
235
+ h +=
236
+ "struct llama_ui_asset {\n"
237
+ " std::string name;\n"
238
+ " const unsigned char * data;\n"
239
+ " std::size_t size;\n"
240
+ " std::string etag;\n"
241
+ " std::string type;\n"
242
+ "};\n\n"
243
+ "const llama_ui_asset * llama_ui_find_asset(const std::string & name);\n"
244
+ "bool llama_ui_use_gzip();\n";
245
+ h += fmt("const std::array<llama_ui_asset, %d> & llama_ui_get_assets();\n", n_assets);
246
+
247
+ std::string cpp;
248
+ cpp += "#include \"ui.h\"\n\n";
249
+
250
+ if (n_assets > 0) {
251
+ for (int i = 0; i < n_assets; i++) {
252
+ std::vector<unsigned char> bytes;
253
+ if (!read_file(assets[i].path, bytes)) {
254
+ return 1;
255
+ }
256
+ if (bytes.empty()) {
257
+ fprintf(stderr, "embed: empty file: %s\n", assets[i].path.generic_string().c_str());
258
+ return 1;
259
+ }
260
+ cpp += fmt("static const unsigned char asset_%d_data[] = {", i);
261
+ append_bytes_hex(cpp, bytes);
262
+ const auto hash = fnv_hash(bytes.data(), bytes.size());
263
+
264
+ cpp += fmt("};\nstatic const std::size_t asset_%d_size = %zu;\n",
265
+ i, bytes.size());
266
+ cpp += fmt("static const char asset_%d_etag[] = \"\\\"0x%016" PRIx64 "\\\"\";\n\n",
267
+ i, hash);
268
+ }
269
+
270
+ cpp += fmt("static const std::array<llama_ui_asset, %d> g_assets = {{\n", n_assets);
271
+ for (int i = 0; i < n_assets; i++) {
272
+ const std::string & name = assets[i].name;
273
+ cpp += fmt(" { \"%s\", asset_%d_data, asset_%d_size, asset_%d_etag, \"%s\" },\n",
274
+ name.c_str(), i, i, i, mime_from_ext(name));
275
+ }
276
+ cpp += "}};\n\n";
277
+
278
+ cpp +=
279
+ "const llama_ui_asset * llama_ui_find_asset(const std::string & name) {\n"
280
+ " for (const auto & a : g_assets) {\n"
281
+ " if (a.name == name) {\n"
282
+ " return &a;\n"
283
+ " }\n"
284
+ " }\n"
285
+ " return nullptr;\n"
286
+ "}\n";
287
+ cpp += fmt("const std::array<llama_ui_asset, %d> & llama_ui_get_assets() {\n", n_assets);
288
+ cpp += " return g_assets;\n"
289
+ "}\n";
290
+ } else {
291
+ cpp +=
292
+ "const llama_ui_asset * llama_ui_find_asset(const std::string &) {\n"
293
+ " return nullptr;\n"
294
+ "}\n"
295
+ "const std::array<llama_ui_asset, 0> & llama_ui_get_assets() {\n"
296
+ " static const std::array<llama_ui_asset, 0> empty{};\n"
297
+ " return empty;\n"
298
+ "}\n";
299
+ }
300
+ cpp += fmt("bool llama_ui_use_gzip() { return %s; }\n", use_gzip ? "true" : "false");
301
+
302
+ bool ok = true;
303
+ ok = write_if_different(out_h, h) && ok;
304
+ ok = write_if_different(out_cpp, cpp) && ok;
305
+ return ok ? 0 : 1;
306
+ }
backend/llama.cpp/tools/ui/eslint.config.js ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // For more info, see https://github.com/storybookjs/eslint-plugin-storybook#configuration-flat-config-format
2
+ import storybook from 'eslint-plugin-storybook';
3
+
4
+ import prettier from 'eslint-config-prettier';
5
+ import { includeIgnoreFile } from '@eslint/compat';
6
+ import js from '@eslint/js';
7
+ import svelte from 'eslint-plugin-svelte';
8
+ import globals from 'globals';
9
+ import { fileURLToPath } from 'node:url';
10
+ import ts from 'typescript-eslint';
11
+ import svelteConfig from './svelte.config.js';
12
+
13
+ const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
14
+
15
+ export default ts.config(
16
+ includeIgnoreFile(gitignorePath),
17
+ js.configs.recommended,
18
+ ...ts.configs.recommended,
19
+ ...svelte.configs.recommended,
20
+ prettier,
21
+ ...svelte.configs.prettier,
22
+ {
23
+ languageOptions: { globals: { ...globals.browser, ...globals.node } },
24
+ rules: {
25
+ // typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
26
+ // see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
27
+ 'no-undef': 'off',
28
+ 'svelte/no-at-html-tags': 'off',
29
+ // This app uses hash-based routing (#/) where resolve() from $app/paths does not apply
30
+ 'svelte/no-navigation-without-resolve': 'off',
31
+
32
+ // Enforce empty line at end of file
33
+ 'eol-last': 'error'
34
+ }
35
+ },
36
+ {
37
+ files: ['**/*.svelte', '**/*.svelte.ts', '**/*.svelte.js'],
38
+ languageOptions: {
39
+ parserOptions: {
40
+ projectService: true,
41
+ extraFileExtensions: ['.svelte'],
42
+ parser: ts.parser,
43
+ svelteConfig
44
+ }
45
+ }
46
+ },
47
+ {
48
+ // Exclude generated build output and Storybook files from ESLint
49
+ ignores: [
50
+ 'dist/**',
51
+ 'build/**',
52
+ '.svelte-kit/**',
53
+ 'test-results/**',
54
+ '.storybook/**/*',
55
+ 'src/lib/services/sandbox-worker.js'
56
+ ]
57
+ },
58
+ storybook.configs['flat/recommended']
59
+ );
backend/llama.cpp/tools/ui/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
backend/llama.cpp/tools/ui/package.json ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "llama-ui",
3
+ "private": true,
4
+ "version": "1.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "npm run build-pwa-assets && vite build",
8
+ "build-pwa-assets": "npx @vite-pwa/assets-generator --root . --config pwa-assets.config.ts && npx @vite-pwa/assets-generator --root . --config pwa-assets-dark.config.ts && node scripts/make-icons-circular.js",
9
+ "dev": "bash scripts/dev.sh",
10
+ "preview": "vite preview",
11
+ "prepare": "svelte-kit sync || echo ''",
12
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
13
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
14
+ "reset": "rm -rf .svelte-kit node_modules",
15
+ "format": "prettier --write .",
16
+ "lint": "prettier --check . && eslint .",
17
+ "test": "npm run test:ui -- --run && npm run test:client -- --run && npm run test:unit -- --run && npm run test:e2e",
18
+ "test:e2e": "playwright test",
19
+ "test:e2e:pwa": "playwright test tests/e2e/pwa.e2e.ts",
20
+ "test:client": "vitest --project=client",
21
+ "test:unit": "vitest --project=unit",
22
+ "test:unit:pwa": "vitest --project=unit --run tests/unit/pwa.spec.ts",
23
+ "test:pwa": "npm run test:unit:pwa && npm run test:e2e:pwa",
24
+ "test:ui": "vitest --project=ui",
25
+ "storybook": "storybook dev -p 6006",
26
+ "build-storybook": "storybook build",
27
+ "cleanup": "rm -rf .svelte-kit build node_modules test-results dist dev-dist debug-storybook.log static/pwa-*.png static/maskable-icon-*.png static/apple-touch-icon-*.png static/apple-splash-*.png static/favicon*.ico"
28
+ },
29
+ "devDependencies": {
30
+ "@chromatic-com/storybook": "5.0.0",
31
+ "@eslint/compat": "1.4.1",
32
+ "@eslint/js": "9.39.2",
33
+ "@internationalized/date": "3.12.2",
34
+ "@lucide/svelte": "0.515.0",
35
+ "@modelcontextprotocol/sdk": "1.26.0",
36
+ "@playwright/test": "1.56.1",
37
+ "@storybook/addon-a11y": "10.2.4",
38
+ "@storybook/addon-docs": "10.2.4",
39
+ "@storybook/addon-svelte-csf": "5.0.10",
40
+ "@storybook/addon-vitest": "10.2.4",
41
+ "@storybook/sveltekit": "10.2.4",
42
+ "@sveltejs/adapter-static": "3.0.10",
43
+ "@sveltejs/kit": "2.60.1",
44
+ "@sveltejs/vite-plugin-svelte": "6.2.1",
45
+ "@tailwindcss/forms": "0.5.10",
46
+ "@tailwindcss/typography": "0.5.16",
47
+ "@tailwindcss/vite": "4.1.11",
48
+ "@types/node": "24.13.0",
49
+ "@vite-pwa/assets-generator": "1.0.2",
50
+ "@vite-pwa/sveltekit": "1.1.0",
51
+ "@vitest/browser": "4.1.8",
52
+ "@vitest/browser-playwright": "4.1.8",
53
+ "@vitest/coverage-v8": "4.1.8",
54
+ "bits-ui": "2.18.1",
55
+ "clsx": "2.1.1",
56
+ "dexie": "4.4.3",
57
+ "dompurify": "3.4.11",
58
+ "eslint": "9.39.4",
59
+ "eslint-config-prettier": "10.1.8",
60
+ "eslint-plugin-storybook": "10.4.2",
61
+ "eslint-plugin-svelte": "3.19.0",
62
+ "fflate": "0.8.3",
63
+ "globals": "16.5.0",
64
+ "highlight.js": "11.11.1",
65
+ "http-server": "14.1.1",
66
+ "mdast": "3.0.0",
67
+ "mdsvex": "0.12.7",
68
+ "mermaid": "11.15.0",
69
+ "mode-watcher": "1.1.0",
70
+ "pdfjs-dist": "5.4.54",
71
+ "playwright": "1.56.1",
72
+ "prettier": "3.8.3",
73
+ "prettier-plugin-svelte": "4.1.0",
74
+ "prettier-plugin-tailwindcss": "0.8.0",
75
+ "rehype-highlight": "7.0.2",
76
+ "rehype-katex": "7.0.1",
77
+ "rehype-stringify": "10.0.1",
78
+ "remark": "15.0.1",
79
+ "remark-breaks": "4.0.0",
80
+ "remark-gfm": "4.0.1",
81
+ "remark-html": "16.0.1",
82
+ "remark-math": "6.0.0",
83
+ "remark-rehype": "11.1.2",
84
+ "sass": "1.100.0",
85
+ "storybook": "10.4.2",
86
+ "svelte": "5.56.1",
87
+ "svelte-check": "4.6.0",
88
+ "svelte-sonner": "1.1.1",
89
+ "tailwind-merge": "3.6.0",
90
+ "tailwind-variants": "3.2.2",
91
+ "tailwindcss": "4.3.0",
92
+ "tw-animate-css": "1.4.0",
93
+ "typescript": "5.9.3",
94
+ "typescript-eslint": "8.60.1",
95
+ "unified": "11.0.5",
96
+ "unist-util-visit": "5.1.0",
97
+ "uuid": "13.0.2",
98
+ "vite": "7.3.5",
99
+ "vite-plugin-devtools-json": "0.2.1",
100
+ "vitest": "4.1.8",
101
+ "vitest-browser-svelte": "2.1.1",
102
+ "workbox-window": "7.4.1"
103
+ },
104
+ "overrides": {
105
+ "cookie": "1.1.1"
106
+ }
107
+ }
backend/llama.cpp/tools/ui/playwright.config.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, devices } from '@playwright/test';
2
+
3
+ export default defineConfig({
4
+ testDir: 'tests/e2e',
5
+ testMatch: ['**/*.e2e.ts'],
6
+ timeout: 30000,
7
+ expect: {
8
+ timeout: 5000
9
+ },
10
+ fullyParallel: true,
11
+ forbidOnly: !!process.env.CI,
12
+ retries: process.env.CI ? 2 : 0,
13
+ workers: process.env.CI ? 1 : undefined,
14
+ reporter: 'line',
15
+ use: {
16
+ baseURL: 'http://localhost:8181',
17
+ trace: 'on-first-retry'
18
+ },
19
+ projects: [
20
+ {
21
+ name: 'chromium',
22
+ use: { ...devices['Desktop Chrome'] }
23
+ }
24
+ ],
25
+ webServer: {
26
+ command: 'npm run build && npx http-server ./dist -p 8181',
27
+ port: 8181,
28
+ timeout: 120000,
29
+ reuseExistingServer: !process.env.CI
30
+ }
31
+ });
backend/llama.cpp/tools/ui/pwa-assets-dark.config.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from '@vite-pwa/assets-generator/config';
2
+ import { FAVICON_COLORS, PWA_ASSET_GENERATOR } from './src/lib/constants/pwa';
3
+ import { writeThemeFavicons } from './scripts/favicon-colorize';
4
+
5
+ writeThemeFavicons(FAVICON_COLORS.LIGHT, FAVICON_COLORS.DARK, {
6
+ padding: PWA_ASSET_GENERATOR.FAVICON_PADDING
7
+ });
8
+
9
+ export default defineConfig({
10
+ headLinkOptions: {
11
+ preset: '2023'
12
+ },
13
+ preset: {
14
+ transparent: {
15
+ sizes: [],
16
+ favicons: [[48, 'favicon-dark.ico']],
17
+ padding: PWA_ASSET_GENERATOR.FAVICON_PADDING
18
+ },
19
+ maskable: {
20
+ sizes: []
21
+ },
22
+ apple: {
23
+ sizes: []
24
+ }
25
+ },
26
+ images: ['static/favicon-dark.svg']
27
+ });
backend/llama.cpp/tools/ui/pwa-assets.config.ts ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import {
2
+ combinePresetAndAppleSplashScreens,
3
+ defineConfig,
4
+ minimal2023Preset
5
+ } from '@vite-pwa/assets-generator/config';
6
+ import { readFileSync } from 'node:fs';
7
+ import { resolve } from 'node:path';
8
+ import {
9
+ THEME_COLORS,
10
+ PWA_GENERATOR_DEVICES,
11
+ PWA_ASSET_GENERATOR,
12
+ FAVICON_COLORS
13
+ } from './src/lib/constants/pwa';
14
+ import { SplashOrientation } from './src/lib/enums/splash.enums';
15
+ import { writeThemeFavicons } from './scripts/favicon-colorize';
16
+
17
+ writeThemeFavicons(FAVICON_COLORS.LIGHT, FAVICON_COLORS.DARK, {
18
+ padding: PWA_ASSET_GENERATOR.FAVICON_PADDING
19
+ });
20
+
21
+ export default defineConfig({
22
+ headLinkOptions: {
23
+ preset: PWA_ASSET_GENERATOR.LINK_PRESET
24
+ },
25
+ preset: combinePresetAndAppleSplashScreens(
26
+ {
27
+ ...minimal2023Preset,
28
+ // tiny margin so favicon.ico / pwa-*.png breathe inside the canvas
29
+ transparent: {
30
+ ...minimal2023Preset.transparent,
31
+ padding: PWA_ASSET_GENERATOR.FAVICON_PADDING
32
+ }
33
+ },
34
+ {
35
+ padding: PWA_ASSET_GENERATOR.SPLASH_PADDING,
36
+ resizeOptions: {
37
+ background: THEME_COLORS.BACKGROUND_LIGHT,
38
+ fit: PWA_ASSET_GENERATOR.FIT_MODE
39
+ },
40
+ darkResizeOptions: {
41
+ background: THEME_COLORS.BACKGROUND_DARK,
42
+ fit: PWA_ASSET_GENERATOR.FIT_MODE
43
+ },
44
+ darkImageResolver: async (imageName: string) => {
45
+ if (imageName.endsWith('favicon.svg')) {
46
+ return readFileSync(resolve('static/favicon-dark.svg'));
47
+ }
48
+ },
49
+ linkMediaOptions: {
50
+ log: true,
51
+ addMediaScreen: PWA_ASSET_GENERATOR.ADD_MEDIA_SCREEN,
52
+ basePath: PWA_ASSET_GENERATOR.BASE_PATH,
53
+ xhtml: PWA_ASSET_GENERATOR.XHTML
54
+ },
55
+ png: {
56
+ compressionLevel: PWA_ASSET_GENERATOR.PNG_COMPRESSION_LEVEL,
57
+ quality: PWA_ASSET_GENERATOR.PNG_QUALITY
58
+ },
59
+ name: (landscape, size, dark) => {
60
+ const orientation = landscape ? SplashOrientation.LANDSCAPE : SplashOrientation.PORTRAIT;
61
+ const darkPrefix = dark ? PWA_ASSET_GENERATOR.DARK_PREFIX : '';
62
+ return `apple-splash-${orientation}-${darkPrefix}${size.width}x${size.height}.png`;
63
+ }
64
+ },
65
+ PWA_GENERATOR_DEVICES
66
+ ),
67
+ images: ['static/favicon.svg']
68
+ });
backend/llama.cpp/tools/ui/scripts/dev.sh ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Development script for llama-ui
4
+ #
5
+ # This script starts the llama-ui development servers (Storybook and Vite).
6
+ # Note: You need to start llama-server separately.
7
+ #
8
+ # Usage:
9
+ # bash scripts/dev.sh
10
+ # npm run dev
11
+
12
+ cd ../../
13
+
14
+ # Ensure node_modules are installed
15
+ if [ ! -d "tools/ui/node_modules" ]; then
16
+ echo "📦 Installing npm dependencies..."
17
+ cd tools/ui && npm install && cd ../../
18
+ fi
19
+
20
+ # Check and install git hooks if missing
21
+ check_and_install_hooks() {
22
+ local hooks_missing=false
23
+
24
+ # Check for required hooks
25
+ if [ ! -f ".git/hooks/pre-commit" ] || [ ! -f ".git/hooks/pre-push" ]; then
26
+ hooks_missing=true
27
+ fi
28
+
29
+ if [ "$hooks_missing" = true ]; then
30
+ echo "🔧 Git hooks missing, installing them..."
31
+ if bash "$(dirname "$0")/git-hooks/install.sh"; then
32
+ echo "✅ Git hooks installed successfully"
33
+ else
34
+ echo "⚠️ Failed to install git hooks, continuing anyway..."
35
+ fi
36
+ else
37
+ echo "✅ Git hooks already installed"
38
+ fi
39
+ }
40
+
41
+ # Install git hooks if needed
42
+ check_and_install_hooks
43
+
44
+ # Cleanup function
45
+ cleanup() {
46
+ echo "🧹 Cleaning up..."
47
+ exit
48
+ }
49
+
50
+ # Set up signal handlers
51
+ trap cleanup SIGINT SIGTERM
52
+
53
+ echo "🚀 Starting development servers..."
54
+ echo "📝 Note: Make sure to start llama-server separately if needed"
55
+ cd tools/ui
56
+ # Use --insecure-http-parser to handle malformed HTTP responses from llama-server
57
+ # (some responses have both Content-Length and Transfer-Encoding headers)
58
+ storybook dev -p 6006 --ci & NODE_OPTIONS="--insecure-http-parser" vite dev --host 0.0.0.0 &
59
+
60
+ # Wait for all background processes
61
+ wait
backend/llama.cpp/tools/ui/scripts/favicon-colorize.ts ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const HERE = dirname(fileURLToPath(import.meta.url));
6
+ const PROJECT_ROOT = resolve(HERE, '..');
7
+
8
+ const DEFAULT_LOGO = resolve(PROJECT_ROOT, 'src/lib/assets/logo.svg');
9
+ const DEFAULT_OUT_DIR = resolve(PROJECT_ROOT, 'static');
10
+ const DEFAULT_OUT_LIGHT = resolve(DEFAULT_OUT_DIR, 'favicon.svg');
11
+ const DEFAULT_OUT_DARK = resolve(DEFAULT_OUT_DIR, 'favicon-dark.svg');
12
+
13
+ const CURRENT_COLOR = 'currentColor';
14
+
15
+ export interface ColorizedFavicon {
16
+ light: string;
17
+ dark: string;
18
+ }
19
+
20
+ export interface WriteThemeFaviconsOptions {
21
+ sourcePath?: string;
22
+ lightOutPath?: string;
23
+ darkOutPath?: string;
24
+ /**
25
+ * Fraction of the icon (0..1) to leave as an even margin on each side.
26
+ * Applied by wrapping the inner content in a `<g transform="...">` so the
27
+ * source `src/lib/assets/logo.svg` is not modified. Pass 0 to disable.
28
+ */
29
+ padding?: number;
30
+ }
31
+
32
+ /**
33
+ * Replace every `currentColor` occurrence in the SVG with the given color.
34
+ * Pure: no filesystem access, so it is straightforward to unit-test.
35
+ */
36
+ export function colorizeFaviconSvg(
37
+ svg: string,
38
+ lightColor: string,
39
+ darkColor: string
40
+ ): ColorizedFavicon {
41
+ return {
42
+ light: svg.replaceAll(CURRENT_COLOR, lightColor),
43
+ dark: svg.replaceAll(CURRENT_COLOR, darkColor)
44
+ };
45
+ }
46
+
47
+ /**
48
+ * Shrink the inner SVG content uniformly and re-center it so `padding` (a
49
+ * 0..1 fraction) is reserved as equal margin on each side. Returns the input
50
+ * unchanged for non-positive padding, missing/invalid `viewBox`, or unexpected
51
+ * markup so the caller always gets a renderable SVG.
52
+ */
53
+ export function padFaviconSvg(svg: string, padding: number): string {
54
+ if (!(padding > 0) || padding >= 1) return svg;
55
+
56
+ const viewBoxMatch = svg.match(/viewBox\s*=\s*["']([^"']+)["']/i);
57
+ if (!viewBoxMatch) return svg;
58
+
59
+ const parts = viewBoxMatch[1]
60
+ .trim()
61
+ .split(/[\s,]+/)
62
+ .map(Number);
63
+ if (parts.length !== 4 || parts.some((n) => !Number.isFinite(n))) return svg;
64
+
65
+ const [, , width, height] = parts;
66
+ if (width <= 0 || height <= 0) return svg;
67
+
68
+ const scale = 1 - padding;
69
+ const translateX = (padding * width) / 2;
70
+ const translateY = (padding * height) / 2;
71
+
72
+ const openTagStart = svg.search(/<svg\b/i);
73
+ if (openTagStart === -1) return svg;
74
+ const openTagEnd = svg.indexOf('>', openTagStart);
75
+ if (openTagEnd === -1) return svg;
76
+ const closeStart = svg.lastIndexOf('</svg');
77
+ if (closeStart === -1 || closeStart <= openTagEnd) return svg;
78
+
79
+ const openTag = svg.slice(0, openTagEnd + 1);
80
+ const inner = svg.slice(openTagEnd + 1, closeStart);
81
+ const closeTag = svg.slice(closeStart);
82
+
83
+ const group = `<g transform="translate(${translateX} ${translateY}) scale(${scale})">`;
84
+ return `${openTag}${group}${inner}</g>${closeTag}`;
85
+ }
86
+
87
+ /**
88
+ * Read `src/lib/assets/logo.svg`, colorize it for both themes, and write
89
+ * the results to the static directory so the PWA asset generator can consume
90
+ * them. Paths can be overridden for tests.
91
+ */
92
+ export function writeThemeFavicons(
93
+ lightColor: string,
94
+ darkColor: string,
95
+ {
96
+ sourcePath = DEFAULT_LOGO,
97
+ lightOutPath = DEFAULT_OUT_LIGHT,
98
+ darkOutPath = DEFAULT_OUT_DARK,
99
+ padding = 0
100
+ }: WriteThemeFaviconsOptions = {}
101
+ ): void {
102
+ const source = readFileSync(sourcePath, 'utf-8');
103
+ const { light, dark } = colorizeFaviconSvg(source, lightColor, darkColor);
104
+ mkdirSync(dirname(lightOutPath), { recursive: true });
105
+ writeFileSync(lightOutPath, padFaviconSvg(light, padding));
106
+ writeFileSync(darkOutPath, padFaviconSvg(dark, padding));
107
+ }
backend/llama.cpp/tools/ui/scripts/git-hooks/install.sh ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Install git hooks for llama-ui
4
+ # Copies pre-commit and pre-push hooks into the repo's .git/hooks directory.
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+ REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
8
+ HOOKS_DIR="$REPO_ROOT/$(cd "$REPO_ROOT" && git rev-parse --git-path hooks)"
9
+
10
+ # Verify package.json exists
11
+ if [ ! -f "$REPO_ROOT/tools/ui/package.json" ]; then
12
+ echo "❌ package.json not found in tools/ui"
13
+ exit 1
14
+ fi
15
+
16
+ echo "Installing git hooks for llama-ui..."
17
+
18
+ for hook in pre-commit pre-push; do
19
+ src="$SCRIPT_DIR/${hook}.sh"
20
+ dst="$HOOKS_DIR/$hook"
21
+
22
+ if cp "$src" "$dst" && chmod +x "$dst"; then
23
+ echo " ✅ $hook"
24
+ else
25
+ echo " ❌ Failed to install $hook"
26
+ exit 1
27
+ fi
28
+ done
29
+
30
+ echo ""
31
+ echo "Pre-commit: format (staged) + type-check"
32
+ echo "Pre-push: lint + test"
33
+ echo ""
34
+ echo "Hooks stash unstaged changes temporarily and restore them after."
35
+ echo "Skip with: git commit --no-verify / git push --no-verify"
backend/llama.cpp/tools/ui/scripts/git-hooks/pre-commit.sh ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Pre-commit hook for llama-ui
4
+ # Runs: format (staged files only) + type-check
5
+ # Stashes unstaged changes temporarily and restores them after.
6
+
7
+ # Only run when there are staged changes in tools/ui/
8
+ if ! git diff --cached --name-only | grep -q "^tools/ui/"; then
9
+ exit 0
10
+ fi
11
+
12
+ REPO_ROOT=$(git rev-parse --show-toplevel)
13
+ cd "$REPO_ROOT/tools/ui"
14
+
15
+ # Check that node_modules exists
16
+ if [ ! -d "node_modules" ]; then
17
+ echo "❌ node_modules not found. Run 'npm install' first."
18
+ exit 1
19
+ fi
20
+
21
+ # Stash unstaged changes in tools/ui/ so they don't interfere
22
+ stash_name="pi-ui-precommit"
23
+ git stash push --keep-index -u -m "$stash_name" -- tools/ui/ 2>/dev/null || true
24
+
25
+ echo "Running pre-commit checks for llama-ui..."
26
+
27
+ # Format only staged files
28
+ staged_ui=$(git diff --cached --name-only -- tools/ui/)
29
+ if [ -n "$staged_ui" ]; then
30
+ echo "$staged_ui" | xargs npm run format
31
+ format_ok=$?
32
+ # Re-stage formatted files
33
+ git add tools/ui/
34
+ else
35
+ format_ok=0
36
+ fi
37
+
38
+ # Type-check the clean tree
39
+ npm run check
40
+ check_ok=$?
41
+
42
+ # Restore stashed changes
43
+ if git stash list | grep -q "$stash_name"; then
44
+ git stash pop 2>/dev/null || true
45
+ fi
46
+
47
+ if [ $format_ok -ne 0 ]; then
48
+ echo "❌ Format failed"
49
+ exit 1
50
+ fi
51
+ if [ $check_ok -ne 0 ]; then
52
+ echo "❌ Type check failed"
53
+ exit 1
54
+ fi
55
+
56
+ echo "✅ Pre-commit checks passed"
57
+ exit 0
backend/llama.cpp/tools/ui/scripts/git-hooks/pre-push.sh ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Pre-push hook for llama-ui
4
+ # Runs: lint + test
5
+ # Ignores unstaged changes (stashes them temporarily and restores after).
6
+
7
+ needs_check=false
8
+
9
+ # Read refs from stdin: local_ref local_sha remote_ref remote_sha
10
+ while read local_ref local_sha remote_ref remote_sha; do
11
+ # New branch or force-push — always check
12
+ if [ "$local_sha" = "0000000000000000000000000000000000000000" ] || \
13
+ [ "$remote_sha" = "0000000000000000000000000000000000000000" ]; then
14
+ needs_check=true
15
+ continue
16
+ fi
17
+
18
+ # Check for changes in tools/ui/ between remote and local
19
+ if git diff --name-only "$remote_sha...$local_sha" -- tools/ui/ | grep -q .; then
20
+ needs_check=true
21
+ fi
22
+ done
23
+
24
+ if [ "$needs_check" = false ]; then
25
+ exit 0
26
+ fi
27
+
28
+ REPO_ROOT=$(git rev-parse --show-toplevel)
29
+ cd "$REPO_ROOT/tools/ui"
30
+
31
+ # Check that node_modules exists
32
+ if [ ! -d "node_modules" ]; then
33
+ echo "❌ node_modules not found. Run 'npm install' first."
34
+ exit 1
35
+ fi
36
+
37
+ # Stash unstaged changes so they don't interfere with checks
38
+ stash_name="pi-ui-prepush"
39
+ git stash push -u -m "$stash_name" -- tools/ui/ 2>/dev/null || true
40
+
41
+ echo "Running pre-push checks for llama-ui..."
42
+
43
+ # Lint
44
+ npm run lint
45
+ lint_ok=$?
46
+
47
+ # Test
48
+ npm test
49
+ test_ok=$?
50
+
51
+ # Restore stashed changes
52
+ if git stash list | grep -q "$stash_name"; then
53
+ git stash pop 2>/dev/null || true
54
+ fi
55
+
56
+ if [ $lint_ok -ne 0 ]; then
57
+ echo "❌ Lint failed"
58
+ exit 1
59
+ fi
60
+
61
+ if [ $test_ok -ne 0 ]; then
62
+ echo "❌ Tests failed"
63
+ exit 1
64
+ fi
65
+
66
+ echo "✅ Pre-push checks passed"
67
+ exit 0
backend/llama.cpp/tools/ui/scripts/make-icons-circular.js ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Apply circular mask to pwa-*.png icons.
5
+ * Uses the maskable icon as source (white bg, full logo) to avoid
6
+ * the small-colormap pwa icons looking bad when cropped to a circle.
7
+ *
8
+ * Usage: node scripts/make-icons-circular.js [--padding-pct <0-50>] [--scale-pct <50-100>]
9
+ *
10
+ * - padding-pct: percentage of icon size kept as padding around the circle (default: 25)
11
+ * - scale-pct: scale down the source image before cropping (default: 85)
12
+ *
13
+ * maskable-icon and apple-touch-icon are left untouched.
14
+ */
15
+
16
+ import sharp from 'sharp';
17
+ import fs from 'fs';
18
+ import path from 'path';
19
+ import { fileURLToPath } from 'url';
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = path.dirname(__filename);
23
+
24
+ const STATIC_DIR = path.resolve(__dirname, '..', 'static');
25
+
26
+ const paddingPct = process.argv.reduce((acc, arg, i, args) => {
27
+ if (arg === '--padding-pct' && args[i + 1]) return parseFloat(args[i + 1]);
28
+ return acc;
29
+ }, 0);
30
+
31
+ // Scale down the source image before cropping to circle
32
+ const scalePct = process.argv.reduce((acc, arg, i, args) => {
33
+ if (arg === '--scale-pct' && args[i + 1]) return parseFloat(args[i + 1]);
34
+ return acc;
35
+ }, 85); // default 85% - icon fills 85% of the circular area
36
+
37
+ // Source for circular icons: the maskable icon (white bg, full logo)
38
+ const sourceIcon = 'maskable-icon-512x512.png';
39
+ const targetIcons = ['pwa-64x64.png', 'pwa-192x192.png', 'pwa-512x512.png'];
40
+
41
+ // maskable-icon and apple-touch-icon stay square
42
+ const untouchedIcons = ['maskable-icon-512x512.png', 'apple-touch-icon-180x180.png'];
43
+
44
+ async function makeCircle(targetFilename) {
45
+ const targetPath = path.join(STATIC_DIR, targetFilename);
46
+ const sourcePath = path.join(STATIC_DIR, sourceIcon);
47
+
48
+ if (!fs.existsSync(sourcePath)) {
49
+ console.log(`⏭️ ${sourceIcon} not found, skipping`);
50
+ return;
51
+ }
52
+ if (!fs.existsSync(targetPath)) {
53
+ console.log(`⏭️ ${targetFilename} not found, skipping`);
54
+ return;
55
+ }
56
+
57
+ const metadata = await sharp(targetPath).metadata();
58
+ const size = Math.max(metadata.width, metadata.height);
59
+ const radius = Math.floor((size * (1 - paddingPct / 100)) / 2);
60
+ const center = Math.floor(size / 2);
61
+
62
+ // Build circular mask as RGBA buffer: white opaque circle on transparent bg
63
+ const maskBuf = Buffer.alloc(size * size * 4, 0);
64
+ for (let y = 0; y < size; y++) {
65
+ for (let x = 0; x < size; x++) {
66
+ const dx = x - center;
67
+ const dy = y - center;
68
+ const dist = Math.sqrt(dx * dx + dy * dy);
69
+ if (dist < radius) {
70
+ const i = (y * size + x) * 4;
71
+ maskBuf[i] = 255;
72
+ maskBuf[i + 1] = 255;
73
+ maskBuf[i + 2] = 255;
74
+ maskBuf[i + 3] = 255;
75
+ }
76
+ }
77
+ }
78
+
79
+ const tmpMask = path.join(STATIC_DIR, '.mask-tmp.png');
80
+ await sharp(maskBuf, {
81
+ raw: { width: size, height: size, channels: 4 }
82
+ })
83
+ .png()
84
+ .toFile(tmpMask);
85
+
86
+ // Step 1: Scale source relative to circle diameter (not full icon), composite centered onto white canvas of full size
87
+ const circleDiameter = Math.floor(size * (1 - paddingPct / 100));
88
+ const scaledSize = Math.floor((circleDiameter * scalePct) / 100);
89
+ const offset = Math.floor((size - scaledSize) / 2);
90
+
91
+ const scaledBuf = await sharp(sourcePath)
92
+ .resize(scaledSize, scaledSize, {
93
+ fit: 'cover',
94
+ background: { r: 255, g: 255, b: 255, alpha: 1 }
95
+ })
96
+ .ensureAlpha()
97
+ .png()
98
+ .toBuffer();
99
+
100
+ // Step 2: Composite scaled image onto white background, then apply circular mask
101
+ const output = await sharp({
102
+ create: {
103
+ width: size,
104
+ height: size,
105
+ channels: 4,
106
+ background: { r: 255, g: 255, b: 255, alpha: 1 }
107
+ }
108
+ })
109
+ .composite([
110
+ { input: scaledBuf, top: offset, left: offset },
111
+ { input: tmpMask, top: 0, left: 0, blend: 'dest-in' }
112
+ ])
113
+ .png()
114
+ .toBuffer();
115
+
116
+ fs.writeFileSync(targetPath, output);
117
+ fs.unlinkSync(tmpMask);
118
+
119
+ console.log(
120
+ `✓ ${targetFilename} → circle from ${sourceIcon}, ${paddingPct}% padding (size=${size}, r=${radius}, scale=${scalePct}%, circleDiameter=${circleDiameter})`
121
+ );
122
+ }
123
+
124
+ async function main() {
125
+ console.log(`Circular mask: ${paddingPct}% padding, ${scalePct}% scale, source=${sourceIcon}\n`);
126
+ for (const icon of targetIcons) {
127
+ await makeCircle(icon);
128
+ }
129
+
130
+ console.log('\nUnchanged:');
131
+ for (const icon of untouchedIcons) {
132
+ const fp = path.join(STATIC_DIR, icon);
133
+ console.log(` ${icon} (${fs.existsSync(fp) ? fs.statSync(fp).size + ' bytes' : 'missing'})`);
134
+ }
135
+ }
136
+
137
+ main();
backend/llama.cpp/tools/ui/scripts/vite-plugin-build-info.ts ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { writeFileSync, existsSync } from 'node:fs';
2
+ import { resolve } from 'path';
3
+ import type { Plugin } from 'vite';
4
+ import { BUILD_CONFIG } from '../src/lib/constants/pwa';
5
+
6
+ let processed = false;
7
+
8
+ const OUTPUT_DIR = process.env.LLAMA_UI_OUT_DIR ?? BUILD_CONFIG.OUTPUT_DIR;
9
+
10
+ /**
11
+ * Write build.json with the llama.cpp release build number.
12
+ *
13
+ * LLAMA_BUILD_NUMBER is passed from CMake -> npm -> vite via env var.
14
+ * Used for display of the current llama-server release (e.g. "b1234").
15
+ */
16
+ export function buildInfoPlugin(): Plugin {
17
+ return {
18
+ name: 'llamacpp:build-info',
19
+ apply: 'build',
20
+ closeBundle() {
21
+ setTimeout(() => {
22
+ try {
23
+ if (processed) return;
24
+ processed = true;
25
+
26
+ const buildNumber = process.env.LLAMA_BUILD_NUMBER || 'b0000';
27
+
28
+ const outDir = resolve(OUTPUT_DIR);
29
+ const indexPath = resolve(outDir, 'index.html');
30
+ if (!existsSync(indexPath)) return;
31
+
32
+ const buildJsonPath = resolve(outDir, 'build.json');
33
+ writeFileSync(buildJsonPath, JSON.stringify({ version: buildNumber }), 'utf-8');
34
+ console.log(`Created build.json (version: ${buildNumber})`);
35
+ } catch (error) {
36
+ console.error('Failed to write build.json:', error);
37
+ }
38
+ }, 100);
39
+ }
40
+ };
41
+ }
backend/llama.cpp/tools/ui/scripts/vite-plugin-relativize-base.ts ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
2
+ import { resolve } from 'path';
3
+ import type { Plugin } from 'vite';
4
+ import { BUILD_CONFIG } from '../src/lib/constants/pwa';
5
+
6
+ let processed = false;
7
+
8
+ const OUTPUT_DIR = process.env.LLAMA_UI_OUT_DIR ?? BUILD_CONFIG.OUTPUT_DIR;
9
+
10
+ function rewrite(path: string, pairs: [string, string][]): void {
11
+ if (!existsSync(path)) {
12
+ return;
13
+ }
14
+ const text = readFileSync(path, 'utf-8');
15
+ let out = text;
16
+ for (const [from, to] of pairs) {
17
+ out = out.split(from).join(to);
18
+ }
19
+ if (out !== text) {
20
+ writeFileSync(path, out, 'utf-8');
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Relativize SvelteKit absolute base refs so the build is relocatable under any subpath.
26
+ *
27
+ * SvelteKit bakes root absolute /_app/ paths into the SPA fallback because paths.relative
28
+ * does not apply to a depth agnostic fallback page. Rewriting to ./_app/ lets a plain
29
+ * recursive copy of the output into /any/subdir/ resolve assets against the document URL.
30
+ * Runs after adapter-static writes index.html and the PWA plugin writes sw.js, deferred the
31
+ * same way as buildInfoPlugin so the emitted files exist.
32
+ */
33
+ export function relativizeBasePlugin(): Plugin {
34
+ return {
35
+ name: 'llamacpp:relativize-base',
36
+ apply: 'build',
37
+ closeBundle() {
38
+ setTimeout(() => {
39
+ try {
40
+ if (processed) return;
41
+ processed = true;
42
+
43
+ const outDir = resolve(OUTPUT_DIR);
44
+
45
+ // index.html: modulepreload, stylesheet and bootstrap import reference "/_app/
46
+ rewrite(resolve(outDir, 'index.html'), [['"/_app/', '"./_app/']]);
47
+
48
+ // sw.js: the only absolute entries are the navigate fallback precache key and handler
49
+ rewrite(resolve(outDir, 'sw.js'), [
50
+ ['{url:"/"', '{url:"./"'],
51
+ ['createHandlerBoundToURL("/"', 'createHandlerBoundToURL("./"']
52
+ ]);
53
+
54
+ console.log('Relativized base refs in index.html and sw.js');
55
+ } catch (error) {
56
+ console.error('Failed to relativize base refs:', error);
57
+ }
58
+ }, 100);
59
+ }
60
+ };
61
+ }
backend/llama.cpp/tools/ui/scripts/vite-plugin-splash-screen.ts ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { readdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
2
+ import { resolve } from 'path';
3
+ import type { Plugin } from 'vite';
4
+ import { TAB, NEWLINE } from '../src/lib/constants/code';
5
+ import { APPLE_DEVICES, BUILD_CONFIG, REGEX_PATTERNS, SPLASH_LINK } from '../src/lib/constants/pwa';
6
+ import type { SplashDimensions } from '../src/lib/types';
7
+ import { SplashOrientation } from '../src/lib/enums/splash.enums';
8
+
9
+ let processed = false;
10
+
11
+ const OUTPUT_DIR = process.env.LLAMA_UI_OUT_DIR ?? BUILD_CONFIG.OUTPUT_DIR;
12
+
13
+ /**
14
+ * Generate iOS splash screen <link> tags from generated apple-splash-*.png files.
15
+ * Returns an array of HTML link strings to be injected into the page head.
16
+ */
17
+ export function generateSplashScreenLinks(outDir: string): string[] {
18
+ const files = readdirSync(outDir).filter((f) => f.match(REGEX_PATTERNS.SPLASH_FILE));
19
+ if (files.length === 0) return [];
20
+
21
+ const dimMap = new Map<string, SplashDimensions>();
22
+ for (const [dims, spec] of Object.entries(APPLE_DEVICES)) {
23
+ const [w, h] = dims.split('x').map(Number);
24
+ // logical-point dimensions
25
+ dimMap.set(`${w}x${h}`, { deviceW: spec.width, deviceH: spec.height, dpr: spec.dpr });
26
+ dimMap.set(`${h}x${w}`, { deviceW: spec.width, deviceH: spec.height, dpr: spec.dpr });
27
+ // pixel dimensions (used by actual generated splash files)
28
+ dimMap.set(`${w * spec.dpr}x${h * spec.dpr}`, {
29
+ deviceW: spec.width,
30
+ deviceH: spec.height,
31
+ dpr: spec.dpr
32
+ });
33
+ dimMap.set(`${h * spec.dpr}x${w * spec.dpr}`, {
34
+ deviceW: spec.width,
35
+ deviceH: spec.height,
36
+ dpr: spec.dpr
37
+ });
38
+ }
39
+
40
+ const lightLinks: string[] = [];
41
+ const darkLinks: string[] = [];
42
+
43
+ for (const file of files) {
44
+ const match = file.match(REGEX_PATTERNS.SPLASH_FILE);
45
+ if (!match) continue;
46
+ const orientation = match[1] as SplashOrientation;
47
+ const isDark = !!match[2];
48
+ const pixelW = parseInt(match[3]);
49
+ const pixelH = parseInt(match[4]);
50
+
51
+ const key = `${pixelW}x${pixelH}`;
52
+ const spec = dimMap.get(key);
53
+ if (!spec) {
54
+ console.warn(`Unknown splash screen dimensions: ${key} (${file})`);
55
+ continue;
56
+ }
57
+
58
+ const { deviceW, deviceH, dpr } = spec;
59
+ const media = `screen and (device-width: ${deviceW}px) and (device-height: ${deviceH}px) and (-webkit-device-pixel-ratio: ${dpr}) and (orientation: ${orientation})`;
60
+ const href = `./${file}`;
61
+
62
+ if (isDark) {
63
+ darkLinks.push(
64
+ `${SPLASH_LINK.HTML} media="${media}${SPLASH_LINK.DARK_MEDIA_SUFFIX}" href="${href}">`
65
+ );
66
+ } else {
67
+ lightLinks.push(`${SPLASH_LINK.HTML} media="${media}" href="${href}">`);
68
+ }
69
+ }
70
+
71
+ return [...lightLinks, ...darkLinks];
72
+ }
73
+
74
+ export function splashScreenPlugin(): Plugin {
75
+ return {
76
+ name: 'llamacpp:splash-screen',
77
+ apply: 'build',
78
+ closeBundle() {
79
+ setTimeout(() => {
80
+ try {
81
+ if (processed) return;
82
+ processed = true;
83
+
84
+ const outDir = resolve(OUTPUT_DIR);
85
+ const indexPath = resolve(outDir, 'index.html');
86
+ if (!existsSync(indexPath)) return;
87
+
88
+ let content = readFileSync(indexPath, 'utf-8');
89
+
90
+ // Inject iOS splash screen <link> tags into <head>.
91
+ // The @vite-pwa/assets-generator generates apple-splash-*.png files;
92
+ // this scans them and creates the <link> tags SvelteKit needs.
93
+ const splashLinks = generateSplashScreenLinks(outDir);
94
+ if (splashLinks.length > 0) {
95
+ console.log(`Generated ${splashLinks.length} apple-splash link tags`);
96
+ const splashHtml = splashLinks.map((l) => TAB + TAB + l).join(NEWLINE);
97
+ content = content.replace(
98
+ REGEX_PATTERNS.HEAD_CLOSE,
99
+ splashHtml + NEWLINE + TAB + TAB + '</head>'
100
+ );
101
+ }
102
+
103
+ // Remove trailing \r from Windows line endings
104
+ content = content.replace(/\r/g, '');
105
+ content = BUILD_CONFIG.GUIDE_COMMENT + NEWLINE + content;
106
+
107
+ writeFileSync(indexPath, content, 'utf-8');
108
+ console.log('Updated index.html');
109
+ } catch (error) {
110
+ console.error('Failed to process build output:', error);
111
+ }
112
+ }, 100);
113
+ }
114
+ };
115
+ }
backend/llama.cpp/tools/ui/sources.cmake ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Inputs used to decide whether the npm build output is up-to-date.
2
+
3
+ set(UI_SOURCE_GLOBS
4
+ src/*
5
+ static/*
6
+ )
7
+
8
+ set(UI_SOURCE_FILES
9
+ package.json
10
+ package-lock.json
11
+ src/.gitignore
12
+ vite.config.ts
13
+ svelte.config.js
14
+ tsconfig.json
15
+ scripts/vite-plugin-llama-cpp-build.ts
16
+ )
backend/llama.cpp/tools/ui/src/.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ !*
backend/llama.cpp/tools/ui/src/app.css ADDED
@@ -0,0 +1,200 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @import 'tailwindcss';
2
+ @source '.';
3
+ @plugin '@tailwindcss/forms';
4
+ @plugin '@tailwindcss/typography';
5
+ @import 'tw-animate-css';
6
+
7
+ @custom-variant dark (&:is(.dark *));
8
+
9
+ :root {
10
+ --radius: 0.625rem;
11
+ --background: oklch(1 0 0);
12
+ --foreground: oklch(0.145 0 0);
13
+ --card: oklch(1 0 0);
14
+ --card-foreground: oklch(0.145 0 0);
15
+ --popover: oklch(1 0 0);
16
+ --popover-foreground: oklch(0.145 0 0);
17
+ --primary: oklch(0.205 0 0);
18
+ --primary-foreground: oklch(0.985 0 0);
19
+ --secondary: oklch(0.95 0 0);
20
+ --secondary-foreground: oklch(0.205 0 0);
21
+ --muted: oklch(0.97 0 0);
22
+ --muted-foreground: oklch(0.556 0 0);
23
+ --accent: oklch(0.95 0 0);
24
+ --accent-foreground: oklch(0.205 0 0);
25
+ --destructive: oklch(0.577 0.245 27.325);
26
+ --border: oklch(0.875 0 0);
27
+ --input: oklch(0.92 0 0);
28
+ --ring: oklch(0.708 0 0);
29
+ --chart-1: oklch(0.646 0.222 41.116);
30
+ --chart-2: oklch(0.6 0.118 184.704);
31
+ --chart-3: oklch(0.398 0.07 227.392);
32
+ --chart-4: oklch(0.828 0.189 84.429);
33
+ --chart-5: oklch(0.769 0.188 70.08);
34
+ --sidebar: oklch(0.985 0 0);
35
+ --sidebar-foreground: oklch(0.145 0 0);
36
+ --sidebar-primary: oklch(0.205 0 0);
37
+ --sidebar-primary-foreground: oklch(0.985 0 0);
38
+ --sidebar-accent: oklch(0.97 0 0);
39
+ --sidebar-accent-foreground: oklch(0.205 0 0);
40
+ --sidebar-border: oklch(0.922 0 0);
41
+ --sidebar-ring: oklch(0.708 0 0);
42
+ --code-background: oklch(0.985 0 0);
43
+ --code-foreground: oklch(0.145 0 0);
44
+ --font-mono:
45
+ ui-monospace, SFMono-Regular, 'SF Mono', Monaco, 'Cascadia Code', 'Roboto Mono', Consolas,
46
+ 'Liberation Mono', Menlo, monospace;
47
+ --layer-popover: 1000000;
48
+
49
+ --chat-form-area-height: 8rem;
50
+ --chat-form-area-offset: 2rem;
51
+ --chat-form-padding-top: 6rem;
52
+ --max-message-height: max(24rem, min(80dvh, calc(100dvh - var(--chat-form-area-height) - 12rem)));
53
+ }
54
+
55
+ @media (min-width: 640px) {
56
+ :root {
57
+ --chat-form-area-height: 24rem;
58
+ --chat-form-area-offset: 12rem;
59
+ --chat-form-padding-top: 6rem;
60
+ }
61
+ }
62
+
63
+ .dark {
64
+ --background: oklch(0.16 0 0);
65
+ --foreground: oklch(0.985 0 0);
66
+ --card: oklch(0.205 0 0);
67
+ --card-foreground: oklch(0.985 0 0);
68
+ --popover: oklch(0.205 0 0);
69
+ --popover-foreground: oklch(0.985 0 0);
70
+ --primary: oklch(0.922 0 0);
71
+ --primary-foreground: oklch(0.205 0 0);
72
+ --secondary: oklch(0.29 0 0);
73
+ --secondary-foreground: oklch(0.985 0 0);
74
+ --muted: oklch(0.269 0 0);
75
+ --muted-foreground: oklch(0.708 0 0);
76
+ --accent: oklch(0.269 0 0);
77
+ --accent-foreground: oklch(0.985 0 0);
78
+ --destructive: oklch(0.704 0.191 22.216);
79
+ --border: oklch(1 0 0 / 30%);
80
+ --input: oklch(1 0 0 / 30%);
81
+ --ring: oklch(0.556 0 0);
82
+ --chart-1: oklch(0.488 0.243 264.376);
83
+ --chart-2: oklch(0.696 0.17 162.48);
84
+ --chart-3: oklch(0.769 0.188 70.08);
85
+ --chart-4: oklch(0.627 0.265 303.9);
86
+ --chart-5: oklch(0.645 0.246 16.439);
87
+ --sidebar: oklch(0.2 0 0);
88
+ --sidebar-foreground: oklch(0.985 0 0);
89
+ --sidebar-primary: oklch(0.488 0.243 264.376);
90
+ --sidebar-primary-foreground: oklch(0.985 0 0);
91
+ --sidebar-accent: oklch(0.269 0 0);
92
+ --sidebar-accent-foreground: oklch(0.985 0 0);
93
+ --sidebar-border: oklch(1 0 0 / 10%);
94
+ --sidebar-ring: oklch(0.556 0 0);
95
+ --code-background: oklch(0.225 0 0);
96
+ --code-foreground: oklch(0.875 0 0);
97
+ }
98
+
99
+ @theme inline {
100
+ --radius-sm: calc(var(--radius) - 4px);
101
+ --radius-md: calc(var(--radius) - 2px);
102
+ --radius-lg: var(--radius);
103
+ --radius-xl: calc(var(--radius) + 4px);
104
+ --color-background: var(--background);
105
+ --color-foreground: var(--foreground);
106
+ --color-card: var(--card);
107
+ --color-card-foreground: var(--card-foreground);
108
+ --color-popover: var(--popover);
109
+ --color-popover-foreground: var(--popover-foreground);
110
+ --color-primary: var(--primary);
111
+ --color-primary-foreground: var(--primary-foreground);
112
+ --color-secondary: var(--secondary);
113
+ --color-secondary-foreground: var(--secondary-foreground);
114
+ --color-muted: var(--muted);
115
+ --color-muted-foreground: var(--muted-foreground);
116
+ --color-accent: var(--accent);
117
+ --color-accent-foreground: var(--accent-foreground);
118
+ --color-destructive: var(--destructive);
119
+ --color-border: var(--border);
120
+ --color-input: var(--input);
121
+ --color-ring: var(--ring);
122
+ --color-chart-1: var(--chart-1);
123
+ --color-chart-2: var(--chart-2);
124
+ --color-chart-3: var(--chart-3);
125
+ --color-chart-4: var(--chart-4);
126
+ --color-chart-5: var(--chart-5);
127
+ --color-sidebar: var(--sidebar);
128
+ --color-sidebar-foreground: var(--sidebar-foreground);
129
+ --color-sidebar-primary: var(--sidebar-primary);
130
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
131
+ --color-sidebar-accent: var(--sidebar-accent);
132
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
133
+ --color-sidebar-border: var(--sidebar-border);
134
+ --color-sidebar-ring: var(--sidebar-ring);
135
+ }
136
+
137
+ @layer base {
138
+ * {
139
+ @apply border-border outline-ring/50;
140
+ }
141
+
142
+ body {
143
+ @apply bg-background text-foreground;
144
+ scrollbar-width: thin;
145
+ scrollbar-gutter: stable;
146
+ }
147
+
148
+ /* Global scrollbar styling - visible only on hover */
149
+ * {
150
+ scrollbar-width: thin;
151
+ scrollbar-color: transparent transparent;
152
+ }
153
+
154
+ *:hover {
155
+ scrollbar-color: hsl(var(--muted-foreground) / 0.3) transparent;
156
+ }
157
+
158
+ *::-webkit-scrollbar {
159
+ width: 6px;
160
+ height: 6px;
161
+ }
162
+
163
+ *::-webkit-scrollbar-track {
164
+ background: transparent;
165
+ }
166
+
167
+ *::-webkit-scrollbar-thumb {
168
+ background: transparent;
169
+ border-radius: 3px;
170
+ transition: background 0.2s ease;
171
+ }
172
+
173
+ *:hover::-webkit-scrollbar-thumb {
174
+ background: hsl(var(--muted-foreground) / 0.3);
175
+ }
176
+
177
+ *::-webkit-scrollbar-thumb:hover {
178
+ background: hsl(var(--muted-foreground) / 0.5);
179
+ }
180
+
181
+ :where(code, pre, kbd, samp) {
182
+ font-family: var(--font-mono);
183
+ }
184
+ }
185
+
186
+ @layer utilities {
187
+ .scrollbar-hide {
188
+ /* Hide scrollbar for Chrome, Safari and Opera */
189
+ &::-webkit-scrollbar {
190
+ display: none;
191
+ }
192
+ /* Hide scrollbar for IE, Edge and Firefox */
193
+ -ms-overflow-style: none;
194
+ scrollbar-width: none;
195
+ }
196
+ }
197
+
198
+ .mermaidTooltip {
199
+ display: none !important;
200
+ }
backend/llama.cpp/tools/ui/src/app.d.ts ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // See https://svelte.dev/docs/kit/types#app.d.ts
2
+ // for information about these interfaces
3
+
4
+ import 'vite-plugin-pwa/pwa-assets';
5
+ import 'vite-plugin-pwa/svelte';
6
+
7
+ // Import chat types from dedicated module
8
+
9
+ import type {
10
+ // API types
11
+ ApiChatCompletionRequest,
12
+ ApiChatCompletionResponse,
13
+ ApiChatCompletionStreamChunk,
14
+ ApiChatCompletionToolCall,
15
+ ApiChatCompletionToolCallDelta,
16
+ ApiChatMessageData,
17
+ ApiChatMessageContentPart,
18
+ ApiContextSizeError,
19
+ ApiErrorResponse,
20
+ ApiLlamaCppServerProps,
21
+ ApiModelDataEntry,
22
+ ApiModelLoadStage,
23
+ ApiModelsSseProgress,
24
+ ApiModelsSseData,
25
+ ApiModelsSseEvent,
26
+ ApiModelListResponse,
27
+ ApiProcessingState,
28
+ ApiRouterModelMeta,
29
+ ApiRouterModelsLoadRequest,
30
+ ApiRouterModelsLoadResponse,
31
+ ApiRouterModelsStatusRequest,
32
+ ApiRouterModelsStatusResponse,
33
+ ApiRouterModelsListResponse,
34
+ ApiRouterModelsUnloadRequest,
35
+ ApiRouterModelsUnloadResponse,
36
+ // Chat types
37
+ ChatAttachmentDisplayItem,
38
+ ChatMessageType,
39
+ ChatRole,
40
+ ChatUploadedFile,
41
+ ChatMessageSiblingInfo,
42
+ ChatMessagePromptProgress,
43
+ ChatMessageTimings,
44
+ // Database types
45
+ DatabaseConversation,
46
+ DatabaseMessage,
47
+ DatabaseMessageExtra,
48
+ DatabaseMessageExtraAudioFile,
49
+ DatabaseMessageExtraVideoFile,
50
+ DatabaseMessageExtraImageFile,
51
+ DatabaseMessageExtraTextFile,
52
+ DatabaseMessageExtraPdfFile,
53
+ DatabaseMessageExtraLegacyContext,
54
+ ExportedConversation,
55
+ ExportedConversations,
56
+ // Model types
57
+ ModelModalities,
58
+ ModelOption,
59
+ ModelLoadProgress,
60
+ // Settings types
61
+ SettingsChatServiceOptions,
62
+ SettingsConfigValue,
63
+ SettingsFieldConfig,
64
+ SettingsConfigType
65
+ } from '$lib/types';
66
+
67
+ import { ServerRole, ServerModelStatus, ModelModality } from '$lib/enums';
68
+
69
+ declare global {
70
+ // namespace App {
71
+ // interface Error {}
72
+ // interface Locals {}
73
+ // interface PageData {}
74
+ // interface PageState {}
75
+ // interface Platform {}
76
+ // }
77
+
78
+ export {
79
+ // API types
80
+ ApiChatCompletionRequest,
81
+ ApiChatCompletionResponse,
82
+ ApiChatCompletionStreamChunk,
83
+ ApiChatCompletionToolCall,
84
+ ApiChatCompletionToolCallDelta,
85
+ ApiChatMessageData,
86
+ ApiChatMessageContentPart,
87
+ ApiContextSizeError,
88
+ ApiErrorResponse,
89
+ ApiLlamaCppServerProps,
90
+ ApiModelDataEntry,
91
+ ApiModelLoadStage,
92
+ ApiModelsSseProgress,
93
+ ApiModelsSseData,
94
+ ApiModelsSseEvent,
95
+ ApiModelListResponse,
96
+ ApiProcessingState,
97
+ ApiRouterModelMeta,
98
+ ApiRouterModelsLoadRequest,
99
+ ApiRouterModelsLoadResponse,
100
+ ApiRouterModelsStatusRequest,
101
+ ApiRouterModelsStatusResponse,
102
+ ApiRouterModelsListResponse,
103
+ ApiRouterModelsUnloadRequest,
104
+ ApiRouterModelsUnloadResponse,
105
+ // Chat types
106
+ ChatAttachmentDisplayItem,
107
+ ChatMessagePromptProgress,
108
+ ChatMessageSiblingInfo,
109
+ ChatMessageTimings,
110
+ ChatMessageType,
111
+ ChatRole,
112
+ ChatUploadedFile,
113
+ // Database types
114
+ DatabaseConversation,
115
+ DatabaseMessage,
116
+ DatabaseMessageExtra,
117
+ DatabaseMessageExtraAudioFile,
118
+ DatabaseMessageExtraVideoFile,
119
+ DatabaseMessageExtraImageFile,
120
+ DatabaseMessageExtraTextFile,
121
+ DatabaseMessageExtraPdfFile,
122
+ DatabaseMessageExtraLegacyContext,
123
+ ExportedConversation,
124
+ ExportedConversations,
125
+ // Enum types
126
+ ModelModality,
127
+ ServerRole,
128
+ ServerModelStatus,
129
+ // Model types
130
+ ModelModalities,
131
+ ModelOption,
132
+ ModelLoadProgress,
133
+ // Settings types
134
+ SettingsChatServiceOptions,
135
+ SettingsConfigValue,
136
+ SettingsFieldConfig,
137
+ SettingsConfigType
138
+ };
139
+ }
140
+
141
+ declare global {
142
+ interface Window {
143
+ idxThemeStyle?: number;
144
+ idxCodeBlock?: number;
145
+ }
146
+ }
backend/llama.cpp/tools/ui/src/app.html ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link rel="icon" href="favicon.ico" sizes="48x48" />
6
+ <link rel="icon" href="favicon.svg" sizes="any" type="image/svg+xml" />
7
+
8
+ <link rel="apple-touch-icon" href="apple-touch-icon-180x180.png" />
9
+
10
+ <link rel="manifest" href="./manifest.webmanifest" />
11
+
12
+ <meta
13
+ name="viewport"
14
+ content="width=device-width, initial-scale=1, interactive-widget=resizes-content"
15
+ />
16
+ %sveltekit.head%
17
+ </head>
18
+
19
+ <body data-sveltekit-preload-data="hover">
20
+ <div style="display: contents">%sveltekit.body%</div>
21
+ </body>
22
+ </html>
backend/llama.cpp/tools/ui/src/lib/actions/fade-in-view.svelte.ts ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { isElementInViewport } from '$lib/utils/viewport';
2
+
3
+ /**
4
+ * Svelte action that fades in an element when it enters the viewport.
5
+ * Uses IntersectionObserver for efficient viewport detection.
6
+ *
7
+ * If skipIfVisible is set and the element is already visible in the viewport
8
+ * when the action attaches (e.g. a markdown block promoted from unstable
9
+ * during streaming), the fade is skipped entirely to avoid a flash.
10
+ */
11
+ export function fadeInView(
12
+ node: HTMLElement,
13
+ options: { duration?: number; y?: number; delay?: number; skipIfVisible?: boolean } = {}
14
+ ) {
15
+ const { duration = 300, y = 0, delay = 0, skipIfVisible = false } = options;
16
+
17
+ if (skipIfVisible && isElementInViewport(node)) {
18
+ return;
19
+ }
20
+
21
+ node.style.opacity = '0';
22
+ node.style.transform = `translateY(${y}px)`;
23
+ node.style.transition = `opacity ${duration}ms ease-out, transform ${duration}ms ease-out`;
24
+
25
+ $effect(() => {
26
+ const observer = new IntersectionObserver(
27
+ (entries) => {
28
+ for (const entry of entries) {
29
+ if (entry.isIntersecting) {
30
+ setTimeout(() => {
31
+ requestAnimationFrame(() => {
32
+ node.style.opacity = '1';
33
+ node.style.transform = 'translateY(0)';
34
+ });
35
+ }, delay);
36
+ observer.disconnect();
37
+ }
38
+ }
39
+ },
40
+ { threshold: 0.05 }
41
+ );
42
+
43
+ observer.observe(node);
44
+
45
+ return () => {
46
+ observer.disconnect();
47
+ };
48
+ });
49
+ }
backend/llama.cpp/tools/ui/src/lib/assets/logo.svg ADDED
backend/llama.cpp/tools/ui/src/lib/components/app/SKILL.md ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ name: app
3
+ description: Opinionated app components building on top of ./ui primitives
4
+ ---
5
+
6
+ - Can include business logic and state management
7
+ - Can include data fetching and caching logic
8
+ - Should use original spelling for HTML-native events and `camelCase` for custom events
9
+ - Props and markup attributes should be listed alphabetically
10
+ - Use JS Objects and Arrays for CSS classes and styles when they are dynamic
11
+ - Whenever there can be repetition in the component's markup, if it's too small to be decoupled as a separate component — use Svelte 5's `{#snippet}` + `{@render}`
backend/llama.cpp/tools/ui/src/lib/components/app/actions/ActionIcon.svelte ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Button, type ButtonVariant, type ButtonSize } from '$lib/components/ui/button';
3
+ import * as Tooltip from '$lib/components/ui/tooltip';
4
+ import type { Component } from 'svelte';
5
+ import { TooltipSide } from '$lib/enums';
6
+
7
+ interface Props {
8
+ ariaLabel?: string;
9
+ class?: string;
10
+ disabled?: boolean;
11
+ href?: string;
12
+ icon: Component;
13
+ iconSize?: string;
14
+ onclick?: (e?: MouseEvent) => void;
15
+ size?: ButtonSize;
16
+ stopPropagationOnClick?: boolean;
17
+ tooltip?: string;
18
+ variant?: ButtonVariant;
19
+ tooltipSide?: TooltipSide;
20
+ }
21
+
22
+ let {
23
+ icon,
24
+ tooltip,
25
+ variant = 'ghost',
26
+ href = '',
27
+ size = 'sm',
28
+ class: className = '',
29
+ disabled = false,
30
+ iconSize = 'h-3 w-3',
31
+ tooltipSide = TooltipSide.TOP,
32
+ stopPropagationOnClick = false,
33
+ onclick,
34
+ ariaLabel
35
+ }: Props = $props();
36
+
37
+ let innerWidth = $state(0);
38
+ const showTooltip = $derived(!!tooltip && innerWidth > 768);
39
+ </script>
40
+
41
+ {#snippet button(props = {})}
42
+ <Button
43
+ {...props}
44
+ {href}
45
+ {variant}
46
+ {size}
47
+ {disabled}
48
+ onclick={(e: MouseEvent) => {
49
+ if (stopPropagationOnClick) e.stopPropagation();
50
+
51
+ onclick?.(e);
52
+ }}
53
+ class="h-6 w-6 p-0 {className} flex hover:bg-transparent data-[state=open]:bg-transparent!"
54
+ aria-label={ariaLabel || tooltip}
55
+ >
56
+ {#if icon}
57
+ {@const IconComponent = icon}
58
+
59
+ <IconComponent class={iconSize} />
60
+ {/if}
61
+ </Button>
62
+ {/snippet}
63
+
64
+ {#if showTooltip}
65
+ <Tooltip.Root>
66
+ <Tooltip.Trigger>
67
+ <!-- prevent another nested button element -->
68
+ {#snippet child({ props })}
69
+ {@render button(props)}
70
+ {/snippet}
71
+ </Tooltip.Trigger>
72
+
73
+ <Tooltip.Content side={tooltipSide}>
74
+ <p>{tooltip}</p>
75
+ </Tooltip.Content>
76
+ </Tooltip.Root>
77
+ {:else}
78
+ {@render button({ href })}
79
+ {/if}
80
+
81
+ <svelte:window bind:innerWidth />
backend/llama.cpp/tools/ui/src/lib/components/app/actions/ActionIconCopyToClipboard.svelte ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Copy } from '@lucide/svelte';
3
+ import { copyToClipboard } from '$lib/utils';
4
+ import ActionIcon from './ActionIcon.svelte';
5
+
6
+ export let ariaLabel: string = 'Copy to clipboard';
7
+ export let canCopy: boolean = true;
8
+ export let text: string;
9
+ </script>
10
+
11
+ <ActionIcon
12
+ icon={Copy}
13
+ tooltip={ariaLabel}
14
+ iconSize="h-4 w-4"
15
+ disabled={!canCopy}
16
+ onclick={() => canCopy && copyToClipboard(text)}
17
+ />
backend/llama.cpp/tools/ui/src/lib/components/app/actions/index.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ *
3
+ * ACTIONS
4
+ *
5
+ * Small interactive components for user actions.
6
+ *
7
+ */
8
+
9
+ /** Styled icon button for action triggers with tooltip. */
10
+ export { default as ActionIcon } from './ActionIcon.svelte';
11
+
12
+ /** Copy-to-clipboard icon button with clipboard logic. */
13
+ export { default as ActionIconCopyToClipboard } from './ActionIconCopyToClipboard.svelte';
backend/llama.cpp/tools/ui/src/lib/components/app/badges/BadgeInfo.svelte ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLButtonAttributes } from 'svelte/elements';
4
+
5
+ interface Props extends HTMLButtonAttributes {
6
+ children: Snippet;
7
+ class?: string;
8
+ icon?: Snippet;
9
+ }
10
+
11
+ let { children, class: className = '', icon, ...rest }: Props = $props();
12
+ </script>
13
+
14
+ <button
15
+ {...rest}
16
+ class={[
17
+ 'inline-flex cursor-pointer items-center gap-1 rounded-sm bg-muted-foreground/15 px-1.5 py-0.75',
18
+ className
19
+ ]}
20
+ >
21
+ {#if icon}
22
+ {@render icon()}
23
+ {/if}
24
+
25
+ {@render children()}
26
+ </button>
backend/llama.cpp/tools/ui/src/lib/components/app/badges/BadgesModality.svelte ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Eye, Mic, Video } from '@lucide/svelte';
3
+ import { ModelModality } from '$lib/enums';
4
+
5
+ interface Props {
6
+ modalities: ModelModality[];
7
+ class?: string;
8
+ }
9
+
10
+ let { modalities, class: className = '' }: Props = $props();
11
+ </script>
12
+
13
+ {#each modalities as modality (modality)}
14
+ {#if modality === ModelModality.VISION || modality === ModelModality.AUDIO || modality === ModelModality.VIDEO}
15
+ <span
16
+ class={[
17
+ 'inline-flex items-center gap-1 rounded-md bg-muted px-2 py-1 text-xs font-medium',
18
+ className
19
+ ]}
20
+ >
21
+ {#if modality === ModelModality.VISION}
22
+ <Eye class="h-3 w-3" />
23
+
24
+ Vision (Image)
25
+ {:else if modality === ModelModality.VIDEO}
26
+ <Video class="h-3 w-3" />
27
+
28
+ Vision (Video)
29
+ {:else}
30
+ <Mic class="h-3 w-3" />
31
+
32
+ Audio
33
+ {/if}
34
+ </span>
35
+ {/if}
36
+ {/each}
backend/llama.cpp/tools/ui/src/lib/components/app/badges/index.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ *
3
+ * BADGES & INDICATORS
4
+ *
5
+ * Small visual indicators for status and metadata.
6
+ *
7
+ */
8
+
9
+ /** Generic info badge with optional tooltip and click handler. */
10
+ export { default as BadgeInfo } from './BadgeInfo.svelte';
11
+
12
+ /** Badge indicating model modality (vision, audio, tools). */
13
+ export { default as BadgesModality } from './BadgesModality.svelte';
backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsList.svelte ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import {
3
+ ChatAttachmentsListItem,
4
+ DialogChatAttachmentsPreview,
5
+ DialogMcpResourcePreview,
6
+ HorizontalScrollCarousel
7
+ } from '$lib/components/app';
8
+ import type { DatabaseMessageExtraMcpResource } from '$lib/types';
9
+ import { getAttachmentDisplayItems, isMcpPrompt, isMcpResource } from '$lib/utils';
10
+
11
+ interface Props {
12
+ class?: string;
13
+ style?: string;
14
+ // For ChatMessage - stored attachments
15
+ attachments?: DatabaseMessageExtra[];
16
+ readonly?: boolean;
17
+ // For ChatForm - pending uploads
18
+ onFileRemove?: (fileId: string) => void;
19
+ uploadedFiles?: ChatUploadedFile[];
20
+ // Image size customization
21
+ imageClass?: string;
22
+ imageHeight?: string;
23
+ imageWidth?: string;
24
+ // Limit display to single row with "+ X more" button
25
+ limitToSingleRow?: boolean;
26
+ // For vision modality check
27
+ activeModelId?: string;
28
+ }
29
+
30
+ let {
31
+ class: className = '',
32
+ style = '',
33
+ attachments = [],
34
+ readonly = false,
35
+ onFileRemove,
36
+ uploadedFiles = $bindable([]),
37
+ // Default to small size for form previews
38
+ imageClass = '',
39
+ imageHeight = 'h-24',
40
+ imageWidth = 'w-auto',
41
+ limitToSingleRow = false,
42
+ activeModelId
43
+ }: Props = $props();
44
+
45
+ let carouselRef: HorizontalScrollCarousel | undefined = $state();
46
+ let mcpResourcePreviewOpen = $state(false);
47
+ let mcpResourcePreviewExtra = $state<DatabaseMessageExtraMcpResource | null>(null);
48
+ let previewFocusIndex = $state(0);
49
+ let viewAllDialogOpen = $state(false);
50
+
51
+ let displayItems = $derived(getAttachmentDisplayItems({ uploadedFiles, attachments }));
52
+
53
+ function openPreview(item: ChatAttachmentDisplayItem, event?: MouseEvent) {
54
+ event?.stopPropagation();
55
+ event?.preventDefault();
56
+
57
+ // Find the index of the clicked item among non-MCP attachments
58
+ const nonMcpItems = displayItems.filter((i) => !isMcpPrompt(i) && !isMcpResource(i));
59
+ const index = nonMcpItems.findIndex((i) => i.id === item.id);
60
+
61
+ previewFocusIndex = index >= 0 ? index : 0;
62
+ viewAllDialogOpen = true;
63
+ }
64
+
65
+ function openMcpResourcePreview(extra: DatabaseMessageExtraMcpResource) {
66
+ mcpResourcePreviewExtra = extra;
67
+ mcpResourcePreviewOpen = true;
68
+ }
69
+
70
+ $effect(() => {
71
+ if (carouselRef && displayItems.length) {
72
+ carouselRef.resetScroll();
73
+ }
74
+ });
75
+ </script>
76
+
77
+ {#snippet attachmentitem(item: ChatAttachmentDisplayItem)}
78
+ <ChatAttachmentsListItem
79
+ {imageClass}
80
+ {imageHeight}
81
+ {imageWidth}
82
+ {item}
83
+ {limitToSingleRow}
84
+ {onFileRemove}
85
+ onMcpResourcePreview={openMcpResourcePreview}
86
+ onPreview={(i: ChatAttachmentDisplayItem, event?: MouseEvent) => openPreview(i, event)}
87
+ {readonly}
88
+ />
89
+ {/snippet}
90
+
91
+ {#if displayItems.length > 0}
92
+ <div class={className} {style}>
93
+ {#if limitToSingleRow}
94
+ <HorizontalScrollCarousel bind:this={carouselRef}>
95
+ {#each displayItems as item (item.id)}
96
+ {@render attachmentitem(item)}
97
+ {/each}
98
+ </HorizontalScrollCarousel>
99
+ {:else}
100
+ <div class="flex flex-wrap items-start justify-end gap-3">
101
+ {#each displayItems as item (item.id)}
102
+ {@render attachmentitem(item)}
103
+ {/each}
104
+ </div>
105
+ {/if}
106
+ </div>
107
+ {/if}
108
+
109
+ <DialogChatAttachmentsPreview
110
+ {activeModelId}
111
+ {attachments}
112
+ bind:open={viewAllDialogOpen}
113
+ {previewFocusIndex}
114
+ {uploadedFiles}
115
+ />
116
+
117
+ {#if mcpResourcePreviewExtra}
118
+ <DialogMcpResourcePreview extra={mcpResourcePreviewExtra} bind:open={mcpResourcePreviewOpen} />
119
+ {/if}
backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItem.svelte ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import {
3
+ ChatAttachmentsListItemMcpPrompt,
4
+ ChatAttachmentsListItemMcpResource,
5
+ ChatAttachmentsListItemThumbnailImage,
6
+ ChatAttachmentsListItemThumbnailFile
7
+ } from '$lib/components/app';
8
+ import { AttachmentType } from '$lib/enums';
9
+ import type {
10
+ ChatAttachmentDisplayItem,
11
+ DatabaseMessageExtraMcpPrompt,
12
+ DatabaseMessageExtraMcpResource,
13
+ MCPResourceAttachment
14
+ } from '$lib/types';
15
+ import { isMcpPrompt, isMcpResource, isPdfFile } from '$lib/utils';
16
+
17
+ interface Props {
18
+ class?: string;
19
+ imageClass?: string;
20
+ imageHeight?: string;
21
+ imageWidth?: string;
22
+ item: ChatAttachmentDisplayItem;
23
+ limitToSingleRow?: boolean;
24
+ onFileRemove?: (fileId: string) => void;
25
+ onMcpResourcePreview?: (extra: DatabaseMessageExtraMcpResource) => void;
26
+ onPreview?: (item: ChatAttachmentDisplayItem) => void;
27
+ readonly?: boolean;
28
+ }
29
+
30
+ let {
31
+ class: className = '',
32
+ imageClass = '',
33
+ imageHeight = 'h-24',
34
+ imageWidth = 'w-auto',
35
+ item,
36
+ limitToSingleRow = false,
37
+ onFileRemove,
38
+ onMcpResourcePreview,
39
+ onPreview,
40
+ readonly = false
41
+ }: Props = $props();
42
+
43
+ const scrollClasses = $derived(limitToSingleRow ? 'first:ml-4 last:mr-4' : '');
44
+
45
+ function toMcpResourceAttachment(
46
+ extra: DatabaseMessageExtraMcpResource,
47
+ id: string
48
+ ): MCPResourceAttachment {
49
+ return {
50
+ id,
51
+ resource: {
52
+ uri: extra.uri,
53
+ name: extra.name,
54
+ title: extra.name,
55
+ serverName: extra.serverName
56
+ }
57
+ };
58
+ }
59
+ </script>
60
+
61
+ {#if isMcpPrompt(item)}
62
+ {@const mcpPrompt =
63
+ item.attachment?.type === AttachmentType.MCP_PROMPT
64
+ ? (item.attachment as DatabaseMessageExtraMcpPrompt)
65
+ : item.uploadedFile?.mcpPrompt
66
+ ? {
67
+ type: AttachmentType.MCP_PROMPT as const,
68
+ name: item.name,
69
+ serverName: item.uploadedFile.mcpPrompt.serverName,
70
+ promptName: item.uploadedFile.mcpPrompt.promptName,
71
+ content: item.textContent ?? '',
72
+ arguments: item.uploadedFile.mcpPrompt.arguments
73
+ }
74
+ : null}
75
+ {#if mcpPrompt}
76
+ <ChatAttachmentsListItemMcpPrompt
77
+ class="max-w-[300px] min-w-[200px] flex-shrink-0 {className} {scrollClasses}"
78
+ prompt={mcpPrompt}
79
+ {readonly}
80
+ isLoading={item.isLoading}
81
+ loadError={item.loadError}
82
+ onRemove={onFileRemove ? () => onFileRemove(item.id) : undefined}
83
+ />
84
+ {/if}
85
+ {:else if isMcpResource(item)}
86
+ {@const mcpResource = item.attachment as DatabaseMessageExtraMcpResource}
87
+
88
+ <ChatAttachmentsListItemMcpResource
89
+ class="flex-shrink-0 {className} {scrollClasses}"
90
+ attachment={toMcpResourceAttachment(mcpResource, item.id)}
91
+ onclick={() => onMcpResourcePreview?.(mcpResource)}
92
+ />
93
+ {:else if item.isImage && item.preview}
94
+ <ChatAttachmentsListItemThumbnailImage
95
+ class="flex-shrink-0 cursor-pointer {className} {scrollClasses}"
96
+ id={item.id}
97
+ name={item.name}
98
+ preview={item.preview}
99
+ {readonly}
100
+ onRemove={onFileRemove}
101
+ height={imageHeight}
102
+ width={imageWidth}
103
+ {imageClass}
104
+ onclick={() => onPreview?.(item)}
105
+ />
106
+ {:else if isPdfFile(item.attachment, item.uploadedFile)}
107
+ <ChatAttachmentsListItemThumbnailFile
108
+ class="flex-shrink-0 cursor-pointer {className} {scrollClasses}"
109
+ id={item.id}
110
+ name={item.name}
111
+ size={item.size}
112
+ {readonly}
113
+ onRemove={onFileRemove}
114
+ textContent={item.textContent}
115
+ attachment={item.attachment}
116
+ uploadedFile={item.uploadedFile}
117
+ onclick={() => onPreview?.(item)}
118
+ />
119
+ {:else}
120
+ <ChatAttachmentsListItemThumbnailFile
121
+ class="flex-shrink-0 cursor-pointer {className} {scrollClasses}"
122
+ id={item.id}
123
+ name={item.name}
124
+ size={item.size}
125
+ {readonly}
126
+ onRemove={onFileRemove}
127
+ textContent={item.textContent}
128
+ attachment={item.attachment}
129
+ uploadedFile={item.uploadedFile}
130
+ onclick={() => onPreview?.(item)}
131
+ />
132
+ {/if}
backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItemMcpPrompt.svelte ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { ChatMessageMcpPromptContent, ActionIcon } from '$lib/components/app';
3
+ import { X } from '@lucide/svelte';
4
+ import type { DatabaseMessageExtraMcpPrompt } from '$lib/types';
5
+ import { McpPromptVariant } from '$lib/enums';
6
+
7
+ interface Props {
8
+ class?: string;
9
+ isLoading?: boolean;
10
+ loadError?: string;
11
+ onRemove?: () => void;
12
+ prompt: DatabaseMessageExtraMcpPrompt;
13
+ readonly?: boolean;
14
+ }
15
+
16
+ let {
17
+ class: className = '',
18
+ isLoading = false,
19
+ loadError,
20
+ onRemove,
21
+ prompt,
22
+ readonly = false
23
+ }: Props = $props();
24
+ </script>
25
+
26
+ <div class="group relative {className}">
27
+ <ChatMessageMcpPromptContent
28
+ {isLoading}
29
+ {loadError}
30
+ {prompt}
31
+ variant={McpPromptVariant.ATTACHMENT}
32
+ />
33
+
34
+ {#if !readonly && onRemove}
35
+ <div
36
+ class="absolute top-10 right-2 flex items-center justify-center opacity-0 transition-opacity group-hover:opacity-100"
37
+ >
38
+ <ActionIcon icon={X} tooltip="Remove" stopPropagationOnClick onclick={() => onRemove?.()} />
39
+ </div>
40
+ {/if}
41
+ </div>
backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItemMcpResource.svelte ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { Loader2, AlertCircle } from '@lucide/svelte';
3
+ import { mcpStore } from '$lib/stores/mcp.svelte';
4
+ import type { MCPResourceAttachment } from '$lib/types';
5
+ import * as Tooltip from '$lib/components/ui/tooltip';
6
+ import { ActionIcon } from '$lib/components/app';
7
+ import { X } from '@lucide/svelte';
8
+ import { getResourceIcon, getResourceDisplayName } from '$lib/utils';
9
+
10
+ interface Props {
11
+ attachment: MCPResourceAttachment;
12
+ class?: string;
13
+ onclick?: () => void;
14
+ onRemove?: (attachmentId: string) => void;
15
+ }
16
+
17
+ let { attachment, class: className, onclick, onRemove }: Props = $props();
18
+
19
+ const ResourceIcon = $derived(
20
+ getResourceIcon(attachment.resource.mimeType, attachment.resource.uri)
21
+ );
22
+ const serverName = $derived(mcpStore.getServerDisplayName(attachment.resource.serverName));
23
+ const favicon = $derived(mcpStore.getServerFavicon(attachment.resource.serverName));
24
+
25
+ function getStatusClass(attachment: MCPResourceAttachment): string {
26
+ if (attachment.error) return 'border-red-500/50 bg-red-500/10';
27
+ if (attachment.loading) return 'border-border/50 bg-muted/30';
28
+
29
+ return 'border-border/50 bg-muted/30';
30
+ }
31
+ </script>
32
+
33
+ <Tooltip.Root>
34
+ <Tooltip.Trigger>
35
+ <button
36
+ class={[
37
+ 'flex flex-shrink-0 items-center gap-1.5 rounded-md border px-2 py-0.75 text-sm transition-colors',
38
+ getStatusClass(attachment),
39
+ onclick && 'cursor-pointer hover:bg-muted/50',
40
+ className
41
+ ]}
42
+ disabled={!onclick}
43
+ {onclick}
44
+ type="button"
45
+ >
46
+ {#if attachment.loading}
47
+ <Loader2 class="h-3 w-3 animate-spin text-muted-foreground" />
48
+ {:else if attachment.error}
49
+ <AlertCircle class="h-3 w-3 text-red-500" />
50
+ {:else}
51
+ <ResourceIcon class="h-3 w-3 text-muted-foreground" />
52
+ {/if}
53
+
54
+ <span class="max-w-[150px] truncate text-xs">
55
+ {getResourceDisplayName(attachment.resource)}
56
+ </span>
57
+
58
+ {#if onRemove}
59
+ <ActionIcon
60
+ class="-my-2 -mr-1.5 bg-transparent"
61
+ icon={X}
62
+ iconSize="h-2 w-2"
63
+ onclick={() => onRemove?.(attachment.id)}
64
+ stopPropagationOnClick
65
+ tooltip="Remove"
66
+ />
67
+ {/if}
68
+ </button>
69
+ </Tooltip.Trigger>
70
+
71
+ <Tooltip.Content>
72
+ <div class="flex items-center gap-1 text-xs">
73
+ {#if favicon}
74
+ <img
75
+ alt={attachment.resource.serverName}
76
+ class="h-3 w-3 shrink-0 rounded-sm"
77
+ onerror={(e) => {
78
+ (e.currentTarget as HTMLImageElement).style.display = 'none';
79
+ }}
80
+ src={favicon}
81
+ />
82
+ {/if}
83
+
84
+ <span class="truncate">
85
+ {serverName}
86
+ </span>
87
+ </div>
88
+ </Tooltip.Content>
89
+ </Tooltip.Root>
backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItemThumbnailFile.svelte ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { X, Music, Video } from '@lucide/svelte';
3
+ import {
4
+ formatFileSize,
5
+ getFileTypeLabel,
6
+ getPreviewText,
7
+ isPdfFile,
8
+ isAudioFile,
9
+ isVideoFile,
10
+ isTextFile
11
+ } from '$lib/utils';
12
+ import { ActionIcon } from '$lib/components/app';
13
+ import { AttachmentType } from '$lib/enums';
14
+
15
+ interface Props {
16
+ attachment?: DatabaseMessageExtra;
17
+ class?: string;
18
+ id: string;
19
+ onclick?: (event: MouseEvent) => void;
20
+ onRemove?: (id: string) => void;
21
+ name: string;
22
+ readonly?: boolean;
23
+ size?: number;
24
+ textContent?: string;
25
+ // Either uploaded file or stored attachment
26
+ uploadedFile?: ChatUploadedFile;
27
+ }
28
+
29
+ let {
30
+ attachment,
31
+ class: className = '',
32
+ id,
33
+ onclick,
34
+ onRemove,
35
+ name,
36
+ readonly = false,
37
+ size,
38
+ textContent,
39
+ uploadedFile
40
+ }: Props = $props();
41
+
42
+ let isPdf = $derived(isPdfFile(attachment, uploadedFile));
43
+ let isAudio = $derived(isAudioFile(attachment, uploadedFile));
44
+ let isVideo = $derived(isVideoFile(attachment, uploadedFile));
45
+ let isPdfWithContent = $derived(isPdf && !!textContent);
46
+
47
+ let isText = $derived(isTextFile(attachment, uploadedFile));
48
+ let isTextWithContent = $derived(isText && !!textContent);
49
+
50
+ let fileTypeLabel = $derived.by(() => {
51
+ if (uploadedFile?.type) {
52
+ return getFileTypeLabel(uploadedFile.type);
53
+ }
54
+
55
+ if (attachment) {
56
+ if ('mimeType' in attachment && attachment.mimeType) {
57
+ return getFileTypeLabel(attachment.mimeType);
58
+ }
59
+
60
+ if (attachment.type) {
61
+ return getFileTypeLabel(attachment.type);
62
+ }
63
+ }
64
+
65
+ return getFileTypeLabel(name);
66
+ });
67
+
68
+ let pdfProcessingMode = $derived.by(() => {
69
+ if (attachment?.type === AttachmentType.PDF) {
70
+ const pdfAttachment = attachment as DatabaseMessageExtraPdfFile;
71
+
72
+ return pdfAttachment.processedAsImages ? 'Sent as Image' : 'Sent as Text';
73
+ }
74
+
75
+ return null;
76
+ });
77
+ </script>
78
+
79
+ {#snippet textPreview(content: string)}
80
+ <div class="relative">
81
+ <div
82
+ class="font-mono text-xs leading-relaxed break-words whitespace-pre-wrap text-muted-foreground {!readonly
83
+ ? 'max-h-3rem line-height-1.2'
84
+ : ''}"
85
+ >
86
+ {getPreviewText(content)}
87
+ </div>
88
+
89
+ {#if content.length > 150}
90
+ <div
91
+ class="pointer-events-none absolute right-0 bottom-0 left-0 h-4 bg-gradient-to-t from-muted to-transparent {readonly
92
+ ? 'h-6'
93
+ : ''}"
94
+ ></div>
95
+ {/if}
96
+ </div>
97
+ {/snippet}
98
+
99
+ {#snippet removeButton()}
100
+ <div
101
+ class="absolute top-2 right-2 opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100"
102
+ >
103
+ <ActionIcon icon={X} tooltip="Remove" stopPropagationOnClick onclick={() => onRemove?.(id)} />
104
+ </div>
105
+ {/snippet}
106
+
107
+ {#snippet fileIcon()}
108
+ <div
109
+ class="flex h-8 w-8 items-center justify-center rounded bg-primary/10 text-xs font-medium text-primary"
110
+ >
111
+ {#if isAudio}
112
+ <Music class="h-4 w-4 text-white/70" />
113
+ {:else if isVideo}
114
+ <Video class="h-4 w-4 text-white/70" />
115
+ {:else}
116
+ {fileTypeLabel}
117
+ {/if}
118
+ </div>
119
+ {/snippet}
120
+
121
+ {#snippet info(text: string | undefined)}
122
+ {#if text}
123
+ <span class="text-xs text-muted-foreground">{text}</span>
124
+ {/if}
125
+ {/snippet}
126
+
127
+ {#if isTextWithContent || isPdfWithContent}
128
+ <button
129
+ aria-label={readonly ? `Preview ${name}` : undefined}
130
+ class="rounded-lg border border-border bg-muted p-3 {className} cursor-pointer {readonly
131
+ ? 'w-full max-w-2xl transition-shadow hover:shadow-md'
132
+ : `group relative text-left ${textContent ? 'max-h-24 max-w-72' : 'max-w-36'}`} overflow-hidden"
133
+ {onclick}
134
+ type="button"
135
+ >
136
+ {#if !readonly}
137
+ {@render removeButton()}
138
+ {/if}
139
+
140
+ <div class={[!readonly && 'pr-8', 'overflow-hidden']}>
141
+ {#if readonly}
142
+ <div class="flex items-start gap-3">
143
+ <div class="flex min-w-0 flex-1 flex-col items-start text-left">
144
+ <span class="w-full truncate text-sm font-medium text-foreground">{name}</span>
145
+
146
+ {@render info(pdfProcessingMode || (size ? formatFileSize(size) : undefined))}
147
+
148
+ {#if textContent}
149
+ {@render textPreview(textContent)}
150
+ {/if}
151
+ </div>
152
+ </div>
153
+ {:else}
154
+ <span class="mb-3 block truncate text-sm font-medium text-foreground">{name}</span>
155
+
156
+ {#if textContent}
157
+ {@render textPreview(textContent)}
158
+ {/if}
159
+ {/if}
160
+ </div>
161
+ </button>
162
+ {:else}
163
+ <button
164
+ class="group flex items-center gap-3 rounded-lg border border-border bg-muted p-3 {className} relative"
165
+ {onclick}
166
+ type="button"
167
+ >
168
+ {@render fileIcon()}
169
+
170
+ <div class="flex flex-col items-start gap-0.5">
171
+ <span
172
+ class="max-w-24 truncate text-sm font-medium text-foreground {readonly
173
+ ? ''
174
+ : 'group-hover:pr-6'} md:max-w-32"
175
+ >
176
+ {name}
177
+ </span>
178
+
179
+ {@render info(pdfProcessingMode || (size ? formatFileSize(size) : undefined))}
180
+ </div>
181
+
182
+ {#if !readonly}
183
+ {@render removeButton()}
184
+ {/if}
185
+ </button>
186
+ {/if}
backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItemThumbnailImage.svelte ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { ActionIcon } from '$lib/components/app';
3
+ import { X } from '@lucide/svelte';
4
+
5
+ interface Props {
6
+ class?: string;
7
+ height?: string;
8
+ id: string;
9
+ imageClass?: string;
10
+ onclick?: (event?: MouseEvent) => void;
11
+ onRemove?: (id: string) => void;
12
+ name: string;
13
+ preview: string;
14
+ readonly?: boolean;
15
+ width?: string;
16
+ }
17
+
18
+ let {
19
+ class: className = '',
20
+ height = 'h-16',
21
+ id,
22
+ imageClass = '',
23
+ onclick,
24
+ onRemove,
25
+ name,
26
+ preview,
27
+ readonly = false,
28
+ width = 'w-auto'
29
+ }: Props = $props();
30
+ </script>
31
+
32
+ {#snippet image()}
33
+ <img src={preview} alt={name} class="{height} {width} cursor-pointer object-cover {imageClass}" />
34
+ {/snippet}
35
+
36
+ <div
37
+ class="group relative overflow-hidden rounded-lg bg-muted shadow-lg dark:border dark:border-muted {className}"
38
+ >
39
+ {#if onclick}
40
+ <button
41
+ aria-label="Preview {name}"
42
+ class="block h-full w-full rounded-lg focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:outline-none"
43
+ {onclick}
44
+ type="button"
45
+ >
46
+ {@render image()}
47
+ </button>
48
+ {:else}
49
+ {@render image()}
50
+ {/if}
51
+
52
+ {#if !readonly}
53
+ <div
54
+ class="absolute top-1 right-1 flex items-center justify-center opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100"
55
+ >
56
+ <ActionIcon
57
+ class="text-white"
58
+ icon={X}
59
+ onclick={() => onRemove?.(id)}
60
+ stopPropagationOnClick
61
+ tooltip="Remove"
62
+ />
63
+ </div>
64
+ {/if}
65
+ </div>
backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview.svelte ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import {
3
+ ChatAttachmentsPreviewCurrentItem,
4
+ ChatAttachmentsPreviewFileInfo,
5
+ ChatAttachmentsPreviewNavButtons,
6
+ ChatAttachmentsPreviewThumbnailStrip
7
+ } from '$lib/components/app';
8
+ import { modelsStore } from '$lib/stores/models.svelte';
9
+ import {
10
+ createBase64DataUrl,
11
+ formatFileSize,
12
+ getAttachmentDisplayItems,
13
+ getLanguageFromFilename,
14
+ isAudioFile,
15
+ isVideoFile,
16
+ isImageFile,
17
+ isMcpPrompt,
18
+ isMcpResource,
19
+ isPdfFile,
20
+ isTextFile
21
+ } from '$lib/utils';
22
+
23
+ interface PreviewItem {
24
+ id: string;
25
+ name: string;
26
+ size?: number;
27
+ preview?: string;
28
+ uploadedFile?: ChatUploadedFile;
29
+ attachment?: DatabaseMessageExtra;
30
+ textContent?: string;
31
+ isImage: boolean;
32
+ isAudio: boolean;
33
+ isVideo: boolean;
34
+ }
35
+
36
+ interface Props {
37
+ uploadedFiles?: ChatUploadedFile[];
38
+ attachments?: DatabaseMessageExtra[];
39
+ activeModelId?: string;
40
+ class?: string;
41
+ previewFocusIndex?: number;
42
+ }
43
+
44
+ let {
45
+ uploadedFiles = [],
46
+ attachments = [],
47
+ activeModelId,
48
+ class: className = '',
49
+ previewFocusIndex = 0
50
+ }: Props = $props();
51
+
52
+ let allItems = $derived(
53
+ getAttachmentDisplayItems({ uploadedFiles, attachments })
54
+ .filter((item) => !isMcpPrompt(item) && !isMcpResource(item))
55
+ .map(
56
+ (item): PreviewItem => ({
57
+ ...item,
58
+ isImage: isImageFile(item.attachment, item.uploadedFile),
59
+ isAudio: isAudioFile(item.attachment, item.uploadedFile),
60
+ isVideo: isVideoFile(item.attachment, item.uploadedFile)
61
+ })
62
+ )
63
+ );
64
+
65
+ let currentIndex = $state(0);
66
+
67
+ $effect(() => {
68
+ if (previewFocusIndex >= 0 && previewFocusIndex < allItems.length) {
69
+ currentIndex = previewFocusIndex;
70
+ }
71
+ });
72
+
73
+ $effect(() => {
74
+ const handler = (e: Event) => {
75
+ const delta = (e as CustomEvent).detail;
76
+
77
+ if (delta < 0) {
78
+ currentIndex = currentIndex > 0 ? currentIndex - 1 : allItems.length - 1;
79
+ } else {
80
+ currentIndex = currentIndex < allItems.length - 1 ? currentIndex + 1 : 0;
81
+ }
82
+ };
83
+
84
+ document.addEventListener('chat-attachments-nav', handler);
85
+
86
+ return () => document.removeEventListener('chat-attachments-nav', handler);
87
+ });
88
+
89
+ $effect(() => {
90
+ const index = currentIndex;
91
+ setTimeout(() => {
92
+ const thumbnail = document.querySelector(`[data-thumbnail-index="${index}"]`);
93
+
94
+ thumbnail?.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' });
95
+ }, 0);
96
+ });
97
+
98
+ let currentItem = $derived(allItems[currentIndex] ?? null);
99
+ let displayName = $derived(
100
+ currentItem?.name ||
101
+ currentItem?.uploadedFile?.name ||
102
+ currentItem?.attachment?.name ||
103
+ 'Unknown File'
104
+ );
105
+ let isAudio = $derived(
106
+ currentItem ? isAudioFile(currentItem.attachment, currentItem.uploadedFile) : false
107
+ );
108
+ let isVideo = $derived(
109
+ currentItem ? isVideoFile(currentItem.attachment, currentItem.uploadedFile) : false
110
+ );
111
+ let isImage = $derived(
112
+ currentItem ? isImageFile(currentItem.attachment, currentItem.uploadedFile) : false
113
+ );
114
+ let isPdf = $derived(
115
+ currentItem ? isPdfFile(currentItem.attachment, currentItem.uploadedFile) : false
116
+ );
117
+ let isText = $derived(
118
+ currentItem ? isTextFile(currentItem.attachment, currentItem.uploadedFile) : false
119
+ );
120
+
121
+ let displayPreview = $derived(
122
+ currentItem?.uploadedFile?.preview ||
123
+ (isImage && currentItem?.attachment && 'base64Url' in currentItem.attachment
124
+ ? currentItem.attachment.base64Url
125
+ : currentItem?.preview)
126
+ );
127
+
128
+ let displayTextContent = $derived(
129
+ currentItem?.uploadedFile?.textContent ||
130
+ (currentItem?.attachment && 'content' in currentItem.attachment
131
+ ? currentItem.attachment.content
132
+ : currentItem?.textContent)
133
+ );
134
+
135
+ let language = $derived(getLanguageFromFilename(displayName));
136
+
137
+ let fileSize = $derived(currentItem?.size ? formatFileSize(currentItem.size) : '');
138
+
139
+ let hasVisionModality = $derived(
140
+ currentItem && activeModelId ? modelsStore.modelSupportsVision(activeModelId) : false
141
+ );
142
+
143
+ let audioSrc = $derived(
144
+ isAudio && currentItem
145
+ ? (currentItem.uploadedFile?.preview ??
146
+ (currentItem.attachment &&
147
+ 'mimeType' in currentItem.attachment &&
148
+ 'base64Data' in currentItem.attachment
149
+ ? createBase64DataUrl(
150
+ currentItem.attachment.mimeType,
151
+ currentItem.attachment.base64Data
152
+ )
153
+ : null))
154
+ : null
155
+ );
156
+
157
+ let videoSrc = $derived(
158
+ isVideo && currentItem
159
+ ? (currentItem.uploadedFile?.preview ??
160
+ (currentItem.attachment &&
161
+ 'mimeType' in currentItem.attachment &&
162
+ 'base64Data' in currentItem.attachment
163
+ ? createBase64DataUrl(
164
+ currentItem.attachment.mimeType,
165
+ currentItem.attachment.base64Data
166
+ )
167
+ : null))
168
+ : null
169
+ );
170
+
171
+ export function prev() {
172
+ currentIndex = currentIndex > 0 ? currentIndex - 1 : allItems.length - 1;
173
+ }
174
+
175
+ export function next() {
176
+ currentIndex = currentIndex < allItems.length - 1 ? currentIndex + 1 : 0;
177
+ }
178
+
179
+ function onNavigate(index: number) {
180
+ currentIndex = index;
181
+ }
182
+ </script>
183
+
184
+ <div class="{className} flex flex-col text-white">
185
+ <div class="relative flex min-h-0 flex-1 items-center justify-center overflow-hidden">
186
+ <ChatAttachmentsPreviewNavButtons onPrev={prev} onNext={next} show={allItems.length > 1} />
187
+
188
+ <div class="flex h-full w-full flex-col items-center justify-start overflow-auto py-4">
189
+ {#if currentItem}
190
+ <ChatAttachmentsPreviewFileInfo {displayName} {fileSize} />
191
+
192
+ <ChatAttachmentsPreviewCurrentItem
193
+ {currentItem}
194
+ {isImage}
195
+ {isAudio}
196
+ {isVideo}
197
+ {isPdf}
198
+ {isText}
199
+ {displayPreview}
200
+ {displayTextContent}
201
+ {audioSrc}
202
+ {videoSrc}
203
+ {language}
204
+ {hasVisionModality}
205
+ {activeModelId}
206
+ />
207
+ {/if}
208
+
209
+ <ChatAttachmentsPreviewThumbnailStrip items={allItems} {currentIndex} {onNavigate} />
210
+ </div>
211
+ </div>
212
+ </div>
backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview/ChatAttachmentsPreviewCurrentItem/ChatAttachmentsPreviewCurrentItem.svelte ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { ChatAttachmentDisplayItem } from '$lib/types';
3
+ import { Image, Music, Video, FileText, FileIcon } from '@lucide/svelte';
4
+ import ChatAttachmentsPreviewCurrentItemPdf from './ChatAttachmentsPreviewCurrentItemPdf.svelte';
5
+ import ChatAttachmentsPreviewCurrentItemImage from './ChatAttachmentsPreviewCurrentItemImage.svelte';
6
+ import ChatAttachmentsPreviewCurrentItemAudio from './ChatAttachmentsPreviewCurrentItemAudio.svelte';
7
+ import ChatAttachmentsPreviewCurrentItemVideo from './ChatAttachmentsPreviewCurrentItemVideo.svelte';
8
+ import ChatAttachmentsPreviewCurrentItemText from './ChatAttachmentsPreviewCurrentItemText.svelte';
9
+ import ChatAttachmentsPreviewCurrentItemUnavailable from './ChatAttachmentsPreviewCurrentItemUnavailable.svelte';
10
+
11
+ interface Props {
12
+ currentItem: ChatAttachmentDisplayItem | null;
13
+ isImage: boolean;
14
+ isAudio: boolean;
15
+ isVideo: boolean;
16
+ isPdf: boolean;
17
+ isText: boolean;
18
+ displayPreview: string | undefined;
19
+ displayTextContent: string | undefined;
20
+ audioSrc: string | null;
21
+ videoSrc: string | null;
22
+ language: string;
23
+ hasVisionModality: boolean;
24
+ activeModelId?: string;
25
+ }
26
+
27
+ let {
28
+ currentItem,
29
+ isImage,
30
+ isAudio,
31
+ isVideo,
32
+ isPdf,
33
+ isText,
34
+ displayPreview,
35
+ displayTextContent,
36
+ audioSrc,
37
+ videoSrc,
38
+ language,
39
+ hasVisionModality,
40
+ activeModelId
41
+ }: Props = $props();
42
+
43
+ let IconComponent = $derived(
44
+ isImage ? Image : isText || isPdf ? FileText : isAudio ? Music : isVideo ? Video : FileIcon
45
+ );
46
+
47
+ let isUnavailable = $derived(
48
+ !isPdf && !isImage && !(isText && displayTextContent) && !isAudio && !isVideo
49
+ );
50
+ </script>
51
+
52
+ {#if currentItem}
53
+ {#key currentItem.id}
54
+ {#if isPdf}
55
+ <ChatAttachmentsPreviewCurrentItemPdf
56
+ {currentItem}
57
+ displayName={currentItem.name}
58
+ {displayTextContent}
59
+ {hasVisionModality}
60
+ {activeModelId}
61
+ />
62
+ {:else if isImage}
63
+ <ChatAttachmentsPreviewCurrentItemImage {currentItem} {displayPreview} />
64
+ {:else if isText && displayTextContent}
65
+ <ChatAttachmentsPreviewCurrentItemText {displayTextContent} {language} />
66
+ {:else if isAudio}
67
+ <ChatAttachmentsPreviewCurrentItemAudio {currentItem} {audioSrc} />
68
+ {:else if isVideo}
69
+ <ChatAttachmentsPreviewCurrentItemVideo {currentItem} {videoSrc} />
70
+ {:else if isUnavailable}
71
+ <ChatAttachmentsPreviewCurrentItemUnavailable {IconComponent} />
72
+ {/if}
73
+ {/key}
74
+ {/if}