From 8495e2f33aac1a06021350925999ea42787a5c74 Mon Sep 17 00:00:00 2001
From: WpyQwq <3911625973@qq.com>
Date: Sat, 19 Sep 2026 11:59:24 +0800
Subject: [PATCH] =?UTF-8?q?Initial=20commit:=20Neural=20Trace=EF=BC=9A?=
=?UTF-8?q?=E8=A7=82=E5=AF=9F=E6=9C=AC=E5=9C=B0=20Qwen3.5-4B=20=E8=AE=A1?=
=?UTF-8?q?=E7=AE=97=E8=BF=87=E7=A8=8B=E7=9A=84=E6=9E=81=E7=AE=80=E5=8F=AF?=
=?UTF-8?q?=E8=A7=86=E5=8C=96=E5=B7=A5=E4=BD=9C=E5=8F=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 5 +
README.md | 47 ++
index.html | 13 +
package-lock.json | 827 ++++++++++++++++++++++++++++++++++
package.json | 18 +
server/__init__.py | 1 +
server/main.py | 149 ++++++
server/model_runtime.py | 512 +++++++++++++++++++++
server/requirements-model.txt | 4 +
server/requirements.txt | 2 +
src/App.jsx | 515 +++++++++++++++++++++
src/Trace3DCanvas.jsx | 150 ++++++
src/main.jsx | 10 +
src/styles.css | 311 +++++++++++++
vite.config.js | 12 +
15 files changed, 2576 insertions(+)
create mode 100644 .gitignore
create mode 100644 README.md
create mode 100644 index.html
create mode 100644 package-lock.json
create mode 100644 package.json
create mode 100644 server/__init__.py
create mode 100644 server/main.py
create mode 100644 server/model_runtime.py
create mode 100644 server/requirements-model.txt
create mode 100644 server/requirements.txt
create mode 100644 src/App.jsx
create mode 100644 src/Trace3DCanvas.jsx
create mode 100644 src/main.jsx
create mode 100644 src/styles.css
create mode 100644 vite.config.js
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7caaaa8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+dist/
+.venv/
+__pycache__/
+*.pyc
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..2f0cf67
--- /dev/null
+++ b/README.md
@@ -0,0 +1,47 @@
+# Neural Trace
+
+一个用于观察本地 Qwen3.5-4B 计算过程的极简可视化工作台。
+
+当前版本已完成:
+
+- 默认 3D trace space:32 层节点、DeltaNet 状态路径、Full Attention 连线、残差流和可点击节点
+- Qwen3.5 混合架构视图:DeltaNet / 线性注意力、Full Attention、Residual Stream、Logits
+- Token 流动、激活场、attention map、输出分布和事件时间轴
+- 可播放/暂停/调速/拖动的前向计算模拟器
+- `D:\watch\_LLM\_think` 模型目录健康检查
+- FastAPI SSE 遥测接口,已准备真实 Transformers forward/backward hooks
+- Forward、Backward、Attention、Delta State 四种观测模式
+- 可选图片输入:通过 POST SSE 将图像送入 Qwen3.5 Vision Encoder,并显示视觉输入/编码事件
+
+## 启动前端
+
+```powershell
+Set-Location 'E:\neural-dialogue-visualizer'
+npm install
+npm run dev
+```
+
+打开 `http://localhost:5173`。
+
+## 启动本地遥测服务
+
+```powershell
+Set-Location 'E:\neural-dialogue-visualizer'
+py -3.12 -m venv .venv
+.\.venv\Scripts\Activate.ps1
+pip install -r server\requirements.txt
+$env:MODEL_DIR = 'D:\watch\_LLM\_think'
+python -m uvicorn server.main:app --reload --port 8000
+```
+
+模型文件齐全后,如需启用真实 hook runner:
+
+```powershell
+pip install -r server\requirements-model.txt
+```
+
+真实运行会尝试懒加载本地 Transformers 模型,并在层级模块上注册 forward hook 和 backward hook。当前真实 hooks 路线使用 SafeTensors checkpoint;GGUF 适合 llama.cpp 推理,但不能直接提供这里所需的 PyTorch autograd 层级事件。4B 模型的 backward 需要较大的显存;如果量化 checkpoint 不支持 autograd,服务会保留 mock 流并返回明确的 runtime error。
+
+文本 trace 使用 GET `/api/stream?prompt=...&mode=...`;带图片时使用 POST `/api/stream`,请求体为 `{"prompt":"...","mode":"forward","image_data":"data:image/png;base64,..."}`。前端的回形针按钮会自动使用 POST 路径,图片限制为 4 MB。
+
+如果模型仍在下载,服务会返回 `mock` 状态,前端继续显示模拟 trace;文件齐全后会显示 `CHECKPOINT READY`。只有 Transformers hook runner 成功加载后才会显示 `MODEL READY`。界面展示的是可观测的激活、状态、注意力和梯度信号,不把隐藏推理文字冒充成“模型思想”。
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..713bbd0
--- /dev/null
+++ b/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Neural Trace
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..1913d0e
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,827 @@
+{
+ "name": "neural-dialogue-visualizer",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "neural-dialogue-visualizer",
+ "version": "0.1.0",
+ "dependencies": {
+ "@vitejs/plugin-react": "latest",
+ "lucide-react": "latest",
+ "react": "latest",
+ "react-dom": "latest",
+ "vite": "latest"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.147.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz",
+ "integrity": "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm-eabi": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz",
+ "integrity": "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.6.tgz",
+ "integrity": "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.6.tgz",
+ "integrity": "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.6.tgz",
+ "integrity": "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.6.tgz",
+ "integrity": "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.6.tgz",
+ "integrity": "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.6.tgz",
+ "integrity": "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.6.tgz",
+ "integrity": "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.6.tgz",
+ "integrity": "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.6.tgz",
+ "integrity": "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.6.tgz",
+ "integrity": "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.6.tgz",
+ "integrity": "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.6.tgz",
+ "integrity": "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.6.tgz",
+ "integrity": "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.6.tgz",
+ "integrity": "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.1.tgz",
+ "integrity": "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw==",
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "oxc-transform-react": "^0.145.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ },
+ "oxc-transform-react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "1.38.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.38.0.tgz",
+ "integrity": "sha512-xZCyBd/wiVUDactoCc+42TjL0aB7EBOXsuX+tjz+W/sGzw2KhHpL1NOH3FIaVUcpimvUBpIYfz34Ofj9S5JEzQ==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz",
+ "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.26",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
+ "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.17",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
+ "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
+ "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.8"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz",
+ "integrity": "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==",
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.147.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm-eabi": "1.2.6",
+ "@rolldown/binding-android-arm64": "1.2.6",
+ "@rolldown/binding-darwin-arm64": "1.2.6",
+ "@rolldown/binding-darwin-x64": "1.2.6",
+ "@rolldown/binding-freebsd-x64": "1.2.6",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.2.6",
+ "@rolldown/binding-linux-arm64-gnu": "1.2.6",
+ "@rolldown/binding-linux-arm64-musl": "1.2.6",
+ "@rolldown/binding-linux-ppc64-gnu": "1.2.6",
+ "@rolldown/binding-linux-s390x-gnu": "1.2.6",
+ "@rolldown/binding-linux-x64-gnu": "1.2.6",
+ "@rolldown/binding-linux-x64-musl": "1.2.6",
+ "@rolldown/binding-openharmony-arm64": "1.2.6",
+ "@rolldown/binding-win32-arm64-msvc": "1.2.6",
+ "@rolldown/binding-win32-x64-msvc": "1.2.6"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.2.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz",
+ "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==",
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.33.0",
+ "picomatch": "^4.0.5",
+ "postcss": "^8.5.26",
+ "rolldown": "~1.2.4",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.4.0 || ^0.5.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..5955e12
--- /dev/null
+++ b/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "neural-dialogue-visualizer",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --host 0.0.0.0",
+ "build": "vite build",
+ "preview": "vite preview --host 0.0.0.0"
+ },
+ "dependencies": {
+ "@vitejs/plugin-react": "latest",
+ "lucide-react": "latest",
+ "react": "latest",
+ "react-dom": "latest",
+ "vite": "latest"
+ }
+}
diff --git a/server/__init__.py b/server/__init__.py
new file mode 100644
index 0000000..b26c46e
--- /dev/null
+++ b/server/__init__.py
@@ -0,0 +1 @@
+"""Neural Trace local telemetry server."""
diff --git a/server/main.py b/server/main.py
new file mode 100644
index 0000000..5e548f4
--- /dev/null
+++ b/server/main.py
@@ -0,0 +1,149 @@
+"""Local telemetry service for Neural Trace.
+
+The service exposes one stable SSE contract for the browser. It uses the
+actual local checkpoint when the optional model runtime can load it; otherwise
+it emits clearly labelled mock events so the UI remains usable during download.
+"""
+
+from __future__ import annotations
+
+import asyncio
+import json
+import os
+from pathlib import Path
+from typing import Any, AsyncIterator
+
+from fastapi import FastAPI, Query
+from fastapi.middleware.cors import CORSMiddleware
+from fastapi.responses import StreamingResponse
+from pydantic import BaseModel
+
+from .model_runtime import ModelRuntime
+
+MODEL_DIR = Path(os.getenv("MODEL_DIR", r"D:\watch\_LLM\_think"))
+MODEL_NAME = os.getenv("MODEL_NAME", "Qwen3.5-4B")
+runtime = ModelRuntime(MODEL_DIR, MODEL_NAME)
+
+
+class StreamRequest(BaseModel):
+ prompt: str = "解释一下量子纠缠"
+ mode: str = "forward"
+ image_data: str | None = None
+
+app = FastAPI(title="Neural Trace Telemetry", version="0.2.0")
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=[
+ "http://localhost:5173",
+ "http://127.0.0.1:5173",
+ "http://localhost:5174",
+ "http://127.0.0.1:5174",
+ ],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+)
+
+
+@app.get("/api/health")
+def health() -> dict[str, Any]:
+ return {
+ "connected": runtime.ready,
+ "downloaded": runtime.checkpoint_ready,
+ "model": MODEL_NAME,
+ "modelPath": str(runtime.checkpoint_path or MODEL_DIR) if MODEL_DIR.exists() else "waiting for local model",
+ "mode": "model" if runtime.ready else "mock",
+ "runtime": runtime.status,
+ "error": runtime.error,
+ "architecture": runtime.architecture,
+ }
+
+
+async def mock_trace(prompt: str, mode: str, image_data: str | None = None) -> AsyncIterator[str]:
+ tokens = list(prompt[:10]) or ["·"]
+ stages = ["L01", "L04", "L08", "L12", "L16", "L20", "L24", "L28"]
+ if mode == "backward":
+ stages = list(reversed(stages))
+ if image_data:
+ vision_payload = {
+ "step": 0,
+ "kind": "vision_encoder",
+ "value": 0.72,
+ "shape": [1, vision_payload_size(image_data), 3],
+ "source": "mock",
+ "mode": mode,
+ }
+ yield f"data: {json.dumps(vision_payload, ensure_ascii=False)}\n\n"
+ for step in range(48):
+ if mode == "backward":
+ kind = "gradient"
+ elif mode == "attention":
+ kind = "attention_output"
+ elif mode == "state":
+ kind = "delta_state"
+ elif step % 16 == 11:
+ kind = "mtp_logits"
+ elif step % 12 == 3:
+ kind = "router_weights"
+ elif step % 12 == 7:
+ kind = "expert_mixture"
+ else:
+ kind = "full_attention" if step % 4 == 0 else "delta_state"
+ payload = {
+ "step": step,
+ "token": tokens[step % len(tokens)],
+ "layer": stages[step % len(stages)],
+ "kind": kind,
+ "value": round(0.2 + ((step * 17) % 70) / 100, 3),
+ "source": "mock",
+ "mode": mode,
+ }
+ yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
+ await asyncio.sleep(0.08)
+ suffix = ",并经过视觉编码分支" if image_data else ""
+ answer = f"演示输出:已完成对“{prompt}”的可观测计算{suffix}。真实模型接入后,这里会替换为本地 Qwen3.5-4B 的生成结果。"
+ yield f"data: {json.dumps({'step': 48, 'kind': 'generation', 'text': answer, 'value': 1.0, 'source': 'mock', 'mode': mode}, ensure_ascii=False)}\n\n"
+
+
+def vision_payload_size(image_data: str) -> int:
+ return 196
+
+
+async def runtime_trace(prompt: str, mode: str, image_data: str | None = None) -> AsyncIterator[str]:
+ events = await asyncio.to_thread(runtime.start_trace, prompt, mode, image_data)
+ if events is None:
+ if runtime.error:
+ yield f"data: {json.dumps({'step': 0, 'kind': 'runtime_error', 'error': runtime.error, 'source': 'model_runtime', 'mode': mode}, ensure_ascii=False)}\n\n"
+ async for event in mock_trace(prompt, mode, image_data):
+ yield event
+ return
+
+ while True:
+ payload = await asyncio.to_thread(events.get)
+ if payload.get("kind") == "done":
+ break
+ yield f"data: {json.dumps(payload, ensure_ascii=False)}\n\n"
+
+
+@app.get("/api/stream")
+async def stream(
+ prompt: str = Query(default="解释一下量子纠缠"),
+ mode: str = Query(default="forward"),
+) -> StreamingResponse:
+ allowed_modes = {"forward", "backward", "attention", "state"}
+ safe_mode = mode if mode in allowed_modes else "forward"
+ return StreamingResponse(
+ runtime_trace(prompt, safe_mode),
+ media_type="text/event-stream",
+ headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
+ )
+
+
+@app.post("/api/stream")
+async def stream_post(request: StreamRequest) -> StreamingResponse:
+ safe_mode = request.mode if request.mode in {"forward", "backward", "attention", "state"} else "forward"
+ return StreamingResponse(
+ runtime_trace(request.prompt.strip() or "解释一下量子纠缠", safe_mode, request.image_data),
+ media_type="text/event-stream",
+ headers={"Cache-Control": "no-cache", "Connection": "keep-alive"},
+ )
diff --git a/server/model_runtime.py b/server/model_runtime.py
new file mode 100644
index 0000000..ac0613e
--- /dev/null
+++ b/server/model_runtime.py
@@ -0,0 +1,512 @@
+"""Lazy local model adapter for observable forward and backward traces.
+
+The adapter is deliberately conservative: it summarizes tensors in the
+runtime process instead of sending activations or weights to the browser.
+Large checkpoints are loaded only after the first stream request and failures
+fall back to the mock event contract in ``main.py``.
+"""
+
+from __future__ import annotations
+
+import importlib.util
+import base64
+import json
+import queue
+import re
+import threading
+from io import BytesIO
+from pathlib import Path
+from typing import Any
+
+DEFAULT_ARCHITECTURE = {
+ "model_type": "qwen3_5",
+ "num_layers": 32,
+ "pattern": "8 × (3L + 1A)",
+ "hidden_size": 2560,
+ "vocab_size": 248320,
+ "context_length": 262144,
+ "intermediate_size": 9216,
+ "delta_heads_v": 32,
+ "delta_heads_qk": 16,
+ "attention_heads": 16,
+ "attention_kv_heads": 4,
+ "attention_head_dim": 256,
+ "rope_dim": 64,
+ "vision_encoder": True,
+ "sparse_moe": True,
+ "mtp": True,
+ "precision": "BF16 / INT4",
+}
+
+
+class ModelRuntime:
+ def __init__(self, model_dir: Path, model_name: str) -> None:
+ self.model_dir = model_dir
+ self.model_name = model_name
+ self.status = "idle"
+ self.error: str | None = None
+ self.model: Any = None
+ self.processor: Any = None
+ self._torch: Any = None
+ self._hooks: list[Any] = []
+ self._load_lock = threading.Lock()
+ self._trace_context = threading.local()
+
+ @property
+ def ready(self) -> bool:
+ return self.status == "ready" and self.model is not None
+
+ @property
+ def checkpoint_path(self) -> Path | None:
+ if not self.model_dir.exists():
+ return None
+ candidates = [self.model_dir]
+ candidates.extend(path for path in sorted(self.model_dir.iterdir()) if path.is_dir())
+ for path in candidates:
+ has_config = (path / "config.json").exists()
+ has_weights = any(path.glob("*.safetensors")) or any(path.glob("*.safetensors.index.json"))
+ if has_config and has_weights:
+ return path
+ return None
+
+ @property
+ def checkpoint_ready(self) -> bool:
+ return self.checkpoint_path is not None
+
+ @property
+ def architecture(self) -> dict[str, Any]:
+ architecture = dict(DEFAULT_ARCHITECTURE)
+ checkpoint_path = self.checkpoint_path
+ if checkpoint_path is None:
+ return architecture
+ try:
+ config = json.loads((checkpoint_path / "config.json").read_text(encoding="utf-8"))
+ except (OSError, ValueError, UnicodeDecodeError):
+ return architecture
+ text_config = config.get("text_config") if isinstance(config.get("text_config"), dict) else config
+ field_map = {
+ "num_hidden_layers": "num_layers",
+ "hidden_size": "hidden_size",
+ "vocab_size": "vocab_size",
+ "max_position_embeddings": "context_length",
+ "intermediate_size": "intermediate_size",
+ "num_attention_heads": "attention_heads",
+ "num_key_value_heads": "attention_kv_heads",
+ "head_dim": "attention_head_dim",
+ "rope_theta_dim": "rope_dim",
+ "model_type": "model_type",
+ }
+ for source_key, target_key in field_map.items():
+ value = text_config.get(source_key)
+ if isinstance(value, (int, float, str)):
+ architecture[target_key] = value
+ if not architecture.get("attention_head_dim") and architecture.get("attention_heads"):
+ architecture["attention_head_dim"] = architecture["hidden_size"] // architecture["attention_heads"]
+ return architecture
+
+ def ensure_loaded(self) -> bool:
+ if self.ready:
+ return True
+ checkpoint_path = self.checkpoint_path
+ if checkpoint_path is None:
+ self.status = "downloading"
+ return False
+
+ with self._load_lock:
+ if self.ready:
+ return True
+ self.status = "loading"
+ self.error = None
+ try:
+ import torch
+ from transformers import AutoModelForCausalLM, AutoProcessor
+
+ self._torch = torch
+ try:
+ self.processor = AutoProcessor.from_pretrained(checkpoint_path, local_files_only=True, trust_remote_code=True)
+ except Exception:
+ from transformers import AutoTokenizer
+
+ self.processor = AutoTokenizer.from_pretrained(checkpoint_path, local_files_only=True, trust_remote_code=True)
+
+ model_class = AutoModelForCausalLM
+ image_text_model = getattr(__import__("transformers", fromlist=["AutoModelForImageTextToText"]), "AutoModelForImageTextToText", None)
+ if image_text_model is not None:
+ model_class = image_text_model
+
+ load_kwargs = {
+ "local_files_only": True,
+ "trust_remote_code": True,
+ "torch_dtype": "auto",
+ }
+ if importlib.util.find_spec("accelerate") is not None:
+ load_kwargs.update(device_map="auto", low_cpu_mem_usage=True)
+
+ try:
+ self.model = model_class.from_pretrained(
+ checkpoint_path,
+ **load_kwargs,
+ )
+ except Exception:
+ self.model = AutoModelForCausalLM.from_pretrained(
+ checkpoint_path,
+ **load_kwargs,
+ )
+
+ self.model.eval()
+ self._install_hooks()
+ self.status = "ready"
+ return True
+ except Exception as exc: # pragma: no cover - depends on local GPU/runtime
+ self.status = "error"
+ self.error = f"{type(exc).__name__}: {exc}"
+ self.model = None
+ return False
+
+ def start_trace(self, prompt: str, mode: str, image_data: str | None = None) -> queue.Queue[dict[str, Any]] | None:
+ if not self.ensure_loaded():
+ return None
+ events: queue.Queue[dict[str, Any]] = queue.Queue()
+ worker = threading.Thread(target=self._trace_worker, args=(prompt, mode, image_data, events), daemon=True)
+ worker.start()
+ return events
+
+ def _install_hooks(self) -> None:
+ if self._hooks or self.model is None:
+ return
+
+ seen_layers: set[int] = set()
+ for name, module in self.model.named_modules():
+ match = re.search(r"(?:^|\.)(?:language_model\.)?(?:layers|(?:model|transformer|decoder)\.(?:layers|h))\.(\d+)$", name)
+ if match is not None:
+ layer_index = int(match.group(1))
+ if layer_index not in seen_layers:
+ seen_layers.add(layer_index)
+ self._hooks.append(module.register_forward_hook(self._make_forward_hook(layer_index)))
+ self._hooks.append(module.register_full_backward_hook(self._make_backward_hook(layer_index)))
+ elif re.search(r"(?:^|\.)\b(?:visual|vision_model|vision_tower)\b$", name):
+ self._hooks.append(module.register_forward_hook(self._make_vision_hook()))
+ elif re.search(r"(?:^|\.)\b(?:router|routing|gate|experts|expert_gate|mtp)\b$", name, re.IGNORECASE):
+ kind = "router_weights" if re.search(r"router|routing|gate", name, re.IGNORECASE) else "expert_mixture"
+ if re.search(r"mtp", name, re.IGNORECASE):
+ kind = "mtp_logits"
+ self._hooks.append(module.register_forward_hook(self._make_aux_hook(name, kind)))
+ elif re.search(r"(?:^|\.)lm_head$", name):
+ self._hooks.append(module.register_forward_hook(self._make_logits_hook()))
+
+ def _make_forward_hook(self, layer_index: int):
+ def forward_hook(_module: Any, _inputs: Any, output: Any) -> None:
+ context = getattr(self._trace_context, "queue", None)
+ if context is None:
+ return
+ tensor = self._first_tensor(output)
+ summary = self._summarize(tensor)
+ full_attention = (layer_index + 1) % 4 == 0
+ mode = getattr(self._trace_context, "mode", "forward")
+ step = getattr(self._trace_context, "step", 0)
+ self._trace_context.step = step + 1
+ if mode == "attention":
+ kind = "attention_output" if full_attention else "delta_context"
+ elif mode == "state":
+ kind = "delta_state"
+ else:
+ kind = "full_attention" if full_attention else "delta_state"
+ context.put({
+ "step": min(step, 511),
+ "layer": f"L{layer_index + 1:02d}",
+ "kind": kind,
+ "mode": mode,
+ "value": summary["rms"],
+ "mean": summary["mean"],
+ "shape": summary["shape"],
+ "source": "forward_hook",
+ })
+
+ return forward_hook
+
+ def _make_vision_hook(self):
+ def vision_hook(_module: Any, _inputs: Any, output: Any) -> None:
+ context = getattr(self._trace_context, "queue", None)
+ if context is None:
+ return
+ tensor = self._first_tensor(output)
+ summary = self._summarize(tensor)
+ step = getattr(self._trace_context, "step", 0)
+ self._trace_context.step = step + 1
+ context.put({
+ "step": min(step, 511),
+ "kind": "vision_encoder",
+ "mode": getattr(self._trace_context, "mode", "forward"),
+ "value": summary["rms"],
+ "mean": summary["mean"],
+ "shape": summary["shape"],
+ "source": "vision_hook",
+ })
+
+ return vision_hook
+
+ def _make_aux_hook(self, module_name: str, kind: str):
+ def aux_hook(_module: Any, _inputs: Any, output: Any) -> None:
+ context = getattr(self._trace_context, "queue", None)
+ if context is None:
+ return
+ tensor = self._first_tensor(output)
+ summary = self._summarize(tensor)
+ layer_match = re.search(r"layers\.(\d+)", module_name)
+ layer = f"L{int(layer_match.group(1)) + 1:02d}" if layer_match else "AUX"
+ step = getattr(self._trace_context, "step", 0)
+ self._trace_context.step = step + 1
+ context.put({
+ "step": min(step, 511),
+ "layer": layer,
+ "kind": kind,
+ "mode": getattr(self._trace_context, "mode", "forward"),
+ "value": summary["rms"],
+ "mean": summary["mean"],
+ "shape": summary["shape"],
+ "module": module_name,
+ "source": "aux_hook",
+ })
+
+ return aux_hook
+
+ def _make_backward_hook(self, layer_index: int):
+ def backward_hook(_module: Any, _grad_input: Any, grad_output: Any) -> None:
+ context = getattr(self._trace_context, "queue", None)
+ if context is None or getattr(self._trace_context, "mode", "forward") != "backward":
+ return
+ tensor = self._first_tensor(grad_output)
+ summary = self._summarize(tensor)
+ step = getattr(self._trace_context, "step", 0)
+ self._trace_context.step = step + 1
+ context.put({
+ "step": min(step, 511),
+ "layer": f"L{layer_index + 1:02d}",
+ "kind": "gradient",
+ "value": summary["rms"],
+ "mean": summary["mean"],
+ "shape": summary["shape"],
+ "source": "backward_hook",
+ })
+
+ return backward_hook
+
+ def _make_logits_hook(self):
+ def logits_hook(_module: Any, _inputs: Any, output: Any) -> None:
+ context = getattr(self._trace_context, "queue", None)
+ if context is None:
+ return
+ tensor = self._first_tensor(output)
+ if tensor is not None and tensor.ndim >= 2:
+ tensor = tensor[:, -1, :]
+ summary = self._summarize(tensor)
+ step = getattr(self._trace_context, "step", 0)
+ self._trace_context.step = step + 1
+ context.put({
+ "step": min(step, 511),
+ "layer": "OUT",
+ "kind": "logits",
+ "value": summary["rms"],
+ "mean": summary["mean"],
+ "shape": summary["shape"],
+ "source": "logits_hook",
+ })
+
+ return logits_hook
+
+ def _trace_worker(self, prompt: str, mode: str, image_data: str | None, events: queue.Queue[dict[str, Any]]) -> None:
+ self._trace_context.queue = events
+ self._trace_context.mode = mode
+ self._trace_context.step = 0
+ try:
+ inputs = self._encode(prompt, image_data)
+ if image_data and "pixel_values" in inputs:
+ summary = self._summarize(inputs["pixel_values"])
+ events.put({
+ "step": 0,
+ "kind": "vision_input",
+ "mode": mode,
+ "value": summary["rms"],
+ "mean": summary["mean"],
+ "shape": summary["shape"],
+ "source": "processor",
+ })
+ if mode == "backward":
+ self._run_backward(inputs, events)
+ elif mode == "attention":
+ self._run_attention(inputs, events)
+ else:
+ self._run_generation(inputs, events)
+ except Exception as exc: # pragma: no cover - depends on checkpoint/runtime
+ events.put({"kind": "runtime_error", "error": f"{type(exc).__name__}: {exc}", "source": "model_runtime"})
+ finally:
+ events.put({"kind": "done", "source": "model_runtime"})
+ self._trace_context.queue = None
+
+ def _run_attention(self, inputs: dict[str, Any], events: queue.Queue[dict[str, Any]]) -> None:
+ torch = self._torch
+ try:
+ with torch.inference_mode():
+ outputs = self.model(
+ **inputs,
+ use_cache=False,
+ output_attentions=True,
+ return_dict=True,
+ )
+ attentions = getattr(outputs, "attentions", None)
+ emitted = False
+ if attentions:
+ for layer_index, attention in enumerate(attentions):
+ if attention is None or getattr(attention, "ndim", 0) < 4:
+ continue
+ size = min(7, int(attention.shape[-1]))
+ matrix = attention[0, :, -size:, -size:].float().mean(dim=0)
+ values = matrix.detach().cpu().reshape(-1).tolist()
+ summary = self._summarize(matrix)
+ step = getattr(self._trace_context, "step", 0)
+ self._trace_context.step = step + 1
+ events.put({
+ "step": min(step, 511),
+ "layer": f"L{layer_index + 1:02d}",
+ "kind": "attention_weight",
+ "mode": "attention",
+ "value": summary["rms"],
+ "mean": summary["mean"],
+ "shape": list(attention.shape),
+ "attention": [round(float(value), 6) for value in values],
+ "attention_shape": [size, size],
+ "source": "attention_output",
+ })
+ emitted = True
+ if not emitted:
+ events.put({
+ "step": min(getattr(self._trace_context, "step", 0), 511),
+ "kind": "attention_unavailable",
+ "mode": "attention",
+ "source": "model_runtime",
+ })
+ except Exception:
+ events.put({
+ "step": min(getattr(self._trace_context, "step", 0), 511),
+ "kind": "attention_unavailable",
+ "mode": "attention",
+ "source": "model_runtime",
+ })
+ self._run_generation(inputs, events)
+
+ def _encode(self, prompt: str, image_data: str | None = None) -> dict[str, Any]:
+ processor = self.processor
+ rendered_prompt = prompt
+ image = None
+ if image_data:
+ try:
+ from PIL import Image
+
+ encoded_image = image_data.split(",", 1)[-1]
+ image = Image.open(BytesIO(base64.b64decode(encoded_image))).convert("RGB")
+ except Exception as exc:
+ raise RuntimeError(f"invalid image input: {type(exc).__name__}") from exc
+ apply_chat_template = getattr(processor, "apply_chat_template", None)
+ if callable(apply_chat_template):
+ try:
+ content = [{"type": "image", "image": image}, {"type": "text", "text": prompt}] if image is not None else prompt
+ rendered_prompt = apply_chat_template(
+ [{"role": "user", "content": content}],
+ tokenize=False,
+ add_generation_prompt=True,
+ )
+ except Exception:
+ rendered_prompt = prompt
+ processor_kwargs: dict[str, Any] = {"text": rendered_prompt, "return_tensors": "pt", "truncation": True, "max_length": 256}
+ if image is not None:
+ processor_kwargs["images"] = image
+ try:
+ encoded = processor(**processor_kwargs)
+ except Exception:
+ processor_kwargs.pop("truncation", None)
+ processor_kwargs.pop("max_length", None)
+ encoded = processor(**processor_kwargs)
+ device = self._input_device()
+ return {key: value.to(device) if hasattr(value, "to") else value for key, value in encoded.items()}
+
+ def _run_generation(self, inputs: dict[str, Any], events: queue.Queue[dict[str, Any]]) -> None:
+ torch = self._torch
+ with torch.inference_mode():
+ generated = self.model.generate(**inputs, max_new_tokens=32, do_sample=False, use_cache=True)
+ if hasattr(generated, "sequences"):
+ generated = generated.sequences
+ input_length = 0
+ if "input_ids" in inputs and getattr(inputs["input_ids"], "ndim", 0) >= 2:
+ input_length = int(inputs["input_ids"].shape[-1])
+ if getattr(generated, "ndim", 0) >= 2 and input_length:
+ generated = generated[:, input_length:]
+ try:
+ text = self.processor.batch_decode(generated, skip_special_tokens=True)[0]
+ except Exception:
+ tokenizer = getattr(self.processor, "tokenizer", None)
+ try:
+ text = tokenizer.batch_decode(generated, skip_special_tokens=True)[0] if tokenizer is not None else ""
+ except Exception:
+ text = ""
+ events.put({
+ "step": min(getattr(self._trace_context, "step", 0), 511),
+ "kind": "generation",
+ "token": text[-1:] if text else "",
+ "text": text,
+ "value": 1.0,
+ "source": "model_runtime",
+ })
+
+ def _run_backward(self, inputs: dict[str, Any], events: queue.Queue[dict[str, Any]]) -> None:
+ torch = self._torch
+ if "input_ids" not in inputs:
+ raise RuntimeError("processor did not return input_ids; backward trace requires text tokens")
+ self.model.zero_grad(set_to_none=True)
+ labels = inputs["input_ids"].clone()
+ with torch.enable_grad():
+ outputs = self.model(**inputs, labels=labels, use_cache=False)
+ loss = outputs.loss
+ loss.backward()
+ events.put({
+ "step": min(getattr(self._trace_context, "step", 0), 511),
+ "kind": "loss",
+ "loss": float(loss.detach().float().item()),
+ "value": float(loss.detach().float().item()),
+ "source": "autograd",
+ })
+
+ def _input_device(self):
+ try:
+ return self.model.device
+ except Exception:
+ for parameter in self.model.parameters():
+ if parameter.device.type != "meta":
+ return parameter.device
+ return self._torch.device("cpu")
+
+ def _first_tensor(self, value: Any):
+ if self._torch is not None and self._torch.is_tensor(value):
+ return value
+ if isinstance(value, (tuple, list)):
+ for item in value:
+ tensor = self._first_tensor(item)
+ if tensor is not None:
+ return tensor
+ if isinstance(value, dict):
+ for item in value.values():
+ tensor = self._first_tensor(item)
+ if tensor is not None:
+ return tensor
+ return None
+
+ def _summarize(self, tensor: Any) -> dict[str, Any]:
+ if tensor is None:
+ return {"mean": 0.0, "rms": 0.0, "shape": []}
+ torch = self._torch
+ sampled = tensor.detach().float().reshape(-1)
+ if sampled.numel() > 4096:
+ stride = max(1, sampled.numel() // 4096)
+ sampled = sampled[::stride]
+ mean = float(sampled.mean().item())
+ rms = float(torch.sqrt(torch.mean(sampled * sampled)).item())
+ return {"mean": round(mean, 6), "rms": round(rms, 6), "shape": list(tensor.shape)}
diff --git a/server/requirements-model.txt b/server/requirements-model.txt
new file mode 100644
index 0000000..e9e726c
--- /dev/null
+++ b/server/requirements-model.txt
@@ -0,0 +1,4 @@
+# Install this optional set after the Qwen3.5 checkpoint is fully downloaded.
+torch>=2.6
+transformers>=4.57
+accelerate>=1.3
diff --git a/server/requirements.txt b/server/requirements.txt
new file mode 100644
index 0000000..1a15839
--- /dev/null
+++ b/server/requirements.txt
@@ -0,0 +1,2 @@
+fastapi>=0.115
+uvicorn[standard]>=0.34
diff --git a/src/App.jsx b/src/App.jsx
new file mode 100644
index 0000000..7f42b18
--- /dev/null
+++ b/src/App.jsx
@@ -0,0 +1,515 @@
+import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react'
+import {
+ Activity,
+ ArrowDown,
+ BrainCircuit,
+ ChevronDown,
+ CircleHelp,
+ CirclePause,
+ CirclePlay,
+ Cpu,
+ Database,
+ GitBranch,
+ Layers3,
+ MoreHorizontal,
+ Paperclip,
+ Pause,
+ Play,
+ Radio,
+ RefreshCw,
+ Search,
+ Send,
+ Settings2,
+ SlidersHorizontal,
+ Sparkles,
+ TerminalSquare,
+ TimerReset,
+ Waypoints,
+ X,
+} from 'lucide-react'
+
+const Trace3DCanvas = lazy(() => import('./Trace3DCanvas.jsx'))
+
+const TOKENS = ['请', '解', '释', '量', '子', '纠', '缠']
+
+const STAGES = [
+ { id: 'stage-01', index: '01', title: 'L01—04', type: 'hybrid', caption: '3L + 1A' },
+ { id: 'stage-02', index: '02', title: 'L05—08', type: 'hybrid', caption: '3L + 1A' },
+ { id: 'stage-03', index: '03', title: 'L09—12', type: 'hybrid', caption: '3L + 1A' },
+ { id: 'stage-04', index: '04', title: 'L13—16', type: 'hybrid', caption: '3L + 1A' },
+ { id: 'stage-05', index: '05', title: 'L17—20', type: 'hybrid', caption: '3L + 1A' },
+ { id: 'stage-06', index: '06', title: 'L21—24', type: 'hybrid', caption: '3L + 1A' },
+ { id: 'stage-07', index: '07', title: 'L25—28', type: 'hybrid', caption: '3L + 1A' },
+ { id: 'stage-08', index: '08', title: 'L29—32', type: 'hybrid', caption: '3L + 1A' },
+]
+
+const EVENT_SEED = [
+ { time: '00:12.84', text: 'residual stream merged', tone: 'cyan' },
+ { time: '00:12.77', text: 'attention pattern stabilized', tone: 'amber' },
+ { time: '00:12.68', text: 'token 03 → layer 08', tone: 'cyan' },
+ { time: '00:12.51', text: 'delta state updated', tone: 'muted' },
+ { time: '00:12.36', text: 'input embedding ready', tone: 'muted' },
+]
+
+const DEFAULT_ARCHITECTURE = {
+ num_layers: 32,
+ pattern: '8 × (3L + 1A)',
+ hidden_size: 2560,
+ vocab_size: 248320,
+ context_length: 262144,
+ intermediate_size: 9216,
+ attention_heads: 16,
+ attention_kv_heads: 4,
+ attention_head_dim: 256,
+ vision_encoder: true,
+ sparse_moe: true,
+ mtp: true,
+ precision: 'BF16 / INT4',
+}
+
+const compactNumber = (value) => {
+ const number = Number(value)
+ if (!Number.isFinite(number)) return '—'
+ if (number >= 1000000) return `${(number / 1000000).toFixed(number % 1000000 ? 1 : 0)}M`
+ if (number >= 100000) return `${Math.round(number / 1000)}K`
+ return number.toLocaleString('en-US')
+}
+
+function IconButton({ label, children, active = false, onClick }) {
+ return (
+
+ )
+}
+
+function StatusDot({ tone = 'green' }) {
+ return
+}
+
+function App() {
+ const [isPlaying, setIsPlaying] = useState(true)
+ const [step, setStep] = useState(128)
+ const [activeToken, setActiveToken] = useState(3)
+ const [selectedLayer, setSelectedLayer] = useState('L08')
+ const [speed, setSpeed] = useState(1)
+ const [prompt, setPrompt] = useState('解释一下量子纠缠')
+ const [attachment, setAttachment] = useState(null)
+ const [responseText, setResponseText] = useState('')
+ const [responseSource, setResponseSource] = useState('demo')
+ const [attentionValues, setAttentionValues] = useState(null)
+ const [liveSignal, setLiveSignal] = useState({ kind: 'waiting', value: null, mean: null, shape: null })
+ const [events, setEvents] = useState(EVENT_SEED)
+ const [runtime, setRuntime] = useState({ connected: false, model: 'Qwen3.5-4B', modelPath: 'waiting for local model', architecture: DEFAULT_ARCHITECTURE })
+ const [activeView, setActiveView] = useState('trace')
+ const [spaceMode, setSpaceMode] = useState('3d')
+ const [processMode, setProcessMode] = useState('forward')
+ const [showHistory, setShowHistory] = useState(false)
+ const [notice, setNotice] = useState('')
+ const noticeTimer = useRef(null)
+ const streamAbortRef = useRef(null)
+ const traceRef = useRef({ step, activeToken, selectedLayer })
+ traceRef.current = { step, activeToken, selectedLayer }
+
+ useEffect(() => {
+ let mounted = true
+ const refreshRuntime = () => {
+ fetch('/api/health')
+ .then((response) => response.json())
+ .then((data) => {
+ if (mounted) setRuntime(data)
+ })
+ .catch(() => {})
+ }
+ refreshRuntime()
+ const timer = window.setInterval(refreshRuntime, 3500)
+ return () => {
+ mounted = false
+ window.clearInterval(timer)
+ }
+ }, [])
+
+ useEffect(() => {
+ if (!isPlaying) return undefined
+ const timer = window.setInterval(() => {
+ setStep((current) => (current >= 512 ? 0 : current + 1))
+ setActiveToken((current) => (current + 1) % TOKENS.length)
+ }, Math.max(180, 850 / speed))
+ return () => window.clearInterval(timer)
+ }, [isPlaying, speed])
+
+ useEffect(() => {
+ const timer = window.setInterval(() => {
+ const currentTrace = traceRef.current
+ setEvents((current) => {
+ const next = [
+ { time: `00:${String(12 + (currentTrace.step % 40)).padStart(2, '0')}.${String(currentTrace.step % 100).padStart(2, '0')}`, text: `${TOKENS[currentTrace.activeToken]} → ${currentTrace.selectedLayer}`, tone: currentTrace.activeToken % 2 ? 'amber' : 'cyan' },
+ ...current,
+ ]
+ return next.slice(0, 5)
+ })
+ }, 1800)
+ return () => window.clearInterval(timer)
+ }, [])
+
+ useEffect(() => () => streamAbortRef.current?.abort(), [])
+
+ const notify = (message) => {
+ setNotice(message)
+ window.clearTimeout(noticeTimer.current)
+ noticeTimer.current = window.setTimeout(() => setNotice(''), 2600)
+ }
+
+ const handleAttachment = (event) => {
+ const file = event.target.files?.[0]
+ event.target.value = ''
+ if (!file) return
+ if (file.size > 4 * 1024 * 1024) {
+ notify('图片需小于 4 MB')
+ return
+ }
+ const reader = new FileReader()
+ reader.onload = () => setAttachment({ name: file.name, dataUrl: String(reader.result) })
+ reader.readAsDataURL(file)
+ }
+
+ const consumeTraceStream = async (tracePrompt, traceMode, traceAttachment = null) => {
+ streamAbortRef.current?.abort()
+ const controller = new AbortController()
+ streamAbortRef.current = controller
+ try {
+ const requestUrl = traceAttachment ? '/api/stream' : `/api/stream?prompt=${encodeURIComponent(tracePrompt)}&mode=${traceMode}`
+ const requestOptions = traceAttachment ? {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ prompt: tracePrompt, mode: traceMode, image_data: traceAttachment.dataUrl }),
+ } : {}
+ const response = await fetch(requestUrl, { ...requestOptions, signal: controller.signal })
+ if (!response.ok || !response.body) return
+ setIsPlaying(false)
+ const reader = response.body.getReader()
+ const decoder = new TextDecoder()
+ let buffer = ''
+ while (true) {
+ const { value, done } = await reader.read()
+ buffer += decoder.decode(value || new Uint8Array(), { stream: !done })
+ const blocks = buffer.split('\n\n')
+ buffer = blocks.pop() || ''
+ for (const block of blocks) {
+ const line = block.split('\n').find((entry) => entry.startsWith('data: '))
+ if (!line) continue
+ const payload = JSON.parse(line.slice(6))
+ const tokenIndex = TOKENS.indexOf(payload.token)
+ setStep(Math.min(512, payload.step * 11))
+ setActiveToken(tokenIndex >= 0 ? tokenIndex : payload.step % TOKENS.length)
+ if (payload.layer) setSelectedLayer(payload.layer)
+ if (payload.text) {
+ setResponseText(payload.text)
+ setResponseSource(payload.source === 'model_runtime' ? 'model' : 'demo')
+ }
+ if (payload.attention) setAttentionValues(payload.attention)
+ if (payload.kind && payload.kind !== 'runtime_error' && payload.kind !== 'generation') {
+ setLiveSignal({
+ kind: payload.kind,
+ value: Number.isFinite(Number(payload.value)) ? Number(payload.value) : null,
+ mean: Number.isFinite(Number(payload.mean)) ? Number(payload.mean) : null,
+ shape: Array.isArray(payload.shape) ? payload.shape : null,
+ })
+ }
+ if (payload.kind === 'runtime_error') notify(`真实模型运行错误:${payload.error}`)
+ const eventText = payload.kind === 'runtime_error'
+ ? `runtime error · ${payload.error}`
+ : [payload.kind, payload.layer].filter(Boolean).join(' · ')
+ const eventTone = ['full_attention', 'attention_weight', 'attention_output', 'gradient', 'loss', 'logits', 'runtime_error', 'attention_unavailable', 'vision_input', 'vision_encoder', 'router_weights', 'expert_mixture', 'mtp_logits'].includes(payload.kind) ? 'amber' : 'cyan'
+ setEvents((current) => [{ time: 'stream', text: eventText, tone: eventTone }, ...current].slice(0, 5))
+ }
+ if (done) break
+ }
+ notify('遥测流已完成')
+ } catch (error) {
+ if (error.name !== 'AbortError') notify('后端未连接,继续使用本地模拟')
+ }
+ }
+
+ const submitPrompt = (event) => {
+ event.preventDefault()
+ if (!prompt.trim()) return
+ setStep(0)
+ setActiveToken(0)
+ setIsPlaying(true)
+ setResponseText('')
+ setResponseSource('demo')
+ setAttentionValues(null)
+ setLiveSignal({ kind: 'waiting', value: null, mean: null, shape: null })
+ setEvents((current) => [{ time: '00:00.00', text: `trace started · ${prompt.trim()}`, tone: 'cyan' }, ...current].slice(0, 5))
+ notify(processMode === 'backward' ? '反向传播观测已开始' : '新一轮计算观测已开始')
+ void consumeTraceStream(prompt.trim(), processMode, attachment)
+ }
+
+ const progress = (step / 512) * 100
+ const graphState = useMemo(() => ({
+ activeStage: Math.floor(step / 64) % STAGES.length,
+ pulse: (step % 32) / 32,
+ }), [step])
+ const selectedLayerNumber = Number(selectedLayer.match(/\d+/)?.[0] || 8)
+ const selectedNodeDescription = selectedLayer === 'OUT' ? 'logits projection' : selectedLayer === 'AUX' ? 'router / expert telemetry' : selectedLayer.includes('—') ? 'hybrid block · 3L + 1A' : selectedLayerNumber % 4 === 0 ? 'full attention block' : 'DeltaNet block'
+ const processLabel = { forward: 'FORWARD PASS', backward: 'BACKWARD PASS', attention: 'ATTENTION WEIGHTS', state: 'DELTA STATE' }[processMode]
+ const architecture = { ...DEFAULT_ARCHITECTURE, ...(runtime.architecture || {}) }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{ setStep(0); setActiveToken(0); notify('trace 已重置') }}>
+
+
+
+
+
INPUT TOKENS
+
+ {TOKENS.map((token, index) => )}
+
+
7 TOKENS
+
+
+
+
PROCESS
+
+
+
+
+
+
+
{processMode === 'backward' ? 'gradient / credit assignment' : processMode === 'attention' ? 'token-to-token routing' : processMode === 'state' ? 'recurrent state update' : 'activation / logits'}
+
+
+ {activeView === 'trace' ? (
+ spaceMode === '3d' ? (
+ Loading 3D trace space}>
+
+
+ ) : (
+
+ )
+ ) : (
+
notify('权重检查需要完整精度模型')} />
+ )}
+
+
+
+
{processMode === 'backward' ? 'GRADIENT FIELD' : 'ACTIVATION FIELD'}{selectedLayer} · {processMode === 'backward' ? 'credit signal' : 'hidden state'}
+
+
+
+
OUTPUT DISTRIBUTIONnext token
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {notice && {notice}
}
+
+ )
+}
+
+function TraceGraph({ graphState, selectedLayer, setSelectedLayer, activeToken }) {
+ const packetOffset = graphState.pulse * 100
+ const selectedLayerNumber = Number(selectedLayer.match(/\d+/)?.[0] || 0)
+ return (
+
+ data flowattentionresidual
X 04.82 Y 08.16 Z 00.00
+
+
+
+
INembeddings7 × 2,560
+ {STAGES.map((stage, index) => {
+ const active = graphState.activeStage === index
+ const selected = selectedLayer === stage.title || (selectedLayerNumber >= index * 4 + 1 && selectedLayerNumber <= index * 4 + 4)
+ return
+ })}
+
OUTlogits248,320 dim
+
gated state update
+
softmax projection
+
+ RESIDUAL STREAMhidden state / layer {selectedLayer.replace('L', '')}
ACTIVE TOKEN{String(activeToken + 1).padStart(2, '0')} · {TOKENS[activeToken]}
+
+ )
+}
+
+function NeuronField({ step }) {
+ const bars = Array.from({ length: 44 }, (_, index) => ((Math.sin(index * 1.7 + step * 0.035) + 1) / 2) * 0.75 + 0.08)
+ return
+ {bars.map((value, index) => )}
+
+}
+
+function AttentionMap({ activeToken, values }) {
+ const liveValues = Array.isArray(values) && values.length > 0 ? values.map(Number) : null
+ const livePeak = liveValues ? Math.max(...liveValues, 0.0001) : 1
+ return {Array.from({ length: 49 }, (_, index) => {
+ const row = Math.floor(index / 7)
+ const column = index % 7
+ const fallback = Math.max(0.06, 0.12 + Math.sin((row + 1) * (column + 2) + activeToken) * 0.11 + (row === activeToken ? 0.28 : 0))
+ const liveIntensity = liveValues ? Math.min(0.88, 0.08 + ((liveValues[index % liveValues.length] || 0) / livePeak) * 0.8) : fallback
+ const focus = row === activeToken || column === activeToken
+ const intensity = focus ? Math.min(0.96, liveIntensity + 0.12) : liveIntensity
+ return
+ })}
+}
+
+function Metric({ label, value }) {
+ return {label}{value}
+}
+
+function WeightsView({ onSelect }) {
+ return Parameter surface
Weight-level inspection is reserved for the full-precision checkpoint.
Use BF16 safetensors for faithful gradientsThe current runtime can still expose activation flow and token-level events.
+}
+
+function TraceTransport({ isPlaying, setIsPlaying, speed, setSpeed, progress, step, setStep, processLabel }) {
+ return {processLabel}Step {step} / 512
setStep(Number(event.target.value))} style={{ '--progress': `${progress}%` }} aria-label="Trace step" />
{isPlaying ? 'LIVE' : 'PAUSED'}
+}
+
+export default App
diff --git a/src/Trace3DCanvas.jsx b/src/Trace3DCanvas.jsx
new file mode 100644
index 0000000..b3e03ae
--- /dev/null
+++ b/src/Trace3DCanvas.jsx
@@ -0,0 +1,150 @@
+import { useMemo, useRef, useState } from 'react'
+
+const DEFAULT_LAYER_COUNT = 32
+const NEURON_COUNT = 6
+
+const layerLabel = (index) => `L${String(index + 1).padStart(2, '0')}`
+
+function layerPosition(index, layerCount = DEFAULT_LAYER_COUNT) {
+ const progress = index / Math.max(1, layerCount - 1)
+ return [
+ (progress - 0.5) * 15.4,
+ Math.sin(progress * Math.PI * 3.2) * 0.68,
+ Math.cos(progress * Math.PI * 2.4) * 0.72,
+ ]
+}
+
+function projectPoint(point, yaw, pitch, zoom = 1) {
+ const [x, y, z] = point
+ const yawCos = Math.cos(yaw)
+ const yawSin = Math.sin(yaw)
+ const rotatedX = x * yawCos - z * yawSin
+ const rotatedZ = x * yawSin + z * yawCos
+ const pitchCos = Math.cos(pitch)
+ const pitchSin = Math.sin(pitch)
+ const rotatedY = y * pitchCos - rotatedZ * pitchSin
+ const depth = y * pitchSin + rotatedZ * pitchCos
+ const perspective = 1 / (1 + depth * 0.047)
+ return {
+ x: 500 + rotatedX * 47 * perspective * zoom,
+ y: 230 - rotatedY * 56 * perspective * zoom,
+ depth,
+ perspective,
+ }
+}
+
+function pointString(points) {
+ return points.map((point) => `${point.x.toFixed(1)},${point.y.toFixed(1)}`).join(' ')
+}
+
+function makeLayers(step, selectedLayer, yaw, pitch, signalValue, layerCount, zoom) {
+ const activeIndex = step % layerCount
+ const liveSignal = Number.isFinite(Number(signalValue)) ? Math.min(1, Math.max(0, Number(signalValue))) : null
+ return Array.from({ length: layerCount }, (_, index) => {
+ const label = layerLabel(index)
+ const fullAttention = (index + 1) % 4 === 0
+ const base = layerPosition(index, layerCount)
+ const simulatedEnergy = 0.2 + ((Math.sin(index * 1.74 + step * 0.11) + 1) / 2) * 0.8
+ const energy = liveSignal == null ? simulatedEnergy : Math.min(1, 0.16 + liveSignal * 0.72 + ((Math.sin(index * 1.74 + step * 0.11) + 1) / 2) * 0.18)
+ const node = projectPoint(base, yaw, pitch, zoom)
+ const neurons = Array.from({ length: NEURON_COUNT }, (_, neuronIndex) => {
+ const angle = (neuronIndex / NEURON_COUNT) * Math.PI * 2
+ const radius = 0.26 + energy * 0.05
+ return projectPoint([base[0] + Math.cos(angle) * radius, base[1] + Math.sin(angle) * radius, base[2] + Math.sin(angle * 1.8) * 0.08], yaw, pitch, zoom)
+ })
+ return { index, label, fullAttention, energy, node, neurons, active: index === activeIndex, selected: selectedLayer === label }
+ })
+}
+
+function FlowPacket({ step, yaw, pitch, mode, layerCount, zoom }) {
+ const reverse = mode === 'backward'
+ const currentIndex = reverse ? layerCount - 1 - (step % layerCount) : step % layerCount
+ const nextIndex = reverse ? Math.max(0, currentIndex - 1) : Math.min(layerCount - 1, currentIndex + 1)
+ const progress = (step % 16) / 16
+ const current = layerPosition(currentIndex, layerCount)
+ const next = layerPosition(nextIndex, layerCount)
+ const point = current.map((value, index) => value + (next[index] - value) * progress)
+ const projected = projectPoint([point[0], point[1] + 0.14, point[2]], yaw, pitch, zoom)
+ return
+}
+
+function ProjectionGrid() {
+ return
+ {Array.from({ length: 13 }, (_, index) => {
+ const x = 40 + index * 77
+ return
+ })}
+ {Array.from({ length: 7 }, (_, index) => {
+ const y = 70 + index * 54
+ return
+ })}
+
+}
+
+export default function Trace3DCanvas({ layerCount = DEFAULT_LAYER_COUNT, pattern = '8 × (3L + 1A)', hasImage = false, signalKind = 'waiting', step, selectedLayer, setSelectedLayer, activeToken, mode = 'forward', signalValue = null }) {
+ const [orbit, setOrbit] = useState({ yaw: -0.12, pitch: 0.13 })
+ const [zoom, setZoom] = useState(1)
+ const dragRef = useRef(null)
+ const safeLayerCount = Math.max(1, Number(layerCount) || DEFAULT_LAYER_COUNT)
+ const layers = useMemo(() => makeLayers(step, selectedLayer, orbit.yaw, orbit.pitch, signalValue, safeLayerCount, zoom), [orbit.pitch, orbit.yaw, safeLayerCount, selectedLayer, signalValue, step, zoom])
+ const sortedLayers = useMemo(() => [...layers].sort((a, b) => a.node.depth - b.node.depth), [layers])
+ const residualPoints = useMemo(() => layers.map((layer) => layer.node), [layers])
+ const attentionLinks = useMemo(() => layers.filter((layer) => layer.fullAttention).map((layer) => {
+ const next = layers[Math.min(layers.length - 1, layer.index + 1)]
+ return { id: layer.label, points: [layer.node, next.node] }
+ }), [layers])
+ const auxLayer = layers.find((layer) => layer.selected) || layers[0]
+ const auxLabel = signalKind === 'router_weights' ? 'ROUTER' : signalKind === 'expert_mixture' ? 'EXPERT MIX' : signalKind === 'mtp_logits' ? 'MTP' : ''
+
+ const handlePointerDown = (event) => {
+ event.currentTarget.setPointerCapture(event.pointerId)
+ dragRef.current = { x: event.clientX, y: event.clientY, ...orbit }
+ }
+
+ const handlePointerMove = (event) => {
+ if (!dragRef.current) return
+ setOrbit({
+ yaw: dragRef.current.yaw + (event.clientX - dragRef.current.x) * 0.007,
+ pitch: Math.max(-0.45, Math.min(0.45, dragRef.current.pitch + (event.clientY - dragRef.current.y) * 0.004)),
+ })
+ }
+
+ const stopDragging = () => { dragRef.current = null }
+ const handleWheel = (event) => {
+ event.preventDefault()
+ setZoom((current) => Math.max(0.72, Math.min(1.55, current - event.deltaY * 0.001)))
+ }
+
+ return
+
+
3D TRACE SPACEobservable compute · step {step} / 512
+
DeltaNet statefull attention{hasImage && vision input}token {String(activeToken + 1).padStart(2, '0')}
+
+
+
+
RESIDUAL STREAM →
+
HIDDEN STATE ↕
+
ACTIVE LAYER{selectedLayer}click a node to inspect
+
DRAG TO ORBIT · SCROLL TO ZOOM
+
+ {safeLayerCount} LAYERS · {pattern}{hasImage ? 'TELEMETRY: VISION / ACTIVATION / STATE / LOGITS' : 'TELEMETRY: ACTIVATION / STATE / LOGITS'}PRECISION: BF16 / INT4
+
+}
diff --git a/src/main.jsx b/src/main.jsx
new file mode 100644
index 0000000..fa62691
--- /dev/null
+++ b/src/main.jsx
@@ -0,0 +1,10 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App.jsx'
+import './styles.css'
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+
+
+ ,
+)
diff --git a/src/styles.css b/src/styles.css
new file mode 100644
index 0000000..e7d4adc
--- /dev/null
+++ b/src/styles.css
@@ -0,0 +1,311 @@
+@import url('https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&display=swap');
+
+:root {
+ color-scheme: dark;
+ font-family: 'IBM Plex Sans', 'Segoe UI', sans-serif;
+ --bg: #0b0d10;
+ --surface: #11151a;
+ --surface-2: #151a20;
+ --surface-3: #1a2027;
+ --border: #242b33;
+ --border-bright: #303a44;
+ --text: #e6edf0;
+ --muted: #7b8790;
+ --muted-2: #56616a;
+ --cyan: #78e4e8;
+ --amber: #efb56b;
+ --green: #8dd8ae;
+ --mono: 'DM Mono', 'Cascadia Code', monospace;
+}
+
+* { box-sizing: border-box; }
+html, body, #root { min-width: 320px; min-height: 100%; margin: 0; }
+body { background: var(--bg); color: var(--text); }
+button, input, select { font: inherit; }
+button { color: inherit; }
+
+.app-shell { min-height: 100vh; background: var(--bg); overflow: hidden; }
+.topbar { height: 58px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; padding: 0 22px; background: rgba(11,13,16,.96); }
+.brand-block, .topbar-actions, .runtime-status, .model-select, .stage-header-actions, .view-switcher, .crumb, .token-strip, .token-list, .graph-toolbar, .graph-legend, .graph-coordinates, .graph-footer, .graph-footer-right, .panel-heading, .node-title-row, .section-heading, .legend, .storage-line, .transport, .transport-meta, .speed-control { display: flex; align-items: center; }
+.brand-block { gap: 10px; }
+.brand-mark { display: grid; place-items: center; width: 26px; height: 26px; color: var(--cyan); border: 1px solid rgba(120,228,232,.4); border-radius: 6px; }
+.brand-name { font-family: var(--mono); font-size: 12px; letter-spacing: .12em; color: #f2f6f6; }
+.brand-divider { height: 14px; width: 1px; background: var(--border-bright); margin: 0 3px; }
+.brand-context, .runtime-status, .model-select, .graph-coordinates, .strip-label, .token-count, .section-kicker, .section-heading, .graph-footer-label, .transport-label, .storage-line, .path-note { font-family: var(--mono); font-size: 10px; letter-spacing: .08em; }
+.brand-context { color: var(--muted-2); }
+.topbar-actions { gap: 16px; }
+.runtime-status { gap: 8px; color: var(--muted); }
+.status-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; }
+.status-green { background: var(--green); box-shadow: 0 0 0 3px rgba(141,216,174,.08); }
+.status-amber { background: var(--amber); box-shadow: 0 0 0 3px rgba(239,181,107,.08); }
+.model-select { gap: 8px; padding: 7px 10px; border: 1px solid var(--border); background: var(--surface); color: #b9c4c8; font-size: 12px; }
+.model-select svg:first-child { color: var(--cyan); }
+.icon-button { display: grid; place-items: center; width: 29px; height: 29px; border: 0; background: transparent; color: var(--muted); cursor: pointer; transition: color .18s ease, background .18s ease; }
+.icon-button:hover, .icon-button.is-active { color: var(--text); background: var(--surface-3); }
+
+.workspace { min-height: calc(100vh - 58px); display: grid; grid-template-columns: 248px minmax(560px, 1fr) 298px; }
+.sidebar, .inspector { background: var(--surface); }
+.sidebar { border-right: 1px solid var(--border); display: flex; flex-direction: column; min-width: 0; }
+.sidebar-head { display: flex; justify-content: space-between; align-items: center; padding: 20px 18px 12px; font-family: var(--mono); font-size: 10px; letter-spacing: .12em; color: var(--muted-2); }
+.new-trace { padding: 8px 15px 22px; }
+.new-trace label { display: block; color: var(--muted); font-size: 11px; margin-bottom: 8px; }
+.prompt-field { display: flex; border: 1px solid var(--border); background: #0d1014; }
+.prompt-field:focus-within { border-color: rgba(120,228,232,.5); }
+.prompt-field input { flex: 1; min-width: 0; padding: 10px; color: var(--text); outline: none; border: 0; background: transparent; font-size: 12px; }
+.attachment-button { position: relative; display: grid; place-items: center; width: 30px; color: var(--muted-2); cursor: pointer; }
+.attachment-button:hover { color: var(--cyan); }
+.attachment-button input { position: absolute; inset: 0; width: 100%; height: 100%; opacity: 0; cursor: pointer; }
+.prompt-field button { width: 34px; border: 0; border-left: 1px solid var(--border); background: transparent; color: var(--cyan); cursor: pointer; }
+.attachment-chip { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: 7px; padding: 6px 8px; border: 1px solid rgba(120,228,232,.25); background: #0d1719; color: #a9d8da; font-size: 10px; }
+.attachment-chip span { display: inline-flex; align-items: center; gap: 6px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.attachment-chip button { display: grid; place-items: center; padding: 0; border: 0; color: var(--muted-2); background: transparent; cursor: pointer; }
+.attachment-chip button:hover { color: var(--cyan); }
+.session-list { display: grid; gap: 2px; padding: 0 9px; }
+.session-row { min-width: 0; display: flex; align-items: center; gap: 10px; padding: 10px 9px; border: 1px solid transparent; background: transparent; text-align: left; cursor: pointer; }
+.session-row:hover, .session-row.is-selected { background: var(--surface-2); border-color: var(--border); }
+.session-icon { width: 27px; height: 27px; display: grid; place-items: center; color: var(--cyan); border: 1px solid rgba(120,228,232,.23); }
+.session-icon.muted { color: var(--muted-2); border-color: var(--border); }
+.session-copy { min-width: 0; display: grid; gap: 4px; }
+.session-copy strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; font-weight: 500; }
+.session-copy span { color: var(--muted-2); font-size: 10px; }
+.session-live { margin-left: auto; align-self: flex-start; padding-top: 5px; }
+.sidebar-footer { margin-top: auto; padding: 18px; border-top: 1px solid var(--border); }
+.storage-line { gap: 8px; color: var(--muted); }
+.storage-line svg { color: var(--muted-2); }
+.storage-value { margin-left: auto; color: #bac5c8; }
+.storage-bar { height: 3px; margin: 12px 0 9px; background: var(--surface-3); }
+.storage-bar span { display: block; height: 100%; background: var(--cyan); }
+.path-note { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--muted-2); font-size: 9px; letter-spacing: 0; }
+
+.main-stage { min-width: 0; padding: 26px 28px 22px; background: #0c0f12; }
+.stage-header { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: 22px; }
+.crumb { gap: 7px; color: var(--muted-2); font-family: var(--mono); font-size: 9px; letter-spacing: .1em; }
+.stage-header h1 { margin: 8px 0 0; font-size: 23px; font-weight: 500; letter-spacing: -.03em; }
+.stage-header-actions { gap: 10px; }
+.view-switcher { border: 1px solid var(--border); padding: 3px; background: var(--surface); }
+.view-switcher button { padding: 6px 10px; border: 0; background: transparent; color: var(--muted); font-size: 11px; cursor: pointer; }
+.view-switcher button.is-selected { color: var(--text); background: var(--surface-3); }
+.token-strip { gap: 18px; height: 42px; border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); }
+.strip-label, .token-count { color: var(--muted-2); white-space: nowrap; }
+.token-list { gap: 5px; }
+.token { min-width: 25px; height: 25px; border: 1px solid var(--border); background: var(--surface); color: #aab6ba; font-family: var(--mono); font-size: 12px; cursor: pointer; }
+.token:hover, .token.is-active { color: var(--cyan); border-color: rgba(120,228,232,.65); background: rgba(120,228,232,.08); }
+.token-count { margin-left: auto; }
+.process-strip { display: flex; align-items: center; gap: 13px; min-height: 40px; border-bottom: 1px solid var(--border); }
+.process-switcher { display: flex; align-items: center; gap: 3px; }
+.process-switcher button { padding: 5px 8px; border: 1px solid transparent; background: transparent; color: var(--muted-2); font-family: var(--mono); font-size: 9px; cursor: pointer; }
+.process-switcher button:hover, .process-switcher button.is-selected { border-color: var(--border-bright); color: var(--text); background: var(--surface-2); }
+.process-switcher button.is-selected { color: var(--cyan); }
+.process-note { margin-left: auto; color: var(--muted-2); font-family: var(--mono); font-size: 9px; }
+
+.graph-panel { margin-top: 18px; border: 1px solid var(--border); background: var(--surface); }
+.graph-toolbar { justify-content: space-between; padding: 11px 14px; border-bottom: 1px solid var(--border); }
+.graph-legend { gap: 14px; color: var(--muted); font-size: 10px; }
+.graph-legend span, .legend span { display: inline-flex; gap: 6px; align-items: center; }
+.legend-swatch { display: inline-block; width: 8px; height: 2px; background: var(--muted-2); }
+.legend-swatch.cyan { background: var(--cyan); }
+.legend-swatch.amber { background: var(--amber); }
+.graph-coordinates { color: var(--muted-2); }
+.graph-canvas { position: relative; height: 390px; overflow: hidden; }
+.grid-overlay { position: absolute; inset: 0; opacity: .4; background-image: linear-gradient(rgba(122,139,150,.07) 1px, transparent 1px), linear-gradient(90deg, rgba(122,139,150,.07) 1px, transparent 1px); background-size: 34px 34px; mask-image: linear-gradient(to bottom, transparent, black 15%, black 85%, transparent); }
+.flow-svg { position: absolute; inset: 0; width: 100%; height: 100%; }
+.flow-line { fill: none; stroke-width: 1.2; vector-effect: non-scaling-stroke; }
+.flow-line.residual { stroke: #3a444c; stroke-dasharray: 2 6; }
+.flow-line.data { stroke: url(#cyan-flow); }
+.flow-line.attention { stroke: url(#amber-flow); }
+.branch-line { stroke: #2b343b; stroke-width: 1; stroke-dasharray: 2 4; }
+.stage-port, .branch-port { fill: #0c0f12; stroke: #53616a; stroke-width: 1.2; }
+.stage-port.active { fill: var(--cyan); stroke: var(--cyan); filter: drop-shadow(0 0 5px rgba(120,228,232,.6)); }
+.stage-port.input, .stage-port.output { fill: #0c0f12; stroke: var(--cyan); }
+.flow-packet { fill: var(--cyan); filter: drop-shadow(0 0 4px rgba(120,228,232,.7)); }
+.flow-packet.amber { fill: var(--amber); filter: drop-shadow(0 0 4px rgba(239,181,107,.7)); }
+.flow-node { position: absolute; top: 50%; transform: translate(-50%, -50%); display: grid; justify-items: center; gap: 3px; min-width: 82px; padding: 9px 6px; border: 1px solid var(--border); background: rgba(17,21,26,.92); text-align: center; z-index: 1; }
+.flow-node strong { font-size: 11px; font-weight: 500; color: #d1dcdf; }
+.flow-node small { font-family: var(--mono); font-size: 8px; color: var(--muted-2); white-space: nowrap; }
+.flow-node .node-index { font-family: var(--mono); color: var(--muted-2); font-size: 8px; }
+.input-node { left: 3%; border-color: rgba(120,228,232,.36); }
+.output-node { left: 97%; border-color: rgba(239,181,107,.36); }
+.stage-node { cursor: pointer; }
+.stage-node:hover, .stage-node.selected, .stage-node.active { border-color: rgba(120,228,232,.72); background: #152024; }
+.stage-node.hybrid.active, .stage-node.hybrid.selected { border-color: rgba(120,228,232,.72); background: #152024; }
+.stage-node.hybrid .node-index { color: var(--cyan); }
+.micro-layers { display: flex; gap: 3px; height: 4px; align-items: center; }
+.micro-layers i { display: block; width: 9px; height: 2px; background: rgba(120,228,232,.58); }
+.micro-layers i.attention-mini { background: var(--amber); }
+.graph-annotation { position: absolute; font-family: var(--mono); color: var(--muted-2); font-size: 9px; display: flex; align-items: center; gap: 7px; }
+.annotation-top { left: 40%; top: 28%; }
+.annotation-bottom { right: 7%; bottom: 28%; }
+.annotation-rule { display: block; width: 22px; height: 1px; background: var(--cyan); }
+.amber-rule { background: var(--amber); }
+.graph-footer { justify-content: space-between; padding: 11px 14px; border-top: 1px solid var(--border); }
+.graph-footer > div { display: grid; gap: 4px; }
+.graph-footer-label { color: var(--muted-2); }
+.graph-footer-value { font-size: 11px; color: #adb9bc; }
+.graph-footer-right { gap: 12px; font-family: var(--mono); font-size: 9px; color: var(--muted-2); }
+.graph-footer-right strong { color: var(--cyan); font-weight: 400; }
+
+.bottom-grid { display: grid; grid-template-columns: 1.35fr 1fr; gap: 18px; margin-top: 18px; }
+.panel-line { min-width: 0; border-top: 1px solid var(--border); padding-top: 12px; }
+.panel-heading { justify-content: space-between; color: var(--muted); font-family: var(--mono); font-size: 10px; letter-spacing: .09em; }
+.panel-meta { color: var(--muted-2); letter-spacing: 0; }
+.neuron-field { height: 64px; display: flex; align-items: flex-end; gap: 4px; padding-top: 12px; }
+.neuron-field span { flex: 1; max-width: 12px; min-height: 3px; background: rgba(120,228,232,.3); transition: height .22s ease; }
+.neuron-field span:nth-child(3n) { background: rgba(120,228,232,.55); }
+.neuron-field span.hot { background: var(--amber); box-shadow: 0 0 6px rgba(239,181,107,.25); }
+.output-panel { padding-left: 10px; }
+.output-row { display: flex; align-items: center; gap: 10px; margin-top: 10px; }
+.output-token { width: 18px; color: #cad5d8; font-family: var(--mono); font-size: 12px; }
+.output-bar { height: 5px; flex: 1; background: var(--surface-3); }
+.output-bar span { display: block; height: 100%; background: var(--cyan); }
+.output-row:nth-child(3) .output-bar span { background: var(--amber); }
+.output-value { width: 34px; color: var(--muted); font-family: var(--mono); font-size: 10px; text-align: right; }
+.transport { gap: 13px; margin-top: 19px; padding: 10px 0 0; border-top: 1px solid var(--border); }
+.transport-play { display: grid; place-items: center; width: 28px; height: 28px; border: 1px solid var(--border-bright); background: var(--surface-2); color: var(--cyan); cursor: pointer; }
+.transport-meta { justify-content: space-between; width: 96px; gap: 5px; flex-direction: column; align-items: flex-start; }
+.transport-label { color: var(--muted-2); }
+.transport-meta strong { font-family: var(--mono); font-size: 10px; color: #bfcbce; font-weight: 400; }
+.scrubber { --progress: 25%; flex: 1; height: 4px; accent-color: var(--cyan); cursor: pointer; }
+.speed-control { gap: 5px; color: var(--muted-2); border-left: 1px solid var(--border); padding-left: 13px; }
+.speed-control select { border: 0; color: var(--muted); background: transparent; outline: none; font-family: var(--mono); font-size: 10px; }
+.transport-state { display: inline-flex; align-items: center; gap: 6px; color: var(--muted-2); font-family: var(--mono); font-size: 9px; }
+.transport-state-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--cyan); }
+
+.inspector { border-left: 1px solid var(--border); min-width: 0; }
+.inspector-tabs { height: 58px; display: flex; align-items: flex-end; gap: 20px; padding: 0 18px; border-bottom: 1px solid var(--border); }
+.inspector-tabs button { position: relative; padding: 0 0 14px; border: 0; background: transparent; color: var(--muted-2); font-family: var(--mono); font-size: 10px; letter-spacing: .08em; cursor: pointer; }
+.inspector-tabs button.is-selected { color: var(--text); }
+.inspector-tabs button.is-selected::after { position: absolute; content: ''; bottom: -1px; left: 0; right: 0; height: 1px; background: var(--cyan); }
+.tab-count { color: var(--cyan); }
+.inspector-scroll { max-height: calc(100vh - 116px); overflow-y: auto; }
+.inspect-section { padding: 18px; border-bottom: 1px solid var(--border); }
+.section-kicker { color: var(--cyan); margin-bottom: 13px; }
+.node-title-row { gap: 10px; align-items: flex-start; }
+.node-symbol { flex: 0 0 auto; display: grid; place-items: center; width: 31px; height: 31px; border: 1px solid rgba(120,228,232,.45); color: var(--cyan); }
+.node-title-row h2 { margin: 0 0 3px; font-size: 14px; font-weight: 500; }
+.node-title-row p { margin: 0; color: var(--muted-2); font-size: 10px; }
+.node-live { margin-left: auto; padding-top: 4px; color: var(--amber); font-family: var(--mono); font-size: 9px; }
+.metric-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; margin-top: 18px; border: 1px solid var(--border); background: var(--border); }
+.metric { display: grid; gap: 6px; padding: 9px; background: var(--surface); }
+.metric span { color: var(--muted-2); font-family: var(--mono); font-size: 8px; }
+.metric strong { color: #c9d5d8; font-family: var(--mono); font-size: 12px; font-weight: 400; }
+.signal-readout { display: grid; grid-template-columns: repeat(2, 1fr); gap: 9px 14px; margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border); }
+.signal-readout div { display: grid; gap: 5px; min-width: 0; }
+.signal-readout span { color: var(--muted-2); font-family: var(--mono); font-size: 8px; letter-spacing: .04em; }
+.signal-readout strong { overflow: hidden; color: #a9d8da; font-family: var(--mono); font-size: 10px; font-weight: 400; text-overflow: ellipsis; white-space: nowrap; }
+.section-heading { justify-content: space-between; color: var(--muted); font-family: var(--mono); font-size: 10px; letter-spacing: .08em; }
+.section-hint { color: var(--muted-2); font-size: 9px; letter-spacing: 0; }
+.attention-map { display: grid; grid-template-columns: repeat(7, 1fr); gap: 2px; margin-top: 14px; padding: 8px; border: 1px solid var(--border); background: #0d1114; }
+.attention-map span { aspect-ratio: 1; min-width: 0; }
+.legend { gap: 13px; margin-top: 10px; color: var(--muted-2); font-size: 9px; }
+.event-list { display: grid; gap: 11px; margin-top: 15px; }
+.event-row { display: grid; grid-template-columns: 6px 54px 1fr; align-items: center; gap: 8px; min-width: 0; }
+.event-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--muted-2); }
+.event-dot.cyan { background: var(--cyan); }
+.event-dot.amber { background: var(--amber); }
+.event-time { color: var(--muted-2); font-family: var(--mono); font-size: 9px; }
+.event-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: #aab6ba; font-size: 10px; }
+.response-copy { min-height: 58px; margin-top: 14px; padding: 11px; border-left: 1px solid var(--border-bright); background: #0d1114; color: var(--muted-2); font-size: 11px; line-height: 1.6; }
+.response-copy.has-output { color: #c3d0d3; }
+.response-caret { width: 5px; height: 13px; margin: -27px 0 0 12px; background: var(--cyan); animation: caret-blink 1s steps(2, start) infinite; }
+@keyframes caret-blink { 50% { opacity: 0; } }
+.architecture-note .section-heading .icon-button { margin: -8px -8px -8px 0; }
+.architecture-line { display: flex; justify-content: space-between; padding-top: 11px; color: var(--muted); font-size: 11px; }
+.architecture-line strong { color: #c8d3d5; font-family: var(--mono); font-size: 10px; font-weight: 400; }
+.weights-panel { min-height: 436px; border: 1px solid var(--border); background: var(--surface); padding: 26px; margin-top: 18px; }
+.weights-header { display: flex; justify-content: space-between; align-items: flex-start; }
+.weights-header h2 { margin: 9px 0 8px; font-size: 20px; font-weight: 500; }
+.weights-header p { margin: 0; color: var(--muted); font-size: 12px; }
+.outline-button { display: inline-flex; align-items: center; gap: 8px; padding: 8px 10px; border: 1px solid var(--border-bright); color: var(--muted); background: transparent; cursor: pointer; font-size: 11px; }
+.weights-empty { min-height: 230px; display: grid; place-content: center; justify-items: center; gap: 10px; color: var(--cyan); text-align: center; }
+.weights-empty span { color: #c7d2d5; font-size: 13px; }
+.weights-empty small { color: var(--muted-2); font-size: 10px; }
+.toast { position: fixed; right: 24px; bottom: 24px; display: flex; align-items: center; gap: 9px; padding: 11px 12px; border: 1px solid rgba(120,228,232,.32); background: #10191b; color: #c9d8da; box-shadow: 0 8px 30px rgba(0,0,0,.25); font-size: 12px; z-index: 5; }
+.toast svg { color: var(--cyan); }
+.toast button { display: grid; place-items: center; margin-left: 6px; padding: 0; border: 0; color: var(--muted); background: transparent; cursor: pointer; }
+.mobile-history { display: none; }
+.three-d-panel { margin-top: 18px; border: 1px solid var(--border); background: #0a0e11; overflow: hidden; }
+.three-d-toolbar { height: 51px; display: flex; align-items: center; justify-content: space-between; padding: 0 14px; border-bottom: 1px solid var(--border); background: rgba(17,21,26,.86); }
+.three-d-title { display: flex; align-items: center; gap: 10px; }
+.three-d-mark { width: 8px; height: 8px; border: 1px solid var(--cyan); background: rgba(120,228,232,.35); box-shadow: 0 0 10px rgba(120,228,232,.42); transform: rotate(45deg); }
+.three-d-title div { display: grid; gap: 3px; }
+.three-d-title strong { color: #d8e2e4; font-family: var(--mono); font-size: 10px; letter-spacing: .09em; font-weight: 400; }
+.three-d-title small { color: var(--muted-2); font-family: var(--mono); font-size: 9px; }
+.three-d-toolbar-meta { display: flex; align-items: center; gap: 13px; color: var(--muted-2); font-family: var(--mono); font-size: 9px; }
+.three-d-toolbar-meta span { display: inline-flex; align-items: center; gap: 6px; }
+.three-d-viewport { position: relative; height: 390px; background: radial-gradient(circle at 50% 42%, rgba(27,49,52,.28), transparent 48%); }
+.three-d-viewport canvas { display: block; background: #0b0d10; }
+.svg-viewport { cursor: grab; touch-action: none; user-select: none; }
+.svg-viewport:active { cursor: grabbing; }
+.svg-viewport svg { display: block; width: 100%; height: 100%; }
+.projection-grid line { stroke: rgba(119,142,151,.09); stroke-width: 1; }
+.svg-residual { fill: none; stroke: url(#stream-line); stroke-width: 2; stroke-dasharray: 3 7; vector-effect: non-scaling-stroke; transition: stroke-width .2s ease, opacity .2s ease; }
+.svg-residual.is-emphasis { stroke-width: 3.1; opacity: 1; }
+.svg-residual.is-backward { stroke: #efb56b; stroke-dasharray: 8 5; }
+.svg-attention { stroke: rgba(239,181,107,.58); stroke-width: 1.15; stroke-dasharray: 3 4; vector-effect: non-scaling-stroke; transition: stroke-width .2s ease, opacity .2s ease; }
+.svg-attention.is-emphasis { stroke-width: 2.4; opacity: 1; }
+.svg-layer { cursor: pointer; }
+.svg-layer:hover circle:last-of-type { stroke: #e6edf0; stroke-width: 1.5; }
+.svg-selection { fill: none; stroke: #e6edf0; stroke-width: 1.3; stroke-dasharray: 2 2; vector-effect: non-scaling-stroke; }
+.svg-layer-label { fill: #92a1a6; font-family: var(--mono); font-size: 10px; letter-spacing: .05em; text-anchor: middle; }
+.svg-packet { filter: drop-shadow(0 0 5px rgba(120,228,232,.8)); }
+.svg-vision-branch line { stroke: var(--amber); stroke-width: 1.2; stroke-dasharray: 4 5; opacity: .7; }
+.svg-vision-branch circle { fill: var(--amber); opacity: .9; }
+.svg-vision-branch text { fill: var(--amber); font-family: var(--mono); font-size: 9px; letter-spacing: .08em; }
+.svg-aux-branch line { stroke: #efb56b; stroke-width: 1.2; stroke-dasharray: 2 4; opacity: .82; }
+.svg-aux-branch circle { fill: #efb56b; filter: drop-shadow(0 0 4px rgba(239,181,107,.75)); }
+.svg-aux-branch text { fill: #efb56b; font-family: var(--mono); font-size: 9px; letter-spacing: .08em; }
+.svg-axes line { stroke: rgba(120,228,232,.45); stroke-width: 1; }
+.svg-axes text { fill: rgba(120,228,232,.6); font-family: var(--mono); font-size: 9px; }
+.three-d-axis { position: absolute; color: rgba(120,228,232,.5); font-family: var(--mono); font-size: 8px; letter-spacing: .08em; pointer-events: none; }
+.three-d-axis span { color: var(--cyan); font-size: 12px; }
+.axis-x { left: 16px; bottom: 16px; }
+.axis-y { left: 16px; top: 18px; writing-mode: vertical-rl; }
+.three-d-readout { position: absolute; top: 16px; right: 16px; display: grid; gap: 4px; padding-left: 10px; border-left: 1px solid rgba(120,228,232,.45); pointer-events: none; }
+.three-d-readout span, .three-d-readout small { color: var(--muted-2); font-family: var(--mono); font-size: 8px; letter-spacing: .08em; }
+.three-d-readout strong { color: var(--cyan); font-family: var(--mono); font-size: 12px; font-weight: 400; }
+.three-d-hint { position: absolute; left: 50%; bottom: 15px; transform: translateX(-50%); color: rgba(123,135,144,.65); font-family: var(--mono); font-size: 8px; letter-spacing: .06em; pointer-events: none; white-space: nowrap; }
+.three-d-footer { display: flex; justify-content: space-between; gap: 12px; padding: 10px 14px; border-top: 1px solid var(--border); color: var(--muted-2); font-family: var(--mono); font-size: 9px; }
+.three-d-panel.is-loading { min-height: 442px; display: grid; place-content: center; justify-items: center; gap: 10px; color: var(--muted); font-family: var(--mono); font-size: 10px; }
+.spin { animation: spin 1.2s linear infinite; color: var(--cyan); }
+@keyframes spin { to { transform: rotate(360deg); } }
+
+@media (max-width: 1180px) {
+ .workspace { grid-template-columns: 220px minmax(520px, 1fr); }
+ .inspector { display: none; }
+ .brand-context { display: none; }
+}
+
+@media (max-width: 780px) {
+ .topbar { padding: 0 14px; }
+ .topbar-actions { gap: 5px; }
+ .runtime-status span, .model-select svg:first-child, .model-select svg:last-child { display: none; }
+ .model-select { padding: 7px; }
+ .workspace { display: block; }
+ .sidebar { position: fixed; top: 58px; left: 0; bottom: 0; width: min(84vw, 280px); z-index: 4; transform: translateX(-100%); transition: transform .2s ease; box-shadow: 12px 0 30px rgba(0,0,0,.3); }
+ .sidebar.is-open { transform: translateX(0); }
+ .main-stage { padding: 20px 14px; }
+ .stage-header { align-items: flex-start; }
+ .stage-header h1 { font-size: 20px; }
+ .graph-toolbar { align-items: flex-start; gap: 10px; }
+ .graph-legend { flex-wrap: wrap; gap: 8px; }
+ .graph-coordinates { display: none; }
+ .graph-canvas { height: 340px; min-width: 620px; }
+ .graph-panel { overflow-x: auto; }
+ .process-strip { gap: 8px; overflow-x: auto; }
+ .process-note { display: none; }
+ .three-d-toolbar { height: auto; min-height: 51px; align-items: flex-start; gap: 10px; padding-top: 11px; padding-bottom: 11px; }
+ .three-d-toolbar-meta { flex-wrap: wrap; justify-content: flex-end; }
+ .three-d-viewport { height: 330px; }
+ .three-d-footer { flex-wrap: wrap; }
+ .three-d-hint { display: none; }
+ .bottom-grid { grid-template-columns: 1fr; gap: 14px; }
+ .output-panel { padding-left: 0; }
+ .transport-meta { width: 78px; }
+ .transport-state { display: none; }
+ .mobile-history { position: fixed; left: 14px; bottom: 14px; display: grid; place-items: center; width: 38px; height: 38px; border: 1px solid var(--border-bright); background: var(--surface-2); color: var(--cyan); z-index: 3; }
+ .toast { right: 14px; bottom: 14px; left: 64px; }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ *, *::before, *::after { scroll-behavior: auto !important; transition-duration: .01ms !important; animation-duration: .01ms !important; }
+}
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..2f55436
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,12 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 5173,
+ proxy: {
+ '/api': 'http://127.0.0.1:8000',
+ },
+ },
+})