Skip to content

Commit a7a98e0

Browse files
authored
SvelteKit-based WebUI (#14839)
1 parent 8f8f227 commit a7a98e0

File tree

288 files changed

+25752
-11505
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

288 files changed

+25752
-11505
lines changed

.editorconfig

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,11 @@ insert_final_newline = unset
5252
[vendor/miniaudio/miniaudio.h]
5353
trim_trailing_whitespace = unset
5454
insert_final_newline = unset
55+
56+
[tools/server/webui/**]
57+
indent_style = unset
58+
indent_size = unset
59+
end_of_line = unset
60+
charset = unset
61+
trim_trailing_whitespace = unset
62+
insert_final_newline = unset

.github/workflows/server.yml

Lines changed: 195 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -76,51 +76,206 @@ jobs:
7676
run: |
7777
pip install -r tools/server/tests/requirements.txt
7878
79-
# Setup nodejs (to be used for verifying bundled index.html)
80-
- uses: actions/setup-node@v4
79+
webui-setup:
80+
name: WebUI Setup
81+
runs-on: ubuntu-latest
82+
steps:
83+
- name: Checkout code
84+
uses: actions/checkout@v4
8185
with:
82-
node-version: '22.11.0'
86+
fetch-depth: 0
87+
ref: ${{ github.event.inputs.sha || github.event.pull_request.head.sha || github.sha || github.head_ref || github.ref_name }}
8388

84-
- name: WebUI - Install dependencies
85-
id: webui_lint
86-
run: |
87-
cd tools/server/webui
88-
npm ci
89+
- name: Setup Node.js
90+
uses: actions/setup-node@v4
91+
with:
92+
node-version: "22"
93+
cache: "npm"
94+
cache-dependency-path: "tools/server/webui/package-lock.json"
95+
96+
- name: Cache node_modules
97+
uses: actions/cache@v4
98+
id: cache-node-modules
99+
with:
100+
path: tools/server/webui/node_modules
101+
key: ${{ runner.os }}-node-modules-${{ hashFiles('tools/server/webui/package-lock.json') }}
102+
restore-keys: |
103+
${{ runner.os }}-node-modules-
104+
105+
- name: Install dependencies
106+
if: steps.cache-node-modules.outputs.cache-hit != 'true'
107+
run: npm ci
108+
working-directory: tools/server/webui
109+
110+
webui-check:
111+
needs: webui-setup
112+
name: WebUI Check
113+
runs-on: ubuntu-latest
114+
steps:
115+
- name: Checkout code
116+
uses: actions/checkout@v4
117+
with:
118+
fetch-depth: 0
119+
ref: ${{ github.event.inputs.sha || github.event.pull_request.head.sha || github.sha || github.head_ref || github.ref_name }}
120+
121+
- name: Setup Node.js
122+
uses: actions/setup-node@v4
123+
with:
124+
node-version: "22"
125+
126+
- name: Restore node_modules cache
127+
uses: actions/cache@v4
128+
with:
129+
path: tools/server/webui/node_modules
130+
key: ${{ runner.os }}-node-modules-${{ hashFiles('tools/server/webui/package-lock.json') }}
131+
restore-keys: |
132+
${{ runner.os }}-node-modules-
133+
134+
- name: Run type checking
135+
run: npm run check
136+
working-directory: tools/server/webui
137+
138+
- name: Run linting
139+
run: npm run lint
140+
working-directory: tools/server/webui
141+
142+
webui-build:
143+
needs: webui-check
144+
name: WebUI Build
145+
runs-on: ubuntu-latest
146+
steps:
147+
- name: Checkout code
148+
uses: actions/checkout@v4
149+
with:
150+
fetch-depth: 0
151+
ref: ${{ github.event.inputs.sha || github.event.pull_request.head.sha || github.sha || github.head_ref || github.ref_name }}
152+
153+
- name: Setup Node.js
154+
uses: actions/setup-node@v4
155+
with:
156+
node-version: "22"
157+
158+
- name: Restore node_modules cache
159+
uses: actions/cache@v4
160+
with:
161+
path: tools/server/webui/node_modules
162+
key: ${{ runner.os }}-node-modules-${{ hashFiles('tools/server/webui/package-lock.json') }}
163+
restore-keys: |
164+
${{ runner.os }}-node-modules-
165+
166+
- name: Build application
167+
run: npm run build
168+
working-directory: tools/server/webui
169+
170+
webui-tests:
171+
needs: webui-build
172+
name: Run WebUI tests
173+
permissions:
174+
contents: read
175+
176+
runs-on: ubuntu-latest
177+
178+
steps:
179+
- name: Checkout code
180+
uses: actions/checkout@v4
181+
182+
- name: Setup Node.js
183+
uses: actions/setup-node@v4
184+
with:
185+
node-version: "22"
186+
187+
- name: Restore node_modules cache
188+
uses: actions/cache@v4
189+
with:
190+
path: tools/server/webui/node_modules
191+
key: ${{ runner.os }}-node-modules-${{ hashFiles('tools/server/webui/package-lock.json') }}
192+
restore-keys: |
193+
${{ runner.os }}-node-modules-
194+
195+
- name: Install Playwright browsers
196+
run: npx playwright install --with-deps
197+
working-directory: tools/server/webui
198+
199+
- name: Build Storybook
200+
run: npm run build-storybook
201+
working-directory: tools/server/webui
202+
203+
- name: Run Client tests
204+
run: npm run test:client
205+
working-directory: tools/server/webui
89206

90-
- name: WebUI - Check code format
91-
id: webui_format
207+
- name: Run Server tests
208+
run: npm run test:server
209+
working-directory: tools/server/webui
210+
211+
- name: Run UI tests
212+
run: npm run test:ui
213+
working-directory: tools/server/webui
214+
215+
- name: Run E2E tests
216+
run: npm run test:e2e
217+
working-directory: tools/server/webui
218+
219+
server-build:
220+
needs: [webui-tests]
221+
runs-on: ubuntu-latest
222+
223+
strategy:
224+
matrix:
225+
sanitizer: [ADDRESS, UNDEFINED] # THREAD is broken
226+
build_type: [RelWithDebInfo]
227+
include:
228+
- build_type: Release
229+
sanitizer: ""
230+
fail-fast: false # While -DLLAMA_SANITIZE_THREAD=ON is broken
231+
232+
steps:
233+
- name: Dependencies
234+
id: depends
92235
run: |
93-
git config --global --add safe.directory $(realpath .)
94-
cd tools/server/webui
95-
git status
96-
97-
npm run format
98-
git status
99-
modified_files="$(git status -s)"
100-
echo "Modified files: ${modified_files}"
101-
if [ -n "${modified_files}" ]; then
102-
echo "Files do not follow coding style. To fix: npm run format"
103-
echo "${modified_files}"
104-
exit 1
105-
fi
106-
107-
- name: Verify bundled index.html
108-
id: verify_server_index_html
236+
sudo apt-get update
237+
sudo apt-get -y install \
238+
build-essential \
239+
xxd \
240+
git \
241+
cmake \
242+
curl \
243+
wget \
244+
language-pack-en \
245+
libcurl4-openssl-dev
246+
247+
- name: Clone
248+
id: checkout
249+
uses: actions/checkout@v4
250+
with:
251+
fetch-depth: 0
252+
ref: ${{ github.event.inputs.sha || github.event.pull_request.head.sha || github.sha || github.head_ref || github.ref_name }}
253+
254+
- name: Python setup
255+
id: setup_python
256+
uses: actions/setup-python@v5
257+
with:
258+
python-version: '3.11'
259+
260+
- name: Tests dependencies
261+
id: test_dependencies
109262
run: |
110-
git config --global --add safe.directory $(realpath .)
111-
cd tools/server/webui
112-
git status
113-
114-
npm run build
115-
git status
116-
modified_files="$(git status -s)"
117-
echo "Modified files: ${modified_files}"
118-
if [ -n "${modified_files}" ]; then
119-
echo "Repository is dirty or server/webui is not built as expected"
120-
echo "Hint: You may need to follow Web UI build guide in server/README.md"
121-
echo "${modified_files}"
122-
exit 1
123-
fi
263+
pip install -r tools/server/tests/requirements.txt
264+
265+
- name: Setup Node.js for WebUI
266+
uses: actions/setup-node@v4
267+
with:
268+
node-version: "22"
269+
cache: "npm"
270+
cache-dependency-path: "tools/server/webui/package-lock.json"
271+
272+
- name: Install WebUI dependencies
273+
run: npm ci
274+
working-directory: tools/server/webui
275+
276+
- name: Build WebUI
277+
run: npm run build
278+
working-directory: tools/server/webui
124279

125280
- name: Build (no OpenMP)
126281
id: cmake_build_no_openmp

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,7 @@ poetry.toml
148148
/run-vim.sh
149149
/run-chat.sh
150150
.ccache/
151+
152+
# Code Workspace
153+
*.code-workspace
154+
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
trigger: manual
3+
---
4+
5+
#### Tailwind & CSS
6+
7+
- We are using Tailwind v4 which uses oklch colors so we now want to refer to the CSS vars directly, without wrapping it with any color function like `hsla/hsl`, `rgba` etc.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
trigger: manual
3+
---
4+
5+
# Coding rules
6+
7+
## Svelte & SvelteKit
8+
9+
### Services vs Stores Separation Pattern
10+
11+
#### `lib/services/` - Pure Business Logic
12+
13+
- **Purpose**: Stateless business logic and external communication
14+
- **Contains**:
15+
- API calls to external services (ApiService)
16+
- Pure business logic functions (ChatService, etc.)
17+
- **Rules**:
18+
- NO Svelte runes ($state, $derived, $effect)
19+
- NO reactive state management
20+
- Pure functions and classes only
21+
- Can import types but not stores
22+
- Focus on "how" - implementation details
23+
24+
#### `lib/stores/` - Reactive State Management
25+
26+
- **Purpose**: Svelte-specific reactive state with runes
27+
- **Contains**:
28+
- Reactive state classes with $state, $derived, $effect
29+
- Database operations (DatabaseStore)
30+
- UI-focused state management
31+
- Store orchestration logic
32+
- **Rules**:
33+
- USE Svelte runes for reactivity
34+
- Import and use services for business logic
35+
- NO direct database operations
36+
- NO direct API calls (use services)
37+
- Focus on "what" - reactive state for UI
38+
39+
#### Enforcement
40+
41+
- Services should be testable without Svelte
42+
- Stores should leverage Svelte's reactivity system
43+
- Clear separation: services handle data, stores handle state
44+
- Services can be reused across multiple stores
45+
46+
#### Misc
47+
48+
- Always use `let` for $derived state variables

.windsurf/rules/tests.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
trigger: manual
3+
---
4+
5+
# Automated Tests
6+
7+
## General rules
8+
9+
- NEVER include any test code in the production code - we should always have it in a separate dedicated files
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
trigger: manual
3+
---
4+
5+
## TypeScript
6+
7+
- Add JSDocs for functions

tools/server/public/index.html.gz

-1.02 MB
Binary file not shown.

tools/server/server.cpp

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5261,6 +5261,42 @@ int main(int argc, char ** argv) {
52615261
svr->Get (params.api_prefix + "/slots", handle_slots);
52625262
svr->Post(params.api_prefix + "/slots/:id_slot", handle_slots_action);
52635263

5264+
// SPA fallback route - serve index.html for any route that doesn't match API endpoints
5265+
// This enables client-side routing for dynamic routes like /chat/[id]
5266+
if (params.webui && params.public_path.empty()) {
5267+
// Only add fallback when using embedded static files
5268+
svr->Get(".*", [](const httplib::Request & req, httplib::Response & res) {
5269+
// Skip API routes - they should have been handled above
5270+
if (req.path.find("/v1/") != std::string::npos ||
5271+
req.path.find("/health") != std::string::npos ||
5272+
req.path.find("/metrics") != std::string::npos ||
5273+
req.path.find("/props") != std::string::npos ||
5274+
req.path.find("/models") != std::string::npos ||
5275+
req.path.find("/api/tags") != std::string::npos ||
5276+
req.path.find("/completions") != std::string::npos ||
5277+
req.path.find("/chat/completions") != std::string::npos ||
5278+
req.path.find("/embeddings") != std::string::npos ||
5279+
req.path.find("/tokenize") != std::string::npos ||
5280+
req.path.find("/detokenize") != std::string::npos ||
5281+
req.path.find("/lora-adapters") != std::string::npos ||
5282+
req.path.find("/slots") != std::string::npos) {
5283+
return false; // Let other handlers process API routes
5284+
}
5285+
5286+
// Serve index.html for all other routes (SPA fallback)
5287+
if (req.get_header_value("Accept-Encoding").find("gzip") == std::string::npos) {
5288+
res.set_content("Error: gzip is not supported by this browser", "text/plain");
5289+
} else {
5290+
res.set_header("Content-Encoding", "gzip");
5291+
// COEP and COOP headers, required by pyodide (python interpreter)
5292+
res.set_header("Cross-Origin-Embedder-Policy", "require-corp");
5293+
res.set_header("Cross-Origin-Opener-Policy", "same-origin");
5294+
res.set_content(reinterpret_cast<const char*>(index_html_gz), index_html_gz_len, "text/html; charset=utf-8");
5295+
}
5296+
return false;
5297+
});
5298+
}
5299+
52645300
//
52655301
// Start the server
52665302
//

tools/server/tests/unit/test_basic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def test_no_webui():
9292
url = f"http://{server.server_host}:{server.server_port}"
9393
res = requests.get(url)
9494
assert res.status_code == 200
95-
assert "<html>" in res.text
95+
assert "<!doctype html>" in res.text
9696
server.stop()
9797

9898
# with --no-webui

0 commit comments

Comments
 (0)