Spaces:
Running
Running
Deploy ICA explorer app
Browse files- .dockerignore +6 -0
- Dockerfile +19 -0
- README.md +2 -2
- configs/comparisons/gemma2_2b.toml +76 -0
- configs/comparisons/gpt2.toml +46 -0
- configs/comparisons/qwen3_5_2b_base.toml +49 -0
- requirements.txt +9 -0
- server/README.md +40 -0
- server/__init__.py +0 -0
- server/app.py +816 -0
- server/artifacts.py +73 -0
- server/config.py +119 -0
- server/model_runtime.py +62 -0
- server/probe.py +242 -0
- server/sae_probe.py +285 -0
- server/static/annotate.html +220 -0
- server/static/component.html +373 -0
- server/static/index.html +1546 -0
- server/static/random_components.html +163 -0
- server/static/sae_explorer.html +673 -0
- server/static/stats.html +557 -0
- server/store.py +772 -0
.dockerignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.git
|
| 2 |
+
__pycache__
|
| 3 |
+
*.pyc
|
| 4 |
+
.pytest_cache
|
| 5 |
+
.venv
|
| 6 |
+
artifacts/fetched/.cache
|
Dockerfile
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.12-slim
|
| 2 |
+
|
| 3 |
+
ENV PYTHONDONTWRITEBYTECODE=1 \
|
| 4 |
+
PYTHONUNBUFFERED=1 \
|
| 5 |
+
ICA_EXPLORER_MODEL_NAME=gpt2 \
|
| 6 |
+
ICA_EXPLORER_ENABLED_MODELS=gpt2 \
|
| 7 |
+
ICA_EXPLORER_DEVICE=cpu \
|
| 8 |
+
ICA_EXPLORER_DTYPE=float32
|
| 9 |
+
|
| 10 |
+
WORKDIR /app
|
| 11 |
+
|
| 12 |
+
COPY requirements.txt .
|
| 13 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 14 |
+
|
| 15 |
+
COPY . .
|
| 16 |
+
|
| 17 |
+
EXPOSE 7860
|
| 18 |
+
|
| 19 |
+
CMD ["python", "-m", "server.app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
CHANGED
|
@@ -3,8 +3,8 @@ title: ICAExplorer
|
|
| 3 |
emoji: 📈
|
| 4 |
colorFrom: red
|
| 5 |
colorTo: gray
|
| 6 |
-
sdk:
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
| 3 |
emoji: 📈
|
| 4 |
colorFrom: red
|
| 5 |
colorTo: gray
|
| 6 |
+
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
+
ICA Lens Explorer Space.
|
configs/comparisons/gemma2_2b.toml
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[model]
|
| 2 |
+
short_name = "gemma2_2b"
|
| 3 |
+
hidden_size = 2304
|
| 4 |
+
num_hidden_layers = 26
|
| 5 |
+
|
| 6 |
+
[ica]
|
| 7 |
+
artifact_set = "v3_frozen_ica"
|
| 8 |
+
artifact_root = "artifacts/fetched/models"
|
| 9 |
+
preprocess = "with_normalization"
|
| 10 |
+
|
| 11 |
+
[sae]
|
| 12 |
+
repo_id = "google/gemma-scope-2b-pt-res"
|
| 13 |
+
checkpoint_format = "npz"
|
| 14 |
+
hook_name_template = "model.layers.{layer}"
|
| 15 |
+
layers = "all_transformer_layers"
|
| 16 |
+
width = 16384
|
| 17 |
+
direction_source = "decoder"
|
| 18 |
+
decoder_key = "W_dec"
|
| 19 |
+
|
| 20 |
+
# Gemma Scope stores concrete average_l0 folders rather than stable canonical files.
|
| 21 |
+
# These width_16k residual SAEs match the v3 comparison configuration.
|
| 22 |
+
[sae.layer_checkpoints]
|
| 23 |
+
"0" = "layer_0/width_16k/average_l0_25/params.npz"
|
| 24 |
+
"1" = "layer_1/width_16k/average_l0_20/params.npz"
|
| 25 |
+
"2" = "layer_2/width_16k/average_l0_24/params.npz"
|
| 26 |
+
"3" = "layer_3/width_16k/average_l0_28/params.npz"
|
| 27 |
+
"4" = "layer_4/width_16k/average_l0_31/params.npz"
|
| 28 |
+
"5" = "layer_5/width_16k/average_l0_34/params.npz"
|
| 29 |
+
"6" = "layer_6/width_16k/average_l0_36/params.npz"
|
| 30 |
+
"7" = "layer_7/width_16k/average_l0_36/params.npz"
|
| 31 |
+
"8" = "layer_8/width_16k/average_l0_37/params.npz"
|
| 32 |
+
"9" = "layer_9/width_16k/average_l0_37/params.npz"
|
| 33 |
+
"10" = "layer_10/width_16k/average_l0_39/params.npz"
|
| 34 |
+
"11" = "layer_11/width_16k/average_l0_41/params.npz"
|
| 35 |
+
"12" = "layer_12/width_16k/average_l0_41/params.npz"
|
| 36 |
+
"13" = "layer_13/width_16k/average_l0_43/params.npz"
|
| 37 |
+
"14" = "layer_14/width_16k/average_l0_43/params.npz"
|
| 38 |
+
"15" = "layer_15/width_16k/average_l0_41/params.npz"
|
| 39 |
+
"16" = "layer_16/width_16k/average_l0_42/params.npz"
|
| 40 |
+
"17" = "layer_17/width_16k/average_l0_42/params.npz"
|
| 41 |
+
"18" = "layer_18/width_16k/average_l0_40/params.npz"
|
| 42 |
+
"19" = "layer_19/width_16k/average_l0_40/params.npz"
|
| 43 |
+
"20" = "layer_20/width_16k/average_l0_38/params.npz"
|
| 44 |
+
"21" = "layer_21/width_16k/average_l0_38/params.npz"
|
| 45 |
+
"22" = "layer_22/width_16k/average_l0_38/params.npz"
|
| 46 |
+
"23" = "layer_23/width_16k/average_l0_38/params.npz"
|
| 47 |
+
"24" = "layer_24/width_16k/average_l0_38/params.npz"
|
| 48 |
+
"25" = "layer_25/width_16k/average_l0_28/params.npz"
|
| 49 |
+
|
| 50 |
+
[direction_overlap]
|
| 51 |
+
top_k = 1
|
| 52 |
+
sae_chunk_size = 4096
|
| 53 |
+
nearest_metric = "maximum_absolute_cosine"
|
| 54 |
+
|
| 55 |
+
[saebench_tpp]
|
| 56 |
+
enabled = true
|
| 57 |
+
saebench_model_name = "gemma-2-2b"
|
| 58 |
+
hook_name_template = "blocks.{layer}.hook_resid_post"
|
| 59 |
+
llm_dtype = "bfloat16"
|
| 60 |
+
llm_batch_size = 32
|
| 61 |
+
sae_batch_size = 125
|
| 62 |
+
random_seed = 42
|
| 63 |
+
lower_vram_usage = false
|
| 64 |
+
row_normalize = false
|
| 65 |
+
signed_identity = false
|
| 66 |
+
n_values = [2, 5, 10, 20, 50, 100, 500]
|
| 67 |
+
dataset_names = [
|
| 68 |
+
"LabHC/bias_in_bios_class_set1",
|
| 69 |
+
"canrager/amazon_reviews_mcauley_1and5",
|
| 70 |
+
]
|
| 71 |
+
|
| 72 |
+
[saebench_tpp.sae_baseline]
|
| 73 |
+
source = "sae_lens_registry"
|
| 74 |
+
release_pattern = "gemma-scope-2b-pt-res"
|
| 75 |
+
id_from_sae_layer_checkpoints = true
|
| 76 |
+
allow_multiple = false
|
configs/comparisons/gpt2.toml
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[model]
|
| 2 |
+
short_name = "gpt2"
|
| 3 |
+
hidden_size = 768
|
| 4 |
+
num_hidden_layers = 12
|
| 5 |
+
|
| 6 |
+
[ica]
|
| 7 |
+
artifact_set = "v3_frozen_ica"
|
| 8 |
+
artifact_root = "artifacts/fetched/models"
|
| 9 |
+
preprocess = "with_normalization"
|
| 10 |
+
|
| 11 |
+
[sae]
|
| 12 |
+
repo_id = "jbloom/GPT2-Small-OAI-v5-32k-resid-post-SAEs"
|
| 13 |
+
checkpoint_template = "v5_32k_layer_{layer}.pt/sae_weights.safetensors"
|
| 14 |
+
checkpoint_format = "safetensors"
|
| 15 |
+
hook_name_template = "blocks.{layer}.hook_resid_post"
|
| 16 |
+
layers = "all_transformer_layers"
|
| 17 |
+
width = 32768
|
| 18 |
+
direction_source = "decoder"
|
| 19 |
+
|
| 20 |
+
[direction_overlap]
|
| 21 |
+
top_k = 1
|
| 22 |
+
sae_chunk_size = 4096
|
| 23 |
+
nearest_metric = "maximum_absolute_cosine"
|
| 24 |
+
|
| 25 |
+
[saebench_tpp]
|
| 26 |
+
enabled = true
|
| 27 |
+
saebench_model_name = "gpt2-small"
|
| 28 |
+
hook_name_template = "blocks.{layer}.hook_resid_post"
|
| 29 |
+
llm_dtype = "float32"
|
| 30 |
+
llm_batch_size = 256
|
| 31 |
+
sae_batch_size = 250
|
| 32 |
+
random_seed = 42
|
| 33 |
+
lower_vram_usage = false
|
| 34 |
+
row_normalize = false
|
| 35 |
+
signed_identity = false
|
| 36 |
+
n_values = [2, 5, 10, 20, 50, 100, 500]
|
| 37 |
+
dataset_names = [
|
| 38 |
+
"LabHC/bias_in_bios_class_set1",
|
| 39 |
+
"canrager/amazon_reviews_mcauley_1and5",
|
| 40 |
+
]
|
| 41 |
+
|
| 42 |
+
[saebench_tpp.sae_baseline]
|
| 43 |
+
source = "sae_lens_registry"
|
| 44 |
+
release_pattern = "gpt2-small-resid-post-v5-32k"
|
| 45 |
+
id_pattern_template = "blocks\\.{layer}\\.hook_resid_post"
|
| 46 |
+
allow_multiple = false
|
configs/comparisons/qwen3_5_2b_base.toml
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[model]
|
| 2 |
+
short_name = "qwen3_5_2b_base"
|
| 3 |
+
hidden_size = 2048
|
| 4 |
+
num_hidden_layers = 24
|
| 5 |
+
|
| 6 |
+
[ica]
|
| 7 |
+
artifact_set = "v3_frozen_ica"
|
| 8 |
+
artifact_root = "artifacts/fetched/models"
|
| 9 |
+
preprocess = "with_normalization"
|
| 10 |
+
|
| 11 |
+
[sae]
|
| 12 |
+
repo_id = "Qwen/SAE-Res-Qwen3.5-2B-Base-W32K-L0_50"
|
| 13 |
+
activation = "topk"
|
| 14 |
+
checkpoint_template = "layer{layer}.sae.pt"
|
| 15 |
+
checkpoint_format = "torch"
|
| 16 |
+
hook_name_template = "model.layers.{layer}"
|
| 17 |
+
layers = "all_transformer_layers"
|
| 18 |
+
width = 32768
|
| 19 |
+
direction_source = "decoder"
|
| 20 |
+
decoder_key = "W_dec"
|
| 21 |
+
top_k = 50
|
| 22 |
+
|
| 23 |
+
[direction_overlap]
|
| 24 |
+
top_k = 1
|
| 25 |
+
sae_chunk_size = 4096
|
| 26 |
+
nearest_metric = "maximum_absolute_cosine"
|
| 27 |
+
|
| 28 |
+
[saebench_tpp]
|
| 29 |
+
enabled = true
|
| 30 |
+
saebench_model_name = "Qwen/Qwen3.5-2B-Base"
|
| 31 |
+
hook_name_template = "blocks.{layer}.hook_resid_post"
|
| 32 |
+
llm_dtype = "bfloat16"
|
| 33 |
+
llm_batch_size = 16
|
| 34 |
+
sae_batch_size = 125
|
| 35 |
+
random_seed = 42
|
| 36 |
+
lower_vram_usage = false
|
| 37 |
+
row_normalize = false
|
| 38 |
+
signed_identity = false
|
| 39 |
+
n_values = [2, 5, 10, 20, 50, 100, 500]
|
| 40 |
+
dataset_names = [
|
| 41 |
+
"LabHC/bias_in_bios_class_set1",
|
| 42 |
+
"canrager/amazon_reviews_mcauley_1and5",
|
| 43 |
+
]
|
| 44 |
+
|
| 45 |
+
[saebench_tpp.sae_baseline]
|
| 46 |
+
source = "custom_checkpoint"
|
| 47 |
+
release_name = "qwen_scope_res_w32k_l0_50_layer_{layer}"
|
| 48 |
+
hook_name_template = "blocks.{layer}.hook_resid_post"
|
| 49 |
+
allow_multiple = false
|
requirements.txt
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi>=0.115.0
|
| 2 |
+
uvicorn[standard]>=0.30.0
|
| 3 |
+
huggingface-hub>=0.23.0
|
| 4 |
+
pydantic>=2.10.0
|
| 5 |
+
numpy>=1.26.0
|
| 6 |
+
torch>=2.5.0
|
| 7 |
+
transformers>=4.44.0
|
| 8 |
+
datasets>=2.20.0
|
| 9 |
+
safetensors>=0.4.0
|
server/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ICA Lens Explorer and Annotator
|
| 2 |
+
|
| 3 |
+
This server is the public `v6` explorer/annotator for released ICA Lens
|
| 4 |
+
artifacts. It serves component statistics, examples, annotations, random
|
| 5 |
+
component samples, and live text probing when the corresponding model weights
|
| 6 |
+
and ICA artifacts are available.
|
| 7 |
+
|
| 8 |
+
Default local inputs:
|
| 9 |
+
|
| 10 |
+
```text
|
| 11 |
+
artifacts/fetched/models/
|
| 12 |
+
artifacts/fetched/databases/ica_probe_mini.sqlite
|
| 13 |
+
```
|
| 14 |
+
|
| 15 |
+
The fetched mini database is the default server database. Use
|
| 16 |
+
`ICA_EXPLORER_DB_PATH` only when you want to point the explorer at another
|
| 17 |
+
SQLite file, such as `artifacts/fetched/databases/ica_probe_full.sqlite` or a
|
| 18 |
+
locally reproduced demo database.
|
| 19 |
+
|
| 20 |
+
Run from `v6`:
|
| 21 |
+
|
| 22 |
+
```bash
|
| 23 |
+
uv run python -m server.app --port 8001
|
| 24 |
+
```
|
| 25 |
+
|
| 26 |
+
Pages:
|
| 27 |
+
|
| 28 |
+
```text
|
| 29 |
+
/ GPT-2 text probe explorer
|
| 30 |
+
/stats component statistics overview
|
| 31 |
+
/component component examples page
|
| 32 |
+
/annotate manual annotation editor
|
| 33 |
+
/random-components random 05.1 component sample list
|
| 34 |
+
```
|
| 35 |
+
|
| 36 |
+
Set `ICA_EXPLORER_DB_PATH`, `ICA_EXPLORER_ICA_ROOT`,
|
| 37 |
+
`ICA_EXPLORER_ICA_DIR`, `ICA_EXPLORER_DEVICE`, or `ICA_EXPLORER_DTYPE` to
|
| 38 |
+
override defaults. If local inputs are missing, the server attempts to fetch
|
| 39 |
+
released artifacts from the configured Hugging Face dataset unless
|
| 40 |
+
`ICA_EXPLORER_DOWNLOAD_MISSING=0`.
|
server/__init__.py
ADDED
|
File without changes
|
server/app.py
ADDED
|
@@ -0,0 +1,816 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
import gc
|
| 5 |
+
import html
|
| 6 |
+
import json
|
| 7 |
+
import threading
|
| 8 |
+
from contextlib import asynccontextmanager
|
| 9 |
+
from dataclasses import replace
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
from typing import Any
|
| 12 |
+
|
| 13 |
+
from fastapi import FastAPI, HTTPException
|
| 14 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 15 |
+
from fastapi.responses import FileResponse, HTMLResponse
|
| 16 |
+
from fastapi.staticfiles import StaticFiles
|
| 17 |
+
from pydantic import BaseModel, ConfigDict, Field
|
| 18 |
+
|
| 19 |
+
from .artifacts import resolve_db_path, resolve_ica_dir, validate_ica_dir
|
| 20 |
+
from .config import Settings, load_settings
|
| 21 |
+
from .model_runtime import load_model_and_tokenizer, load_tokenizer
|
| 22 |
+
from .probe import _decode_byte_level_token, fastica_artifact_path, interpret_text_probe, list_ica_layer_keys
|
| 23 |
+
from .sae_probe import interpret_text_sae_probe, list_sae_layers, load_sae_config
|
| 24 |
+
from .store import (
|
| 25 |
+
connect,
|
| 26 |
+
get_annotation,
|
| 27 |
+
get_component_row,
|
| 28 |
+
get_examples_by_region,
|
| 29 |
+
infer_default_annotation_sign,
|
| 30 |
+
init_db,
|
| 31 |
+
list_annotated_components,
|
| 32 |
+
list_component_example_details,
|
| 33 |
+
list_component_examples,
|
| 34 |
+
list_component_metadata,
|
| 35 |
+
list_component_neighbors,
|
| 36 |
+
list_component_stats,
|
| 37 |
+
list_components,
|
| 38 |
+
list_chosen_random_components,
|
| 39 |
+
list_layers,
|
| 40 |
+
list_models,
|
| 41 |
+
pick_default_region,
|
| 42 |
+
search_components,
|
| 43 |
+
update_annotation,
|
| 44 |
+
validate_db,
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
STATIC_DIR = Path(__file__).resolve().parent / "static"
|
| 49 |
+
V6_ROOT = Path(__file__).resolve().parents[1]
|
| 50 |
+
GPT2_LAYER11_PATCH_ROOT = V6_ROOT / "patch_gpt2_layer11"
|
| 51 |
+
GPT2_LAYER11_PATCH_DB = GPT2_LAYER11_PATCH_ROOT / "data" / "server" / "db" / "ica_probe_gpt2_layer11_patch.sqlite"
|
| 52 |
+
GPT2_LAYER11_PATCH_ICA_ROOT = GPT2_LAYER11_PATCH_ROOT / "data" / "ica"
|
| 53 |
+
ANNOTATION_TYPES = ["Form", "Word", "Phrase", "Sentence", "Long-Range Context", "Global", "Position", "Sophisticated"]
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
class ProbeRequest(BaseModel):
|
| 57 |
+
text: str = Field(..., min_length=1, max_length=100_000)
|
| 58 |
+
model_name: str = "gpt2"
|
| 59 |
+
layer: str
|
| 60 |
+
top_k: int = Field(5, ge=1, le=32)
|
| 61 |
+
highlights: list[int] = Field(default_factory=list, max_length=256)
|
| 62 |
+
max_length: int | None = Field(None, ge=16, le=8192)
|
| 63 |
+
keep_models: bool = False
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class SaeProbeRequest(BaseModel):
|
| 67 |
+
text: str = Field(..., min_length=1, max_length=100_000)
|
| 68 |
+
model_name: str = "gpt2"
|
| 69 |
+
layer: str
|
| 70 |
+
top_k: int = Field(5, ge=1, le=128)
|
| 71 |
+
max_length: int | None = Field(None, ge=16, le=8192)
|
| 72 |
+
keep_models: bool = False
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
class AnnotationUpdate(BaseModel):
|
| 76 |
+
model_config = ConfigDict(extra="forbid")
|
| 77 |
+
|
| 78 |
+
model_name: str
|
| 79 |
+
layer: str
|
| 80 |
+
component: int
|
| 81 |
+
positive_label: str = ""
|
| 82 |
+
positive_confidence: str = "unclear"
|
| 83 |
+
positive_interpretation_types: list[str] = Field(default_factory=list)
|
| 84 |
+
negative_label: str = ""
|
| 85 |
+
negative_confidence: str = "unclear"
|
| 86 |
+
negative_interpretation_types: list[str] = Field(default_factory=list)
|
| 87 |
+
summary: str = ""
|
| 88 |
+
notes: str = ""
|
| 89 |
+
include_as_case_study: bool = False
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def create_app(settings: Settings | None = None) -> FastAPI:
|
| 93 |
+
settings = settings or load_settings()
|
| 94 |
+
db_path = resolve_db_path(settings)
|
| 95 |
+
ica_dirs = {}
|
| 96 |
+
for model_name, model_settings in settings.models.items():
|
| 97 |
+
try:
|
| 98 |
+
ica_dirs[model_name] = resolve_ica_dir(settings, model_name=model_name, ica_dir=model_settings.ica_dir)
|
| 99 |
+
except FileNotFoundError:
|
| 100 |
+
continue
|
| 101 |
+
if not ica_dirs:
|
| 102 |
+
raise FileNotFoundError("No ICA artifact directories are available for the configured models.")
|
| 103 |
+
|
| 104 |
+
@asynccontextmanager
|
| 105 |
+
async def lifespan(app: FastAPI):
|
| 106 |
+
conn = connect(db_path)
|
| 107 |
+
init_db(conn)
|
| 108 |
+
validate_db(conn, None)
|
| 109 |
+
app.state.conn = conn
|
| 110 |
+
app.state.settings = settings
|
| 111 |
+
app.state.ica_dirs = ica_dirs
|
| 112 |
+
app.state.ica_dir = ica_dirs.get(settings.model_name, next(iter(ica_dirs.values())))
|
| 113 |
+
app.state.use_gpt2_layer11_patch = bool(getattr(settings, "use_gpt2_layer11_patch", False)) or _db_marks_gpt2_layer11_raw_hook(conn)
|
| 114 |
+
app.state.db_lock = threading.RLock()
|
| 115 |
+
app.state.runtime_lock = threading.Lock()
|
| 116 |
+
app.state.runtimes = {}
|
| 117 |
+
app.state.runtime = None
|
| 118 |
+
app.state.runtime_key = None
|
| 119 |
+
app.state.full_context_cache = {}
|
| 120 |
+
try:
|
| 121 |
+
yield
|
| 122 |
+
finally:
|
| 123 |
+
app.state.runtimes.clear()
|
| 124 |
+
app.state.runtime = None
|
| 125 |
+
app.state.runtime_key = None
|
| 126 |
+
conn.close()
|
| 127 |
+
_collect_memory()
|
| 128 |
+
|
| 129 |
+
app = FastAPI(title="ICA Lens Explorer", lifespan=lifespan)
|
| 130 |
+
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
|
| 131 |
+
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
|
| 132 |
+
|
| 133 |
+
@app.get("/api/health")
|
| 134 |
+
def health() -> dict[str, str]:
|
| 135 |
+
return {"status": "ok"}
|
| 136 |
+
|
| 137 |
+
@app.get("/api/meta")
|
| 138 |
+
def meta(model: str | None = None) -> dict[str, Any]:
|
| 139 |
+
model_name = model or app.state.settings.model_name
|
| 140 |
+
return _model_meta(app, model_name)
|
| 141 |
+
|
| 142 |
+
@app.post("/api/probe")
|
| 143 |
+
def probe(body: ProbeRequest) -> dict[str, Any]:
|
| 144 |
+
model_settings = _model_settings(app, body.model_name)
|
| 145 |
+
ica_dir = app.state.ica_dirs.get(model_settings.model_name)
|
| 146 |
+
if ica_dir is None:
|
| 147 |
+
raise HTTPException(status_code=404, detail=f"No ICA artifacts for model {model_settings.model_name!r}")
|
| 148 |
+
artifact = fastica_artifact_path(ica_dir, body.layer)
|
| 149 |
+
if not artifact.is_file():
|
| 150 |
+
raise HTTPException(status_code=404, detail=f"No ICA artifact for {model_settings.model_name!r} layer {body.layer!r}")
|
| 151 |
+
try:
|
| 152 |
+
with app.state.runtime_lock:
|
| 153 |
+
runtimes = app.state.runtimes
|
| 154 |
+
if not body.keep_models:
|
| 155 |
+
for loaded_name in list(runtimes):
|
| 156 |
+
if loaded_name != model_settings.model_name:
|
| 157 |
+
del runtimes[loaded_name]
|
| 158 |
+
_collect_memory()
|
| 159 |
+
runtime = runtimes.get(model_settings.model_name)
|
| 160 |
+
if runtime is None:
|
| 161 |
+
runtime = load_model_and_tokenizer(
|
| 162 |
+
model_settings.model_id,
|
| 163 |
+
device=app.state.settings.device,
|
| 164 |
+
dtype=model_settings.dtype,
|
| 165 |
+
)
|
| 166 |
+
runtimes[model_settings.model_name] = runtime
|
| 167 |
+
app.state.runtime = runtime
|
| 168 |
+
app.state.runtime_key = model_settings.model_name
|
| 169 |
+
model, tokenizer = runtime
|
| 170 |
+
result = interpret_text_probe(
|
| 171 |
+
model=model,
|
| 172 |
+
tokenizer=tokenizer,
|
| 173 |
+
text=body.text,
|
| 174 |
+
layer=body.layer,
|
| 175 |
+
ica_artifact_path=artifact,
|
| 176 |
+
top_k=body.top_k,
|
| 177 |
+
highlight_components=body.highlights,
|
| 178 |
+
max_length=body.max_length or model_settings.context_length,
|
| 179 |
+
raw_gpt2_block_index=_raw_gpt2_block_index_for_probe(app, model_settings.model_name, body.layer),
|
| 180 |
+
)
|
| 181 |
+
except ValueError as exc:
|
| 182 |
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 183 |
+
except RuntimeError as exc:
|
| 184 |
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
| 185 |
+
components = sorted({int(pair["component"]) for token in result.get("tokens", []) for pair in token.get("top", [])})
|
| 186 |
+
with app.state.db_lock:
|
| 187 |
+
annotated = list_component_metadata(app.state.conn, model_settings.model_name, body.layer, components)
|
| 188 |
+
return {**result, "annotated_components": annotated}
|
| 189 |
+
|
| 190 |
+
@app.get("/api/sae-meta")
|
| 191 |
+
def sae_meta(model: str | None = None) -> dict[str, Any]:
|
| 192 |
+
model_name = model or app.state.settings.model_name
|
| 193 |
+
model_settings = _model_settings(app, model_name)
|
| 194 |
+
try:
|
| 195 |
+
sae_config = load_sae_config(model_settings.model_name)
|
| 196 |
+
layers = list_sae_layers(model_settings.model_name)
|
| 197 |
+
except (FileNotFoundError, KeyError, ValueError) as exc:
|
| 198 |
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
| 199 |
+
return {
|
| 200 |
+
"model_name": model_settings.model_name,
|
| 201 |
+
"display_name": model_settings.display_name,
|
| 202 |
+
"model_id": model_settings.model_id,
|
| 203 |
+
"context_length": model_settings.context_length,
|
| 204 |
+
"layers": layers,
|
| 205 |
+
"device": app.state.settings.device,
|
| 206 |
+
"dtype": model_settings.dtype,
|
| 207 |
+
"sae": {
|
| 208 |
+
"repo_id": sae_config.get("repo_id"),
|
| 209 |
+
"width": sae_config.get("width"),
|
| 210 |
+
"top_k": sae_config.get("top_k"),
|
| 211 |
+
"activation": sae_config.get("activation"),
|
| 212 |
+
},
|
| 213 |
+
"model_loaded": model_settings.model_name in getattr(app.state, "runtimes", {}),
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
@app.post("/api/sae-probe")
|
| 217 |
+
def sae_probe(body: SaeProbeRequest) -> dict[str, Any]:
|
| 218 |
+
model_settings = _model_settings(app, body.model_name)
|
| 219 |
+
try:
|
| 220 |
+
if body.layer not in list_sae_layers(model_settings.model_name):
|
| 221 |
+
raise ValueError(f"No SAE configured for {model_settings.model_name!r} layer {body.layer!r}")
|
| 222 |
+
with app.state.runtime_lock:
|
| 223 |
+
runtimes = app.state.runtimes
|
| 224 |
+
if not body.keep_models:
|
| 225 |
+
for loaded_name in list(runtimes):
|
| 226 |
+
if loaded_name != model_settings.model_name:
|
| 227 |
+
del runtimes[loaded_name]
|
| 228 |
+
_collect_memory()
|
| 229 |
+
runtime = runtimes.get(model_settings.model_name)
|
| 230 |
+
if runtime is None:
|
| 231 |
+
runtime = load_model_and_tokenizer(
|
| 232 |
+
model_settings.model_id,
|
| 233 |
+
device=app.state.settings.device,
|
| 234 |
+
dtype=model_settings.dtype,
|
| 235 |
+
)
|
| 236 |
+
runtimes[model_settings.model_name] = runtime
|
| 237 |
+
app.state.runtime = runtime
|
| 238 |
+
app.state.runtime_key = model_settings.model_name
|
| 239 |
+
model, tokenizer = runtime
|
| 240 |
+
return interpret_text_sae_probe(
|
| 241 |
+
model=model,
|
| 242 |
+
tokenizer=tokenizer,
|
| 243 |
+
text=body.text,
|
| 244 |
+
model_name=model_settings.model_name,
|
| 245 |
+
layer=body.layer,
|
| 246 |
+
top_k=body.top_k,
|
| 247 |
+
max_length=body.max_length or model_settings.context_length,
|
| 248 |
+
)
|
| 249 |
+
except ValueError as exc:
|
| 250 |
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 251 |
+
except (FileNotFoundError, KeyError) as exc:
|
| 252 |
+
raise HTTPException(status_code=404, detail=str(exc)) from exc
|
| 253 |
+
except RuntimeError as exc:
|
| 254 |
+
raise HTTPException(status_code=500, detail=str(exc)) from exc
|
| 255 |
+
|
| 256 |
+
@app.get("/api/component-stats")
|
| 257 |
+
def component_stats(model: str | None = None) -> dict[str, Any]:
|
| 258 |
+
model_name = model or app.state.settings.model_name
|
| 259 |
+
with app.state.db_lock:
|
| 260 |
+
rows = list_component_stats(app.state.conn, model_name)
|
| 261 |
+
layers: list[dict[str, Any]] = []
|
| 262 |
+
for row in rows:
|
| 263 |
+
if not layers or layers[-1]["layer"] != row["layer"]:
|
| 264 |
+
layers.append({"layer": row["layer"], "components": []})
|
| 265 |
+
layers[-1]["components"].append({key: value for key, value in row.items() if key != "layer"})
|
| 266 |
+
return {"model_name": model_name, "layers": layers}
|
| 267 |
+
|
| 268 |
+
@app.get("/api/component-examples")
|
| 269 |
+
def component_examples(layer: str, component: int, model: str | None = None) -> dict[str, Any]:
|
| 270 |
+
model_name = model or app.state.settings.model_name
|
| 271 |
+
with app.state.db_lock:
|
| 272 |
+
rows = list_components(app.state.conn, model_name, layer=layer, component=component)
|
| 273 |
+
if not rows:
|
| 274 |
+
raise HTTPException(status_code=404, detail="Unknown layer/component")
|
| 275 |
+
examples = list_component_example_details(app.state.conn, model_name, layer=layer, component=component)
|
| 276 |
+
tokenizer = None
|
| 277 |
+
model_settings = app.state.settings.models.get(model_name)
|
| 278 |
+
if model_settings is not None:
|
| 279 |
+
tokenizer = load_tokenizer(model_settings.model_id)
|
| 280 |
+
for example in examples:
|
| 281 |
+
example["token"] = _visible_token_text(example.get("token"), token_id=example.get("token_id"), tokenizer=tokenizer)
|
| 282 |
+
bands: dict[str, list[dict[str, Any]]] = {}
|
| 283 |
+
for example in examples:
|
| 284 |
+
bands.setdefault(str(example["region"] or "examples"), []).append(example)
|
| 285 |
+
return {
|
| 286 |
+
"model_name": model_name,
|
| 287 |
+
"layer": layer,
|
| 288 |
+
"component": component,
|
| 289 |
+
"excess_kurtosis": rows[0]["excess_kurtosis"],
|
| 290 |
+
"bands": [{"region": region, "examples": items} for region, items in sorted(bands.items(), key=lambda item: _example_band_sort_key(item[0]))],
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
@app.get("/api/component-token-stats")
|
| 294 |
+
def component_token_stats(layer: str, component: int, model: str | None = None) -> dict[str, Any]:
|
| 295 |
+
model_name = model or app.state.settings.model_name
|
| 296 |
+
with app.state.db_lock:
|
| 297 |
+
rows = list_components(app.state.conn, model_name, layer=layer, component=component)
|
| 298 |
+
if not rows:
|
| 299 |
+
raise HTTPException(status_code=404, detail="Unknown layer/component")
|
| 300 |
+
examples = list_component_examples(app.state.conn, model_name, layer=layer, component=component).get((layer, component), [])
|
| 301 |
+
tokenizer = None
|
| 302 |
+
model_settings = app.state.settings.models.get(model_name)
|
| 303 |
+
if model_settings is not None:
|
| 304 |
+
tokenizer = load_tokenizer(model_settings.model_id)
|
| 305 |
+
counts: dict[str, int] = {}
|
| 306 |
+
first_seen: dict[str, int] = {}
|
| 307 |
+
total_count = 0
|
| 308 |
+
for example in examples:
|
| 309 |
+
if example["region"] not in {"top_abs", "top_abs_sample_500"}:
|
| 310 |
+
continue
|
| 311 |
+
token = _visible_token_text(example["token"], token_id=example.get("token_id"), tokenizer=tokenizer)
|
| 312 |
+
if not token:
|
| 313 |
+
continue
|
| 314 |
+
total_count += 1
|
| 315 |
+
first_seen.setdefault(token, total_count)
|
| 316 |
+
counts[token] = counts.get(token, 0) + 1
|
| 317 |
+
ordered_tokens = sorted(counts.items(), key=lambda item: (-item[1], first_seen[item[0]]))
|
| 318 |
+
return {"model_name": model_name, "layer": layer, "component": component, "total_count": total_count, "tokens": [{"token": token, "count": count} for token, count in ordered_tokens]}
|
| 319 |
+
|
| 320 |
+
@app.get("/api/component-neighbors")
|
| 321 |
+
def component_neighbors(layer: str, component: int, model: str | None = None) -> dict[str, Any]:
|
| 322 |
+
model_name = model or app.state.settings.model_name
|
| 323 |
+
with app.state.db_lock:
|
| 324 |
+
rows = list_components(app.state.conn, model_name, layer=layer, component=component)
|
| 325 |
+
if not rows:
|
| 326 |
+
raise HTTPException(status_code=404, detail="Unknown layer/component")
|
| 327 |
+
neighbors = list_component_neighbors(app.state.conn, model_name, layer=layer, component=component)
|
| 328 |
+
return {"model_name": model_name, "layer": layer, "component": component, "neighbors": neighbors}
|
| 329 |
+
|
| 330 |
+
@app.get("/api/models")
|
| 331 |
+
def api_models() -> dict[str, Any]:
|
| 332 |
+
models = []
|
| 333 |
+
with app.state.db_lock:
|
| 334 |
+
model_names = list_models(app.state.conn)
|
| 335 |
+
for model_name in model_names:
|
| 336 |
+
model_settings = app.state.settings.models.get(model_name)
|
| 337 |
+
ica_dir = app.state.ica_dirs.get(model_name)
|
| 338 |
+
models.append(
|
| 339 |
+
{
|
| 340 |
+
"model_name": model_name,
|
| 341 |
+
"display_name": model_settings.display_name if model_settings else model_name,
|
| 342 |
+
"model_id": model_settings.model_id if model_settings else "",
|
| 343 |
+
"context_length": model_settings.context_length if model_settings else None,
|
| 344 |
+
"has_examples": True,
|
| 345 |
+
"probe_supported": model_settings is not None and ica_dir is not None,
|
| 346 |
+
"ica_layers": list_ica_layer_keys(ica_dir) if ica_dir else [],
|
| 347 |
+
}
|
| 348 |
+
)
|
| 349 |
+
return {"models": models}
|
| 350 |
+
|
| 351 |
+
@app.get("/api/layers")
|
| 352 |
+
def api_layers(model: str) -> dict[str, Any]:
|
| 353 |
+
with app.state.db_lock:
|
| 354 |
+
layers = list_layers(app.state.conn, model)
|
| 355 |
+
return {"layers": layers}
|
| 356 |
+
|
| 357 |
+
@app.get("/api/components")
|
| 358 |
+
def api_components(model: str, layer: str | None = None, search: str | None = None) -> dict[str, Any]:
|
| 359 |
+
with app.state.db_lock:
|
| 360 |
+
components = list_components(app.state.conn, model_name=model, layer=layer, search=search)
|
| 361 |
+
return {"components": components}
|
| 362 |
+
|
| 363 |
+
@app.get("/api/search/components")
|
| 364 |
+
def api_search_components(model: str | None = None, q: str | None = None, confidence: str | None = None, type: str | None = None, include_examples: bool = False, limit: int = 200) -> dict[str, Any]:
|
| 365 |
+
with app.state.db_lock:
|
| 366 |
+
results = search_components(app.state.conn, model_name=model, query=q or "", confidence=confidence or "", annotation_type=type or "", include_examples=include_examples, limit=limit)
|
| 367 |
+
return {"results": results}
|
| 368 |
+
|
| 369 |
+
@app.get("/api/random-components")
|
| 370 |
+
def api_random_components(model: str | None = None, selection: str | None = None) -> dict[str, Any]:
|
| 371 |
+
with app.state.db_lock:
|
| 372 |
+
rows = list_chosen_random_components(app.state.conn, model_name=model, selection_name=selection)
|
| 373 |
+
runs_by_key: dict[tuple[str, str], dict[str, Any]] = {}
|
| 374 |
+
for row in rows:
|
| 375 |
+
model_name = str(row["model_name"])
|
| 376 |
+
selection_name = str(row["selection_name"])
|
| 377 |
+
key = (model_name, selection_name)
|
| 378 |
+
run = runs_by_key.setdefault(
|
| 379 |
+
key,
|
| 380 |
+
{
|
| 381 |
+
"model": model_name,
|
| 382 |
+
"selection_name": selection_name,
|
| 383 |
+
"source_json": row.get("source_json"),
|
| 384 |
+
"settings": {
|
| 385 |
+
"n": row.get("requested_n"),
|
| 386 |
+
"seed": row.get("seed"),
|
| 387 |
+
},
|
| 388 |
+
"inventory_size": row.get("inventory_size"),
|
| 389 |
+
"selected_size": row.get("selected_size"),
|
| 390 |
+
"selected_components": [],
|
| 391 |
+
},
|
| 392 |
+
)
|
| 393 |
+
component = int(row["component"])
|
| 394 |
+
layer = str(row["layer"])
|
| 395 |
+
default_sign = infer_default_annotation_sign(app.state.conn, model_name, layer, component)
|
| 396 |
+
run["selected_components"].append(
|
| 397 |
+
{
|
| 398 |
+
**row,
|
| 399 |
+
"component_index": component,
|
| 400 |
+
"top_abs_sign": default_sign,
|
| 401 |
+
"annotation": _annotation_response(row),
|
| 402 |
+
"annotate_url": f"/annotate?model={model_name}&layer={layer}&component={component}",
|
| 403 |
+
}
|
| 404 |
+
)
|
| 405 |
+
return {"runs": list(runs_by_key.values())}
|
| 406 |
+
|
| 407 |
+
@app.get("/api/annotations/component")
|
| 408 |
+
def api_get_annotation(model: str, layer: str, component: int) -> dict[str, Any]:
|
| 409 |
+
with app.state.db_lock:
|
| 410 |
+
row = get_annotation(app.state.conn, model, layer, component)
|
| 411 |
+
if not row:
|
| 412 |
+
sign = infer_default_annotation_sign(app.state.conn, model, layer, component)
|
| 413 |
+
return _blank_annotation(model, layer, component, default_sign=sign)
|
| 414 |
+
return _annotation_response(row)
|
| 415 |
+
|
| 416 |
+
@app.post("/api/annotations/component")
|
| 417 |
+
def api_post_annotation(body: AnnotationUpdate) -> dict[str, str]:
|
| 418 |
+
with app.state.db_lock:
|
| 419 |
+
if get_component_row(app.state.conn, body.model_name, body.layer, body.component) is None:
|
| 420 |
+
raise HTTPException(status_code=404, detail="Unknown model/layer/component")
|
| 421 |
+
update_annotation(
|
| 422 |
+
app.state.conn,
|
| 423 |
+
model_name=body.model_name,
|
| 424 |
+
layer=body.layer,
|
| 425 |
+
component=body.component,
|
| 426 |
+
positive_label=body.positive_label,
|
| 427 |
+
positive_confidence=body.positive_confidence,
|
| 428 |
+
positive_interpretation_types=body.positive_interpretation_types,
|
| 429 |
+
negative_label=body.negative_label,
|
| 430 |
+
negative_confidence=body.negative_confidence,
|
| 431 |
+
negative_interpretation_types=body.negative_interpretation_types,
|
| 432 |
+
summary=body.summary,
|
| 433 |
+
notes=body.notes,
|
| 434 |
+
include_as_case_study=body.include_as_case_study,
|
| 435 |
+
)
|
| 436 |
+
return {"status": "ok"}
|
| 437 |
+
|
| 438 |
+
@app.get("/api/examples/component")
|
| 439 |
+
def api_examples_component(model: str, layer: str, component: int) -> dict[str, Any]:
|
| 440 |
+
with app.state.db_lock:
|
| 441 |
+
if get_component_row(app.state.conn, model, layer, component) is None:
|
| 442 |
+
raise HTTPException(status_code=404, detail="Unknown model/layer/component")
|
| 443 |
+
regions, examples_by_region = get_examples_by_region(app.state.conn, model, layer, component)
|
| 444 |
+
tokenizer = None
|
| 445 |
+
model_settings = app.state.settings.models.get(model)
|
| 446 |
+
if model_settings is not None:
|
| 447 |
+
tokenizer = load_tokenizer(model_settings.model_id)
|
| 448 |
+
for examples in examples_by_region.values():
|
| 449 |
+
for example in examples:
|
| 450 |
+
example["token"] = _visible_token_text(example.get("token"), token_id=example.get("token_id"), tokenizer=tokenizer)
|
| 451 |
+
return {"model_name": model, "layer": layer, "component": component, "regions": regions, "default_region": pick_default_region(regions, examples_by_region), "examples_by_region": examples_by_region}
|
| 452 |
+
|
| 453 |
+
@app.get("/api/annotation-types")
|
| 454 |
+
def api_annotation_types() -> dict[str, Any]:
|
| 455 |
+
return {"types": ANNOTATION_TYPES}
|
| 456 |
+
|
| 457 |
+
@app.get("/context")
|
| 458 |
+
def full_context_page(model: str, doc_id: int, position: int | None = None, target: str | None = None) -> HTMLResponse:
|
| 459 |
+
model_settings = _model_settings(app, model)
|
| 460 |
+
text = _load_dataset_doc_text(app, model_settings, doc_id)
|
| 461 |
+
target_span = _target_token_char_span(model_settings, text, position)
|
| 462 |
+
if target_span is None:
|
| 463 |
+
target_span = _fallback_target_char_span(text, target)
|
| 464 |
+
return HTMLResponse(
|
| 465 |
+
_render_full_context_page(
|
| 466 |
+
model_name=model_settings.model_name,
|
| 467 |
+
doc_id=doc_id,
|
| 468 |
+
position=position,
|
| 469 |
+
text=text,
|
| 470 |
+
target_span=target_span,
|
| 471 |
+
)
|
| 472 |
+
)
|
| 473 |
+
|
| 474 |
+
@app.get("/")
|
| 475 |
+
def index() -> FileResponse:
|
| 476 |
+
return FileResponse(STATIC_DIR / "index.html")
|
| 477 |
+
|
| 478 |
+
@app.get("/stats")
|
| 479 |
+
def stats() -> FileResponse:
|
| 480 |
+
return FileResponse(STATIC_DIR / "stats.html")
|
| 481 |
+
|
| 482 |
+
@app.get("/component")
|
| 483 |
+
def component_page() -> FileResponse:
|
| 484 |
+
return FileResponse(STATIC_DIR / "component.html")
|
| 485 |
+
|
| 486 |
+
@app.get("/annotate")
|
| 487 |
+
def annotate_page() -> FileResponse:
|
| 488 |
+
return FileResponse(STATIC_DIR / "annotate.html")
|
| 489 |
+
|
| 490 |
+
@app.get("/random-components")
|
| 491 |
+
def random_components_page() -> FileResponse:
|
| 492 |
+
return FileResponse(STATIC_DIR / "random_components.html")
|
| 493 |
+
|
| 494 |
+
@app.get("/sae-explorer")
|
| 495 |
+
def sae_explorer_page() -> FileResponse:
|
| 496 |
+
return FileResponse(STATIC_DIR / "sae_explorer.html")
|
| 497 |
+
|
| 498 |
+
return app
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
def _model_settings(app: FastAPI, model_name: str):
|
| 502 |
+
model_settings = app.state.settings.models.get(model_name)
|
| 503 |
+
if model_settings is None:
|
| 504 |
+
raise HTTPException(status_code=404, detail=f"Model {model_name!r} is not configured for text probing.")
|
| 505 |
+
return model_settings
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
def _raw_gpt2_block_index_for_probe(app: FastAPI, model_name: str, layer: str) -> int | None:
|
| 509 |
+
if not getattr(app.state, "use_gpt2_layer11_patch", False):
|
| 510 |
+
return None
|
| 511 |
+
if model_name == "gpt2" and layer == "layer_11":
|
| 512 |
+
return 11
|
| 513 |
+
return None
|
| 514 |
+
|
| 515 |
+
|
| 516 |
+
def _db_marks_gpt2_layer11_raw_hook(conn) -> bool:
|
| 517 |
+
try:
|
| 518 |
+
row = conn.execute(
|
| 519 |
+
"""
|
| 520 |
+
SELECT value
|
| 521 |
+
FROM import_meta
|
| 522 |
+
WHERE model_name = ?
|
| 523 |
+
AND key = ?
|
| 524 |
+
""",
|
| 525 |
+
("gpt2", "gpt2_layer11_probe_site"),
|
| 526 |
+
).fetchone()
|
| 527 |
+
except Exception:
|
| 528 |
+
return False
|
| 529 |
+
if row is None:
|
| 530 |
+
return False
|
| 531 |
+
value = str(row[0]).strip().lower()
|
| 532 |
+
return value in {"raw_block_11_resid_post", "patched_raw_block_11_resid_post"}
|
| 533 |
+
|
| 534 |
+
|
| 535 |
+
def _settings_with_gpt2_layer11_patch(settings: Settings) -> Settings:
|
| 536 |
+
gpt2_settings = settings.models.get("gpt2")
|
| 537 |
+
if gpt2_settings is None:
|
| 538 |
+
raise RuntimeError("GPT-2 settings are required for --use-gpt2-layer11-patch.")
|
| 539 |
+
if not GPT2_LAYER11_PATCH_DB.is_file():
|
| 540 |
+
raise FileNotFoundError(f"Patch SQLite database does not exist: {GPT2_LAYER11_PATCH_DB}")
|
| 541 |
+
if not (GPT2_LAYER11_PATCH_ICA_ROOT / "gpt2" / "layer_11_fastica.pt").is_file():
|
| 542 |
+
raise FileNotFoundError(f"Patch ICA artifact does not exist: {GPT2_LAYER11_PATCH_ICA_ROOT / 'gpt2' / 'layer_11_fastica.pt'}")
|
| 543 |
+
models = dict(settings.models)
|
| 544 |
+
models["gpt2"] = replace(gpt2_settings, ica_dir=GPT2_LAYER11_PATCH_ICA_ROOT / "gpt2")
|
| 545 |
+
patched = replace(
|
| 546 |
+
settings,
|
| 547 |
+
db_path=GPT2_LAYER11_PATCH_DB,
|
| 548 |
+
ica_root=GPT2_LAYER11_PATCH_ICA_ROOT,
|
| 549 |
+
ica_dir=GPT2_LAYER11_PATCH_ICA_ROOT / "gpt2",
|
| 550 |
+
model_name="gpt2",
|
| 551 |
+
download_missing=False,
|
| 552 |
+
models=models,
|
| 553 |
+
use_gpt2_layer11_patch=True,
|
| 554 |
+
)
|
| 555 |
+
return patched
|
| 556 |
+
|
| 557 |
+
|
| 558 |
+
def _model_meta(app: FastAPI, model_name: str) -> dict[str, Any]:
|
| 559 |
+
model_settings = _model_settings(app, model_name)
|
| 560 |
+
ica_dir = app.state.ica_dirs.get(model_settings.model_name)
|
| 561 |
+
layers = list_ica_layer_keys(ica_dir) if ica_dir else []
|
| 562 |
+
with app.state.db_lock:
|
| 563 |
+
db_layers = set(list_layers(app.state.conn, model_settings.model_name))
|
| 564 |
+
return {
|
| 565 |
+
"model_name": model_settings.model_name,
|
| 566 |
+
"display_name": model_settings.display_name,
|
| 567 |
+
"model_id": model_settings.model_id,
|
| 568 |
+
"context_length": model_settings.context_length,
|
| 569 |
+
"layers": [layer for layer in layers if layer in db_layers],
|
| 570 |
+
"device": app.state.settings.device,
|
| 571 |
+
"dtype": model_settings.dtype,
|
| 572 |
+
"model_loaded": model_settings.model_name in getattr(app.state, "runtimes", {}),
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
|
| 576 |
+
def _blank_annotation(model: str, layer: str, component: int, *, default_sign: int) -> dict[str, Any]:
|
| 577 |
+
positive_label = "" if default_sign >= 0 else "?"
|
| 578 |
+
negative_label = "?" if default_sign >= 0 else ""
|
| 579 |
+
return {
|
| 580 |
+
"model_name": model,
|
| 581 |
+
"layer": layer,
|
| 582 |
+
"component": component,
|
| 583 |
+
"positive_label": positive_label,
|
| 584 |
+
"positive_confidence": "unclear",
|
| 585 |
+
"positive_interpretation_types": [],
|
| 586 |
+
"negative_label": negative_label,
|
| 587 |
+
"negative_confidence": "unclear",
|
| 588 |
+
"negative_interpretation_types": [],
|
| 589 |
+
"summary": "",
|
| 590 |
+
"notes": "",
|
| 591 |
+
"include_as_case_study": False,
|
| 592 |
+
"updated_at": None,
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
|
| 596 |
+
def _annotation_response(row: dict[str, Any]) -> dict[str, Any]:
|
| 597 |
+
return {
|
| 598 |
+
"model_name": row["model_name"],
|
| 599 |
+
"layer": row["layer"],
|
| 600 |
+
"component": int(row["component"]),
|
| 601 |
+
"positive_label": row.get("positive_label") or "",
|
| 602 |
+
"positive_confidence": row.get("positive_confidence") or "unclear",
|
| 603 |
+
"positive_interpretation_types": _json_list(row.get("positive_interpretation_types_json")),
|
| 604 |
+
"negative_label": row.get("negative_label") or "",
|
| 605 |
+
"negative_confidence": row.get("negative_confidence") or "unclear",
|
| 606 |
+
"negative_interpretation_types": _json_list(row.get("negative_interpretation_types_json")),
|
| 607 |
+
"summary": row.get("summary") or "",
|
| 608 |
+
"notes": row.get("notes") or "",
|
| 609 |
+
"include_as_case_study": bool(row.get("include_as_case_study")),
|
| 610 |
+
"updated_at": row.get("updated_at"),
|
| 611 |
+
}
|
| 612 |
+
|
| 613 |
+
|
| 614 |
+
def _json_list(value: Any) -> list[str]:
|
| 615 |
+
import json
|
| 616 |
+
try:
|
| 617 |
+
parsed = json.loads(value or "[]")
|
| 618 |
+
except json.JSONDecodeError:
|
| 619 |
+
return []
|
| 620 |
+
return [str(item) for item in parsed] if isinstance(parsed, list) else []
|
| 621 |
+
|
| 622 |
+
|
| 623 |
+
def _load_dataset_doc_text(app: FastAPI, model_settings: Any, doc_id: int) -> str:
|
| 624 |
+
if doc_id < 0:
|
| 625 |
+
raise HTTPException(status_code=400, detail="doc_id must be non-negative")
|
| 626 |
+
key = (model_settings.model_name, doc_id)
|
| 627 |
+
cached = app.state.full_context_cache.get(key)
|
| 628 |
+
if cached is not None:
|
| 629 |
+
return cached
|
| 630 |
+
try:
|
| 631 |
+
from datasets import load_dataset
|
| 632 |
+
except ImportError as exc:
|
| 633 |
+
raise HTTPException(status_code=500, detail="datasets is required to open full document context") from exc
|
| 634 |
+
dataset_kwargs: dict[str, Any] = {
|
| 635 |
+
"path": model_settings.dataset_path,
|
| 636 |
+
"split": model_settings.dataset_split,
|
| 637 |
+
"streaming": model_settings.dataset_streaming,
|
| 638 |
+
}
|
| 639 |
+
if model_settings.dataset_name:
|
| 640 |
+
dataset_kwargs["name"] = model_settings.dataset_name
|
| 641 |
+
try:
|
| 642 |
+
dataset = load_dataset(**dataset_kwargs)
|
| 643 |
+
if model_settings.dataset_streaming:
|
| 644 |
+
for idx, row in enumerate(dataset):
|
| 645 |
+
if idx == doc_id:
|
| 646 |
+
text = str(row[model_settings.dataset_text_column])
|
| 647 |
+
app.state.full_context_cache[key] = text
|
| 648 |
+
return text
|
| 649 |
+
if idx > doc_id:
|
| 650 |
+
break
|
| 651 |
+
else:
|
| 652 |
+
row = dataset[doc_id]
|
| 653 |
+
text = str(row[model_settings.dataset_text_column])
|
| 654 |
+
app.state.full_context_cache[key] = text
|
| 655 |
+
return text
|
| 656 |
+
except IndexError as exc:
|
| 657 |
+
raise HTTPException(status_code=404, detail=f"Document {doc_id} not found in {model_settings.dataset_path}/{model_settings.dataset_split}") from exc
|
| 658 |
+
except KeyError as exc:
|
| 659 |
+
raise HTTPException(status_code=500, detail=f"Dataset row does not contain text column {model_settings.dataset_text_column!r}") from exc
|
| 660 |
+
raise HTTPException(status_code=404, detail=f"Document {doc_id} not found in {model_settings.dataset_path}/{model_settings.dataset_split}")
|
| 661 |
+
|
| 662 |
+
|
| 663 |
+
def _target_token_char_span(model_settings: Any, text: str, position: int | None) -> tuple[int, int] | None:
|
| 664 |
+
if position is None or position < 0:
|
| 665 |
+
return None
|
| 666 |
+
tokenizer = load_tokenizer(model_settings.model_id)
|
| 667 |
+
try:
|
| 668 |
+
encoded = tokenizer(
|
| 669 |
+
text,
|
| 670 |
+
truncation=True,
|
| 671 |
+
max_length=model_settings.context_length,
|
| 672 |
+
return_offsets_mapping=True,
|
| 673 |
+
)
|
| 674 |
+
except (NotImplementedError, TypeError, ValueError):
|
| 675 |
+
return None
|
| 676 |
+
offsets = encoded.get("offset_mapping")
|
| 677 |
+
if offsets is None or position >= len(offsets):
|
| 678 |
+
return None
|
| 679 |
+
start, stop = offsets[position]
|
| 680 |
+
start = int(start)
|
| 681 |
+
stop = int(stop)
|
| 682 |
+
if stop <= start:
|
| 683 |
+
return None
|
| 684 |
+
return start, stop
|
| 685 |
+
|
| 686 |
+
|
| 687 |
+
def _fallback_target_char_span(text: str, target: str | None) -> tuple[int, int] | None:
|
| 688 |
+
if not target:
|
| 689 |
+
return None
|
| 690 |
+
candidates = [target.replace("\ufffd", "")]
|
| 691 |
+
stripped = candidates[0].strip()
|
| 692 |
+
if stripped and stripped != candidates[0]:
|
| 693 |
+
candidates.append(stripped)
|
| 694 |
+
for candidate in candidates:
|
| 695 |
+
if not candidate:
|
| 696 |
+
continue
|
| 697 |
+
start = text.find(candidate)
|
| 698 |
+
if start >= 0:
|
| 699 |
+
return start, start + len(candidate)
|
| 700 |
+
return None
|
| 701 |
+
|
| 702 |
+
|
| 703 |
+
def _highlighted_text_html(text: str, target_span: tuple[int, int] | None) -> str:
|
| 704 |
+
if target_span is None:
|
| 705 |
+
return html.escape(text)
|
| 706 |
+
start, stop = target_span
|
| 707 |
+
start = max(0, min(start, len(text)))
|
| 708 |
+
stop = max(start, min(stop, len(text)))
|
| 709 |
+
return f'{html.escape(text[:start])}<mark id="target-token">{html.escape(text[start:stop])}</mark>{html.escape(text[stop:])}'
|
| 710 |
+
|
| 711 |
+
|
| 712 |
+
def _render_full_context_page(*, model_name: str, doc_id: int, position: int | None, text: str, target_span: tuple[int, int] | None) -> str:
|
| 713 |
+
pos = "" if position is None else f" - pos={html.escape(str(position))}"
|
| 714 |
+
title = f"{model_name} - doc={doc_id}{pos}"
|
| 715 |
+
content = _highlighted_text_html(text, target_span)
|
| 716 |
+
target_status = "" if target_span is not None else '<span class="target-status">target token not located</span>'
|
| 717 |
+
return f'''<!doctype html>
|
| 718 |
+
<html lang="en">
|
| 719 |
+
<head>
|
| 720 |
+
<meta charset="utf-8" />
|
| 721 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
| 722 |
+
<title>{html.escape(title)}</title>
|
| 723 |
+
<style>
|
| 724 |
+
body {{ margin:0; background:#f8fafc; color:#0f172a; font:16px/1.55 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }}
|
| 725 |
+
header {{ position:sticky; top:0; background:#fff; border-bottom:1px solid #cbd5e1; padding:14px 18px; color:#475569; font-size:13px; box-shadow:0 1px 2px rgb(15 23 42 / .08); }}
|
| 726 |
+
pre {{ max-width:980px; margin:24px auto; padding:0 20px 48px; white-space:pre-wrap; overflow-wrap:anywhere; font:inherit; }}
|
| 727 |
+
mark {{ background:#bfdbfe; color:#172554; border-radius:4px; padding:1px 3px; box-shadow:0 0 0 1px #93c5fd inset; }}
|
| 728 |
+
.target-status {{ margin-left:10px; color:#b45309; }}
|
| 729 |
+
</style>
|
| 730 |
+
</head>
|
| 731 |
+
<body>
|
| 732 |
+
<header>{html.escape(title)}{target_status}</header>
|
| 733 |
+
<pre>{content}</pre>
|
| 734 |
+
<script>const target=document.getElementById("target-token"); if(target) target.scrollIntoView({{block:"center"}});</script>
|
| 735 |
+
</body>
|
| 736 |
+
</html>'''
|
| 737 |
+
|
| 738 |
+
|
| 739 |
+
def _collect_memory() -> None:
|
| 740 |
+
gc.collect()
|
| 741 |
+
try:
|
| 742 |
+
import torch
|
| 743 |
+
if torch.cuda.is_available():
|
| 744 |
+
torch.cuda.empty_cache()
|
| 745 |
+
except Exception:
|
| 746 |
+
pass
|
| 747 |
+
|
| 748 |
+
|
| 749 |
+
def _example_band_sort_key(region: str) -> tuple[int, str]:
|
| 750 |
+
name = str(region or "").lower().replace("-", "_")
|
| 751 |
+
compact = name.replace("_", "")
|
| 752 |
+
if name == "top_abs" or compact == "topabs":
|
| 753 |
+
return (0, name)
|
| 754 |
+
if "sample" in name:
|
| 755 |
+
return (1, name)
|
| 756 |
+
if "near_zero" in name or "nearzero" in compact:
|
| 757 |
+
return (2, name)
|
| 758 |
+
if "opposite" in name:
|
| 759 |
+
return (3, name)
|
| 760 |
+
return (4, name)
|
| 761 |
+
|
| 762 |
+
|
| 763 |
+
def _visible_token_text(token: Any, *, token_id: Any = None, tokenizer: Any = None) -> str:
|
| 764 |
+
raw = str(token or "")
|
| 765 |
+
text = raw.strip()
|
| 766 |
+
if (not text or "\ufffd" in text) and tokenizer is not None and token_id is not None:
|
| 767 |
+
try:
|
| 768 |
+
token_id_int = int(token_id)
|
| 769 |
+
decoded = str(tokenizer.decode([token_id_int], skip_special_tokens=False, clean_up_tokenization_spaces=False))
|
| 770 |
+
except Exception:
|
| 771 |
+
decoded = ""
|
| 772 |
+
if decoded and "\ufffd" not in decoded:
|
| 773 |
+
raw = decoded
|
| 774 |
+
text = raw.strip()
|
| 775 |
+
else:
|
| 776 |
+
try:
|
| 777 |
+
raw_token = str(tokenizer.convert_ids_to_tokens(token_id_int))
|
| 778 |
+
except Exception:
|
| 779 |
+
raw_token = ""
|
| 780 |
+
byte_decoded = _decode_byte_level_token(raw_token) if raw_token else None
|
| 781 |
+
if byte_decoded:
|
| 782 |
+
raw = byte_decoded
|
| 783 |
+
text = raw.strip()
|
| 784 |
+
elif raw_token and "\ufffd" not in raw_token:
|
| 785 |
+
raw = raw_token
|
| 786 |
+
text = raw.strip()
|
| 787 |
+
if text:
|
| 788 |
+
return text
|
| 789 |
+
if raw == " ":
|
| 790 |
+
return "[space]"
|
| 791 |
+
if raw == "\n":
|
| 792 |
+
return "[newline]"
|
| 793 |
+
if raw == "\t":
|
| 794 |
+
return "[tab]"
|
| 795 |
+
return raw.replace("\ufffd", "[invalid]")
|
| 796 |
+
|
| 797 |
+
|
| 798 |
+
def main() -> None:
|
| 799 |
+
import uvicorn
|
| 800 |
+
parser = argparse.ArgumentParser(description="Run the ICA Lens explorer/annotator server.")
|
| 801 |
+
parser.add_argument("--host", default="127.0.0.1")
|
| 802 |
+
parser.add_argument("--port", type=int, default=8764)
|
| 803 |
+
parser.add_argument(
|
| 804 |
+
"--use-gpt2-layer11-patch",
|
| 805 |
+
action="store_true",
|
| 806 |
+
help="Use the isolated corrected GPT-2 layer-11 patch DB/artifacts and raw block hook for live layer-11 probes.",
|
| 807 |
+
)
|
| 808 |
+
args = parser.parse_args()
|
| 809 |
+
settings = load_settings()
|
| 810 |
+
if args.use_gpt2_layer11_patch:
|
| 811 |
+
settings = _settings_with_gpt2_layer11_patch(settings)
|
| 812 |
+
uvicorn.run(create_app(settings), host=args.host, port=args.port, log_level="info")
|
| 813 |
+
|
| 814 |
+
|
| 815 |
+
if __name__ == "__main__":
|
| 816 |
+
main()
|
server/artifacts.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
|
| 5 |
+
from huggingface_hub import hf_hub_download, snapshot_download
|
| 6 |
+
|
| 7 |
+
from .config import Settings
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
RELEASED_DATABASES = {"ica_probe_mini.sqlite", "ica_probe_full.sqlite"}
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def resolve_db_path(settings: Settings) -> Path:
|
| 14 |
+
if settings.db_path.is_file():
|
| 15 |
+
_validate_readable_file(settings.db_path)
|
| 16 |
+
return settings.db_path
|
| 17 |
+
if not settings.download_missing:
|
| 18 |
+
raise FileNotFoundError(f"SQLite database does not exist: {settings.db_path}")
|
| 19 |
+
if settings.db_path.name not in RELEASED_DATABASES:
|
| 20 |
+
raise FileNotFoundError(
|
| 21 |
+
f"SQLite database does not exist: {settings.db_path}. "
|
| 22 |
+
"Automatic download is only supported for released mini/full databases."
|
| 23 |
+
)
|
| 24 |
+
settings.db_path.parent.mkdir(parents=True, exist_ok=True)
|
| 25 |
+
local_dir = settings.db_path.parent.parent if settings.db_path.parent.name == "databases" else settings.db_path.parent
|
| 26 |
+
downloaded = Path(
|
| 27 |
+
hf_hub_download(
|
| 28 |
+
repo_id=settings.db_repo,
|
| 29 |
+
repo_type="dataset",
|
| 30 |
+
filename=f"databases/{settings.db_path.name}",
|
| 31 |
+
revision=settings.hf_revision,
|
| 32 |
+
local_dir=local_dir,
|
| 33 |
+
)
|
| 34 |
+
)
|
| 35 |
+
_validate_readable_file(downloaded)
|
| 36 |
+
return downloaded
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def resolve_ica_dir(settings: Settings, *, model_name: str | None = None, ica_dir: Path | None = None) -> Path:
|
| 40 |
+
target_model = model_name or settings.model_name
|
| 41 |
+
target_dir = ica_dir or settings.ica_dir
|
| 42 |
+
if _has_fastica_artifacts(target_dir):
|
| 43 |
+
return target_dir
|
| 44 |
+
if not settings.download_missing:
|
| 45 |
+
raise FileNotFoundError(f"ICA artifacts do not exist: {target_dir}")
|
| 46 |
+
target_dir.parent.parent.mkdir(parents=True, exist_ok=True)
|
| 47 |
+
snapshot_download(
|
| 48 |
+
repo_id=settings.artifact_repo,
|
| 49 |
+
repo_type="dataset",
|
| 50 |
+
revision=settings.hf_revision,
|
| 51 |
+
allow_patterns=[f"models/{target_model}/**"],
|
| 52 |
+
local_dir=target_dir.parent.parent,
|
| 53 |
+
)
|
| 54 |
+
if not _has_fastica_artifacts(target_dir):
|
| 55 |
+
raise FileNotFoundError(f"Downloaded artifacts but found no FastICA files in {target_dir}")
|
| 56 |
+
return target_dir
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def validate_ica_dir(ica_dir: Path) -> None:
|
| 60 |
+
if not _has_fastica_artifacts(ica_dir):
|
| 61 |
+
raise FileNotFoundError(f"No *_fastica.pt artifacts found in {ica_dir}")
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _has_fastica_artifacts(path: Path) -> bool:
|
| 65 |
+
return path.is_dir() and any(path.glob("*_fastica.pt"))
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def _validate_readable_file(path: Path) -> None:
|
| 69 |
+
try:
|
| 70 |
+
with path.open("rb"):
|
| 71 |
+
pass
|
| 72 |
+
except OSError as exc:
|
| 73 |
+
raise RuntimeError(f"File exists but cannot be accessed: {path}") from exc
|
server/config.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
V6_ROOT = Path(__file__).resolve().parents[1]
|
| 9 |
+
DEFAULT_ARTIFACT_REPO = "sida/ica-lens-paper"
|
| 10 |
+
DEFAULT_DB_REPO = "sida/ica-lens-paper"
|
| 11 |
+
DEFAULT_DB_FILENAME = "ica_probe_mini.sqlite"
|
| 12 |
+
DEFAULT_MODEL_ID = "openai-community/gpt2"
|
| 13 |
+
DEFAULT_MODEL_NAME = "gpt2"
|
| 14 |
+
DEFAULT_MODEL_REGISTRY = {
|
| 15 |
+
"gpt2": {"model_id": "openai-community/gpt2", "display_name": "GPT-2", "context_length": 1024, "dtype": "bfloat16", "dataset_path": "NeelNanda/pile-10k", "dataset_name": "", "dataset_split": "train", "dataset_text_column": "text", "dataset_streaming": False},
|
| 16 |
+
"gemma2_2b": {"model_id": "google/gemma-2-2b", "display_name": "Gemma 2 2B", "context_length": 1024, "dtype": "bfloat16", "dataset_path": "NeelNanda/pile-10k", "dataset_name": "", "dataset_split": "train", "dataset_text_column": "text", "dataset_streaming": False},
|
| 17 |
+
"qwen3_5_2b_base": {"model_id": "Qwen/Qwen3.5-2B-Base", "display_name": "Qwen3.5 2B Base", "context_length": 1024, "dtype": "bfloat16", "dataset_path": "NeelNanda/pile-10k", "dataset_name": "", "dataset_split": "train", "dataset_text_column": "text", "dataset_streaming": False},
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass(frozen=True)
|
| 22 |
+
class ModelSettings:
|
| 23 |
+
model_name: str
|
| 24 |
+
model_id: str
|
| 25 |
+
display_name: str
|
| 26 |
+
ica_dir: Path
|
| 27 |
+
context_length: int
|
| 28 |
+
dtype: str
|
| 29 |
+
dataset_path: str
|
| 30 |
+
dataset_name: str | None
|
| 31 |
+
dataset_split: str
|
| 32 |
+
dataset_text_column: str
|
| 33 |
+
dataset_streaming: bool
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@dataclass(frozen=True)
|
| 37 |
+
class Settings:
|
| 38 |
+
db_path: Path
|
| 39 |
+
ica_dir: Path
|
| 40 |
+
ica_root: Path
|
| 41 |
+
artifact_repo: str
|
| 42 |
+
db_repo: str
|
| 43 |
+
hf_revision: str | None
|
| 44 |
+
model_id: str
|
| 45 |
+
model_name: str
|
| 46 |
+
device: str
|
| 47 |
+
dtype: str
|
| 48 |
+
context_length: int
|
| 49 |
+
download_missing: bool
|
| 50 |
+
models: dict[str, ModelSettings]
|
| 51 |
+
use_gpt2_layer11_patch: bool = False
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def load_settings() -> Settings:
|
| 55 |
+
fetched_root = V6_ROOT / "artifacts" / "fetched"
|
| 56 |
+
db_path = Path(os.environ.get("ICA_EXPLORER_DB_PATH", str(fetched_root / "databases" / DEFAULT_DB_FILENAME))).expanduser()
|
| 57 |
+
ica_root = Path(os.environ.get("ICA_EXPLORER_ICA_ROOT", str(fetched_root / "models"))).expanduser()
|
| 58 |
+
ica_dir = Path(os.environ.get("ICA_EXPLORER_ICA_DIR", str(ica_root / DEFAULT_MODEL_NAME))).expanduser()
|
| 59 |
+
enabled_models = _enabled_model_names()
|
| 60 |
+
models = {
|
| 61 |
+
model_name: ModelSettings(
|
| 62 |
+
model_name=model_name,
|
| 63 |
+
model_id=str(meta["model_id"]),
|
| 64 |
+
display_name=str(meta["display_name"]),
|
| 65 |
+
ica_dir=ica_root / model_name,
|
| 66 |
+
context_length=int(meta["context_length"]),
|
| 67 |
+
dtype=str(meta["dtype"]),
|
| 68 |
+
dataset_path=str(meta["dataset_path"]),
|
| 69 |
+
dataset_name=str(meta["dataset_name"]) or None,
|
| 70 |
+
dataset_split=str(meta["dataset_split"]),
|
| 71 |
+
dataset_text_column=str(meta["dataset_text_column"]),
|
| 72 |
+
dataset_streaming=bool(meta["dataset_streaming"]),
|
| 73 |
+
)
|
| 74 |
+
for model_name, meta in DEFAULT_MODEL_REGISTRY.items()
|
| 75 |
+
if model_name in enabled_models
|
| 76 |
+
}
|
| 77 |
+
if os.environ.get("ICA_EXPLORER_ICA_DIR"):
|
| 78 |
+
default = models[DEFAULT_MODEL_NAME]
|
| 79 |
+
models[DEFAULT_MODEL_NAME] = ModelSettings(
|
| 80 |
+
model_name=default.model_name,
|
| 81 |
+
model_id=default.model_id,
|
| 82 |
+
display_name=default.display_name,
|
| 83 |
+
ica_dir=ica_dir,
|
| 84 |
+
context_length=default.context_length,
|
| 85 |
+
dtype=default.dtype,
|
| 86 |
+
dataset_path=default.dataset_path,
|
| 87 |
+
dataset_name=default.dataset_name,
|
| 88 |
+
dataset_split=default.dataset_split,
|
| 89 |
+
dataset_text_column=default.dataset_text_column,
|
| 90 |
+
dataset_streaming=default.dataset_streaming,
|
| 91 |
+
)
|
| 92 |
+
return Settings(
|
| 93 |
+
db_path=db_path,
|
| 94 |
+
ica_dir=ica_dir,
|
| 95 |
+
ica_root=ica_root,
|
| 96 |
+
artifact_repo=os.environ.get("ICA_EXPLORER_ARTIFACT_REPO", DEFAULT_ARTIFACT_REPO),
|
| 97 |
+
db_repo=os.environ.get("ICA_EXPLORER_DB_REPO", DEFAULT_DB_REPO),
|
| 98 |
+
hf_revision=os.environ.get("ICA_EXPLORER_HF_REVISION") or None,
|
| 99 |
+
model_id=os.environ.get("ICA_EXPLORER_MODEL_ID", DEFAULT_MODEL_ID),
|
| 100 |
+
model_name=os.environ.get("ICA_EXPLORER_MODEL_NAME", DEFAULT_MODEL_NAME),
|
| 101 |
+
device=os.environ.get("ICA_EXPLORER_DEVICE", "auto"),
|
| 102 |
+
dtype=os.environ.get("ICA_EXPLORER_DTYPE", "bfloat16"),
|
| 103 |
+
context_length=int(os.environ.get("ICA_EXPLORER_CONTEXT_LENGTH", "1024")),
|
| 104 |
+
download_missing=os.environ.get("ICA_EXPLORER_DOWNLOAD_MISSING", "1").strip().lower() not in {"0", "false", "no"},
|
| 105 |
+
models=models,
|
| 106 |
+
)
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def _enabled_model_names() -> set[str]:
|
| 110 |
+
raw = os.environ.get("ICA_EXPLORER_ENABLED_MODELS")
|
| 111 |
+
if not raw:
|
| 112 |
+
return set(DEFAULT_MODEL_REGISTRY)
|
| 113 |
+
names = {name.strip() for name in raw.split(",") if name.strip()}
|
| 114 |
+
unknown = names - set(DEFAULT_MODEL_REGISTRY)
|
| 115 |
+
if unknown:
|
| 116 |
+
raise ValueError(f"Unknown ICA_EXPLORER_ENABLED_MODELS value(s): {', '.join(sorted(unknown))}")
|
| 117 |
+
if not names:
|
| 118 |
+
raise ValueError("ICA_EXPLORER_ENABLED_MODELS did not contain any model names.")
|
| 119 |
+
return names
|
server/model_runtime.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from functools import lru_cache
|
| 4 |
+
from typing import Any
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def torch_dtype(name: str) -> torch.dtype:
|
| 11 |
+
aliases = {
|
| 12 |
+
"float16": torch.float16,
|
| 13 |
+
"fp16": torch.float16,
|
| 14 |
+
"bfloat16": torch.bfloat16,
|
| 15 |
+
"bf16": torch.bfloat16,
|
| 16 |
+
"float32": torch.float32,
|
| 17 |
+
"fp32": torch.float32,
|
| 18 |
+
}
|
| 19 |
+
try:
|
| 20 |
+
return aliases[name.lower()]
|
| 21 |
+
except KeyError as exc:
|
| 22 |
+
raise ValueError(f"Unsupported torch dtype {name!r}") from exc
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def resolve_device(requested: str) -> torch.device:
|
| 26 |
+
if requested == "auto":
|
| 27 |
+
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| 28 |
+
if requested.startswith("cuda") and not torch.cuda.is_available():
|
| 29 |
+
raise RuntimeError("CUDA was requested but torch.cuda.is_available() is false.")
|
| 30 |
+
return torch.device(requested)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@lru_cache(maxsize=None)
|
| 34 |
+
def load_tokenizer(model_id: str) -> Any:
|
| 35 |
+
tokenizer = AutoTokenizer.from_pretrained(model_id)
|
| 36 |
+
ensure_pad_token(tokenizer)
|
| 37 |
+
return tokenizer
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def load_model_and_tokenizer(model_id: str, *, device: str, dtype: str) -> tuple[torch.nn.Module, Any]:
|
| 41 |
+
torch_device = resolve_device(device)
|
| 42 |
+
tokenizer = load_tokenizer(model_id)
|
| 43 |
+
|
| 44 |
+
model_dtype = torch_dtype(dtype)
|
| 45 |
+
if torch_device.type == "cpu" and model_dtype in {torch.float16, torch.bfloat16}:
|
| 46 |
+
model_dtype = torch.float32
|
| 47 |
+
|
| 48 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 49 |
+
model_id,
|
| 50 |
+
torch_dtype=model_dtype,
|
| 51 |
+
low_cpu_mem_usage=True,
|
| 52 |
+
)
|
| 53 |
+
model.to(torch_device)
|
| 54 |
+
model.eval()
|
| 55 |
+
return model, tokenizer
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def ensure_pad_token(tokenizer: Any) -> None:
|
| 59 |
+
if tokenizer.pad_token_id is None:
|
| 60 |
+
if tokenizer.eos_token_id is None:
|
| 61 |
+
raise ValueError("Tokenizer has neither pad_token_id nor eos_token_id.")
|
| 62 |
+
tokenizer.pad_token = tokenizer.eos_token
|
server/probe.py
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from collections.abc import Collection
|
| 4 |
+
from functools import lru_cache
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def list_ica_layer_keys(ica_dir: Path) -> list[str]:
|
| 12 |
+
if not ica_dir.is_dir():
|
| 13 |
+
return []
|
| 14 |
+
layers = [path.name[: -len("_fastica.pt")] for path in ica_dir.glob("*_fastica.pt")]
|
| 15 |
+
return sorted(layers, key=_layer_sort_key)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def fastica_artifact_path(ica_dir: Path, layer: str) -> Path:
|
| 19 |
+
return ica_dir / f"{layer}_fastica.pt"
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def interpret_text_probe(
|
| 23 |
+
*,
|
| 24 |
+
model: torch.nn.Module,
|
| 25 |
+
tokenizer: Any,
|
| 26 |
+
text: str,
|
| 27 |
+
layer: str,
|
| 28 |
+
ica_artifact_path: Path,
|
| 29 |
+
top_k: int,
|
| 30 |
+
highlight_components: Collection[int] | None,
|
| 31 |
+
max_length: int,
|
| 32 |
+
raw_gpt2_block_index: int | None = None,
|
| 33 |
+
) -> dict[str, Any]:
|
| 34 |
+
num_transformer_layers = int(model.config.num_hidden_layers)
|
| 35 |
+
hidden_index = _layer_key_to_hidden_index(layer, num_transformer_layers=num_transformer_layers)
|
| 36 |
+
show_predictions = layer == f"layer_{num_transformer_layers - 1:02d}"
|
| 37 |
+
full_ids = tokenizer.encode(text, add_special_tokens=True)
|
| 38 |
+
truncated = len(full_ids) > max_length
|
| 39 |
+
encoded = tokenizer(text, return_tensors="pt", truncation=True, max_length=max_length)
|
| 40 |
+
input_ids = encoded["input_ids"].to(next(model.parameters()).device)
|
| 41 |
+
attention_mask = encoded.get("attention_mask")
|
| 42 |
+
if attention_mask is not None:
|
| 43 |
+
attention_mask = attention_mask.to(input_ids.device)
|
| 44 |
+
|
| 45 |
+
predictions: list[dict[str, Any]] | None = None
|
| 46 |
+
with torch.inference_mode():
|
| 47 |
+
if layer == "embedding":
|
| 48 |
+
hidden = model.get_input_embeddings()(input_ids)[0]
|
| 49 |
+
elif raw_gpt2_block_index is not None:
|
| 50 |
+
captured: dict[str, torch.Tensor] = {}
|
| 51 |
+
blocks = getattr(getattr(model, "transformer", None), "h", None)
|
| 52 |
+
if blocks is None:
|
| 53 |
+
raise RuntimeError("raw_gpt2_block_index requires a GPT-2 style model.transformer.h module list.")
|
| 54 |
+
|
| 55 |
+
def hook(_module: Any, _inputs: Any, output: Any) -> None:
|
| 56 |
+
captured["hidden"] = output[0].detach() if isinstance(output, tuple) else output.detach()
|
| 57 |
+
|
| 58 |
+
handle = blocks[int(raw_gpt2_block_index)].register_forward_hook(hook)
|
| 59 |
+
try:
|
| 60 |
+
outputs = model(
|
| 61 |
+
input_ids=input_ids,
|
| 62 |
+
attention_mask=attention_mask,
|
| 63 |
+
use_cache=False,
|
| 64 |
+
)
|
| 65 |
+
finally:
|
| 66 |
+
handle.remove()
|
| 67 |
+
captured_hidden = captured.get("hidden")
|
| 68 |
+
if captured_hidden is None:
|
| 69 |
+
raise RuntimeError("GPT-2 raw block hook did not capture hidden states.")
|
| 70 |
+
hidden = captured_hidden[0]
|
| 71 |
+
if show_predictions:
|
| 72 |
+
pred_ids = torch.argmax(outputs.logits[0], dim=-1).detach().cpu().tolist()
|
| 73 |
+
predictions = [
|
| 74 |
+
{
|
| 75 |
+
"token_id": int(pred_id),
|
| 76 |
+
"token": tokenizer.convert_ids_to_tokens(int(pred_id)),
|
| 77 |
+
"token_text": _decode_token_text(tokenizer, int(pred_id)),
|
| 78 |
+
}
|
| 79 |
+
for pred_id in pred_ids
|
| 80 |
+
]
|
| 81 |
+
else:
|
| 82 |
+
outputs = model(
|
| 83 |
+
input_ids=input_ids,
|
| 84 |
+
attention_mask=attention_mask,
|
| 85 |
+
output_hidden_states=True,
|
| 86 |
+
use_cache=False,
|
| 87 |
+
)
|
| 88 |
+
hidden_states = outputs.hidden_states
|
| 89 |
+
if hidden_states is None:
|
| 90 |
+
raise RuntimeError("Model did not return hidden states.")
|
| 91 |
+
hidden = hidden_states[hidden_index][0]
|
| 92 |
+
if show_predictions:
|
| 93 |
+
pred_ids = torch.argmax(outputs.logits[0], dim=-1).detach().cpu().tolist()
|
| 94 |
+
predictions = [
|
| 95 |
+
{
|
| 96 |
+
"token_id": int(pred_id),
|
| 97 |
+
"token": tokenizer.convert_ids_to_tokens(int(pred_id)),
|
| 98 |
+
"token_text": _decode_token_text(tokenizer, int(pred_id)),
|
| 99 |
+
}
|
| 100 |
+
for pred_id in pred_ids
|
| 101 |
+
]
|
| 102 |
+
|
| 103 |
+
artifact = _load_fastica_artifact(ica_artifact_path)
|
| 104 |
+
scores = _all_source_scores(hidden, **artifact)
|
| 105 |
+
idx, vals = _topk_components_per_token(scores, top_k=top_k)
|
| 106 |
+
forced = sorted({int(component) for component in (highlight_components or [])})
|
| 107 |
+
out_of_range = [component for component in forced if component < 0 or component >= int(scores.shape[1])]
|
| 108 |
+
if out_of_range:
|
| 109 |
+
raise ValueError(f"highlight component out of range: {out_of_range[0]}")
|
| 110 |
+
|
| 111 |
+
ids = input_ids[0].detach().cpu().tolist()
|
| 112 |
+
idx_cpu = idx.detach().cpu().tolist()
|
| 113 |
+
vals_cpu = vals.detach().cpu().tolist()
|
| 114 |
+
forced_scores = scores[:, forced].detach().cpu().tolist() if forced else []
|
| 115 |
+
|
| 116 |
+
tokens = []
|
| 117 |
+
for pos, token_id in enumerate(ids):
|
| 118 |
+
top = [
|
| 119 |
+
{"component": int(idx_cpu[pos][j]), "score": float(vals_cpu[pos][j])}
|
| 120 |
+
for j in range(len(idx_cpu[pos]))
|
| 121 |
+
]
|
| 122 |
+
seen = {item["component"] for item in top}
|
| 123 |
+
for j, component in enumerate(forced):
|
| 124 |
+
if component not in seen:
|
| 125 |
+
top.append({"component": component, "score": float(forced_scores[pos][j]), "highlighted": True})
|
| 126 |
+
token_item = {
|
| 127 |
+
"position": pos,
|
| 128 |
+
"token_id": int(token_id),
|
| 129 |
+
"token": tokenizer.convert_ids_to_tokens(int(token_id)),
|
| 130 |
+
"token_text": _decode_token_text(tokenizer, int(token_id)),
|
| 131 |
+
"top": top,
|
| 132 |
+
}
|
| 133 |
+
if predictions is not None:
|
| 134 |
+
token_item["prediction"] = predictions[pos]
|
| 135 |
+
tokens.append(token_item)
|
| 136 |
+
|
| 137 |
+
return {
|
| 138 |
+
"layer": layer,
|
| 139 |
+
"top_k": int(top_k),
|
| 140 |
+
"max_length": int(max_length),
|
| 141 |
+
"tokens": tokens,
|
| 142 |
+
"seq_len": len(tokens),
|
| 143 |
+
"truncated": truncated,
|
| 144 |
+
"n_components": int(scores.shape[1]),
|
| 145 |
+
"predictions_available": predictions is not None,
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
@lru_cache(maxsize=None)
|
| 150 |
+
def _load_fastica_artifact(path: Path) -> dict[str, torch.Tensor | float]:
|
| 151 |
+
blob = torch.load(path, map_location="cpu")
|
| 152 |
+
tensors = blob["tensors"]
|
| 153 |
+
meta = blob.get("metadata") or {}
|
| 154 |
+
mean = tensors["mean"].to(torch.float32)
|
| 155 |
+
if mean.dim() == 1:
|
| 156 |
+
mean = mean.unsqueeze(0)
|
| 157 |
+
return {
|
| 158 |
+
"mean": mean,
|
| 159 |
+
"components": tensors["components"].to(torch.float32),
|
| 160 |
+
"norm_eps": float(meta.get("norm_eps", 1e-12)),
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def _all_source_scores(
|
| 165 |
+
activations: torch.Tensor,
|
| 166 |
+
*,
|
| 167 |
+
mean: torch.Tensor,
|
| 168 |
+
components: torch.Tensor,
|
| 169 |
+
norm_eps: float,
|
| 170 |
+
) -> torch.Tensor:
|
| 171 |
+
x = activations.to(dtype=torch.float32)
|
| 172 |
+
normalized = x / torch.linalg.vector_norm(x, dim=1, keepdim=True).clamp_min(norm_eps)
|
| 173 |
+
return (normalized - mean.to(x.device)) @ components.to(x.device).T
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def _topk_components_per_token(scores: torch.Tensor, *, top_k: int) -> tuple[torch.Tensor, torch.Tensor]:
|
| 177 |
+
k = min(int(top_k), int(scores.shape[1]))
|
| 178 |
+
if k <= 0:
|
| 179 |
+
raise ValueError("top_k must be positive.")
|
| 180 |
+
idx = torch.topk(scores.abs(), k, dim=1).indices
|
| 181 |
+
row_idx = torch.arange(scores.shape[0], device=scores.device, dtype=torch.long).unsqueeze(1).expand_as(idx)
|
| 182 |
+
return idx, scores[row_idx, idx]
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def _layer_key_to_hidden_index(layer: str, *, num_transformer_layers: int) -> int:
|
| 186 |
+
if layer == "embedding":
|
| 187 |
+
return 0
|
| 188 |
+
if not layer.startswith("layer_"):
|
| 189 |
+
raise ValueError(f"Unknown layer key: {layer!r}")
|
| 190 |
+
idx = int(layer.split("_", maxsplit=1)[1])
|
| 191 |
+
if idx < 0 or idx >= num_transformer_layers:
|
| 192 |
+
raise ValueError(f"Layer index out of range: {layer!r}")
|
| 193 |
+
return idx + 1
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
def _layer_sort_key(layer: str) -> tuple[int, int | str]:
|
| 197 |
+
if layer == "embedding":
|
| 198 |
+
return (0, 0)
|
| 199 |
+
if layer.startswith("layer_"):
|
| 200 |
+
return (1, int(layer.removeprefix("layer_")))
|
| 201 |
+
return (2, layer)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def _decode_token_text(tokenizer: Any, token_id: int) -> str:
|
| 205 |
+
try:
|
| 206 |
+
decoded = str(tokenizer.decode([token_id], skip_special_tokens=False, clean_up_tokenization_spaces=False))
|
| 207 |
+
except Exception:
|
| 208 |
+
decoded = ""
|
| 209 |
+
raw_token = str(tokenizer.convert_ids_to_tokens(token_id))
|
| 210 |
+
if "\ufffd" in decoded:
|
| 211 |
+
byte_text = _decode_byte_level_token(raw_token)
|
| 212 |
+
if byte_text is not None:
|
| 213 |
+
return byte_text
|
| 214 |
+
return decoded or raw_token
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
def _decode_byte_level_token(token: str) -> str | None:
|
| 218 |
+
byte_decoder = _gpt2_byte_decoder()
|
| 219 |
+
try:
|
| 220 |
+
raw = bytes(byte_decoder[ch] for ch in token)
|
| 221 |
+
except KeyError:
|
| 222 |
+
return None
|
| 223 |
+
try:
|
| 224 |
+
return raw.decode("utf-8")
|
| 225 |
+
except UnicodeDecodeError:
|
| 226 |
+
return "".join(f"\\x{byte:02X}" for byte in raw)
|
| 227 |
+
|
| 228 |
+
|
| 229 |
+
def _gpt2_byte_decoder() -> dict[str, int]:
|
| 230 |
+
bs = (
|
| 231 |
+
list(range(ord("!"), ord("~") + 1))
|
| 232 |
+
+ list(range(ord("¡"), ord("¬") + 1))
|
| 233 |
+
+ list(range(ord("®"), ord("ÿ") + 1))
|
| 234 |
+
)
|
| 235 |
+
cs = bs[:]
|
| 236 |
+
n = 0
|
| 237 |
+
for byte in range(256):
|
| 238 |
+
if byte not in bs:
|
| 239 |
+
bs.append(byte)
|
| 240 |
+
cs.append(256 + n)
|
| 241 |
+
n += 1
|
| 242 |
+
return {chr(char): byte for byte, char in zip(bs, cs, strict=True)}
|
server/sae_probe.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import tomllib
|
| 4 |
+
from functools import lru_cache
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import torch
|
| 10 |
+
from huggingface_hub import hf_hub_download
|
| 11 |
+
from safetensors.torch import load_file
|
| 12 |
+
|
| 13 |
+
from .probe import _decode_token_text, _layer_key_to_hidden_index
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
V6_ROOT = Path(__file__).resolve().parents[1]
|
| 17 |
+
CONFIG_ROOT = V6_ROOT / "configs" / "comparisons"
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def list_sae_layers(model_name: str) -> list[str]:
|
| 21 |
+
config = load_sae_config(model_name)
|
| 22 |
+
model_config = load_comparison_config(model_name).get("model", {})
|
| 23 |
+
n_layers = int(model_config.get("num_hidden_layers") or 0)
|
| 24 |
+
layer_checkpoints = config.get("layer_checkpoints")
|
| 25 |
+
if isinstance(layer_checkpoints, dict) and layer_checkpoints:
|
| 26 |
+
indices = sorted(int(key) for key in layer_checkpoints)
|
| 27 |
+
else:
|
| 28 |
+
indices = list(range(n_layers))
|
| 29 |
+
return [f"layer_{idx:02d}" for idx in indices]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def load_comparison_config(model_name: str) -> dict[str, Any]:
|
| 33 |
+
path = CONFIG_ROOT / f"{model_name}.toml"
|
| 34 |
+
if not path.is_file():
|
| 35 |
+
raise FileNotFoundError(f"No SAE comparison config for {model_name!r}: {path}")
|
| 36 |
+
return tomllib.loads(path.read_text(encoding="utf-8"))
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def load_sae_config(model_name: str) -> dict[str, Any]:
|
| 40 |
+
data = load_comparison_config(model_name)
|
| 41 |
+
if "sae" not in data:
|
| 42 |
+
raise KeyError(f"Comparison config for {model_name!r} has no [sae] section.")
|
| 43 |
+
return dict(data["sae"])
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def interpret_text_sae_probe(
|
| 47 |
+
*,
|
| 48 |
+
model: torch.nn.Module,
|
| 49 |
+
tokenizer: Any,
|
| 50 |
+
text: str,
|
| 51 |
+
model_name: str,
|
| 52 |
+
layer: str,
|
| 53 |
+
top_k: int,
|
| 54 |
+
max_length: int,
|
| 55 |
+
) -> dict[str, Any]:
|
| 56 |
+
if layer == "embedding":
|
| 57 |
+
raise ValueError("SAE Explorer currently supports transformer layers, not embedding.")
|
| 58 |
+
layer_index = int(layer.removeprefix("layer_"))
|
| 59 |
+
hidden_index = _layer_key_to_hidden_index(layer, num_transformer_layers=int(model.config.num_hidden_layers))
|
| 60 |
+
full_ids = tokenizer.encode(text, add_special_tokens=True)
|
| 61 |
+
truncated = len(full_ids) > max_length
|
| 62 |
+
encoded = tokenizer(text, return_tensors="pt", truncation=True, max_length=max_length)
|
| 63 |
+
input_ids = encoded["input_ids"].to(next(model.parameters()).device)
|
| 64 |
+
attention_mask = encoded.get("attention_mask")
|
| 65 |
+
if attention_mask is not None:
|
| 66 |
+
attention_mask = attention_mask.to(input_ids.device)
|
| 67 |
+
|
| 68 |
+
with torch.inference_mode():
|
| 69 |
+
outputs = model(
|
| 70 |
+
input_ids=input_ids,
|
| 71 |
+
attention_mask=attention_mask,
|
| 72 |
+
output_hidden_states=True,
|
| 73 |
+
use_cache=False,
|
| 74 |
+
)
|
| 75 |
+
hidden_states = outputs.hidden_states
|
| 76 |
+
if hidden_states is None:
|
| 77 |
+
raise RuntimeError("Model did not return hidden states.")
|
| 78 |
+
hidden = hidden_states[hidden_index][0].to(dtype=torch.float32)
|
| 79 |
+
sae = load_sae(model_name, layer_index, hidden_size=int(hidden.shape[-1]), device=hidden.device)
|
| 80 |
+
pre, acts = sae_forward(hidden, sae=sae)
|
| 81 |
+
idx, vals = topk_features(acts, top_k=top_k)
|
| 82 |
+
|
| 83 |
+
ids = input_ids[0].detach().cpu().tolist()
|
| 84 |
+
idx_cpu = idx.detach().cpu().tolist()
|
| 85 |
+
vals_cpu = vals.detach().cpu().tolist()
|
| 86 |
+
pre_cpu = pre.detach().cpu()
|
| 87 |
+
tokens = []
|
| 88 |
+
for pos, token_id in enumerate(ids):
|
| 89 |
+
top = []
|
| 90 |
+
for j in range(len(idx_cpu[pos])):
|
| 91 |
+
feature = int(idx_cpu[pos][j])
|
| 92 |
+
top.append(
|
| 93 |
+
{
|
| 94 |
+
"feature": feature,
|
| 95 |
+
"activation": float(vals_cpu[pos][j]),
|
| 96 |
+
"preactivation": float(pre_cpu[pos, feature].item()),
|
| 97 |
+
}
|
| 98 |
+
)
|
| 99 |
+
tokens.append(
|
| 100 |
+
{
|
| 101 |
+
"position": pos,
|
| 102 |
+
"token_id": int(token_id),
|
| 103 |
+
"token": tokenizer.convert_ids_to_tokens(int(token_id)),
|
| 104 |
+
"token_text": _decode_token_text(tokenizer, int(token_id)),
|
| 105 |
+
"top": top,
|
| 106 |
+
}
|
| 107 |
+
)
|
| 108 |
+
return {
|
| 109 |
+
"model_name": model_name,
|
| 110 |
+
"layer": layer,
|
| 111 |
+
"top_k": int(top_k),
|
| 112 |
+
"max_length": int(max_length),
|
| 113 |
+
"tokens": tokens,
|
| 114 |
+
"seq_len": len(tokens),
|
| 115 |
+
"truncated": truncated,
|
| 116 |
+
"sae_width": int(sae["w_enc"].shape[1]),
|
| 117 |
+
"activation_rule": sae["activation_rule"],
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
@lru_cache(maxsize=64)
|
| 122 |
+
def _load_sae_cpu(model_name: str, layer: int, hidden_size: int) -> dict[str, Any]:
|
| 123 |
+
config = load_sae_config(model_name)
|
| 124 |
+
path = _resolve_sae_weights_path(config, layer=layer)
|
| 125 |
+
weights = _load_sae_weights(path, config=config)
|
| 126 |
+
w_dec = _decoder_weight(weights, hidden_size=hidden_size, preferred_key=config.get("decoder_key")).to(dtype=torch.float32)
|
| 127 |
+
w_enc = _encoder_weight(weights, hidden_size=hidden_size, d_sae=int(w_dec.shape[0])).to(dtype=torch.float32).contiguous()
|
| 128 |
+
b_enc = _vector_weight(weights, names=("b_enc", "encoder.bias"), length=int(w_enc.shape[1]))
|
| 129 |
+
b_dec = _vector_weight(weights, names=("b_dec", "decoder.bias", "bias"), length=hidden_size)
|
| 130 |
+
threshold = weights.get("threshold")
|
| 131 |
+
return {
|
| 132 |
+
"w_enc": w_enc,
|
| 133 |
+
"b_enc": b_enc.to(dtype=torch.float32).contiguous(),
|
| 134 |
+
"b_dec": b_dec.to(dtype=torch.float32).contiguous(),
|
| 135 |
+
"threshold": threshold.to(dtype=torch.float32).contiguous() if threshold is not None else None,
|
| 136 |
+
"top_k": _sae_top_k(config),
|
| 137 |
+
"activation_rule": _activation_rule_label(config),
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
def load_sae(model_name: str, layer: int, *, hidden_size: int, device: torch.device) -> dict[str, Any]:
|
| 142 |
+
cpu = _load_sae_cpu(model_name, layer, hidden_size)
|
| 143 |
+
return {
|
| 144 |
+
key: value.to(device=device, dtype=torch.float32, non_blocking=True) if isinstance(value, torch.Tensor) else value
|
| 145 |
+
for key, value in cpu.items()
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def sae_forward(hidden: torch.Tensor, *, sae: dict[str, Any]) -> tuple[torch.Tensor, torch.Tensor]:
|
| 150 |
+
centered = hidden.to(dtype=torch.float32) - sae["b_dec"].unsqueeze(0)
|
| 151 |
+
pre = centered @ sae["w_enc"] + sae["b_enc"].unsqueeze(0)
|
| 152 |
+
threshold = sae["threshold"]
|
| 153 |
+
if threshold is not None:
|
| 154 |
+
return pre, torch.where(pre > threshold.unsqueeze(0), pre, torch.zeros_like(pre))
|
| 155 |
+
top_k = sae["top_k"]
|
| 156 |
+
if top_k is not None:
|
| 157 |
+
k = min(int(top_k), int(pre.shape[1]))
|
| 158 |
+
values, indices = torch.topk(pre, k=k, dim=1)
|
| 159 |
+
acts = torch.zeros_like(pre)
|
| 160 |
+
acts.scatter_(1, indices, torch.clamp(values, min=0.0))
|
| 161 |
+
return pre, acts
|
| 162 |
+
return pre, torch.relu(pre)
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def topk_features(acts: torch.Tensor, *, top_k: int) -> tuple[torch.Tensor, torch.Tensor]:
|
| 166 |
+
k = min(int(top_k), int(acts.shape[1]))
|
| 167 |
+
if k <= 0:
|
| 168 |
+
raise ValueError("top_k must be positive.")
|
| 169 |
+
vals, idx = torch.topk(acts, k=k, dim=1)
|
| 170 |
+
return idx, vals
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def _resolve_sae_weights_path(config: dict[str, Any], *, layer: int) -> str:
|
| 174 |
+
layer_checkpoints = config.get("layer_checkpoints")
|
| 175 |
+
if isinstance(layer_checkpoints, dict) and str(layer) in layer_checkpoints:
|
| 176 |
+
filename = str(layer_checkpoints[str(layer)])
|
| 177 |
+
else:
|
| 178 |
+
filename = str(config["checkpoint_template"]).format(layer=layer)
|
| 179 |
+
return hf_hub_download(repo_id=str(config["repo_id"]), filename=filename, revision=config.get("revision"))
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
def _load_sae_weights(path: str, *, config: dict[str, Any]) -> dict[str, torch.Tensor]:
|
| 183 |
+
checkpoint_format = str(config.get("checkpoint_format", "")).lower()
|
| 184 |
+
if checkpoint_format == "safetensors":
|
| 185 |
+
return load_file(path, device="cpu")
|
| 186 |
+
if checkpoint_format == "npz":
|
| 187 |
+
with np.load(path) as arrays:
|
| 188 |
+
return {key: torch.from_numpy(arrays[key]) for key in arrays.files}
|
| 189 |
+
if checkpoint_format in {"torch", "pt", "pth"}:
|
| 190 |
+
try:
|
| 191 |
+
loaded = torch.load(path, map_location="cpu", weights_only=True)
|
| 192 |
+
except TypeError:
|
| 193 |
+
loaded = torch.load(path, map_location="cpu")
|
| 194 |
+
if not isinstance(loaded, dict):
|
| 195 |
+
raise TypeError(f"Expected torch checkpoint {path} to contain a dict, got {type(loaded).__name__}.")
|
| 196 |
+
return _flatten_tensor_dict(loaded)
|
| 197 |
+
raise ValueError(f"Unsupported SAE checkpoint_format {checkpoint_format!r}.")
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def _flatten_tensor_dict(payload: dict[str, Any], prefix: str = "") -> dict[str, torch.Tensor]:
|
| 201 |
+
flattened: dict[str, torch.Tensor] = {}
|
| 202 |
+
for key, value in payload.items():
|
| 203 |
+
name = f"{prefix}.{key}" if prefix else str(key)
|
| 204 |
+
if isinstance(value, torch.Tensor):
|
| 205 |
+
flattened[name] = value
|
| 206 |
+
flattened[str(key)] = value
|
| 207 |
+
elif isinstance(value, dict):
|
| 208 |
+
flattened.update(_flatten_tensor_dict(value, name))
|
| 209 |
+
return flattened
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def _encoder_weight(weights: dict[str, torch.Tensor], *, hidden_size: int, d_sae: int) -> torch.Tensor:
|
| 213 |
+
for key in ("W_enc", "encoder.weight", "W_enc.weight", "encoder.W_enc"):
|
| 214 |
+
tensor = weights.get(key)
|
| 215 |
+
if tensor is not None:
|
| 216 |
+
return _orient_encoder(tensor, hidden_size=hidden_size, d_sae=d_sae)
|
| 217 |
+
return _decoder_weight(weights, hidden_size=hidden_size).T.contiguous()
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
def _decoder_weight(
|
| 221 |
+
weights: dict[str, torch.Tensor],
|
| 222 |
+
*,
|
| 223 |
+
hidden_size: int,
|
| 224 |
+
preferred_key: object = None,
|
| 225 |
+
) -> torch.Tensor:
|
| 226 |
+
if preferred_key is not None:
|
| 227 |
+
key = str(preferred_key)
|
| 228 |
+
if key not in weights:
|
| 229 |
+
raise KeyError(f"Configured decoder_key {key!r} not found. Available keys: {sorted(weights)}")
|
| 230 |
+
return _orient_decoder(weights[key], hidden_size=hidden_size)
|
| 231 |
+
for key in ("W_dec", "decoder.weight", "W_dec.weight", "decoder.W_dec"):
|
| 232 |
+
tensor = weights.get(key)
|
| 233 |
+
if tensor is not None:
|
| 234 |
+
return _orient_decoder(tensor, hidden_size=hidden_size)
|
| 235 |
+
candidates = [tensor for tensor in weights.values() if tensor.ndim == 2 and hidden_size in tensor.shape]
|
| 236 |
+
if len(candidates) == 1:
|
| 237 |
+
return _orient_decoder(candidates[0], hidden_size=hidden_size)
|
| 238 |
+
raise KeyError(f"Could not identify SAE decoder weight. Available keys: {sorted(weights)}")
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
def _vector_weight(weights: dict[str, torch.Tensor], *, names: tuple[str, ...], length: int) -> torch.Tensor:
|
| 242 |
+
for name in names:
|
| 243 |
+
tensor = weights.get(name)
|
| 244 |
+
if tensor is not None:
|
| 245 |
+
if int(tensor.numel()) != int(length):
|
| 246 |
+
raise ValueError(f"SAE vector {name!r} has length {tensor.numel()}, expected {length}.")
|
| 247 |
+
return tensor.reshape(length).to(dtype=torch.float32)
|
| 248 |
+
return torch.zeros(length, dtype=torch.float32)
|
| 249 |
+
|
| 250 |
+
|
| 251 |
+
def _orient_decoder(tensor: torch.Tensor, *, hidden_size: int) -> torch.Tensor:
|
| 252 |
+
if int(tensor.shape[1]) == hidden_size:
|
| 253 |
+
return tensor
|
| 254 |
+
if int(tensor.shape[0]) == hidden_size:
|
| 255 |
+
return tensor.T
|
| 256 |
+
raise ValueError(f"Decoder weight shape {tuple(tensor.shape)} does not contain hidden size {hidden_size}.")
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
def _orient_encoder(tensor: torch.Tensor, *, hidden_size: int, d_sae: int) -> torch.Tensor:
|
| 260 |
+
if tuple(tensor.shape) == (hidden_size, d_sae):
|
| 261 |
+
return tensor
|
| 262 |
+
if tuple(tensor.shape) == (d_sae, hidden_size):
|
| 263 |
+
return tensor.T
|
| 264 |
+
if int(tensor.shape[0]) == hidden_size:
|
| 265 |
+
return tensor
|
| 266 |
+
if int(tensor.shape[1]) == hidden_size:
|
| 267 |
+
return tensor.T
|
| 268 |
+
raise ValueError(f"Encoder weight shape {tuple(tensor.shape)} does not match hidden={hidden_size}, d_sae={d_sae}.")
|
| 269 |
+
|
| 270 |
+
|
| 271 |
+
def _sae_top_k(config: dict[str, Any]) -> int | None:
|
| 272 |
+
if config.get("top_k") is not None:
|
| 273 |
+
return int(config["top_k"])
|
| 274 |
+
repo = str(config.get("repo_id") or "")
|
| 275 |
+
if "GPT2-Small-OAI-v5-32k-resid-post-SAEs" in repo:
|
| 276 |
+
return 32
|
| 277 |
+
return None
|
| 278 |
+
|
| 279 |
+
|
| 280 |
+
def _activation_rule_label(config: dict[str, Any]) -> str:
|
| 281 |
+
if config.get("top_k") is not None:
|
| 282 |
+
return f"topk-{int(config['top_k'])}"
|
| 283 |
+
if str(config.get("activation") or "").lower() == "topk":
|
| 284 |
+
return "topk"
|
| 285 |
+
return "relu_or_threshold"
|
server/static/annotate.html
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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" />
|
| 6 |
+
<title>ICA Annotator</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root { --bg:#f8fafc; --panel:#fff; --subtle:#f1f5f9; --text:#0f172a; --muted:#64748b; --border:#cbd5e1; --accent:#2563eb; --good:#059669; --bad:#dc2626; --shadow:0 1px 2px rgb(15 23 42 / .08),0 8px 24px rgb(15 23 42 / .05); }
|
| 9 |
+
* { box-sizing:border-box; }
|
| 10 |
+
body { margin:0; background:var(--bg); color:var(--text); font:14px/1.45 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
|
| 11 |
+
header { position:sticky; top:0; z-index:5; background:#fff; border-bottom:1px solid var(--border); padding:12px 16px; box-shadow:var(--shadow); }
|
| 12 |
+
.top { display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap; }
|
| 13 |
+
h1 { margin:0; font-size:20px; }
|
| 14 |
+
nav { display:flex; gap:6px; }
|
| 15 |
+
nav a { color:var(--muted); text-decoration:none; padding:7px 10px; border-radius:7px; font-weight:650; }
|
| 16 |
+
nav a:hover, nav a.active { color:var(--accent); background:#eff6ff; }
|
| 17 |
+
.toolbar { display:flex; align-items:end; flex-wrap:wrap; gap:10px; margin-top:10px; }
|
| 18 |
+
label { color:#475569; font-size:12px; font-weight:700; }
|
| 19 |
+
select,input,textarea,button { font:inherit; border:1px solid var(--border); border-radius:7px; padding:7px 9px; background:#fff; color:var(--text); }
|
| 20 |
+
textarea { width:100%; resize:vertical; }
|
| 21 |
+
button { cursor:pointer; background:var(--subtle); }
|
| 22 |
+
button:hover { color:var(--accent); border-color:var(--accent); background:#eff6ff; }
|
| 23 |
+
.primary { color:#fff; background:var(--accent); border-color:#1d4ed8; font-weight:800; }
|
| 24 |
+
.primary:hover { color:#fff; background:#1d4ed8; }
|
| 25 |
+
main { display:grid; grid-template-columns:320px minmax(0,1fr) 360px; gap:14px; padding:14px; }
|
| 26 |
+
aside,.panel { background:var(--panel); border:1px solid var(--border); border-radius:8px; box-shadow:var(--shadow); }
|
| 27 |
+
aside { max-height:calc(100vh - 108px); overflow:auto; padding:10px; position:sticky; top:94px; }
|
| 28 |
+
.panel { padding:12px; }
|
| 29 |
+
.panel-title-row { display:flex; align-items:center; justify-content:space-between; gap:10px; margin-bottom:10px; }
|
| 30 |
+
.panel-title-row h2 { margin:0; }
|
| 31 |
+
.component-metrics { display:flex; flex-wrap:wrap; gap:6px; justify-content:flex-end; }
|
| 32 |
+
.metric-badge { display:inline-flex; align-items:center; min-height:24px; border:1px solid var(--border); border-radius:999px; padding:2px 8px; background:#f8fafc; color:#475569; font-size:12px; font-weight:800; font-variant-numeric:tabular-nums; }
|
| 33 |
+
.component-list { display:flex; flex-direction:column; gap:7px; margin-top:10px; }
|
| 34 |
+
.component-button { text-align:left; background:#fff; width:100%; border-radius:7px; }
|
| 35 |
+
.component-button.active { border-color:var(--accent); box-shadow:inset 3px 0 0 var(--accent); }
|
| 36 |
+
.component-title { display:flex; justify-content:space-between; gap:8px; font-weight:800; }
|
| 37 |
+
.label-line { color:#334155; margin-top:4px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
| 38 |
+
.muted { color:var(--muted); }
|
| 39 |
+
.pill { display:inline-flex; border:1px solid var(--border); border-radius:999px; padding:1px 7px; color:var(--muted); font-size:12px; }
|
| 40 |
+
.examples { display:grid; grid-template-columns:repeat(auto-fit,minmax(310px,1fr)); gap:10px; }
|
| 41 |
+
.example { border:1px solid #e2e8f0; border-radius:8px; padding:10px; background:#fff; }
|
| 42 |
+
.example-head { display:flex; justify-content:space-between; gap:8px; color:var(--muted); font-size:12px; margin-bottom:8px; }
|
| 43 |
+
.example-meta { display:flex; flex-wrap:wrap; align-items:center; gap:6px; min-width:0; }
|
| 44 |
+
.doc-link { color:#2563eb; text-decoration:none; font-weight:800; }
|
| 45 |
+
.doc-link:hover { text-decoration:underline; }
|
| 46 |
+
.score { font-weight:850; color:#b91c1c; font-variant-numeric:tabular-nums; }
|
| 47 |
+
.score.neg { color:#1d4ed8; }
|
| 48 |
+
.token { background:#fee2e2; color:#7f1d1d; border-radius:5px; padding:1px 5px; font-weight:800; }
|
| 49 |
+
pre { white-space:pre-wrap; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; margin:8px 0 0; }
|
| 50 |
+
.context { margin:8px 0 0; white-space:pre-wrap; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:14px; line-height:1.45; }
|
| 51 |
+
.context-token { border-radius:3px; padding:0 1px; }
|
| 52 |
+
.context-token.target { outline:1px solid rgb(15 23 42 / .28); outline-offset:1px; }
|
| 53 |
+
.tabs { display:flex; flex-wrap:wrap; gap:7px; margin:10px 0; }
|
| 54 |
+
.tabs button.active { color:#fff; background:var(--accent); border-color:var(--accent); }
|
| 55 |
+
.editor { position:sticky; top:94px; max-height:calc(100vh - 108px); overflow:auto; }
|
| 56 |
+
.editor h2,.panel h2 { margin:0 0 10px; font-size:16px; }
|
| 57 |
+
.neighbor-row { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-bottom:12px; }
|
| 58 |
+
.neighbor-card { display:grid; gap:4px; min-height:68px; border:1px solid var(--border); border-radius:8px; padding:8px; background:#fff; color:var(--text); text-decoration:none; }
|
| 59 |
+
.neighbor-card:hover { border-color:var(--accent); color:var(--text); text-decoration:none; background:#eff6ff; }
|
| 60 |
+
.neighbor-card.missing { visibility:hidden; }
|
| 61 |
+
.neighbor-head { display:flex; justify-content:space-between; align-items:start; gap:8px; }
|
| 62 |
+
.neighbor-target { min-width:0; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; color:var(--muted); font-size:11px; font-weight:750; }
|
| 63 |
+
.neighbor-link { min-width:0; color:var(--muted); text-decoration:none; }
|
| 64 |
+
.neighbor-link:hover { color:var(--accent); text-decoration:underline; }
|
| 65 |
+
.neighbor-copy { width:auto; min-height:24px; padding:2px 7px; border-radius:6px; font-size:11px; font-weight:800; white-space:nowrap; }
|
| 66 |
+
.neighbor-label { min-width:0; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; color:var(--text); font-size:14px; font-weight:850; line-height:1.2; }
|
| 67 |
+
.neighbor-metrics { color:var(--muted); font-size:11px; font-weight:500; font-variant-numeric:tabular-nums; }
|
| 68 |
+
.neighbor-cos { min-height:20px; border:1px solid #d7dee9; border-radius:5px; padding:2px 6px; background:linear-gradient(90deg,rgb(37 99 235 / .14) 0,rgb(37 99 235 / .14) var(--cos-width,0%),transparent var(--cos-width,0%),transparent 100%); color:var(--muted); font-size:12px; font-weight:800; font-variant-numeric:tabular-nums; }
|
| 69 |
+
.field { margin-bottom:10px; }
|
| 70 |
+
.direction-block { border:1px solid #e2e8f0; border-left-width:4px; border-radius:8px; padding:10px; margin-bottom:10px; }
|
| 71 |
+
.direction-block.positive { border-left-color:#e2e8f0; }
|
| 72 |
+
.direction-block.negative { border-left-color:#e2e8f0; }
|
| 73 |
+
.direction-block.positive.active-direction { border-left-color:#dc2626; background:#fef2f2; }
|
| 74 |
+
.direction-block.negative.active-direction { border-left-color:#2563eb; background:#eff6ff; }
|
| 75 |
+
.direction-block h3 { margin:0 0 9px; font-size:14px; }
|
| 76 |
+
.field-grid { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:8px; }
|
| 77 |
+
.field-grid select { width:100%; min-width:0; }
|
| 78 |
+
.label-confidence-high { border-color:#16a34a; box-shadow:0 0 0 2px rgb(22 163 74 / .12); }
|
| 79 |
+
.label-confidence-high:focus { border-color:#16a34a; outline:2px solid rgb(22 163 74 / .22); outline-offset:1px; box-shadow:0 0 0 2px rgb(22 163 74 / .12); }
|
| 80 |
+
.label-confidence-medium { border-color:#d9b94e; box-shadow:0 0 0 2px rgb(217 185 78 / .16); }
|
| 81 |
+
.label-confidence-medium:focus { border-color:#d9b94e; outline:2px solid rgb(217 185 78 / .25); outline-offset:1px; box-shadow:0 0 0 2px rgb(217 185 78 / .16); }
|
| 82 |
+
.label-confidence-low { border-color:#f9a8d4; box-shadow:0 0 0 2px rgb(249 168 212 / .18); }
|
| 83 |
+
.label-confidence-low:focus { border-color:#f9a8d4; outline:2px solid rgb(249 168 212 / .28); outline-offset:1px; box-shadow:0 0 0 2px rgb(249 168 212 / .18); }
|
| 84 |
+
.label-confidence-unclear { border-color:#cbd5e1; box-shadow:none; }
|
| 85 |
+
.label-confidence-unclear:focus { border-color:#94a3b8; outline:2px solid rgb(148 163 184 / .28); outline-offset:1px; box-shadow:none; }
|
| 86 |
+
.status { margin-left:auto; color:var(--muted); }
|
| 87 |
+
.empty,.error { border:1px dashed var(--border); border-radius:8px; padding:18px; color:var(--muted); background:#fff; }
|
| 88 |
+
.error { color:#991b1b; border-color:#fecaca; background:#fef2f2; }
|
| 89 |
+
@media(max-width:1050px){ main{grid-template-columns:1fr;} aside,.editor{position:static;max-height:none;} }
|
| 90 |
+
</style>
|
| 91 |
+
</head>
|
| 92 |
+
<body>
|
| 93 |
+
<header>
|
| 94 |
+
<div class="top">
|
| 95 |
+
<h1>ICA Annotator</h1>
|
| 96 |
+
<nav>
|
| 97 |
+
<a href="/">Explorer</a>
|
| 98 |
+
<a href="/sae-explorer">SAE Explorer</a>
|
| 99 |
+
<a href="/stats">Stats</a>
|
| 100 |
+
<a href="/annotate" class="active">Annotate</a>
|
| 101 |
+
<a href="/random-components">Random</a>
|
| 102 |
+
</nav>
|
| 103 |
+
</div>
|
| 104 |
+
<div class="toolbar">
|
| 105 |
+
<label>Model<br><select id="model"></select></label>
|
| 106 |
+
<label>Layer<br><select id="layer"></select></label>
|
| 107 |
+
<label style="flex:1;min-width:220px">Search<br><input id="search" type="search" placeholder="component id or label" /></label>
|
| 108 |
+
<button id="refresh">Refresh</button>
|
| 109 |
+
<span id="status" class="status"></span>
|
| 110 |
+
</div>
|
| 111 |
+
</header>
|
| 112 |
+
<main>
|
| 113 |
+
<aside>
|
| 114 |
+
<div class="muted" id="count">Loading...</div>
|
| 115 |
+
<div id="components" class="component-list"></div>
|
| 116 |
+
</aside>
|
| 117 |
+
<section class="panel">
|
| 118 |
+
<div class="panel-title-row">
|
| 119 |
+
<h2 id="detailTitle">Examples</h2>
|
| 120 |
+
<div id="componentMetrics" class="component-metrics"></div>
|
| 121 |
+
</div>
|
| 122 |
+
<div id="tabs" class="tabs"></div>
|
| 123 |
+
<div id="examples" class="examples"><div class="empty">Choose a component.</div></div>
|
| 124 |
+
</section>
|
| 125 |
+
<section class="panel editor">
|
| 126 |
+
<div id="neighborCards" class="neighbor-row" hidden></div>
|
| 127 |
+
<h2>Manual Annotation</h2>
|
| 128 |
+
<div id="editorEmpty" class="empty">Choose a component to annotate.</div>
|
| 129 |
+
<form id="form" hidden>
|
| 130 |
+
<div id="positiveBlock" class="direction-block positive">
|
| 131 |
+
<h3>Positive direction</h3>
|
| 132 |
+
<div class="field"><label>Label<br><textarea id="positiveLabel" rows="2"></textarea></label></div>
|
| 133 |
+
<div class="field-grid">
|
| 134 |
+
<label>Confidence<br><select id="positiveConfidence"></select></label>
|
| 135 |
+
<label>Interpretation type<br><select id="positiveTypes"></select></label>
|
| 136 |
+
</div>
|
| 137 |
+
</div>
|
| 138 |
+
<div id="negativeBlock" class="direction-block negative">
|
| 139 |
+
<h3>Negative direction</h3>
|
| 140 |
+
<div class="field"><label>Label<br><textarea id="negativeLabel" rows="2"></textarea></label></div>
|
| 141 |
+
<div class="field-grid">
|
| 142 |
+
<label>Confidence<br><select id="negativeConfidence"></select></label>
|
| 143 |
+
<label>Interpretation type<br><select id="negativeTypes"></select></label>
|
| 144 |
+
</div>
|
| 145 |
+
</div>
|
| 146 |
+
<div class="field"><label>Summary<br><textarea id="summary" rows="3"></textarea></label></div>
|
| 147 |
+
<div class="field"><label>Notes<br><textarea id="notes" rows="4"></textarea></label></div>
|
| 148 |
+
<div class="field"><label style="display:flex;gap:6px;align-items:center;color:var(--text);font-weight:600"><input id="caseStudy" type="checkbox"> include as case study</label></div>
|
| 149 |
+
<button class="primary" type="submit">Save annotation</button>
|
| 150 |
+
</form>
|
| 151 |
+
</section>
|
| 152 |
+
</main>
|
| 153 |
+
<script>
|
| 154 |
+
const TYPES = ["Form", "Word", "Phrase", "Sentence", "Long-Range Context", "Global", "Position", "Sophisticated"];
|
| 155 |
+
const CONF = ["high", "medium", "low", "unclear"];
|
| 156 |
+
const state = { model:"", layer:"", components:[], selected:null, examples:null, region:"", annotation:null, neighbors:null };
|
| 157 |
+
const el = Object.fromEntries(["model","layer","search","refresh","status","count","components","detailTitle","componentMetrics","tabs","examples","neighborCards","editorEmpty","form","positiveBlock","negativeBlock","positiveLabel","positiveConfidence","negativeLabel","negativeConfidence","positiveTypes","negativeTypes","summary","notes","caseStudy"].map(id=>[id,document.getElementById(id)]));
|
| 158 |
+
async function api(path, options){ const r=await fetch(path, options); if(!r.ok){let msg=await r.text(); try{msg=JSON.parse(msg).detail||msg}catch{} throw new Error(msg)} return r.json(); }
|
| 159 |
+
function esc(s){return String(s??"").replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">","\"":""","'":"'"}[c]));}
|
| 160 |
+
function scoreClass(v){return Number(v)<0?'score neg':'score';}
|
| 161 |
+
function setStatus(s){el.status.textContent=s||"";}
|
| 162 |
+
function fillSelect(select, values, current){select.innerHTML=values.map(v=>`<option value="${esc(v)}" ${v===current?'selected':''}>${esc(v)}</option>`).join('');}
|
| 163 |
+
function fillConfidence(){ for(const id of ['positiveConfidence','negativeConfidence']) fillSelect(el[id], CONF, 'unclear'); }
|
| 164 |
+
function fillTypes(){ for(const id of ['positiveTypes','negativeTypes']) fillSelect(el[id], ["", ...TYPES], ""); el.positiveTypes.options[0].textContent="none"; el.negativeTypes.options[0].textContent="none"; }
|
| 165 |
+
function selectedTypes(select){ return select.value ? [select.value] : []; }
|
| 166 |
+
function setTypes(select, values){ const first=(values||[]).find(Boolean)||""; select.value=first; }
|
| 167 |
+
async function init(){ fillConfidence(); fillTypes(); const params=new URLSearchParams(location.search); state.targetComponent=Number(params.get('component')); const requestedModel=params.get('model')||'gpt2'; const requestedLayer=params.get('layer')||''; const models=await api('/api/models'); fillSelect(el.model, models.models.map(m=>m.model_name), requestedModel); state.model=el.model.value; state.layer=requestedLayer; await loadLayers({component:Number.isFinite(state.targetComponent)?state.targetComponent:null}); bind(); }
|
| 168 |
+
function bind(){ el.model.onchange=async()=>{state.model=el.model.value; state.targetComponent=null; await loadLayers();}; el.layer.onchange=async()=>{state.layer=el.layer.value; state.targetComponent=null; await loadComponents();}; el.search.oninput=()=>renderComponents(); el.refresh.onclick=loadComponents; el.form.onsubmit=saveAnnotation; el.positiveConfidence.onchange=updateLabelConfidenceStyles; el.negativeConfidence.onchange=updateLabelConfidenceStyles; el.positiveBlock.onclick=()=>setActiveDirection('positive'); el.negativeBlock.onclick=()=>setActiveDirection('negative'); el.positiveBlock.onfocusin=()=>setActiveDirection('positive'); el.negativeBlock.onfocusin=()=>setActiveDirection('negative'); }
|
| 169 |
+
async function loadLayers(options={}){ const out=await api(`/api/layers?model=${encodeURIComponent(state.model)}`); fillSelect(el.layer,out.layers,out.layers.includes(state.layer)?state.layer:out.layers[0]); state.layer=el.layer.value; await loadComponents(options); }
|
| 170 |
+
async function loadComponents(options={}){ setStatus('Loading components...'); const out=await api(`/api/components?model=${encodeURIComponent(state.model)}&layer=${encodeURIComponent(state.layer)}`); state.components=out.components; state.selected=null; renderComponents(); clearDetail(); setStatus(''); const component=options.component; if(Number.isFinite(component) && state.components.some(c=>c.component===component)) await selectComponent(component); }
|
| 171 |
+
function renderComponents(){ const q=el.search.value.trim().toLowerCase(); const items=state.components.filter(c=>!q||String(c.component).includes(q)||[c.positive_label,c.negative_label,c.summary,c.notes].join(' ').toLowerCase().includes(q)); el.count.textContent=`${items.length} components`; el.components.innerHTML=items.map(c=>`<button class="component-button ${state.selected&&state.selected.component===c.component?'active':''}" data-c="${c.component}"><div class="component-title"><span>${esc(c.layer)} / ${c.component}</span><span class="pill">k=${c.excess_kurtosis==null?'?':Number(c.excess_kurtosis).toFixed(1)}</span></div><div class="label-line">+ ${esc(c.positive_label||'')} </div><div class="label-line">- ${esc(c.negative_label||'')}</div></button>`).join('') || '<div class="empty">No components.</div>'; el.components.querySelectorAll('button[data-c]').forEach(b=>b.onclick=()=>selectComponent(Number(b.dataset.c))); }
|
| 172 |
+
async function selectComponent(component){ state.selected=state.components.find(c=>c.component===component); renderComponents(); setStatus('Loading examples...'); const q=new URLSearchParams({model:state.model,layer:state.layer,component:String(component)}); const [examples, annotation, neighbors]=await Promise.all([api(`/api/examples/component?${q}`), api(`/api/annotations/component?${q}`), api(`/api/component-neighbors?${q}`)]); state.examples=examples; state.annotation=annotation; state.neighbors=neighbors; state.region=examples.default_region||examples.regions[0]||''; renderExamples(); renderEditor(); setStatus(''); }
|
| 173 |
+
function clearDetail(){ el.detailTitle.textContent='Examples'; el.componentMetrics.innerHTML=''; el.tabs.innerHTML=''; el.examples.innerHTML='<div class="empty">Choose a component.</div>'; el.neighborCards.hidden=true; el.neighborCards.innerHTML=''; el.form.hidden=true; el.editorEmpty.hidden=false; setActiveDirection(null); }
|
| 174 |
+
function renderExamples(){ const c=state.selected; el.detailTitle.textContent=`${state.model} ${state.layer} / ${c.component}`; renderComponentMetrics(c); el.tabs.innerHTML=(state.examples.regions||[]).map(r=>`<button class="${r===state.region?'active':''}" data-r="${esc(r)}">${esc(r)}</button>`).join(''); el.tabs.querySelectorAll('button').forEach(b=>b.onclick=()=>{state.region=b.dataset.r; renderExamples();}); const items=(state.examples.examples_by_region||{})[state.region]||[]; el.examples.innerHTML=items.map(renderExample).join('') || '<div class="empty">No examples in this band.</div>'; }
|
| 175 |
+
function renderExample(x){ return `<article class="example"><div class="example-head"><span class="example-meta"><span>#${esc(x.rank)}</span>${docPositionLink(x)}<span class="token">${esc(visibleToken(x.token))}</span></span><span class="${scoreClass(x.source_score)}">${Number(x.source_score||0).toFixed(3)}</span></div><div class="context">${renderContext(x)}</div></article>`; }
|
| 176 |
+
function docPositionLink(x){ if(x.doc_id==null||Number(x.doc_id)<0) return x.position==null?'':`<span>pos ${esc(x.position)}</span>`; const q=new URLSearchParams({model:state.model,doc_id:String(x.doc_id)}); if(x.position!=null) q.set('position',String(x.position)); const target=visibleToken(x.token); if(target) q.set('target',target); const text=`doc ${x.doc_id}${x.position==null?'':` · pos ${x.position}`}`; return `<a class="doc-link" href="/context?${esc(q.toString())}" target="_blank" rel="noopener" title="Open full document context">${esc(text)}</a>`; }
|
| 177 |
+
function renderContext(x){ const tokens=Array.isArray(x.context_token_scores)?x.context_token_scores:[]; if(!tokens.length) return esc(x.context_to_target||x.context||''); const maxAbs=Math.max(1e-9, Number(x.context_score_max_abs||0), ...tokens.map(t=>Math.abs(Number(t.source_score||0)))); return tokens.map(t=>{ const score=Number(t.source_score||0); const alpha=tokenAlpha(score,maxAbs); const color=alpha===0?'transparent':score>0?`rgba(220,38,38,${alpha})`:`rgba(37,99,235,${alpha})`; const title=`score=${Number.isFinite(score)?score.toFixed(4):'n/a'}`; return `<span class="context-token ${t.is_target?'target':''}" style="background:${color}" title="${esc(title)}">${esc(visibleContextToken(t.token))}</span>`; }).join(''); }
|
| 178 |
+
function tokenAlpha(score,maxAbs){ const magnitude=Math.abs(Number(score||0)); if(magnitude<1e-6) return 0; return Math.min(0.55,0.47*Math.log1p(magnitude)/Math.log1p(maxAbs)); }
|
| 179 |
+
function visibleToken(value){ const text=String(value||''); if(text===' ') return '[space]'; if(text==='\n') return '[newline]'; if(text==='\f') return '\\f'; return text.replace(/\r/g,'\\r').replace(/\n/g,'\\n').replace(/\t/g,'\\t').replace(/\f/g,'\\f'); }
|
| 180 |
+
function visibleContextToken(value){ return String(value||'').replace(/\r/g,'\\r').replace(/\n/g,'\\n').replace(/\t/g,'\\t').replace(/\f/g,'\\f'); }
|
| 181 |
+
function renderComponentMetrics(component){ const parts=[]; const erf=Number(component.effective_context_mean); const k=Number(component.excess_kurtosis); if(Number.isFinite(erf)) parts.push(`<span class="metric-badge" title="Effective Receptive Field (ERF): estimated mean number of token positions in the local context that materially affect this component.">ERF=${erf.toFixed(1)}</span>`); if(Number.isFinite(k)) parts.push(`<span class="metric-badge" title="Kurtosis (K): excess kurtosis of this component's source scores over the sampled activation rows; larger values indicate a heavier-tailed direction.">K=${k.toFixed(1)}</span>`); el.componentMetrics.innerHTML=parts.join(''); }
|
| 182 |
+
function renderEditor(){ const a=state.annotation; el.form.hidden=false; el.editorEmpty.hidden=true; el.positiveLabel.value=a.positive_label||''; el.positiveConfidence.value=a.positive_confidence||'unclear'; el.negativeLabel.value=a.negative_label||''; el.negativeConfidence.value=a.negative_confidence||'unclear'; el.summary.value=a.summary||''; el.notes.value=a.notes||''; el.caseStudy.checked=!!a.include_as_case_study; setTypes(el.positiveTypes,a.positive_interpretation_types||[]); setTypes(el.negativeTypes,a.negative_interpretation_types||[]); updateLabelConfidenceStyles(); const direction=topAbsDirection(); setActiveDirection(direction); renderNeighborCards(direction); }
|
| 183 |
+
function topAbsDirection(){ const top=(state.examples?.examples_by_region?.top_abs||[])[0]; const score=Number(top?.source_score); if(!Number.isFinite(score) || score===0) return null; return score>0?'positive':'negative'; }
|
| 184 |
+
function setActiveDirection(direction){ el.positiveBlock.classList.toggle('active-direction', direction==='positive'); el.negativeBlock.classList.toggle('active-direction', direction==='negative'); }
|
| 185 |
+
function renderNeighborCards(sourceDirection){ const rows=state.neighbors?.neighbors||[]; const byDirection=new Map(rows.map(n=>[n.direction,n])); const sourceLabel=sourceDirection==='negative'?visibleAnnotationLabel(state.annotation?.negative_label,state.annotation?.negative_confidence):visibleAnnotationLabel(state.annotation?.positive_label,state.annotation?.positive_confidence); el.neighborCards.hidden=false; el.neighborCards.innerHTML=`${neighborCard(byDirection.get('prev'),sourceDirection,sourceLabel)}${neighborCard(byDirection.get('next'),sourceDirection,sourceLabel)}`; el.neighborCards.querySelectorAll('.neighbor-copy').forEach(b=>b.onclick=()=>copyNeighborAnnotation(b.dataset.direction)); }
|
| 186 |
+
function neighborCard(n, sourceDirection, sourceLabel){ if(!n) return '<div class="neighbor-card missing" aria-hidden="true"></div>'; const layer=String(n.neighbor_layer||''); const component=Number(n.neighbor_component); const label=neighborLabel(n,sourceDirection,sourceLabel); const cos=Number(n.abs_cosine); const q=new URLSearchParams({model:state.model,layer,component:String(component)}); return `<div class="neighbor-card" title="${esc(layer)} C${component}"><div class="neighbor-head"><a class="neighbor-link" href="/annotate?${esc(q.toString())}"><span class="neighbor-target">${esc(layer)} C${component}</span></a><button class="neighbor-copy" type="button" data-direction="${esc(n.direction)}" title="Copy this neighbor annotation into the active direction">Copy</button></div><span class="neighbor-label">${esc(label)}</span><span class="neighbor-metrics">${neighborMetrics(n)}</span><span class="neighbor-cos" style="--cos-width:${cosWidth(cos)}%">cos=${Number.isFinite(cos)?cos.toFixed(3):'?'}</span></div>`; }
|
| 187 |
+
function neighborLabel(n, sourceDirection, sourceLabel){ return neighborAnnotationSide(n,sourceDirection,sourceLabel).label || 'unlabeled'; }
|
| 188 |
+
function neighborAnnotationSide(n, sourceDirection, sourceLabel){ const positive=visibleAnnotationLabel(n.positive_label,n.positive_confidence); const negative=visibleAnnotationLabel(n.negative_label,n.negative_confidence); const pm=labelSimilarity(sourceLabel,positive); const nm=labelSimilarity(sourceLabel,negative); if(pm>nm&&pm>0) return {side:'positive', label:positive, confidence:n.positive_confidence||'unclear', types:n.positive_types||[]}; if(nm>pm&&nm>0) return {side:'negative', label:negative, confidence:n.negative_confidence||'unclear', types:n.negative_types||[]}; const sourceSign=sourceDirection==='negative'?-1:1; const neighborSign=Number(n.neighbor_sign)<0?-1:1; if(sourceSign*neighborSign<0) return {side:'negative', label:negative, confidence:n.negative_confidence||'unclear', types:n.negative_types||[]}; return {side:'positive', label:positive, confidence:n.positive_confidence||'unclear', types:n.positive_types||[]}; }
|
| 189 |
+
function copyNeighborAnnotation(direction){ const n=(state.neighbors?.neighbors||[]).find(x=>x.direction===direction); if(!n) return; const active=currentAnnotationDirection(); const sourceLabel=active==='negative'?visibleAnnotationLabel(state.annotation?.negative_label,state.annotation?.negative_confidence):visibleAnnotationLabel(state.annotation?.positive_label,state.annotation?.positive_confidence); const picked=neighborAnnotationSide(n,active,sourceLabel); if(active==='negative'){ el.negativeLabel.value=picked.label||''; el.negativeConfidence.value=picked.confidence||'unclear'; setTypes(el.negativeTypes,picked.types||[]); } else { el.positiveLabel.value=picked.label||''; el.positiveConfidence.value=picked.confidence||'unclear'; setTypes(el.positiveTypes,picked.types||[]); } updateLabelConfidenceStyles(); setActiveDirection(active); setStatus(`Copied ${direction} neighbor into ${active} direction.`); }
|
| 190 |
+
function currentAnnotationDirection(){ if(el.negativeBlock.classList.contains('active-direction')) return 'negative'; if(el.positiveBlock.classList.contains('active-direction')) return 'positive'; return topAbsDirection()||'positive'; }
|
| 191 |
+
function visibleAnnotationLabel(value, confidence){ const text=String(value||'').trim(); if(!text) return ''; if(text==='?' && String(confidence||'unclear')==='unclear') return ''; return text.replace(/\r/g,'\\r').replace(/\n/g,'\\n').replace(/\t/g,'\\t'); }
|
| 192 |
+
function labelSimilarity(a,b){ const aa=labelTokens(a); const bb=labelTokens(b); if(!aa.size||!bb.size) return 0; let overlap=0; aa.forEach(t=>{if(bb.has(t)) overlap+=1;}); return overlap/Math.max(aa.size,bb.size); }
|
| 193 |
+
function labelTokens(value){ const text=String(value||'').toLowerCase().replace(/[^a-z0-9]+/g,' ').trim(); if(!text||text==='?') return new Set(); return new Set(text.split(/\s+/).filter(Boolean)); }
|
| 194 |
+
function cosWidth(value){ const cos=Math.max(0,Math.min(1,Number(value)||0)); return (100*cos).toFixed(1); }
|
| 195 |
+
function neighborMetrics(n){ const parts=[]; const erf=Number(n?.effective_context_mean); const k=Number(n?.excess_kurtosis); if(Number.isFinite(erf)) parts.push(`ERF=${erf.toFixed(1)}`); if(Number.isFinite(k)) parts.push(`K=${k.toFixed(1)}`); return esc(parts.join(' · ')); }
|
| 196 |
+
function updateLabelConfidenceStyles(){ setLabelConfidenceStyle(el.positiveLabel, el.positiveConfidence.value); setLabelConfidenceStyle(el.negativeLabel, el.negativeConfidence.value); }
|
| 197 |
+
function setLabelConfidenceStyle(node, confidence){ node.classList.remove('label-confidence-high','label-confidence-medium','label-confidence-low','label-confidence-unclear'); node.classList.add(`label-confidence-${CONF.includes(confidence)?confidence:'unclear'}`); }
|
| 198 |
+
async function saveAnnotation(ev){
|
| 199 |
+
ev.preventDefault();
|
| 200 |
+
if(!state.selected) return;
|
| 201 |
+
const component=state.selected.component;
|
| 202 |
+
const body={model_name:state.model, layer:state.layer, component, positive_label:el.positiveLabel.value, positive_confidence:el.positiveConfidence.value, positive_interpretation_types:selectedTypes(el.positiveTypes), negative_label:el.negativeLabel.value, negative_confidence:el.negativeConfidence.value, negative_interpretation_types:selectedTypes(el.negativeTypes), summary:el.summary.value, notes:el.notes.value, include_as_case_study:el.caseStudy.checked};
|
| 203 |
+
setStatus('Saving...');
|
| 204 |
+
await api('/api/annotations/component',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
| 205 |
+
const q=new URLSearchParams({model:state.model,layer:state.layer,component:String(component)});
|
| 206 |
+
const [annotation, components]=await Promise.all([
|
| 207 |
+
api(`/api/annotations/component?${q}`),
|
| 208 |
+
api(`/api/components?model=${encodeURIComponent(state.model)}&layer=${encodeURIComponent(state.layer)}`),
|
| 209 |
+
]);
|
| 210 |
+
state.annotation=annotation;
|
| 211 |
+
state.components=components.components;
|
| 212 |
+
state.selected=state.components.find(c=>c.component===component) || state.selected;
|
| 213 |
+
renderComponents();
|
| 214 |
+
renderEditor();
|
| 215 |
+
setStatus('Saved.');
|
| 216 |
+
}
|
| 217 |
+
init().catch(e=>{setStatus(e.message); el.components.innerHTML=`<div class="error">${esc(e.message)}</div>`});
|
| 218 |
+
</script>
|
| 219 |
+
</body>
|
| 220 |
+
</html>
|
server/static/component.html
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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" />
|
| 6 |
+
<title>Component Examples - GPT-2 Explorer</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root {
|
| 9 |
+
--bg: #f6f7f9;
|
| 10 |
+
--panel: #fff;
|
| 11 |
+
--text: #151922;
|
| 12 |
+
--muted: #647084;
|
| 13 |
+
--border: #cbd3df;
|
| 14 |
+
--accent: #1f6feb;
|
| 15 |
+
--positive: #b91c1c;
|
| 16 |
+
--negative: #1d4ed8;
|
| 17 |
+
--mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
| 18 |
+
--shadow: 0 1px 2px rgb(20 25 34 / .08), 0 10px 30px rgb(20 25 34 / .06);
|
| 19 |
+
}
|
| 20 |
+
* { box-sizing: border-box; }
|
| 21 |
+
body {
|
| 22 |
+
margin: 0;
|
| 23 |
+
background: var(--bg);
|
| 24 |
+
color: var(--text);
|
| 25 |
+
font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
| 26 |
+
}
|
| 27 |
+
header {
|
| 28 |
+
position: sticky;
|
| 29 |
+
top: 0;
|
| 30 |
+
z-index: 5;
|
| 31 |
+
background: var(--panel);
|
| 32 |
+
border-bottom: 1px solid var(--border);
|
| 33 |
+
box-shadow: var(--shadow);
|
| 34 |
+
padding: 12px 18px;
|
| 35 |
+
}
|
| 36 |
+
.top {
|
| 37 |
+
display: flex;
|
| 38 |
+
align-items: center;
|
| 39 |
+
justify-content: space-between;
|
| 40 |
+
gap: 14px;
|
| 41 |
+
}
|
| 42 |
+
h1 { margin: 0; font-size: 19px; letter-spacing: 0; }
|
| 43 |
+
.nav {
|
| 44 |
+
display: flex;
|
| 45 |
+
align-items: center;
|
| 46 |
+
gap: 14px;
|
| 47 |
+
}
|
| 48 |
+
a {
|
| 49 |
+
color: var(--accent);
|
| 50 |
+
font-weight: 750;
|
| 51 |
+
text-decoration: none;
|
| 52 |
+
}
|
| 53 |
+
a:hover { text-decoration: underline; }
|
| 54 |
+
main {
|
| 55 |
+
width: 100%;
|
| 56 |
+
margin: 0;
|
| 57 |
+
padding: 12px;
|
| 58 |
+
}
|
| 59 |
+
.summary {
|
| 60 |
+
display: flex;
|
| 61 |
+
flex-wrap: wrap;
|
| 62 |
+
align-items: center;
|
| 63 |
+
gap: 8px 14px;
|
| 64 |
+
margin-bottom: 12px;
|
| 65 |
+
color: var(--muted);
|
| 66 |
+
font-size: 12px;
|
| 67 |
+
font-weight: 750;
|
| 68 |
+
}
|
| 69 |
+
.band {
|
| 70 |
+
margin-bottom: 14px;
|
| 71 |
+
background: var(--panel);
|
| 72 |
+
border: 1px solid var(--border);
|
| 73 |
+
border-radius: 8px;
|
| 74 |
+
box-shadow: var(--shadow);
|
| 75 |
+
overflow: hidden;
|
| 76 |
+
}
|
| 77 |
+
.band-toggle {
|
| 78 |
+
display: flex;
|
| 79 |
+
align-items: center;
|
| 80 |
+
justify-content: space-between;
|
| 81 |
+
gap: 12px;
|
| 82 |
+
width: 100%;
|
| 83 |
+
margin: 0;
|
| 84 |
+
padding: 9px 12px;
|
| 85 |
+
border-bottom: 1px solid #e5eaf2;
|
| 86 |
+
border-left: 0;
|
| 87 |
+
border-right: 0;
|
| 88 |
+
border-top: 0;
|
| 89 |
+
font-size: 14px;
|
| 90 |
+
font-weight: 850;
|
| 91 |
+
letter-spacing: 0;
|
| 92 |
+
background: #fbfcfe;
|
| 93 |
+
color: var(--text);
|
| 94 |
+
text-align: left;
|
| 95 |
+
cursor: pointer;
|
| 96 |
+
font-family: inherit;
|
| 97 |
+
}
|
| 98 |
+
.band-toggle:hover { background: #f1f5f9; }
|
| 99 |
+
.band-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
| 100 |
+
.band-title { min-width: 0; overflow-wrap: anywhere; }
|
| 101 |
+
.band-state { color: var(--muted); font-size: 12px; white-space: nowrap; }
|
| 102 |
+
.band.collapsed .examples { display: none; }
|
| 103 |
+
.examples {
|
| 104 |
+
display: grid;
|
| 105 |
+
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
| 106 |
+
gap: 0;
|
| 107 |
+
}
|
| 108 |
+
.example {
|
| 109 |
+
min-width: 0;
|
| 110 |
+
border-right: 1px solid #e5eaf2;
|
| 111 |
+
border-bottom: 1px solid #e5eaf2;
|
| 112 |
+
padding: 10px 12px;
|
| 113 |
+
}
|
| 114 |
+
.example-head {
|
| 115 |
+
display: flex;
|
| 116 |
+
align-items: center;
|
| 117 |
+
justify-content: space-between;
|
| 118 |
+
gap: 10px;
|
| 119 |
+
margin-bottom: 6px;
|
| 120 |
+
color: var(--muted);
|
| 121 |
+
font-size: 12px;
|
| 122 |
+
font-weight: 750;
|
| 123 |
+
}
|
| 124 |
+
.example-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; min-width: 0; }
|
| 125 |
+
.doc-link { color: var(--accent); text-decoration: none; font-weight: 850; }
|
| 126 |
+
.doc-link:hover { text-decoration: underline; }
|
| 127 |
+
.score {
|
| 128 |
+
font-variant-numeric: tabular-nums;
|
| 129 |
+
white-space: nowrap;
|
| 130 |
+
}
|
| 131 |
+
.score.positive { color: var(--positive); }
|
| 132 |
+
.score.negative { color: var(--negative); }
|
| 133 |
+
.token {
|
| 134 |
+
display: inline-block;
|
| 135 |
+
margin-bottom: 6px;
|
| 136 |
+
padding: 2px 6px;
|
| 137 |
+
border: 1px solid var(--border);
|
| 138 |
+
border-radius: 5px;
|
| 139 |
+
background: #f8fafc;
|
| 140 |
+
font-weight: 850;
|
| 141 |
+
font-family: var(--mono);
|
| 142 |
+
font-size: 12px;
|
| 143 |
+
overflow-wrap: anywhere;
|
| 144 |
+
}
|
| 145 |
+
.token.positive { color: var(--positive); border-color: #fecaca; background: #fff1f2; }
|
| 146 |
+
.token.negative { color: var(--negative); border-color: #bfdbfe; background: #eff6ff; }
|
| 147 |
+
.context {
|
| 148 |
+
margin: 0;
|
| 149 |
+
white-space: pre-wrap;
|
| 150 |
+
overflow-wrap: anywhere;
|
| 151 |
+
font-family: var(--mono);
|
| 152 |
+
font-size: 12px;
|
| 153 |
+
line-height: 1.55;
|
| 154 |
+
}
|
| 155 |
+
.context-token {
|
| 156 |
+
border-radius: 3px;
|
| 157 |
+
padding: 0 1px;
|
| 158 |
+
}
|
| 159 |
+
.context-token.target {
|
| 160 |
+
box-shadow: inset 0 -2px 0 currentColor;
|
| 161 |
+
font-weight: 850;
|
| 162 |
+
}
|
| 163 |
+
.context-token.positive { color: var(--positive); }
|
| 164 |
+
.context-token.negative { color: var(--negative); }
|
| 165 |
+
.empty, .error {
|
| 166 |
+
border: 1px dashed var(--border);
|
| 167 |
+
border-radius: 8px;
|
| 168 |
+
padding: 18px;
|
| 169 |
+
color: var(--muted);
|
| 170 |
+
background: #fff;
|
| 171 |
+
}
|
| 172 |
+
.error { color: #a41414; border-color: #f0b9b9; background: #fff7f7; }
|
| 173 |
+
@media (max-width: 760px) {
|
| 174 |
+
main { padding: 8px; }
|
| 175 |
+
header { padding: 10px 12px; }
|
| 176 |
+
.top { align-items: flex-start; flex-direction: column; }
|
| 177 |
+
.examples { grid-template-columns: 1fr; }
|
| 178 |
+
}
|
| 179 |
+
</style>
|
| 180 |
+
</head>
|
| 181 |
+
<body>
|
| 182 |
+
<header>
|
| 183 |
+
<div class="top">
|
| 184 |
+
<h1 id="title">Component Examples</h1>
|
| 185 |
+
<nav class="nav" aria-label="Primary">
|
| 186 |
+
<a href="/">Explorer</a>
|
| 187 |
+
<a href="/sae-explorer">SAE Explorer</a>
|
| 188 |
+
<a href="/stats">Stats</a>
|
| 189 |
+
<a href="/annotate">Annotate</a>
|
| 190 |
+
<a href="/random-components">Random</a>
|
| 191 |
+
</nav>
|
| 192 |
+
</div>
|
| 193 |
+
</header>
|
| 194 |
+
|
| 195 |
+
<main>
|
| 196 |
+
<div id="summary" class="summary">Loading component examples...</div>
|
| 197 |
+
<div id="message" class="empty">Loading...</div>
|
| 198 |
+
<div id="content" hidden></div>
|
| 199 |
+
</main>
|
| 200 |
+
|
| 201 |
+
<script>
|
| 202 |
+
const els = {
|
| 203 |
+
title: document.getElementById("title"),
|
| 204 |
+
summary: document.getElementById("summary"),
|
| 205 |
+
message: document.getElementById("message"),
|
| 206 |
+
content: document.getElementById("content"),
|
| 207 |
+
};
|
| 208 |
+
|
| 209 |
+
async function api(path) {
|
| 210 |
+
const res = await fetch(path, { headers: { "content-type": "application/json" } });
|
| 211 |
+
if (!res.ok) {
|
| 212 |
+
let detail = res.statusText;
|
| 213 |
+
try { detail = (await res.json()).detail || detail; } catch {}
|
| 214 |
+
throw new Error(detail);
|
| 215 |
+
}
|
| 216 |
+
return res.json();
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
async function init() {
|
| 220 |
+
const params = new URLSearchParams(location.search);
|
| 221 |
+
const model = params.get("model") || "gpt2";
|
| 222 |
+
const layer = params.get("layer") || "";
|
| 223 |
+
const component = params.get("component") || "";
|
| 224 |
+
if (!layer || !component) {
|
| 225 |
+
showError("Missing layer or component.");
|
| 226 |
+
return;
|
| 227 |
+
}
|
| 228 |
+
try {
|
| 229 |
+
const query = new URLSearchParams({ model, layer, component });
|
| 230 |
+
const data = await api(`/api/component-examples?${query.toString()}`);
|
| 231 |
+
render(data);
|
| 232 |
+
} catch (err) {
|
| 233 |
+
showError(err.message);
|
| 234 |
+
}
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
function render(data) {
|
| 238 |
+
const bands = data.bands || [];
|
| 239 |
+
const total = bands.reduce((count, band) => count + (band.examples || []).length, 0);
|
| 240 |
+
const kurtosis = Number.isFinite(Number(data.excess_kurtosis)) ? `kurtosis ${Number(data.excess_kurtosis).toFixed(3)}` : "kurtosis n/a";
|
| 241 |
+
els.title.textContent = `${data.layer} C${data.component}`;
|
| 242 |
+
els.summary.textContent = `${data.model_name} - ${data.layer} component ${data.component} - ${kurtosis} - ${bands.length} bands - ${total} examples`;
|
| 243 |
+
if (!bands.length) {
|
| 244 |
+
els.message.hidden = false;
|
| 245 |
+
els.message.className = "empty";
|
| 246 |
+
els.message.textContent = "No examples found for this component.";
|
| 247 |
+
els.content.hidden = true;
|
| 248 |
+
return;
|
| 249 |
+
}
|
| 250 |
+
els.message.hidden = true;
|
| 251 |
+
els.content.hidden = false;
|
| 252 |
+
els.content.innerHTML = bands.map(renderBand).join("");
|
| 253 |
+
wireBandToggles();
|
| 254 |
+
}
|
| 255 |
+
|
| 256 |
+
function renderBand(band) {
|
| 257 |
+
const examples = band.examples || [];
|
| 258 |
+
const collapsed = shouldCollapseBand(band.region);
|
| 259 |
+
return `
|
| 260 |
+
<section class="band ${collapsed ? "collapsed" : ""}">
|
| 261 |
+
<button class="band-toggle" type="button" aria-expanded="${collapsed ? "false" : "true"}">
|
| 262 |
+
<span class="band-title">${escapeHtml(band.region)}</span>
|
| 263 |
+
<span class="band-state">${collapsed ? "show" : "hide"}</span>
|
| 264 |
+
</button>
|
| 265 |
+
<div class="examples">${examples.map(renderExample).join("")}</div>
|
| 266 |
+
</section>
|
| 267 |
+
`;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
function wireBandToggles() {
|
| 271 |
+
els.content.querySelectorAll(".band-toggle").forEach(button => {
|
| 272 |
+
button.addEventListener("click", () => {
|
| 273 |
+
const band = button.closest(".band");
|
| 274 |
+
const collapsed = band.classList.toggle("collapsed");
|
| 275 |
+
button.setAttribute("aria-expanded", String(!collapsed));
|
| 276 |
+
const state = button.querySelector(".band-state");
|
| 277 |
+
if (state) state.textContent = collapsed ? "show" : "hide";
|
| 278 |
+
});
|
| 279 |
+
});
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
function shouldCollapseBand(region) {
|
| 283 |
+
const name = String(region || "").toLowerCase().replace(/[_-]+/g, " ");
|
| 284 |
+
return name.includes("opposite") || name.includes("near zero") || name.includes("nearzero");
|
| 285 |
+
}
|
| 286 |
+
|
| 287 |
+
function renderExample(example) {
|
| 288 |
+
const score = Number(example.source_score);
|
| 289 |
+
const scoreClass = score > 0 ? "positive" : score < 0 ? "negative" : "";
|
| 290 |
+
return `
|
| 291 |
+
<article class="example">
|
| 292 |
+
<div class="example-head">
|
| 293 |
+
<span class="example-meta"><span>#${escapeHtml(example.rank)}</span>${docPositionLink(example)}</span>
|
| 294 |
+
<span class="score ${scoreClass}">${formatScore(score)}</span>
|
| 295 |
+
</div>
|
| 296 |
+
<div class="token ${scoreClass}">${escapeHtml(visibleToken(example.token))}</div>
|
| 297 |
+
<p class="context">${renderContext(example)}</p>
|
| 298 |
+
</article>
|
| 299 |
+
`;
|
| 300 |
+
}
|
| 301 |
+
|
| 302 |
+
function docPositionLink(example) {
|
| 303 |
+
if (example.doc_id == null || Number(example.doc_id) < 0) {
|
| 304 |
+
return example.position == null ? "" : `<span>pos ${escapeHtml(example.position)}</span>`;
|
| 305 |
+
}
|
| 306 |
+
const query = new URLSearchParams({ model: state.model, doc_id: String(example.doc_id) });
|
| 307 |
+
if (example.position != null) query.set("position", String(example.position));
|
| 308 |
+
const target = visibleToken(example.token);
|
| 309 |
+
if (target) query.set("target", target);
|
| 310 |
+
const text = `doc ${example.doc_id}${example.position == null ? "" : ` · pos ${example.position}`}`;
|
| 311 |
+
return `<a class="doc-link" href="/context?${escapeAttr(query.toString())}" target="_blank" rel="noopener" title="Open full document context">${escapeHtml(text)}</a>`;
|
| 312 |
+
}
|
| 313 |
+
|
| 314 |
+
function renderContext(example) {
|
| 315 |
+
const tokens = Array.isArray(example.context_token_scores) ? example.context_token_scores : [];
|
| 316 |
+
if (!tokens.length) return escapeHtml(example.context_to_target || example.context || "");
|
| 317 |
+
const maxAbs = Math.max(
|
| 318 |
+
1e-9,
|
| 319 |
+
Number(example.context_score_max_abs || 0),
|
| 320 |
+
...tokens.map(token => Math.abs(Number(token.source_score || 0)))
|
| 321 |
+
);
|
| 322 |
+
return tokens.map(token => {
|
| 323 |
+
const score = Number(token.source_score || 0);
|
| 324 |
+
const alpha = tokenAlpha(score, maxAbs);
|
| 325 |
+
const color = alpha === 0 ? "transparent" : score > 0 ? `rgba(220,38,38,${alpha})` : `rgba(37,99,235,${alpha})`;
|
| 326 |
+
const signClass = score > 0 ? "positive" : score < 0 ? "negative" : "";
|
| 327 |
+
const title = `score=${formatScore(score)}`;
|
| 328 |
+
return `<span class="context-token ${token.is_target ? "target" : ""} ${signClass}" style="background:${color}" title="${escapeAttr(title)}">${escapeHtml(visibleContextToken(token.token))}</span>`;
|
| 329 |
+
}).join("");
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
function tokenAlpha(score, maxAbs) {
|
| 333 |
+
const magnitude = Math.abs(Number(score || 0));
|
| 334 |
+
if (magnitude < 1e-6) return 0;
|
| 335 |
+
return Math.min(0.55, 0.47 * Math.log1p(magnitude) / Math.log1p(maxAbs));
|
| 336 |
+
}
|
| 337 |
+
|
| 338 |
+
function visibleToken(value) {
|
| 339 |
+
const text = String(value || "");
|
| 340 |
+
if (text === " ") return "[space]";
|
| 341 |
+
if (text === "\n") return "[newline]";
|
| 342 |
+
if (text === "\f") return "\\f";
|
| 343 |
+
return text.replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/\f/g, "\\f");
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
function visibleContextToken(value) {
|
| 347 |
+
return String(value || "").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t").replace(/\f/g, "\\f");
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
function formatScore(value) {
|
| 351 |
+
return Number.isFinite(value) ? value.toFixed(4).replace(/0+$/, "").replace(/\.$/, "") : "n/a";
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
function showError(message) {
|
| 355 |
+
els.summary.textContent = "";
|
| 356 |
+
els.message.hidden = false;
|
| 357 |
+
els.message.className = "error";
|
| 358 |
+
els.message.textContent = message;
|
| 359 |
+
els.content.hidden = true;
|
| 360 |
+
}
|
| 361 |
+
|
| 362 |
+
function escapeHtml(value) {
|
| 363 |
+
return String(value).replace(/[&<>"']/g, char => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char]));
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
function escapeAttr(value) {
|
| 367 |
+
return escapeHtml(value);
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
init();
|
| 371 |
+
</script>
|
| 372 |
+
</body>
|
| 373 |
+
</html>
|
server/static/index.html
ADDED
|
@@ -0,0 +1,1546 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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" />
|
| 6 |
+
<title>ICA Explorer</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root {
|
| 9 |
+
--bg: #f6f7f9;
|
| 10 |
+
--panel: #fff;
|
| 11 |
+
--text: #151922;
|
| 12 |
+
--muted: #647084;
|
| 13 |
+
--border: #cbd3df;
|
| 14 |
+
--accent: #1f6feb;
|
| 15 |
+
--hot: #b42318;
|
| 16 |
+
--cold: #1e5bb8;
|
| 17 |
+
--good: #087443;
|
| 18 |
+
--warn: #a15c00;
|
| 19 |
+
--shadow: 0 1px 2px rgb(20 25 34 / .08), 0 10px 30px rgb(20 25 34 / .06);
|
| 20 |
+
--token-card-width: 140px;
|
| 21 |
+
}
|
| 22 |
+
* { box-sizing: border-box; }
|
| 23 |
+
body {
|
| 24 |
+
margin: 0;
|
| 25 |
+
background: var(--bg);
|
| 26 |
+
color: var(--text);
|
| 27 |
+
font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
| 28 |
+
}
|
| 29 |
+
header {
|
| 30 |
+
position: sticky;
|
| 31 |
+
top: 0;
|
| 32 |
+
z-index: 5;
|
| 33 |
+
background: var(--panel);
|
| 34 |
+
border-bottom: 1px solid var(--border);
|
| 35 |
+
box-shadow: var(--shadow);
|
| 36 |
+
padding: 12px 18px;
|
| 37 |
+
}
|
| 38 |
+
.top {
|
| 39 |
+
display: flex;
|
| 40 |
+
align-items: center;
|
| 41 |
+
justify-content: space-between;
|
| 42 |
+
gap: 14px;
|
| 43 |
+
}
|
| 44 |
+
.nav {
|
| 45 |
+
display: flex;
|
| 46 |
+
align-items: center;
|
| 47 |
+
gap: 14px;
|
| 48 |
+
}
|
| 49 |
+
.nav a {
|
| 50 |
+
color: var(--accent);
|
| 51 |
+
font-weight: 750;
|
| 52 |
+
text-decoration: none;
|
| 53 |
+
}
|
| 54 |
+
.nav a:hover { text-decoration: underline; }
|
| 55 |
+
h1 { margin: 0; font-size: 19px; letter-spacing: 0; }
|
| 56 |
+
main {
|
| 57 |
+
max-width: 1240px;
|
| 58 |
+
margin: 0 auto;
|
| 59 |
+
padding: 18px;
|
| 60 |
+
}
|
| 61 |
+
footer {
|
| 62 |
+
max-width: 1240px;
|
| 63 |
+
margin: 0 auto;
|
| 64 |
+
padding: 0 18px 22px;
|
| 65 |
+
color: var(--muted);
|
| 66 |
+
font-size: 12px;
|
| 67 |
+
}
|
| 68 |
+
footer a {
|
| 69 |
+
color: var(--accent);
|
| 70 |
+
font-weight: 750;
|
| 71 |
+
text-decoration: none;
|
| 72 |
+
}
|
| 73 |
+
footer a:hover { text-decoration: underline; }
|
| 74 |
+
.controls {
|
| 75 |
+
display: grid;
|
| 76 |
+
grid-template-columns: minmax(0, 1fr) 450px;
|
| 77 |
+
gap: 12px;
|
| 78 |
+
align-items: stretch;
|
| 79 |
+
}
|
| 80 |
+
.control-stack {
|
| 81 |
+
display: flex;
|
| 82 |
+
flex-wrap: wrap;
|
| 83 |
+
align-content: flex-start;
|
| 84 |
+
align-items: center;
|
| 85 |
+
gap: 10px;
|
| 86 |
+
}
|
| 87 |
+
.inline-control {
|
| 88 |
+
display: grid;
|
| 89 |
+
grid-template-columns: max-content max-content;
|
| 90 |
+
align-items: center;
|
| 91 |
+
column-gap: 8px;
|
| 92 |
+
}
|
| 93 |
+
.inline-control > span { white-space: nowrap; }
|
| 94 |
+
.model-control { flex: 0 0 auto; }
|
| 95 |
+
.layer-control { flex: 0 0 auto; }
|
| 96 |
+
.control-break { flex-basis: 100%; height: 0; }
|
| 97 |
+
.topk-control,
|
| 98 |
+
.card-width-control,
|
| 99 |
+
.opacity-control { flex: 0 0 auto; }
|
| 100 |
+
.run-button {
|
| 101 |
+
width: auto;
|
| 102 |
+
min-height: 32px;
|
| 103 |
+
padding: 6px 12px;
|
| 104 |
+
border-color: #1458c8;
|
| 105 |
+
background: var(--accent);
|
| 106 |
+
color: #fff;
|
| 107 |
+
font-weight: 850;
|
| 108 |
+
}
|
| 109 |
+
.run-button:hover { background: #1458c8; color: #fff; }
|
| 110 |
+
.memory-control {
|
| 111 |
+
position: fixed;
|
| 112 |
+
right: 12px;
|
| 113 |
+
bottom: 10px;
|
| 114 |
+
z-index: 4;
|
| 115 |
+
display: inline-flex;
|
| 116 |
+
align-items: center;
|
| 117 |
+
gap: 5px;
|
| 118 |
+
width: auto;
|
| 119 |
+
min-height: 24px;
|
| 120 |
+
padding: 3px 6px;
|
| 121 |
+
border: 1px solid #d7dee9;
|
| 122 |
+
border-radius: 6px;
|
| 123 |
+
background: rgb(255 255 255 / .78);
|
| 124 |
+
color: #94a3b8;
|
| 125 |
+
font-size: 11px;
|
| 126 |
+
font-weight: 650;
|
| 127 |
+
white-space: nowrap;
|
| 128 |
+
}
|
| 129 |
+
.memory-control input { width: auto; margin: 0; padding: 0; accent-color: #94a3b8; }
|
| 130 |
+
.memory-control:hover { color: #64748b; border-color: #cbd5e1; background: #fff; }
|
| 131 |
+
#topK { width: 48px; min-width: 0; }
|
| 132 |
+
#cardWidth { width: 78px; min-width: 0; }
|
| 133 |
+
#weakRatio { width: 58px; min-width: 0; }
|
| 134 |
+
.model-control select,
|
| 135 |
+
.layer-control select { width: 100%; }
|
| 136 |
+
label {
|
| 137 |
+
display: grid;
|
| 138 |
+
gap: 5px;
|
| 139 |
+
color: #435066;
|
| 140 |
+
font-size: 12px;
|
| 141 |
+
font-weight: 700;
|
| 142 |
+
}
|
| 143 |
+
textarea, select, input, button {
|
| 144 |
+
width: 100%;
|
| 145 |
+
border: 1px solid var(--border);
|
| 146 |
+
border-radius: 7px;
|
| 147 |
+
background: #fff;
|
| 148 |
+
color: var(--text);
|
| 149 |
+
font: inherit;
|
| 150 |
+
padding: 8px 10px;
|
| 151 |
+
}
|
| 152 |
+
textarea {
|
| 153 |
+
height: 92px;
|
| 154 |
+
min-height: 92px;
|
| 155 |
+
resize: vertical;
|
| 156 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
| 157 |
+
}
|
| 158 |
+
button {
|
| 159 |
+
cursor: pointer;
|
| 160 |
+
min-height: 39px;
|
| 161 |
+
background: #eef2f7;
|
| 162 |
+
font-weight: 750;
|
| 163 |
+
}
|
| 164 |
+
button:hover { border-color: var(--accent); color: var(--accent); background: #edf5ff; }
|
| 165 |
+
button.primary { background: var(--accent); border-color: #1458c8; color: #fff; }
|
| 166 |
+
button.primary:hover { background: #1458c8; color: #fff; }
|
| 167 |
+
.panel {
|
| 168 |
+
margin-top: 14px;
|
| 169 |
+
background: var(--panel);
|
| 170 |
+
border: 1px solid var(--border);
|
| 171 |
+
border-radius: 8px;
|
| 172 |
+
box-shadow: var(--shadow);
|
| 173 |
+
padding: 12px;
|
| 174 |
+
}
|
| 175 |
+
.results {
|
| 176 |
+
display: grid;
|
| 177 |
+
grid-template-columns: repeat(auto-fill, minmax(var(--token-card-width), 1fr));
|
| 178 |
+
gap: 10px;
|
| 179 |
+
margin-top: 12px;
|
| 180 |
+
}
|
| 181 |
+
.token-card {
|
| 182 |
+
position: relative;
|
| 183 |
+
border: 1px solid var(--border);
|
| 184 |
+
border-radius: 8px;
|
| 185 |
+
padding: 6px;
|
| 186 |
+
background: #fff;
|
| 187 |
+
min-width: 0;
|
| 188 |
+
text-align: center;
|
| 189 |
+
}
|
| 190 |
+
.token-text {
|
| 191 |
+
min-height: 25px;
|
| 192 |
+
font-weight: 850;
|
| 193 |
+
text-align: center;
|
| 194 |
+
overflow-wrap: anywhere;
|
| 195 |
+
margin-bottom: 6px;
|
| 196 |
+
}
|
| 197 |
+
.score-row {
|
| 198 |
+
position: relative;
|
| 199 |
+
min-height: 24px;
|
| 200 |
+
margin-top: 3px;
|
| 201 |
+
}
|
| 202 |
+
.badge {
|
| 203 |
+
position: relative;
|
| 204 |
+
display: flex;
|
| 205 |
+
align-items: center;
|
| 206 |
+
width: 100%;
|
| 207 |
+
min-height: 24px;
|
| 208 |
+
padding: 3px 5px;
|
| 209 |
+
border: 1px solid #d5dce7;
|
| 210 |
+
border-radius: 5px;
|
| 211 |
+
background:
|
| 212 |
+
linear-gradient(
|
| 213 |
+
90deg,
|
| 214 |
+
var(--score-bg, #edf2f7) 0,
|
| 215 |
+
var(--score-bg, #edf2f7) var(--score-width, 100%),
|
| 216 |
+
transparent var(--score-width, 100%),
|
| 217 |
+
transparent 100%
|
| 218 |
+
);
|
| 219 |
+
color: var(--text);
|
| 220 |
+
font-size: 10px;
|
| 221 |
+
font-weight: 400;
|
| 222 |
+
text-align: left;
|
| 223 |
+
cursor: pointer;
|
| 224 |
+
}
|
| 225 |
+
.badge:hover {
|
| 226 |
+
border-color: var(--component-color, var(--accent));
|
| 227 |
+
color: var(--text);
|
| 228 |
+
}
|
| 229 |
+
.badge.hot {
|
| 230 |
+
border-color: var(--component-color, var(--accent));
|
| 231 |
+
--score-bg: color-mix(in srgb, var(--component-color, var(--accent)) 16%, white);
|
| 232 |
+
box-shadow: inset 3px 0 0 var(--component-color, var(--accent));
|
| 233 |
+
}
|
| 234 |
+
.badge.hot:hover {
|
| 235 |
+
color: var(--text);
|
| 236 |
+
}
|
| 237 |
+
.badge.weak {
|
| 238 |
+
opacity: .1;
|
| 239 |
+
}
|
| 240 |
+
.badge.weak:hover,
|
| 241 |
+
.badge.weak.hot {
|
| 242 |
+
opacity: 1;
|
| 243 |
+
}
|
| 244 |
+
.badge-main {
|
| 245 |
+
display: inline-flex;
|
| 246 |
+
align-items: center;
|
| 247 |
+
gap: 3px;
|
| 248 |
+
min-width: 0;
|
| 249 |
+
max-width: calc(100% - 28px);
|
| 250 |
+
overflow: hidden;
|
| 251 |
+
white-space: nowrap;
|
| 252 |
+
text-overflow: ellipsis;
|
| 253 |
+
}
|
| 254 |
+
.badge-label {
|
| 255 |
+
min-width: 0;
|
| 256 |
+
overflow: hidden;
|
| 257 |
+
white-space: nowrap;
|
| 258 |
+
text-overflow: ellipsis;
|
| 259 |
+
}
|
| 260 |
+
.annotation-dot {
|
| 261 |
+
display: inline-flex;
|
| 262 |
+
align-items: center;
|
| 263 |
+
justify-content: center;
|
| 264 |
+
min-width: 13px;
|
| 265 |
+
height: 13px;
|
| 266 |
+
border-radius: 4px;
|
| 267 |
+
flex: 0 0 auto;
|
| 268 |
+
background: #e5e7eb;
|
| 269 |
+
box-shadow: 0 0 0 1px rgb(255 255 255 / .85);
|
| 270 |
+
color: #fff;
|
| 271 |
+
font-size: 9px;
|
| 272 |
+
font-weight: 850;
|
| 273 |
+
line-height: 1;
|
| 274 |
+
}
|
| 275 |
+
.annotation-dot.high { background: #16a34a; box-shadow: 0 0 0 1px #15803d; }
|
| 276 |
+
.annotation-dot.medium { color: #166534; background: #fef3c7; box-shadow: 0 0 0 1px #d9b94e; }
|
| 277 |
+
.annotation-dot.low { color: #9f1239; background: #ffe4e6; box-shadow: 0 0 0 1px #f9a8d4; }
|
| 278 |
+
.annotation-dot.unclear { color: #475569; background: #e5e7eb; box-shadow: 0 0 0 1px #cbd5e1; }
|
| 279 |
+
.score {
|
| 280 |
+
position: absolute;
|
| 281 |
+
left: auto;
|
| 282 |
+
right: 5px;
|
| 283 |
+
top: 50%;
|
| 284 |
+
transform: translateY(-50%);
|
| 285 |
+
width: 28px;
|
| 286 |
+
text-align: right;
|
| 287 |
+
color: var(--muted);
|
| 288 |
+
font-size: 9px;
|
| 289 |
+
font-variant-numeric: tabular-nums;
|
| 290 |
+
pointer-events: none;
|
| 291 |
+
}
|
| 292 |
+
.prediction-row {
|
| 293 |
+
display: flex;
|
| 294 |
+
align-items: center;
|
| 295 |
+
gap: 5px;
|
| 296 |
+
min-height: 24px;
|
| 297 |
+
margin-top: 5px;
|
| 298 |
+
padding: 3px 5px;
|
| 299 |
+
border: 1px solid #d8e0eb;
|
| 300 |
+
border-radius: 5px;
|
| 301 |
+
background: #f6f8fb;
|
| 302 |
+
color: #314158;
|
| 303 |
+
font-size: 10px;
|
| 304 |
+
font-weight: 650;
|
| 305 |
+
text-align: left;
|
| 306 |
+
}
|
| 307 |
+
.prediction-label {
|
| 308 |
+
color: var(--muted);
|
| 309 |
+
font-weight: 850;
|
| 310 |
+
}
|
| 311 |
+
.prediction-token {
|
| 312 |
+
min-width: 0;
|
| 313 |
+
overflow: hidden;
|
| 314 |
+
text-overflow: ellipsis;
|
| 315 |
+
white-space: nowrap;
|
| 316 |
+
}
|
| 317 |
+
.empty, .error {
|
| 318 |
+
margin-top: 12px;
|
| 319 |
+
border: 1px dashed var(--border);
|
| 320 |
+
border-radius: 8px;
|
| 321 |
+
padding: 18px;
|
| 322 |
+
color: var(--muted);
|
| 323 |
+
background: #fff;
|
| 324 |
+
}
|
| 325 |
+
.error { color: #a41414; border-color: #f0b9b9; background: #fff7f7; }
|
| 326 |
+
.request-indicator {
|
| 327 |
+
position: fixed;
|
| 328 |
+
left: 14px;
|
| 329 |
+
top: 62px;
|
| 330 |
+
z-index: 8;
|
| 331 |
+
display: none;
|
| 332 |
+
align-items: center;
|
| 333 |
+
gap: 8px;
|
| 334 |
+
min-height: 34px;
|
| 335 |
+
padding: 8px 10px;
|
| 336 |
+
border: 1px solid var(--border);
|
| 337 |
+
border-radius: 8px;
|
| 338 |
+
background: var(--panel);
|
| 339 |
+
box-shadow: var(--shadow);
|
| 340 |
+
color: #435066;
|
| 341 |
+
font-size: 12px;
|
| 342 |
+
font-weight: 800;
|
| 343 |
+
}
|
| 344 |
+
.request-indicator.visible { display: inline-flex; }
|
| 345 |
+
.request-spinner {
|
| 346 |
+
width: 14px;
|
| 347 |
+
height: 14px;
|
| 348 |
+
border: 2px solid #cbd5e1;
|
| 349 |
+
border-top-color: var(--accent);
|
| 350 |
+
border-radius: 50%;
|
| 351 |
+
animation: request-spin .8s linear infinite;
|
| 352 |
+
}
|
| 353 |
+
@keyframes request-spin { to { transform: rotate(360deg); } }
|
| 354 |
+
.selection-panel {
|
| 355 |
+
position: fixed;
|
| 356 |
+
right: 14px;
|
| 357 |
+
top: 82px;
|
| 358 |
+
z-index: 6;
|
| 359 |
+
display: none;
|
| 360 |
+
width: 500px;
|
| 361 |
+
max-width: calc(100vw - 28px);
|
| 362 |
+
max-height: calc(100vh - 104px);
|
| 363 |
+
overflow: auto;
|
| 364 |
+
border: 1px solid var(--border);
|
| 365 |
+
border-radius: 8px;
|
| 366 |
+
background: var(--panel);
|
| 367 |
+
box-shadow: var(--shadow);
|
| 368 |
+
padding: 12px;
|
| 369 |
+
}
|
| 370 |
+
.selection-panel.visible { display: block; }
|
| 371 |
+
.selection-title {
|
| 372 |
+
display: flex;
|
| 373 |
+
align-items: center;
|
| 374 |
+
justify-content: space-between;
|
| 375 |
+
gap: 8px;
|
| 376 |
+
margin-bottom: 10px;
|
| 377 |
+
color: var(--muted);
|
| 378 |
+
font-size: 12px;
|
| 379 |
+
font-weight: 850;
|
| 380 |
+
}
|
| 381 |
+
.selection-close {
|
| 382 |
+
display: inline-flex;
|
| 383 |
+
align-items: center;
|
| 384 |
+
justify-content: center;
|
| 385 |
+
width: 18px;
|
| 386 |
+
height: 18px;
|
| 387 |
+
min-height: 18px;
|
| 388 |
+
padding: 0;
|
| 389 |
+
border-radius: 5px;
|
| 390 |
+
color: var(--muted);
|
| 391 |
+
font-size: 13px;
|
| 392 |
+
line-height: 1;
|
| 393 |
+
}
|
| 394 |
+
.selection-actions {
|
| 395 |
+
display: inline-flex;
|
| 396 |
+
align-items: center;
|
| 397 |
+
gap: 5px;
|
| 398 |
+
}
|
| 399 |
+
.selection-export-toggle {
|
| 400 |
+
display: none;
|
| 401 |
+
width: auto;
|
| 402 |
+
min-height: 18px;
|
| 403 |
+
padding: 2px 7px;
|
| 404 |
+
border-radius: 5px;
|
| 405 |
+
color: var(--muted);
|
| 406 |
+
font-size: 11px;
|
| 407 |
+
font-weight: 800;
|
| 408 |
+
line-height: 1.2;
|
| 409 |
+
}
|
| 410 |
+
.selection-export-toggle.visible { display: inline-flex; align-items: center; }
|
| 411 |
+
.selection-list {
|
| 412 |
+
display: flex;
|
| 413 |
+
flex-direction: column;
|
| 414 |
+
gap: 8px;
|
| 415 |
+
}
|
| 416 |
+
.selection-entry {
|
| 417 |
+
display: grid;
|
| 418 |
+
grid-template-columns: minmax(0, 1fr) 145px;
|
| 419 |
+
gap: 8px;
|
| 420 |
+
align-items: stretch;
|
| 421 |
+
}
|
| 422 |
+
.selection-link {
|
| 423 |
+
display: flex;
|
| 424 |
+
align-items: center;
|
| 425 |
+
justify-content: space-between;
|
| 426 |
+
gap: 8px;
|
| 427 |
+
min-height: 30px;
|
| 428 |
+
padding: 7px 10px 5px;
|
| 429 |
+
color: var(--text);
|
| 430 |
+
font-weight: 850;
|
| 431 |
+
text-decoration: none;
|
| 432 |
+
}
|
| 433 |
+
.selection-link:hover {
|
| 434 |
+
color: var(--accent);
|
| 435 |
+
text-decoration: none;
|
| 436 |
+
}
|
| 437 |
+
.selection-item {
|
| 438 |
+
border: 1px solid var(--component-color, var(--accent));
|
| 439 |
+
border-radius: 7px;
|
| 440 |
+
background: color-mix(in srgb, var(--component-color, var(--accent)) 5%, white);
|
| 441 |
+
overflow: hidden;
|
| 442 |
+
box-shadow: inset 3px 0 0 var(--component-color, var(--accent));
|
| 443 |
+
}
|
| 444 |
+
.selection-item .selection-link {
|
| 445 |
+
border: 0;
|
| 446 |
+
border-radius: 0;
|
| 447 |
+
background: transparent;
|
| 448 |
+
}
|
| 449 |
+
.selection-score {
|
| 450 |
+
color: var(--muted);
|
| 451 |
+
font-size: 11px;
|
| 452 |
+
font-weight: 800;
|
| 453 |
+
font-variant-numeric: tabular-nums;
|
| 454 |
+
}
|
| 455 |
+
.selection-token-stats {
|
| 456 |
+
display: flex;
|
| 457 |
+
flex-wrap: wrap;
|
| 458 |
+
gap: 4px;
|
| 459 |
+
padding: 0 10px 10px;
|
| 460 |
+
color: var(--muted);
|
| 461 |
+
font-size: 11px;
|
| 462 |
+
line-height: 1.25;
|
| 463 |
+
}
|
| 464 |
+
.selection-neighbors {
|
| 465 |
+
display: grid;
|
| 466 |
+
grid-template-rows: repeat(2, minmax(58px, auto));
|
| 467 |
+
gap: 6px;
|
| 468 |
+
}
|
| 469 |
+
.neighbor-card {
|
| 470 |
+
display: grid;
|
| 471 |
+
gap: 3px;
|
| 472 |
+
min-height: 58px;
|
| 473 |
+
padding: 7px;
|
| 474 |
+
border: 1px solid #d7dee9;
|
| 475 |
+
border-radius: 6px;
|
| 476 |
+
background: #fff;
|
| 477 |
+
color: var(--text);
|
| 478 |
+
text-decoration: none;
|
| 479 |
+
}
|
| 480 |
+
.neighbor-card:hover {
|
| 481 |
+
border-color: var(--accent);
|
| 482 |
+
color: var(--text);
|
| 483 |
+
text-decoration: none;
|
| 484 |
+
}
|
| 485 |
+
.neighbor-card.missing {
|
| 486 |
+
visibility: hidden;
|
| 487 |
+
}
|
| 488 |
+
.neighbor-card.loading {
|
| 489 |
+
color: var(--muted);
|
| 490 |
+
background: #f8fafc;
|
| 491 |
+
cursor: default;
|
| 492 |
+
}
|
| 493 |
+
.neighbor-target {
|
| 494 |
+
min-width: 0;
|
| 495 |
+
overflow: hidden;
|
| 496 |
+
white-space: nowrap;
|
| 497 |
+
text-overflow: ellipsis;
|
| 498 |
+
color: var(--muted);
|
| 499 |
+
font-size: 10px;
|
| 500 |
+
font-weight: 750;
|
| 501 |
+
}
|
| 502 |
+
.neighbor-label {
|
| 503 |
+
min-width: 0;
|
| 504 |
+
overflow: hidden;
|
| 505 |
+
white-space: nowrap;
|
| 506 |
+
text-overflow: ellipsis;
|
| 507 |
+
color: var(--text);
|
| 508 |
+
font-size: 13px;
|
| 509 |
+
font-weight: 850;
|
| 510 |
+
line-height: 1.2;
|
| 511 |
+
}
|
| 512 |
+
.neighbor-cos {
|
| 513 |
+
position: relative;
|
| 514 |
+
min-height: 18px;
|
| 515 |
+
padding: 2px 5px;
|
| 516 |
+
border: 1px solid #d7dee9;
|
| 517 |
+
border-radius: 5px;
|
| 518 |
+
background:
|
| 519 |
+
linear-gradient(
|
| 520 |
+
90deg,
|
| 521 |
+
color-mix(in srgb, var(--accent) 18%, white) 0,
|
| 522 |
+
color-mix(in srgb, var(--accent) 18%, white) var(--cos-width, 0%),
|
| 523 |
+
transparent var(--cos-width, 0%),
|
| 524 |
+
transparent 100%
|
| 525 |
+
);
|
| 526 |
+
color: var(--muted);
|
| 527 |
+
font-size: 11px;
|
| 528 |
+
font-weight: 800;
|
| 529 |
+
font-variant-numeric: tabular-nums;
|
| 530 |
+
}
|
| 531 |
+
.neighbor-metrics {
|
| 532 |
+
color: var(--muted);
|
| 533 |
+
font-size: 10px;
|
| 534 |
+
font-weight: 500;
|
| 535 |
+
font-variant-numeric: tabular-nums;
|
| 536 |
+
}
|
| 537 |
+
.selection-token-chip {
|
| 538 |
+
display: inline-flex;
|
| 539 |
+
align-items: center;
|
| 540 |
+
gap: 4px;
|
| 541 |
+
min-height: 20px;
|
| 542 |
+
max-width: 100%;
|
| 543 |
+
padding: 2px 6px;
|
| 544 |
+
border: 1px solid #d7dee9;
|
| 545 |
+
border-radius: 999px;
|
| 546 |
+
background: #fff;
|
| 547 |
+
color: #435066;
|
| 548 |
+
font-size: var(--token-font-size, 11px);
|
| 549 |
+
}
|
| 550 |
+
.selection-token-text {
|
| 551 |
+
min-width: 0;
|
| 552 |
+
overflow: hidden;
|
| 553 |
+
text-overflow: ellipsis;
|
| 554 |
+
white-space: nowrap;
|
| 555 |
+
}
|
| 556 |
+
.selection-export {
|
| 557 |
+
position: fixed;
|
| 558 |
+
left: 14px;
|
| 559 |
+
bottom: 10px;
|
| 560 |
+
z-index: 4;
|
| 561 |
+
display: none;
|
| 562 |
+
width: min(520px, calc(100vw - 180px));
|
| 563 |
+
height: 150px;
|
| 564 |
+
border: 1px solid var(--border);
|
| 565 |
+
border-radius: 7px;
|
| 566 |
+
background: rgb(255 255 255 / .94);
|
| 567 |
+
box-shadow: var(--shadow);
|
| 568 |
+
color: #334155;
|
| 569 |
+
font: 12px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
| 570 |
+
resize: vertical;
|
| 571 |
+
}
|
| 572 |
+
.selection-export.visible { display: block; }
|
| 573 |
+
.selection-export-copy {
|
| 574 |
+
position: fixed;
|
| 575 |
+
left: 14px;
|
| 576 |
+
bottom: 164px;
|
| 577 |
+
z-index: 5;
|
| 578 |
+
display: none;
|
| 579 |
+
width: auto;
|
| 580 |
+
min-height: 24px;
|
| 581 |
+
padding: 3px 8px;
|
| 582 |
+
border-radius: 6px;
|
| 583 |
+
color: #64748b;
|
| 584 |
+
background: rgb(255 255 255 / .92);
|
| 585 |
+
font-size: 11px;
|
| 586 |
+
font-weight: 800;
|
| 587 |
+
}
|
| 588 |
+
.selection-export-copy.visible { display: inline-flex; align-items: center; }
|
| 589 |
+
@media (max-width: 760px) {
|
| 590 |
+
main { padding: 12px; }
|
| 591 |
+
footer { padding: 0 12px 18px; }
|
| 592 |
+
.controls { grid-template-columns: 1fr; }
|
| 593 |
+
.control-stack { align-content: flex-start; }
|
| 594 |
+
.memory-control { right: 8px; bottom: 8px; }
|
| 595 |
+
.top { align-items: flex-start; flex-direction: column; }
|
| 596 |
+
.selection-panel {
|
| 597 |
+
left: 8px;
|
| 598 |
+
right: 8px;
|
| 599 |
+
top: auto;
|
| 600 |
+
bottom: 8px;
|
| 601 |
+
width: auto;
|
| 602 |
+
}
|
| 603 |
+
.selection-entry {
|
| 604 |
+
grid-template-columns: minmax(0, 1fr) 128px;
|
| 605 |
+
}
|
| 606 |
+
.selection-export {
|
| 607 |
+
left: 8px;
|
| 608 |
+
bottom: 40px;
|
| 609 |
+
width: calc(100vw - 16px);
|
| 610 |
+
height: 120px;
|
| 611 |
+
}
|
| 612 |
+
.selection-export-copy {
|
| 613 |
+
left: 8px;
|
| 614 |
+
bottom: 164px;
|
| 615 |
+
}
|
| 616 |
+
}
|
| 617 |
+
</style>
|
| 618 |
+
</head>
|
| 619 |
+
<body>
|
| 620 |
+
<header>
|
| 621 |
+
<div class="top">
|
| 622 |
+
<h1>ICA Explorer</h1>
|
| 623 |
+
<nav class="nav" aria-label="Primary">
|
| 624 |
+
<a href="/">Explorer</a>
|
| 625 |
+
<a href="/sae-explorer">SAE Explorer</a>
|
| 626 |
+
<a href="/stats">Stats</a>
|
| 627 |
+
<a href="/annotate">Annotate</a>
|
| 628 |
+
<a href="/random-components">Random</a>
|
| 629 |
+
</nav>
|
| 630 |
+
</div>
|
| 631 |
+
</header>
|
| 632 |
+
|
| 633 |
+
<main>
|
| 634 |
+
<div class="panel">
|
| 635 |
+
<div class="controls">
|
| 636 |
+
<label>
|
| 637 |
+
Text
|
| 638 |
+
<textarea id="probeText" spellcheck="false">Maya stopped at the bank before the trip, waiting in line to deposit a check and withdraw enough cash for the weekend.</textarea>
|
| 639 |
+
</label>
|
| 640 |
+
<div class="control-stack">
|
| 641 |
+
<label class="inline-control model-control">
|
| 642 |
+
<span>Model</span>
|
| 643 |
+
<select id="modelSelect"></select>
|
| 644 |
+
</label>
|
| 645 |
+
<label class="inline-control layer-control">
|
| 646 |
+
<span>Layer</span>
|
| 647 |
+
<select id="layerSelect"></select>
|
| 648 |
+
</label>
|
| 649 |
+
<span class="control-break" aria-hidden="true"></span>
|
| 650 |
+
<label class="inline-control topk-control">
|
| 651 |
+
<span>Top K</span>
|
| 652 |
+
<input id="topK" type="number" min="1" max="32" value="5" />
|
| 653 |
+
</label>
|
| 654 |
+
<label class="inline-control card-width-control">
|
| 655 |
+
<span>Card Width</span>
|
| 656 |
+
<input id="cardWidth" type="number" min="100" max="360" step="20" value="140" />
|
| 657 |
+
</label>
|
| 658 |
+
<label class="inline-control opacity-control">
|
| 659 |
+
<span>Opacity Cutoff</span>
|
| 660 |
+
<input id="weakRatio" type="number" min="0" max="1" step="0.05" value="0.5" />
|
| 661 |
+
</label>
|
| 662 |
+
<button id="runProbe" class="run-button" type="button">Run</button>
|
| 663 |
+
</div>
|
| 664 |
+
<label class="memory-control" title="Keep loaded models in VRAM when switching.">
|
| 665 |
+
<input id="keepModels" type="checkbox" checked />
|
| 666 |
+
<span>Cache LLMs in VRAM</span>
|
| 667 |
+
</label>
|
| 668 |
+
</div>
|
| 669 |
+
</div>
|
| 670 |
+
|
| 671 |
+
<div id="message" class="empty">Choose a layer and run the probe.</div>
|
| 672 |
+
<div id="results" class="results"></div>
|
| 673 |
+
</main>
|
| 674 |
+
<aside id="selectionPanel" class="selection-panel" aria-live="polite">
|
| 675 |
+
<div class="selection-title">
|
| 676 |
+
<span>Selected components</span>
|
| 677 |
+
<div class="selection-actions">
|
| 678 |
+
<button id="selectionExportToggle" class="selection-export-toggle" type="button">Hide text</button>
|
| 679 |
+
<button id="selectionClose" class="selection-close" type="button" title="Hide selected components" aria-label="Hide selected components">×</button>
|
| 680 |
+
</div>
|
| 681 |
+
</div>
|
| 682 |
+
<div id="selectionList" class="selection-list"></div>
|
| 683 |
+
</aside>
|
| 684 |
+
<div id="requestIndicator" class="request-indicator" role="status" aria-live="polite">
|
| 685 |
+
<span class="request-spinner" aria-hidden="true"></span>
|
| 686 |
+
<span>Waiting for server...</span>
|
| 687 |
+
</div>
|
| 688 |
+
<button id="selectionExportCopy" class="selection-export-copy" type="button">Copy</button>
|
| 689 |
+
<textarea id="selectionExport" class="selection-export" readonly aria-label="Selected component summary"></textarea>
|
| 690 |
+
|
| 691 |
+
<script>
|
| 692 |
+
const STORAGE_KEYS = {
|
| 693 |
+
probeText: "icaExplorer.probeText",
|
| 694 |
+
model: "icaExplorer.model",
|
| 695 |
+
layer: "icaExplorer.layer",
|
| 696 |
+
topK: "icaExplorer.topK",
|
| 697 |
+
cardWidth: "icaExplorer.cardWidth",
|
| 698 |
+
weakRatio: "icaExplorer.opacityCutoff",
|
| 699 |
+
keepModels: "icaExplorer.keepModels",
|
| 700 |
+
highlights: "icaExplorer.selectedComponents",
|
| 701 |
+
};
|
| 702 |
+
|
| 703 |
+
const state = {
|
| 704 |
+
meta: null,
|
| 705 |
+
models: [],
|
| 706 |
+
highlights: new Set(),
|
| 707 |
+
currentScores: new Map(),
|
| 708 |
+
tokenStats: new Map(),
|
| 709 |
+
tokenStatsRetries: new Map(),
|
| 710 |
+
componentNeighbors: new Map(),
|
| 711 |
+
componentNeighborsRetries: new Map(),
|
| 712 |
+
layerComponentMeta: new Map(),
|
| 713 |
+
layerComponentMetaLoading: new Set(),
|
| 714 |
+
selectionPanelHidden: false,
|
| 715 |
+
selectionExportHidden: false,
|
| 716 |
+
requestId: 0,
|
| 717 |
+
textTimer: 0,
|
| 718 |
+
pendingRequests: 0,
|
| 719 |
+
lastProbeOutput: null,
|
| 720 |
+
};
|
| 721 |
+
|
| 722 |
+
const els = {
|
| 723 |
+
text: document.getElementById("probeText"),
|
| 724 |
+
model: document.getElementById("modelSelect"),
|
| 725 |
+
layer: document.getElementById("layerSelect"),
|
| 726 |
+
topK: document.getElementById("topK"),
|
| 727 |
+
cardWidth: document.getElementById("cardWidth"),
|
| 728 |
+
weakRatio: document.getElementById("weakRatio"),
|
| 729 |
+
runProbe: document.getElementById("runProbe"),
|
| 730 |
+
keepModels: document.getElementById("keepModels"),
|
| 731 |
+
message: document.getElementById("message"),
|
| 732 |
+
results: document.getElementById("results"),
|
| 733 |
+
selectionPanel: document.getElementById("selectionPanel"),
|
| 734 |
+
selectionList: document.getElementById("selectionList"),
|
| 735 |
+
selectionClose: document.getElementById("selectionClose"),
|
| 736 |
+
selectionExportCopy: document.getElementById("selectionExportCopy"),
|
| 737 |
+
selectionExportToggle: document.getElementById("selectionExportToggle"),
|
| 738 |
+
selectionExport: document.getElementById("selectionExport"),
|
| 739 |
+
requestIndicator: document.getElementById("requestIndicator"),
|
| 740 |
+
};
|
| 741 |
+
|
| 742 |
+
async function api(path, options = {}) {
|
| 743 |
+
beginRequest();
|
| 744 |
+
try {
|
| 745 |
+
const res = await fetch(path, {
|
| 746 |
+
headers: { "content-type": "application/json" },
|
| 747 |
+
...options,
|
| 748 |
+
});
|
| 749 |
+
if (!res.ok) {
|
| 750 |
+
let detail = res.statusText;
|
| 751 |
+
try { detail = (await res.json()).detail || detail; } catch {}
|
| 752 |
+
throw new Error(detail);
|
| 753 |
+
}
|
| 754 |
+
return res.json();
|
| 755 |
+
} finally {
|
| 756 |
+
endRequest();
|
| 757 |
+
}
|
| 758 |
+
}
|
| 759 |
+
|
| 760 |
+
function beginRequest() {
|
| 761 |
+
state.pendingRequests += 1;
|
| 762 |
+
els.requestIndicator.classList.toggle("visible", state.pendingRequests > 0);
|
| 763 |
+
}
|
| 764 |
+
|
| 765 |
+
function endRequest() {
|
| 766 |
+
state.pendingRequests = Math.max(0, state.pendingRequests - 1);
|
| 767 |
+
els.requestIndicator.classList.toggle("visible", state.pendingRequests > 0);
|
| 768 |
+
}
|
| 769 |
+
|
| 770 |
+
async function init() {
|
| 771 |
+
const savedText = localStorage.getItem(STORAGE_KEYS.probeText);
|
| 772 |
+
restoreControlValues();
|
| 773 |
+
if (savedText !== null) els.text.value = savedText;
|
| 774 |
+
try {
|
| 775 |
+
const modelsOut = await api("/api/models");
|
| 776 |
+
state.models = (modelsOut.models || []).filter(model => model.probe_supported);
|
| 777 |
+
if (!state.models.length) throw new Error("No probe-supported models are available.");
|
| 778 |
+
els.model.innerHTML = state.models.map(model => `<option value="${escapeAttr(model.model_name)}">${escapeHtml(model.display_name || model.model_name)}</option>`).join("");
|
| 779 |
+
const savedModel = localStorage.getItem(STORAGE_KEYS.model);
|
| 780 |
+
if (savedModel && state.models.some(model => model.model_name === savedModel)) els.model.value = savedModel;
|
| 781 |
+
else if (state.models.some(model => model.model_name === "gpt2")) els.model.value = "gpt2";
|
| 782 |
+
await loadModelMeta({ restoreLayer: true });
|
| 783 |
+
runProbe();
|
| 784 |
+
} catch (err) {
|
| 785 |
+
showError(err.message);
|
| 786 |
+
}
|
| 787 |
+
}
|
| 788 |
+
|
| 789 |
+
async function runProbe() {
|
| 790 |
+
const requestId = ++state.requestId;
|
| 791 |
+
els.message.className = "empty";
|
| 792 |
+
if (!els.text.value.trim()) {
|
| 793 |
+
state.highlights.clear();
|
| 794 |
+
els.results.innerHTML = "";
|
| 795 |
+
els.message.hidden = false;
|
| 796 |
+
els.message.textContent = "Enter text to run the probe.";
|
| 797 |
+
return;
|
| 798 |
+
}
|
| 799 |
+
if (!els.results.children.length) {
|
| 800 |
+
els.message.hidden = false;
|
| 801 |
+
els.message.textContent = "Running probe...";
|
| 802 |
+
}
|
| 803 |
+
try {
|
| 804 |
+
const out = await api("/api/probe", {
|
| 805 |
+
method: "POST",
|
| 806 |
+
body: JSON.stringify({
|
| 807 |
+
text: els.text.value,
|
| 808 |
+
model_name: els.model.value,
|
| 809 |
+
layer: els.layer.value,
|
| 810 |
+
top_k: Number(els.topK.value || 5),
|
| 811 |
+
highlights: [],
|
| 812 |
+
keep_models: els.keepModels.checked,
|
| 813 |
+
}),
|
| 814 |
+
});
|
| 815 |
+
if (requestId !== state.requestId) return;
|
| 816 |
+
restoreHighlightsForContext(out);
|
| 817 |
+
renderResults(out);
|
| 818 |
+
} catch (err) {
|
| 819 |
+
if (requestId !== state.requestId) return;
|
| 820 |
+
showError(err.message);
|
| 821 |
+
}
|
| 822 |
+
}
|
| 823 |
+
|
| 824 |
+
function scheduleProbe() {
|
| 825 |
+
localStorage.setItem(STORAGE_KEYS.probeText, els.text.value);
|
| 826 |
+
persistControls();
|
| 827 |
+
persistHighlights();
|
| 828 |
+
renderSelectionExport();
|
| 829 |
+
}
|
| 830 |
+
|
| 831 |
+
function handleTextKeydown(event) {
|
| 832 |
+
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
| 833 |
+
event.preventDefault();
|
| 834 |
+
localStorage.setItem(STORAGE_KEYS.probeText, els.text.value);
|
| 835 |
+
persistControls();
|
| 836 |
+
runProbe();
|
| 837 |
+
}
|
| 838 |
+
}
|
| 839 |
+
|
| 840 |
+
function updateCardWidth() {
|
| 841 |
+
const value = Math.max(100, Math.min(360, Number(els.cardWidth.value || 140)));
|
| 842 |
+
document.documentElement.style.setProperty("--token-card-width", `${value}px`);
|
| 843 |
+
localStorage.setItem(STORAGE_KEYS.cardWidth, String(value));
|
| 844 |
+
}
|
| 845 |
+
|
| 846 |
+
function rerenderLastProbe() {
|
| 847 |
+
if (state.lastProbeOutput) renderResults(state.lastProbeOutput);
|
| 848 |
+
}
|
| 849 |
+
|
| 850 |
+
function renderResults(out) {
|
| 851 |
+
state.lastProbeOutput = out;
|
| 852 |
+
if (out.truncated) {
|
| 853 |
+
els.message.hidden = false;
|
| 854 |
+
els.message.className = "empty";
|
| 855 |
+
els.message.textContent = `Input was truncated to ${out.max_length} tokens.`;
|
| 856 |
+
} else {
|
| 857 |
+
els.message.hidden = true;
|
| 858 |
+
}
|
| 859 |
+
const annotationMeta = annotationMetaMap(out.annotated_components || []);
|
| 860 |
+
state.annotationMeta = annotationMeta;
|
| 861 |
+
state.currentScores = new Map();
|
| 862 |
+
(out.tokens || []).forEach(token => (token.top || []).forEach(pair => {
|
| 863 |
+
const component = Number(pair.component);
|
| 864 |
+
const score = Math.abs(Number(pair.score || 0));
|
| 865 |
+
const previous = state.currentScores.get(component);
|
| 866 |
+
if (!previous || score > Math.abs(Number(previous.score || 0))) {
|
| 867 |
+
state.currentScores.set(component, { component, score: Number(pair.score || 0) });
|
| 868 |
+
}
|
| 869 |
+
}));
|
| 870 |
+
|
| 871 |
+
els.results.innerHTML = out.tokens.map(token => `
|
| 872 |
+
<div class="token-card">
|
| 873 |
+
<div class="token-text" title="${escapeAttr(token.token)}">${escapeHtml(token.token_text || token.token)}</div>
|
| 874 |
+
${token.top.map(pair => scoreBadge(pair, annotationMeta, tokenTopAbsScore(token))).join("")}
|
| 875 |
+
${predictionRow(token)}
|
| 876 |
+
</div>
|
| 877 |
+
`).join("");
|
| 878 |
+
els.results.querySelectorAll(".badge").forEach(node => {
|
| 879 |
+
node.addEventListener("click", event => selectComponent(event, Number(node.dataset.component), node.dataset.selectionKey));
|
| 880 |
+
});
|
| 881 |
+
paintHighlights();
|
| 882 |
+
}
|
| 883 |
+
|
| 884 |
+
function predictionRow(token) {
|
| 885 |
+
const pred = token.prediction;
|
| 886 |
+
if (!pred) return "";
|
| 887 |
+
const text = visibleToken(pred.token_text || pred.token || "");
|
| 888 |
+
return `
|
| 889 |
+
<div class="prediction-row" title="${escapeAttr(`next token: ${text}`)}">
|
| 890 |
+
<span class="prediction-label">next</span>
|
| 891 |
+
<span class="prediction-token">${escapeHtml(text)}</span>
|
| 892 |
+
</div>
|
| 893 |
+
`;
|
| 894 |
+
}
|
| 895 |
+
|
| 896 |
+
function scoreBadge(pair, annotationMeta, tokenTopAbs) {
|
| 897 |
+
const component = Number(pair.component);
|
| 898 |
+
const score = Number(pair.score || 0);
|
| 899 |
+
const selectionKey = componentSelectionKey(component, score);
|
| 900 |
+
const active = state.highlights.has(selectionKey);
|
| 901 |
+
const ratio = tokenTopAbs > 0 ? Math.abs(score) / tokenTopAbs : 0;
|
| 902 |
+
const width = 100 * Math.max(0, Math.min(1, ratio));
|
| 903 |
+
const meta = annotationForScore(annotationMeta.get(component), score);
|
| 904 |
+
const dot = meta ? `<span class="annotation-dot ${escapeAttr(meta.confidence)}" aria-hidden="true">${escapeHtml(meta.type_letter)}</span>` : "";
|
| 905 |
+
const label = meta ? meta.label : `C${component}`;
|
| 906 |
+
const title = meta ? `C${component}: ${annotationHint(meta)}` : `C${component}`;
|
| 907 |
+
const cutoff = Math.max(0, Math.min(1, Number(els.weakRatio.value || 0.5)));
|
| 908 |
+
const weak = Number.isFinite(tokenTopAbs) && tokenTopAbs > 0 && Math.abs(score) < tokenTopAbs * cutoff;
|
| 909 |
+
return `
|
| 910 |
+
<div class="score-row">
|
| 911 |
+
<button class="badge ${active ? "hot" : ""} ${weak ? "weak" : ""}" type="button" data-component="${component}" data-selection-key="${escapeAttr(selectionKey)}" data-score="${escapeAttr(score)}" aria-pressed="${active ? "true" : "false"}" title="${escapeAttr(title)}" style="--score-width:${width.toFixed(1)}%;--component-color:${componentColor(component)}">
|
| 912 |
+
<span class="badge-main">${dot}<b class="badge-label">${escapeHtml(label)}</b></span>
|
| 913 |
+
</button>
|
| 914 |
+
<span class="score">${formatScore(pair.score)}</span>
|
| 915 |
+
</div>
|
| 916 |
+
`;
|
| 917 |
+
}
|
| 918 |
+
|
| 919 |
+
function tokenTopAbsScore(token) {
|
| 920 |
+
const scores = (token.top || []).map(pair => Math.abs(Number(pair.score || 0)));
|
| 921 |
+
return scores.length ? Math.max(...scores) : 0;
|
| 922 |
+
}
|
| 923 |
+
|
| 924 |
+
function selectComponent(event, component, selectionKey) {
|
| 925 |
+
state.selectionPanelHidden = false;
|
| 926 |
+
if (event.ctrlKey || event.metaKey) {
|
| 927 |
+
if (state.highlights.has(selectionKey)) state.highlights.delete(selectionKey);
|
| 928 |
+
else state.highlights.add(selectionKey);
|
| 929 |
+
} else {
|
| 930 |
+
state.highlights.clear();
|
| 931 |
+
state.highlights.add(selectionKey);
|
| 932 |
+
}
|
| 933 |
+
persistHighlights();
|
| 934 |
+
paintHighlights();
|
| 935 |
+
}
|
| 936 |
+
|
| 937 |
+
|
| 938 |
+
function highlightStorageKey(model = els.model.value, layer = els.layer.value) {
|
| 939 |
+
return `${model}:${layer}`;
|
| 940 |
+
}
|
| 941 |
+
|
| 942 |
+
function readHighlightStore() {
|
| 943 |
+
try {
|
| 944 |
+
const parsed = JSON.parse(localStorage.getItem(STORAGE_KEYS.highlights) || "{}");
|
| 945 |
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
| 946 |
+
} catch {
|
| 947 |
+
return {};
|
| 948 |
+
}
|
| 949 |
+
}
|
| 950 |
+
|
| 951 |
+
function persistHighlights() {
|
| 952 |
+
const store = readHighlightStore();
|
| 953 |
+
const key = highlightStorageKey();
|
| 954 |
+
const values = [...state.highlights].filter(isValidSelectionKey).sort(compareSelectionKeys);
|
| 955 |
+
if (values.length) store[key] = values;
|
| 956 |
+
else delete store[key];
|
| 957 |
+
localStorage.setItem(STORAGE_KEYS.highlights, JSON.stringify(store));
|
| 958 |
+
}
|
| 959 |
+
|
| 960 |
+
function restoreHighlightsForContext(out) {
|
| 961 |
+
const store = readHighlightStore();
|
| 962 |
+
const saved = Array.isArray(store[highlightStorageKey()]) ? store[highlightStorageKey()] : [];
|
| 963 |
+
state.highlights = new Set(saved.map(normalizeSelectionKey).filter(Boolean));
|
| 964 |
+
}
|
| 965 |
+
|
| 966 |
+
function paintHighlights() {
|
| 967 |
+
els.results.querySelectorAll(".badge").forEach(node => {
|
| 968 |
+
const active = state.highlights.has(node.dataset.selectionKey);
|
| 969 |
+
node.classList.toggle("hot", active);
|
| 970 |
+
node.setAttribute("aria-pressed", active ? "true" : "false");
|
| 971 |
+
});
|
| 972 |
+
renderSelectionPanel();
|
| 973 |
+
renderSelectionExport();
|
| 974 |
+
}
|
| 975 |
+
|
| 976 |
+
function renderSelectionPanel() {
|
| 977 |
+
const selections = [...state.highlights].filter(isValidSelectionKey).sort(compareSelectionKeys).map(parseSelectionKey);
|
| 978 |
+
els.selectionPanel.classList.toggle("visible", selections.length > 0 && !state.selectionPanelHidden);
|
| 979 |
+
els.selectionList.innerHTML = selections.map(selection => {
|
| 980 |
+
const component = selection.component;
|
| 981 |
+
const score = selection.sign * Math.abs(Number(state.currentScores?.get(component)?.score || 1));
|
| 982 |
+
ensureLayerComponentMeta();
|
| 983 |
+
ensureTokenStats(component);
|
| 984 |
+
ensureComponentNeighbors(component);
|
| 985 |
+
const stats = state.tokenStats.get(selectionStatsKey(component));
|
| 986 |
+
const neighbors = state.componentNeighbors.get(selectionStatsKey(component));
|
| 987 |
+
const sourceLabel = sourceSideLabel(componentMeta(component), score);
|
| 988 |
+
return `
|
| 989 |
+
<div class="selection-entry" style="--component-color:${componentColor(component)}">
|
| 990 |
+
<div class="selection-item">
|
| 991 |
+
<a class="selection-link" href="${escapeAttr(componentAnnotateUrl(component))}">
|
| 992 |
+
<span>${escapeHtml(els.layer.value)} C${component}${selection.sign < 0 ? " -" : " +"}</span>
|
| 993 |
+
<span class="selection-score">${selectionMetrics(component)}</span>
|
| 994 |
+
</a>
|
| 995 |
+
<div class="selection-token-stats">${renderTokenStats(stats)}</div>
|
| 996 |
+
</div>
|
| 997 |
+
${renderComponentNeighbors(neighbors, score, sourceLabel)}
|
| 998 |
+
</div>
|
| 999 |
+
`;
|
| 1000 |
+
}).join("");
|
| 1001 |
+
}
|
| 1002 |
+
|
| 1003 |
+
function renderSelectionExport() {
|
| 1004 |
+
const selections = [...state.highlights].filter(isValidSelectionKey).sort(compareSelectionKeys).map(parseSelectionKey);
|
| 1005 |
+
if (!selections.length) {
|
| 1006 |
+
els.selectionExport.classList.remove("visible");
|
| 1007 |
+
els.selectionExportCopy.classList.remove("visible");
|
| 1008 |
+
els.selectionExportToggle.classList.remove("visible");
|
| 1009 |
+
els.selectionExport.value = "";
|
| 1010 |
+
return;
|
| 1011 |
+
}
|
| 1012 |
+
const selection = selections[0];
|
| 1013 |
+
els.selectionExport.value = componentExportText(selection);
|
| 1014 |
+
els.selectionExportToggle.classList.add("visible");
|
| 1015 |
+
els.selectionExportToggle.textContent = state.selectionExportHidden ? "Show text" : "Hide text";
|
| 1016 |
+
els.selectionExport.classList.toggle("visible", !state.selectionExportHidden);
|
| 1017 |
+
els.selectionExportCopy.classList.toggle("visible", !state.selectionExportHidden);
|
| 1018 |
+
}
|
| 1019 |
+
|
| 1020 |
+
function componentExportText(selection) {
|
| 1021 |
+
const component = selection.component;
|
| 1022 |
+
const meta = componentMeta(component);
|
| 1023 |
+
const erf = Number(meta?.effective_context_mean);
|
| 1024 |
+
const erfText = Number.isFinite(erf) ? Number(erf).toFixed(1).replace(/\.0$/, "") : "?";
|
| 1025 |
+
const activations = componentTokenActivations(component, selection.sign);
|
| 1026 |
+
const activationLines = activations.length
|
| 1027 |
+
? activations.map(item => `${item.index}: ${item.token} -> ${item.score.toFixed(1)}`).join("\n")
|
| 1028 |
+
: "(component is not present in the current top-k token activations)";
|
| 1029 |
+
return [
|
| 1030 |
+
`[Component C${component}, Effective Receptive Field=${erfText}]`,
|
| 1031 |
+
"",
|
| 1032 |
+
"INPUT TEXT:",
|
| 1033 |
+
els.text.value,
|
| 1034 |
+
"",
|
| 1035 |
+
"TOKEN ACTIVATIONS:",
|
| 1036 |
+
activationLines,
|
| 1037 |
+
"",
|
| 1038 |
+
"",
|
| 1039 |
+
].join("\n");
|
| 1040 |
+
}
|
| 1041 |
+
|
| 1042 |
+
function componentTokenActivations(component, sign) {
|
| 1043 |
+
const rows = [];
|
| 1044 |
+
(state.lastProbeOutput?.tokens || []).forEach(token => {
|
| 1045 |
+
const item = (token.top || []).find(pair => Number(pair.component) === Number(component));
|
| 1046 |
+
if (!item) return;
|
| 1047 |
+
const score = Number(item.score || 0);
|
| 1048 |
+
if (!Number.isFinite(score) || score === 0) return;
|
| 1049 |
+
if ((score < 0 ? -1 : 1) !== sign) return;
|
| 1050 |
+
rows.push({ index: Number(token.position), token: exportTokenText(token.token_text || token.token), score });
|
| 1051 |
+
});
|
| 1052 |
+
return rows.sort((a, b) => Math.abs(b.score) - Math.abs(a.score));
|
| 1053 |
+
}
|
| 1054 |
+
|
| 1055 |
+
async function copySelectionExport() {
|
| 1056 |
+
const text = els.selectionExport.value;
|
| 1057 |
+
if (!text) return;
|
| 1058 |
+
try {
|
| 1059 |
+
await navigator.clipboard.writeText(text);
|
| 1060 |
+
} catch {
|
| 1061 |
+
els.selectionExport.focus();
|
| 1062 |
+
els.selectionExport.select();
|
| 1063 |
+
document.execCommand("copy");
|
| 1064 |
+
els.selectionExport.setSelectionRange(0, 0);
|
| 1065 |
+
}
|
| 1066 |
+
const previous = els.selectionExportCopy.textContent;
|
| 1067 |
+
els.selectionExportCopy.textContent = "Copied";
|
| 1068 |
+
window.setTimeout(() => { els.selectionExportCopy.textContent = previous; }, 900);
|
| 1069 |
+
}
|
| 1070 |
+
|
| 1071 |
+
function selectionMetrics(component) {
|
| 1072 |
+
const parts = [];
|
| 1073 |
+
const meta = componentMeta(component);
|
| 1074 |
+
const ecl = meta?.effective_context_mean;
|
| 1075 |
+
const kurtosis = meta?.excess_kurtosis;
|
| 1076 |
+
if (Number.isFinite(ecl)) parts.push(`ERF=${Number(ecl).toFixed(1)}`);
|
| 1077 |
+
if (Number.isFinite(kurtosis)) parts.push(`K=${Number(kurtosis).toFixed(1)}`);
|
| 1078 |
+
return escapeHtml(parts.join(" · "));
|
| 1079 |
+
}
|
| 1080 |
+
|
| 1081 |
+
function selectionStatsKey(component) {
|
| 1082 |
+
return `${els.model.value}:${els.layer.value}:${component}`;
|
| 1083 |
+
}
|
| 1084 |
+
|
| 1085 |
+
function layerMetaKey(model = els.model.value, layer = els.layer.value) {
|
| 1086 |
+
return `${model}:${layer}`;
|
| 1087 |
+
}
|
| 1088 |
+
|
| 1089 |
+
function componentMeta(component) {
|
| 1090 |
+
return state.annotationMeta?.get(component) || state.layerComponentMeta.get(layerMetaKey())?.get(Number(component)) || null;
|
| 1091 |
+
}
|
| 1092 |
+
|
| 1093 |
+
function ensureLayerComponentMeta() {
|
| 1094 |
+
const key = layerMetaKey();
|
| 1095 |
+
if (state.layerComponentMeta.has(key) || state.layerComponentMetaLoading.has(key)) return;
|
| 1096 |
+
state.layerComponentMetaLoading.add(key);
|
| 1097 |
+
const params = new URLSearchParams({
|
| 1098 |
+
model: state.meta?.model_name || els.model.value,
|
| 1099 |
+
layer: els.layer.value,
|
| 1100 |
+
});
|
| 1101 |
+
api(`/api/components?${params.toString()}`)
|
| 1102 |
+
.then(data => {
|
| 1103 |
+
state.layerComponentMeta.set(key, componentMetaMap(data.components || []));
|
| 1104 |
+
renderSelectionPanel();
|
| 1105 |
+
renderSelectionExport();
|
| 1106 |
+
})
|
| 1107 |
+
.catch(() => {
|
| 1108 |
+
state.layerComponentMeta.set(key, new Map());
|
| 1109 |
+
})
|
| 1110 |
+
.finally(() => {
|
| 1111 |
+
state.layerComponentMetaLoading.delete(key);
|
| 1112 |
+
});
|
| 1113 |
+
}
|
| 1114 |
+
|
| 1115 |
+
function componentSelectionKey(component, score) {
|
| 1116 |
+
return `${Number(component)}:${Number(score) < 0 ? "-" : "+"}`;
|
| 1117 |
+
}
|
| 1118 |
+
|
| 1119 |
+
function normalizeSelectionKey(value) {
|
| 1120 |
+
if (typeof value === "number" && Number.isFinite(value)) return `${value}:+`;
|
| 1121 |
+
const text = String(value || "");
|
| 1122 |
+
if (/^\d+:[+-]$/.test(text)) return text;
|
| 1123 |
+
const legacy = Number(text);
|
| 1124 |
+
return Number.isFinite(legacy) ? `${legacy}:+` : "";
|
| 1125 |
+
}
|
| 1126 |
+
|
| 1127 |
+
function isValidSelectionKey(value) {
|
| 1128 |
+
return /^\d+:[+-]$/.test(String(value || ""));
|
| 1129 |
+
}
|
| 1130 |
+
|
| 1131 |
+
function parseSelectionKey(value) {
|
| 1132 |
+
const [component, sign] = String(value).split(":");
|
| 1133 |
+
return { component: Number(component), sign: sign === "-" ? -1 : 1 };
|
| 1134 |
+
}
|
| 1135 |
+
|
| 1136 |
+
function compareSelectionKeys(a, b) {
|
| 1137 |
+
const aa = parseSelectionKey(a);
|
| 1138 |
+
const bb = parseSelectionKey(b);
|
| 1139 |
+
return aa.component - bb.component || bb.sign - aa.sign;
|
| 1140 |
+
}
|
| 1141 |
+
|
| 1142 |
+
function hasHighlightedComponent(component) {
|
| 1143 |
+
return state.highlights.has(`${Number(component)}:+`) || state.highlights.has(`${Number(component)}:-`);
|
| 1144 |
+
}
|
| 1145 |
+
|
| 1146 |
+
function ensureTokenStats(component) {
|
| 1147 |
+
const key = selectionStatsKey(component);
|
| 1148 |
+
if (state.tokenStats.has(key)) return;
|
| 1149 |
+
const retries = state.tokenStatsRetries.get(key) || 0;
|
| 1150 |
+
if (retries >= 2) {
|
| 1151 |
+
state.tokenStats.set(key, { error: "token stats unavailable", tokens: [] });
|
| 1152 |
+
return;
|
| 1153 |
+
}
|
| 1154 |
+
state.tokenStatsRetries.set(key, retries + 1);
|
| 1155 |
+
state.tokenStats.set(key, null);
|
| 1156 |
+
const params = new URLSearchParams({
|
| 1157 |
+
model: state.meta?.model_name || els.model.value,
|
| 1158 |
+
layer: els.layer.value,
|
| 1159 |
+
component: String(component),
|
| 1160 |
+
});
|
| 1161 |
+
api(`/api/component-token-stats?${params.toString()}`)
|
| 1162 |
+
.then(data => {
|
| 1163 |
+
state.tokenStats.set(key, data);
|
| 1164 |
+
if (hasHighlightedComponent(component) && key === selectionStatsKey(component)) renderSelectionPanel();
|
| 1165 |
+
})
|
| 1166 |
+
.catch(err => {
|
| 1167 |
+
state.tokenStats.set(key, { error: err.message || "token stats unavailable", tokens: [] });
|
| 1168 |
+
if (hasHighlightedComponent(component) && key === selectionStatsKey(component)) renderSelectionPanel();
|
| 1169 |
+
});
|
| 1170 |
+
}
|
| 1171 |
+
|
| 1172 |
+
function ensureComponentNeighbors(component) {
|
| 1173 |
+
const key = selectionStatsKey(component);
|
| 1174 |
+
if (state.componentNeighbors.has(key)) return;
|
| 1175 |
+
const retries = state.componentNeighborsRetries.get(key) || 0;
|
| 1176 |
+
if (retries >= 2) {
|
| 1177 |
+
state.componentNeighbors.set(key, { error: "neighbors unavailable", neighbors: [] });
|
| 1178 |
+
return;
|
| 1179 |
+
}
|
| 1180 |
+
state.componentNeighborsRetries.set(key, retries + 1);
|
| 1181 |
+
state.componentNeighbors.set(key, null);
|
| 1182 |
+
const params = new URLSearchParams({
|
| 1183 |
+
model: state.meta?.model_name || els.model.value,
|
| 1184 |
+
layer: els.layer.value,
|
| 1185 |
+
component: String(component),
|
| 1186 |
+
});
|
| 1187 |
+
api(`/api/component-neighbors?${params.toString()}`)
|
| 1188 |
+
.then(data => {
|
| 1189 |
+
state.componentNeighbors.set(key, data);
|
| 1190 |
+
if (hasHighlightedComponent(component) && key === selectionStatsKey(component)) renderSelectionPanel();
|
| 1191 |
+
})
|
| 1192 |
+
.catch(err => {
|
| 1193 |
+
state.componentNeighbors.set(key, { error: err.message || "neighbors unavailable", neighbors: [] });
|
| 1194 |
+
if (hasHighlightedComponent(component) && key === selectionStatsKey(component)) renderSelectionPanel();
|
| 1195 |
+
});
|
| 1196 |
+
}
|
| 1197 |
+
|
| 1198 |
+
function renderComponentNeighbors(data, sourceScore, sourceLabel) {
|
| 1199 |
+
if (data === null) {
|
| 1200 |
+
return `
|
| 1201 |
+
<div class="selection-neighbors">
|
| 1202 |
+
${renderLoadingNeighbor("prev")}
|
| 1203 |
+
${renderLoadingNeighbor("next")}
|
| 1204 |
+
</div>
|
| 1205 |
+
`;
|
| 1206 |
+
}
|
| 1207 |
+
const byDirection = new Map((data?.neighbors || []).map(item => [item.direction, item]));
|
| 1208 |
+
return `
|
| 1209 |
+
<div class="selection-neighbors">
|
| 1210 |
+
${renderNeighborCard(byDirection.get("prev"), "prev", sourceScore, sourceLabel)}
|
| 1211 |
+
${renderNeighborCard(byDirection.get("next"), "next", sourceScore, sourceLabel)}
|
| 1212 |
+
</div>
|
| 1213 |
+
`;
|
| 1214 |
+
}
|
| 1215 |
+
|
| 1216 |
+
function renderNeighborCard(neighbor, direction, sourceScore, sourceLabel) {
|
| 1217 |
+
if (!neighbor) return renderMissingNeighbor(direction);
|
| 1218 |
+
const layer = String(neighbor.neighbor_layer || "");
|
| 1219 |
+
const component = Number(neighbor.neighbor_component);
|
| 1220 |
+
const label = neighborLabel(neighbor, sourceScore, sourceLabel);
|
| 1221 |
+
const cos = Number(neighbor.abs_cosine);
|
| 1222 |
+
const href = componentAnnotateUrl(component, layer);
|
| 1223 |
+
return `
|
| 1224 |
+
<a class="neighbor-card" href="${escapeAttr(href)}" title="${escapeAttr(`${direction}: ${layer} C${component} ${label}`)}">
|
| 1225 |
+
<span class="neighbor-target">${escapeHtml(`${layer} C${component}`)}</span>
|
| 1226 |
+
<span class="neighbor-label">${escapeHtml(label)}</span>
|
| 1227 |
+
<span class="neighbor-metrics">${neighborMetrics(neighbor)}</span>
|
| 1228 |
+
<span class="neighbor-cos" style="--cos-width:${cosWidth(cos)}%">cos=${Number.isFinite(cos) ? cos.toFixed(3) : "?"}</span>
|
| 1229 |
+
</a>
|
| 1230 |
+
`;
|
| 1231 |
+
}
|
| 1232 |
+
|
| 1233 |
+
function renderMissingNeighbor(direction) {
|
| 1234 |
+
return `
|
| 1235 |
+
<div class="neighbor-card missing" aria-hidden="true">
|
| 1236 |
+
<span class="neighbor-target"></span>
|
| 1237 |
+
<span class="neighbor-label"></span>
|
| 1238 |
+
<span class="neighbor-metrics"></span>
|
| 1239 |
+
<span class="neighbor-cos"></span>
|
| 1240 |
+
</div>
|
| 1241 |
+
`;
|
| 1242 |
+
}
|
| 1243 |
+
|
| 1244 |
+
function renderLoadingNeighbor(direction) {
|
| 1245 |
+
return `
|
| 1246 |
+
<div class="neighbor-card loading">
|
| 1247 |
+
<span class="neighbor-target">loading</span>
|
| 1248 |
+
<span class="neighbor-label"></span>
|
| 1249 |
+
<span class="neighbor-metrics"></span>
|
| 1250 |
+
<span class="neighbor-cos"></span>
|
| 1251 |
+
</div>
|
| 1252 |
+
`;
|
| 1253 |
+
}
|
| 1254 |
+
|
| 1255 |
+
function neighborLabel(neighbor, sourceScore, sourceLabel) {
|
| 1256 |
+
const positive = visibleAnnotationLabel(neighbor?.positive_label, neighbor?.positive_confidence);
|
| 1257 |
+
const negative = visibleAnnotationLabel(neighbor?.negative_label, neighbor?.negative_confidence);
|
| 1258 |
+
const positiveMatch = labelSimilarity(sourceLabel, positive);
|
| 1259 |
+
const negativeMatch = labelSimilarity(sourceLabel, negative);
|
| 1260 |
+
if (positiveMatch > negativeMatch && positiveMatch > 0) return positive;
|
| 1261 |
+
if (negativeMatch > positiveMatch && negativeMatch > 0) return negative;
|
| 1262 |
+
const sourceSign = Number(sourceScore) < 0 ? -1 : 1;
|
| 1263 |
+
const neighborSign = Number(neighbor?.neighbor_sign) < 0 ? -1 : 1;
|
| 1264 |
+
if (sourceSign * neighborSign < 0) return negative || "unlabeled";
|
| 1265 |
+
return positive || "unlabeled";
|
| 1266 |
+
}
|
| 1267 |
+
|
| 1268 |
+
function cosWidth(value) {
|
| 1269 |
+
const cos = Math.max(0, Math.min(1, Number(value) || 0));
|
| 1270 |
+
return (100 * cos).toFixed(1);
|
| 1271 |
+
}
|
| 1272 |
+
|
| 1273 |
+
function neighborMetrics(neighbor) {
|
| 1274 |
+
const parts = [];
|
| 1275 |
+
const ecl = Number(neighbor?.effective_context_mean);
|
| 1276 |
+
const kurtosis = Number(neighbor?.excess_kurtosis);
|
| 1277 |
+
if (Number.isFinite(ecl)) parts.push(`ERF=${ecl.toFixed(1)}`);
|
| 1278 |
+
if (Number.isFinite(kurtosis)) parts.push(`K=${kurtosis.toFixed(1)}`);
|
| 1279 |
+
return escapeHtml(parts.join(" · "));
|
| 1280 |
+
}
|
| 1281 |
+
|
| 1282 |
+
function sourceSideLabel(meta, score) {
|
| 1283 |
+
if (!meta) return "";
|
| 1284 |
+
return Number(score) < 0
|
| 1285 |
+
? visibleAnnotationLabel(meta.negative_label, meta.negative_confidence)
|
| 1286 |
+
: visibleAnnotationLabel(meta.positive_label, meta.positive_confidence);
|
| 1287 |
+
}
|
| 1288 |
+
|
| 1289 |
+
function labelSimilarity(a, b) {
|
| 1290 |
+
const aTokens = labelTokens(a);
|
| 1291 |
+
const bTokens = labelTokens(b);
|
| 1292 |
+
if (!aTokens.size || !bTokens.size) return 0;
|
| 1293 |
+
let overlap = 0;
|
| 1294 |
+
aTokens.forEach(token => { if (bTokens.has(token)) overlap += 1; });
|
| 1295 |
+
return overlap / Math.max(aTokens.size, bTokens.size);
|
| 1296 |
+
}
|
| 1297 |
+
|
| 1298 |
+
function labelTokens(value) {
|
| 1299 |
+
const text = String(value || "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
| 1300 |
+
if (!text || text === "?") return new Set();
|
| 1301 |
+
return new Set(text.split(/\s+/).filter(Boolean));
|
| 1302 |
+
}
|
| 1303 |
+
|
| 1304 |
+
function renderTokenStats(stats) {
|
| 1305 |
+
if (stats === null) return `<span>loading tokens...</span>`;
|
| 1306 |
+
if (stats?.error) return `<span>${escapeHtml(stats.error)}</span>`;
|
| 1307 |
+
const tokens = stats?.tokens || [];
|
| 1308 |
+
if (!tokens.length) return `<span>no example tokens</span>`;
|
| 1309 |
+
const maxCount = Math.max(1, ...tokens.map(item => Number(item.count || 0)));
|
| 1310 |
+
const tokenChips = tokens.map(item => `
|
| 1311 |
+
<span class="selection-token-chip" title="${escapeAttr(`${visibleToken(item.token)}: ${item.count}`)}" style="--token-font-size:${tokenFontSize(item.count, maxCount)}px">
|
| 1312 |
+
<span class="selection-token-text">${escapeHtml(visibleToken(item.token))}</span>
|
| 1313 |
+
</span>
|
| 1314 |
+
`).join("");
|
| 1315 |
+
return tokenChips;
|
| 1316 |
+
}
|
| 1317 |
+
|
| 1318 |
+
function tokenFontSize(count, maxCount) {
|
| 1319 |
+
const value = Math.max(0, Number(count || 0));
|
| 1320 |
+
const ratio = maxCount <= 1 ? 0 : Math.log1p(value) / Math.log1p(maxCount);
|
| 1321 |
+
return (9 + 5 * ratio).toFixed(1);
|
| 1322 |
+
}
|
| 1323 |
+
|
| 1324 |
+
function visibleToken(value) {
|
| 1325 |
+
const text = String(value || "");
|
| 1326 |
+
if (text === " ") return "[space]";
|
| 1327 |
+
if (text === "\n") return "[newline]";
|
| 1328 |
+
return text.replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
| 1329 |
+
}
|
| 1330 |
+
|
| 1331 |
+
function exportTokenText(value) {
|
| 1332 |
+
const text = String(value || "");
|
| 1333 |
+
if (text === ".") return "period";
|
| 1334 |
+
if (text === ",") return "comma";
|
| 1335 |
+
if (text === " ") return "[space]";
|
| 1336 |
+
if (text === "\n") return "[newline]";
|
| 1337 |
+
return visibleToken(text).trim() || visibleToken(text);
|
| 1338 |
+
}
|
| 1339 |
+
|
| 1340 |
+
function componentAnnotateUrl(component, layer = els.layer.value) {
|
| 1341 |
+
const params = new URLSearchParams({
|
| 1342 |
+
model: state.meta?.model_name || els.model.value,
|
| 1343 |
+
layer,
|
| 1344 |
+
component: String(component),
|
| 1345 |
+
});
|
| 1346 |
+
return `/annotate?${params.toString()}`;
|
| 1347 |
+
}
|
| 1348 |
+
|
| 1349 |
+
async function loadModelMeta(options = {}) {
|
| 1350 |
+
state.meta = await api(`/api/meta?model=${encodeURIComponent(els.model.value)}`);
|
| 1351 |
+
els.layer.innerHTML = state.meta.layers.map(layer => `<option value="${escapeAttr(layer)}">${escapeHtml(layer)}</option>`).join("");
|
| 1352 |
+
if (!state.meta.layers.length) throw new Error(`No ICA layers are available for ${state.meta.display_name || state.meta.model_name}.`);
|
| 1353 |
+
const savedLayer = localStorage.getItem(STORAGE_KEYS.layer);
|
| 1354 |
+
if (options.restoreLayer && savedLayer && state.meta.layers.includes(savedLayer)) els.layer.value = savedLayer;
|
| 1355 |
+
}
|
| 1356 |
+
|
| 1357 |
+
function restoreControlValues() {
|
| 1358 |
+
setNumberInputFromStorage(els.topK, STORAGE_KEYS.topK, 1, 32);
|
| 1359 |
+
setNumberInputFromStorage(els.cardWidth, STORAGE_KEYS.cardWidth, 100, 360);
|
| 1360 |
+
setNumberInputFromStorage(els.weakRatio, STORAGE_KEYS.weakRatio, 0, 1);
|
| 1361 |
+
els.keepModels.checked = localStorage.getItem(STORAGE_KEYS.keepModels) !== "0";
|
| 1362 |
+
}
|
| 1363 |
+
|
| 1364 |
+
function setNumberInputFromStorage(input, key, min, max) {
|
| 1365 |
+
const raw = localStorage.getItem(key);
|
| 1366 |
+
if (raw === null) return;
|
| 1367 |
+
const value = Number(raw);
|
| 1368 |
+
if (!Number.isFinite(value)) return;
|
| 1369 |
+
input.value = String(Math.max(min, Math.min(max, value)));
|
| 1370 |
+
}
|
| 1371 |
+
|
| 1372 |
+
function persistControls() {
|
| 1373 |
+
localStorage.setItem(STORAGE_KEYS.model, els.model.value);
|
| 1374 |
+
localStorage.setItem(STORAGE_KEYS.layer, els.layer.value);
|
| 1375 |
+
localStorage.setItem(STORAGE_KEYS.topK, els.topK.value);
|
| 1376 |
+
localStorage.setItem(STORAGE_KEYS.cardWidth, els.cardWidth.value);
|
| 1377 |
+
localStorage.setItem(STORAGE_KEYS.weakRatio, els.weakRatio.value);
|
| 1378 |
+
localStorage.setItem(STORAGE_KEYS.keepModels, els.keepModels.checked ? "1" : "0");
|
| 1379 |
+
}
|
| 1380 |
+
|
| 1381 |
+
function showError(message) {
|
| 1382 |
+
els.message.hidden = false;
|
| 1383 |
+
els.message.className = "error";
|
| 1384 |
+
els.message.textContent = message;
|
| 1385 |
+
}
|
| 1386 |
+
|
| 1387 |
+
function componentColor(componentId) {
|
| 1388 |
+
return `hsl(${(37 * Number(componentId)) % 360} 78% 48%)`;
|
| 1389 |
+
}
|
| 1390 |
+
|
| 1391 |
+
function annotationMetaMap(raw) {
|
| 1392 |
+
const out = new Map();
|
| 1393 |
+
if (!Array.isArray(raw)) return out;
|
| 1394 |
+
raw.forEach(item => {
|
| 1395 |
+
const component = Number(item?.component);
|
| 1396 |
+
if (!Number.isFinite(component)) return;
|
| 1397 |
+
out.set(component, {
|
| 1398 |
+
positive_label: String(item.positive_label || "").trim(),
|
| 1399 |
+
positive_confidence: normalizedConfidence(item.positive_confidence),
|
| 1400 |
+
positive_types: Array.isArray(item.positive_types) ? item.positive_types.map(String) : [],
|
| 1401 |
+
negative_label: String(item.negative_label || "").trim(),
|
| 1402 |
+
negative_confidence: normalizedConfidence(item.negative_confidence),
|
| 1403 |
+
negative_types: Array.isArray(item.negative_types) ? item.negative_types.map(String) : [],
|
| 1404 |
+
excess_kurtosis: Number.isFinite(Number(item.excess_kurtosis)) ? Number(item.excess_kurtosis) : null,
|
| 1405 |
+
effective_context_mean: Number.isFinite(Number(item.effective_context_mean)) ? Number(item.effective_context_mean) : null,
|
| 1406 |
+
});
|
| 1407 |
+
});
|
| 1408 |
+
return out;
|
| 1409 |
+
}
|
| 1410 |
+
|
| 1411 |
+
function componentMetaMap(raw) {
|
| 1412 |
+
const out = new Map();
|
| 1413 |
+
if (!Array.isArray(raw)) return out;
|
| 1414 |
+
raw.forEach(item => {
|
| 1415 |
+
const component = Number(item?.component);
|
| 1416 |
+
if (!Number.isFinite(component)) return;
|
| 1417 |
+
out.set(component, {
|
| 1418 |
+
positive_label: String(item.positive_label || "").trim(),
|
| 1419 |
+
positive_confidence: normalizedConfidence(item.positive_confidence),
|
| 1420 |
+
positive_types: Array.isArray(item.positive_types) ? item.positive_types.map(String) : [],
|
| 1421 |
+
negative_label: String(item.negative_label || "").trim(),
|
| 1422 |
+
negative_confidence: normalizedConfidence(item.negative_confidence),
|
| 1423 |
+
negative_types: Array.isArray(item.negative_types) ? item.negative_types.map(String) : [],
|
| 1424 |
+
excess_kurtosis: Number.isFinite(Number(item.excess_kurtosis)) ? Number(item.excess_kurtosis) : null,
|
| 1425 |
+
effective_context_mean: Number.isFinite(Number(item.effective_context_mean)) ? Number(item.effective_context_mean) : null,
|
| 1426 |
+
});
|
| 1427 |
+
});
|
| 1428 |
+
return out;
|
| 1429 |
+
}
|
| 1430 |
+
|
| 1431 |
+
function annotationForScore(meta, score) {
|
| 1432 |
+
if (!meta) return null;
|
| 1433 |
+
const positive = Number(score || 0) >= 0;
|
| 1434 |
+
const label = positive ? meta.positive_label : meta.negative_label;
|
| 1435 |
+
const confidence = positive ? meta.positive_confidence : meta.negative_confidence;
|
| 1436 |
+
const types = positive ? meta.positive_types : meta.negative_types;
|
| 1437 |
+
if (!visibleAnnotationLabel(label, confidence)) return null;
|
| 1438 |
+
return {
|
| 1439 |
+
label: visibleAnnotationLabel(label, confidence),
|
| 1440 |
+
confidence,
|
| 1441 |
+
type_letter: typeLetter(types),
|
| 1442 |
+
};
|
| 1443 |
+
}
|
| 1444 |
+
|
| 1445 |
+
function annotationHint(meta) {
|
| 1446 |
+
return `${meta.label} (${meta.confidence})`;
|
| 1447 |
+
}
|
| 1448 |
+
|
| 1449 |
+
function visibleAnnotationLabel(value, confidence) {
|
| 1450 |
+
const text = String(value || "").trim();
|
| 1451 |
+
if (!text) return "";
|
| 1452 |
+
if (text === "?" && normalizedConfidence(confidence) === "unclear") return "";
|
| 1453 |
+
return text.replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
| 1454 |
+
}
|
| 1455 |
+
|
| 1456 |
+
function normalizedConfidence(value) {
|
| 1457 |
+
const confidence = String(value || "unclear").toLowerCase();
|
| 1458 |
+
return ["high", "medium", "low", "unclear"].includes(confidence) ? confidence : "unclear";
|
| 1459 |
+
}
|
| 1460 |
+
|
| 1461 |
+
function typeLetter(types) {
|
| 1462 |
+
const labels = Array.isArray(types) ? types : [];
|
| 1463 |
+
const priority = [
|
| 1464 |
+
["Form", "F"],
|
| 1465 |
+
["Word", "W"],
|
| 1466 |
+
["Phrase", "P"],
|
| 1467 |
+
["Sentence", "S"],
|
| 1468 |
+
["Long-Range Context", "L"],
|
| 1469 |
+
["Global", "G"],
|
| 1470 |
+
["Position", "O"],
|
| 1471 |
+
["Sophisticated", "X"],
|
| 1472 |
+
];
|
| 1473 |
+
for (const [name, letter] of priority) {
|
| 1474 |
+
if (labels.some(label => String(label).toLowerCase() === name.toLowerCase())) return letter;
|
| 1475 |
+
}
|
| 1476 |
+
const first = labels.find(label => String(label).trim());
|
| 1477 |
+
return first ? String(first).trim()[0].toUpperCase() : "";
|
| 1478 |
+
}
|
| 1479 |
+
|
| 1480 |
+
function formatScore(value) {
|
| 1481 |
+
const abs = Math.abs(value);
|
| 1482 |
+
if (abs >= 100) return value.toFixed(0);
|
| 1483 |
+
if (abs >= 10) return value.toFixed(1);
|
| 1484 |
+
return value.toFixed(2);
|
| 1485 |
+
}
|
| 1486 |
+
|
| 1487 |
+
function escapeHtml(value) {
|
| 1488 |
+
return String(value).replace(/[&<>"']/g, char => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char]));
|
| 1489 |
+
}
|
| 1490 |
+
|
| 1491 |
+
function escapeAttr(value) {
|
| 1492 |
+
return escapeHtml(value);
|
| 1493 |
+
}
|
| 1494 |
+
|
| 1495 |
+
els.model.addEventListener("change", async () => {
|
| 1496 |
+
persistHighlights();
|
| 1497 |
+
state.highlights.clear();
|
| 1498 |
+
state.tokenStats.clear();
|
| 1499 |
+
state.tokenStatsRetries.clear();
|
| 1500 |
+
state.componentNeighbors.clear();
|
| 1501 |
+
state.componentNeighborsRetries.clear();
|
| 1502 |
+
persistControls();
|
| 1503 |
+
try {
|
| 1504 |
+
await loadModelMeta();
|
| 1505 |
+
persistControls();
|
| 1506 |
+
runProbe();
|
| 1507 |
+
} catch (err) {
|
| 1508 |
+
showError(err.message);
|
| 1509 |
+
}
|
| 1510 |
+
});
|
| 1511 |
+
els.layer.addEventListener("change", () => {
|
| 1512 |
+
persistHighlights();
|
| 1513 |
+
state.highlights.clear();
|
| 1514 |
+
state.componentNeighbors.clear();
|
| 1515 |
+
state.componentNeighborsRetries.clear();
|
| 1516 |
+
persistControls();
|
| 1517 |
+
runProbe();
|
| 1518 |
+
});
|
| 1519 |
+
els.topK.addEventListener("change", () => { persistControls(); runProbe(); });
|
| 1520 |
+
els.topK.addEventListener("input", () => { persistControls(); runProbe(); });
|
| 1521 |
+
els.cardWidth.addEventListener("change", updateCardWidth);
|
| 1522 |
+
els.cardWidth.addEventListener("input", updateCardWidth);
|
| 1523 |
+
els.weakRatio.addEventListener("change", () => { persistControls(); rerenderLastProbe(); });
|
| 1524 |
+
els.keepModels.addEventListener("change", () => { persistControls(); if (!els.keepModels.checked) runProbe(); });
|
| 1525 |
+
els.selectionClose.addEventListener("click", () => {
|
| 1526 |
+
state.selectionPanelHidden = true;
|
| 1527 |
+
renderSelectionPanel();
|
| 1528 |
+
});
|
| 1529 |
+
els.selectionExportToggle.addEventListener("click", () => {
|
| 1530 |
+
state.selectionExportHidden = !state.selectionExportHidden;
|
| 1531 |
+
renderSelectionExport();
|
| 1532 |
+
});
|
| 1533 |
+
els.selectionExportCopy.addEventListener("click", copySelectionExport);
|
| 1534 |
+
els.weakRatio.addEventListener("input", () => { persistControls(); rerenderLastProbe(); });
|
| 1535 |
+
els.text.addEventListener("input", scheduleProbe);
|
| 1536 |
+
els.text.addEventListener("keydown", handleTextKeydown);
|
| 1537 |
+
els.runProbe.addEventListener("click", () => {
|
| 1538 |
+
localStorage.setItem(STORAGE_KEYS.probeText, els.text.value);
|
| 1539 |
+
persistControls();
|
| 1540 |
+
runProbe();
|
| 1541 |
+
});
|
| 1542 |
+
updateCardWidth();
|
| 1543 |
+
init();
|
| 1544 |
+
</script>
|
| 1545 |
+
</body>
|
| 1546 |
+
</html>
|
server/static/random_components.html
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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" />
|
| 6 |
+
<title>Random ICA Components</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root { --bg:#f8fafc; --panel:#fff; --text:#0f172a; --muted:#64748b; --border:#cbd5e1; --accent:#2563eb; --subtle:#f1f5f9; --shadow:0 1px 2px rgb(15 23 42 / .08),0 8px 24px rgb(15 23 42 / .05); }
|
| 9 |
+
* { box-sizing:border-box; }
|
| 10 |
+
body { margin:0; background:var(--bg); color:var(--text); font:14px/1.45 system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }
|
| 11 |
+
header { position:sticky; top:0; z-index:5; background:#fff; border-bottom:1px solid var(--border); padding:12px 16px; box-shadow:var(--shadow); }
|
| 12 |
+
.top { display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap; }
|
| 13 |
+
h1 { margin:0; font-size:20px; }
|
| 14 |
+
nav { display:flex; gap:6px; }
|
| 15 |
+
nav a { color:var(--muted); text-decoration:none; padding:7px 10px; border-radius:7px; font-weight:650; }
|
| 16 |
+
nav a:hover, nav a.active { color:var(--accent); background:#eff6ff; }
|
| 17 |
+
.toolbar { display:flex; align-items:end; flex-wrap:wrap; gap:10px; margin-top:10px; }
|
| 18 |
+
label { color:#475569; font-size:12px; font-weight:700; }
|
| 19 |
+
select,input,button { font:inherit; border:1px solid var(--border); border-radius:7px; padding:7px 9px; background:#fff; color:var(--text); }
|
| 20 |
+
button { cursor:pointer; background:var(--subtle); font-weight:750; }
|
| 21 |
+
button:hover { color:var(--accent); border-color:var(--accent); background:#eff6ff; }
|
| 22 |
+
main { padding:14px; }
|
| 23 |
+
.panel { background:var(--panel); border:1px solid var(--border); border-radius:8px; box-shadow:var(--shadow); padding:12px; }
|
| 24 |
+
.summary { color:var(--muted); margin-bottom:10px; }
|
| 25 |
+
.run { margin-top:14px; }
|
| 26 |
+
.run h2 { display:flex; align-items:baseline; justify-content:space-between; gap:10px; margin:0 0 8px; font-size:16px; }
|
| 27 |
+
.run-meta { color:var(--muted); font-size:12px; font-weight:500; }
|
| 28 |
+
table { width:100%; border-collapse:collapse; background:#fff; }
|
| 29 |
+
th,td { border-bottom:1px solid #e2e8f0; padding:8px 9px; text-align:left; vertical-align:top; }
|
| 30 |
+
th { position:sticky; top:94px; background:#f8fafc; color:#475569; font-size:12px; text-transform:uppercase; letter-spacing:.02em; z-index:1; }
|
| 31 |
+
tr:hover td { background:#f8fafc; }
|
| 32 |
+
a { color:var(--accent); text-decoration:none; font-weight:800; }
|
| 33 |
+
a:hover { text-decoration:underline; }
|
| 34 |
+
.muted { color:var(--muted); }
|
| 35 |
+
.pill { display:inline-flex; border:1px solid var(--border); border-radius:999px; padding:1px 7px; color:var(--muted); background:#fff; font-size:12px; font-weight:750; white-space:nowrap; }
|
| 36 |
+
.label { max-width:360px; color:#334155; }
|
| 37 |
+
.label-badge { display:inline-flex; align-items:center; border-radius:999px; padding:2px 8px; font-size:12px; font-weight:850; line-height:1.3; }
|
| 38 |
+
.label-badge.high { color:#166534; background:#dcfce7; border:1px solid #86efac; }
|
| 39 |
+
.label-badge.medium { color:#854d0e; background:#fef3c7; border:1px solid #fde68a; }
|
| 40 |
+
.label-badge.low { color:#9f1239; background:#ffe4e6; border:1px solid #f9a8d4; }
|
| 41 |
+
.label-badge.unclear { color:#475569; background:#e5e7eb; border:1px solid #cbd5e1; }
|
| 42 |
+
.status { margin-left:auto; color:var(--muted); }
|
| 43 |
+
.empty,.error { border:1px dashed var(--border); border-radius:8px; padding:18px; color:var(--muted); background:#fff; }
|
| 44 |
+
.error { color:#991b1b; border-color:#fecaca; background:#fef2f2; }
|
| 45 |
+
@media(max-width:760px){ th:nth-child(5),td:nth-child(5),th:nth-child(6),td:nth-child(6){display:none;} }
|
| 46 |
+
</style>
|
| 47 |
+
</head>
|
| 48 |
+
<body>
|
| 49 |
+
<header>
|
| 50 |
+
<div class="top">
|
| 51 |
+
<h1>Random ICA Components</h1>
|
| 52 |
+
<nav aria-label="Primary">
|
| 53 |
+
<a href="/">Explorer</a>
|
| 54 |
+
<a href="/sae-explorer">SAE Explorer</a>
|
| 55 |
+
<a href="/stats">Stats</a>
|
| 56 |
+
<a href="/annotate">Annotate</a>
|
| 57 |
+
<a href="/random-components" class="active">Random</a>
|
| 58 |
+
</nav>
|
| 59 |
+
</div>
|
| 60 |
+
<div class="toolbar">
|
| 61 |
+
<label>Model<br><select id="model"><option value="">all models</option></select></label>
|
| 62 |
+
<label style="flex:1;min-width:260px">Selection<br><input id="selection" placeholder="optional: random_components_n50_seed20260524" /></label>
|
| 63 |
+
<button id="load">Load</button>
|
| 64 |
+
<span id="status" class="status"></span>
|
| 65 |
+
</div>
|
| 66 |
+
</header>
|
| 67 |
+
<main>
|
| 68 |
+
<section class="panel">
|
| 69 |
+
<div id="summary" class="summary">Loading random component selections...</div>
|
| 70 |
+
<div id="content"></div>
|
| 71 |
+
</section>
|
| 72 |
+
</main>
|
| 73 |
+
<script>
|
| 74 |
+
const state = { models: [] };
|
| 75 |
+
const el = Object.fromEntries(["model","selection","load","status","summary","content"].map(id=>[id,document.getElementById(id)]));
|
| 76 |
+
async function api(path){ const r=await fetch(path); if(!r.ok){let msg=await r.text(); try{msg=JSON.parse(msg).detail||msg}catch{} throw new Error(msg)} return r.json(); }
|
| 77 |
+
function esc(s){return String(s??"").replace(/[&<>"']/g,c=>({"&":"&","<":"<",">":">","\"":""","'":"'"}[c]));}
|
| 78 |
+
function setStatus(text){ el.status.textContent = text || ""; }
|
| 79 |
+
async function init(){
|
| 80 |
+
const params = new URLSearchParams(location.search);
|
| 81 |
+
el.selection.value = params.get("selection") || "random_components_n50_seed0";
|
| 82 |
+
const requestedModel = params.get("model") || "qwen3_5_2b_base";
|
| 83 |
+
try {
|
| 84 |
+
const models = await api("/api/models");
|
| 85 |
+
state.models = models.models || [];
|
| 86 |
+
el.model.innerHTML = `<option value="">all models</option>` + state.models.map(m=>`<option value="${esc(m.model_name)}">${esc(m.display_name || m.model_name)}</option>`).join("");
|
| 87 |
+
el.model.value = requestedModel;
|
| 88 |
+
} catch {}
|
| 89 |
+
el.load.onclick = () => loadRuns(true);
|
| 90 |
+
el.model.onchange = () => loadRuns(true);
|
| 91 |
+
el.selection.onkeydown = event => { if(event.key === "Enter") loadRuns(true); };
|
| 92 |
+
await loadRuns(false);
|
| 93 |
+
}
|
| 94 |
+
async function loadRuns(push){
|
| 95 |
+
setStatus("Loading...");
|
| 96 |
+
el.content.innerHTML = "";
|
| 97 |
+
try {
|
| 98 |
+
const params = new URLSearchParams();
|
| 99 |
+
if(el.model.value) params.set("model", el.model.value);
|
| 100 |
+
if(el.selection.value.trim()) params.set("selection", el.selection.value.trim());
|
| 101 |
+
if(push) history.replaceState(null, "", `/random-components${params.toString() ? `?${params}` : ""}`);
|
| 102 |
+
const data = await api(`/api/random-components${params.toString() ? `?${params}` : ""}`);
|
| 103 |
+
render(data);
|
| 104 |
+
setStatus("");
|
| 105 |
+
} catch (error) {
|
| 106 |
+
el.summary.textContent = "";
|
| 107 |
+
el.content.innerHTML = `<div class="error">${esc(error.message)}</div>`;
|
| 108 |
+
setStatus("");
|
| 109 |
+
}
|
| 110 |
+
}
|
| 111 |
+
function render(data){
|
| 112 |
+
const runs = data.runs || [];
|
| 113 |
+
const total = runs.reduce((sum, run) => sum + Number(run.selected_size || 0), 0);
|
| 114 |
+
el.summary.textContent = `${runs.length} selection file${runs.length===1?"":"s"} - ${total} selected components`;
|
| 115 |
+
el.content.innerHTML = runs.map(renderRun).join("") || '<div class="empty">No random component selections found.</div>';
|
| 116 |
+
}
|
| 117 |
+
function renderRun(run){
|
| 118 |
+
const settings = run.settings || {};
|
| 119 |
+
const rows = run.selected_components || [];
|
| 120 |
+
return `<section class="run">
|
| 121 |
+
<h2>
|
| 122 |
+
<span>${esc(run.model)}</span>
|
| 123 |
+
<span class="run-meta">n=${esc(settings.n)} seed=${esc(settings.seed)} inventory=${esc(run.inventory_size)} selection=${esc(run.selection_name || "")}</span>
|
| 124 |
+
</h2>
|
| 125 |
+
<table>
|
| 126 |
+
<thead><tr><th>#</th><th>Component</th><th>Annotate</th><th>K</th><th>Current label</th></tr></thead>
|
| 127 |
+
<tbody>${rows.map((item,index)=>renderRow(item,index)).join("")}</tbody>
|
| 128 |
+
</table>
|
| 129 |
+
</section>`;
|
| 130 |
+
}
|
| 131 |
+
function renderRow(item,index){
|
| 132 |
+
const annotation = item.annotation || {};
|
| 133 |
+
const sign = Number(item.top_abs_sign) < 0 ? -1 : 1;
|
| 134 |
+
const rawLabel = sign < 0 ? annotation.negative_label : annotation.positive_label;
|
| 135 |
+
const confidence = normalizeConfidence(sign < 0 ? annotation.negative_confidence : annotation.positive_confidence);
|
| 136 |
+
const label = visibleLabel(rawLabel);
|
| 137 |
+
const labels = label
|
| 138 |
+
? `<span class="label-badge ${esc(confidence)}">${label}</span>`
|
| 139 |
+
: '<span class="label-badge unclear">unlabeled</span>';
|
| 140 |
+
const k = Number(item.excess_kurtosis);
|
| 141 |
+
const erf = Number(item.effective_context_mean);
|
| 142 |
+
const metrics = `${Number.isFinite(erf) ? `<span class="pill">ERF=${erf.toFixed(1)}</span>` : ""} ${Number.isFinite(k) ? `<span class="pill">K=${k.toFixed(1)}</span>` : ""}`;
|
| 143 |
+
return `<tr>
|
| 144 |
+
<td class="muted">${index + 1}</td>
|
| 145 |
+
<td><strong>${esc(item.layer)} C${esc(item.component_index)}</strong><br><span class="muted">${esc(item.component_id || "")}</span></td>
|
| 146 |
+
<td><a href="${esc(item.annotate_url)}">Annotate</a></td>
|
| 147 |
+
<td>${metrics || '<span class="muted">?</span>'}</td>
|
| 148 |
+
<td class="label">${labels}</td>
|
| 149 |
+
</tr>`;
|
| 150 |
+
}
|
| 151 |
+
function visibleLabel(value){
|
| 152 |
+
const text = String(value || "").trim();
|
| 153 |
+
if(!text) return "";
|
| 154 |
+
return esc(text);
|
| 155 |
+
}
|
| 156 |
+
function normalizeConfidence(value){
|
| 157 |
+
const text = String(value || "unclear").trim().toLowerCase();
|
| 158 |
+
return ["high","medium","low","unclear"].includes(text) ? text : "unclear";
|
| 159 |
+
}
|
| 160 |
+
init();
|
| 161 |
+
</script>
|
| 162 |
+
</body>
|
| 163 |
+
</html>
|
server/static/sae_explorer.html
ADDED
|
@@ -0,0 +1,673 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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" />
|
| 6 |
+
<title>SAE Explorer</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root {
|
| 9 |
+
--bg: #f6f7f9;
|
| 10 |
+
--panel: #fff;
|
| 11 |
+
--text: #151922;
|
| 12 |
+
--muted: #647084;
|
| 13 |
+
--border: #cbd3df;
|
| 14 |
+
--accent: #1f6feb;
|
| 15 |
+
--shadow: 0 1px 2px rgb(20 25 34 / .08), 0 10px 30px rgb(20 25 34 / .06);
|
| 16 |
+
--token-card-width: 140px;
|
| 17 |
+
}
|
| 18 |
+
* { box-sizing: border-box; }
|
| 19 |
+
body {
|
| 20 |
+
margin: 0;
|
| 21 |
+
background: var(--bg);
|
| 22 |
+
color: var(--text);
|
| 23 |
+
font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
| 24 |
+
}
|
| 25 |
+
header {
|
| 26 |
+
position: sticky;
|
| 27 |
+
top: 0;
|
| 28 |
+
z-index: 5;
|
| 29 |
+
background: var(--panel);
|
| 30 |
+
border-bottom: 1px solid var(--border);
|
| 31 |
+
box-shadow: var(--shadow);
|
| 32 |
+
padding: 12px 18px;
|
| 33 |
+
}
|
| 34 |
+
.top {
|
| 35 |
+
display: flex;
|
| 36 |
+
align-items: center;
|
| 37 |
+
justify-content: space-between;
|
| 38 |
+
gap: 14px;
|
| 39 |
+
}
|
| 40 |
+
h1 { margin: 0; font-size: 19px; letter-spacing: 0; }
|
| 41 |
+
.nav {
|
| 42 |
+
display: flex;
|
| 43 |
+
align-items: center;
|
| 44 |
+
gap: 14px;
|
| 45 |
+
}
|
| 46 |
+
.nav a {
|
| 47 |
+
color: var(--accent);
|
| 48 |
+
font-weight: 750;
|
| 49 |
+
text-decoration: none;
|
| 50 |
+
}
|
| 51 |
+
.nav a.active { color: #0f172a; }
|
| 52 |
+
.nav a:hover { text-decoration: underline; }
|
| 53 |
+
main {
|
| 54 |
+
max-width: 1240px;
|
| 55 |
+
margin: 0 auto;
|
| 56 |
+
padding: 18px;
|
| 57 |
+
}
|
| 58 |
+
.panel {
|
| 59 |
+
margin-top: 14px;
|
| 60 |
+
background: var(--panel);
|
| 61 |
+
border: 1px solid var(--border);
|
| 62 |
+
border-radius: 8px;
|
| 63 |
+
box-shadow: var(--shadow);
|
| 64 |
+
padding: 12px;
|
| 65 |
+
}
|
| 66 |
+
.controls {
|
| 67 |
+
display: grid;
|
| 68 |
+
grid-template-columns: minmax(0, 1fr) 450px;
|
| 69 |
+
gap: 12px;
|
| 70 |
+
align-items: stretch;
|
| 71 |
+
}
|
| 72 |
+
.control-stack {
|
| 73 |
+
display: flex;
|
| 74 |
+
flex-wrap: wrap;
|
| 75 |
+
align-content: flex-start;
|
| 76 |
+
align-items: center;
|
| 77 |
+
gap: 10px;
|
| 78 |
+
}
|
| 79 |
+
.inline-control {
|
| 80 |
+
display: grid;
|
| 81 |
+
grid-template-columns: max-content max-content;
|
| 82 |
+
align-items: center;
|
| 83 |
+
column-gap: 8px;
|
| 84 |
+
}
|
| 85 |
+
.inline-control > span { white-space: nowrap; }
|
| 86 |
+
.control-break { flex-basis: 100%; height: 0; }
|
| 87 |
+
label {
|
| 88 |
+
display: grid;
|
| 89 |
+
gap: 5px;
|
| 90 |
+
color: #435066;
|
| 91 |
+
font-size: 12px;
|
| 92 |
+
font-weight: 700;
|
| 93 |
+
}
|
| 94 |
+
textarea, select, input, button {
|
| 95 |
+
width: 100%;
|
| 96 |
+
border: 1px solid var(--border);
|
| 97 |
+
border-radius: 7px;
|
| 98 |
+
background: #fff;
|
| 99 |
+
color: var(--text);
|
| 100 |
+
font: inherit;
|
| 101 |
+
padding: 8px 10px;
|
| 102 |
+
}
|
| 103 |
+
textarea {
|
| 104 |
+
height: 92px;
|
| 105 |
+
min-height: 92px;
|
| 106 |
+
resize: vertical;
|
| 107 |
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
| 108 |
+
}
|
| 109 |
+
#topK { width: 58px; min-width: 0; }
|
| 110 |
+
#cardWidth { width: 78px; min-width: 0; }
|
| 111 |
+
#weakRatio { width: 58px; min-width: 0; }
|
| 112 |
+
button {
|
| 113 |
+
cursor: pointer;
|
| 114 |
+
min-height: 39px;
|
| 115 |
+
background: #eef2f7;
|
| 116 |
+
font-weight: 750;
|
| 117 |
+
}
|
| 118 |
+
.run-button {
|
| 119 |
+
width: auto;
|
| 120 |
+
min-height: 32px;
|
| 121 |
+
padding: 6px 12px;
|
| 122 |
+
border-color: #1458c8;
|
| 123 |
+
background: var(--accent);
|
| 124 |
+
color: #fff;
|
| 125 |
+
font-weight: 850;
|
| 126 |
+
}
|
| 127 |
+
.memory-control {
|
| 128 |
+
position: fixed;
|
| 129 |
+
right: 12px;
|
| 130 |
+
bottom: 10px;
|
| 131 |
+
z-index: 4;
|
| 132 |
+
display: inline-flex;
|
| 133 |
+
align-items: center;
|
| 134 |
+
gap: 5px;
|
| 135 |
+
width: auto;
|
| 136 |
+
min-height: 24px;
|
| 137 |
+
padding: 3px 6px;
|
| 138 |
+
border: 1px solid #d7dee9;
|
| 139 |
+
border-radius: 6px;
|
| 140 |
+
background: rgb(255 255 255 / .78);
|
| 141 |
+
color: #94a3b8;
|
| 142 |
+
font-size: 11px;
|
| 143 |
+
font-weight: 650;
|
| 144 |
+
white-space: nowrap;
|
| 145 |
+
}
|
| 146 |
+
.memory-control input { width: auto; margin: 0; padding: 0; accent-color: #94a3b8; }
|
| 147 |
+
.results {
|
| 148 |
+
display: grid;
|
| 149 |
+
grid-template-columns: repeat(auto-fill, minmax(var(--token-card-width), 1fr));
|
| 150 |
+
gap: 10px;
|
| 151 |
+
margin-top: 12px;
|
| 152 |
+
}
|
| 153 |
+
.token-card {
|
| 154 |
+
border: 1px solid var(--border);
|
| 155 |
+
border-radius: 8px;
|
| 156 |
+
padding: 6px;
|
| 157 |
+
background: #fff;
|
| 158 |
+
min-width: 0;
|
| 159 |
+
text-align: center;
|
| 160 |
+
}
|
| 161 |
+
.token-text {
|
| 162 |
+
min-height: 25px;
|
| 163 |
+
font-weight: 850;
|
| 164 |
+
text-align: center;
|
| 165 |
+
overflow-wrap: anywhere;
|
| 166 |
+
margin-bottom: 6px;
|
| 167 |
+
}
|
| 168 |
+
.score-row {
|
| 169 |
+
position: relative;
|
| 170 |
+
min-height: 24px;
|
| 171 |
+
margin-top: 3px;
|
| 172 |
+
}
|
| 173 |
+
.badge {
|
| 174 |
+
position: relative;
|
| 175 |
+
display: flex;
|
| 176 |
+
align-items: center;
|
| 177 |
+
width: 100%;
|
| 178 |
+
min-height: 24px;
|
| 179 |
+
padding: 3px 5px;
|
| 180 |
+
border: 1px solid #d5dce7;
|
| 181 |
+
border-radius: 5px;
|
| 182 |
+
background:
|
| 183 |
+
linear-gradient(
|
| 184 |
+
90deg,
|
| 185 |
+
var(--score-bg, #edf2f7) 0,
|
| 186 |
+
var(--score-bg, #edf2f7) var(--score-width, 100%),
|
| 187 |
+
transparent var(--score-width, 100%),
|
| 188 |
+
transparent 100%
|
| 189 |
+
);
|
| 190 |
+
color: var(--text);
|
| 191 |
+
font-size: 10px;
|
| 192 |
+
font-weight: 700;
|
| 193 |
+
text-align: left;
|
| 194 |
+
cursor: pointer;
|
| 195 |
+
}
|
| 196 |
+
.badge:hover {
|
| 197 |
+
border-color: var(--feature-color, var(--accent));
|
| 198 |
+
}
|
| 199 |
+
.badge.hot {
|
| 200 |
+
opacity: 1;
|
| 201 |
+
border-color: var(--feature-color, var(--accent));
|
| 202 |
+
--score-bg: color-mix(in srgb, var(--feature-color, var(--accent)) 18%, white);
|
| 203 |
+
box-shadow: inset 3px 0 0 var(--feature-color, var(--accent));
|
| 204 |
+
}
|
| 205 |
+
.badge.weak { opacity: .16; }
|
| 206 |
+
.badge.weak.hot,
|
| 207 |
+
.badge.weak:hover { opacity: 1; }
|
| 208 |
+
.badge-main {
|
| 209 |
+
min-width: 0;
|
| 210 |
+
max-width: calc(100% - 35px);
|
| 211 |
+
overflow: hidden;
|
| 212 |
+
white-space: nowrap;
|
| 213 |
+
text-overflow: ellipsis;
|
| 214 |
+
}
|
| 215 |
+
.score {
|
| 216 |
+
position: absolute;
|
| 217 |
+
right: 5px;
|
| 218 |
+
top: 50%;
|
| 219 |
+
transform: translateY(-50%);
|
| 220 |
+
width: 34px;
|
| 221 |
+
text-align: right;
|
| 222 |
+
color: var(--muted);
|
| 223 |
+
font-size: 9px;
|
| 224 |
+
font-variant-numeric: tabular-nums;
|
| 225 |
+
pointer-events: none;
|
| 226 |
+
}
|
| 227 |
+
.empty, .error {
|
| 228 |
+
margin-top: 12px;
|
| 229 |
+
border: 1px dashed var(--border);
|
| 230 |
+
border-radius: 8px;
|
| 231 |
+
padding: 18px;
|
| 232 |
+
color: var(--muted);
|
| 233 |
+
background: #fff;
|
| 234 |
+
}
|
| 235 |
+
.error { color: #a41414; border-color: #f0b9b9; background: #fff7f7; }
|
| 236 |
+
.request-indicator {
|
| 237 |
+
position: fixed;
|
| 238 |
+
left: 14px;
|
| 239 |
+
top: 62px;
|
| 240 |
+
z-index: 8;
|
| 241 |
+
display: none;
|
| 242 |
+
align-items: center;
|
| 243 |
+
gap: 8px;
|
| 244 |
+
min-height: 34px;
|
| 245 |
+
padding: 8px 10px;
|
| 246 |
+
border: 1px solid var(--border);
|
| 247 |
+
border-radius: 8px;
|
| 248 |
+
background: var(--panel);
|
| 249 |
+
box-shadow: var(--shadow);
|
| 250 |
+
color: #435066;
|
| 251 |
+
font-size: 12px;
|
| 252 |
+
font-weight: 800;
|
| 253 |
+
}
|
| 254 |
+
.request-indicator.visible { display: inline-flex; }
|
| 255 |
+
.request-spinner {
|
| 256 |
+
width: 14px;
|
| 257 |
+
height: 14px;
|
| 258 |
+
border: 2px solid #cbd5e1;
|
| 259 |
+
border-top-color: var(--accent);
|
| 260 |
+
border-radius: 50%;
|
| 261 |
+
animation: request-spin .8s linear infinite;
|
| 262 |
+
}
|
| 263 |
+
@keyframes request-spin { to { transform: rotate(360deg); } }
|
| 264 |
+
.meta-line {
|
| 265 |
+
margin-top: 10px;
|
| 266 |
+
color: var(--muted);
|
| 267 |
+
font-size: 12px;
|
| 268 |
+
font-weight: 650;
|
| 269 |
+
}
|
| 270 |
+
@media (max-width: 760px) {
|
| 271 |
+
main { padding: 12px; }
|
| 272 |
+
.controls { grid-template-columns: 1fr; }
|
| 273 |
+
.top { align-items: flex-start; flex-direction: column; }
|
| 274 |
+
}
|
| 275 |
+
</style>
|
| 276 |
+
</head>
|
| 277 |
+
<body>
|
| 278 |
+
<header>
|
| 279 |
+
<div class="top">
|
| 280 |
+
<h1>SAE Explorer</h1>
|
| 281 |
+
<nav class="nav" aria-label="Primary">
|
| 282 |
+
<a href="/">Explorer</a>
|
| 283 |
+
<a href="/sae-explorer" class="active">SAE Explorer</a>
|
| 284 |
+
<a href="/stats">Stats</a>
|
| 285 |
+
<a href="/annotate">Annotate</a>
|
| 286 |
+
<a href="/random-components">Random</a>
|
| 287 |
+
</nav>
|
| 288 |
+
</div>
|
| 289 |
+
</header>
|
| 290 |
+
|
| 291 |
+
<main>
|
| 292 |
+
<div class="panel">
|
| 293 |
+
<div class="controls">
|
| 294 |
+
<label>
|
| 295 |
+
Text
|
| 296 |
+
<textarea id="probeText" spellcheck="false">Maya stopped at the bank before the trip, waiting in line to deposit a check and withdraw enough cash for the weekend.</textarea>
|
| 297 |
+
</label>
|
| 298 |
+
<div class="control-stack">
|
| 299 |
+
<label class="inline-control">
|
| 300 |
+
<span>Model</span>
|
| 301 |
+
<select id="modelSelect"></select>
|
| 302 |
+
</label>
|
| 303 |
+
<label class="inline-control">
|
| 304 |
+
<span>Layer</span>
|
| 305 |
+
<select id="layerSelect"></select>
|
| 306 |
+
</label>
|
| 307 |
+
<span class="control-break" aria-hidden="true"></span>
|
| 308 |
+
<label class="inline-control">
|
| 309 |
+
<span>Top K</span>
|
| 310 |
+
<input id="topK" type="number" min="1" max="128" value="5" />
|
| 311 |
+
</label>
|
| 312 |
+
<label class="inline-control">
|
| 313 |
+
<span>Card Width</span>
|
| 314 |
+
<input id="cardWidth" type="number" min="100" max="360" step="20" value="140" />
|
| 315 |
+
</label>
|
| 316 |
+
<label class="inline-control">
|
| 317 |
+
<span>Opacity Cutoff</span>
|
| 318 |
+
<input id="weakRatio" type="number" min="0" max="1" step="0.05" value="0.5" />
|
| 319 |
+
</label>
|
| 320 |
+
<button id="runProbe" class="run-button" type="button">Run</button>
|
| 321 |
+
</div>
|
| 322 |
+
<label class="memory-control" title="Keep loaded models in VRAM when switching.">
|
| 323 |
+
<input id="keepModels" type="checkbox" checked />
|
| 324 |
+
<span>Cache LLMs in VRAM</span>
|
| 325 |
+
</label>
|
| 326 |
+
</div>
|
| 327 |
+
<div id="metaLine" class="meta-line"></div>
|
| 328 |
+
</div>
|
| 329 |
+
|
| 330 |
+
<div id="message" class="empty">Choose a layer and run the SAE probe.</div>
|
| 331 |
+
<div id="results" class="results"></div>
|
| 332 |
+
</main>
|
| 333 |
+
<div id="requestIndicator" class="request-indicator" role="status" aria-live="polite">
|
| 334 |
+
<span class="request-spinner" aria-hidden="true"></span>
|
| 335 |
+
<span>Waiting for server...</span>
|
| 336 |
+
</div>
|
| 337 |
+
|
| 338 |
+
<script>
|
| 339 |
+
const STORAGE_KEYS = {
|
| 340 |
+
probeText: "saeExplorer.probeText",
|
| 341 |
+
model: "saeExplorer.model",
|
| 342 |
+
layer: "saeExplorer.layer",
|
| 343 |
+
topK: "saeExplorer.topK",
|
| 344 |
+
cardWidth: "saeExplorer.cardWidth",
|
| 345 |
+
weakRatio: "saeExplorer.opacityCutoff",
|
| 346 |
+
keepModels: "saeExplorer.keepModels",
|
| 347 |
+
highlights: "saeExplorer.selectedFeatures",
|
| 348 |
+
};
|
| 349 |
+
|
| 350 |
+
const state = {
|
| 351 |
+
models: [],
|
| 352 |
+
meta: null,
|
| 353 |
+
requestId: 0,
|
| 354 |
+
pendingRequests: 0,
|
| 355 |
+
lastProbeOutput: null,
|
| 356 |
+
selectedFeatures: new Set(),
|
| 357 |
+
};
|
| 358 |
+
|
| 359 |
+
const els = {
|
| 360 |
+
text: document.getElementById("probeText"),
|
| 361 |
+
model: document.getElementById("modelSelect"),
|
| 362 |
+
layer: document.getElementById("layerSelect"),
|
| 363 |
+
topK: document.getElementById("topK"),
|
| 364 |
+
cardWidth: document.getElementById("cardWidth"),
|
| 365 |
+
weakRatio: document.getElementById("weakRatio"),
|
| 366 |
+
runProbe: document.getElementById("runProbe"),
|
| 367 |
+
keepModels: document.getElementById("keepModels"),
|
| 368 |
+
metaLine: document.getElementById("metaLine"),
|
| 369 |
+
message: document.getElementById("message"),
|
| 370 |
+
results: document.getElementById("results"),
|
| 371 |
+
requestIndicator: document.getElementById("requestIndicator"),
|
| 372 |
+
};
|
| 373 |
+
|
| 374 |
+
async function api(path, options = {}) {
|
| 375 |
+
beginRequest();
|
| 376 |
+
try {
|
| 377 |
+
const res = await fetch(path, {
|
| 378 |
+
headers: { "content-type": "application/json" },
|
| 379 |
+
...options,
|
| 380 |
+
});
|
| 381 |
+
if (!res.ok) {
|
| 382 |
+
let detail = res.statusText;
|
| 383 |
+
try { detail = (await res.json()).detail || detail; } catch {}
|
| 384 |
+
throw new Error(detail);
|
| 385 |
+
}
|
| 386 |
+
return res.json();
|
| 387 |
+
} finally {
|
| 388 |
+
endRequest();
|
| 389 |
+
}
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
function beginRequest() {
|
| 393 |
+
state.pendingRequests += 1;
|
| 394 |
+
els.requestIndicator.classList.toggle("visible", state.pendingRequests > 0);
|
| 395 |
+
}
|
| 396 |
+
|
| 397 |
+
function endRequest() {
|
| 398 |
+
state.pendingRequests = Math.max(0, state.pendingRequests - 1);
|
| 399 |
+
els.requestIndicator.classList.toggle("visible", state.pendingRequests > 0);
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
async function init() {
|
| 403 |
+
restoreControlValues();
|
| 404 |
+
const savedText = localStorage.getItem(STORAGE_KEYS.probeText);
|
| 405 |
+
if (savedText !== null) els.text.value = savedText;
|
| 406 |
+
try {
|
| 407 |
+
const modelsOut = await api("/api/models");
|
| 408 |
+
state.models = modelsOut.models || [];
|
| 409 |
+
els.model.innerHTML = state.models.map(model => `<option value="${escapeAttr(model.model_name)}">${escapeHtml(model.display_name || model.model_name)}</option>`).join("");
|
| 410 |
+
const savedModel = localStorage.getItem(STORAGE_KEYS.model);
|
| 411 |
+
if (savedModel && state.models.some(model => model.model_name === savedModel)) els.model.value = savedModel;
|
| 412 |
+
else if (state.models.some(model => model.model_name === "gpt2")) els.model.value = "gpt2";
|
| 413 |
+
await loadSaeMeta({ restoreLayer: true });
|
| 414 |
+
runProbe();
|
| 415 |
+
} catch (err) {
|
| 416 |
+
showError(err.message);
|
| 417 |
+
}
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
async function loadSaeMeta(options = {}) {
|
| 421 |
+
state.meta = await api(`/api/sae-meta?model=${encodeURIComponent(els.model.value)}`);
|
| 422 |
+
els.layer.innerHTML = state.meta.layers.map(layer => `<option value="${escapeAttr(layer)}">${escapeHtml(layer)}</option>`).join("");
|
| 423 |
+
const savedLayer = localStorage.getItem(STORAGE_KEYS.layer);
|
| 424 |
+
if (options.restoreLayer && savedLayer && state.meta.layers.includes(savedLayer)) els.layer.value = savedLayer;
|
| 425 |
+
else if (state.meta.layers.includes("layer_05")) els.layer.value = "layer_05";
|
| 426 |
+
restoreFeatureHighlightsForContext();
|
| 427 |
+
renderMetaLine();
|
| 428 |
+
}
|
| 429 |
+
|
| 430 |
+
function renderMetaLine() {
|
| 431 |
+
const sae = state.meta?.sae || {};
|
| 432 |
+
const parts = [
|
| 433 |
+
sae.repo_id ? `SAE: ${sae.repo_id}` : "",
|
| 434 |
+
sae.width ? `width=${sae.width}` : "",
|
| 435 |
+
sae.top_k ? `SAE top-k=${sae.top_k}` : "",
|
| 436 |
+
sae.activation ? `activation=${sae.activation}` : "",
|
| 437 |
+
].filter(Boolean);
|
| 438 |
+
els.metaLine.textContent = parts.join(" · ");
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
async function runProbe() {
|
| 442 |
+
const requestId = ++state.requestId;
|
| 443 |
+
if (!els.text.value.trim()) {
|
| 444 |
+
els.results.innerHTML = "";
|
| 445 |
+
els.message.hidden = false;
|
| 446 |
+
els.message.className = "empty";
|
| 447 |
+
els.message.textContent = "Enter text to run the SAE probe.";
|
| 448 |
+
return;
|
| 449 |
+
}
|
| 450 |
+
if (!els.results.children.length) {
|
| 451 |
+
els.message.hidden = false;
|
| 452 |
+
els.message.className = "empty";
|
| 453 |
+
els.message.textContent = "Running SAE probe...";
|
| 454 |
+
}
|
| 455 |
+
try {
|
| 456 |
+
const out = await api("/api/sae-probe", {
|
| 457 |
+
method: "POST",
|
| 458 |
+
body: JSON.stringify({
|
| 459 |
+
text: els.text.value,
|
| 460 |
+
model_name: els.model.value,
|
| 461 |
+
layer: els.layer.value,
|
| 462 |
+
top_k: Number(els.topK.value || 5),
|
| 463 |
+
keep_models: els.keepModels.checked,
|
| 464 |
+
}),
|
| 465 |
+
});
|
| 466 |
+
if (requestId !== state.requestId) return;
|
| 467 |
+
renderResults(out);
|
| 468 |
+
} catch (err) {
|
| 469 |
+
if (requestId !== state.requestId) return;
|
| 470 |
+
showError(err.message);
|
| 471 |
+
}
|
| 472 |
+
}
|
| 473 |
+
|
| 474 |
+
function renderResults(out) {
|
| 475 |
+
state.lastProbeOutput = out;
|
| 476 |
+
if (out.truncated) {
|
| 477 |
+
els.message.hidden = false;
|
| 478 |
+
els.message.className = "empty";
|
| 479 |
+
els.message.textContent = `Input was truncated to ${out.max_length} tokens.`;
|
| 480 |
+
} else {
|
| 481 |
+
els.message.hidden = true;
|
| 482 |
+
}
|
| 483 |
+
els.results.innerHTML = (out.tokens || []).map(token => `
|
| 484 |
+
<div class="token-card">
|
| 485 |
+
<div class="token-text" title="${escapeAttr(token.token)}">${escapeHtml(token.token_text || token.token)}</div>
|
| 486 |
+
${(token.top || []).map(feature => featureBadge(feature, tokenTopActivation(token))).join("")}
|
| 487 |
+
</div>
|
| 488 |
+
`).join("");
|
| 489 |
+
els.results.querySelectorAll(".badge[data-feature]").forEach(node => {
|
| 490 |
+
node.addEventListener("click", event => selectFeature(event, Number(node.dataset.feature)));
|
| 491 |
+
});
|
| 492 |
+
paintFeatureHighlights();
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
function featureBadge(feature, tokenTop) {
|
| 496 |
+
const id = Number(feature.feature);
|
| 497 |
+
const activation = Number(feature.activation || 0);
|
| 498 |
+
const ratio = tokenTop > 0 ? activation / tokenTop : 0;
|
| 499 |
+
const width = 100 * Math.max(0, Math.min(1, ratio));
|
| 500 |
+
const cutoff = Math.max(0, Math.min(1, Number(els.weakRatio.value || 0.5)));
|
| 501 |
+
const weak = Number.isFinite(tokenTop) && tokenTop > 0 && activation < tokenTop * cutoff;
|
| 502 |
+
const active = state.selectedFeatures.has(id);
|
| 503 |
+
return `
|
| 504 |
+
<div class="score-row">
|
| 505 |
+
<button class="badge ${active ? "hot" : ""} ${weak ? "weak" : ""}" type="button" data-feature="${id}" aria-pressed="${active ? "true" : "false"}" title="${escapeAttr(`F${id}: activation ${formatScore(activation)}, preactivation ${formatScore(Number(feature.preactivation || 0))}`)}" style="--score-width:${width.toFixed(1)}%;--score-bg:${featureColor(id)}22;--feature-color:${featureColor(id)}">
|
| 506 |
+
<span class="badge-main">F${id}</span>
|
| 507 |
+
</button>
|
| 508 |
+
<span class="score">${formatScore(activation)}</span>
|
| 509 |
+
</div>
|
| 510 |
+
`;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
function tokenTopActivation(token) {
|
| 514 |
+
const values = (token.top || []).map(item => Number(item.activation || 0));
|
| 515 |
+
return values.length ? Math.max(...values) : 0;
|
| 516 |
+
}
|
| 517 |
+
|
| 518 |
+
function scheduleProbe() {
|
| 519 |
+
localStorage.setItem(STORAGE_KEYS.probeText, els.text.value);
|
| 520 |
+
persistControls();
|
| 521 |
+
}
|
| 522 |
+
|
| 523 |
+
function handleTextKeydown(event) {
|
| 524 |
+
if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
|
| 525 |
+
event.preventDefault();
|
| 526 |
+
localStorage.setItem(STORAGE_KEYS.probeText, els.text.value);
|
| 527 |
+
persistControls();
|
| 528 |
+
runProbe();
|
| 529 |
+
}
|
| 530 |
+
}
|
| 531 |
+
|
| 532 |
+
function updateCardWidth() {
|
| 533 |
+
const value = Math.max(100, Math.min(360, Number(els.cardWidth.value || 140)));
|
| 534 |
+
document.documentElement.style.setProperty("--token-card-width", `${value}px`);
|
| 535 |
+
localStorage.setItem(STORAGE_KEYS.cardWidth, String(value));
|
| 536 |
+
}
|
| 537 |
+
|
| 538 |
+
function rerenderLastProbe() {
|
| 539 |
+
if (state.lastProbeOutput) renderResults(state.lastProbeOutput);
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
function selectFeature(event, feature) {
|
| 543 |
+
const id = Number(feature);
|
| 544 |
+
if (!Number.isFinite(id)) return;
|
| 545 |
+
if (event.ctrlKey || event.metaKey) {
|
| 546 |
+
if (state.selectedFeatures.has(id)) state.selectedFeatures.delete(id);
|
| 547 |
+
else state.selectedFeatures.add(id);
|
| 548 |
+
} else if (state.selectedFeatures.size === 1 && state.selectedFeatures.has(id)) {
|
| 549 |
+
state.selectedFeatures.clear();
|
| 550 |
+
} else {
|
| 551 |
+
state.selectedFeatures.clear();
|
| 552 |
+
state.selectedFeatures.add(id);
|
| 553 |
+
}
|
| 554 |
+
persistFeatureHighlights();
|
| 555 |
+
paintFeatureHighlights();
|
| 556 |
+
}
|
| 557 |
+
|
| 558 |
+
function paintFeatureHighlights() {
|
| 559 |
+
els.results.querySelectorAll(".badge[data-feature]").forEach(node => {
|
| 560 |
+
const active = state.selectedFeatures.has(Number(node.dataset.feature));
|
| 561 |
+
node.classList.toggle("hot", active);
|
| 562 |
+
node.setAttribute("aria-pressed", active ? "true" : "false");
|
| 563 |
+
});
|
| 564 |
+
}
|
| 565 |
+
|
| 566 |
+
function featureHighlightStorageKey(model = els.model.value, layer = els.layer.value) {
|
| 567 |
+
return `${model}:${layer}`;
|
| 568 |
+
}
|
| 569 |
+
|
| 570 |
+
function readFeatureHighlightStore() {
|
| 571 |
+
try {
|
| 572 |
+
const parsed = JSON.parse(localStorage.getItem(STORAGE_KEYS.highlights) || "{}");
|
| 573 |
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
| 574 |
+
} catch {
|
| 575 |
+
return {};
|
| 576 |
+
}
|
| 577 |
+
}
|
| 578 |
+
|
| 579 |
+
function persistFeatureHighlights() {
|
| 580 |
+
const store = readFeatureHighlightStore();
|
| 581 |
+
const key = featureHighlightStorageKey();
|
| 582 |
+
const values = [...state.selectedFeatures].filter(Number.isFinite).sort((a, b) => a - b);
|
| 583 |
+
if (values.length) store[key] = values;
|
| 584 |
+
else delete store[key];
|
| 585 |
+
localStorage.setItem(STORAGE_KEYS.highlights, JSON.stringify(store));
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
function restoreFeatureHighlightsForContext() {
|
| 589 |
+
const store = readFeatureHighlightStore();
|
| 590 |
+
const saved = Array.isArray(store[featureHighlightStorageKey()]) ? store[featureHighlightStorageKey()] : [];
|
| 591 |
+
state.selectedFeatures = new Set(saved.map(Number).filter(Number.isFinite));
|
| 592 |
+
}
|
| 593 |
+
|
| 594 |
+
function restoreControlValues() {
|
| 595 |
+
setNumberInputFromStorage(els.topK, STORAGE_KEYS.topK, 1, 128);
|
| 596 |
+
setNumberInputFromStorage(els.cardWidth, STORAGE_KEYS.cardWidth, 100, 360);
|
| 597 |
+
setNumberInputFromStorage(els.weakRatio, STORAGE_KEYS.weakRatio, 0, 1);
|
| 598 |
+
els.keepModels.checked = localStorage.getItem(STORAGE_KEYS.keepModels) !== "0";
|
| 599 |
+
}
|
| 600 |
+
|
| 601 |
+
function setNumberInputFromStorage(input, key, min, max) {
|
| 602 |
+
const raw = localStorage.getItem(key);
|
| 603 |
+
if (raw === null) return;
|
| 604 |
+
const value = Number(raw);
|
| 605 |
+
if (!Number.isFinite(value)) return;
|
| 606 |
+
input.value = String(Math.max(min, Math.min(max, value)));
|
| 607 |
+
}
|
| 608 |
+
|
| 609 |
+
function persistControls() {
|
| 610 |
+
localStorage.setItem(STORAGE_KEYS.model, els.model.value);
|
| 611 |
+
localStorage.setItem(STORAGE_KEYS.layer, els.layer.value);
|
| 612 |
+
localStorage.setItem(STORAGE_KEYS.topK, els.topK.value);
|
| 613 |
+
localStorage.setItem(STORAGE_KEYS.cardWidth, els.cardWidth.value);
|
| 614 |
+
localStorage.setItem(STORAGE_KEYS.weakRatio, els.weakRatio.value);
|
| 615 |
+
localStorage.setItem(STORAGE_KEYS.keepModels, els.keepModels.checked ? "1" : "0");
|
| 616 |
+
}
|
| 617 |
+
|
| 618 |
+
function showError(message) {
|
| 619 |
+
els.message.hidden = false;
|
| 620 |
+
els.message.className = "error";
|
| 621 |
+
els.message.textContent = message;
|
| 622 |
+
}
|
| 623 |
+
|
| 624 |
+
function featureColor(featureId) {
|
| 625 |
+
return `hsl(${(37 * Number(featureId)) % 360} 74% 48%)`;
|
| 626 |
+
}
|
| 627 |
+
|
| 628 |
+
function formatScore(value) {
|
| 629 |
+
const number = Number(value || 0);
|
| 630 |
+
const abs = Math.abs(number);
|
| 631 |
+
if (abs >= 100) return number.toFixed(0);
|
| 632 |
+
if (abs >= 10) return number.toFixed(1);
|
| 633 |
+
return number.toFixed(2);
|
| 634 |
+
}
|
| 635 |
+
|
| 636 |
+
function escapeHtml(value) {
|
| 637 |
+
return String(value).replace(/[&<>"']/g, char => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char]));
|
| 638 |
+
}
|
| 639 |
+
|
| 640 |
+
function escapeAttr(value) {
|
| 641 |
+
return escapeHtml(value);
|
| 642 |
+
}
|
| 643 |
+
|
| 644 |
+
els.model.addEventListener("change", async () => {
|
| 645 |
+
persistControls();
|
| 646 |
+
try {
|
| 647 |
+
await loadSaeMeta();
|
| 648 |
+
persistControls();
|
| 649 |
+
runProbe();
|
| 650 |
+
} catch (err) {
|
| 651 |
+
showError(err.message);
|
| 652 |
+
}
|
| 653 |
+
});
|
| 654 |
+
els.layer.addEventListener("change", () => { persistControls(); restoreFeatureHighlightsForContext(); runProbe(); });
|
| 655 |
+
els.topK.addEventListener("change", () => { persistControls(); runProbe(); });
|
| 656 |
+
els.topK.addEventListener("input", () => { persistControls(); runProbe(); });
|
| 657 |
+
els.cardWidth.addEventListener("change", updateCardWidth);
|
| 658 |
+
els.cardWidth.addEventListener("input", updateCardWidth);
|
| 659 |
+
els.weakRatio.addEventListener("change", () => { persistControls(); rerenderLastProbe(); });
|
| 660 |
+
els.weakRatio.addEventListener("input", () => { persistControls(); rerenderLastProbe(); });
|
| 661 |
+
els.keepModels.addEventListener("change", () => { persistControls(); if (!els.keepModels.checked) runProbe(); });
|
| 662 |
+
els.text.addEventListener("input", scheduleProbe);
|
| 663 |
+
els.text.addEventListener("keydown", handleTextKeydown);
|
| 664 |
+
els.runProbe.addEventListener("click", () => {
|
| 665 |
+
localStorage.setItem(STORAGE_KEYS.probeText, els.text.value);
|
| 666 |
+
persistControls();
|
| 667 |
+
runProbe();
|
| 668 |
+
});
|
| 669 |
+
updateCardWidth();
|
| 670 |
+
init();
|
| 671 |
+
</script>
|
| 672 |
+
</body>
|
| 673 |
+
</html>
|
server/static/stats.html
ADDED
|
@@ -0,0 +1,557 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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" />
|
| 6 |
+
<title>ICA Annotation Stats</title>
|
| 7 |
+
<style>
|
| 8 |
+
:root {
|
| 9 |
+
--bg: #f6f7f9;
|
| 10 |
+
--panel: #fff;
|
| 11 |
+
--text: #151922;
|
| 12 |
+
--muted: #647084;
|
| 13 |
+
--border: #cbd3df;
|
| 14 |
+
--accent: #1f6feb;
|
| 15 |
+
--shadow: 0 1px 2px rgb(20 25 34 / .08), 0 10px 30px rgb(20 25 34 / .06);
|
| 16 |
+
}
|
| 17 |
+
* { box-sizing: border-box; }
|
| 18 |
+
body {
|
| 19 |
+
margin: 0;
|
| 20 |
+
background: var(--bg);
|
| 21 |
+
color: var(--text);
|
| 22 |
+
font: 14px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
| 23 |
+
}
|
| 24 |
+
header {
|
| 25 |
+
position: sticky;
|
| 26 |
+
top: 0;
|
| 27 |
+
z-index: 5;
|
| 28 |
+
background: var(--panel);
|
| 29 |
+
border-bottom: 1px solid var(--border);
|
| 30 |
+
box-shadow: var(--shadow);
|
| 31 |
+
padding: 12px 18px;
|
| 32 |
+
}
|
| 33 |
+
.top {
|
| 34 |
+
display: flex;
|
| 35 |
+
align-items: center;
|
| 36 |
+
justify-content: space-between;
|
| 37 |
+
gap: 14px;
|
| 38 |
+
}
|
| 39 |
+
h1 { margin: 0; font-size: 19px; letter-spacing: 0; }
|
| 40 |
+
.nav {
|
| 41 |
+
display: flex;
|
| 42 |
+
align-items: center;
|
| 43 |
+
gap: 14px;
|
| 44 |
+
}
|
| 45 |
+
a {
|
| 46 |
+
color: var(--accent);
|
| 47 |
+
font-weight: 750;
|
| 48 |
+
text-decoration: none;
|
| 49 |
+
}
|
| 50 |
+
a:hover { text-decoration: underline; }
|
| 51 |
+
main {
|
| 52 |
+
width: 100%;
|
| 53 |
+
margin: 0;
|
| 54 |
+
padding: 12px;
|
| 55 |
+
}
|
| 56 |
+
.panel {
|
| 57 |
+
background: var(--panel);
|
| 58 |
+
border: 1px solid var(--border);
|
| 59 |
+
border-radius: 8px;
|
| 60 |
+
box-shadow: var(--shadow);
|
| 61 |
+
padding: 12px;
|
| 62 |
+
}
|
| 63 |
+
.toolbar {
|
| 64 |
+
display: flex;
|
| 65 |
+
align-items: center;
|
| 66 |
+
justify-content: space-between;
|
| 67 |
+
gap: 12px;
|
| 68 |
+
margin-bottom: 12px;
|
| 69 |
+
color: var(--muted);
|
| 70 |
+
font-size: 12px;
|
| 71 |
+
font-weight: 700;
|
| 72 |
+
}
|
| 73 |
+
.model-control {
|
| 74 |
+
display: inline-flex;
|
| 75 |
+
align-items: center;
|
| 76 |
+
gap: 7px;
|
| 77 |
+
color: #435066;
|
| 78 |
+
font-size: 12px;
|
| 79 |
+
font-weight: 800;
|
| 80 |
+
white-space: nowrap;
|
| 81 |
+
}
|
| 82 |
+
.model-control select {
|
| 83 |
+
width: 210px;
|
| 84 |
+
border: 1px solid var(--border);
|
| 85 |
+
border-radius: 7px;
|
| 86 |
+
background: #fff;
|
| 87 |
+
color: var(--text);
|
| 88 |
+
font: inherit;
|
| 89 |
+
padding: 7px 9px;
|
| 90 |
+
}
|
| 91 |
+
.layer-control select {
|
| 92 |
+
width: 160px;
|
| 93 |
+
}
|
| 94 |
+
.toolbar-left {
|
| 95 |
+
display: flex;
|
| 96 |
+
flex-wrap: wrap;
|
| 97 |
+
align-items: center;
|
| 98 |
+
gap: 10px 14px;
|
| 99 |
+
}
|
| 100 |
+
.legend {
|
| 101 |
+
display: flex;
|
| 102 |
+
flex-wrap: wrap;
|
| 103 |
+
gap: 8px 12px;
|
| 104 |
+
align-items: center;
|
| 105 |
+
}
|
| 106 |
+
.legend-item {
|
| 107 |
+
display: inline-flex;
|
| 108 |
+
align-items: center;
|
| 109 |
+
gap: 5px;
|
| 110 |
+
white-space: nowrap;
|
| 111 |
+
}
|
| 112 |
+
.legend-toggle {
|
| 113 |
+
width: auto;
|
| 114 |
+
border: 0;
|
| 115 |
+
background: transparent;
|
| 116 |
+
color: inherit;
|
| 117 |
+
font: inherit;
|
| 118 |
+
font-weight: inherit;
|
| 119 |
+
padding: 0;
|
| 120 |
+
cursor: pointer;
|
| 121 |
+
}
|
| 122 |
+
.legend-toggle:hover { color: var(--accent); }
|
| 123 |
+
.matrix {
|
| 124 |
+
overflow: visible;
|
| 125 |
+
border: 1px solid var(--border);
|
| 126 |
+
border-radius: 7px;
|
| 127 |
+
background: #fff;
|
| 128 |
+
}
|
| 129 |
+
.layer-row {
|
| 130 |
+
display: grid;
|
| 131 |
+
grid-template-columns: 104px 86px minmax(0, 1fr);
|
| 132 |
+
align-items: start;
|
| 133 |
+
gap: 8px;
|
| 134 |
+
width: 100%;
|
| 135 |
+
padding: 6px 8px;
|
| 136 |
+
border-bottom: 1px solid #e5eaf2;
|
| 137 |
+
}
|
| 138 |
+
.layer-row:last-child { border-bottom: 0; }
|
| 139 |
+
.layer-name {
|
| 140 |
+
font-weight: 850;
|
| 141 |
+
white-space: nowrap;
|
| 142 |
+
}
|
| 143 |
+
.layer-count {
|
| 144 |
+
color: var(--muted);
|
| 145 |
+
font-size: 12px;
|
| 146 |
+
font-variant-numeric: tabular-nums;
|
| 147 |
+
text-align: right;
|
| 148 |
+
white-space: nowrap;
|
| 149 |
+
}
|
| 150 |
+
.dots {
|
| 151 |
+
display: flex;
|
| 152 |
+
flex-wrap: wrap;
|
| 153 |
+
gap: 3px;
|
| 154 |
+
align-items: flex-start;
|
| 155 |
+
min-width: 0;
|
| 156 |
+
}
|
| 157 |
+
.component-pair {
|
| 158 |
+
display: inline-flex;
|
| 159 |
+
gap: 1px;
|
| 160 |
+
margin-right: 2px;
|
| 161 |
+
flex: 0 0 auto;
|
| 162 |
+
}
|
| 163 |
+
.dot {
|
| 164 |
+
display: inline-flex;
|
| 165 |
+
align-items: center;
|
| 166 |
+
justify-content: center;
|
| 167 |
+
width: 13px;
|
| 168 |
+
height: 14px;
|
| 169 |
+
border: 1px solid #cbd5e1;
|
| 170 |
+
border-radius: 4px;
|
| 171 |
+
background: #f8fafc;
|
| 172 |
+
color: #64748b;
|
| 173 |
+
font-size: 8px;
|
| 174 |
+
font-weight: 850;
|
| 175 |
+
line-height: 1;
|
| 176 |
+
font-variant-numeric: tabular-nums;
|
| 177 |
+
flex: 0 0 auto;
|
| 178 |
+
text-decoration: none;
|
| 179 |
+
}
|
| 180 |
+
.dot:hover { text-decoration: none; outline: 2px solid rgb(31 111 235 / .25); outline-offset: 1px; }
|
| 181 |
+
.dot.high { color: #fff; background: #16a34a; border-color: #15803d; }
|
| 182 |
+
.dot.medium { color: #166534; background: #fef3c7; border-color: #d9b94e; }
|
| 183 |
+
.dot.low { color: #9f1239; background: #ffe4e6; border-color: #f9a8d4; }
|
| 184 |
+
.dot.unclear { color: #475569; background: #e5e7eb; border-color: #cbd5e1; }
|
| 185 |
+
.dot.auto {
|
| 186 |
+
color: #3b2600;
|
| 187 |
+
background: #f6c343;
|
| 188 |
+
border-color: #b77900;
|
| 189 |
+
box-shadow: inset 0 0 0 1px rgb(255 255 255 / .38);
|
| 190 |
+
}
|
| 191 |
+
.dot.erf-value {
|
| 192 |
+
overflow: hidden;
|
| 193 |
+
font-size: 7px;
|
| 194 |
+
}
|
| 195 |
+
.empty, .error {
|
| 196 |
+
border: 1px dashed var(--border);
|
| 197 |
+
border-radius: 8px;
|
| 198 |
+
padding: 18px;
|
| 199 |
+
color: var(--muted);
|
| 200 |
+
background: #fff;
|
| 201 |
+
}
|
| 202 |
+
.error { color: #a41414; border-color: #f0b9b9; background: #fff7f7; }
|
| 203 |
+
@media (max-width: 760px) {
|
| 204 |
+
main { padding: 8px; }
|
| 205 |
+
header { padding: 10px 12px; }
|
| 206 |
+
.top { align-items: flex-start; flex-direction: column; }
|
| 207 |
+
.toolbar { align-items: flex-start; flex-direction: column; }
|
| 208 |
+
.layer-row { grid-template-columns: 74px 58px minmax(0, 1fr); gap: 6px; padding: 6px; }
|
| 209 |
+
.dot { width: 11px; height: 12px; border-radius: 3px; font-size: 7px; }
|
| 210 |
+
}
|
| 211 |
+
</style>
|
| 212 |
+
</head>
|
| 213 |
+
<body>
|
| 214 |
+
<header>
|
| 215 |
+
<div class="top">
|
| 216 |
+
<h1>ICA Annotation Stats</h1>
|
| 217 |
+
<nav class="nav" aria-label="Primary">
|
| 218 |
+
<a href="/">Explorer</a>
|
| 219 |
+
<a href="/sae-explorer">SAE Explorer</a>
|
| 220 |
+
<a href="/stats">Stats</a>
|
| 221 |
+
<a href="/annotate">Annotate</a>
|
| 222 |
+
<a href="/random-components">Random</a>
|
| 223 |
+
</nav>
|
| 224 |
+
</div>
|
| 225 |
+
</header>
|
| 226 |
+
|
| 227 |
+
<main>
|
| 228 |
+
<div class="panel">
|
| 229 |
+
<div class="toolbar">
|
| 230 |
+
<div class="toolbar-left">
|
| 231 |
+
<label class="model-control">
|
| 232 |
+
Model
|
| 233 |
+
<select id="modelSelect"></select>
|
| 234 |
+
</label>
|
| 235 |
+
<label class="model-control layer-control">
|
| 236 |
+
Layer
|
| 237 |
+
<select id="layerSelect" disabled></select>
|
| 238 |
+
</label>
|
| 239 |
+
<div id="summary">Please select a model.</div>
|
| 240 |
+
</div>
|
| 241 |
+
<div class="legend" aria-label="Confidence legend">
|
| 242 |
+
<span class="legend-item"><span class="dot high">W</span> high</span>
|
| 243 |
+
<span class="legend-item"><span class="dot medium">W</span> medium</span>
|
| 244 |
+
<span class="legend-item"><span class="dot low">W</span> low</span>
|
| 245 |
+
<span class="legend-item"><span class="dot unclear">?</span> unclear</span>
|
| 246 |
+
<button id="autoToggle" class="legend-item legend-toggle" type="button" aria-pressed="true" title="Toggle auto-annotation coloring"><span class="dot auto">W</span> auto</button>
|
| 247 |
+
<button id="erfToggle" class="legend-item legend-toggle" type="button" aria-pressed="false" title="Toggle Effective Receptive Field labels"><span class="dot erf-value">11</span> ERF</button>
|
| 248 |
+
<span class="legend-item"><span class="dot"></span> blank</span>
|
| 249 |
+
</div>
|
| 250 |
+
</div>
|
| 251 |
+
<div id="message" class="empty">Loading all component stats...</div>
|
| 252 |
+
<div id="matrix" class="matrix" hidden></div>
|
| 253 |
+
</div>
|
| 254 |
+
</main>
|
| 255 |
+
<script>
|
| 256 |
+
const els = {
|
| 257 |
+
model: document.getElementById("modelSelect"),
|
| 258 |
+
layer: document.getElementById("layerSelect"),
|
| 259 |
+
summary: document.getElementById("summary"),
|
| 260 |
+
message: document.getElementById("message"),
|
| 261 |
+
matrix: document.getElementById("matrix"),
|
| 262 |
+
autoToggle: document.getElementById("autoToggle"),
|
| 263 |
+
erfToggle: document.getElementById("erfToggle"),
|
| 264 |
+
};
|
| 265 |
+
|
| 266 |
+
const STORAGE_KEYS = {
|
| 267 |
+
model: "icaExplorer.model",
|
| 268 |
+
layer: "icaExplorer.layer",
|
| 269 |
+
};
|
| 270 |
+
|
| 271 |
+
const state = {
|
| 272 |
+
model: "",
|
| 273 |
+
layer: "",
|
| 274 |
+
layers: [],
|
| 275 |
+
showAuto: true,
|
| 276 |
+
showErf: false,
|
| 277 |
+
};
|
| 278 |
+
|
| 279 |
+
async function api(path) {
|
| 280 |
+
const res = await fetch(path, { headers: { "content-type": "application/json" } });
|
| 281 |
+
if (!res.ok) {
|
| 282 |
+
let detail = res.statusText;
|
| 283 |
+
try { detail = (await res.json()).detail || detail; } catch {}
|
| 284 |
+
throw new Error(detail);
|
| 285 |
+
}
|
| 286 |
+
return res.json();
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
async function init() {
|
| 290 |
+
try {
|
| 291 |
+
const params = new URLSearchParams(location.search);
|
| 292 |
+
const urlModel = params.get("model") || "";
|
| 293 |
+
const urlLayer = normalizeUrlLayer(params.get("layer") || "");
|
| 294 |
+
const storedModel = localStorage.getItem(STORAGE_KEYS.model) || "";
|
| 295 |
+
const storedLayer = localStorage.getItem(STORAGE_KEYS.layer) || "";
|
| 296 |
+
const requestedModel = urlModel || storedModel;
|
| 297 |
+
const requestedLayer = urlLayer || storedLayer;
|
| 298 |
+
const models = await api("/api/models");
|
| 299 |
+
const names = (models.models || []).map(model => model.model_name);
|
| 300 |
+
fillSelect(els.model, ["", ...names], names.includes(requestedModel) ? requestedModel : "");
|
| 301 |
+
els.model.options[0].textContent = "Please select";
|
| 302 |
+
fillSelect(els.layer, [""], "");
|
| 303 |
+
els.layer.options[0].textContent = "Please select";
|
| 304 |
+
state.model = els.model.value;
|
| 305 |
+
els.model.addEventListener("change", () => {
|
| 306 |
+
state.model = els.model.value;
|
| 307 |
+
state.layer = "";
|
| 308 |
+
persistSelection();
|
| 309 |
+
updateUrl();
|
| 310 |
+
loadLayers();
|
| 311 |
+
});
|
| 312 |
+
els.layer.addEventListener("change", () => {
|
| 313 |
+
state.layer = els.layer.value;
|
| 314 |
+
persistSelection();
|
| 315 |
+
updateUrl();
|
| 316 |
+
loadStats();
|
| 317 |
+
});
|
| 318 |
+
if (state.model) await loadLayers({ requestedLayer });
|
| 319 |
+
else clearStats("Please select a model.");
|
| 320 |
+
} catch (err) {
|
| 321 |
+
showError(err.message);
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
async function loadLayers(options = {}) {
|
| 326 |
+
state.layers = [];
|
| 327 |
+
els.matrix.innerHTML = "";
|
| 328 |
+
els.matrix.hidden = true;
|
| 329 |
+
if (!state.model) {
|
| 330 |
+
els.layer.disabled = true;
|
| 331 |
+
fillSelect(els.layer, [""], "");
|
| 332 |
+
els.layer.options[0].textContent = "Please select";
|
| 333 |
+
clearStats("Please select a model.");
|
| 334 |
+
return;
|
| 335 |
+
}
|
| 336 |
+
els.summary.textContent = "Loading layers...";
|
| 337 |
+
const out = await api(`/api/layers?model=${encodeURIComponent(state.model)}`);
|
| 338 |
+
const layerValues = ["", "__all__", ...(out.layers || [])];
|
| 339 |
+
const requestedLayer = layerValues.includes(options.requestedLayer) ? options.requestedLayer : "";
|
| 340 |
+
fillSelect(els.layer, layerValues, requestedLayer);
|
| 341 |
+
els.layer.options[0].textContent = "Please select";
|
| 342 |
+
els.layer.options[1].textContent = "All layers";
|
| 343 |
+
els.layer.disabled = false;
|
| 344 |
+
state.layer = els.layer.value;
|
| 345 |
+
persistSelection();
|
| 346 |
+
if (state.layer) await loadStats();
|
| 347 |
+
else clearStats("Please select a layer.");
|
| 348 |
+
}
|
| 349 |
+
|
| 350 |
+
async function loadStats() {
|
| 351 |
+
if (!state.model) {
|
| 352 |
+
clearStats("Please select a model.");
|
| 353 |
+
return;
|
| 354 |
+
}
|
| 355 |
+
if (!state.layer) {
|
| 356 |
+
clearStats("Please select a layer.");
|
| 357 |
+
return;
|
| 358 |
+
}
|
| 359 |
+
els.summary.textContent = state.layer === "__all__" ? "Loading all layers..." : `Loading ${state.layer}...`;
|
| 360 |
+
if (state.layer === "__all__") {
|
| 361 |
+
const out = await api(`/api/component-stats?model=${encodeURIComponent(state.model)}`);
|
| 362 |
+
state.layers = out.layers || [];
|
| 363 |
+
} else {
|
| 364 |
+
const out = await api(`/api/components?model=${encodeURIComponent(state.model)}&layer=${encodeURIComponent(state.layer)}`);
|
| 365 |
+
state.layers = [{ layer: state.layer, components: out.components || [] }];
|
| 366 |
+
}
|
| 367 |
+
renderStats(state.layers);
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
function renderStats(layers) {
|
| 371 |
+
const components = layers.flatMap(layer => layer.components || []);
|
| 372 |
+
const annotated = components.reduce((total, component) => total + annotatedDirectionCount(component), 0);
|
| 373 |
+
const totalDirections = components.length * 2;
|
| 374 |
+
const scope = state.layer === "__all__" ? `${layers.length} layers` : state.layer;
|
| 375 |
+
els.summary.textContent = `${state.model} - ${scope} - ${annotated} / ${totalDirections} directions annotated`;
|
| 376 |
+
if (!layers.length) {
|
| 377 |
+
els.message.hidden = false;
|
| 378 |
+
els.message.className = "empty";
|
| 379 |
+
els.message.textContent = "No components found.";
|
| 380 |
+
els.matrix.hidden = true;
|
| 381 |
+
return;
|
| 382 |
+
}
|
| 383 |
+
els.message.hidden = true;
|
| 384 |
+
els.matrix.hidden = false;
|
| 385 |
+
els.matrix.innerHTML = layers.map(layer => layerRow(layer)).join("");
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
function clearStats(message) {
|
| 389 |
+
state.layers = [];
|
| 390 |
+
els.summary.textContent = message;
|
| 391 |
+
els.message.hidden = false;
|
| 392 |
+
els.message.className = "empty";
|
| 393 |
+
els.message.textContent = message;
|
| 394 |
+
els.matrix.hidden = true;
|
| 395 |
+
els.matrix.innerHTML = "";
|
| 396 |
+
}
|
| 397 |
+
|
| 398 |
+
function updateUrl() {
|
| 399 |
+
const params = new URLSearchParams();
|
| 400 |
+
if (state.model) params.set("model", state.model);
|
| 401 |
+
if (state.layer) params.set("layer", state.layer === "__all__" ? "all" : state.layer);
|
| 402 |
+
const query = params.toString();
|
| 403 |
+
history.replaceState(null, "", query ? `${location.pathname}?${query}` : location.pathname);
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
function persistSelection() {
|
| 407 |
+
if (state.model) localStorage.setItem(STORAGE_KEYS.model, state.model);
|
| 408 |
+
else localStorage.removeItem(STORAGE_KEYS.model);
|
| 409 |
+
if (state.layer && state.layer !== "__all__") localStorage.setItem(STORAGE_KEYS.layer, state.layer);
|
| 410 |
+
}
|
| 411 |
+
|
| 412 |
+
function normalizeUrlLayer(value) {
|
| 413 |
+
return value === "all" ? "__all__" : value;
|
| 414 |
+
}
|
| 415 |
+
|
| 416 |
+
els.autoToggle.addEventListener("click", () => {
|
| 417 |
+
state.showAuto = !state.showAuto;
|
| 418 |
+
els.autoToggle.setAttribute("aria-pressed", state.showAuto ? "true" : "false");
|
| 419 |
+
if (state.layers.length) renderStats(state.layers);
|
| 420 |
+
});
|
| 421 |
+
|
| 422 |
+
els.erfToggle.addEventListener("click", () => {
|
| 423 |
+
state.showErf = !state.showErf;
|
| 424 |
+
els.erfToggle.setAttribute("aria-pressed", state.showErf ? "true" : "false");
|
| 425 |
+
if (state.layers.length) renderStats(state.layers);
|
| 426 |
+
});
|
| 427 |
+
|
| 428 |
+
function layerRow(layer) {
|
| 429 |
+
const components = layer.components || [];
|
| 430 |
+
const annotated = components.reduce((total, component) => total + annotatedDirectionCount(component), 0);
|
| 431 |
+
const totalDirections = components.length * 2;
|
| 432 |
+
return `
|
| 433 |
+
<div class="layer-row">
|
| 434 |
+
<div class="layer-name">${escapeHtml(layer.layer)}</div>
|
| 435 |
+
<div class="layer-count">${annotated}/${totalDirections}</div>
|
| 436 |
+
<div class="dots">${components.map(component => componentPair(layer.layer, component)).join("")}</div>
|
| 437 |
+
</div>
|
| 438 |
+
`;
|
| 439 |
+
}
|
| 440 |
+
|
| 441 |
+
function componentPair(layer, component) {
|
| 442 |
+
return `
|
| 443 |
+
<span class="component-pair">
|
| 444 |
+
${componentDirectionDot(layer, component, "positive")}
|
| 445 |
+
${componentDirectionDot(layer, component, "negative")}
|
| 446 |
+
</span>
|
| 447 |
+
`;
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
function componentDirectionDot(layer, component, side) {
|
| 451 |
+
const annotation = annotationSide(component, side);
|
| 452 |
+
const title = componentTitle(component, annotation, side);
|
| 453 |
+
const href = componentExamplesUrl(layer, component);
|
| 454 |
+
const text = state.showErf ? erfText(component) : (annotation?.typeLetter || "?");
|
| 455 |
+
const erfClass = state.showErf ? " erf-value" : "";
|
| 456 |
+
if (!annotation) {
|
| 457 |
+
return `<a class="dot${erfClass}" href="${escapeAttr(href)}" title="${escapeAttr(title)}" aria-label="${escapeAttr(title)}">${escapeHtml(state.showErf ? text : "")}</a>`;
|
| 458 |
+
}
|
| 459 |
+
const classes = ["dot", annotation.confidence];
|
| 460 |
+
if (state.showErf) classes.push("erf-value");
|
| 461 |
+
if (state.showAuto && component[`${side}_auto_annotated`] && annotation.annotated) classes.push("auto");
|
| 462 |
+
return `<a class="${escapeAttr(classes.join(" "))}" href="${escapeAttr(href)}" title="${escapeAttr(title)}" aria-label="${escapeAttr(title)}">${escapeHtml(text)}</a>`;
|
| 463 |
+
}
|
| 464 |
+
|
| 465 |
+
function erfText(component) {
|
| 466 |
+
const value = Number(component.effective_context_mean);
|
| 467 |
+
return Number.isFinite(value) ? String(Math.round(value)) : "";
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
function componentExamplesUrl(layer, component) {
|
| 471 |
+
const params = new URLSearchParams({
|
| 472 |
+
model: state.model,
|
| 473 |
+
layer: String(layer),
|
| 474 |
+
component: String(component.component),
|
| 475 |
+
});
|
| 476 |
+
return `/component?${params.toString()}`;
|
| 477 |
+
}
|
| 478 |
+
|
| 479 |
+
function annotatedDirectionCount(component) {
|
| 480 |
+
return ["positive", "negative"].reduce((total, side) => {
|
| 481 |
+
const annotation = annotationSide(component, side);
|
| 482 |
+
return total + (annotation?.annotated ? 1 : 0);
|
| 483 |
+
}, 0);
|
| 484 |
+
}
|
| 485 |
+
|
| 486 |
+
function annotationSide(component, side) {
|
| 487 |
+
const confidence = normalizedConfidence(component[`${side}_confidence`]);
|
| 488 |
+
const label = visibleAnnotationLabel(component[`${side}_label`], confidence);
|
| 489 |
+
if (!label) return null;
|
| 490 |
+
const types = Array.isArray(component[`${side}_types`]) ? component[`${side}_types`] : [];
|
| 491 |
+
const annotated = !(label === "?" && confidence === "unclear");
|
| 492 |
+
return { side, label, confidence, types, typeLetter: typeLetter(types), annotated };
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
function componentTitle(component, annotation, side) {
|
| 496 |
+
const id = `C${Number(component.component)}`;
|
| 497 |
+
const kurtosis = Number.isFinite(Number(component.excess_kurtosis)) ? ` - kurtosis ${Number(component.excess_kurtosis).toFixed(2)}` : "";
|
| 498 |
+
if (!annotation) return `${id}: ${side} blank${kurtosis}`;
|
| 499 |
+
const type = annotation.types.length ? ` - ${annotation.types.join(", ")}` : "";
|
| 500 |
+
const source = state.showAuto && component[`${side}_auto_annotated`] && annotation.annotated ? " - auto-annotated" : "";
|
| 501 |
+
const erf = Number.isFinite(Number(component.effective_context_mean)) ? ` - mean ERF ${Number(component.effective_context_mean).toFixed(2)}` : "";
|
| 502 |
+
return `${id}: ${annotation.side} ${annotation.label} - ${annotation.confidence}${type}${source}${erf}${kurtosis}`;
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
function visibleAnnotationLabel(value, confidence) {
|
| 506 |
+
const text = String(value || "").trim();
|
| 507 |
+
if (!text) return "";
|
| 508 |
+
return text.replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
|
| 509 |
+
}
|
| 510 |
+
|
| 511 |
+
function normalizedConfidence(value) {
|
| 512 |
+
const confidence = String(value || "unclear").toLowerCase();
|
| 513 |
+
return ["high", "medium", "low", "unclear"].includes(confidence) ? confidence : "unclear";
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
function typeLetter(types) {
|
| 517 |
+
const labels = Array.isArray(types) ? types : [];
|
| 518 |
+
const priority = [
|
| 519 |
+
["Form", "F"],
|
| 520 |
+
["Word", "W"],
|
| 521 |
+
["Phrase", "P"],
|
| 522 |
+
["Sentence", "S"],
|
| 523 |
+
["Long-Range Context", "L"],
|
| 524 |
+
["Global", "G"],
|
| 525 |
+
["Position", "O"],
|
| 526 |
+
["Sophisticated", "X"],
|
| 527 |
+
];
|
| 528 |
+
for (const [name, letter] of priority) {
|
| 529 |
+
if (labels.some(label => String(label).toLowerCase() === name.toLowerCase())) return letter;
|
| 530 |
+
}
|
| 531 |
+
const first = labels.find(label => String(label).trim());
|
| 532 |
+
return first ? String(first).trim()[0].toUpperCase() : "";
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
function showError(message) {
|
| 536 |
+
els.message.hidden = false;
|
| 537 |
+
els.message.className = "error";
|
| 538 |
+
els.message.textContent = message;
|
| 539 |
+
els.matrix.hidden = true;
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
function fillSelect(select, values, current) {
|
| 543 |
+
select.innerHTML = values.map(value => `<option value="${escapeAttr(value)}" ${value === current ? "selected" : ""}>${escapeHtml(value)}</option>`).join("");
|
| 544 |
+
}
|
| 545 |
+
|
| 546 |
+
function escapeHtml(value) {
|
| 547 |
+
return String(value).replace(/[&<>"']/g, char => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[char]));
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
function escapeAttr(value) {
|
| 551 |
+
return escapeHtml(value);
|
| 552 |
+
}
|
| 553 |
+
|
| 554 |
+
init();
|
| 555 |
+
</script>
|
| 556 |
+
</body>
|
| 557 |
+
</html>
|
server/store.py
ADDED
|
@@ -0,0 +1,772 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
import sqlite3
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import Any, Iterable
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
SCHEMA = """
|
| 10 |
+
CREATE TABLE IF NOT EXISTS annotations (
|
| 11 |
+
model_name TEXT NOT NULL,
|
| 12 |
+
layer TEXT NOT NULL,
|
| 13 |
+
component INTEGER NOT NULL,
|
| 14 |
+
label TEXT,
|
| 15 |
+
confidence TEXT,
|
| 16 |
+
positive_label TEXT,
|
| 17 |
+
positive_confidence TEXT,
|
| 18 |
+
negative_label TEXT,
|
| 19 |
+
negative_confidence TEXT,
|
| 20 |
+
positive_interpretation_types_json TEXT,
|
| 21 |
+
negative_interpretation_types_json TEXT,
|
| 22 |
+
interpretation_types_json TEXT,
|
| 23 |
+
summary TEXT,
|
| 24 |
+
notes TEXT,
|
| 25 |
+
include_as_case_study INTEGER NOT NULL DEFAULT 0,
|
| 26 |
+
updated_at TEXT NOT NULL,
|
| 27 |
+
PRIMARY KEY (model_name, layer, component)
|
| 28 |
+
);
|
| 29 |
+
|
| 30 |
+
CREATE TABLE IF NOT EXISTS chosen_random_components (
|
| 31 |
+
selection_name TEXT NOT NULL,
|
| 32 |
+
model_name TEXT NOT NULL,
|
| 33 |
+
selection_index INTEGER NOT NULL,
|
| 34 |
+
layer TEXT NOT NULL,
|
| 35 |
+
component INTEGER NOT NULL,
|
| 36 |
+
component_id TEXT,
|
| 37 |
+
source_json TEXT,
|
| 38 |
+
seed INTEGER,
|
| 39 |
+
requested_n INTEGER,
|
| 40 |
+
inventory_size INTEGER,
|
| 41 |
+
selected_size INTEGER,
|
| 42 |
+
fit_converged INTEGER,
|
| 43 |
+
fit_iterations INTEGER,
|
| 44 |
+
fit_final_lim REAL,
|
| 45 |
+
fit_final_lim_p95 REAL,
|
| 46 |
+
fit_seed INTEGER,
|
| 47 |
+
inserted_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
| 48 |
+
PRIMARY KEY (selection_name, model_name, selection_index)
|
| 49 |
+
);
|
| 50 |
+
|
| 51 |
+
CREATE INDEX IF NOT EXISTS idx_chosen_random_components_component
|
| 52 |
+
ON chosen_random_components(model_name, layer, component);
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def connect(db_path: Path) -> sqlite3.Connection:
|
| 57 |
+
db_path.parent.mkdir(parents=True, exist_ok=True)
|
| 58 |
+
conn = sqlite3.connect(str(db_path), check_same_thread=False)
|
| 59 |
+
conn.row_factory = sqlite3.Row
|
| 60 |
+
conn.execute("PRAGMA foreign_keys = ON")
|
| 61 |
+
return conn
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def init_db(conn: sqlite3.Connection) -> None:
|
| 65 |
+
conn.executescript(SCHEMA)
|
| 66 |
+
_ensure_annotation_columns(conn)
|
| 67 |
+
conn.commit()
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
def validate_db(conn: sqlite3.Connection, model_name: str | None = None) -> None:
|
| 71 |
+
tables = {str(row[0]) for row in conn.execute("SELECT name FROM sqlite_master WHERE type = 'table'").fetchall()}
|
| 72 |
+
missing = {"components", "examples", "annotations"} - tables
|
| 73 |
+
if missing:
|
| 74 |
+
raise RuntimeError(f"SQLite DB is missing required table(s): {', '.join(sorted(missing))}")
|
| 75 |
+
if model_name:
|
| 76 |
+
count = conn.execute("SELECT COUNT(*) FROM components WHERE model_name = ?", (model_name,)).fetchone()[0]
|
| 77 |
+
if int(count) == 0:
|
| 78 |
+
raise RuntimeError(f"SQLite DB has no components for model_name={model_name!r}")
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def list_models(conn: sqlite3.Connection) -> list[str]:
|
| 82 |
+
rows = conn.execute(
|
| 83 |
+
"""
|
| 84 |
+
SELECT model_name FROM components
|
| 85 |
+
UNION
|
| 86 |
+
SELECT model_name FROM annotations
|
| 87 |
+
ORDER BY model_name
|
| 88 |
+
"""
|
| 89 |
+
).fetchall()
|
| 90 |
+
return [str(row[0]) for row in rows]
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def list_layers(conn: sqlite3.Connection, model_name: str) -> list[str]:
|
| 94 |
+
rows = conn.execute(
|
| 95 |
+
"SELECT DISTINCT layer FROM components WHERE model_name = ?",
|
| 96 |
+
(model_name,),
|
| 97 |
+
).fetchall()
|
| 98 |
+
return sorted([str(row[0]) for row in rows], key=layer_sort_key)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def list_components(conn: sqlite3.Connection, model_name: str, layer: str | None = None, component: int | None = None, search: str | None = None) -> list[dict[str, Any]]:
|
| 102 |
+
erf_join, erf_select = _erf_join_sql(conn)
|
| 103 |
+
rows = conn.execute(
|
| 104 |
+
f"""
|
| 105 |
+
SELECT c.layer,
|
| 106 |
+
c.component,
|
| 107 |
+
c.excess_kurtosis,
|
| 108 |
+
{erf_select}
|
| 109 |
+
a.positive_label,
|
| 110 |
+
a.positive_confidence,
|
| 111 |
+
a.negative_label,
|
| 112 |
+
a.negative_confidence,
|
| 113 |
+
a.positive_interpretation_types_json,
|
| 114 |
+
a.negative_interpretation_types_json,
|
| 115 |
+
a.summary,
|
| 116 |
+
a.notes,
|
| 117 |
+
a.include_as_case_study
|
| 118 |
+
FROM components c
|
| 119 |
+
LEFT JOIN annotations a
|
| 120 |
+
ON a.model_name = c.model_name
|
| 121 |
+
AND a.layer = c.layer
|
| 122 |
+
AND a.component = c.component
|
| 123 |
+
{erf_join}
|
| 124 |
+
WHERE c.model_name = ?
|
| 125 |
+
AND (? IS NULL OR c.layer = ?)
|
| 126 |
+
AND (? IS NULL OR c.component = ?)
|
| 127 |
+
""",
|
| 128 |
+
(model_name, layer, layer, component, component),
|
| 129 |
+
).fetchall()
|
| 130 |
+
query = str(search or "").strip().lower()
|
| 131 |
+
out = []
|
| 132 |
+
for row in rows:
|
| 133 |
+
item = {
|
| 134 |
+
"layer": str(row["layer"]),
|
| 135 |
+
"component": int(row["component"]),
|
| 136 |
+
"excess_kurtosis": float(row["excess_kurtosis"]) if row["excess_kurtosis"] is not None else None,
|
| 137 |
+
"effective_context_mean": float(row["effective_context_mean"]) if row["effective_context_mean"] is not None else None,
|
| 138 |
+
**_annotation_row(row),
|
| 139 |
+
}
|
| 140 |
+
text = " ".join(str(item.get(key, "")) for key in ("component", "positive_label", "negative_label", "summary", "notes")).lower()
|
| 141 |
+
if query and query not in text:
|
| 142 |
+
continue
|
| 143 |
+
out.append(item)
|
| 144 |
+
return sorted(out, key=lambda row: (layer_sort_key(row["layer"]), row["component"]))
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def list_component_stats(conn: sqlite3.Connection, model_name: str) -> list[dict[str, Any]]:
|
| 148 |
+
erf_join, erf_select = _erf_join_sql(conn)
|
| 149 |
+
rows = conn.execute(
|
| 150 |
+
f"""
|
| 151 |
+
SELECT c.layer,
|
| 152 |
+
c.component,
|
| 153 |
+
c.excess_kurtosis,
|
| 154 |
+
{erf_select}
|
| 155 |
+
a.positive_label,
|
| 156 |
+
a.positive_confidence,
|
| 157 |
+
a.negative_label,
|
| 158 |
+
a.negative_confidence,
|
| 159 |
+
a.positive_interpretation_types_json,
|
| 160 |
+
a.negative_interpretation_types_json,
|
| 161 |
+
a.notes
|
| 162 |
+
FROM components c
|
| 163 |
+
LEFT JOIN annotations a
|
| 164 |
+
ON a.model_name = c.model_name
|
| 165 |
+
AND a.layer = c.layer
|
| 166 |
+
AND a.component = c.component
|
| 167 |
+
{erf_join}
|
| 168 |
+
WHERE c.model_name = ?
|
| 169 |
+
""",
|
| 170 |
+
(model_name,),
|
| 171 |
+
).fetchall()
|
| 172 |
+
return sorted(
|
| 173 |
+
[
|
| 174 |
+
{
|
| 175 |
+
"layer": str(row["layer"]),
|
| 176 |
+
"effective_context_mean": float(row["effective_context_mean"]) if row["effective_context_mean"] is not None else None,
|
| 177 |
+
**_annotation_row(row),
|
| 178 |
+
}
|
| 179 |
+
for row in rows
|
| 180 |
+
],
|
| 181 |
+
key=lambda row: (layer_sort_key(row["layer"]), row["component"]),
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
|
| 185 |
+
def list_annotated_components(conn: sqlite3.Connection, model_name: str, layer: str) -> list[dict[str, Any]]:
|
| 186 |
+
erf_join, erf_select = _erf_join_sql(conn)
|
| 187 |
+
rows = conn.execute(
|
| 188 |
+
f"""
|
| 189 |
+
SELECT a.component,
|
| 190 |
+
{erf_select}
|
| 191 |
+
a.positive_label, a.positive_confidence,
|
| 192 |
+
a.negative_label, a.negative_confidence,
|
| 193 |
+
a.positive_interpretation_types_json,
|
| 194 |
+
a.negative_interpretation_types_json,
|
| 195 |
+
a.notes,
|
| 196 |
+
c.excess_kurtosis
|
| 197 |
+
FROM annotations a
|
| 198 |
+
LEFT JOIN components c
|
| 199 |
+
ON c.model_name = a.model_name
|
| 200 |
+
AND c.layer = a.layer
|
| 201 |
+
AND c.component = a.component
|
| 202 |
+
{erf_join}
|
| 203 |
+
WHERE a.model_name = ? AND a.layer = ?
|
| 204 |
+
AND (
|
| 205 |
+
(a.positive_label IS NOT NULL AND TRIM(a.positive_label) != '')
|
| 206 |
+
OR (a.negative_label IS NOT NULL AND TRIM(a.negative_label) != '')
|
| 207 |
+
)
|
| 208 |
+
ORDER BY a.component
|
| 209 |
+
""",
|
| 210 |
+
(model_name, layer),
|
| 211 |
+
).fetchall()
|
| 212 |
+
out = []
|
| 213 |
+
for row in rows:
|
| 214 |
+
item = _annotation_row(row)
|
| 215 |
+
item["effective_context_mean"] = float(row["effective_context_mean"]) if row["effective_context_mean"] is not None else None
|
| 216 |
+
out.append(item)
|
| 217 |
+
return out
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
|
| 221 |
+
def list_component_metadata(conn: sqlite3.Connection, model_name: str, layer: str, components: list[int]) -> list[dict[str, Any]]:
|
| 222 |
+
if not components:
|
| 223 |
+
return []
|
| 224 |
+
erf_join, erf_select = _erf_join_sql(conn)
|
| 225 |
+
unique_components = sorted({int(component) for component in components})
|
| 226 |
+
placeholders = ",".join("?" for _ in unique_components)
|
| 227 |
+
rows = conn.execute(
|
| 228 |
+
f"""
|
| 229 |
+
SELECT c.component,
|
| 230 |
+
c.excess_kurtosis,
|
| 231 |
+
{erf_select}
|
| 232 |
+
a.positive_label,
|
| 233 |
+
a.positive_confidence,
|
| 234 |
+
a.negative_label,
|
| 235 |
+
a.negative_confidence,
|
| 236 |
+
a.positive_interpretation_types_json,
|
| 237 |
+
a.negative_interpretation_types_json,
|
| 238 |
+
a.summary,
|
| 239 |
+
a.notes,
|
| 240 |
+
a.include_as_case_study
|
| 241 |
+
FROM components c
|
| 242 |
+
LEFT JOIN annotations a
|
| 243 |
+
ON a.model_name = c.model_name
|
| 244 |
+
AND a.layer = c.layer
|
| 245 |
+
AND a.component = c.component
|
| 246 |
+
{erf_join}
|
| 247 |
+
WHERE c.model_name = ? AND c.layer = ? AND c.component IN ({placeholders})
|
| 248 |
+
ORDER BY c.component
|
| 249 |
+
""",
|
| 250 |
+
(model_name, layer, *unique_components),
|
| 251 |
+
).fetchall()
|
| 252 |
+
out = []
|
| 253 |
+
for row in rows:
|
| 254 |
+
item = _annotation_row(row)
|
| 255 |
+
item["effective_context_mean"] = float(row["effective_context_mean"]) if row["effective_context_mean"] is not None else None
|
| 256 |
+
out.append(item)
|
| 257 |
+
return out
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def list_component_neighbors(conn: sqlite3.Connection, model_name: str, layer: str, component: int) -> list[dict[str, Any]]:
|
| 261 |
+
if not _table_exists(conn, "component_neighbors"):
|
| 262 |
+
return []
|
| 263 |
+
neighbor_columns = _table_columns(conn, "component_neighbors")
|
| 264 |
+
neighbor_sign_select = "n.neighbor_sign" if "neighbor_sign" in neighbor_columns else "CASE WHEN n.signed_cosine >= 0 THEN 1 ELSE -1 END"
|
| 265 |
+
erf_join, erf_select = _component_neighbor_erf_join_sql(conn)
|
| 266 |
+
rows = conn.execute(
|
| 267 |
+
f"""
|
| 268 |
+
SELECT n.direction,
|
| 269 |
+
n.neighbor_layer,
|
| 270 |
+
n.neighbor_component AS component,
|
| 271 |
+
n.abs_cosine,
|
| 272 |
+
n.signed_cosine,
|
| 273 |
+
{neighbor_sign_select} AS neighbor_sign,
|
| 274 |
+
n.source_n_components,
|
| 275 |
+
n.neighbor_n_components,
|
| 276 |
+
{erf_select}
|
| 277 |
+
a.positive_label,
|
| 278 |
+
a.positive_confidence,
|
| 279 |
+
a.negative_label,
|
| 280 |
+
a.negative_confidence,
|
| 281 |
+
a.positive_interpretation_types_json,
|
| 282 |
+
a.negative_interpretation_types_json,
|
| 283 |
+
a.summary,
|
| 284 |
+
a.notes,
|
| 285 |
+
a.include_as_case_study,
|
| 286 |
+
c.excess_kurtosis
|
| 287 |
+
FROM component_neighbors n
|
| 288 |
+
LEFT JOIN annotations a
|
| 289 |
+
ON a.model_name = n.model_name
|
| 290 |
+
AND a.layer = n.neighbor_layer
|
| 291 |
+
AND a.component = n.neighbor_component
|
| 292 |
+
LEFT JOIN components c
|
| 293 |
+
ON c.model_name = n.model_name
|
| 294 |
+
AND c.layer = n.neighbor_layer
|
| 295 |
+
AND c.component = n.neighbor_component
|
| 296 |
+
{erf_join}
|
| 297 |
+
WHERE n.model_name = ?
|
| 298 |
+
AND n.layer = ?
|
| 299 |
+
AND n.component = ?
|
| 300 |
+
ORDER BY CASE n.direction WHEN 'prev' THEN 0 WHEN 'next' THEN 1 ELSE 2 END
|
| 301 |
+
""",
|
| 302 |
+
(model_name, layer, int(component)),
|
| 303 |
+
).fetchall()
|
| 304 |
+
out = []
|
| 305 |
+
for row in rows:
|
| 306 |
+
item = _annotation_row(row)
|
| 307 |
+
item.update(
|
| 308 |
+
{
|
| 309 |
+
"direction": str(row["direction"]),
|
| 310 |
+
"neighbor_layer": str(row["neighbor_layer"]),
|
| 311 |
+
"neighbor_component": int(row["component"]),
|
| 312 |
+
"abs_cosine": float(row["abs_cosine"]),
|
| 313 |
+
"signed_cosine": float(row["signed_cosine"]),
|
| 314 |
+
"neighbor_sign": int(row["neighbor_sign"]),
|
| 315 |
+
"source_n_components": int(row["source_n_components"]),
|
| 316 |
+
"neighbor_n_components": int(row["neighbor_n_components"]),
|
| 317 |
+
"effective_context_mean": float(row["effective_context_mean"]) if row["effective_context_mean"] is not None else None,
|
| 318 |
+
}
|
| 319 |
+
)
|
| 320 |
+
out.append(item)
|
| 321 |
+
return out
|
| 322 |
+
|
| 323 |
+
|
| 324 |
+
def list_chosen_random_components(conn: sqlite3.Connection, model_name: str | None = None, selection_name: str | None = None) -> list[dict[str, Any]]:
|
| 325 |
+
if not _table_exists(conn, "chosen_random_components"):
|
| 326 |
+
return []
|
| 327 |
+
erf_join, erf_select = _erf_join_sql(conn)
|
| 328 |
+
rows = conn.execute(
|
| 329 |
+
f"""
|
| 330 |
+
SELECT r.selection_name,
|
| 331 |
+
r.model_name,
|
| 332 |
+
r.selection_index,
|
| 333 |
+
r.layer,
|
| 334 |
+
r.component,
|
| 335 |
+
r.component_id,
|
| 336 |
+
r.source_json,
|
| 337 |
+
r.seed,
|
| 338 |
+
r.requested_n,
|
| 339 |
+
r.inventory_size,
|
| 340 |
+
r.selected_size,
|
| 341 |
+
r.fit_converged,
|
| 342 |
+
r.fit_iterations,
|
| 343 |
+
r.fit_final_lim,
|
| 344 |
+
r.fit_final_lim_p95,
|
| 345 |
+
r.fit_seed,
|
| 346 |
+
c.excess_kurtosis,
|
| 347 |
+
{erf_select}
|
| 348 |
+
a.positive_label,
|
| 349 |
+
a.positive_confidence,
|
| 350 |
+
a.negative_label,
|
| 351 |
+
a.negative_confidence,
|
| 352 |
+
a.positive_interpretation_types_json,
|
| 353 |
+
a.negative_interpretation_types_json,
|
| 354 |
+
a.summary,
|
| 355 |
+
a.notes,
|
| 356 |
+
a.include_as_case_study
|
| 357 |
+
FROM chosen_random_components r
|
| 358 |
+
LEFT JOIN components c
|
| 359 |
+
ON c.model_name = r.model_name
|
| 360 |
+
AND c.layer = r.layer
|
| 361 |
+
AND c.component = r.component
|
| 362 |
+
LEFT JOIN annotations a
|
| 363 |
+
ON a.model_name = r.model_name
|
| 364 |
+
AND a.layer = r.layer
|
| 365 |
+
AND a.component = r.component
|
| 366 |
+
{erf_join}
|
| 367 |
+
WHERE (? IS NULL OR r.model_name = ?)
|
| 368 |
+
AND (? IS NULL OR r.selection_name = ?)
|
| 369 |
+
ORDER BY r.model_name, r.selection_name, r.selection_index
|
| 370 |
+
""",
|
| 371 |
+
(model_name, model_name, selection_name, selection_name),
|
| 372 |
+
).fetchall()
|
| 373 |
+
out = []
|
| 374 |
+
for row in rows:
|
| 375 |
+
item = _annotation_row(row)
|
| 376 |
+
item.update(
|
| 377 |
+
{
|
| 378 |
+
"selection_name": str(row["selection_name"]),
|
| 379 |
+
"model_name": str(row["model_name"]),
|
| 380 |
+
"selection_index": int(row["selection_index"]),
|
| 381 |
+
"layer": str(row["layer"]),
|
| 382 |
+
"component": int(row["component"]),
|
| 383 |
+
"component_id": str(row["component_id"] or ""),
|
| 384 |
+
"source_json": str(row["source_json"] or ""),
|
| 385 |
+
"seed": int(row["seed"]) if row["seed"] is not None else None,
|
| 386 |
+
"requested_n": int(row["requested_n"]) if row["requested_n"] is not None else None,
|
| 387 |
+
"inventory_size": int(row["inventory_size"]) if row["inventory_size"] is not None else None,
|
| 388 |
+
"selected_size": int(row["selected_size"]) if row["selected_size"] is not None else None,
|
| 389 |
+
"fit_converged": bool(row["fit_converged"]) if row["fit_converged"] is not None else None,
|
| 390 |
+
"fit_iterations": int(row["fit_iterations"]) if row["fit_iterations"] is not None else None,
|
| 391 |
+
"fit_final_lim": float(row["fit_final_lim"]) if row["fit_final_lim"] is not None else None,
|
| 392 |
+
"fit_final_lim_p95": float(row["fit_final_lim_p95"]) if row["fit_final_lim_p95"] is not None else None,
|
| 393 |
+
"fit_seed": int(row["fit_seed"]) if row["fit_seed"] is not None else None,
|
| 394 |
+
"effective_context_mean": float(row["effective_context_mean"]) if row["effective_context_mean"] is not None else None,
|
| 395 |
+
}
|
| 396 |
+
)
|
| 397 |
+
out.append(item)
|
| 398 |
+
return out
|
| 399 |
+
|
| 400 |
+
|
| 401 |
+
def list_component_examples(conn: sqlite3.Connection, model_name: str, layer: str | None = None, component: int | None = None, *, region: str | None = None, limit: int | None = None) -> dict[tuple[str, int], list[dict[str, Any]]]:
|
| 402 |
+
rows = conn.execute(
|
| 403 |
+
"""
|
| 404 |
+
SELECT layer,
|
| 405 |
+
component,
|
| 406 |
+
region,
|
| 407 |
+
rank,
|
| 408 |
+
row_index,
|
| 409 |
+
doc_id,
|
| 410 |
+
token_id,
|
| 411 |
+
token,
|
| 412 |
+
source_score,
|
| 413 |
+
direction_cosine,
|
| 414 |
+
position,
|
| 415 |
+
context_to_target,
|
| 416 |
+
context,
|
| 417 |
+
context_score_max_abs
|
| 418 |
+
FROM examples
|
| 419 |
+
WHERE model_name = ?
|
| 420 |
+
AND (? IS NULL OR layer = ?)
|
| 421 |
+
AND (? IS NULL OR component = ?)
|
| 422 |
+
AND (? IS NULL OR region = ?)
|
| 423 |
+
AND (? IS NULL OR rank <= ?)
|
| 424 |
+
ORDER BY layer, component, region, rank
|
| 425 |
+
""",
|
| 426 |
+
(model_name, layer, layer, component, component, region, region, limit, limit),
|
| 427 |
+
).fetchall()
|
| 428 |
+
examples: dict[tuple[str, int], list[dict[str, Any]]] = {}
|
| 429 |
+
for row in rows:
|
| 430 |
+
key = (str(row["layer"]), int(row["component"]))
|
| 431 |
+
examples.setdefault(key, []).append(_example_row(row))
|
| 432 |
+
return examples
|
| 433 |
+
|
| 434 |
+
|
| 435 |
+
def list_component_example_details(conn: sqlite3.Connection, model_name: str, *, layer: str, component: int) -> list[dict[str, Any]]:
|
| 436 |
+
rows = conn.execute(
|
| 437 |
+
"""
|
| 438 |
+
SELECT id,
|
| 439 |
+
region,
|
| 440 |
+
rank,
|
| 441 |
+
row_index,
|
| 442 |
+
doc_id,
|
| 443 |
+
token_id,
|
| 444 |
+
token,
|
| 445 |
+
source_score,
|
| 446 |
+
direction_cosine,
|
| 447 |
+
position,
|
| 448 |
+
context_to_target,
|
| 449 |
+
context,
|
| 450 |
+
context_score_max_abs
|
| 451 |
+
FROM examples
|
| 452 |
+
WHERE model_name = ? AND layer = ? AND component = ?
|
| 453 |
+
ORDER BY region, rank
|
| 454 |
+
""",
|
| 455 |
+
(model_name, layer, component),
|
| 456 |
+
).fetchall()
|
| 457 |
+
examples = []
|
| 458 |
+
by_id: dict[int, dict[str, Any]] = {}
|
| 459 |
+
for row in rows:
|
| 460 |
+
example = _example_row(row)
|
| 461 |
+
example["context_token_scores"] = []
|
| 462 |
+
examples.append(example)
|
| 463 |
+
by_id[int(row["id"])] = example
|
| 464 |
+
for ids in _chunks(list(by_id), 500):
|
| 465 |
+
if not ids:
|
| 466 |
+
continue
|
| 467 |
+
placeholders = ",".join("?" for _ in ids)
|
| 468 |
+
token_rows = conn.execute(
|
| 469 |
+
f"""
|
| 470 |
+
SELECT example_id, seq, token_position, token, source_score, direction_cosine, is_target
|
| 471 |
+
FROM context_tokens
|
| 472 |
+
WHERE example_id IN ({placeholders})
|
| 473 |
+
ORDER BY example_id, seq
|
| 474 |
+
""",
|
| 475 |
+
tuple(ids),
|
| 476 |
+
).fetchall()
|
| 477 |
+
for token_row in token_rows:
|
| 478 |
+
example = by_id.get(int(token_row["example_id"]))
|
| 479 |
+
if example is None:
|
| 480 |
+
continue
|
| 481 |
+
example["context_token_scores"].append(
|
| 482 |
+
{
|
| 483 |
+
"position": int(token_row["token_position"]) if token_row["token_position"] is not None else None,
|
| 484 |
+
"token": str(token_row["token"] or ""),
|
| 485 |
+
"source_score": float(token_row["source_score"]) if token_row["source_score"] is not None else None,
|
| 486 |
+
"direction_cosine": float(token_row["direction_cosine"]) if token_row["direction_cosine"] is not None else None,
|
| 487 |
+
"is_target": bool(token_row["is_target"]),
|
| 488 |
+
}
|
| 489 |
+
)
|
| 490 |
+
return examples
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
def get_component_row(conn: sqlite3.Connection, model_name: str, layer: str, component: int) -> dict[str, Any] | None:
|
| 494 |
+
rows = list_components(conn, model_name, layer=layer, component=component)
|
| 495 |
+
return rows[0] if rows else None
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
def get_annotation(conn: sqlite3.Connection, model_name: str, layer: str, component: int) -> dict[str, Any] | None:
|
| 499 |
+
row = conn.execute(
|
| 500 |
+
"""
|
| 501 |
+
SELECT * FROM annotations
|
| 502 |
+
WHERE model_name = ? AND layer = ? AND component = ?
|
| 503 |
+
""",
|
| 504 |
+
(model_name, layer, component),
|
| 505 |
+
).fetchone()
|
| 506 |
+
return dict(row) if row else None
|
| 507 |
+
|
| 508 |
+
|
| 509 |
+
def update_annotation(conn: sqlite3.Connection, *, model_name: str, layer: str, component: int, positive_label: str, positive_confidence: str, positive_interpretation_types: list[str], negative_label: str, negative_confidence: str, negative_interpretation_types: list[str], summary: str, notes: str, include_as_case_study: bool) -> None:
|
| 510 |
+
conn.execute(
|
| 511 |
+
"""
|
| 512 |
+
INSERT INTO annotations(
|
| 513 |
+
model_name, layer, component,
|
| 514 |
+
positive_label, positive_confidence, positive_interpretation_types_json,
|
| 515 |
+
negative_label, negative_confidence, negative_interpretation_types_json,
|
| 516 |
+
summary, notes, include_as_case_study, updated_at
|
| 517 |
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'))
|
| 518 |
+
ON CONFLICT(model_name, layer, component) DO UPDATE SET
|
| 519 |
+
positive_label = excluded.positive_label,
|
| 520 |
+
positive_confidence = excluded.positive_confidence,
|
| 521 |
+
positive_interpretation_types_json = excluded.positive_interpretation_types_json,
|
| 522 |
+
negative_label = excluded.negative_label,
|
| 523 |
+
negative_confidence = excluded.negative_confidence,
|
| 524 |
+
negative_interpretation_types_json = excluded.negative_interpretation_types_json,
|
| 525 |
+
summary = excluded.summary,
|
| 526 |
+
notes = excluded.notes,
|
| 527 |
+
include_as_case_study = excluded.include_as_case_study,
|
| 528 |
+
updated_at = datetime('now')
|
| 529 |
+
""",
|
| 530 |
+
(
|
| 531 |
+
model_name,
|
| 532 |
+
layer,
|
| 533 |
+
int(component),
|
| 534 |
+
positive_label,
|
| 535 |
+
_normalize_confidence(positive_confidence),
|
| 536 |
+
json.dumps([str(item) for item in positive_interpretation_types if str(item)]),
|
| 537 |
+
negative_label,
|
| 538 |
+
_normalize_confidence(negative_confidence),
|
| 539 |
+
json.dumps([str(item) for item in negative_interpretation_types if str(item)]),
|
| 540 |
+
summary,
|
| 541 |
+
notes,
|
| 542 |
+
1 if include_as_case_study else 0,
|
| 543 |
+
),
|
| 544 |
+
)
|
| 545 |
+
conn.commit()
|
| 546 |
+
|
| 547 |
+
|
| 548 |
+
def infer_default_annotation_sign(conn: sqlite3.Connection, model_name: str, layer: str, component: int) -> int:
|
| 549 |
+
row = conn.execute(
|
| 550 |
+
"""
|
| 551 |
+
SELECT source_score
|
| 552 |
+
FROM examples
|
| 553 |
+
WHERE model_name = ? AND layer = ? AND component = ?
|
| 554 |
+
AND region IN ('top_abs', 'top_abs_sample_500', 'top_abs_sample_5000')
|
| 555 |
+
ORDER BY ABS(source_score) DESC, rank ASC
|
| 556 |
+
LIMIT 1
|
| 557 |
+
""",
|
| 558 |
+
(model_name, layer, component),
|
| 559 |
+
).fetchone()
|
| 560 |
+
if row is None or row["source_score"] is None:
|
| 561 |
+
return 1
|
| 562 |
+
return 1 if float(row["source_score"]) >= 0 else -1
|
| 563 |
+
|
| 564 |
+
|
| 565 |
+
def get_examples_by_region(conn: sqlite3.Connection, model_name: str, layer: str, component: int) -> tuple[list[str], dict[str, list[dict[str, Any]]]]:
|
| 566 |
+
examples = list_component_example_details(conn, model_name, layer=layer, component=component)
|
| 567 |
+
by_region: dict[str, list[dict[str, Any]]] = {}
|
| 568 |
+
for example in examples:
|
| 569 |
+
by_region.setdefault(str(example["region"] or "examples"), []).append(example)
|
| 570 |
+
regions = sorted(by_region, key=_example_band_sort_key)
|
| 571 |
+
return regions, by_region
|
| 572 |
+
|
| 573 |
+
|
| 574 |
+
def pick_default_region(regions: list[str], examples_by_region: dict[str, list[dict[str, Any]]]) -> str:
|
| 575 |
+
for candidate in ("top_abs", "top_abs_sample_500", "top_abs_sample_5000"):
|
| 576 |
+
if candidate in examples_by_region:
|
| 577 |
+
return candidate
|
| 578 |
+
return regions[0] if regions else ""
|
| 579 |
+
|
| 580 |
+
|
| 581 |
+
def search_components(conn: sqlite3.Connection, *, model_name: str | None, query: str, confidence: str, annotation_type: str, include_examples: bool, limit: int) -> list[dict[str, Any]]:
|
| 582 |
+
models = [model_name] if model_name else list_models(conn)
|
| 583 |
+
results = []
|
| 584 |
+
query_l = query.strip().lower()
|
| 585 |
+
for model in models:
|
| 586 |
+
for item in list_components(conn, model):
|
| 587 |
+
labels = [item.get("positive_label", ""), item.get("negative_label", ""), item.get("summary", ""), item.get("notes", "")]
|
| 588 |
+
haystack = " ".join(str(x) for x in labels).lower()
|
| 589 |
+
if query_l and query_l not in haystack:
|
| 590 |
+
continue
|
| 591 |
+
if confidence and confidence not in {item.get("positive_confidence"), item.get("negative_confidence")}:
|
| 592 |
+
continue
|
| 593 |
+
if annotation_type and annotation_type not in set(item.get("positive_types", []) + item.get("negative_types", [])):
|
| 594 |
+
continue
|
| 595 |
+
row = {"model_name": model, **item}
|
| 596 |
+
if include_examples:
|
| 597 |
+
ex = list_component_examples(conn, model, layer=item["layer"], component=item["component"], limit=3).get((item["layer"], item["component"]), [])
|
| 598 |
+
row["examples"] = ex
|
| 599 |
+
results.append(row)
|
| 600 |
+
if len(results) >= limit:
|
| 601 |
+
return results
|
| 602 |
+
return results
|
| 603 |
+
|
| 604 |
+
|
| 605 |
+
def layer_sort_key(layer: str) -> tuple[int, int | str]:
|
| 606 |
+
if layer == "embedding":
|
| 607 |
+
return (0, 0)
|
| 608 |
+
if layer.startswith("layer_"):
|
| 609 |
+
return (1, int(layer.removeprefix("layer_")))
|
| 610 |
+
return (2, layer)
|
| 611 |
+
|
| 612 |
+
|
| 613 |
+
def _erf_join_sql(conn: sqlite3.Connection) -> tuple[str, str]:
|
| 614 |
+
table = _first_existing_table(conn, ["effective_receptive_fields", "effective_context_lengths"])
|
| 615 |
+
if table is None:
|
| 616 |
+
return "", "NULL AS effective_context_mean,"
|
| 617 |
+
columns = _table_columns(conn, table)
|
| 618 |
+
value_column = _first_existing_name(columns, ["mean_erf", "erf_mean", "mean_length", "effective_receptive_field_mean", "effective_context_mean"])
|
| 619 |
+
if value_column is None:
|
| 620 |
+
return "", "NULL AS effective_context_mean,"
|
| 621 |
+
quoted_table = _quote_identifier(table)
|
| 622 |
+
quoted_value = _quote_identifier(value_column)
|
| 623 |
+
return (
|
| 624 |
+
f"""
|
| 625 |
+
LEFT JOIN {quoted_table} erf
|
| 626 |
+
ON erf.model_name = c.model_name
|
| 627 |
+
AND erf.layer = c.layer
|
| 628 |
+
AND erf.component = c.component
|
| 629 |
+
""",
|
| 630 |
+
f"erf.{quoted_value} AS effective_context_mean,",
|
| 631 |
+
)
|
| 632 |
+
|
| 633 |
+
|
| 634 |
+
def _component_neighbor_erf_join_sql(conn: sqlite3.Connection) -> tuple[str, str]:
|
| 635 |
+
table = _first_existing_table(conn, ["effective_receptive_fields", "effective_context_lengths"])
|
| 636 |
+
if table is None:
|
| 637 |
+
return "", "NULL AS effective_context_mean,"
|
| 638 |
+
columns = _table_columns(conn, table)
|
| 639 |
+
value_column = _first_existing_name(columns, ["mean_erf", "erf_mean", "mean_length", "effective_receptive_field_mean", "effective_context_mean"])
|
| 640 |
+
if value_column is None:
|
| 641 |
+
return "", "NULL AS effective_context_mean,"
|
| 642 |
+
quoted_table = _quote_identifier(table)
|
| 643 |
+
quoted_value = _quote_identifier(value_column)
|
| 644 |
+
return (
|
| 645 |
+
f"""
|
| 646 |
+
LEFT JOIN {quoted_table} erf
|
| 647 |
+
ON erf.model_name = n.model_name
|
| 648 |
+
AND erf.layer = n.neighbor_layer
|
| 649 |
+
AND erf.component = n.neighbor_component
|
| 650 |
+
""",
|
| 651 |
+
f"erf.{quoted_value} AS effective_context_mean,",
|
| 652 |
+
)
|
| 653 |
+
|
| 654 |
+
|
| 655 |
+
def _first_existing_table(conn: sqlite3.Connection, names: list[str]) -> str | None:
|
| 656 |
+
for name in names:
|
| 657 |
+
if _table_exists(conn, name):
|
| 658 |
+
return name
|
| 659 |
+
return None
|
| 660 |
+
|
| 661 |
+
|
| 662 |
+
def _table_columns(conn: sqlite3.Connection, table_name: str) -> set[str]:
|
| 663 |
+
return {str(row["name"]) for row in conn.execute(f"PRAGMA table_info({_quote_identifier(table_name)})").fetchall()}
|
| 664 |
+
|
| 665 |
+
|
| 666 |
+
def _first_existing_name(names: set[str], candidates: list[str]) -> str | None:
|
| 667 |
+
for candidate in candidates:
|
| 668 |
+
if candidate in names:
|
| 669 |
+
return candidate
|
| 670 |
+
return None
|
| 671 |
+
|
| 672 |
+
|
| 673 |
+
def _quote_identifier(name: str) -> str:
|
| 674 |
+
return '"' + str(name).replace('"', '""') + '"'
|
| 675 |
+
|
| 676 |
+
|
| 677 |
+
def _table_exists(conn: sqlite3.Connection, table_name: str) -> bool:
|
| 678 |
+
return conn.execute("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?", (table_name,)).fetchone() is not None
|
| 679 |
+
|
| 680 |
+
|
| 681 |
+
def _ensure_annotation_columns(conn: sqlite3.Connection) -> None:
|
| 682 |
+
columns = {str(row["name"]) for row in conn.execute("PRAGMA table_info(annotations)").fetchall()}
|
| 683 |
+
required = {
|
| 684 |
+
"label": "TEXT",
|
| 685 |
+
"confidence": "TEXT",
|
| 686 |
+
"positive_label": "TEXT",
|
| 687 |
+
"positive_confidence": "TEXT",
|
| 688 |
+
"negative_label": "TEXT",
|
| 689 |
+
"negative_confidence": "TEXT",
|
| 690 |
+
"positive_interpretation_types_json": "TEXT",
|
| 691 |
+
"negative_interpretation_types_json": "TEXT",
|
| 692 |
+
"interpretation_types_json": "TEXT",
|
| 693 |
+
"summary": "TEXT",
|
| 694 |
+
"notes": "TEXT",
|
| 695 |
+
"include_as_case_study": "INTEGER NOT NULL DEFAULT 0",
|
| 696 |
+
"updated_at": "TEXT",
|
| 697 |
+
}
|
| 698 |
+
for column, ddl in required.items():
|
| 699 |
+
if column not in columns:
|
| 700 |
+
conn.execute(f"ALTER TABLE annotations ADD COLUMN {column} {ddl}")
|
| 701 |
+
|
| 702 |
+
|
| 703 |
+
def _annotation_row(row: sqlite3.Row | dict[str, Any]) -> dict[str, Any]:
|
| 704 |
+
keys = row.keys() if hasattr(row, "keys") else row
|
| 705 |
+
def get(key: str, default: Any = None) -> Any:
|
| 706 |
+
return row[key] if key in keys else default
|
| 707 |
+
positive_label = str(get("positive_label") or "")
|
| 708 |
+
negative_label = str(get("negative_label") or "")
|
| 709 |
+
auto_annotated = "auto-annotation" in str(get("notes") or "").lower()
|
| 710 |
+
return {
|
| 711 |
+
"component": int(get("component")),
|
| 712 |
+
"positive_label": positive_label,
|
| 713 |
+
"positive_confidence": _normalize_confidence(get("positive_confidence")),
|
| 714 |
+
"negative_label": negative_label,
|
| 715 |
+
"negative_confidence": _normalize_confidence(get("negative_confidence")),
|
| 716 |
+
"positive_types": _json_list(get("positive_interpretation_types_json")),
|
| 717 |
+
"negative_types": _json_list(get("negative_interpretation_types_json")),
|
| 718 |
+
"summary": str(get("summary") or ""),
|
| 719 |
+
"notes": str(get("notes") or ""),
|
| 720 |
+
"include_as_case_study": bool(get("include_as_case_study") or 0),
|
| 721 |
+
"excess_kurtosis": float(get("excess_kurtosis")) if get("excess_kurtosis") is not None else None,
|
| 722 |
+
"auto_annotated": auto_annotated,
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
|
| 726 |
+
def _example_row(row: sqlite3.Row) -> dict[str, Any]:
|
| 727 |
+
return {
|
| 728 |
+
"region": str(row["region"] or ""),
|
| 729 |
+
"rank": int(row["rank"]),
|
| 730 |
+
"row_index": int(row["row_index"]) if "row_index" in row.keys() and row["row_index"] is not None else None,
|
| 731 |
+
"doc_id": int(row["doc_id"]) if "doc_id" in row.keys() and row["doc_id"] is not None else None,
|
| 732 |
+
"token_id": int(row["token_id"]) if "token_id" in row.keys() and row["token_id"] is not None else None,
|
| 733 |
+
"token": str(row["token"] or ""),
|
| 734 |
+
"source_score": float(row["source_score"]) if row["source_score"] is not None else None,
|
| 735 |
+
"direction_cosine": float(row["direction_cosine"]) if row["direction_cosine"] is not None else None,
|
| 736 |
+
"position": int(row["position"]) if row["position"] is not None else None,
|
| 737 |
+
"context_to_target": str(row["context_to_target"] or ""),
|
| 738 |
+
"context": str(row["context"] or ""),
|
| 739 |
+
"context_score_max_abs": float(row["context_score_max_abs"]) if row["context_score_max_abs"] is not None else None,
|
| 740 |
+
}
|
| 741 |
+
|
| 742 |
+
|
| 743 |
+
def _json_list(value: Any) -> list[str]:
|
| 744 |
+
try:
|
| 745 |
+
parsed = json.loads(value or "[]")
|
| 746 |
+
except json.JSONDecodeError:
|
| 747 |
+
return []
|
| 748 |
+
return [str(item) for item in parsed] if isinstance(parsed, list) else []
|
| 749 |
+
|
| 750 |
+
|
| 751 |
+
def _normalize_confidence(value: Any) -> str:
|
| 752 |
+
text = str(value or "unclear").strip().lower()
|
| 753 |
+
return text if text in {"high", "medium", "low", "unclear"} else "unclear"
|
| 754 |
+
|
| 755 |
+
|
| 756 |
+
def _chunks(items: list[int], size: int) -> Iterable[list[int]]:
|
| 757 |
+
for index in range(0, len(items), size):
|
| 758 |
+
yield items[index : index + size]
|
| 759 |
+
|
| 760 |
+
|
| 761 |
+
def _example_band_sort_key(region: str) -> tuple[int, str]:
|
| 762 |
+
name = str(region or "").lower().replace("-", "_")
|
| 763 |
+
compact = name.replace("_", "")
|
| 764 |
+
if name == "top_abs" or compact == "topabs":
|
| 765 |
+
return (0, name)
|
| 766 |
+
if "sample" in name:
|
| 767 |
+
return (1, name)
|
| 768 |
+
if "near_zero" in name or "nearzero" in compact:
|
| 769 |
+
return (2, name)
|
| 770 |
+
if "opposite" in name:
|
| 771 |
+
return (3, name)
|
| 772 |
+
return (4, name)
|