abgunaydin commited on
Commit
e26c434
Β·
0 Parent(s):

sync from github@30b8f47

Browse files
This view is limited to 50 files because it contains too many changes. Β  See raw diff
Files changed (50) hide show
  1. .gitattributes +4 -0
  2. 404.html +29 -0
  3. README.md +120 -0
  4. _headers +8 -0
  5. _redirects +16 -0
  6. agent-host.html +78 -0
  7. assets/agent-host-eDUnNJBhJx.js +84 -0
  8. assets/cache-probe-BYX_n9OPRB.js +1 -0
  9. assets/chat-flow-eTAzKAlIYe.js +6 -0
  10. assets/chat-ui-CQAxV1po6S.js +23 -0
  11. assets/engine-core-CLR3cxsqf4.js +0 -0
  12. assets/feats-dlBfBxqpI8.js +1 -0
  13. assets/index-BqNbylMY24.js +70 -0
  14. assets/landing-chat-Bo__3GxplQ.js +49 -0
  15. assets/landing-room-BSBnFYP4bg.js +17 -0
  16. assets/loading-ui-CjCwSRuXvh.js +7 -0
  17. assets/mascot-BCKWddkWrV.js +307 -0
  18. assets/model-registry-BoK-ObgACo.js +1 -0
  19. assets/model-select-DhhV9mCi0T.js +39 -0
  20. assets/preload-helper-ckwbz45pO3.js +1 -0
  21. assets/room-host-C6R3zseuU5.js +1 -0
  22. assets/share-TXmFQ6ZAWJ.js +13 -0
  23. assets/validate-D2Aic8RAjT.js +16 -0
  24. assets/variants-De-VVc9BRz.js +1 -0
  25. assets/weight-loader-ByOLMd9f5C.js +2 -0
  26. assets/zero-tvm-nxGrkTTcQf.js +29 -0
  27. chat-ui.css +523 -0
  28. docs.html +1316 -0
  29. entrance/backring.svg +18 -0
  30. entrance/banner.svg +13 -0
  31. entrance/bg.svg +27 -0
  32. entrance/border.svg +6 -0
  33. entrance/column.svg +20 -0
  34. entrance/corner.svg +11 -0
  35. entrance/divider.svg +13 -0
  36. entrance/pframe.svg +7 -0
  37. entrance/ring.svg +22 -0
  38. entrance/spires.svg +31 -0
  39. favicon.svg +7 -0
  40. fonts.css +131 -0
  41. fonts/f1-fraunce.woff2 +3 -0
  42. fonts/f2-fraunce.woff2 +3 -0
  43. fonts/f3-fraunce.woff2 +3 -0
  44. fonts/f4-plinean.woff2 +3 -0
  45. fonts/f5-plinean.woff2 +3 -0
  46. fonts/f6-plineanmono.woff2 +3 -0
  47. fonts/f7-plineanmono.woff2 +3 -0
  48. index.html +151 -0
  49. landing.css +1442 -0
  50. og.png +3 -0
.gitattributes ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ *.png filter=lfs diff=lfs merge=lfs -text
2
+ *.jpg filter=lfs diff=lfs merge=lfs -text
3
+ *.webp filter=lfs diff=lfs merge=lfs -text
4
+ *.woff2 filter=lfs diff=lfs merge=lfs -text
404.html ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <!-- Presence of this file switches Cloudflare Pages from SPA fallback (200 +
3
+ index.html for any miss) to real 404 responses. That matters beyond
4
+ cosmetics: a miss under /assets/* used to return the landing page WITH
5
+ that path's immutable cache header, and one such response got cached at
6
+ the edge for a year (2026-08-06). A real 404 is not cacheable that way.
7
+ Every route on this site is a real file, so nothing relied on the
8
+ fallback. -->
9
+ <html lang="en">
10
+ <head>
11
+ <meta charset="UTF-8">
12
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
13
+ <title>404 β€” Zero-TVM</title>
14
+ <meta name="robots" content="noindex">
15
+ <style>
16
+ body { background:#0a0a0f; color:#ccc; font:15px/1.6 ui-monospace,SFMono-Regular,Menlo,monospace;
17
+ display:flex; align-items:center; justify-content:center; min-height:100vh; margin:0 }
18
+ main { text-align:center; padding:2rem }
19
+ h1 { font-size:1.2rem; color:#eee; font-weight:600 }
20
+ a { color:#6ea8ff }
21
+ </style>
22
+ </head>
23
+ <body>
24
+ <main>
25
+ <h1>404 β€” no such page</h1>
26
+ <p><a href="/">zerotvm.com</a> β€” pick a model, it runs in your tab.</p>
27
+ </main>
28
+ </body>
29
+ </html>
README.md ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Zero-TVM
3
+ emoji: 🧠
4
+ colorFrom: indigo
5
+ colorTo: purple
6
+ sdk: static
7
+ pinned: true
8
+ license: mit
9
+ thumbnail: https://huggingface.co/spaces/abgunaydin/zero-tvm/resolve/main/og.png
10
+ short_description: Phi-3 to a 35B MoE in the browser, on hand-written WGSL
11
+ tags:
12
+ - webgpu
13
+ - llm
14
+ - inference
15
+ - on-device
16
+ - wgsl
17
+ models:
18
+ - microsoft/Phi-3-mini-4k-instruct
19
+ - Qwen/Qwen3-4B
20
+ - Qwen/Qwen3.5-4B
21
+ - meta-llama/Llama-3.2-1B-Instruct
22
+ - mlc-ai/Phi-3-mini-4k-instruct-q4f16_1-MLC
23
+ - mlc-ai/Qwen3-4B-q4f16_1-MLC
24
+ - mlc-ai/Qwen3.5-4B-q4f16_1-MLC
25
+ - mlx-community/Qwen3-4B-4bit
26
+ - mlx-community/Qwen3-30B-A3B-4bit
27
+ - mlx-community/Llama-3.2-1B-Instruct-4bit
28
+ - lmstudio-community/Qwen3.6-35B-A3B-MLX-4bit
29
+ - abgunaydin/Qwen3.6-35B-A3B-MLX-q3exp
30
+ ---
31
+
32
+ # Zero-TVM
33
+
34
+ Models from a 3.8B dense to a 35B sparse MoE, running in the browser on hand-written WGSL. No TVM, no ONNX, no WASM runtime.
35
+
36
+ The standard way to run a modern LLM in a browser is [WebLLM / MLC-LLM](https://webllm.mlc.ai/), which ships an Apache-TVM compiler pipeline that autotunes its WGSL β€” 85 shaders captured across a session, around 11 of them on the decode path. This Space replaces that entire stack with **10 kernel roles** of hand-written WGSL β€” and runs models WebLLM ships (same weights, measured faster) as well as one it does not: Qwen3.6-35B-A3B, a 256-expert sparse MoE.
37
+
38
+ The whole forward pass β€” 32 transformer layers, paged KV cache, int4-dequant matmul, RoPE, fused FFN, RMSNorm, paged attention, argmax sampling β€” is readable end-to-end in a single sitting. That is the point.
39
+
40
+ ## Headline result
41
+
42
+ Measured on an Apple M2 Max (Chrome 150, WebLLM v0.2.80, identical Phi-3-mini-q4f16_1 weights, same-session head-to-head, 2026-07-30). Two numbers, always both: **total** is wall-clock throughput with prefill included (both engines doing identical work), **decode** excludes prefill.
43
+
44
+ | | WebLLM (TVM) | Zero-TVM |
45
+ |---|---|---|
46
+ | WGSL kernels (decode path) | **~11 TVM-generated** (85 captured across a session) | **10 hand-written roles** |
47
+ | Dispatches per decode step | **342** | **260** default (228 with `?splitk=0`) |
48
+ | Total throughput (prefill + decode) | **59.95 tok/s** | **69.55 tok/s** β€” **+16.0%** |
49
+ | Decode only | **63.23 tok/s** (self-reported) | **83.10 tok/s** β€” **+31.4%** |
50
+ | Time to first token (~35-tok prompt) | **~150 ms** (implied) | **291 ms** β€” *WebLLM wins this one* |
51
+ | JS bundle (excl. weights) | **~6.0 MB** / ~2.2 MB gz | **~460 kB** / ~126 kB gz |
52
+
53
+ **The hand-written kernels don't just keep up with the autotuning compiler β€” on this hardware they beat it on throughput.** For decoder-only LLMs of this shape, most of the compiler's complexity budget isn't buying much β€” the expensive parts are matmul, attention, and int4 dequant. Everything else is plumbing, and ten hand-written roles cover it.
54
+
55
+ **The gap grows with how recent the architecture is** β€” +16% / +31% (total / decode) on 2024's Phi-3, +32% / +58% on 2025's Qwen3-4B, +100% / +114% on 2026's Qwen3.5-4B. Consistent with compiler stacks having had less time to tune newer architectures; an observation across three points on one machine, not a proven law.
56
+
57
+ **Honest negative, published not buried:** WebLLM reaches the first token faster than we do on short prompts (~150 ms implied vs our 291 ms on Phi-3; 453 ms on Qwen3-4B). We win sustained throughput and lose the first-token sprint on short inputs β€” our chunked prefill is strong on long ones (202 tok/s at 816 tokens) and prefix reuse makes follow-up turns free.
58
+
59
+ Exact medians, full methodology, and the optimization experiments that were measured and *dropped* live in [BENCH.md](https://github.com/abgnydn/zero-tvm/blob/main/BENCH.md). (An earlier measurement on an M2 Pro with an older, buggier engine read 22% *behind* β€” that history is preserved there too, not retconned. So are two 2026-07-29 Qwen pairs that a bench-harness defect made non-comparable; they are marked withdrawn with the reason, not deleted.)
60
+
61
+ ## Run it
62
+
63
+ The Space opens on a character-select entrance: pick a model and it loads and chats in place. [`zero-tvm.html`](./zero-tvm.html) is the same chat as a direct link, if you want to deep-link a specific model.
64
+
65
+ Picking a character starts the download immediately β€” the sheet shows the weight size before you click, and there is no second confirmation. It streams ~2 GB of Phi-3-mini-q4f16_1 weights from the Hugging Face mirror at [`mlc-ai/Phi-3-mini-4k-instruct-q4f16_1-MLC`](https://huggingface.co/mlc-ai/Phi-3-mini-4k-instruct-q4f16_1-MLC) into OPFS (Origin Private File System).
66
+
67
+ Subsequent loads are instant β€” the weights stay in OPFS, so returning to a model you have already downloaded starts it straight away.
68
+
69
+ **Requirements**: Chrome / Edge with WebGPU enabled and the `shader-f16` feature available (default on macOS Apple-Silicon, enabled on most modern Windows / Linux GPUs).
70
+
71
+ ## Models
72
+
73
+ All three pairs re-measured 2026-07-30 under the corrected protocol β€” same session, identical local weight bytes, both engines paying a full prefill on every run, total and decode reported side by side.
74
+
75
+ - **Phi-3-mini-4k-instruct (3.8B, q4f16_1)** β€” the default; every headline number above is Phi-3, no URL flag needed. Zero-TVM **69.55 tok/s total** (TTFT 291 ms, decode 83.10) vs WebLLM 0.2.80's **59.95 tok/s total** (self-reported decode 63.23) β€” **+16.0% total, +31.4% decode**.
76
+ - **Qwen3-4B (q4f16_1)** β€” append `?model=qwen3` to `zero-tvm.html` or `validate.html`; weights stream from [`mlc-ai/Qwen3-4B-q4f16_1-MLC`](https://huggingface.co/mlc-ai/Qwen3-4B-q4f16_1-MLC) (~2.3 GB). A port on the spec-parameterized engine: GQA 32/8, per-head QK-norm, byte-level BPE, tied lm_head, with the tuning round's fused post-matmul chain (`qk_norm_rope_append`, 8 dispatches/layer) and K%512 wide loads. Zero-TVM **59.85 tok/s total** (TTFT 453 ms, decode 75.49) vs WebLLM 0.2.84's prebuilt Qwen3-4B at **45.46 tok/s total** (self-reported decode 47.77) β€” **+31.7% total, +58.0% decode**. This is the model where WebLLM most clearly beats us on first-token latency (~150 ms implied vs our 453 ms). The previously published "75.7 vs 43.8, +73%" pair is **withdrawn** β€” a bench-harness defect had stopped our half paying prefill; and the 2026-07-28 pair (25.43 vs 14.15) was a degraded session that did not reproduce. Both are kept as dated history with the reason in [BENCH.md](https://github.com/abgnydn/zero-tvm/blob/main/BENCH.md).
77
+ - **Qwen3.5-4B (q4f16_1)** β€” append `?model=qwen35` to `zero-tvm.html` or `validate.html`; weights stream from [`mlc-ai/Qwen3.5-4B-q4f16_1-MLC`](https://huggingface.co/mlc-ai/Qwen3.5-4B-q4f16_1-MLC) (~2.6 GB). The first *hybrid* on the engine: 24 gated-DeltaNet (linear-attention) layers + 8 gated-attention layers (GQA 16/4, head_dim 256, partial RoPE, sigmoid attention gate) β€” to our knowledge the first hand-written-kernel int4 gated-DeltaNet hybrid in a browser. Zero-TVM **65.28 tok/s total** (TTFT 171 ms, decode 73.30) vs WebLLM 0.2.84's prebuilt Qwen3.5-4B at **32.56 tok/s total** (self-reported decode 34.32) β€” **+100.5% total, +113.6% decode**, and the one model where first-token latency is a wash rather than a loss. The GDN decode kernels are still scalar (non-subgroup), so the decode number is a floor; prefill runs in chunks of ≀64 (202 tok/s on an 816-token prompt) and every model reuses its cross-turn prefix (turn-3 first token 14.3 s β†’ 0.19 s, logits verified bit-identical to a fresh prefill). The previously published "65.7 vs 34.0, +93%" cross-check is **withdrawn** (same harness defect); the earlier 53.07/32.36 (+64%) and 47.99/31.99 (+50%) pairs were like-for-like but are superseded. One machine, one pair each; caveats in [BENCH.md](https://github.com/abgnydn/zero-tvm/blob/main/BENCH.md).
78
+
79
+ ## Pages in this Space
80
+
81
+ - [`index.html`](./index.html) β€” the entrance: pick a model, chat in place (start here)
82
+ - [`zero-tvm.html`](./zero-tvm.html) β€” the chat surface as a direct link
83
+ - [`validate.html`](./validate.html) β€” multi-prompt smoke test
84
+ - [`docs.html`](./docs.html) β€” annotated reference, including the kernel walkthrough
85
+
86
+ Six pages were removed on 2026-08-14 β€” `architecture`, `demo`, `compiler-chat`,
87
+ `dump`, `shaders`, `webllm-bench`. Three of them started multi-gigabyte
88
+ downloads on page load. `docs.html` carries what the first two explained.
89
+
90
+ ## URL flags
91
+
92
+ `zero-tvm.html` accepts query flags for A/B-ing shader variants without rebuilding:
93
+
94
+ - `?sg=0` β€” disable all subgroup shaders (argmax / attention / QKV matmul)
95
+ - `?sgqkv=0` / `?sgattn=0` / `?sgargmax=0` β€” disable one at a time
96
+ - `?qkvtile=1` / `?qkvtile2=1` β€” opt into tiled QKV variants
97
+ - `?ffnsg=1` β€” opt into the tiled-subgroup fused FFN
98
+ - `?kv8=1` β€” opt into the int8 KV cache path
99
+
100
+ ## How it relates to a published paper
101
+
102
+ Zero-TVM is the LLM-decoding application of a broader thesis: **per-dispatch overhead is the dominant tax on browser GPU compute, and most workloads collapse from "needs a server" to "runs in a browser tab" once you fuse the loop.** Two preprints document the underlying technique:
103
+
104
+ - [Single-Kernel Fusion for Sequential Fitness Evaluation via WebGPU Compute Shaders](https://doi.org/10.5281/zenodo.19342888) (the foundational result)
105
+ - [Single-Kernel Fusion for Autoregressive Transformer Decoding via WebGPU Compute Shaders](https://doi.org/10.5281/zenodo.19344277) (the LLM application β€” the work this Space embodies)
106
+
107
+ ## Source / canonical site
108
+
109
+ - **GitHub**: [github.com/abgnydn/zero-tvm](https://github.com/abgnydn/zero-tvm)
110
+ - **Canonical site**: [zerotvm.com](https://zerotvm.com)
111
+ - **Research umbrella**: [kernelfusion.dev](https://kernelfusion.dev)
112
+ - **Companion benchmarks**: [gpubench.dev](https://gpubench.dev)
113
+
114
+ ## License
115
+
116
+ MIT. Reference comparison code (WebLLM compiler-chat path) belongs to its respective authors and is governed by their licenses.
117
+
118
+ ## Author
119
+
120
+ Ahmet Barış Günaydın · [barisgunaydin.com](https://barisgunaydin.com) · senior full-stack consultant by day, independent researcher by night.
_headers ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ # Cloudflare Pages headers (migrated from the old vercel.json).
2
+ # Cross-origin isolation is required for SharedArrayBuffer (WebGPU itself does not need it).
3
+ /*
4
+ Cross-Origin-Opener-Policy: same-origin
5
+ Cross-Origin-Embedder-Policy: credentialless
6
+
7
+ /assets/*
8
+ Cache-Control: public, max-age=31536000, immutable
_redirects ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Six pages deleted in the 2026-08-14 redesign, five of them in the sitemap
2
+ # Google is crawling at priority 0.6-0.9. Without these, every bookmark, README
3
+ # link and search result for them lands on a bare 404 and the accumulated link
4
+ # equity is dropped instead of passed to the successor page.
5
+ /demo / 301
6
+ /demo.html / 301
7
+ /architecture /docs 301
8
+ /architecture.html /docs 301
9
+ /shaders /docs#shaders 301
10
+ /shaders.html /docs#shaders 301
11
+ /dump /docs 301
12
+ /dump.html /docs 301
13
+ /compiler-chat /zero-tvm 301
14
+ /compiler-chat.html /zero-tvm 301
15
+ /webllm-bench /docs#vs-webllm 301
16
+ /webllm-bench.html /docs#vs-webllm 301
agent-host.html ADDED
@@ -0,0 +1,78 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <script type="module" crossorigin src="/assets/agent-host-eDUnNJBhJx.js"></script>
3
+ <link rel="modulepreload" crossorigin href="/assets/model-registry-BoK-ObgACo.js">
4
+ <link rel="modulepreload" crossorigin href="/assets/preload-helper-ckwbz45pO3.js">
5
+ <link rel="modulepreload" crossorigin href="/assets/weight-loader-ByOLMd9f5C.js">
6
+ <link rel="modulepreload" crossorigin href="/assets/model-select-DhhV9mCi0T.js">
7
+ <link rel="modulepreload" crossorigin href="/assets/variants-De-VVc9BRz.js">
8
+ <link rel="modulepreload" crossorigin href="/assets/engine-core-CLR3cxsqf4.js">
9
+ <link rel="modulepreload" crossorigin href="/assets/loading-ui-CjCwSRuXvh.js">
10
+ <meta charset="utf-8"><title>zero-tvm Β· agent host</title>
11
+ <meta name="viewport" content="width=device-width,initial-scale=1">
12
+ <!-- Not a public destination: it needs a local agent-server on 127.0.0.1:8017,
13
+ so a visitor arriving from search pays for weights and gets "booting…"
14
+ forever. share.html and 404.html already carry this; this page did not,
15
+ while robots.txt is Allow: / β€” so it was indexable. -->
16
+ <meta name="robots" content="noindex">
17
+ <!-- tokens.css FIRST: chat-ui.css is an alias layer over it, so without this
18
+ 14 custom properties were undefined here β€” including --accent, which made
19
+ `outline: 2px solid var(--accent)` invalid at computed-value time and so
20
+ suppressed the browser focus ring page-wide with nothing replacing it. -->
21
+ <link rel="stylesheet" href="/tokens.css">
22
+ <link rel="stylesheet" href="/chat-ui.css">
23
+ <style>
24
+ body { font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; margin: 0; padding: 2rem;
25
+ background: #0b0d10; color: #d6dbe1; }
26
+ h1 { font-size: 1.1rem; margin: 0 0 .25rem; letter-spacing: .02em }
27
+ .sub { color: #7d8590; margin-bottom: 1.5rem }
28
+ .card { border: 1px solid #21262d; border-radius: 8px; padding: 1rem 1.25rem; margin-bottom: 1rem;
29
+ background: #0f1216 }
30
+ #status { font-weight: 600 }
31
+ #status.ok { color: #3fb950 } #status.err { color: #f85149 }
32
+ #log { white-space: pre-wrap; color: #8b949e; font-size: 12.5px; max-height: 22rem; overflow: auto;
33
+ margin: 0 }
34
+ code { background: #161b22; padding: .1rem .35rem; border-radius: 4px; color: #d6dbe1 }
35
+ .warn { color: #d29922 }
36
+ #progress-wrap { display: none }
37
+ #progress-wrap.active { display: block }
38
+ #progress-bar { height: 6px; background: #d29922; width: 0%; border-radius: 3px; transition: width .2s }
39
+ #progress-status { margin-top: .6rem; font-size: 13px }
40
+ #progress-detail { color: #7d8590; font-size: 12px }
41
+ #progress-log { white-space: pre-wrap; color: #8b949e; font-size: 12px; max-height: 14rem;
42
+ overflow: auto; margin: .6rem 0 0 }
43
+ </style>
44
+ <h1>zero-tvm Β· agent host</h1>
45
+ <div class="sub">This tab runs the model. <code>scripts/agent-server.mjs</code> gives it an OpenAI-shaped address on :8017.
46
+ <br><small>Alternative, and the one to prefer for real use: <code>npm run station</code> runs the engine in-process on
47
+ dawn.node (no browser tab, no throttling) and takes the same :8017. They cannot both hold the port β€” run one.</small></div>
48
+
49
+ <div class="card">
50
+ <div id="status">booting…</div>
51
+ <div class="sub" style="margin:.35rem 0 0" id="model"></div>
52
+ </div>
53
+
54
+ <div class="card">
55
+ <div style="margin-bottom:.5rem">Point a client at <code>http://127.0.0.1:8017/v1</code></div>
56
+ <div class="sub" style="margin:0">
57
+ <b>Cline</b> β€” API Provider β€œOpenAI Compatible”, Base URL above, any model id.<br>
58
+ <b>pi</b> β€” <code>~/.pi/agent/models.json</code>, <code>api: "openai-completions"</code>, that baseUrl.<br>
59
+ <b>curl</b> β€” <code>curl 127.0.0.1:8017/v1/chat/completions -d '{"messages":[{"role":"user","content":"hi"}]}' -H 'content-type: application/json'</code>
60
+ </div>
61
+ </div>
62
+
63
+ <div class="card">
64
+ <div class="warn" style="margin-bottom:.5rem">Keep this tab visible.</div>
65
+ <div class="sub" style="margin:0">Chrome throttles background tabs hard β€” share.ts measured ~23 tok/s
66
+ backgrounded against ~65 focused. An agent run is long and unattended, which is exactly when a tab
67
+ stops being focused.</div>
68
+ </div>
69
+
70
+ <div class="card"><pre id="log"></pre></div>
71
+
72
+ <!-- loading-ui writes to progress-bar / -status / -detail / -log and reveals
73
+ this by adding .active. The bar was id="progress", there was no log sink,
74
+ and `hidden` is an attribute that .active cannot override β€” so a
75
+ multi-gigabyte download showed "booting…" and nothing else. Same
76
+ display:none + .active shape share.html already uses. -->
77
+ <div id="progress-wrap" class="card"><div id="progress-bar"></div><div id="progress-status"></div><div id="progress-detail"></div><pre id="progress-log"></pre></div>
78
+ <div id="badge" hidden></div>
assets/agent-host-eDUnNJBhJx.js ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/cache-probe-BYX_n9OPRB.js","assets/weight-loader-ByOLMd9f5C.js","assets/model-registry-BoK-ObgACo.js"])))=>i.map(i=>d[i]);
2
+ var te=Object.defineProperty;var ne=(e,t,n)=>t in e?te(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var P=(e,t,n)=>ne(e,typeof t!="symbol"?t+"":t,n);import{m as re}from"./model-registry-BoK-ObgACo.js";import{_ as oe}from"./preload-helper-ckwbz45pO3.js";import{bootEngine as ae}from"./loading-ui-CjCwSRuXvh.js";import{buildDecodeEngine as se,allocKVPages as ie}from"./engine-core-CLR3cxsqf4.js";import{parseVariantFlags as le}from"./variants-De-VVc9BRz.js";import{specFromSearch as ce,buildChatPromptFor as ue}from"./model-select-DhhV9mCi0T.js";import"./weight-loader-ByOLMd9f5C.js";const M=0xffffffffffffffffn,fe=0x243f6a8885a308d3n,me=0x13198a2e03707344n;function he(e){return e=(e^e>>30n)*0xbf58476d1ce4e5b9n&M,e=(e^e>>27n)*0x94d049bb133111ebn&M,(e^e>>31n)&M}function X(e,t){let n=e;for(let o=0;o<t.length;o++)n=he(n^BigInt(t[o]>>>0)+0x9e3779b97f4a7c15n&M);return n}function pe(e){const t=X(fe,e),n=X(me,e);return t.toString(16).padStart(16,"0")+n.toString(16).padStart(16,"0")}const de=new Set(["splitK"]),ge="zero-tvm/kv-prefix/1";function ye(e){const t=e.variants,n=Object.keys(t).filter(o=>!de.has(o)).sort().map(o=>` ${o}=${String(t[o])}`);return[ge,`spec=${e.specId}`,`weights=${e.weightRevision}`,`fused=${e.fused}`,`int8kv=${e.int8KV}`,"layers=all",`prefill=${e.prefillPath}`,`adapter=${e.adapter.vendor}|${e.adapter.architecture}`,"variants:",...n].join(`
3
+ `)}function we(e){const t=ye(e),n=new Array(t.length);for(let o=0;o<t.length;o++)n[o]=t.charCodeAt(o);return pe(n)}const be="zero-tvm-kvpool",F=2;function Y(e){const t={specId:e.spec.id,weightRevision:e.weightRevision,variants:e.variants,fused:e.fused,int8KV:e.int8KV,prefillPath:e.prefillPath,adapter:e.adapter};return we(t)}async function z(e,t){try{return await(await(await navigator.storage.getDirectory()).getDirectoryHandle(be,{create:t})).getDirectoryHandle(e,{create:t})}catch{return null}}async function $e(e,t){var f,s;const n=await e.exportKV();if(!n)return{tokens:0,reason:"engine state not poolable (MLA/int8/stage/invalid/mid-chunk)"};const o=await z(Y(t),!0);if(!o)return{tokens:0,reason:"OPFS unavailable"};const r={format:F,tokens:n.tokens,ids:n.ids,layerBytes:((f=n.layers[0])==null?void 0:f.byteLength)??0,scaleBytes:((s=n.scales[0])==null?void 0:s.byteLength)??0,layerCount:n.layers.length,gdnBytes:n.gdn.map(l=>l.byteLength),savedAt:Date.now()},a=await(await o.getFileHandle("entry.bin",{create:!0})).createWritable();for(const l of n.layers)await a.write(l);for(const l of n.scales)await a.write(l);for(const l of n.gdn)await a.write(l);await a.close();const i=await(await o.getFileHandle("meta.json",{create:!0})).createWritable();return await i.write(JSON.stringify(r)),await i.close(),{tokens:n.tokens}}async function xe(e,t,n){const o=await z(Y(t),!1);if(!o)return{restored:0,reason:"no entry for this fingerprint"};let r,a;try{r=JSON.parse(await(await(await o.getFileHandle("meta.json")).getFile()).text()),a=await(await(await o.getFileHandle("entry.bin")).getFile()).arrayBuffer()}catch{return{restored:0,reason:"no entry for this fingerprint"}}if(r.format!==F)return{restored:0,reason:`entry format ${r.format} != ${F}`};if(r.tokens>=n.length)return{restored:0,reason:"prompt does not extend the entry"};for(let u=0;u<r.tokens;u++)if(n[u]!==r.ids[u])return{restored:0,reason:`id mismatch at ${u}`};const i=(r.layerBytes+r.scaleBytes)*r.layerCount+r.gdnBytes.reduce((u,g)=>u+g,0);if(a.byteLength!==i)return{restored:0,reason:"entry truncated"};const f=[];let s=0;for(let u=0;u<r.layerCount;u++)f.push(a.slice(s,s+r.layerBytes)),s+=r.layerBytes;const l=[],h=[];if(r.scaleBytes>0)for(let u=0;u<r.layerCount;u++)h.push(a.slice(s,s+r.scaleBytes)),s+=r.scaleBytes;for(const u of r.gdnBytes)l.push(a.slice(s,s+u)),s+=u;return e.importKV({tokens:r.tokens,ids:r.ids,layers:f,scales:h,gdn:l})?{restored:r.tokens}:{restored:0,reason:"engine refused the snapshot"}}function _(e){if(e==null)return"null";if(Array.isArray(e))return`[${e.map(_).join(", ")}]`;if(typeof e=="object"){const t=e,n=[];for(const o of Object.keys(t))t[o]!==void 0&&n.push(`${JSON.stringify(o)}: ${_(t[o])}`);return`{${n.join(", ")}}`}return JSON.stringify(e)}const ke=`# Tools
4
+
5
+ You may call one or more functions to assist with the user query.
6
+
7
+ You are provided with function signatures within <tools></tools> XML tags:
8
+ <tools>`,ve=`
9
+ </tools>
10
+
11
+ For each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:
12
+ <tool_call>
13
+ {"name": <function-name>, "arguments": <args-json-object>}
14
+ </tool_call>`,_e=`# Tools
15
+
16
+ You have access to the following functions:
17
+
18
+ <tools>`,Oe=`
19
+ </tools>
20
+
21
+ If you choose to call a function ONLY reply in the following format with NO suffix:
22
+
23
+ <tool_call>
24
+ <function=example_function_name>
25
+ <parameter=example_parameter_1>
26
+ value_1
27
+ </parameter>
28
+ <parameter=example_parameter_2>
29
+ This is the value for the second parameter
30
+ that can span
31
+ multiple lines
32
+ </parameter>
33
+ </function>
34
+ </tool_call>
35
+
36
+ <IMPORTANT>
37
+ Reminder:
38
+ - Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags
39
+ - Required parameters MUST be specified
40
+ - You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after
41
+ - If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls
42
+ </IMPORTANT>`,Se=`Given the following functions, please respond with a JSON for a function call with its proper arguments that best answers the given prompt.
43
+
44
+ Respond in the format {"name": function name, "parameters": dictionary of argument name and its value}.Do not use variables.
45
+
46
+ `;function Le(e,t,n){var s;if(n.length===0)return t.slice();const o=n.map(l=>`
47
+ ${_(l)}`).join("");if(e==="llama3"){const l=t.slice(),h=l.findIndex(u=>u.role==="user");if(h<0)throw new Error("Cannot put tools in the first user message when there is no user message");const $=n.map(u=>`${JSON.stringify(u,null,4)}
48
+
49
+ `).join("");return l[h]={role:"user",content:Se+$+l[h].content.trim()},l}const r=e==="chatml-xml"?_e+o+Oe:ke+o+ve,a=t.slice(),i=((s=a[0])==null?void 0:s.role)==="system"?a[0].content:null;let f;if(e==="chatml-xml"){const l=(i??"").trim();f=l?`${r}
50
+
51
+ ${l}`:r}else f=i?`${i}
52
+
53
+ ${r}`:r;return i===null?a.unshift({role:"system",content:f}):a[0]={role:"system",content:f},a}function Ee(e,t,n){if(n.length===0)return t;if(e==="llama3"){if(n.length>1)throw new Error("llama3 renders a single tool call per assistant message");return`{"name": "${n[0].name}", "parameters": ${_(n[0].arguments)}}`}if(e==="chatml-xml"){let r=t.trim();return n.forEach((a,i)=>{r+=i===0?r?`
54
+
55
+ `:"":`
56
+ `,r+=`<tool_call>
57
+ <function=${a.name}>
58
+ `;for(const[f,s]of Object.entries(a.arguments))r+=`<parameter=${f}>
59
+ ${typeof s=="string"?s:_(s)}
60
+ </parameter>
61
+ `;r+=`</function>
62
+ </tool_call>`}),r}let o=t;return n.forEach((r,a)=>{(a>0||t)&&(o+=`
63
+ `),o+=`<tool_call>
64
+ {"name": "${r.name}", "arguments": ${_(r.arguments)}}
65
+ </tool_call>`}),o}const B="<tool_call>",K="</tool_call>";function G(e,t){const n=e.indexOf("{",t);if(n<0)return null;let o=0,r=!1,a=!1;for(let i=n;i<e.length;i++){const f=e[i];if(r){a?a=!1:f==="\\"?a=!0:f==='"'&&(r=!1);continue}if(f==='"')r=!0;else if(f==="{")o++;else if(f==="}"&&--o===0)return{start:n,end:i+1}}return{start:n,end:-1}}function Te(e,t,n){const o=Math.min(n.length-1,e.length-t);for(let r=o;r>0;r--)if(e.endsWith(n.slice(0,r)))return r;return 0}function Q(e,t){if(typeof e!="object"||e===null||Array.isArray(e))return{name:"",arguments:{},raw:t,error:"tool call is not a JSON object"};const n=e;if(typeof n.name!="string"||n.name==="")return{name:"",arguments:{},raw:t,error:'tool call has no "name"'};let o=n.arguments??n.parameters;if(typeof o=="string")try{o=JSON.parse(o)}catch{}return o==null?{name:n.name,arguments:{},raw:t}:typeof o!="object"||Array.isArray(o)?{name:n.name,arguments:{},raw:t,error:"tool call arguments are not an object"}:{name:n.name,arguments:o,raw:t}}function Ce(e){const t=G(e,0);if(!t||t.end<0)return{name:"",arguments:{},raw:e,error:"no JSON object in <tool_call> block"};const n=e.slice(t.start,t.end);let o;try{o=JSON.parse(n)}catch(r){return{name:"",arguments:{},raw:e,error:`invalid JSON in <tool_call> block: ${r.message}`}}return Q(o,e)}function Ae(e){const t=e.indexOf("<function="),n=t<0?-1:e.indexOf(">",t);if(t<0||n<0)return{name:"",arguments:{},raw:e,error:"no <function=...> in <tool_call> block"};const o=e.slice(t+10,n),r=e.indexOf("</function>",n);if(r<0)return{name:o,arguments:{},raw:e,error:"unterminated <function=...> block"};const a={},i=e.slice(n+1,r);let f=0;for(;;){const s=i.indexOf("<parameter=",f);if(s<0)break;const l=i.indexOf(">",s);if(l<0)return{name:o,arguments:a,raw:e,error:"unterminated <parameter=...> tag"};let h=-1;for(let g=i.indexOf("</parameter>",l);g>=0;g=i.indexOf("</parameter>",g+1)){const C=i.slice(g+12).trim();if(C===""||C.startsWith("<parameter=")){h=g;break}}if(h<0)return{name:o,arguments:a,raw:e,error:"unterminated <parameter> value"};const $=i.slice(s+11,l);let u=i.slice(l+1,h);u.startsWith(`
66
+ `)&&(u=u.slice(1)),u.endsWith(`
67
+ `)&&(u=u.slice(0,-1)),a[$]=Ne(u),f=h+12}return{name:o,arguments:a,raw:e}}function Ne(e){try{const t=JSON.parse(e);return typeof t=="string"?e:t}catch{return e}}function Re(e,t){return e==="llama3"?Pe(t):Me(e,t)}function Me(e,t){const n=[];let o="",r=!1,a=0;for(;a<t.length;){const i=t.indexOf(B,a);if(i<0){const h=Te(t,a,B);o+=t.slice(a,t.length-h),r=h>=2;break}o+=t.slice(a,i);const f=i+B.length,s=t.indexOf(K,f);if(s<0){r=!0;break}const l=t.slice(f,s);n.push(e==="chatml-xml"?Ae(l):Ce(l)),a=s+K.length}return{text:o,calls:n,partial:r}}function Pe(e){const t=[];let n="",o=!1,r=0;for(;r<e.length;){const a=G(e,r);if(!a){n+=e.slice(r);break}let i=e.slice(r,a.start);if(i.endsWith("<|python_tag|>")&&(i=i.slice(0,-14)),a.end<0){n+=i,o=!0;break}const f=e.slice(a.start,a.end);let s=null;try{const l=Q(JSON.parse(f),f);l.error||(s=l)}catch{}n+=s?i:e.slice(r,a.end),s&&t.push(s),r=a.end}return{text:n,calls:t,partial:o}}const R=new URL(location.href).searchParams.get("server")??"http://127.0.0.1:8017",J=e=>document.getElementById(e),T=(e,t="")=>{const n=J("status");n.textContent=e,n.className=t},w=e=>{const t=J("log");t.textContent=`${new Date().toLocaleTimeString()} ${e}
68
+ ${t.textContent}`.split(`
69
+ `).slice(0,200).join(`
70
+ `)},m=ce(location.search),U=re(m);J("model").textContent=`${U.name} β€” ${U.params}`;const Be=new Set(["qwen36","qwen3-8-27b"]);function Fe(e){return e==="llama3"?"llama3":[...Be].some(t=>m.id.startsWith(t))?"chatml-xml":"chatml-json"}async function Je(){const{isModelCached:e}=await oe(async()=>{const{isModelCached:n}=await import("./cache-probe-BYX_n9OPRB.js");return{isModelCached:n}},__vite__mapDeps([0,1,2]));if(await e(m))return;const t=document.createElement("dialog");t.id="agent-gate",t.style.cssText="width:calc(100vw - 2rem);max-width:32rem;background:#0f1216;color:#d6dbe1;border:1px solid #21262d;border-radius:10px;padding:1.5rem 1.6rem;font:inherit",t.innerHTML=`
71
+ <h2 style="font-size:1rem;margin:0 0 .7rem">Run ${m.id} in this tab?</h2>
72
+ <p style="color:#8b949e;margin:0 0 .7rem">
73
+ This is a developer surface. It downloads the model
74
+ and then talks to an agent-server on <code>127.0.0.1:8017</code> β€”
75
+ start it with <code>npm run agent</code> from the repo. Without that
76
+ server running, this page has nothing to serve.
77
+ </p>
78
+ <p class="warn" style="margin:0 0 1.1rem">Weights download once and cache locally.</p>
79
+ <div style="display:flex;gap:.8rem;justify-content:flex-end">
80
+ <a href="/" style="color:#7d8590">Not now</a>
81
+ <button type="button" id="agent-gate-go" autofocus
82
+ style="background:#d29922;color:#0b0d10;border:0;border-radius:8px;padding:.55rem 1.1rem;
83
+ font:inherit;font-weight:600;cursor:pointer">Download &amp; boot</button>
84
+ </div>`,document.body.appendChild(t),t.showModal(),await new Promise(n=>{var o;(o=t.querySelector("#agent-gate-go"))==null||o.addEventListener("click",()=>{t.close(),t.remove(),n()},{once:!0})})}async function Ie(){let e=null;if(!("gpu"in navigator)){T("cannot boot: this browser has no WebGPU β€” Chrome and Edge ship it","err");return}await Je();const t=await ae({spec:m,optionalFeatures:["subgroups","chromium-experimental-subgroup-matrix"],probeSubgroups:!0,buildEngine:({device:c,weights:d,sgSizeOk:O,spec:S})=>(e=le(location.search,{hasSubgroupsFeature:c.features.has("subgroups"),sgSizeOk:O}),se(c,d,ie(c,S),{spec:S,variants:e}))});if(!t.ok)throw T(`cannot boot: ${t.reason}`,"err"),new Error(t.reason);const{engine:n,tokenizer:o}=t,r=Fe(m.chatTemplateId),a=new URL(location.href).searchParams.get("pool")!=="0",i=()=>({spec:m,weightRevision:m.hfRepo,variants:e,fused:!1,int8KV:!1,prefillPath:!m.moe&&e.subgroups?"chunked":"per-token",adapter:{vendor:"browser",architecture:"webgpu"}});let f=!1,s=null,l=null;const h=()=>{a&&(s!==null&&clearTimeout(s),s=self.setTimeout(()=>{s=null,!g&&(l=(async()=>{try{const c=await $e(n,i());c.tokens&&w(`pool: saved ${c.tokens} tokens`)}catch(c){w(`pool: save failed (${c.message})`)}})().finally(()=>{l=null}))},1500))};T(`ready β€” ${m.maxContext.toLocaleString()} token context, ${r}`,"ok"),w(`engine up: ${m.id}, dialect ${r}`);class ${constructor(d){P(this,"buf","");P(this,"timer",null);this.id=d}push(d){this.buf+=d,this.timer===null&&(this.timer=self.setTimeout(()=>{this.timer=null,this.flush()},60))}async flush(){if(this.timer!==null&&(clearTimeout(this.timer),this.timer=null),!this.buf)return;const d=this.buf;this.buf="",await u({type:"delta",id:this.id,text:d})}}async function u(c){try{await fetch(`${R}/agent/emit`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(c)})}catch(d){w(`emit failed: ${d.message}`)}}let g=!1;async function C(c){var S,I;if(g){await u({type:"error",id:c.id,message:"busy: this tab serves one request at a time"});return}g=!0,s!==null&&(clearTimeout(s),s=null),l&&await l;const d=performance.now(),O=new $(c.id);try{let x=c.messages.map(p=>{var E;if(p.role!=="assistant"||!((E=p.toolCalls)!=null&&E.length))return{role:p.role,content:p.content};const k=p.toolCalls.map(v=>{var j,q;let W={};try{W=JSON.parse(((j=v.function)==null?void 0:j.arguments)||"{}")}catch{}return{name:((q=v.function)==null?void 0:q.name)??"",arguments:W}});return{role:p.role,content:Ee(r,p.content??"",k)}});(S=c.tools)!=null&&S.length&&(x=Le(r,x,c.tools));const y=ue(m,x,o);if(a&&!f){f=!0;const p=await xe(n,i(),y);w(p.restored?`pool: restored ${p.restored} tokens`:`pool: miss (${p.reason})`)}if(y.length>=m.maxContext)throw new Error(`prompt is ${y.length} tokens, context is ${m.maxContext}. Lower the client's context window, or use a spec with a bigger one (qwen35 and llama32 are 32,768).`);const D=m.maxContext-y.length,H=Math.max(1,Math.min(c.maxTokens??D,D));n.setSampling(c.temperature!=null&&c.temperature>0?{temperature:c.temperature,topP:c.topP??1}:null);let b="";const A={stop:null},L=await n.generatePipelined(y,H,p=>{var E;const k=o.decode([p]);if(b+=k,O.push(k),(E=c.stop)!=null&&E.length){for(const v of c.stop)if(v&&b.endsWith(v)){A.stop=v;break}}},()=>A.stop!==null);await O.flush(),A.stop&&(b=b.slice(0,b.length-A.stop.length));const V=(I=c.tools)!=null&&I.length?Re(r,b):{text:b,calls:[]},N=V.calls.map((p,k)=>({id:`call_${c.id.slice(0,8)}_${k}`,type:"function",function:{name:p.name,arguments:JSON.stringify(p.arguments??{})}})),ee=(performance.now()-d)/1e3;w(`${c.id.slice(0,8)} ${y.length} in / ${L.length} out ${(L.length/ee).toFixed(1)} tok/s`+(N.length?` ${N.length} tool call(s)`:"")),await u({type:"done",id:c.id,text:V.text,toolCalls:N,finishReason:N.length?"tool_calls":L.length>=H?"length":"stop",usage:{prompt_tokens:y.length,completion_tokens:L.length,total_tokens:y.length+L.length}})}catch(x){await O.flush(),w(`${c.id.slice(0,8)} ERROR ${x.message}`),await u({type:"error",id:c.id,message:x.message})}finally{g=!1,h()}}function Z(){const c=new EventSource(`${R}/agent/jobs?model=${encodeURIComponent(m.id)}`);c.onopen=()=>{T(`hosting ${m.id} β†’ ${R}/v1`,"ok"),w("connected to agent-server")},c.onmessage=d=>{C(JSON.parse(d.data))},c.onerror=()=>{T(`agent-server unreachable at ${R} β€” retrying`,"err")}}Z()}Ie();
assets/cache-probe-BYX_n9OPRB.js ADDED
@@ -0,0 +1 @@
 
 
1
+ import{p as s,a as l,o as f}from"./weight-loader-ByOLMd9f5C.js";import{o as c}from"./model-registry-BoK-ObgACo.js";async function g(t){return t.weightFormat!=="mlx-safetensors",!1}async function y(t){var n;if(typeof navigator>"u"||!((n=navigator.storage)!=null&&n.getDirectory))return!1;try{const o=await(await navigator.storage.getDirectory()).getDirectoryHandle(c(t));if(t.weightFormat==="mlx-safetensors"){const e=s(t),r=e[e.length-1];await o.getFileHandle(l(r.plan,r.layer))}else{const e=await o.getFileHandle(t.manifestName??"ndarray-cache.json"),r=JSON.parse(await(await e.getFile()).text()),i=[...new Set((r.records??[]).map(a=>a.dataPath).filter(a=>!!a))];if(i.length===0)return!1;for(const a of i)await o.getFileHandle(f(a))}return!0}catch{return await g(t)}}export{y as isModelCached};
assets/chat-flow-eTAzKAlIYe.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ import{buildChatPromptFor as B}from"./model-select-DhhV9mCi0T.js";import{allocKVPagesInt8 as ce,allocKVPages as de,buildDecodeEngine as ue}from"./engine-core-CLR3cxsqf4.js";import{parseVariantFlags as he}from"./variants-De-VVc9BRz.js";import{bootEngine as pe,setBadge as _,log as O}from"./loading-ui-CjCwSRuXvh.js";import{s as fe,q as me,a as Y,w as ge,b as ee,c as te,o as ke}from"./chat-ui-CQAxV1po6S.js";import{m as we}from"./model-registry-BoK-ObgACo.js";function be(t,o,u=3,l=3){let i=o,p=0,g=0,r=0;for(;i<t.length;){if(p++,i<u){i++;continue}const e=i-u;let c=-1;for(let a=e-1;a>=0;a--){if(a+u>e)continue;let m=!0;for(let d=0;d<u;d++)if(t[a+d]!==t[e+d]){m=!1;break}if(m){c=a;break}}if(c<0){i++;continue}r++;let s=0;for(let a=0;a<l;a++){const m=c+u+a,d=i+a;if(m>=t.length||d>=t.length||t[m]!==t[d])break;s++}g+=s,i+=1+s}return{totalSteps:p,totalAccepted:g,totalHits:r}}function ye(t,o,u,l=3,i=3){const{totalSteps:p,totalAccepted:g,totalHits:r}=be(o,u,l,i),e=p>0?g/p:0,c=i>0?e/i:0,s=2*(1+e)/(i+1),a=p>0?r/p:0;return{name:t,N:l,K:i,totalSteps:p,totalAccepted:g,meanAcceptedPerStep:e,acceptanceRate:c,theoreticalSpeedup:s,hitRate:a}}const xe="Section %d. The transport of water through a plant begins at the root hairs, where dissolved minerals enter by active transport and water follows by osmosis. Cohesion between water molecules and adhesion to the xylem walls maintain a continuous column that is pulled upward as vapour escapes from the stomata. The rate of that escape depends on humidity, temperature, and wind, so a plant that closes its stomata to conserve water also limits the carbon dioxide available to its chloroplasts. ";function q(t){let o="";for(let u=1;u<=t;u++)o+=xe.replace("%d",String(u));return o}function $e(t,o,u=4){const i=o(q(4)).length,p=o("").length,g=Math.max(1,(i-p)/4);let r=Math.max(0,Math.round((t-p)/g)),e=o(q(r));for(let a=0;a<200&&Math.abs(e.length-t)>u&&!(e.length>t&&r===0||(r+=e.length<t?1:-1,e=o(q(r)),r===0));a++);let c=q(r),s=c.split(" ");for(let a=0;a<400&&e.length>t+u&&s.length>1;a++)s=s.slice(0,-1),c=s.join(" "),e=o(c);for(let a=0;a<400&&e.length<t-u;a++)c+=" water",e=o(c);return{userMessage:c,actualTokens:e.length,blocks:r}}const Se=[64,256,1024,2048];function ve(t){const{engine:o,tokenizer:u}=t;window.bench=async(l=128,i=3,p=!1)=>{if(t.isBusy())return console.warn("[bench] decode already in flight β€” skipping"),null;t.setBusy(!0);try{const r=[{role:"system",content:"You are a helpful assistant."},{role:"user",content:"Write a four-sentence explanation of how photosynthesis works."}],e=B(o.spec,r,u);if(p){const f=await o.profileStep(e);return f?(console.log(`[profile] total = ${f.totalMs.toFixed(3)} ms/token across ${f.kernels.reduce((w,T)=>w+T.calls,0)} dispatches`),console.table(f.kernels.map(w=>({kernel:w.label,calls:w.calls,"ms/token":+w.totalMs.toFixed(3),"ms/call":+(w.totalMs/w.calls).toFixed(4),"% total":+w.pctOfTotal.toFixed(1)}))),f):(console.warn("[bench] profile requested but timestamp-query feature unavailable"),null)}const c=[];for(let f=0;f<=i;f++){const w=f===0?"warmup":`run${f}/${i}`;o.resetKVTracking();const T=performance.now();let $=0,P=0;await o.generatePipelined(e,l,()=>{$===0&&(P=performance.now()),$++});const C=performance.now(),A=(C-T)/1e3,R=$/A,D=P?P-T:A*1e3,z=$>1&&P?($-1)/((C-P)/1e3):R;console.log(`[bench] ${w}: ${$} tok / ${A.toFixed(2)}s = ${R.toFixed(2)} tok/s total Β· ttft ${D.toFixed(0)}ms Β· decode ${z.toFixed(2)} tok/s`),f>0&&c.push({tokens:$,seconds:A,tokPerS:R,ttftMs:D,decodeTokPerS:z})}const s=f=>f.slice().sort((w,T)=>w-T)[Math.floor(f.length/2)],a=c.map(f=>f.tokPerS).sort((f,w)=>f-w),m=a[Math.floor(a.length/2)],d=a.reduce((f,w)=>f+w,0)/a.length,b=a[0],S=a[a.length-1],k=s(c.map(f=>f.decodeTokPerS)),x=s(c.map(f=>f.ttftMs)),y={runs:c,median:m,mean:d,min:b,max:S,medianDecode:k,medianTtftMs:x};return console.log(`[bench] summary: median=${m.toFixed(2)} mean=${d.toFixed(2)} min=${b.toFixed(2)} max=${S.toFixed(2)} tok/s Β· decode=${k.toFixed(2)} tok/s Β· ttft=${x.toFixed(0)}ms`),y}finally{t.setBusy(!1),t.onIdle()}},window.benchTtft=async(l=Se,i=8,p=3)=>{if(t.isBusy())return console.warn("[ttft] decode already in flight β€” skipping"),null;t.setBusy(!0);try{const g=s=>s.slice().sort((a,m)=>a-m)[Math.floor(s.length/2)],r=s=>B(o.spec,[{role:"system",content:"You are a helpful assistant."},{role:"user",content:s}],u),e=[];for(const s of l){const a=$e(s,r),m=r(a.userMessage);if(m.length>o.maxContext){console.warn(`[ttft] target ${s}: ${m.length} tok exceeds maxContext ${o.maxContext} β€” skipped`);continue}const d=[],b=[];let S=0;o.resetKVTracking(),await o.generatePipelined(m,i,()=>{});for(let k=0;k<p;k++){o.resetKVTracking();const x=performance.now();let y=0,f=0;await o.generatePipelined(m,i,()=>{y===0&&(f=performance.now()),y++});const w=performance.now(),T=(w-x)/1e3,$=f?f-x:T*1e3,P=y>1&&f?(y-1)/((w-f)/1e3):y/T,C=o.getLastPrefill();C&&(S=C.chunks),d.push($),b.push(P),console.log(`[ttft] prompt=${m.length} tok (target ${s}) run${k+1}: ttft ${$.toFixed(0)}ms Β· ${y} gen tok Β· decode ${P.toFixed(2)} tok/s`+(C?` Β· prefill chunks ${C.chunks} (reused ${C.reused})`:""))}e.push({targetTokens:s,promptTokens:m.length,ttftMsRuns:d,ttftMs:g(d),decodeTokPerS:g(b),chunks:S})}console.log(""),console.log(" prompt tok | TTFT ms | ms/prompt-token | vs shortest | chunks"),console.log(" -----------+----------+------------------+--------------+-------");const c=e[0];for(const s of e)console.log(` ${String(s.promptTokens).padStart(10)} | ${s.ttftMs.toFixed(0).padStart(8)} | ${(s.ttftMs/s.promptTokens).toFixed(3).padStart(16)} | ${(s.ttftMs/c.ttftMs).toFixed(2).padStart(12)}x | ${String(s.chunks).padStart(6)}`);return{mode:"ttft-sweep",engine:"zero-tvm",runsPerPoint:p,genTokensPerPoint:i,points:e}}finally{t.setBusy(!1),t.onIdle()}},window.benchBatched=async(l=4,i=500,p="ffnDown")=>t.isBusy()?(console.warn("[batched-bench] decode in flight β€” skipping"),null):o.benchBatchedFfnDown(l,i,p),window.specSim=async(l=160,i=3,p=3)=>{if(t.isBusy())return console.warn("[specSim] decode in flight β€” skipping"),null;t.setBusy(!0);try{const g=[{name:"prose",messages:[{role:"system",content:"You are a helpful assistant."},{role:"user",content:"Write a four-sentence explanation of how photosynthesis works."}]},{name:"code",messages:[{role:"system",content:"You are a helpful coding assistant. Reply with only code, no prose."},{role:"user",content:"Write a TypeScript function that computes the nth Fibonacci number using memoization. Include a short test block that prints fib(10), fib(20), and fib(30)."}]},{name:"summary",messages:[{role:"system",content:"You are a helpful assistant."},{role:"user",content:"List the top five considerations when designing a REST API, with a one-sentence explanation for each."}]}],r=[];for(const e of g){const c=B(o.spec,e.messages,u),s=[];await o.generatePipelined(c,l,d=>{s.push(d)});const a=[...c,...s],m=ye(e.name,a,c.length,i,p);r.push({...m,generatedTokens:s.length}),console.log(`[specSim] ${e.name}: ${s.length} gen tok, hit=${(m.hitRate*100).toFixed(0)}%, Ξ±=${(m.acceptanceRate*100).toFixed(0)}%, accepted/step=${m.meanAcceptedPerStep.toFixed(2)}/${p}, theoretical speedup=${m.theoreticalSpeedup.toFixed(2)}Γ—`)}return console.table(r.map(e=>({prompt:e.name,gen:e.generatedTokens,"hit %":+(e.hitRate*100).toFixed(1),"accept Ξ±":+(e.acceptanceRate*100).toFixed(1),"accepted/step":+e.meanAcceptedPerStep.toFixed(2),speedup:+e.theoreticalSpeedup.toFixed(2)}))),r}finally{t.setBusy(!1),t.onIdle()}},window.benchPrefill=async(l=800,i=3)=>{var p;if(t.isBusy())return console.warn("[benchPrefill] decode in flight β€” skipping"),null;t.setBusy(!0);try{const g="Photosynthesis is the process by which green plants convert light energy into chemical energy, storing it in the bonds of glucose molecules that fuel growth, respiration, and reproduction. ";let r=4,e=[];const c=d=>B(o.spec,[{role:"system",content:"You are a helpful assistant."},{role:"user",content:g.repeat(d)+`
2
+ Summarize the passage above in one sentence.`}],u);for(e=c(r);e.length<l;)r=Math.max(r+1,Math.ceil(r*l/e.length)),e=c(r);console.log(`[benchPrefill] prompt: ${e.length} tokens (target ${l})`);const s=[];for(let d=0;d<=i;d++){o.resetKVTracking();const b=performance.now();await o.generatePipelined(e,1,()=>{});const S=performance.now()-b,k=e.length/(S/1e3),x=d===0?"warmup":`run${d}/${i}`;console.log(`[benchPrefill] ${x}: ${S.toFixed(0)} ms to first token = ${k.toFixed(1)} prefill tok/s`),d>0&&s.push({ttftMs:S,tokPerS:k})}const a=s.map(d=>d.tokPerS).sort((d,b)=>d-b),m=a[Math.floor(a.length/2)];return console.log(`[benchPrefill] median: ${m.toFixed(1)} prefill tok/s (${((p=o.getLastPrefill())==null?void 0:p.chunks)??0} chunks/run)`),{promptTokens:e.length,runs:s,median:m,lastPrefill:o.getLastPrefill()}}finally{t.setBusy(!1),t.onIdle()}},window.benchTurns=async(l=120)=>{if(t.isBusy())return console.warn("[benchTurns] decode in flight β€” skipping"),null;t.setBusy(!0);try{const i="The city library reopened after a two-year renovation that added a glass atrium, a children's wing, quiet study pods, and a rooftop reading garden overlooking the river. ",p=[`Here is some background material: ${i.repeat(9)}
3
+ What changed in the renovation? Answer in detail.`,`More context: ${i.repeat(9)}
4
+ How do the study pods differ from the reading garden?`,"Now, in one short sentence: what overlooks the river?"];o.resetKVTracking();const g=[{role:"system",content:"You are a helpful, concise assistant."}],r=[];for(let e=0;e<p.length;e++){g.push({role:"user",content:p[e]});const c=B(o.spec,g,u),s=[],a=performance.now();let m=0;await o.generatePipelined(c,l,b=>{s.length===0&&(m=performance.now()-a),s.push(b)}),g.push({role:"assistant",content:u.decode(s)});const d=o.getLastPrefill();console.log(`[benchTurns] turn ${e+1}: prompt ${c.length} tok, reused ${(d==null?void 0:d.reused)??0}, chunks ${(d==null?void 0:d.chunks)??0}, TTFT ${m.toFixed(0)} ms, generated ${s.length}`),r.push({turn:e+1,promptTokens:c.length,ttftMs:m,...d})}return r}finally{t.setBusy(!1),t.onIdle()}},window.checkReuse=async()=>{var l;if(t.isBusy())return console.warn("[checkReuse] decode in flight β€” skipping"),null;t.setBusy(!0);try{o.resetKVTracking();const i=[{role:"system",content:"You are a helpful, concise assistant."},{role:"user",content:"Name three primary colors."}],p=B(o.spec,i,u),g=[];await o.generatePipelined(p,48,a=>g.push(a)),i.push({role:"assistant",content:u.decode(g)}),i.push({role:"user",content:"Which of those is the warmest?"});const r=B(o.spec,i,u),e=await o.debugCompareReuse(r),c=(((l=o.getLastPrefill())==null?void 0:l.chunks)??0)>0,s=e.maxAbsDiff===0?" (bit-exact βœ“)":c?" (nonzero: prefix built by CHUNKED prefill vs per-token replay β€” kernel-variant numerics; rerun with ?chunk=0 for the bit-exact assertion)":" (MISMATCH β€” reuse rules broken!)";return console.log(`[checkReuse] prompt ${e.promptLen} tok, reused prefix ${e.startPos} β€” logits max|Ξ”| = ${e.maxAbsDiff} mean|Ξ”| = ${e.meanAbsDiff}`+s),{...e,chunkedPrefix:c}}finally{t.setBusy(!1),t.onIdle()}},console.log("[bench] harness ready β€” call `await bench(128, 3)` or `await bench(0, 0, true)` for per-kernel profile"),console.log("[bench] prefill: `await benchPrefill(800, 3)` Β· multi-turn TTFT: `await benchTurns()` Β· reuse assertion: `await checkReuse()`"),console.log("[bench] batched primitive: `await benchBatched(4, 500)` β€” ffnDown weight-reuse falsifiability test"),console.log("[bench] PLD acceptance sim: `await specSim(160, 3, 3)` β€” measures spec-decode upside without GPU changes")}const F=t=>document.getElementById(t);function ne(t){const o=F("stats");o&&(o.textContent=t)}function Le(t,o){const u=F("loading-error");if(u){if(u.textContent=t,o){const l=document.createElement("button");l.type="button",l.id="retry-download-btn",l.textContent="Retry download",l.style.cssText="display:block;margin-top:0.6rem;padding:0.45rem 1.1rem;background:var(--accent);color:#000;font-weight:600;font-size:0.8rem;border:none;border-radius:6px;cursor:pointer",l.addEventListener("click",()=>{u.classList.remove("visible"),u.replaceChildren(),o()},{once:!0}),u.appendChild(l)}u.classList.add("visible")}}function Ee(t){const{spec:o}=t,u=t.search??location.search,l=t.poolSlots??0;return pe({spec:o,...l?{expertPool:l}:{},optionalFeatures:["subgroups","timestamp-query","chromium-experimental-subgroup-matrix"],probeSubgroups:!0,onDeviceLost:t.onDeviceLost,buildEngine:({device:i,weights:p,sgSizeOk:g,spec:r})=>{const e=he(u,{hasSubgroupsFeature:i.features.has("subgroups"),sgSizeOk:g}),c=!r.qkNorm&&r.weightFormat!=="mlx-safetensors",s=e.int8KV&&!r.mla;e.int8KV&&!s&&O(`?kv8=1 ignored for ${r.id} β€” int8 KV does not cover MLA, which caches a latent rather than per-head K/V`);const a=Math.round(r.maxContext*r.kvBytesPerToken/(1024*1024)/(s?2:1));O(`Allocating ${s?"int8 ":""}KV cache β€” ${r.maxPages} pages Γ— ${r.pageSize} = ${r.maxContext} tokens Β· ~${a} MiB`);const m=s?ce(i,r):de(i,r);O("Building decode engine…");const d=new URLSearchParams(u),b=(k,x)=>{const y=d.get(k);if(y===null)return x;const f=Number(y);return Number.isFinite(f)?f:x},S=b("temp",0);return S>0&&O(`Sampling: temperature ${S}, top-p ${b("topp",1)}, min-p ${b("minp",0)}`),ue(i,p,m,{variants:e,fused:c,int8KV:s,spec:r,...l?{expertPool:l}:{},prefixReuse:d.get("reuse")!=="0",chunkedPrefill:d.get("chunk")!=="0",pooledChunkedPrefill:d.get("chunk")==="1",sampling:S>0?{temperature:S,topP:b("topp",1),minP:b("minp",0),seed:b("seed",0)}:void 0})},warmup:async(i,p,g)=>{const r=B(o,[{role:"user",content:"Hi."}],p),e=o.moe?r.slice(0,1):r,c=performance.now();g==null||g(`Warming up pipeline β€” ${e.length} token(s)${o.moe?" (single-pass: MoE)":""}…`),await i.generatePipelined(e,1,()=>{}),g==null||g(`Warmup done in ${((performance.now()-c)/1e3).toFixed(1)}s`)}})}function Ie(t){var J;const{spec:o,tokenizer:u,engine:l}=t,i=we(o),p=t.log??O,g=t.onPhase??(()=>{});fe(i.name,me(o));const r=F("composer"),e=F("inp"),c=F("btn"),s=F("stop-btn"),a=F("new-chat-btn"),m=F("suggest-grid"),d=F("ctx-hint"),b=F("welcome");if(!r||!e||!c||!s){console.error("[zero-tvm] chat UI elements missing");return}const S="You are a helpful, concise assistant. Use Markdown (numbered lists, **bold**, and fenced ```code``` blocks with a language tag) when it clarifies the answer.\n\nYou are running inside zero-tvm (zerotvm.com): an LLM inference engine written by hand in WGSL and TypeScript, running entirely in this browser tab on the user's own GPU through WebGPU β€” no server, no WebLLM, no TVM. Model weights download once from Hugging Face and cache on this device; prompts, generated tokens and the KV cache never leave the machine. The whole forward pass is 10 hand-written WGSL kernel roles, readable end-to-end. If asked what zero-tvm is, answer from this paragraph.",k=[{role:"system",content:S}];let x=!1,y=!1;const f=`zt-chat-${o.id}`;function w(){try{const n=k.filter(h=>h.role!=="system").slice(-40);localStorage.setItem(f,JSON.stringify(n))}catch{}}function T(){let n=[];try{n=JSON.parse(localStorage.getItem(f)??"[]")}catch{return}!Array.isArray(n)||n.length===0||(n.forEach((h,v)=>{var L;if(h.role==="user"&&typeof h.content=="string")k.push({role:"user",content:h.content}),ee(h.content);else if(h.role==="assistant"&&typeof h.content=="string"){const M={role:"assistant",content:h.content,ids:Array.isArray(h.ids)?h.ids:void 0};k.push(M),te().finish({fullText:h.content,tokens:((L=M.ids)==null?void 0:L.length)??0,tokPerS:0,...v===n.length-1?{onRegenerate:()=>{j()}}:{}})}}),C(B(o,k,u).length))}function $(){!e||!c||(c.disabled=x||e.value.trim().length===0)}function P(n){!e||!c||!s||(x=n,c.hidden=n,s.hidden=!n,s.disabled=!n,e.disabled=n,a&&(a.disabled=n),e.placeholder=n?"Generating…":`Ask ${i.name} anything…`)}function C(n){if(d)if(!n)d.textContent="Zero TVM Β· 10 WGSL kernel roles Β· β€œ/” for commands";else if(n>=l.maxContext)d.textContent=`Context full β€” ${l.maxContext} / ${l.maxContext} tokens Β· start a new chat`;else{const h=Math.round(n/l.maxContext*100);d.textContent=`Context ${n} / ${l.maxContext} tokens (${h}%)`}}function A(){if(x)return;k.length=0,k.push({role:"system",content:S});try{localStorage.removeItem(f)}catch{}const n=F("messages");n&&n.replaceChildren(),b==null||b.classList.remove("hidden"),ne(""),_("Ready","ready"),C(),matchMedia("(pointer: coarse)").matches||e==null||e.focus()}e.disabled=!1,e.placeholder=`Ask ${i.name} anything…`,a&&(a.disabled=!1),Y(e),$(),C(),ge(),T();const R=!matchMedia("(pointer: coarse)").matches;R&&e.focus(),(J=t.lock)==null||J.onChange(n=>{x||(e.disabled=n,c.disabled=n||e.value.trim().length===0,e.placeholder=n?"Serving a room guest…":`Ask ${i.name} anything…`)}),e.addEventListener("input",()=>{Y(e),$()}),e.addEventListener("keydown",n=>{n.key==="Enter"&&!n.shiftKey&&(n.preventDefault(),c.disabled||r.requestSubmit())}),r.addEventListener("submit",n=>{n.preventDefault(),c.disabled||se()}),s.addEventListener("click",()=>{y=!0,s.disabled=!0}),a==null||a.addEventListener("click",A),m==null||m.addEventListener("click",n=>{const h=n.target.closest(".suggest");if(!h)return;const v=h.dataset.prompt;v&&(e.value=v,Y(e),$(),r.requestSubmit())});async function D(n,h,v,L){const M=l.maxContext-h.length;p(`Prompt: ${h.length} tokens Β· reply budget ${M} tokens`),C(h.length);const E=v.length>0;E||n.showThinking(),g(E?"generating":"thinking");const K=performance.now();let I=0,V=!0;const H=v.slice();let W=E?u.decode(H):"";try{await l.generatePipelined(h,M,N=>{var X;(X=t.onToken)==null||X.call(t),V&&(E||n.body.replaceChildren(n.cursor),V=!1,g("generating")),I++,H.push(N),W=u.decode(H),n.render(W);const le=(performance.now()-K)/1e3;ne(`${I} tok Β· ${(I/le).toFixed(1)} tok/s`)},()=>y)}catch(N){W+=`
5
+
6
+ _[Error: ${N}]_`,p(`Error: ${N}`)}const re=(performance.now()-K)/1e3,ae=I/Math.max(re,.001),Z=h.length+I,Q=I>=M,ie=y&&!Q;return n.finish({fullText:W,tokens:H.length,tokPerS:ae,onRegenerate:()=>{j()},notice:Q?{text:`Cut off β€” the ${l.maxContext}-token context window is full. Start a new chat to keep going.`}:ie?{text:"Stopped β€” this reply is incomplete.",onContinue:L}:void 0}),g("idle"),C(Z),Z>=l.maxContext&&(_("Context full","error"),p(`Context window full (${l.maxContext} tokens) β€” start a new chat to continue.`)),{text:W,ids:H}}async function z(){var M,E,K,I;P(!0),y=!1,await((M=t.lock)==null?void 0:M.acquire()),(E=F("messages"))==null||E.querySelectorAll(".truncation-btn").forEach(V=>V.remove());const n=te(),h=B(o,k,u);if(h.length>=l.maxContext){const V=`Context full β€” the conversation (${h.length} tokens) exceeds the ${l.maxContext}-token window. Start a new chat.`;n.finish({fullText:`_${V}_`,tokens:0,tokPerS:0}),_("Context full","error"),C(h.length),p(V),(K=t.lock)==null||K.release(),P(!1),y=!1,$();return}const v={role:"assistant",content:""};k.push(v);const L=await D(n,h,[],()=>{G(v,n)});v.content=L.text,v.ids=L.ids,w(),(I=t.lock)==null||I.release(),P(!1),y=!1,$(),R&&(e==null||e.focus())}async function G(n,h){var M,E;if(x||!n.ids||k[k.length-1]!==n)return;P(!0),y=!1,await((M=t.lock)==null?void 0:M.acquire());const v=B(o,k.slice(0,-1),u).concat(n.ids),L=await D(h,v,n.ids,()=>{G(n,h)});n.content=L.text,n.ids=L.ids,w(),(E=t.lock)==null||E.release(),P(!1),y=!1,$(),R&&(e==null||e.focus())}const U={new:()=>A(),canvas:()=>{ke()||oe("No runnable code block yet β€” ask for html, svg, or js.")},...t.commands};function oe(n){var v;const h=document.createElement("div");h.className="sys-note",h.textContent=n,(v=F("messages"))==null||v.appendChild(h)}async function se(){if(x||!e)return;const n=e.value.trim();if(!n)return;const h=n.startsWith("/")?n.slice(1).split(/\s/)[0].toLowerCase():"";if(h&&U[h]){e.value="",Y(e),$(),U[h]();return}e.value="",Y(e),ee(n),k.push({role:"user",content:n}),w(),await z()}async function j(){var h;if(x||k.length===0||k[k.length-1].role!=="assistant")return;k.pop(),w();const n=(h=F("messages"))==null?void 0:h.querySelectorAll(".msg.ai");n&&n.length>0&&n[n.length-1].remove(),await z()}ve({engine:l,tokenizer:u,isBusy:()=>x,setBusy:P,onIdle:$})}export{Ee as b,Le as s,Ie as w};
assets/chat-ui-CQAxV1po6S.js ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import{_}from"./preload-helper-ckwbz45pO3.js";const x='<svg class="icon" viewBox="0 0 24 24"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/></svg>',H='<svg class="icon" viewBox="0 0 24 24"><path d="M20 6L9 17l-5-5"/></svg>',q='<svg class="icon" viewBox="0 0 24 24"><path d="M23 4v6h-6M1 20v-6h6M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/></svg>';function M(t,s){t.innerHTML=`${x}<span>Copy</span>`,t.addEventListener("click",async()=>{try{await navigator.clipboard.writeText(s()),t.classList.add("copied"),t.innerHTML=`${H}<span>Copied</span>`,setTimeout(()=>{t.classList.remove("copied"),t.innerHTML=`${x}<span>Copy</span>`},1500)}catch{}})}function u(t,s){const n=/(```)|(`[^`]+`)|(\*\*[^*]+\*\*)|(\*[^*\n]+\*)|\[([^\]]+)\]\(([^)\s]+)\)/g;let e=0,c;for(;(c=n.exec(t))!==null;)if(!c[1]){if(c.index>e&&s.appendChild(document.createTextNode(t.slice(e,c.index))),c[2]){const o=document.createElement("code");o.textContent=c[2].slice(1,-1),s.appendChild(o)}else if(c[3]){const o=document.createElement("strong");o.textContent=c[3].slice(2,-2),s.appendChild(o)}else if(c[4]){const o=document.createElement("em");o.textContent=c[4].slice(1,-1),s.appendChild(o)}else if(c[5]&&c[6])if(/^https?:\/\//i.test(c[6])){const o=document.createElement("a");o.href=c[6],o.textContent=c[5],o.target="_blank",o.rel="noopener noreferrer",s.appendChild(o)}else s.appendChild(document.createTextNode(c[0]));e=c.index+c[0].length}e<t.length&&s.appendChild(document.createTextNode(t.slice(e)))}const S='<svg class="icon" viewBox="0 0 24 24"><polygon points="6 4 20 12 6 20 6 4"/></svg>',B=new Set(["html","svg","js","javascript"]);let C=null;function F(){return C?(T(C.code,C.lang),!0):!1}function T(t,s){var r;_(()=>import("./feats-dlBfBxqpI8.js"),[]).then(a=>a.recordFeat("canvas")).catch(()=>{}),(r=document.getElementById("code-canvas"))==null||r.remove();const n=document.createElement("div");n.id="code-canvas",n.innerHTML=`
2
+ <div class="canvas-frame" role="dialog" aria-modal="true" aria-label="Code preview">
3
+ <div class="canvas-head">
4
+ <span class="canvas-title">canvas Β· ${s}</span>
5
+ <button type="button" class="canvas-close" aria-label="Close preview">βœ• close</button>
6
+ </div>
7
+ <iframe class="canvas-view" sandbox="allow-scripts" title="Code preview"></iframe>
8
+ </div>`;const e=document.activeElement,c=()=>{var a;n.remove(),window.removeEventListener("keydown",o),(a=e==null?void 0:e.focus)==null||a.call(e)},o=a=>{a.key==="Escape"&&c()};n.addEventListener("click",a=>{const i=a.target;(i===n||i.closest(".canvas-close"))&&c()}),window.addEventListener("keydown",o);const p=t.replace(/<\/script/gi,"<\\/script"),l=s==="html"?t:s==="svg"?`<!doctype html><body style="margin:0;display:grid;place-items:center;min-height:100vh;background:#fff">${t}`:`<!doctype html><body style="margin:0;padding:14px;background:#fff;color:#111;font:13px/1.5 ui-monospace,monospace"><pre id="out"></pre><script>
9
+ var out=document.getElementById('out');
10
+ var log=function(){out.textContent+=Array.prototype.map.call(arguments,function(x){return typeof x==='object'?JSON.stringify(x):String(x)}).join(' ')+'\\n'};
11
+ console.log=console.info=console.warn=console.error=log;
12
+ window.addEventListener('error',function(e){log('Error:',e.message)});
13
+ <\/script><script>${p}<\/script>`;n.querySelector(".canvas-view").srcdoc=l,document.body.appendChild(n),n.querySelector(".canvas-close").focus()}function I(t,s){const n=document.createElement("div");n.className="code-block";const e=document.createElement("div");e.className="code-head";const c=document.createElement("span");c.className="code-lang",c.textContent=s||"code",e.appendChild(c);const o=(s||"").toLowerCase();if(B.has(o)){C={code:t,lang:o};const a=document.createElement("button");a.type="button",a.className="code-run",a.innerHTML=`${S}<span>Canvas</span>`,a.addEventListener("click",()=>T(t,o)),e.appendChild(a)}const p=document.createElement("button");p.type="button",p.className="code-copy",M(p,()=>t),e.appendChild(p),n.appendChild(e);const l=document.createElement("pre"),r=document.createElement("code");return s&&(r.className=`lang-${s}`),r.textContent=t,l.appendChild(r),n.appendChild(l),n}function L(t){const s=document.createDocumentFragment(),n=t.split(`
14
+ `);let e=0;for(;e<n.length;){const c=n[e];if(c.trim()===""){e++;continue}const o=/^\s*```(.*)$/.exec(c);if(o){const a=o[1].trim();e++;const i=[];for(;e<n.length&&!/^\s*```/.test(n[e]);)i.push(n[e]),e++;e<n.length&&e++,s.appendChild(I(i.join(`
15
+ `),a));continue}if(/^\s*(---|\*\*\*|___)\s*$/.test(c)){s.appendChild(document.createElement("hr")),e++;continue}const p=/^(#{1,6})\s+(.*)$/.exec(c);if(p){const a=Math.min(6,p[1].length),i=document.createElement(`h${a}`);u(p[2].trim(),i),s.appendChild(i),e++;continue}if(/^\s*>\s?/.test(c)){const a=document.createElement("blockquote"),i=[];for(;e<n.length&&/^\s*>\s?/.test(n[e]);)i.push(n[e].replace(/^\s*>\s?/,"")),e++;u(i.join(" "),a),s.appendChild(a);continue}if(/^\s*[-*]\s+/.test(c)){const a=document.createElement("ul");for(;e<n.length&&/^\s*[-*]\s+/.test(n[e]);){const i=document.createElement("li");u(n[e].replace(/^\s*[-*]\s+/,""),i),a.appendChild(i),e++}s.appendChild(a);continue}if(/^\s*\d+\.\s+/.test(c)){const a=document.createElement("ol");for(;e<n.length&&/^\s*\d+\.\s+/.test(n[e]);){const i=document.createElement("li");u(n[e].replace(/^\s*\d+\.\s+/,""),i),a.appendChild(i),e++}s.appendChild(a);continue}const l=e+1<n.length?n[e+1]:"";if(c.trim()!==""&&/^\s*(=+|-{2,})\s*$/.test(l)){const a=document.createElement(l.includes("=")?"h1":"h2");u(c.trim(),a),s.appendChild(a),e+=2;continue}const r=[];for(;e<n.length&&n[e].trim()!==""&&!/^\s*```/.test(n[e])&&!/^#{1,6}\s+/.test(n[e])&&!/^\s*>\s?/.test(n[e])&&!/^\s*[-*]\s+/.test(n[e])&&!/^\s*\d+\.\s+/.test(n[e])&&!/^\s*(=+|-{2,})\s*$/.test(n[e+1]??"");)r.push(n[e]),e++;if(r.length>0){const a=document.createElement("p");u(r.join(" "),a),s.appendChild(a)}}return s}const h=t=>document.getElementById(t);let b={name:"…",tag:""};function j(t,s){b={name:t,tag:s}}function O(t){var s;return t.weightFormat==="mlx-safetensors"?((s=t.moe)==null?void 0:s.bits)===3?"MLX 3-bit experts":"MLX 4-bit":"q4f16_1"}function k(){const t=h("welcome");t&&!t.classList.contains("hidden")&&t.classList.add("hidden")}function R(t){t.style.height="auto",t.style.height=Math.min(t.scrollHeight,200)+"px"}function E(t=!1){const s=h("chat-main");if(!s)return;const n=s.scrollHeight-s.scrollTop-s.clientHeight;(t||n<120)&&(s.scrollTop=s.scrollHeight)}let N=!1;function P(){if(N)return;const t=h("chat-main"),s=h("scroll-fab");if(!t||!s)return;N=!0;const n=()=>{const e=t.scrollHeight-t.scrollTop-t.clientHeight;s.classList.toggle("visible",e>140)};t.addEventListener("scroll",n,{passive:!0}),s.addEventListener("click",()=>E(!0)),n()}function D(t){var e;k();const s=document.createElement("div");s.className="msg user";const n=document.createElement("div");n.className="bubble",n.textContent=t,s.appendChild(n),(e=h("messages"))==null||e.appendChild(s),E(!0)}function V(){var p;k();const t=document.createElement("div");t.className="msg ai",t.innerHTML=`
16
+ <div class="role">
17
+ <span class="role-dot">Z</span>
18
+ <span class="role-name"></span>
19
+ <span class="model-tag"></span>
20
+ </div>
21
+ <div class="body"></div>
22
+ <div class="actions"></div>
23
+ `,t.querySelector(".role-name").textContent=b.name,t.querySelector(".model-tag").textContent=b.tag;const s=t.querySelector(".body"),n=document.createElement("span");n.className="cursor",s.appendChild(n),(p=h("messages"))==null||p.appendChild(t),E(!0);let e=!1,c="";const o=()=>{e=!1;const l=L(c);s.replaceChildren(l,n),E(!1)};return{wrap:t,body:s,cursor:n,showThinking(){s.replaceChildren();const l=document.createElement("span");l.className="thinking",l.innerHTML="<span></span><span></span><span></span>",s.appendChild(l)},render(l){c=l,e||(e=!0,requestAnimationFrame(o))},finish({fullText:l,tokens:r,tokPerS:a,onRegenerate:i,notice:g}){var y;const $=L(l);s.replaceChildren($),t.classList.add("msg-done"),(y=t.querySelector(".truncation"))==null||y.remove();const m=t.querySelector(".actions");if(!m)return;m.replaceChildren();const v=document.createElement("button");if(v.type="button",v.className="action-btn",M(v,()=>l),m.appendChild(v),i){const d=document.createElement("button");d.type="button",d.className="action-btn",d.innerHTML=`${q}<span>Regenerate</span>`,d.addEventListener("click",i),m.appendChild(d)}if(r>0){const d=document.createElement("span");d.className="msg-stats",d.textContent=a>0?`${r} tok Β· ${a.toFixed(1)} tok/s`:`${r} tok`,m.appendChild(d)}if(g){const d=document.createElement("div");d.className="truncation";const w=document.createElement("span");if(w.textContent=g.text,d.appendChild(w),g.onContinue){const f=document.createElement("button");f.type="button",f.className="truncation-btn",f.textContent="Continue",f.addEventListener("click",g.onContinue),d.appendChild(f)}t.insertBefore(d,m)}}}}export{R as a,D as b,V as c,F as o,O as q,j as s,P as w};
assets/engine-core-CLR3cxsqf4.js ADDED
The diff for this file is too large to render. See raw diff
 
assets/feats-dlBfBxqpI8.js ADDED
@@ -0,0 +1 @@
 
 
1
+ const o=[{id:"first-summon",name:"First Summoning",desc:"Boot a model in the entrance"},{id:"lane-dense",name:"Dense Communion",desc:"Speak with a dense-attention model"},{id:"lane-hybrid",name:"Delta Communion",desc:"Speak with a recurrent hybrid"},{id:"lane-moe",name:"Woke the Swarm",desc:"Speak with a sparse MoE"},{id:"heavy",name:"Heavyweight",desc:"Boot a β‰₯10 GB footprint"},{id:"pooled",name:"Lean Build",desc:"Boot an expert-pool memory build"},{id:"long-ctx",name:"Long Sight",desc:"Boot a long-context build"},{id:"canvas",name:"Made It Real",desc:"Run model-written code in the canvas"},{id:"room",name:"Opened the Gates",desc:"Host a room for other machines"}],n="zt-feats";function a(){try{const e=JSON.parse(localStorage.getItem(n)??"[]");return new Set(Array.isArray(e)?e.filter(t=>typeof t=="string"):[])}catch{return new Set}}function r(e){try{const t=a();if(t.has(e))return;t.add(e),localStorage.setItem(n,JSON.stringify([...t])),document.dispatchEvent(new CustomEvent("zt-feat",{detail:e}))}catch{}}export{o as FEATS,a as feats,r as recordFeat};
assets/index-BqNbylMY24.js ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/cache-probe-BYX_n9OPRB.js","assets/weight-loader-ByOLMd9f5C.js","assets/model-registry-BoK-ObgACo.js","assets/landing-chat-Bo__3GxplQ.js","assets/preload-helper-ckwbz45pO3.js","assets/mascot-BCKWddkWrV.js","assets/chat-flow-eTAzKAlIYe.js","assets/model-select-DhhV9mCi0T.js","assets/engine-core-CLR3cxsqf4.js","assets/variants-De-VVc9BRz.js","assets/loading-ui-CjCwSRuXvh.js","assets/chat-ui-CQAxV1po6S.js","assets/feats-dlBfBxqpI8.js"])))=>i.map(i=>d[i]);
2
+ import{S as ie,m as I,s as he}from"./model-registry-BoK-ObgACo.js";import{_ as ce}from"./preload-helper-ckwbz45pO3.js";import{m as se,a as ae}from"./mascot-BCKWddkWrV.js";import{feats as pe,FEATS as ve}from"./feats-dlBfBxqpI8.js";const z={moe:'<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2l3 3-3 3-3-3zM5 9l2.4 2.4L5 13.8 2.6 11.4zM19 9l2.4 2.4L19 13.8l-2.4-2.4zM12 16l3 3-3 3-3-3z" opacity="0.9"/><circle cx="12" cy="11.4" r="1.6"/></svg>',hybrid:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M2 9c3 0 3-4 6-4s3 4 6 4 3-4 6-4"/><path d="M2 16h6l3-5 3 8 2-3h6" opacity="0.85"/></svg>',dense:'<svg viewBox="0 0 24 24" fill="currentColor"><rect x="4" y="4" width="7" height="7" rx="1"/><rect x="13" y="4" width="7" height="7" rx="1" opacity="0.55"/><rect x="4" y="13" width="7" height="7" rx="1" opacity="0.55"/><rect x="13" y="13" width="7" height="7" rx="1"/></svg>',mla:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3l8 9-8 9-8-9z"/><path d="M12 8l4 4-4 4-4-4z" fill="currentColor" stroke="none" opacity="0.7"/></svg>',embed:'<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M3 12c2.5-4 6-6 9-6s6.5 2 9 6c-2.5 4-6 6-9 6s-6.5-2-9-6z"/><path d="M8 12h8" opacity="0.8"/></svg>'};function be(t){const s=[],o=t.layerKinds.filter(u=>u==="gdn").length;return t.moe&&(s.push({name:"Sparse Routing",desc:`${t.moe.topK} of ${t.moe.experts} experts wake per token`}),t.sharedExpertIndex>=0&&s.push({name:"Constant Companion",desc:"one shared expert always answers"})),o>0&&s.push({name:"Delta Rule",desc:`${o} recurrent layers β€” memory that does not grow`}),t.mla&&s.push({name:"Latent Gaze",desc:"attends through a compressed latent cache"}),!t.moe&&o===0&&!t.mla&&!t.embeddingOnly&&s.push({name:"Full Attention",desc:`every token sees every token, ${t.layers} layers deep`}),t.qkNorm&&s.push({name:"Steady Gaze",desc:"QK-norm holds attention in range"}),t.maxSeq>=131072&&s.push({name:"Long Sight",desc:`trained to a ${Math.round(t.maxSeq/1024)}k window`}),s.slice(0,3)}function H(t){return t.embeddingOnly?"embed":t.mla?"mla":t.moe?"moe":t.layerKinds.some(s=>s==="gdn")?"hybrid":"dense"}function ge(t){if(t.embeddingOnly)return"Returns a vector. It does not speak.";if(t.mla)return"Attends through a compressed latent β€” the cache is 7Γ— smaller than it looks.";if(t.moe){const o=t.layerKinds.filter(u=>u==="gdn").length;return`Routes every token through ${t.moe.topK} of ${t.moe.experts} experts`+(o>0?`, ${o} of ${t.layers} layers recurrent.`:".")}const s=t.layerKinds.filter(o=>o==="gdn").length;return s>0?`${s} recurrent layers, ${t.layers-s} attention β€” memory that does not grow.`:`${t.layers} layers straight through, ${t.heads} heads each.`}const P=t=>t>=1024?`${Math.round(t/1024)}k`:String(t),F=new URLSearchParams(location.search).get("kv8")!=="0",le=(t,s)=>s&&!t.mla?t.kvBytesPerToken/2+2*t.kvHeads*2:t.kvBytesPerToken,ne=(t,s,o)=>{const u=s*le(t,o);return u>=2**30?`${(u/2**30).toFixed(1)} GB KV`:`${Math.round(u/2**20)} MB KV`};function G(t){const s=[{name:"Standard",tokens:t.maxContext}],o=Math.min(t.maxContext*4,t.maxSeq);return o>t.maxContext&&s.push({name:"Long",tokens:o}),t.maxSeq>o&&s.push({name:"Full",tokens:t.maxSeq}),s}function fe(t){const s=t.indexOf(" Β· ");return s<0?{family:t,variant:""}:{family:t.slice(0,s),variant:t.slice(s+3)}}function ye(){const t=new Map;for(const{param:s,spec:o}of ie){if(o.embeddingOnly)continue;const u=I(o);if(u.pending)continue;const{family:M,variant:y}=fe(u.params);let b=t.get(u.name);b||(b={name:u.name,params:M,variants:[]},t.set(u.name,b)),b.variants.push({param:s,spec:o,label:y||M})}return[...t.values()]}const f=ye(),re={Weights:'<svg viewBox="0 0 16 16" fill="currentColor"><path d="M8 1l3 3-3 3-3-3z"/><path d="M4 8l3 3-3 3-3-3z" opacity="0.6"/><path d="M12 8l3 3-3 3-3-3z" opacity="0.6"/></svg>',Context:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M3 2h8l2 2v10H3z"/><path d="M5 6h6M5 9h6M5 12h4" opacity="0.7"/></svg>',Speed:'<svg viewBox="0 0 16 16" fill="currentColor"><path d="M9 1L3 9h4l-1 6 6-8H8z"/></svg>',Footprint:'<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6"><path d="M2 11h12v3H2z"/><path d="M4 7h8v4H4z" opacity="0.75"/><path d="M6 3h4v4H6z" opacity="0.5"/></svg>'},xe=t=>{const s=/([\d.]+)\s*GB/.exec(t);return s?parseFloat(s[1]):NaN},oe=t=>{var s;try{return(((s=localStorage.getItem(`zt-chat-${t}`))==null?void 0:s.length)??0)>2}catch{return!1}},w=new Set;async function we(t){var s;if(!(!("gpu"in navigator)||!((s=navigator.storage)!=null&&s.getDirectory)))try{const{isModelCached:o}=await ce(async()=>{const{isModelCached:u}=await import("./cache-probe-BYX_n9OPRB.js");return{isModelCached:u}},__vite__mapDeps([0,1,2]));for(const{spec:u}of ie)await o(u)&&w.add(u.id);t()}catch{}}function $e(){var j,W;const t=document.getElementById("model-browser");if(t===null)return;const s=t;t.innerHTML=`
3
+ <div class="cs-splash" aria-hidden="true">
4
+ <div class="cs-splash-ring" aria-hidden="true"></div>
5
+ <svg class="cs-emblem" viewBox="0 0 96 96" fill="none">
6
+ <path d="M48 6 L90 48 L48 90 L6 48 Z" stroke="currentColor" stroke-width="3"/>
7
+ <path d="M48 20 L76 48 L48 76 L20 48 Z" stroke="currentColor" stroke-width="1.4" opacity="0.55"/>
8
+ <path d="M30 48 h12 l6 -10 l6 20 l6 -10 h6" stroke="currentColor" stroke-width="3" stroke-linejoin="round" stroke-linecap="round"/>
9
+ <path d="M48 2 l4 4 l-4 4 l-4 -4 Z M48 86 l4 4 l-4 4 l-4 -4 Z M2 48 l4 -4 l4 4 l-4 4 Z M86 48 l4 -4 l4 4 l-4 4 Z" fill="currentColor"/>
10
+ </svg>
11
+ <span class="cs-logo">zero<b>-tvm</b></span>
12
+ <span class="cs-sub">LLM inference in the browser Β· hand-written WGSL</span>
13
+ </div>
14
+ <div class="cs-spires" aria-hidden="true"></div>
15
+ <div class="cs-col cs-col-l" aria-hidden="true"></div>
16
+ <div class="cs-col cs-col-r" aria-hidden="true"></div>
17
+ <div class="cs-comet" aria-hidden="true"></div>
18
+ <div class="cs-sigilbg" aria-hidden="true"></div>
19
+ <div class="cs-stream" aria-hidden="true"><i>β—†</i><i>β—‡</i><i>β—†</i><i>β—‡</i><i>β—†</i><i>β—‡</i></div>
20
+ <div class="cs-fog" aria-hidden="true"><i></i><i></i></div>
21
+ <div class="cs-dust" aria-hidden="true"></div>
22
+ <div class="mb-plate">
23
+ <div class="cs-banner" aria-hidden="true"></div>
24
+ <div class="mb-name"></div>
25
+ <div class="mb-params"><span class="mb-sigil" aria-hidden="true"></span><span class="mb-params-text"></span></div>
26
+ <div class="cs-lore"></div>
27
+ </div>
28
+ <div class="mb-stage">
29
+ <button class="mb-arrow" data-dir="-1" aria-label="Previous model">&lsaquo;</button>
30
+ <div class="mb-art">
31
+ <div class="mb-pedestal" aria-hidden="true"></div>
32
+ <canvas class="mb-mascot" aria-hidden="true"></canvas>
33
+ </div>
34
+ <button class="mb-arrow" data-dir="1" aria-label="Next model">&rsaquo;</button>
35
+ </div>
36
+ <aside class="mb-info">
37
+ <div class="mb-panel">
38
+ <div class="mb-row-label mb-variants-label">Quantisation</div>
39
+ <div class="mb-variants" role="tablist" aria-label="Quantisation"></div>
40
+ <dl class="mb-stats"></dl>
41
+ <ul class="mb-abilities"></ul>
42
+ <div class="mb-row-label mb-modes-label" hidden>Memory build</div>
43
+ <div class="mb-modes" role="tablist" aria-label="Memory build"></div>
44
+ <div class="mb-row-label mb-ctxs-label" hidden>Context</div>
45
+ <div class="mb-modes mb-ctxs" role="tablist" aria-label="Context"></div>
46
+ <p class="mb-cached" hidden>Already on this device β€” opens in seconds</p>
47
+ <p class="mb-save" hidden>β—ˆ Saved conversation β€” it continues where you left it</p>
48
+ <p class="mb-ram"></p>
49
+ </div>
50
+ </aside>
51
+ ${"gpu"in navigator?'<div class="cs-deeds" role="list" aria-label="Deeds"></div>':""}
52
+ <div class="cs-roster-head" aria-hidden="true">Roster</div>
53
+ <div class="mb-dots mb-roster" role="tablist" aria-label="Models"></div>
54
+ <div class="cs-enter">
55
+ <a class="mb-cta btn btn-primary">Enter chat β–Έ</a>
56
+ <a class="mb-cta-room">⟁ Enter &amp; open a room β€” serve this model to other machines</a>
57
+ </div>
58
+ <div class="cs-live" aria-live="polite"></div>
59
+ <div class="cs-wipe" aria-hidden="true"></div>
60
+ <div class="cs-engage" aria-hidden="true"></div>
61
+ <div class="cs-borderline-t" aria-hidden="true"></div>
62
+ <div class="cs-borderline-b" aria-hidden="true"></div>
63
+ <div class="cs-corner cs-corner-l" aria-hidden="true">registry-rendered Β· measured, or marked est</div>
64
+ <div class="cs-corner cs-corner-r" aria-hidden="true">WebGPU Β· hand-written WGSL</div>
65
+ ${"gpu"in navigator?"":'<p class="note cs-note">This browser has no WebGPU β€” the chat needs Chrome, Edge, or another WebGPU-enabled browser.</p>'}
66
+ `;{const e=t.querySelector(".cs-splash");if(matchMedia("(prefers-reduced-motion: reduce)").matches||sessionStorage.getItem("zt-intro"))e.remove();else{sessionStorage.setItem("zt-intro","1"),e.addEventListener("animationend",i=>{i.target===e&&e.remove()});const n=()=>e.remove();e.addEventListener("pointerdown",n),window.addEventListener("keydown",n,{once:!0})}}const o=e=>t.querySelector(e),u=o(".mb-mascot"),M=o(".mb-dots");let y=0,b=0,$=0,k=0,l=null;const _=(e,a)=>a&&e.moe?a/(e.moe.experts+(e.sharedExpertIndex>=0?1:0)):0;M.innerHTML=f.map((e,a)=>{const n=e.variants[0].spec,i=se(n);return`<button class="mb-dot" data-i="${a}" role="tab" title="${e.name}" aria-label="${e.name}">
67
+ <span class="mb-dot-face" aria-hidden="true"></span>
68
+ <span class="mb-dot-text"><b>${e.name.replace(/-Instruct.*$/,"")}</b><i>${e.params}</i></span>
69
+ <span class="mb-dot-sigil" style="color:${i.accent}" aria-hidden="true">${z[H(n)]??""}</span>
70
+ </button>`}).join("");async function D(){const e=document.createElement("canvas");e.style.cssText="position:absolute;left:-9999px;width:96px;height:96px",document.body.appendChild(e);const a=await ae(e,f[0].variants[0].spec);if(!a){e.remove();return}for(let n=0;n<f.length;n++){const i=f[n].variants[0].spec;a.setSpec(i,f[n].variants.some(h=>w.has(h.spec.id))),await new Promise(h=>setTimeout(h,40));const p=await a.snapshot(!0),m=s.querySelector(`.mb-dot[data-i="${n}"] .mb-dot-face`);m&&m.style.setProperty("--thumb",`url("${p}")`)}a.destroy(),e.remove()}D();function L(){const e=f[y],a=e.variants[b],n=I(a.spec);o(".mb-name").textContent=e.name,o(".mb-params-text").textContent=e.params,o(".mb-sigil").innerHTML=z[H(a.spec)]??"",o(".cs-lore").textContent=ge(a.spec);{const r=s.querySelector(".cs-sigilbg"),c=z[H(a.spec)];if(r&&c){const v=`url("data:image/svg+xml,${encodeURIComponent(c)}")`;r.style.webkitMaskImage=v,r.style.maskImage=v}}const i=parseFloat(n.rateLabel.replace(/[^0-9.]/g,""));s.style.setProperty("--cs-tempo",String(Number.isFinite(i)?Math.min(2.2,.7+i/140):1)),o(".mb-art").toggleAttribute("data-armed",w.has(a.spec.id));const p=s.querySelector(".cs-live");p&&(p.textContent=`${e.name}, ${e.params}`);const m=se(a.spec);s.style.setProperty("--cs-accent",m.accent),s.style.setProperty("--cs-accent-hi",m.accentHi);const h=n.poolModes??[],d=h[$]??h[0],g=G(a.spec),x=g[k]??g[0];{const r=[];a.param&&r.push(`model=${a.param}`),d&&d.slots&&r.push(`pool=${d.slots}`),x.tokens!==a.spec.maxContext&&r.push(`ctx=${x.tokens}`),o(".mb-cta").href=`zero-tvm.html${r.length?"?"+r.join("&"):""}`,o(".mb-cta-room").href=`share.html${r.length?"?"+r.join("&"):""}`}o(".mb-modes").innerHTML=h.length<2?"":h.map((r,c)=>`<button class="mb-variant mb-mode" data-m="${c}" role="tab" aria-selected="${c===$}"><span class="mb-gauge" aria-hidden="true">${"β—†".repeat(h.length-c)}${"β—‡".repeat(c)}</span>${r.label}</button>`).join(""),o(".mb-modes-label").hidden=h.length<2,o(".mb-ctxs").innerHTML=g.length<2?"":g.map((r,c)=>`<button class="mb-variant mb-ctx" data-x="${c}" role="tab" aria-selected="${c===k}">${r.name} Β· ${P(r.tokens)} Β· ~${ne(a.spec,r.tokens,F)}</button>`).join(""),o(".mb-ctxs-label").hidden=g.length<2,o(".mb-variants").innerHTML=e.variants.length<2?"":e.variants.map((r,c)=>`<button class="mb-variant" data-v="${c}" role="tab" aria-selected="${c===b}"${w.has(r.spec.id)?" data-cached":""}>${r.label}</button>`).join(""),o(".mb-variants-label").hidden=e.variants.length<2;const E=[["Weights",n.sizeLabel],["Context",`${P(x.tokens)} tokens`]];n.rateLabel&&!(d&&d.slots)&&E.push(["Speed",`${n.rateLabel} <span class="mb-hw">M2 Max</span>`]);let T=NaN;{const r=xe(d&&d.slots?d.label:n.sizeLabel);if(Number.isFinite(r)){const c=a.spec,v=c.layerKinds.filter(A=>A==="gdn").length,S=v?4*v*((c.gdnConvK-1)*c.gdnQkvDim*2+c.gdnVHeads*c.gdnStatePerHead*4)/2**30:0;T=r+x.tokens*le(c,F)/2**30+S,E.push(["Footprint",`~${T.toFixed(1)} GB <span class="mb-hw">weights + KV${S?" + state":""}</span>`])}}const U=Math.min(1,x.tokens/a.spec.maxSeq),N=d&&d.slots&&_(a.spec,d.slots)||1,V=(r,c)=>`<span class="mb-bar" title="${c}"><i style="width:${Math.round(r*100)}%"></i></span>`;if(o(".mb-stats").innerHTML=E.map(([r,c])=>{const v=re[r]?`<span class="mb-stat-ico" aria-hidden="true">${re[r]}</span>`:"",S=r==="Context"?V(U,`${Math.round(U*100)}% of the checkpoint's trained ${P(a.spec.maxSeq)} window`):r==="Weights"&&N<1?V(N,`${Math.round(N*100)}% of experts resident in this build`):"";return`<div><dt>${v}${r}</dt><dd>${c}${S}</dd></div>`}).join(""),!matchMedia("(prefers-reduced-motion: reduce)").matches)for(const r of s.querySelectorAll(".mb-stats dd")){const c=r.innerHTML,v=/^([^0-9]*)([0-9]+(?:\.[0-9]+)?)([\s\S]*)$/.exec(r.textContent??"");if(!v)continue;const S=parseFloat(v[2]),A=v[2].includes(".")?1:0,me=performance.now(),te=()=>{const B=Math.min(1,(performance.now()-me)/300),ue=1-(1-B)*(1-B);r.textContent=`${v[1]}${(S*(.4+.6*ue)).toFixed(A)}${v[3]}`,B<1?requestAnimationFrame(te):r.innerHTML=c};requestAnimationFrame(te)}o(".mb-abilities").innerHTML=be(a.spec).map(r=>`<li><b>${r.name}</b><span>${r.desc}</span></li>`).join("");const Z=o(".mb-ram"),Q=Number.isFinite(T)?Math.round(T*1.15):NaN,X=d&&d.slots?d.note??"":n.ramNote??"",Y=X.replace(/needs\s*~?[\d.]+\s*GB free RAM/i,"").replace(/^[\sβ€”-]+/,"").trim(),J=Number.isFinite(Q)?`needs ~${Q} GB free RAM${Y?` β€” ${Y}`:""}`:X,ee=x.tokens>a.spec.maxContext?`long context allocates ~${ne(a.spec,x.tokens,F)} at boot, on top of the weights`:"";Z.textContent=[J,ee].filter(Boolean).join(" β€” "),Z.hidden=!(J||ee);for(const r of s.querySelectorAll(".mb-dot")){r.setAttribute("aria-selected",String(Number(r.dataset.i)===y));const c=f[Number(r.dataset.i)];r.toggleAttribute("data-cached",c.variants.some(v=>w.has(v.spec.id))),r.toggleAttribute("data-save",c.variants.some(v=>oe(v.spec.id)))}const de=o(".mb-cached");de.hidden=!w.has(a.spec.id),o(".mb-save").hidden=!oe(a.spec.id),l==null||l.setSpec(he(a.spec,x.tokens),w.has(a.spec.id),d?_(a.spec,d.slots):0)}function K(){const e=s.querySelector(".cs-deeds");if(!e)return;const a=pe();e.innerHTML=ve.map(n=>{const i=`${a.has(n.id)?"Earned":"Not yet earned"}: ${n.name} β€” ${n.desc}`;return`<span class="cs-deed" role="listitem"${a.has(n.id)?" data-on":""} title="${n.name} β€” ${n.desc}" aria-label="${i}">β—†<i>${n.name}</i></span>`}).join("")}document.addEventListener("zt-feat",K),K();function O(){const e=s.querySelector(".cs-wipe");e&&(e.classList.remove("cs-go"),e.offsetWidth,e.classList.add("cs-go"));for(const a of[".mb-art",".mb-plate",".mb-panel"]){const n=s.querySelector(a);n&&(n.classList.remove("cs-in"),n.offsetWidth,n.classList.add("cs-in"))}}function q(e){y=(e+f.length)%f.length,b=0,$=0,k=0,L(),O()}t.addEventListener("click",e=>{const a=e.target,n=a.closest(".mb-arrow");if(n){q(y+Number(n.dataset.dir));return}const i=a.closest(".mb-dot");if(i){q(Number(i.dataset.i));return}const p=g=>{var x;(x=s.querySelector(g))==null||x.focus()},m=a.closest(".mb-mode");if(m){$=Number(m.dataset.m),L(),p(`.mb-mode[data-m="${$}"]`);return}const h=a.closest(".mb-ctx");if(h){k=Number(h.dataset.x),L(),p(`.mb-ctx[data-x="${k}"]`);return}const d=a.closest(".mb-variant");d&&(b=Number(d.dataset.v),$=0,k=0,L(),p(`.mb-variant[data-v="${b}"]`))}),t.tabIndex=0,t.setAttribute("role","application"),t.setAttribute("aria-label","Character select β€” Up and Down arrows change model, Enter opens the chat"),t.addEventListener("keydown",e=>{var a;s.classList.contains("cs-chatting")||(e.key==="ArrowRight"||e.key==="ArrowDown"?(e.preventDefault(),q(y+1)):e.key==="ArrowLeft"||e.key==="ArrowUp"?(e.preventDefault(),q(y-1)):e.key==="Enter"&&e.target.tagName!=="BUTTON"&&e.target.tagName!=="A"&&((a=s.querySelector(".mb-cta"))==null||a.click()))});const C=o(".mb-art");if(C.addEventListener("mouseenter",()=>l==null?void 0:l.setHover(!0)),C.addEventListener("mouseleave",()=>l==null?void 0:l.setHover(!1)),M.addEventListener("mouseenter",()=>{C.style.rotate="y -6deg",l==null||l.setHover(!0)},!0),M.addEventListener("mouseleave",()=>{C.style.rotate="none",l==null||l.setHover(!1)},!0),!matchMedia("(prefers-reduced-motion: reduce)").matches){const e=[[s.querySelector(".cs-spires"),6,2],[s.querySelector(".cs-col-l"),34,8],[s.querySelector(".cs-col-r"),34,8],[s.querySelector(".cs-fog"),26,10],[s.querySelector(".cs-dust"),12,5],[s.querySelector(".cs-sigilbg"),9,4],[s.querySelector(".mb-art"),-7,-3]];s.addEventListener("pointermove",a=>{const n=s.getBoundingClientRect(),i=(a.clientX-n.left)/n.width-.5,p=(a.clientY-n.top)/n.height-.5;for(const[m,h,d]of e)m&&(m.style.transform=`translate(${i*h}px, ${p*d}px)`);l==null||l.setGaze(i*2,p*2)})}(async()=>{var e;try{const a=await((e=navigator.gpu)==null?void 0:e.requestAdapter()),n=a==null?void 0:a.info,i=s.querySelector(".cs-corner-r");i&&n&&(n.vendor||n.architecture)&&(i.textContent=`realm: ${[n.vendor,n.architecture].filter(Boolean).join(" Β· ")} β€” online`)}catch{}})();const R=(e,a)=>{var d;if(e.metaKey||e.ctrlKey||e.shiftKey||e.button!==0)return;if(s.classList.contains("cs-chatting")){e.preventDefault();return}if(!("gpu"in navigator))return;const n=e.currentTarget.href;e.preventDefault(),matchMedia("(prefers-reduced-motion: reduce)").matches||(d=s.querySelector(".cs-engage"))==null||d.classList.add("cs-go");const i=f[y].variants[b],p=I(i.spec).poolModes??[],m=p[$]??p[0],h=G(i.spec)[k]??G(i.spec)[0];ce(async()=>{const{enterChat:g}=await import("./landing-chat-Bo__3GxplQ.js");return{enterChat:g}},__vite__mapDeps([3,4,5,2,6,7,1,8,9,10,11,12])).then(({enterChat:g})=>g({root:s,spec:i.spec,param:i.param,poolSlots:(m==null?void 0:m.slots)??0,poolLabel:m&&m.slots?m.label:"",ctxTokens:h.tokens!==i.spec.maxContext?h.tokens:0,openRoom:a,mascot:l})).catch(g=>{console.error("[landing] in-place chat failed, navigating:",g),location.href=n})};if((j=s.querySelector(".mb-cta"))==null||j.addEventListener("click",e=>R(e,!1)),(W=s.querySelector(".mb-cta-room"))==null||W.addEventListener("click",e=>R(e,!0)),!matchMedia("(prefers-reduced-motion: reduce)").matches){let e=0,a=0;const n=()=>["cs-summoning","cs-thinking","cs-generating"].some(m=>s.classList.contains(m)),i=()=>{if(n()){a=window.setTimeout(i,5e4);return}s.classList.add("cs-asleep"),l==null||l.setMood("sleepy")},p=()=>{s.classList.contains("cs-asleep")&&(s.classList.remove("cs-asleep"),n()||l==null||l.setMood("idle")),s.classList.remove("cs-idle"),clearTimeout(e),clearTimeout(a),e=window.setTimeout(()=>s.classList.add("cs-idle"),18e3),a=window.setTimeout(i,5e4)};for(const m of["pointermove","pointerdown","keydown"])s.addEventListener(m,p);p()}L(),O(),we(()=>{L(),D()}),ae(u,f[y].variants[b].spec).then(e=>{if(!e){C.style.display="none";return}l=e,e.setSpec(f[y].variants[b].spec,w.has(f[y].variants[b].spec.id))})}$e();export{z as L,ge as a,H as l};
assets/landing-chat-Bo__3GxplQ.js ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/landing-room-BSBnFYP4bg.js","assets/mascot-BCKWddkWrV.js","assets/model-registry-BoK-ObgACo.js","assets/model-select-DhhV9mCi0T.js","assets/weight-loader-ByOLMd9f5C.js","assets/room-host-C6R3zseuU5.js","assets/chat-ui-CQAxV1po6S.js","assets/preload-helper-ckwbz45pO3.js","assets/feats-dlBfBxqpI8.js"])))=>i.map(i=>d[i]);
2
+ import{_ as x}from"./preload-helper-ckwbz45pO3.js";import{m as y}from"./mascot-BCKWddkWrV.js";import{s as S,m as $}from"./model-registry-BoK-ObgACo.js";import{b as C,s as w,w as P}from"./chat-flow-eTAzKAlIYe.js";import{l as L,L as T,a as E}from"./index-BqNbylMY24.js";import{recordFeat as d}from"./feats-dlBfBxqpI8.js";import"./model-select-DhhV9mCi0T.js";import"./weight-loader-ByOLMd9f5C.js";import"./engine-core-CLR3cxsqf4.js";import"./variants-De-VVc9BRz.js";import"./loading-ui-CjCwSRuXvh.js";import"./chat-ui-CQAxV1po6S.js";function z(){let o=!1;const s=[],t=[],a=()=>{for(const i of t)i(o)};return{acquire(){return o?new Promise(i=>s.push(()=>{i()})):(o=!0,a(),Promise.resolve())},release(){const i=s.shift();if(i){i();return}o=!1,a()},held:()=>o,onChange(i){t.push(i)}}}const M='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12l14-7-7 14-2-5-5-2z"/></svg>',_='<svg viewBox="0 0 24 24" fill="currentColor" stroke="none"><rect x="7" y="7" width="10" height="10" rx="2"/></svg>',W='<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M19 12l-7 7-7-7"/></svg>';function B(o,s,t){const a=T[L(o)]??"",i=s.params.split(/[\sΒ·]/)[0],m=t!==s.params?`Build: ${t}`:s.ramNote??"";return`
3
+ <div class="cs-chat-head">
4
+ <span class="cs-chat-sigil" aria-hidden="true">${a}</span>
5
+ <div class="cs-chat-id">
6
+ <b>${s.name}</b>
7
+ <i>${t}</i>
8
+ </div>
9
+ <span class="badge" id="badge"><span class="dot"></span><span id="badge-text">Summoning</span></span>
10
+ <button class="cs-chat-tool" id="new-chat-btn" type="button" title="New chat" disabled>New chat</button>
11
+ <a class="cs-chat-tool" id="cs-roster-link" href="/" title="Back to the character select">⟨ Roster</a>
12
+ </div>
13
+ <div class="cs-boot" id="progress-wrap">
14
+ <div class="cs-boot-title" id="loading-title">Summoning ${s.name}</div>
15
+ <div class="cs-boot-status" id="progress-status" aria-live="polite">Preparing…</div>
16
+ <div class="cs-boot-track"><i id="progress-bar"></i></div>
17
+ <div class="cs-boot-detail" id="progress-detail">${s.sizeLabel} Β· cached after first load β€” next visit starts in seconds</div>
18
+ ${m?`<div class="cs-boot-note">${m}</div>`:""}
19
+ <details class="cs-boot-log"><summary>Rite log</summary><pre id="progress-log"></pre></details>
20
+ <div class="cs-boot-error" id="loading-error"></div>
21
+ </div>
22
+ <main class="chat-main" id="chat-main">
23
+ <div class="chat-inner">
24
+ <div class="welcome cs-welcome cs-wait" id="welcome">
25
+ <div class="cs-welcome-title">Speak with ${s.name}</div>
26
+ <div class="cs-welcome-lore">${E(o)} Prompts, tokens and the KV cache stay in this tab.</div>
27
+ <div class="cs-sug-grid" id="suggest-grid">
28
+ <button class="suggest" data-prompt="What is zero-tvm, and what makes it different from other ways of running an LLM in a browser?">What is zero-tvm?</button>
29
+ <button class="suggest" data-prompt="Explain WebGPU compute shaders in three sentences.">Explain WebGPU compute shaders</button>
30
+ <button class="suggest" data-prompt="Write a TypeScript memoized fibonacci with a short test block that prints fib(10), fib(20), and fib(30).">Write a memoized fibonacci in TypeScript</button>
31
+ <button class="suggest" data-prompt="Summarize how a transformer decode step works, focusing on what the KV cache actually stores.">What does the KV cache actually store?</button>
32
+ <button class="suggest" data-prompt="List four kinds of questions where a ${i} model like ${s.name} is likely to fall short compared to a frontier LLM, with one-sentence reasons.">Where does a ${i} model fall short?</button>
33
+ </div>
34
+ </div>
35
+ <div id="messages"></div>
36
+ </div>
37
+ <button class="scroll-fab" id="scroll-fab" title="Scroll to bottom" aria-label="Scroll to bottom">${W}</button>
38
+ </main>
39
+ <div class="composer-wrap">
40
+ <form class="composer" id="composer">
41
+ <textarea id="inp" rows="1" placeholder="Summoning…" aria-label="Message" disabled></textarea>
42
+ <button class="composer-btn" data-variant="send" id="btn" type="submit" disabled aria-label="Send">${M}</button>
43
+ <button class="composer-btn" data-variant="stop" id="stop-btn" type="button" hidden aria-label="Stop">${_}</button>
44
+ </form>
45
+ <div class="composer-hint">
46
+ <span id="ctx-hint">Zero TVM Β· 10 WGSL kernel roles</span>
47
+ <span class="cs-chat-stats" id="stats"></span>
48
+ </div>
49
+ </div>`}async function N(o){var v,b,f;const{root:s,mascot:t}=o,a=o.ctxTokens?S(o.spec,o.ctxTokens):o.spec,i=$(a),m=[o.poolLabel||i.params,a.maxContext!==o.spec.maxContext?`${Math.round(a.maxContext/1024)}k ctx`:""].filter(Boolean).join(" Β· ");{const{accent:e,accentHi:c}=y(a),r=document.documentElement.style;r.setProperty("--accent",e),r.setProperty("--accent-hi",c),r.setProperty("--accent-2",c),r.setProperty("--accent-dim",`${e}22`),r.setProperty("--accent-tint",`${e}1f`)}document.title=`${i.name} Β· zero-tvm`;const n=document.createElement("section");n.className="cs-chat",n.setAttribute("role","region"),n.setAttribute("aria-label",`Chat with ${i.name}`),n.innerHTML=B(a,i,m),s.appendChild(n),s.classList.add("cs-chatting"),n.tabIndex=-1,n.focus(),(v=n.querySelector("#cs-roster-link"))==null||v.addEventListener("click",e=>{e.preventDefault(),location.reload()});let g=0;const u=e=>{clearInterval(g),s.classList.toggle("cs-summoning",e==="summon"),s.classList.toggle("cs-thinking",e==="thinking"),s.classList.toggle("cs-generating",e==="generating"),t==null||t.setHover(e==="thinking"),t==null||t.setMood(e==="generating"?"talking":e==="thinking"?"thinking":"idle"),e==="summon"&&(g=window.setInterval(()=>t==null?void 0:t.pulse(),640))};u("summon");const l=await C({spec:a,poolSlots:o.poolSlots,search:location.search,onDeviceLost:e=>{w(`GPU device lost: ${e.message||e.reason}. Reload the page to recover.`)}});if(!l.ok){u("idle"),s.classList.add("cs-boot-failed");const e=n.querySelector("#loading-title");e&&(e.textContent="The summoning failed"),w(l.reason,()=>{n.remove(),s.classList.remove("cs-chatting","cs-boot-failed"),N(o)});return}u("idle"),s.classList.add("cs-ready"),(b=n.querySelector(".cs-boot"))==null||b.classList.add("cs-done"),(f=n.querySelector("#welcome"))==null||f.classList.remove("cs-wait"),t==null||t.setMood("greet"),t==null||t.pulse(),setTimeout(()=>{!s.classList.contains("cs-thinking")&&!s.classList.contains("cs-generating")&&(t==null||t.setMood("idle"))},1600),d("first-summon");const p=L(a);(p==="dense"||p==="hybrid"||p==="moe")&&d(`lane-${p}`),o.poolSlots&&d("pooled"),o.ctxTokens&&d("long-ctx");{const e=/([\d.]+)\s*GB/.exec(o.poolLabel||i.sizeLabel);(e?parseFloat(e[1]):0)+a.maxContext*a.kvBytesPerToken/2**30>=10&&d("heavy")}const h=z();P({spec:a,tokenizer:l.tokenizer,engine:l.engine,lock:h,onToken:()=>t==null?void 0:t.pulse(),onPhase:e=>u(e),commands:{roster:()=>location.reload()}}),x(async()=>{const{mountRoomTool:e}=await import("./landing-room-BSBnFYP4bg.js");return{mountRoomTool:e}},__vite__mapDeps([0,1,2,3,4,5,6,7,8])).then(({mountRoomTool:e})=>e({root:s,panel:n,spec:a,param:o.param,engine:l.engine,tokenizer:l.tokenizer,mascot:t,lock:h,poolSlots:o.poolSlots,openStrip:o.openRoom===!0})).catch(e=>console.warn("[landing] room tool failed to mount:",e));{const e=n.querySelector("#inp");let c=0;const r=()=>{s.classList.remove("cs-listening"),s.classList.contains("cs-thinking")||t==null||t.setHover(!1)},k=()=>{s.classList.add("cs-listening"),t==null||t.setHover(!0),clearTimeout(c),c=window.setTimeout(r,1600)};e==null||e.addEventListener("input",k),e==null||e.addEventListener("focus",k),e==null||e.addEventListener("blur",()=>{clearTimeout(c),r()})}}export{N as enterChat};
assets/landing-room-BSBnFYP4bg.js ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import{a as y}from"./mascot-BCKWddkWrV.js";import{m as f}from"./model-registry-BoK-ObgACo.js";import{buildChatPromptFor as k}from"./model-select-DhhV9mCi0T.js";import{h as C}from"./room-host-C6R3zseuU5.js";import{recordFeat as w}from"./feats-dlBfBxqpI8.js";import"./weight-loader-ByOLMd9f5C.js";import"./chat-ui-CQAxV1po6S.js";import"./preload-helper-ckwbz45pO3.js";function A(t){const h=t.panel.querySelector(".cs-chat-head");if(!h)return;const v=f(t.spec),s=document.createElement("button");s.type="button",s.className="cs-chat-tool",s.id="room-btn",s.textContent="⟁ Room",s.title="Serve this model to other machines",h.insertBefore(s,h.querySelector("#new-chat-btn"));const c=document.createElement("div");c.className="cs-room",c.hidden=!t.openStrip,c.innerHTML=`
2
+ <div class="cs-room-consent">
3
+ <p>Open a room and whoever has the link chats with <b>${v.name}</b> running on
4
+ THIS machine. Their prompts run on your GPU; every request is listed here as it
5
+ arrives. Guests can also copy the model's cached weights from this machine to
6
+ run it locally. Keep this tab in the foreground while serving.</p>
7
+ <button type="button" class="cs-chat-tool" id="room-open">Open room β†’</button>
8
+ </div>
9
+ <div class="cs-room-live" hidden>
10
+ <div class="cs-room-linkrow">
11
+ <input id="room-link" readonly aria-label="Room link">
12
+ <button type="button" class="cs-chat-tool" id="room-copy">Copy</button>
13
+ <button type="button" class="cs-chat-tool" id="room-close">Close room</button>
14
+ </div>
15
+ <div class="cs-room-members" id="room-members" aria-live="polite">waiting for guests…</div>
16
+ <ul class="cs-room-log" id="room-log" role="log" aria-live="polite" aria-label="Guest requests"></ul>
17
+ </div>`,h.insertAdjacentElement("afterend",c);const n=l=>c.querySelector(l);let a=null,d=null;const u=[];function b(l){var i;const e=t.root.querySelector(".mb-art");if(e){for(;u.length<Math.min(l,6);){const o=u.length,r=document.createElement("canvas");r.className=`cs-guest-mascot cs-guest-${o}`,r.setAttribute("aria-hidden","true"),e.appendChild(r);const p={canvas:r,handle:null};u.push(p),y(r,t.spec).then(m=>{if(!m){r.remove();return}if(!r.isConnected){m.destroy();return}p.handle=m})}for(;u.length>l;){const o=u.pop();(i=o==null?void 0:o.handle)==null||i.destroy(),o==null||o.canvas.remove()}}}s.setAttribute("aria-expanded",String(!c.hidden)),s.addEventListener("click",()=>{c.hidden=!c.hidden,s.setAttribute("aria-expanded",String(!c.hidden))}),n("#room-open").addEventListener("click",()=>{var l;a||(a=C({spec:t.spec,brand:t.poolSlots?{...v,rateLabel:""}:v,param:t.param,engine:t.engine,tokenizer:t.tokenizer,lock:t.lock,encode:e=>k(t.spec,e,t.tokenizer),ui:{row:(e,i)=>{var m;const o=n("#room-log"),r=document.createElement("li");for(r.innerHTML="<b></b><span></span><i></i>",r.querySelector("b").textContent=e.slice(0,8),r.querySelector("span").textContent=i,o.prepend(r);o.children.length>6;)(m=o.lastChild)==null||m.remove();const p=r.querySelector("i");return g=>{p.textContent=g}},onMembers:({hosts:e,guests:i,connected:o})=>{n("#room-members").textContent=o===0?"waiting for guests…":`${e} ${e===1?"machine":"machines"} serving Β· ${i} ${i===1?"guest":"guests"} in the room`,b(o)},onToken:()=>{var e;return(e=t.mascot)==null?void 0:e.pulse()},onBusy:e=>{var i;(i=t.mascot)==null||i.setMood(e?"talking":"idle"),t.root.classList.toggle("cs-generating",e)}}}),n("#room-link").value=a.link,n(".cs-room-consent").hidden=!0,n(".cs-room-live").hidden=!1,s.classList.add("cs-tool-live"),w("room"),(l=navigator.wakeLock)==null||l.request("screen").then(e=>{d=e}).catch(()=>{}))}),n("#room-copy").addEventListener("click",()=>{a&&(navigator.clipboard.writeText(a.link),n("#room-copy").textContent="Copied",setTimeout(()=>{n("#room-copy").textContent="Copy"},1200))}),n("#room-close").addEventListener("click",()=>{a==null||a.close(),a=null,d==null||d.release().catch(()=>{}),d=null,b(0),n(".cs-room-live").hidden=!0,n(".cs-room-consent").hidden=!1,s.classList.remove("cs-tool-live"),n("#room-members").textContent="waiting for guests…"})}export{A as mountRoomTool};
assets/loading-ui-CjCwSRuXvh.js ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import{l as P,f as y}from"./weight-loader-ByOLMd9f5C.js";import{loadTokenizerFor as B,buildChatPromptFor as w}from"./model-select-DhhV9mCi0T.js";import{allocKVPages as G,buildDecodeEngine as U}from"./engine-core-CLR3cxsqf4.js";import{SCALAR_VARIANTS as E}from"./variants-De-VVc9BRz.js";import{P as $}from"./model-registry-BoK-ObgACo.js";function n(e){const o=document.getElementById("progress-log");o&&(o.textContent+=e+`
2
+ `,o.scrollTop=o.scrollHeight)}function i(e,o="idle"){const t=document.getElementById("badge");if(!t)return;const u=document.getElementById("badge-text");u?u.textContent=e:t.textContent=e,t.className=`badge ${o}`}function a(e,o,t){const u=document.getElementById("progress-bar"),d=document.getElementById("progress-status"),s=document.getElementById("progress-detail");u&&(u.style.width=`${Math.min(100,e)}%`),o&&d&&(d.textContent=o),t!==void 0&&s&&(s.textContent=t)}function k(){var e;(e=document.getElementById("progress-wrap"))==null||e.classList.add("active")}function M(){var e;(e=document.getElementById("progress-wrap"))==null||e.classList.remove("active")}function m(e){return e>=1e9?(e/1e9).toFixed(1)+" GB":e>=1e6?(e/1e6).toFixed(0)+" MB":e>=1e3?(e/1e3).toFixed(0)+" KB":e+" B"}async function S(e){try{const o=e.createBuffer({size:16,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_SRC}),t=e.createBuffer({size:16,usage:GPUBufferUsage.MAP_READ|GPUBufferUsage.COPY_DST}),u=e.createShaderModule({code:`enable subgroups;
3
+ @group(0) @binding(0) var<storage, read_write> out : array<u32>;
4
+ @compute @workgroup_size(32, 1, 1)
5
+ fn probe(@builtin(local_invocation_id) tid : vec3<u32>, @builtin(subgroup_size) s : u32) {
6
+ if (tid.x == 0u) { out[0] = s; }
7
+ }`}),d=e.createComputePipeline({layout:"auto",compute:{module:u,entryPoint:"probe"}}),s=e.createBindGroup({layout:d.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:o}}]}),g=e.createCommandEncoder(),l=g.beginComputePass();l.setPipeline(d),l.setBindGroup(0,s),l.dispatchWorkgroups(1),l.end(),g.copyBufferToBuffer(o,0,t,0,16),e.queue.submit([g.finish()]),await t.mapAsync(GPUMapMode.READ);const f=new Uint32Array(t.getMappedRange().slice(0))[0];t.unmap();const c=f===32;return n(`subgroups feature: yes Β· probed size: ${f} Β· path: ${c?"_sg":"scalar"}`),o.destroy(),t.destroy(),c}catch(o){return n(`subgroups probe failed: ${o} Β· falling back to scalar`),!1}}async function L(e={}){const o=e.spec??$;if(!navigator.gpu)return i("No WebGPU","error"),a(0,"No WebGPU available"),{ok:!1,reason:"WebGPU is not available in this browser"};k(),i("Initializing...","loading"),a(0,"Requesting GPU access...");const t=await navigator.gpu.requestAdapter({powerPreference:"high-performance"});if(!t)return i("No GPU","error"),a(0,"No GPU adapter found"),{ok:!1,reason:"No GPU adapter found"};const u=["shader-f16"];for(const r of e.optionalFeatures??[])t.features.has(r)&&u.push(r);const d={};t.limits&&(d.maxStorageBufferBindingSize=t.limits.maxStorageBufferBindingSize,d.maxBufferSize=t.limits.maxBufferSize);let s;try{s=await t.requestDevice({requiredFeatures:u,requiredLimits:d})}catch{return i("shader-f16 missing","error"),a(0,"GPU does not support shader-f16 β€” Chrome or Edge required"),{ok:!1,reason:"shader-f16 unsupported β€” Chrome/Edge required"}}n(`GPU ready β€” features: ${u.join(", ")}`),s.lost.then(r=>{var p;r.reason!=="destroyed"&&(i("GPU lost","error"),a(0,`GPU device lost: ${r.message||r.reason} β€” reload the page to recover`),n(`ERROR: GPU device lost (${r.reason}): ${r.message}`),(p=e.onDeviceLost)==null||p.call(e,r))});let g=!1;if((e.probeSubgroups||o.moe)&&(s.features.has("subgroups")?(a(3,"Probing GPU subgroups..."),g=await S(s)):n("subgroups feature: no Β· path: scalar"),o.moe&&!g))return i("GPU unsupported","error"),a(0,`${o.id} is a sparse MoE and needs GPU subgroups (a Chromium WebGPU feature) β€” this GPU/browser does not expose them. The dense models on the landing page still run.`),{ok:!1,reason:"MoE spec requires the subgroups feature"};a(5,"Loading tokenizer..."),i("Tokenizer...","loading");let l;try{l=await B(o,r=>n(r))}catch(r){return i("Tokenizer failed","error"),a(5,`Tokenizer error: ${r}`),{ok:!1,reason:`Tokenizer load failed: ${r}`}}n("Tokenizer loaded"),a(10,"Loading model weights..."),i("Downloading...","loading");let f;try{f=await P(s,r=>n(r),r=>{const p=r.totalBytes>0?Math.min(1,r.bytesLoaded/r.totalBytes):0,b=Math.round(p*100);a(10+p*80,`Downloading weights β€” shard ${r.shardsLoaded} / ${r.totalShards}`,`${m(r.bytesLoaded)} / ${m(r.totalBytes)} Β· ${r.mbPerSec.toFixed(0)} MB/s`+(r.etaSec>0?` Β· ~${y(r.etaSec)} left`:"")),i(`${b}%`,"loading")},o,e.layerRange,e.expertPool)}catch(r){return i("Download failed","error"),a(10,`Weight load error: ${r}`),n(`ERROR: ${r}`),{ok:!1,reason:`Weight load failed: ${r}`}}a(92,"Allocating KV cache...");let c;if(e.buildEngine)a(96,"Compiling shaders..."),c=e.buildEngine({device:s,weights:f,sgSizeOk:g,spec:o});else{n(`Allocating KV cache (${o.layers} layers Γ— ${o.maxPages} pages)`);const r=G(s,o);a(96,"Compiling shaders..."),n("Compiling WGSL kernels (10 roles)");const p=o.moe&&g?{...E,subgroups:!0,matmul:"tiled"}:void 0;c=U(s,f,r,{spec:o,variants:p})}a(98,"Warming up GPU pipelines...");const h=performance.now();if(e.warmup)await e.warmup(c,l,n);else{const r=w(o,[{role:"user",content:"Hi."}],l);await c.forwardLogits(r)}return n(`Warmup forward: ${Math.round(performance.now()-h)} ms`),a(100,"Ready"),n("Ready. Zero TVM. 10 hand-written WGSL kernel roles."),i("Ready","ready"),{ok:!0,device:s,tokenizer:l,weights:f,engine:c}}export{L as bootEngine,m as formatBytes,M as hideProgress,n as log,i as setBadge,a as setProgress,k as showProgress};
assets/mascot-BCKWddkWrV.js ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import{m as T}from"./model-registry-BoK-ObgACo.js";const C=`
2
+ struct U {
3
+ res: vec2<f32>, time: f32, hover: f32,
4
+ headR: f32, spikes: f32, coil: f32, kvh: f32,
5
+ experts: f32, topk: f32, sharedFused: f32, bands: f32,
6
+ // mood: what the model is DOING right now β€” 0 idle, 1 thinking (prefill),
7
+ // 2 talking (streaming), 3 sleepy (long idle), 4 greet (summon complete).
8
+ // Thinking is a face (irises up, mouth pursed, ears perked); talking routes
9
+ // the per-token beat into the MOUTH instead of a body bob, so the flap rate
10
+ // is the real token cadence; sleepy is heavy lids and slow breathing; greet
11
+ // is a wide smile and a happy squint, held for a beat by the page.
12
+ ink: f32, eyesOpen: f32, bandShift: f32, mood: f32,
13
+ hair: vec3<f32>, _p1: f32,
14
+ iris: vec3<f32>, cached: f32,
15
+ // tempo: idle-clock multiplier from the model's MEASURED rate β€” a 256 tok/s
16
+ // model fidgets, a 35B breathes. beat: decaying pulse bumped once per real
17
+ // generated token. earCtx: ear length from log2(maxContext) — the 6k→32k
18
+ // raise is visible. poolFrac: experts resident / experts β€” the memory
19
+ // picker's fraction; sprites split into a close resident orbit and a far
20
+ // ghosted drift, and the body leans out. All four are read from the spec,
21
+ // the registry's measured labels, or live engine events β€” same rule as
22
+ // every other trait: the picture cannot disagree with the table.
23
+ tempo: f32, beat: f32, earCtx: f32, poolFrac: f32,
24
+ // gaze: pointer position in [-1,1], written per frame β€” the irises follow
25
+ // the hand. Suppressed while thinking (the upward drift owns the eyes).
26
+ // pose.x: portrait flag β€” one frame with no sway, no bob, no blink, so
27
+ // every roster snapshot is the same front-facing framing.
28
+ gaze: vec2<f32>, pose: vec2<f32>,
29
+ };
30
+ @group(0) @binding(0) var<uniform> u: U;
31
+
32
+ fn rot(a: f32) -> mat2x2<f32> { let c = cos(a); let s = sin(a); return mat2x2<f32>(c, -s, s, c); }
33
+ fn smin(a: f32, b: f32, k: f32) -> f32 {
34
+ let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
35
+ return mix(b, a, h) - k * h * (1.0 - h);
36
+ }
37
+ fn sdEllip(p: vec3<f32>, r: vec3<f32>) -> f32 {
38
+ let k0 = length(p / r);
39
+ let k1 = length(p / (r * r));
40
+ return k0 * (k0 - 1.0) / k1;
41
+ }
42
+ fn sdCap(p: vec3<f32>, h: f32, r: f32) -> f32 {
43
+ return length(vec3<f32>(p.x, p.y - clamp(p.y, -h, h), p.z)) - r;
44
+ }
45
+
46
+ // x = distance, y = material
47
+ // 0 skin 1 hair 2 eye white 3 iris 4 sprite
48
+ fn map2(pIn: vec3<f32>) -> vec2<f32> {
49
+ var p = pIn;
50
+ let T = u.time * u.tempo;
51
+ let talking = step(1.5, u.mood) * (1.0 - step(2.5, u.mood));
52
+ let thinking = step(0.5, u.mood) * (1.0 - step(1.5, u.mood));
53
+ let sleepy = step(2.5, u.mood) * (1.0 - step(3.5, u.mood));
54
+ let greet = step(3.5, u.mood);
55
+ // Idle bob always β€” slower and deeper while asleep (breathing); the token
56
+ // pulse bobs the BODY only when not talking β€” while talking the same beat
57
+ // drives the mouth instead (below).
58
+ let live = 1.0 - u.pose.x; // 0 while posing a portrait
59
+ p.y = p.y - (sin(T * mix(1.2, 0.55, sleepy)) * mix(0.018, 0.028, sleepy)
60
+ + u.beat * 0.020 * (1.0 - talking)) * live;
61
+ let m = rot(sin(T * 0.4) * 0.42 * live); // sway, never a full spin
62
+ let xz = m * vec2<f32>(p.x, p.z);
63
+ p = vec3<f32>(xz.x, p.y, xz.y);
64
+
65
+ let R = u.headR;
66
+ let hy = R * 0.62; // head centre
67
+ var q = p; q.x = abs(q.x); // opSymX β€” free symmetry
68
+
69
+ // CHIBI PROPORTION. The head is the character; the body is a hint under it.
70
+ var d = sdEllip(p - vec3<f32>(0.0, hy, 0.0), vec3<f32>(R, R * 1.02, R * 0.96));
71
+ var mat = 0.0;
72
+
73
+ // Body + arms, deliberately small.
74
+ // Memory mode: fewer experts resident, leaner figure. 12% at the quarter
75
+ // pool β€” visible beside the full-mode sibling, not a different character.
76
+ let lean = select(0.0, (1.0 - u.poolFrac) * 0.12, u.poolFrac > 0.0);
77
+ let body = sdCap(p - vec3<f32>(0.0, -R * 0.72, 0.0), R * 0.30 * (1.0 - lean), R * 0.40);
78
+ d = smin(d, body, R * 0.18);
79
+ let arm = sdCap(q - vec3<f32>(R * 0.46, -R * 0.72, 0.0), R * 0.16, R * 0.14);
80
+ d = smin(d, arm, R * 0.06);
81
+
82
+ // HAIR: a cap over the skull, plus spikes by polar repetition so the count
83
+ // follows the layer count instead of being a fixed loop.
84
+ var hair = sdEllip(p - vec3<f32>(0.0, hy + R * 0.10, -R * 0.02), vec3<f32>(R * 1.06, R * 1.05, R * 1.04));
85
+ // FRINGE. Cut hair only where it would cover the face: below the fringe line
86
+ // AND on the front side. A flat horizontal cut put the fringe at eye level
87
+ // and the eyes ended up inside the hair.
88
+ // The fringe is POINTED, not a flat line β€” a horizontal cut reads as a
89
+ // swim cap. Modulating the cut height by angle gives it locks.
90
+ let fringeY = hy + R * 0.10 - abs(sin(atan2(p.x, p.z) * 3.0)) * R * 0.20;
91
+ hair = max(hair, -max(p.y - fringeY, -p.z));
92
+ {
93
+ let n = max(u.spikes, 3.0);
94
+ let sect = 6.28318 / n;
95
+ let a = atan2(p.z, p.x);
96
+ let i = round(a / sect);
97
+ let ang = i * sect;
98
+ let dir = vec3<f32>(cos(ang), 0.0, sin(ang));
99
+ let tip = vec3<f32>(0.0, hy + R * 0.42, 0.0) + dir * R * 0.92;
100
+ let spike = sdCap((p - tip) * vec3<f32>(1.0, 0.70, 1.0), R * 0.26, R * 0.115);
101
+ hair = smin(hair, spike, R * 0.16);
102
+ }
103
+ // Ear tufts, from the KV head count. They sit ON TOP of the head β€” the old
104
+ // placement (R*0.88 sideways) was INSIDE the R*1.06 hair shell, so the ears
105
+ // existed in the field and never in the picture. Length still follows
106
+ // kvHeads and the context ceiling; attention (hover) and thinking perk them.
107
+ {
108
+ let perk = 1.0 + 0.35 * max(u.hover, thinking);
109
+ let eh = R * 0.42 * clamp(u.kvh / 16.0, 0.65, 1.2) * (0.8 + 0.5 * u.earCtx) * perk;
110
+ let t = sdEllip(q - vec3<f32>(R * 0.58, hy + R * 0.92 + eh * 0.35, 0.0),
111
+ vec3<f32>(R * 0.19, eh, R * 0.16));
112
+ hair = smin(hair, t, R * 0.10);
113
+ }
114
+ if (hair < d) { d = hair; mat = 1.0; }
115
+
116
+ // Recurrent tail.
117
+ if (u.coil > 0.01) {
118
+ for (var i = 0.0; i < 5.0; i = i + 1.0) {
119
+ let t = i / 4.0;
120
+ let a = t * 5.0 + u.time * 0.6;
121
+ let rad = R * (0.30 + t * 0.42);
122
+ let c = p - vec3<f32>(cos(a) * rad, -R * (1.02 + t * 0.34), sin(a) * rad);
123
+ let seg = length(c) - R * 0.13 * (1.0 - t * 0.45);
124
+ if (seg < d) { d = seg; mat = 1.0; }
125
+ }
126
+ }
127
+
128
+ // EYES. Large, flattened, sitting proud of the face β€” additive, never carved.
129
+ // A concave dimple cannot hold a catchlight, and the catchlight is the whole
130
+ // reason an anime eye reads as alive.
131
+ if (u.eyesOpen > 0.5) {
132
+ // BLINK: two incommensurate phases so the rhythm never reads as a loop.
133
+ // Each spike closes the lids for ~a tenth of its cycle. Sleep holds them
134
+ // three-quarters shut on top; the greet is a happy squint.
135
+ let b1 = abs(fract(T * 0.20) - 0.04);
136
+ let b2 = abs(fract(T * 0.077 + 0.5) - 0.04);
137
+ let blink = (1.0 - smoothstep(0.025, 0.05, min(b1, b2))) * live;
138
+ let eyeY = (1.0 - 0.85 * max(blink, sleepy * 0.75)) * (1.0 - 0.2 * greet);
139
+ let ec = vec3<f32>(R * 0.38, hy - R * 0.22, R * 0.74);
140
+ let white = sdEllip(q - ec, vec3<f32>(R * 0.30, R * 0.38 * eyeY, R * 0.20));
141
+ if (white < d) { d = white; mat = 2.0; }
142
+ // Thinking face: the irises drift up β€” the classic looking-for-the-word.
143
+ // Otherwise they FOLLOW the pointer: q is mirrored (|x|), so a world-x
144
+ // gaze needs the sign of the eye's own side to keep both looking the
145
+ // same way instead of crossing.
146
+ let lookUp = thinking * R * 0.10;
147
+ let sx = select(-1.0, 1.0, p.x >= 0.0);
148
+ let gz = vec3<f32>(u.gaze.x * sx, -u.gaze.y, 0.0) * R * 0.07 * (1.0 - thinking) * (1.0 - sleepy);
149
+ let iris = sdEllip(q - (ec + vec3<f32>(0.0, -R * 0.04 + lookUp, R * 0.11) + gz),
150
+ vec3<f32>(R * 0.23, R * 0.30 * eyeY, R * 0.17));
151
+ if (iris < d) { d = iris; mat = 3.0; }
152
+ } else {
153
+ // Closed: a lash line, so the face still has a feature.
154
+ let lash = sdEllip(q - vec3<f32>(R * 0.38, hy - R * 0.20, R * 0.80),
155
+ vec3<f32>(R * 0.28, R * 0.05, R * 0.10));
156
+ if (lash < d) { d = lash; mat = 3.0; }
157
+ }
158
+
159
+ // The mouth. Anime faces are eyes plus almost nothing, but "almost nothing"
160
+ // is not "nothing" β€” and it is the one feature that ACTS: while talking the
161
+ // per-token beat opens it (each flap is a real emitted token), while
162
+ // thinking it purses to a dot. Material is a negative sentinel so it can
163
+ // never collide with the sprite id range.
164
+ if (u.eyesOpen > 0.5) {
165
+ // Envelope is the decaying per-token beat (real signal: no tokens, mouth
166
+ // closes); the 12 Hz chatter under it is what makes an open mouth read as
167
+ // SPEAKING rather than surprise. At high token rates beat alone pins the
168
+ // mouth open β€” the chatter is the flap.
169
+ let open = clamp(u.beat * 1.5, 0.0, 1.0) * talking * (0.30 + 0.70 * abs(sin(u.time * 12.0)));
170
+ var mwB = mix(0.11, 0.055, thinking);
171
+ mwB = mix(mwB, 0.20, greet); // the smile is WIDE
172
+ var mhB = mix(0.055, 0.025, thinking);
173
+ mhB = mix(mhB, 0.020, sleepy); // asleep: barely there
174
+ mhB = mix(mhB, 0.028, greet); // ...and shallow
175
+ let mw = R * mix(mwB, 0.085, open);
176
+ let mh = R * (mhB + 0.11 * open);
177
+ let mo = sdEllip(p - vec3<f32>(0.0, hy - R * 0.62 - open * R * 0.04, R * 0.80),
178
+ vec3<f32>(mw, mh, R * 0.08));
179
+ if (mo < d) { d = mo; mat = -1.0; }
180
+ }
181
+
182
+ // Expert sprites in orbit.
183
+ if (u.experts > 0.5) {
184
+ let n = min(u.experts, 24.0);
185
+ let sect = 6.28318 / n;
186
+ let a = atan2(p.z, p.x) + u.time * u.tempo * 0.5;
187
+ let i = round(a / sect);
188
+ let ang = i * sect - u.time * u.tempo * 0.5;
189
+ // Memory mode: the resident fraction keeps the close orbit; the streamed
190
+ // remainder drifts out and shrinks β€” the pool, drawn honestly.
191
+ let resident = select(1.0, f32((i / n) < u.poolFrac), u.poolFrac > 0.0);
192
+ let orbitR = mix(R * 2.15, R * 1.55, resident);
193
+ let sprR = mix(R * 0.045, R * 0.075, resident);
194
+ let c = p - vec3<f32>(cos(ang) * orbitR, hy + sin(i * 2.0) * R * 0.35, sin(ang) * orbitR);
195
+ let s = length(c) - sprR;
196
+ // Wrap the sector index into [0, n) before it becomes a material id β€”
197
+ // atan2 makes i negative on half the circle, and 4+i walked into the
198
+ // eye/iris ids there (and past the old mouth id on the other half).
199
+ let id = i - n * floor(i / n);
200
+ if (s < d) { d = s; mat = 4.0 + id; }
201
+ }
202
+ return vec2<f32>(d, mat);
203
+ }
204
+
205
+ fn map(p: vec3<f32>) -> f32 { return map2(p).x; }
206
+
207
+ fn normalAt(p: vec3<f32>) -> vec3<f32> {
208
+ let e = vec2<f32>(0.0012, 0.0);
209
+ return normalize(vec3<f32>(
210
+ map(p + e.xyy) - map(p - e.xyy),
211
+ map(p + e.yxy) - map(p - e.yxy),
212
+ map(p + e.yyx) - map(p - e.yyx)));
213
+ }
214
+
215
+ // CEL SHADING. Anime light is banded with a hard terminator, not a ramp. Band
216
+ // COUNT comes from the checkpoint's bit width and the split from its quant
217
+ // group, so a coarser build is visibly more posterised.
218
+ fn cel(x: f32, bands: f32, shift: f32) -> f32 {
219
+ let t = clamp(x, 0.0, 1.0);
220
+ let s = floor(t * bands + shift) / bands;
221
+ let f = fract(t * bands + shift);
222
+ return clamp(s + smoothstep(0.46, 0.54, f) / bands, 0.42, 1.0);
223
+ }
224
+
225
+ @vertex
226
+ fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
227
+ var p = array<vec2<f32>, 3>(vec2<f32>(-1.0, -3.0), vec2<f32>(-1.0, 1.0), vec2<f32>(3.0, 1.0));
228
+ return vec4<f32>(p[i], 0.0, 1.0);
229
+ }
230
+
231
+ @fragment
232
+ fn fs(@builtin(position) frag: vec4<f32>) -> @location(0) vec4<f32> {
233
+ let uv = (frag.xy - 0.5 * u.res) / u.res.y;
234
+ let ro = vec3<f32>(0.0, 0.05, 3.05);
235
+ let rd = normalize(vec3<f32>(uv.x, -uv.y, -1.5));
236
+
237
+ var t = 0.0; var hit = false; var mat = 0.0;
238
+ for (var i = 0; i < 92; i = i + 1) {
239
+ let r = map2(ro + rd * t);
240
+ if (r.x < 0.0012) { hit = true; mat = r.y; break; }
241
+ t = t + r.x * 0.85;
242
+ if (t > 6.0) { break; }
243
+ }
244
+
245
+ var shadowA = 0.0;
246
+ if (rd.y < -0.0001) {
247
+ let ft = (-0.92 - ro.y) / rd.y;
248
+ if (ft > 0.0 && (!hit || ft < t)) {
249
+ let fp = ro + rd * ft;
250
+ shadowA = exp(-dot(fp.xz, fp.xz) * 6.0) * 0.42;
251
+ }
252
+ }
253
+ if (!hit) { return vec4<f32>(0.0, 0.0, 0.0, shadowA); }
254
+
255
+ let p = ro + rd * t;
256
+ let nrm = normalAt(p);
257
+ let key = normalize(vec3<f32>(0.45, 0.70, 0.70));
258
+ let diff = clamp(dot(nrm, key) * 0.5 + 0.5, 0.0, 1.0);
259
+ let lit = cel(diff, u.bands, u.bandShift);
260
+ let fres = pow(1.0 - clamp(dot(nrm, -rd), 0.0, 1.0), 1.8);
261
+
262
+ let SKIN = vec3<f32>(0.98, 0.86, 0.80);
263
+ var col: vec3<f32>;
264
+
265
+ if (mat < -0.5) {
266
+ // Mouth: dark interior, darker the wider it opens.
267
+ col = vec3<f32>(0.42, 0.22, 0.24) * (1.0 - 0.5 * clamp(u.beat, 0.0, 1.0));
268
+ } else if (mat >= 4.0) {
269
+ let id = mat - 4.0;
270
+ let n = min(u.experts, 24.0);
271
+ let m = max(n / max(u.topk, 1.0), 1.0);
272
+ let s = id + floor(u.time * 1.6);
273
+ let hot = select(0.0, 1.0, (s - m * floor(s / m)) < 1.0);
274
+ let fused = select(0.0, 1.0, u.sharedFused > 0.5 && abs(id) < 0.5);
275
+ col = mix(u.hair * 0.45, vec3<f32>(1.0, 0.82, 0.55), max(hot, fused));
276
+ } else if (mat >= 3.0) {
277
+ // Iris: dark base, bright rim low, and one hard catchlight upper-left. The
278
+ // eye must hold both the darkest mass and the brightest pixel in frame.
279
+ let cl = pow(clamp(dot(reflect(-key, nrm), -rd), 0.0, 1.0), 60.0);
280
+ col = u.iris * (0.30 + 0.45 * lit)
281
+ + u.iris * 1.8 * pow(clamp(-nrm.y, 0.0, 1.0), 2.0) * 0.5
282
+ + vec3<f32>(1.0) * step(0.55, cl) * 1.6;
283
+ } else if (mat >= 2.0) {
284
+ col = vec3<f32>(0.97, 0.97, 1.0) * (0.82 + 0.18 * lit);
285
+ } else if (mat >= 1.0) {
286
+ col = u.hair * lit;
287
+ } else {
288
+ col = SKIN * lit;
289
+ // A blush band, low on the cheek β€” cheap, and it is a strong anime cue.
290
+ let cheek = smoothstep(0.30, 0.0, length(vec2<f32>(abs(p.x) - u.headR * 0.52, p.y - u.headR * 0.24)));
291
+ col = mix(col, vec3<f32>(1.0, 0.62, 0.62), cheek * 0.30);
292
+ }
293
+
294
+ // INK OUTLINE. Weight follows the bit width: a coarser build is drawn with a
295
+ // heavier pen.
296
+ col = mix(col, vec3<f32>(0.04, 0.04, 0.07), smoothstep(1.0 - u.ink, 0.995, fres));
297
+ // ALREADY ON THIS DEVICE. A cached model is lit β€” it can start now, where an
298
+ // uncached one costs a download first. The line under the card says so in
299
+ // words; this says it before the words are read.
300
+ if (u.cached > 0.5) {
301
+ col = col + u.hair * fres * 0.55 + u.hair * 0.06;
302
+ }
303
+ col = col * (0.94 + 0.14 * u.hover);
304
+ col = pow(clamp(col, vec3<f32>(0.0), vec3<f32>(1.0)), vec3<f32>(1.0 / 2.05));
305
+ return vec4<f32>(col, 1.0);
306
+ }
307
+ `;let z=null;function F(){return z||(z=(async()=>{if(!("gpu"in navigator))return null;try{const e=await navigator.gpu.requestAdapter();return await(e==null?void 0:e.requestDevice())??null}catch{return null}})()),z}const O={dense:[.42,.62,.95],hybrid:[.3,.84,.7],moe:[.96,.6,.36],mla:[.72,.54,.95],embed:[.62,.66,.74]},S={dense:[.3,.52,.92],hybrid:[.16,.72,.6],moe:[.94,.52,.24],mla:[.6,.4,.92],embed:[.45,.5,.58]};function P(e){const i=e.layerKinds.filter(r=>r==="gdn").length/Math.max(e.layers,1),h=e.embeddingOnly===!0?"embed":e.moe!=null?"moe":e.mla!=null?"mla":i>.1?"hybrid":"dense",s=r=>"#"+r.map(t=>Math.round(Math.min(1,t)*255).toString(16).padStart(2,"0")).join(""),n=O[h];return{accent:s(n),accentHi:s(n.map(r=>r*.55+.45))}}function B(e,w=!1,i=0){var b;const s=e.layerKinds.filter(k=>k==="gdn").length/Math.max(e.layers,1),n=e.moe!=null,r=e.mla!=null,t=e.embeddingOnly===!0,d=t?"embed":n?"moe":r?"mla":s>.1?"hybrid":"dense",f=O[d],o=S[d],m=((b=e.moe)==null?void 0:b.bits)??4,p=e.weightFormat==="mlx-safetensors"?64:32,u=parseFloat(T(e).rateLabel.replace(/[^0-9.]/g,"")),g=Number.isFinite(u)?Math.min(1.9,.75+u/160):1,y=Math.max(0,Math.min(1.4,(Math.log2(e.maxContext)-12)/6)),v=new Float32Array(new ArrayBuffer(128));return v.set([0,0,0,0,.3+Math.log2(e.d)*.016,Math.max(4,Math.min(14,Math.round(e.layers/3))),s,e.kvHeads,n?e.moe.experts:0,n?e.moe.topK:0,e.sharedExpertIndex>=0?1:0,m<=3?2:3,m<=3?.46:.34,t?0:1,p===64?.5:0,0,f[0],f[1],f[2],0,o[0],o[1],o[2],w?1:0,g,0,y,i,0,0,0,0]),v}async function N(e,w){const i=await F(),h=i?e.getContext("webgpu"):null;if(!i||!h)return null;const s=navigator.gpu.getPreferredCanvasFormat();h.configure({device:i,format:s,alphaMode:"premultiplied"});const n=i.createShaderModule({code:C}),r=i.createRenderPipeline({layout:"auto",vertex:{module:n,entryPoint:"vs"},fragment:{module:n,entryPoint:"fs",targets:[{format:s,blend:{color:{srcFactor:"one",dstFactor:"one-minus-src-alpha"},alpha:{srcFactor:"one",dstFactor:"one-minus-src-alpha"}}}]}});let t=B(w);const d=i.createBuffer({size:t.byteLength,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),f=i.createBindGroup({layout:r.getBindGroupLayout(0),entries:[{binding:0,resource:{buffer:d}}]}),o=matchMedia("(prefers-reduced-motion: reduce)").matches;let m=0,p=0,u=0,g=!1,y=0,v=0,b=0,k=0,E=!1;const A=()=>{const a=Math.min(devicePixelRatio||1,2),l=Math.max(1,Math.round(e.clientWidth*a)),R=Math.max(1,Math.round(e.clientHeight*a));(e.width!==l||e.height!==R)&&(e.width=l,e.height=R),t[0]=e.width,t[1]=e.height,t[2]=p,t[3]=m,t[15]=v,t[25]=y,t[28]=b,t[29]=k,y*=.9,i.queue.writeBuffer(d,0,t);const x=i.createCommandEncoder(),M=x.beginRenderPass({colorAttachments:[{view:h.getCurrentTexture().createView(),clearValue:{r:0,g:0,b:0,a:0},loadOp:"clear",storeOp:"store"}]});M.setPipeline(r),M.setBindGroup(0,f),M.draw(3),M.end(),i.queue.submit([x.finish()])},c=()=>{g||(E||A(),o||(p+=1/60,u=requestAnimationFrame(c)))},I=()=>{o&&!g&&c()};return window.addEventListener("resize",I),c(),{pulse(){y=1},setSpec(a,l=!1,R=0){const x=B(a,l,R);x[2]=p,t=x,o&&c()},setHover(a){m=a?1:0},setMood(a){v=a==="talking"?2:a==="thinking"?1:a==="sleepy"?3:a==="greet"?4:0,o&&c()},setGaze(a,l){b=Math.max(-1,Math.min(1,a)),k=Math.max(-1,Math.min(1,l)),o&&c()},async snapshot(a=!1){a&&(E=!0,t[30]=1,A()),await i.queue.onSubmittedWorkDone();const l=e.toDataURL("image/png");return a&&(t[30]=0,E=!1),l},destroy(){g=!0,cancelAnimationFrame(u),window.removeEventListener("resize",I),d.destroy()}}}export{N as a,P as m};
assets/model-registry-BoK-ObgACo.js ADDED
@@ -0,0 +1 @@
 
 
1
+ (function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))n(o);new MutationObserver(o=>{for(const i of o)if(i.type==="childList")for(const m of i.addedNodes)m.tagName==="LINK"&&m.rel==="modulepreload"&&n(m)}).observe(document,{childList:!0,subtree:!0});function a(o){const i={};return o.integrity&&(i.integrity=o.integrity),o.referrerPolicy&&(i.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?i.credentials="include":o.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function n(o){if(o.ep)return;o.ep=!0;const i=a(o);fetch(o.href,i)}})();function g(e){const t=`${e}model.`;return{embedWeight:[`${t}embed_tokens.weight`],embedScale:[`${t}embed_tokens.scales`],lmHeadWeight:[`${e}lm_head.weight`],lmHeadScale:[`${e}lm_head.scales`],finalNorm:[`${t}norm.weight`],biasFor:a=>a.replace(/\.weight$/,".biases"),layer:a=>{const n=`${t}layers.${a}`,o=`${n}.linear_attn`;return{qkvWeight:[`${n}.self_attn.q_proj.weight`],qkvScale:[`${n}.self_attn.q_proj.scales`],oProjWeight:[`${n}.self_attn.o_proj.weight`],oProjScale:[`${n}.self_attn.o_proj.scales`],norm1:[`${n}.input_layernorm.weight`],norm2:[`${n}.post_attention_layernorm.weight`],ffnWeight:[`${n}.mlp.gate_proj.weight`],ffnScale:[`${n}.mlp.gate_proj.scales`],ffnDownWeight:[`${n}.mlp.down_proj.weight`],ffnDownScale:[`${n}.mlp.down_proj.scales`],qNorm:[`${n}.self_attn.q_norm.weight`],kNorm:[`${n}.self_attn.k_norm.weight`],gdnQkvWeight:[`${o}.in_proj_qkv.weight`],gdnQkvScale:[`${o}.in_proj_qkv.scales`],gdnZWeight:[`${o}.in_proj_z.weight`],gdnZScale:[`${o}.in_proj_z.scales`],gdnAWeight:[`${o}.in_proj_a.weight`],gdnAScale:[`${o}.in_proj_a.scales`],gdnBWeight:[`${o}.in_proj_b.weight`],gdnBScale:[`${o}.in_proj_b.scales`],gdnALog:[`${o}.A_log`],gdnDtBias:[`${o}.dt_bias`],gdnConvWeight:[`${o}.conv1d.weight`],gdnNormWeight:[`${o}.norm.weight`],gdnOutWeight:[`${o}.out_proj.weight`],gdnOutScale:[`${o}.out_proj.scales`]}}}}function l(e){var k,v;if(e.heads%e.kvHeads!==0)throw new Error(`${e.id}: heads not divisible by kvHeads`);if(e.headDim%2!==0)throw new Error(`${e.id}: headDim must be even (RoPE pairs)`);if(e.d%32!==0)throw new Error(`${e.id}: d must be divisible by 32 (int4 scale groups)`);if(e.fullAttnInterval&&!e.gdn)throw new Error(`${e.id}: fullAttnInterval requires gdn dims`);const t=e.heads*e.headDim,a=e.kvHeads*e.headDim,n=e.headDim/4,o=e.gdn??{kHeads:e.heads,vHeads:e.heads,headK:e.headDim,headV:e.headDim,convK:4};if(o.vHeads%o.kHeads!==0)throw new Error(`${e.id}: gdn vHeads not divisible by kHeads`);const i=e.fullAttnInterval??0,m=Object.freeze(Array.from({length:e.layers},(p,u)=>i&&(u+1)%i!==0?"gdn":"attn")),s=new Set(((k=e.moe)==null?void 0:k.denseLayers)??[]);if(s.size&&((v=e.moe)==null?void 0:v.denseFfn)===void 0)throw new Error(`${e.id}: moe.denseLayers needs moe.denseFfn β€” a dense layer's width is not moe_intermediate_size`);const d=Object.freeze(Array.from({length:e.layers},(p,u)=>e.moe&&!s.has(u)?"moe":"dense")),c=m.filter(p=>p==="attn").length,r=e.mla;if(r){if(r.vHeadDim!==e.headDim)throw new Error(`${e.id}: mla.vHeadDim ${r.vHeadDim} must equal headDim ${e.headDim} (o_proj contracts over heads*vHeadDim, and qDim is derived from headDim)`);if(r.qLoraRank!==null)throw new Error(`${e.id}: mla.qLoraRank ${r.qLoraRank} β€” a q_a_proj/q_b_proj split checkpoint (V2-full, V3). The loader plans a single q_proj; nothing here produces those records.`)}const h=r?r.kvLoraRank+r.qkRopeHeadDim:0,w=r?e.maxPages*e.pageSize*r.kvLoraRank*2:0;if(r&&w%256!==0)throw new Error(`${e.id}: MLA latent region is ${w} B, not a multiple of 256`);const q=e.mla?e.mla.qkRopeHeadDim:Math.round(e.headDim*(e.partialRotaryFactor??1));if(q%2!==0)throw new Error(`${e.id}: rotaryDim must be even (RoPE pairs)`);return Object.freeze({...e,layerKinds:m,ffnKinds:d,ffnWidthAt:p=>d[p]==="dense"&&e.moe?e.moe.denseFfn:e.ffn,rotaryDim:q,halfRotary:q/2,cAttnDim:t+2*a+(e.attnGate?t:0),gdnKHeads:o.kHeads,gdnVHeads:o.vHeads,gdnHeadK:o.headK,gdnHeadV:o.headV,gdnConvK:o.convK,gdnGvaGroup:o.vHeads/o.kHeads,gdnKDim:o.kHeads*o.headK,gdnVDim:o.vHeads*o.headV,gdnQkvDim:2*o.kHeads*o.headK+o.vHeads*o.headV,gdnStatePerHead:o.headK*o.headV,gdnProjRows:2*o.kHeads*o.headK+o.vHeads*o.headV+o.vHeads*o.headV+2*o.vHeads,qDim:t,kvDim:a,qkvDim:t+2*a,gqaGroup:e.heads/e.kvHeads,halfHeadDim:e.headDim/2,dPacked:e.d/8,dScales:e.d/32,qkvGroupPairs:e.heads*(e.headDim/2),qkvPairs:(t+2*a)/2,headPageStride:e.pageSize*e.headDim,kvPageStride:2*e.kvHeads*e.pageSize*e.headDim,vPageOffset:e.kvHeads*e.pageSize*e.headDim,maxContext:e.maxPages*e.pageSize,kvBytesPerToken:r?h*2*c:2*e.kvHeads*e.headDim*2*c,mlaCachePerToken:h,mlaQProjRows:r?e.heads*(r.qkNopeHeadDim+r.qkRopeHeadDim):0,mlaKvaRows:r?r.kvLoraRank+r.qkRopeHeadDim:0,mlaKvbRows:r?e.heads*(r.qkNopeHeadDim+r.vHeadDim):0,mlaLatentBytes:w,mlaCacheBytes:r?e.maxPages*e.pageSize*h*2:0,kvI8RowWords:n,kvI8SlotWords:2*n,kvI8HeadWords:e.pageSize*2*n,kvI8PageWords:e.kvHeads*e.pageSize*2*n,kvScalesPerSlot:2,kvScalesPerHead:e.pageSize*2,kvScalesPerPage:e.kvHeads*e.pageSize*2,moeSlots:e.moe?e.moe.topK+(e.moe.sharedExpert??!0?1:0):1,sharedExpertIndex:e.moe&&(e.moe.sharedExpert??!0)?e.moe.experts:-1})}function L(e){const t=e.ropeScaling;if(!t||t.ropeType!=="yarn"||!t.mscaleAllDim||t.factor<=1)return 1;const a=.1*t.mscaleAllDim*Math.log(t.factor)+1;return a*a}function E(e){const t=new Float32Array(e.halfRotary),a=e.ropeScaling;if((a==null?void 0:a.ropeType)==="yarn"){const n=e.rotaryDim,o=s=>n*Math.log(a.originalMaxPositionEmbeddings/(s*2*Math.PI))/(2*Math.log(e.ropeTheta)),i=Math.max(Math.floor(o(a.betaFast)),0);let m=Math.min(Math.ceil(o(a.betaSlow)),n-1);m===i&&(m=i+.001);for(let s=0;s<e.halfRotary;s++){const d=Math.pow(e.ropeTheta,-(2*s)/n),c=d/a.factor,h=1-Math.min(Math.max((s-i)/(m-i),0),1);t[s]=c*(1-h)+d*h}return t}for(let n=0;n<e.halfRotary;n++){let o=Math.pow(e.ropeTheta,-(2*n)/e.rotaryDim);if(a){const i=a.originalMaxPositionEmbeddings/a.lowFreqFactor,m=a.originalMaxPositionEmbeddings/a.highFreqFactor,s=2*Math.PI/o;if(s>i)o=o/a.factor;else if(!(s<m)){const d=(a.originalMaxPositionEmbeddings/s-a.lowFreqFactor)/(a.highFreqFactor-a.lowFreqFactor);o=(1-d)*(o/a.factor)+d*o}}t[n]=o}return t}const _=l({id:"phi3-mini",d:3072,layers:32,heads:32,kvHeads:32,headDim:96,ffn:8192,vocab:32064,pageSize:16,maxPages:257,maxSeq:4096,ropeTheta:1e4,rmsEps:1e-5,tiedEmbeddings:!1,qkNorm:!1,stops:[2,32e3,32007],chatTemplateId:"phi3",tokenizerKind:"spm",hfRepo:"mlc-ai/Phi-3-mini-4k-instruct-q4f16_1-MLC",paramNaming:{embedWeight:["transformer.embd.q_weight","embed_tokens.q_weight","model.embed_tokens.q_weight"],embedScale:["transformer.embd.q_scale","embed_tokens.q_scale","model.embed_tokens.q_scale"],lmHeadWeight:["lm_head.q_weight","model.lm_head.q_weight"],lmHeadScale:["lm_head.q_scale","model.lm_head.q_scale"],finalNorm:["transformer.norm.weight","model.norm.weight","norm.weight"],layer:e=>{const t=`transformer.h.${e}`,a=`model.layers.${e}`;return{qkvWeight:[`${t}.mixer.qkv_proj.q_weight`,`${a}.self_attn.qkv_proj.q_weight`],qkvScale:[`${t}.mixer.qkv_proj.q_scale`,`${a}.self_attn.qkv_proj.q_scale`],oProjWeight:[`${t}.mixer.out_proj.q_weight`,`${a}.self_attn.o_proj.q_weight`],oProjScale:[`${t}.mixer.out_proj.q_scale`,`${a}.self_attn.o_proj.q_scale`],norm1:[`${t}.ln.weight`,`${a}.input_layernorm.weight`],norm2:[`${t}.post_attention_layernorm.weight`,`${a}.post_attention_layernorm.weight`],ffnWeight:[`${t}.mlp.gate_up_proj.q_weight`,`${a}.mlp.gate_up_proj.q_weight`],ffnScale:[`${t}.mlp.gate_up_proj.q_scale`,`${a}.mlp.gate_up_proj.q_scale`],ffnDownWeight:[`${t}.mlp.down_proj.q_weight`,`${a}.mlp.down_proj.q_weight`],ffnDownScale:[`${t}.mlp.down_proj.q_scale`,`${a}.mlp.down_proj.q_scale`]}}}}),y=l({id:"qwen3-4b",d:2560,layers:36,heads:32,kvHeads:8,headDim:128,ffn:9728,vocab:151936,pageSize:16,maxPages:448,maxSeq:40960,ropeTheta:1e6,rmsEps:1e-6,tiedEmbeddings:!0,qkNorm:!0,stops:[151645,151643],chatTemplateId:"chatml",tokenizerKind:"byteLevel",hfRepo:"mlc-ai/Qwen3-4B-q4f16_1-MLC",paramNaming:{embedWeight:["model.embed_tokens.q_weight"],embedScale:["model.embed_tokens.q_scale"],lmHeadWeight:["model.embed_tokens.q_weight"],lmHeadScale:["model.embed_tokens.q_scale"],finalNorm:["model.norm.weight"],layer:e=>{const t=`model.layers.${e}`;return{qkvWeight:[`${t}.self_attn.c_attn.q_weight`,`${t}.self_attn.qkv_proj.q_weight`],qkvScale:[`${t}.self_attn.c_attn.q_scale`,`${t}.self_attn.qkv_proj.q_scale`],oProjWeight:[`${t}.self_attn.o_proj.q_weight`],oProjScale:[`${t}.self_attn.o_proj.q_scale`],norm1:[`${t}.input_layernorm.weight`],norm2:[`${t}.post_attention_layernorm.weight`],ffnWeight:[`${t}.mlp.gate_up_proj.q_weight`],ffnScale:[`${t}.mlp.gate_up_proj.q_scale`],ffnDownWeight:[`${t}.mlp.down_proj.q_weight`],ffnDownScale:[`${t}.mlp.down_proj.q_scale`],qNorm:[`${t}.self_attn.q_norm.weight`],kNorm:[`${t}.self_attn.k_norm.weight`]}}}}),j=l({id:"qwen35-4b",d:2560,layers:32,heads:16,kvHeads:4,headDim:256,ffn:9216,vocab:248320,pageSize:16,maxPages:2048,maxSeq:262144,ropeTheta:1e7,rmsEps:1e-6,tiedEmbeddings:!0,qkNorm:!0,stops:[248046,248044],chatTemplateId:"chatml",tokenizerKind:"byteLevel",hfRepo:"mlc-ai/Qwen3.5-4B-q4f16_1-MLC",manifestName:"tensor-cache.json",fullAttnInterval:4,gdn:{kHeads:16,vHeads:32,headK:128,headV:128,convK:4},partialRotaryFactor:.25,attnGate:!0,paramNaming:{embedWeight:["model.embed_tokens.q_weight"],embedScale:["model.embed_tokens.q_scale"],lmHeadWeight:["model.embed_tokens.q_weight"],lmHeadScale:["model.embed_tokens.q_scale"],finalNorm:["model.norm.weight"],layer:e=>{const t=`model.layers.${e}`,a=`${t}.linear_attn`;return{qkvWeight:[`${t}.self_attn.c_attn.q_weight`],qkvScale:[`${t}.self_attn.c_attn.q_scale`],oProjWeight:[`${t}.self_attn.o_proj.q_weight`],oProjScale:[`${t}.self_attn.o_proj.q_scale`],norm1:[`${t}.input_layernorm.weight`],norm2:[`${t}.post_attention_layernorm.weight`],ffnWeight:[`${t}.mlp.gate_up_proj.q_weight`],ffnScale:[`${t}.mlp.gate_up_proj.q_scale`],ffnDownWeight:[`${t}.mlp.down_proj.q_weight`],ffnDownScale:[`${t}.mlp.down_proj.q_scale`],qNorm:[`${t}.self_attn.q_norm.weight`],kNorm:[`${t}.self_attn.k_norm.weight`],gdnQkvWeight:[`${a}.in_proj_qkv.q_weight`],gdnQkvScale:[`${a}.in_proj_qkv.q_scale`],gdnZWeight:[`${a}.in_proj_z.q_weight`],gdnZScale:[`${a}.in_proj_z.q_scale`],gdnAWeight:[`${a}.in_proj_a.q_weight`],gdnAScale:[`${a}.in_proj_a.q_scale`],gdnBWeight:[`${a}.in_proj_b.q_weight`],gdnBScale:[`${a}.in_proj_b.q_scale`],gdnALog:[`${a}.A_log`],gdnDtBias:[`${a}.dt_bias`],gdnConvWeight:[`${a}.conv1d_weight`],gdnNormWeight:[`${a}.norm.weight`],gdnOutWeight:[`${a}.out_proj.q_weight`],gdnOutScale:[`${a}.out_proj.q_scale`]}}}}),f=l({id:"qwen36-35b-a3b",d:2048,layers:40,heads:16,kvHeads:2,headDim:256,ffn:512,vocab:248320,pageSize:16,maxPages:2048,maxSeq:262144,ropeTheta:1e7,rmsEps:1e-6,tiedEmbeddings:!1,qkNorm:!0,stops:[248046,248044],chatTemplateId:"chatml",tokenizerKind:"byteLevel",hfRepo:"lmstudio-community/Qwen3.6-35B-A3B-MLX-4bit",manifestName:"model.safetensors.index.json",weightFormat:"mlx-safetensors",mlxPrefix:"language_model.",fullAttnInterval:4,gdn:{kHeads:16,vHeads:32,headK:128,headV:128,convK:4},partialRotaryFactor:.25,attnGate:!0,moe:{experts:256,topK:8,normTopkProb:!0},paramNaming:{embedWeight:["language_model.model.embed_tokens.weight"],embedScale:["language_model.model.embed_tokens.scales"],lmHeadWeight:["language_model.lm_head.weight"],lmHeadScale:["language_model.lm_head.scales"],finalNorm:["language_model.model.norm.weight"],biasFor:e=>e.replace(/\.weight$/,".biases"),layer:e=>{const t=`language_model.model.layers.${e}`,a=`${t}.linear_attn`;return{qkvWeight:[`${t}.self_attn.q_proj.weight`],qkvScale:[`${t}.self_attn.q_proj.scales`],oProjWeight:[`${t}.self_attn.o_proj.weight`],oProjScale:[`${t}.self_attn.o_proj.scales`],norm1:[`${t}.input_layernorm.weight`],norm2:[`${t}.post_attention_layernorm.weight`],ffnWeight:[`${t}.mlp.switch_mlp.gate_proj.weight`],ffnScale:[`${t}.mlp.switch_mlp.gate_proj.scales`],ffnDownWeight:[`${t}.mlp.switch_mlp.down_proj.weight`],ffnDownScale:[`${t}.mlp.switch_mlp.down_proj.scales`],qNorm:[`${t}.self_attn.q_norm.weight`],kNorm:[`${t}.self_attn.k_norm.weight`],gdnQkvWeight:[`${a}.in_proj_qkv.weight`],gdnQkvScale:[`${a}.in_proj_qkv.scales`],gdnZWeight:[`${a}.in_proj_z.weight`],gdnZScale:[`${a}.in_proj_z.scales`],gdnAWeight:[`${a}.in_proj_a.weight`],gdnAScale:[`${a}.in_proj_a.scales`],gdnBWeight:[`${a}.in_proj_b.weight`],gdnBScale:[`${a}.in_proj_b.scales`],gdnALog:[`${a}.A_log`],gdnDtBias:[`${a}.dt_bias`],gdnConvWeight:[`${a}.conv1d.weight`],gdnNormWeight:[`${a}.norm.weight`],gdnOutWeight:[`${a}.out_proj.weight`],gdnOutScale:[`${a}.out_proj.scales`]}}}}),B=l({...f,id:"qwen36-35b-a3b-q3",hfRepo:"abgunaydin/Qwen3.6-35B-A3B-MLX-q3exp",moe:{...f.moe,bits:3}}),b=l({id:"qwen3-8-27b-4bit",d:5120,layers:64,heads:24,kvHeads:4,headDim:256,ffn:17408,vocab:248320,pageSize:16,maxPages:1024,maxSeq:262144,ropeTheta:1e7,rmsEps:1e-6,tiedEmbeddings:!1,qkNorm:!0,stops:[248046,248044],chatTemplateId:"chatml",tokenizerKind:"byteLevel",hfRepo:"mlx-community/Qwen3.8-27B-4bit",manifestName:"model.safetensors.index.json",weightFormat:"mlx-safetensors",mlxPrefix:"language_model.",fullAttnInterval:4,gdn:{kHeads:16,vHeads:48,headK:128,headV:128,convK:4},attnGate:!0,partialRotaryFactor:.25,paramNaming:g("language_model.")}),x=l({id:"qwen3-5-9b-mlx-4bit",d:4096,layers:32,heads:16,kvHeads:4,headDim:256,ffn:12288,vocab:248320,pageSize:16,maxPages:2048,maxSeq:262144,ropeTheta:1e7,rmsEps:1e-6,tiedEmbeddings:!1,qkNorm:!0,stops:[248046,248044],chatTemplateId:"chatml",tokenizerKind:"byteLevel",hfRepo:"lmstudio-community/Qwen3.5-9B-MLX-4bit",manifestName:"model.safetensors.index.json",weightFormat:"mlx-safetensors",mlxPrefix:"language_model.",fullAttnInterval:4,gdn:{kHeads:16,vHeads:32,headK:128,headV:128,convK:4},attnGate:!0,partialRotaryFactor:.25,paramNaming:g("language_model.")}),D=l({id:"qwen3-embedding-0-6b-4bit-dwq",d:1024,layers:28,heads:16,kvHeads:8,headDim:128,ffn:3072,vocab:151669,pageSize:16,maxPages:512,maxSeq:32768,ropeTheta:1e6,rmsEps:1e-6,tiedEmbeddings:!0,qkNorm:!0,stops:[151643],chatTemplateId:"chatml",tokenizerKind:"byteLevel",hfRepo:"mlx-community/Qwen3-Embedding-0.6B-4bit-DWQ",manifestName:"model.safetensors.index.json",weightFormat:"mlx-safetensors",embeddingOnly:!0,paramNaming:g("")});l({id:"deepseek-v2-lite-chat-4bit",d:2048,layers:27,heads:16,kvHeads:16,headDim:128,ffn:1408,vocab:102400,pageSize:16,maxPages:2112,maxSeq:163840,ropeTheta:1e4,rmsEps:1e-6,tiedEmbeddings:!1,qkNorm:!1,stops:[100001],chatTemplateId:"deepseek",tokenizerKind:"byteLevel",hfRepo:"mlx-community/DeepSeek-V2-Lite-Chat-4bit-mlx",manifestName:"model.safetensors.index.json",weightFormat:"mlx-safetensors",mla:{kvLoraRank:512,qLoraRank:null,qkNopeHeadDim:128,qkRopeHeadDim:64,vHeadDim:128},moe:{experts:64,topK:6,normTopkProb:!1,sharedExpert:!0,routerBits:16,denseLayers:[0],denseFfn:10944},ropeScaling:{ropeType:"yarn",factor:40,betaFast:32,betaSlow:1,originalMaxPositionEmbeddings:4096,mscale:.707,mscaleAllDim:.707},paramNaming:g("")});const H=l({id:"qwen3-30b-a3b-4bit",d:2048,layers:48,heads:32,kvHeads:4,headDim:128,ffn:768,vocab:151936,pageSize:16,maxPages:640,maxSeq:40960,ropeTheta:1e6,rmsEps:1e-6,tiedEmbeddings:!1,qkNorm:!0,stops:[151645,151643],chatTemplateId:"chatml",tokenizerKind:"byteLevel",hfRepo:"mlx-community/Qwen3-30B-A3B-4bit",manifestName:"model.safetensors.index.json",weightFormat:"mlx-safetensors",moe:{experts:128,topK:8,normTopkProb:!0,sharedExpert:!1,routerBits:4},paramNaming:g("")}),N=l({id:"llama-3-2-1b-instruct-4bit",d:2048,layers:16,heads:32,kvHeads:8,headDim:64,ffn:8192,vocab:128256,pageSize:16,maxPages:2048,maxSeq:131072,ropeTheta:5e5,ropeScaling:{ropeType:"llama3",factor:32,lowFreqFactor:1,highFreqFactor:4,originalMaxPositionEmbeddings:8192},rmsEps:1e-5,tiedEmbeddings:!0,qkNorm:!1,stops:[128009,128001],chatTemplateId:"llama3",tokenizerKind:"byteLevel",hfRepo:"mlx-community/Llama-3.2-1B-Instruct-4bit",manifestName:"model.safetensors.index.json",weightFormat:"mlx-safetensors",paramNaming:g("")}),P=l({id:"qwen3-4b-mlx",d:2560,layers:36,heads:32,kvHeads:8,headDim:128,ffn:9728,vocab:151936,pageSize:16,maxPages:448,maxSeq:40960,ropeTheta:1e6,rmsEps:1e-6,tiedEmbeddings:!0,qkNorm:!0,stops:[151645,151643],chatTemplateId:"chatml",tokenizerKind:"byteLevel",hfRepo:"mlx-community/Qwen3-4B-4bit",manifestName:"model.safetensors.index.json",weightFormat:"mlx-safetensors",paramNaming:g("")}),W=[{param:"",spec:_},{param:"qwen3",spec:y},{param:"qwen35",spec:j},{param:"qwen36q3",spec:B},{param:"qwen36",spec:f},{param:"qwen3mlx",spec:P},{param:"llama32",spec:N},{param:"qwen30b",spec:H},{param:"embed",spec:D},{param:"qwen35mlx",spec:x},{param:"qwen38",spec:b}];function z(e){const t=e&&W.find(a=>a.param===e);return t?t.spec:_}function M(e,t){if(!Number.isFinite(t)||t<=0)return e;const a=Math.max(1,Math.min(Math.ceil(t/e.pageSize),Math.floor(e.maxSeq/e.pageSize)));return a===e.maxPages?e:l({...e,maxPages:a})}const $="zero-tvm-weights";function A(e){return e.id===_.id?$:`${$}.${e.id}`}const S={[B.id]:{name:"Qwen3.6-35B-A3B",params:"35B-A3B MoE Β· 3-bit experts",sizeLabel:"~16.4 GB",rateLabel:"~66 t/s",poolModes:[{slots:0,label:"Full Β· 15.7 GB resident Β· ~66 t/s"},{slots:128,label:"Half Β· ~8.4 GB resident Β· ~15 t/s",note:"pooled rates are the 2026-08-15 AC round, whose own unpooled control read 58.6 t/s β€” compare against that, not the 66 above"},{slots:64,label:"Quarter Β· ~4.8 GB resident Β· ~12 t/s",note:"long prompts run token-by-token"}],ramNote:"needs ~20 GB free RAM"},[f.id]:{name:"Qwen3.6-35B-A3B",params:"35B-A3B MoE Β· full 4-bit",sizeLabel:"~19.5 GB",poolModes:[{slots:0,label:"Full Β· 19.7 GB resident"},{slots:128,label:"Half Β· ~11 GB est"},{slots:64,label:"Quarter Β· ~6 GB est",note:"long prompts run token-by-token"}],rateLabel:"",ramNote:"needs ~24 GB free RAM (64 GB Mac recommended)"},[j.id]:{name:"Qwen3.5-4B",params:"4B hybrid (DeltaNet)",sizeLabel:"~2.6 GB",rateLabel:"~65 t/s"},[y.id]:{name:"Qwen3-4B",params:"4B dense Β· q4f16_1",sizeLabel:"~2.3 GB",rateLabel:"~60 t/s"},[_.id]:{name:"Phi-3-mini",params:"3.8B dense",sizeLabel:"~2 GB",rateLabel:"~70 t/s"},[P.id]:{name:"Qwen3-4B",params:"4B dense Β· MLX 4-bit",sizeLabel:"~2.1 GB",rateLabel:"~81 t/s"},[N.id]:{name:"Llama-3.2-1B-Instruct",params:"1B dense",sizeLabel:"~0.6 GB",rateLabel:"~256 t/s"},[H.id]:{name:"Qwen3-30B-A3B",params:"30B-A3B MoE",sizeLabel:"~17.2 GB",rateLabel:"~75 t/s",ramNote:"needs ~20 GB free RAM",poolModes:[{slots:0,label:"Full Β· ~17 GB Β· ~75 t/s"},{slots:96,label:"ΒΎ Β· ~13 GB est"},{slots:64,label:"Half Β· ~9.5 GB est",note:"long prompts run token-by-token"}]},[D.id]:{name:"Qwen3-Embedding-0.6B",params:"0.6B embedding Β· last-token pooled",sizeLabel:"~0.35 GB",rateLabel:""},[x.id]:{name:"Qwen3.5-9B",params:"9B hybrid (DeltaNet)",sizeLabel:"~4.7 GB",rateLabel:"~43 t/s"},[b.id]:{name:"Qwen3.8-27B",params:"27B hybrid (DeltaNet)",sizeLabel:"~14.1 GB",rateLabel:"",ramNote:"needs ~18 GB free RAM"}};function R(e){return S[e.id]??S[_.id]}export{_ as P,W as S,$ as W,z as a,L as b,R as m,A as o,E as r,M as s};
assets/model-select-DhhV9mCi0T.js ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import{s as D,a as B}from"./model-registry-BoK-ObgACo.js";import{S as pt,m as mt}from"./model-registry-BoK-ObgACo.js";import{P as O,r as F}from"./weight-loader-ByOLMd9f5C.js";async function j(e){try{const c=await caches.keys();for(const t of c){const r=await(await caches.open(t)).match(e);if(r)return r.text()}}catch{}const n=await fetch(e);if(!n.ok)throw new Error(`HTTP ${n.status} fetching tokenizer.json`);return n.text()}const I="▁",U=new RegExp(I,"g"),H=/^<0x([0-9A-Fa-f]{2})>$/,J=new TextEncoder;function W(e){const n=e.model.vocab,c=e.model.merges,t=new Array(Math.max(...Object.values(n))+1);for(const[i,f]of Object.entries(n))t[f]=i;for(const i of e.added_tokens??[])t[i.id]=i.content;const d=new Map;for(let i=0;i<c.length;i++)d.set(c[i],i);const r=new Map,a=new Set;for(const i of e.added_tokens??[])r.set(i.content,{id:i.id,lstrip:i.lstrip,rstrip:i.rstrip}),a.add(i.id);const m=[...r.keys()].sort((i,f)=>f.length-i.length).map(i=>i.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join("|"),$=m?new RegExp(`(${m})`):null,E=n["<s>"]??1,T=n["</s>"]??2,y=n["<unk>"]??0,b=new Int32Array(256).fill(y);for(let i=0;i<256;i++){const f=n[`<0x${i.toString(16).toUpperCase().padStart(2,"0")}>`];f!==void 0&&(b[i]=f)}const _=new Map,x=new Set;for(let i=0;i<t.length;i++){const f=t[i];if(!f)continue;const u=H.exec(f);if(u){_.set(i,parseInt(u[1],16));continue}if(f==="<s>"||f==="</s>"||f==="<pad>"){x.add(i);continue}f.startsWith("<|")&&f.endsWith("|>")&&x.add(i)}function A(i){if(i.length<=1)return i;for(;;){let f=1/0,u=-1;for(let h=0;h<i.length-1;h++){const o=i[h]+" "+i[h+1],s=d.get(o);s!==void 0&&s<f&&(f=s,u=h)}if(u===-1)break;const k=i[u]+i[u+1];i=[...i.slice(0,u),k,...i.slice(u+2)]}return i}const M=n[I];function C(i){if(i.length===0)return[];const f=I+i.replaceAll(I," "),u=$?f.split($):[f];for(let h=1;h<u.length;h+=2){const o=r.get(u[h]);o&&(o.lstrip&&h>0&&(u[h-1]=u[h-1].trimEnd()),o.rstrip&&h<u.length-1&&(u[h+1]=u[h+1].trimStart()))}const k=[];for(let h=0;h<u.length;h++){const o=u[h];if(!o)continue;if(h%2===1){const l=r.get(o);if(l!==void 0){k.push(l.id);continue}}const s=o.replaceAll(" ",I);for(const l of A([...s])){const p=n[l];if(p!==void 0)k.push(p);else for(const g of J.encode(l))k.push(b[g])}}return k.length>1&&k[0]===M&&a.has(k[1])&&k.shift(),k}const L=new TextDecoder("utf-8",{fatal:!1});function R(i){let f="",u=[];const k=()=>{u.length!==0&&(f+=L.decode(Uint8Array.from(u)),u=[])};for(const h of i){if(h<0||x.has(h)){k();continue}const o=_.get(h);if(o!==void 0){u.push(o);continue}const s=t[h];s&&(k(),f+=s)}return k(),f=f.replace(U," "),f.startsWith(" ")?f.slice(1):f}return{encode:C,decode:R,bosId:E,eosId:T}}async function K(e,n=O){e==null||e("Loading tokenizer.json...");const c=n+"tokenizer.json",t=JSON.parse(await j(c)),d=W(t);return e==null||e("Tokenizer ready"),d}function q(e,n){let c="";for(const t of e)t.role==="system"?c+=`<|system|>
2
+ ${t.content}<|end|>
3
+ `:t.role==="user"?c+=`<|user|>
4
+ ${t.content}<|end|>
5
+ `:c+=`<|assistant|>
6
+ ${t.content}<|end|>
7
+ `;return c+=`<|assistant|>
8
+ `,n.encode(c)}const Q="https://huggingface.co/mlc-ai/Qwen3-4B-q4f16_1-MLC/resolve/main/";async function V(e){try{const c=await caches.keys();for(const t of c){const r=await(await caches.open(t)).match(e);if(r)return r.text()}}catch{}const n=await fetch(e);if(!n.ok)throw new Error(`HTTP ${n.status} fetching tokenizer.json`);return n.text()}const N=new RegExp("'(?:[sS]|[tT]|[rR][eE]|[vV][eE]|[mM]|[lL][lL]|[dD])|[^\\r\\n\\p{L}\\p{N}]?\\p{L}+|\\p{N}| ?[^\\s\\p{L}\\p{N}]+[\\r\\n]*|\\s*[\\r\\n]+|\\s+(?!\\S)|\\s+","gu");function Y(e){var r,a;const n=e.pre_tokenizer,t=(a=(r=((n==null?void 0:n.pretokenizers)??(n?[n]:[])).find(m=>m.type==="Split"))==null?void 0:r.pattern)==null?void 0:a.Regex;if(typeof t!="string")return N;const d=t.replace("(?i:'s|'t|'re|'ve|'m|'ll|'d)","'(?:[sS]|[tT]|[rR][eE]|[vV][eE]|[mM]|[lL][lL]|[dD])");try{return new RegExp(d,"gu")}catch{return N}}function P(){const e=new Array(256),n=new Map;let c=0;for(let t=0;t<256;t++){const d=t>=33&&t<=126||t>=161&&t<=172||t>=174&&t<=255,r=String.fromCharCode(d?t:256+c++);e[t]=r,n.set(r,t)}return{byteToChar:e,charToByte:n}}const Z=new TextEncoder;function G(e){const n=e.model.vocab,{byteToChar:c,charToByte:t}=P(),d=Y(e),r=e.normalizer===void 0||e.normalizer!==null;let a=0;for(const o of Object.values(n))o>a&&(a=o);const m=new Array(a+1);for(const[o,s]of Object.entries(n))m[s]=o;const $=new Map,E=e.model.merges;for(let o=0;o<E.length;o++){const s=E[o];$.set(typeof s=="string"?s:s[0]+" "+s[1],o)}const T=new Map,y=new Set,b=new Map;for(const o of e.added_tokens??[])T.set(o.content,o.id),o.special?y.add(o.id):b.set(o.id,o.content);const _=[...T.keys()].sort((o,s)=>s.length-o.length).map(o=>o.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join("|"),x=_?new RegExp(`(${_})`):null,A=T.get("<|im_end|>")??T.get("<|eot_id|>")??151645,M=T.get("<|endoftext|>")??T.get("<|begin_of_text|>")??151643,C=[A,T.get("<|end_of_text|>")??M],L=new Map;function R(o){const s=L.get(o);if(s!==void 0)return s;let l=[...o];for(;l.length>1;){let p=1/0,g=-1;for(let w=0;w<l.length-1;w++){const v=$.get(l[w]+" "+l[w+1]);v!==void 0&&v<p&&(p=v,g=w)}if(g===-1)break;const S=l[g]+l[g+1];l=[...l.slice(0,g),S,...l.slice(g+2)]}return L.set(o,l),l}function i(o){const s=[];let l=0;for(const p of o.matchAll(d))p.index>l&&s.push(o.slice(l,p.index)),s.push(p[0]),l=p.index+p[0].length;return l<o.length&&s.push(o.slice(l)),s}function f(o){if(o.length===0)return[];const s=x?o.split(x):[o],l=[];for(let p=0;p<s.length;p++){const g=s[p];if(g){if(p%2===1){const S=T.get(g);if(S!==void 0){l.push(S);continue}}for(const S of i(r?g.normalize("NFC"):g)){let w="";for(const v of Z.encode(S))w+=c[v];for(const v of R(w)){const z=n[v];z!==void 0&&l.push(z)}}}}return l}const u=new Array(m.length).fill(null);for(let o=0;o<m.length;o++){const s=m[o];if(!s)continue;const l=new Uint8Array(s.length);for(let p=0;p<s.length;p++)l[p]=t.get(s[p])??0;u[o]=l}const k=new TextDecoder("utf-8",{fatal:!1});function h(o){let s="",l=[];const p=()=>{l.length!==0&&(s+=k.decode(Uint8Array.from(l)),l=[])};for(const g of o){if(g<0||y.has(g)){p();continue}const S=b.get(g);if(S!==void 0){p(),s+=S;continue}const w=u[g];if(w)for(const v of w)l.push(v)}return p(),s}return{encode:f,decode:h,bosId:M,eosId:A,stopIds:C}}async function X(e,n=Q){e==null||e("Loading tokenizer.json...");const c=n+"tokenizer.json",t=JSON.parse(await V(c)),d=G(t);return e==null||e("Tokenizer ready"),d}function tt(e,n,c){let t="";for(const d of e){const r=d.role==="assistant"?`<think>
9
+
10
+ </think>
11
+
12
+ ${d.content}`:d.content;t+=`<|im_start|>${d.role}
13
+ ${r}<|im_end|>
14
+ `}return t+=`<|im_start|>assistant
15
+ `,t+=`<think>
16
+
17
+ </think>
18
+
19
+ `,n.encode(t)}const et=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function nt(e=new Date){return`${String(e.getDate()).padStart(2,"0")} ${et[e.getMonth()]} ${e.getFullYear()}`}function ot(e,n,c){const t="<|begin▁of▁sentence|>",d="<|end▁of▁sentence|>";let r=t;for(const a of e)a.role==="user"?r+=`User: ${a.content}
20
+
21
+ `:a.role==="assistant"?r+=`Assistant: ${a.content}${d}`:a.role==="system"&&(r+=`${a.content}
22
+
23
+ `);return r+="Assistant:",n.encode(r)}function ct(e,n,c){var m;const t=nt();let d=e,r="";((m=d[0])==null?void 0:m.role)==="system"&&(r=d[0].content.trim(),d=d.slice(1));let a=`<|begin_of_text|><|start_header_id|>system<|end_header_id|>
24
+
25
+ `;a+=`Cutting Knowledge Date: December 2023
26
+ Today Date: ${t}
27
+
28
+ `,a+=`${r}<|eot_id|>`;for(const $ of d)a+=`<|start_header_id|>${$.role}<|end_header_id|>
29
+
30
+ ${$.content.trim()}<|eot_id|>`;return a+=`<|start_header_id|>assistant<|end_header_id|>
31
+
32
+ `,n.encode(a)}function it(e,n,c){var T;const t=(c==null?void 0:c.variant)??"v1",d=(c==null?void 0:c.bosToken)??"<s>",r=(c==null?void 0:c.eosToken)??"</s>";let a=e,m="";((T=a[0])==null?void 0:T.role)==="system"&&(m=a[0].content,a=a.slice(1));const $=m===""?-1:t==="v1"?a.findIndex(y=>y.role==="user"):a.length-1;let E=d;for(let y=0;y<a.length;y++){const b=a[y];if(b.role==="assistant"){const x=t==="ministral"?b.content.trim():b.content;E+=t==="v1"?` ${x} ${r}`:` ${x}${r}`;continue}const _=y===$?`${m}
33
+
34
+ ${b.content}`:b.content;E+=t==="v1"?` [INST] ${_} [/INST]`:t==="nemo"?`[INST] ${_}[/INST]`:`[INST]${_}[/INST]`}return n.encode(E)}function rt(e,n,c){const t=(c==null?void 0:c.eosToken)??"<|endoftext|>";let d=(c==null?void 0:c.bosToken)??"";for(let r=0;r<e.length;r++){const a=e[r],m=r===e.length-1;d+=a.role==="assistant"?`<|assistant|>
35
+ ${a.content}${t}${m?"":`
36
+ `}`:`<|${a.role}|>
37
+ ${a.content}
38
+ `,m&&(d+=`<|assistant|>
39
+ `)}return n.encode(d)}function lt(e){const n=new URLSearchParams(e);return D(B(n.get("model")),Number(n.get("ctx")))}async function dt(e,n){const c=await F(e);return e.tokenizerKind==="byteLevel"?X(n,c):K(n,c)}function ft(e,n,c){const t=e.chatTemplateId;return t==="chatml"?tt(n,c):t==="deepseek"?ot(n,c):t==="llama3"?ct(n,c):t==="mistral"||t==="mistral-nemo"||t==="ministral"?it(n,c,{variant:t==="mistral"?"v1":t==="mistral-nemo"?"nemo":"ministral"}):t==="olmo2"||t==="falcon3"?rt(n,c,t==="olmo2"?{bosToken:"<|endoftext|>"}:void 0):q(n,c)}export{pt as SHIPPED_MODELS,ft as buildChatPromptFor,dt as loadTokenizerFor,mt as modelBranding,lt as specFromSearch};
assets/preload-helper-ckwbz45pO3.js ADDED
@@ -0,0 +1 @@
 
 
1
+ const v="modulepreload",E=function(i){return"/"+i},d={},y=function(f,l,p){let c=Promise.resolve();if(l&&l.length>0){let n=function(t){return Promise.all(t.map(o=>Promise.resolve(o).then(s=>({status:"fulfilled",value:s}),s=>({status:"rejected",reason:s}))))};document.getElementsByTagName("link");const e=document.querySelector("meta[property=csp-nonce]"),u=(e==null?void 0:e.nonce)||(e==null?void 0:e.getAttribute("nonce"));c=n(l.map(t=>{if(t=E(t),t in d)return;d[t]=!0;const o=t.endsWith(".css"),s=o?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${t}"]${s}`))return;const r=document.createElement("link");if(r.rel=o?"stylesheet":v,o||(r.as="script"),r.crossOrigin="",r.href=t,u&&r.setAttribute("nonce",u),document.head.appendChild(r),o)return new Promise((m,h)=>{r.addEventListener("load",m),r.addEventListener("error",()=>h(new Error(`Unable to preload CSS for ${t}`)))})}))}function a(n){const e=new Event("vite:preloadError",{cancelable:!0});if(e.payload=n,window.dispatchEvent(e),!e.defaultPrevented)throw n}return c.then(n=>{for(const e of n||[])e.status==="rejected"&&a(e.reason);return f().catch(a)})};export{y as _};
assets/room-host-C6R3zseuU5.js ADDED
@@ -0,0 +1 @@
 
 
1
+ import{q as pe}from"./chat-ui-CQAxV1po6S.js";import{o as ue}from"./model-registry-BoK-ObgACo.js";const Y=240*1024,Z=8*1024*1024,ge=8*1024*1024,ye=1*1024*1024;async function H(e,s){var i;if(!((i=navigator.storage)!=null&&i.getDirectory))return null;try{return await(await navigator.storage.getDirectory()).getDirectoryHandle(ue(e),{create:s})}catch{return null}}async function R(e){const s=[];for await(const[i,c]of e){if(c.kind!=="file")continue;const h=await c.getFile();s.push({name:i,size:h.size})}return s.sort((i,c)=>i.size-c.size),s}const he=e=>[...new Uint8Array(e)].map(s=>s.toString(16).padStart(2,"0")).join("");async function B(e){const s=e.slice();return he(await crypto.subtle.digest("SHA-256",s))}function ee(e,s){const i=new TextEncoder().encode(JSON.stringify(e)),c=new Uint8Array(4+i.byteLength+s.byteLength);return new DataView(c.buffer).setUint32(0,i.byteLength,!0),c.set(i,4),c.set(s,4+i.byteLength),c.buffer}function me(e){const s=new DataView(e).getUint32(0,!0);return{header:JSON.parse(new TextDecoder().decode(new Uint8Array(e,4,s))),payload:new Uint8Array(e,4+s)}}function we(e,s,i){e.bufferedAmountLowThreshold=ye,e.binaryType="arraybuffer";async function c(){for(;e.readyState==="open"&&e.bufferedAmount>ge;)await new Promise(d=>{const a=()=>{clearTimeout(n),e.removeEventListener("bufferedamountlow",a),d()},n=setTimeout(a,50);e.addEventListener("bufferedamountlow",a,{once:!0})})}e.addEventListener("message",d=>{if(typeof d.data!="string")return;let a;try{a=JSON.parse(d.data)}catch{return}a.type==="inventory"?h():a.type==="pull"&&S(a.have??[])});async function h(){const d=await H(s,!1),a=d?await R(d):[],n={files:a.length,bytes:a.reduce((u,g)=>u+g.size,0),names:a.map(u=>u.name)};e.send(JSON.stringify({type:"inventory",...n})),i==null||i(`inventory: ${n.files} files, ${(n.bytes/1e9).toFixed(2)} GB`)}async function S(d){const a=await H(s,!1);if(!a){e.send(JSON.stringify({type:"pull-error",message:"no cached weights on the host"}));return}const n=new Set(d),u=(await R(a)).filter(g=>!n.has(g.name));i==null||i(`serving ${u.length} files (${(u.reduce((g,y)=>g+y.size,0)/1e9).toFixed(2)} GB)`);try{for(const{name:g,size:y}of u){const E=await(await a.getFileHandle(g)).getFile();if(y===0){e.send(ee({name:g,off:0,total:0,sha:await B(new Uint8Array(0))},new Uint8Array(0)));continue}for(let m=0;m<y;m+=Z){const I=new Uint8Array(await E.slice(m,Math.min(m+Z,y)).arrayBuffer());for(let O=0;O<I.byteLength;O+=Y){const N=I.subarray(O,Math.min(O+Y,I.byteLength)),v={name:g,off:m+O,total:y,sha:await B(N)};if(await c(),e.readyState!=="open")return;e.send(ee(v,N))}}}e.send(JSON.stringify({type:"pull-done"})),i==null||i("serve complete")}catch(g){e.send(JSON.stringify({type:"pull-error",message:g instanceof Error?g.message:String(g)}))}}}function Oe(e,s=15e3){return new Promise((i,c)=>{const h=setTimeout(()=>{d(),c(new Error("peer did not answer the inventory request"))},s),S=a=>{if(typeof a.data!="string")return;const n=JSON.parse(a.data);n.type==="inventory"&&(d(),i({files:n.files,bytes:n.bytes,names:n.names}))},d=()=>{clearTimeout(h),e.removeEventListener("message",S)};e.addEventListener("message",S),e.send(JSON.stringify({type:"inventory"}))})}async function be(e){const s=await H(e,!1);return s?(await R(s)).map(i=>i.name):[]}async function Ae(e,s,i,c){e.binaryType="arraybuffer";const h=await H(s,!0);if(!h)throw new Error("this browser has no OPFS β€” cannot cache weights here");const S=new Set(await be(s)),d=(c==null?void 0:c.bytes)??0,a=(c==null?void 0:c.files)??0,n=performance.now();let u=S.size,g=0,y=null;return await new Promise((M,E)=>{const m=v=>{e.removeEventListener("message",O),v?E(v):M({files:u,bytes:g,seconds:(performance.now()-n)/1e3})};let I=Promise.resolve();const O=v=>{I=I.then(()=>N(v))};async function N(v){if(typeof v.data=="string"){const p=JSON.parse(v.data);p.type==="pull-done"?m():p.type==="pull-error"&&m(new Error(p.message??"peer failed to serve"));return}try{const{header:p,payload:w}=me(v.data);if(await B(w)!==p.sha)throw new Error(`${p.name}+${p.off}: piece failed its SHA-256 β€” transfer corrupted`);if(!y||y.name!==p.name){if(y)throw new Error(`peer moved to ${p.name} with ${y.name} incomplete`);y={name:p.name,buf:new Uint8Array(p.total),got:0}}if(y.buf.set(w,p.off),y.got+=w.byteLength,g+=w.byteLength,y.got>=p.total){const C=await(await h.getFileHandle(y.name,{create:!0})).createWritable();await C.write(y.buf),await C.close(),y=null,u++}i==null||i({filesDone:u,filesTotal:a,bytesDone:g,bytesTotal:d,rate:g/Math.max((performance.now()-n)/1e3,.001)})}catch(p){m(p instanceof Error?p:new Error(String(p)))}}e.addEventListener("message",O),e.send(JSON.stringify({type:"pull",have:[...S]}))})}const j=8;function te(e,s,i){const c=new Uint8Array(j+i.byteLength),h=new DataView(c.buffer);return h.setUint32(0,e,!0),h.setUint32(4,s,!0),c.set(new Uint8Array(i),j),c.buffer}function ne(e){const s=new DataView(e);return{reqId:s.getUint32(0,!0),position:s.getUint32(4,!0),residual:e.slice(j)}}function Ee(e,s,i){e.binaryType="arraybuffer";let c=Promise.resolve(),h=0;e.addEventListener("message",S=>{typeof S.data!="string"&&(c=c.then(async()=>{const{reqId:d,position:a,residual:n}=ne(S.data);try{const u=await s.pipelineStep({residual:n},a);"tokenId"in u?e.send(JSON.stringify({type:"stage-token",req:d,tokenId:u.tokenId})):e.send(te(d,a,u.residual)),++h%32===1&&(i==null||i(`served ${h} token${h===1?"":"s"}`))}catch(u){e.send(JSON.stringify({type:"stage-error",req:d,message:u instanceof Error?u.message:String(u)}))}}))})}function $e(e){e.binaryType="arraybuffer";const s=new Map;let i=1,c=0,h=0;const S=(d,a)=>{const n=s.get(d);n&&(s.delete(d),a(n))};return e.addEventListener("message",d=>{if(typeof d.data!="string"){const{reqId:n,residual:u}=ne(d.data);S(n,g=>{c++,h+=performance.now()-g.t0,g.resolve({residual:u})});return}const a=JSON.parse(d.data);S(a.req,n=>{a.type==="stage-token"&&typeof a.tokenId=="number"?(c++,h+=performance.now()-n.t0,n.resolve({tokenId:a.tokenId})):n.reject(new Error(a.message??"the other stage failed"))})}),e.addEventListener("close",()=>{for(const d of s.values())d.reject(new Error("a stage of the model disconnected"));s.clear()}),{step(d,a){if(e.readyState!=="open")return Promise.reject(new Error("a stage of the model is not connected"));const n=i++,u=new Promise((g,y)=>{s.set(n,{resolve:g,reject:y,t0:performance.now()})});return e.send(te(n,d,a)),u},meanHopMs:()=>c?h/c:0}}const Se="wss://zero-tvm-share-signal.abgunaydin94.workers.dev";function ve(){return{base:Se,override:null}}const ke={iceServers:[{urls:["stun:stun.cloudflare.com:3478","stun:stun.l.google.com:19302"]}]};function Ne(e){const{spec:s,brand:i,param:c,engine:h,tokenizer:S,encode:d,stageRange:a,ui:n}=e,{base:u}=ve(),g=crypto.getRandomValues(new Uint8Array(16)),y=e.existingRoom??btoa(String.fromCharCode(...g)).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+$/,""),M=`${location.origin}${e.guestPath??"/share.html"}#${y}`,E=new WebSocket(`${u}/room/${y}?role=host`),m=new Map,I=new Map,O=[];let N=!1;const v=new Map,p=(o,t)=>{var l;const r=(l=m.get(o))==null?void 0:l.dc;(r==null?void 0:r.readyState)==="open"&&r.send(JSON.stringify(t))},w=[],U=new Map,C=()=>w.length?w[w.length-1].end:a.end,q=()=>!!a&&C()===s.layers;async function se(o,t,r,l){if(!q())throw new Error(`layers ${C()}-${s.layers} of this model are not connected`);const f=[...w],k=async(L,T)=>{let D=await h.pipelineStep({tokenId:L},T);for(const A of f){if("tokenId"in D)throw new Error("a stage ended the model before the last one");D=await A.step(T,D.residual)}if(!("tokenId"in D))throw new Error("the last stage returned a residual, not a token");return D.tokenId};let b=0;for(let L=0;L<o.length;L++)b=await k(o[L],L);const $=[];for(let L=0;L<t&&!(s.stops.includes(b)||l());L++)$.push(b),r(b),b=await k(b,o.length+L);return $}async function G(){var k,b,$,L,T,D;if(N)return;const o=O.shift();if(!o)return;N=!0;const{guest:t,req:r}=o,l=((k=r.messages.at(-1))==null?void 0:k.content.slice(0,80))??"",f=n.row(t,l);(b=e.lock)!=null&&b.held()&&f("waiting β€” the host is chatting…"),await(($=e.lock)==null?void 0:$.acquire());try{const A=d(r.messages),P=s.maxContext-A.length;if(P<16)throw new Error(`prompt is ${A.length} tokens β€” over this model's ${s.maxContext}-token context`);const F=[],le=performance.now();f("generating…"),(L=n.onBusy)==null||L.call(n,!0);const K=z=>{var x;F.push(z),(x=n.onToken)==null||x.call(n),p(t,{type:"text",id:r.id,full:S.decode(F)})},Q=()=>v.get(t)===r.id||!m.has(t);a?await se(A,P,K,Q):await h.generatePipelined(A,P,K,Q);const de=(performance.now()-le)/1e3,X=F.length/Math.max(de,.001);p(t,{type:"done",id:r.id,tokens:F.length,tps:X});const fe=w.reduce((z,x)=>z+x.meanHopMs(),0);f(`${F.length} tok Β· ${X.toFixed(1)} tok/s`+(w.length?` Β· ${fe.toFixed(1)} ms/hop across ${w.length} stage${w.length===1?"":"s"}`:""))}catch(A){p(t,{type:"error",id:r.id,message:A instanceof Error?A.message:String(A)}),f("error")}finally{(T=e.lock)==null||T.release(),N=!1,(D=n.onBusy)==null||D.call(n,!1),G()}}function re(o,t){let r;try{r=JSON.parse(t)}catch{return}if(r.type==="stop"){v.set(o,r.id);return}if(r.type==="stage-offer"){oe(o,r);return}if(r.type!=="chat"||!Array.isArray(r.messages))return;v.delete(o),O.push({guest:o,req:r});const l=O.length-(N?0:1);l>0&&p(o,{type:"busy",id:r.id,pos:l}),G()}let V={hosts:1,guests:0};function J(){var o,t;(o=n.onMembers)==null||o.call(n,{...V,connected:m.size}),(t=n.onChain)==null||t.call(n,a?ae():"")}function ae(){const o=`${a.start}-${a.end} here`,t=w.map(l=>`${l.start}-${l.end}`).join(" β†’ "),r=q()?"":` Β· waiting for layers ${C()}-${s.layers}`;return`split model β€” layers ${o}${t?` β†’ ${t}`:""}${r}`}function oe(o,t){const r=I.get(o),l=a?t.specId!==s.id?`different model (${t.specId} vs ${s.id})`:r?t.start<t.end?t.end>s.layers?`layers ${t.start}-${t.end} runs past this model's ${s.layers}`:t.start<a.end?`layers ${t.start}-${t.end} overlaps the ${a.start}-${a.end} this host holds`:w.some(f=>t.start<f.end&&f.start<t.end)?`layers ${t.start}-${t.end} overlap a stage already in the chain`:null:`layers ${t.start}-${t.end} is not a range`:"the pipeline channel is not open":"this host runs the whole model";if(l){p(o,{type:"stage-reject",message:l}),n.row(o,`stage offer refused οΏ½οΏ½ ${l}`);return}if(U.set(o,t),W(),U.has(o)){const f=`layers ${t.start}-${t.end} held β€” the chain needs ${C()} next`;p(o,{type:"stage-wait",message:f}),n.row(o,f)}}function W(){var o;for(;;){const t=C(),r=[...U].find(([,$])=>$.start===t);if(!r)break;const[l,f]=r,k=I.get(l);if(!k){U.delete(l);continue}U.delete(l);const b=$e(k);w.push({guest:l,start:f.start,end:f.end,step:b.step,meanHopMs:b.meanHopMs}),p(l,{type:"stage-accept",start:f.start,end:f.end}),n.row(l,`serving layers ${f.start}-${f.end}`)}J(),q()&&(n.row("room",`the model is complete across ${w.length+1} stages`),(o=n.onPaired)==null||o.call(n,!0))}function ie(o){var l;const t=w.findIndex(f=>f.guest===o);if(t<0){U.delete(o);return}const r=w.splice(t);for(const f of r.slice(1))m.has(f.guest)&&U.set(f.guest,{type:"stage-offer",start:f.start,end:f.end,specId:s.id});W(),(l=n.onPaired)==null||l.call(n,q())}function ce(o){var b;(b=m.get(o))==null||b.pc.close();const t=new RTCPeerConnection(ke),r={pc:t,dc:null};m.set(o,r);const l=t.createDataChannel("chat",{ordered:!0});r.dc=l;const f=t.createDataChannel("weights",{ordered:!0});we(f,s,$=>n.row(o,`weights β€” ${$}`));const k=t.createDataChannel("pipeline",{ordered:!0});k.binaryType="arraybuffer",I.set(o,k),l.onopen=()=>{p(o,{type:"info",name:i.name,params:i.params,rateLabel:i.rateLabel,tag:pe(s),param:c,specId:s.id}),J()},l.onmessage=$=>re(o,String($.data)),t.onicecandidate=$=>{$.candidate&&E.send(JSON.stringify({to:o,type:"ice",candidate:$.candidate}))},t.createOffer().then(async $=>{await t.setLocalDescription($),E.send(JSON.stringify({to:o,type:"offer",sdp:$}))}),J()}E.onmessage=o=>{var l,f,k;const t=JSON.parse(String(o.data));if(t.type==="room"){V={hosts:t.hosts??1,guests:t.guests??0},J();return}const r=t.from;r&&(t.type==="peer-joined"?ce(r):t.type==="peer-left"?((l=m.get(r))==null||l.pc.close(),m.delete(r),I.delete(r),a&&(w.some(b=>b.guest===r)||U.has(r))&&(n.row(r,"a stage of the model left"),ie(r)),J()):t.type==="answer"&&t.sdp?(f=m.get(r))==null||f.pc.setRemoteDescription(t.sdp):t.type==="ice"&&t.candidate&&((k=m.get(r))==null||k.pc.addIceCandidate(t.candidate)))};const _=()=>E.close();return window.addEventListener("beforeunload",_),J(),{roomId:y,link:M,close(){window.removeEventListener("beforeunload",_),E.close();for(const[,o]of m)o.pc.close();m.clear(),I.clear()}}}export{ke as I,ve as a,Oe as f,Ne as h,Ae as p,Ee as s};
assets/share-TXmFQ6ZAWJ.js ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/cache-probe-BYX_n9OPRB.js","assets/weight-loader-ByOLMd9f5C.js","assets/model-registry-BoK-ObgACo.js","assets/model-select-DhhV9mCi0T.js","assets/loading-ui-CjCwSRuXvh.js","assets/engine-core-CLR3cxsqf4.js","assets/variants-De-VVc9BRz.js"])))=>i.map(i=>d[i]);
2
+ import{a as J}from"./model-registry-BoK-ObgACo.js";import{_ as x}from"./preload-helper-ckwbz45pO3.js";import{w as U,a as G,b as W,c as z,s as K}from"./chat-ui-CQAxV1po6S.js";import{h as j,I as R,s as Y,a as Z,f as Q,p as X}from"./room-host-C6R3zseuU5.js";const{base:B}=Z(),n=h=>document.getElementById(h);function ee(h){const t=h.replace(/^#/,"");return/^[A-Za-z0-9_-]{16,64}$/.test(t)?t:null}function te(h){const t=/^(\d+)-(\d+)$/.exec(new URLSearchParams(h).get("layers")??"");return t?{start:Number(t[1]),end:Number(t[2])}:null}const T=ee(location.hash),ne=new URLSearchParams(location.search).has("model"),I=te(location.search);T&&I&&I.start>0?se(T,I):T&&!ne?ae(T):oe(T,I);function H(){const h=n("awake");if(!h)return;let t=null,r=null;const b=async c=>{if(c){try{t=await navigator.wakeLock.request("screen")}catch{}if(!r){r=new AudioContext;const i=r.createOscillator(),f=r.createGain();f.gain.value=1e-4,i.connect(f).connect(r.destination),i.start()}r.resume()}else t==null||t.release().catch(()=>{}),t=null,r==null||r.suspend()};h.addEventListener("change",()=>void b(h.checked)),document.addEventListener("visibilitychange",()=>{h.checked&&document.visibilityState==="visible"&&b(!0)})}async function V(h,t){const{isModelCached:r}=await x(async()=>{const{isModelCached:i}=await import("./cache-probe-BYX_n9OPRB.js");return{isModelCached:i}},__vite__mapDeps([0,1,2])),b=await r(h),c=document.createElement("dialog");c.id="share-gate",c.innerHTML=`
3
+ <h2>Host ${t.name} from this tab</h2>
4
+ <p>
5
+ Hosting runs the model on THIS machine and serves it to whoever opens
6
+ your room link. ${b?"The weights are already cached on this device.":`The weights download once (${t.sizeLabel}) and are cached locally; every later visit starts from disk.`}
7
+ </p>
8
+ ${t.ramNote?`<p class="warn">${t.ramNote}</p>`:""}
9
+ <p class="fine">Guests' prompts run on your GPU. You see every request as it arrives.</p>
10
+ <div class="acts">
11
+ <a href="/">Not now</a>
12
+ <button type="button" id="share-gate-go" autofocus>${b?"Start hosting β†’":"Download &amp; host β†’"}</button>
13
+ </div>`,document.body.appendChild(c),c.showModal(),await new Promise(i=>{var f;(f=c.querySelector("#share-gate-go"))==null||f.addEventListener("click",()=>{c.close(),c.remove(),i()},{once:!0})})}async function oe(h,t){if(n("host-view").classList.remove("hidden"),!("gpu"in navigator)){const o=n("loading-error-top");o.style.display="block",o.textContent="Hosting needs WebGPU with shader-f16 β€” Chrome and Edge ship it; Safari from 26. This browser can still JOIN a room as a guest: guests run nothing locally.",n("badge").className="badge error",n("badge-text").textContent="No WebGPU";return}const[{specFromSearch:r,modelBranding:b,buildChatPromptFor:c},i,f,_]=await Promise.all([x(()=>import("./model-select-DhhV9mCi0T.js"),__vite__mapDeps([3,2,1])),x(()=>import("./loading-ui-CjCwSRuXvh.js"),__vite__mapDeps([4,1,2,3,5,6])),x(()=>import("./variants-De-VVc9BRz.js"),__vite__mapDeps([6,2])),x(()=>import("./engine-core-CLR3cxsqf4.js"),__vite__mapDeps([5,2,6]))]),g=r(location.search),m=b(g);if(n("page-title").textContent=t?`Sharing ${m.name} β€” layers ${t.start}-${t.end} here`:`Sharing ${m.name}`,t&&t.start!==0)throw new Error(`share: a hosting stage must start at layer 0 (got ${t.start}); later stages join a room as helpers`);H(),await V(g,m);const l=(()=>{if(!g.moe)return 0;const o=new URLSearchParams(location.search).get("pool");if(!o)return 0;const v=g.moe.experts,e=o==="half"?Math.round(v/2):o==="quarter"?Math.round(v/4):Number(o);return Number.isFinite(e)&&e>0?e:0})(),w=await i.bootEngine({spec:g,optionalFeatures:["subgroups"],probeSubgroups:!0,layerRange:t??void 0,...l?{expertPool:l}:{},buildEngine:({device:o,weights:v,sgSizeOk:e,spec:s})=>{const u=f.parseVariantFlags(location.search,{hasSubgroupsFeature:o.features.has("subgroups"),sgSizeOk:e}),k=!s.qkNorm&&s.weightFormat!=="mlx-safetensors";window.__variants={...u,fused:k};const P=_.allocKVPages(o,s);return _.buildDecodeEngine(o,v,P,{variants:u,fused:k,spec:s,layerRange:t??void 0,...l?{expertPool:l}:{}})},warmup:t?async o=>{await o.pipelineStep({tokenId:1},0)}:void 0});if(!w.ok){const o=n("loading-error-top");o.style.display="block",o.textContent=w.reason,n("loading-error").textContent=w.reason;return}const{engine:p,tokenizer:$}=w,E=(o,v)=>{var s;(s=n("req-empty"))==null||s.remove();const e=document.createElement("li");return e.innerHTML='<span class="who"></span> Β· <span class="body"></span> <span class="st"></span>',e.querySelector(".who").textContent=o,e.querySelector(".body").textContent=v,n("req-log").prepend(e),e.querySelector(".st")};let a="1 machine serving Β· 0 guests connected",d="";function y(){n("room-stats").textContent=d?`${d} Β· ${a}`:a}const S=j({spec:g,brand:l?{...m,rateLabel:""}:m,param:new URLSearchParams(location.search).get("model")??"",engine:p,tokenizer:$,encode:o=>c(g,o,$),existingRoom:h,stageRange:t,guestPath:location.pathname,ui:{row:(o,v)=>{const e=E(o,v);return s=>{e.textContent=s}},onMembers:({hosts:o,guests:v})=>{a=`${o} ${o===1?"machine":"machines"} serving Β· ${v} ${v===1?"guest":"guests"} connected`,y()},onChain:o=>{d=o,y()},onPaired:o=>{window.__stagePaired=o}}});h&&(n("page-title").textContent=`Serving ${m.name} in a shared room`),n("share-link").value=S.link,n("copy-link").addEventListener("click",()=>{navigator.clipboard.writeText(S.link),n("copy-link").textContent="Copied",setTimeout(()=>{n("copy-link").textContent="Copy"},1200)}),window.__shareLink=S.link,window.__shareReady=!0}async function se(h,t){var E;n("host-view").classList.remove("hidden"),(E=n("share-link").closest("section"))==null||E.remove(),H();const[{specFromSearch:r,modelBranding:b},c,i,f]=await Promise.all([x(()=>import("./model-select-DhhV9mCi0T.js"),__vite__mapDeps([3,2,1])),x(()=>import("./loading-ui-CjCwSRuXvh.js"),__vite__mapDeps([4,1,2,3,5,6])),x(()=>import("./variants-De-VVc9BRz.js"),__vite__mapDeps([6,2])),x(()=>import("./engine-core-CLR3cxsqf4.js"),__vite__mapDeps([5,2,6]))]),_=r(location.search),g=b(_);n("page-title").textContent=`Serving ${g.name} layers ${t.start}-${t.end}`;const m=n("room-stats");m.textContent=`loading layers ${t.start}-${t.end} of ${_.layers}…`,await V(_,g);const l=await c.bootEngine({spec:_,optionalFeatures:["subgroups"],probeSubgroups:!0,layerRange:t,buildEngine:({device:a,weights:d,sgSizeOk:y,spec:S})=>{const o=i.parseVariantFlags(location.search,{hasSubgroupsFeature:a.features.has("subgroups"),sgSizeOk:y});return window.__variants={...o,fused:!1},f.buildDecodeEngine(a,d,f.allocKVPages(a,S),{variants:o,fused:!1,spec:S,layerRange:t})},warmup:async a=>{await a.pipelineStep({residual:new ArrayBuffer(_.d*2)},0)}});if(!l.ok){const a=n("loading-error-top");a.style.display="block",a.textContent=l.reason,n("loading-error").textContent=l.reason;return}const w=new WebSocket(`${B}/room/${h}?role=guest`),p=new RTCPeerConnection(R);let $=null;p.onicecandidate=a=>{a.candidate&&w.send(JSON.stringify({type:"ice",candidate:a.candidate}))},p.ondatachannel=a=>{if(a.channel.label==="pipeline"){Y(a.channel,l.engine,d=>{m.textContent=`layers ${t.start}-${t.end} Β· ${d}`});return}a.channel.label==="chat"&&($=a.channel,$.onopen=()=>$.send(JSON.stringify({type:"stage-offer",start:t.start,end:t.end,specId:_.id})),$.onmessage=d=>{const y=JSON.parse(String(d.data));y.type==="stage-accept"?(m.textContent=`in the chain β€” this device runs layers ${t.start}-${t.end} of ${_.layers}`,window.__helperPaired=!0):y.type==="stage-wait"?m.textContent=`holding layers ${t.start}-${t.end} β€” ${y.message}`:y.type==="stage-reject"&&(m.textContent=`the host refused this stage: ${y.message}`)})},w.onmessage=async a=>{const d=JSON.parse(String(a.data));if(d.type==="offer"&&d.sdp){await p.setRemoteDescription(d.sdp);const y=await p.createAnswer();await p.setLocalDescription(y),w.send(JSON.stringify({type:"answer",sdp:y}))}else d.type==="ice"&&d.candidate?await p.addIceCandidate(d.candidate):(d.type==="no-host"||d.type==="host-left")&&(m.textContent="no host in this room to pair with β€” open the room link on the machine holding the first layers")},window.__helperReady=!0}async function ae(h){n("guest-view").classList.remove("hidden");const t=e=>{n("guest-status").textContent=e},r=(e,s)=>{n("guest-badge").className=`badge ${s}`,n("guest-badge-text").textContent=e};r("Connecting","loading"),U();const b=new WebSocket(`${B}/room/${h}?role=guest`);let c=new RTCPeerConnection(R),i=null,f=()=>{},_=new Promise(e=>{f=e});const g=[];let m=0,l=null,w="";const p=n("inp"),$=n("btn"),E=n("stop-btn"),a=e=>{$.hidden=e,E.hidden=!e,$.disabled=e},d=()=>{l=null,a(!1)},y=e=>{e.type==="info"?(K(e.name,e.tag),n("guest-model").textContent=`${e.name} β€” remote`,t(`${e.params}${e.rateLabel?` Β· ${e.rateLabel}`:""} Β· runs on the host machine β€” the host can read what you send; this page holds only the conversation.`),r("Ready","ready"),p.disabled=!1,p.placeholder="Message the remote model…",$.disabled=!1,o(e.param,e.specId)):e.type==="text"?(w=e.full,l==null||l.render(w)):e.type==="done"?(l&&(l.finish({fullText:w,tokens:e.tokens,tokPerS:e.tps}),g.push({role:"assistant",content:w})),d()):e.type==="busy"?t(`Host is generating for someone else β€” queue position ${e.pos}.`):e.type==="error"&&(l&&(l.body.textContent=`⚠ ${e.message}`),d())};function S(){c.onicecandidate=e=>{e.candidate&&b.send(JSON.stringify({type:"ice",candidate:e.candidate}))},c.ondatachannel=e=>{if(e.channel.label==="weights"){const u=e.channel;u.binaryType="arraybuffer",u.readyState==="open"?f(u):u.addEventListener("open",()=>f(u),{once:!0});return}if(e.channel.label!=="chat")return;i=e.channel,i.onmessage=u=>y(JSON.parse(String(u.data)));const s=i;i.onclose=()=>{i===s&&r("Disconnected","error")}}}S(),b.onmessage=async e=>{const s=JSON.parse(String(e.data));if(s.type==="no-host")r("No host","error"),t("Nobody is serving a model in this room β€” every host tab is closed, or the link expired.");else if(s.type==="host-left")r("Host left","error"),t("Every machine serving this room went away. The conversation stays here; it resumes if one comes back."),p.disabled=!0,$.disabled=!0;else if(s.type==="host-changed")r("Switching host","loading"),t("The machine serving you went away; another one in the room is taking over…"),c.close(),c=new RTCPeerConnection(R),i=null,_=new Promise(u=>{f=u}),S(),d(),p.disabled=!0,$.disabled=!0;else if(s.type==="room"){const u=s.hosts??0;n("room-count").textContent=u>1?`${u} machines serving`:""}else if(s.type==="offer"&&s.sdp){await c.setRemoteDescription(s.sdp);const u=await c.createAnswer();await c.setLocalDescription(u),b.send(JSON.stringify({type:"answer",sdp:u}))}else s.type==="ice"&&s.candidate&&await c.addIceCandidate(s.candidate)};async function o(e,s){const u=n("local-copy"),k=J(e);if(k.id!==s)return;const P=await Promise.race([_,new Promise(F=>setTimeout(()=>F(null),2e4))]);if(!P)return;const D=n("lc-status"),L=n("lc-btn");let N;try{N=await Q(P)}catch{return}if(!N.files)return;const O=(N.bytes/1e9).toFixed(2);u.classList.remove("hidden"),D.textContent=`The host has ${O} GB cached. Copying it here lets this device run ${k.id} on its own GPU.`,L.textContent=`Copy ${O} GB to this device`,L.addEventListener("click",()=>{L.disabled=!0;const F=performance.now();X(P,k,C=>{const M=C.bytesTotal?Math.round(C.bytesDone/C.bytesTotal*100):0;D.textContent=`${M}% Β· ${(C.bytesDone/1e9).toFixed(2)}/${O} GB Β· ${C.filesDone}/${C.filesTotal} files Β· ${(C.rate/1e6).toFixed(0)} MB/s`},N).then(C=>{const M=((performance.now()-F)/1e3).toFixed(0),A=encodeURIComponent(e),q="";D.innerHTML=`Done β€” ${(C.bytes/1e9).toFixed(2)} GB in ${M}s. <a href="/zero-tvm.html?model=${A}">Open the chat on this device β†’</a><br><a href="/share.html?model=${A}${q}#${h}">…or serve this room from here too β†’</a>`,L.remove(),window.__pullDone=C}).catch(C=>{D.textContent=`Copy failed: ${C.message}`,L.disabled=!1})})}const v=()=>{const e=p.value.trim();!e||l||!i||i.readyState!=="open"||(p.value="",G(p),g.push({role:"user",content:e}),W(e),l=z(),l.showThinking(),w="",a(!0),i.send(JSON.stringify({type:"chat",id:++m,messages:[...g]})))};n("composer").addEventListener("submit",e=>{e.preventDefault(),v()}),E.addEventListener("click",()=>{(i==null?void 0:i.readyState)==="open"&&i.send(JSON.stringify({type:"stop",id:m}))}),p.addEventListener("input",()=>G(p)),p.addEventListener("keydown",e=>{e.key==="Enter"&&!e.shiftKey&&(e.preventDefault(),v())})}
assets/validate-D2Aic8RAjT.js ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import{m as w}from"./model-registry-BoK-ObgACo.js";import{specFromSearch as x,buildChatPromptFor as v}from"./model-select-DhhV9mCi0T.js";import{bootEngine as S,hideProgress as M,setBadge as g,log as E}from"./loading-ui-CjCwSRuXvh.js";import"./weight-loader-ByOLMd9f5C.js";import"./engine-core-CLR3cxsqf4.js";import"./variants-De-VVc9BRz.js";const y=x(location.search),h=[{label:"factual-recall",system:"You are a helpful assistant.",user:"What is the capital of France?",hint:"Paris"},{label:"arithmetic",system:"You are a helpful assistant. Answer briefly.",user:"What is 17 plus 25?",hint:"42"},{label:"code-completion",system:"You are a coding assistant.",user:"Write a one-line Python expression that returns the length of a list called xs.",hint:"len"},{label:"instruction-follow",system:"You are a helpful assistant.",user:'Reply with exactly the word "yes" and nothing else.',hint:"yes"},{label:"open-ended",system:"You are a helpful assistant.",user:"Name three colors."}];function p(t){E(t)}function u(t){const s=document.getElementById("status");s&&(s.textContent=t)}function b(t){const s=document.getElementById("results-wrap"),a=document.createElement("div");a.className="prompt-result",a.innerHTML=t,s.appendChild(a)}function m(t){return t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function F(t,s,a){let i=-1/0;for(let o=0;o<t.length;o++)t[o]>i&&(i=t[o]);let c=0;const e=new Float32Array(t.length);for(let o=0;o<t.length;o++)e[o]=Math.exp(t[o]-i),c+=e[o];const n=new Set,r=[];for(let o=0;o<s;o++){let l=-1,f=-1/0;for(let d=0;d<t.length;d++)n.has(d)||t[d]>f&&(f=t[d],l=d);if(l<0)break;n.add(l),r.push({id:l,logit:f,prob:e[l]/c,text:a.decode([l])})}return r}function k(t){let s=-1/0;for(let e=0;e<t.length;e++)t[e]>s&&(s=t[e]);let a=0;for(let e=0;e<t.length;e++)a+=Math.exp(t[e]-s);const i=Math.log(a)+s;let c=0;for(let e=0;e<t.length;e++){const n=Math.exp(t[e]-i);n>1e-12&&(c-=n*Math.log(n))}return c}async function P(t,s,a){const i=v(t.spec,[{role:"system",content:a.system},{role:"user",content:a.user}],s);t.resetKVTracking();const c=performance.now(),e=await t.forwardLogits(i),n=performance.now()-c,r=F(e,10,s),o=k(e),l=[],f=performance.now();await t.generate(i,i.length,24,$=>{l.push($)});const d=performance.now()-f;return{label:a.label,prompt:a.user,topK:r,entropyNats:o,continuation:s.decode(l),forwardMs:n,decodeMs:d,decodeTokens:l.length}}function I(t){const s=t.topK.map((a,i)=>{const c=(a.prob*100).toFixed(2),e="β–ˆ".repeat(Math.max(1,Math.round(a.prob*30))),n=JSON.stringify(a.text).slice(1,-1);return` ${String(i+1).padStart(2)}. ${n.padEnd(20)} ${c.padStart(6)}% ${e}`}).join(`
2
+ `);return`
3
+ <div class="prompt-label">${m(t.label)}</div>
4
+ <div class="prompt-text">${m(t.prompt)}</div>
5
+ <div class="prompt-meta">
6
+ forward: ${t.forwardMs.toFixed(0)} ms Β·
7
+ decode: ${t.decodeMs.toFixed(0)} ms (${t.decodeTokens} tok, ${(t.decodeTokens/(t.decodeMs/1e3)).toFixed(1)} tok/s) Β·
8
+ entropy: ${t.entropyNats.toFixed(2)} nats
9
+ </div>
10
+ <div class="prompt-section">Top-10 next tokens</div>
11
+ <pre class="topk">${m(s)}</pre>
12
+ <div class="prompt-section">Greedy continuation (≀24 tokens)</div>
13
+ <pre class="continuation">${m(t.continuation)}</pre>
14
+ `}async function R(){var c,e;if(!navigator.gpu){u("No WebGPU available");const n=document.getElementById("start-btn");n&&(n.disabled=!0,n.textContent="WebGPU not available in this browser");const r=document.querySelector(".start-screen .desc");r&&(r.textContent="This harness needs WebGPU with shader-f16 β€” Chrome and Edge ship it; Safari from 26.");return}if(y.id!=="phi3-mini"){const n=w(y),r=document.querySelector(".start-hint");r&&(r.textContent=`${n.sizeLabel} ${n.name} weights download on first run, cached after`)}const t=document.getElementById("start-btn");await new Promise(n=>{t.addEventListener("click",()=>{t.disabled=!0,t.textContent="Loading...",n()})}),(c=document.getElementById("start-screen"))==null||c.remove(),(e=document.getElementById("results-wrap"))==null||e.classList.add("active"),u("Booting engine...");const s=await S({spec:y});if(!s.ok){u(s.reason);return}const{tokenizer:a,engine:i}=s;M(),g("Running","loading"),u(`Running ${h.length} test prompts...`);for(let n=0;n<h.length;n++){const r=h[n];u(`[${n+1}/${h.length}] ${r.label}`),p(`
15
+ --- ${r.label} ---`),p(`prompt: ${r.user}`);try{const o=await P(i,a,r);p(`top-1: ${JSON.stringify(o.topK[0].text)} (${(o.topK[0].prob*100).toFixed(1)}%)`),p(`entropy: ${o.entropyNats.toFixed(2)} nats`),p(`continuation: ${JSON.stringify(o.continuation)}`),b(I(o))}catch(o){p(`ERROR: ${o}`),b(`<div class="prompt-error">${m(r.label)}: ${m(String(o))}</div>`)}}u("Done"),g("Ready","ready"),p(`
16
+ === validation complete ===`)}R().catch(t=>{console.error(t),p(`FATAL: ${t}`),u(`FATAL: ${t}`)});
assets/variants-De-VVc9BRz.js ADDED
@@ -0,0 +1 @@
 
 
1
+ import{P as q}from"./model-registry-BoK-ObgACo.js";const w={subgroups:!1,sgAttn:!1,sgArgmax:!1,sgQkv:!1,qkvTile:!1,qkvTile2:!1,sgFfn:!1,matmul:"scalar",int8KV:!1,vec4:!1,vec4Half:!1,vec4Qkv:!1,vec4Affine:!1,vec4Moe:!1,fuseQkNorm:!1,splitK:0,fusePrologue:!1};function G(i,e){const t=new URLSearchParams(i),l=t.get("sg")!=="0";return{subgroups:l&&e.hasSubgroupsFeature&&e.sgSizeOk,sgAttn:l&&t.get("sgattn")!=="0",sgArgmax:l&&t.get("sgargmax")!=="0",sgQkv:l&&t.get("sgqkv")!=="0",qkvTile:l&&t.get("qkvtile")==="1",qkvTile2:l&&t.get("qkvtile2")==="1",sgFfn:l&&t.get("sgffn")!=="0",matmul:t.get("matmul")??(l?"tiled":"scalar"),int8KV:t.get("kv8")!=="0",vec4:l&&t.get("vec4")!=="0",vec4Half:l&&t.get("vec4")!=="0"&&t.get("vec4h")!=="0",vec4Qkv:l&&t.get("vec4qkv")!=="0",vec4Affine:l&&t.get("vec4")!=="0"&&t.get("vec4aff")!=="0",vec4Moe:l&&t.get("vec4moe")==="1",fuseQkNorm:t.get("fuseqk")!=="0",splitK:h(t.get("splitk")??(l?"8":"0")),fusePrologue:t.get("fuseprologue")==="1"}}function h(i){const e=Number.parseInt(i??"0",10);return Number.isFinite(e)&&e>=2&&e<=16?e:0}function m(i,e,t=!1,l,r=t,o=!1,c=!1){if(o){const u=l!==void 0&&c&&l%512===0?"vec4h":"none";return u==="vec4h"&&i!=="scalar"&&e.int4MatmulTiledVec4hAffine&&e.int4MatmulF32TiledVec4hAffine?{pipeline:e.int4MatmulTiledVec4hAffine,pipelineF32:e.int4MatmulF32TiledVec4hAffine,rowsPerWG:4,label:"tiled_vec4h_affine"}:u==="vec4h"&&e.int4MatmulSgVec4hAffine&&e.int4MatmulF32SgVec4hAffine?{pipeline:e.int4MatmulSgVec4hAffine,pipelineF32:e.int4MatmulF32SgVec4hAffine,rowsPerWG:1,label:"sg_vec4h_affine"}:i!=="scalar"&&e.int4MatmulTiledAffine&&e.int4MatmulF32TiledAffine?{pipeline:e.int4MatmulTiledAffine,pipelineF32:e.int4MatmulF32TiledAffine,rowsPerWG:4,label:"tiled_affine"}:e.int4MatmulSgAffine&&e.int4MatmulF32SgAffine?{pipeline:e.int4MatmulSgAffine,pipelineF32:e.int4MatmulF32SgAffine,rowsPerWG:1,label:"sg_affine"}:{pipeline:e.int4MatmulAffine,pipelineF32:e.int4MatmulF32Affine,rowsPerWG:1,label:"scalar_affine"}}let s=!1;return l!==void 0&&l%1024!==0&&(s=r&&l%512===0,t=!1),s&&i==="tiled"&&e.int4MatmulTiledVec4h&&e.int4MatmulF32TiledVec4h?{pipeline:e.int4MatmulTiledVec4h,pipelineF32:e.int4MatmulF32TiledVec4h,rowsPerWG:4,label:"tiled_vec4h"}:s&&i==="sg"&&e.int4MatmulSgVec4h&&e.int4MatmulF32SgVec4h?{pipeline:e.int4MatmulSgVec4h,pipelineF32:e.int4MatmulF32SgVec4h,rowsPerWG:1,label:"sg_vec4h"}:t&&i==="tiled"&&e.int4MatmulTiledVec4&&e.int4MatmulF32TiledVec4?{pipeline:e.int4MatmulTiledVec4,pipelineF32:e.int4MatmulF32TiledVec4,rowsPerWG:4,label:"tiled_vec4"}:t&&i==="sg"&&e.int4MatmulSgVec4&&e.int4MatmulF32SgVec4?{pipeline:e.int4MatmulSgVec4,pipelineF32:e.int4MatmulF32SgVec4,rowsPerWG:1,label:"sg_vec4"}:i==="tiled8"&&e.int4MatmulTiled8&&e.int4MatmulF32Tiled8?{pipeline:e.int4MatmulTiled8,pipelineF32:e.int4MatmulF32Tiled8,rowsPerWG:8,label:"tiled8"}:i==="tiled"&&e.int4MatmulTiled&&e.int4MatmulF32Tiled?{pipeline:e.int4MatmulTiled,pipelineF32:e.int4MatmulF32Tiled,rowsPerWG:4,label:"tiled"}:i==="sg"&&e.int4MatmulSg&&e.int4MatmulF32Sg?{pipeline:e.int4MatmulSg,pipelineF32:e.int4MatmulF32Sg,rowsPerWG:1,label:"sg"}:{pipeline:e.int4Matmul,pipelineF32:e.lmHead,rowsPerWG:1,label:"scalar"}}function W(i,e,t=q){const l=t.weightFormat==="mlx-safetensors",{pipeline:r,pipelineF32:o,rowsPerWG:c,label:s}=m(i.matmul,e,i.vec4,t.d,i.vec4Half,l,i.vec4Affine),{pipeline:u}=m(i.matmul,e,i.vec4,t.qDim,i.vec4Half,l,i.vec4Affine),{pipeline:v}=m(i.matmul,e,i.vec4,t.ffn,i.vec4Half,l,i.vec4Affine),d=i.sgAttn&&e.attentionSg?e.attentionSg:e.attention,f=i.splitK>=2?i.splitK:0,g=f?i.sgAttn&&e.attentionSplitKSg?e.attentionSplitKSg:e.attentionSplitK:null,S=f?e.attentionCombine:null,M=g?`splitk${f}${g===e.attentionSplitKSg?"_sg":""}`:d===e.attentionSg?"_sg":"scalar",p=i.sgArgmax&&e.argmaxSg?e.argmaxSg:e.argmax,n=i.vec4Qkv&&t.d%1024===0&&e.qkvFusedSgVec4?e.qkvFusedSgVec4:i.qkvTile2&&e.qkvFusedTiled2Sg?e.qkvFusedTiled2Sg:i.qkvTile&&e.qkvFusedTiledSg?e.qkvFusedTiledSg:i.sgQkv&&e.qkvFusedSg?e.qkvFusedSg:e.qkvFused,T=n===e.qkvFusedTiledSg||n===e.qkvFusedTiled2Sg?2:1,k=n===e.qkvFusedSgVec4?"_sg_vec4":n===e.qkvFusedTiled2Sg?"tiled2_sg":n===e.qkvFusedTiledSg?"tiled_sg":n===e.qkvFusedSg?"_sg":"scalar",F=i.fusePrologue,a=F?i.sgFfn&&e.fusedFfnTiledSgPrologue?e.fusedFfnTiledSgPrologue:e.fusedFfnPrologue:i.sgFfn&&e.fusedFfnTiledSg?e.fusedFfnTiledSg:e.fusedFfn,A=a===e.fusedFfnTiledSg||a===e.fusedFfnTiledSgPrologue?4:1,V=a===e.fusedFfnTiledSgPrologue?"tiled_sg_prologue":a===e.fusedFfnPrologue?"scalar_prologue":a===e.fusedFfnTiledSg?"tiled_sg":"scalar";return{matmul:r,matmulF32:o,matmulOProj:u,matmulFfnDown:v,matmulRowsPerWG:c,matmulLabel:s,attention:d,attentionLabel:M,attentionSplitK:g,attentionCombine:S,splitK:g?f:0,argmax:p,argmaxLabel:p===e.argmaxSg?"_sg":"scalar",qkvFused:n,qkvPairsPerWG:T,qkvLabel:k,ffn:a,ffnRowsPerWG:A,ffnLabel:V,ffnPrologue:F}}export{w as SCALAR_VARIANTS,G as parseVariantFlags,m as resolveMatmul,W as resolveVariantPipelines};
assets/weight-loader-ByOLMd9f5C.js ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ import{P as ie,o as ye,W as _e}from"./model-registry-BoK-ObgACo.js";function ge(t){if(t.byteLength<8)throw new Error("safetensors: need at least 8 bytes for the header length");const e=new DataView(t),o=e.getUint32(0,!0);if(e.getUint32(4,!0)!==0)throw new Error("safetensors: header longer than 4 GB");if(t.byteLength<8+o)throw new Error(`safetensors: header is ${o} bytes, only ${t.byteLength-8} supplied`);const l=JSON.parse(new TextDecoder().decode(new Uint8Array(t,8,o))),a={};for(const[n,s]of Object.entries(l)){if(n==="__metadata__")continue;const i=s;a[n]={dtype:i.dtype,shape:i.shape,begin:i.data_offsets[0],end:i.data_offsets[1]}}return{tensors:a,dataStart:8+o}}function Be(t){const e=new Uint16Array(t.length);let o=0;for(let r=0;r<t.length;r++){const l=t[r],a=l&32768,n=l>>7&255,s=(l&127)<<16;if(n===255){e[r]=a|31744|(s?512:0);continue}const i=n-112;if(i>=31){e[r]=a|31743,o++;continue}if(i<=0){if(i<-10){e[r]=a;continue}const f=s|8388608,c=14-i;let u=f>>>c;const w=f&(1<<c)-1,d=1<<c-1;(w>d||w===d&&u&1)&&u++,e[r]=a|u;continue}e[r]=a|i<<10|s>>>13}return{out:e,overflow:o}}function ve(t){const e=new Float32Array(t.length),o=new Uint32Array(e.buffer);for(let r=0;r<t.length;r++)o[r]=t[r]<<16;return e}const K=new Float32Array(1),ce=new Uint32Array(K.buffer);function re(t){K[0]=t;const e=ce[0],o=e>>>16&32768;let r=e>>>23&255;const l=e&8388607;if(r===255)return o|31744|(l?512:0);if(r=r-127+15,r>=31)return o|31744;if(r<=0){if(r<-10)return o;const s=l|8388608,i=14-r;let f=s>>>i;const c=s&(1<<i)-1,u=1<<i-1;return(c>u||c===u&&f&1)&&f++,o|f}let a=r<<10|l>>>13;const n=l&8191;return(n>4096||n===4096&&a&1)&&a++,o|a}function ne(t){const e=(t&32768)<<16,o=t>>>10&31,r=t&1023;let l;if(o===0)if(r===0)l=e;else{let a=-1,n=r;do a++,n<<=1;while(!(n&1024));l=e|112-a<<23|(n&1023)<<13}else o===31?l=e|2139095040|r<<13:l=e|o-15+127<<23|r<<13;return ce[0]=l>>>0,K[0]}function fe(t,e,o){const r=`${`${t.mlxPrefix??""}model.`}layers.${e}`,l=s=>[{record:`${s}.weight`,convert:"raw"},{record:`${s}.scales`,convert:"bf16->f16"},{record:`${s}.biases`,convert:"bf16->f16"}],a=(s,i)=>{const f=i.map(l);return[{name:`${s}_w`,parts:f.map(c=>c[0])},{name:`${s}_s`,parts:f.map(c=>c[1])},{name:`${s}_b`,parts:f.map(c=>c[2])}]},n=[{name:"norm1",parts:[{record:`${r}.input_layernorm.weight`,convert:"bf16->f16"}]},{name:"norm2",parts:[{record:`${r}.post_attention_layernorm.weight`,convert:"bf16->f16"}]}];if(t.layerKinds[e]==="attn"&&t.mla){const s=t.mla,i=s.qkRopeHeadDim,f=d=>d<i/2?2*d:2*(d-i/2)+1,c=Array.from({length:t.mlaQProjRows},(d,b)=>b);for(let d=0;d<t.heads;d++){const b=d*(s.qkNopeHeadDim+i)+s.qkNopeHeadDim;for(let $=0;$<i;$++)c[b+$]=b+f($)}const u=Array.from({length:t.mlaKvaRows},(d,b)=>b);for(let d=0;d<i;d++)u[s.kvLoraRank+d]=s.kvLoraRank+f(d);const w=(d,b)=>d.map($=>({...$,op:b}));n.push(...w(a("mla_q",[`${r}.self_attn.q_proj`]),{kind:"permuteRows",rows:t.mlaQProjRows,src:c})),n.push(...w(a("mla_kva",[`${r}.self_attn.kv_a_proj_with_mqa`]),{kind:"permuteRows",rows:t.mlaKvaRows,src:u})),n.push({name:"mla_kva_norm",parts:[{record:`${r}.self_attn.kv_a_layernorm.weight`,convert:"bf16->f16"}]}),n.push({name:"mla_kvb",parts:l(`${r}.self_attn.kv_b_proj`),op:{kind:"dequantAffineSplit",bits:4,group:64,k:s.kvLoraRank,heads:t.heads,nope:s.qkNopeHeadDim,v:s.vHeadDim}}),n.push(...a("o_proj",[`${r}.self_attn.o_proj`]))}else if(t.layerKinds[e]==="attn")n.push(...a("c_attn",[`${r}.self_attn.q_proj`,`${r}.self_attn.k_proj`,`${r}.self_attn.v_proj`])),n.push(...a("o_proj",[`${r}.self_attn.o_proj`])),t.qkNorm&&(n.push({name:"q_norm",parts:[{record:`${r}.self_attn.q_norm.weight`,convert:"bf16->f16"}]}),n.push({name:"k_norm",parts:[{record:`${r}.self_attn.k_norm.weight`,convert:"bf16->f16"}]}));else{const s=`${r}.linear_attn`;n.push(...a("gdn_proj",[`${s}.in_proj_qkv`,`${s}.in_proj_z`,`${s}.in_proj_a`,`${s}.in_proj_b`])),n.push(...a("gdn_out",[`${s}.out_proj`])),n.push({name:"gdn_conv",parts:[{record:`${s}.conv1d.weight`,convert:"bf16->f16"}]}),n.push({name:"gdn_norm",parts:[{record:`${s}.norm.weight`,convert:"bf16->f16"}]}),n.push({name:"gdn_a_log",parts:[{record:`${s}.A_log`,convert:"bf16->f32"}]}),n.push({name:"gdn_dt_bias",parts:[{record:`${s}.dt_bias`,convert:"bf16->f32"}]})}if(t.ffnKinds[e]==="moe"&&t.moe){const s=t.moe.sharedExpert??!0;for(const i of["gate_proj","up_proj","down_proj"])n.push(...a(`moe_${i}`,s?[`${r}.mlp.switch_mlp.${i}`,`${r}.mlp.shared_expert.${i}`]:[`${r}.mlp.switch_mlp.${i}`]));if(t.moe.routerBits===16){const i=s?[`${r}.mlp.gate`,`${r}.mlp.shared_expert_gate`]:[`${r}.mlp.gate`];n.push({name:"router_w",parts:i.map(f=>({record:`${f}.weight`,convert:"bf16->f16"}))})}else n.push(...a("router",s?[`${r}.mlp.gate`,`${r}.mlp.shared_expert_gate`]:[`${r}.mlp.gate`]))}else n.push(...a("ffn",[`${r}.mlp.gate_proj`,`${r}.mlp.up_proj`])),n.push(...a("ffn_down",[`${r}.mlp.down_proj`]));return n}function Se(t,e,o){const r=t.mlxPrefix??"",l=((o==null?void 0:o.start)??0)===0,a=((o==null?void 0:o.end)??t.layers)===t.layers,n=t.paramNaming.biasFor;if(!n)throw new Error(`${t.id}: planGlobal needs paramNaming.biasFor (affine checkpoints only)`);const s=(i,f)=>[{name:`${i}_w`,parts:[{record:`${f}.weight`,convert:"raw"}]},{name:`${i}_s`,parts:[{record:`${f}.scales`,convert:"bf16->f16"}]},{name:`${i}_b`,parts:[{record:n(`${f}.weight`),convert:"bf16->f16"}]}];return[...l||a&&t.tiedEmbeddings?s("embed",`${r}model.embed_tokens`):[],...t.tiedEmbeddings||!a?[]:s("lm_head",`${r}lm_head`),...a?[{name:"final_norm",parts:[{record:`${r}model.norm.weight`,convert:"bf16->f16"}]}]:[]]}function Ee(t,e,o){var n,s;const r=[];let l=0;for(const i of t.parts){const f=e(i.record),c=(o==null?void 0:o(i.record))??"BF16";if(i.convert==="raw")r.push(f);else if(i.convert==="bf16->f16"&&c==="F16")r.push(f);else if(i.convert==="bf16->f32"&&c==="F32")r.push(f);else{if(c!=="BF16")throw new Error(`${t.name}: ${i.record} is ${c}, expected BF16 for ${i.convert}`);{const u=new Uint16Array(f.buffer,f.byteOffset,f.byteLength/2);if(i.convert==="bf16->f16"){const w=Be(u);l+=w.overflow,r.push(new Uint8Array(w.out.buffer))}else r.push(new Uint8Array(ve(u).buffer))}}}let a;if(((n=t.op)==null?void 0:n.kind)==="dequantAffineSplit")a=ke(t.name,t.op,r);else{const i=r.reduce((c,u)=>c+u.byteLength,0);a=new Uint8Array(new ArrayBuffer(i));let f=0;for(const c of r)a.set(c,f),f+=c.byteLength;((s=t.op)==null?void 0:s.kind)==="permuteRows"&&(a=xe(t.name,t.op,a))}if(l>0)throw new Error(`${t.name}: ${l} bf16 value(s) exceed f16 range and were clamped β€” these weights cannot be represented at f16 and the model would run wrong`);return{data:a,overflow:l}}function xe(t,e,o){if(e.src.length!==e.rows)throw new Error(`${t}: permuteRows has ${e.src.length} source indices for ${e.rows} rows`);if(o.byteLength%e.rows!==0)throw new Error(`${t}: ${o.byteLength} B does not divide into ${e.rows} rows`);const r=o.byteLength/e.rows,l=new Uint8Array(new ArrayBuffer(o.byteLength));for(let a=0;a<e.rows;a++){const n=e.src[a];if(n<0||n>=e.rows)throw new Error(`${t}: permuteRows src[${a}] = ${n} is out of range`);l.set(o.subarray(n*r,(n+1)*r),a*r)}return l}function ke(t,e,o){if(o.length!==3)throw new Error(`${t}: dequantAffineSplit needs exactly [weight, scales, biases], got ${o.length} parts`);const{k:r,heads:l,nope:a,v:n,group:s,bits:i}=e,f=32/i,c=l*(a+n),u=r/f,w=r/s,[d,b,$]=o,p=(m,g,j)=>{if(m!==g)throw new Error(`${t}: ${j} is ${g} B, expected ${m} B for [${c}, ${r}] at ${i} bits`)};p(c*u*4,d.byteLength,"weight"),p(c*w*2,b.byteLength,"scales"),p(c*w*2,$.byteLength,"biases");const _=new DataView(d.buffer,d.byteOffset,d.byteLength),v=new DataView(b.buffer,b.byteOffset,b.byteLength),M=new DataView($.buffer,$.byteOffset,$.byteLength),k=new Uint16Array(c*r),D=l*r*a,F=new Float32Array(r);let B=0;const T=(1<<i)-1;for(let m=0;m<l;m++)for(let g=0;g<a+n;g++){const j=m*(a+n)+g;for(let x=0;x<w;x++){const S=ne(v.getUint16((j*w+x)*2,!0)),O=ne(M.getUint16((j*w+x)*2,!0));for(let H=x*s;H<(x+1)*s;H++){const z=_.getUint32((j*u+(H/f|0))*4,!0);F[H]=(z>>>H%f*i&T)*S+O}}if(g<a){const x=m*r*a+g;for(let S=0;S<r;S++)Math.abs(F[S])>65504&&B++,k[x+S*a]=re(F[S])}else{const x=D+m*n*r+(g-a)*r;for(let S=0;S<r;S++)Math.abs(F[S])>65504&&B++,k[x+S]=re(F[S])}}if(B>0)throw new Error(`${t}: ${B} dequantized value(s) exceed f16 range β€” these weights cannot be represented at f16 and the model would run wrong`);return new Uint8Array(k.buffer)}async function je(t){const o=JSON.parse(new TextDecoder().decode(await t.whole("model.safetensors.index.json"))).weight_map,r=[...new Set(Object.values(o))].sort(),l={};for(const n of r){const s=await t.range(n,0,8),i=new DataView(s.buffer,s.byteOffset,8);if(i.getUint32(4,!0)!==0)throw new Error(`${n}: safetensors header longer than 4 GB`);const f=i.getUint32(0,!0),c=await t.range(n,0,8+f);l[n]=ge(c.buffer.slice(c.byteOffset,c.byteOffset+c.byteLength))}return{locate:n=>{const s=o[n];if(!s)throw new Error(`record not in checkpoint index: ${n}`);const i=l[s],f=i.tensors[n];if(!f)throw new Error(`record in index but not in ${s}'s header: ${n}`);return{file:s,begin:i.dataStart+f.begin,end:i.dataStart+f.end,info:f}},records:Object.keys(o),shards:r}}function le(t,e){const o=(e==null?void 0:e.start)??0,r=(e==null?void 0:e.end)??t.layers,l=[];for(const a of Se(t,void 0,e))l.push({plan:a,layer:null});for(let a=o;a<r;a++)for(const n of fe(t,a))l.push({plan:n,layer:a});return l}function de(t,e){const o=e===null?`g.${t.name}`:`l${e}.${t.name}`;return t.op?`${o}.${Fe(t)}`:o}function Fe(t){const e=t.op,o=e.kind==="permuteRows"?`rows=${e.rows};src=${e.src.join(",")}`:`bits=${e.bits};group=${e.group};k=${e.k};heads=${e.heads};nope=${e.nope};v=${e.v}`,r=`${e.kind};${o};${t.parts.map(a=>`${a.record}:${a.convert}`).join(",")}`;let l=2166136261;for(let a=0;a<r.length;a++)l^=r.charCodeAt(a),l=Math.imul(l,16777619);return((l>>>16^l)&65535).toString(16).padStart(4,"0")}async function ae(t,e,o){const r=new Map;for(const a of t.parts){const n=e(a.record);r.set(a.record,await o.range(n.file,n.begin,n.end))}const{data:l}=Ee(t,a=>{const n=r.get(a);if(!n)throw new Error(`internal: range for ${a} was not fetched`);return n},a=>e(a).info.dtype);return l}const We={embed_w:["root","embdWeights"],embed_s:["root","embdScales"],embed_b:["root","embdBiases"],lm_head_w:["root","lmHeadWeights"],lm_head_s:["root","lmHeadScales"],lm_head_b:["root","lmHeadBiases"],final_norm:["root","finalNormGamma"],norm1:["layer","normGamma1"],norm2:["layer","normGamma2"],c_attn_w:["layer","qkvWeights"],c_attn_s:["layer","qkvScales"],c_attn_b:["layer","qkvBiases"],o_proj_w:["layer","oProjWeights"],o_proj_s:["layer","oProjScales"],o_proj_b:["layer","oProjBiases"],q_norm:["layer","qNormGamma"],k_norm:["layer","kNormGamma"],mla_q_w:["mla","qWeights"],mla_q_s:["mla","qScales"],mla_q_b:["mla","qBiases"],mla_kva_w:["mla","kvaWeights"],mla_kva_s:["mla","kvaScales"],mla_kva_b:["mla","kvaBiases"],mla_kva_norm:["mla","kvaNormGamma"],mla_kvb:["mla","kvbF16"],gdn_proj_w:["gdn","projWeights"],gdn_proj_s:["gdn","projScales"],gdn_proj_b:["gdn","projBiases"],gdn_out_w:["gdn","outWeights"],gdn_out_s:["gdn","outScales"],gdn_out_b:["gdn","outBiases"],gdn_conv:["gdn","convWeight"],gdn_norm:["gdn","normGamma"],gdn_a_log:["gdn","aLog"],gdn_dt_bias:["gdn","dtBias"],moe_gate_proj_w:["moe","gateWeights"],moe_gate_proj_s:["moe","gateScales"],moe_gate_proj_b:["moe","gateBiases"],moe_up_proj_w:["moe","upWeights"],moe_up_proj_s:["moe","upScales"],moe_up_proj_b:["moe","upBiases"],moe_down_proj_w:["moe","downWeights"],moe_down_proj_s:["moe","downScales"],moe_down_proj_b:["moe","downBiases"],router_w:["moe","routerWeights"],router_s:["moe","routerScales"],router_b:["moe","routerBiases"],ffn_w:["layer","ffnWeights"],ffn_s:["layer","ffnScales"],ffn_b:["layer","ffnBiases"],ffn_down_w:["layer","ffnDownWeights"],ffn_down_s:["layer","ffnDownScales"],ffn_down_b:["layer","ffnDownBiases"]};function Ae(t){const e=/^moe_(gate|up|down)_proj_([wsb])$/.exec(t);return e?[e[1],e[2]]:null}async function Le(t,e,o,r,l,a={},n){var w,d,b,$,p;const s=le(e,n),i={device:t},f=Array.from({length:e.layers},()=>({}));let c=0,u=0;for(const{plan:_,layer:v}of s){const M=We[_.name];if(!M)throw new Error(`assembleMlx: no LoadedWeights slot for plan '${_.name}'`);const k=de(_,v),D=g=>{var S;const[j,x]=M;if(j==="root")i[x]=g;else{if(v===null)throw new Error(`assembleMlx: '${_.name}' is a layer plan with no layer`);if(j==="layer")f[v][x]=g;else{const O=(S=f[v])[j]??(S[j]={});O[x]=g}}},F=v!==null&&a.expertPool?Ae(_.name):null;if(F&&a.expertPool){const{slots:g,source:j}=a.expertPool,x=j.slabBytes(v,F[0],F[1]);if(a.cacheWrite&&!await((w=a.cacheHas)==null?void 0:w.call(a,k))){const O=await ae(_,r,o);a.cacheWrite(k,O.buffer.slice(O.byteOffset,O.byteOffset+O.byteLength))}const S=t.createBuffer({size:g*x,usage:l,label:`${k}.slots`});D(S),u+=g*x,c++,(d=a.onBuffer)==null||d.call(a,c,s.length,u);continue}let B;const T=await((b=a.cacheRead)==null?void 0:b.call(a,k));T?B=new Uint8Array(T):(B=await ae(_,r,o),($=a.cacheWrite)==null||$.call(a,k,B.buffer.slice(B.byteOffset,B.byteOffset+B.byteLength)));const m=t.createBuffer({size:B.byteLength,usage:l,label:k});t.queue.writeBuffer(m,0,B),u+=B.byteLength,c++,(p=a.onBuffer)==null||p.call(a,c,s.length,u),D(m)}return i.layers=f,e.tiedEmbeddings&&(i.lmHeadWeights=i.embdWeights,i.lmHeadScales=i.embdScales,i.lmHeadBiases=i.embdBiases),i.initNormGamma=f[(n==null?void 0:n.start)??0].normGamma1,i}const Me={BOOL:1,U8:1,I8:1,U16:2,I16:2,F16:2,BF16:2,U32:4,I32:4,F32:4,U64:8,I64:8,F64:8};function Oe(t,e,o,r,l){const a=`moe_${r}_proj_${l}`,n=fe(t,o).find($=>$.name===a);if(!n)throw new Error(`slab-source: ${t.id} layer ${o} has no '${a}' buffer β€” not a MoE layer`);if(n.op)throw new Error(`slab-source: '${a}' carries a ${n.op.kind} op; its bytes are not the stacked tensor`);if(n.parts.length>2)throw new Error(`slab-source: '${a}' has ${n.parts.length} parts, expected [stacked] or [stacked, shared]`);const s=e(n.parts[0].record),i=s.info.shape;if(i.length!==3)throw new Error(`slab-source: ${n.parts[0].record} has shape [${i}], expected [experts, N, K]`);const f=Me[s.info.dtype];if(!f)throw new Error(`slab-source: ${n.parts[0].record} has unsupported dtype ${s.info.dtype}`);const c=i[0],u=i[1]*i[2]*f,w=s.end-s.begin;if(w!==c*u)throw new Error(`slab-source: ${n.parts[0].record} declares ${w} B but [${i}] at ${s.info.dtype} is ${c*u} B`);if(t.moe&&t.moe.experts!==c)throw new Error(`slab-source: ${t.id} declares ${t.moe.experts} experts but ${n.parts[0].record} is stacked ${c} deep`);let d=null;if(n.parts.length===2){d=e(n.parts[1].record);const $=d.info.shape;if($.length!==2||$[0]!==i[1]||$[1]!==i[2]||d.info.dtype!==s.info.dtype)throw new Error(`slab-source: ${n.parts[1].record} is [${$}] ${d.info.dtype}, cannot stack as expert ${c} of [${i}] ${s.info.dtype}`);if(d.end-d.begin!==u)throw new Error(`slab-source: ${n.parts[1].record} declares ${d.end-d.begin} B, expected one slab of ${u} B`);if(n.parts[1].convert!==n.parts[0].convert)throw new Error(`slab-source: '${a}' converts its two parts differently (${n.parts[0].convert} vs ${n.parts[1].convert})`)}const b=n.parts[0].convert;if(b!=="raw"&&b!=="bf16->f16")throw new Error(`slab-source: '${a}' converts ${b}, which changes the slab's byte length`);return{experts:c,stride:u,stacked:s,shared:d,key:de(n,o),builtBytes:(c+(d?1:0))*u,convert:b,dtype:s.info.dtype}}function qe(t,e){const o=new Map;return(r,l,a)=>{const n=`${r}.${l}.${a}`;let s=o.get(n);return s||o.set(n,s=Oe(t,e,r,l,a)),s}}function Ue(t,e){const o=t.experts+(t.shared?1:0);if(!Number.isInteger(e)||e<0||e>=o)throw new Error(`slab-source: expert ${e} is outside 0..${o-1}`+(t.shared?` (${t.experts} routed + shared)`:` (${t.experts} routed, no shared expert)`));return e*t.stride}function oe(t){return t.replace(/[^A-Za-z0-9._-]/g,"_")}function De(t,e,o){const r=qe(t,e),l=new Map,a=new Map;let n=!0;const s=f=>{let c=l.get(f);return c||(c=o.getFileHandle(oe(f)).catch(u=>{throw l.delete(f),new Error(`slab-source: OPFS has no cached buffer '${oe(f)}' (${u})`)}),l.set(f,c)),c},i=async f=>{if(!n)return null;const c=a.get(f);if(c)return c;const u=await s(f);if(!u.createSyncAccessHandle)return n=!1,null;try{const w=await u.createSyncAccessHandle();return a.set(f,w),w}catch{return n=!1,null}};return{slabBytes:(f,c,u)=>r(f,c,u).stride,read:async(f,c,u,w)=>{const d=r(f,c,u),b=Ue(d,w),$=await i(d.key);if($){const v=$.getSize();if(v!==d.builtBytes)throw new Error(`slab-source: ${d.key} is ${v} B, expected ${d.builtBytes} B (${d.experts}${d.shared?"+1":""} slabs of ${d.stride} B)`);const M=new Uint8Array(new ArrayBuffer(d.stride)),k=$.read(M,{at:b});if(k!==d.stride)throw new Error(`slab-source: ${d.key} read ${k} of ${d.stride} B at ${b}`);return M}const p=await(await s(d.key)).getFile();if(p.size!==d.builtBytes)throw new Error(`slab-source: ${d.key} is ${p.size} B, expected ${d.builtBytes} B (${d.experts}${d.shared?"+1":""} slabs of ${d.stride} B)`);const _=await p.slice(b,b+d.stride).arrayBuffer();if(_.byteLength!==d.stride)throw new Error(`slab-source: ${d.key} returned ${_.byteLength} of ${d.stride} B at ${b}`);return new Uint8Array(_)},close:()=>{for(const f of a.values())try{f.close()}catch{}a.clear(),l.clear()}}}function Q(t,e=500){return e*Math.pow(2,t)*(.5+Math.random())}async function He(t,e,o,r={}){const l=r.retries??0,a=r.baseDelayMs??500,n=typeof e=="function"?e:()=>e,s=new Array(t.length),i=new AbortController;let f,c=!1,u=0;const w=async b=>{for(;!c&&u<t.length&&(b===0||b<n());){const $=u++;for(let p=0;;p++)try{s[$]=await o(t[$],$,i.signal);break}catch(_){if(i.signal.aborted)return;if(p<l){if(await new Promise(v=>setTimeout(v,Q(p,a))),i.signal.aborted)return;continue}c=!0,f=_,i.abort(_ instanceof Error?_:new Error(String(_)));return}}},d=Math.max(1,Math.min(n(),t.length));if(await Promise.all(Array.from({length:d},(b,$)=>w($))),c)throw f instanceof Error?f:new Error(String(f));return s}function Y(t){return`https://huggingface.co/${t.hfRepo}/resolve/main/`}const Ye=Y(ie),ue=8,he=3,C=4;let I=!1;async function Ne(t){var e,o,r,l,a;try{if(typeof navigator>"u"||!((e=navigator.storage)!=null&&e.getDirectory))return{dir:null,persisted:!1};let n=!1;try{n=await((r=(o=navigator.storage).persisted)==null?void 0:r.call(o))??!1,n||(n=await((a=(l=navigator.storage).persist)==null?void 0:a.call(l))??!1)}catch{}const s=await navigator.storage.getDirectory();try{const f=[];for await(const[u,w]of s)w.kind==="directory"&&u.startsWith(`${_e}-`)&&f.push(u);const c=s;await Promise.all(f.map(u=>c.removeEntry(u,{recursive:!0})))}catch{}return{dir:await s.getDirectoryHandle(t,{create:!0}),persisted:n}}catch{return{dir:null,persisted:!1}}}function Z(t){return t.replace(/[^A-Za-z0-9._-]/g,"_")}async function me(t,e){if(!t)return null;try{return await(await(await t.getFileHandle(Z(e))).getFile()).arrayBuffer()}catch{return null}}async function Te(t,e){if(!t)return!1;try{return await t.getFileHandle(Z(e)),!0}catch{return!1}}async function we(t,e,o){if(t)try{const l=await(await t.getFileHandle(Z(e),{create:!0})).createWritable();await l.write(o),await l.close()}catch{}}async function pe(t,e,o,r=C){const l=t.split("/").at(-1);let a=[],n=0,s;const i=f=>{s=f,I||(I=!0,o==null||o(`network unstable β€” dropping shard concurrency ${ue} β†’ ${he}`))};for(let f=0;f<=r;f++){if(f>0&&(o==null||o(`[retry ${f}/${r}] ${l}`+(n>0?` β€” resuming at ${(n/1e6).toFixed(1)} MB`:"")+` (${s})`),await new Promise(p=>setTimeout(p,Q(f-1)))),e!=null&&e.aborted)throw e.reason??new DOMException("Aborted","AbortError");let c;try{const p={};n>0&&(p.Range=`bytes=${n}-`),c=await fetch(t,{credentials:"omit",signal:e,headers:p})}catch(p){if(e!=null&&e.aborted)throw p;n>0&&(a=[],n=0),i(p);continue}if(c.status===416){a=[],n=0,s=new Error(`HTTP 416 ${c.statusText}`);continue}if(!c.ok){const p=new Error(`HTTP ${c.status} ${c.statusText}`);if(c.status<500&&c.status!==429&&c.status!==408)throw p;s=p;continue}c.status!==206&&n>0&&(a=[],n=0);const u=c.headers.get("content-length"),w=u?n+Number(u):null;try{if(c.body){const p=c.body.getReader();for(;;){const{done:_,value:v}=await p.read();if(_)break;a.push(v),n+=v.byteLength}}else{const p=await c.arrayBuffer();a.push(new Uint8Array(p)),n+=p.byteLength}}catch(p){if(e!=null&&e.aborted)throw p;i(p);continue}if(w!==null&&n!==w){i(new Error(`truncated body: got ${n} of ${w} bytes`));continue}const d=a.length===1?a[0]:null;if(d&&d.byteOffset===0&&d.byteLength===d.buffer.byteLength)return d.buffer;const b=new Uint8Array(n);let $=0;for(const p of a)b.set(p,$),$+=p.byteLength;return b.buffer}throw s instanceof Error?s:new Error(String(s))}function Re(t){return null}async function Ze(t,e="tokenizer.json"){return Y(t)}async function se(t,e,o,r,l,a,n,s){const i=t.split("/").at(-1),f=w=>{a.push(we(r,e,w))},c=await me(r,e);if(c)return n==null||n(`[opfs] ${i}`),c;for(const w of l)try{const d=await w.match(t);if(d){n==null||n(`[cache] ${i}`);const b=await d.arrayBuffer();return f(b),b}}catch{}n==null||n(`[fetch] ${i}`);const u=await pe(t,s,n);return f(u),u}function Ge(t,e,o,r){const l=a=>t+a;return{whole:async a=>new Uint8Array(await pe(l(a),r,o)),range:async(a,n,s)=>{let i;for(let f=0;f<=C;f++){f>0&&await new Promise(c=>setTimeout(c,Q(f-1)));try{const c=await fetch(l(a),{credentials:"omit",signal:r,headers:{Range:`bytes=${n}-${s-1}`}});if(c.status===200)throw new Error(`${a}: server ignored Range (200, not 206) β€” cannot stream a multi-GB shard`);if(!c.ok)throw new Error(`${a}: HTTP ${c.status}`);const u=new Uint8Array(await c.arrayBuffer());if(u.byteLength!==s-n)throw new Error(`${a}: asked for ${s-n} bytes, got ${u.byteLength}`);return u}catch(c){i=c,o==null||o(`[retry ${f+1}/${C}] ${a} ${n}-${s} (${c})`)}}throw new Error(`range fetch failed after ${C} retries: ${i}`)}}}async function Ce(){try{if(typeof caches>"u")return[];const t=await caches.keys();return await Promise.all(t.map(e=>caches.open(e)))}catch{return[]}}function ze(t){const e=[];for(const o of t.records)if("records"in o&&Array.isArray(o.records))for(const r of o.records)e.push({...r,dataPath:r.dataPath??o.dataPath});else e.push(o);return e}const V=()=>GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_SRC|GPUBufferUsage.COPY_DST;function Ie(t,e){return t?e?"OPFS persistent":"OPFS best-effort":"OPFS unavailable"}function Ve(t){if(!isFinite(t)||t<=0)return"β€”";if(t<60)return`${Math.round(t)}s`;const e=Math.floor(t/60),o=Math.round(t-e*60);return`${e}m ${o}s`}function Ke(t,e,o){const r=o.dtype==="float32"&&o.format==="f32-to-bf16",l=r?o.nbytes*2:o.nbytes;if(l>t.limits.maxStorageBufferBindingSize)throw new Error(`Weight record '${o.name}' (${l} bytes) exceeds device.limits.maxStorageBufferBindingSize (${t.limits.maxStorageBufferBindingSize}). Request a higher requiredLimits.maxStorageBufferBindingSize at device creation.`);const a=t.createBuffer({size:Math.max(l,4),usage:V(),label:o.name});if(r){const n=new Uint16Array(e,o.byteOffset,o.nbytes/2),s=new Uint32Array(n.length);for(let i=0;i<n.length;i++)s[i]=n[i]<<16;return t.queue.writeBuffer(a,0,s),a}return t.queue.writeBuffer(a,0,new Uint8Array(e,o.byteOffset,o.nbytes)),a}async function Je(t,e,o,r=ie,l,a){if(l&&r.weightFormat!=="mlx-safetensors")throw new Error(`loadWeights: layerRange needs an MLX checkpoint; ${r.id} ships MLC shards`);if(a&&r.weightFormat!=="mlx-safetensors")throw new Error(`loadWeights: expertPool needs an MLX checkpoint; ${r.id} ships MLC shards`);const n=Y(r),s=ye(r),i=Re(),f=r.manifestName??"ndarray-cache.json";e==null||e(`Loading ${f}…`);const{dir:c,persisted:u}=await Ne(s);e==null||e(c?`OPFS ready (${s}, ${u?"persistent":"best-effort"})`:"OPFS unavailable");const w=await Ce();w.length>0&&(e==null||e(`Cache API: ${w.length} store(s) open`));const d=[];if(r.weightFormat==="mlx-safetensors"){const y=Ge(n,i,e);e==null||e("Reading safetensors headers…");const{locate:h,shards:q}=await je(y),R=le(r,l).length;e==null||e(`${q.length} shards, ${R} buffers to build`);let A;if(a){if(!r.moe)throw new Error(`loadWeights: expertPool is a sparse-MoE feature and ${r.id} has no experts`);if(!c)throw new Error("loadWeights: expertPool has nowhere to read an expert slab from β€” no dev mirror, and OPFS is unavailable in this runtime");A={source:De(r,h,c),slots:a},e==null||e(`expert pool: ${a} slots per layer, slabs from OPFS`)}const N=performance.now(),W=await Le(t,r,y,h,V(),{onProgress:e,cacheRead:E=>me(c,E),cacheHas:E=>Te(c,E),...A?{expertPool:{slots:A.slots,source:A.source}}:{},cacheWrite:(E,U)=>{d.push(we(c,E,U))},onBuffer:(E,U,G)=>{if(E%25!==0&&E!==U)return;const L=(performance.now()-N)/1e3,$e=G/(E/U);e==null||e(`${E}/${U} buffers β€” ${(G/1e9).toFixed(2)} GB in ${L.toFixed(0)}s`),o==null||o({shardsLoaded:E,totalShards:U,bytesLoaded:G,totalBytes:Math.round($e),mbPerSec:L>0?G/1e6/L:0,etaSec:E>0?L/E*(U-E):0,persisted:u})}},l);return await Promise.all(d),e==null||e(`Weights ready (${((performance.now()-N)/1e3).toFixed(0)}s)`),A&&(W.moeSlabs=A),W}const b=await se(n+f,f,i,c,w,d,e),$=JSON.parse(new TextDecoder().decode(b)),p=ze($);e==null||e(`Manifest: ${p.length} parameters`);const _=new Map;for(const y of p){const h=_.get(y.dataPath);h?h.push(y):_.set(y.dataPath,[y])}const v=new Map,M=_.size,k=p.reduce((y,h)=>y+h.nbytes,0),D=performance.now();let F=0,B=0;const T=[..._.entries()];try{await He(T,()=>I?he:ue,async([y,h],q,R)=>{let A;try{A=await se(n+y,y,i,c,w,d,e,R)}catch(L){throw R.aborted?L:new Error(`Shard '${y}' failed: ${L instanceof Error?L.message:L}`)}for(const L of h)v.set(L.name,Ke(t,A,L));F++,B+=A.byteLength;const N=(performance.now()-D)/1e3,W=B/1e6/Math.max(N,.1),E=W>0?(k-B)/(W*1e6):0,U=(B/1e6).toFixed(0),G=(k/1e6).toFixed(0);e==null||e(`[${F}/${M}] ${y} Β· ${U}/${G} MB Β· ${W.toFixed(1)} MB/s Β· ETA ${Ve(E)}`),o==null||o({shardsLoaded:F,totalShards:M,bytesLoaded:B,totalBytes:k,mbPerSec:W,etaSec:E,persisted:u})},{retries:1})}catch(y){await Promise.allSettled(d);const h=y instanceof Error?y.message:String(y);throw new Error(`${h} β€” ${F}/${M} shards (${(B/1e6).toFixed(0)} MB) are safely cached (OPFS); reloading resumes from cache and only re-downloads what's missing. If you use an ad/privacy blocker, allow huggingface.co and hf.co β€” HF's bot protection can kill downloads when blocked.`)}function m(...y){for(const h of y){const q=v.get(h);if(q)return q}throw new Error(`Weight not found. Tried: ${y.join(", ")}
2
+ Available: ${[...v.keys()].slice(0,20).join(", ")}`)}const g=r.paramNaming,j=m(...g.embedWeight),x=m(...g.embedScale),S=m(...g.layer(0).norm1),O=m(...g.lmHeadWeight),H=m(...g.lmHeadScale),z=m(...g.finalNorm),J=t.createCommandEncoder({label:"gdnProjPack"}),X=[];function P(y,h,q){const R=y.reduce((W,E)=>W+E.rows,0),A=t.createBuffer({size:R*h,usage:V(),label:q});let N=0;for(const W of y)J.copyBufferToBuffer(W.buf,0,A,N,W.rows*h),N+=W.rows*h,X.push(W.buf);return A}const ee=[];for(let y=0;y<r.layers;y++){const h=g.layer(y),q=r.layerKinds[y];ee.push({normGamma1:m(...h.norm1),normGamma2:m(...h.norm2),ffnWeights:m(...h.ffnWeight),ffnScales:m(...h.ffnScale),ffnDownWeights:m(...h.ffnDownWeight),ffnDownScales:m(...h.ffnDownScale),...q==="attn"?{qkvWeights:m(...h.qkvWeight),qkvScales:m(...h.qkvScale),oProjWeights:m(...h.oProjWeight),oProjScales:m(...h.oProjScale),qNormGamma:h.qNorm?m(...h.qNorm):void 0,kNormGamma:h.kNorm?m(...h.kNorm):void 0}:{gdn:{projWeights:P([{buf:m(...h.gdnQkvWeight),rows:r.gdnQkvDim},{buf:m(...h.gdnZWeight),rows:r.gdnVDim},{buf:m(...h.gdnAWeight),rows:r.gdnVHeads},{buf:m(...h.gdnBWeight),rows:r.gdnVHeads}],r.dPacked*4,`gdnProjWeights_${y}`),projScales:P([{buf:m(...h.gdnQkvScale),rows:r.gdnQkvDim},{buf:m(...h.gdnZScale),rows:r.gdnVDim},{buf:m(...h.gdnAScale),rows:r.gdnVHeads},{buf:m(...h.gdnBScale),rows:r.gdnVHeads}],r.dScales*2,`gdnProjScales_${y}`),aLog:m(...h.gdnALog),dtBias:m(...h.gdnDtBias),convWeight:m(...h.gdnConvWeight),normGamma:m(...h.gdnNormWeight),outWeights:m(...h.gdnOutWeight),outScales:m(...h.gdnOutScale)}}})}t.queue.submit([J.finish()]);for(const y of X)y.destroy();d.length>0&&await Promise.allSettled(d);const te=(performance.now()-D)/1e3,be=B/1e6/Math.max(te,.1);return e==null||e(`All weights loaded Β· ${(k/1e6).toFixed(0)} MB in ${te.toFixed(1)}s Β· avg ${be.toFixed(1)} MB/s Β· ${Ie(c,u)}`),{device:t,embdWeights:j,embdScales:x,lmHeadWeights:O,lmHeadScales:H,initNormGamma:S,finalNormGamma:z,layers:ee}}export{Ye as P,de as a,Ve as f,Je as l,Z as o,le as p,Ze as r};
assets/zero-tvm-nxGrkTTcQf.js ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/cache-probe-BYX_n9OPRB.js","assets/weight-loader-ByOLMd9f5C.js","assets/model-registry-BoK-ObgACo.js"])))=>i.map(i=>d[i]);
2
+ import{S as T,m as y}from"./model-registry-BoK-ObgACo.js";import{_ as x}from"./preload-helper-ckwbz45pO3.js";import{specFromSearch as B}from"./model-select-DhhV9mCi0T.js";import{m as q,a as b}from"./mascot-BCKWddkWrV.js";import{setBadge as w}from"./loading-ui-CjCwSRuXvh.js";import{b as N,s as L,w as _}from"./chat-flow-eTAzKAlIYe.js";import{q as $,s as A}from"./chat-ui-CQAxV1po6S.js";import"./weight-loader-ByOLMd9f5C.js";import"./engine-core-CLR3cxsqf4.js";import"./variants-De-VVc9BRz.js";const E=T.filter(e=>!y(e.spec).pending&&e.spec.embeddingOnly!==!0);function k(e){const o=new URL(window.location.href);e?o.searchParams.set("model",e):o.searchParams.delete("model"),window.location.href=o.toString()}function z(e){const o=document.getElementById("model-switch"),t=document.getElementById("model-menu");if(!o||!t)return;const n=new Map;function a(){t.innerHTML="";for(const{param:i,spec:c}of E){const s=y(c),m=c.id===e.id,p=document.createElement("button");p.type="button",p.role="option",p.setAttribute("aria-selected",String(m));const I=n.get(c.id)?'<span class="mm-meta mm-cached">cached</span>':`<span class="mm-meta">${s.sizeLabel} download${s.ramNote?` Β· ${s.ramNote}`:""}</span>`;p.innerHTML=`<span class="mm-name">${s.name}</span>${I}`,m||p.addEventListener("click",()=>k(i)),t.appendChild(p)}}function d(){t.hidden=!0,o.setAttribute("aria-expanded","false")}o.addEventListener("click",i=>{i.stopPropagation();const c=t.hidden;t.hidden=!c,o.setAttribute("aria-expanded",String(c))}),document.addEventListener("click",d),document.addEventListener("keydown",i=>{i.key==="Escape"&&d()}),t.addEventListener("click",i=>i.stopPropagation()),a(),S(e,n),x(()=>import("./cache-probe-BYX_n9OPRB.js"),__vite__mapDeps([0,1,2])).then(async({isModelCached:i})=>{await Promise.all(E.map(async({spec:c})=>n.set(c.id,await i(c)))),a(),S(e,n)}).catch(()=>{})}function S(e,o){const t=document.getElementById("dialog-models");if(t){t.innerHTML="";for(const{param:n,spec:a}of E){const d=y(a),i=a.id===e.id,c=document.createElement("button");c.type="button",c.className="dm-chip",c.setAttribute("aria-pressed",String(i));const s=o.get(a.id)?"cached":d.sizeLabel;c.innerHTML=`<span class="n">${d.name}</span><span class="s">${s}</span>`,i||c.addEventListener("click",()=>k(n)),t.appendChild(c)}}}const u=e=>document.getElementById(e);function U(){const e=new URLSearchParams(location.search),o=(t,n)=>{const a=Number(e.get(t));return Number.isFinite(a)&&e.get(t)!==null?a:n};return{temperature:o("temp",0),topP:o("topp",1),minP:o("minp",0),seed:o("seed",0)}}function W(e){const o=new URL(location.href),t=(n,a,d)=>{a===d?o.searchParams.delete(n):o.searchParams.set(n,String(a))};t("temp",e.temperature,0),t("topp",e.topP??1,1),t("minp",e.minP??0,0),t("seed",e.seed??0,0),history.replaceState(null,"",o.toString())}const v=[{key:"temperature",id:"samp-temp",label:"Temperature",min:0,max:2,step:.05,dflt:0,note:"0 = greedy (argmax). Higher is more varied."},{key:"topP",id:"samp-topp",label:"Top-p",min:.05,max:1,step:.01,dflt:1,note:"Keep the smallest set of tokens covering this much probability. 1 = off."},{key:"minP",id:"samp-minp",label:"Min-p",min:0,max:.5,step:.005,dflt:0,note:"Drop tokens below this fraction of the top token. 0 = off."}];function R(e){const o=u("sampling-btn"),t=u("sampling-menu");if(!o||!t)return;let n=U();const a=v.map(s=>`
3
+ <label class="samp-row" for="${s.id}">
4
+ <span class="samp-label">${s.label}<b id="${s.id}-val"></b></span>
5
+ <input id="${s.id}" type="range" min="${s.min}" max="${s.max}" step="${s.step}">
6
+ <span class="samp-note">${s.note}</span>
7
+ </label>`).join("");t.innerHTML=`
8
+ <div class="samp-head"><b>Sampling</b><span id="samp-mode"></span></div>
9
+ ${a}
10
+ <label class="samp-row" for="samp-seed">
11
+ <span class="samp-label">Seed<b id="samp-seed-val"></b></span>
12
+ <div class="samp-seed-row">
13
+ <input id="samp-seed" type="number" min="0" step="1">
14
+ <button type="button" id="samp-reseed" class="samp-btn">New</button>
15
+ </div>
16
+ <span class="samp-note">The same seed replays the same text, token for token.</span>
17
+ </label>
18
+ <button type="button" id="samp-reset" class="samp-btn samp-reset">Back to greedy</button>`;const d=()=>{for(const m of v){const p=n[m.key]??m.dflt;u(m.id).value=String(p),u(`${m.id}-val`).textContent=m.key==="temperature"&&p===0?"off":String(p)}u("samp-seed").value=String(n.seed??0);const s=n.temperature<=0;u("samp-mode").textContent=s?"greedy Β· argmax":"sampling",t.classList.toggle("samp-greedy",s),o.classList.toggle("active",!s),o.title=s?"Sampling: off (greedy)":`Sampling: temp ${n.temperature}`},i=()=>{var s;W(n),d(),(s=e())==null||s.setSampling(n.temperature>0?n:null)};for(const s of v)u(s.id).addEventListener("input",m=>{n={...n,[s.key]:Number(m.target.value)},i()});u("samp-seed").addEventListener("input",s=>{n={...n,seed:Math.max(0,Math.floor(Number(s.target.value)||0))},i()}),u("samp-reseed").addEventListener("click",()=>{n={...n,seed:Math.floor(Math.random()*2**31)},i()}),u("samp-reset").addEventListener("click",()=>{n={temperature:0,topP:1,minP:0,seed:n.seed??0},i()});const c=()=>{t.hidden=!0,o.setAttribute("aria-expanded","false")};o.addEventListener("click",s=>{s.stopPropagation(),t.hidden=!t.hidden,o.setAttribute("aria-expanded",String(!t.hidden))}),t.addEventListener("click",s=>s.stopPropagation()),document.addEventListener("click",c),document.addEventListener("keydown",s=>{s.key==="Escape"&&c()}),d()}const r=B(location.search),l=y(r);function H(){if(!r.moe)return 0;const e=new URLSearchParams(location.search).get("pool");if(!e)return 0;const o=r.moe.experts,t=e==="half"?Math.round(o/2):e==="quarter"?Math.round(o/4):Number(e);return Number.isFinite(t)&&t>0?t:0}const h=H();function O(){if(!h)return;const e=(l.poolModes??[]).find(n=>n.slots===h),o=document.querySelector("#start-dialog .dialog-notes ul");if(!o)return;const t=document.createElement("li");t.textContent=`Memory build: ${e?e.label:`${h} expert slots`}`+(e!=null&&e.note?` β€” ${e.note}`:""),t.style.color="var(--warn, #d9a441)",o.prepend(t)}const P=$(r);A(l.name,P);const D=e=>document.getElementById(e);function f(e){const o=document.getElementById("progress-log");if(!o){console.log(e);return}o.textContent+=e+`
19
+ `,o.scrollTop=o.scrollHeight}function F(){var e;(e=D("loading-overlay"))==null||e.classList.remove("active")}async function G(){if("serviceWorker"in navigator)try{await navigator.serviceWorker.register("/weights-cache-sw.js",{scope:"/"}),await navigator.serviceWorker.ready}catch(e){console.warn("[zero-tvm] weight-cache SW registration failed:",e)}}function V(){return new Promise(e=>{const o=document.getElementById("start-dialog");if(o!=null&&o.showModal){w("Waiting");const i=o.querySelector("#start-btn");i==null||i.addEventListener("click",()=>{o.close(),o.remove(),e()},{once:!0}),o.showModal();return}const t=document.getElementById("chat");if(!t){e();return}t.innerHTML=`
20
+ <div id="start-screen" class="start-screen" style="display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:60vh;gap:1.25rem;text-align:center;padding:2rem">
21
+ <div class="title" style="font-size:1.1rem;font-weight:600;color:#ccc"></div>
22
+ <div class="desc" style="font-size:0.85rem;color:#888;max-width:440px;line-height:1.6">
23
+ First load downloads the ${P} weights and caches them
24
+ locally (OPFS). Every subsequent visit is instant; weights are
25
+ served from the on-device cache.
26
+ </div>
27
+ <button id="start-btn" class="start-btn" style="background:#10b981;color:#000;font-weight:600;font-size:0.95rem;padding:0.7rem 2rem;border:none;border-radius:8px;cursor:pointer">Download &amp; Start</button>
28
+ <div class="start-hint" style="font-size:0.68rem;color:#444"></div>
29
+ </div>`;const n=t.querySelector("#start-screen .title");n&&(n.textContent=`${l.name}, in your browser`);const a=t.querySelector("#start-screen .start-hint");a&&(a.textContent=`${r.hfRepo.split("/")[1]} Β· ${l.sizeLabel} Β· cached after first load`),w("Waiting");const d=document.getElementById("start-btn");d==null||d.addEventListener("click",()=>{d.disabled=!0,d.textContent="Starting...",t.innerHTML="",e()},{once:!0})})}async function M(){w("Initializing…","loading"),f(`Zero-TVM ${l.name} β€” No WebLLM, No TVM`),f("10 hand-written WGSL kernel roles"),f("");const e=await N({spec:r,poolSlots:h,search:location.search,onDeviceLost:n=>{L(`GPU device lost: ${n.message||n.reason}. Reload the page to recover.`)}});if(!e.ok){L(e.reason,()=>{M()}),f(`ERROR: ${e.reason}`);return}const{tokenizer:o,engine:t}=e;C=t,F(),_({spec:r,tokenizer:o,engine:t,onToken:()=>g==null?void 0:g.pulse(),onPhase:n=>g==null?void 0:g.setMood(n==="generating"?"talking":n==="thinking"?"thinking":"idle"),commands:{roster:()=>{location.href="/"}},log:f})}function j(){const e=(t,n)=>{const a=document.querySelector(t);a&&(a.textContent=n)};e("#welcome-quant",`${$(r)} Β· ${l.params}`),e("#welcome-ctx",`${Math.round(r.maxContext/1024)} K context`);{const t=l.params.split(/[\sΒ·]/)[0],n=document.querySelector('.suggest[data-prompt*="fall short"]');n&&(n.dataset.prompt=`List four kinds of questions where a ${t} model like ${l.name} is likely to fall short compared to a frontier LLM, with one-sentence reasons.`,e('.suggest[data-prompt*="fall short"] .suggest-hint',`Where a ${t} model falls short`))}K();const o=document.querySelectorAll("#start-dialog .dialog-stats .dstat .val");if(o[0]&&(o[0].textContent=l.sizeLabel),o[1]&&(l.rateLabel?o[1].textContent=l.rateLabel:o[1].parentElement.style.display="none"),e("#gate-ctx",`${Math.round(r.maxContext/1024)}K`),l.ramNote){const t=document.querySelector("#start-dialog .dialog-notes ul");if(t){const n=document.createElement("li");n.textContent=l.ramNote,n.style.color="var(--warn, #d9a441)",t.prepend(n)}}O(),r.id!=="phi3-mini"&&(document.title=`Zero-TVM Chat β€” ${l.name}`,e("#start-dialog .dialog-title",`${l.name} on raw WebGPU`),e(".model-info h1",l.name),e("#welcome .welcome-title",`Chat with ${l.name}`))}let g=null;function K(){var t;const e=document.querySelector(".model-info");if(e&&!document.getElementById("header-mascot")){const n=document.createElement("canvas");n.id="header-mascot",n.className="header-mascot",n.setAttribute("aria-hidden","true"),(t=e.parentElement)==null||t.insertBefore(n,e),b(n,r).then(a=>{if(!a){n.remove();return}g=a,h&&r.moe&&a.setSpec(r,!1,h/(r.moe.experts+(r.sharedExpertIndex>=0?1:0)))}).catch(()=>n.remove())}const o=document.getElementById("welcome-logo");if(o&&!o.querySelector("canvas")){const n=document.createElement("canvas");n.setAttribute("aria-hidden","true"),o.appendChild(n),o.classList.add("has-mascot"),b(n,r).then(async a=>{if(!a){n.remove(),o.classList.remove("has-mascot");return}const d=await a.snapshot(!0);document.documentElement.style.setProperty("--mascot-avatar",`url("${d}")`),document.documentElement.classList.add("has-mascot-avatar")}).catch(()=>{n.remove(),o.classList.remove("has-mascot")})}}let C=null;function Z(){var n,a;const e=document.getElementById("loading-title"),o=document.getElementById("progress-status"),t=document.getElementById("progress-detail");e&&(e.textContent="This browser cannot run the engine"),o&&(o.textContent="zero-tvm needs WebGPU with shader-f16. Chrome and Edge ship it; Safari from 26. The MoE models additionally need GPU subgroups (Chromium)."),t&&(t.textContent=""),(n=document.getElementById("start-dialog"))==null||n.remove(),(a=document.querySelector(".progress-track"))==null||a.remove()}async function Q(){var o;if(!("gpu"in navigator)){Z();return}j(),z(r);{const{accent:t,accentHi:n}=q(r),a=document.documentElement.style;a.setProperty("--accent",t),a.setProperty("--accent-hi",n),a.setProperty("--accent-2",n),a.setProperty("--accent-dim",`${t}22`),a.setProperty("--accent-tint",`${t}1f`)}{const t=document.getElementById("gate-mascot");t&&b(t,r).then(n=>{if(!n){t.remove();return}h&&r.moe&&n.setSpec(r,!1,h/(r.moe.experts+(r.sharedExpertIndex>=0?1:0)))}).catch(()=>t.remove())}R(()=>C),await G();const{isModelCached:e}=await x(async()=>{const{isModelCached:t}=await import("./cache-probe-BYX_n9OPRB.js");return{isModelCached:t}},__vite__mapDeps([0,1,2]));await e(r)?(o=document.getElementById("start-dialog"))==null||o.remove():await V(),await M()}Q().catch(e=>{console.error(e),f(`FATAL: ${e}`)});
chat-ui.css ADDED
@@ -0,0 +1,523 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* CHAT-UI β€” the conversational surface shared by zero-tvm.html (chat) and
2
+ * share.html (remote guest): palette, chat area, message bubbles, streaming
3
+ * markdown, actions, scroll FAB, composer. Moved VERBATIM out of
4
+ * zero-tvm.html's inline styles so the two pages cannot drift; the page-
5
+ * specific chrome (header, welcome, gate, overlay) stays inline per page.
6
+ * Companion module: src/zero-tvm/chat-ui.ts. */
7
+
8
+ /* ALIAS LAYER onto the family tokens (public/tokens.css), which must be
9
+ * linked BEFORE this file. The chat surface kept its own palette β€” a green
10
+ * accent on a different ground β€” so the instrument and the landing page
11
+ * looked like two products. Only the names this file does not share with
12
+ * tokens.css are redefined here; --bg, --border, --text, --accent and
13
+ * --mono come straight through.
14
+ *
15
+ * --muted is the exception and must be restated: this file uses it for the
16
+ * DIMMEST text, where the token set uses it for mid-tone. Letting the token
17
+ * value through would brighten every caption on the page. */
18
+ :root {
19
+ --panel: var(--surface);
20
+ --panel-2: var(--surface-2);
21
+ --border-2: var(--border-hi);
22
+ --text-dim: #a9b3c6;
23
+ --muted: var(--dim);
24
+ --accent-2: var(--accent-hi);
25
+ --accent-tint: var(--accent-dim);
26
+ --user-bubble: var(--surface-2);
27
+ --danger: var(--err);
28
+ --radius: 12px;
29
+ --composer-max: 760px;
30
+ --msg-max: 760px;
31
+ --font: var(--body);
32
+ }
33
+
34
+ /* The browser's default focus ring is blue, which on this palette reads as
35
+ * a second selected state next to the amber one β€” in the model picker two
36
+ * chips looked chosen at once. Shared by the chat page and the rooms guest,
37
+ * matching landing.css. */
38
+ :focus-visible {
39
+ outline: 2px solid var(--accent);
40
+ outline-offset: 2px;
41
+ border-radius: var(--radius);
42
+ }
43
+
44
+ /* ─────────── Chat area ─────────── */
45
+ /* share.html's guest view is a flex column, not zero-tvm.html's auto/1fr/auto
46
+ grid β€” without flex:1 this collapsed to ~56px on an empty room and the
47
+ composer floated near the top of a blank page. Scoped: on zero-tvm.html the
48
+ grid row sizes it and #guest-view does not exist. */
49
+ #guest-view .chat-main { flex: 1; min-height: 0; }
50
+ .chat-main {
51
+ overflow-y: auto;
52
+ scroll-behavior: smooth;
53
+ position: relative;
54
+ }
55
+ .chat-main::-webkit-scrollbar { width: 8px; }
56
+ .chat-main::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 4px; }
57
+ .chat-main::-webkit-scrollbar-track { background: transparent; }
58
+
59
+ .chat-inner {
60
+ max-width: var(--msg-max);
61
+ margin: 0 auto;
62
+ padding: 1.5rem 1rem 2rem;
63
+ }
64
+
65
+
66
+ /* ─────────── Messages ─────────── */
67
+ .msg { margin-bottom: 1.75rem; animation: fade-in 0.2s ease; }
68
+ .msg:last-child { margin-bottom: 0.5rem; }
69
+ @keyframes fade-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
70
+
71
+ .msg.user {
72
+ display: flex;
73
+ justify-content: flex-end;
74
+ gap: 0.5rem;
75
+ }
76
+ .msg.user .bubble {
77
+ max-width: 80%;
78
+ background: var(--user-bubble);
79
+ border: 1px solid var(--border);
80
+ padding: 0.65rem 0.95rem;
81
+ border-radius: 16px 16px 4px 16px;
82
+ white-space: pre-wrap;
83
+ word-wrap: break-word;
84
+ line-height: 1.55;
85
+ }
86
+
87
+ .msg.ai .role {
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 0.5rem;
91
+ font-size: 0.72rem;
92
+ color: var(--muted);
93
+ margin-bottom: 0.45rem;
94
+ font-weight: 500;
95
+ }
96
+ .msg.ai .role .role-name { color: var(--text-dim); }
97
+ .msg.ai .role .model-tag {
98
+ margin-left: auto;
99
+ font-size: 0.65rem;
100
+ color: var(--muted);
101
+ padding: 2px 7px;
102
+ border: 1px solid var(--border);
103
+ border-radius: 999px;
104
+ background: var(--panel);
105
+ font-variant-numeric: tabular-nums;
106
+ }
107
+ .msg.ai .role-dot {
108
+ width: 22px; height: 22px; border-radius: 7px;
109
+ /* Was a fixed violet, so every model's replies were badged in Phi-3's
110
+ colours. Both stops come from the model's own lane now. */
111
+ background: linear-gradient(135deg, var(--accent), var(--accent-hi));
112
+ display: inline-flex; align-items: center; justify-content: center;
113
+ font-size: 0.62rem; font-weight: 800; color: #000;
114
+ letter-spacing: -0.05em;
115
+ }
116
+ /* One still frame of the model's mascot, captured once at boot and reused
117
+ by every reply. The letter stays as the fallback for browsers that gave
118
+ us no mascot to capture. */
119
+ .has-mascot-avatar .msg.ai .role-dot {
120
+ background: var(--mascot-avatar) center/cover no-repeat,
121
+ linear-gradient(135deg, var(--accent), var(--accent-hi));
122
+ font-size: 0;
123
+ }
124
+ .msg.ai .body {
125
+ color: var(--text);
126
+ word-wrap: break-word;
127
+ line-height: 1.65;
128
+ }
129
+ .msg.ai .body > *:first-child { margin-top: 0; }
130
+ .msg.ai .body > *:last-child { margin-bottom: 0; }
131
+ .msg.ai .body p { margin: 0 0 0.7rem; }
132
+ .msg.ai .body ul, .msg.ai .body ol {
133
+ margin: 0 0 0.7rem;
134
+ padding-left: 1.4rem;
135
+ }
136
+ .msg.ai .body li { margin-bottom: 0.25rem; }
137
+ .msg.ai .body li > p { margin: 0; }
138
+ .msg.ai .body h1, .msg.ai .body h2, .msg.ai .body h3,
139
+ .msg.ai .body h4, .msg.ai .body h5, .msg.ai .body h6 {
140
+ font-weight: 600;
141
+ letter-spacing: -0.01em;
142
+ margin: 1rem 0 0.45rem;
143
+ line-height: 1.3;
144
+ color: #fff;
145
+ }
146
+ .msg.ai .body h1 { font-size: 1.25rem; }
147
+ .msg.ai .body h2 { font-size: 1.12rem; }
148
+ .msg.ai .body h3 { font-size: 1.02rem; }
149
+ .msg.ai .body h4, .msg.ai .body h5, .msg.ai .body h6 { font-size: 0.95rem; }
150
+ .msg.ai .body code {
151
+ font-family: var(--mono);
152
+ font-size: 0.85em;
153
+ background: var(--panel-2);
154
+ padding: 1px 5px;
155
+ border-radius: 4px;
156
+ border: 1px solid var(--border);
157
+ }
158
+ .msg.ai .body blockquote {
159
+ border-left: 3px solid var(--border-2);
160
+ padding: 0.1rem 0 0.1rem 0.85rem;
161
+ color: var(--text-dim);
162
+ margin: 0 0 0.7rem;
163
+ }
164
+ .msg.ai .body hr {
165
+ border: 0;
166
+ border-top: 1px solid var(--border);
167
+ margin: 1rem 0;
168
+ }
169
+ .msg.ai .body a { color: var(--accent-2); text-decoration: underline; text-underline-offset: 2px; }
170
+ .msg.ai .body strong { color: #fff; font-weight: 600; }
171
+
172
+ /* Fenced code block β€” top bar with lang pill + copy button */
173
+ .msg.ai .body .code-block {
174
+ margin: 0.7rem 0;
175
+ border-radius: 10px;
176
+ border: 1px solid var(--border);
177
+ background: var(--panel);
178
+ overflow: hidden;
179
+ }
180
+ .msg.ai .body .code-head {
181
+ display: flex;
182
+ align-items: center;
183
+ justify-content: space-between;
184
+ padding: 0.4rem 0.6rem 0.4rem 0.75rem;
185
+ background: var(--panel-2);
186
+ border-bottom: 1px solid var(--border);
187
+ font-size: 0.68rem;
188
+ color: var(--muted);
189
+ }
190
+ .msg.ai .body .code-lang {
191
+ text-transform: uppercase;
192
+ letter-spacing: 0.08em;
193
+ font-weight: 600;
194
+ }
195
+ .msg.ai .body .code-copy {
196
+ display: inline-flex;
197
+ align-items: center;
198
+ gap: 4px;
199
+ padding: 3px 8px;
200
+ border-radius: 6px;
201
+ color: var(--text-dim);
202
+ font-size: 0.68rem;
203
+ transition: background 0.15s, color 0.15s;
204
+ }
205
+ .msg.ai .body .code-copy:hover { background: rgba(255,255,255,0.06); color: var(--text); }
206
+ .msg.ai .body .code-copy.copied { color: var(--accent); }
207
+ .msg.ai .body .code-copy .icon { width: 11px; height: 11px; stroke-width: 2.3; }
208
+ .msg.ai .body .code-block pre {
209
+ margin: 0;
210
+ padding: 0.75rem 0.9rem;
211
+ overflow-x: auto;
212
+ background: none;
213
+ border: none;
214
+ }
215
+ .msg.ai .body .code-block pre code {
216
+ background: none;
217
+ border: none;
218
+ padding: 0;
219
+ font-size: 0.82rem;
220
+ line-height: 1.5;
221
+ color: var(--text);
222
+ }
223
+
224
+ .msg.ai .cursor {
225
+ display: inline-block;
226
+ width: 7px;
227
+ height: 1.05em;
228
+ background: var(--accent);
229
+ margin-left: 2px;
230
+ vertical-align: text-bottom;
231
+ border-radius: 1px;
232
+ animation: blink 1.1s steps(2) infinite;
233
+ opacity: 0.9;
234
+ }
235
+ @keyframes blink { 50% { opacity: 0.1; } }
236
+
237
+ /* Thinking indicator β€” 3 dots that wave while prefill runs */
238
+ .thinking {
239
+ display: inline-flex;
240
+ align-items: center;
241
+ gap: 4px;
242
+ padding: 2px 0;
243
+ }
244
+ .thinking span {
245
+ width: 6px; height: 6px;
246
+ border-radius: 50%;
247
+ background: var(--muted);
248
+ animation: wave 1.2s ease-in-out infinite;
249
+ }
250
+ .thinking span:nth-child(2) { animation-delay: 0.15s; }
251
+ .thinking span:nth-child(3) { animation-delay: 0.3s; }
252
+ @keyframes wave {
253
+ 0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
254
+ 30% { transform: translateY(-3px); opacity: 1; }
255
+ }
256
+
257
+ .msg .actions {
258
+ display: flex;
259
+ align-items: center;
260
+ gap: 0.15rem;
261
+ margin-top: 0.55rem;
262
+ opacity: 0;
263
+ transition: opacity 0.2s;
264
+ }
265
+ .msg:hover .actions,
266
+ .msg:focus-within .actions,
267
+ .msg .actions:focus-within { opacity: 1; }
268
+ .msg.ai .actions { margin-left: -0.35rem; }
269
+ .msg.ai .actions .msg-stats {
270
+ margin-left: auto;
271
+ font-size: 0.66rem;
272
+ color: var(--muted);
273
+ font-variant-numeric: tabular-nums;
274
+ padding-right: 0.2rem;
275
+ }
276
+ .msg.user .actions { justify-content: flex-end; }
277
+
278
+ /* Reply cut short (context window full, or the user hit Stop). Unlike
279
+ .actions this is always visible β€” a truncated answer must never look
280
+ like a finished one. */
281
+ .msg.ai .truncation {
282
+ display: flex;
283
+ align-items: center;
284
+ gap: 0.6rem;
285
+ flex-wrap: wrap;
286
+ margin-top: 0.7rem;
287
+ padding: 0.45rem 0.7rem;
288
+ border: 1px solid var(--border-2);
289
+ border-left: 2px solid var(--accent-2);
290
+ border-radius: 8px;
291
+ background: var(--panel-2);
292
+ font-size: 0.72rem;
293
+ line-height: 1.45;
294
+ color: var(--text-dim);
295
+ }
296
+ .truncation-btn {
297
+ padding: 3px 11px;
298
+ border-radius: 6px;
299
+ font-size: 0.7rem;
300
+ font-weight: 600;
301
+ color: #000;
302
+ background: var(--accent);
303
+ }
304
+ .truncation-btn:hover { background: var(--accent-2); }
305
+ .truncation-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
306
+ .action-btn {
307
+ display: inline-flex;
308
+ align-items: center;
309
+ gap: 4px;
310
+ padding: 4px 8px;
311
+ border-radius: 6px;
312
+ font-size: 0.7rem;
313
+ color: var(--muted);
314
+ transition: background 0.15s, color 0.15s;
315
+ }
316
+ .action-btn:hover { background: var(--panel-2); color: var(--text); }
317
+ .action-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
318
+ .action-btn.copied { color: var(--accent); }
319
+ .action-btn .icon { width: 13px; height: 13px; }
320
+
321
+ /* ─────────── Scroll-to-bottom FAB ─────────── */
322
+ .scroll-fab {
323
+ position: absolute;
324
+ bottom: 1rem;
325
+ left: 50%;
326
+ transform: translateX(-50%);
327
+ width: 34px;
328
+ height: 34px;
329
+ border-radius: 50%;
330
+ background: var(--panel-2);
331
+ border: 1px solid var(--border-2);
332
+ display: flex;
333
+ align-items: center;
334
+ justify-content: center;
335
+ color: var(--text-dim);
336
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
337
+ opacity: 0;
338
+ pointer-events: none;
339
+ transition: opacity 0.2s, transform 0.2s;
340
+ z-index: 5;
341
+ }
342
+ .scroll-fab.visible { opacity: 1; pointer-events: auto; visibility: visible; }
343
+ .scroll-fab:hover { background: var(--panel); color: var(--text); }
344
+
345
+ /* ─────────── Composer ─────────── */
346
+ .composer-wrap {
347
+ padding: 0.75rem 1rem 1rem;
348
+ border-top: 1px solid var(--border);
349
+ background: var(--bg);
350
+ }
351
+ .composer {
352
+ max-width: var(--composer-max);
353
+ margin: 0 auto;
354
+ display: flex;
355
+ align-items: flex-end;
356
+ gap: 0.5rem;
357
+ background: var(--panel);
358
+ border: 1px solid var(--border-2);
359
+ border-radius: 14px;
360
+ padding: 0.5rem 0.5rem 0.5rem 0.9rem;
361
+ transition: border-color 0.15s, background 0.15s;
362
+ }
363
+ /* border-color used to restate the UNFOCUSED border β€” a focus style that
364
+ cancelled itself, leaving a 1.07:1 background shift as the only cue on
365
+ the product's main control. The accent is the ring every other control
366
+ already uses. */
367
+ .composer:focus-within {
368
+ border-color: var(--accent);
369
+ background: var(--panel-2);
370
+ }
371
+ .composer textarea {
372
+ flex: 1;
373
+ background: none;
374
+ border: none;
375
+ outline: none;
376
+ color: var(--text);
377
+ font: inherit;
378
+ resize: none;
379
+ padding: 0.4rem 0;
380
+ max-height: 200px;
381
+ line-height: 1.5;
382
+ }
383
+ .composer textarea::placeholder { color: var(--muted); }
384
+ .composer textarea:disabled { cursor: not-allowed; }
385
+
386
+ .composer-btn {
387
+ display: flex;
388
+ align-items: center;
389
+ justify-content: center;
390
+ width: 34px;
391
+ height: 34px;
392
+ border-radius: 10px;
393
+ transition: background 0.15s, color 0.15s, opacity 0.15s;
394
+ flex-shrink: 0;
395
+ }
396
+ .composer-btn[hidden] { display: none; }
397
+ .composer-btn[data-variant="send"] {
398
+ background: var(--accent);
399
+ color: #000;
400
+ }
401
+ .composer-btn[data-variant="send"]:hover:not(:disabled) { background: var(--accent-2); }
402
+ .composer-btn[data-variant="send"]:disabled {
403
+ background: var(--border);
404
+ color: var(--muted);
405
+ cursor: not-allowed;
406
+ }
407
+ .composer-btn[data-variant="stop"] {
408
+ background: var(--panel-2);
409
+ color: var(--text);
410
+ border: 1px solid var(--border-2);
411
+ }
412
+ .composer-btn[data-variant="stop"]:hover { background: #222228; }
413
+
414
+ .composer-hint {
415
+ max-width: var(--composer-max);
416
+ margin: 0.4rem auto 0;
417
+ text-align: center;
418
+ font-size: 0.68rem;
419
+ color: var(--muted);
420
+ }
421
+ .composer-hint kbd {
422
+ background: var(--panel-2);
423
+ border: 1px solid var(--border);
424
+ padding: 1px 5px;
425
+ border-radius: 4px;
426
+ font-family: var(--mono);
427
+ font-size: 0.62rem;
428
+ }
429
+
430
+
431
+ /* ─────────── The Canvas (model-written code, actually run) ─────────── */
432
+ /* Run button beside copy; the lang pill pushes both right so the pair
433
+ stays grouped even though .code-head is space-between. */
434
+ .msg.ai .body .code-head .code-lang { margin-right: auto; }
435
+ .msg.ai .body .code-head { gap: 6px; }
436
+ .msg.ai .body .code-run {
437
+ display: inline-flex;
438
+ align-items: center;
439
+ gap: 4px;
440
+ padding: 3px 8px;
441
+ border-radius: 6px;
442
+ color: var(--accent);
443
+ font-size: 0.68rem;
444
+ font-weight: 600;
445
+ transition: background 0.15s, color 0.15s;
446
+ }
447
+ .msg.ai .body .code-run:hover { background: var(--accent-tint); }
448
+ .msg.ai .body .code-run .icon { width: 11px; height: 11px; stroke-width: 2.3; }
449
+
450
+ #code-canvas {
451
+ position: fixed;
452
+ inset: 0;
453
+ z-index: 120;
454
+ display: flex;
455
+ align-items: center;
456
+ justify-content: center;
457
+ padding: 4vh 4vw;
458
+ background: rgba(5, 7, 13, 0.72);
459
+ backdrop-filter: blur(5px);
460
+ }
461
+ .canvas-frame {
462
+ width: min(940px, 100%);
463
+ height: min(660px, 100%);
464
+ display: flex;
465
+ flex-direction: column;
466
+ background: var(--panel);
467
+ border: 1px solid var(--border-2);
468
+ border-radius: 12px;
469
+ overflow: hidden;
470
+ box-shadow: 0 24px 80px rgba(0, 0, 0, 0.6);
471
+ }
472
+ .canvas-head {
473
+ display: flex;
474
+ align-items: center;
475
+ justify-content: space-between;
476
+ padding: 8px 10px 8px 14px;
477
+ border-bottom: 1px solid var(--border);
478
+ font-size: 0.7rem;
479
+ color: var(--muted);
480
+ text-transform: uppercase;
481
+ letter-spacing: 0.08em;
482
+ }
483
+ .canvas-close {
484
+ padding: 3px 9px;
485
+ border-radius: 6px;
486
+ color: var(--text-dim);
487
+ font-size: 0.7rem;
488
+ transition: background 0.15s, color 0.15s;
489
+ }
490
+ .canvas-close:hover { background: var(--panel-2); color: var(--text); }
491
+ /* The iframe ground is white on purpose: model-written pages assume a
492
+ default (light) document, and an unstyled preview should look like one. */
493
+ .canvas-view { flex: 1; width: 100%; border: 0; background: #fff; }
494
+
495
+ /* ─────────── Command feedback + reply settle ─────────── */
496
+ /* A quiet system line in the message column (slash-command feedback) β€”
497
+ styled as stage direction, not as a conversation turn. */
498
+ .sys-note {
499
+ margin: 0.6rem auto 1rem;
500
+ max-width: 80%;
501
+ text-align: center;
502
+ font-family: var(--mono);
503
+ font-size: 0.68rem;
504
+ letter-spacing: 0.04em;
505
+ color: var(--muted);
506
+ }
507
+ /* A finished reply settles with a brief accent wash. */
508
+ .msg.ai.msg-done { animation: msgDone 0.8s ease; border-radius: 10px; }
509
+ @keyframes msgDone {
510
+ 0% { background: var(--accent-tint); }
511
+ 100% { background: transparent; }
512
+ }
513
+ @media (prefers-reduced-motion: reduce) { .msg.ai.msg-done { animation: none; } }
514
+
515
+ /* Reduced motion: the message surface holds still too β€” instant scroll,
516
+ no entrance fade, steady cursor and thinking dots (they stay VISIBLE;
517
+ only the movement goes). */
518
+ @media (prefers-reduced-motion: reduce) {
519
+ .chat-main { scroll-behavior: auto; }
520
+ .msg { animation: none; }
521
+ .msg.ai .cursor { animation: none; }
522
+ .thinking span { animation: none; opacity: 0.6; }
523
+ }
docs.html ADDED
@@ -0,0 +1,1316 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <title>Docs β€” Zero-TVM</title>
7
+ <meta name="description" content="Zero-TVM docs: shader catalog, decode pipeline, weight loader, URL flags, benchmarks. LLMs up to a 35B MoE in the browser on 10 hand-written WGSL kernel roles.">
8
+ <meta name="theme-color" content="#0a0a0c">
9
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml">
10
+ <meta property="og:type" content="article">
11
+ <meta property="og:url" content="https://zerotvm.com/docs.html">
12
+ <meta property="og:title" content="Docs β€” Zero-TVM">
13
+ <meta property="og:description" content="Zero-TVM docs: shader catalog, decode pipeline, weight loader, URL flags, benchmarks. LLMs up to a 35B MoE in the browser on 10 hand-written WGSL kernel roles.">
14
+ <meta property="og:image" content="https://zerotvm.com/og.png">
15
+ <meta name="twitter:card" content="summary_large_image">
16
+ <meta name="twitter:title" content="Docs β€” Zero-TVM">
17
+ <meta name="twitter:description" content="Zero-TVM docs: shader catalog, decode pipeline, weight loader, URL flags, benchmarks. LLMs up to a 35B MoE in the browser on 10 hand-written WGSL kernel roles.">
18
+ <meta name="twitter:image" content="https://zerotvm.com/og.png">
19
+ <style>
20
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
21
+
22
+ :root {
23
+ --bg: #0a0a0c;
24
+ --surface: #111114;
25
+ --surface2: #18181c;
26
+ --border: #1e1e24;
27
+ --accent: #6c63ff;
28
+ --green: #00d4aa;
29
+ --yellow: #f5c542;
30
+ --red: #ff5f57;
31
+ --text: #e8e8ec;
32
+ --muted: #666672;
33
+ --dim: #3a3a44;
34
+ --sidebar-w: 260px;
35
+ --topbar-h: 52px;
36
+ --code-bg: #0d0d10;
37
+ }
38
+
39
+ html { scroll-behavior: smooth; }
40
+
41
+ body {
42
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
43
+ background: var(--bg);
44
+ color: var(--text);
45
+ line-height: 1.65;
46
+ display: flex;
47
+ flex-direction: column;
48
+ min-height: 100vh;
49
+ }
50
+
51
+ /* ── TOPBAR ── */
52
+ .topbar {
53
+ height: var(--topbar-h);
54
+ background: var(--surface);
55
+ border-bottom: 1px solid var(--border);
56
+ display: flex;
57
+ align-items: center;
58
+ padding: 0 1.5rem;
59
+ gap: 1.5rem;
60
+ position: fixed;
61
+ top: 0; left: 0; right: 0;
62
+ z-index: 100;
63
+ }
64
+
65
+ .logo {
66
+ font-weight: 800;
67
+ font-size: 0.95rem;
68
+ letter-spacing: -0.03em;
69
+ color: #fff;
70
+ text-decoration: none;
71
+ }
72
+ .logo span { color: var(--accent); }
73
+
74
+ .topbar-sep { color: var(--border); }
75
+ .topbar-title { font-size: 0.82rem; color: var(--muted); }
76
+
77
+ .topbar-nav {
78
+ margin-left: auto;
79
+ display: flex;
80
+ gap: 1.25rem;
81
+ align-items: center;
82
+ }
83
+
84
+ .topbar-nav a {
85
+ font-size: 0.8rem;
86
+ color: var(--muted);
87
+ text-decoration: none;
88
+ transition: color 0.15s;
89
+ }
90
+ .topbar-nav a:hover { color: var(--text); }
91
+
92
+ .topbar-cta {
93
+ background: var(--accent);
94
+ color: #fff !important;
95
+ padding: 0.3rem 0.85rem;
96
+ border-radius: 7px;
97
+ font-weight: 600 !important;
98
+ }
99
+
100
+ /* ── LAYOUT ── */
101
+ .layout {
102
+ display: flex;
103
+ flex: 1;
104
+ padding-top: var(--topbar-h);
105
+ }
106
+
107
+ /* ── SIDEBAR ── */
108
+ aside {
109
+ width: var(--sidebar-w);
110
+ min-width: var(--sidebar-w);
111
+ background: var(--surface);
112
+ border-right: 1px solid var(--border);
113
+ position: fixed;
114
+ top: var(--topbar-h);
115
+ bottom: 0;
116
+ left: 0;
117
+ overflow-y: auto;
118
+ padding: 1.5rem 0;
119
+ }
120
+
121
+ aside::-webkit-scrollbar { width: 4px; }
122
+ aside::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
123
+
124
+ .sidebar-section {
125
+ padding: 0 1rem 1rem;
126
+ }
127
+
128
+ .sidebar-section-label {
129
+ font-size: 0.6rem;
130
+ font-weight: 700;
131
+ letter-spacing: 0.1em;
132
+ text-transform: uppercase;
133
+ color: var(--muted);
134
+ padding: 0.5rem 0.5rem 0.4rem;
135
+ display: block;
136
+ }
137
+
138
+ .sidebar-link {
139
+ display: block;
140
+ padding: 0.35rem 0.5rem;
141
+ font-size: 0.78rem;
142
+ color: var(--muted);
143
+ text-decoration: none;
144
+ border-radius: 6px;
145
+ transition: background 0.1s, color 0.1s;
146
+ border-left: 2px solid transparent;
147
+ }
148
+
149
+ .sidebar-link:hover { color: var(--text); background: var(--surface2); }
150
+ .sidebar-link.active { color: var(--accent); border-left-color: var(--accent); background: #6c63ff0d; }
151
+ .sidebar-link.sub { padding-left: 1.25rem; font-size: 0.73rem; }
152
+
153
+ /* ── MAIN CONTENT ── */
154
+ main {
155
+ margin-left: var(--sidebar-w);
156
+ flex: 1;
157
+ padding: 3.5rem 3rem 6rem;
158
+ max-width: 900px;
159
+ }
160
+
161
+ /* ── TYPOGRAPHY ── */
162
+ h1 {
163
+ font-size: 2rem;
164
+ font-weight: 800;
165
+ letter-spacing: -0.03em;
166
+ margin-bottom: 0.5rem;
167
+ line-height: 1.2;
168
+ }
169
+
170
+ h2 {
171
+ font-size: 1.35rem;
172
+ font-weight: 700;
173
+ letter-spacing: -0.02em;
174
+ margin-top: 3rem;
175
+ margin-bottom: 0.75rem;
176
+ padding-top: 2rem;
177
+ border-top: 1px solid var(--border);
178
+ scroll-margin-top: calc(var(--topbar-h) + 1rem);
179
+ }
180
+
181
+ h2:first-of-type { border-top: none; margin-top: 1.5rem; padding-top: 0; }
182
+
183
+ h3 {
184
+ font-size: 1rem;
185
+ font-weight: 700;
186
+ margin-top: 1.75rem;
187
+ margin-bottom: 0.5rem;
188
+ color: var(--text);
189
+ scroll-margin-top: calc(var(--topbar-h) + 1rem);
190
+ }
191
+
192
+ h4 {
193
+ font-size: 0.88rem;
194
+ font-weight: 700;
195
+ margin-top: 1.25rem;
196
+ margin-bottom: 0.35rem;
197
+ color: var(--muted);
198
+ text-transform: uppercase;
199
+ letter-spacing: 0.05em;
200
+ }
201
+
202
+ p {
203
+ font-size: 0.9rem;
204
+ color: #b0b0bc;
205
+ margin-bottom: 1rem;
206
+ line-height: 1.75;
207
+ }
208
+
209
+ a { color: var(--accent); text-decoration: none; }
210
+ a:hover { text-decoration: underline; }
211
+
212
+ ul, ol {
213
+ margin: 0.5rem 0 1rem 1.5rem;
214
+ font-size: 0.88rem;
215
+ color: #b0b0bc;
216
+ line-height: 1.8;
217
+ }
218
+
219
+ li { margin-bottom: 0.2rem; }
220
+
221
+ strong { color: var(--text); font-weight: 600; }
222
+ em { color: var(--yellow); font-style: normal; }
223
+
224
+ /* ── LEAD / SUBTITLE ── */
225
+ .lead {
226
+ font-size: 1.05rem;
227
+ color: var(--muted);
228
+ margin-bottom: 2rem;
229
+ line-height: 1.7;
230
+ }
231
+
232
+ /* ── PILL BADGES ── */
233
+ .pill {
234
+ display: inline-flex;
235
+ align-items: center;
236
+ gap: 0.25rem;
237
+ padding: 0.15rem 0.5rem;
238
+ border-radius: 999px;
239
+ font-size: 0.65rem;
240
+ font-weight: 600;
241
+ border: 1px solid;
242
+ vertical-align: middle;
243
+ }
244
+ .pill.green { background: #00d4aa12; color: var(--green); border-color: #00d4aa30; }
245
+ .pill.purple { background: #6c63ff12; color: var(--accent); border-color: #6c63ff30; }
246
+ .pill.yellow { background: #f5c54212; color: var(--yellow); border-color: #f5c54230; }
247
+ .pill.red { background: #ff5f5712; color: var(--red); border-color: #ff5f5730; }
248
+
249
+ /* ── CODE ── */
250
+ code {
251
+ font-family: 'SF Mono', 'Fira Code', monospace;
252
+ font-size: 0.8em;
253
+ background: var(--surface2);
254
+ border: 1px solid var(--border);
255
+ padding: 0.1em 0.4em;
256
+ border-radius: 4px;
257
+ color: #c8c8e0;
258
+ }
259
+
260
+ pre {
261
+ background: var(--code-bg);
262
+ border: 1px solid var(--border);
263
+ border-radius: 10px;
264
+ padding: 1.25rem 1.5rem;
265
+ overflow-x: auto;
266
+ margin: 1rem 0 1.5rem;
267
+ font-family: 'SF Mono', 'Fira Code', monospace;
268
+ font-size: 0.78rem;
269
+ line-height: 1.65;
270
+ }
271
+
272
+ pre code {
273
+ background: none;
274
+ border: none;
275
+ padding: 0;
276
+ font-size: inherit;
277
+ color: #c8c8e0;
278
+ }
279
+
280
+ /* syntax highlight classes */
281
+ .kw { color: #c792ea; }
282
+ .fn { color: #82aaff; }
283
+ .str { color: #c3e88d; }
284
+ .num { color: #f78c6c; }
285
+ .cm { color: #546e7a; font-style: italic; }
286
+ .ty { color: #ffcb6b; }
287
+ .op { color: #89ddff; }
288
+
289
+ /* ── PRE HEADER (filename label) ── */
290
+ .code-block { margin: 1rem 0 1.5rem; }
291
+ .code-label {
292
+ background: var(--surface2);
293
+ border: 1px solid var(--border);
294
+ border-bottom: none;
295
+ border-radius: 8px 8px 0 0;
296
+ padding: 0.4rem 1rem;
297
+ font-family: 'SF Mono', 'Fira Code', monospace;
298
+ font-size: 0.7rem;
299
+ color: var(--muted);
300
+ display: flex;
301
+ align-items: center;
302
+ gap: 0.5rem;
303
+ }
304
+ .code-block pre { margin: 0; border-radius: 0 0 8px 8px; }
305
+
306
+ /* ── CALLOUTS ── */
307
+ .callout {
308
+ border-radius: 10px;
309
+ padding: 1rem 1.25rem;
310
+ margin: 1.25rem 0;
311
+ font-size: 0.85rem;
312
+ line-height: 1.65;
313
+ border-left: 3px solid;
314
+ }
315
+ .callout p { margin: 0; color: inherit; font-size: inherit; }
316
+ .callout.info { background: #6c63ff0d; border-color: var(--accent); color: #a0a0d0; }
317
+ .callout.tip { background: #00d4aa0d; border-color: var(--green); color: #80c8bc; }
318
+ .callout.warning { background: #f5c5420d; border-color: var(--yellow); color: #c0a060; }
319
+ .callout.danger { background: #ff5f570d; border-color: var(--red); color: #c08080; }
320
+
321
+ .callout-title { font-weight: 700; margin-bottom: 0.3rem; color: inherit; }
322
+
323
+ /* ── NARROW SCREENS ──
324
+ This page shipped with NO media query at all, behind a 260px fixed
325
+ sidebar. At 390px that left main a 130px border-box and, after its 3rem
326
+ side padding, about 34px of readable text β€” with the sidebar covering
327
+ two thirds of the screen. It is nav-linked from every page and sits in
328
+ the sitemap, so a shared link opened on a phone landed on that. */
329
+ @media (max-width: 900px) {
330
+ :root { --sidebar-w: 0px; }
331
+ aside {
332
+ position: static; width: auto; min-width: 0; height: auto;
333
+ border-right: 0; border-bottom: 1px solid var(--border);
334
+ max-height: 40vh; overflow-y: auto;
335
+ }
336
+ main { margin-left: 0; padding: 2rem 1.1rem 4rem; max-width: none; }
337
+ /* THE FLEX CONTEXT IS .layout, NOT body. The first version of this
338
+ query reblocked body β€” which was already stacked β€” and left .layout
339
+ a flex row, so the sidebar stayed a 124px side column of wrapped
340
+ links beside ~230px of text. Reviewed at 390px; this is the line
341
+ that actually stacks them. */
342
+ .layout { display: block; }
343
+ .topbar { padding: 0 0.8rem; gap: 0.8rem; }
344
+ }
345
+
346
+ /* ── TABLES ──
347
+ Nineteen of them are direct children of main with no wrapper. The
348
+ longest unbreakable cell is a weight name like
349
+ transformer.h.N.post_attention_layernorm.weight, which cannot shrink,
350
+ so on a phone the table pushed the BODY sideways rather than scrolling
351
+ inside itself. Each table gets its own scroll container instead. */
352
+ .table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; margin: 1rem 0; }
353
+ table {
354
+ width: 100%;
355
+ border-collapse: collapse;
356
+ font-size: 0.82rem;
357
+ margin: 1rem 0 1.5rem;
358
+ border: 1px solid var(--border);
359
+ border-radius: 8px;
360
+ overflow: hidden;
361
+ }
362
+ th {
363
+ background: var(--surface2);
364
+ padding: 0.65rem 1rem;
365
+ text-align: left;
366
+ font-weight: 600;
367
+ color: var(--text);
368
+ border-bottom: 1px solid var(--border);
369
+ }
370
+ td {
371
+ padding: 0.6rem 1rem;
372
+ color: #b0b0bc;
373
+ border-bottom: 1px solid var(--border);
374
+ vertical-align: middle;
375
+ }
376
+ tr:last-child td { border-bottom: none; }
377
+ tr:hover td { background: #ffffff03; }
378
+
379
+ /* ── SHADER CARD ── */
380
+ .shader-doc-card {
381
+ background: var(--surface);
382
+ border: 1px solid var(--border);
383
+ border-radius: 12px;
384
+ padding: 1.25rem 1.5rem;
385
+ margin: 1.25rem 0;
386
+ }
387
+
388
+ .shader-doc-card h4 {
389
+ font-size: 0.88rem;
390
+ font-weight: 700;
391
+ color: var(--text);
392
+ text-transform: none;
393
+ letter-spacing: 0;
394
+ margin-top: 0;
395
+ display: flex;
396
+ align-items: center;
397
+ gap: 0.65rem;
398
+ }
399
+
400
+ .shader-doc-card p { font-size: 0.82rem; color: var(--muted); margin: 0.4rem 0 0; }
401
+
402
+ .binding-table { margin: 0.75rem 0 0; }
403
+ .binding-table th, .binding-table td { padding: 0.4rem 0.75rem; font-size: 0.75rem; }
404
+
405
+ /* ── STEP FLOW ── */
406
+ .step-flow { display: flex; flex-direction: column; gap: 0; margin: 1rem 0 1.5rem; }
407
+
408
+ .step-row {
409
+ display: flex;
410
+ gap: 1rem;
411
+ align-items: flex-start;
412
+ }
413
+
414
+ .step-line {
415
+ display: flex;
416
+ flex-direction: column;
417
+ align-items: center;
418
+ flex-shrink: 0;
419
+ }
420
+
421
+ .step-dot {
422
+ width: 28px; height: 28px;
423
+ border-radius: 50%;
424
+ background: var(--accent);
425
+ color: #fff;
426
+ font-size: 0.68rem;
427
+ font-weight: 700;
428
+ display: flex;
429
+ align-items: center;
430
+ justify-content: center;
431
+ flex-shrink: 0;
432
+ }
433
+
434
+ .step-line-seg {
435
+ width: 2px;
436
+ flex: 1;
437
+ background: var(--border);
438
+ margin: 4px 0;
439
+ min-height: 20px;
440
+ }
441
+
442
+ .step-content { padding: 0.05rem 0 1.25rem; }
443
+ .step-content h5 { font-size: 0.88rem; font-weight: 700; color: var(--text); margin-bottom: 0.25rem; }
444
+ .step-content p { font-size: 0.8rem; color: var(--muted); margin: 0; }
445
+
446
+ /* ── DIVIDER ── */
447
+ hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
448
+
449
+ /* ── SCROLL MARGIN ── */
450
+ [id] { scroll-margin-top: calc(var(--topbar-h) + 1.5rem); }
451
+ </style>
452
+ </head>
453
+ <body>
454
+
455
+ <!-- ── TOPBAR ── -->
456
+ <div class="topbar">
457
+ <a href="/" class="logo">Zero<span>TVM</span></a>
458
+ <span class="topbar-sep">/</span>
459
+ <span class="topbar-title">Documentation</span>
460
+ <nav class="topbar-nav">
461
+ <a href="/">Home</a>
462
+ <a href="zero-tvm.html" class="topbar-cta">Open Chat β†’</a>
463
+ </nav>
464
+ </div>
465
+
466
+ <div class="layout">
467
+
468
+ <!-- ── SIDEBAR ── -->
469
+ <aside>
470
+ <div class="sidebar-section">
471
+ <span class="sidebar-section-label">Getting Started</span>
472
+ <a href="#intro" class="sidebar-link active">Introduction</a>
473
+ <a href="#quickstart" class="sidebar-link">Quick start</a>
474
+ <a href="#how-it-works" class="sidebar-link">How it works</a>
475
+ </div>
476
+
477
+ <div class="sidebar-section">
478
+ <span class="sidebar-section-label">Architecture</span>
479
+ <a href="#overview" class="sidebar-link">Overview</a>
480
+ <a href="#weight-loader" class="sidebar-link">Weight loader</a>
481
+ <a href="#tokenizer" class="sidebar-link">Tokenizer</a>
482
+ <a href="#kv-cache" class="sidebar-link">KV cache</a>
483
+ <a href="#decode-loop" class="sidebar-link">Decode loop</a>
484
+ <a href="#ping-pong" class="sidebar-link">Ping-pong buffers</a>
485
+ </div>
486
+
487
+ <div class="sidebar-section">
488
+ <span class="sidebar-section-label">WGSL Kernel Roles</span>
489
+ <a href="#shaders" class="sidebar-link">Overview</a>
490
+ <a href="#shader-embedding" class="sidebar-link sub">1 Β· Embedding</a>
491
+ <a href="#shader-rmsnorm" class="sidebar-link sub">2 Β· RMSNorm</a>
492
+ <a href="#shader-qkv" class="sidebar-link sub">3 Β· QKV+RoPE+KV (fused)</a>
493
+ <a href="#shader-attention" class="sidebar-link sub">4 Β· Paged Attention</a>
494
+ <a href="#shader-oproj" class="sidebar-link sub">5 Β· int4 Matmul</a>
495
+ <a href="#shader-ffn" class="sidebar-link sub">6 Β· Fused FFN</a>
496
+ <a href="#shader-addnorm" class="sidebar-link sub">7 Β· Add + RMSNorm</a>
497
+ <a href="#shader-kvappend" class="sidebar-link sub">8 Β· KV Append (prefill)</a>
498
+ <a href="#shader-rope" class="sidebar-link sub">9 Β· RoPE (prefill)</a>
499
+ <a href="#shader-lmhead" class="sidebar-link sub">10 Β· Argmax</a>
500
+ </div>
501
+
502
+ <div class="sidebar-section">
503
+ <span class="sidebar-section-label">Model</span>
504
+ <a href="#phi3" class="sidebar-link">Phi-3 constants</a>
505
+ <a href="#quantization" class="sidebar-link">Q4F16 format</a>
506
+ <a href="#weight-loader" class="sidebar-link">Weight naming</a>
507
+ <a href="#qwen3" class="sidebar-link">Qwen3-4B port</a>
508
+ <a href="#qwen35" class="sidebar-link">Qwen3.5-4B hybrid</a>
509
+ <a href="#qwen36" class="sidebar-link">Qwen3.6 MoE</a>
510
+ </div>
511
+
512
+ <div class="sidebar-section">
513
+ <span class="sidebar-section-label">Extending</span>
514
+ <a href="#porting" class="sidebar-link">Port to Phi-4 / Qwen3</a>
515
+ <a href="#local-weights" class="sidebar-link">Local weight serving</a>
516
+ <a href="#debugging" class="sidebar-link">Debugging tips</a>
517
+ </div>
518
+
519
+ <div class="sidebar-section">
520
+ <span class="sidebar-section-label">Reference</span>
521
+ <a href="#bugs-fixed" class="sidebar-link">Bugs we fixed</a>
522
+ <a href="#vs-webllm" class="sidebar-link">vs WebLLM</a>
523
+ </div>
524
+ </aside>
525
+
526
+ <!-- ── MAIN ── -->
527
+ <main>
528
+
529
+ <!-- INTRO -->
530
+ <div id="intro">
531
+ <h1>Zero TVM Documentation</h1>
532
+ <p class="lead">
533
+ A complete LLM inference engine in the browser β€” <strong>no WebLLM, no TVM, no ONNX, no WASM runtime</strong>.
534
+ 10 hand-written WGSL kernel roles, a BPE tokenizer, and raw WebGPU.
535
+ Measured +16.0% on total wall-clock throughput and +31.4% on decode against WebLLM's TVM-autotuned
536
+ kernels, identical weights, same session (M2 Max, 2026-07-30).
537
+ </p>
538
+
539
+ <div style="display:flex;gap:0.5rem;flex-wrap:wrap;margin-bottom:1.5rem;">
540
+ <span class="pill green">WebGPU</span>
541
+ <span class="pill purple">10 kernel roles</span>
542
+ <span class="pill yellow">Q4F16_1</span>
543
+ <span class="pill green">Phi-3-mini 3.8B</span>
544
+ <span class="pill purple">Paged KV Cache</span>
545
+ <span class="pill green">0 dependencies</span>
546
+ </div>
547
+
548
+ <p>
549
+ This project implements the full Phi-3-mini-4k-instruct transformer forward pass using only
550
+ WebGPU compute shaders written by hand in WGSL. Weights are loaded directly from HuggingFace
551
+ in MLC Q4F16_1 format (cached in OPFS after the first load).
552
+ The tokenizer is implemented in pure TypeScript β€” no SentencePiece WASM.
553
+ </p>
554
+
555
+ <div class="callout tip">
556
+ <div class="callout-title">Why?</div>
557
+ <p>WebLLM compiles its kernels with TVM; transformers.js runs ONNX Runtime Web. Both work well. Neither is something you read β€” this is.
558
+ This project exists to show that you can understand <em>every single step</em> of a modern transformer
559
+ inference pipeline, written at the GPU level, in a browser tab.</p>
560
+ </div>
561
+ </div>
562
+
563
+ <!-- QUICKSTART -->
564
+ <h2 id="quickstart">Quick start</h2>
565
+
566
+ <p>No install. Just open the chat page. Weights load from your browser cache (if you've used WebLLM before) or download fresh from HuggingFace (~2 GB).</p>
567
+
568
+ <div class="step-flow">
569
+ <div class="step-row">
570
+ <div class="step-line"><div class="step-dot">1</div><div class="step-line-seg"></div></div>
571
+ <div class="step-content">
572
+ <h5>Open the chat</h5>
573
+ <p>Navigate to <code>zero-tvm.html</code>. WebGPU initializes automatically. Requires WebGPU with shader-f16: Chrome 113+ or Edge 113+. Safari did not enable WebGPU by default until 26, and the writable OPFS API the weight cache needs (createWritable) is missing before then. Subgroups are required for the MoE models.</p>
574
+ </div>
575
+ </div>
576
+ <div class="step-row">
577
+ <div class="step-line"><div class="step-dot">2</div><div class="step-line-seg"></div></div>
578
+ <div class="step-content">
579
+ <h5>Wait for weights</h5>
580
+ <p>First load downloads ~2 GB. Subsequent loads are instant from OPFS. On a small machine, <code>?ctx=</code> shrinks the context window and with it the KV allocation (~1.5 GB at Phi-3’s default 4K). Progress shown per shard in the log panel.</p>
581
+ </div>
582
+ </div>
583
+ <div class="step-row">
584
+ <div class="step-line"><div class="step-dot">3</div><div class="step-line-seg"></div></div>
585
+ <div class="step-content">
586
+ <h5>Chat</h5>
587
+ <p>Phi-3-mini runs at 69.6 tok/s total (83.1 tok/s decode) on M2 Max β€” +16.0% / +31.4% vs WebLLM on identical weights, same session. Your conversation never leaves the browser. No API calls during inference.</p>
588
+ </div>
589
+ </div>
590
+ <div class="step-row">
591
+ <div class="step-line"><div class="step-dot">4</div></div>
592
+ <div class="step-content">
593
+ <h5>(Optional) Cache weights locally</h5>
594
+ <p>Run <code>node scripts/download-weights.mjs</code> once to save the shards to <code>.weights-local/</code>. Subsequent loads are served from localhost at full disk speed.</p>
595
+ </div>
596
+ </div>
597
+ </div>
598
+
599
+ <div class="callout warning">
600
+ <div class="callout-title">GPU requirement</div>
601
+ <p>Requires a GPU with <code>shader-f16</code> WebGPU feature (f16 arithmetic). Most M-series Macs and recent NVIDIA/AMD GPUs support this. Intel integrated graphics may not.</p>
602
+ </div>
603
+
604
+ <!-- HOW IT WORKS -->
605
+ <h2 id="how-it-works">How it works</h2>
606
+
607
+ <p>The engine has three phases per generated token:</p>
608
+
609
+ <ol>
610
+ <li><strong>Prefill</strong> β€” process the prompt in chunks (per token on specs that cannot chunk), building up the KV cache</li>
611
+ <li><strong>First decode</strong> β€” the last prefill step produces the first generated token</li>
612
+ <li><strong>Decode loop</strong> β€” each step takes the previous token as input, runs the full forward pass with the KV cache providing attention context, produces the next token</li>
613
+ </ol>
614
+
615
+ <p>Each forward pass runs <strong>10 kernel roles</strong> through 32 transformer layers β€” <strong>260 dispatches per token</strong> on the default path β€” split-K attention has been on by default since 2026-07-27 and adds a combine dispatch per layer; <code>?splitk=0</code> runs the 228-dispatch reference chain β€” then reads one i32 token ID back from the GPU. For comparison, WebLLM's TVM-generated decode path fires 342 dispatches per token β€” around 11 distinct shaders on that path, of 85 captured across a whole session.</p>
616
+
617
+ <!-- ARCHITECTURE -->
618
+ <h2 id="overview">Architecture overview</h2>
619
+
620
+ <p>The engine’s main source files:</p>
621
+
622
+ <div class="table-scroll"><table>
623
+ <thead><tr><th>File</th><th>Role</th></tr></thead>
624
+ <tbody>
625
+ <tr><td><code>src/zero-tvm/chat.ts</code></td><td>Main decode engine + UI. Allocates buffers, builds bind groups, runs the decode loop.</td></tr>
626
+ <tr><td><code>src/zero-tvm/weight-loader.ts</code></td><td>Fetches ndarray-cache.json, downloads shards, uploads to GPU buffers.</td></tr>
627
+ <tr><td><code>src/zero-tvm/tokenizer.ts</code></td><td>BPE tokenizer: encode text β†’ token IDs, decode IDs β†’ text.</td></tr>
628
+ <tr><td><code>src/compiler/compiler.ts</code></td><td>Compiles every shader β€” the hand-written WGSL files plus the generated int4-matmul variants (tiled/subgroup/affine/MoE) β€” into <code>GPUComputePipeline</code> objects.</td></tr>
629
+ </tbody>
630
+ </table></div>
631
+
632
+ <!-- WEIGHT LOADER -->
633
+ <h2 id="weight-loader">Weight loader</h2>
634
+
635
+ <p>
636
+ Weights are stored in MLC's <strong>ndarray-cache.json</strong> format β€” an index file listing every parameter,
637
+ which shard binary it lives in, its byte offset, and byte size.
638
+ The loader reads this index then fetches each referenced shard.
639
+ </p>
640
+
641
+ <h3>Fetch priority</h3>
642
+ <ol>
643
+ <li><strong>OPFS</strong> β€” where this engine caches; a returning visitor loads from here</li>
644
+ <li><strong>Browser Cache API</strong> β€” read-only leftover from prior WebLLM sessions</li>
645
+ <li><strong>HuggingFace</strong> β€” direct HTTPS fetch from <code>huggingface.co/mlc-ai/Phi-3-mini-4k-instruct-q4f16_1-MLC</code></li>
646
+ </ol>
647
+
648
+ <div class="callout danger">
649
+ <div class="callout-title">Version mismatch pitfall</div>
650
+ <p>Do not mix a locally-downloaded <code>ndarray-cache.json</code> with shards from the browser cache.
651
+ The byte offsets in the index must match the shards exactly. If you download the index fresh but use
652
+ cached shards from an older model version, all weight slices will be wrong β†’ zero logits β†’ <code>&lt;unk&gt;</code> output.</p>
653
+ </div>
654
+
655
+ <h3>Parameter naming (MLC format)</h3>
656
+ <p>MLC uses non-standard parameter names. The actual names in the cache:</p>
657
+
658
+ <div class="table-scroll"><table>
659
+ <thead><tr><th>MLC name</th><th>Role</th></tr></thead>
660
+ <tbody>
661
+ <tr><td><code>transformer.embd.q_weight</code></td><td>Embedding weights (uint32 packed int4)</td></tr>
662
+ <tr><td><code>transformer.embd.q_scale</code></td><td>Embedding scales (f16)</td></tr>
663
+ <tr><td><code>transformer.norm.weight</code></td><td>Final RMSNorm gamma (after all layers)</td></tr>
664
+ <tr><td><code>lm_head.q_weight</code></td><td>LM head weights</td></tr>
665
+ <tr><td><code>transformer.h.N.ln.weight</code></td><td>Layer N input_layernorm (normGamma1)</td></tr>
666
+ <tr><td><code>transformer.h.N.post_attention_layernorm.weight</code></td><td>Layer N post-attention norm (normGamma2)</td></tr>
667
+ <tr><td><code>transformer.h.N.mixer.qkv_proj.q_weight</code></td><td>Layer N QKV projection weights</td></tr>
668
+ <tr><td><code>transformer.h.N.mixer.out_proj.q_weight</code></td><td>Layer N output projection weights</td></tr>
669
+ <tr><td><code>transformer.h.N.mlp.gate_up_proj.q_weight</code></td><td>Layer N FFN gate+up weights</td></tr>
670
+ <tr><td><code>transformer.h.N.mlp.down_proj.q_weight</code></td><td>Layer N FFN down weights</td></tr>
671
+ </tbody>
672
+ </table></div>
673
+
674
+ <!-- TOKENIZER -->
675
+ <h2 id="tokenizer">Tokenizer</h2>
676
+
677
+ <p>
678
+ A hand-written BPE tokenizer in TypeScript. No SentencePiece WASM, no HuggingFace tokenizers bundle.
679
+ Reads <code>tokenizer.json</code> directly.
680
+ </p>
681
+
682
+ <h3>Key steps</h3>
683
+ <ul>
684
+ <li><strong>Pre-tokenization</strong> β€” Metaspace: spaces become <code>▁</code>, words are split on whitespace</li>
685
+ <li><strong>BPE encoding</strong> β€” merge pairs by rank from the merge table in tokenizer.json</li>
686
+ <li><strong>Special tokens</strong> β€” <code>&lt;|system|&gt;</code>, <code>&lt;|user|&gt;</code>, <code>&lt;|assistant|&gt;</code>, <code>&lt;|end|&gt;</code></li>
687
+ <li><strong>Chat template</strong> β€” Phi-3 format applied by <code>buildChatPrompt()</code></li>
688
+ </ul>
689
+
690
+ <h3>Phi-3 chat template</h3>
691
+ <div class="code-block">
692
+ <div class="code-label">πŸ“„ prompt format</div>
693
+ <pre><code>&lt;|system|&gt;
694
+ You are a helpful assistant.&lt;|end|&gt;
695
+ &lt;|user|&gt;
696
+ What is the capital of Australia?&lt;|end|&gt;
697
+ &lt;|assistant|&gt;</code></pre>
698
+ </div>
699
+
700
+ <p>Stop tokens: <code>2</code> (EOS), <code>32000</code> (<code>&lt;|end|&gt;</code>), <code>32007</code> (<code>&lt;|endoftext|&gt;</code>).</p>
701
+
702
+ <!-- KV CACHE -->
703
+ <h2 id="kv-cache">KV cache</h2>
704
+
705
+ <p>
706
+ Uses a <strong>paged KV cache</strong> layout. Memory is divided into fixed-size pages (16 slots each)
707
+ and a page table maps logical positions to physical pages. This is <em>not</em> vLLM-style relocatable
708
+ blocks: K is RoPE&rsquo;d before the cache write on every path, so a cached page is valid only at the
709
+ positions it was written at. It is a prefix pool, and cannot become a block pool without moving RoPE.
710
+ </p>
711
+
712
+ <div class="table-scroll"><table>
713
+ <thead><tr><th>Parameter</th><th>Value</th><th>Notes</th></tr></thead>
714
+ <tbody>
715
+ <tr><td><code>PAGE_SIZE</code></td><td>16</td><td>slots per page</td></tr>
716
+ <tr><td><code>MAX_PAGES</code></td><td>257</td><td>β‰ˆ 4096 context tokens</td></tr>
717
+ <tr><td>Bytes per page</td><td>196,608</td><td>32 heads Γ— 16 slots Γ— 96 dims Γ— 2 (K+V) Γ— 2 bytes</td></tr>
718
+ <tr><td>Total KV buffer</td><td>~50 MB per layer</td><td>32 layers = ~1.6 GB</td></tr>
719
+ </tbody>
720
+ </table></div>
721
+
722
+ <p>Each layer has its own <code>GPUBuffer</code> for KV pages. The page table is a simple identity mapping (page i β†’ physical page i) for single-sequence inference.</p>
723
+
724
+ <!-- DECODE LOOP -->
725
+ <h2 id="decode-loop">Decode loop</h2>
726
+
727
+ <p>Each call to <code>decodeToken(tokenId, position)</code> submits one command encoder with the full forward pass:</p>
728
+
729
+ <div class="code-block">
730
+ <div class="code-label">src/zero-tvm/chat.ts Β· decodeToken()</div>
731
+ <pre><code><span class="cm">// Per-token GPU state written via writeBuffer</span>
732
+ B.inputIds ← [tokenId] <span class="cm">// i32</span>
733
+ B.posMap ← [position] <span class="cm">// i32</span>
734
+ B.pageIndptr ← [0, nnzPages] <span class="cm">// page range</span>
735
+ B.lengthInfo ← [position+1, 0, 0] <span class="cm">// seq length</span>
736
+
737
+ <span class="cm">// Forward pass (one command encoder)</span>
738
+ embedding(B.residual) <span class="cm">// token β†’ hidden state</span>
739
+ rmsNorm(B.hidden1, B.residual) <span class="cm">// initial norm</span>
740
+
741
+ <span class="kw">for</span> L <span class="op">in</span> <span class="num">0</span>..<span class="num">32</span>:
742
+ <span class="cm">// QKV matmul + RoPE + KV-append in ONE dispatch (M4 fusion)</span>
743
+ qkvFused(B.qOut, kvPages[L], B.hidden1)
744
+ attention(B.attnOut, B.qOut, kvPages[L])
745
+ int4Matmul(B.hidden2, B.attnOut) <span class="cm">// O projection</span>
746
+ addNorm(B.hidden2, resIn β†’ B.hidden1, resOut) <span class="cm">// residual + RMSNorm, ping-pong</span>
747
+ <span class="cm">// Gate + Up + SiLU + mul + Down in ONE dispatch</span>
748
+ fusedFfn(B.hidden2, B.hidden1)
749
+ addNorm(B.hidden2, resIn β†’ B.hidden1, resOut) <span class="cm">// residual + RMSNorm, ping-pong</span>
750
+
751
+ int4Matmul(B.logits, B.hidden1) <span class="cm">// LM head</span>
752
+ argmax(B.tokenOut, B.logits) <span class="cm">// β†’ next token ID</span></code></pre>
753
+ </div>
754
+
755
+ <!-- PING-PONG -->
756
+ <h2 id="ping-pong">Ping-pong residual buffers</h2>
757
+
758
+ <p>
759
+ WebGPU's validation rules forbid binding the same buffer as both <code>read</code> and <code>read_write</code>
760
+ in the same dispatch. The add_norm shader needs to <em>read</em> the old residual and <em>write</em> the new one.
761
+ </p>
762
+
763
+ <p>Solution: two residual buffers that alternate each dispatch.</p>
764
+
765
+ <div class="code-block">
766
+ <div class="code-label">ping-pong pattern</div>
767
+ <pre><code><span class="kw">let</span> resIn = B.residual <span class="cm">// ping (starts with embedding)</span>
768
+ <span class="kw">let</span> resOut = B.residual2 <span class="cm">// pong (uninitialized)</span>
769
+
770
+ <span class="cm">// Each add_norm:</span>
771
+ dispatch(addNorm, [delta, resIn, gamma, hidden1, resOut])
772
+ [resIn, resOut] = [resOut, resIn] <span class="cm">// swap β€” O(1), no GPU copy</span></code></pre>
773
+ </div>
774
+
775
+ <div class="callout info">
776
+ <p>The swap is just two JavaScript variable reassignments β€” no GPU buffer copy. Both buffers always exist on the GPU; we just change which one we tell the bind group to read vs write.</p>
777
+ </div>
778
+
779
+ <!-- SHADERS -->
780
+ <h2 id="shaders">WGSL Kernel Roles</h2>
781
+
782
+ <p>All shaders live in <code>src/compiler/shaders/</code>, implementing 10 distinct kernel roles. The rest are tiled and subgroup variants of the same role, selectable at runtime via URL flags. The compiler compiles them all at startup into <code>GPUComputePipeline</code> objects.</p>
783
+
784
+ <p>Binding convention: <code>@group(0)</code> always. Binding indices are <strong>zero-based</strong> and match the order you pass buffers to <code>bg(device, pipeline, [...bufs])</code>.</p>
785
+
786
+ <div class="callout tip">
787
+ <div class="callout-title">Decode vs prefill paths diverge</div>
788
+ <p>The decode loop uses the fused <code>qkv_fused</code> kernel (QKV matmul + RoPE + KV-append all in one dispatch). Prefill still uses separate <code>int4_matmul</code> + <code>rope</code> + <code>kv_append</code> dispatches because prefill processes many tokens at once, and the fusion win only lands for <code>ntoken=1</code>.</p>
789
+ </div>
790
+
791
+ <hr>
792
+
793
+ <!-- SHADER 1 -->
794
+ <div id="shader-embedding">
795
+ <div class="shader-doc-card">
796
+ <h4>πŸ”€ 1 Β· Embedding <span class="pill purple">embedding.wgsl</span></h4>
797
+ <p>Token ID lookup with Q4F16 dequantization. Each output element is dequantized from a packed int4 value: <code>(nibble - 7) Γ— scale</code>.</p>
798
+ <div class="table-scroll"><table class="binding-table">
799
+ <thead><tr><th>Binding</th><th>Type</th><th>Role</th></tr></thead>
800
+ <tbody>
801
+ <tr><td><code>@0</code></td><td><code>read_write f16[]</code></td><td>output hidden state</td></tr>
802
+ <tr><td><code>@1</code></td><td><code>read i32[]</code></td><td>input token IDs</td></tr>
803
+ <tr><td><code>@2</code></td><td><code>read f16[]</code></td><td>scales (group_size=32)</td></tr>
804
+ <tr><td><code>@3</code></td><td><code>read u32[]</code></td><td>packed weights (8 int4 per u32)</td></tr>
805
+ <tr><td><code>@4</code></td><td><code>uniform</code></td><td><code>{ seq_len, packGridDimX }</code></td></tr>
806
+ </tbody>
807
+ </table></div>
808
+ <p style="margin-top:0.5rem;"><strong>Dispatch:</strong> 12 workgroups Γ— 256 threads = 3072 output elements (D=3072)</p>
809
+ </div>
810
+ </div>
811
+
812
+ <!-- SHADER 2 -->
813
+ <div id="shader-rmsnorm">
814
+ <div class="shader-doc-card">
815
+ <h4>πŸ“ 2 Β· RMSNorm <span class="pill purple">rms_norm.wgsl</span></h4>
816
+ <p>Root mean square layer normalization. Computes <code>x / sqrt(mean(xΒ²) + Ξ΅) Γ— gamma</code>. Uses 256-thread tree reduction in workgroup shared memory.</p>
817
+ <div class="table-scroll"><table class="binding-table">
818
+ <thead><tr><th>Binding</th><th>Type</th><th>Role</th></tr></thead>
819
+ <tbody>
820
+ <tr><td><code>@0</code></td><td><code>read_write f16[]</code></td><td>normalized output</td></tr>
821
+ <tr><td><code>@1</code></td><td><code>read f16[]</code></td><td>input</td></tr>
822
+ <tr><td><code>@2</code></td><td><code>read f16[]</code></td><td>gamma weights</td></tr>
823
+ <tr><td><code>@3</code></td><td><code>uniform</code></td><td><code>{ packGridDimX }</code></td></tr>
824
+ </tbody>
825
+ </table></div>
826
+ <p style="margin-top:0.5rem;"><strong>Dispatch:</strong> 1 workgroup (one token, D=3072)</p>
827
+ </div>
828
+ </div>
829
+
830
+ <!-- SHADER 3 Β· QKV FUSED (decode) -->
831
+ <div id="shader-qkv">
832
+ <div class="shader-doc-card">
833
+ <h4>⚑ 3 · QKV + RoPE + KV-append (fused, decode) <span class="pill purple">qkv_fused.wgsl</span></h4>
834
+ <p>
835
+ The big M4 fusion. One dispatch replaces three on the decode path: the int4 QKV matmul, the RoPE rotation of Q and K, and the write of K/V into the paged KV cache. Each workgroup computes two output rows that form a RoPE pair (dim and dim+48 within the same head), rotates the pair in registers, and writes K/V straight into <code>kv_pages</code> β€” the intermediate <code>qkv</code> / <code>k_out</code> / <code>v_out</code> buffers from the pre-fusion path are skipped entirely.
836
+ </p>
837
+ <div class="table-scroll"><table class="binding-table">
838
+ <thead><tr><th>Binding</th><th>Type</th><th>Role</th></tr></thead>
839
+ <tbody>
840
+ <tr><td><code>@0</code></td><td><code>read_write f16[]</code></td><td>q_out [3072]</td></tr>
841
+ <tr><td><code>@1</code></td><td><code>read_write f16[]</code></td><td>kv_pages (paged KV cache)</td></tr>
842
+ <tr><td><code>@2</code></td><td><code>read f16[]</code></td><td>hidden [3072]</td></tr>
843
+ <tr><td><code>@3</code></td><td><code>read f16[]</code></td><td>scales [9216 Γ— 96]</td></tr>
844
+ <tr><td><code>@4</code></td><td><code>read u32[]</code></td><td>packed weights [9216 Γ— 384]</td></tr>
845
+ <tr><td><code>@5</code></td><td><code>read i32[]</code></td><td>position map</td></tr>
846
+ <tr><td><code>@6</code></td><td><code>uniform</code></td><td><code>{ position_map_elem_offset, pages_elem_offset, packGridDimX }</code></td></tr>
847
+ </tbody>
848
+ </table></div>
849
+ <p style="margin-top:0.5rem;"><strong>Dispatch:</strong> 4,608 workgroups (down from 9,216 matmul + 36 RoPE + 12 KV-append = 9,264 in the pre-fusion path). Decode-only; prefill still uses the 3-dispatch path (see shaders 8 and 9).</p>
850
+ </div>
851
+ </div>
852
+
853
+ <!-- SHADER 4 Β· PAGED ATTENTION -->
854
+ <div id="shader-attention">
855
+ <div class="shader-doc-card">
856
+ <h4>πŸ‘οΈ 4 Β· Paged Attention <span class="pill purple">attention.wgsl</span></h4>
857
+ <p>Multi-head attention over the paged KV cache. Reads K and V from pages, computes scaled dot-product attention with an online-softmax reduction in shared memory. Each workgroup handles one attention head.</p>
858
+ <div class="table-scroll"><table class="binding-table">
859
+ <thead><tr><th>Binding</th><th>Type</th><th>Role</th></tr></thead>
860
+ <tbody>
861
+ <tr><td><code>@0</code></td><td><code>read f16[]</code></td><td>Q [3072]</td></tr>
862
+ <tr><td><code>@1</code></td><td><code>read i32[]</code></td><td>page indptr</td></tr>
863
+ <tr><td><code>@2</code></td><td><code>read i32[]</code></td><td>page values (page table)</td></tr>
864
+ <tr><td><code>@3</code></td><td><code>read f16[]</code></td><td>KV pages</td></tr>
865
+ <tr><td><code>@4</code></td><td><code>read i32[]</code></td><td>length info</td></tr>
866
+ <tr><td><code>@5</code></td><td><code>read_write f16[]</code></td><td>attn output [3072]</td></tr>
867
+ <tr><td><code>@6</code></td><td><code>uniform</code></td><td>attention config (scale, pages)</td></tr>
868
+ </tbody>
869
+ </table></div>
870
+ <p style="margin-top:0.5rem;"><strong>Dispatch:</strong> 1 Γ— HEADS workgroups (1 Γ— 32). An <code>attention_int8.wgsl</code> variant reads an int8-quantized KV cache; enable via <code>?kv8=1</code>.</p>
871
+ </div>
872
+ </div>
873
+
874
+ <!-- SHADER 5 Β· INT4 MATMUL -->
875
+ <div id="shader-oproj">
876
+ <div class="shader-doc-card">
877
+ <h4>βœ–οΈ 5 Β· int4 Matmul (output projection + LM head) <span class="pill purple">int4_matmul.wgsl</span></h4>
878
+ <p>General-purpose dequantize-on-the-fly int4 Γ— f16 matmul. Used for the attention <em>output projection</em> (3072 β†’ 3072) and the <em>LM head</em> (3072 β†’ 32064). Weights are Q4F16_1: <code>N</code> output rows Γ— 384 u32 columns (each u32 = 8 int4 values = 32 elements, group_size=32). The tiled / subgroup / vec4 variants are emitted by one generator beside it (<code>int4_matmul.gen.ts</code>); the runtime picks one via the <code>?matmul=</code> URL flag.</p>
879
+ <div class="table-scroll"><table class="binding-table">
880
+ <thead><tr><th>Uniform field</th><th>Value</th></tr></thead>
881
+ <tbody>
882
+ <tr><td><code>K_groups</code></td><td>384 (= input_dim / 8)</td></tr>
883
+ <tr><td><code>scale_stride</code></td><td>96 (= input_dim / group_size)</td></tr>
884
+ <tr><td><code>N</code></td><td>3072 (o-proj) or 32064 (lm_head)</td></tr>
885
+ </tbody>
886
+ </table></div>
887
+ <p style="margin-top:0.5rem;"><strong>Dispatch:</strong> <code>N</code> workgroups β€” 3,072 for o-proj, 32,064 for lm_head.</p>
888
+ </div>
889
+ </div>
890
+
891
+ <!-- SHADER 6 Β· FUSED FFN -->
892
+ <div id="shader-ffn">
893
+ <div class="shader-doc-card">
894
+ <h4>πŸ”€ 6 Β· Fused FFN (Gate Β· Up Β· SiLU Β· Mul Β· Down) <span class="pill purple">fused_ffn.wgsl</span></h4>
895
+ <p>
896
+ Gate and up projections (both int4 matmuls sharing the 16,384-row <code>gate_up_proj</code> weight matrix), the SiLU activation and the elementwise multiply, in one dispatch. The down projection back to 3,072 dims is its own <code>int4_matmul</code> dispatch β€” which is why a layer costs 7 dispatches, not 6.
897
+ </p>
898
+ <div class="table-scroll"><table class="binding-table">
899
+ <thead><tr><th>Binding</th><th>Type</th><th>Role</th></tr></thead>
900
+ <tbody>
901
+ <tr><td><code>@0</code></td><td><code>read_write f16[]</code></td><td>output [3072]</td></tr>
902
+ <tr><td><code>@1</code></td><td><code>read f16[]</code></td><td>input [3072]</td></tr>
903
+ <tr><td><code>@2</code></td><td><code>read f16[]</code></td><td>gate_up scales</td></tr>
904
+ <tr><td><code>@3</code></td><td><code>read u32[]</code></td><td>gate_up packed weights (16,384 Γ— 384)</td></tr>
905
+ <tr><td><code>@4</code></td><td><code>read f16[]</code></td><td>down_proj scales</td></tr>
906
+ <tr><td><code>@5</code></td><td><code>read u32[]</code></td><td>down_proj packed weights (3,072 Γ— 1,024)</td></tr>
907
+ <tr><td><code>@6</code></td><td><code>uniform</code></td><td>FFN config</td></tr>
908
+ </tbody>
909
+ </table></div>
910
+ <p style="margin-top:0.5rem;"><strong>Dispatch:</strong> 3,072 workgroups β€” one per output row of the down projection. A <code>fused_ffn_tiled_sg.wgsl</code> variant uses subgroup reductions; selectable via URL flag.</p>
911
+ </div>
912
+ </div>
913
+
914
+ <!-- SHADER 7 Β· ADD + RMSNORM -->
915
+ <div id="shader-addnorm">
916
+ <div class="shader-doc-card">
917
+ <h4>βž• 7 Β· Fused Add + RMSNorm <span class="pill purple">add_norm.wgsl</span></h4>
918
+ <p>Residual add + RMSNorm in one pass. Computes <code>residual_out = A + B</code>, then <code>output = RMSNorm(residual_out) Γ— gamma</code>. Used twice per layer (post-attention and post-FFN). Mirrors TVM's <code>fuse_add_norm_decode</code>.</p>
919
+ <div class="table-scroll"><table class="binding-table">
920
+ <thead><tr><th>Binding</th><th>Type</th><th>Role</th></tr></thead>
921
+ <tbody>
922
+ <tr><td><code>@0</code></td><td><code>read f16[]</code></td><td>A β€” the new contribution (O-proj or FFN-down output)</td></tr>
923
+ <tr><td><code>@1</code></td><td><code>read f16[]</code></td><td>B β€” the running residual (resIn)</td></tr>
924
+ <tr><td><code>@2</code></td><td><code>read f16[]</code></td><td>gamma β€” normalization weights</td></tr>
925
+ <tr><td><code>@3</code></td><td><code>read_write f16[]</code></td><td>normalized output (B.hidden1)</td></tr>
926
+ <tr><td><code>@4</code></td><td><code>read_write f16[]</code></td><td>new residual (resOut β€” ping-pong)</td></tr>
927
+ <tr><td><code>@5</code></td><td><code>uniform</code></td><td><code>{ packGridDimX }</code></td></tr>
928
+ </tbody>
929
+ </table></div>
930
+ <p style="margin-top:0.5rem;"><strong>Dispatch:</strong> 1 workgroup Β· 256 threads Β· 12 elements each = 3,072.</p>
931
+ </div>
932
+ </div>
933
+
934
+ <!-- SHADER 8 Β· KV APPEND (prefill) -->
935
+ <div id="shader-kvappend">
936
+ <div class="shader-doc-card">
937
+ <h4>πŸ’Ύ 8 Β· KV Append (prefill path) <span class="pill purple">kv_append.wgsl</span></h4>
938
+ <p>Writes K and V vectors into the paged KV cache at the correct slot for each position. On the decode path this work is folded into <code>qkv_fused</code>; on prefill it runs as a separate dispatch because prefill processes many tokens at once and the per-token fusion no longer pays off.</p>
939
+ <div class="table-scroll"><table class="binding-table">
940
+ <thead><tr><th>Binding</th><th>Type</th><th>Role</th></tr></thead>
941
+ <tbody>
942
+ <tr><td><code>@0</code></td><td><code>read f16[]</code></td><td>k_out [3072]</td></tr>
943
+ <tr><td><code>@1</code></td><td><code>read f16[]</code></td><td>v_out [3072]</td></tr>
944
+ <tr><td><code>@2</code></td><td><code>read_write f16[]</code></td><td>KV pages buffer</td></tr>
945
+ <tr><td><code>@3</code></td><td><code>read i32[]</code></td><td>position map</td></tr>
946
+ <tr><td><code>@4</code></td><td><code>uniform</code></td><td>page config</td></tr>
947
+ </tbody>
948
+ </table></div>
949
+ <p style="margin-top:0.5rem;"><strong>Dispatch:</strong> 12 workgroups per token (HEADS=32, HEAD_DIM=96).</p>
950
+ </div>
951
+ </div>
952
+
953
+ <!-- SHADER 9 Β· ROPE (prefill) -->
954
+ <div id="shader-rope">
955
+ <div class="shader-doc-card">
956
+ <h4>πŸŒ€ 9 Β· RoPE (prefill path) <span class="pill purple">rope.wgsl</span></h4>
957
+ <p>Rotary position embeddings applied to Q and K. Prefill-only β€” the decode path folds RoPE into <code>qkv_fused</code>. Splits the concatenated 9,216-dim QKV buffer into Q / K / V, rotates Q and K in place based on position, and copies V unchanged.</p>
958
+ <div class="table-scroll"><table class="binding-table">
959
+ <thead><tr><th>Binding</th><th>Type</th><th>Role</th></tr></thead>
960
+ <tbody>
961
+ <tr><td><code>@0</code></td><td><code>read_write f16[]</code></td><td>q_out [3072]</td></tr>
962
+ <tr><td><code>@1</code></td><td><code>read_write f16[]</code></td><td>k_out [3072]</td></tr>
963
+ <tr><td><code>@2</code></td><td><code>read_write f16[]</code></td><td>v_out [3072]</td></tr>
964
+ <tr><td><code>@3</code></td><td><code>read f16[]</code></td><td>qkv input [9216]</td></tr>
965
+ <tr><td><code>@4</code></td><td><code>read i32[]</code></td><td>position map</td></tr>
966
+ <tr><td><code>@5</code></td><td><code>uniform</code></td><td>RoPE config</td></tr>
967
+ </tbody>
968
+ </table></div>
969
+ <div class="callout danger" style="margin-top:0.75rem;">
970
+ <div class="callout-title">Critical binding order</div>
971
+ <p>The binding order <strong>must</strong> be <code>[q_out, k_out, v_out, qkv, posMap, uniform]</code>. Swapping these caused a garbage-output bug during development.</p>
972
+ </div>
973
+ <p><strong>Dispatch:</strong> 36 workgroups Γ— 256 threads = 9,216 = 3 Γ— 3,072.</p>
974
+ </div>
975
+ </div>
976
+
977
+ <!-- SHADER 10 Β· ARGMAX -->
978
+ <div id="shader-lmhead">
979
+ <div class="shader-doc-card">
980
+ <h4>🎯 10 · Argmax Sampler <span class="pill purple">argmax.wgsl</span></h4>
981
+ <p>
982
+ Parallel-reduction argmax over the 32,064-entry logit buffer produced by the LM-head <code>int4_matmul</code>. Replaces TVM's ~20-dispatch sampling chain (penalty β†’ softmax β†’ cumsum β†’ argsort β†’ gather β†’ …) with a single dispatch. Greedy decoding only; top-k / top-p not wired up yet.
983
+ </p>
984
+ <div class="table-scroll"><table class="binding-table">
985
+ <thead><tr><th>Binding</th><th>Type</th><th>Role</th></tr></thead>
986
+ <tbody>
987
+ <tr><td><code>@0</code></td><td><code>read f16[]</code></td><td>logits [32064]</td></tr>
988
+ <tr><td><code>@1</code></td><td><code>read_write i32[]</code></td><td>output token id [1]</td></tr>
989
+ </tbody>
990
+ </table></div>
991
+ <p style="margin-top:0.5rem;"><strong>Dispatch:</strong> 1 workgroup (tree reduction over 32,064 logits). An <code>argmax_sg.wgsl</code> subgroup variant is available.</p>
992
+ </div>
993
+ </div>
994
+
995
+ <!-- PHI-3 CONSTANTS -->
996
+ <h2 id="phi3">Phi-3 model constants</h2>
997
+
998
+ <div class="code-block">
999
+ <div class="code-label">src/compiler/compiler.ts</div>
1000
+ <pre><code><span class="kw">export const</span> PHI3 = {
1001
+ D: <span class="num">3072</span>, <span class="cm">// hidden dimension</span>
1002
+ HEADS: <span class="num">32</span>, <span class="cm">// attention heads</span>
1003
+ HEAD_DIM: <span class="num">96</span>, <span class="cm">// D / HEADS</span>
1004
+ LAYERS: <span class="num">32</span>, <span class="cm">// transformer layers</span>
1005
+ FFN: <span class="num">8192</span>, <span class="cm">// FFN intermediate dimension</span>
1006
+ VOCAB: <span class="num">32064</span>, <span class="cm">// vocabulary size</span>
1007
+ PAGE_SIZE:<span class="num">16</span>, <span class="cm">// KV cache slots per page</span>
1008
+ MAX_PAGES:<span class="num">257</span>, <span class="cm">// max pages (β‰ˆ 4096 context)</span>
1009
+ }</code></pre>
1010
+ </div>
1011
+
1012
+ <!-- Q4F16 -->
1013
+ <h2 id="quantization">Q4F16 quantization format</h2>
1014
+
1015
+ <p>
1016
+ MLC's Q4F16_1 format packs 8 int4 values into each <code>uint32</code>.
1017
+ Scales are stored as <code>float16</code> with <code>group_size=32</code> β€” one scale per 32 weights.
1018
+ </p>
1019
+
1020
+ <div class="code-block">
1021
+ <div class="code-label">Dequantization formula (from embedding.wgsl)</div>
1022
+ <pre><code><span class="cm">// Extract nibble for element i within a u32</span>
1023
+ <span class="kw">let</span> nibble = (packed_u32 >> (i * <span class="num">4</span>)) & <span class="num">0xF</span>;
1024
+
1025
+ <span class="cm">// Dequantize: center around 0, multiply by scale</span>
1026
+ <span class="kw">let</span> value = <span class="ty">f16</span>(i32(nibble) - <span class="num">7</span>) * scale;</code></pre>
1027
+ </div>
1028
+
1029
+ <p>Weight shapes in Q4F16 (for Phi-3-mini):</p>
1030
+
1031
+ <div class="table-scroll"><table>
1032
+ <thead><tr><th>Parameter</th><th>q_weight shape (u32)</th><th>q_scale shape (f16)</th></tr></thead>
1033
+ <tbody>
1034
+ <tr><td>Embedding</td><td>[32064, 384]</td><td>[32064, 96]</td></tr>
1035
+ <tr><td>QKV proj (per layer)</td><td>[9216, 384]</td><td>[9216, 96]</td></tr>
1036
+ <tr><td>O proj (per layer)</td><td>[3072, 384]</td><td>[3072, 96]</td></tr>
1037
+ <tr><td>Gate+Up FFN (per layer)</td><td>[16384, 384]</td><td>[16384, 96]</td></tr>
1038
+ <tr><td>Down FFN (per layer)</td><td>[3072, 1024]</td><td>[3072, 256]</td></tr>
1039
+ <tr><td>LM head</td><td>[32064, 384]</td><td>[32064, 96]</td></tr>
1040
+ </tbody>
1041
+ </table></div>
1042
+
1043
+ <!-- QWEN3 -->
1044
+ <h2 id="qwen3">Qwen3-4B (<code>?model=qwen3</code>)</h2>
1045
+
1046
+ <p>
1047
+ The engine is parameterized over a <code>ModelSpec</code>, and a v1 Qwen3-4B (q4f16_1) port ships alongside Phi-3.
1048
+ Append <code>?model=qwen3</code> to <code>zero-tvm.html</code> or <code>validate.html</code> β€” it works on the live
1049
+ site (weights stream from HuggingFace, ~2.3 GB), and <code>node scripts/download-weights.mjs --model qwen3</code>
1050
+ primes the local dev mirror. Phi-3 stays the default; all existing URLs keep their exact behavior.
1051
+ </p>
1052
+
1053
+ <p>What the port exercises that Phi-3 doesn't:</p>
1054
+
1055
+ <div class="table-scroll"><table>
1056
+ <thead><tr><th></th><th>Phi-3-mini (default)</th><th>Qwen3-4B</th></tr></thead>
1057
+ <tbody>
1058
+ <tr><td>Attention</td><td>MHA, 32/32 heads</td><td>GQA, 32 query heads over 8 KV heads</td></tr>
1059
+ <tr><td>QK-norm</td><td>none</td><td>per-head RMSNorm on Q and K between projection and RoPE</td></tr>
1060
+ <tr><td>Tokenizer</td><td>SentencePiece</td><td>byte-level BPE (Qwen2-style <code>tokenizer.json</code>)</td></tr>
1061
+ <tr><td>LM head</td><td>separate</td><td>tied β€” logits reuse the quantized embedding matrix (151,936 vocab)</td></tr>
1062
+ <tr><td>Decode path</td><td>fused, 7 dispatches/layer</td><td>unfused QKV + fused qk_norm+RoPE+append, 8 dispatches/layer</td></tr>
1063
+ </tbody>
1064
+ </table></div>
1065
+
1066
+ <p>
1067
+ Measured 2026-07-30 on an Apple M2 Max under the corrected protocol (same session, identical local weight
1068
+ bytes, both engines paying a full prefill on every run): Zero-TVM <strong>59.85 tok/s total</strong>
1069
+ (TTFT 453 ms, decode 75.49) vs WebLLM 0.2.84's prebuilt Qwen3-4B at <strong>45.46 tok/s total</strong>
1070
+ (self-reported decode 47.77) β€” <strong>+31.7% total, +58.0% decode</strong>. Unlike the Phi-3 headline,
1071
+ these figures are static dated text, not synced from <code>bench/results.json</code>; the machine-readable
1072
+ record is <code>bench/results/qwen3-4b.json</code>.
1073
+ </p>
1074
+
1075
+ <div class="callout warning">
1076
+ <div class="callout-title">Read it cautiously β€” one pair, one machine, and the previous number was withdrawn</div>
1077
+ <p>The <strong>"75.74 vs 43.75, +73%" pair published on 2026-07-29 is withdrawn.</strong> It was measured after cross-turn prefix reuse shipped (PR #24) but before the bench harness was fixed to reset it, so the Zero-TVM half prefilled a single token per run while the WebLLM half prefilled the whole prompt β€” not like-for-like. Full writeup at the top of BENCH.md. The engine work itself is unchanged and its Zero-TVM-vs-Zero-TVM A/Bs still stand: QK-norm must run between the QKV matmul and RoPE, which is incompatible with the fused QKV+RoPE+KV-append kernel β€” the QKV matmul stays a separate dispatch β€” but since the 2026-07-29 tuning round everything after it is fused (<code>qk_norm_rope_append</code>: per-head norm + RoPE + paged KV write in one pass, 8 dispatches/layer; <code>?fuseqk=0</code> restores the 10-dispatch reference chain) and the K%512 <code>_vec4h</code> matmul variants give d=2560 / ffn=9728 wide loads (<code>?vec4h=0</code> opts out). Same-day A/Bs: fused-qk +2.3%, vec4h +5.7%, combined +5.8% over the flags-off half. The earlier 2026-07-28 pair (25.43 vs 14.15) did not reproduce on the same machine β€” both engines moved ~3Γ— together (degraded session; control-run details in BENCH.md's tuning-round session note). Qwen3-4B is also the model where WebLLM most clearly beats us on time-to-first-token: its 263–271 tok/s prefill implies ~150 ms against our 453 ms.</p>
1078
+ </div>
1079
+
1080
+ <!-- QWEN35 -->
1081
+ <h2 id="qwen35">Qwen3.5-4B hybrid (<code>?model=qwen35</code>)</h2>
1082
+
1083
+ <p>
1084
+ The third model, and the first <em>hybrid</em> architecture on the engine: 24 gated-DeltaNet (linear-attention)
1085
+ layers interleaved with 8 gated full-attention layers (attention on every 4th layer). To our knowledge this is
1086
+ the first hand-written-kernel int4 implementation of a gated-DeltaNet hybrid running in a browser. Append
1087
+ <code>?model=qwen35</code> to <code>zero-tvm.html</code> or <code>validate.html</code>;
1088
+ <code>node scripts/download-weights.mjs --model qwen35</code> primes the local dev mirror (~2.6 GB).
1089
+ </p>
1090
+
1091
+ <p>What the hybrid adds over Qwen3-4B:</p>
1092
+
1093
+ <div class="table-scroll"><table>
1094
+ <thead><tr><th></th><th>Qwen3-4B</th><th>Qwen3.5-4B</th></tr></thead>
1095
+ <tbody>
1096
+ <tr><td>Layer stack</td><td>36 Γ— attention</td><td>24 Γ— gated DeltaNet + 8 Γ— gated attention (every 4th layer)</td></tr>
1097
+ <tr><td>Sequence mixer (most layers)</td><td>GQA attention + KV cache</td><td>delta-rule recurrent state (16 k-heads / 32 v-heads, head dims 128, short conv K=4) β€” no KV cache on those layers</td></tr>
1098
+ <tr><td>Attention layers</td><td>GQA 32/8, head_dim 128, full RoPE</td><td>GQA 16/4, head_dim 256, partial RoPE (64 of 256 dims), sigmoid output gate per head</td></tr>
1099
+ <tr><td>Vocab</td><td>151,936</td><td>248,320 (renumbered specials β€” the shipped <code>mlc-chat-config.json</code> still lists stale Qwen3 stop ids; stops resolve from <code>tokenizer.json</code>)</td></tr>
1100
+ <tr><td>Weight manifest</td><td><code>ndarray-cache.json</code></td><td><code>tensor-cache.json</code> (MLC renamed it)</td></tr>
1101
+ </tbody>
1102
+ </table></div>
1103
+
1104
+ <p>
1105
+ Measured 2026-07-30 on an Apple M2 Max under the corrected protocol (same session, identical local weight
1106
+ bytes, both engines paying a full prefill on every run): Zero-TVM <strong>65.28 tok/s total</strong>
1107
+ (TTFT 171 ms, decode 73.30) vs WebLLM 0.2.84's prebuilt Qwen3.5-4B at <strong>32.56 tok/s total</strong>
1108
+ (self-reported decode 34.32) β€” <strong>+100.5% total, +113.6% decode</strong>. This is the one model where
1109
+ first-token latency is roughly a wash rather than a loss (our 171 ms against an implied ~0.2 s from
1110
+ WebLLM's 175–177 tok/s prefill). Static dated text, not synced from <code>bench/results.json</code>;
1111
+ the machine-readable record is <code>bench/results/qwen35-4b.json</code>.
1112
+ </p>
1113
+
1114
+ <div class="callout warning">
1115
+ <div class="callout-title">Read it cautiously β€” the GDN kernels are still scalar, so decode remains a floor, and the previous number was withdrawn</div>
1116
+ <p>The <strong>"65.67 vs 34.04, +93%" cross-check published on 2026-07-29 is withdrawn</strong> β€” same defect as the Qwen3 pair: measured after cross-turn prefix reuse shipped and before the bench harness reset it, so only the WebLLM half was paying prefill. The two earlier pairs (53.07 vs 32.36, +64%, from the hybrid perf round; 47.99 vs 31.99, +50%, the v1 floor) predate prefix reuse and were like-for-like β€” superseded, not defective. Full writeup at the top of BENCH.md. Engine caveats unchanged: the 24 DeltaNet layers run scalar (non-subgroup) kernels, so the decode number is a floor; the input projections are fused 4β†’1 per GDN layer and prompts prefill in chunks of ≀64 since the 2026-07-29 prefill round, but the rest of the Phi-3 fusion story has not been applied to the GDN half. One machine, one pair; full protocol and caveats in BENCH.md.</p>
1117
+ </div>
1118
+
1119
+ <!-- PORTING -->
1120
+ <h2 id="qwen36">Qwen3.6-35B-A3B MoE (<code>?model=qwen36q3</code> / <code>?model=qwen36</code>)</h2>
1121
+ <p>
1122
+ The fourth model, shipped 2026-08-05, and three firsts at once: the first sparse MoE
1123
+ (256 experts, top-8 plus a shared expert on every layer), the first MLX-format
1124
+ checkpoint (affine <code>w&nbsp;=&nbsp;sΒ·q&nbsp;+&nbsp;b</code>, group 64, per-tensor
1125
+ biases, loaded by byte range β€” a 5.3&nbsp;GB safetensors shard is never one
1126
+ <code>ArrayBuffer</code>), and the first model here with <strong>no WebLLM build to
1127
+ benchmark against</strong>. <code>?model=qwen36q3</code> is the 3-bit-expert build
1128
+ (~16.4&nbsp;GB, needs ~20&nbsp;GB free RAM, ~66&nbsp;tok/s on a quiet 32&nbsp;GB
1129
+ M2&nbsp;Max); <code>?model=qwen36</code> is full 4-bit (~19.5&nbsp;GB, needs
1130
+ ~24&nbsp;GB free RAM). The MoE block runs in 7 dispatches with the expert index in
1131
+ grid&nbsp;<code>z</code>; every layer is validated against mlx_lm's own modules
1132
+ (<code>npm run test:kernels:real</code>). Full engineering notes live in the repo:
1133
+ <a href="https://github.com/abgnydn/zero-tvm#readme">README</a>,
1134
+ <a href="https://github.com/abgnydn/zero-tvm/blob/main/BENCH.md">BENCH.md</a>,
1135
+ <a href="https://github.com/abgnydn/zero-tvm/blob/main/CLAUDE.md">CLAUDE.md</a>.
1136
+ </p>
1137
+
1138
+ <h2 id="porting">Port to Phi-4-mini or Qwen3</h2>
1139
+
1140
+ <p>Both are available as MLC Q4F16 packages. The Qwen3 port has since landed (see <a href="#qwen3">Qwen3-4B</a> above) β€” these are the steps it followed, and the recipe for the next model:</p>
1141
+
1142
+ <ol>
1143
+ <li><strong>Update <code>PHI3</code> constants</strong> in <code>compiler.ts</code> β€” D, HEADS, HEAD_DIM, LAYERS, FFN, VOCAB</li>
1144
+ <li><strong>Check parameter names</strong> β€” fetch <code>ndarray-cache.json</code> and log all keys. Update <code>weight-loader.ts</code> candidates to match</li>
1145
+ <li><strong>Check for GQA</strong> β€” if KV heads β‰  Q heads (grouped-query attention), the attention shader needs a small change to repeat KV heads</li>
1146
+ <li><strong>Update chat template</strong> β€” each model has its own special tokens and prompt format</li>
1147
+ <li><strong>Update HuggingFace base URL</strong> β€” change <code>PHI3_MODEL_BASE</code> in <code>weight-loader.ts</code></li>
1148
+ </ol>
1149
+
1150
+ <div class="callout tip">
1151
+ <div class="callout-title">Phi-4-mini is the easiest port</div>
1152
+ <p>Same family as Phi-3. MLC package is already available at <code>mlc-ai/Phi-4-mini-instruct-q4f16_1-MLC</code>. Parameter naming is likely identical or very similar.</p>
1153
+ </div>
1154
+
1155
+ <!-- LOCAL WEIGHTS -->
1156
+ <h2 id="local-weights">Local weight serving</h2>
1157
+
1158
+ <p>Run the download script once to save all shards locally. Subsequent page loads are instant (served from localhost, no network).</p>
1159
+
1160
+ <div class="code-block">
1161
+ <div class="code-label">terminal</div>
1162
+ <pre><code>node scripts/download-weights.mjs
1163
+
1164
+ <span class="cm"># Downloads to: .weights-local/Phi-3-mini-4k-instruct-q4f16_1-MLC/</span>
1165
+ <span class="cm"># Served at: /local-weights/Phi-3-mini-4k-instruct-q4f16_1-MLC/</span>
1166
+ <span class="cm"># Size: ~2 GB</span></code></pre>
1167
+ </div>
1168
+
1169
+ <div class="callout danger">
1170
+ <div class="callout-title">Keep index + shards in sync</div>
1171
+ <p>Always download everything together. Never mix a freshly-downloaded <code>ndarray-cache.json</code> with old cached shards β€” the byte offsets will not match and all weights will be corrupted.</p>
1172
+ </div>
1173
+
1174
+ <!-- DEBUGGING -->
1175
+ <h2 id="debugging">Debugging tips</h2>
1176
+
1177
+ <h3>All output is <code>&lt;unk&gt;</code></h3>
1178
+ <ul>
1179
+ <li>Weight version mismatch β€” <code>ndarray-cache.json</code> offsets don't match shard content</li>
1180
+ <li>Buffer aliasing β€” <code>add_norm</code> dispatched with same buffer as both <code>@1</code> and <code>@4</code></li>
1181
+ <li>Wrong rope binding order β€” check <code>@0=q_out, @1=k_out, @2=v_out, @3=qkv, @4=posMap</code></li>
1182
+ </ul>
1183
+
1184
+ <h3>Garbage / repetitive output</h3>
1185
+ <ul>
1186
+ <li>Rope bindings are in the wrong order (this was our bug β€” garbage like <code>-,unlintzegesenma</code>)</li>
1187
+ <li>Wrong uniform values for a shader (K_groups, N, etc.)</li>
1188
+ </ul>
1189
+
1190
+ <h3>WebGPU validation error about aliasing</h3>
1191
+ <ul>
1192
+ <li>Same buffer bound as <code>read_write</code> and <code>read</code> in one dispatch</li>
1193
+ <li>Fix: use ping-pong buffers. Never bind <code>B.residual</code> as both <code>@1</code> and <code>@4</code> to <code>add_norm</code></li>
1194
+ </ul>
1195
+
1196
+ <h3>Model not loading (<code>Weight not found</code>)</h3>
1197
+ <ul>
1198
+ <li>Log all available keys: the weight loader prints them to console on load</li>
1199
+ <li>MLC names differ from HuggingFace standard names (<code>transformer.h.N.mixer.*</code> not <code>model.layers.N.self_attn.*</code>)</li>
1200
+ </ul>
1201
+
1202
+ <!-- BUGS FIXED -->
1203
+ <h2 id="bugs-fixed">Bugs we fixed (and how)</h2>
1204
+
1205
+ <div class="table-scroll"><table>
1206
+ <thead><tr><th>Bug</th><th>Symptom</th><th>Fix</th></tr></thead>
1207
+ <tbody>
1208
+ <tr>
1209
+ <td>Wrong MLC param names</td>
1210
+ <td><code>Weight not found</code> error on load</td>
1211
+ <td>Logged all 325 param names from console, updated candidates to <code>transformer.h.*</code> prefix</td>
1212
+ </tr>
1213
+ <tr>
1214
+ <td>Buffer aliasing in add_norm</td>
1215
+ <td><code>&lt;unk&gt;</code> Γ— 500 at 314 tok/s + WebGPU validation error</td>
1216
+ <td>Added <code>B.residual2</code> (pong buffer), ping-pong with JS variable swap</td>
1217
+ </tr>
1218
+ <tr>
1219
+ <td>Wrong rope binding order</td>
1220
+ <td>Garbage: <code>-,unlintzegesenma\dOCĆalloqueIAL</code> repeated</td>
1221
+ <td>Read rope.wgsl β€” bindings are <code>@0=q_out @1=k_out @2=v_out @3=qkv @4=posMap</code></td>
1222
+ </tr>
1223
+ <tr>
1224
+ <td>Mixed ndarray-cache.json version</td>
1225
+ <td><code>&lt;unk&gt;</code> after downloading index locally but using old cached shards</td>
1226
+ <td>Always fetch index and shards from the same source atomically</td>
1227
+ </tr>
1228
+ </tbody>
1229
+ </table></div>
1230
+
1231
+ <!-- VS WEBLLM -->
1232
+ <h2 id="vs-webllm">vs WebLLM</h2>
1233
+
1234
+ <p>Head-to-head on Phi-3-mini-4k-instruct Q4F16_1, same weights, same session, same browser (Chrome 150 with WebGPU), Apple M2 Max, WebLLM 0.2.80 (the Qwen pairs below ran against 0.2.84) β€” <code>npm run bench</code>, 128-token target Γ— 5 runs, median, 2026-07-30 corrected protocol (latest run recorded in <code>bench/results.json</code>). Every run pays a full prefill on both sides, and both metrics are reported: <em>total</em> is wall-clock throughput including prefill, <em>decode</em> excludes it.</p>
1235
+
1236
+ <div class="table-scroll"><table>
1237
+ <thead><tr><th></th><th>Zero-TVM</th><th>WebLLM</th></tr></thead>
1238
+ <tbody>
1239
+ <tr><td>Total throughput (prefill + decode)</td><td><!--bench:zt-->69.55<!--/bench:zt--> tok/s</td><td><!--bench:webllm-->60.0<!--/bench:webllm--> tok/s</td></tr>
1240
+ <tr><td>Decode only</td><td>83.10 tok/s</td><td>63.23 tok/s (self-reported)</td></tr>
1241
+ <tr><td>Time to first token (~35-token prompt)</td><td>291 ms</td><td><strong>~150 ms</strong> (implied from 251 tok/s prefill) β€” WebLLM ahead</td></tr>
1242
+ <tr><td>Gap</td><td colspan="2"><!--bench:gap-->-16<!--/bench:gap-->% on total relative to WebLLM (negative = Zero-TVM ahead), i.e. +16.0% total and +31.4% decode-only. Same-session pair; cross-session absolute tok/s drifts and the old "βˆ’28β€¦βˆ’31% stable band" was retired on 2026-07-30 (see BENCH.md).</td></tr>
1243
+ <tr><td>Dispatches / token</td><td>260 default (228 with <code>?splitk=0</code>)</td><td>342</td></tr>
1244
+ <tr><td>Distinct shaders</td><td>10 hand-written kernel roles</td><td>~11 TVM-generated on the decode path (85 captured across a session)</td></tr>
1245
+ <tr><td>Shipped JS bundle</td><td>~460 kB / ~126 kB gz (<code>zero-tvm.html</code>)</td><td>~6.0 MB / ~2.2 MB gz (@mlc-ai/web-llm 0.2.84)</td></tr>
1246
+ <tr><td>Bandwidth utilization (total / decode)</td><td>~36% / ~44% of the 191 tok/s ceiling</td><td>~31% / ~33% of the same ceiling</td></tr>
1247
+ <tr><td>Paged attention</td><td>βœ“ Hand-written</td><td>βœ“ TVM compiled</td></tr>
1248
+ <tr><td>Readable kernels?</td><td>βœ“ Yes β€” every .wgsl file is in the repo</td><td>βœ— No β€” emitted by the TVM compiler</td></tr>
1249
+ </tbody>
1250
+ </table></div>
1251
+
1252
+ <h3>The corrected protocol (2026-07-30) β€” and what it invalidated</h3>
1253
+
1254
+ <div class="callout warning">
1255
+ <div class="callout-title">Our half of the A/B had stopped paying prefill</div>
1256
+ <p><code>bench()</code> in <code>src/zero-tvm/bench-console.ts</code> looped its runs against the same prompt and never called <code>engine.resetKVTracking()</code> β€” while <code>benchPrefill()</code>, <code>specSim()</code> and <code>validate.ts</code> all did. That was harmless until cross-turn prefix reuse shipped on 2026-07-29 (PR #24). After it, runs 2..N of every bench found the whole prompt already absorbed and prefilled exactly <strong>one</strong> token, while the WebLLM half β€” a fresh chat completion per run β€” kept paying a full prefill inside its wall clock. The two halves were measuring different work.</p>
1257
+ <p><strong>Withdrawn as a result:</strong> the Qwen3-4B "75.74 / 43.75, +73.1%" and Qwen3.5-4B "65.67 / 34.04, +92.9%" pairs, both 2026-07-29. Everything earlier predates prefix reuse and was like-for-like. <strong>Fixed:</strong> <code>bench()</code> resets before every run, both halves split TTFT from decode, and WebLLM's own per-run decode/prefill rates are captured instead of logged once and discarded. Prior published numbers stay in BENCH.md as dated history with the defect explained in place β€” nothing was silently overwritten.</p>
1258
+ </div>
1259
+
1260
+ <h3>The advantage grows with architecture recency</h3>
1261
+
1262
+ <p>Monotonic on both metrics across the three models then shipped, all measured 2026-07-30 on the same machine (Qwen3.6-35B-A3B came later and has no WebLLM baseline to pair against β€” see BENCH.md):</p>
1263
+
1264
+ <div class="table-scroll"><table>
1265
+ <thead><tr><th>Model</th><th>Architecture released</th><th>Ξ” total</th><th>Ξ” decode</th></tr></thead>
1266
+ <tbody>
1267
+ <tr><td>Phi-3-mini</td><td>2024</td><td>+16.0%</td><td>+31.4%</td></tr>
1268
+ <tr><td>Qwen3-4B</td><td>2025</td><td>+31.7%</td><td>+58.0%</td></tr>
1269
+ <tr><td>Qwen3.5-4B</td><td>2026</td><td>+100.5%</td><td>+113.6%</td></tr>
1270
+ </tbody>
1271
+ </table></div>
1272
+
1273
+ <p>The reading that fits: compiler stacks have had less time to tune newer architectures, so there is more headroom for a hand-written kernel set to take. That is an <strong>observation across three points</strong>, not a proven law β€” one machine, one browser, one day, no mechanism isolated, no control for how differently each model stresses the two engines β€” and the baseline is not constant across the rows (WebLLM 0.2.80 for Phi-3, 0.2.84 for both Qwen rows). Worth testing on a fourth model.</p>
1274
+
1275
+ <h3>Where we lose: time to first token on short prompts</h3>
1276
+
1277
+ <div class="callout warning">
1278
+ <div class="callout-title">WebLLM reaches the first token sooner than we do</div>
1279
+ <p>WebLLM's self-reported prefill runs at 251 tok/s on Phi-3 and 263–271 tok/s on Qwen3-4B. Against the ~35-token bench prompt that implies a TTFT of roughly <strong>150 ms</strong> on Phi-3, where we measure <strong>291 ms</strong>; on Qwen3-4B our <strong>453 ms</strong> is worse still. Only Qwen3.5-4B is a wash (our 171 ms vs an implied ~0.2 s). Stated plainly: <strong>we win sustained decode decisively and lose the first-token sprint on short inputs.</strong></p>
1280
+ <p>It is specifically a <em>short</em>-prompt weakness. Chunked prefill measures 202 tok/s on an 816-token prompt (2026-07-29), and cross-turn prefix reuse removes prefill entirely on follow-up turns. What is not competitive is the fixed cost of a short, cold prefill with nothing cached to reuse. It is the top open item on BENCH.md's levers list.</p>
1281
+ </div>
1282
+
1283
+ <h3>How the result flipped (22% behind &rarr; +16% total / +31% decode ahead)</h3>
1284
+ <p>An earlier head-to-head (M2 Pro, 2026-06) read 42.14 vs ~51.5 tok/s β€” Zero-TVM 22% <em>behind</em>. Both the hardware and the engine changed since, so the two Zero-TVM numbers are not a pure optimization delta; the same-session, same-machine WebLLM number is the valid comparator for the current Zero-TVM median (both in the table above). What changed in the engine:</p>
1285
+ <ul>
1286
+ <li><strong>Correctness fixes</strong>. f32 accumulation in the fused FFN, a workgroup-barrier fix in attention, and a decode off-by-one fix β€” the old number understates even the old hardware.</li>
1287
+ <li><strong>vec4 loads, now default</strong>. Re-declaring weight + activation buffers as <code>array&lt;vec4&lt;u32&gt;&gt;</code> in the int4 matmuls and <code>qkv_fused</code> measured +7.1% on M2 Max; opt out with <code>?vec4=0</code> / <code>?vec4qkv=0</code>.</li>
1288
+ <li><strong>Tiled + subgroup kernels</strong>. The 4-row tiled subgroup FFN and the <code>_sg</code> matmul/attention/argmax variants are the default path on Apple sg32 hardware.</li>
1289
+ </ul>
1290
+ <p>Still default since 2026-07-27 (`?splitk=0` to disable): split-K attention (<code>?splitk=N</code>, ~+3% at short context, needs a long-context A/B); the best measured opt-in config of that era (<code>?vec4=1&amp;vec4qkv=1&amp;splitk=8</code>, 68.36 tok/s on 2026-07-25) is now simply the default path. Falsified and not shipped: FFN prologue fusion (<code>?fuseprologue=1</code>, βˆ’13.7% on M2 Max). Dispatch overhead was never the gap β€” Zero-TVM already submitted fewer dispatches per token (260 vs 342) when it was slower. Full A/B tables in BENCH.md.</p>
1291
+
1292
+ <h3>The memory-bandwidth ceiling</h3>
1293
+ <p>Phi-3-mini Q4F16_1 touches ~2.09 GB of weights per decode token (the weight-shape table above, summed: 32 layers plus the LM head; the embedding contributes one row). On M2 Max's 400 GB/s memory bus that's ~5.2 ms/token, or ~191 tok/s theoretical max. Zero-TVM's measured decode-only median sits at roughly 44% of that ceiling (36% on total, which pays prefill too); WebLLM's at roughly 33% decode-only / 31% total (exact medians in the table above). Neither engine can exceed the ceiling without changing weight layout or quantization (int8 KV shaves a bit, which is why it's exposed behind a flag).</p>
1294
+
1295
+ </main>
1296
+ </div>
1297
+
1298
+ <script>
1299
+ // Highlight active sidebar link on scroll
1300
+ const links = document.querySelectorAll('.sidebar-link[href^="#"]')
1301
+ const observer = new IntersectionObserver((entries) => {
1302
+ for (const entry of entries) {
1303
+ if (entry.isIntersecting) {
1304
+ const id = entry.target.id
1305
+ links.forEach(l => {
1306
+ l.classList.toggle('active', l.getAttribute('href') === '#' + id)
1307
+ })
1308
+ }
1309
+ }
1310
+ }, { rootMargin: '-60px 0px -60% 0px' })
1311
+
1312
+ document.querySelectorAll('[id]').forEach(el => observer.observe(el))
1313
+ </script>
1314
+
1315
+ </body>
1316
+ </html>
entrance/backring.svg ADDED
entrance/banner.svg ADDED
entrance/bg.svg ADDED
entrance/border.svg ADDED
entrance/column.svg ADDED
entrance/corner.svg ADDED
entrance/divider.svg ADDED
entrance/pframe.svg ADDED
entrance/ring.svg ADDED
entrance/spires.svg ADDED
favicon.svg ADDED
fonts.css ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Self-hosted. The site's claim is that nothing leaves your machine;
2
+ a webfont request to Google would leak every visitor's IP and make
3
+ that claim false on the landing page itself. 292 KB, 7 files. */
4
+ @font-face {
5
+ font-family: 'Fraunces';
6
+ font-style: normal;
7
+ font-weight: 400;
8
+ font-display: swap;
9
+ src: url('/fonts/f2-fraunce.woff2') format('woff2');
10
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
11
+ }
12
+ @font-face {
13
+ font-family: 'Fraunces';
14
+ font-style: normal;
15
+ font-weight: 400;
16
+ font-display: swap;
17
+ src: url('/fonts/f3-fraunce.woff2') format('woff2');
18
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
19
+ }
20
+ @font-face {
21
+ font-family: 'Fraunces';
22
+ font-style: normal;
23
+ font-weight: 400;
24
+ font-display: swap;
25
+ src: url('/fonts/f1-fraunce.woff2') format('woff2');
26
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
27
+ }
28
+ @font-face {
29
+ font-family: 'Fraunces';
30
+ font-style: normal;
31
+ font-weight: 600;
32
+ font-display: swap;
33
+ src: url('/fonts/f2-fraunce.woff2') format('woff2');
34
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
35
+ }
36
+ @font-face {
37
+ font-family: 'Fraunces';
38
+ font-style: normal;
39
+ font-weight: 600;
40
+ font-display: swap;
41
+ src: url('/fonts/f3-fraunce.woff2') format('woff2');
42
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
43
+ }
44
+ @font-face {
45
+ font-family: 'Fraunces';
46
+ font-style: normal;
47
+ font-weight: 600;
48
+ font-display: swap;
49
+ src: url('/fonts/f1-fraunce.woff2') format('woff2');
50
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
51
+ }
52
+ @font-face {
53
+ font-family: 'Spline Sans';
54
+ font-style: normal;
55
+ font-weight: 400;
56
+ font-display: swap;
57
+ src: url('/fonts/f5-plinean.woff2') format('woff2');
58
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
59
+ }
60
+ @font-face {
61
+ font-family: 'Spline Sans';
62
+ font-style: normal;
63
+ font-weight: 400;
64
+ font-display: swap;
65
+ src: url('/fonts/f4-plinean.woff2') format('woff2');
66
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
67
+ }
68
+ @font-face {
69
+ font-family: 'Spline Sans';
70
+ font-style: normal;
71
+ font-weight: 500;
72
+ font-display: swap;
73
+ src: url('/fonts/f5-plinean.woff2') format('woff2');
74
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
75
+ }
76
+ @font-face {
77
+ font-family: 'Spline Sans';
78
+ font-style: normal;
79
+ font-weight: 500;
80
+ font-display: swap;
81
+ src: url('/fonts/f4-plinean.woff2') format('woff2');
82
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
83
+ }
84
+ @font-face {
85
+ font-family: 'Spline Sans';
86
+ font-style: normal;
87
+ font-weight: 600;
88
+ font-display: swap;
89
+ src: url('/fonts/f5-plinean.woff2') format('woff2');
90
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
91
+ }
92
+ @font-face {
93
+ font-family: 'Spline Sans';
94
+ font-style: normal;
95
+ font-weight: 600;
96
+ font-display: swap;
97
+ src: url('/fonts/f4-plinean.woff2') format('woff2');
98
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
99
+ }
100
+ @font-face {
101
+ font-family: 'Spline Sans Mono';
102
+ font-style: normal;
103
+ font-weight: 400;
104
+ font-display: swap;
105
+ src: url('/fonts/f7-plineanmono.woff2') format('woff2');
106
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
107
+ }
108
+ @font-face {
109
+ font-family: 'Spline Sans Mono';
110
+ font-style: normal;
111
+ font-weight: 400;
112
+ font-display: swap;
113
+ src: url('/fonts/f6-plineanmono.woff2') format('woff2');
114
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
115
+ }
116
+ @font-face {
117
+ font-family: 'Spline Sans Mono';
118
+ font-style: normal;
119
+ font-weight: 500;
120
+ font-display: swap;
121
+ src: url('/fonts/f7-plineanmono.woff2') format('woff2');
122
+ unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
123
+ }
124
+ @font-face {
125
+ font-family: 'Spline Sans Mono';
126
+ font-style: normal;
127
+ font-weight: 500;
128
+ font-display: swap;
129
+ src: url('/fonts/f6-plineanmono.woff2') format('woff2');
130
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
131
+ }
fonts/f1-fraunce.woff2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:48282a415ec22e31beaf0a0666e6fae0c8cbddcd0b1f6e729f27c3ade8a64e43
3
+ size 67388
fonts/f2-fraunce.woff2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7e3c04662669cd2b8ee672e4e5d63146d21f6aa36654b211718af5fe0bb3bd51
3
+ size 19748
fonts/f3-fraunce.woff2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f120089b9440f3e35a980a2137347b1851ad46ee4b8ad5c63b6c07e053fa40e2
3
+ size 59540
fonts/f4-plinean.woff2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:14ed7b4db18fb8fd77f252da580df74f457f81a788cff0b71f357e882279156c
3
+ size 57844
fonts/f5-plinean.woff2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d13b339c84af67cc3b402cc08f2f53e08f29b65b9ca5ac7b729b5b4243a02638
3
+ size 21048
fonts/f6-plineanmono.woff2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2b193a4f0fef3fc4c6a3fa756bb5dab6c10b489acd5e0718736847755077e9ec
3
+ size 36528
fonts/f7-plineanmono.woff2 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bde42c142265c31a450781d82cc72e1859bd42cba8e438fb96f77f1d69ebc8d6
3
+ size 20748
index.html ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ <title>zero-tvm β€” LLM inference in the browser, on hand-written WGSL</title>
7
+ <meta name="description" content="zero-tvm is an LLM inference engine written by hand in WGSL and TypeScript. It runs entirely in the browser: weights download once, cache locally, and inference runs on your own GPU.">
8
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml">
9
+ <link rel="canonical" href="https://zerotvm.com/">
10
+
11
+ <meta property="og:type" content="website">
12
+ <meta property="og:url" content="https://zerotvm.com/">
13
+ <meta property="og:title" content="zero-tvm β€” LLM inference in the browser, on hand-written WGSL">
14
+ <meta property="og:description" content="An LLM inference engine written by hand in WGSL and TypeScript. Weights download once, cache locally, and inference runs on your own GPU.">
15
+ <!-- The canonical URL is the one that actually gets posted, and it was the
16
+ only page declaring summary_large_image without an image to put in it β€”
17
+ so every share of https://zerotvm.com/ unfurled as a bare text card
18
+ while /docs.html unfurled richly. og.png ships already. -->
19
+ <meta property="og:image" content="https://zerotvm.com/og.png">
20
+ <meta name="twitter:card" content="summary_large_image">
21
+ <meta name="twitter:image" content="https://zerotvm.com/og.png">
22
+ <meta name="twitter:title" content="zero-tvm β€” LLM inference in the browser, on hand-written WGSL">
23
+ <meta name="twitter:description" content="An LLM inference engine written by hand in WGSL and TypeScript. Weights download once, cache locally, and inference runs on your own GPU.">
24
+
25
+ <link rel="preload" href="/entrance/bg.svg" as="image" type="image/svg+xml">
26
+ <link rel="preload" href="/entrance/ring.svg" as="image" type="image/svg+xml">
27
+ <link rel="preload" href="/fonts/f3-fraunce.woff2" as="font" type="font/woff2" crossorigin>
28
+ <link rel="preload" href="/fonts/f4-plinean.woff2" as="font" type="font/woff2" crossorigin>
29
+ <link rel="stylesheet" href="/fonts.css">
30
+ <link rel="stylesheet" href="/tokens.css">
31
+ <!-- chat-ui.css: the shared message surface β€” ENTER opens the chat IN the
32
+ entrance (landing-chat.ts), so this page renders bubbles and the
33
+ composer too. Before landing.css, which carries the panel chrome. -->
34
+ <link rel="stylesheet" href="/chat-ui.css">
35
+ <link rel="stylesheet" href="/landing.css">
36
+
37
+ <script type="application/ld+json">
38
+ {
39
+ "@context": "https://schema.org",
40
+ "@type": "SoftwareSourceCode",
41
+ "name": "zero-tvm",
42
+ "url": "https://zerotvm.com/",
43
+ "description": "An LLM inference engine written by hand in WGSL and TypeScript, running entirely in the browser.",
44
+ "programmingLanguage": [
45
+ "WGSL",
46
+ "TypeScript"
47
+ ],
48
+ "codeRepository": "https://github.com/abgnydn/zero-tvm",
49
+ "license": "https://opensource.org/licenses/MIT",
50
+ "isPartOf": {
51
+ "@type": "Collection",
52
+ "name": "WebGPU research line",
53
+ "url": "https://kernelfusion.dev"
54
+ },
55
+ "author": {
56
+ "@type": "Person",
57
+ "name": "Ahmet Barış Günaydın",
58
+ "url": "https://zerotvm.com/",
59
+ "sameAs": [
60
+ "https://barisgunaydin.com",
61
+ "https://github.com/abgnydn",
62
+ "https://huggingface.co/abgunaydin",
63
+ "https://x.com/abgnydn_",
64
+ "https://www.linkedin.com/in/abgnydn/"
65
+ ]
66
+ }
67
+ }
68
+ </script>
69
+ <script type="module" crossorigin src="/assets/index-BqNbylMY24.js"></script>
70
+ <link rel="modulepreload" crossorigin href="/assets/model-registry-BoK-ObgACo.js">
71
+ <link rel="modulepreload" crossorigin href="/assets/preload-helper-ckwbz45pO3.js">
72
+ <link rel="modulepreload" crossorigin href="/assets/mascot-BCKWddkWrV.js">
73
+ <link rel="modulepreload" crossorigin href="/assets/feats-dlBfBxqpI8.js">
74
+ </head>
75
+ <body>
76
+
77
+ <nav class="nav">
78
+ <div class="wrap nav-in">
79
+ <a href="/" class="nav-logo">zero<span>-tvm</span></a>
80
+ <div class="nav-links">
81
+ <a href="#models">Models</a>
82
+ <!-- "Rooms" pointed at share.html's bare host page (a floating modal
83
+ over black). Hosting lives in the entrance now β€” enter a character,
84
+ then ⟁ Room. share.html remains the GUEST surface links point at,
85
+ and the footer still lists it. -->
86
+ <!-- "Agent" was here. agent-host.html only works against a local
87
+ agent-server on 127.0.0.1:8017, so a public visitor paid a ~2 GB
88
+ download for a page that then sat on "booting…" forever. It is a
89
+ repo workflow (`npm run agent`), documented in the README, not a
90
+ destination on the site. -->
91
+ <a href="docs.html#vs-webllm">Proof</a>
92
+ <a href="docs.html">Docs</a>
93
+ <!-- "Open chat β†’" left with the in-place chat: the page below IS the
94
+ chat entrance, and the CTA duplicated ENTER while routing through
95
+ the old standalone gate. zero-tvm.html remains the deep-link
96
+ surface (footer, docs, ?model= links). -->
97
+ </div>
98
+ </div>
99
+ </nav>
100
+
101
+ <section id="models">
102
+ <!-- The game entrance. Full-viewport character select β€” landing.ts renders
103
+ the whole screen (splash, stage, roster, sheet) into this root. Every
104
+ figure and number still comes from the registry; the scroll below keeps
105
+ the essay and the kernel window for the reader who wants the why. -->
106
+ <div id="model-browser" class="cs-root"></div>
107
+ </section>
108
+
109
+ <!-- The page IS the character select now β€” nav above, footer below,
110
+ nothing between. The essay, the kernel window and the deep copy left
111
+ with the 2026-08-15 entrance redesign; docs.html carries the why. -->
112
+
113
+ <footer>
114
+ <div class="wrap">
115
+ <div class="foot-grid">
116
+ <div>
117
+ <a href="/" class="nav-logo">zero<span>-tvm</span></a>
118
+ <p class="foot-about">
119
+ An LLM inference engine written by hand in WGSL and TypeScript. Part
120
+ of a research line on WebGPU kernels by Ahmet Barış Günaydın.
121
+ </p>
122
+ </div>
123
+ <div>
124
+ <h4>This site</h4>
125
+ <ul>
126
+ <li><a href="zero-tvm.html">Chat</a></li>
127
+ <li><a href="share.html">Host a room</a></li>
128
+ <li><a href="docs.html">Docs</a></li>
129
+ <li><a href="docs.html#vs-webllm">Proof</a></li>
130
+ <li><a href="validate.html">Validate</a></li>
131
+ </ul>
132
+ </div>
133
+ <div>
134
+ <h4>Research line</h4>
135
+ <ul id="sibling-links">
136
+ <li><a href="https://kernelfusion.dev">kernelfusion.dev</a></li>
137
+ <li><a href="https://github.com/abgnydn">github.com/abgnydn</a></li>
138
+ <li><a href="https://huggingface.co/abgunaydin">huggingface.co/abgunaydin</a></li>
139
+ </ul>
140
+ </div>
141
+ </div>
142
+ <div class="foot-bottom">
143
+ <span>MIT licensed Β· weights are fetched once from HuggingFace and cached in your browser; prompts and generation stay on your machine in the chat. In a room, a guest&rsquo;s prompts run on the host&rsquo;s GPU.</span>
144
+ <a href="https://github.com/abgnydn/zero-tvm">github.com/abgnydn/zero-tvm β†—</a>
145
+ </div>
146
+ </div>
147
+ </footer>
148
+
149
+
150
+ </body>
151
+ </html>
landing.css ADDED
@@ -0,0 +1,1442 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* LANDING β€” Foundry chassis. docs/SITE_REDESIGN.md.
2
+ No figure is written here. Anything numeric on this page is rendered from
3
+ model-registry.ts at runtime or it is absent, so adding a model updates the
4
+ page by itself and a stale number cannot be committed. */
5
+
6
+ *, *::before, *::after { box-sizing: border-box; }
7
+
8
+ body {
9
+ margin: 0;
10
+ background: var(--bg);
11
+ color: var(--text);
12
+ font-family: var(--body);
13
+ font-size: var(--step-0);
14
+ line-height: 1.65;
15
+ -webkit-font-smoothing: antialiased;
16
+ }
17
+
18
+ a { color: var(--accent); text-decoration: none; }
19
+ a:hover { color: var(--accent-hi); }
20
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: var(--rad-s); }
21
+
22
+ .wrap { max-width: var(--maxw); margin: 0 auto; padding: 0 24px; }
23
+
24
+ /* ── eyebrow ───────────────────────────────────────────────────────── */
25
+ .eyebrow {
26
+ display: inline-flex; align-items: center; gap: 8px;
27
+ font-family: var(--mono); font-size: var(--step--1);
28
+ letter-spacing: 0.08em; text-transform: uppercase; color: var(--dim);
29
+ }
30
+ .pulse {
31
+ width: 7px; height: 7px; border-radius: 50%; background: var(--accent);
32
+ box-shadow: 0 0 0 0 var(--accent-dim); animation: pulse 2.4s ease-out infinite;
33
+ }
34
+ @keyframes pulse {
35
+ 0% { box-shadow: 0 0 0 0 #f0a86066; }
36
+ 70% { box-shadow: 0 0 0 9px #f0a86000; }
37
+ 100% { box-shadow: 0 0 0 0 #f0a86000; }
38
+ }
39
+ @media (prefers-reduced-motion: reduce) { .pulse { animation: none; } }
40
+
41
+ /* ── nav ───────────────────────────────────────────────────────────── */
42
+ .nav {
43
+ position: sticky; top: 0; z-index: 40;
44
+ background: color-mix(in srgb, var(--bg) 88%, transparent);
45
+ backdrop-filter: blur(10px);
46
+ border-bottom: 1px solid var(--border);
47
+ }
48
+ .nav-in { display: flex; align-items: center; gap: 28px; height: 60px; }
49
+ .nav-logo {
50
+ font-family: var(--mono); font-weight: 500; color: var(--text);
51
+ letter-spacing: -0.02em; margin-right: auto;
52
+ }
53
+ .nav-logo span { color: var(--accent); }
54
+ .nav a:not(.nav-logo):not(.nav-cta) { color: var(--muted); font-size: var(--step--1); }
55
+ .nav a:not(.nav-logo):not(.nav-cta):hover { color: var(--text); }
56
+ .nav-cta {
57
+ font-size: var(--step--1); padding: 7px 15px; border-radius: var(--rad-s);
58
+ border: 1px solid var(--accent); color: var(--accent);
59
+ }
60
+ .nav-cta:hover { background: var(--accent); color: var(--bg); }
61
+ .nav-links { display: flex; gap: 22px; align-items: center; }
62
+ /* On phones the nav used to collapse to ONLY "Open chat" β€” the single action
63
+ that costs gigabytes β€” while Docs and everything explanatory vanished. Docs
64
+ stays; the heavyweight links are the ones that can go. */
65
+ @media (max-width: 720px) { .nav-links a:not(.nav-cta):not([href="docs.html"]) { display: none; } }
66
+
67
+ /* ── hero ──────────────────────────────────────────────────────────── */
68
+ .hero { padding: 84px 0 64px; }
69
+ .hero h1 {
70
+ font-family: var(--display);
71
+ font-size: var(--step-3);
72
+ font-weight: 400;
73
+ line-height: 1.08;
74
+ letter-spacing: -0.02em;
75
+ margin: 20px 0 0;
76
+ text-wrap: balance;
77
+ }
78
+ .hero h1 em { font-style: italic; color: var(--accent); }
79
+ .hero p {
80
+ color: var(--muted); max-width: 62ch; margin: 20px 0 0; font-size: var(--step-1);
81
+ line-height: 1.6;
82
+ }
83
+ .hero-actions { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 28px; }
84
+ .btn {
85
+ font-family: var(--body); font-size: var(--step-0);
86
+ padding: 11px 20px; border-radius: var(--rad-s); border: 1px solid var(--border-hi);
87
+ color: var(--text); background: transparent; cursor: pointer;
88
+ }
89
+ .btn:hover { border-color: var(--accent); color: var(--accent); }
90
+ .btn-primary { background: var(--accent); border-color: var(--accent); color: #1a1206; font-weight: 500; }
91
+ .btn-primary:hover { background: var(--accent-hi); border-color: var(--accent-hi); color: #1a1206; }
92
+
93
+ /* ── kernel window ─────────────────────────────────────────────────── */
94
+ .kw {
95
+ margin-top: 44px; border: 1px solid var(--border); border-radius: var(--rad);
96
+ background: var(--surface); overflow: hidden;
97
+ }
98
+ .kw-bar {
99
+ display: flex; align-items: center; gap: 10px;
100
+ padding: 0 14px; height: 40px; border-bottom: 1px solid var(--border);
101
+ background: var(--surface-2);
102
+ }
103
+ .kw-dots { display: flex; gap: 6px; margin-right: 6px; }
104
+ .kw-dots i { width: 10px; height: 10px; border-radius: 50%; background: var(--border-hi); }
105
+ .kw-tabs { display: flex; gap: 2px; overflow-x: auto; scrollbar-width: none; }
106
+ .kw-tabs::-webkit-scrollbar { display: none; }
107
+ .kw-tab {
108
+ font-family: var(--mono); font-size: var(--step--1); color: var(--dim);
109
+ background: none; border: 0; padding: 6px 11px; border-radius: var(--rad-s);
110
+ cursor: pointer; white-space: nowrap;
111
+ }
112
+ .kw-tab:hover { color: var(--muted); }
113
+ .kw-tab[aria-selected="true"] { color: var(--accent); background: var(--accent-dim); }
114
+ .kw-body { overflow-x: auto; }
115
+ .kw-body pre {
116
+ margin: 0; padding: 20px 22px;
117
+ font-family: var(--mono); font-size: var(--step--1); line-height: 1.7;
118
+ color: var(--muted);
119
+ }
120
+ .kw-body pre[hidden] { display: none; }
121
+ .kw-foot {
122
+ display: flex; justify-content: space-between; align-items: center; gap: 12px;
123
+ padding: 10px 16px; border-top: 1px solid var(--border);
124
+ font-family: var(--mono); font-size: var(--step--1); color: var(--dim);
125
+ flex-wrap: wrap;
126
+ }
127
+
128
+ /* WGSL syntax β€” one hue family, so the code reads as material rather than
129
+ as a rainbow. Comments recede; the accent is spent on identifiers. */
130
+ .t-key { color: #c58bff; }
131
+ .t-fn { color: var(--accent-hi); }
132
+ .t-num { color: var(--ok); }
133
+ .t-str { color: #9fd3ff; }
134
+ .t-com { color: var(--dim); font-style: italic; }
135
+ .t-attr { color: #7e8aa0; }
136
+ .t-type { color: #8fd0ff; }
137
+
138
+ /* ── capability band ───────────────────────────────────────────────── */
139
+ .band { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: var(--border);
140
+ border: 1px solid var(--border); border-radius: var(--rad); overflow: hidden; }
141
+ @media (max-width: 840px) { .band { grid-template-columns: repeat(2, 1fr); } }
142
+ @media (max-width: 480px) { .band { grid-template-columns: 1fr; } }
143
+ .band a { background: var(--surface); padding: 22px; display: block; color: inherit; }
144
+ .band a:hover { background: var(--surface-2); }
145
+ .band h3 {
146
+ font-family: var(--mono); font-size: var(--step--1); text-transform: uppercase;
147
+ letter-spacing: 0.08em; color: var(--accent); margin: 0 0 8px; font-weight: 500;
148
+ }
149
+ .band p { margin: 0; color: var(--muted); font-size: var(--step--1); line-height: 1.6; }
150
+
151
+ /* ── sections ──────────────────────────────────────────────────────── */
152
+ section { padding: 72px 0; }
153
+ .sec-head { margin-bottom: 32px; }
154
+ .sec-head h2 {
155
+ font-family: var(--display); font-weight: 400; font-size: var(--step-2);
156
+ margin: 12px 0 0; letter-spacing: -0.01em;
157
+ }
158
+ .sec-head p { color: var(--muted); margin: 12px 0 0; max-width: 66ch; }
159
+
160
+ /* ── model browser ─────────────────────────────────────────────────── */
161
+ /* The container IS the keyboard interface β€” index.html tells the reader the
162
+ arrow keys move between models, and this is the element that must be
163
+ focused for that to work. Removing its ring left no way to tell. */
164
+ #model-browser:focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
165
+ .mb-stage { display: flex; align-items: center; gap: 8px; }
166
+ .mb-arrow {
167
+ flex: 0 0 auto; width: 42px; height: 42px; border-radius: 50%;
168
+ background: var(--surface); border: 1px solid var(--border); color: var(--muted);
169
+ font-size: 22px; line-height: 1; cursor: pointer; font-family: var(--body);
170
+ }
171
+ .mb-arrow:hover { border-color: var(--accent); color: var(--accent); }
172
+ .mb-slide {
173
+ flex: 1 1 auto; display: grid; grid-template-columns: 1fr 1fr; gap: 32px;
174
+ align-items: center; min-height: 380px;
175
+ border: 1px solid var(--border); border-radius: var(--rad);
176
+ background: linear-gradient(180deg, var(--surface-2), var(--surface));
177
+ padding: 28px 32px;
178
+ }
179
+ @media (max-width: 780px) {
180
+ .mb-slide { grid-template-columns: 1fr; text-align: center; min-height: 0; }
181
+ .mb-stats { justify-items: center; }
182
+ .mb-variants, .mb-info { justify-content: center; align-items: center; }
183
+ }
184
+ .mb-art { position: relative; padding-bottom: 66px; }
185
+ .mb-mascot { display: block; width: 100%; height: 320px; position: relative; z-index: 1; }
186
+ @media (max-width: 780px) { .mb-mascot { height: 220px; } }
187
+ /* The pedestal: a class-coloured light pool under the figure β€” the character
188
+ stands ON something, which is most of what makes a select screen read as a
189
+ stage rather than a card. */
190
+ .mb-pedestal {
191
+ position: absolute; left: 50%; bottom: 46px; transform: translateX(-50%);
192
+ width: 78%; height: 64px; border-radius: 50%;
193
+ background: radial-gradient(ellipse at center,
194
+ color-mix(in srgb, var(--cs-accent, var(--accent)) 34%, transparent) 0%,
195
+ color-mix(in srgb, var(--cs-accent, var(--accent)) 10%, transparent) 55%,
196
+ transparent 72%);
197
+ filter: blur(2px);
198
+ }
199
+ /* Nameplate under the stage, class-coloured keyline. */
200
+ .mb-plate {
201
+ position: absolute; left: 50%; bottom: 0; transform: translateX(-50%);
202
+ text-align: center; padding: 8px 22px 10px; min-width: 60%;
203
+ border: 1px solid color-mix(in srgb, var(--cs-accent, var(--accent)) 45%, var(--border));
204
+ border-radius: 10px;
205
+ background: color-mix(in srgb, var(--cs-accent, var(--accent)) 7%, var(--surface));
206
+ }
207
+ .mb-panel {
208
+ border: 1px solid var(--border); border-radius: 12px;
209
+ background: linear-gradient(180deg, var(--surface-2), transparent);
210
+ padding: 16px 18px 14px;
211
+ }
212
+ .mb-row-label {
213
+ font-family: var(--mono); font-size: var(--step--1); color: var(--dim);
214
+ text-transform: uppercase; letter-spacing: 0.09em; margin: 10px 0 6px;
215
+ }
216
+ .mb-row-label:first-child { margin-top: 0; }
217
+
218
+ .mb-info { display: flex; flex-direction: column; gap: 14px; }
219
+ .mb-name { font-family: var(--display); font-size: var(--step-2); line-height: 1.1; }
220
+ .mb-params { font-family: var(--mono); font-size: var(--step--1); color: var(--dim); margin-top: 2px; }
221
+
222
+ .mb-variants { display: flex; gap: 6px; flex-wrap: wrap; }
223
+ /* Memory builds β€” the character screen's loadout row. Same pill grammar as
224
+ the quantisation picker: a build is a choice about THIS character, and the
225
+ selected one re-poses the figure (leaner, streamed experts drifting out). */
226
+ .mb-modes { display: flex; gap: 6px; flex-wrap: wrap; }
227
+ .mb-variant {
228
+ font-family: var(--mono); font-size: var(--step--1); cursor: pointer;
229
+ padding: 5px 12px; border-radius: 99px;
230
+ border: 1px solid var(--border-hi); background: transparent; color: var(--muted);
231
+ }
232
+ .mb-variant:hover { color: var(--text); border-color: var(--accent); }
233
+ .mb-variant[aria-selected="true"] { border-color: var(--accent); color: var(--accent); background: var(--accent-dim); }
234
+
235
+ .mb-stats { display: grid; grid-template-columns: repeat(3, auto); gap: 22px; margin: 14px 0 4px; justify-content: start; }
236
+ @media (max-width: 780px) { .mb-stats { justify-content: center; } }
237
+ /* At 390px the three-column stats row set a ~204px min-content floor which,
238
+ plus slide padding and the two 42px arrows, needed ~368px of a 342px
239
+ viewport β€” the body scrolled sideways. Flex-wrap has no min-content floor:
240
+ each stat drops to the next line instead of pushing the page. min-width: 0
241
+ on the grid/flex items is what lets the slide actually shrink. */
242
+ @media (max-width: 480px) {
243
+ .mb-stats { display: flex; flex-wrap: wrap; gap: 10px 20px; }
244
+ .mb-slide { min-width: 0; padding: 20px 18px; }
245
+ .mb-art, .mb-info { min-width: 0; }
246
+ .mb-arrow { width: 34px; height: 34px; font-size: 18px; }
247
+ }
248
+ .mb-stats dt {
249
+ font-family: var(--mono); font-size: var(--step--1); color: var(--dim);
250
+ text-transform: uppercase; letter-spacing: 0.07em;
251
+ }
252
+ .mb-stats dd { margin: 4px 0 0; font-family: var(--mono); font-size: var(--step-0); color: var(--text); }
253
+ .mb-hw { color: var(--dim); font-size: var(--step--1); }
254
+ .mb-cached { color: var(--ok); font-size: var(--step--1); margin: 14px 0 0; }
255
+ .mb-ram { color: var(--warn); font-size: var(--step--1); margin: 14px 0 0; }
256
+ .mb-cta {
257
+ align-self: stretch; text-align: center; font-size: var(--step-0);
258
+ padding: 0.85rem 1.2rem;
259
+ background: var(--cs-accent, var(--accent));
260
+ border-color: var(--cs-accent, var(--accent));
261
+ }
262
+ .mb-cta:hover { filter: brightness(1.12); }
263
+ @media (max-width: 780px) { .mb-cta { align-self: center; } }
264
+
265
+ /* The roster rail β€” party lineup instead of dots. Each slot carries the
266
+ character's own snapshotted face (--thumb, painted async); cached models
267
+ were snapshotted lit, which is the visual for "unlocked". */
268
+ .mb-dots { display: flex; gap: 10px; justify-content: center; margin-top: 22px; flex-wrap: wrap; }
269
+ .mb-dot {
270
+ width: 64px; height: 76px; padding: 0 0 4px; cursor: pointer;
271
+ border: 1px solid var(--border-hi); border-radius: 10px;
272
+ background:
273
+ var(--thumb, radial-gradient(circle at 50% 40%, var(--border-hi) 0%, transparent 60%))
274
+ center 2px / 58px 58px no-repeat,
275
+ var(--surface);
276
+ display: flex; align-items: flex-end; justify-content: center;
277
+ transition: border-color 0.15s, transform 0.15s;
278
+ }
279
+ .mb-dot:hover { border-color: var(--muted); transform: translateY(-2px); }
280
+ .mb-dot[aria-selected="true"] {
281
+ border-color: var(--cs-accent, var(--accent));
282
+ box-shadow: 0 0 0 1px var(--cs-accent, var(--accent)),
283
+ 0 6px 18px color-mix(in srgb, var(--cs-accent, var(--accent)) 25%, transparent);
284
+ }
285
+ .mb-dot-name {
286
+ font-family: var(--mono); font-size: 9px; color: var(--dim);
287
+ max-width: 60px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
288
+ }
289
+ .mb-dot[aria-selected="true"] .mb-dot-name { color: var(--text); }
290
+ @media (max-width: 480px) { .mb-dot { width: 52px; height: 64px; } .mb-dot-name { font-size: 8px; } }
291
+ .note { color: var(--muted); font-size: var(--step--1); margin-top: 16px; }
292
+
293
+ /* ── kernel wall ───────────────────────────────────────────────────── */
294
+ /* Five columns so the ten tiles fill exactly two rows β€” auto-fill left a
295
+ half-empty third row that read as a rendering fault. */
296
+ .wall { display: grid; grid-template-columns: repeat(5, 1fr); gap: 1px;
297
+ background: var(--border); border: 1px solid var(--border); border-radius: var(--rad);
298
+ overflow: hidden; }
299
+ @media (max-width: 900px) { .wall { grid-template-columns: repeat(3, 1fr); } }
300
+ @media (max-width: 560px) { .wall { grid-template-columns: repeat(2, 1fr); } }
301
+ .wall a {
302
+ background: var(--surface); padding: 16px 14px; color: var(--muted);
303
+ font-family: var(--mono); font-size: var(--step--1); display: block;
304
+ }
305
+ .wall a:hover { background: var(--surface-2); color: var(--accent); }
306
+ .wall b { display: block; color: var(--text); font-weight: 500; margin-bottom: 4px; }
307
+ .wall a:hover b { color: var(--accent); }
308
+ .wall span { color: var(--dim); }
309
+
310
+ /* ── proof strip ───────────────────────────────────────────────────── */
311
+ .proof {
312
+ border: 1px solid var(--border); border-left: 2px solid var(--accent);
313
+ border-radius: var(--rad); background: var(--surface); padding: 26px 28px;
314
+ }
315
+ .proof h2 { font-family: var(--display); font-weight: 400; font-size: var(--step-1); margin: 0; }
316
+ .proof p { color: var(--muted); margin: 10px 0 0; max-width: 70ch; }
317
+
318
+ /* ── footer ────────────────────────────────────────────────────────── */
319
+ footer { border-top: 1px solid var(--border); padding: 56px 0 72px; }
320
+ .foot-grid { display: grid; grid-template-columns: 1.4fr 1fr 1fr; gap: 40px; }
321
+ @media (max-width: 780px) { .foot-grid { grid-template-columns: 1fr; gap: 28px; } }
322
+ footer h4 {
323
+ font-family: var(--mono); font-size: var(--step--1); text-transform: uppercase;
324
+ letter-spacing: 0.08em; color: var(--dim); margin: 0 0 12px; font-weight: 500;
325
+ }
326
+ footer ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 8px; }
327
+ footer li a { color: var(--muted); font-size: var(--step--1); }
328
+ footer li a:hover { color: var(--accent); }
329
+ .foot-about { color: var(--muted); font-size: var(--step--1); max-width: 44ch; }
330
+ .foot-bottom {
331
+ margin-top: 44px; padding-top: 22px; border-top: 1px solid var(--border);
332
+ display: flex; justify-content: space-between; gap: 16px; flex-wrap: wrap;
333
+ font-family: var(--mono); font-size: var(--step--1); color: var(--dim);
334
+ }
335
+
336
+
337
+ /* ══ THE GAME ENTRANCE ═════════════════════════════════════════════════════
338
+ #models is the site: a full-viewport character select. The chrome is the
339
+ circuit-rune asset set in /entrance (bg painting, pedestal ring, corner
340
+ brackets, divider) β€” monochrome SVGs applied through CSS masks so every
341
+ ornament tints to the class accent of the model on stage. Data unchanged:
342
+ every value is the registry row. */
343
+ #models { padding: 0; }
344
+ #models .cs-root {
345
+ position: relative;
346
+ min-height: 100dvh;
347
+ display: grid;
348
+ grid-template-columns: minmax(270px, 350px) 1fr minmax(250px, 320px);
349
+ grid-template-rows: auto 1fr auto auto;
350
+ grid-template-areas:
351
+ 'plate plate roster'
352
+ 'sheet stage roster'
353
+ 'enter enter roster'
354
+ 'note note note';
355
+ gap: 10px 24px;
356
+ padding: calc(var(--nav-h, 64px) + 14px) 30px 24px;
357
+ background:
358
+ radial-gradient(ellipse at 50% 92%,
359
+ color-mix(in srgb, var(--cs-accent, var(--accent)) 13%, transparent) 0%,
360
+ transparent 52%),
361
+ url('/entrance/bg.svg') center / cover no-repeat,
362
+ var(--bg);
363
+ overflow: hidden;
364
+ }
365
+
366
+ /* Atmosphere: two fog bands drifting opposite ways, and a dust field. All
367
+ accent-neutral; the accent lives in the radial wash above. */
368
+ .cs-fog { position: absolute; inset: 0; pointer-events: none; z-index: 0; }
369
+ .cs-fog i {
370
+ position: absolute; width: 90vw; height: 34vh; border-radius: 50%;
371
+ background: radial-gradient(ellipse at center, #18223a55 0%, transparent 65%);
372
+ filter: blur(30px);
373
+ }
374
+ .cs-fog i:nth-child(1) { left: -20vw; bottom: 4vh; animation: fogA 46s ease-in-out infinite alternate; }
375
+ .cs-fog i:nth-child(2) { right: -25vw; bottom: 12vh; animation: fogB 58s ease-in-out infinite alternate; }
376
+ @keyframes fogA { from { transform: translateX(0); } to { transform: translateX(9vw); } }
377
+ @keyframes fogB { from { transform: translateX(0); } to { transform: translateX(-11vw); } }
378
+ .cs-dust {
379
+ position: absolute; inset: -10% 0 0 0; pointer-events: none; z-index: 0; opacity: 0.5;
380
+ background-image:
381
+ radial-gradient(1.2px 1.2px at 12% 30%, #cfe0ff88 50%, transparent 51%),
382
+ radial-gradient(1px 1px at 34% 68%, #cfe0ff66 50%, transparent 51%),
383
+ radial-gradient(1.4px 1.4px at 58% 22%, #cfe0ff77 50%, transparent 51%),
384
+ radial-gradient(1px 1px at 71% 55%, #cfe0ff55 50%, transparent 51%),
385
+ radial-gradient(1.2px 1.2px at 86% 34%, #cfe0ff77 50%, transparent 51%),
386
+ radial-gradient(1px 1px at 45% 84%, #cfe0ff44 50%, transparent 51%);
387
+ background-size: 900px 700px;
388
+ animation: dust 90s linear infinite;
389
+ }
390
+ @keyframes dust { from { background-position: 0 0; } to { background-position: -900px 350px; } }
391
+
392
+ /* Nameplate: display face over an ornamental divider. */
393
+ #models .mb-plate {
394
+ grid-area: plate; position: relative; z-index: 1; justify-self: center;
395
+ text-align: center; margin-top: 4px; padding: 0 26px 14px;
396
+ border: 0; background: none;
397
+ }
398
+ #models .mb-name {
399
+ font-family: var(--display); font-size: clamp(1.9rem, 3.6vw, 2.9rem);
400
+ line-height: 1.05; text-shadow: 0 2px 24px #000c;
401
+ }
402
+ #models .mb-params {
403
+ font-family: var(--mono); font-size: var(--step--1); letter-spacing: 0.14em;
404
+ text-transform: uppercase; margin-top: 6px;
405
+ color: color-mix(in srgb, var(--cs-accent, var(--accent)) 75%, var(--text));
406
+ }
407
+ #models .mb-plate::after {
408
+ content: ''; display: block; height: 16px; margin: 10px auto 0; width: min(360px, 80%);
409
+ background: color-mix(in srgb, var(--cs-accent, var(--accent)) 80%, #fff);
410
+ -webkit-mask: url('/entrance/divider.svg') center / contain no-repeat;
411
+ mask: url('/entrance/divider.svg') center / contain no-repeat;
412
+ }
413
+
414
+ /* The stage. */
415
+ #models .mb-stage { grid-area: stage; display: flex; align-items: stretch; gap: 4px; min-height: 0; position: relative; z-index: 1; }
416
+ #models .mb-art { flex: 1; padding: 0; display: flex; align-items: center; justify-content: center; position: relative; }
417
+ #models .mb-mascot { height: min(54dvh, 500px); max-width: 100%; position: relative; z-index: 2; }
418
+ #models .mb-pedestal {
419
+ position: absolute; left: 50%; bottom: 2%; transform: translateX(-50%);
420
+ width: min(560px, 86%); height: 150px; border-radius: 0; filter: none;
421
+ background: radial-gradient(ellipse at center,
422
+ color-mix(in srgb, var(--cs-accent, var(--accent)) 30%, transparent) 0%,
423
+ color-mix(in srgb, var(--cs-accent, var(--accent)) 8%, transparent) 52%,
424
+ transparent 70%);
425
+ }
426
+ #models .mb-pedestal::after {
427
+ content: ''; position: absolute; inset: 0;
428
+ background: color-mix(in srgb, var(--cs-accent, var(--accent)) 85%, #fff);
429
+ -webkit-mask: url('/entrance/ring.svg') center bottom / 100% auto no-repeat;
430
+ mask: url('/entrance/ring.svg') center bottom / 100% auto no-repeat;
431
+ animation: ringPulse 5s ease-in-out infinite;
432
+ }
433
+ @keyframes ringPulse { 0%, 100% { opacity: 0.85; } 50% { opacity: 0.5; } }
434
+ #models .mb-arrow {
435
+ align-self: center; width: 52px; height: 76px; border-radius: 12px;
436
+ background: transparent; border: 1px solid transparent; font-size: 34px;
437
+ color: var(--dim); z-index: 2;
438
+ }
439
+ #models .mb-arrow:hover { color: var(--cs-accent, var(--accent)); border-color: var(--border); background: #0b101cbb; }
440
+
441
+ /* The sheet: glass plate with rune corners. */
442
+ #models .mb-info { grid-area: sheet; align-self: center; z-index: 1; }
443
+ #models .mb-panel {
444
+ position: relative; border: 1px solid color-mix(in srgb, var(--cs-accent, var(--accent)) 22%, var(--border));
445
+ border-radius: 4px; padding: 20px 20px 16px;
446
+ background: linear-gradient(180deg, #0c1220ee, #090e18e6);
447
+ box-shadow: inset 0 1px 0 #ffffff0a, 0 18px 50px #0009;
448
+ backdrop-filter: blur(4px);
449
+ }
450
+ #models .mb-panel::before, #models .mb-panel::after {
451
+ content: ''; position: absolute; width: 40px; height: 40px;
452
+ background: color-mix(in srgb, var(--cs-accent, var(--accent)) 85%, #fff);
453
+ -webkit-mask: url('/entrance/corner.svg') 0 0 / contain no-repeat;
454
+ mask: url('/entrance/corner.svg') 0 0 / contain no-repeat;
455
+ }
456
+ #models .mb-panel::before { top: -5px; left: -5px; }
457
+ #models .mb-panel::after { bottom: -5px; right: -5px; transform: rotate(180deg); }
458
+ #models .mb-row-label {
459
+ font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.16em;
460
+ color: color-mix(in srgb, var(--cs-accent, var(--accent)) 55%, var(--dim));
461
+ text-transform: uppercase; margin: 14px 0 7px;
462
+ }
463
+ #models .mb-row-label:first-child { margin-top: 0; }
464
+ #models .mb-stats { display: block; margin: 12px 0 2px; }
465
+ #models .mb-stats > div {
466
+ display: flex; justify-content: space-between; align-items: baseline;
467
+ padding: 6px 2px; border-bottom: 1px solid #ffffff0d;
468
+ }
469
+ #models .mb-stats dt {
470
+ font-family: var(--mono); font-size: 0.66rem; letter-spacing: 0.12em; color: var(--dim);
471
+ }
472
+ #models .mb-stats dd {
473
+ margin: 0; font-family: var(--mono); font-size: var(--step-0);
474
+ color: color-mix(in srgb, var(--cs-accent, var(--accent)) 45%, var(--text));
475
+ }
476
+ #models .mb-variant {
477
+ border-radius: 3px; border-color: #ffffff1c; background: #ffffff07;
478
+ }
479
+ #models .mb-variant[aria-selected="true"] {
480
+ border-color: var(--cs-accent, var(--accent));
481
+ background: color-mix(in srgb, var(--cs-accent, var(--accent)) 18%, transparent);
482
+ color: color-mix(in srgb, var(--cs-accent, var(--accent)) 70%, #fff);
483
+ }
484
+ #models .mb-cached { color: var(--ok); font-size: var(--step--1); margin: 10px 0 0; }
485
+ #models .mb-ram { font-size: var(--step--1); margin: 8px 0 0; }
486
+
487
+ /* Roster rail. */
488
+ #models .cs-roster-head {
489
+ grid-area: roster; align-self: start; z-index: 1;
490
+ font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.18em;
491
+ text-transform: uppercase; color: var(--dim);
492
+ padding: 2px 0 0 16px; height: 20px;
493
+ }
494
+ #models .mb-roster {
495
+ grid-area: roster; z-index: 1; margin: 26px 0 0; padding: 0 4px 4px 14px;
496
+ display: flex; flex-direction: column; gap: 8px; justify-content: flex-start;
497
+ overflow-y: auto; border-left: 1px solid #ffffff12;
498
+ scrollbar-width: thin; scrollbar-color: var(--border-hi) transparent;
499
+ }
500
+ #models .mb-dot {
501
+ width: 100%; height: auto; min-height: 64px; padding: 7px 10px;
502
+ display: flex; flex-direction: row; align-items: center; gap: 11px;
503
+ text-align: left; border-radius: 4px; cursor: pointer;
504
+ border: 1px solid #ffffff14;
505
+ background: linear-gradient(180deg, #ffffff08, transparent 60%), #0a0f1acc;
506
+ transition: border-color 0.15s, transform 0.15s, box-shadow 0.15s;
507
+ position: relative;
508
+ }
509
+ #models .mb-dot:hover { border-color: var(--muted); transform: translateX(-2px); }
510
+ #models .mb-dot[aria-selected="true"] {
511
+ border-color: var(--cs-accent, var(--accent));
512
+ box-shadow: inset 0 0 0 1px var(--cs-accent, var(--accent)),
513
+ 0 0 22px color-mix(in srgb, var(--cs-accent, var(--accent)) 30%, transparent);
514
+ }
515
+ #models .mb-dot[data-cached]::after {
516
+ content: 'READY'; position: absolute; top: 5px; right: 7px;
517
+ font-family: var(--mono); font-size: 0.5rem; letter-spacing: 0.12em;
518
+ color: var(--ok); border: 1px solid color-mix(in srgb, var(--ok) 55%, transparent);
519
+ border-radius: 3px; padding: 1px 4px;
520
+ }
521
+ .mb-dot-face {
522
+ flex: 0 0 46px; height: 46px; border-radius: 4px;
523
+ background: var(--thumb, radial-gradient(circle at 50% 40%, var(--border-hi) 0%, transparent 60%))
524
+ center / cover no-repeat, #0d1322;
525
+ border: 1px solid #ffffff10;
526
+ }
527
+ .mb-dot-text { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
528
+ .mb-dot-text b { font-size: 0.8rem; font-weight: 600; color: var(--text);
529
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
530
+ .mb-dot-text i { font-style: normal; font-family: var(--mono); font-size: 0.6rem;
531
+ color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
532
+
533
+ /* ENTER. */
534
+ #models .cs-enter { grid-area: enter; display: flex; justify-content: center; padding: 4px 0 2px; z-index: 1; }
535
+ #models .cs-enter .mb-cta {
536
+ min-width: 340px; text-align: center; letter-spacing: 0.14em; text-transform: uppercase;
537
+ font-weight: 700; font-size: var(--step-0); padding: 0.95rem 1.4rem;
538
+ border-radius: 4px; position: relative; overflow: hidden;
539
+ color: #0b0d10;
540
+ background: linear-gradient(180deg,
541
+ color-mix(in srgb, var(--cs-accent, var(--accent)) 80%, #fff) 0%,
542
+ var(--cs-accent, var(--accent)) 55%,
543
+ color-mix(in srgb, var(--cs-accent, var(--accent)) 70%, #000) 100%);
544
+ border: 1px solid color-mix(in srgb, var(--cs-accent, var(--accent)) 60%, #fff);
545
+ box-shadow: 0 10px 34px color-mix(in srgb, var(--cs-accent, var(--accent)) 40%, transparent),
546
+ inset 0 1px 0 #ffffff66;
547
+ }
548
+ #models .cs-enter .mb-cta:hover { filter: brightness(1.1); }
549
+ #models .cs-enter .mb-cta::after {
550
+ content: ''; position: absolute; top: 0; bottom: 0; width: 40%;
551
+ left: -60%; transform: skewX(-20deg);
552
+ background: linear-gradient(90deg, transparent, #ffffff55, transparent);
553
+ transition: left 0.5s ease;
554
+ }
555
+ #models .cs-enter .mb-cta:hover::after { left: 130%; }
556
+ #models .cs-note { grid-area: note; text-align: center; z-index: 1; }
557
+
558
+ /* The logo opening. */
559
+ .cs-splash {
560
+ position: absolute; inset: 0; z-index: 30;
561
+ display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 18px;
562
+ background: radial-gradient(ellipse at 50% 42%, #0d1424 0%, var(--bg) 70%);
563
+ animation: csSplashOut 2.6s ease forwards;
564
+ }
565
+ .cs-emblem {
566
+ width: 84px; height: 84px; color: var(--accent);
567
+ animation: csEmblem 1.4s cubic-bezier(0.16, 1, 0.3, 1) both;
568
+ }
569
+ .cs-logo {
570
+ font-family: var(--display); font-size: clamp(3rem, 9vw, 6.5rem); color: var(--text);
571
+ animation: csLogoIn 1.6s cubic-bezier(0.16, 1, 0.3, 1) both;
572
+ }
573
+ .cs-logo b { color: var(--accent); }
574
+ .cs-sub {
575
+ font-family: var(--mono); font-size: 0.78rem; letter-spacing: 0.22em;
576
+ text-transform: uppercase; color: var(--dim);
577
+ animation: csSub 1s ease 0.7s both;
578
+ }
579
+ @keyframes csEmblem {
580
+ 0% { opacity: 0; transform: rotate(-90deg) scale(0.6); }
581
+ 100% { opacity: 1; transform: rotate(0deg) scale(1); }
582
+ }
583
+ @keyframes csLogoIn {
584
+ 0% { opacity: 0; transform: scale(1.22); letter-spacing: 0.16em; filter: blur(12px); }
585
+ 55% { opacity: 1; filter: blur(0); }
586
+ 100% { opacity: 1; transform: scale(1); letter-spacing: 0.01em; }
587
+ }
588
+ @keyframes csSub { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
589
+ @keyframes csSplashOut { 0%, 78% { opacity: 1; } 100% { opacity: 0; visibility: hidden; } }
590
+
591
+ @media (prefers-reduced-motion: reduce) {
592
+ .cs-fog i, .cs-dust, #models .mb-pedestal::after { animation: none; }
593
+ }
594
+
595
+ /* Phones: stage first, roster as a strip, sheet, enter. */
596
+ @media (max-width: 900px) {
597
+ #models .cs-root {
598
+ grid-template-columns: 1fr;
599
+ grid-template-areas: 'plate' 'stage' 'roster' 'sheet' 'enter' 'note';
600
+ padding: calc(var(--nav-h, 64px) + 8px) 14px 18px;
601
+ gap: 8px;
602
+ }
603
+ #models .mb-mascot { height: min(36dvh, 300px); }
604
+ #models .cs-roster-head { display: none; }
605
+ #models .mb-roster {
606
+ flex-direction: row; overflow-x: auto; overflow-y: hidden; margin-top: 0;
607
+ border-left: 0; border-top: 1px solid #ffffff12; padding: 10px 2px 4px;
608
+ }
609
+ #models .mb-dot { width: auto; min-width: 168px; flex: 0 0 auto; }
610
+ #models .cs-enter .mb-cta { min-width: 0; width: 100%; }
611
+ }
612
+
613
+
614
+ /* ══ ROUND TWO: depth, transitions, identity ═══════════════════════════════ */
615
+
616
+ /* The floor reflection β€” Chromium's box-reflect, which is the browser class
617
+ the engine requires anyway. Elsewhere it simply does not draw. */
618
+ #models .mb-mascot {
619
+ -webkit-box-reflect: below -6px linear-gradient(transparent 62%, rgba(255,255,255,0.10));
620
+ }
621
+
622
+ /* Selection transition. go() re-arms .cs-in, so switching characters flashes
623
+ the pedestal and walks the plate and sheet back in β€” selection FEELS like
624
+ selection. Names are transform/opacity only; nothing relayouts. */
625
+ #models .mb-art.cs-in .mb-pedestal::after { animation: ringFlash 0.7s ease-out, ringPulse 5s ease-in-out 0.7s infinite; }
626
+ @keyframes ringFlash {
627
+ 0% { opacity: 1; transform: scale(0.92); filter: brightness(2.2); }
628
+ 100% { opacity: 0.85; transform: scale(1); filter: brightness(1); }
629
+ }
630
+ #models .mb-art.cs-in .mb-mascot { animation: charIn 0.5s cubic-bezier(0.2, 1.4, 0.4, 1); }
631
+ @keyframes charIn {
632
+ 0% { opacity: 0; transform: translateY(14px) scale(0.965); }
633
+ 100% { opacity: 1; transform: translateY(0) scale(1); }
634
+ }
635
+ #models .mb-plate.cs-in .mb-name { animation: plateIn 0.45s ease both; }
636
+ #models .mb-plate.cs-in .mb-params { animation: plateIn 0.45s ease 0.08s both; }
637
+ @keyframes plateIn {
638
+ 0% { opacity: 0; transform: translateY(-8px); letter-spacing: 0.08em; }
639
+ 100% { opacity: 1; transform: translateY(0); }
640
+ }
641
+ #models .mb-panel.cs-in > * { animation: rowIn 0.4s ease both; }
642
+ #models .mb-panel.cs-in > *:nth-child(2) { animation-delay: 0.05s; }
643
+ #models .mb-panel.cs-in > *:nth-child(3) { animation-delay: 0.10s; }
644
+ #models .mb-panel.cs-in > *:nth-child(4) { animation-delay: 0.15s; }
645
+ #models .mb-panel.cs-in > *:nth-child(5) { animation-delay: 0.20s; }
646
+ #models .mb-panel.cs-in > *:nth-child(n+6) { animation-delay: 0.25s; }
647
+ @keyframes rowIn {
648
+ 0% { opacity: 0; transform: translateX(-10px); }
649
+ 100% { opacity: 1; transform: translateX(0); }
650
+ }
651
+
652
+ /* Class sigil beside the class line. */
653
+ #models .mb-params { display: inline-flex; align-items: center; gap: 8px; justify-content: center; }
654
+ .mb-sigil { display: inline-flex; width: 16px; height: 16px; }
655
+ .mb-sigil svg { width: 100%; height: 100%; }
656
+
657
+ /* Realm corners: tiny mono statements, both literally true. */
658
+ .cs-corner {
659
+ position: absolute; bottom: 10px; z-index: 1;
660
+ font-family: var(--mono); font-size: 0.58rem; letter-spacing: 0.14em;
661
+ text-transform: uppercase; color: color-mix(in srgb, var(--dim) 70%, transparent);
662
+ pointer-events: none;
663
+ }
664
+ .cs-corner-l { left: 16px; }
665
+ .cs-corner-r { right: 16px; }
666
+
667
+ /* READY glint: a light pass over the tag every few seconds β€” unlocked should
668
+ glimmer. Masked to the tag only. */
669
+ #models .mb-dot[data-cached]::after { overflow: hidden; }
670
+ #models .mb-dot[data-cached] { isolation: isolate; }
671
+ #models .mb-dot[data-cached]::before {
672
+ content: ''; position: absolute; top: 3px; right: 5px; width: 44px; height: 14px;
673
+ background: linear-gradient(110deg, transparent 30%, #ffffff2e 50%, transparent 70%) no-repeat;
674
+ background-size: 200% 100%;
675
+ animation: readyGlint 4.5s ease-in-out infinite;
676
+ pointer-events: none; z-index: 1;
677
+ }
678
+ @keyframes readyGlint {
679
+ 0%, 72% { background-position: 180% 0; }
680
+ 92%, 100% { background-position: -80% 0; }
681
+ }
682
+
683
+ /* Enter: charge on hover, visible keyboard focus. */
684
+ #models .cs-enter .mb-cta { transition: transform 0.18s ease, filter 0.18s ease, box-shadow 0.18s ease; }
685
+ #models .cs-enter .mb-cta:hover {
686
+ transform: translateY(-1px) scale(1.015);
687
+ box-shadow: 0 14px 44px color-mix(in srgb, var(--cs-accent, var(--accent)) 55%, transparent),
688
+ inset 0 1px 0 #ffffff77;
689
+ }
690
+ #models .cs-enter .mb-cta:focus-visible {
691
+ outline: 2px solid #fff; outline-offset: 3px;
692
+ }
693
+
694
+ /* Parallax layers move on transform β€” give them will-change and keep the
695
+ fog's own drift composed with the pointer offset via nesting (the <i>
696
+ children animate, the container translates). */
697
+ .cs-fog, .cs-dust, #models .mb-art { will-change: transform; transition: transform 0.3s ease-out; }
698
+
699
+ @media (prefers-reduced-motion: reduce) {
700
+ #models .mb-art.cs-in .mb-mascot,
701
+ #models .mb-plate.cs-in .mb-name, #models .mb-plate.cs-in .mb-params,
702
+ #models .mb-panel.cs-in > *, #models .mb-dot[data-cached]::before { animation: none; }
703
+ }
704
+
705
+
706
+ /* ══ ROUNDS 3-6: the aura, the juice, the frame ════════════════════════════ */
707
+
708
+ /* The aura: motes of the class light rising off the pedestal, their pace set
709
+ by the model's measured rate (--cs-tempo β€” the same number the mascot's
710
+ idle clock reads). A fast little model boils; a 35B smoulders. */
711
+ .cs-aura { position: absolute; left: 50%; bottom: 8%; width: min(480px, 70%); height: 60%;
712
+ transform: translateX(-50%); pointer-events: none; z-index: 1; }
713
+ .cs-aura i {
714
+ position: absolute; bottom: 0; width: 4px; height: 4px; border-radius: 50%;
715
+ background: color-mix(in srgb, var(--cs-accent, var(--accent)) 85%, #fff);
716
+ opacity: 0;
717
+ animation: mote calc(5.2s / var(--cs-tempo, 1)) ease-in infinite;
718
+ }
719
+ .cs-aura i:nth-child(1) { left: 6%; animation-delay: 0.0s; }
720
+ .cs-aura i:nth-child(2) { left: 16%; animation-delay: 1.1s; width: 3px; height: 3px; }
721
+ .cs-aura i:nth-child(3) { left: 27%; animation-delay: 2.3s; }
722
+ .cs-aura i:nth-child(4) { left: 38%; animation-delay: 0.6s; width: 5px; height: 5px; }
723
+ .cs-aura i:nth-child(5) { left: 50%; animation-delay: 1.7s; width: 3px; height: 3px; }
724
+ .cs-aura i:nth-child(6) { left: 61%; animation-delay: 2.9s; }
725
+ .cs-aura i:nth-child(7) { left: 70%; animation-delay: 0.3s; }
726
+ .cs-aura i:nth-child(8) { left: 80%; animation-delay: 1.9s; width: 3px; height: 3px; }
727
+ .cs-aura i:nth-child(9) { left: 89%; animation-delay: 2.6s; }
728
+ .cs-aura i:nth-child(10) { left: 95%; animation-delay: 1.3s; width: 5px; height: 5px; }
729
+ @keyframes mote {
730
+ 0% { opacity: 0; transform: translateY(0) scale(1); }
731
+ 12% { opacity: 0.9; }
732
+ 100% { opacity: 0; transform: translateY(-46vh) scale(0.4); }
733
+ }
734
+
735
+ /* ARMED: a cached character's circle runs a rotating highlight β€” the same
736
+ OPFS truth as the READY tag, drawn as ceremony. Conic light clipped by the
737
+ very ring mask. */
738
+ #models .mb-art[data-armed] .mb-pedestal::before {
739
+ content: ''; position: absolute; inset: 0;
740
+ background: conic-gradient(from 0deg,
741
+ transparent 0deg, transparent 290deg,
742
+ color-mix(in srgb, var(--cs-accent, var(--accent)) 90%, #fff) 330deg,
743
+ transparent 360deg);
744
+ -webkit-mask: url('/entrance/ring.svg') center bottom / 100% auto no-repeat;
745
+ mask: url('/entrance/ring.svg') center bottom / 100% auto no-repeat;
746
+ animation: armedSpin 6s linear infinite;
747
+ }
748
+ @keyframes armedSpin { to { transform: rotate(360deg); } }
749
+
750
+ /* Lore under the nameplate: computed from the spec, styled as inscription. */
751
+ .cs-lore {
752
+ margin-top: 8px; font-size: 0.8rem; font-style: italic;
753
+ color: color-mix(in srgb, var(--text) 62%, transparent);
754
+ letter-spacing: 0.02em;
755
+ }
756
+
757
+ /* Juice: chips stamp when chosen, roster cards press, ENTER presses. */
758
+ #models .mb-variant[aria-selected="true"] { animation: stamp 0.22s ease; }
759
+ @keyframes stamp { 0% { transform: scale(1.08); } 100% { transform: scale(1); } }
760
+ #models .mb-dot:active { transform: translateX(-2px) scale(0.98); }
761
+ #models .cs-enter .mb-cta:active { transform: translateY(1px) scale(0.995); box-shadow: 0 6px 20px color-mix(in srgb, var(--cs-accent, var(--accent)) 35%, transparent); }
762
+
763
+ /* The cinematic frame: a breath of dark at the top and bottom edges, so the
764
+ scene reads composed rather than cropped. */
765
+ #models .cs-root::before, #models .cs-root::after {
766
+ content: ''; position: absolute; left: 0; right: 0; height: 90px;
767
+ pointer-events: none; z-index: 2;
768
+ }
769
+ #models .cs-root::before { top: 0; background: linear-gradient(180deg, #05070dcc, transparent); }
770
+ #models .cs-root::after { bottom: 0; background: linear-gradient(0deg, #05070d99, transparent); }
771
+
772
+ /* Screen-reader announcer. */
773
+ .cs-live { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
774
+
775
+ @media (prefers-reduced-motion: reduce) {
776
+ .cs-aura i, #models .mb-art[data-armed] .mb-pedestal::before,
777
+ #models .mb-variant[aria-selected="true"] { animation: none; }
778
+ .cs-aura i { opacity: 0.35; transform: translateY(-8vh); }
779
+ }
780
+
781
+
782
+ /* ══ ROUND 7: THE ENVIRONMENT ══════════════════════════════════════════════
783
+ The realm gains architecture. Silhouette layers between backdrop and
784
+ stage, each on its own parallax rate: obelisk skyline far, rune columns
785
+ near, the character floating between them under an aerial ring. */
786
+ .cs-spires {
787
+ position: absolute; left: -3%; right: -3%; bottom: 0; height: 42vh;
788
+ background: url('/entrance/spires.svg') bottom center / cover no-repeat;
789
+ opacity: 0.85; pointer-events: none; z-index: 0;
790
+ }
791
+ .cs-col {
792
+ position: absolute; bottom: -2vh; width: min(240px, 16vw); height: 92vh;
793
+ background: url('/entrance/column.svg') bottom / contain no-repeat;
794
+ pointer-events: none; z-index: 1; opacity: 0.9;
795
+ }
796
+ .cs-col-l { left: max(6px, 2vw); }
797
+ .cs-col-r { right: max(6px, 2vw); transform: scaleX(-1); }
798
+
799
+ /* The aerial ring: a great slow wheel behind the character, in the class
800
+ light at whisper opacity. The armed floor ring is ceremony; this is
801
+ architecture. */
802
+ .cs-backring {
803
+ position: absolute; left: 50%; top: 46%; width: min(64dvh, 620px); aspect-ratio: 1;
804
+ translate: -50% -50%;
805
+ background: color-mix(in srgb, var(--cs-accent, var(--accent)) 70%, #fff);
806
+ -webkit-mask: url('/entrance/backring.svg') center / contain no-repeat;
807
+ mask: url('/entrance/backring.svg') center / contain no-repeat;
808
+ opacity: 0.14; z-index: 0;
809
+ animation: backringSpin 80s linear infinite;
810
+ }
811
+ @keyframes backringSpin { to { rotate: 360deg; } }
812
+
813
+ /* Nameplate banner wings behind the name. */
814
+ #models .mb-plate { padding-top: 10px; }
815
+ .cs-banner {
816
+ position: absolute; left: 50%; top: 16px; transform: translateX(-50%);
817
+ width: min(640px, 92vw); height: 80px; pointer-events: none;
818
+ background: color-mix(in srgb, var(--cs-accent, var(--accent)) 65%, #fff);
819
+ -webkit-mask: url('/entrance/banner.svg') center / contain no-repeat;
820
+ mask: url('/entrance/banner.svg') center / contain no-repeat;
821
+ opacity: 0.5; z-index: -1;
822
+ }
823
+
824
+ /* Portrait frames on the roster faces. */
825
+ .mb-dot-face { position: relative; }
826
+ .mb-dot-face::after {
827
+ content: ''; position: absolute; inset: -3px;
828
+ background: color-mix(in srgb, var(--cs-accent, var(--accent)) 55%, #9fb4d8);
829
+ -webkit-mask: url('/entrance/pframe.svg') center / contain no-repeat;
830
+ mask: url('/entrance/pframe.svg') center / contain no-repeat;
831
+ opacity: 0.35; transition: opacity 0.15s;
832
+ }
833
+ #models .mb-dot[aria-selected="true"] .mb-dot-face::after,
834
+ #models .mb-dot:hover .mb-dot-face::after { opacity: 0.9; }
835
+
836
+ /* The cinematic edges upgraded: the dark breath now carries the rune border
837
+ strip along its inner line. */
838
+ #models .cs-root::before {
839
+ background:
840
+ linear-gradient(180deg, #05070dcc, transparent),
841
+ linear-gradient(180deg, transparent 76px, transparent 76px);
842
+ }
843
+ #models .cs-root > .cs-edge-t, #models .cs-root > .cs-edge-b { display: none; }
844
+ #models .cs-root::after {
845
+ background: linear-gradient(0deg, #05070d99, transparent);
846
+ }
847
+ .cs-borderline-t, .cs-borderline-b {
848
+ position: absolute; left: 8%; right: 8%; height: 12px; z-index: 2; pointer-events: none;
849
+ background: color-mix(in srgb, var(--cs-accent, var(--accent)) 40%, #6c7f9f);
850
+ -webkit-mask: url('/entrance/border.svg') center / auto 100% repeat-x;
851
+ mask: url('/entrance/border.svg') center / auto 100% repeat-x;
852
+ opacity: 0.4;
853
+ }
854
+ .cs-borderline-t { top: calc(var(--nav-h, 64px) + 2px); }
855
+ .cs-borderline-b { bottom: 8px; }
856
+
857
+ /* The splash's ring burst: the aerial ring blooming open behind the emblem. */
858
+ .cs-splash { position: absolute; }
859
+ .cs-splash-ring {
860
+ position: absolute; width: 420px; aspect-ratio: 1;
861
+ background: var(--accent);
862
+ -webkit-mask: url('/entrance/backring.svg') center / contain no-repeat;
863
+ mask: url('/entrance/backring.svg') center / contain no-repeat;
864
+ animation: splashRing 2.2s cubic-bezier(0.16, 1, 0.3, 1) both;
865
+ opacity: 0.22;
866
+ }
867
+ @keyframes splashRing {
868
+ 0% { transform: scale(0.4) rotate(-40deg); opacity: 0; }
869
+ 40% { opacity: 0.3; }
870
+ 100% { transform: scale(1.05) rotate(8deg); opacity: 0.18; }
871
+ }
872
+
873
+ /* The realm's cursor: a small trace-diamond. Entrance only. */
874
+ #models .cs-root, #models .cs-root a, #models .cs-root button {
875
+ cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 22 22"><path d="M11 1 L21 11 L11 21 L1 11 Z" fill="none" stroke="white" stroke-width="2"/><path d="M11 7 L15 11 L11 15 L7 11 Z" fill="white"/></svg>') 11 11, auto;
876
+ }
877
+
878
+ @media (prefers-reduced-motion: reduce) {
879
+ .cs-backring, .cs-splash-ring { animation: none; }
880
+ }
881
+ @media (max-width: 900px) {
882
+ .cs-col { display: none; }
883
+ .cs-spires { height: 26vh; opacity: 0.6; }
884
+ .cs-backring { width: min(78vw, 380px); top: 40%; }
885
+ .cs-banner { height: 56px; opacity: 0.4; }
886
+ .cs-borderline-t, .cs-borderline-b { display: none; }
887
+ }
888
+
889
+
890
+ /* ══ ROUND 8: THE ALIGNMENT PASS ═══════════════════════════════════════════
891
+ One rhythm: an 8px gutter system, a hard max-width so ultrawide screens
892
+ frame the scene instead of stretching it, the sheet and roster hung from
893
+ the same top line, and the enter slab locked to the stage's centre line. */
894
+ #models .cs-root {
895
+ --cs-gutter: 24px;
896
+ max-width: 1720px; margin: 0 auto;
897
+ grid-template-columns: minmax(280px, 340px) minmax(0, 1fr) minmax(260px, 310px);
898
+ gap: 8px var(--cs-gutter);
899
+ padding: calc(var(--nav-h, 64px) + 12px) max(28px, 4vw) 22px;
900
+ }
901
+ /* Sheet and roster share a top line: both start where the stage starts. */
902
+ #models .mb-info { align-self: start; padding-top: 34px; }
903
+ #models .cs-roster-head { padding-top: 8px; height: 26px; }
904
+ #models .mb-roster { margin-top: 34px; }
905
+ /* The plate is the one centred element; give it a fixed rhythm below. */
906
+ #models .mb-plate { margin: 2px 0 0; padding-bottom: 6px; }
907
+ /* Enter hangs centred under the stage column only, not the whole row β€”
908
+ optically aligned with the character above it. */
909
+ #models .cs-enter { grid-column: 2; justify-self: center; }
910
+ #models .cs-corner-l { left: max(28px, 4vw); }
911
+ #models .cs-corner-r { right: max(28px, 4vw); }
912
+ /* Border strips share the content inset. */
913
+ .cs-borderline-t, .cs-borderline-b { left: max(28px, 4vw); right: max(28px, 4vw); }
914
+ /* Stat rows: value column right-aligned on a shared edge, tabular digits. */
915
+ #models .mb-stats dd { font-variant-numeric: tabular-nums; text-align: right; }
916
+ #models .mb-stats dt { padding-right: 12px; }
917
+ /* Chips wrap on the same 8px rhythm. */
918
+ #models .mb-variants, #models .mb-modes { gap: 8px; }
919
+ @media (max-width: 900px) {
920
+ #models .cs-enter { grid-column: 1; justify-self: stretch; }
921
+ #models .mb-info { padding-top: 0; }
922
+ #models .mb-roster { margin-top: 0; }
923
+ }
924
+
925
+ /* ══ ROUND 15: THE PARTY READS AS CLASSES ══ */
926
+ #models .mb-dot { padding-right: 26px; }
927
+ .mb-dot-sigil {
928
+ position: absolute; right: 7px; bottom: 6px; width: 13px; height: 13px; opacity: 0.75;
929
+ }
930
+ .mb-dot-sigil svg { width: 100%; height: 100%; }
931
+
932
+ /* ══ ROUND 17: THE SPLASH YIELDS ══ */
933
+ .cs-splash { cursor: pointer; }
934
+
935
+
936
+ /* ══ ROUND 9: ATTENTION ══ */
937
+ #models .mb-art { transition: rotate 0.4s ease, transform 0.3s ease-out; perspective: 900px; }
938
+
939
+ /* ══ ROUND 11: THE SELECTION WIPE ══ A sheet of the class light crossing the
940
+ stage on every selection β€” the cut between characters reads as a cut. */
941
+ .cs-wipe {
942
+ position: absolute; inset: 0; z-index: 3; pointer-events: none; opacity: 0;
943
+ background: linear-gradient(100deg, transparent 30%,
944
+ color-mix(in srgb, var(--cs-accent, var(--accent)) 26%, transparent) 50%,
945
+ transparent 70%);
946
+ transform: translateX(-100%);
947
+ }
948
+ .cs-wipe.cs-go { animation: csWipe 0.55s ease-out; }
949
+ @keyframes csWipe {
950
+ 0% { opacity: 1; transform: translateX(-100%); }
951
+ 100% { opacity: 1; transform: translateX(100%); }
952
+ }
953
+
954
+ /* ══ ROUND 16: THE ENGAGE ══ Clicking ENTER flashes the realm in the class
955
+ light and falls to black before the chat page takes over β€” an exit that
956
+ answers the entrance. Reduced motion (and modified clicks) skip it. */
957
+ .cs-engage {
958
+ position: absolute; inset: 0; z-index: 40; pointer-events: none; opacity: 0;
959
+ background:
960
+ radial-gradient(ellipse at 50% 60%,
961
+ color-mix(in srgb, var(--cs-accent, var(--accent)) 45%, transparent) 0%, transparent 60%),
962
+ #05070d;
963
+ }
964
+ .cs-engage.cs-go { animation: csEngage 0.34s ease-in forwards; }
965
+ @keyframes csEngage {
966
+ 0% { opacity: 0; }
967
+ 45% { opacity: 0.65; }
968
+ 100% { opacity: 1; }
969
+ }
970
+ @media (prefers-reduced-motion: reduce) { .cs-wipe.cs-go, .cs-engage.cs-go { animation: none; } }
971
+
972
+
973
+ /* ══ ROUND 10: STAT ICONOGRAPHY + HONEST BARS ══ Icons name the stat; the
974
+ bars are within-character fractions only (context: shipped ceiling over
975
+ the checkpoint's own max; memory: the build's residency) β€” never a
976
+ cross-model ranking. */
977
+ .mb-stat-ico { display: inline-flex; width: 12px; height: 12px; margin-right: 7px;
978
+ vertical-align: -1px; color: color-mix(in srgb, var(--cs-accent, var(--accent)) 60%, var(--dim)); }
979
+ .mb-stat-ico svg { width: 100%; height: 100%; }
980
+ .mb-bar { display: block; height: 3px; margin-top: 5px; border-radius: 2px;
981
+ background: #ffffff12; overflow: hidden; }
982
+ .mb-bar i { display: block; height: 100%; border-radius: 2px;
983
+ background: linear-gradient(90deg,
984
+ color-mix(in srgb, var(--cs-accent, var(--accent)) 55%, transparent),
985
+ var(--cs-accent, var(--accent)));
986
+ transition: width 0.45s cubic-bezier(0.22, 1, 0.36, 1); }
987
+ #models .mb-stats dd { min-width: 128px; }
988
+
989
+ /* ══ ROUND 13: THE LOADOUT GAUGE ══ Filled diamonds for what the build
990
+ keeps, hollow for what it streams β€” ordinal, not a measurement. */
991
+ .mb-gauge { font-size: 0.6rem; letter-spacing: 0.08em; margin-right: 7px;
992
+ color: color-mix(in srgb, var(--cs-accent, var(--accent)) 70%, var(--dim)); }
993
+
994
+
995
+ /* ══ ROUND 12: IDLE LIFE ══ Left alone, the realm breathes: the stage sways
996
+ a few pixels on a long period and the aura thickens slightly. Any input
997
+ snaps the camera back to the hand. */
998
+ #models .cs-root.cs-idle .mb-art { animation: idleSway 14s ease-in-out infinite; }
999
+ @keyframes idleSway {
1000
+ 0%, 100% { transform: translate(0, 0); }
1001
+ 25% { transform: translate(6px, -3px); }
1002
+ 75% { transform: translate(-6px, 2px); }
1003
+ }
1004
+ #models .cs-root.cs-idle .cs-aura i { animation-duration: calc(3.8s / var(--cs-tempo, 1)); }
1005
+
1006
+ /* ══ ROUND 14: REALM WEATHER ══ A falling trace crosses the high sky every
1007
+ ~16 s β€” thin, fast, gone. And the fog drinks a hint of the class light, so
1008
+ the weather itself changes when the character does. */
1009
+ .cs-comet {
1010
+ position: absolute; top: 12%; left: -8%; width: 130px; height: 2px;
1011
+ background: linear-gradient(90deg, transparent, #cfe0ffbb 60%, #fff);
1012
+ border-radius: 2px; opacity: 0; pointer-events: none; z-index: 0;
1013
+ transform: rotate(9deg);
1014
+ animation: comet 16s linear infinite;
1015
+ animation-delay: 5s;
1016
+ }
1017
+ @keyframes comet {
1018
+ 0%, 93% { opacity: 0; left: -8%; top: 12%; }
1019
+ 94% { opacity: 0.9; }
1020
+ 100% { opacity: 0; left: 108%; top: 26%; }
1021
+ }
1022
+ .cs-fog i {
1023
+ background: radial-gradient(ellipse at center,
1024
+ color-mix(in srgb, var(--cs-accent, var(--accent)) 12%, #18223a) 0%, transparent 65%);
1025
+ opacity: 0.36;
1026
+ }
1027
+ @media (prefers-reduced-motion: reduce) {
1028
+ .cs-comet, #models .cs-root.cs-idle .mb-art { animation: none; }
1029
+ }
1030
+
1031
+
1032
+ /* ══ CHAT MODE ══ ENTER pressed: the roster and the sheet step aside, the
1033
+ conversation takes the right column, and the character stays on stage β€”
1034
+ summoning heartbeat while the weights land, faster heartbeat while it
1035
+ thinks, one bounce per real token while it speaks. Requires chat-ui.css
1036
+ (linked by index.html) for the message bubbles and the composer; below is
1037
+ only the panel chrome and the stagecraft. */
1038
+
1039
+ #models .cs-root.cs-chatting {
1040
+ /* The nav is sticky IN FLOW (60px + border), so a flat 100dvh hangs the
1041
+ composer 61px below the fold. In chat mode the composer is the primary
1042
+ control β€” the chamber must end above it. The tightened padding-top gives
1043
+ the reclaimed room to the conversation. */
1044
+ height: calc(100dvh - 61px);
1045
+ min-height: 0;
1046
+ padding-top: 22px;
1047
+ grid-template-columns: 1fr minmax(420px, 600px);
1048
+ grid-template-rows: auto 1fr auto;
1049
+ grid-template-areas:
1050
+ 'plate chat'
1051
+ 'stage chat'
1052
+ 'note chat';
1053
+ }
1054
+ /* #models-prefixed: the base rules carry the #models id, and a bare class
1055
+ pair loses that specificity fight β€” the roster stayed visible and its
1056
+ orphaned grid-area spawned implicit tracks that crushed the real columns
1057
+ (caught by the first browser pass). */
1058
+ #models .cs-chatting .mb-info, #models .cs-chatting .mb-roster,
1059
+ #models .cs-chatting .cs-roster-head, #models .cs-chatting .cs-enter,
1060
+ #models .cs-chatting .mb-arrow, #models .cs-chatting .cs-corner-r { display: none; }
1061
+
1062
+ /* The engage flash no longer hands off to another page β€” it lifts to reveal
1063
+ the chamber. Starts opaque (the .cs-go fade ran under it), holds long
1064
+ enough to cover the swap, then clears. */
1065
+ #models .cs-root.cs-chatting .cs-engage.cs-go { animation: csEngageReveal 0.9s ease forwards; }
1066
+ @keyframes csEngageReveal {
1067
+ 0%, 30% { opacity: 1; }
1068
+ 100% { opacity: 0; visibility: hidden; }
1069
+ }
1070
+
1071
+ /* The panel: the sheet's glass and rune corners, grown into an instrument. */
1072
+ #models .cs-chat {
1073
+ /* min-width 0: grid items default to min-content width, and the nowrap
1074
+ composer-hint line was wider than a phone β€” the panel overflowed the
1075
+ viewport instead of shrinking into its column. */
1076
+ grid-area: chat; z-index: 2; min-height: 0; min-width: 0; position: relative;
1077
+ /* It is a <section>, and the page's generic section rule pads 72px into
1078
+ the panel β€” zero it; the head/composer carry their own spacing. */
1079
+ padding: 0;
1080
+ display: flex; flex-direction: column;
1081
+ border: 1px solid color-mix(in srgb, var(--cs-accent, var(--accent)) 22%, var(--border));
1082
+ border-radius: 4px;
1083
+ background: linear-gradient(180deg, #0c1220ee, #090e18e6);
1084
+ box-shadow: inset 0 1px 0 #ffffff0a, 0 18px 50px #0009;
1085
+ backdrop-filter: blur(4px);
1086
+ animation: csChatIn 0.5s cubic-bezier(0.2, 1, 0.3, 1) 0.1s both;
1087
+ }
1088
+ @keyframes csChatIn {
1089
+ from { opacity: 0; transform: translateX(26px); }
1090
+ to { opacity: 1; transform: translateX(0); }
1091
+ }
1092
+ #models .cs-chat::before, #models .cs-chat::after {
1093
+ content: ''; position: absolute; width: 40px; height: 40px; z-index: 3; pointer-events: none;
1094
+ background: color-mix(in srgb, var(--cs-accent, var(--accent)) 85%, #fff);
1095
+ -webkit-mask: url('/entrance/corner.svg') 0 0 / contain no-repeat;
1096
+ mask: url('/entrance/corner.svg') 0 0 / contain no-repeat;
1097
+ }
1098
+ #models .cs-chat::before { top: -5px; left: -5px; }
1099
+ #models .cs-chat::after { bottom: -5px; right: -5px; transform: rotate(180deg); }
1100
+
1101
+ /* Panel header: sigil, name, build, live badge, tools. */
1102
+ .cs-chat-head {
1103
+ display: flex; align-items: center; gap: 10px;
1104
+ padding: 12px 14px; border-bottom: 1px solid #ffffff0f;
1105
+ }
1106
+ .cs-chat-sigil { flex: none; width: 22px; height: 22px;
1107
+ color: var(--cs-accent, var(--accent)); }
1108
+ .cs-chat-sigil svg { width: 100%; height: 100%; }
1109
+ .cs-chat-id { display: flex; flex-direction: column; gap: 1px; min-width: 0; margin-right: auto; }
1110
+ .cs-chat-id b { font-family: var(--display); font-weight: 600; font-size: 1.02rem;
1111
+ line-height: 1.15; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1112
+ .cs-chat-id i { font-style: normal; font-family: var(--mono); font-size: 0.58rem;
1113
+ letter-spacing: 0.12em; text-transform: uppercase;
1114
+ color: color-mix(in srgb, var(--cs-accent, var(--accent)) 70%, var(--text));
1115
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1116
+ .cs-chat-tool {
1117
+ flex: none; font-family: var(--mono); font-size: 0.62rem; letter-spacing: 0.1em;
1118
+ text-transform: uppercase; color: var(--dim); text-decoration: none;
1119
+ padding: 5px 9px; border: 1px solid #ffffff1c; border-radius: 3px; background: #ffffff07;
1120
+ cursor: pointer; transition: color 0.15s, border-color 0.15s;
1121
+ }
1122
+ .cs-chat-tool:hover { color: var(--text); border-color: var(--muted); }
1123
+ .cs-chat-tool:disabled { opacity: 0.4; cursor: not-allowed; }
1124
+
1125
+ /* Live badge β€” written by loading-ui.ts (Summoning β†’ % β†’ Ready). */
1126
+ .cs-chat .badge {
1127
+ flex: none; display: inline-flex; align-items: center; gap: 6px;
1128
+ font-family: var(--mono); font-size: 0.6rem; letter-spacing: 0.1em; text-transform: uppercase;
1129
+ padding: 4px 9px; border: 1px solid #ffffff1c; border-radius: 999px; color: var(--dim);
1130
+ font-variant-numeric: tabular-nums; white-space: nowrap;
1131
+ }
1132
+ .cs-chat .badge .dot { width: 6px; height: 6px; border-radius: 50%; background: var(--muted); }
1133
+ .cs-chat .badge.loading { color: #fbbf24; border-color: #fbbf2440; }
1134
+ .cs-chat .badge.loading .dot { background: #fbbf24; animation: csBadgePulse 1.4s infinite; }
1135
+ .cs-chat .badge.ready { color: var(--cs-accent, var(--accent));
1136
+ border-color: color-mix(in srgb, var(--cs-accent, var(--accent)) 40%, transparent); }
1137
+ .cs-chat .badge.ready .dot { background: var(--cs-accent, var(--accent)); }
1138
+ .cs-chat .badge.error { color: var(--err); border-color: color-mix(in srgb, var(--err) 45%, transparent); }
1139
+ .cs-chat .badge.error .dot { background: var(--err); }
1140
+ @keyframes csBadgePulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
1141
+
1142
+ /* The rite card: download + compile progress, then it folds away. */
1143
+ .cs-boot { padding: 16px 16px 14px; border-bottom: 1px solid #ffffff0f; }
1144
+ .cs-boot.cs-done { display: none; }
1145
+ .cs-boot-title { font-family: var(--display); font-size: 1.05rem; margin-bottom: 2px; }
1146
+ .cs-boot-status { font-size: 0.78rem; color: var(--text-dim, #a9b3c6); min-height: 1.2em; margin-bottom: 10px; }
1147
+ .cs-boot-track { height: 4px; border-radius: 2px; background: #ffffff12; overflow: hidden; }
1148
+ .cs-boot-track i {
1149
+ display: block; height: 100%; width: 0; border-radius: 2px;
1150
+ background: linear-gradient(90deg,
1151
+ color-mix(in srgb, var(--cs-accent, var(--accent)) 55%, transparent),
1152
+ var(--cs-accent, var(--accent)));
1153
+ transition: width 0.25s ease;
1154
+ }
1155
+ .cs-boot-detail { margin-top: 7px; font-family: var(--mono); font-size: 0.64rem;
1156
+ color: var(--dim); font-variant-numeric: tabular-nums; }
1157
+ .cs-boot-note { margin-top: 7px; font-size: 0.72rem; color: var(--warn, #d9a441); }
1158
+ .cs-boot-log { margin-top: 9px; }
1159
+ .cs-boot-log summary {
1160
+ font-family: var(--mono); font-size: 0.6rem; letter-spacing: 0.14em; text-transform: uppercase;
1161
+ color: var(--dim); cursor: pointer; user-select: none;
1162
+ }
1163
+ .cs-boot-log summary:hover { color: var(--text-dim, #a9b3c6); }
1164
+ .cs-boot-log pre {
1165
+ margin-top: 6px; max-height: 130px; overflow-y: auto;
1166
+ padding: 8px 10px; border: 1px solid #ffffff0f; border-radius: 3px; background: #070b13;
1167
+ font-family: var(--mono); font-size: 0.62rem; line-height: 1.5; color: var(--text-dim, #a9b3c6);
1168
+ white-space: pre-wrap;
1169
+ }
1170
+ .cs-boot-error { display: none; margin-top: 10px; padding: 9px 11px;
1171
+ border: 1px solid color-mix(in srgb, var(--err) 40%, transparent); border-radius: 3px;
1172
+ background: color-mix(in srgb, var(--err) 9%, transparent);
1173
+ font-size: 0.76rem; line-height: 1.5; color: color-mix(in srgb, var(--err) 60%, #fff); }
1174
+ .cs-boot-error.visible { display: block; }
1175
+
1176
+ /* Conversation column: bubbles ride the panel glass, not their own ground. */
1177
+ .cs-chat .chat-main { flex: 1; min-height: 0; }
1178
+ .cs-chat .welcome.cs-wait, .cs-chat .welcome.hidden { display: none; }
1179
+ .cs-chat .welcome { padding: 1.4rem 0.2rem 1rem; }
1180
+ .cs-welcome-title { font-family: var(--display); font-size: 1.3rem; margin-bottom: 6px; }
1181
+ .cs-welcome-lore { font-size: 0.8rem; font-style: italic; line-height: 1.55;
1182
+ color: color-mix(in srgb, var(--text) 62%, transparent); margin-bottom: 14px; }
1183
+ .cs-sug-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
1184
+ .cs-chat .suggest {
1185
+ text-align: left; font-size: 0.76rem; line-height: 1.4; color: var(--text-dim, #a9b3c6);
1186
+ padding: 10px 12px; border: 1px solid #ffffff14; border-radius: 3px; background: #ffffff06;
1187
+ cursor: pointer; transition: border-color 0.15s, color 0.15s, background 0.15s;
1188
+ }
1189
+ .cs-chat .suggest:hover {
1190
+ color: var(--text); background: color-mix(in srgb, var(--cs-accent, var(--accent)) 8%, transparent);
1191
+ border-color: color-mix(in srgb, var(--cs-accent, var(--accent)) 45%, transparent);
1192
+ }
1193
+
1194
+ /* Composer, re-cut to the entrance's edge language (chat-ui.css carries the
1195
+ mechanics; only the chrome changes here). */
1196
+ .cs-chat .composer-wrap { background: transparent; border-top: 1px solid #ffffff0f; padding: 11px 13px 10px; }
1197
+ .cs-chat .composer { background: #0a101ccc; border-radius: 3px;
1198
+ border-color: color-mix(in srgb, var(--cs-accent, var(--accent)) 26%, var(--border)); }
1199
+ .cs-chat .composer:focus-within { background: #0c1322; border-color: var(--cs-accent, var(--accent)); }
1200
+ .cs-chat .composer-btn { border-radius: 3px; }
1201
+ .cs-chat .composer-hint {
1202
+ display: flex; justify-content: space-between; gap: 4px 10px; flex-wrap: wrap; text-align: left;
1203
+ font-family: var(--mono); font-size: 0.6rem; letter-spacing: 0.06em; color: var(--dim);
1204
+ max-width: none;
1205
+ }
1206
+ .cs-chat-stats { font-variant-numeric: tabular-nums; white-space: nowrap; }
1207
+
1208
+ /* ── Stagecraft: what the realm does while the model works ──────────────── */
1209
+
1210
+ /* Summoning: the circle beats slowly and the ring runs the armed sweep β€” the
1211
+ character is being drawn through. */
1212
+ #models .cs-summoning .mb-pedestal::after { animation: ringPulse 1.5s ease-in-out infinite; }
1213
+ #models .cs-summoning .mb-pedestal::before {
1214
+ content: ''; position: absolute; inset: 0;
1215
+ background: conic-gradient(from 0deg,
1216
+ transparent 0deg, transparent 290deg,
1217
+ color-mix(in srgb, var(--cs-accent, var(--accent)) 90%, #fff) 330deg,
1218
+ transparent 360deg);
1219
+ -webkit-mask: url('/entrance/ring.svg') center bottom / 100% auto no-repeat;
1220
+ mask: url('/entrance/ring.svg') center bottom / 100% auto no-repeat;
1221
+ animation: armedSpin 2.6s linear infinite;
1222
+ }
1223
+ /* Thinking: prefill β€” the ring tightens to a fast shallow breath. */
1224
+ #models .cs-thinking .mb-pedestal::after { animation: ringPulse 0.9s ease-in-out infinite; }
1225
+ #models .cs-thinking .mb-pedestal { filter: brightness(1.18); }
1226
+ /* Generating: the floor burns steady and bright; the bounce on stage is the
1227
+ token stream itself (mascot.pulse per emitted token). */
1228
+ #models .cs-generating .mb-pedestal { filter: brightness(1.3); }
1229
+ #models .cs-generating .mb-pedestal::after { animation: none; opacity: 1; }
1230
+ #models .mb-pedestal { transition: filter 0.4s ease; }
1231
+
1232
+ /* Phones: the character folds to a small familiar above the conversation. */
1233
+ @media (max-width: 900px) {
1234
+ #models .cs-root.cs-chatting {
1235
+ grid-template-columns: 1fr;
1236
+ grid-template-rows: auto auto 1fr;
1237
+ grid-template-areas: 'plate' 'stage' 'chat';
1238
+ }
1239
+ #models .cs-chatting .mb-mascot { height: 110px; -webkit-box-reflect: none; }
1240
+ #models .cs-chatting .mb-pedestal { height: 60px; }
1241
+ #models .cs-chatting .cs-lore { display: none; }
1242
+ #models .cs-chatting .mb-plate { padding-bottom: 4px; }
1243
+ #models .cs-chatting .mb-name { font-size: 1.3rem; }
1244
+ .cs-sug-grid { grid-template-columns: 1fr; }
1245
+ .cs-chat-tool#new-chat-btn { display: none; }
1246
+ }
1247
+ @media (prefers-reduced-motion: reduce) {
1248
+ #models .cs-chat, #models .cs-root.cs-chatting .cs-engage.cs-go { animation: none; }
1249
+ #models .cs-summoning .mb-pedestal::before { animation: none; }
1250
+ }
1251
+
1252
+ /* ══ DOWNLOADED = READY ══ The READY tag gains a fill so it reads at roster
1253
+ distance, and a variant chip that is already on this device carries a dot β€”
1254
+ both from the same OPFS probe as everything else. */
1255
+ #models .mb-dot[data-cached]::after {
1256
+ background: color-mix(in srgb, var(--ok) 16%, transparent);
1257
+ }
1258
+ #models .mb-variant[data-cached]::after {
1259
+ content: '●'; margin-left: 7px; font-size: 0.5rem; vertical-align: 2px;
1260
+ color: var(--ok);
1261
+ }
1262
+
1263
+ /* ══ GAME ROUND: abilities, saves, listening, the token stream ═════════════ */
1264
+
1265
+ /* Abilities: the spec's mechanics as passives β€” rune bullet, name, clause. */
1266
+ #models .mb-abilities { list-style: none; margin: 12px 0 0; padding: 0;
1267
+ display: flex; flex-direction: column; gap: 6px; }
1268
+ #models .mb-abilities li { display: flex; align-items: baseline; gap: 8px;
1269
+ font-size: 0.74rem; line-height: 1.4; }
1270
+ #models .mb-abilities li::before { content: 'β—†'; flex: none; font-size: 0.5rem;
1271
+ color: color-mix(in srgb, var(--cs-accent, var(--accent)) 80%, #fff); }
1272
+ #models .mb-abilities b { flex: none; font-weight: 600; letter-spacing: 0.02em;
1273
+ color: color-mix(in srgb, var(--cs-accent, var(--accent)) 55%, var(--text)); }
1274
+ #models .mb-abilities span { color: var(--dim); }
1275
+
1276
+ /* Save slot: a stored conversation marks its character β€” roster glyph +
1277
+ sheet line. Presence only, never a count. */
1278
+ #models .mb-dot[data-save] .mb-dot-text b::after {
1279
+ content: ' β—ˆ'; font-size: 0.7em; vertical-align: 1px;
1280
+ color: #cfe0ffaa;
1281
+ }
1282
+ #models .mb-save { font-size: var(--step--1); margin: 10px 0 0;
1283
+ color: color-mix(in srgb, var(--cs-accent, var(--accent)) 60%, var(--text)); }
1284
+
1285
+ /* LISTENING: keys are landing β€” the character turns toward the panel. The
1286
+ ears perk through the mascot's hover state; this is the body language. */
1287
+ #models .cs-chatting.cs-listening .mb-art { rotate: y 9deg; }
1288
+ #models .mb-art { transition: transform 0.3s ease-out, rotate 0.45s ease; }
1289
+
1290
+ /* THE TOKEN STREAM: while the model speaks, glyphs fly from the character's
1291
+ mouth into the conversation. Visible ONLY under cs-generating β€” the stream
1292
+ exists exactly when tokens do. */
1293
+ .cs-stream { display: none; }
1294
+ #models .cs-chatting.cs-generating .cs-stream { display: block;
1295
+ position: absolute; left: 26%; right: 0; top: 0; bottom: 0;
1296
+ pointer-events: none; z-index: 2; }
1297
+ .cs-stream i {
1298
+ position: absolute; left: 0; top: 44%;
1299
+ font-style: normal; font-size: 11px;
1300
+ color: color-mix(in srgb, var(--cs-accent, var(--accent)) 85%, #fff);
1301
+ text-shadow: 0 0 6px color-mix(in srgb, var(--cs-accent, var(--accent)) 60%, transparent);
1302
+ opacity: 0;
1303
+ animation: tokFly 1.15s linear infinite;
1304
+ }
1305
+ .cs-stream i:nth-child(2) { animation-delay: 0.19s; font-size: 8px; top: 45.5%; }
1306
+ .cs-stream i:nth-child(3) { animation-delay: 0.38s; top: 43%; }
1307
+ .cs-stream i:nth-child(4) { animation-delay: 0.57s; font-size: 8px; top: 46%; }
1308
+ .cs-stream i:nth-child(5) { animation-delay: 0.76s; top: 44.5%; }
1309
+ .cs-stream i:nth-child(6) { animation-delay: 0.95s; font-size: 9px; top: 43.5%; }
1310
+ @keyframes tokFly {
1311
+ 0% { transform: translateX(0) translateY(0); opacity: 0; }
1312
+ 12% { opacity: 0.9; }
1313
+ 55% { transform: translateX(22vw) translateY(-1.2vh); opacity: 0.85; }
1314
+ 92% { opacity: 0.5; }
1315
+ 100% { transform: translateX(38vw) translateY(-0.6vh); opacity: 0; }
1316
+ }
1317
+
1318
+ /* SUMMON FAILURE: the circle goes cold. The rite card carries the flavour
1319
+ line; the diagnostic stays in the error panel. */
1320
+ #models .cs-boot-failed .mb-pedestal { filter: grayscale(0.7) brightness(0.55); }
1321
+ #models .cs-boot-failed .mb-pedestal::after { animation: none; opacity: 0.3; }
1322
+ #models .cs-boot-failed .mb-pedestal::before { display: none; }
1323
+
1324
+ @media (max-width: 900px) {
1325
+ /* The stream's geometry assumes stage-left/panel-right; stacked phones
1326
+ have neither, and the abilities pad the sheet. */
1327
+ .cs-stream { display: none !important; }
1328
+ }
1329
+ @media (prefers-reduced-motion: reduce) {
1330
+ .cs-stream { display: none !important; }
1331
+ #models .cs-chatting.cs-listening .mb-art { rotate: none; }
1332
+ }
1333
+
1334
+ /* ══ EXTENDED ROUND: sleep, the constellation, the deeds rail ══════════════ */
1335
+
1336
+ /* ASLEEP (50 s idle): the ring cools and slows with the character β€” the
1337
+ sleepy face itself is the mascot's mood, this is the room's light. */
1338
+ #models .cs-asleep .mb-pedestal { filter: brightness(0.7) saturate(0.8); }
1339
+ #models .cs-asleep .mb-pedestal::after { animation-duration: 10s; }
1340
+ #models .cs-asleep .cs-fog i { opacity: 0.2; }
1341
+
1342
+ /* THE CONSTELLATION: the lane's sigil, huge and faint, behind the stage.
1343
+ Masked accent β€” it recolours with the character and drifts on parallax. */
1344
+ .cs-sigilbg {
1345
+ position: absolute; z-index: 0; pointer-events: none;
1346
+ left: 50%; top: 50%;
1347
+ width: min(74vh, 58vw); height: min(74vh, 58vw);
1348
+ margin: calc(min(74vh, 58vw) / -2) 0 0 calc(min(74vh, 58vw) / -2);
1349
+ background: color-mix(in srgb, var(--cs-accent, var(--accent)) 80%, #fff);
1350
+ opacity: 0.05;
1351
+ -webkit-mask-size: contain; mask-size: contain;
1352
+ -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
1353
+ -webkit-mask-position: center; mask-position: center;
1354
+ }
1355
+ #models .cs-chatting .cs-sigilbg { left: 27%; }
1356
+
1357
+ /* THE DEEDS RAIL: things that happened on this device. Unearned = a dim rune
1358
+ with the trigger in its tooltip (a quest list, not decoration); earned =
1359
+ lit, named. No counts anywhere. */
1360
+ #models .cs-deeds {
1361
+ grid-area: note; z-index: 1; justify-self: center;
1362
+ display: flex; flex-wrap: wrap; gap: 6px 14px; align-items: center;
1363
+ padding: 2px 0;
1364
+ }
1365
+ .cs-deed {
1366
+ font-family: var(--mono); font-size: 0.56rem; letter-spacing: 0.12em;
1367
+ text-transform: uppercase; color: color-mix(in srgb, var(--dim) 55%, transparent);
1368
+ cursor: default;
1369
+ }
1370
+ .cs-deed i { font-style: normal; display: none; margin-left: 5px; }
1371
+ .cs-deed[data-on] {
1372
+ color: color-mix(in srgb, var(--cs-accent, var(--accent)) 75%, #fff);
1373
+ text-shadow: 0 0 8px color-mix(in srgb, var(--cs-accent, var(--accent)) 45%, transparent);
1374
+ }
1375
+ .cs-deed[data-on] i { display: inline; }
1376
+ @media (max-width: 900px) { #models .cs-deeds { display: none; } }
1377
+
1378
+ /* ══ THE ROOM ══ Hosting from inside the game: the strip in the panel, and
1379
+ one small mascot per connected guest flanking the character. */
1380
+ .cs-room { padding: 12px 14px; border-bottom: 1px solid #ffffff0f; }
1381
+ .cs-room-consent p { font-size: 0.76rem; line-height: 1.55; color: var(--text-dim, #a9b3c6);
1382
+ margin: 0 0 10px; }
1383
+ .cs-room-linkrow { display: flex; gap: 6px; align-items: center; }
1384
+ .cs-room-linkrow input {
1385
+ flex: 1; min-width: 0; padding: 6px 9px; font-family: var(--mono); font-size: 0.66rem;
1386
+ color: var(--text); background: #0a101ccc; border-radius: 3px;
1387
+ border: 1px solid color-mix(in srgb, var(--cs-accent, var(--accent)) 30%, var(--border));
1388
+ }
1389
+ .cs-room-members { margin-top: 8px; font-family: var(--mono); font-size: 0.62rem;
1390
+ letter-spacing: 0.08em; color: var(--dim); }
1391
+ .cs-room-log { list-style: none; margin: 8px 0 0; padding: 0; display: flex;
1392
+ flex-direction: column; gap: 4px; }
1393
+ .cs-room-log li { display: flex; gap: 8px; align-items: baseline; font-size: 0.7rem;
1394
+ color: var(--text-dim, #a9b3c6); overflow: hidden; white-space: nowrap; }
1395
+ .cs-room-log b { font-family: var(--mono); font-size: 0.6rem; font-weight: 400;
1396
+ color: color-mix(in srgb, var(--cs-accent, var(--accent)) 70%, var(--text)); flex: none; }
1397
+ .cs-room-log span { overflow: hidden; text-overflow: ellipsis; }
1398
+ .cs-room-log i { font-style: normal; font-family: var(--mono); font-size: 0.6rem;
1399
+ color: var(--dim); margin-left: auto; flex: none; }
1400
+ .cs-tool-live { border-color: var(--cs-accent, var(--accent)) !important;
1401
+ color: color-mix(in srgb, var(--cs-accent, var(--accent)) 80%, #fff) !important; }
1402
+
1403
+ /* Guests on stage: small familiars on the ring's edge. */
1404
+ .cs-guest-mascot { position: absolute; width: 90px; height: 90px; z-index: 1;
1405
+ pointer-events: none; animation: guestIn 0.5s ease both; }
1406
+ .cs-guest-0 { left: 8%; bottom: 10%; }
1407
+ .cs-guest-1 { right: 8%; bottom: 10%; }
1408
+ .cs-guest-2 { left: 2%; bottom: 34%; }
1409
+ .cs-guest-3 { right: 2%; bottom: 34%; }
1410
+ .cs-guest-4 { left: 16%; bottom: 0; }
1411
+ .cs-guest-5 { right: 16%; bottom: 0; }
1412
+ @keyframes guestIn { from { opacity: 0; transform: translateY(10px) scale(0.8); }
1413
+ to { opacity: 1; transform: none; } }
1414
+ @media (max-width: 900px) { .cs-guest-mascot { display: none; } }
1415
+
1416
+ /* The room path under ENTER: quiet sibling, same class light. */
1417
+ #models .cs-enter { flex-direction: column; align-items: center; gap: 8px; }
1418
+ #models .mb-cta-room {
1419
+ font-family: var(--mono); font-size: 0.64rem; letter-spacing: 0.1em;
1420
+ text-transform: uppercase; text-decoration: none; cursor: pointer;
1421
+ color: color-mix(in srgb, var(--cs-accent, var(--accent)) 65%, var(--dim));
1422
+ padding: 4px 10px; border: 1px solid transparent; border-radius: 3px;
1423
+ transition: color 0.15s, border-color 0.15s;
1424
+ }
1425
+ #models .mb-cta-room:hover {
1426
+ color: color-mix(in srgb, var(--cs-accent, var(--accent)) 85%, #fff);
1427
+ border-color: color-mix(in srgb, var(--cs-accent, var(--accent)) 40%, transparent);
1428
+ }
1429
+
1430
+ /* ══ LENS ROUND 2026-08-17 FIXES ═══════════════════════════════════════════ */
1431
+
1432
+ /* The cinematic bottom fade must not wash the chat panel's composer β€” the
1433
+ panel IS the bottom of the screen in chat mode. */
1434
+ #models .cs-root.cs-chatting::after { display: none; }
1435
+
1436
+ @media (prefers-reduced-motion: reduce) {
1437
+ /* The mood states re-armed the ring pulse that the base reduce block had
1438
+ disabled; and guests should simply appear. */
1439
+ #models .cs-summoning .mb-pedestal::after,
1440
+ #models .cs-thinking .mb-pedestal::after { animation: none; }
1441
+ .cs-guest-mascot { animation: none; }
1442
+ }
og.png ADDED

Git LFS Details

  • SHA256: 6184b54547a35ce2a9547b86758bf65559983da776d51700bf2ae9b3fdea6366
  • Pointer size: 131 Bytes
  • Size of remote file: 111 kB