ryanhlewis commited on
Commit
de46f8e
·
verified ·
1 Parent(s): 44db5a5

Publish static Gemma WebGPU app

Browse files
README.md CHANGED
@@ -1,10 +1,78 @@
1
  ---
2
- title: Gemma4 Webgpu
3
- emoji: 🐨
4
- colorFrom: gray
5
- colorTo: yellow
6
  sdk: static
7
- pinned: false
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Gemma 4 WebGPU
 
 
 
3
  sdk: static
4
+ app_file: dist/index.html
5
+ app_build_command: npm install && npm run build
6
+ models:
7
+ - google/gemma-4-E2B-it-qat-q4_0-gguf
8
+ tags:
9
+ - webgpu
10
+ - gguf
11
+ - llama.cpp
12
+ - wllama
13
+ - gemma
14
+ custom_headers:
15
+ cross-origin-embedder-policy: require-corp
16
+ cross-origin-opener-policy: same-origin
17
+ cross-origin-resource-policy: cross-origin
18
  ---
19
 
20
+ # Gemma 4 WebGPU
21
+
22
+ Static browser chat app for `google/gemma-4-E2B-it-qat-q4_0-gguf`.
23
+
24
+ The Space serves only HTML, CSS, JavaScript, and Wllama WASM assets. The GGUF files are downloaded by the browser and inference runs locally through Wllama / llama.cpp WebGPU.
25
+
26
+ ## Model Assets
27
+
28
+ The official Google repository contains:
29
+
30
+ | File | Size | Use |
31
+ | --- | ---: | --- |
32
+ | `gemma-4-E2B_q4_0-it.gguf` | 3.35 GB | language model |
33
+ | `gemma-4-E2B-it-mmproj.gguf` | 987 MB | vision projector |
34
+
35
+ Wllama supports split GGUF loading, so the app defaults to a public mirror containing split shards of the 3.35 GB model file plus the mmproj file. This keeps each browser-fetched LLM shard under the per-file browser/WASM limit while preserving the same model weights.
36
+
37
+ ## Local Development
38
+
39
+ ```powershell
40
+ npm install
41
+ npm run dev
42
+ ```
43
+
44
+ Open:
45
+
46
+ ```text
47
+ http://127.0.0.1:8030/
48
+ ```
49
+
50
+ Cross-origin isolation headers are enabled in Vite because Wllama uses browser features such as WebGPU, WebAssembly, OPFS, and SharedArrayBuffer when available.
51
+
52
+ ## URL Overrides
53
+
54
+ The app accepts:
55
+
56
+ ```text
57
+ ?modelUrl=https://huggingface.co/<repo>/resolve/main/model-00001-of-00005.gguf
58
+ ?mmprojUrl=https://huggingface.co/<repo>/resolve/main/gemma-4-E2B-it-mmproj.gguf
59
+ ?ctx=4096&threads=4&maxTokens=384
60
+ ```
61
+
62
+ For split models, pass the first shard URL. Wllama discovers the remaining shards from the `-00001-of-00005.gguf` naming pattern.
63
+
64
+ ## Producing Split Shards
65
+
66
+ Use the helper script after installing Docker and Hugging Face CLI auth:
67
+
68
+ ```powershell
69
+ .\scripts\split-gemma-gguf.ps1
70
+ ```
71
+
72
+ It downloads the official Google GGUF, uses the llama.cpp Docker image to run `llama-gguf-split`, and uploads the split shards plus the mmproj file to a public Hugging Face model repo.
73
+
74
+ ## References
75
+
76
+ - Wllama supports direct browser inference, WebGPU, multimodal inputs, and split GGUF loading.
77
+ - Hugging Face Static Spaces support custom COOP/COEP/CORP headers through README metadata.
78
+ - `google/gemma-4-E2B-it-qat-q4_0-gguf` is Apache-2.0 licensed.
dist/assets/index-CsOkhJZm.js ADDED
The diff for this file is too large to render. See raw diff
 
