Initial commit: Neural Trace:观察本地 Qwen3.5-4B 计算过程的极简可视化工作台
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@@ -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`。界面展示的是可观测的激活、状态、注意力和梯度信号,不把隐藏推理文字冒充成“模型思想”。
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#0b0d10" />
|
||||
<title>Neural Trace</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+827
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
"""Neural Trace local telemetry server."""
|
||||
+149
@@ -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"},
|
||||
)
|
||||
@@ -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)}
|
||||
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
fastapi>=0.115
|
||||
uvicorn[standard]>=0.34
|
||||
+515
@@ -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 (
|
||||
<button className={`icon-button ${active ? 'is-active' : ''}`} aria-label={label} title={label} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function StatusDot({ tone = 'green' }) {
|
||||
return <span className={`status-dot status-${tone}`} aria-hidden="true" />
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="app-shell">
|
||||
<header className="topbar">
|
||||
<div className="brand-block">
|
||||
<div className="brand-mark"><BrainCircuit size={16} strokeWidth={1.8} /></div>
|
||||
<span className="brand-name">NEURAL TRACE</span>
|
||||
<span className="brand-divider" />
|
||||
<span className="brand-context">LOCAL OBSERVABILITY</span>
|
||||
</div>
|
||||
<div className="topbar-actions">
|
||||
<div className="runtime-status">
|
||||
<StatusDot tone={runtime.connected ? 'green' : runtime.downloaded ? 'cyan' : 'amber'} />
|
||||
<span>{runtime.connected ? 'MODEL READY' : runtime.downloaded ? 'CHECKPOINT READY' : 'DEMO TELEMETRY'}</span>
|
||||
</div>
|
||||
<div className="model-select"><Cpu size={14} /><span>{runtime.model}</span><ChevronDown size={13} /></div>
|
||||
<IconButton label="Help"><CircleHelp size={16} /></IconButton>
|
||||
<IconButton label="Settings"><Settings2 size={16} /></IconButton>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="workspace">
|
||||
<aside className={`sidebar ${showHistory ? 'is-open' : ''}`}>
|
||||
<div className="sidebar-head">
|
||||
<span>SESSIONS</span>
|
||||
<IconButton label="More session options"><MoreHorizontal size={16} /></IconButton>
|
||||
</div>
|
||||
<form className="new-trace" onSubmit={submitPrompt}>
|
||||
<label htmlFor="prompt">Start a trace</label>
|
||||
<div className="prompt-field">
|
||||
<input id="prompt" value={prompt} onChange={(event) => setPrompt(event.target.value)} />
|
||||
<label className="attachment-button" title="Attach image" aria-label="Attach image"><Paperclip size={14} /><input type="file" accept="image/*" onChange={handleAttachment} /></label>
|
||||
<button type="submit" aria-label="Start trace"><Send size={14} /></button>
|
||||
</div>
|
||||
{attachment && <div className="attachment-chip"><span><Paperclip size={12} />{attachment.name}</span><button type="button" onClick={() => setAttachment(null)} aria-label="Remove image"><X size={12} /></button></div>}
|
||||
</form>
|
||||
<div className="session-list">
|
||||
<button className="session-row is-selected" onClick={() => notify('当前 trace 已选中')}>
|
||||
<div className="session-icon"><Activity size={15} /></div>
|
||||
<div className="session-copy"><strong>量子纠缠 · trace 04</strong><span>现在 · 128 / 512 steps</span></div>
|
||||
<span className="session-live"><StatusDot tone="green" /></span>
|
||||
</button>
|
||||
<button className="session-row" onClick={() => notify('历史 trace 还原功能即将接入')}>
|
||||
<div className="session-icon muted"><GitBranch size={15} /></div>
|
||||
<div className="session-copy"><strong>DeltaNet 状态比较</strong><span>昨天 · 384 steps</span></div>
|
||||
</button>
|
||||
<button className="session-row" onClick={() => notify('历史 trace 还原功能即将接入')}>
|
||||
<div className="session-icon muted"><Waypoints size={15} /></div>
|
||||
<div className="session-copy"><strong>长上下文流向</strong><span>8 月 30 日 · 1.2k steps</span></div>
|
||||
</button>
|
||||
</div>
|
||||
<div className="sidebar-footer">
|
||||
<div className="storage-line"><Database size={14} /><span>LOCAL CACHE</span><span className="storage-value">4.06 GB</span></div>
|
||||
<div className="storage-bar"><span style={{ width: '36%' }} /></div>
|
||||
<div className="path-note">{runtime.modelPath || 'D:\\watch\\_LLM\\_think'}</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="main-stage">
|
||||
<div className="stage-header">
|
||||
<div>
|
||||
<div className="crumb"><span>TRACE</span><ArrowDown size={12} /><span>{processLabel}</span></div>
|
||||
<h1>Token flow</h1>
|
||||
</div>
|
||||
<div className="stage-header-actions">
|
||||
<div className="view-switcher" role="tablist" aria-label="Trace view">
|
||||
<button className={activeView === 'trace' && spaceMode === '2d' ? 'is-selected' : ''} onClick={() => { setActiveView('trace'); setSpaceMode('2d') }}>2D Graph</button>
|
||||
<button className={activeView === 'trace' && spaceMode === '3d' ? 'is-selected' : ''} onClick={() => { setActiveView('trace'); setSpaceMode('3d') }}>3D Space</button>
|
||||
<button className={activeView === 'weights' ? 'is-selected' : ''} onClick={() => setActiveView('weights')}>Weights</button>
|
||||
</div>
|
||||
<IconButton label="Reset trace" onClick={() => { setStep(0); setActiveToken(0); notify('trace 已重置') }}><RefreshCw size={15} /></IconButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="token-strip">
|
||||
<span className="strip-label">INPUT TOKENS</span>
|
||||
<div className="token-list">
|
||||
{TOKENS.map((token, index) => <button key={token + index} className={`token ${activeToken === index ? 'is-active' : ''}`} onClick={() => setActiveToken(index)}>{token}</button>)}
|
||||
</div>
|
||||
<span className="token-count">7 TOKENS</span>
|
||||
</div>
|
||||
|
||||
<div className="process-strip">
|
||||
<span className="strip-label">PROCESS</span>
|
||||
<div className="process-switcher" role="tablist" aria-label="Observable compute process">
|
||||
<button className={processMode === 'forward' ? 'is-selected' : ''} onClick={() => setProcessMode('forward')}>Forward</button>
|
||||
<button className={processMode === 'backward' ? 'is-selected' : ''} onClick={() => setProcessMode('backward')}>Backward</button>
|
||||
<button className={processMode === 'attention' ? 'is-selected' : ''} onClick={() => setProcessMode('attention')}>Attention</button>
|
||||
<button className={processMode === 'state' ? 'is-selected' : ''} onClick={() => setProcessMode('state')}>State</button>
|
||||
</div>
|
||||
<span className="process-note">{processMode === 'backward' ? 'gradient / credit assignment' : processMode === 'attention' ? 'token-to-token routing' : processMode === 'state' ? 'recurrent state update' : 'activation / logits'}</span>
|
||||
</div>
|
||||
|
||||
{activeView === 'trace' ? (
|
||||
spaceMode === '3d' ? (
|
||||
<Suspense fallback={<div className="three-d-panel is-loading"><RefreshCw size={17} className="spin" /><span>Loading 3D trace space</span></div>}>
|
||||
<Trace3DCanvas layerCount={Number(architecture.num_layers) || 32} pattern={architecture.pattern} hasImage={Boolean(attachment)} signalKind={liveSignal.kind} step={step} selectedLayer={selectedLayer} setSelectedLayer={setSelectedLayer} activeToken={activeToken} mode={processMode} signalValue={liveSignal.value} />
|
||||
</Suspense>
|
||||
) : (
|
||||
<TraceGraph graphState={graphState} selectedLayer={selectedLayer} setSelectedLayer={setSelectedLayer} activeToken={activeToken} />
|
||||
)
|
||||
) : (
|
||||
<WeightsView onSelect={() => notify('权重检查需要完整精度模型')} />
|
||||
)}
|
||||
|
||||
<div className="bottom-grid">
|
||||
<div className="activation-panel panel-line">
|
||||
<div className="panel-heading"><span>{processMode === 'backward' ? 'GRADIENT FIELD' : 'ACTIVATION FIELD'}</span><span className="panel-meta">{selectedLayer} · {processMode === 'backward' ? 'credit signal' : 'hidden state'}</span></div>
|
||||
<NeuronField step={step} />
|
||||
</div>
|
||||
<div className="output-panel panel-line">
|
||||
<div className="panel-heading"><span>OUTPUT DISTRIBUTION</span><span className="panel-meta">next token</span></div>
|
||||
<div className="output-row"><span className="output-token">纠</span><div className="output-bar"><span style={{ width: '68%' }} /></div><span className="output-value">0.68</span></div>
|
||||
<div className="output-row"><span className="output-token">缠</span><div className="output-bar"><span style={{ width: '22%' }} /></div><span className="output-value">0.22</span></div>
|
||||
<div className="output-row"><span className="output-token">是</span><div className="output-bar"><span style={{ width: '07%' }} /></div><span className="output-value">0.07</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TraceTransport isPlaying={isPlaying} setIsPlaying={setIsPlaying} speed={speed} setSpeed={setSpeed} progress={progress} step={step} setStep={setStep} processLabel={processLabel} />
|
||||
</main>
|
||||
|
||||
<aside className="inspector">
|
||||
<div className="inspector-tabs">
|
||||
<button className="is-selected">INSPECTOR</button>
|
||||
<button>EVENTS <span className="tab-count">{events.length}</span></button>
|
||||
</div>
|
||||
<div className="inspector-scroll">
|
||||
<section className="inspect-section selected-node">
|
||||
<div className="section-kicker">SELECTED NODE</div>
|
||||
<div className="node-title-row"><div className="node-symbol hybrid"><Layers3 size={17} /></div><div><h2>{selectedLayer}</h2><p>{selectedNodeDescription}</p></div><span className="node-live">ACTIVE</span></div>
|
||||
<div className="metric-grid"><Metric label="HEADS" value={architecture.attention_heads} /><Metric label="KV HEADS" value={architecture.attention_kv_heads} /><Metric label="HEAD DIM" value={architecture.attention_head_dim} /><Metric label="RESIDUAL" value={compactNumber(architecture.hidden_size)} /></div>
|
||||
<div className="signal-readout"><div><span>LAST EVENT</span><strong>{liveSignal.kind}</strong></div><div><span>RMS</span><strong>{liveSignal.value == null ? '—' : liveSignal.value.toFixed(4)}</strong></div><div><span>MEAN</span><strong>{liveSignal.mean == null ? '—' : liveSignal.mean.toFixed(4)}</strong></div><div><span>SHAPE</span><strong>{liveSignal.shape?.length ? liveSignal.shape.join(' × ') : '—'}</strong></div></div>
|
||||
</section>
|
||||
|
||||
<section className="inspect-section">
|
||||
<div className="section-heading"><span>ATTENTION MAP</span><span className="section-hint">token × token</span></div>
|
||||
<AttentionMap activeToken={activeToken} values={attentionValues} />
|
||||
<div className="legend"><span><i className="legend-swatch cyan" />active path</span><span><i className="legend-swatch gray" />context</span></div>
|
||||
</section>
|
||||
|
||||
<section className="inspect-section">
|
||||
<div className="section-heading"><span>LIVE EVENTS</span><span className="section-hint">streaming</span></div>
|
||||
<div className="event-list">
|
||||
{events.map((event, index) => <div className="event-row" key={`${event.time}-${index}`}><span className={`event-dot ${event.tone}`} /><span className="event-time">{event.time}</span><span className="event-text">{event.text}</span></div>)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="inspect-section response-section">
|
||||
<div className="section-heading"><span>ASSISTANT OUTPUT</span><span className="section-hint">{responseSource}</span></div>
|
||||
<div className={`response-copy ${responseText ? 'has-output' : ''}`}>{responseText || '等待生成事件…'}</div>
|
||||
{!responseText && <div className="response-caret" />}
|
||||
</section>
|
||||
|
||||
<section className="inspect-section architecture-note">
|
||||
<div className="section-heading"><span>MODEL ARCHITECTURE</span><IconButton label="Architecture info"><CircleHelp size={14} /></IconButton></div>
|
||||
<div className="architecture-line"><span>Layers</span><strong>{architecture.num_layers}</strong></div>
|
||||
<div className="architecture-line"><span>Pattern</span><strong>{architecture.pattern}</strong></div>
|
||||
<div className="architecture-line"><span>Context</span><strong>{compactNumber(architecture.context_length)}</strong></div>
|
||||
<div className="architecture-line"><span>FFN</span><strong>{compactNumber(architecture.intermediate_size)}</strong></div>
|
||||
<div className="architecture-line"><span>Vocabulary</span><strong>{compactNumber(architecture.vocab_size)}</strong></div>
|
||||
<div className="architecture-line"><span>Vision encoder</span><strong>{architecture.vision_encoder ? 'unified' : '—'}</strong></div>
|
||||
<div className="architecture-line"><span>Sparse MoE</span><strong>{architecture.sparse_moe ? 'router + experts' : '—'}</strong></div>
|
||||
<div className="architecture-line"><span>MTP head</span><strong>{architecture.mtp ? 'trained' : '—'}</strong></div>
|
||||
<div className="architecture-line"><span>Precision</span><strong>{architecture.precision}</strong></div>
|
||||
</section>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<button className="mobile-history" onClick={() => setShowHistory((current) => !current)} aria-label="Toggle sessions"><Search size={17} /></button>
|
||||
{notice && <div className="toast"><Radio size={14} />{notice}<button onClick={() => setNotice('')} aria-label="Dismiss"><X size={13} /></button></div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TraceGraph({ graphState, selectedLayer, setSelectedLayer, activeToken }) {
|
||||
const packetOffset = graphState.pulse * 100
|
||||
const selectedLayerNumber = Number(selectedLayer.match(/\d+/)?.[0] || 0)
|
||||
return (
|
||||
<section className="graph-panel">
|
||||
<div className="graph-toolbar"><div className="graph-legend"><span><i className="legend-swatch cyan" />data flow</span><span><i className="legend-swatch amber" />attention</span><span><i className="legend-swatch gray" />residual</span></div><div className="graph-coordinates">X 04.82 Y 08.16 Z 00.00</div></div>
|
||||
<div className="graph-canvas">
|
||||
<div className="grid-overlay" />
|
||||
<svg className="flow-svg" viewBox="0 0 1080 390" preserveAspectRatio="none" aria-label="Model architecture data flow">
|
||||
<defs>
|
||||
<linearGradient id="cyan-flow" x1="0" x2="1"><stop offset="0" stopColor="#78e4e8" stopOpacity="0.18" /><stop offset="1" stopColor="#78e4e8" stopOpacity="0.8" /></linearGradient>
|
||||
<linearGradient id="amber-flow" x1="0" x2="1"><stop offset="0" stopColor="#efb56b" stopOpacity="0.12" /><stop offset="1" stopColor="#efb56b" stopOpacity="0.75" /></linearGradient>
|
||||
</defs>
|
||||
<path className="flow-line residual" d="M72 194 H1008" />
|
||||
{STAGES.map((stage, index) => {
|
||||
const x = 150 + index * 112
|
||||
const active = graphState.activeStage === index
|
||||
return <g key={stage.id}>
|
||||
<path className="flow-line data" d={`M${x - 44} 194 H${x + 44}`} />
|
||||
{active && <circle className={`flow-packet ${index % 2 ? 'amber' : ''}`} cx={x - 44 + packetOffset * 0.88} cy="194" r="4" />}
|
||||
<line className="branch-line" x1={x} y1="194" x2={x} y2="124" />
|
||||
<line className="branch-line" x1={x} y1="194" x2={x} y2="264" />
|
||||
<circle className={`stage-port ${active ? 'active' : ''}`} cx={x} cy="194" r="7" />
|
||||
<circle className="branch-port" cx={x} cy="124" r="3" />
|
||||
<circle className="branch-port" cx={x} cy="264" r="3" />
|
||||
</g>
|
||||
})}
|
||||
<path className="flow-line data" d="M32 194 H108" />
|
||||
<path className="flow-line data" d="M972 194 H1048" />
|
||||
<circle className="stage-port input" cx="32" cy="194" r="7" />
|
||||
<circle className="stage-port output" cx="1048" cy="194" r="7" />
|
||||
</svg>
|
||||
<div className="flow-node input-node"><span className="node-index">IN</span><strong>embeddings</strong><small>7 × 2,560</small></div>
|
||||
{STAGES.map((stage, index) => {
|
||||
const active = graphState.activeStage === index
|
||||
const selected = selectedLayer === stage.title || (selectedLayerNumber >= index * 4 + 1 && selectedLayerNumber <= index * 4 + 4)
|
||||
return <button key={stage.id} className={`flow-node stage-node ${stage.type} ${active ? 'active' : ''} ${selected ? 'selected' : ''}`} style={{ left: `${11.1 + index * 10.37}%` }} onClick={() => setSelectedLayer(stage.title)}>
|
||||
<span className="node-index">{stage.index}</span><strong>{stage.title}</strong><span className="micro-layers" aria-label="3 DeltaNet layers and 1 full attention layer"><i /><i /><i /><i className="attention-mini" /></span><small>{stage.caption}</small>
|
||||
</button>
|
||||
})}
|
||||
<div className="flow-node output-node"><span className="node-index">OUT</span><strong>logits</strong><small>248,320 dim</small></div>
|
||||
<div className="graph-annotation annotation-top"><span className="annotation-rule" />gated state update</div>
|
||||
<div className="graph-annotation annotation-bottom"><span className="annotation-rule amber-rule" />softmax projection</div>
|
||||
</div>
|
||||
<div className="graph-footer"><div><span className="graph-footer-label">RESIDUAL STREAM</span><span className="graph-footer-value">hidden state / layer {selectedLayer.replace('L', '')}</span></div><div className="graph-footer-right"><span>ACTIVE TOKEN</span><strong>{String(activeToken + 1).padStart(2, '0')} · {TOKENS[activeToken]}</strong></div></div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
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 <div className="neuron-field" aria-label="Activation field">
|
||||
{bars.map((value, index) => <span key={index} className={index % 9 === 0 ? 'hot' : ''} style={{ height: `${value * 100}%` }} />)}
|
||||
</div>
|
||||
}
|
||||
|
||||
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 <div className="attention-map">{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 <span key={index} style={{ backgroundColor: `rgba(120, 228, 232, ${intensity})` }} />
|
||||
})}</div>
|
||||
}
|
||||
|
||||
function Metric({ label, value }) {
|
||||
return <div className="metric"><span>{label}</span><strong>{value}</strong></div>
|
||||
}
|
||||
|
||||
function WeightsView({ onSelect }) {
|
||||
return <section className="weights-panel"><div className="weights-header"><div><div className="crumb"><span>TRACE</span><ArrowDown size={12} /><span>WEIGHTS</span></div><h2>Parameter surface</h2><p>Weight-level inspection is reserved for the full-precision checkpoint.</p></div><button className="outline-button" onClick={onSelect}><SlidersHorizontal size={14} />View requirement</button></div><div className="weights-empty"><Sparkles size={22} /><span>Use BF16 safetensors for faithful gradients</span><small>The current runtime can still expose activation flow and token-level events.</small></div></section>
|
||||
}
|
||||
|
||||
function TraceTransport({ isPlaying, setIsPlaying, speed, setSpeed, progress, step, setStep, processLabel }) {
|
||||
return <div className="transport"><button className="transport-play" onClick={() => setIsPlaying((current) => !current)} aria-label={isPlaying ? 'Pause trace' : 'Play trace'}>{isPlaying ? <Pause size={15} fill="currentColor" /> : <Play size={15} fill="currentColor" />}</button><div className="transport-meta"><span className="transport-label">{processLabel}</span><strong>Step {step} / 512</strong></div><input className="scrubber" type="range" min="0" max="512" value={step} onChange={(event) => setStep(Number(event.target.value))} style={{ '--progress': `${progress}%` }} aria-label="Trace step" /><div className="speed-control"><TimerReset size={14} /><select value={speed} onChange={(event) => setSpeed(Number(event.target.value))} aria-label="Playback speed"><option value="0.5">0.5×</option><option value="1">1×</option><option value="2">2×</option><option value="4">4×</option></select></div><span className="transport-state"><span className="transport-state-dot" />{isPlaying ? 'LIVE' : 'PAUSED'}</span></div>
|
||||
}
|
||||
|
||||
export default App
|
||||
@@ -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 <circle cx={projected.x} cy={projected.y} r={4.4 * projected.perspective} fill={reverse ? '#efb56b' : '#78e4e8'} className="svg-packet" />
|
||||
}
|
||||
|
||||
function ProjectionGrid() {
|
||||
return <g className="projection-grid">
|
||||
{Array.from({ length: 13 }, (_, index) => {
|
||||
const x = 40 + index * 77
|
||||
return <line key={`v-${index}`} x1={x} y1="58" x2={x} y2="407" />
|
||||
})}
|
||||
{Array.from({ length: 7 }, (_, index) => {
|
||||
const y = 70 + index * 54
|
||||
return <line key={`h-${index}`} x1="30" y1={y} x2="970" y2={y} />
|
||||
})}
|
||||
</g>
|
||||
}
|
||||
|
||||
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 <section className="three-d-panel">
|
||||
<div className="three-d-toolbar">
|
||||
<div className="three-d-title"><span className="three-d-mark" /><div><strong>3D TRACE SPACE</strong><small>observable compute · step {step} / 512</small></div></div>
|
||||
<div className="three-d-toolbar-meta"><span><i className="legend-swatch cyan" />DeltaNet state</span><span><i className="legend-swatch amber" />full attention</span>{hasImage && <span><i className="legend-swatch amber" />vision input</span>}<span>token {String(activeToken + 1).padStart(2, '0')}</span></div>
|
||||
</div>
|
||||
<div className="three-d-viewport svg-viewport" onPointerDown={handlePointerDown} onPointerMove={handlePointerMove} onPointerUp={stopDragging} onPointerCancel={stopDragging} onPointerLeave={stopDragging} onWheel={handleWheel}>
|
||||
<svg viewBox="0 0 1000 460" role="img" aria-label="3D projected model architecture">
|
||||
<defs>
|
||||
<filter id="node-glow" x="-100%" y="-100%" width="300%" height="300%"><feGaussianBlur stdDeviation="3" result="blur" /><feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge></filter>
|
||||
</defs>
|
||||
<rect width="1000" height="460" fill="#0b0d10" />
|
||||
<ProjectionGrid />
|
||||
{hasImage && <g className="svg-vision-branch"><line x1="76" y1="365" x2={layers[0].node.x} y2={layers[0].node.y} /><circle cx="76" cy="365" r="6" /><text x="58" y="386">IMAGE</text></g>}
|
||||
<polyline points={pointString(residualPoints)} className={`svg-residual ${mode === 'state' ? 'is-emphasis' : ''} ${mode === 'backward' ? 'is-backward' : ''}`} />
|
||||
{attentionLinks.map((link) => <line key={link.id} x1={link.points[0].x} y1={link.points[0].y} x2={link.points[1].x} y2={link.points[1].y} className={`svg-attention ${mode === 'attention' ? 'is-emphasis' : ''}`} />)}
|
||||
{auxLabel && auxLayer && <g className="svg-aux-branch"><line x1={auxLayer.node.x} y1={auxLayer.node.y} x2={auxLayer.node.x + 58} y2={auxLayer.node.y - 34} /><circle cx={auxLayer.node.x + 58} cy={auxLayer.node.y - 34} r="4" /><text x={auxLayer.node.x + 66} y={auxLayer.node.y - 31}>{auxLabel}</text></g>}
|
||||
{sortedLayers.map((layer) => <g key={layer.label} role="button" tabIndex="0" aria-label={`Inspect ${layer.label}`} className={`svg-layer ${layer.selected ? 'is-selected' : ''} ${layer.active ? 'is-active' : ''}`} onClick={(event) => { event.stopPropagation(); setSelectedLayer(layer.label) }} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); setSelectedLayer(layer.label) } }}>
|
||||
{layer.neurons.map((neuron, index) => <circle key={index} cx={neuron.x} cy={neuron.y} r={layer.active ? 2.5 : 1.8} fill={layer.fullAttention ? '#efb56b' : '#78e4e8'} opacity={(0.18 + layer.energy * 0.45) * neuron.perspective} />)}
|
||||
{layer.selected && <circle cx={layer.node.x} cy={layer.node.y} r={12 * layer.node.perspective} className="svg-selection" />}
|
||||
<circle cx={layer.node.x} cy={layer.node.y} r={(layer.active ? 7.6 : 5.2) * layer.node.perspective} fill={layer.fullAttention ? '#efb56b' : '#78e4e8'} opacity={0.98} filter={layer.active ? 'url(#node-glow)' : undefined} />
|
||||
<text x={layer.node.x} y={layer.node.y - 14 * layer.node.perspective} className="svg-layer-label" opacity={0.45 + layer.node.perspective * 0.55}>{layer.label}</text>
|
||||
</g>)}
|
||||
<FlowPacket step={step} yaw={orbit.yaw} pitch={orbit.pitch} mode={mode} layerCount={safeLayerCount} zoom={zoom} />
|
||||
<g className="svg-axes"><line x1="48" y1="416" x2="127" y2="416" /><line x1="48" y1="416" x2="48" y2="337" /><text x="132" y="420">X</text><text x="43" y="330">Y</text><text x="35" y="438">Z / depth</text></g>
|
||||
</svg>
|
||||
<div className="three-d-axis axis-x">RESIDUAL STREAM <span>→</span></div>
|
||||
<div className="three-d-axis axis-y">HIDDEN STATE <span>↕</span></div>
|
||||
<div className="three-d-readout"><span>ACTIVE LAYER</span><strong>{selectedLayer}</strong><small>click a node to inspect</small></div>
|
||||
<div className="three-d-hint">DRAG TO ORBIT · SCROLL TO ZOOM</div>
|
||||
</div>
|
||||
<div className="three-d-footer"><span>{safeLayerCount} LAYERS · {pattern}</span><span>{hasImage ? 'TELEMETRY: VISION / ACTIVATION / STATE / LOGITS' : 'TELEMETRY: ACTIVATION / STATE / LOGITS'}</span><span>PRECISION: BF16 / INT4</span></div>
|
||||
</section>
|
||||
}
|
||||
@@ -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(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
)
|
||||
+311
@@ -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; }
|
||||
}
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
})
|
||||
Reference in New Issue
Block a user