Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub (part 8)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +2 -0
- backend/llama.cpp/tools/ui/README.md +688 -0
- backend/llama.cpp/tools/ui/docs/architecture/high-level-architecture.md +373 -0
- backend/llama.cpp/tools/ui/docs/flows/chat-flow.md +228 -0
- backend/llama.cpp/tools/ui/docs/flows/conversations-flow.md +183 -0
- backend/llama.cpp/tools/ui/docs/flows/data-flow-simplified-model-mode.md +45 -0
- backend/llama.cpp/tools/ui/docs/flows/data-flow-simplified-router-mode.md +77 -0
- backend/llama.cpp/tools/ui/docs/flows/database-flow.md +174 -0
- backend/llama.cpp/tools/ui/docs/flows/mcp-flow.md +226 -0
- backend/llama.cpp/tools/ui/docs/flows/models-flow.md +181 -0
- backend/llama.cpp/tools/ui/docs/flows/server-flow.md +76 -0
- backend/llama.cpp/tools/ui/docs/flows/settings-flow.md +156 -0
- backend/llama.cpp/tools/ui/embed.cpp +306 -0
- backend/llama.cpp/tools/ui/eslint.config.js +59 -0
- backend/llama.cpp/tools/ui/package-lock.json +0 -0
- backend/llama.cpp/tools/ui/package.json +107 -0
- backend/llama.cpp/tools/ui/playwright.config.ts +31 -0
- backend/llama.cpp/tools/ui/pwa-assets-dark.config.ts +27 -0
- backend/llama.cpp/tools/ui/pwa-assets.config.ts +68 -0
- backend/llama.cpp/tools/ui/scripts/dev.sh +61 -0
- backend/llama.cpp/tools/ui/scripts/favicon-colorize.ts +107 -0
- backend/llama.cpp/tools/ui/scripts/git-hooks/install.sh +35 -0
- backend/llama.cpp/tools/ui/scripts/git-hooks/pre-commit.sh +57 -0
- backend/llama.cpp/tools/ui/scripts/git-hooks/pre-push.sh +67 -0
- backend/llama.cpp/tools/ui/scripts/make-icons-circular.js +137 -0
- backend/llama.cpp/tools/ui/scripts/vite-plugin-build-info.ts +41 -0
- backend/llama.cpp/tools/ui/scripts/vite-plugin-relativize-base.ts +61 -0
- backend/llama.cpp/tools/ui/scripts/vite-plugin-splash-screen.ts +115 -0
- backend/llama.cpp/tools/ui/sources.cmake +16 -0
- backend/llama.cpp/tools/ui/src/.gitignore +1 -0
- backend/llama.cpp/tools/ui/src/app.css +200 -0
- backend/llama.cpp/tools/ui/src/app.d.ts +146 -0
- backend/llama.cpp/tools/ui/src/app.html +22 -0
- backend/llama.cpp/tools/ui/src/lib/actions/fade-in-view.svelte.ts +49 -0
- backend/llama.cpp/tools/ui/src/lib/assets/logo.svg +7 -0
- backend/llama.cpp/tools/ui/src/lib/components/app/SKILL.md +11 -0
- backend/llama.cpp/tools/ui/src/lib/components/app/actions/ActionIcon.svelte +81 -0
- backend/llama.cpp/tools/ui/src/lib/components/app/actions/ActionIconCopyToClipboard.svelte +17 -0
- backend/llama.cpp/tools/ui/src/lib/components/app/actions/index.ts +13 -0
- backend/llama.cpp/tools/ui/src/lib/components/app/badges/BadgeInfo.svelte +26 -0
- backend/llama.cpp/tools/ui/src/lib/components/app/badges/BadgesModality.svelte +36 -0
- backend/llama.cpp/tools/ui/src/lib/components/app/badges/index.ts +13 -0
- backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsList.svelte +119 -0
- backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItem.svelte +132 -0
- backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItemMcpPrompt.svelte +41 -0
- backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItemMcpResource.svelte +89 -0
- backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItemThumbnailFile.svelte +186 -0
- backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsList/ChatAttachmentsListItem/ChatAttachmentsListItemThumbnailImage.svelte +65 -0
- backend/llama.cpp/tools/ui/src/lib/components/app/chat/ChatAttachments/ChatAttachmentsPreview.svelte +212 -0
- 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<string, McpServerOverride>
|
| 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<string>
|
| 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}
|