dist/assets/index-CyVSHnDs.css ADDED
@@ -0,0 +1 @@
 
 
1
+ :root{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;color:#f4f0e8;font-synthesis:none;text-rendering:optimizelegibility;-webkit-font-smoothing:antialiased;background:#10100f;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;line-height:1.4}*{box-sizing:border-box}body{background:linear-gradient(#39453a38,#0000 360px),#10100f;min-width:320px;min-height:100vh;margin:0}button,input,textarea,select{font:inherit}button{border:0}.app{width:min(1180px,100vw - 32px);margin:0 auto;padding:24px 0 36px}.topbar{justify-content:space-between;align-items:center;gap:16px;margin-bottom:20px;display:flex}.brand{align-items:center;gap:12px;display:flex}.mark{color:#15170f;background:#d7ff7a;border:1px solid #f4f0e829;border-radius:8px;place-items:center;width:38px;height:38px;font-weight:800;display:grid}h1,h2,h3,p{margin:0}h1{letter-spacing:0;font-size:19px;font-weight:720}.subtle{color:#a7a093;font-size:13px}.actions{align-items:center;gap:8px;display:flex}.icon-button,.button{color:#f4f0e8;cursor:pointer;background:#24231f;border:1px solid #f4f0e81f;border-radius:8px;justify-content:center;align-items:center;gap:8px;min-height:38px;padding:0 14px;display:inline-flex}.icon-button{width:38px;padding:0}.button.primary{color:#15170f;background:#d7ff7a;border-color:#d7ff7a;font-weight:740}.button.danger{color:#ffd9d9;background:#3a2525;border-color:#6e3737}.button:disabled,.icon-button:disabled{cursor:not-allowed;opacity:.55}.shell{grid-template-columns:minmax(0,1fr) 340px;gap:18px;display:grid}.panel{background:#1a1916f0;border:1px solid #f4f0e81f;border-radius:8px}.chat-panel{grid-template-rows:auto minmax(0,1fr) auto;min-height:690px;display:grid}.status-strip{background:#f4f0e814;border-bottom:1px solid #f4f0e81a;grid-template-columns:repeat(4,minmax(0,1fr));gap:1px;display:grid}.metric{background:#1a1916;min-width:0;padding:12px 14px}.metric span{color:#a7a093;text-transform:uppercase;font-size:11px;display:block}.metric strong{text-overflow:ellipsis;white-space:nowrap;margin-top:2px;font-size:14px;font-weight:680;display:block;overflow:hidden}.messages{padding:18px;overflow:auto}.message-list{flex-direction:column;gap:14px;display:flex}.message{white-space:pre-wrap;overflow-wrap:anywhere;border:1px solid #f4f0e81a;border-radius:8px;max-width:86%;padding:13px 14px}.message.user{background:#243124;align-self:flex-end}.message.assistant{background:#22211d;align-self:flex-start}.empty{color:#a7a093;text-align:center;place-items:center;min-height:340px;display:grid}.composer{border-top:1px solid #f4f0e81a;gap:10px;padding:14px;display:grid}textarea,input,select{color:#f4f0e8;background:#121210;border:1px solid #f4f0e81f;border-radius:8px;outline:none;width:100%}textarea:focus,input:focus,select:focus{border-color:#d7ff7acc;box-shadow:0 0 0 3px #d7ff7a1f}textarea{resize:vertical;min-height:96px;padding:12px}input,select{min-height:38px;padding:0 10px}.composer-row{justify-content:space-between;align-items:center;gap:10px;display:flex}.file-chip{color:#a7a093;flex:1;align-items:center;gap:8px;min-width:0;font-size:13px;display:flex}.file-chip input{display:none}.chip-button{color:#f4f0e8;cursor:pointer;background:#24231f;border-radius:8px;align-items:center;gap:7px;min-height:34px;padding:0 10px;display:inline-flex}.file-name{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.side{flex-direction:column;gap:14px;display:flex}.card{padding:16px}.card h2{margin-bottom:10px;font-size:15px;font-weight:720}.field{gap:6px;margin-top:12px;display:grid}.field label{color:#c9c1b2;font-size:12px;font-weight:650}.field-grid{grid-template-columns:1fr 1fr;gap:10px;display:grid}.progress{background:#11110f;border-radius:999px;height:10px;overflow:hidden}.bar{border-radius:inherit;background:#d7ff7a;width:0%;height:100%;transition:width .18s}.log{color:#c9c1b2;white-space:pre-wrap;background:#11110f;border-radius:8px;min-height:106px;max-height:190px;padding:10px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;overflow:auto}.pill-row{flex-wrap:wrap;gap:8px;margin-top:12px;display:flex}.pill{color:#c9c1b2;border:1px solid #f4f0e81f;border-radius:999px;padding:5px 9px;font-size:12px}.source{overflow-wrap:anywhere}@media (max-width:900px){.app{width:min(100vw - 20px,720px);padding-top:14px}.shell{grid-template-columns:1fr}.status-strip{grid-template-columns:1fr 1fr}.message{max-width:94%}.topbar{flex-direction:column;align-items:flex-start}}
dist/assets/wllama-ByHMPmd7.wasm ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a3e827b9fc3536fd1b061eee8b130e46f6366d25b062438a791d8b17b2a72a30
3
+ size 7308965
dist/index.html ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta
7
+ http-equiv="origin-trial"
8
+ content=""
9
+ />
10
+ <title>Gemma 4 WebGPU</title>
11
+ <link rel="preconnect" href="https://huggingface.co" />
12
+ <link rel="preconnect" href="https://cdn.jsdelivr.net" />
13
+ <link
14
+ href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght@400;500&display=swap"
15
+ rel="stylesheet"
16
+ />
17
+ <script type="module" crossorigin src="./assets/index-CsOkhJZm.js"></script>
18
+ <link rel="stylesheet" crossorigin href="./assets/index-CyVSHnDs.css">
19
+ </head>
20
+ <body>
21
+ <div id="app"></div>
22
+ </body>
23
+ </html>
index.html CHANGED
@@ -1,19 +1,22 @@
1
  <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
 
 
 
19
  </html>
 
1
  <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta
7
+ http-equiv="origin-trial"
8
+ content=""
9
+ />
10
+ <title>Gemma 4 WebGPU</title>
11
+ <link rel="preconnect" href="https://huggingface.co" />
12
+ <link rel="preconnect" href="https://cdn.jsdelivr.net" />
13
+ <link
14
+ href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght@400;500&display=swap"
15
+ rel="stylesheet"
16
+ />
17
+ </head>
18
+ <body>
19
+ <div id="app"></div>
20
+ <script type="module" src="/src/main.ts"></script>
21
+ </body>
22
  </html>
package-lock.json ADDED
@@ -0,0 +1,848 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "gemma4-webgpu",
3
+ "version": "1.0.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "gemma4-webgpu",
9
+ "version": "1.0.0",
10
+ "license": "Apache-2.0",
11
+ "dependencies": {
12
+ "@wllama/wllama": "^3.4.1",
13
+ "typescript": "^6.0.3",
14
+ "vite": "^8.0.16"
15
+ }
16
+ },
17
+ "node_modules/@emnapi/core": {
18
+ "version": "1.10.0",
19
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
20
+ "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
21
+ "license": "MIT",
22
+ "optional": true,
23
+ "dependencies": {
24
+ "@emnapi/wasi-threads": "1.2.1",
25
+ "tslib": "^2.4.0"
26
+ }
27
+ },
28
+ "node_modules/@emnapi/runtime": {
29
+ "version": "1.10.0",
30
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
31
+ "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
32
+ "license": "MIT",
33
+ "optional": true,
34
+ "dependencies": {
35
+ "tslib": "^2.4.0"
36
+ }
37
+ },
38
+ "node_modules/@emnapi/wasi-threads": {
39
+ "version": "1.2.1",
40
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
41
+ "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
42
+ "license": "MIT",
43
+ "optional": true,
44
+ "dependencies": {
45
+ "tslib": "^2.4.0"
46
+ }
47
+ },
48
+ "node_modules/@napi-rs/wasm-runtime": {
49
+ "version": "1.1.4",
50
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
51
+ "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
52
+ "license": "MIT",
53
+ "optional": true,
54
+ "dependencies": {
55
+ "@tybys/wasm-util": "^0.10.1"
56
+ },
57
+ "funding": {
58
+ "type": "github",
59
+ "url": "https://github.com/sponsors/Brooooooklyn"
60
+ },
61
+ "peerDependencies": {
62
+ "@emnapi/core": "^1.7.1",
63
+ "@emnapi/runtime": "^1.7.1"
64
+ }
65
+ },
66
+ "node_modules/@oxc-project/types": {
67
+ "version": "0.133.0",
68
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz",
69
+ "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==",
70
+ "license": "MIT",
71
+ "funding": {
72
+ "url": "https://github.com/sponsors/Boshen"
73
+ }
74
+ },
75
+ "node_modules/@rolldown/binding-android-arm64": {
76
+ "version": "1.0.3",
77
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz",
78
+ "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==",
79
+ "cpu": [
80
+ "arm64"
81
+ ],
82
+ "license": "MIT",
83
+ "optional": true,
84
+ "os": [
85
+ "android"
86
+ ],
87
+ "engines": {
88
+ "node": "^20.19.0 || >=22.12.0"
89
+ }
90
+ },
91
+ "node_modules/@rolldown/binding-darwin-arm64": {
92
+ "version": "1.0.3",
93
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz",
94
+ "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==",
95
+ "cpu": [
96
+ "arm64"
97
+ ],
98
+ "license": "MIT",
99
+ "optional": true,
100
+ "os": [
101
+ "darwin"
102
+ ],
103
+ "engines": {
104
+ "node": "^20.19.0 || >=22.12.0"
105
+ }
106
+ },
107
+ "node_modules/@rolldown/binding-darwin-x64": {
108
+ "version": "1.0.3",
109
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz",
110
+ "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==",
111
+ "cpu": [
112
+ "x64"
113
+ ],
114
+ "license": "MIT",
115
+ "optional": true,
116
+ "os": [
117
+ "darwin"
118
+ ],
119
+ "engines": {
120
+ "node": "^20.19.0 || >=22.12.0"
121
+ }
122
+ },
123
+ "node_modules/@rolldown/binding-freebsd-x64": {
124
+ "version": "1.0.3",
125
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz",
126
+ "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==",
127
+ "cpu": [
128
+ "x64"
129
+ ],
130
+ "license": "MIT",
131
+ "optional": true,
132
+ "os": [
133
+ "freebsd"
134
+ ],
135
+ "engines": {
136
+ "node": "^20.19.0 || >=22.12.0"
137
+ }
138
+ },
139
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
140
+ "version": "1.0.3",
141
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz",
142
+ "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==",
143
+ "cpu": [
144
+ "arm"
145
+ ],
146
+ "license": "MIT",
147
+ "optional": true,
148
+ "os": [
149
+ "linux"
150
+ ],
151
+ "engines": {
152
+ "node": "^20.19.0 || >=22.12.0"
153
+ }
154
+ },
155
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
156
+ "version": "1.0.3",
157
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz",
158
+ "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==",
159
+ "cpu": [
160
+ "arm64"
161
+ ],
162
+ "license": "MIT",
163
+ "optional": true,
164
+ "os": [
165
+ "linux"
166
+ ],
167
+ "engines": {
168
+ "node": "^20.19.0 || >=22.12.0"
169
+ }
170
+ },
171
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
172
+ "version": "1.0.3",
173
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz",
174
+ "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==",
175
+ "cpu": [
176
+ "arm64"
177
+ ],
178
+ "license": "MIT",
179
+ "optional": true,
180
+ "os": [
181
+ "linux"
182
+ ],
183
+ "engines": {
184
+ "node": "^20.19.0 || >=22.12.0"
185
+ }
186
+ },
187
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
188
+ "version": "1.0.3",
189
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz",
190
+ "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==",
191
+ "cpu": [
192
+ "ppc64"
193
+ ],
194
+ "license": "MIT",
195
+ "optional": true,
196
+ "os": [
197
+ "linux"
198
+ ],
199
+ "engines": {
200
+ "node": "^20.19.0 || >=22.12.0"
201
+ }
202
+ },
203
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
204
+ "version": "1.0.3",
205
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz",
206
+ "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==",
207
+ "cpu": [
208
+ "s390x"
209
+ ],
210
+ "license": "MIT",
211
+ "optional": true,
212
+ "os": [
213
+ "linux"
214
+ ],
215
+ "engines": {
216
+ "node": "^20.19.0 || >=22.12.0"
217
+ }
218
+ },
219
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
220
+ "version": "1.0.3",
221
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz",
222
+ "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==",
223
+ "cpu": [
224
+ "x64"
225
+ ],
226
+ "license": "MIT",
227
+ "optional": true,
228
+ "os": [
229
+ "linux"
230
+ ],
231
+ "engines": {
232
+ "node": "^20.19.0 || >=22.12.0"
233
+ }
234
+ },
235
+ "node_modules/@rolldown/binding-linux-x64-musl": {
236
+ "version": "1.0.3",
237
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz",
238
+ "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==",
239
+ "cpu": [
240
+ "x64"
241
+ ],
242
+ "license": "MIT",
243
+ "optional": true,
244
+ "os": [
245
+ "linux"
246
+ ],
247
+ "engines": {
248
+ "node": "^20.19.0 || >=22.12.0"
249
+ }
250
+ },
251
+ "node_modules/@rolldown/binding-openharmony-arm64": {
252
+ "version": "1.0.3",
253
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz",
254
+ "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==",
255
+ "cpu": [
256
+ "arm64"
257
+ ],
258
+ "license": "MIT",
259
+ "optional": true,
260
+ "os": [
261
+ "openharmony"
262
+ ],
263
+ "engines": {
264
+ "node": "^20.19.0 || >=22.12.0"
265
+ }
266
+ },
267
+ "node_modules/@rolldown/binding-wasm32-wasi": {
268
+ "version": "1.0.3",
269
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz",
270
+ "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==",
271
+ "cpu": [
272
+ "wasm32"
273
+ ],
274
+ "license": "MIT",
275
+ "optional": true,
276
+ "dependencies": {
277
+ "@emnapi/core": "1.10.0",
278
+ "@emnapi/runtime": "1.10.0",
279
+ "@napi-rs/wasm-runtime": "^1.1.4"
280
+ },
281
+ "engines": {
282
+ "node": "^20.19.0 || >=22.12.0"
283
+ }
284
+ },
285
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
286
+ "version": "1.0.3",
287
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz",
288
+ "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==",
289
+ "cpu": [
290
+ "arm64"
291
+ ],
292
+ "license": "MIT",
293
+ "optional": true,
294
+ "os": [
295
+ "win32"
296
+ ],
297
+ "engines": {
298
+ "node": "^20.19.0 || >=22.12.0"
299
+ }
300
+ },
301
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
302
+ "version": "1.0.3",
303
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz",
304
+ "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==",
305
+ "cpu": [
306
+ "x64"
307
+ ],
308
+ "license": "MIT",
309
+ "optional": true,
310
+ "os": [
311
+ "win32"
312
+ ],
313
+ "engines": {
314
+ "node": "^20.19.0 || >=22.12.0"
315
+ }
316
+ },
317
+ "node_modules/@rolldown/pluginutils": {
318
+ "version": "1.0.1",
319
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
320
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
321
+ "license": "MIT"
322
+ },
323
+ "node_modules/@tybys/wasm-util": {
324
+ "version": "0.10.2",
325
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
326
+ "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==",
327
+ "license": "MIT",
328
+ "optional": true,
329
+ "dependencies": {
330
+ "tslib": "^2.4.0"
331
+ }
332
+ },
333
+ "node_modules/@wllama/wllama": {
334
+ "version": "3.4.1",
335
+ "resolved": "https://registry.npmjs.org/@wllama/wllama/-/wllama-3.4.1.tgz",
336
+ "integrity": "sha512-izq3CLVsIIpFoi4lI5/yvpM1tbzTBnDU8WuVi6I1q1oQgImSXSGF8eZo8WwDNBgbmhVJpcPGyw8cg24jKk0sbg==",
337
+ "license": "MIT"
338
+ },
339
+ "node_modules/detect-libc": {
340
+ "version": "2.1.2",
341
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
342
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
343
+ "license": "Apache-2.0",
344
+ "engines": {
345
+ "node": ">=8"
346
+ }
347
+ },
348
+ "node_modules/fdir": {
349
+ "version": "6.5.0",
350
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
351
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
352
+ "license": "MIT",
353
+ "engines": {
354
+ "node": ">=12.0.0"
355
+ },
356
+ "peerDependencies": {
357
+ "picomatch": "^3 || ^4"
358
+ },
359
+ "peerDependenciesMeta": {
360
+ "picomatch": {
361
+ "optional": true
362
+ }
363
+ }
364
+ },
365
+ "node_modules/fsevents": {
366
+ "version": "2.3.3",
367
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
368
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
369
+ "hasInstallScript": true,
370
+ "license": "MIT",
371
+ "optional": true,
372
+ "os": [
373
+ "darwin"
374
+ ],
375
+ "engines": {
376
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
377
+ }
378
+ },
379
+ "node_modules/lightningcss": {
380
+ "version": "1.32.0",
381
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
382
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
383
+ "license": "MPL-2.0",
384
+ "dependencies": {
385
+ "detect-libc": "^2.0.3"
386
+ },
387
+ "engines": {
388
+ "node": ">= 12.0.0"
389
+ },
390
+ "funding": {
391
+ "type": "opencollective",
392
+ "url": "https://opencollective.com/parcel"
393
+ },
394
+ "optionalDependencies": {
395
+ "lightningcss-android-arm64": "1.32.0",
396
+ "lightningcss-darwin-arm64": "1.32.0",
397
+ "lightningcss-darwin-x64": "1.32.0",
398
+ "lightningcss-freebsd-x64": "1.32.0",
399
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
400
+ "lightningcss-linux-arm64-gnu": "1.32.0",
401
+ "lightningcss-linux-arm64-musl": "1.32.0",
402
+ "lightningcss-linux-x64-gnu": "1.32.0",
403
+ "lightningcss-linux-x64-musl": "1.32.0",
404
+ "lightningcss-win32-arm64-msvc": "1.32.0",
405
+ "lightningcss-win32-x64-msvc": "1.32.0"
406
+ }
407
+ },
408
+ "node_modules/lightningcss-android-arm64": {
409
+ "version": "1.32.0",
410
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
411
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
412
+ "cpu": [
413
+ "arm64"
414
+ ],
415
+ "license": "MPL-2.0",
416
+ "optional": true,
417
+ "os": [
418
+ "android"
419
+ ],
420
+ "engines": {
421
+ "node": ">= 12.0.0"
422
+ },
423
+ "funding": {
424
+ "type": "opencollective",
425
+ "url": "https://opencollective.com/parcel"
426
+ }
427
+ },
428
+ "node_modules/lightningcss-darwin-arm64": {
429
+ "version": "1.32.0",
430
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
431
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
432
+ "cpu": [
433
+ "arm64"
434
+ ],
435
+ "license": "MPL-2.0",
436
+ "optional": true,
437
+ "os": [
438
+ "darwin"
439
+ ],
440
+ "engines": {
441
+ "node": ">= 12.0.0"
442
+ },
443
+ "funding": {
444
+ "type": "opencollective",
445
+ "url": "https://opencollective.com/parcel"
446
+ }
447
+ },
448
+ "node_modules/lightningcss-darwin-x64": {
449
+ "version": "1.32.0",
450
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
451
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
452
+ "cpu": [
453
+ "x64"
454
+ ],
455
+ "license": "MPL-2.0",
456
+ "optional": true,
457
+ "os": [
458
+ "darwin"
459
+ ],
460
+ "engines": {
461
+ "node": ">= 12.0.0"
462
+ },
463
+ "funding": {
464
+ "type": "opencollective",
465
+ "url": "https://opencollective.com/parcel"
466
+ }
467
+ },
468
+ "node_modules/lightningcss-freebsd-x64": {
469
+ "version": "1.32.0",
470
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
471
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
472
+ "cpu": [
473
+ "x64"
474
+ ],
475
+ "license": "MPL-2.0",
476
+ "optional": true,
477
+ "os": [
478
+ "freebsd"
479
+ ],
480
+ "engines": {
481
+ "node": ">= 12.0.0"
482
+ },
483
+ "funding": {
484
+ "type": "opencollective",
485
+ "url": "https://opencollective.com/parcel"
486
+ }
487
+ },
488
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
489
+ "version": "1.32.0",
490
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
491
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
492
+ "cpu": [
493
+ "arm"
494
+ ],
495
+ "license": "MPL-2.0",
496
+ "optional": true,
497
+ "os": [
498
+ "linux"
499
+ ],
500
+ "engines": {
501
+ "node": ">= 12.0.0"
502
+ },
503
+ "funding": {
504
+ "type": "opencollective",
505
+ "url": "https://opencollective.com/parcel"
506
+ }
507
+ },
508
+ "node_modules/lightningcss-linux-arm64-gnu": {
509
+ "version": "1.32.0",
510
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
511
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
512
+ "cpu": [
513
+ "arm64"
514
+ ],
515
+ "license": "MPL-2.0",
516
+ "optional": true,
517
+ "os": [
518
+ "linux"
519
+ ],
520
+ "engines": {
521
+ "node": ">= 12.0.0"
522
+ },
523
+ "funding": {
524
+ "type": "opencollective",
525
+ "url": "https://opencollective.com/parcel"
526
+ }
527
+ },
528
+ "node_modules/lightningcss-linux-arm64-musl": {
529
+ "version": "1.32.0",
530
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
531
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
532
+ "cpu": [
533
+ "arm64"
534
+ ],
535
+ "license": "MPL-2.0",
536
+ "optional": true,
537
+ "os": [
538
+ "linux"
539
+ ],
540
+ "engines": {
541
+ "node": ">= 12.0.0"
542
+ },
543
+ "funding": {
544
+ "type": "opencollective",
545
+ "url": "https://opencollective.com/parcel"
546
+ }
547
+ },
548
+ "node_modules/lightningcss-linux-x64-gnu": {
549
+ "version": "1.32.0",
550
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
551
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
552
+ "cpu": [
553
+ "x64"
554
+ ],
555
+ "license": "MPL-2.0",
556
+ "optional": true,
557
+ "os": [
558
+ "linux"
559
+ ],
560
+ "engines": {
561
+ "node": ">= 12.0.0"
562
+ },
563
+ "funding": {
564
+ "type": "opencollective",
565
+ "url": "https://opencollective.com/parcel"
566
+ }
567
+ },
568
+ "node_modules/lightningcss-linux-x64-musl": {
569
+ "version": "1.32.0",
570
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
571
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
572
+ "cpu": [
573
+ "x64"
574
+ ],
575
+ "license": "MPL-2.0",
576
+ "optional": true,
577
+ "os": [
578
+ "linux"
579
+ ],
580
+ "engines": {
581
+ "node": ">= 12.0.0"
582
+ },
583
+ "funding": {
584
+ "type": "opencollective",
585
+ "url": "https://opencollective.com/parcel"
586
+ }
587
+ },
588
+ "node_modules/lightningcss-win32-arm64-msvc": {
589
+ "version": "1.32.0",
590
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
591
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
592
+ "cpu": [
593
+ "arm64"
594
+ ],
595
+ "license": "MPL-2.0",
596
+ "optional": true,
597
+ "os": [
598
+ "win32"
599
+ ],
600
+ "engines": {
601
+ "node": ">= 12.0.0"
602
+ },
603
+ "funding": {
604
+ "type": "opencollective",
605
+ "url": "https://opencollective.com/parcel"
606
+ }
607
+ },
608
+ "node_modules/lightningcss-win32-x64-msvc": {
609
+ "version": "1.32.0",
610
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
611
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
612
+ "cpu": [
613
+ "x64"
614
+ ],
615
+ "license": "MPL-2.0",
616
+ "optional": true,
617
+ "os": [
618
+ "win32"
619
+ ],
620
+ "engines": {
621
+ "node": ">= 12.0.0"
622
+ },
623
+ "funding": {
624
+ "type": "opencollective",
625
+ "url": "https://opencollective.com/parcel"
626
+ }
627
+ },
628
+ "node_modules/nanoid": {
629
+ "version": "3.3.12",
630
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
631
+ "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
632
+ "funding": [
633
+ {
634
+ "type": "github",
635
+ "url": "https://github.com/sponsors/ai"
636
+ }
637
+ ],
638
+ "license": "MIT",
639
+ "bin": {
640
+ "nanoid": "bin/nanoid.cjs"
641
+ },
642
+ "engines": {
643
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
644
+ }
645
+ },
646
+ "node_modules/picocolors": {
647
+ "version": "1.1.1",
648
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
649
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
650
+ "license": "ISC"
651
+ },
652
+ "node_modules/picomatch": {
653
+ "version": "4.0.4",
654
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
655
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
656
+ "license": "MIT",
657
+ "engines": {
658
+ "node": ">=12"
659
+ },
660
+ "funding": {
661
+ "url": "https://github.com/sponsors/jonschlinkert"
662
+ }
663
+ },
664
+ "node_modules/postcss": {
665
+ "version": "8.5.15",
666
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
667
+ "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
668
+ "funding": [
669
+ {
670
+ "type": "opencollective",
671
+ "url": "https://opencollective.com/postcss/"
672
+ },
673
+ {
674
+ "type": "tidelift",
675
+ "url": "https://tidelift.com/funding/github/npm/postcss"
676
+ },
677
+ {
678
+ "type": "github",
679
+ "url": "https://github.com/sponsors/ai"
680
+ }
681
+ ],
682
+ "license": "MIT",
683
+ "dependencies": {
684
+ "nanoid": "^3.3.12",
685
+ "picocolors": "^1.1.1",
686
+ "source-map-js": "^1.2.1"
687
+ },
688
+ "engines": {
689
+ "node": "^10 || ^12 || >=14"
690
+ }
691
+ },
692
+ "node_modules/rolldown": {
693
+ "version": "1.0.3",
694
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz",
695
+ "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==",
696
+ "license": "MIT",
697
+ "dependencies": {
698
+ "@oxc-project/types": "=0.133.0",
699
+ "@rolldown/pluginutils": "^1.0.0"
700
+ },
701
+ "bin": {
702
+ "rolldown": "bin/cli.mjs"
703
+ },
704
+ "engines": {
705
+ "node": "^20.19.0 || >=22.12.0"
706
+ },
707
+ "optionalDependencies": {
708
+ "@rolldown/binding-android-arm64": "1.0.3",
709
+ "@rolldown/binding-darwin-arm64": "1.0.3",
710
+ "@rolldown/binding-darwin-x64": "1.0.3",
711
+ "@rolldown/binding-freebsd-x64": "1.0.3",
712
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.3",
713
+ "@rolldown/binding-linux-arm64-gnu": "1.0.3",
714
+ "@rolldown/binding-linux-arm64-musl": "1.0.3",
715
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.3",
716
+ "@rolldown/binding-linux-s390x-gnu": "1.0.3",
717
+ "@rolldown/binding-linux-x64-gnu": "1.0.3",
718
+ "@rolldown/binding-linux-x64-musl": "1.0.3",
719
+ "@rolldown/binding-openharmony-arm64": "1.0.3",
720
+ "@rolldown/binding-wasm32-wasi": "1.0.3",
721
+ "@rolldown/binding-win32-arm64-msvc": "1.0.3",
722
+ "@rolldown/binding-win32-x64-msvc": "1.0.3"
723
+ }
724
+ },
725
+ "node_modules/source-map-js": {
726
+ "version": "1.2.1",
727
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
728
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
729
+ "license": "BSD-3-Clause",
730
+ "engines": {
731
+ "node": ">=0.10.0"
732
+ }
733
+ },
734
+ "node_modules/tinyglobby": {
735
+ "version": "0.2.17",
736
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
737
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
738
+ "license": "MIT",
739
+ "dependencies": {
740
+ "fdir": "^6.5.0",
741
+ "picomatch": "^4.0.4"
742
+ },
743
+ "engines": {
744
+ "node": ">=12.0.0"
745
+ },
746
+ "funding": {
747
+ "url": "https://github.com/sponsors/SuperchupuDev"
748
+ }
749
+ },
750
+ "node_modules/tslib": {
751
+ "version": "2.8.1",
752
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
753
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
754
+ "license": "0BSD",
755
+ "optional": true
756
+ },
757
+ "node_modules/typescript": {
758
+ "version": "6.0.3",
759
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
760
+ "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
761
+ "license": "Apache-2.0",
762
+ "bin": {
763
+ "tsc": "bin/tsc",
764
+ "tsserver": "bin/tsserver"
765
+ },
766
+ "engines": {
767
+ "node": ">=14.17"
768
+ }
769
+ },
770
+ "node_modules/vite": {
771
+ "version": "8.0.16",
772
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz",
773
+ "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==",
774
+ "license": "MIT",
775
+ "dependencies": {
776
+ "lightningcss": "^1.32.0",
777
+ "picomatch": "^4.0.4",
778
+ "postcss": "^8.5.15",
779
+ "rolldown": "1.0.3",
780
+ "tinyglobby": "^0.2.17"
781
+ },
782
+ "bin": {
783
+ "vite": "bin/vite.js"
784
+ },
785
+ "engines": {
786
+ "node": "^20.19.0 || >=22.12.0"
787
+ },
788
+ "funding": {
789
+ "url": "https://github.com/vitejs/vite?sponsor=1"
790
+ },
791
+ "optionalDependencies": {
792
+ "fsevents": "~2.3.3"
793
+ },
794
+ "peerDependencies": {
795
+ "@types/node": "^20.19.0 || >=22.12.0",
796
+ "@vitejs/devtools": "^0.1.18",
797
+ "esbuild": "^0.27.0 || ^0.28.0",
798
+ "jiti": ">=1.21.0",
799
+ "less": "^4.0.0",
800
+ "sass": "^1.70.0",
801
+ "sass-embedded": "^1.70.0",
802
+ "stylus": ">=0.54.8",
803
+ "sugarss": "^5.0.0",
804
+ "terser": "^5.16.0",
805
+ "tsx": "^4.8.1",
806
+ "yaml": "^2.4.2"
807
+ },
808
+ "peerDependenciesMeta": {
809
+ "@types/node": {
810
+ "optional": true
811
+ },
812
+ "@vitejs/devtools": {
813
+ "optional": true
814
+ },
815
+ "esbuild": {
816
+ "optional": true
817
+ },
818
+ "jiti": {
819
+ "optional": true
820
+ },
821
+ "less": {
822
+ "optional": true
823
+ },
824
+ "sass": {
825
+ "optional": true
826
+ },
827
+ "sass-embedded": {
828
+ "optional": true
829
+ },
830
+ "stylus": {
831
+ "optional": true
832
+ },
833
+ "sugarss": {
834
+ "optional": true
835
+ },
836
+ "terser": {
837
+ "optional": true
838
+ },
839
+ "tsx": {
840
+ "optional": true
841
+ },
842
+ "yaml": {
843
+ "optional": true
844
+ }
845
+ }
846
+ }
847
+ }
848
+ }
package.json ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "gemma4-webgpu",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite --host 127.0.0.1 --port 8030",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview --host 127.0.0.1 --port 8030",
10
+ "check": "tsc --noEmit && vite build"
11
+ },
12
+ "keywords": [
13
+ "gemma",
14
+ "gguf",
15
+ "webgpu",
16
+ "wllama",
17
+ "llama.cpp"
18
+ ],
19
+ "author": "ryanhlewis",
20
+ "license": "Apache-2.0",
21
+ "description": "Static browser WebGPU chat app for Gemma 4 E2B GGUF using Wllama.",
22
+ "dependencies": {
23
+ "@wllama/wllama": "^3.4.1",
24
+ "typescript": "^6.0.3",
25
+ "vite": "^8.0.16"
26
+ }
27
+ }
scripts/split-gemma-gguf.ps1 ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ param(
2
+ [string]$SourceRepo = "google/gemma-4-E2B-it-qat-q4_0-gguf",
3
+ [string]$TargetRepo = "ryanhlewis/gemma-4-E2B-it-qat-q4_0-gguf-webgpu",
4
+ [string]$ModelFile = "gemma-4-E2B_q4_0-it.gguf",
5
+ [string]$MmprojFile = "gemma-4-E2B-it-mmproj.gguf",
6
+ [string]$SplitMaxSize = "512M",
7
+ [string]$WorkDir = "model-cache"
8
+ )
9
+
10
+ $ErrorActionPreference = "Stop"
11
+
12
+ New-Item -ItemType Directory -Force -Path $WorkDir | Out-Null
13
+
14
+ Write-Host "Downloading $SourceRepo / $ModelFile"
15
+ hf download $SourceRepo $ModelFile --local-dir $WorkDir
16
+
17
+ Write-Host "Downloading $SourceRepo / $MmprojFile"
18
+ hf download $SourceRepo $MmprojFile --local-dir $WorkDir
19
+
20
+ $dockerWork = (Resolve-Path $WorkDir).Path
21
+
22
+ Write-Host "Splitting model into $SplitMaxSize shards"
23
+ docker run --rm --entrypoint /app/llama-gguf-split -v "${dockerWork}:/work" ghcr.io/ggml-org/llama.cpp:full `
24
+ --split-max-size $SplitMaxSize "/work/$ModelFile" "/work/gemma-4-E2B_q4_0-it"
25
+
26
+ Write-Host "Creating or updating Hugging Face model repo $TargetRepo"
27
+ hf repo create $TargetRepo --type model --yes
28
+
29
+ @"
30
+ ---
31
+ license: apache-2.0
32
+ base_model: google/gemma-4-E2B-it
33
+ tags:
34
+ - gguf
35
+ - webgpu
36
+ - wllama
37
+ - llama.cpp
38
+ - gemma
39
+ ---
40
+
41
+ # Gemma 4 E2B IT QAT Q4_0 GGUF WebGPU Shards
42
+
43
+ Browser-safe split mirror of `google/gemma-4-E2B-it-qat-q4_0-gguf`.
44
+
45
+ The LLM GGUF is split with `llama-gguf-split --split-max-size $SplitMaxSize` for Wllama/WebGPU browser loading. The mirror also includes the matching mmproj file:
46
+
47
+ `gemma-4-E2B-it-mmproj.gguf`
48
+
49
+ Use the first shard URL in Wllama:
50
+
51
+ ```text
52
+ gemma-4-E2B_q4_0-it-00001-of-00005.gguf
53
+ ```
54
+ "@ | Set-Content -Path (Join-Path $WorkDir "README.md") -Encoding UTF8
55
+
56
+ Write-Host "Uploading split shards and README"
57
+ $env:HF_HUB_DISABLE_XET = "1"
58
+ hf upload $TargetRepo (Join-Path $WorkDir "README.md") README.md --repo-type model
59
+ Get-ChildItem $WorkDir -Filter "gemma-4-E2B_q4_0-it-*.gguf" | Sort-Object Name | ForEach-Object {
60
+ hf upload $TargetRepo $_.FullName $_.Name --repo-type model
61
+ }
62
+ hf upload $TargetRepo (Join-Path $WorkDir $MmprojFile) $MmprojFile --repo-type model
63
+
64
+ Write-Host "Done."
src/main.ts ADDED
@@ -0,0 +1,530 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { LoggerWithoutDebug, Wllama } from "@wllama/wllama";
2
+ import type { ChatCompletionChunk, ChatCompletionMessage, ResultTimings } from "@wllama/wllama";
3
+ import wasmUrl from "@wllama/wllama/esm/wasm/wllama.wasm?url";
4
+ import "./style.css";
5
+
6
+ const DEFAULT_LLM_URL =
7
+ "https://huggingface.co/ryanhlewis/gemma-4-E2B-it-qat-q4_0-gguf-webgpu/resolve/main/gemma-4-E2B_q4_0-it-00001-of-00005.gguf";
8
+ const DEFAULT_MMPROJ_URL =
9
+ "https://huggingface.co/ryanhlewis/gemma-4-E2B-it-qat-q4_0-gguf-webgpu/resolve/main/gemma-4-E2B-it-mmproj.gguf";
10
+ const GOOGLE_SINGLE_GGUF =
11
+ "https://huggingface.co/google/gemma-4-E2B-it-qat-q4_0-gguf/resolve/main/gemma-4-E2B_q4_0-it.gguf";
12
+ const TOTAL_DOWNLOAD_BYTES = 4_340_000_000;
13
+
14
+ type Role = "user" | "assistant";
15
+
16
+ type ChatEntry = {
17
+ role: Role;
18
+ content: string;
19
+ };
20
+
21
+ type LoadStats = {
22
+ loaded: number;
23
+ total: number;
24
+ startedAt: number;
25
+ doneAt?: number;
26
+ };
27
+
28
+ const params = new URLSearchParams(window.location.search);
29
+ const root = document.querySelector<HTMLDivElement>("#app");
30
+ if (!root) throw new Error("#app missing");
31
+ const app = root;
32
+
33
+ let wllama: Wllama | null = null;
34
+ let modelLoaded = false;
35
+ let loading = false;
36
+ let generating = false;
37
+ let loadStats: LoadStats = { loaded: 0, total: TOTAL_DOWNLOAD_BYTES, startedAt: 0 };
38
+ let chat: ChatEntry[] = [];
39
+ let imageFile: File | null = null;
40
+ let abortController: AbortController | null = null;
41
+ let lastTimings: ResultTimings | undefined;
42
+
43
+ const state = {
44
+ llmUrl: params.get("modelUrl") || params.get("llmUrl") || DEFAULT_LLM_URL,
45
+ mmprojUrl: params.get("mmprojUrl") || DEFAULT_MMPROJ_URL,
46
+ context: Number(params.get("ctx") || 4096),
47
+ threads: Number(params.get("threads") || 4),
48
+ batch: Number(params.get("batch") || 512),
49
+ gpuLayers: Number(params.get("gpuLayers") || 999),
50
+ maxTokens: Number(params.get("maxTokens") || 384),
51
+ temperature: Number(params.get("temperature") || 0.7),
52
+ topP: Number(params.get("topP") || 0.95),
53
+ seed: Number(params.get("seed") || -1),
54
+ prompt:
55
+ params.get("prompt") ||
56
+ "Explain what makes Gemma 4 E2B useful for browser-local AI in three concise bullets.",
57
+ };
58
+
59
+ function formatBytes(bytes: number): string {
60
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
61
+ const units = ["B", "KiB", "MiB", "GiB"];
62
+ let value = bytes;
63
+ let unit = 0;
64
+ while (value >= 1024 && unit < units.length - 1) {
65
+ value /= 1024;
66
+ unit += 1;
67
+ }
68
+ return `${value.toFixed(unit === 0 ? 0 : 1)} ${units[unit]}`;
69
+ }
70
+
71
+ function formatSeconds(seconds: number): string {
72
+ if (!Number.isFinite(seconds) || seconds < 0) return "0.0s";
73
+ return `${seconds.toFixed(1)}s`;
74
+ }
75
+
76
+ function elapsedLoadSeconds(): number {
77
+ if (!loadStats.startedAt) return 0;
78
+ const end = loadStats.doneAt || performance.now();
79
+ return (end - loadStats.startedAt) / 1000;
80
+ }
81
+
82
+ function loadPercent(): number {
83
+ if (!loadStats.total) return 0;
84
+ return Math.max(0, Math.min(100, (loadStats.loaded / loadStats.total) * 100));
85
+ }
86
+
87
+ function webgpuStatus(): string {
88
+ if (!("gpu" in navigator)) return "unavailable";
89
+ return modelLoaded && wllama?.isSupportWebGPU() ? "ready" : "available";
90
+ }
91
+
92
+ function setLog(message: string): void {
93
+ currentLog = message;
94
+ const el = document.querySelector<HTMLDivElement>("#log");
95
+ if (el) el.textContent = message;
96
+ }
97
+
98
+ function appendLog(message: string): void {
99
+ currentLog = `${currentLog ? `${currentLog}\n` : ""}${message}`;
100
+ const el = document.querySelector<HTMLDivElement>("#log");
101
+ if (!el) return;
102
+ el.textContent = currentLog;
103
+ el.scrollTop = el.scrollHeight;
104
+ }
105
+
106
+ function render(): void {
107
+ const progress = loadPercent();
108
+ const loadLabel = modelLoaded
109
+ ? "Model ready"
110
+ : loading
111
+ ? `${formatBytes(loadStats.loaded)} / ${formatBytes(loadStats.total)}`
112
+ : "Download model assets";
113
+ const loadSub = loading
114
+ ? `${progress.toFixed(1)}% | ${formatBytes(loadStats.loaded / Math.max(elapsedLoadSeconds(), 0.1))}/s | ${formatSeconds(elapsedLoadSeconds())}`
115
+ : "4.34 GB";
116
+
117
+ app.innerHTML = `
118
+ <main class="app">
119
+ <header class="topbar">
120
+ <div class="brand">
121
+ <div class="mark">G4</div>
122
+ <div>
123
+ <h1>Gemma 4 WebGPU</h1>
124
+ <p class="subtle">GGUF chat running locally in your browser with Wllama and llama.cpp WebGPU.</p>
125
+ </div>
126
+ </div>
127
+ <div class="actions">
128
+ <button class="icon-button" id="clearButton" title="Clear chat">C</button>
129
+ <button class="icon-button" id="cacheButton" title="Clear model cache">R</button>
130
+ </div>
131
+ </header>
132
+
133
+ <section class="shell">
134
+ <section class="panel chat-panel">
135
+ <div class="status-strip">
136
+ <div class="metric"><span>Status</span><strong>${modelLoaded ? "ready" : loading ? "loading" : "idle"}</strong></div>
137
+ <div class="metric"><span>WebGPU</span><strong>${webgpuStatus()}</strong></div>
138
+ <div class="metric"><span>Prompt</span><strong>${lastTimings ? `${lastTimings.prompt_per_second.toFixed(1)} tok/s` : "-"}</strong></div>
139
+ <div class="metric"><span>Generate</span><strong>${lastTimings ? `${lastTimings.predicted_per_second.toFixed(1)} tok/s` : "-"}</strong></div>
140
+ </div>
141
+
142
+ <div class="messages">
143
+ ${
144
+ chat.length
145
+ ? `<div class="message-list">${chat
146
+ .map((item) => `<div class="message ${item.role}">${escapeHtml(item.content)}</div>`)
147
+ .join("")}</div>`
148
+ : `<div class="empty"><div><h2>Ask Gemma 4 locally</h2><p class="subtle">Download once, then chat from browser cache.</p></div></div>`
149
+ }
150
+ </div>
151
+
152
+ <form class="composer" id="chatForm">
153
+ <textarea id="promptInput" placeholder="Message Gemma">${escapeHtml(state.prompt)}</textarea>
154
+ <div class="composer-row">
155
+ <label class="file-chip">
156
+ <span class="chip-button">Add image</span>
157
+ <input id="imageInput" type="file" accept="image/*" />
158
+ <span class="file-name">${imageFile ? escapeHtml(imageFile.name) : "Optional image input"}</span>
159
+ </label>
160
+ ${imageFile ? `<button class="button" type="button" id="removeImageButton">Remove image</button>` : ""}
161
+ ${
162
+ generating
163
+ ? `<button class="button danger" id="stopButton" type="button">Stop</button>`
164
+ : `<button class="button primary" type="submit" ${!modelLoaded ? "disabled" : ""}>Send</button>`
165
+ }
166
+ </div>
167
+ </form>
168
+ </section>
169
+
170
+ <aside class="side">
171
+ <section class="panel card">
172
+ <h2>${loadLabel}</h2>
173
+ <p class="subtle">${loadSub}</p>
174
+ <div class="field">
175
+ <div class="progress" aria-label="Download progress">
176
+ <div class="bar" style="width: ${progress}%"></div>
177
+ </div>
178
+ </div>
179
+ <div class="field">
180
+ <button class="button primary" id="loadButton" ${loading || modelLoaded ? "disabled" : ""}>Start download</button>
181
+ </div>
182
+ <div class="pill-row">
183
+ <span class="pill">LLM q4_0 split</span>
184
+ <span class="pill">mmproj</span>
185
+ <span class="pill">WebGPU layers</span>
186
+ </div>
187
+ </section>
188
+
189
+ <section class="panel card">
190
+ <h2>Generation</h2>
191
+ <div class="field-grid">
192
+ <div class="field">
193
+ <label for="maxTokens">Max tokens</label>
194
+ <input id="maxTokens" type="number" min="16" max="4096" step="16" value="${state.maxTokens}" />
195
+ </div>
196
+ <div class="field">
197
+ <label for="temperature">Temperature</label>
198
+ <input id="temperature" type="number" min="0" max="2" step="0.05" value="${state.temperature}" />
199
+ </div>
200
+ </div>
201
+ <div class="field-grid">
202
+ <div class="field">
203
+ <label for="topP">Top-p</label>
204
+ <input id="topP" type="number" min="0.01" max="1" step="0.01" value="${state.topP}" />
205
+ </div>
206
+ <div class="field">
207
+ <label for="seed">Seed</label>
208
+ <input id="seed" type="number" step="1" value="${state.seed}" />
209
+ </div>
210
+ </div>
211
+ </section>
212
+
213
+ <section class="panel card">
214
+ <h2>Runtime</h2>
215
+ <div class="field-grid">
216
+ <div class="field">
217
+ <label for="context">Context</label>
218
+ <input id="context" type="number" min="512" max="32768" step="512" value="${state.context}" />
219
+ </div>
220
+ <div class="field">
221
+ <label for="threads">Threads</label>
222
+ <input id="threads" type="number" min="1" max="16" step="1" value="${state.threads}" />
223
+ </div>
224
+ </div>
225
+ <div class="field-grid">
226
+ <div class="field">
227
+ <label for="batch">Batch</label>
228
+ <input id="batch" type="number" min="64" max="2048" step="64" value="${state.batch}" />
229
+ </div>
230
+ <div class="field">
231
+ <label for="gpuLayers">GPU layers</label>
232
+ <input id="gpuLayers" type="number" min="0" max="999" step="1" value="${state.gpuLayers}" />
233
+ </div>
234
+ </div>
235
+ </section>
236
+
237
+ <section class="panel card">
238
+ <h2>Model source</h2>
239
+ <div class="field">
240
+ <label for="llmUrl">LLM GGUF first shard</label>
241
+ <input id="llmUrl" class="source" value="${escapeAttr(state.llmUrl)}" />
242
+ </div>
243
+ <div class="field">
244
+ <label for="mmprojUrl">mmproj GGUF</label>
245
+ <input id="mmprojUrl" class="source" value="${escapeAttr(state.mmprojUrl)}" />
246
+ </div>
247
+ <div class="field">
248
+ <button class="button" id="googleSingleButton" type="button">Use Google single file</button>
249
+ </div>
250
+ </section>
251
+
252
+ <section class="panel card">
253
+ <h2>Console</h2>
254
+ <div class="log" id="log">${escapeHtml(currentLog)}</div>
255
+ </section>
256
+ </aside>
257
+ </section>
258
+ </main>
259
+ `;
260
+
261
+ bindEvents();
262
+ requestAnimationFrame(() => {
263
+ const messages = document.querySelector<HTMLDivElement>(".messages");
264
+ if (messages) messages.scrollTop = messages.scrollHeight;
265
+ });
266
+ }
267
+
268
+ let currentLog = "Ready.";
269
+
270
+ function escapeHtml(value: string): string {
271
+ return value
272
+ .replaceAll("&", "&amp;")
273
+ .replaceAll("<", "&lt;")
274
+ .replaceAll(">", "&gt;")
275
+ .replaceAll('"', "&quot;")
276
+ .replaceAll("'", "&#039;");
277
+ }
278
+
279
+ function escapeAttr(value: string): string {
280
+ return escapeHtml(value).replaceAll("\n", " ");
281
+ }
282
+
283
+ function syncInputs(): void {
284
+ state.prompt = getTextAreaValue("promptInput", state.prompt);
285
+ state.llmUrl = getInputValue("llmUrl", state.llmUrl);
286
+ state.mmprojUrl = getInputValue("mmprojUrl", state.mmprojUrl);
287
+ state.context = getNumberValue("context", state.context);
288
+ state.threads = getNumberValue("threads", state.threads);
289
+ state.batch = getNumberValue("batch", state.batch);
290
+ state.gpuLayers = getNumberValue("gpuLayers", state.gpuLayers);
291
+ state.maxTokens = getNumberValue("maxTokens", state.maxTokens);
292
+ state.temperature = getNumberValue("temperature", state.temperature);
293
+ state.topP = getNumberValue("topP", state.topP);
294
+ state.seed = getNumberValue("seed", state.seed);
295
+ }
296
+
297
+ function getInputValue(id: string, fallback: string): string {
298
+ return document.querySelector<HTMLInputElement>(`#${id}`)?.value.trim() || fallback;
299
+ }
300
+
301
+ function getTextAreaValue(id: string, fallback: string): string {
302
+ return document.querySelector<HTMLTextAreaElement>(`#${id}`)?.value.trim() || fallback;
303
+ }
304
+
305
+ function getNumberValue(id: string, fallback: number): number {
306
+ const value = Number(document.querySelector<HTMLInputElement>(`#${id}`)?.value);
307
+ return Number.isFinite(value) ? value : fallback;
308
+ }
309
+
310
+ function bindEvents(): void {
311
+ document.querySelector<HTMLButtonElement>("#loadButton")?.addEventListener("click", () => {
312
+ void loadModel();
313
+ });
314
+
315
+ document.querySelector<HTMLFormElement>("#chatForm")?.addEventListener("submit", (event) => {
316
+ event.preventDefault();
317
+ void sendMessage();
318
+ });
319
+
320
+ document.querySelector<HTMLButtonElement>("#stopButton")?.addEventListener("click", () => {
321
+ abortController?.abort();
322
+ });
323
+
324
+ document.querySelector<HTMLInputElement>("#imageInput")?.addEventListener("change", (event) => {
325
+ const files = (event.target as HTMLInputElement).files;
326
+ imageFile = files?.[0] || null;
327
+ render();
328
+ });
329
+
330
+ document.querySelector<HTMLButtonElement>("#removeImageButton")?.addEventListener("click", () => {
331
+ imageFile = null;
332
+ render();
333
+ });
334
+
335
+ document.querySelector<HTMLButtonElement>("#clearButton")?.addEventListener("click", () => {
336
+ chat = [];
337
+ lastTimings = undefined;
338
+ render();
339
+ });
340
+
341
+ document.querySelector<HTMLButtonElement>("#cacheButton")?.addEventListener("click", () => {
342
+ void clearCache();
343
+ });
344
+
345
+ document.querySelector<HTMLButtonElement>("#googleSingleButton")?.addEventListener("click", () => {
346
+ state.llmUrl = GOOGLE_SINGLE_GGUF;
347
+ modelLoaded = false;
348
+ render();
349
+ appendLog("Switched to the official single GGUF URL. Browsers may reject files over 2 GB; split shards are recommended.");
350
+ });
351
+ }
352
+
353
+ async function loadModel(): Promise<void> {
354
+ if (loading || modelLoaded) return;
355
+ syncInputs();
356
+
357
+ loading = true;
358
+ loadStats = { loaded: 0, total: TOTAL_DOWNLOAD_BYTES, startedAt: performance.now() };
359
+ currentLog = "Creating Wllama runtime...";
360
+ render();
361
+
362
+ try {
363
+ wllama = new Wllama(
364
+ { default: wasmUrl },
365
+ {
366
+ logger: LoggerWithoutDebug,
367
+ suppressNativeLog: true,
368
+ parallelDownloads: 5,
369
+ }
370
+ );
371
+ wllama.setCompat(null);
372
+
373
+ if (!wllama.isSupportWebGPU()) {
374
+ appendLog("WebGPU is not available. Wllama may fall back or fail depending on this browser.");
375
+ }
376
+
377
+ appendLog(`Loading ${state.llmUrl}`);
378
+ await wllama.loadModelFromUrl(
379
+ {
380
+ url: state.llmUrl,
381
+ mmprojUrl: state.mmprojUrl || undefined,
382
+ },
383
+ {
384
+ useCache: true,
385
+ n_ctx: state.context,
386
+ n_threads: state.threads,
387
+ n_batch: state.batch,
388
+ n_gpu_layers: state.gpuLayers,
389
+ flash_attn: true,
390
+ cache_type_k: "f16",
391
+ cache_type_v: "f16",
392
+ mmproj_offload: true,
393
+ seed: state.seed >= 0 ? state.seed : Math.floor(Math.random() * 2_147_483_647),
394
+ progressCallback: ({ loaded, total }) => {
395
+ loadStats.loaded = loaded;
396
+ loadStats.total = total || loadStats.total || TOTAL_DOWNLOAD_BYTES;
397
+ currentLog = `Downloading model assets...\n${formatBytes(loadStats.loaded)} / ${formatBytes(loadStats.total)}`;
398
+ render();
399
+ },
400
+ }
401
+ );
402
+
403
+ loadStats.doneAt = performance.now();
404
+ modelLoaded = true;
405
+ currentLog = [
406
+ `Loaded in ${formatSeconds(elapsedLoadSeconds())}`,
407
+ `libllama ${Wllama.getLibllamaVersion()}`,
408
+ `threads ${wllama.getNumThreads()}${wllama.isMultithread() ? " multi-thread" : " single-thread"}`,
409
+ `modalities image=${safeSupport("image")} audio=${safeSupport("audio")}`,
410
+ ].join("\n");
411
+ } catch (error) {
412
+ currentLog = `Load failed:\n${errorToText(error)}`;
413
+ modelLoaded = false;
414
+ void wllama?.exit().catch(() => undefined);
415
+ wllama = null;
416
+ } finally {
417
+ loading = false;
418
+ render();
419
+ }
420
+ }
421
+
422
+ function safeSupport(kind: "image" | "audio"): string {
423
+ try {
424
+ return wllama?.supportInputModality(kind) ? "yes" : "no";
425
+ } catch {
426
+ return "unknown";
427
+ }
428
+ }
429
+
430
+ async function clearCache(): Promise<void> {
431
+ if (!wllama) {
432
+ wllama = new Wllama({ default: wasmUrl }, { logger: LoggerWithoutDebug, suppressNativeLog: true });
433
+ }
434
+ await wllama.cacheManager.clear();
435
+ modelLoaded = false;
436
+ currentLog = "Browser model cache cleared.";
437
+ render();
438
+ }
439
+
440
+ async function sendMessage(): Promise<void> {
441
+ if (!wllama || !modelLoaded || generating) return;
442
+ syncInputs();
443
+ if (!state.prompt) return;
444
+
445
+ const userMessage = state.prompt;
446
+ const imageData = imageFile ? await imageFile.arrayBuffer() : null;
447
+ const content = imageData
448
+ ? [
449
+ { type: "text" as const, text: userMessage },
450
+ { type: "image" as const, data: imageData },
451
+ ]
452
+ : userMessage;
453
+
454
+ const history: ChatCompletionMessage[] = chat.map((entry) => ({
455
+ role: entry.role,
456
+ content: entry.content,
457
+ }));
458
+
459
+ chat.push({ role: "user", content: imageFile ? `${userMessage}\n[image: ${imageFile.name}]` : userMessage });
460
+ chat.push({ role: "assistant", content: "" });
461
+ state.prompt = "";
462
+ generating = true;
463
+ abortController = new AbortController();
464
+ lastTimings = undefined;
465
+ const startedAt = performance.now();
466
+ render();
467
+
468
+ try {
469
+ await wllama.createChatCompletion({
470
+ messages: [
471
+ ...history,
472
+ {
473
+ role: "user",
474
+ content,
475
+ },
476
+ ],
477
+ stream: true,
478
+ max_tokens: state.maxTokens,
479
+ temperature: state.temperature,
480
+ top_p: state.topP,
481
+ cache_prompt: true,
482
+ timings_per_token: true,
483
+ abortSignal: abortController.signal,
484
+ onData: (chunk) => {
485
+ const delta = chunk.choices?.[0]?.delta?.content || "";
486
+ lastTimings = chunk.timings || lastTimings;
487
+ const last = chat[chat.length - 1];
488
+ if (last?.role === "assistant") last.content += delta;
489
+ const elapsed = (performance.now() - startedAt) / 1000;
490
+ currentLog = [
491
+ `Generating ${formatSeconds(elapsed)}`,
492
+ lastTimings ? `Prompt ${lastTimings.prompt_per_second.toFixed(1)} tok/s` : "",
493
+ lastTimings ? `Output ${lastTimings.predicted_per_second.toFixed(1)} tok/s` : "",
494
+ ]
495
+ .filter(Boolean)
496
+ .join("\n");
497
+ render();
498
+ },
499
+ });
500
+ const elapsed = (performance.now() - startedAt) / 1000;
501
+ currentLog = formatDoneLog(elapsed, lastTimings);
502
+ } catch (error) {
503
+ const last = chat[chat.length - 1];
504
+ if (last?.role === "assistant" && !last.content) {
505
+ last.content = `Error: ${errorToText(error)}`;
506
+ }
507
+ currentLog = `Generation stopped:\n${errorToText(error)}`;
508
+ } finally {
509
+ generating = false;
510
+ abortController = null;
511
+ render();
512
+ }
513
+ }
514
+
515
+ function formatDoneLog(elapsed: number, timings?: ResultTimings): string {
516
+ return [
517
+ `Done in ${formatSeconds(elapsed)}`,
518
+ timings ? `Prompt ${timings.prompt_n} tokens at ${timings.prompt_per_second.toFixed(1)} tok/s` : "",
519
+ timings ? `Generated ${timings.predicted_n} tokens at ${timings.predicted_per_second.toFixed(1)} tok/s` : "",
520
+ ]
521
+ .filter(Boolean)
522
+ .join("\n");
523
+ }
524
+
525
+ function errorToText(error: unknown): string {
526
+ if (error instanceof Error) return error.stack || error.message;
527
+ return String(error);
528
+ }
529
+
530
+ render();
src/style.css ADDED
@@ -0,0 +1,399 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ color-scheme: dark;
3
+ font-family:
4
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
5
+ sans-serif;
6
+ background: #10100f;
7
+ color: #f4f0e8;
8
+ line-height: 1.4;
9
+ font-synthesis: none;
10
+ text-rendering: optimizeLegibility;
11
+ -webkit-font-smoothing: antialiased;
12
+ }
13
+
14
+ * {
15
+ box-sizing: border-box;
16
+ }
17
+
18
+ body {
19
+ margin: 0;
20
+ min-width: 320px;
21
+ min-height: 100vh;
22
+ background:
23
+ linear-gradient(180deg, rgba(57, 69, 58, 0.22), transparent 360px),
24
+ #10100f;
25
+ }
26
+
27
+ button,
28
+ input,
29
+ textarea,
30
+ select {
31
+ font: inherit;
32
+ }
33
+
34
+ button {
35
+ border: 0;
36
+ }
37
+
38
+ .app {
39
+ width: min(1180px, calc(100vw - 32px));
40
+ margin: 0 auto;
41
+ padding: 24px 0 36px;
42
+ }
43
+
44
+ .topbar {
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: space-between;
48
+ gap: 16px;
49
+ margin-bottom: 20px;
50
+ }
51
+
52
+ .brand {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: 12px;
56
+ }
57
+
58
+ .mark {
59
+ display: grid;
60
+ width: 38px;
61
+ height: 38px;
62
+ place-items: center;
63
+ border: 1px solid rgba(244, 240, 232, 0.16);
64
+ border-radius: 8px;
65
+ background: #d7ff7a;
66
+ color: #15170f;
67
+ font-weight: 800;
68
+ }
69
+
70
+ h1,
71
+ h2,
72
+ h3,
73
+ p {
74
+ margin: 0;
75
+ }
76
+
77
+ h1 {
78
+ font-size: 19px;
79
+ font-weight: 720;
80
+ letter-spacing: 0;
81
+ }
82
+
83
+ .subtle {
84
+ color: #a7a093;
85
+ font-size: 13px;
86
+ }
87
+
88
+ .actions {
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 8px;
92
+ }
93
+
94
+ .icon-button,
95
+ .button {
96
+ display: inline-flex;
97
+ min-height: 38px;
98
+ align-items: center;
99
+ justify-content: center;
100
+ gap: 8px;
101
+ border-radius: 8px;
102
+ padding: 0 14px;
103
+ border: 1px solid rgba(244, 240, 232, 0.12);
104
+ background: #24231f;
105
+ color: #f4f0e8;
106
+ cursor: pointer;
107
+ }
108
+
109
+ .icon-button {
110
+ width: 38px;
111
+ padding: 0;
112
+ }
113
+
114
+ .button.primary {
115
+ background: #d7ff7a;
116
+ border-color: #d7ff7a;
117
+ color: #15170f;
118
+ font-weight: 740;
119
+ }
120
+
121
+ .button.danger {
122
+ background: #3a2525;
123
+ border-color: #6e3737;
124
+ color: #ffd9d9;
125
+ }
126
+
127
+ .button:disabled,
128
+ .icon-button:disabled {
129
+ cursor: not-allowed;
130
+ opacity: 0.55;
131
+ }
132
+
133
+ .shell {
134
+ display: grid;
135
+ grid-template-columns: minmax(0, 1fr) 340px;
136
+ gap: 18px;
137
+ }
138
+
139
+ .panel {
140
+ border: 1px solid rgba(244, 240, 232, 0.12);
141
+ border-radius: 8px;
142
+ background: rgba(26, 25, 22, 0.94);
143
+ }
144
+
145
+ .chat-panel {
146
+ min-height: 690px;
147
+ display: grid;
148
+ grid-template-rows: auto minmax(0, 1fr) auto;
149
+ }
150
+
151
+ .status-strip {
152
+ display: grid;
153
+ grid-template-columns: repeat(4, minmax(0, 1fr));
154
+ gap: 1px;
155
+ border-bottom: 1px solid rgba(244, 240, 232, 0.1);
156
+ background: rgba(244, 240, 232, 0.08);
157
+ }
158
+
159
+ .metric {
160
+ min-width: 0;
161
+ padding: 12px 14px;
162
+ background: #1a1916;
163
+ }
164
+
165
+ .metric span {
166
+ display: block;
167
+ color: #a7a093;
168
+ font-size: 11px;
169
+ text-transform: uppercase;
170
+ }
171
+
172
+ .metric strong {
173
+ display: block;
174
+ overflow: hidden;
175
+ margin-top: 2px;
176
+ text-overflow: ellipsis;
177
+ white-space: nowrap;
178
+ font-size: 14px;
179
+ font-weight: 680;
180
+ }
181
+
182
+ .messages {
183
+ overflow: auto;
184
+ padding: 18px;
185
+ }
186
+
187
+ .message-list {
188
+ display: flex;
189
+ flex-direction: column;
190
+ gap: 14px;
191
+ }
192
+
193
+ .message {
194
+ max-width: 86%;
195
+ padding: 13px 14px;
196
+ border: 1px solid rgba(244, 240, 232, 0.1);
197
+ border-radius: 8px;
198
+ white-space: pre-wrap;
199
+ overflow-wrap: anywhere;
200
+ }
201
+
202
+ .message.user {
203
+ align-self: flex-end;
204
+ background: #243124;
205
+ }
206
+
207
+ .message.assistant {
208
+ align-self: flex-start;
209
+ background: #22211d;
210
+ }
211
+
212
+ .empty {
213
+ display: grid;
214
+ min-height: 340px;
215
+ place-items: center;
216
+ color: #a7a093;
217
+ text-align: center;
218
+ }
219
+
220
+ .composer {
221
+ display: grid;
222
+ gap: 10px;
223
+ padding: 14px;
224
+ border-top: 1px solid rgba(244, 240, 232, 0.1);
225
+ }
226
+
227
+ textarea,
228
+ input,
229
+ select {
230
+ width: 100%;
231
+ border: 1px solid rgba(244, 240, 232, 0.12);
232
+ border-radius: 8px;
233
+ background: #121210;
234
+ color: #f4f0e8;
235
+ outline: none;
236
+ }
237
+
238
+ textarea:focus,
239
+ input:focus,
240
+ select:focus {
241
+ border-color: rgba(215, 255, 122, 0.8);
242
+ box-shadow: 0 0 0 3px rgba(215, 255, 122, 0.12);
243
+ }
244
+
245
+ textarea {
246
+ min-height: 96px;
247
+ resize: vertical;
248
+ padding: 12px;
249
+ }
250
+
251
+ input,
252
+ select {
253
+ min-height: 38px;
254
+ padding: 0 10px;
255
+ }
256
+
257
+ .composer-row {
258
+ display: flex;
259
+ align-items: center;
260
+ justify-content: space-between;
261
+ gap: 10px;
262
+ }
263
+
264
+ .file-chip {
265
+ display: flex;
266
+ min-width: 0;
267
+ flex: 1;
268
+ align-items: center;
269
+ gap: 8px;
270
+ color: #a7a093;
271
+ font-size: 13px;
272
+ }
273
+
274
+ .file-chip input {
275
+ display: none;
276
+ }
277
+
278
+ .chip-button {
279
+ display: inline-flex;
280
+ min-height: 34px;
281
+ align-items: center;
282
+ gap: 7px;
283
+ border-radius: 8px;
284
+ padding: 0 10px;
285
+ background: #24231f;
286
+ color: #f4f0e8;
287
+ cursor: pointer;
288
+ }
289
+
290
+ .file-name {
291
+ overflow: hidden;
292
+ text-overflow: ellipsis;
293
+ white-space: nowrap;
294
+ }
295
+
296
+ .side {
297
+ display: flex;
298
+ flex-direction: column;
299
+ gap: 14px;
300
+ }
301
+
302
+ .card {
303
+ padding: 16px;
304
+ }
305
+
306
+ .card h2 {
307
+ margin-bottom: 10px;
308
+ font-size: 15px;
309
+ font-weight: 720;
310
+ }
311
+
312
+ .field {
313
+ display: grid;
314
+ gap: 6px;
315
+ margin-top: 12px;
316
+ }
317
+
318
+ .field label {
319
+ color: #c9c1b2;
320
+ font-size: 12px;
321
+ font-weight: 650;
322
+ }
323
+
324
+ .field-grid {
325
+ display: grid;
326
+ grid-template-columns: 1fr 1fr;
327
+ gap: 10px;
328
+ }
329
+
330
+ .progress {
331
+ height: 10px;
332
+ overflow: hidden;
333
+ border-radius: 999px;
334
+ background: #11110f;
335
+ }
336
+
337
+ .bar {
338
+ height: 100%;
339
+ width: 0%;
340
+ border-radius: inherit;
341
+ background: #d7ff7a;
342
+ transition: width 180ms ease;
343
+ }
344
+
345
+ .log {
346
+ min-height: 106px;
347
+ max-height: 190px;
348
+ overflow: auto;
349
+ padding: 10px;
350
+ border-radius: 8px;
351
+ background: #11110f;
352
+ color: #c9c1b2;
353
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
354
+ font-size: 12px;
355
+ white-space: pre-wrap;
356
+ }
357
+
358
+ .pill-row {
359
+ display: flex;
360
+ flex-wrap: wrap;
361
+ gap: 8px;
362
+ margin-top: 12px;
363
+ }
364
+
365
+ .pill {
366
+ border: 1px solid rgba(244, 240, 232, 0.12);
367
+ border-radius: 999px;
368
+ padding: 5px 9px;
369
+ color: #c9c1b2;
370
+ font-size: 12px;
371
+ }
372
+
373
+ .source {
374
+ overflow-wrap: anywhere;
375
+ }
376
+
377
+ @media (max-width: 900px) {
378
+ .app {
379
+ width: min(100vw - 20px, 720px);
380
+ padding-top: 14px;
381
+ }
382
+
383
+ .shell {
384
+ grid-template-columns: 1fr;
385
+ }
386
+
387
+ .status-strip {
388
+ grid-template-columns: 1fr 1fr;
389
+ }
390
+
391
+ .message {
392
+ max-width: 94%;
393
+ }
394
+
395
+ .topbar {
396
+ align-items: flex-start;
397
+ flex-direction: column;
398
+ }
399
+ }
src/vite-env.d.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ /// <reference types="vite/client" />
tsconfig.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "module": "ESNext",
6
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
7
+ "allowJs": false,
8
+ "skipLibCheck": true,
9
+ "esModuleInterop": true,
10
+ "allowSyntheticDefaultImports": true,
11
+ "strict": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "moduleResolution": "Bundler",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "noEmit": true
17
+ },
18
+ "include": ["src"]
19
+ }
vite.config.ts ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from "vite";
2
+
3
+ const crossOriginHeaders = {
4
+ "Cross-Origin-Embedder-Policy": "require-corp",
5
+ "Cross-Origin-Opener-Policy": "same-origin",
6
+ "Cross-Origin-Resource-Policy": "cross-origin",
7
+ };
8
+
9
+ export default defineConfig({
10
+ base: "./",
11
+ server: {
12
+ headers: crossOriginHeaders,
13
+ },
14
+ preview: {
15
+ headers: crossOriginHeaders,
16
+ },
17
+ build: {
18
+ target: "es2022",
19
+ assetsInlineLimit: 0,
20
+ },
21
+ });