Spaces:
Sleeping
Sleeping
Omar commited on
Commit ·
36079e7
0
Parent(s):
Init commit
Browse files- .dockerignore +14 -0
- Dockerfile +20 -0
- LICENSE +21 -0
- README.md +143 -0
- backend/architecture_parser.py +959 -0
- backend/main.py +173 -0
- backend/requirements.txt +9 -0
- backend/run.sh +7 -0
- frontend/css/styles.css +885 -0
- frontend/index.html +153 -0
- frontend/js/api.js +108 -0
- frontend/js/app.js +710 -0
- frontend/js/treemap.js +638 -0
.dockerignore
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__
|
| 2 |
+
*.pyc
|
| 3 |
+
*.pyo
|
| 4 |
+
.git
|
| 5 |
+
.gitignore
|
| 6 |
+
.env
|
| 7 |
+
*.md
|
| 8 |
+
.venv
|
| 9 |
+
venv
|
| 10 |
+
.idea
|
| 11 |
+
.vscode
|
| 12 |
+
*.egg-info
|
| 13 |
+
dist
|
| 14 |
+
build
|
Dockerfile
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Install dependencies
|
| 6 |
+
COPY backend/requirements.txt .
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
# Copy application code
|
| 10 |
+
COPY backend/ ./backend/
|
| 11 |
+
COPY frontend/ ./frontend/
|
| 12 |
+
|
| 13 |
+
# Set working directory to backend
|
| 14 |
+
WORKDIR /app/backend
|
| 15 |
+
|
| 16 |
+
# Expose port for HuggingFace Spaces
|
| 17 |
+
EXPOSE 7860
|
| 18 |
+
|
| 19 |
+
# Run the application
|
| 20 |
+
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2026 Omar Kamali
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ---
|
| 2 |
+
title: "LLM Scope"
|
| 3 |
+
subtitle: "Explore the inners of your favorite LLMs"
|
| 4 |
+
sdk: docker
|
| 5 |
+
license: mit
|
| 6 |
+
tags:
|
| 7 |
+
- llm
|
| 8 |
+
- visualization
|
| 9 |
+
- transformer
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# LLM Scope — Explore the inners of your favorite LLMs
|
| 13 |
+
|
| 14 |
+
Explore the inners of your favorite LLMs. Visualize transformer architectures and parameter counts without downloading weights.
|
| 15 |
+
|
| 16 |
+
**Space:** https://huggingface.co/spaces/omarkamali/llm-scope
|
| 17 |
+
|
| 18 |
+
**Contact:** omarkamali.com · omneitylabs.com · hf:omarkamali · x.com/omarkamali · linkedin.com/in/omar-kamali
|
| 19 |
+
|
| 20 |
+
---
|
| 21 |
+
|
| 22 |
+
## About / Model Inspector
|
| 23 |
+
|
| 24 |
+
A beautiful, interactive treemap visualizer for transformer model architectures. Enter a HuggingFace model ID or upload a config.json to see a proportional visualization of the model's structure - all without downloading weights.
|
| 25 |
+
|
| 26 |
+
## Features
|
| 27 |
+
|
| 28 |
+
- **No Weights Required**: Calculates parameter counts mathematically from config dimensions only
|
| 29 |
+
- **Interactive Treemap**: D3.js zoomable treemap with area proportional to parameter count
|
| 30 |
+
- **Multiple Architectures**: Supports LLaMA, Mistral, Mixtral, GPT-2, BERT, T5, Falcon, Gemma, Qwen, and more
|
| 31 |
+
- **Beautiful Dark Theme**: Modern, minimal UI designed for ML/AI workflows
|
| 32 |
+
- **Drag & Drop Upload**: Upload your own config.json files for analysis
|
| 33 |
+
|
| 34 |
+
## Local Development
|
| 35 |
+
|
| 36 |
+
### Prerequisites
|
| 37 |
+
|
| 38 |
+
- Python 3.9+
|
| 39 |
+
- Node.js (for serving frontend during development)
|
| 40 |
+
|
| 41 |
+
### Setup
|
| 42 |
+
|
| 43 |
+
1. Clone the repository:
|
| 44 |
+
```bash
|
| 45 |
+
git clone https://huggingface.co/omarkamali/llm-scope.git
|
| 46 |
+
cd model-inspector
|
| 47 |
+
```
|
| 48 |
+
|
| 49 |
+
2. Install Python dependencies:
|
| 50 |
+
```bash
|
| 51 |
+
cd backend
|
| 52 |
+
pip install -r requirements.txt
|
| 53 |
+
```
|
| 54 |
+
|
| 55 |
+
3. Run the server:
|
| 56 |
+
```bash
|
| 57 |
+
uvicorn main:app --reload --port 7860
|
| 58 |
+
```
|
| 59 |
+
|
| 60 |
+
4. Open http://localhost:7860 in your browser
|
| 61 |
+
|
| 62 |
+
### Running with Docker
|
| 63 |
+
|
| 64 |
+
```bash
|
| 65 |
+
docker build -t model-inspector .
|
| 66 |
+
docker run -p 7860:7860 model-inspector
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
## API Endpoints
|
| 70 |
+
|
| 71 |
+
### POST /api/inspect
|
| 72 |
+
|
| 73 |
+
Inspect a model by HuggingFace model ID or config object.
|
| 74 |
+
|
| 75 |
+
**Request Body:**
|
| 76 |
+
```json
|
| 77 |
+
{
|
| 78 |
+
"model_id": "qwen/qwen3-4b-instruct-2507"
|
| 79 |
+
}
|
| 80 |
+
```
|
| 81 |
+
|
| 82 |
+
or
|
| 83 |
+
|
| 84 |
+
```json
|
| 85 |
+
{
|
| 86 |
+
"config": { /* config.json contents */ }
|
| 87 |
+
}
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
**Response:**
|
| 91 |
+
```json
|
| 92 |
+
{
|
| 93 |
+
"tree": {
|
| 94 |
+
"name": "Qwen Model",
|
| 95 |
+
"type": "model",
|
| 96 |
+
"params": 1234,
|
| 97 |
+
"children": [...]
|
| 98 |
+
},
|
| 99 |
+
"metadata": {
|
| 100 |
+
"model_id": "qwen/qwen3-4b-instruct-2507",
|
| 101 |
+
"model_type": "llama",
|
| 102 |
+
"total_params": 1234,
|
| 103 |
+
"formatted_params": "...",
|
| 104 |
+
"config": {...}
|
| 105 |
+
}
|
| 106 |
+
}
|
| 107 |
+
```
|
| 108 |
+
|
| 109 |
+
### POST /api/upload
|
| 110 |
+
|
| 111 |
+
Upload a config.json file for inspection.
|
| 112 |
+
|
| 113 |
+
**Request:** `multipart/form-data` with `file` field
|
| 114 |
+
|
| 115 |
+
### GET /api/health
|
| 116 |
+
|
| 117 |
+
Health check endpoint.
|
| 118 |
+
|
| 119 |
+
## Parameter Calculation
|
| 120 |
+
|
| 121 |
+
Parameters are calculated mathematically from config dimensions:
|
| 122 |
+
|
| 123 |
+
| Component | Formula |
|
| 124 |
+
|-----------|---------|
|
| 125 |
+
| Embedding | vocab_size × hidden_size |
|
| 126 |
+
| Attention Q/K/V | hidden_size × (num_heads × head_dim) |
|
| 127 |
+
| Attention Output | (num_heads × head_dim) × hidden_size |
|
| 128 |
+
| MLP Up | hidden_size × intermediate_size |
|
| 129 |
+
| MLP Gate (SwiGLU) | hidden_size × intermediate_size |
|
| 130 |
+
| MLP Down | intermediate_size × hidden_size |
|
| 131 |
+
| LayerNorm | 2 × hidden_size (weight + bias) |
|
| 132 |
+
| RMSNorm | hidden_size (weight only) |
|
| 133 |
+
| LM Head | hidden_size × vocab_size (if not tied) |
|
| 134 |
+
|
| 135 |
+
## Tech Stack
|
| 136 |
+
|
| 137 |
+
- **Backend**: FastAPI (Python)
|
| 138 |
+
- **Frontend**: Vanilla JS + D3.js
|
| 139 |
+
- **Deployment**: Docker-based HuggingFace Space
|
| 140 |
+
|
| 141 |
+
## License
|
| 142 |
+
|
| 143 |
+
MIT
|
backend/architecture_parser.py
ADDED
|
@@ -0,0 +1,959 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Architecture parser - produces LINEAR PIPELINE representation of transformer models.
|
| 3 |
+
Shows the sequential flow of data through the model as a flowchart.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import re
|
| 7 |
+
from typing import Dict, Any, List, Optional, Tuple
|
| 8 |
+
from collections import OrderedDict
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
import torch.nn as nn
|
| 12 |
+
from transformers import AutoConfig, AutoModel, AutoModelForCausalLM, AutoModelForSeq2SeqLM
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def format_params(count: int) -> str:
|
| 16 |
+
"""Format parameter count in human-readable form."""
|
| 17 |
+
if count >= 1e12:
|
| 18 |
+
return f"{count / 1e12:.2f}T"
|
| 19 |
+
elif count >= 1e9:
|
| 20 |
+
return f"{count / 1e9:.2f}B"
|
| 21 |
+
elif count >= 1e6:
|
| 22 |
+
return f"{count / 1e6:.2f}M"
|
| 23 |
+
elif count >= 1e3:
|
| 24 |
+
return f"{count / 1e3:.2f}K"
|
| 25 |
+
else:
|
| 26 |
+
return str(count)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def get_module_type(module: nn.Module, name: str) -> str:
|
| 30 |
+
"""Infer module type from class name and module name."""
|
| 31 |
+
class_name = module.__class__.__name__.lower()
|
| 32 |
+
name_lower = name.lower()
|
| 33 |
+
|
| 34 |
+
# Check if this is a model wrapper (contains "model" in class name) - should be treated as module
|
| 35 |
+
is_model_wrapper = 'model' in class_name and ('for' in class_name or class_name.endswith('model'))
|
| 36 |
+
|
| 37 |
+
if is_model_wrapper:
|
| 38 |
+
return 'module'
|
| 39 |
+
|
| 40 |
+
if 'embedding' in class_name:
|
| 41 |
+
return 'embedding'
|
| 42 |
+
elif 'attention' in class_name or 'attn' in class_name:
|
| 43 |
+
return 'attention'
|
| 44 |
+
elif 'mlp' in class_name or 'feedforward' in class_name or 'ffn' in class_name:
|
| 45 |
+
return 'mlp'
|
| 46 |
+
elif 'layernorm' in class_name or 'rmsnorm' in class_name:
|
| 47 |
+
return 'norm'
|
| 48 |
+
elif 'linear' in class_name:
|
| 49 |
+
return 'linear'
|
| 50 |
+
elif 'conv' in class_name:
|
| 51 |
+
return 'linear'
|
| 52 |
+
elif 'dropout' in class_name:
|
| 53 |
+
return 'dropout'
|
| 54 |
+
elif 'pool' in class_name:
|
| 55 |
+
return 'pooler'
|
| 56 |
+
elif 'head' in class_name or 'lm_head' in name_lower:
|
| 57 |
+
return 'head'
|
| 58 |
+
# Check for MoE/expert - but only for actual MoE layers, not model wrappers
|
| 59 |
+
elif ('expert' in class_name or 'moe' in class_name) and 'layer' in class_name:
|
| 60 |
+
return 'mlp'
|
| 61 |
+
elif 'expert' in class_name and 'model' not in class_name:
|
| 62 |
+
return 'mlp'
|
| 63 |
+
|
| 64 |
+
# Check name patterns
|
| 65 |
+
if 'embed' in name_lower:
|
| 66 |
+
return 'embedding'
|
| 67 |
+
elif 'attn' in name_lower or 'attention' in name_lower:
|
| 68 |
+
return 'attention'
|
| 69 |
+
elif 'mlp' in name_lower or 'fc' in name_lower or 'ffn' in name_lower:
|
| 70 |
+
return 'mlp'
|
| 71 |
+
elif 'norm' in name_lower or 'ln' in name_lower:
|
| 72 |
+
return 'norm'
|
| 73 |
+
elif 'head' in name_lower:
|
| 74 |
+
return 'head'
|
| 75 |
+
elif 'expert' in name_lower and 'model' not in name_lower:
|
| 76 |
+
return 'mlp'
|
| 77 |
+
|
| 78 |
+
return 'module'
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def count_parameters(module: nn.Module) -> int:
|
| 82 |
+
"""Count all parameters in a module recursively."""
|
| 83 |
+
return sum(p.numel() for p in module.parameters())
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def humanize_name(name: str) -> str:
|
| 87 |
+
"""Convert module name to human-readable format."""
|
| 88 |
+
# Handle indexed names like "0", "1" etc
|
| 89 |
+
if name.isdigit():
|
| 90 |
+
return f"Layer {name}"
|
| 91 |
+
|
| 92 |
+
# Convert snake_case to Title Case
|
| 93 |
+
name = name.replace('_', ' ')
|
| 94 |
+
|
| 95 |
+
# Handle common abbreviations
|
| 96 |
+
replacements = {
|
| 97 |
+
'Wte': 'Token Embedding',
|
| 98 |
+
'Wpe': 'Position Embedding',
|
| 99 |
+
'Ln F': 'Final LayerNorm',
|
| 100 |
+
'Ln 1': 'LayerNorm 1',
|
| 101 |
+
'Ln 2': 'LayerNorm 2',
|
| 102 |
+
'Attn': 'Attention',
|
| 103 |
+
'Mlp': 'MLP',
|
| 104 |
+
'Lm Head': 'LM Head',
|
| 105 |
+
'Q Proj': 'Query',
|
| 106 |
+
'K Proj': 'Key',
|
| 107 |
+
'V Proj': 'Value',
|
| 108 |
+
'O Proj': 'Output',
|
| 109 |
+
'Out Proj': 'Output',
|
| 110 |
+
'C Attn': 'QKV Projection',
|
| 111 |
+
'C Proj': 'Output Projection',
|
| 112 |
+
'C Fc': 'Up Projection',
|
| 113 |
+
'Up Proj': 'Up Projection',
|
| 114 |
+
'Down Proj': 'Down Projection',
|
| 115 |
+
'Gate Proj': 'Gate Projection',
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
result = name.title()
|
| 119 |
+
for old, new in replacements.items():
|
| 120 |
+
result = result.replace(old, new)
|
| 121 |
+
|
| 122 |
+
return result
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
def is_modality_encoder(name: str, module: nn.Module) -> bool:
|
| 126 |
+
"""
|
| 127 |
+
Check if a module is a separate MODALITY encoder (vision tower, audio encoder, etc.)
|
| 128 |
+
This should only match top-level modality-specific encoders, not internal components.
|
| 129 |
+
"""
|
| 130 |
+
name_lower = name.lower()
|
| 131 |
+
class_lower = module.__class__.__name__.lower()
|
| 132 |
+
|
| 133 |
+
# Specific patterns for modality encoders (must have modality keyword)
|
| 134 |
+
modality_keywords = ['vision', 'image', 'audio', 'video', 'visual', 'pixel']
|
| 135 |
+
|
| 136 |
+
# Must contain a modality keyword
|
| 137 |
+
has_modality = any(kw in name_lower or kw in class_lower for kw in modality_keywords)
|
| 138 |
+
if not has_modality:
|
| 139 |
+
return False
|
| 140 |
+
|
| 141 |
+
# And should be a substantial module (tower, model, encoder)
|
| 142 |
+
structure_keywords = ['tower', 'model', 'encoder', 'backbone']
|
| 143 |
+
has_structure = any(kw in name_lower or kw in class_lower for kw in structure_keywords)
|
| 144 |
+
|
| 145 |
+
# Or just "vision_tower", "image_encoder" style names
|
| 146 |
+
return has_structure or name_lower in ['vision', 'visual', 'image']
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def extract_pipeline_steps(module: nn.Module, name: str, depth: int = 0, max_depth: int = 4, detect_parallel: bool = True) -> List[Dict[str, Any]]:
|
| 150 |
+
"""
|
| 151 |
+
Extract pipeline steps from a module.
|
| 152 |
+
Handles both linear and parallel (multimodal) architectures.
|
| 153 |
+
Returns a list of steps where parallel branches are marked.
|
| 154 |
+
|
| 155 |
+
detect_parallel: Only look for parallel modality encoders at top level (depth 0-1)
|
| 156 |
+
"""
|
| 157 |
+
steps = []
|
| 158 |
+
children = list(module.named_children())
|
| 159 |
+
|
| 160 |
+
if not children:
|
| 161 |
+
return steps
|
| 162 |
+
|
| 163 |
+
# Categorize children
|
| 164 |
+
embeddings = []
|
| 165 |
+
vision_modules = [] # Vision tower, projector
|
| 166 |
+
language_model = None # Main language model
|
| 167 |
+
layer_container = None
|
| 168 |
+
layer_list = []
|
| 169 |
+
norms = []
|
| 170 |
+
heads = []
|
| 171 |
+
others = []
|
| 172 |
+
|
| 173 |
+
for child_name, child_module in children:
|
| 174 |
+
child_params = count_parameters(child_module)
|
| 175 |
+
if child_params == 0:
|
| 176 |
+
continue
|
| 177 |
+
|
| 178 |
+
child_type = get_module_type(child_module, child_name)
|
| 179 |
+
name_lower = child_name.lower()
|
| 180 |
+
class_lower = child_module.__class__.__name__.lower()
|
| 181 |
+
|
| 182 |
+
# Detect multimodal components at appropriate depth
|
| 183 |
+
if detect_parallel and depth <= 1:
|
| 184 |
+
# Vision tower or projector
|
| 185 |
+
if is_modality_encoder(child_name, child_module) or 'projector' in name_lower or 'projector' in class_lower:
|
| 186 |
+
vision_modules.append((child_name, child_module))
|
| 187 |
+
continue
|
| 188 |
+
# Main language model (separate from vision)
|
| 189 |
+
if 'language_model' in name_lower or 'text_model' in name_lower:
|
| 190 |
+
language_model = (child_name, child_module)
|
| 191 |
+
continue
|
| 192 |
+
|
| 193 |
+
if child_type == 'embedding':
|
| 194 |
+
embeddings.append((child_name, child_module))
|
| 195 |
+
elif child_type == 'norm':
|
| 196 |
+
norms.append((child_name, child_module))
|
| 197 |
+
elif child_type == 'head':
|
| 198 |
+
heads.append((child_name, child_module))
|
| 199 |
+
elif child_name.isdigit():
|
| 200 |
+
layer_list.append((child_name, child_module))
|
| 201 |
+
elif 'layer' in name_lower or 'block' in name_lower or name_lower == 'h':
|
| 202 |
+
sub_children = list(child_module.named_children())
|
| 203 |
+
if sub_children and sub_children[0][0].isdigit():
|
| 204 |
+
layer_container = (child_name, child_module)
|
| 205 |
+
else:
|
| 206 |
+
others.append((child_name, child_module))
|
| 207 |
+
else:
|
| 208 |
+
others.append((child_name, child_module))
|
| 209 |
+
|
| 210 |
+
# Handle multimodal: vision path + language model as parallel branches
|
| 211 |
+
if vision_modules and language_model:
|
| 212 |
+
parallel_branches = []
|
| 213 |
+
|
| 214 |
+
# Vision branch: vision_tower + projector in sequence
|
| 215 |
+
vision_steps = []
|
| 216 |
+
for vm_name, vm_module in vision_modules:
|
| 217 |
+
vm_substeps = extract_pipeline_steps(vm_module, vm_name, depth + 1, max_depth, detect_parallel=False)
|
| 218 |
+
if vm_substeps:
|
| 219 |
+
step = {
|
| 220 |
+
"name": humanize_name(vm_name),
|
| 221 |
+
"type": "encoder",
|
| 222 |
+
"params": count_parameters(vm_module),
|
| 223 |
+
"class": vm_module.__class__.__name__,
|
| 224 |
+
"substeps": vm_substeps,
|
| 225 |
+
"_collapsed": True,
|
| 226 |
+
}
|
| 227 |
+
else:
|
| 228 |
+
step = build_step(vm_module, vm_name, depth + 1, max_depth)
|
| 229 |
+
vision_steps.append(step)
|
| 230 |
+
|
| 231 |
+
vision_branch = {
|
| 232 |
+
"name": "Vision Path",
|
| 233 |
+
"type": "encoder",
|
| 234 |
+
"params": sum(count_parameters(m) for _, m in vision_modules),
|
| 235 |
+
"substeps": vision_steps,
|
| 236 |
+
"_collapsed": False,
|
| 237 |
+
}
|
| 238 |
+
parallel_branches.append(vision_branch)
|
| 239 |
+
|
| 240 |
+
# Language model branch
|
| 241 |
+
lm_name, lm_module = language_model
|
| 242 |
+
lm_steps = extract_pipeline_steps(lm_module, lm_name, depth + 1, max_depth, detect_parallel=False)
|
| 243 |
+
if not lm_steps:
|
| 244 |
+
lm_steps = [build_step(lm_module, lm_name, depth + 1, max_depth)]
|
| 245 |
+
|
| 246 |
+
lang_branch = {
|
| 247 |
+
"name": "Language Model",
|
| 248 |
+
"type": "module",
|
| 249 |
+
"params": count_parameters(lm_module),
|
| 250 |
+
"class": lm_module.__class__.__name__,
|
| 251 |
+
"substeps": lm_steps,
|
| 252 |
+
"_collapsed": False,
|
| 253 |
+
}
|
| 254 |
+
parallel_branches.append(lang_branch)
|
| 255 |
+
|
| 256 |
+
steps.append({
|
| 257 |
+
"name": "Multimodal Processing",
|
| 258 |
+
"type": "parallel",
|
| 259 |
+
"params": sum(b.get("params", 0) for b in parallel_branches),
|
| 260 |
+
"branches": parallel_branches,
|
| 261 |
+
"_collapsed": False,
|
| 262 |
+
})
|
| 263 |
+
|
| 264 |
+
# Skip normal processing - we handled everything
|
| 265 |
+
embeddings = []
|
| 266 |
+
norms = []
|
| 267 |
+
layer_container = None
|
| 268 |
+
layer_list = []
|
| 269 |
+
others = []
|
| 270 |
+
|
| 271 |
+
# Handle case where only vision modules exist (no separate language_model)
|
| 272 |
+
elif vision_modules:
|
| 273 |
+
for enc_name, enc_module in vision_modules:
|
| 274 |
+
enc_steps = extract_pipeline_steps(enc_module, enc_name, depth + 1, max_depth, detect_parallel=False)
|
| 275 |
+
if enc_steps:
|
| 276 |
+
steps.append({
|
| 277 |
+
"name": humanize_name(enc_name),
|
| 278 |
+
"type": "encoder",
|
| 279 |
+
"params": count_parameters(enc_module),
|
| 280 |
+
"class": enc_module.__class__.__name__,
|
| 281 |
+
"substeps": enc_steps,
|
| 282 |
+
"_collapsed": True,
|
| 283 |
+
})
|
| 284 |
+
else:
|
| 285 |
+
steps.append(build_step(enc_module, enc_name, depth + 1, max_depth))
|
| 286 |
+
|
| 287 |
+
# 1. Regular embeddings (if not already handled in parallel)
|
| 288 |
+
for child_name, child_module in embeddings:
|
| 289 |
+
step = build_step(child_module, child_name, depth + 1, max_depth)
|
| 290 |
+
steps.append(step)
|
| 291 |
+
|
| 292 |
+
# 2. Transformer layers
|
| 293 |
+
if layer_container:
|
| 294 |
+
container_name, container_module = layer_container
|
| 295 |
+
layer_children = [(n, m) for n, m in container_module.named_children() if count_parameters(m) > 0]
|
| 296 |
+
|
| 297 |
+
if layer_children:
|
| 298 |
+
first_layer = layer_children[0][1]
|
| 299 |
+
total_params = sum(count_parameters(m) for _, m in layer_children)
|
| 300 |
+
layer_substeps = extract_layer_internals(first_layer, depth + 2, max_depth)
|
| 301 |
+
layer_shape = get_layer_shape_info(first_layer)
|
| 302 |
+
|
| 303 |
+
layer_step = {
|
| 304 |
+
"name": f"Transformer Layers",
|
| 305 |
+
"type": "layers",
|
| 306 |
+
"params": total_params,
|
| 307 |
+
"class": first_layer.__class__.__name__,
|
| 308 |
+
"count": len(layer_children),
|
| 309 |
+
"substeps": layer_substeps,
|
| 310 |
+
"_collapsed": False,
|
| 311 |
+
}
|
| 312 |
+
if layer_shape:
|
| 313 |
+
layer_step["shape"] = layer_shape
|
| 314 |
+
steps.append(layer_step)
|
| 315 |
+
elif layer_list:
|
| 316 |
+
first_layer = layer_list[0][1]
|
| 317 |
+
total_params = sum(count_parameters(m) for _, m in layer_list)
|
| 318 |
+
layer_substeps = extract_layer_internals(first_layer, depth + 2, max_depth)
|
| 319 |
+
layer_shape = get_layer_shape_info(first_layer)
|
| 320 |
+
|
| 321 |
+
layer_step = {
|
| 322 |
+
"name": f"Transformer Layers",
|
| 323 |
+
"type": "layers",
|
| 324 |
+
"params": total_params,
|
| 325 |
+
"class": first_layer.__class__.__name__,
|
| 326 |
+
"count": len(layer_list),
|
| 327 |
+
"substeps": layer_substeps,
|
| 328 |
+
"_collapsed": False,
|
| 329 |
+
}
|
| 330 |
+
if layer_shape:
|
| 331 |
+
layer_step["shape"] = layer_shape
|
| 332 |
+
steps.append(layer_step)
|
| 333 |
+
|
| 334 |
+
# 3. Other modules
|
| 335 |
+
for child_name, child_module in others:
|
| 336 |
+
child_type = get_module_type(child_module, child_name)
|
| 337 |
+
if child_type == 'module':
|
| 338 |
+
sub_steps = extract_pipeline_steps(child_module, child_name, depth + 1, max_depth, detect_parallel=detect_parallel)
|
| 339 |
+
if sub_steps:
|
| 340 |
+
steps.extend(sub_steps)
|
| 341 |
+
else:
|
| 342 |
+
step = build_step(child_module, child_name, depth + 1, max_depth)
|
| 343 |
+
steps.append(step)
|
| 344 |
+
else:
|
| 345 |
+
step = build_step(child_module, child_name, depth + 1, max_depth)
|
| 346 |
+
steps.append(step)
|
| 347 |
+
|
| 348 |
+
# 4. Final norms
|
| 349 |
+
for child_name, child_module in norms:
|
| 350 |
+
step = build_step(child_module, child_name, depth + 1, max_depth)
|
| 351 |
+
steps.append(step)
|
| 352 |
+
|
| 353 |
+
# 5. Output heads
|
| 354 |
+
for child_name, child_module in heads:
|
| 355 |
+
step = build_step(child_module, child_name, depth + 1, max_depth)
|
| 356 |
+
steps.append(step)
|
| 357 |
+
|
| 358 |
+
return steps
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
def extract_layer_internals(layer_module: nn.Module, depth: int, max_depth: int) -> List[Dict[str, Any]]:
|
| 362 |
+
"""Extract the internal flow of a single transformer layer."""
|
| 363 |
+
steps = []
|
| 364 |
+
children = list(layer_module.named_children())
|
| 365 |
+
|
| 366 |
+
# Categorize
|
| 367 |
+
norms = []
|
| 368 |
+
attentions = []
|
| 369 |
+
mlps = []
|
| 370 |
+
others = []
|
| 371 |
+
|
| 372 |
+
for child_name, child_module in children:
|
| 373 |
+
child_params = count_parameters(child_module)
|
| 374 |
+
if child_params == 0:
|
| 375 |
+
continue
|
| 376 |
+
|
| 377 |
+
child_type = get_module_type(child_module, child_name)
|
| 378 |
+
|
| 379 |
+
if child_type == 'norm':
|
| 380 |
+
norms.append((child_name, child_module))
|
| 381 |
+
elif child_type == 'attention':
|
| 382 |
+
attentions.append((child_name, child_module))
|
| 383 |
+
elif child_type == 'mlp':
|
| 384 |
+
mlps.append((child_name, child_module))
|
| 385 |
+
else:
|
| 386 |
+
others.append((child_name, child_module))
|
| 387 |
+
|
| 388 |
+
# Typical transformer layer flow: norm1 -> attn -> norm2 -> mlp
|
| 389 |
+
# But order depends on architecture (pre-norm vs post-norm)
|
| 390 |
+
|
| 391 |
+
# For now, just order: attention first, then MLP, with norms interspersed
|
| 392 |
+
norm_idx = 0
|
| 393 |
+
|
| 394 |
+
# Attention block
|
| 395 |
+
if norms and norm_idx < len(norms):
|
| 396 |
+
step = build_step(norms[norm_idx][1], norms[norm_idx][0], depth, max_depth)
|
| 397 |
+
steps.append(step)
|
| 398 |
+
norm_idx += 1
|
| 399 |
+
|
| 400 |
+
for child_name, child_module in attentions:
|
| 401 |
+
step = build_step(child_module, child_name, depth, max_depth)
|
| 402 |
+
steps.append(step)
|
| 403 |
+
|
| 404 |
+
# MLP block
|
| 405 |
+
if norms and norm_idx < len(norms):
|
| 406 |
+
step = build_step(norms[norm_idx][1], norms[norm_idx][0], depth, max_depth)
|
| 407 |
+
steps.append(step)
|
| 408 |
+
norm_idx += 1
|
| 409 |
+
|
| 410 |
+
for child_name, child_module in mlps:
|
| 411 |
+
step = build_step(child_module, child_name, depth, max_depth)
|
| 412 |
+
steps.append(step)
|
| 413 |
+
|
| 414 |
+
# Remaining norms
|
| 415 |
+
while norm_idx < len(norms):
|
| 416 |
+
step = build_step(norms[norm_idx][1], norms[norm_idx][0], depth, max_depth)
|
| 417 |
+
steps.append(step)
|
| 418 |
+
norm_idx += 1
|
| 419 |
+
|
| 420 |
+
# Others
|
| 421 |
+
for child_name, child_module in others:
|
| 422 |
+
step = build_step(child_module, child_name, depth, max_depth)
|
| 423 |
+
steps.append(step)
|
| 424 |
+
|
| 425 |
+
return steps
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
def get_module_shape(module: nn.Module) -> Optional[str]:
|
| 429 |
+
"""Extract shape information from a module."""
|
| 430 |
+
class_name = module.__class__.__name__
|
| 431 |
+
|
| 432 |
+
# Linear layers
|
| 433 |
+
if hasattr(module, 'in_features') and hasattr(module, 'out_features'):
|
| 434 |
+
return f"{module.in_features} → {module.out_features}"
|
| 435 |
+
|
| 436 |
+
# Embedding layers
|
| 437 |
+
if hasattr(module, 'num_embeddings') and hasattr(module, 'embedding_dim'):
|
| 438 |
+
return f"{module.num_embeddings} × {module.embedding_dim}"
|
| 439 |
+
|
| 440 |
+
# LayerNorm / RMSNorm - check multiple possible attribute names
|
| 441 |
+
if hasattr(module, 'normalized_shape'):
|
| 442 |
+
shape = module.normalized_shape
|
| 443 |
+
if isinstance(shape, (list, tuple)):
|
| 444 |
+
return f"dim={shape[0]}" if len(shape) == 1 else str(shape)
|
| 445 |
+
return f"dim={shape}"
|
| 446 |
+
|
| 447 |
+
# RMSNorm often uses 'weight' shape
|
| 448 |
+
if 'rmsnorm' in class_name.lower() or 'layernorm' in class_name.lower():
|
| 449 |
+
if hasattr(module, 'weight') and module.weight is not None:
|
| 450 |
+
return f"dim={module.weight.shape[0]}"
|
| 451 |
+
|
| 452 |
+
# Conv layers
|
| 453 |
+
if hasattr(module, 'in_channels') and hasattr(module, 'out_channels'):
|
| 454 |
+
kernel = getattr(module, 'kernel_size', None)
|
| 455 |
+
if kernel:
|
| 456 |
+
return f"{module.in_channels}→{module.out_channels}, k={kernel}"
|
| 457 |
+
return f"{module.in_channels} → {module.out_channels}"
|
| 458 |
+
|
| 459 |
+
# Attention - try to get num_heads and head_dim
|
| 460 |
+
if hasattr(module, 'num_heads'):
|
| 461 |
+
head_dim = getattr(module, 'head_dim', None)
|
| 462 |
+
if head_dim:
|
| 463 |
+
return f"heads={module.num_heads}, dim={head_dim}"
|
| 464 |
+
return f"heads={module.num_heads}"
|
| 465 |
+
|
| 466 |
+
if hasattr(module, 'num_attention_heads'):
|
| 467 |
+
head_dim = getattr(module, 'head_dim', None)
|
| 468 |
+
if head_dim:
|
| 469 |
+
return f"heads={module.num_attention_heads}, dim={head_dim}"
|
| 470 |
+
return f"heads={module.num_attention_heads}"
|
| 471 |
+
|
| 472 |
+
# MLP/FFN - try to infer from children
|
| 473 |
+
if 'mlp' in class_name.lower() or 'feedforward' in class_name.lower():
|
| 474 |
+
# Look for up/gate projection to get intermediate size
|
| 475 |
+
for child_name, child in module.named_children():
|
| 476 |
+
if hasattr(child, 'out_features'):
|
| 477 |
+
return f"→ {child.out_features}"
|
| 478 |
+
|
| 479 |
+
# Try to get hidden_size from config stored on module
|
| 480 |
+
if hasattr(module, 'config'):
|
| 481 |
+
cfg = module.config
|
| 482 |
+
if hasattr(cfg, 'hidden_size'):
|
| 483 |
+
return f"hidden={cfg.hidden_size}"
|
| 484 |
+
|
| 485 |
+
return None
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
def get_layer_shape_info(layer_module: nn.Module) -> Optional[str]:
|
| 489 |
+
"""Extract shape info from a transformer layer by looking at its components."""
|
| 490 |
+
hidden_size = None
|
| 491 |
+
intermediate_size = None
|
| 492 |
+
num_heads = None
|
| 493 |
+
|
| 494 |
+
for name, child in layer_module.named_modules():
|
| 495 |
+
name_lower = name.lower()
|
| 496 |
+
|
| 497 |
+
# Find num_heads
|
| 498 |
+
if not num_heads:
|
| 499 |
+
if hasattr(child, 'num_heads'):
|
| 500 |
+
num_heads = child.num_heads
|
| 501 |
+
elif hasattr(child, 'num_attention_heads'):
|
| 502 |
+
num_heads = child.num_attention_heads
|
| 503 |
+
|
| 504 |
+
# Find hidden_size from multiple sources
|
| 505 |
+
if not hidden_size:
|
| 506 |
+
# From attention head_dim * num_heads
|
| 507 |
+
if hasattr(child, 'num_heads') and hasattr(child, 'head_dim'):
|
| 508 |
+
hidden_size = child.num_heads * child.head_dim
|
| 509 |
+
# From hidden_size attribute
|
| 510 |
+
elif hasattr(child, 'hidden_size'):
|
| 511 |
+
hidden_size = child.hidden_size
|
| 512 |
+
# From norm layers
|
| 513 |
+
elif hasattr(child, 'normalized_shape'):
|
| 514 |
+
shape = child.normalized_shape
|
| 515 |
+
if isinstance(shape, (list, tuple)):
|
| 516 |
+
hidden_size = shape[0]
|
| 517 |
+
else:
|
| 518 |
+
hidden_size = shape
|
| 519 |
+
# From norm weight shape
|
| 520 |
+
elif ('norm' in name_lower or 'ln' in name_lower) and hasattr(child, 'weight') and child.weight is not None:
|
| 521 |
+
try:
|
| 522 |
+
hidden_size = child.weight.shape[0]
|
| 523 |
+
except:
|
| 524 |
+
pass
|
| 525 |
+
# From q_proj or similar linear layers (in_features = hidden_size)
|
| 526 |
+
elif ('q_proj' in name_lower or 'query' in name_lower) and hasattr(child, 'in_features'):
|
| 527 |
+
hidden_size = child.in_features
|
| 528 |
+
# From o_proj output (out_features = hidden_size)
|
| 529 |
+
elif ('o_proj' in name_lower or 'out_proj' in name_lower) and hasattr(child, 'out_features'):
|
| 530 |
+
hidden_size = child.out_features
|
| 531 |
+
|
| 532 |
+
# Find intermediate size from MLP
|
| 533 |
+
if not intermediate_size:
|
| 534 |
+
if ('up' in name_lower or 'gate' in name_lower or 'fc1' in name_lower or 'w1' in name_lower or 'w2' in name_lower) and hasattr(child, 'out_features'):
|
| 535 |
+
intermediate_size = child.out_features
|
| 536 |
+
|
| 537 |
+
parts = []
|
| 538 |
+
if hidden_size:
|
| 539 |
+
parts.append(f"d={hidden_size}")
|
| 540 |
+
if intermediate_size:
|
| 541 |
+
parts.append(f"ffn={intermediate_size}")
|
| 542 |
+
if num_heads:
|
| 543 |
+
parts.append(f"h={num_heads}")
|
| 544 |
+
|
| 545 |
+
return ", ".join(parts) if parts else None
|
| 546 |
+
|
| 547 |
+
|
| 548 |
+
def build_step(module: nn.Module, name: str, depth: int, max_depth: int) -> Dict[str, Any]:
|
| 549 |
+
"""Build a single pipeline step from a module."""
|
| 550 |
+
params = count_parameters(module)
|
| 551 |
+
module_type = get_module_type(module, name)
|
| 552 |
+
display_name = humanize_name(name)
|
| 553 |
+
|
| 554 |
+
step = {
|
| 555 |
+
"name": display_name,
|
| 556 |
+
"type": module_type,
|
| 557 |
+
"params": params,
|
| 558 |
+
"class": module.__class__.__name__,
|
| 559 |
+
}
|
| 560 |
+
|
| 561 |
+
# Add shape information
|
| 562 |
+
shape = get_module_shape(module)
|
| 563 |
+
if shape:
|
| 564 |
+
step["shape"] = shape
|
| 565 |
+
|
| 566 |
+
# Add substeps for complex modules (if not too deep)
|
| 567 |
+
if depth < max_depth:
|
| 568 |
+
children = list(module.named_children())
|
| 569 |
+
if children:
|
| 570 |
+
substeps = []
|
| 571 |
+
for child_name, child_module in children:
|
| 572 |
+
child_params = count_parameters(child_module)
|
| 573 |
+
if child_params > 0:
|
| 574 |
+
child_step = build_step(child_module, child_name, depth + 1, max_depth)
|
| 575 |
+
substeps.append(child_step)
|
| 576 |
+
if substeps:
|
| 577 |
+
step["substeps"] = substeps
|
| 578 |
+
step["_collapsed"] = True
|
| 579 |
+
|
| 580 |
+
return step
|
| 581 |
+
|
| 582 |
+
|
| 583 |
+
def build_pipeline(model: nn.Module, model_name: str = "Model") -> Dict[str, Any]:
|
| 584 |
+
"""
|
| 585 |
+
Build a linear pipeline structure from a PyTorch model.
|
| 586 |
+
This shows the sequential flow of data through the model.
|
| 587 |
+
"""
|
| 588 |
+
total_params = count_parameters(model)
|
| 589 |
+
|
| 590 |
+
# Extract pipeline steps
|
| 591 |
+
steps = extract_pipeline_steps(model, model_name, depth=0, max_depth=4)
|
| 592 |
+
|
| 593 |
+
return {
|
| 594 |
+
"name": model_name,
|
| 595 |
+
"params": total_params,
|
| 596 |
+
"class": model.__class__.__name__,
|
| 597 |
+
"steps": steps
|
| 598 |
+
}
|
| 599 |
+
|
| 600 |
+
|
| 601 |
+
def load_model_for_inspection(model_id: str) -> Tuple[nn.Module, AutoConfig]:
|
| 602 |
+
"""Load a model architecture without downloading weights."""
|
| 603 |
+
from huggingface_hub import hf_hub_download, list_repo_files
|
| 604 |
+
import json
|
| 605 |
+
|
| 606 |
+
# Check if this repo uses Mistral's native format (params.json instead of config.json)
|
| 607 |
+
try:
|
| 608 |
+
repo_files = list_repo_files(repo_id=model_id)
|
| 609 |
+
has_params_json = 'params.json' in repo_files
|
| 610 |
+
has_config_json = 'config.json' in repo_files
|
| 611 |
+
except:
|
| 612 |
+
has_params_json = False
|
| 613 |
+
has_config_json = True
|
| 614 |
+
|
| 615 |
+
if has_params_json and not has_config_json:
|
| 616 |
+
# Load Mistral native format and convert to pipeline directly
|
| 617 |
+
return None, None # Signal to use parse_mistral_params instead
|
| 618 |
+
|
| 619 |
+
config = AutoConfig.from_pretrained(model_id, trust_remote_code=True)
|
| 620 |
+
|
| 621 |
+
# Use meta device to avoid allocating actual memory for weights
|
| 622 |
+
with torch.device('meta'):
|
| 623 |
+
model = None
|
| 624 |
+
errors = []
|
| 625 |
+
|
| 626 |
+
try:
|
| 627 |
+
model = AutoModelForCausalLM.from_config(config, trust_remote_code=True)
|
| 628 |
+
except Exception as e:
|
| 629 |
+
errors.append(f"CausalLM: {e}")
|
| 630 |
+
|
| 631 |
+
if model is None:
|
| 632 |
+
try:
|
| 633 |
+
model = AutoModelForSeq2SeqLM.from_config(config, trust_remote_code=True)
|
| 634 |
+
except Exception as e:
|
| 635 |
+
errors.append(f"Seq2SeqLM: {e}")
|
| 636 |
+
|
| 637 |
+
if model is None:
|
| 638 |
+
try:
|
| 639 |
+
model = AutoModel.from_config(config, trust_remote_code=True)
|
| 640 |
+
except Exception as e:
|
| 641 |
+
errors.append(f"AutoModel: {e}")
|
| 642 |
+
|
| 643 |
+
if model is None:
|
| 644 |
+
raise ValueError(f"Could not load model architecture. Errors: {errors}")
|
| 645 |
+
|
| 646 |
+
return model, config
|
| 647 |
+
|
| 648 |
+
|
| 649 |
+
def parse_mistral_native_format(model_id: str) -> Dict[str, Any]:
|
| 650 |
+
"""Parse Mistral's native params.json format."""
|
| 651 |
+
from huggingface_hub import hf_hub_download
|
| 652 |
+
import json
|
| 653 |
+
|
| 654 |
+
params_path = hf_hub_download(repo_id=model_id, filename='params.json')
|
| 655 |
+
with open(params_path) as f:
|
| 656 |
+
params = json.load(f)
|
| 657 |
+
|
| 658 |
+
# Extract dimensions
|
| 659 |
+
hidden_size = params.get('dim', 0)
|
| 660 |
+
num_layers = params.get('n_layers', 0)
|
| 661 |
+
num_heads = params.get('n_heads', 0)
|
| 662 |
+
num_kv_heads = params.get('n_kv_heads', num_heads)
|
| 663 |
+
vocab_size = params.get('vocab_size', 0)
|
| 664 |
+
intermediate_size = params.get('hidden_dim', hidden_size * 4)
|
| 665 |
+
head_dim = params.get('head_dim', hidden_size // num_heads if num_heads > 0 else 0)
|
| 666 |
+
|
| 667 |
+
# Check for MoE
|
| 668 |
+
moe_config = params.get('moe', {})
|
| 669 |
+
num_experts = moe_config.get('num_experts', 0)
|
| 670 |
+
num_experts_per_tok = moe_config.get('num_experts_per_tok', 2)
|
| 671 |
+
expert_hidden_dim = moe_config.get('expert_hidden_dim', intermediate_size)
|
| 672 |
+
num_shared_experts = moe_config.get('num_shared_experts', 0)
|
| 673 |
+
first_k_dense = moe_config.get('first_k_dense_replace', 0) # First K layers use dense MLP
|
| 674 |
+
|
| 675 |
+
# Check for vision encoder
|
| 676 |
+
vision_config = params.get('vision_encoder', None)
|
| 677 |
+
|
| 678 |
+
# Calculate parameters
|
| 679 |
+
embed_params = vocab_size * hidden_size
|
| 680 |
+
|
| 681 |
+
# Attention params per layer (with potential LoRA/MLA components)
|
| 682 |
+
q_lora_rank = params.get('q_lora_rank', 0)
|
| 683 |
+
kv_lora_rank = params.get('kv_lora_rank', 0)
|
| 684 |
+
v_head_dim = params.get('v_head_dim', head_dim) # V uses different head dim
|
| 685 |
+
|
| 686 |
+
if q_lora_rank > 0:
|
| 687 |
+
# Multi-head Latent Attention (MLA) - compressed projections
|
| 688 |
+
# Q: down_proj + up_proj
|
| 689 |
+
q_params = hidden_size * q_lora_rank + q_lora_rank * num_heads * head_dim
|
| 690 |
+
# K: down_proj + up_proj (shared with V in latent space)
|
| 691 |
+
k_params = hidden_size * kv_lora_rank + kv_lora_rank * num_kv_heads * head_dim
|
| 692 |
+
# V: uses v_head_dim
|
| 693 |
+
v_params = hidden_size * kv_lora_rank + kv_lora_rank * num_kv_heads * v_head_dim
|
| 694 |
+
# O: output projection from v_head_dim back to hidden
|
| 695 |
+
o_params = num_heads * v_head_dim * hidden_size
|
| 696 |
+
attn_params = q_params + k_params + v_params + o_params
|
| 697 |
+
else:
|
| 698 |
+
q_params = hidden_size * num_heads * head_dim
|
| 699 |
+
kv_params = hidden_size * num_kv_heads * head_dim
|
| 700 |
+
attn_params = q_params + 2 * kv_params + num_heads * head_dim * hidden_size
|
| 701 |
+
|
| 702 |
+
norm_params = hidden_size
|
| 703 |
+
|
| 704 |
+
# MLP params - handle dense vs MoE layers
|
| 705 |
+
dense_mlp_params = 3 * hidden_size * intermediate_size
|
| 706 |
+
|
| 707 |
+
if num_experts > 0:
|
| 708 |
+
# MoE: each expert has gate + up + down projections
|
| 709 |
+
single_expert_params = 3 * hidden_size * expert_hidden_dim
|
| 710 |
+
moe_mlp_params = num_experts * single_expert_params
|
| 711 |
+
if num_shared_experts > 0:
|
| 712 |
+
# Shared experts use same size as routed experts
|
| 713 |
+
moe_mlp_params += num_shared_experts * single_expert_params
|
| 714 |
+
moe_mlp_params += hidden_size * num_experts # Router
|
| 715 |
+
|
| 716 |
+
# Calculate layer params for dense and MoE layers separately
|
| 717 |
+
num_dense_layers = min(first_k_dense, num_layers)
|
| 718 |
+
num_moe_layers = num_layers - num_dense_layers
|
| 719 |
+
|
| 720 |
+
dense_layer_params = attn_params + dense_mlp_params + 2 * norm_params
|
| 721 |
+
moe_layer_params = attn_params + moe_mlp_params + 2 * norm_params
|
| 722 |
+
|
| 723 |
+
total_layer_params = (dense_layer_params * num_dense_layers) + (moe_layer_params * num_moe_layers)
|
| 724 |
+
mlp_params = moe_mlp_params # For display purposes, show MoE params
|
| 725 |
+
else:
|
| 726 |
+
mlp_params = dense_mlp_params
|
| 727 |
+
layer_params = attn_params + mlp_params + 2 * norm_params
|
| 728 |
+
total_layer_params = layer_params * num_layers
|
| 729 |
+
lm_head_params = 0 if params.get('tied_embeddings', True) else vocab_size * hidden_size
|
| 730 |
+
total_params = embed_params + total_layer_params + norm_params + lm_head_params
|
| 731 |
+
|
| 732 |
+
# Vision encoder params
|
| 733 |
+
vision_params = 0
|
| 734 |
+
vision_steps = []
|
| 735 |
+
if vision_config:
|
| 736 |
+
v_hidden = vision_config.get('hidden_size', 0)
|
| 737 |
+
v_layers = vision_config.get('num_hidden_layers', 0)
|
| 738 |
+
v_intermediate = vision_config.get('intermediate_size', v_hidden * 4)
|
| 739 |
+
v_heads = vision_config.get('num_attention_heads', 0)
|
| 740 |
+
patch_size = vision_config.get('patch_size', 14)
|
| 741 |
+
|
| 742 |
+
patch_embed_params = 3 * (patch_size ** 2) * v_hidden
|
| 743 |
+
v_attn = 4 * v_hidden * v_hidden
|
| 744 |
+
v_mlp = 2 * v_hidden * v_intermediate
|
| 745 |
+
v_layer_params = v_attn + v_mlp + 2 * v_hidden
|
| 746 |
+
vision_params = patch_embed_params + v_layer_params * v_layers
|
| 747 |
+
|
| 748 |
+
vision_steps = [
|
| 749 |
+
{
|
| 750 |
+
"name": "Patch Embedding",
|
| 751 |
+
"type": "embedding",
|
| 752 |
+
"params": patch_embed_params,
|
| 753 |
+
"shape": f"{patch_size}×{patch_size} patches → {v_hidden}",
|
| 754 |
+
"class": "Conv2d"
|
| 755 |
+
},
|
| 756 |
+
{
|
| 757 |
+
"name": "Vision Transformer Layers",
|
| 758 |
+
"type": "layers",
|
| 759 |
+
"params": v_layer_params * v_layers,
|
| 760 |
+
"count": v_layers,
|
| 761 |
+
"shape": f"d={v_hidden}, h={v_heads}",
|
| 762 |
+
"class": "ViTBlock",
|
| 763 |
+
"_collapsed": True
|
| 764 |
+
}
|
| 765 |
+
]
|
| 766 |
+
total_params += vision_params
|
| 767 |
+
|
| 768 |
+
# Build pipeline
|
| 769 |
+
steps = []
|
| 770 |
+
|
| 771 |
+
# Embedding
|
| 772 |
+
steps.append({
|
| 773 |
+
"name": "Token Embedding",
|
| 774 |
+
"type": "embedding",
|
| 775 |
+
"params": embed_params,
|
| 776 |
+
"shape": f"{vocab_size:,} × {hidden_size}",
|
| 777 |
+
"class": "Embedding"
|
| 778 |
+
})
|
| 779 |
+
|
| 780 |
+
# Build layer substeps
|
| 781 |
+
layer_substeps = [
|
| 782 |
+
{
|
| 783 |
+
"name": "Input LayerNorm",
|
| 784 |
+
"type": "norm",
|
| 785 |
+
"params": norm_params,
|
| 786 |
+
"shape": f"dim={hidden_size}",
|
| 787 |
+
"class": "RMSNorm"
|
| 788 |
+
},
|
| 789 |
+
{
|
| 790 |
+
"name": "Self Attention",
|
| 791 |
+
"type": "attention",
|
| 792 |
+
"params": attn_params,
|
| 793 |
+
"shape": f"heads={num_heads}, kv_heads={num_kv_heads}, dim={head_dim}",
|
| 794 |
+
"class": "Attention",
|
| 795 |
+
"_collapsed": True
|
| 796 |
+
},
|
| 797 |
+
{
|
| 798 |
+
"name": "Post-Attention LayerNorm",
|
| 799 |
+
"type": "norm",
|
| 800 |
+
"params": norm_params,
|
| 801 |
+
"shape": f"dim={hidden_size}",
|
| 802 |
+
"class": "RMSNorm"
|
| 803 |
+
}
|
| 804 |
+
]
|
| 805 |
+
|
| 806 |
+
if num_experts > 0:
|
| 807 |
+
layer_substeps.append({
|
| 808 |
+
"name": "MoE",
|
| 809 |
+
"type": "mlp",
|
| 810 |
+
"params": mlp_params,
|
| 811 |
+
"shape": f"{num_experts} experts, top-{num_experts_per_tok}",
|
| 812 |
+
"class": "MixtureOfExperts",
|
| 813 |
+
"_collapsed": True
|
| 814 |
+
})
|
| 815 |
+
layer_shape = f"d={hidden_size}, ffn={expert_hidden_dim}, h={num_heads}, experts={num_experts}"
|
| 816 |
+
else:
|
| 817 |
+
layer_substeps.append({
|
| 818 |
+
"name": "MLP",
|
| 819 |
+
"type": "mlp",
|
| 820 |
+
"params": mlp_params,
|
| 821 |
+
"shape": f"{hidden_size} → {intermediate_size} → {hidden_size}",
|
| 822 |
+
"class": "MLP",
|
| 823 |
+
"_collapsed": True
|
| 824 |
+
})
|
| 825 |
+
layer_shape = f"d={hidden_size}, ffn={intermediate_size}, h={num_heads}"
|
| 826 |
+
|
| 827 |
+
moe_label = " (MoE)" if num_experts > 0 else ""
|
| 828 |
+
steps.append({
|
| 829 |
+
"name": f"Transformer Layers{moe_label}",
|
| 830 |
+
"type": "layers",
|
| 831 |
+
"params": total_layer_params,
|
| 832 |
+
"count": num_layers,
|
| 833 |
+
"shape": layer_shape,
|
| 834 |
+
"class": "TransformerBlock",
|
| 835 |
+
"substeps": layer_substeps,
|
| 836 |
+
"_collapsed": False
|
| 837 |
+
})
|
| 838 |
+
|
| 839 |
+
# Final norm
|
| 840 |
+
steps.append({
|
| 841 |
+
"name": "Final LayerNorm",
|
| 842 |
+
"type": "norm",
|
| 843 |
+
"params": norm_params,
|
| 844 |
+
"shape": f"dim={hidden_size}",
|
| 845 |
+
"class": "RMSNorm"
|
| 846 |
+
})
|
| 847 |
+
|
| 848 |
+
# LM Head
|
| 849 |
+
steps.append({
|
| 850 |
+
"name": "LM Head",
|
| 851 |
+
"type": "head",
|
| 852 |
+
"params": lm_head_params if lm_head_params > 0 else embed_params,
|
| 853 |
+
"shape": f"{hidden_size} → {vocab_size:,}" + (" (tied)" if lm_head_params == 0 else ""),
|
| 854 |
+
"class": "Linear"
|
| 855 |
+
})
|
| 856 |
+
|
| 857 |
+
# Wrap with vision if present
|
| 858 |
+
if vision_config:
|
| 859 |
+
vision_branch = {
|
| 860 |
+
"name": "Vision Encoder",
|
| 861 |
+
"type": "encoder",
|
| 862 |
+
"params": vision_params,
|
| 863 |
+
"substeps": vision_steps,
|
| 864 |
+
"_collapsed": True
|
| 865 |
+
}
|
| 866 |
+
|
| 867 |
+
lang_branch = {
|
| 868 |
+
"name": "Language Model",
|
| 869 |
+
"type": "module",
|
| 870 |
+
"params": total_params - vision_params,
|
| 871 |
+
"substeps": steps,
|
| 872 |
+
"_collapsed": False
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
steps = [{
|
| 876 |
+
"name": "Multimodal Processing",
|
| 877 |
+
"type": "parallel",
|
| 878 |
+
"params": total_params,
|
| 879 |
+
"branches": [vision_branch, lang_branch],
|
| 880 |
+
"_collapsed": False
|
| 881 |
+
}]
|
| 882 |
+
|
| 883 |
+
model_type = "mistral"
|
| 884 |
+
if num_experts > 0:
|
| 885 |
+
model_type = "mistral_moe"
|
| 886 |
+
|
| 887 |
+
return {
|
| 888 |
+
"name": model_type.upper(),
|
| 889 |
+
"params": total_params,
|
| 890 |
+
"formatted_params": format_params(total_params),
|
| 891 |
+
"model_type": model_type,
|
| 892 |
+
"class": "MistralModel",
|
| 893 |
+
"steps": steps
|
| 894 |
+
}
|
| 895 |
+
|
| 896 |
+
|
| 897 |
+
def load_model_from_config(config_dict: Dict[str, Any]) -> Tuple[nn.Module, AutoConfig]:
|
| 898 |
+
"""Load a model architecture from a config dictionary."""
|
| 899 |
+
config = AutoConfig.for_model(**config_dict)
|
| 900 |
+
|
| 901 |
+
with torch.device('meta'):
|
| 902 |
+
model = None
|
| 903 |
+
errors = []
|
| 904 |
+
|
| 905 |
+
try:
|
| 906 |
+
model = AutoModelForCausalLM.from_config(config, trust_remote_code=True)
|
| 907 |
+
except Exception as e:
|
| 908 |
+
errors.append(f"CausalLM: {e}")
|
| 909 |
+
|
| 910 |
+
if model is None:
|
| 911 |
+
try:
|
| 912 |
+
model = AutoModelForSeq2SeqLM.from_config(config, trust_remote_code=True)
|
| 913 |
+
except Exception as e:
|
| 914 |
+
errors.append(f"Seq2SeqLM: {e}")
|
| 915 |
+
|
| 916 |
+
if model is None:
|
| 917 |
+
try:
|
| 918 |
+
model = AutoModel.from_config(config, trust_remote_code=True)
|
| 919 |
+
except Exception as e:
|
| 920 |
+
errors.append(f"AutoModel: {e}")
|
| 921 |
+
|
| 922 |
+
if model is None:
|
| 923 |
+
raise ValueError(f"Could not load model from config. Errors: {errors}")
|
| 924 |
+
|
| 925 |
+
return model, config
|
| 926 |
+
|
| 927 |
+
|
| 928 |
+
def parse_model(model_id: str) -> Dict[str, Any]:
|
| 929 |
+
"""Parse a model from HuggingFace and return pipeline structure."""
|
| 930 |
+
model, config = load_model_for_inspection(model_id)
|
| 931 |
+
|
| 932 |
+
# If model is None, it means we need to use Mistral native format
|
| 933 |
+
if model is None and config is None:
|
| 934 |
+
return parse_mistral_native_format(model_id)
|
| 935 |
+
|
| 936 |
+
model_name = getattr(config, 'model_type', 'Model').upper()
|
| 937 |
+
pipeline = build_pipeline(model, model_name)
|
| 938 |
+
|
| 939 |
+
total_params = count_parameters(model)
|
| 940 |
+
pipeline["params"] = total_params
|
| 941 |
+
pipeline["formatted_params"] = format_params(total_params)
|
| 942 |
+
pipeline["model_type"] = getattr(config, 'model_type', 'unknown')
|
| 943 |
+
|
| 944 |
+
return pipeline
|
| 945 |
+
|
| 946 |
+
|
| 947 |
+
def parse_config(config_dict: Dict[str, Any]) -> Dict[str, Any]:
|
| 948 |
+
"""Parse a model from config dict and return pipeline structure."""
|
| 949 |
+
model, config = load_model_from_config(config_dict)
|
| 950 |
+
|
| 951 |
+
model_name = getattr(config, 'model_type', 'Model').upper()
|
| 952 |
+
pipeline = build_pipeline(model, model_name)
|
| 953 |
+
|
| 954 |
+
total_params = count_parameters(model)
|
| 955 |
+
pipeline["params"] = total_params
|
| 956 |
+
pipeline["formatted_params"] = format_params(total_params)
|
| 957 |
+
pipeline["model_type"] = getattr(config, 'model_type', 'unknown')
|
| 958 |
+
|
| 959 |
+
return pipeline
|
backend/main.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
FastAPI backend for Model Inspector.
|
| 3 |
+
Provides endpoints to inspect model architectures from HuggingFace.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import json
|
| 7 |
+
import os
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
from typing import Optional, Dict, Any
|
| 10 |
+
|
| 11 |
+
from fastapi import FastAPI, HTTPException, UploadFile, File
|
| 12 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 13 |
+
from fastapi.staticfiles import StaticFiles
|
| 14 |
+
from fastapi.responses import FileResponse, HTMLResponse
|
| 15 |
+
from pydantic import BaseModel
|
| 16 |
+
|
| 17 |
+
from architecture_parser import parse_model, parse_config, format_params
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# Get paths
|
| 21 |
+
BACKEND_DIR = Path(__file__).parent
|
| 22 |
+
FRONTEND_DIR = BACKEND_DIR.parent / "frontend"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
app = FastAPI(
|
| 26 |
+
title="Model Inspector API",
|
| 27 |
+
description="Inspect transformer model architectures without downloading weights",
|
| 28 |
+
version="1.0.0"
|
| 29 |
+
)
|
| 30 |
+
|
| 31 |
+
# CORS for local development
|
| 32 |
+
app.add_middleware(
|
| 33 |
+
CORSMiddleware,
|
| 34 |
+
allow_origins=["*"],
|
| 35 |
+
allow_credentials=True,
|
| 36 |
+
allow_methods=["*"],
|
| 37 |
+
allow_headers=["*"],
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class InspectRequest(BaseModel):
|
| 42 |
+
model_config = {"protected_namespaces": ()}
|
| 43 |
+
model_id: Optional[str] = None
|
| 44 |
+
config: Optional[Dict[str, Any]] = None
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class ModelMetadata(BaseModel):
|
| 48 |
+
model_config = {"protected_namespaces": ()}
|
| 49 |
+
model_id: Optional[str]
|
| 50 |
+
model_type: str
|
| 51 |
+
total_params: int
|
| 52 |
+
formatted_params: str
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class InspectResponse(BaseModel):
|
| 56 |
+
pipeline: Dict[str, Any]
|
| 57 |
+
metadata: ModelMetadata
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
@app.post("/api/inspect", response_model=InspectResponse)
|
| 61 |
+
async def inspect_model(request: InspectRequest):
|
| 62 |
+
"""
|
| 63 |
+
Inspect a model architecture.
|
| 64 |
+
|
| 65 |
+
Provide either:
|
| 66 |
+
- model_id: HuggingFace model ID (e.g., "meta-llama/Llama-2-7b-hf")
|
| 67 |
+
- config: Direct config.json object
|
| 68 |
+
"""
|
| 69 |
+
if request.model_id is None and request.config is None:
|
| 70 |
+
raise HTTPException(
|
| 71 |
+
status_code=400,
|
| 72 |
+
detail="Must provide either model_id or config"
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
try:
|
| 76 |
+
if request.model_id is not None:
|
| 77 |
+
# Parse from HuggingFace model ID
|
| 78 |
+
pipeline = parse_model(request.model_id)
|
| 79 |
+
model_id = request.model_id
|
| 80 |
+
else:
|
| 81 |
+
# Parse from config dict
|
| 82 |
+
pipeline = parse_config(request.config)
|
| 83 |
+
model_id = None
|
| 84 |
+
|
| 85 |
+
metadata = ModelMetadata(
|
| 86 |
+
model_id=model_id,
|
| 87 |
+
model_type=pipeline.get("model_type", "unknown"),
|
| 88 |
+
total_params=pipeline.get("params", 0),
|
| 89 |
+
formatted_params=pipeline.get("formatted_params", "0"),
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
return InspectResponse(pipeline=pipeline, metadata=metadata)
|
| 93 |
+
|
| 94 |
+
except Exception as e:
|
| 95 |
+
raise HTTPException(
|
| 96 |
+
status_code=500,
|
| 97 |
+
detail=f"Error inspecting model: {str(e)}"
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
@app.post("/api/upload", response_model=InspectResponse)
|
| 102 |
+
async def upload_config(file: UploadFile = File(...)):
|
| 103 |
+
"""
|
| 104 |
+
Upload a config.json file for inspection.
|
| 105 |
+
"""
|
| 106 |
+
if not file.filename.endswith('.json'):
|
| 107 |
+
raise HTTPException(
|
| 108 |
+
status_code=400,
|
| 109 |
+
detail="File must be a JSON file"
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
try:
|
| 113 |
+
content = await file.read()
|
| 114 |
+
config_dict = json.loads(content)
|
| 115 |
+
except json.JSONDecodeError:
|
| 116 |
+
raise HTTPException(
|
| 117 |
+
status_code=400,
|
| 118 |
+
detail="Invalid JSON file"
|
| 119 |
+
)
|
| 120 |
+
|
| 121 |
+
try:
|
| 122 |
+
pipeline = parse_config(config_dict)
|
| 123 |
+
|
| 124 |
+
metadata = ModelMetadata(
|
| 125 |
+
model_id=None,
|
| 126 |
+
model_type=pipeline.get("model_type", "unknown"),
|
| 127 |
+
total_params=pipeline.get("params", 0),
|
| 128 |
+
formatted_params=pipeline.get("formatted_params", "0"),
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
return InspectResponse(pipeline=pipeline, metadata=metadata)
|
| 132 |
+
|
| 133 |
+
except Exception as e:
|
| 134 |
+
raise HTTPException(
|
| 135 |
+
status_code=500,
|
| 136 |
+
detail=f"Error parsing config: {str(e)}"
|
| 137 |
+
)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
@app.get("/api/health")
|
| 141 |
+
async def health_check():
|
| 142 |
+
"""Health check endpoint."""
|
| 143 |
+
return {"status": "healthy"}
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
# Serve frontend static files
|
| 147 |
+
@app.get("/")
|
| 148 |
+
async def serve_index():
|
| 149 |
+
"""Serve the main index.html."""
|
| 150 |
+
return FileResponse(FRONTEND_DIR / "index.html")
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
@app.get("/css/{path:path}")
|
| 154 |
+
async def serve_css(path: str):
|
| 155 |
+
"""Serve CSS files."""
|
| 156 |
+
file_path = FRONTEND_DIR / "css" / path
|
| 157 |
+
if file_path.exists():
|
| 158 |
+
return FileResponse(file_path, media_type="text/css")
|
| 159 |
+
raise HTTPException(status_code=404, detail="File not found")
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
@app.get("/js/{path:path}")
|
| 163 |
+
async def serve_js(path: str):
|
| 164 |
+
"""Serve JavaScript files."""
|
| 165 |
+
file_path = FRONTEND_DIR / "js" / path
|
| 166 |
+
if file_path.exists():
|
| 167 |
+
return FileResponse(file_path, media_type="application/javascript")
|
| 168 |
+
raise HTTPException(status_code=404, detail="File not found")
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
if __name__ == "__main__":
|
| 172 |
+
import uvicorn
|
| 173 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
backend/requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.109.0
|
| 2 |
+
uvicorn[standard]==0.27.0
|
| 3 |
+
httpx==0.26.0
|
| 4 |
+
pydantic==2.5.3
|
| 5 |
+
python-multipart==0.0.6
|
| 6 |
+
transformers>=4.36.0
|
| 7 |
+
torch>=2.0.0
|
| 8 |
+
accelerate>=0.25.0
|
| 9 |
+
huggingface_hub>=0.20.0
|
backend/run.sh
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# Run the Model Inspector server with the correct Python
|
| 3 |
+
|
| 4 |
+
cd "$(dirname "$0")"
|
| 5 |
+
|
| 6 |
+
# Use python3 from pyenv (not system uvicorn which uses Python 3.9)
|
| 7 |
+
exec python3 -m uvicorn main:app --host 0.0.0.0 --port 7860 "$@"
|
frontend/css/styles.css
ADDED
|
@@ -0,0 +1,885 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* LLM Scope - Professional Minimalist Theme */
|
| 2 |
+
|
| 3 |
+
:root {
|
| 4 |
+
/* Base colors - dark, muted palette */
|
| 5 |
+
--bg-primary: #0a0a0b;
|
| 6 |
+
--bg-secondary: #111113;
|
| 7 |
+
--bg-tertiary: #18181b;
|
| 8 |
+
--bg-elevated: #1f1f23;
|
| 9 |
+
|
| 10 |
+
/* Text colors */
|
| 11 |
+
--text-primary: #fafafa;
|
| 12 |
+
--text-secondary: #a1a1aa;
|
| 13 |
+
--text-muted: #52525b;
|
| 14 |
+
|
| 15 |
+
/* Borders */
|
| 16 |
+
--border-subtle: #27272a;
|
| 17 |
+
--border-default: #3f3f46;
|
| 18 |
+
|
| 19 |
+
/* Accent - single professional blue */
|
| 20 |
+
--accent: #3b82f6;
|
| 21 |
+
--accent-muted: #1d4ed8;
|
| 22 |
+
|
| 23 |
+
/* Semantic colors for node types - muted, professional */
|
| 24 |
+
--color-embedding: #f59e0b;
|
| 25 |
+
--color-attention: #8b5cf6;
|
| 26 |
+
--color-mlp: #10b981;
|
| 27 |
+
--color-norm: #06b6d4;
|
| 28 |
+
--color-head: #ec4899;
|
| 29 |
+
--color-layers: #6366f1;
|
| 30 |
+
--color-default: #71717a;
|
| 31 |
+
|
| 32 |
+
/* Layout */
|
| 33 |
+
--header-height: 52px;
|
| 34 |
+
--legend-height: 32px;
|
| 35 |
+
|
| 36 |
+
/* Spacing */
|
| 37 |
+
--space-1: 4px;
|
| 38 |
+
--space-2: 8px;
|
| 39 |
+
--space-3: 12px;
|
| 40 |
+
--space-4: 16px;
|
| 41 |
+
--space-5: 20px;
|
| 42 |
+
--space-6: 24px;
|
| 43 |
+
|
| 44 |
+
/* Radius */
|
| 45 |
+
--radius-sm: 4px;
|
| 46 |
+
--radius-md: 6px;
|
| 47 |
+
--radius-lg: 8px;
|
| 48 |
+
|
| 49 |
+
/* Transitions */
|
| 50 |
+
--transition: 150ms ease;
|
| 51 |
+
|
| 52 |
+
/* Typography */
|
| 53 |
+
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
| 54 |
+
--font-mono: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/* Reset */
|
| 58 |
+
*, *::before, *::after {
|
| 59 |
+
box-sizing: border-box;
|
| 60 |
+
margin: 0;
|
| 61 |
+
padding: 0;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
html, body {
|
| 65 |
+
height: 100%;
|
| 66 |
+
overflow: hidden;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
body {
|
| 70 |
+
font-family: var(--font-sans);
|
| 71 |
+
background-color: var(--bg-primary);
|
| 72 |
+
color: var(--text-primary);
|
| 73 |
+
line-height: 1.5;
|
| 74 |
+
-webkit-font-smoothing: antialiased;
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
/* App Layout */
|
| 78 |
+
.app {
|
| 79 |
+
display: flex;
|
| 80 |
+
flex-direction: column;
|
| 81 |
+
height: 100vh;
|
| 82 |
+
width: 100vw;
|
| 83 |
+
overflow: hidden;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/* Header */
|
| 87 |
+
.header {
|
| 88 |
+
height: var(--header-height);
|
| 89 |
+
min-height: var(--header-height);
|
| 90 |
+
background: var(--bg-secondary);
|
| 91 |
+
border-bottom: 1px solid var(--border-subtle);
|
| 92 |
+
display: flex;
|
| 93 |
+
align-items: center;
|
| 94 |
+
justify-content: space-between;
|
| 95 |
+
padding: 0 var(--space-4);
|
| 96 |
+
gap: var(--space-4);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.header-left, .header-right {
|
| 100 |
+
display: flex;
|
| 101 |
+
align-items: center;
|
| 102 |
+
gap: var(--space-3);
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
.header-center {
|
| 106 |
+
flex: 1;
|
| 107 |
+
display: flex;
|
| 108 |
+
justify-content: center;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
/* Logo */
|
| 112 |
+
.logo {
|
| 113 |
+
display: flex;
|
| 114 |
+
align-items: center;
|
| 115 |
+
gap: var(--space-3);
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
.logo-icon {
|
| 119 |
+
width: 26px;
|
| 120 |
+
height: 26px;
|
| 121 |
+
color: var(--text-primary);
|
| 122 |
+
opacity: 0.9;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
.logo-text {
|
| 126 |
+
display: flex;
|
| 127 |
+
flex-direction: column;
|
| 128 |
+
gap: 0;
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
.logo h1 {
|
| 132 |
+
font-size: 1rem;
|
| 133 |
+
font-weight: 600;
|
| 134 |
+
letter-spacing: -0.02em;
|
| 135 |
+
color: var(--text-primary);
|
| 136 |
+
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
|
| 137 |
+
line-height: 1.2;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
.attribution {
|
| 141 |
+
font-size: 0.65rem;
|
| 142 |
+
color: var(--text-muted);
|
| 143 |
+
text-decoration: none;
|
| 144 |
+
transition: color var(--transition);
|
| 145 |
+
line-height: 1;
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
.attribution:hover {
|
| 149 |
+
color: var(--text-secondary);
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
/* Quick Models */
|
| 153 |
+
.quick-models {
|
| 154 |
+
display: flex;
|
| 155 |
+
align-items: center;
|
| 156 |
+
gap: var(--space-1);
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
.quick-btn {
|
| 160 |
+
background: transparent;
|
| 161 |
+
border: 1px solid var(--border-subtle);
|
| 162 |
+
border-radius: var(--radius-sm);
|
| 163 |
+
padding: var(--space-1) var(--space-2);
|
| 164 |
+
color: var(--text-muted);
|
| 165 |
+
font-size: 0.7rem;
|
| 166 |
+
font-weight: 500;
|
| 167 |
+
cursor: pointer;
|
| 168 |
+
transition: all var(--transition);
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.quick-btn:hover {
|
| 172 |
+
background: var(--bg-tertiary);
|
| 173 |
+
color: var(--text-secondary);
|
| 174 |
+
border-color: var(--border-default);
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
/* Toggle Control */
|
| 178 |
+
.toggle-control {
|
| 179 |
+
display: flex;
|
| 180 |
+
align-items: center;
|
| 181 |
+
gap: var(--space-2);
|
| 182 |
+
cursor: pointer;
|
| 183 |
+
user-select: none;
|
| 184 |
+
}
|
| 185 |
+
|
| 186 |
+
.toggle-control input {
|
| 187 |
+
width: 14px;
|
| 188 |
+
height: 14px;
|
| 189 |
+
cursor: pointer;
|
| 190 |
+
accent-color: var(--accent);
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
.toggle-label {
|
| 194 |
+
color: var(--text-muted);
|
| 195 |
+
font-size: 0.7rem;
|
| 196 |
+
font-weight: 500;
|
| 197 |
+
}
|
| 198 |
+
|
| 199 |
+
/* Header Buttons */
|
| 200 |
+
.header-btn {
|
| 201 |
+
display: flex;
|
| 202 |
+
align-items: center;
|
| 203 |
+
justify-content: center;
|
| 204 |
+
width: 32px;
|
| 205 |
+
height: 32px;
|
| 206 |
+
background: var(--bg-tertiary);
|
| 207 |
+
border: 1px solid var(--border-subtle);
|
| 208 |
+
border-radius: var(--radius-md);
|
| 209 |
+
color: var(--text-secondary);
|
| 210 |
+
cursor: pointer;
|
| 211 |
+
transition: all var(--transition);
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
.header-btn:hover {
|
| 215 |
+
background: var(--bg-elevated);
|
| 216 |
+
color: var(--text-primary);
|
| 217 |
+
border-color: var(--border-default);
|
| 218 |
+
}
|
| 219 |
+
|
| 220 |
+
.header-btn.primary {
|
| 221 |
+
background: var(--accent-muted);
|
| 222 |
+
border-color: var(--accent);
|
| 223 |
+
color: white;
|
| 224 |
+
}
|
| 225 |
+
|
| 226 |
+
.header-btn.primary:hover {
|
| 227 |
+
background: var(--accent);
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
.header-btn svg {
|
| 231 |
+
width: 16px;
|
| 232 |
+
height: 16px;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
/* Legend Bar */
|
| 236 |
+
.legend-bar {
|
| 237 |
+
height: var(--legend-height);
|
| 238 |
+
min-height: var(--legend-height);
|
| 239 |
+
background: var(--bg-secondary);
|
| 240 |
+
border-bottom: 1px solid var(--border-subtle);
|
| 241 |
+
display: flex;
|
| 242 |
+
align-items: center;
|
| 243 |
+
padding: 0 var(--space-4);
|
| 244 |
+
overflow-x: auto;
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
.legend {
|
| 248 |
+
display: flex;
|
| 249 |
+
gap: var(--space-4);
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
.legend-item {
|
| 253 |
+
display: flex;
|
| 254 |
+
align-items: center;
|
| 255 |
+
gap: var(--space-1);
|
| 256 |
+
white-space: nowrap;
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
.legend-color {
|
| 260 |
+
width: 10px;
|
| 261 |
+
height: 10px;
|
| 262 |
+
border-radius: 2px;
|
| 263 |
+
flex-shrink: 0;
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
.legend-label {
|
| 267 |
+
font-size: 0.65rem;
|
| 268 |
+
color: var(--text-muted);
|
| 269 |
+
text-transform: lowercase;
|
| 270 |
+
font-weight: 500;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
/* Panels Container */
|
| 274 |
+
.panels-container {
|
| 275 |
+
flex: 1;
|
| 276 |
+
display: flex;
|
| 277 |
+
overflow: hidden;
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
/* Panel */
|
| 281 |
+
.panel {
|
| 282 |
+
flex: 1;
|
| 283 |
+
min-width: 280px;
|
| 284 |
+
display: flex;
|
| 285 |
+
flex-direction: column;
|
| 286 |
+
border-right: 1px solid var(--border-subtle);
|
| 287 |
+
background: var(--bg-primary);
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.panel:last-child {
|
| 291 |
+
border-right: none;
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
.panel-header {
|
| 295 |
+
display: flex;
|
| 296 |
+
align-items: center;
|
| 297 |
+
gap: var(--space-2);
|
| 298 |
+
padding: var(--space-2) var(--space-3);
|
| 299 |
+
background: var(--bg-secondary);
|
| 300 |
+
border-bottom: 1px solid var(--border-subtle);
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
.panel-input-wrapper {
|
| 304 |
+
flex: 1;
|
| 305 |
+
display: flex;
|
| 306 |
+
gap: var(--space-1);
|
| 307 |
+
}
|
| 308 |
+
|
| 309 |
+
.panel-input {
|
| 310 |
+
flex: 1;
|
| 311 |
+
background: var(--bg-tertiary);
|
| 312 |
+
border: 1px solid var(--border-subtle);
|
| 313 |
+
border-radius: var(--radius-md);
|
| 314 |
+
padding: var(--space-1) var(--space-2);
|
| 315 |
+
font-size: 0.75rem;
|
| 316 |
+
color: var(--text-primary);
|
| 317 |
+
font-family: var(--font-mono);
|
| 318 |
+
transition: border-color var(--transition);
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
.panel-input:focus {
|
| 322 |
+
outline: none;
|
| 323 |
+
border-color: var(--accent);
|
| 324 |
+
}
|
| 325 |
+
|
| 326 |
+
.panel-input::placeholder {
|
| 327 |
+
color: var(--text-muted);
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.panel-go-btn {
|
| 331 |
+
background: var(--accent-muted);
|
| 332 |
+
color: white;
|
| 333 |
+
border: none;
|
| 334 |
+
border-radius: var(--radius-md);
|
| 335 |
+
padding: var(--space-1) var(--space-3);
|
| 336 |
+
font-size: 0.75rem;
|
| 337 |
+
font-weight: 500;
|
| 338 |
+
cursor: pointer;
|
| 339 |
+
transition: background var(--transition);
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
.panel-go-btn:hover:not(:disabled) {
|
| 343 |
+
background: var(--accent);
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
.panel-go-btn:disabled {
|
| 347 |
+
opacity: 0.5;
|
| 348 |
+
cursor: not-allowed;
|
| 349 |
+
}
|
| 350 |
+
|
| 351 |
+
.panel-close-btn {
|
| 352 |
+
background: transparent;
|
| 353 |
+
border: none;
|
| 354 |
+
color: var(--text-muted);
|
| 355 |
+
cursor: pointer;
|
| 356 |
+
padding: var(--space-1);
|
| 357 |
+
border-radius: var(--radius-sm);
|
| 358 |
+
transition: all var(--transition);
|
| 359 |
+
display: flex;
|
| 360 |
+
align-items: center;
|
| 361 |
+
justify-content: center;
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
.panel-close-btn:hover {
|
| 365 |
+
background: rgba(239, 68, 68, 0.15);
|
| 366 |
+
color: #ef4444;
|
| 367 |
+
}
|
| 368 |
+
|
| 369 |
+
.panel-close-btn svg {
|
| 370 |
+
width: 14px;
|
| 371 |
+
height: 14px;
|
| 372 |
+
}
|
| 373 |
+
|
| 374 |
+
/* Panel Info */
|
| 375 |
+
.panel-info {
|
| 376 |
+
display: flex;
|
| 377 |
+
align-items: center;
|
| 378 |
+
gap: var(--space-3);
|
| 379 |
+
padding: var(--space-2) var(--space-3);
|
| 380 |
+
background: var(--bg-tertiary);
|
| 381 |
+
border-bottom: 1px solid var(--border-subtle);
|
| 382 |
+
font-size: 0.7rem;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
.panel-model-name {
|
| 386 |
+
font-weight: 600;
|
| 387 |
+
font-family: var(--font-mono);
|
| 388 |
+
color: var(--text-primary);
|
| 389 |
+
flex: 1;
|
| 390 |
+
overflow: hidden;
|
| 391 |
+
text-overflow: ellipsis;
|
| 392 |
+
white-space: nowrap;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.panel-params {
|
| 396 |
+
color: var(--accent);
|
| 397 |
+
font-family: var(--font-mono);
|
| 398 |
+
font-weight: 600;
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
.panel-type {
|
| 402 |
+
color: var(--text-muted);
|
| 403 |
+
text-transform: uppercase;
|
| 404 |
+
font-size: 0.6rem;
|
| 405 |
+
letter-spacing: 0.05em;
|
| 406 |
+
font-weight: 500;
|
| 407 |
+
}
|
| 408 |
+
|
| 409 |
+
/* Panel Error */
|
| 410 |
+
.panel-error {
|
| 411 |
+
background: rgba(239, 68, 68, 0.1);
|
| 412 |
+
border-bottom: 1px solid rgba(239, 68, 68, 0.2);
|
| 413 |
+
padding: var(--space-2) var(--space-3);
|
| 414 |
+
color: #fca5a5;
|
| 415 |
+
font-size: 0.7rem;
|
| 416 |
+
}
|
| 417 |
+
|
| 418 |
+
/* Panel Canvas */
|
| 419 |
+
.panel-canvas {
|
| 420 |
+
flex: 1;
|
| 421 |
+
position: relative;
|
| 422 |
+
overflow: hidden;
|
| 423 |
+
}
|
| 424 |
+
|
| 425 |
+
.panel-viz {
|
| 426 |
+
width: 100%;
|
| 427 |
+
height: 100%;
|
| 428 |
+
cursor: grab;
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
.panel-viz:active {
|
| 432 |
+
cursor: grabbing;
|
| 433 |
+
}
|
| 434 |
+
|
| 435 |
+
.panel-viz svg {
|
| 436 |
+
width: 100%;
|
| 437 |
+
height: 100%;
|
| 438 |
+
}
|
| 439 |
+
|
| 440 |
+
/* Panel Empty State */
|
| 441 |
+
.panel-empty {
|
| 442 |
+
position: absolute;
|
| 443 |
+
top: 50%;
|
| 444 |
+
left: 50%;
|
| 445 |
+
transform: translate(-50%, -50%);
|
| 446 |
+
text-align: center;
|
| 447 |
+
color: var(--text-muted);
|
| 448 |
+
pointer-events: none;
|
| 449 |
+
}
|
| 450 |
+
|
| 451 |
+
.panel-empty svg {
|
| 452 |
+
width: 48px;
|
| 453 |
+
height: 48px;
|
| 454 |
+
margin-bottom: var(--space-2);
|
| 455 |
+
opacity: 0.2;
|
| 456 |
+
}
|
| 457 |
+
|
| 458 |
+
.panel-empty p {
|
| 459 |
+
font-size: 0.75rem;
|
| 460 |
+
}
|
| 461 |
+
|
| 462 |
+
/* Spinner */
|
| 463 |
+
.spinner {
|
| 464 |
+
width: 14px;
|
| 465 |
+
height: 14px;
|
| 466 |
+
animation: spin 1s linear infinite;
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
@keyframes spin {
|
| 470 |
+
from { transform: rotate(0deg); }
|
| 471 |
+
to { transform: rotate(360deg); }
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
/* Tooltip */
|
| 475 |
+
.tooltip {
|
| 476 |
+
position: fixed;
|
| 477 |
+
background: var(--bg-elevated);
|
| 478 |
+
border: 1px solid var(--border-default);
|
| 479 |
+
border-radius: var(--radius-lg);
|
| 480 |
+
padding: var(--space-3);
|
| 481 |
+
pointer-events: none;
|
| 482 |
+
z-index: 1000;
|
| 483 |
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
| 484 |
+
max-width: 260px;
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
.tooltip-title {
|
| 488 |
+
font-weight: 600;
|
| 489 |
+
margin-bottom: var(--space-1);
|
| 490 |
+
font-size: 0.8rem;
|
| 491 |
+
color: var(--text-primary);
|
| 492 |
+
}
|
| 493 |
+
|
| 494 |
+
.tooltip-content {
|
| 495 |
+
font-size: 0.7rem;
|
| 496 |
+
color: var(--text-secondary);
|
| 497 |
+
}
|
| 498 |
+
|
| 499 |
+
.tooltip-row {
|
| 500 |
+
display: flex;
|
| 501 |
+
justify-content: space-between;
|
| 502 |
+
gap: var(--space-3);
|
| 503 |
+
padding: 1px 0;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
.tooltip-label {
|
| 507 |
+
color: var(--text-muted);
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
.tooltip-value {
|
| 511 |
+
font-family: var(--font-mono);
|
| 512 |
+
color: var(--text-primary);
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
/* Modal */
|
| 516 |
+
.modal {
|
| 517 |
+
position: fixed;
|
| 518 |
+
inset: 0;
|
| 519 |
+
z-index: 1000;
|
| 520 |
+
display: flex;
|
| 521 |
+
align-items: center;
|
| 522 |
+
justify-content: center;
|
| 523 |
+
}
|
| 524 |
+
|
| 525 |
+
.modal-backdrop {
|
| 526 |
+
position: absolute;
|
| 527 |
+
inset: 0;
|
| 528 |
+
background: rgba(0, 0, 0, 0.7);
|
| 529 |
+
}
|
| 530 |
+
|
| 531 |
+
.modal-content {
|
| 532 |
+
position: relative;
|
| 533 |
+
background: var(--bg-secondary);
|
| 534 |
+
border: 1px solid var(--border-default);
|
| 535 |
+
border-radius: var(--radius-lg);
|
| 536 |
+
width: 90%;
|
| 537 |
+
max-width: 800px;
|
| 538 |
+
max-height: 80vh;
|
| 539 |
+
display: flex;
|
| 540 |
+
flex-direction: column;
|
| 541 |
+
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
.modal-header {
|
| 545 |
+
display: flex;
|
| 546 |
+
align-items: center;
|
| 547 |
+
justify-content: space-between;
|
| 548 |
+
padding: var(--space-4);
|
| 549 |
+
border-bottom: 1px solid var(--border-subtle);
|
| 550 |
+
}
|
| 551 |
+
|
| 552 |
+
.modal-header h2 {
|
| 553 |
+
font-size: 0.95rem;
|
| 554 |
+
font-weight: 600;
|
| 555 |
+
color: var(--text-primary);
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
.modal-close {
|
| 559 |
+
background: transparent;
|
| 560 |
+
border: none;
|
| 561 |
+
color: var(--text-muted);
|
| 562 |
+
cursor: pointer;
|
| 563 |
+
padding: var(--space-1);
|
| 564 |
+
border-radius: var(--radius-sm);
|
| 565 |
+
display: flex;
|
| 566 |
+
align-items: center;
|
| 567 |
+
justify-content: center;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
.modal-close:hover {
|
| 571 |
+
color: var(--text-primary);
|
| 572 |
+
background: var(--bg-tertiary);
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
.modal-close svg {
|
| 576 |
+
width: 18px;
|
| 577 |
+
height: 18px;
|
| 578 |
+
}
|
| 579 |
+
|
| 580 |
+
.modal-body {
|
| 581 |
+
flex: 1;
|
| 582 |
+
overflow-y: auto;
|
| 583 |
+
padding: var(--space-4);
|
| 584 |
+
}
|
| 585 |
+
|
| 586 |
+
/* Comparison Results */
|
| 587 |
+
.compare-empty {
|
| 588 |
+
color: var(--text-muted);
|
| 589 |
+
text-align: center;
|
| 590 |
+
padding: var(--space-6);
|
| 591 |
+
font-size: 0.85rem;
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
.compare-summary {
|
| 595 |
+
display: grid;
|
| 596 |
+
grid-template-columns: 1fr 1fr;
|
| 597 |
+
gap: var(--space-4);
|
| 598 |
+
margin-bottom: var(--space-4);
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
.compare-model {
|
| 602 |
+
background: var(--bg-tertiary);
|
| 603 |
+
border-radius: var(--radius-md);
|
| 604 |
+
padding: var(--space-3);
|
| 605 |
+
}
|
| 606 |
+
|
| 607 |
+
.compare-model-name {
|
| 608 |
+
font-family: var(--font-mono);
|
| 609 |
+
font-weight: 600;
|
| 610 |
+
font-size: 0.8rem;
|
| 611 |
+
color: var(--text-primary);
|
| 612 |
+
margin-bottom: var(--space-1);
|
| 613 |
+
}
|
| 614 |
+
|
| 615 |
+
.compare-model-params {
|
| 616 |
+
color: var(--accent);
|
| 617 |
+
font-family: var(--font-mono);
|
| 618 |
+
font-size: 0.75rem;
|
| 619 |
+
}
|
| 620 |
+
|
| 621 |
+
.compare-section {
|
| 622 |
+
margin-bottom: var(--space-4);
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
.compare-section-title {
|
| 626 |
+
font-size: 0.75rem;
|
| 627 |
+
font-weight: 600;
|
| 628 |
+
color: var(--text-secondary);
|
| 629 |
+
text-transform: uppercase;
|
| 630 |
+
letter-spacing: 0.05em;
|
| 631 |
+
margin-bottom: var(--space-2);
|
| 632 |
+
display: flex;
|
| 633 |
+
align-items: center;
|
| 634 |
+
gap: var(--space-2);
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
.compare-badge {
|
| 638 |
+
font-size: 0.65rem;
|
| 639 |
+
padding: 2px 6px;
|
| 640 |
+
border-radius: var(--radius-sm);
|
| 641 |
+
font-weight: 500;
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
.compare-badge.same {
|
| 645 |
+
background: rgba(16, 185, 129, 0.15);
|
| 646 |
+
color: #10b981;
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
.compare-badge.diff {
|
| 650 |
+
background: rgba(245, 158, 11, 0.15);
|
| 651 |
+
color: #f59e0b;
|
| 652 |
+
}
|
| 653 |
+
|
| 654 |
+
.compare-badge.added {
|
| 655 |
+
background: rgba(59, 130, 246, 0.15);
|
| 656 |
+
color: #3b82f6;
|
| 657 |
+
}
|
| 658 |
+
|
| 659 |
+
.compare-badge.removed {
|
| 660 |
+
background: rgba(239, 68, 68, 0.15);
|
| 661 |
+
color: #ef4444;
|
| 662 |
+
}
|
| 663 |
+
|
| 664 |
+
.compare-list {
|
| 665 |
+
list-style: none;
|
| 666 |
+
font-size: 0.75rem;
|
| 667 |
+
}
|
| 668 |
+
|
| 669 |
+
.compare-list li {
|
| 670 |
+
padding: var(--space-2) var(--space-3);
|
| 671 |
+
background: var(--bg-tertiary);
|
| 672 |
+
border-radius: var(--radius-sm);
|
| 673 |
+
margin-bottom: var(--space-1);
|
| 674 |
+
display: flex;
|
| 675 |
+
justify-content: space-between;
|
| 676 |
+
align-items: center;
|
| 677 |
+
}
|
| 678 |
+
|
| 679 |
+
.compare-list .item-name {
|
| 680 |
+
color: var(--text-primary);
|
| 681 |
+
font-family: var(--font-mono);
|
| 682 |
+
}
|
| 683 |
+
|
| 684 |
+
.compare-list .item-detail {
|
| 685 |
+
color: var(--text-muted);
|
| 686 |
+
font-family: var(--font-mono);
|
| 687 |
+
font-size: 0.7rem;
|
| 688 |
+
}
|
| 689 |
+
|
| 690 |
+
.compare-list .item-diff {
|
| 691 |
+
display: flex;
|
| 692 |
+
gap: var(--space-2);
|
| 693 |
+
font-family: var(--font-mono);
|
| 694 |
+
font-size: 0.7rem;
|
| 695 |
+
}
|
| 696 |
+
|
| 697 |
+
.compare-list .model-a {
|
| 698 |
+
color: #3b82f6;
|
| 699 |
+
}
|
| 700 |
+
|
| 701 |
+
.compare-list .model-b {
|
| 702 |
+
color: #8b5cf6;
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
.compare-similarity {
|
| 706 |
+
display: flex;
|
| 707 |
+
align-items: center;
|
| 708 |
+
gap: var(--space-3);
|
| 709 |
+
padding: var(--space-3);
|
| 710 |
+
background: var(--bg-tertiary);
|
| 711 |
+
border-radius: var(--radius-md);
|
| 712 |
+
margin-bottom: var(--space-4);
|
| 713 |
+
}
|
| 714 |
+
|
| 715 |
+
.similarity-bar {
|
| 716 |
+
flex: 1;
|
| 717 |
+
height: 8px;
|
| 718 |
+
background: var(--bg-elevated);
|
| 719 |
+
border-radius: 4px;
|
| 720 |
+
overflow: hidden;
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
.similarity-fill {
|
| 724 |
+
height: 100%;
|
| 725 |
+
background: linear-gradient(90deg, #ef4444 0%, #f59e0b 50%, #10b981 100%);
|
| 726 |
+
border-radius: 4px;
|
| 727 |
+
transition: width 0.3s ease;
|
| 728 |
+
}
|
| 729 |
+
|
| 730 |
+
.similarity-value {
|
| 731 |
+
font-family: var(--font-mono);
|
| 732 |
+
font-weight: 600;
|
| 733 |
+
font-size: 0.85rem;
|
| 734 |
+
color: var(--text-primary);
|
| 735 |
+
min-width: 48px;
|
| 736 |
+
text-align: right;
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
.similarity-label {
|
| 740 |
+
font-size: 0.75rem;
|
| 741 |
+
color: var(--text-secondary);
|
| 742 |
+
}
|
| 743 |
+
|
| 744 |
+
/* Error hint */
|
| 745 |
+
.error-hint {
|
| 746 |
+
margin-top: var(--space-2);
|
| 747 |
+
padding: var(--space-2) var(--space-3);
|
| 748 |
+
background: rgba(59, 130, 246, 0.1);
|
| 749 |
+
border: 1px solid rgba(59, 130, 246, 0.2);
|
| 750 |
+
border-radius: var(--radius-md);
|
| 751 |
+
font-size: 0.7rem;
|
| 752 |
+
color: #93c5fd;
|
| 753 |
+
}
|
| 754 |
+
|
| 755 |
+
.error-hint code {
|
| 756 |
+
background: rgba(0,0,0,0.3);
|
| 757 |
+
padding: 1px 4px;
|
| 758 |
+
border-radius: 3px;
|
| 759 |
+
font-family: var(--font-mono);
|
| 760 |
+
}
|
| 761 |
+
|
| 762 |
+
/* Header buttons with text */
|
| 763 |
+
.header-btn.with-text {
|
| 764 |
+
width: auto;
|
| 765 |
+
padding: 0 var(--space-3);
|
| 766 |
+
gap: var(--space-1);
|
| 767 |
+
}
|
| 768 |
+
|
| 769 |
+
.header-btn.with-text span {
|
| 770 |
+
font-size: 0.7rem;
|
| 771 |
+
font-weight: 500;
|
| 772 |
+
}
|
| 773 |
+
|
| 774 |
+
.header-btn .dropdown-arrow {
|
| 775 |
+
width: 12px;
|
| 776 |
+
height: 12px;
|
| 777 |
+
margin-left: var(--space-1);
|
| 778 |
+
}
|
| 779 |
+
|
| 780 |
+
/* Export Dropdown */
|
| 781 |
+
.export-dropdown {
|
| 782 |
+
position: relative;
|
| 783 |
+
}
|
| 784 |
+
|
| 785 |
+
.export-menu {
|
| 786 |
+
position: absolute;
|
| 787 |
+
top: 100%;
|
| 788 |
+
right: 0;
|
| 789 |
+
margin-top: var(--space-1);
|
| 790 |
+
background: var(--bg-elevated);
|
| 791 |
+
border: 1px solid var(--border-default);
|
| 792 |
+
border-radius: var(--radius-md);
|
| 793 |
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
| 794 |
+
opacity: 0;
|
| 795 |
+
visibility: hidden;
|
| 796 |
+
transform: translateY(-4px);
|
| 797 |
+
transition: all var(--transition);
|
| 798 |
+
z-index: 100;
|
| 799 |
+
min-width: 140px;
|
| 800 |
+
}
|
| 801 |
+
|
| 802 |
+
.export-dropdown:hover .export-menu,
|
| 803 |
+
.export-menu:hover {
|
| 804 |
+
opacity: 1;
|
| 805 |
+
visibility: visible;
|
| 806 |
+
transform: translateY(0);
|
| 807 |
+
}
|
| 808 |
+
|
| 809 |
+
.export-option {
|
| 810 |
+
display: block;
|
| 811 |
+
width: 100%;
|
| 812 |
+
padding: var(--space-2) var(--space-3);
|
| 813 |
+
background: transparent;
|
| 814 |
+
border: none;
|
| 815 |
+
color: var(--text-secondary);
|
| 816 |
+
font-size: 0.75rem;
|
| 817 |
+
text-align: left;
|
| 818 |
+
cursor: pointer;
|
| 819 |
+
transition: all var(--transition);
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
+
.export-option:first-child {
|
| 823 |
+
border-radius: var(--radius-md) var(--radius-md) 0 0;
|
| 824 |
+
}
|
| 825 |
+
|
| 826 |
+
.export-option:last-child {
|
| 827 |
+
border-radius: 0 0 var(--radius-md) var(--radius-md);
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
.export-option:hover {
|
| 831 |
+
background: var(--bg-tertiary);
|
| 832 |
+
color: var(--text-primary);
|
| 833 |
+
}
|
| 834 |
+
|
| 835 |
+
/* Loading Status */
|
| 836 |
+
.loading-status {
|
| 837 |
+
display: flex;
|
| 838 |
+
flex-direction: column;
|
| 839 |
+
align-items: center;
|
| 840 |
+
gap: var(--space-3);
|
| 841 |
+
}
|
| 842 |
+
|
| 843 |
+
.loading-spinner {
|
| 844 |
+
width: 32px;
|
| 845 |
+
height: 32px;
|
| 846 |
+
animation: spin 1s linear infinite;
|
| 847 |
+
opacity: 0.5;
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
.status-text {
|
| 851 |
+
font-size: 0.75rem;
|
| 852 |
+
color: var(--text-muted);
|
| 853 |
+
animation: pulse 2s ease-in-out infinite;
|
| 854 |
+
}
|
| 855 |
+
|
| 856 |
+
@keyframes pulse {
|
| 857 |
+
0%, 100% { opacity: 0.5; }
|
| 858 |
+
50% { opacity: 1; }
|
| 859 |
+
}
|
| 860 |
+
|
| 861 |
+
/* Utility */
|
| 862 |
+
.hidden {
|
| 863 |
+
display: none !important;
|
| 864 |
+
}
|
| 865 |
+
|
| 866 |
+
/* Responsive */
|
| 867 |
+
@media (max-width: 768px) {
|
| 868 |
+
.header-center {
|
| 869 |
+
display: none;
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
.panels-container {
|
| 873 |
+
flex-direction: column;
|
| 874 |
+
}
|
| 875 |
+
|
| 876 |
+
.panel {
|
| 877 |
+
min-width: 100%;
|
| 878 |
+
border-right: none;
|
| 879 |
+
border-bottom: 1px solid var(--border-subtle);
|
| 880 |
+
}
|
| 881 |
+
|
| 882 |
+
.panel:last-child {
|
| 883 |
+
border-bottom: none;
|
| 884 |
+
}
|
| 885 |
+
}
|
frontend/index.html
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>LLM Scope - Visualize Model Architectures</title>
|
| 7 |
+
<link rel="stylesheet" href="css/styles.css">
|
| 8 |
+
<script src="https://d3js.org/d3.v7.min.js"></script>
|
| 9 |
+
</head>
|
| 10 |
+
<body>
|
| 11 |
+
<div class="app">
|
| 12 |
+
<!-- Top Header -->
|
| 13 |
+
<header class="header">
|
| 14 |
+
<div class="header-left">
|
| 15 |
+
<div class="logo">
|
| 16 |
+
<svg class="logo-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
| 17 |
+
<circle cx="10" cy="10" r="7"/>
|
| 18 |
+
<line x1="15" y1="15" x2="21" y2="21" stroke-width="2.5" stroke-linecap="round"/>
|
| 19 |
+
<circle cx="10" cy="10" r="3" stroke-dasharray="2 2"/>
|
| 20 |
+
</svg>
|
| 21 |
+
<div class="logo-text">
|
| 22 |
+
<h1>LLM Scope</h1>
|
| 23 |
+
<a href="https://omarkamali.com" target="_blank" rel="noopener" class="attribution">by Omar Kamali</a>
|
| 24 |
+
</div>
|
| 25 |
+
</div>
|
| 26 |
+
</div>
|
| 27 |
+
<div class="header-center">
|
| 28 |
+
<div class="quick-models">
|
| 29 |
+
<button class="quick-btn" data-model="openai/gpt-oss-120b">GPT-OSS 120B</button>
|
| 30 |
+
<button class="quick-btn" data-model="Qwen/Qwen3-4B">Qwen3 4B</button>
|
| 31 |
+
<button class="quick-btn" data-model="Qwen/Qwen3-30B-A3B">Qwen MoE 30B</button>
|
| 32 |
+
<button class="quick-btn" data-model="deepseek-ai/DeepSeek-V3">DeepSeek V3</button>
|
| 33 |
+
<button class="quick-btn" data-model="google/gemma-3-27b-it">Gemma3 27B</button>
|
| 34 |
+
<button class="quick-btn" data-model="moonshotai/Kimi-K2-Instruct">Kimi K2</button>
|
| 35 |
+
<button class="quick-btn" data-model="mistralai/Mistral-Large-3-675B-Instruct-2512">Mistral 675B</button>
|
| 36 |
+
<button class="quick-btn" data-model="nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16">Nemotron 30B</button>
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
<div class="header-right">
|
| 40 |
+
<label class="toggle-control">
|
| 41 |
+
<input type="checkbox" id="zoom-lock" checked>
|
| 42 |
+
<span class="toggle-label">Lock Scale</span>
|
| 43 |
+
</label>
|
| 44 |
+
<button id="compare-btn" class="header-btn with-text" title="Compare architectures">
|
| 45 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 46 |
+
<path d="M16 3h5v5M8 3H3v5M3 16v5h5M21 16v5h-5M12 8v8M8 12h8"/>
|
| 47 |
+
</svg>
|
| 48 |
+
<span>Compare</span>
|
| 49 |
+
</button>
|
| 50 |
+
<div class="export-dropdown">
|
| 51 |
+
<button id="export-btn" class="header-btn with-text" title="Export visualization">
|
| 52 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 53 |
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
| 54 |
+
<polyline points="7 10 12 15 17 10"/>
|
| 55 |
+
<line x1="12" y1="15" x2="12" y2="3"/>
|
| 56 |
+
</svg>
|
| 57 |
+
<span>Export</span>
|
| 58 |
+
<svg class="dropdown-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 59 |
+
<polyline points="6 9 12 15 18 9"/>
|
| 60 |
+
</svg>
|
| 61 |
+
</button>
|
| 62 |
+
<div class="export-menu" id="export-menu">
|
| 63 |
+
<button class="export-option" data-format="svg">Export as SVG</button>
|
| 64 |
+
<button class="export-option" data-format="png">Export as PNG</button>
|
| 65 |
+
</div>
|
| 66 |
+
</div>
|
| 67 |
+
<button id="add-panel-btn" class="header-btn primary" title="Add panel">
|
| 68 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 69 |
+
<line x1="12" y1="5" x2="12" y2="19"/>
|
| 70 |
+
<line x1="5" y1="12" x2="19" y2="12"/>
|
| 71 |
+
</svg>
|
| 72 |
+
</button>
|
| 73 |
+
</div>
|
| 74 |
+
</header>
|
| 75 |
+
|
| 76 |
+
<!-- Legend Bar -->
|
| 77 |
+
<div class="legend-bar" id="legend-bar">
|
| 78 |
+
<div class="legend" id="legend"></div>
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
<!-- Panels Container -->
|
| 82 |
+
<main class="panels-container" id="panels-container">
|
| 83 |
+
</main>
|
| 84 |
+
</div>
|
| 85 |
+
|
| 86 |
+
<!-- Panel Template -->
|
| 87 |
+
<template id="panel-template">
|
| 88 |
+
<div class="panel" data-panel-id="">
|
| 89 |
+
<div class="panel-header">
|
| 90 |
+
<div class="panel-input-wrapper">
|
| 91 |
+
<input type="text" class="panel-input" placeholder="Model ID (e.g., gpt2)" />
|
| 92 |
+
<button class="panel-go-btn">
|
| 93 |
+
<span class="btn-text">Load</span>
|
| 94 |
+
<span class="btn-loader hidden">
|
| 95 |
+
<svg class="spinner" viewBox="0 0 24 24">
|
| 96 |
+
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="3" fill="none" stroke-dasharray="30 70"/>
|
| 97 |
+
</svg>
|
| 98 |
+
</span>
|
| 99 |
+
</button>
|
| 100 |
+
</div>
|
| 101 |
+
<button class="panel-close-btn" title="Remove panel">
|
| 102 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 103 |
+
<line x1="18" y1="6" x2="6" y2="18"/>
|
| 104 |
+
<line x1="6" y1="6" x2="18" y2="18"/>
|
| 105 |
+
</svg>
|
| 106 |
+
</button>
|
| 107 |
+
</div>
|
| 108 |
+
<div class="panel-info hidden">
|
| 109 |
+
<span class="panel-model-name"></span>
|
| 110 |
+
<span class="panel-params"></span>
|
| 111 |
+
<span class="panel-type"></span>
|
| 112 |
+
</div>
|
| 113 |
+
<div class="panel-error hidden"></div>
|
| 114 |
+
<div class="panel-canvas">
|
| 115 |
+
<div class="panel-viz"></div>
|
| 116 |
+
<div class="panel-empty">
|
| 117 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
|
| 118 |
+
<circle cx="10" cy="10" r="7"/>
|
| 119 |
+
<line x1="15" y1="15" x2="21" y2="21" stroke-width="2" stroke-linecap="round"/>
|
| 120 |
+
</svg>
|
| 121 |
+
<p>Enter a model ID to inspect</p>
|
| 122 |
+
</div>
|
| 123 |
+
</div>
|
| 124 |
+
</div>
|
| 125 |
+
</template>
|
| 126 |
+
|
| 127 |
+
<!-- Tooltip -->
|
| 128 |
+
<div id="tooltip" class="tooltip hidden"></div>
|
| 129 |
+
|
| 130 |
+
<!-- Comparison Modal -->
|
| 131 |
+
<div id="compare-modal" class="modal hidden">
|
| 132 |
+
<div class="modal-backdrop"></div>
|
| 133 |
+
<div class="modal-content">
|
| 134 |
+
<div class="modal-header">
|
| 135 |
+
<h2>Architecture Comparison</h2>
|
| 136 |
+
<button class="modal-close" id="compare-modal-close">
|
| 137 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
| 138 |
+
<line x1="18" y1="6" x2="6" y2="18"/>
|
| 139 |
+
<line x1="6" y1="6" x2="18" y2="18"/>
|
| 140 |
+
</svg>
|
| 141 |
+
</button>
|
| 142 |
+
</div>
|
| 143 |
+
<div class="modal-body" id="compare-results">
|
| 144 |
+
<p class="compare-empty">Load at least 2 models to compare architectures</p>
|
| 145 |
+
</div>
|
| 146 |
+
</div>
|
| 147 |
+
</div>
|
| 148 |
+
|
| 149 |
+
<script src="js/api.js"></script>
|
| 150 |
+
<script src="js/treemap.js"></script>
|
| 151 |
+
<script src="js/app.js"></script>
|
| 152 |
+
</body>
|
| 153 |
+
</html>
|
frontend/js/api.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* API client for Model Inspector backend
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
const API = {
|
| 6 |
+
baseUrl: '', // Same origin
|
| 7 |
+
|
| 8 |
+
/**
|
| 9 |
+
* Inspect a model by HuggingFace model ID
|
| 10 |
+
* @param {string} modelId - HuggingFace model ID
|
| 11 |
+
* @returns {Promise<{tree: Object, metadata: Object}>}
|
| 12 |
+
*/
|
| 13 |
+
async inspectModel(modelId) {
|
| 14 |
+
const response = await fetch(`${this.baseUrl}/api/inspect`, {
|
| 15 |
+
method: 'POST',
|
| 16 |
+
headers: {
|
| 17 |
+
'Content-Type': 'application/json',
|
| 18 |
+
},
|
| 19 |
+
body: JSON.stringify({ model_id: modelId }),
|
| 20 |
+
});
|
| 21 |
+
|
| 22 |
+
if (!response.ok) {
|
| 23 |
+
const error = await response.json();
|
| 24 |
+
const detail = error.detail || 'Failed to inspect model';
|
| 25 |
+
|
| 26 |
+
// Check for common auth/access errors
|
| 27 |
+
const isAuthError = detail.includes('401') ||
|
| 28 |
+
detail.includes('403') ||
|
| 29 |
+
detail.includes('gated') ||
|
| 30 |
+
detail.includes('access') ||
|
| 31 |
+
detail.includes('token') ||
|
| 32 |
+
detail.includes('authorization') ||
|
| 33 |
+
detail.includes('authenticate');
|
| 34 |
+
|
| 35 |
+
if (isAuthError) {
|
| 36 |
+
const err = new Error(detail);
|
| 37 |
+
err.isAuthError = true;
|
| 38 |
+
throw err;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
throw new Error(detail);
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
return response.json();
|
| 45 |
+
},
|
| 46 |
+
|
| 47 |
+
/**
|
| 48 |
+
* Inspect a model by config object
|
| 49 |
+
* @param {Object} config - Model config.json object
|
| 50 |
+
* @returns {Promise<{tree: Object, metadata: Object}>}
|
| 51 |
+
*/
|
| 52 |
+
async inspectConfig(config) {
|
| 53 |
+
const response = await fetch(`${this.baseUrl}/api/inspect`, {
|
| 54 |
+
method: 'POST',
|
| 55 |
+
headers: {
|
| 56 |
+
'Content-Type': 'application/json',
|
| 57 |
+
},
|
| 58 |
+
body: JSON.stringify({ config }),
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
if (!response.ok) {
|
| 62 |
+
const error = await response.json();
|
| 63 |
+
throw new Error(error.detail || 'Failed to inspect config');
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
return response.json();
|
| 67 |
+
},
|
| 68 |
+
|
| 69 |
+
/**
|
| 70 |
+
* Upload a config.json file
|
| 71 |
+
* @param {File} file - The config.json file
|
| 72 |
+
* @returns {Promise<{tree: Object, metadata: Object}>}
|
| 73 |
+
*/
|
| 74 |
+
async uploadConfig(file) {
|
| 75 |
+
const formData = new FormData();
|
| 76 |
+
formData.append('file', file);
|
| 77 |
+
|
| 78 |
+
const response = await fetch(`${this.baseUrl}/api/upload`, {
|
| 79 |
+
method: 'POST',
|
| 80 |
+
body: formData,
|
| 81 |
+
});
|
| 82 |
+
|
| 83 |
+
if (!response.ok) {
|
| 84 |
+
const error = await response.json();
|
| 85 |
+
throw new Error(error.detail || 'Failed to upload config');
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
return response.json();
|
| 89 |
+
},
|
| 90 |
+
|
| 91 |
+
/**
|
| 92 |
+
* Format parameter count for display
|
| 93 |
+
* @param {number} count - Parameter count
|
| 94 |
+
* @returns {string} Formatted string
|
| 95 |
+
*/
|
| 96 |
+
formatParams(count) {
|
| 97 |
+
if (count >= 1e12) {
|
| 98 |
+
return `${(count / 1e12).toFixed(2)}T`;
|
| 99 |
+
} else if (count >= 1e9) {
|
| 100 |
+
return `${(count / 1e9).toFixed(2)}B`;
|
| 101 |
+
} else if (count >= 1e6) {
|
| 102 |
+
return `${(count / 1e6).toFixed(2)}M`;
|
| 103 |
+
} else if (count >= 1e3) {
|
| 104 |
+
return `${(count / 1e3).toFixed(2)}K`;
|
| 105 |
+
}
|
| 106 |
+
return count.toLocaleString();
|
| 107 |
+
}
|
| 108 |
+
};
|
frontend/js/app.js
ADDED
|
@@ -0,0 +1,710 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* LLM Scope - Multi-panel Model Inspector
|
| 3 |
+
*/
|
| 4 |
+
|
| 5 |
+
(function() {
|
| 6 |
+
const panels = new Map();
|
| 7 |
+
let panelIdCounter = 0;
|
| 8 |
+
let zoomLocked = true;
|
| 9 |
+
let globalMaxParams = 0;
|
| 10 |
+
const loadingPanels = new Set(); // Track panels currently loading
|
| 11 |
+
|
| 12 |
+
const panelsContainer = document.getElementById('panels-container');
|
| 13 |
+
const panelTemplate = document.getElementById('panel-template');
|
| 14 |
+
const addPanelBtn = document.getElementById('add-panel-btn');
|
| 15 |
+
const exportMenu = document.getElementById('export-menu');
|
| 16 |
+
const compareBtn = document.getElementById('compare-btn');
|
| 17 |
+
const zoomLockCheckbox = document.getElementById('zoom-lock');
|
| 18 |
+
const legend = document.getElementById('legend');
|
| 19 |
+
const compareModal = document.getElementById('compare-modal');
|
| 20 |
+
const compareModalClose = document.getElementById('compare-modal-close');
|
| 21 |
+
const compareResults = document.getElementById('compare-results');
|
| 22 |
+
|
| 23 |
+
function init() {
|
| 24 |
+
addPanelBtn.addEventListener('click', () => addPanel());
|
| 25 |
+
compareBtn.addEventListener('click', handleCompare);
|
| 26 |
+
zoomLockCheckbox.addEventListener('change', handleZoomLockChange);
|
| 27 |
+
|
| 28 |
+
// Export options
|
| 29 |
+
exportMenu.querySelectorAll('.export-option').forEach(btn => {
|
| 30 |
+
btn.addEventListener('click', (e) => {
|
| 31 |
+
const format = e.target.dataset.format;
|
| 32 |
+
handleExport(format);
|
| 33 |
+
});
|
| 34 |
+
});
|
| 35 |
+
|
| 36 |
+
// Modal close
|
| 37 |
+
compareModalClose.addEventListener('click', closeCompareModal);
|
| 38 |
+
compareModal.querySelector('.modal-backdrop').addEventListener('click', closeCompareModal);
|
| 39 |
+
|
| 40 |
+
document.querySelectorAll('.quick-btn').forEach(btn => {
|
| 41 |
+
btn.addEventListener('click', () => {
|
| 42 |
+
const modelId = btn.dataset.model;
|
| 43 |
+
const emptyPanel = findEmptyPanel();
|
| 44 |
+
if (emptyPanel) {
|
| 45 |
+
loadModelInPanel(emptyPanel, modelId);
|
| 46 |
+
} else {
|
| 47 |
+
const panel = addPanel();
|
| 48 |
+
loadModelInPanel(panel.id, modelId);
|
| 49 |
+
}
|
| 50 |
+
});
|
| 51 |
+
});
|
| 52 |
+
|
| 53 |
+
const urlState = parseUrlState();
|
| 54 |
+
if (urlState.models.length > 0) {
|
| 55 |
+
urlState.models.forEach((modelId) => {
|
| 56 |
+
const panel = addPanel();
|
| 57 |
+
if (modelId) {
|
| 58 |
+
loadModelInPanel(panel.id, modelId);
|
| 59 |
+
}
|
| 60 |
+
});
|
| 61 |
+
zoomLocked = urlState.zoomLocked;
|
| 62 |
+
zoomLockCheckbox.checked = zoomLocked;
|
| 63 |
+
} else {
|
| 64 |
+
addPanel();
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
updateLegend();
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
function findEmptyPanel() {
|
| 71 |
+
for (const [panelId, panel] of panels) {
|
| 72 |
+
// Don't reuse panels that are loading or already have data
|
| 73 |
+
if (!panel.data && !loadingPanels.has(panelId)) return panelId;
|
| 74 |
+
}
|
| 75 |
+
return null;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
function addPanel() {
|
| 79 |
+
const id = `panel-${panelIdCounter++}`;
|
| 80 |
+
const content = panelTemplate.content.cloneNode(true);
|
| 81 |
+
const panelEl = content.querySelector('.panel');
|
| 82 |
+
panelEl.dataset.panelId = id;
|
| 83 |
+
|
| 84 |
+
const vizContainer = panelEl.querySelector('.panel-viz');
|
| 85 |
+
vizContainer.id = `viz-${id}`;
|
| 86 |
+
|
| 87 |
+
panelsContainer.appendChild(content);
|
| 88 |
+
|
| 89 |
+
const panel = panelsContainer.querySelector(`[data-panel-id="${id}"]`);
|
| 90 |
+
const viz = new TreemapViz(`viz-${id}`);
|
| 91 |
+
|
| 92 |
+
panels.set(id, {
|
| 93 |
+
el: panel,
|
| 94 |
+
viz: viz,
|
| 95 |
+
data: null,
|
| 96 |
+
modelId: null
|
| 97 |
+
});
|
| 98 |
+
|
| 99 |
+
setupPanelEvents(id, panel);
|
| 100 |
+
updateCloseButtons();
|
| 101 |
+
|
| 102 |
+
return { id, el: panel };
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
function setupPanelEvents(panelId, panelEl) {
|
| 106 |
+
const input = panelEl.querySelector('.panel-input');
|
| 107 |
+
const goBtn = panelEl.querySelector('.panel-go-btn');
|
| 108 |
+
const closeBtn = panelEl.querySelector('.panel-close-btn');
|
| 109 |
+
|
| 110 |
+
goBtn.addEventListener('click', () => {
|
| 111 |
+
const modelId = input.value.trim();
|
| 112 |
+
if (modelId) loadModelInPanel(panelId, modelId);
|
| 113 |
+
});
|
| 114 |
+
|
| 115 |
+
input.addEventListener('keypress', (e) => {
|
| 116 |
+
if (e.key === 'Enter') {
|
| 117 |
+
const modelId = input.value.trim();
|
| 118 |
+
if (modelId) loadModelInPanel(panelId, modelId);
|
| 119 |
+
}
|
| 120 |
+
});
|
| 121 |
+
|
| 122 |
+
closeBtn.addEventListener('click', () => removePanel(panelId));
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
function removePanel(panelId) {
|
| 126 |
+
if (panels.size <= 1) return;
|
| 127 |
+
|
| 128 |
+
const panel = panels.get(panelId);
|
| 129 |
+
if (panel) {
|
| 130 |
+
panel.viz.destroy();
|
| 131 |
+
panel.el.remove();
|
| 132 |
+
panels.delete(panelId);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
updateCloseButtons();
|
| 136 |
+
updateGlobalScale();
|
| 137 |
+
updateUrlState();
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
function updateCloseButtons() {
|
| 141 |
+
const canClose = panels.size > 1;
|
| 142 |
+
panels.forEach(panel => {
|
| 143 |
+
const closeBtn = panel.el.querySelector('.panel-close-btn');
|
| 144 |
+
closeBtn.style.visibility = canClose ? 'visible' : 'hidden';
|
| 145 |
+
});
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
async function loadModelInPanel(panelId, modelId) {
|
| 149 |
+
const panel = panels.get(panelId);
|
| 150 |
+
if (!panel) return;
|
| 151 |
+
|
| 152 |
+
const input = panel.el.querySelector('.panel-input');
|
| 153 |
+
const goBtn = panel.el.querySelector('.panel-go-btn');
|
| 154 |
+
const btnText = goBtn.querySelector('.btn-text');
|
| 155 |
+
const btnLoader = goBtn.querySelector('.btn-loader');
|
| 156 |
+
const infoEl = panel.el.querySelector('.panel-info');
|
| 157 |
+
const errorEl = panel.el.querySelector('.panel-error');
|
| 158 |
+
const emptyEl = panel.el.querySelector('.panel-empty');
|
| 159 |
+
|
| 160 |
+
input.value = modelId;
|
| 161 |
+
loadingPanels.add(panelId);
|
| 162 |
+
|
| 163 |
+
goBtn.disabled = true;
|
| 164 |
+
btnText.classList.add('hidden');
|
| 165 |
+
btnLoader.classList.remove('hidden');
|
| 166 |
+
errorEl.classList.add('hidden');
|
| 167 |
+
infoEl.classList.add('hidden');
|
| 168 |
+
|
| 169 |
+
// Remove any existing hint
|
| 170 |
+
const existingHint = panel.el.querySelector('.error-hint');
|
| 171 |
+
if (existingHint) existingHint.remove();
|
| 172 |
+
|
| 173 |
+
// Show loading status in canvas
|
| 174 |
+
const statusMessages = [
|
| 175 |
+
`Fetching config for ${modelId}...`,
|
| 176 |
+
'Downloading model configuration...',
|
| 177 |
+
'Building architecture graph...',
|
| 178 |
+
'Calculating parameter counts...',
|
| 179 |
+
'Rendering visualization...'
|
| 180 |
+
];
|
| 181 |
+
let statusIndex = 0;
|
| 182 |
+
|
| 183 |
+
emptyEl.innerHTML = `
|
| 184 |
+
<div class="loading-status">
|
| 185 |
+
<svg class="loading-spinner" viewBox="0 0 24 24">
|
| 186 |
+
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" stroke-dasharray="30 70"/>
|
| 187 |
+
</svg>
|
| 188 |
+
<p class="status-text">${statusMessages[0]}</p>
|
| 189 |
+
</div>
|
| 190 |
+
`;
|
| 191 |
+
emptyEl.classList.remove('hidden');
|
| 192 |
+
|
| 193 |
+
// Cycle through status messages
|
| 194 |
+
const statusInterval = setInterval(() => {
|
| 195 |
+
statusIndex = (statusIndex + 1) % statusMessages.length;
|
| 196 |
+
const statusText = emptyEl.querySelector('.status-text');
|
| 197 |
+
if (statusText) {
|
| 198 |
+
statusText.textContent = statusMessages[statusIndex];
|
| 199 |
+
}
|
| 200 |
+
}, 2000);
|
| 201 |
+
|
| 202 |
+
try {
|
| 203 |
+
const result = await API.inspectModel(modelId);
|
| 204 |
+
|
| 205 |
+
panel.data = result;
|
| 206 |
+
panel.modelId = modelId;
|
| 207 |
+
|
| 208 |
+
const nameEl = infoEl.querySelector('.panel-model-name');
|
| 209 |
+
const paramsEl = infoEl.querySelector('.panel-params');
|
| 210 |
+
const typeEl = infoEl.querySelector('.panel-type');
|
| 211 |
+
|
| 212 |
+
nameEl.textContent = result.metadata.model_id || modelId;
|
| 213 |
+
paramsEl.textContent = result.metadata.formatted_params;
|
| 214 |
+
typeEl.textContent = result.metadata.model_type.toUpperCase();
|
| 215 |
+
infoEl.classList.remove('hidden');
|
| 216 |
+
|
| 217 |
+
emptyEl.classList.add('hidden');
|
| 218 |
+
|
| 219 |
+
updateGlobalScale();
|
| 220 |
+
updateUrlState();
|
| 221 |
+
updateLegend();
|
| 222 |
+
|
| 223 |
+
} catch (error) {
|
| 224 |
+
// Restore empty state
|
| 225 |
+
emptyEl.innerHTML = `
|
| 226 |
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
|
| 227 |
+
<circle cx="10" cy="10" r="7"/>
|
| 228 |
+
<line x1="15" y1="15" x2="21" y2="21" stroke-width="2" stroke-linecap="round"/>
|
| 229 |
+
</svg>
|
| 230 |
+
<p>Enter a model ID to inspect</p>
|
| 231 |
+
`;
|
| 232 |
+
|
| 233 |
+
errorEl.textContent = error.message;
|
| 234 |
+
errorEl.classList.remove('hidden');
|
| 235 |
+
|
| 236 |
+
// Add auth hint if it's an auth error
|
| 237 |
+
if (error.isAuthError) {
|
| 238 |
+
const hint = document.createElement('div');
|
| 239 |
+
hint.className = 'error-hint';
|
| 240 |
+
hint.innerHTML = `
|
| 241 |
+
This model may require authentication. Make sure you're logged in to HuggingFace:<br>
|
| 242 |
+
<code>huggingface-cli login</code> or set <code>HF_TOKEN</code> environment variable.
|
| 243 |
+
<br><br>
|
| 244 |
+
For gated models, you may also need to accept the model's license on the HuggingFace website.
|
| 245 |
+
`;
|
| 246 |
+
errorEl.after(hint);
|
| 247 |
+
}
|
| 248 |
+
} finally {
|
| 249 |
+
clearInterval(statusInterval);
|
| 250 |
+
loadingPanels.delete(panelId);
|
| 251 |
+
goBtn.disabled = false;
|
| 252 |
+
btnText.classList.remove('hidden');
|
| 253 |
+
btnLoader.classList.add('hidden');
|
| 254 |
+
}
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
function updateGlobalScale() {
|
| 258 |
+
if (zoomLocked) {
|
| 259 |
+
globalMaxParams = 0;
|
| 260 |
+
panels.forEach(panel => {
|
| 261 |
+
if (panel.data) {
|
| 262 |
+
globalMaxParams = Math.max(globalMaxParams, panel.data.pipeline.params);
|
| 263 |
+
}
|
| 264 |
+
});
|
| 265 |
+
|
| 266 |
+
panels.forEach(panel => {
|
| 267 |
+
if (panel.data) {
|
| 268 |
+
panel.viz.setData(panel.data.pipeline, globalMaxParams);
|
| 269 |
+
}
|
| 270 |
+
});
|
| 271 |
+
} else {
|
| 272 |
+
globalMaxParams = 0;
|
| 273 |
+
panels.forEach(panel => {
|
| 274 |
+
if (panel.data) {
|
| 275 |
+
panel.viz.setData(panel.data.pipeline, null);
|
| 276 |
+
}
|
| 277 |
+
});
|
| 278 |
+
}
|
| 279 |
+
}
|
| 280 |
+
|
| 281 |
+
function handleZoomLockChange() {
|
| 282 |
+
zoomLocked = zoomLockCheckbox.checked;
|
| 283 |
+
updateGlobalScale();
|
| 284 |
+
updateUrlState();
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
function handleExport(format = 'svg') {
|
| 288 |
+
const loadedPanels = [];
|
| 289 |
+
panels.forEach(panel => {
|
| 290 |
+
if (panel.data && panel.modelId) {
|
| 291 |
+
loadedPanels.push(panel);
|
| 292 |
+
}
|
| 293 |
+
});
|
| 294 |
+
|
| 295 |
+
if (loadedPanels.length === 0) return;
|
| 296 |
+
|
| 297 |
+
const panel = loadedPanels[0];
|
| 298 |
+
const blob = panel.viz.exportSVG(panel.modelId);
|
| 299 |
+
const filename = `${panel.modelId.replace(/\//g, '-')}-architecture`;
|
| 300 |
+
|
| 301 |
+
if (format === 'svg') {
|
| 302 |
+
const url = URL.createObjectURL(blob);
|
| 303 |
+
const a = document.createElement('a');
|
| 304 |
+
a.href = url;
|
| 305 |
+
a.download = `${filename}.svg`;
|
| 306 |
+
document.body.appendChild(a);
|
| 307 |
+
a.click();
|
| 308 |
+
document.body.removeChild(a);
|
| 309 |
+
URL.revokeObjectURL(url);
|
| 310 |
+
} else if (format === 'png') {
|
| 311 |
+
// Convert SVG to PNG
|
| 312 |
+
const svgText = URL.createObjectURL(blob);
|
| 313 |
+
const img = new Image();
|
| 314 |
+
|
| 315 |
+
img.onload = () => {
|
| 316 |
+
const scale = 2; // Higher resolution
|
| 317 |
+
const canvas = document.createElement('canvas');
|
| 318 |
+
canvas.width = img.width * scale;
|
| 319 |
+
canvas.height = img.height * scale;
|
| 320 |
+
|
| 321 |
+
const ctx = canvas.getContext('2d');
|
| 322 |
+
ctx.scale(scale, scale);
|
| 323 |
+
ctx.fillStyle = '#0a0a0b'; // Background color
|
| 324 |
+
ctx.fillRect(0, 0, img.width, img.height);
|
| 325 |
+
ctx.drawImage(img, 0, 0);
|
| 326 |
+
|
| 327 |
+
canvas.toBlob((pngBlob) => {
|
| 328 |
+
const url = URL.createObjectURL(pngBlob);
|
| 329 |
+
const a = document.createElement('a');
|
| 330 |
+
a.href = url;
|
| 331 |
+
a.download = `${filename}.png`;
|
| 332 |
+
document.body.appendChild(a);
|
| 333 |
+
a.click();
|
| 334 |
+
document.body.removeChild(a);
|
| 335 |
+
URL.revokeObjectURL(url);
|
| 336 |
+
}, 'image/png');
|
| 337 |
+
|
| 338 |
+
URL.revokeObjectURL(svgText);
|
| 339 |
+
};
|
| 340 |
+
|
| 341 |
+
img.src = svgText;
|
| 342 |
+
}
|
| 343 |
+
}
|
| 344 |
+
|
| 345 |
+
function handleCompare() {
|
| 346 |
+
const loadedPanels = [];
|
| 347 |
+
panels.forEach(panel => {
|
| 348 |
+
if (panel.data && panel.modelId) {
|
| 349 |
+
loadedPanels.push(panel);
|
| 350 |
+
}
|
| 351 |
+
});
|
| 352 |
+
|
| 353 |
+
if (loadedPanels.length < 2) {
|
| 354 |
+
compareResults.innerHTML = '<p class="compare-empty">Load at least 2 models to compare architectures</p>';
|
| 355 |
+
} else {
|
| 356 |
+
const comparison = compareArchitectures(loadedPanels[0], loadedPanels[1]);
|
| 357 |
+
renderComparison(comparison, loadedPanels[0], loadedPanels[1]);
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
compareModal.classList.remove('hidden');
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
function closeCompareModal() {
|
| 364 |
+
compareModal.classList.add('hidden');
|
| 365 |
+
}
|
| 366 |
+
|
| 367 |
+
/**
|
| 368 |
+
* Compare two model architectures
|
| 369 |
+
*/
|
| 370 |
+
function compareArchitectures(panelA, panelB) {
|
| 371 |
+
const a = panelA.data.pipeline;
|
| 372 |
+
const b = panelB.data.pipeline;
|
| 373 |
+
|
| 374 |
+
const result = {
|
| 375 |
+
modelA: { name: panelA.modelId, params: a.params },
|
| 376 |
+
modelB: { name: panelB.modelId, params: b.params },
|
| 377 |
+
paramRatio: a.params > 0 ? b.params / a.params : 0,
|
| 378 |
+
similarities: [],
|
| 379 |
+
differences: [],
|
| 380 |
+
onlyInA: [],
|
| 381 |
+
onlyInB: [],
|
| 382 |
+
similarity: 0
|
| 383 |
+
};
|
| 384 |
+
|
| 385 |
+
// Flatten steps for comparison
|
| 386 |
+
const stepsA = flattenSteps(a.steps || []);
|
| 387 |
+
const stepsB = flattenSteps(b.steps || []);
|
| 388 |
+
|
| 389 |
+
// Compare by type
|
| 390 |
+
const typesA = new Map();
|
| 391 |
+
const typesB = new Map();
|
| 392 |
+
|
| 393 |
+
stepsA.forEach(s => {
|
| 394 |
+
const key = s.type + ':' + s.name;
|
| 395 |
+
typesA.set(key, s);
|
| 396 |
+
});
|
| 397 |
+
|
| 398 |
+
stepsB.forEach(s => {
|
| 399 |
+
const key = s.type + ':' + s.name;
|
| 400 |
+
typesB.set(key, s);
|
| 401 |
+
});
|
| 402 |
+
|
| 403 |
+
// Find common and different
|
| 404 |
+
let matchScore = 0;
|
| 405 |
+
const totalSteps = Math.max(typesA.size, typesB.size);
|
| 406 |
+
|
| 407 |
+
typesA.forEach((stepA, key) => {
|
| 408 |
+
if (typesB.has(key)) {
|
| 409 |
+
const stepB = typesB.get(key);
|
| 410 |
+
matchScore++;
|
| 411 |
+
|
| 412 |
+
// Check if params are similar
|
| 413 |
+
const paramDiff = stepA.params > 0 ? Math.abs(stepA.params - stepB.params) / stepA.params : 0;
|
| 414 |
+
|
| 415 |
+
if (paramDiff < 0.05) {
|
| 416 |
+
result.similarities.push({
|
| 417 |
+
name: stepA.name,
|
| 418 |
+
type: stepA.type,
|
| 419 |
+
paramsA: stepA.params,
|
| 420 |
+
paramsB: stepB.params,
|
| 421 |
+
shapeA: stepA.shape,
|
| 422 |
+
shapeB: stepB.shape
|
| 423 |
+
});
|
| 424 |
+
} else {
|
| 425 |
+
result.differences.push({
|
| 426 |
+
name: stepA.name,
|
| 427 |
+
type: stepA.type,
|
| 428 |
+
paramsA: stepA.params,
|
| 429 |
+
paramsB: stepB.params,
|
| 430 |
+
shapeA: stepA.shape,
|
| 431 |
+
shapeB: stepB.shape,
|
| 432 |
+
paramDiff: paramDiff
|
| 433 |
+
});
|
| 434 |
+
}
|
| 435 |
+
} else {
|
| 436 |
+
result.onlyInA.push({
|
| 437 |
+
name: stepA.name,
|
| 438 |
+
type: stepA.type,
|
| 439 |
+
params: stepA.params,
|
| 440 |
+
shape: stepA.shape
|
| 441 |
+
});
|
| 442 |
+
}
|
| 443 |
+
});
|
| 444 |
+
|
| 445 |
+
typesB.forEach((stepB, key) => {
|
| 446 |
+
if (!typesA.has(key)) {
|
| 447 |
+
result.onlyInB.push({
|
| 448 |
+
name: stepB.name,
|
| 449 |
+
type: stepB.type,
|
| 450 |
+
params: stepB.params,
|
| 451 |
+
shape: stepB.shape
|
| 452 |
+
});
|
| 453 |
+
}
|
| 454 |
+
});
|
| 455 |
+
|
| 456 |
+
// Calculate similarity
|
| 457 |
+
result.similarity = totalSteps > 0 ? (matchScore / totalSteps) * 100 : 0;
|
| 458 |
+
|
| 459 |
+
// Compare layer counts
|
| 460 |
+
const layersA = stepsA.find(s => s.type === 'layers');
|
| 461 |
+
const layersB = stepsB.find(s => s.type === 'layers');
|
| 462 |
+
|
| 463 |
+
if (layersA && layersB) {
|
| 464 |
+
result.layerComparison = {
|
| 465 |
+
countA: layersA.count || 1,
|
| 466 |
+
countB: layersB.count || 1,
|
| 467 |
+
paramsA: layersA.params,
|
| 468 |
+
paramsB: layersB.params,
|
| 469 |
+
shapeA: layersA.shape,
|
| 470 |
+
shapeB: layersB.shape
|
| 471 |
+
};
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
return result;
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
/**
|
| 478 |
+
* Flatten nested steps for comparison
|
| 479 |
+
*/
|
| 480 |
+
function flattenSteps(steps, result = []) {
|
| 481 |
+
for (const step of steps) {
|
| 482 |
+
result.push(step);
|
| 483 |
+
if (step.substeps) {
|
| 484 |
+
flattenSteps(step.substeps, result);
|
| 485 |
+
}
|
| 486 |
+
if (step.branches) {
|
| 487 |
+
for (const branch of step.branches) {
|
| 488 |
+
result.push(branch);
|
| 489 |
+
if (branch.substeps) {
|
| 490 |
+
flattenSteps(branch.substeps, result);
|
| 491 |
+
}
|
| 492 |
+
}
|
| 493 |
+
}
|
| 494 |
+
}
|
| 495 |
+
return result;
|
| 496 |
+
}
|
| 497 |
+
|
| 498 |
+
/**
|
| 499 |
+
* Render comparison results
|
| 500 |
+
*/
|
| 501 |
+
function renderComparison(comparison, panelA, panelB) {
|
| 502 |
+
const fmt = API.formatParams;
|
| 503 |
+
|
| 504 |
+
let html = `
|
| 505 |
+
<div class="compare-summary">
|
| 506 |
+
<div class="compare-model">
|
| 507 |
+
<div class="compare-model-name">${comparison.modelA.name}</div>
|
| 508 |
+
<div class="compare-model-params">${fmt(comparison.modelA.params)}</div>
|
| 509 |
+
</div>
|
| 510 |
+
<div class="compare-model">
|
| 511 |
+
<div class="compare-model-name">${comparison.modelB.name}</div>
|
| 512 |
+
<div class="compare-model-params">${fmt(comparison.modelB.params)}</div>
|
| 513 |
+
</div>
|
| 514 |
+
</div>
|
| 515 |
+
|
| 516 |
+
<div class="compare-similarity">
|
| 517 |
+
<span class="similarity-label">Structural Similarity</span>
|
| 518 |
+
<div class="similarity-bar">
|
| 519 |
+
<div class="similarity-fill" style="width: ${comparison.similarity}%"></div>
|
| 520 |
+
</div>
|
| 521 |
+
<span class="similarity-value">${comparison.similarity.toFixed(0)}%</span>
|
| 522 |
+
</div>
|
| 523 |
+
`;
|
| 524 |
+
|
| 525 |
+
// Layer comparison
|
| 526 |
+
if (comparison.layerComparison) {
|
| 527 |
+
const lc = comparison.layerComparison;
|
| 528 |
+
html += `
|
| 529 |
+
<div class="compare-section">
|
| 530 |
+
<div class="compare-section-title">Transformer Layers</div>
|
| 531 |
+
<ul class="compare-list">
|
| 532 |
+
<li>
|
| 533 |
+
<span class="item-name">Layer Count</span>
|
| 534 |
+
<span class="item-diff">
|
| 535 |
+
<span class="model-a">${lc.countA}</span>
|
| 536 |
+
<span>/</span>
|
| 537 |
+
<span class="model-b">${lc.countB}</span>
|
| 538 |
+
</span>
|
| 539 |
+
</li>
|
| 540 |
+
<li>
|
| 541 |
+
<span class="item-name">Total Params</span>
|
| 542 |
+
<span class="item-diff">
|
| 543 |
+
<span class="model-a">${fmt(lc.paramsA)}</span>
|
| 544 |
+
<span>/</span>
|
| 545 |
+
<span class="model-b">${fmt(lc.paramsB)}</span>
|
| 546 |
+
</span>
|
| 547 |
+
</li>
|
| 548 |
+
${lc.shapeA || lc.shapeB ? `
|
| 549 |
+
<li>
|
| 550 |
+
<span class="item-name">Dimensions</span>
|
| 551 |
+
<span class="item-diff">
|
| 552 |
+
<span class="model-a">${lc.shapeA || '-'}</span>
|
| 553 |
+
<span>/</span>
|
| 554 |
+
<span class="model-b">${lc.shapeB || '-'}</span>
|
| 555 |
+
</span>
|
| 556 |
+
</li>
|
| 557 |
+
` : ''}
|
| 558 |
+
</ul>
|
| 559 |
+
</div>
|
| 560 |
+
`;
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
// Similarities
|
| 564 |
+
if (comparison.similarities.length > 0) {
|
| 565 |
+
html += `
|
| 566 |
+
<div class="compare-section">
|
| 567 |
+
<div class="compare-section-title">
|
| 568 |
+
Similar Components
|
| 569 |
+
<span class="compare-badge same">${comparison.similarities.length}</span>
|
| 570 |
+
</div>
|
| 571 |
+
<ul class="compare-list">
|
| 572 |
+
${comparison.similarities.slice(0, 10).map(s => `
|
| 573 |
+
<li>
|
| 574 |
+
<span class="item-name">${s.name}</span>
|
| 575 |
+
<span class="item-detail">${s.type}</span>
|
| 576 |
+
</li>
|
| 577 |
+
`).join('')}
|
| 578 |
+
${comparison.similarities.length > 10 ? `<li><span class="item-detail">...and ${comparison.similarities.length - 10} more</span></li>` : ''}
|
| 579 |
+
</ul>
|
| 580 |
+
</div>
|
| 581 |
+
`;
|
| 582 |
+
}
|
| 583 |
+
|
| 584 |
+
// Differences
|
| 585 |
+
if (comparison.differences.length > 0) {
|
| 586 |
+
html += `
|
| 587 |
+
<div class="compare-section">
|
| 588 |
+
<div class="compare-section-title">
|
| 589 |
+
Different Sizes
|
| 590 |
+
<span class="compare-badge diff">${comparison.differences.length}</span>
|
| 591 |
+
</div>
|
| 592 |
+
<ul class="compare-list">
|
| 593 |
+
${comparison.differences.slice(0, 10).map(d => `
|
| 594 |
+
<li>
|
| 595 |
+
<span class="item-name">${d.name}</span>
|
| 596 |
+
<span class="item-diff">
|
| 597 |
+
<span class="model-a">${fmt(d.paramsA)}</span>
|
| 598 |
+
<span>/</span>
|
| 599 |
+
<span class="model-b">${fmt(d.paramsB)}</span>
|
| 600 |
+
</span>
|
| 601 |
+
</li>
|
| 602 |
+
`).join('')}
|
| 603 |
+
${comparison.differences.length > 10 ? `<li><span class="item-detail">...and ${comparison.differences.length - 10} more</span></li>` : ''}
|
| 604 |
+
</ul>
|
| 605 |
+
</div>
|
| 606 |
+
`;
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
// Only in A
|
| 610 |
+
if (comparison.onlyInA.length > 0) {
|
| 611 |
+
html += `
|
| 612 |
+
<div class="compare-section">
|
| 613 |
+
<div class="compare-section-title">
|
| 614 |
+
Only in ${comparison.modelA.name.split('/').pop()}
|
| 615 |
+
<span class="compare-badge removed">${comparison.onlyInA.length}</span>
|
| 616 |
+
</div>
|
| 617 |
+
<ul class="compare-list">
|
| 618 |
+
${comparison.onlyInA.map(s => `
|
| 619 |
+
<li>
|
| 620 |
+
<span class="item-name">${s.name}</span>
|
| 621 |
+
<span class="item-detail">${s.type} - ${fmt(s.params)}</span>
|
| 622 |
+
</li>
|
| 623 |
+
`).join('')}
|
| 624 |
+
</ul>
|
| 625 |
+
</div>
|
| 626 |
+
`;
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
// Only in B
|
| 630 |
+
if (comparison.onlyInB.length > 0) {
|
| 631 |
+
html += `
|
| 632 |
+
<div class="compare-section">
|
| 633 |
+
<div class="compare-section-title">
|
| 634 |
+
Only in ${comparison.modelB.name.split('/').pop()}
|
| 635 |
+
<span class="compare-badge added">${comparison.onlyInB.length}</span>
|
| 636 |
+
</div>
|
| 637 |
+
<ul class="compare-list">
|
| 638 |
+
${comparison.onlyInB.map(s => `
|
| 639 |
+
<li>
|
| 640 |
+
<span class="item-name">${s.name}</span>
|
| 641 |
+
<span class="item-detail">${s.type} - ${fmt(s.params)}</span>
|
| 642 |
+
</li>
|
| 643 |
+
`).join('')}
|
| 644 |
+
</ul>
|
| 645 |
+
</div>
|
| 646 |
+
`;
|
| 647 |
+
}
|
| 648 |
+
|
| 649 |
+
compareResults.innerHTML = html;
|
| 650 |
+
}
|
| 651 |
+
|
| 652 |
+
function updateLegend() {
|
| 653 |
+
const allTypes = new Set();
|
| 654 |
+
const colors = {};
|
| 655 |
+
|
| 656 |
+
panels.forEach(panel => {
|
| 657 |
+
if (panel.data && panel.viz) {
|
| 658 |
+
const items = panel.viz.getLegendItems();
|
| 659 |
+
items.forEach(item => {
|
| 660 |
+
allTypes.add(item.type);
|
| 661 |
+
colors[item.type] = item.color;
|
| 662 |
+
});
|
| 663 |
+
}
|
| 664 |
+
});
|
| 665 |
+
|
| 666 |
+
legend.innerHTML = '';
|
| 667 |
+
allTypes.forEach(type => {
|
| 668 |
+
const el = document.createElement('div');
|
| 669 |
+
el.className = 'legend-item';
|
| 670 |
+
el.innerHTML = `
|
| 671 |
+
<div class="legend-color" style="background-color: ${colors[type]}"></div>
|
| 672 |
+
<span class="legend-label">${type}</span>
|
| 673 |
+
`;
|
| 674 |
+
legend.appendChild(el);
|
| 675 |
+
});
|
| 676 |
+
}
|
| 677 |
+
|
| 678 |
+
function parseUrlState() {
|
| 679 |
+
const hash = window.location.hash.slice(1);
|
| 680 |
+
if (!hash) return { models: [], zoomLocked: true };
|
| 681 |
+
|
| 682 |
+
try {
|
| 683 |
+
const params = new URLSearchParams(hash);
|
| 684 |
+
const models = (params.get('models') || '').split(',').filter(Boolean);
|
| 685 |
+
const zoomLocked = params.get('lock') !== '0';
|
| 686 |
+
return { models, zoomLocked };
|
| 687 |
+
} catch {
|
| 688 |
+
return { models: [], zoomLocked: true };
|
| 689 |
+
}
|
| 690 |
+
}
|
| 691 |
+
|
| 692 |
+
function updateUrlState() {
|
| 693 |
+
const models = [];
|
| 694 |
+
panels.forEach(panel => {
|
| 695 |
+
models.push(panel.modelId || '');
|
| 696 |
+
});
|
| 697 |
+
|
| 698 |
+
const params = new URLSearchParams();
|
| 699 |
+
params.set('models', models.join(','));
|
| 700 |
+
params.set('lock', zoomLocked ? '1' : '0');
|
| 701 |
+
|
| 702 |
+
window.history.replaceState(null, '', `#${params.toString()}`);
|
| 703 |
+
}
|
| 704 |
+
|
| 705 |
+
if (document.readyState === 'loading') {
|
| 706 |
+
document.addEventListener('DOMContentLoaded', init);
|
| 707 |
+
} else {
|
| 708 |
+
init();
|
| 709 |
+
}
|
| 710 |
+
})();
|
frontend/js/treemap.js
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/**
|
| 2 |
+
* LLM Scope - Pipeline Visualization
|
| 3 |
+
* Professional, minimalist architecture diagrams
|
| 4 |
+
*/
|
| 5 |
+
|
| 6 |
+
class ModelTreeViz {
|
| 7 |
+
constructor(containerId) {
|
| 8 |
+
this.containerId = containerId;
|
| 9 |
+
this.container = document.getElementById(containerId);
|
| 10 |
+
this.pipelineData = null;
|
| 11 |
+
this.totalParams = 0;
|
| 12 |
+
this.globalMaxParams = null;
|
| 13 |
+
|
| 14 |
+
// Layout
|
| 15 |
+
this.baseNodeHeight = 48;
|
| 16 |
+
this.minNodeHeight = 36;
|
| 17 |
+
this.maxNodeHeight = 72;
|
| 18 |
+
this.nodeGap = 12;
|
| 19 |
+
this.containerPadding = 14;
|
| 20 |
+
this.containerGap = 8;
|
| 21 |
+
this.padding = 24;
|
| 22 |
+
this.branchGap = 24;
|
| 23 |
+
|
| 24 |
+
// Width
|
| 25 |
+
this.minNodeWidth = 110;
|
| 26 |
+
this.maxNodeWidth = 240;
|
| 27 |
+
|
| 28 |
+
this.transform = d3.zoomIdentity;
|
| 29 |
+
|
| 30 |
+
// Professional muted color palette
|
| 31 |
+
this.colors = {
|
| 32 |
+
embedding: '#d97706', // amber
|
| 33 |
+
attention: '#7c3aed', // violet
|
| 34 |
+
mlp: '#059669', // emerald
|
| 35 |
+
norm: '#0891b2', // cyan
|
| 36 |
+
head: '#db2777', // pink
|
| 37 |
+
layers: '#4f46e5', // indigo
|
| 38 |
+
encoder: '#6366f1', // indigo lighter
|
| 39 |
+
linear: '#6b7280', // gray
|
| 40 |
+
module: '#52525b', // zinc
|
| 41 |
+
model: '#3f3f46', // zinc darker
|
| 42 |
+
block: '#52525b',
|
| 43 |
+
pooler: '#0d9488', // teal
|
| 44 |
+
parallel: '#3f3f46',
|
| 45 |
+
default: '#52525b'
|
| 46 |
+
};
|
| 47 |
+
|
| 48 |
+
this._init();
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
_init() {
|
| 52 |
+
this.svg = d3.select(`#${this.containerId}`)
|
| 53 |
+
.append('svg')
|
| 54 |
+
.attr('width', '100%')
|
| 55 |
+
.attr('height', '100%');
|
| 56 |
+
|
| 57 |
+
this.zoom = d3.zoom()
|
| 58 |
+
.scaleExtent([0.2, 3])
|
| 59 |
+
.on('zoom', (event) => {
|
| 60 |
+
this.transform = event.transform;
|
| 61 |
+
this.g.attr('transform', event.transform);
|
| 62 |
+
});
|
| 63 |
+
|
| 64 |
+
this.svg.call(this.zoom);
|
| 65 |
+
|
| 66 |
+
this.g = this.svg.append('g');
|
| 67 |
+
this.containersGroup = this.g.append('g').attr('class', 'containers');
|
| 68 |
+
this.arrowsGroup = this.g.append('g').attr('class', 'arrows');
|
| 69 |
+
this.nodesGroup = this.g.append('g').attr('class', 'nodes');
|
| 70 |
+
|
| 71 |
+
// Arrow marker
|
| 72 |
+
this.svg.append('defs').append('marker')
|
| 73 |
+
.attr('id', `arrow-${this.containerId}`)
|
| 74 |
+
.attr('viewBox', '0 -4 8 8')
|
| 75 |
+
.attr('refX', 7)
|
| 76 |
+
.attr('refY', 0)
|
| 77 |
+
.attr('markerWidth', 5)
|
| 78 |
+
.attr('markerHeight', 5)
|
| 79 |
+
.attr('orient', 'auto')
|
| 80 |
+
.append('path')
|
| 81 |
+
.attr('d', 'M0,-4L8,0L0,4')
|
| 82 |
+
.attr('fill', '#52525b');
|
| 83 |
+
|
| 84 |
+
this.resizeObserver = new ResizeObserver(() => this._onResize());
|
| 85 |
+
this.resizeObserver.observe(this.container);
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
_onResize() {
|
| 89 |
+
if (this.pipelineData) {
|
| 90 |
+
this._render();
|
| 91 |
+
}
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
getColor(type) {
|
| 95 |
+
return this.colors[type] || this.colors.default;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
_getNodeWidth(params) {
|
| 99 |
+
const refParams = this.globalMaxParams || this.totalParams;
|
| 100 |
+
if (refParams === 0 || params === 0) return this.minNodeWidth;
|
| 101 |
+
const ratio = params / refParams;
|
| 102 |
+
const scale = Math.sqrt(ratio);
|
| 103 |
+
return this.minNodeWidth + (this.maxNodeWidth - this.minNodeWidth) * scale;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
_getNodeHeight(params) {
|
| 107 |
+
const refParams = this.globalMaxParams || this.totalParams;
|
| 108 |
+
if (refParams === 0 || params === 0) return this.minNodeHeight;
|
| 109 |
+
const ratio = params / refParams;
|
| 110 |
+
const scale = Math.pow(ratio, 0.25);
|
| 111 |
+
return this.minNodeHeight + (this.maxNodeHeight - this.minNodeHeight) * scale;
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
setData(data, globalMaxParams = null) {
|
| 115 |
+
this.pipelineData = data;
|
| 116 |
+
this.totalParams = data.params || 0;
|
| 117 |
+
this.globalMaxParams = globalMaxParams;
|
| 118 |
+
this._render();
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
_calcStepWidth(step) {
|
| 122 |
+
const nodeWidth = this._getNodeWidth(step.params || 0);
|
| 123 |
+
|
| 124 |
+
if (step.type === 'parallel' && step.branches) {
|
| 125 |
+
let totalWidth = 0;
|
| 126 |
+
for (const branch of step.branches) {
|
| 127 |
+
totalWidth += this._calcStepWidth(branch);
|
| 128 |
+
}
|
| 129 |
+
totalWidth += (step.branches.length - 1) * this.branchGap;
|
| 130 |
+
return Math.max(nodeWidth, totalWidth);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
if (step.substeps && step._collapsed === false) {
|
| 134 |
+
let maxChildWidth = 0;
|
| 135 |
+
for (const sub of step.substeps) {
|
| 136 |
+
maxChildWidth = Math.max(maxChildWidth, this._calcStepWidth(sub));
|
| 137 |
+
}
|
| 138 |
+
return Math.max(nodeWidth, maxChildWidth + this.containerPadding * 2);
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
return nodeWidth;
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
_layoutPipeline(data) {
|
| 145 |
+
const nodes = [];
|
| 146 |
+
const arrows = [];
|
| 147 |
+
const containers = [];
|
| 148 |
+
const centerX = 350;
|
| 149 |
+
|
| 150 |
+
const layoutSteps = (steps, startY, parentCenterX) => {
|
| 151 |
+
if (!steps || steps.length === 0) {
|
| 152 |
+
return { endY: startY, lastNodes: [] };
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
let y = startY;
|
| 156 |
+
let prevNodes = [];
|
| 157 |
+
|
| 158 |
+
for (let i = 0; i < steps.length; i++) {
|
| 159 |
+
const step = steps[i];
|
| 160 |
+
|
| 161 |
+
if (step.type === 'parallel' && step.branches && step.branches.length > 1) {
|
| 162 |
+
const branchResults = [];
|
| 163 |
+
const branchWidths = step.branches.map(b => this._calcStepWidth(b));
|
| 164 |
+
const totalWidth = branchWidths.reduce((a, b) => a + b, 0) + (step.branches.length - 1) * this.branchGap;
|
| 165 |
+
|
| 166 |
+
let branchX = parentCenterX - totalWidth / 2;
|
| 167 |
+
const branchStartY = y;
|
| 168 |
+
|
| 169 |
+
for (let bi = 0; bi < step.branches.length; bi++) {
|
| 170 |
+
const branch = step.branches[bi];
|
| 171 |
+
const branchWidth = branchWidths[bi];
|
| 172 |
+
const branchCenterX = branchX + branchWidth / 2;
|
| 173 |
+
|
| 174 |
+
const result = layoutSteps([branch], branchStartY, branchCenterX);
|
| 175 |
+
branchResults.push({
|
| 176 |
+
result,
|
| 177 |
+
centerX: branchCenterX,
|
| 178 |
+
firstNode: nodes.find(n => n.data === branch)
|
| 179 |
+
});
|
| 180 |
+
|
| 181 |
+
branchX += branchWidth + this.branchGap;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
for (const prev of prevNodes) {
|
| 185 |
+
for (const br of branchResults) {
|
| 186 |
+
if (br.firstNode) {
|
| 187 |
+
arrows.push({ source: prev, target: br.firstNode });
|
| 188 |
+
}
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
let maxEndY = y;
|
| 193 |
+
let allLastNodes = [];
|
| 194 |
+
for (const br of branchResults) {
|
| 195 |
+
maxEndY = Math.max(maxEndY, br.result.endY);
|
| 196 |
+
if (br.result.lastNodes) {
|
| 197 |
+
allLastNodes.push(...br.result.lastNodes);
|
| 198 |
+
}
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
y = maxEndY;
|
| 202 |
+
prevNodes = allLastNodes;
|
| 203 |
+
continue;
|
| 204 |
+
}
|
| 205 |
+
|
| 206 |
+
const nodeWidth = this._getNodeWidth(step.params || 0);
|
| 207 |
+
const nodeHeight = this._getNodeHeight(step.params || 0);
|
| 208 |
+
const hasSubsteps = !!(step.substeps && step.substeps.length > 0);
|
| 209 |
+
const isExpanded = hasSubsteps && step._collapsed === false;
|
| 210 |
+
const x = parentCenterX - nodeWidth / 2;
|
| 211 |
+
|
| 212 |
+
const node = {
|
| 213 |
+
data: step,
|
| 214 |
+
x: x,
|
| 215 |
+
y: y,
|
| 216 |
+
width: nodeWidth,
|
| 217 |
+
height: nodeHeight,
|
| 218 |
+
hasSubsteps: hasSubsteps,
|
| 219 |
+
collapsed: !isExpanded
|
| 220 |
+
};
|
| 221 |
+
|
| 222 |
+
nodes.push(node);
|
| 223 |
+
|
| 224 |
+
for (const prev of prevNodes) {
|
| 225 |
+
arrows.push({ source: prev, target: node });
|
| 226 |
+
}
|
| 227 |
+
|
| 228 |
+
y += nodeHeight;
|
| 229 |
+
|
| 230 |
+
if (isExpanded) {
|
| 231 |
+
const containerStartY = y + this.containerGap;
|
| 232 |
+
const childrenMaxWidth = Math.max(...step.substeps.map(s => this._calcStepWidth(s)));
|
| 233 |
+
const containerWidth = Math.max(nodeWidth, childrenMaxWidth) + this.containerPadding * 2;
|
| 234 |
+
const containerX = parentCenterX - containerWidth / 2;
|
| 235 |
+
|
| 236 |
+
const childResult = layoutSteps(
|
| 237 |
+
step.substeps,
|
| 238 |
+
containerStartY + this.containerPadding,
|
| 239 |
+
parentCenterX
|
| 240 |
+
);
|
| 241 |
+
|
| 242 |
+
const containerEndY = childResult.endY + this.containerPadding - this.nodeGap;
|
| 243 |
+
|
| 244 |
+
containers.push({
|
| 245 |
+
x: containerX,
|
| 246 |
+
y: containerStartY,
|
| 247 |
+
width: containerWidth,
|
| 248 |
+
height: containerEndY - containerStartY,
|
| 249 |
+
color: this.getColor(step.type)
|
| 250 |
+
});
|
| 251 |
+
|
| 252 |
+
const firstChild = nodes.find(n => n.data === step.substeps[0]);
|
| 253 |
+
if (firstChild) {
|
| 254 |
+
arrows.push({ source: node, target: firstChild });
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
y = containerEndY + this.containerGap;
|
| 258 |
+
prevNodes = [{
|
| 259 |
+
isContainerBottom: true,
|
| 260 |
+
containerBottomY: containerEndY,
|
| 261 |
+
centerX: parentCenterX
|
| 262 |
+
}];
|
| 263 |
+
} else {
|
| 264 |
+
y += this.nodeGap;
|
| 265 |
+
prevNodes = [node];
|
| 266 |
+
}
|
| 267 |
+
}
|
| 268 |
+
|
| 269 |
+
return { endY: y, lastNodes: prevNodes };
|
| 270 |
+
};
|
| 271 |
+
|
| 272 |
+
layoutSteps(data.steps || [], this.padding, centerX);
|
| 273 |
+
|
| 274 |
+
return { nodes, arrows, containers };
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
_render() {
|
| 278 |
+
if (!this.pipelineData) return;
|
| 279 |
+
|
| 280 |
+
const { nodes, arrows, containers } = this._layoutPipeline(this.pipelineData);
|
| 281 |
+
const arrowId = `arrow-${this.containerId}`;
|
| 282 |
+
|
| 283 |
+
// Containers
|
| 284 |
+
this.containersGroup.selectAll('.container-box').remove();
|
| 285 |
+
containers.forEach(container => {
|
| 286 |
+
this.containersGroup.append('rect')
|
| 287 |
+
.attr('class', 'container-box')
|
| 288 |
+
.attr('x', container.x)
|
| 289 |
+
.attr('y', container.y)
|
| 290 |
+
.attr('width', container.width)
|
| 291 |
+
.attr('height', container.height)
|
| 292 |
+
.attr('rx', 6)
|
| 293 |
+
.attr('ry', 6)
|
| 294 |
+
.attr('fill', 'rgba(24, 24, 27, 0.6)')
|
| 295 |
+
.attr('stroke', container.color)
|
| 296 |
+
.attr('stroke-width', 1)
|
| 297 |
+
.attr('stroke-opacity', 0.3);
|
| 298 |
+
});
|
| 299 |
+
|
| 300 |
+
// Arrows
|
| 301 |
+
this.arrowsGroup.selectAll('.arrow').remove();
|
| 302 |
+
arrows.forEach(arrow => {
|
| 303 |
+
let sx, sy;
|
| 304 |
+
if (arrow.source.isContainerBottom) {
|
| 305 |
+
sx = arrow.source.centerX;
|
| 306 |
+
sy = arrow.source.containerBottomY;
|
| 307 |
+
} else {
|
| 308 |
+
sx = arrow.source.x + arrow.source.width / 2;
|
| 309 |
+
sy = arrow.source.y + arrow.source.height;
|
| 310 |
+
}
|
| 311 |
+
|
| 312 |
+
const tx = arrow.target.x + arrow.target.width / 2;
|
| 313 |
+
const ty = arrow.target.y;
|
| 314 |
+
|
| 315 |
+
if (Math.abs(sx - tx) > 5) {
|
| 316 |
+
const midY = (sy + ty) / 2;
|
| 317 |
+
this.arrowsGroup.append('path')
|
| 318 |
+
.attr('class', 'arrow')
|
| 319 |
+
.attr('d', `M${sx},${sy} C${sx},${midY} ${tx},${midY} ${tx},${ty - 3}`)
|
| 320 |
+
.attr('fill', 'none')
|
| 321 |
+
.attr('stroke', '#3f3f46')
|
| 322 |
+
.attr('stroke-width', 1.5)
|
| 323 |
+
.attr('marker-end', `url(#${arrowId})`);
|
| 324 |
+
} else {
|
| 325 |
+
this.arrowsGroup.append('line')
|
| 326 |
+
.attr('class', 'arrow')
|
| 327 |
+
.attr('x1', sx)
|
| 328 |
+
.attr('y1', sy)
|
| 329 |
+
.attr('x2', tx)
|
| 330 |
+
.attr('y2', ty - 3)
|
| 331 |
+
.attr('stroke', '#3f3f46')
|
| 332 |
+
.attr('stroke-width', 1.5)
|
| 333 |
+
.attr('marker-end', `url(#${arrowId})`);
|
| 334 |
+
}
|
| 335 |
+
});
|
| 336 |
+
|
| 337 |
+
// Nodes
|
| 338 |
+
const nodeGroups = this.nodesGroup.selectAll('.node')
|
| 339 |
+
.data(nodes, (d, i) => d.data.name + '-' + i)
|
| 340 |
+
.join('g')
|
| 341 |
+
.attr('class', 'node')
|
| 342 |
+
.attr('transform', d => `translate(${d.x}, ${d.y})`)
|
| 343 |
+
.style('cursor', d => d.hasSubsteps ? 'pointer' : 'default');
|
| 344 |
+
|
| 345 |
+
nodeGroups.selectAll('rect')
|
| 346 |
+
.data(d => [d])
|
| 347 |
+
.join('rect')
|
| 348 |
+
.attr('width', d => d.width)
|
| 349 |
+
.attr('height', d => d.height)
|
| 350 |
+
.attr('rx', 6)
|
| 351 |
+
.attr('ry', 6)
|
| 352 |
+
.attr('fill', d => this.getColor(d.data.type))
|
| 353 |
+
.attr('stroke', d => d.hasSubsteps && d.collapsed ? 'rgba(255,255,255,0.25)' : 'none')
|
| 354 |
+
.attr('stroke-width', 1)
|
| 355 |
+
.attr('stroke-dasharray', d => d.hasSubsteps && d.collapsed ? '3,2' : 'none');
|
| 356 |
+
|
| 357 |
+
// Name
|
| 358 |
+
nodeGroups.selectAll('.node-name')
|
| 359 |
+
.data(d => [d])
|
| 360 |
+
.join('text')
|
| 361 |
+
.attr('class', 'node-name')
|
| 362 |
+
.attr('x', d => d.width / 2)
|
| 363 |
+
.attr('y', d => d.data.shape ? 13 : (d.height / 2 - 3))
|
| 364 |
+
.attr('text-anchor', 'middle')
|
| 365 |
+
.attr('fill', 'white')
|
| 366 |
+
.attr('font-size', '10px')
|
| 367 |
+
.attr('font-weight', '600')
|
| 368 |
+
.attr('font-family', '-apple-system, BlinkMacSystemFont, sans-serif')
|
| 369 |
+
.text(d => {
|
| 370 |
+
let name = d.data.name;
|
| 371 |
+
if (d.data.count) name += ` (${d.data.count}x)`;
|
| 372 |
+
return name;
|
| 373 |
+
})
|
| 374 |
+
.each(function(d) {
|
| 375 |
+
const textEl = d3.select(this);
|
| 376 |
+
let text = textEl.text();
|
| 377 |
+
const maxWidth = d.width - 20;
|
| 378 |
+
while (this.getComputedTextLength() > maxWidth && text.length > 0) {
|
| 379 |
+
text = text.slice(0, -1);
|
| 380 |
+
textEl.text(text + '...');
|
| 381 |
+
}
|
| 382 |
+
});
|
| 383 |
+
|
| 384 |
+
// Shape
|
| 385 |
+
nodeGroups.selectAll('.node-shape')
|
| 386 |
+
.data(d => d.data.shape ? [d] : [])
|
| 387 |
+
.join('text')
|
| 388 |
+
.attr('class', 'node-shape')
|
| 389 |
+
.attr('x', d => d.width / 2)
|
| 390 |
+
.attr('y', d => d.height / 2)
|
| 391 |
+
.attr('text-anchor', 'middle')
|
| 392 |
+
.attr('fill', 'rgba(255,255,255,0.7)')
|
| 393 |
+
.attr('font-size', '8px')
|
| 394 |
+
.attr('font-family', 'SF Mono, monospace')
|
| 395 |
+
.text(d => d.data.shape)
|
| 396 |
+
.each(function(d) {
|
| 397 |
+
const textEl = d3.select(this);
|
| 398 |
+
let text = textEl.text();
|
| 399 |
+
const maxWidth = d.width - 14;
|
| 400 |
+
while (this.getComputedTextLength() > maxWidth && text.length > 0) {
|
| 401 |
+
text = text.slice(0, -1);
|
| 402 |
+
textEl.text(text + '...');
|
| 403 |
+
}
|
| 404 |
+
});
|
| 405 |
+
|
| 406 |
+
// Params
|
| 407 |
+
nodeGroups.selectAll('.node-params')
|
| 408 |
+
.data(d => [d])
|
| 409 |
+
.join('text')
|
| 410 |
+
.attr('class', 'node-params')
|
| 411 |
+
.attr('x', d => d.width / 2)
|
| 412 |
+
.attr('y', d => d.data.shape ? d.height - 7 : (d.height / 2 + 10))
|
| 413 |
+
.attr('text-anchor', 'middle')
|
| 414 |
+
.attr('fill', 'rgba(255,255,255,0.5)')
|
| 415 |
+
.attr('font-size', '9px')
|
| 416 |
+
.attr('font-family', 'SF Mono, monospace')
|
| 417 |
+
.text(d => API.formatParams(d.data.params || 0));
|
| 418 |
+
|
| 419 |
+
// Expand indicator
|
| 420 |
+
nodeGroups.selectAll('.expand-indicator')
|
| 421 |
+
.data(d => d.hasSubsteps ? [d] : [])
|
| 422 |
+
.join('text')
|
| 423 |
+
.attr('class', 'expand-indicator')
|
| 424 |
+
.attr('x', d => d.width - 10)
|
| 425 |
+
.attr('y', 12)
|
| 426 |
+
.attr('fill', 'rgba(255,255,255,0.5)')
|
| 427 |
+
.attr('font-size', '8px')
|
| 428 |
+
.text(d => d.collapsed ? '+' : '-');
|
| 429 |
+
|
| 430 |
+
nodeGroups
|
| 431 |
+
.on('click', (event, d) => this._handleClick(event, d))
|
| 432 |
+
.on('mouseenter', (event, d) => this._handleMouseEnter(event, d))
|
| 433 |
+
.on('mouseleave', (event, d) => this._handleMouseLeave(event, d));
|
| 434 |
+
|
| 435 |
+
this._fitView(nodes, containers);
|
| 436 |
+
}
|
| 437 |
+
|
| 438 |
+
_fitView(nodes, containers) {
|
| 439 |
+
if (nodes.length === 0) return;
|
| 440 |
+
|
| 441 |
+
const rect = this.container.getBoundingClientRect();
|
| 442 |
+
const padding = 32;
|
| 443 |
+
|
| 444 |
+
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
| 445 |
+
|
| 446 |
+
for (const node of nodes) {
|
| 447 |
+
minX = Math.min(minX, node.x);
|
| 448 |
+
minY = Math.min(minY, node.y);
|
| 449 |
+
maxX = Math.max(maxX, node.x + node.width);
|
| 450 |
+
maxY = Math.max(maxY, node.y + node.height);
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
for (const container of containers) {
|
| 454 |
+
minX = Math.min(minX, container.x);
|
| 455 |
+
minY = Math.min(minY, container.y);
|
| 456 |
+
maxX = Math.max(maxX, container.x + container.width);
|
| 457 |
+
maxY = Math.max(maxY, container.y + container.height);
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
const contentWidth = maxX - minX + padding * 2;
|
| 461 |
+
const contentHeight = maxY - minY + padding * 2;
|
| 462 |
+
|
| 463 |
+
const scaleX = rect.width / contentWidth;
|
| 464 |
+
const scaleY = rect.height / contentHeight;
|
| 465 |
+
const scale = Math.min(scaleX, scaleY, 1.5);
|
| 466 |
+
|
| 467 |
+
const translateX = (rect.width - contentWidth * scale) / 2 - minX * scale + padding * scale;
|
| 468 |
+
const translateY = (rect.height - contentHeight * scale) / 2 - minY * scale + padding * scale;
|
| 469 |
+
|
| 470 |
+
this.svg.transition().duration(250).call(
|
| 471 |
+
this.zoom.transform,
|
| 472 |
+
d3.zoomIdentity.translate(translateX, translateY).scale(scale)
|
| 473 |
+
);
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
_handleClick(event, d) {
|
| 477 |
+
event.stopPropagation();
|
| 478 |
+
if (d.hasSubsteps) {
|
| 479 |
+
d.data._collapsed = d.data._collapsed === false ? true : false;
|
| 480 |
+
this._render();
|
| 481 |
+
}
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
_handleMouseEnter(event, d) {
|
| 485 |
+
this._showTooltip(event, d);
|
| 486 |
+
}
|
| 487 |
+
|
| 488 |
+
_handleMouseLeave(event, d) {
|
| 489 |
+
this._hideTooltip();
|
| 490 |
+
}
|
| 491 |
+
|
| 492 |
+
_showTooltip(event, d) {
|
| 493 |
+
const tooltip = document.getElementById('tooltip');
|
| 494 |
+
const params = d.data.params || 0;
|
| 495 |
+
const refParams = this.globalMaxParams || this.totalParams;
|
| 496 |
+
const percent = refParams > 0 ? ((params / refParams) * 100).toFixed(1) : '0';
|
| 497 |
+
|
| 498 |
+
let extra = '';
|
| 499 |
+
if (d.data.shape) {
|
| 500 |
+
extra += `<div class="tooltip-row"><span class="tooltip-label">Shape</span><span class="tooltip-value">${d.data.shape}</span></div>`;
|
| 501 |
+
}
|
| 502 |
+
if (d.data.count) {
|
| 503 |
+
extra += `<div class="tooltip-row"><span class="tooltip-label">Layers</span><span class="tooltip-value">${d.data.count}</span></div>`;
|
| 504 |
+
}
|
| 505 |
+
if (d.data.substeps && d.data.substeps.length > 0) {
|
| 506 |
+
extra += `<div class="tooltip-row"><span class="tooltip-label">Components</span><span class="tooltip-value">${d.data.substeps.length}</span></div>`;
|
| 507 |
+
}
|
| 508 |
+
|
| 509 |
+
tooltip.innerHTML = `
|
| 510 |
+
<div class="tooltip-title">${d.data.name}</div>
|
| 511 |
+
<div class="tooltip-content">
|
| 512 |
+
<div class="tooltip-row"><span class="tooltip-label">Parameters</span><span class="tooltip-value">${API.formatParams(params)}</span></div>
|
| 513 |
+
<div class="tooltip-row"><span class="tooltip-label">Proportion</span><span class="tooltip-value">${percent}%</span></div>
|
| 514 |
+
<div class="tooltip-row"><span class="tooltip-label">Type</span><span class="tooltip-value">${d.data.type}</span></div>
|
| 515 |
+
${extra}
|
| 516 |
+
</div>
|
| 517 |
+
`;
|
| 518 |
+
|
| 519 |
+
tooltip.classList.remove('hidden');
|
| 520 |
+
|
| 521 |
+
const pad = 10;
|
| 522 |
+
let x = event.clientX + pad;
|
| 523 |
+
let y = event.clientY + pad;
|
| 524 |
+
|
| 525 |
+
const tooltipRect = tooltip.getBoundingClientRect();
|
| 526 |
+
if (x + tooltipRect.width > window.innerWidth) x = event.clientX - tooltipRect.width - pad;
|
| 527 |
+
if (y + tooltipRect.height > window.innerHeight) y = event.clientY - tooltipRect.height - pad;
|
| 528 |
+
|
| 529 |
+
tooltip.style.left = `${x}px`;
|
| 530 |
+
tooltip.style.top = `${y}px`;
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
_hideTooltip() {
|
| 534 |
+
document.getElementById('tooltip').classList.add('hidden');
|
| 535 |
+
}
|
| 536 |
+
|
| 537 |
+
getLegendItems() {
|
| 538 |
+
if (!this.pipelineData || !this.pipelineData.steps) return [];
|
| 539 |
+
|
| 540 |
+
const types = new Set();
|
| 541 |
+
const collect = (steps) => {
|
| 542 |
+
if (!steps) return;
|
| 543 |
+
for (const step of steps) {
|
| 544 |
+
if (step.type) types.add(step.type);
|
| 545 |
+
if (step.substeps) collect(step.substeps);
|
| 546 |
+
if (step.branches) {
|
| 547 |
+
for (const branch of step.branches) {
|
| 548 |
+
if (branch.type) types.add(branch.type);
|
| 549 |
+
if (branch.substeps) collect(branch.substeps);
|
| 550 |
+
}
|
| 551 |
+
}
|
| 552 |
+
}
|
| 553 |
+
};
|
| 554 |
+
collect(this.pipelineData.steps);
|
| 555 |
+
|
| 556 |
+
return Array.from(types).map(type => ({ type, color: this.getColor(type) }));
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
+
/**
|
| 560 |
+
* Export SVG with watermark
|
| 561 |
+
*/
|
| 562 |
+
exportSVG(modelName) {
|
| 563 |
+
const svgElement = this.svg.node();
|
| 564 |
+
const clone = svgElement.cloneNode(true);
|
| 565 |
+
|
| 566 |
+
// Get bounds
|
| 567 |
+
const rect = this.container.getBoundingClientRect();
|
| 568 |
+
clone.setAttribute('width', rect.width);
|
| 569 |
+
clone.setAttribute('height', rect.height);
|
| 570 |
+
clone.setAttribute('viewBox', `0 0 ${rect.width} ${rect.height}`);
|
| 571 |
+
|
| 572 |
+
// Add background
|
| 573 |
+
const bg = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
| 574 |
+
bg.setAttribute('width', '100%');
|
| 575 |
+
bg.setAttribute('height', '100%');
|
| 576 |
+
bg.setAttribute('fill', '#0a0a0b');
|
| 577 |
+
clone.insertBefore(bg, clone.firstChild);
|
| 578 |
+
|
| 579 |
+
// Add watermark
|
| 580 |
+
const watermark = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
| 581 |
+
watermark.setAttribute('x', rect.width - 10);
|
| 582 |
+
watermark.setAttribute('y', rect.height - 10);
|
| 583 |
+
watermark.setAttribute('text-anchor', 'end');
|
| 584 |
+
watermark.setAttribute('fill', '#3f3f46');
|
| 585 |
+
watermark.setAttribute('font-size', '11');
|
| 586 |
+
watermark.setAttribute('font-family', '-apple-system, sans-serif');
|
| 587 |
+
watermark.textContent = 'omarkamali.com/llmscope';
|
| 588 |
+
clone.appendChild(watermark);
|
| 589 |
+
|
| 590 |
+
// Add model name
|
| 591 |
+
if (modelName) {
|
| 592 |
+
const title = document.createElementNS('http://www.w3.org/2000/svg', 'text');
|
| 593 |
+
title.setAttribute('x', '10');
|
| 594 |
+
title.setAttribute('y', '20');
|
| 595 |
+
title.setAttribute('fill', '#fafafa');
|
| 596 |
+
title.setAttribute('font-size', '12');
|
| 597 |
+
title.setAttribute('font-weight', '600');
|
| 598 |
+
title.setAttribute('font-family', '-apple-system, sans-serif');
|
| 599 |
+
title.textContent = modelName;
|
| 600 |
+
clone.appendChild(title);
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
const serializer = new XMLSerializer();
|
| 604 |
+
const svgString = serializer.serializeToString(clone);
|
| 605 |
+
const blob = new Blob([svgString], { type: 'image/svg+xml' });
|
| 606 |
+
|
| 607 |
+
return blob;
|
| 608 |
+
}
|
| 609 |
+
|
| 610 |
+
resetZoom() {
|
| 611 |
+
if (this.pipelineData) {
|
| 612 |
+
const expandAll = (steps) => {
|
| 613 |
+
if (!steps) return;
|
| 614 |
+
for (const step of steps) {
|
| 615 |
+
step._collapsed = false;
|
| 616 |
+
if (step.substeps) expandAll(step.substeps);
|
| 617 |
+
if (step.branches) {
|
| 618 |
+
for (const branch of step.branches) {
|
| 619 |
+
branch._collapsed = false;
|
| 620 |
+
if (branch.substeps) expandAll(branch.substeps);
|
| 621 |
+
}
|
| 622 |
+
}
|
| 623 |
+
}
|
| 624 |
+
};
|
| 625 |
+
if (this.pipelineData.steps) {
|
| 626 |
+
expandAll(this.pipelineData.steps);
|
| 627 |
+
}
|
| 628 |
+
this._render();
|
| 629 |
+
}
|
| 630 |
+
}
|
| 631 |
+
|
| 632 |
+
destroy() {
|
| 633 |
+
if (this.resizeObserver) this.resizeObserver.disconnect();
|
| 634 |
+
this.svg.remove();
|
| 635 |
+
}
|
| 636 |
+
}
|
| 637 |
+
|
| 638 |
+
const TreemapViz = ModelTreeViz;
|