github-actions[bot] commited on
Commit
f45a0cf
·
1 Parent(s): 23680f2

Deploy hyper3labs/HyperView from Hyper3Labs/hyperview-spaces@f1c0a76

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +3 -53
  2. .gitattributes +0 -35
  3. .github/workflows/devin-review.yml +0 -23
  4. .github/workflows/require_frontend_export.yml +0 -53
  5. .gitignore +0 -74
  6. Dockerfile +11 -87
  7. LICENSE +0 -21
  8. README.md +18 -137
  9. app_hf.py +0 -49
  10. demo.py +140 -0
  11. docs/architecture.md +0 -54
  12. docs/colab.md +0 -37
  13. docs/datasets.md +0 -96
  14. docs/index.html +0 -465
  15. frontend/components.json +0 -22
  16. frontend/eslint.config.mjs +0 -22
  17. frontend/next-env.d.ts +0 -6
  18. frontend/next.config.ts +0 -24
  19. frontend/package-lock.json +0 -0
  20. frontend/package.json +0 -51
  21. frontend/postcss.config.mjs +0 -9
  22. frontend/src/app/globals.css +0 -224
  23. frontend/src/app/layout.tsx +0 -26
  24. frontend/src/app/page.tsx +0 -277
  25. frontend/src/components/DockviewWorkspace.tsx +0 -765
  26. frontend/src/components/ExplorerPanel.tsx +0 -181
  27. frontend/src/components/Header.tsx +0 -244
  28. frontend/src/components/ImageGrid.tsx +0 -338
  29. frontend/src/components/Panel.tsx +0 -43
  30. frontend/src/components/PanelHeader.tsx +0 -47
  31. frontend/src/components/PlaceholderPanel.tsx +0 -39
  32. frontend/src/components/ScatterPanel.tsx +0 -174
  33. frontend/src/components/icons.tsx +0 -73
  34. frontend/src/components/index.ts +0 -9
  35. frontend/src/components/ui/button.tsx +0 -57
  36. frontend/src/components/ui/collapsible.tsx +0 -11
  37. frontend/src/components/ui/command.tsx +0 -153
  38. frontend/src/components/ui/dialog.tsx +0 -122
  39. frontend/src/components/ui/dropdown-menu.tsx +0 -201
  40. frontend/src/components/ui/popover.tsx +0 -33
  41. frontend/src/components/ui/radio-group.tsx +0 -44
  42. frontend/src/components/ui/scroll-area.tsx +0 -49
  43. frontend/src/components/ui/separator.tsx +0 -31
  44. frontend/src/components/ui/toggle-group.tsx +0 -61
  45. frontend/src/components/ui/toggle.tsx +0 -45
  46. frontend/src/components/ui/tooltip.tsx +0 -32
  47. frontend/src/components/useHyperScatter.ts +0 -614
  48. frontend/src/components/useLabelLegend.ts +0 -85
  49. frontend/src/lib/api.ts +0 -103
  50. frontend/src/lib/labelColors.ts +0 -153
.dockerignore CHANGED
@@ -1,65 +1,15 @@
1
  # Git
2
  .git
3
- .gitignore
4
 
5
  # Python
6
  __pycache__
7
  *.py[cod]
8
- *$py.class
9
- *.so
10
- .Python
11
  .venv
12
  venv
13
- env
14
- .env
15
- *.egg-info
16
- dist
17
- build
18
- .eggs
19
 
20
- # Node
21
- node_modules
22
- .npm
23
- .pnpm-store
24
-
25
- # IDE
26
- .idea
27
- .vscode
28
- *.swp
29
- *.swo
30
-
31
- # Testing
32
  .pytest_cache
33
- .coverage
34
- htmlcov
35
- .tox
36
-
37
- # Documentation (not needed in image)
38
- docs
39
 
40
- # Development files
41
- *.log
42
  .DS_Store
43
- Thumbs.db
44
-
45
- # Notebooks (not needed for deployment)
46
- notebooks
47
- *.ipynb
48
-
49
- # POC code
50
- poc
51
-
52
- # Local data
53
- *.lancedb
54
- data/
55
-
56
- # Frontend build output (we build fresh)
57
- frontend/out
58
- frontend/.next
59
- frontend/node_modules
60
-
61
- # hyper-scatter (built separately if present)
62
- hyper-scatter
63
-
64
- # Assets (README images)
65
- assets
 
1
  # Git
2
  .git
 
3
 
4
  # Python
5
  __pycache__
6
  *.py[cod]
 
 
 
7
  .venv
8
  venv
 
 
 
 
 
 
9
 
10
+ # Caches
11
+ .mypy_cache
 
 
 
 
 
 
 
 
 
 
12
  .pytest_cache
 
 
 
 
 
 
13
 
14
+ # Misc
 
15
  .DS_Store
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/workflows/devin-review.yml DELETED
@@ -1,23 +0,0 @@
1
- name: Devin Review
2
-
3
- on:
4
- pull_request:
5
- types: [opened, synchronize, reopened]
6
-
7
- jobs:
8
- devin-review:
9
- runs-on: ubuntu-latest
10
-
11
- steps:
12
- - name: Checkout repository
13
- uses: actions/checkout@v4
14
- with:
15
- fetch-depth: 0
16
-
17
- - name: Setup Node.js
18
- uses: actions/setup-node@v4
19
- with:
20
- node-version: '20'
21
-
22
- - name: Run Devin Review
23
- run: npx devin-review ${{ github.event.pull_request.html_url }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.github/workflows/require_frontend_export.yml DELETED
@@ -1,53 +0,0 @@
1
- name: Require Frontend Export
2
-
3
- on:
4
- pull_request:
5
- types: [opened, synchronize, reopened]
6
- paths:
7
- - "frontend/**"
8
- - "scripts/export_frontend.sh"
9
- - "src/hyperview/server/static/**"
10
-
11
- jobs:
12
- require-frontend-export:
13
- runs-on: ubuntu-latest
14
-
15
- steps:
16
- - name: Checkout repository
17
- uses: actions/checkout@v4
18
- with:
19
- fetch-depth: 0
20
- ref: ${{ github.event.pull_request.head.sha }}
21
-
22
- - name: Verify static export updated when frontend changes
23
- run: |
24
- set -euo pipefail
25
-
26
- base_sha="${{ github.event.pull_request.base.sha }}"
27
- head_sha="${{ github.event.pull_request.head.sha }}"
28
-
29
- changed_files="$(git diff --name-only "$base_sha" "$head_sha")"
30
-
31
- echo "Changed files:"
32
- echo "$changed_files"
33
-
34
- frontend_changed="false"
35
- static_changed="false"
36
-
37
- if echo "$changed_files" | grep -qE '^(frontend/|scripts/export_frontend\.sh$)'; then
38
- frontend_changed="true"
39
- fi
40
-
41
- if echo "$changed_files" | grep -q '^src/hyperview/server/static/'; then
42
- static_changed="true"
43
- fi
44
-
45
- if [[ "$frontend_changed" == "true" && "$static_changed" != "true" ]]; then
46
- echo ""
47
- echo "ERROR: frontend/ changed but src/hyperview/server/static/ was not updated."
48
- echo "Run: bash scripts/export_frontend.sh"
49
- echo "Then commit the updated src/hyperview/server/static/ output."
50
- exit 1
51
- fi
52
-
53
- echo "OK: export requirements satisfied."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore DELETED
@@ -1,74 +0,0 @@
1
- # Python
2
- __pycache__/
3
- *.pyc
4
- *.pyo
5
- *.pyd
6
- .Python
7
- env/
8
- venv/
9
- .venv/
10
- /uv.lock
11
- *.egg-info/
12
- .pytest_cache/
13
- .coverage
14
- htmlcov/
15
-
16
- # Jupyter Notebooks
17
- .ipynb_checkpoints
18
-
19
- # macOS
20
- .DS_Store
21
-
22
- # VS Code
23
- .vscode/
24
-
25
- # Generated assets
26
- assets/demo_animation_frames/
27
- *.gif
28
-
29
- # Frontend
30
- frontend/node_modules/
31
- frontend/.next/
32
- frontend/out/
33
-
34
- # Bundled frontend in Python package (built with scripts/export_frontend.sh)
35
- # Not ignored - needed for pip install from git / sdist
36
- # src/hyperview/server/static/
37
-
38
- # Python package build
39
- dist/
40
- build/
41
- *.egg-info/
42
-
43
- # Data cache
44
- *.hf/
45
- .cache/
46
-
47
- # external repo (https://github.com/Hyper3Labs/hyper-scatter)
48
- hyper-scatter/
49
-
50
- # nohup
51
- nohup.out
52
- frontend/nohup.out
53
-
54
- # Local logs / tool artifacts
55
- .hyperview-*.log
56
- .hyperview-*.pid
57
- .playwright-mcp/
58
- frontend/tsconfig.tsbuildinfo
59
-
60
- # Hyperbolic model zoo (kept as a separate repo)
61
- hyper_model_zoo/
62
- hyper_models/
63
- scripts_ignored/
64
-
65
- # AI Context (Agent files)
66
- .claude/
67
- context/
68
- CLAUDE.md
69
- TASKS.md
70
- TESTS.md
71
- AGENTS.md
72
- **/AGENTS.md
73
- .github/agents/
74
- .specstory/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile CHANGED
@@ -1,46 +1,5 @@
1
- # =============================================================================
2
- # HyperView - Hugging Face Spaces Dockerfile
3
- # =============================================================================
4
- # Multi-stage build for deploying HyperView to HuggingFace Spaces.
5
- #
6
- # Features:
7
- # - CLIP embeddings (Euclidean) via embed-anything
8
- # - HyCoCLIP embeddings (Hyperbolic) via hyper-models ONNX
9
- # - Pre-computed demo dataset (300 CIFAR-10 samples)
10
- # - Torch-free runtime for minimal image size
11
- #
12
- # Deploy: https://huggingface.co/spaces/Hyper3Labs/HyperView
13
- # =============================================================================
14
 
15
- # -----------------------------------------------------------------------------
16
- # Stage 1: Build Frontend (Next.js static export)
17
- # -----------------------------------------------------------------------------
18
- FROM node:20-slim AS frontend-builder
19
-
20
- WORKDIR /app/frontend
21
-
22
- # Install dependencies first (better caching)
23
- COPY frontend/package.json frontend/package-lock.json ./
24
- RUN npm ci --prefer-offline
25
-
26
- # Build hyper-scatter (installed from source tarball, dist-lib not prebuilt)
27
- RUN cd node_modules/hyper-scatter \
28
- && npm install \
29
- && npm run build:lib
30
-
31
- # Copy frontend source and build
32
- COPY frontend/ ./
33
- RUN npm run build
34
-
35
- # Verify output exists
36
- RUN ls -la out/ && echo "Frontend build complete"
37
-
38
- # -----------------------------------------------------------------------------
39
- # Stage 2: Python Runtime
40
- # -----------------------------------------------------------------------------
41
- FROM python:3.11-slim AS runtime
42
-
43
- # Install system dependencies
44
  RUN apt-get update && apt-get install -y --no-install-recommends \
45
  build-essential \
46
  curl \
@@ -49,13 +8,9 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
49
  pkg-config \
50
  && rm -rf /var/lib/apt/lists/*
51
 
52
- # HuggingFace Spaces requirement: create user with UID 1000
53
  RUN useradd -m -u 1000 user
54
-
55
- # Switch to user
56
  USER user
57
 
58
- # Set environment variables
59
  ENV HOME=/home/user \
60
  PATH=/home/user/.local/bin:$PATH \
61
  HF_HOME=/home/user/.cache/huggingface \
@@ -64,60 +19,29 @@ ENV HOME=/home/user \
64
 
65
  WORKDIR $HOME/app
66
 
67
- # Upgrade pip
68
  RUN pip install --upgrade pip
69
 
70
- # Copy Python package files
71
- COPY --chown=user pyproject.toml README.md LICENSE ./
72
- COPY --chown=user src/ ./src/
73
- COPY --chown=user scripts/ ./scripts/
74
-
75
- # Install Python package (without ML extras - we use ONNX)
76
- RUN pip install -e .
77
 
78
- # Copy built frontend to static directory
79
- COPY --from=frontend-builder --chown=user /app/frontend/out ./src/hyperview/server/static/
80
 
81
- # Verify frontend is in place
82
- RUN ls -la src/hyperview/server/static/ && echo "Frontend copied successfully"
83
 
84
- # -----------------------------------------------------------------------------
85
- # Stage 3: Pre-compute Demo Dataset
86
- # -----------------------------------------------------------------------------
87
- # Create output directories
88
- RUN mkdir -p $HOME/app/demo_data/datasets $HOME/app/demo_data/media
89
-
90
- # Set environment for precomputation
91
  ENV HYPERVIEW_DATASETS_DIR=/home/user/app/demo_data/datasets \
92
  HYPERVIEW_MEDIA_DIR=/home/user/app/demo_data/media \
93
- DEMO_SAMPLES=300
94
-
95
- # Pre-download HuggingFace models and compute embeddings
96
- # This runs during build to ensure fast startup
97
- RUN python scripts/precompute_hf_demo.py
98
-
99
- # Verify dataset was created
100
- RUN ls -la demo_data/ && echo "Demo dataset pre-computed successfully"
101
 
102
- # -----------------------------------------------------------------------------
103
- # Final Configuration
104
- # -----------------------------------------------------------------------------
105
- # Copy entrypoint
106
- COPY --chown=user app_hf.py ./
107
 
108
- # Set runtime environment
109
  ENV HOST=0.0.0.0 \
110
- PORT=7860 \
111
- DEMO_DATASET=cifar10_hf_demo \
112
- HYPERVIEW_DATASETS_DIR=/home/user/app/demo_data/datasets \
113
- HYPERVIEW_MEDIA_DIR=/home/user/app/demo_data/media
114
 
115
- # Expose port (HuggingFace Spaces default)
116
  EXPOSE 7860
117
 
118
- # Health check
119
  HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
120
  CMD curl -f http://localhost:7860/__hyperview__/health || exit 1
121
 
122
- # Start server
123
- CMD ["python", "app_hf.py"]
 
1
+ FROM python:3.11-slim
 
 
 
 
 
 
 
 
 
 
 
 
2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  RUN apt-get update && apt-get install -y --no-install-recommends \
4
  build-essential \
5
  curl \
 
8
  pkg-config \
9
  && rm -rf /var/lib/apt/lists/*
10
 
 
11
  RUN useradd -m -u 1000 user
 
 
12
  USER user
13
 
 
14
  ENV HOME=/home/user \
15
  PATH=/home/user/.local/bin:$PATH \
16
  HF_HOME=/home/user/.cache/huggingface \
 
19
 
20
  WORKDIR $HOME/app
21
 
 
22
  RUN pip install --upgrade pip
23
 
24
+ # Install latest releases from PyPI.
25
+ # For reproducible builds, pin versions here:
26
+ # RUN pip install --upgrade hyperview==0.1.1 hyper-models==0.1.0
27
+ RUN pip install --upgrade hyperview hyper-models
 
 
 
28
 
29
+ COPY --chown=user demo.py ./demo.py
 
30
 
31
+ ARG DEMO_SAMPLES=300
 
32
 
 
 
 
 
 
 
 
33
  ENV HYPERVIEW_DATASETS_DIR=/home/user/app/demo_data/datasets \
34
  HYPERVIEW_MEDIA_DIR=/home/user/app/demo_data/media \
35
+ DEMO_SAMPLES=${DEMO_SAMPLES}
 
 
 
 
 
 
 
36
 
37
+ RUN python demo.py --precompute
 
 
 
 
38
 
 
39
  ENV HOST=0.0.0.0 \
40
+ PORT=7860
 
 
 
41
 
 
42
  EXPOSE 7860
43
 
 
44
  HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
45
  CMD curl -f http://localhost:7860/__hyperview__/health || exit 1
46
 
47
+ CMD ["python", "demo.py"]
 
LICENSE DELETED
@@ -1,21 +0,0 @@
1
- MIT License
2
-
3
- Copyright (c) 2025 Matin Mahmood
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -6,150 +6,31 @@ colorTo: blue
6
  sdk: docker
7
  app_port: 7860
8
  pinned: false
9
- tags:
10
- - data-visualization
11
- - embeddings
12
- - hyperbolic
13
- - poincare
14
- - clip
15
- - dataset-curation
16
- - computer-vision
17
- - umap
18
- short_description: Dataset visualization with Euclidean & hyperbolic embeddings
19
- models:
20
- - openai/clip-vit-base-patch32
21
- - mnm-matin/hyperbolic-clip
22
- datasets:
23
- - uoft-cs/cifar10
24
  ---
25
 
26
- # HyperView
27
 
28
- > **Open-source dataset curation + embedding visualization (Euclidean + Poincaré disk)**
29
 
30
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/Hyper3Labs/HyperView) [![Hugging Face Spaces](https://img.shields.io/badge/%F0%9F%A4%97%20Hugging%20Face-Spaces-blue)](https://huggingface.co/spaces/Hyper3Labs/HyperView)
 
31
 
32
- <p align="center">
33
- <a href="https://youtu.be/XLaa8FHSQtc" target="_blank">
34
- <img src="https://raw.githubusercontent.com/Hyper3Labs/HyperView/main/assets/screenshot.png" alt="HyperView Screenshot" width="100%">
35
- </a>
36
- <br>
37
- <a href="https://youtu.be/XLaa8FHSQtc" target="_blank">Watch the Demo Video</a>
38
- </p>
39
 
40
- ---
41
-
42
- ## Try it Online
43
-
44
- **[Launch HyperView on Hugging Face Spaces](https://huggingface.co/spaces/Hyper3Labs/HyperView)** - no installation required!
45
-
46
- The demo showcases:
47
- - 300 CIFAR-10 images with pre-computed embeddings
48
- - CLIP embeddings visualized in Euclidean space (UMAP)
49
- - HyCoCLIP embeddings visualized on the Poincaré disk
50
-
51
- ---
52
-
53
- ## Features
54
-
55
- - **Dual-Panel UI**: Image grid + scatter plot with bidirectional selection
56
- - **Euclidean/Poincaré Toggle**: Switch between standard 2D UMAP and Poincaré disk visualization
57
- - **HuggingFace Integration**: Load datasets directly from HuggingFace Hub
58
- - **Fast Embeddings**: Uses EmbedAnything for CLIP-based image embeddings
59
-
60
- ## Quick Start
61
-
62
- **Docs:** [docs/datasets.md](docs/datasets.md) · [docs/colab.md](docs/colab.md) · [CONTRIBUTING.md](CONTRIBUTING.md) · [TESTS.md](TESTS.md)
63
-
64
- ### Installation
65
-
66
- ```bash
67
- git clone https://github.com/Hyper3Labs/HyperView.git
68
- cd HyperView
69
-
70
- # Install with uv
71
- uv venv .venv
72
- source .venv/bin/activate
73
- uv pip install -e ".[dev]"
74
- ```
75
-
76
- ### Run the Demo
77
-
78
- ```bash
79
- hyperview demo --samples 500
80
- ```
81
-
82
- This will:
83
- 1. Load 500 samples from CIFAR-100
84
- 2. Compute CLIP embeddings
85
- 3. Generate Euclidean and Poincaré visualizations
86
- 4. Start the server at **http://127.0.0.1:6262**
87
-
88
- ### Python API
89
-
90
- ```python
91
- import hyperview as hv
92
-
93
- # Create dataset
94
- dataset = hv.Dataset("my_dataset")
95
-
96
- # Load from HuggingFace
97
- dataset.add_from_huggingface(
98
- "uoft-cs/cifar100",
99
- split="train",
100
- max_samples=1000
101
- )
102
-
103
- # Or load from local directory
104
- # dataset.add_images_dir("/path/to/images", label_from_folder=True)
105
-
106
- # Compute embeddings and visualization
107
- dataset.compute_embeddings()
108
- dataset.compute_visualization()
109
-
110
- # Launch the UI
111
- hv.launch(dataset) # Opens http://127.0.0.1:6262
112
- ```
113
-
114
- ### Google Colab
115
-
116
- See [docs/colab.md](docs/colab.md) for a fast Colab smoke test and notebook-friendly launch behavior.
117
-
118
- ### Save and Load Datasets
119
-
120
- ```python
121
- # Save dataset with embeddings
122
- dataset.save("my_dataset.json")
123
-
124
- # Load later
125
- dataset = hv.Dataset.load("my_dataset.json")
126
- hv.launch(dataset)
127
- ```
128
-
129
- ## Why Hyperbolic?
130
-
131
- Traditional Euclidean embeddings struggle with hierarchical data. In Euclidean space, volume grows polynomially ($r^d$), causing **Representation Collapse** where minority classes get crushed together.
132
-
133
- **Hyperbolic space** (Poincaré disk) has exponential volume growth ($e^r$), naturally preserving hierarchical structure and keeping rare classes distinct.
134
-
135
- <p align="center">
136
- <img src="https://raw.githubusercontent.com/Hyper3Labs/HyperView/main/assets/hyperview_infographic.png" alt="Euclidean vs Hyperbolic" width="100%">
137
- </p>
138
-
139
- ## Contributing
140
-
141
- Development setup, frontend hot-reload, and backend API notes live in [CONTRIBUTING.md](CONTRIBUTING.md).
142
-
143
- ## Related projects
144
-
145
- - **hyper-scatter**: High-performance WebGL scatterplot engine (Euclidean + Poincaré) used by the frontend: https://github.com/Hyper3Labs/hyper-scatter
146
- - **hyper-models**: Non-Euclidean model zoo + ONNX exports (e.g. for hyperbolic VLM experiments): https://github.com/Hyper3Labs/hyper-models
147
 
148
- ## References
149
 
150
- - [Poincaré Embeddings for Learning Hierarchical Representations](https://arxiv.org/abs/1705.08039) (Nickel & Kiela, 2017)
151
- - [Hyperbolic Neural Networks](https://arxiv.org/abs/1805.09112) (Ganea et al., 2018)
 
 
 
 
 
152
 
153
- ## License
154
 
155
- MIT License - see [LICENSE](LICENSE) for details.
 
 
6
  sdk: docker
7
  app_port: 7860
8
  pinned: false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  ---
10
 
11
+ # HyperView — Imagenette (CLIP + HyCoCLIP)
12
 
13
+ This Hugging Face Space runs HyperView with:
14
 
15
+ - CLIP embeddings (`openai/clip-vit-base-patch32`) for Euclidean layout
16
+ - HyCoCLIP embeddings (`hycoclip-vit-s`) for Poincaré layout
17
 
18
+ The Docker image installs the **latest HyperView from PyPI** and precomputes
19
+ embeddings/layouts during build for fast runtime startup.
 
 
 
 
 
20
 
21
+ ## Configuration
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
+ Environment variables:
24
 
25
+ - `DEMO_HF_DATASET` (default: `Multimodal-Fatima/Imagenette_validation`)
26
+ - `DEMO_HF_SPLIT` (default: `validation`)
27
+ - `DEMO_HF_IMAGE_KEY` (default: `image`)
28
+ - `DEMO_HF_LABEL_KEY` (default: `label`)
29
+ - `DEMO_SAMPLES` (default: `300`)
30
+ - `DEMO_CLIP_MODEL` (default: `openai/clip-vit-base-patch32`)
31
+ - `DEMO_HYPER_MODEL` (default: `hycoclip-vit-s`)
32
 
33
+ ## Deploy source
34
 
35
+ This folder is synchronized to Hugging Face Spaces by GitHub Actions from the
36
+ `hyperview-spaces` deployment repository.
app_hf.py DELETED
@@ -1,49 +0,0 @@
1
- #!/usr/bin/env python
2
- """Hugging Face Spaces entrypoint for HyperView.
3
-
4
- This script serves a pre-computed demo dataset stored in LanceDB.
5
- The dataset is computed at Docker build time by scripts/precompute_hf_demo.py.
6
-
7
- For HuggingFace Spaces deployment under Hyper3Labs/HyperView.
8
- """
9
-
10
- import os
11
-
12
- # Configuration from environment
13
- HOST = os.environ.get("HOST", "0.0.0.0")
14
- PORT = int(os.environ.get("PORT", 7860))
15
- DATASET_NAME = os.environ.get("DEMO_DATASET", "cifar10_hf_demo")
16
-
17
-
18
- def main() -> None:
19
- """Load pre-computed dataset and launch server."""
20
- import hyperview as hv
21
-
22
- dataset = hv.Dataset(DATASET_NAME)
23
-
24
- spaces = dataset.list_spaces()
25
- layouts = dataset.list_layouts()
26
-
27
- if not spaces or not layouts:
28
- print("Pre-computed embeddings not found in storage.")
29
- print("Falling back to computing on startup (this will be slow)...")
30
-
31
- from scripts.precompute_hf_demo import create_demo_dataset
32
-
33
- dataset = create_demo_dataset()
34
- else:
35
- print(f"Loaded dataset '{DATASET_NAME}' with pre-computed embeddings")
36
-
37
- print(f"\nStarting HyperView server on {HOST}:{PORT}")
38
- print("=" * 50)
39
-
40
- hv.launch(
41
- dataset,
42
- host=HOST,
43
- port=PORT,
44
- open_browser=False,
45
- )
46
-
47
-
48
- if __name__ == "__main__":
49
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
demo.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ """HyperView Hugging Face Space demo: CLIP + HyCoCLIP on Imagenette.
3
+
4
+ Usage:
5
+ python demo.py --precompute # run during Docker build
6
+ python demo.py # run as app entrypoint
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import os
12
+ import sys
13
+
14
+ import hyperview as hv
15
+
16
+ HOST = os.environ.get("HOST", "0.0.0.0")
17
+ PORT = int(os.environ.get("PORT", "7860"))
18
+
19
+ DATASET_NAME = os.environ.get("DEMO_DATASET", "imagenette_clip_hycoclip")
20
+ HF_DATASET = os.environ.get("DEMO_HF_DATASET", "Multimodal-Fatima/Imagenette_validation")
21
+ HF_SPLIT = os.environ.get("DEMO_HF_SPLIT", "validation")
22
+ HF_IMAGE_KEY = os.environ.get("DEMO_HF_IMAGE_KEY", "image")
23
+ HF_LABEL_KEY = os.environ.get("DEMO_HF_LABEL_KEY", "label")
24
+ NUM_SAMPLES = int(os.environ.get("DEMO_SAMPLES", "300"))
25
+
26
+ CLIP_MODEL_ID = os.environ.get("DEMO_CLIP_MODEL", "openai/clip-vit-base-patch32")
27
+ HYPER_MODEL_ID = os.environ.get("DEMO_HYPER_MODEL", "hycoclip-vit-s")
28
+
29
+
30
+ def _truthy_env(name: str, default: bool = True) -> bool:
31
+ value = os.environ.get(name)
32
+ if value is None:
33
+ return default
34
+ return value.strip().lower() not in {"0", "false", "no", "off", ""}
35
+
36
+
37
+ def _ensure_demo_ready(dataset: hv.Dataset) -> None:
38
+ if len(dataset) == 0:
39
+ print(f"Loading samples from {HF_DATASET} ({HF_SPLIT})...")
40
+ dataset.add_from_huggingface(
41
+ HF_DATASET,
42
+ split=HF_SPLIT,
43
+ image_key=HF_IMAGE_KEY,
44
+ label_key=HF_LABEL_KEY,
45
+ max_samples=NUM_SAMPLES,
46
+ shuffle=True,
47
+ )
48
+
49
+ spaces = dataset.list_spaces()
50
+
51
+ clip_space = next(
52
+ (
53
+ space
54
+ for space in spaces
55
+ if getattr(space, "provider", None) == "embed-anything"
56
+ and getattr(space, "model_id", None) == CLIP_MODEL_ID
57
+ ),
58
+ None,
59
+ )
60
+
61
+ if clip_space is None:
62
+ print(f"Computing CLIP embeddings ({CLIP_MODEL_ID})...")
63
+ dataset.compute_embeddings(model=CLIP_MODEL_ID, provider="embed-anything", show_progress=True)
64
+ spaces = dataset.list_spaces()
65
+ clip_space = next(
66
+ (
67
+ space
68
+ for space in spaces
69
+ if getattr(space, "provider", None) == "embed-anything"
70
+ and getattr(space, "model_id", None) == CLIP_MODEL_ID
71
+ ),
72
+ None,
73
+ )
74
+
75
+ if clip_space is None:
76
+ raise RuntimeError("Failed to create CLIP embedding space")
77
+
78
+ compute_hyperbolic = _truthy_env("DEMO_COMPUTE_HYPERBOLIC", default=True)
79
+ hyper_space = next(
80
+ (
81
+ space
82
+ for space in spaces
83
+ if getattr(space, "provider", None) == "hyper-models"
84
+ and getattr(space, "model_id", None) == HYPER_MODEL_ID
85
+ ),
86
+ None,
87
+ )
88
+
89
+ if compute_hyperbolic and hyper_space is None:
90
+ try:
91
+ print(f"Computing hyperbolic embeddings ({HYPER_MODEL_ID})...")
92
+ dataset.compute_embeddings(model=HYPER_MODEL_ID, provider="hyper-models", show_progress=True)
93
+ spaces = dataset.list_spaces()
94
+ hyper_space = next(
95
+ (
96
+ space
97
+ for space in spaces
98
+ if getattr(space, "provider", None) == "hyper-models"
99
+ and getattr(space, "model_id", None) == HYPER_MODEL_ID
100
+ ),
101
+ None,
102
+ )
103
+ except Exception as exc:
104
+ print(f"WARNING: hyperbolic embeddings failed ({type(exc).__name__}: {exc})")
105
+
106
+ layouts = dataset.list_layouts()
107
+ geometries = {getattr(layout, "geometry", None) for layout in layouts}
108
+
109
+ if "euclidean" not in geometries:
110
+ print("Computing euclidean layout...")
111
+ dataset.compute_visualization(space_key=clip_space.space_key, geometry="euclidean")
112
+
113
+ if "poincare" not in geometries:
114
+ print("Computing poincaré layout...")
115
+ poincare_space_key = hyper_space.space_key if hyper_space is not None else clip_space.space_key
116
+ dataset.compute_visualization(space_key=poincare_space_key, geometry="poincare")
117
+
118
+
119
+ def main() -> None:
120
+ dataset = hv.Dataset(DATASET_NAME)
121
+
122
+ if len(dataset) == 0 or not dataset.list_layouts():
123
+ print("Preparing demo dataset...")
124
+ _ensure_demo_ready(dataset)
125
+ else:
126
+ print(
127
+ f"Loaded cached dataset '{DATASET_NAME}' with "
128
+ f"{len(dataset.list_spaces())} spaces and {len(dataset.list_layouts())} layouts"
129
+ )
130
+
131
+ if "--precompute" in sys.argv:
132
+ print("Precompute complete")
133
+ return
134
+
135
+ print(f"Starting HyperView on {HOST}:{PORT}")
136
+ hv.launch(dataset, host=HOST, port=PORT, open_browser=False)
137
+
138
+
139
+ if __name__ == "__main__":
140
+ main()
docs/architecture.md DELETED
@@ -1,54 +0,0 @@
1
- # HyperView System Architecture
2
-
3
- ## The Integrated Pipeline Approach
4
-
5
- HyperView is built as a three-stage pipeline that turns raw multimodal data into an interactive, fairness-aware view of a dataset. Each stage uses the tool best suited for the job:
6
-
7
- * **Ingestion – Python (PyTorch/Geoopt):** Differentiable manifold operations and training of the Hyperbolic Adapter.
8
- * **Storage & Retrieval – Rust (Qdrant):** Low-latency vector search with a custom Poincaré distance metric.
9
- * **Visualization – Browser (WebGL/Deck.gl):** GPU-accelerated rendering of the Poincaré disk in the browser.
10
-
11
- ## System Diagram
12
-
13
- <p align="center">
14
- <img src="../assets/hyperview_architecture.png" alt="HyperView System Architecture: The Integrated Pipeline Approach" width="100%">
15
- </p>
16
-
17
- ## Component Breakdown
18
-
19
- ### 1. Ingestion: Hyperbolic Adapter (Python)
20
- * **Role:** The bridge between flat (Euclidean) model embeddings and curved (hyperbolic) space.
21
- * **Input:** Raw data (images/text) → standard model embeddings (e.g. CLIP/ResNet vectors).
22
- * **Tech:** PyTorch, Geoopt.
23
- * **Function:**
24
- * Learns a small Hyperbolic Adapter using differentiable manifold operations.
25
- * Uses the exponential map (`expmap0`) to project Euclidean vectors into the Poincaré ball.
26
- * This is where minority and rare cases are expanded away from the crowded center so they remain distinguishable.
27
-
28
- ### 2. Storage & Retrieval: Vector Engine (Rust / Qdrant)
29
- * **Role:** The memory that stores and retrieves hyperbolic embeddings at scale.
30
- * **Tech:** Qdrant (forked/extended in Rust).
31
- * **Challenge:** Standard vector DBs only support dot, cosine, or Euclidean distance.
32
- * **Solution:**
33
- * Implement a custom `PoincareDistance` metric in Rust:
34
- $$d(u, v) = \text{arccosh}\left(1 + 2 \frac{\lVert u - v\rVert^2}{(1 - \lVert u\rVert^2)(1 - \lVert v\rVert^2)}\right)$$
35
- * Plug this metric into Qdrant’s HNSW index for fast nearest-neighbor search in hyperbolic space.
36
- * This allows search results to respect the hierarchy in the data instead of collapsing the long tail.
37
-
38
- ### 3. Visualization: Poincaré Disk Viewer (WebGL)
39
- * **Role:** The lens that lets humans explore the structure of the dataset.
40
- * **Tech:** React, Deck.gl, custom WebGL shaders.
41
- * **Challenge:** Rendering 1M points in non-Euclidean geometry directly in the browser.
42
- * **Solution:**
43
- * Send raw hyperbolic coordinates to the GPU and render them directly onto the Poincaré disk using a custom shader (no CPU-side projection).
44
- * Provide pan/zoom/selection so curators can inspect minority clusters, isolate rare subgroups at the boundary, and export curated subsets.
45
-
46
- ## Data Flow: The Fairness Pipeline
47
-
48
- 1. **Ingest:** User uploads a dataset (e.g. medical images, biodiversity data).
49
- 2. **Embed:** Standard models (CLIP/ResNet/Whisper) produce Euclidean embeddings.
50
- 3. **Expand:** The Hyperbolic Adapter projects them into the Poincaré ball; rare cases move towards the boundary instead of being crushed.
51
- 4. **Index:** Qdrant stores these hyperbolic vectors with the custom Poincaré distance metric.
52
- 5. **Query:** A user clicks on a minority example or defines a region of interest.
53
- 6. **Search:** Qdrant returns semantic neighbors according to Poincaré distance, preserving the hierarchy between majority, minority, and rare subgroups.
54
- 7. **Visualize & Curate:** The browser renders the Poincaré disk, highlighting clusters and long-tail regions so users can see gaps, remove duplicates, and build fairer training sets.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/colab.md DELETED
@@ -1,37 +0,0 @@
1
- # Running HyperView in Google Colab
2
-
3
- HyperView works natively in Google Colab. Because Colab runs on a remote VM, you cannot access `localhost` directly. HyperView handles this automatically.
4
-
5
- ## Usage
6
-
7
- ### 1. Install HyperView
8
-
9
- ```bash
10
- !pip install -q git+https://github.com/Hyper3Labs/HyperView.git
11
- ```
12
-
13
- ### 2. Launch the visualizer
14
-
15
- When you run `hv.launch()`, a button labeled **“Open HyperView in a new tab”** will appear in the output. Click it to open the visualization.
16
-
17
- ```python
18
- # Minimal example
19
- import numpy as np
20
- import hyperview as hv
21
- from hyperview.core.sample import SampleFromArray
22
-
23
- dataset = hv.Dataset("colab_smoke", persist=False)
24
-
25
- rng = np.random.default_rng(0)
26
- for i in range(200):
27
- img = (rng.random((64, 64, 3)) * 255).astype(np.uint8)
28
- sample = SampleFromArray.from_array(id=f"s{i}", image_array=img, label="demo")
29
- sample.embedding_2d = [float(rng.normal()), float(rng.normal())] # Dummy 2D points
30
- dataset.add_sample(sample)
31
-
32
- hv.launch(dataset)
33
- ```
34
-
35
- ## Technical Details
36
-
37
- To support Colab, HyperView uses `google.colab.kernel.proxyPort` to expose the backend server. The UI is opened via a specially constructed "launcher" page that embeds the proxied application in a full-page iframe. This workaround ensures compatibility with modern browser security policies (like third-party cookie blocking) that often break direct proxy URLs.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/datasets.md DELETED
@@ -1,96 +0,0 @@
1
- # Datasets
2
-
3
- ## Creating a Dataset
4
-
5
- ```python
6
- import hyperview as hv
7
-
8
- # Persistent dataset (default) - survives restarts
9
- dataset = hv.Dataset("my_dataset")
10
-
11
- # In-memory dataset - lost when process exits
12
- dataset = hv.Dataset("my_dataset", persist=False)
13
- ```
14
-
15
- **Storage location:** `~/.hyperview/datasets/` (configurable via `HYPERVIEW_DATABASE_DIR`)
16
-
17
- Internally, each dataset is stored as two Lance tables (directories) inside that folder:
18
- - `hyperview_{dataset_name}.lance/` (samples)
19
- - `hyperview_{dataset_name}_meta.lance/` (metadata like label colors)
20
-
21
- ## Adding Samples
22
-
23
- ### From HuggingFace
24
- ```python
25
- dataset.add_from_huggingface(
26
- "uoft-cs/cifar100",
27
- split="train",
28
- image_key="img",
29
- label_key="fine_label",
30
- max_samples=1000,
31
- )
32
- ```
33
-
34
- ### From Directory
35
- ```python
36
- dataset.add_images_dir("/path/to/images", label_from_folder=True)
37
- ```
38
-
39
- ## Persistence Model: Additive
40
-
41
- HyperView uses an **additive** persistence model:
42
-
43
- | Action | Behavior |
44
- |--------|----------|
45
- | Add samples | New samples inserted, existing skipped by ID |
46
- | Request fewer than exist | Existing samples preserved (no deletion) |
47
- | Request more than exist | Only new samples added |
48
- | Embeddings | Cached per-sample, reused across sessions |
49
- | Projections | Recomputed when new samples added (UMAP requires refit) |
50
-
51
- **Example:**
52
- ```python
53
- dataset = hv.Dataset("my_dataset")
54
-
55
- dataset.add_from_huggingface(..., max_samples=200) # 200 samples
56
- dataset.add_from_huggingface(..., max_samples=400) # +200 new → 400 total
57
- dataset.add_from_huggingface(..., max_samples=300) # no change → 400 total
58
- dataset.add_from_huggingface(..., max_samples=500) # +100 new → 500 total
59
- ```
60
-
61
- Samples are **never implicitly deleted**. Use `hv.Dataset.delete("name")` for explicit removal.
62
-
63
- ## Computing Embeddings
64
-
65
- ```python
66
- # High-dimensional embeddings (CLIP/ResNet)
67
- dataset.compute_embeddings(model="clip", show_progress=True)
68
-
69
- # 2D projections for visualization
70
- dataset.compute_visualization() # UMAP to Euclidean + Hyperbolic
71
- ```
72
-
73
- Embeddings are stored per-sample. If a sample already has embeddings, it's skipped.
74
-
75
- ## Listing & Deleting Datasets
76
-
77
- ```python
78
- # List all persistent datasets
79
- hv.Dataset.list_datasets() # ['cifar100_demo', 'my_dataset', ...]
80
-
81
- # Delete a dataset
82
- hv.Dataset.delete("my_dataset")
83
-
84
- # Check existence
85
- hv.Dataset.exists("my_dataset") # True/False
86
- ```
87
-
88
- ## Dataset Info
89
-
90
- ```python
91
- len(dataset) # Number of samples
92
- dataset.name # Dataset name
93
- dataset.labels # Unique labels
94
- dataset.samples # Iterator over all samples
95
- dataset[sample_id] # Get sample by ID
96
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/index.html DELETED
@@ -1,465 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>HyperView: Interactive Poincaré Disk</title>
7
- <style>
8
- body {
9
- margin: 0;
10
- overflow: hidden;
11
- background-color: #f0f2f5;
12
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
13
- display: flex;
14
- flex-direction: column;
15
- align-items: center;
16
- justify-content: center;
17
- height: 100vh;
18
- }
19
- canvas {
20
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
21
- border-radius: 50%;
22
- background: white;
23
- cursor: grab;
24
- }
25
- canvas:active {
26
- cursor: grabbing;
27
- }
28
- .controls {
29
- position: absolute;
30
- top: 20px;
31
- left: 20px;
32
- background: rgba(255, 255, 255, 0.9);
33
- padding: 15px;
34
- border-radius: 8px;
35
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
36
- max-width: 300px;
37
- z-index: 100;
38
- }
39
- h1 { margin: 0 0 10px 0; font-size: 18px; color: #333; }
40
- p { margin: 0 0 10px 0; font-size: 14px; color: #666; line-height: 1.4; }
41
- .legend { display: flex; gap: 10px; font-size: 12px; margin-top: 10px; }
42
- .dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 5px; }
43
-
44
- .mode-btn {
45
- margin-top: 15px;
46
- padding: 8px 16px;
47
- background: #333;
48
- color: white;
49
- border: none;
50
- border-radius: 4px;
51
- cursor: pointer;
52
- font-weight: bold;
53
- width: 100%;
54
- transition: background 0.3s;
55
- }
56
- .mode-btn:hover { background: #555; }
57
- .mode-btn.active { background: #d93025; } /* Red for danger/collapse */
58
-
59
- .status-box {
60
- margin-top: 10px;
61
- padding: 10px;
62
- background: #f8f9fa;
63
- border-left: 4px solid #ccc;
64
- font-size: 13px;
65
- transition: all 0.3s;
66
- }
67
- .status-box.collapse {
68
- border-left-color: #d93025;
69
- background: #fce8e6;
70
- color: #a50e0e;
71
- }
72
- .status-box.expand {
73
- border-left-color: #188038;
74
- background: #e6f4ea;
75
- color: #137333;
76
- }
77
-
78
- /* About Overlay Styles */
79
- .about-btn {
80
- position: absolute;
81
- top: 20px;
82
- right: 20px;
83
- background: #fff;
84
- border: 1px solid #ccc;
85
- border-radius: 50%;
86
- width: 40px;
87
- height: 40px;
88
- font-size: 20px;
89
- cursor: pointer;
90
- box-shadow: 0 2px 5px rgba(0,0,0,0.1);
91
- display: flex;
92
- align-items: center;
93
- justify-content: center;
94
- color: #555;
95
- transition: all 0.2s;
96
- z-index: 100;
97
- }
98
- .about-btn:hover { background: #f0f0f0; color: #333; }
99
-
100
- .overlay-backdrop {
101
- position: fixed;
102
- top: 0;
103
- left: 0;
104
- width: 100%;
105
- height: 100%;
106
- background: rgba(0,0,0,0.5);
107
- display: flex;
108
- align-items: center;
109
- justify-content: center;
110
- z-index: 1000;
111
- opacity: 0;
112
- visibility: hidden;
113
- transition: opacity 0.3s, visibility 0.3s;
114
- }
115
- .overlay-backdrop.visible {
116
- opacity: 1;
117
- visibility: visible;
118
- }
119
- .overlay-content {
120
- background: white;
121
- padding: 30px;
122
- border-radius: 12px;
123
- max-width: 600px;
124
- width: 90%;
125
- max-height: 80vh;
126
- overflow-y: auto;
127
- box-shadow: 0 10px 25px rgba(0,0,0,0.2);
128
- position: relative;
129
- }
130
- .close-btn {
131
- position: absolute;
132
- top: 15px;
133
- right: 15px;
134
- background: none;
135
- border: none;
136
- font-size: 24px;
137
- cursor: pointer;
138
- color: #999;
139
- }
140
- .close-btn:hover { color: #333; }
141
-
142
- .overlay-content h2 { margin-top: 0; color: #333; }
143
- .overlay-content h3 { color: #444; margin-top: 20px; margin-bottom: 10px; font-size: 16px; }
144
- .overlay-content p { font-size: 15px; line-height: 1.6; color: #555; margin-bottom: 15px; }
145
-
146
- .faq-item {
147
- background: #f9f9f9;
148
- padding: 15px;
149
- border-radius: 8px;
150
- margin-bottom: 15px;
151
- border-left: 4px solid #0066cc;
152
- }
153
- .faq-item h4 { margin: 0 0 8px 0; color: #0066cc; font-size: 15px; }
154
- .faq-item p { margin: 0; font-size: 14px; }
155
- </style>
156
- </head>
157
- <body>
158
- <button id="aboutBtn" class="about-btn" title="About HyperView">?</button>
159
-
160
- <div id="aboutOverlay" class="overlay-backdrop">
161
- <div class="overlay-content">
162
- <button id="closeOverlay" class="close-btn">&times;</button>
163
- <h2>Why HyperView?</h2>
164
- <p>
165
- Modern AI curation tools rely on <strong>Euclidean geometry</strong> (flat space).
166
- But real-world data—like biological taxonomies, social hierarchies, and medical diagnoses—is
167
- complex and hierarchical.
168
- </p>
169
- <p>
170
- When you force this complex data into a flat box, you run out of room.
171
- To fit the "Majority," the math crushes the "Minority" and "Rare" cases together.
172
- We call this <strong>Representation Collapse</strong>.
173
- </p>
174
-
175
- <h3>The Solution: Hyperbolic Space</h3>
176
- <p>
177
- HyperView uses the <strong>Poincaré disk</strong>, a model of hyperbolic geometry where space expands exponentially towards the edge.
178
- This gives "infinite" room for outliers, ensuring they remain distinct and visible.
179
- </p>
180
-
181
- <h3>FAQ: Why does this matter?</h3>
182
-
183
- <div class="faq-item">
184
- <h4>The "Hidden Diagnosis" Problem</h4>
185
- <p>
186
- Imagine training an AI doctor on 10,000 chest X-rays:
187
- <br>• <strong>9,000 Healthy</strong> (Majority)
188
- <br>• <strong>900 Common Pneumonia</strong> (Minority)
189
- <br>• <strong>100 Rare Early-Stage Tuberculosis</strong> (Rare Subgroup)
190
- </p>
191
- <p style="margin-top: 10px;">
192
- <strong>In Euclidean Space:</strong> The model runs out of room. It crushes the 100 Tuberculosis cases into the Pneumonia cluster. To the AI, they look like noise. The patient is misdiagnosed.
193
- </p>
194
- <p style="margin-top: 10px;">
195
- <strong>In HyperView:</strong> The Tuberculosis cases are pushed to the edge. They form a distinct, visible island. You can see them, select them, and ensure the AI learns to save those patients.
196
- </p>
197
- </div>
198
- </div>
199
- </div>
200
-
201
- <div class="controls">
202
- <h1>HyperView Interactive Demo</h1>
203
- <p>
204
- <strong>Drag to Pan.</strong> Experience the "infinite" space.
205
- Notice how the red "Rare" points expand and separate as you bring them towards the center.
206
- </p>
207
- <div class="legend">
208
- <div><span class="dot" style="background: #ccc;"></span>Majority</div>
209
- <div><span class="dot" style="background: #0066cc;"></span>Minority</div>
210
- <div><span class="dot" style="background: #ff0000;"></span>Rare</div>
211
- </div>
212
-
213
- <div id="statusBox" class="status-box expand">
214
- <strong>Hyperbolic Mode:</strong><br>
215
- Space expands exponentially.<br>
216
- Rare items are distinct.
217
- </div>
218
- <button id="toggleBtn" class="mode-btn">Simulate Euclidean Collapse</button>
219
- </div>
220
- <canvas id="poincareCanvas"></canvas>
221
-
222
- <script>
223
- const canvas = document.getElementById('poincareCanvas');
224
- const ctx = canvas.getContext('2d');
225
- const toggleBtn = document.getElementById('toggleBtn');
226
- const statusBox = document.getElementById('statusBox');
227
-
228
- // Configuration
229
- const RADIUS = 300;
230
- const WIDTH = RADIUS * 2;
231
- const HEIGHT = RADIUS * 2;
232
-
233
- canvas.width = WIDTH;
234
- canvas.height = HEIGHT;
235
-
236
- // Complex Number Utilities
237
- class Complex {
238
- constructor(re, im) { this.re = re; this.im = im; }
239
-
240
- add(other) { return new Complex(this.re + other.re, this.im + other.im); }
241
- sub(other) { return new Complex(this.re - other.re, this.im - other.im); }
242
- mul(other) {
243
- return new Complex(
244
- this.re * other.re - this.im * other.im,
245
- this.re * other.im + this.im * other.re
246
- );
247
- }
248
- div(other) {
249
- const denom = other.re * other.re + other.im * other.im;
250
- return new Complex(
251
- (this.re * other.re + this.im * other.im) / denom,
252
- (this.im * other.re - this.re * other.im) / denom
253
- );
254
- }
255
- conj() { return new Complex(this.re, -this.im); }
256
- modSq() { return this.re * this.re + this.im * this.im; }
257
- }
258
-
259
- // Mobius Transformation: (z + a) / (1 + conj(a)z)
260
- function mobiusAdd(z, a) {
261
- const num = z.add(a);
262
- const den = new Complex(1, 0).add(a.conj().mul(z));
263
- return num.div(den);
264
- }
265
-
266
- // Data Generation (Hierarchy)
267
- const points = [];
268
-
269
- function addCluster(count, r_hyp, r_euc, theta_center, spread_hyp, spread_euc, type) {
270
- for (let i = 0; i < count; i++) {
271
- // Hyperbolic Position
272
- const rh_noise = (Math.random() - 0.5) * spread_hyp;
273
- const th_noise = (Math.random() - 0.5) * spread_hyp;
274
- const rh = Math.min(0.99, Math.max(0, r_hyp + rh_noise));
275
- const th = theta_center + th_noise;
276
-
277
- // Euclidean Position (Crushed)
278
- const re_noise = (Math.random() - 0.5) * spread_euc;
279
- const re = Math.min(0.99, Math.max(0, r_euc + re_noise));
280
-
281
- const hypZ = new Complex(rh * Math.cos(th), rh * Math.sin(th));
282
- const eucZ = new Complex(re * Math.cos(th), re * Math.sin(th));
283
-
284
- points.push({
285
- hypZ: hypZ,
286
- eucZ: eucZ,
287
- currentZ: hypZ, // Start in Hyperbolic
288
- type: type
289
- });
290
- }
291
- }
292
-
293
- // 1. Majority (Center)
294
- addCluster(300, 0.1, 0.1, 0, 0.5, 0.2, 'majority');
295
-
296
- // 2. Minority (Edge) - r=0.85 (Hyp) vs r=0.5 (Euc)
297
- addCluster(50, 0.85, 0.5, Math.PI/4, 0.2, 0.1, 'minority');
298
-
299
- // 3. Rare (Deep Edge) - r=0.95 (Hyp) vs r=0.52 (Euc - Overlapping Minority)
300
- addCluster(10, 0.95, 0.52, Math.PI/4, 0.05, 0.02, 'rare');
301
-
302
- // View State
303
- let isEuclidean = false;
304
- let animationProgress = 0; // 0 = Hyperbolic, 1 = Euclidean
305
- let viewOffset = new Complex(0, 0);
306
- let isDragging = false;
307
- let lastMouse = null;
308
-
309
- function screenToComplex(x, y) {
310
- const rect = canvas.getBoundingClientRect();
311
- const cx = x - rect.left - RADIUS;
312
- const cy = y - rect.top - RADIUS;
313
- return new Complex(cx / RADIUS, cy / RADIUS);
314
- }
315
-
316
- function lerpComplex(a, b, t) {
317
- return new Complex(
318
- a.re + (b.re - a.re) * t,
319
- a.im + (b.im - a.im) * t
320
- );
321
- }
322
-
323
-
324
- function update() {
325
- // Animate transition
326
- const target = isEuclidean ? 1 : 0;
327
- const speed = 0.05;
328
-
329
- if (Math.abs(animationProgress - target) > 0.001) {
330
- animationProgress += (target - animationProgress) * speed;
331
- } else {
332
- animationProgress = target;
333
- }
334
-
335
- // Update point positions
336
- points.forEach(p => {
337
- // Interpolate between base positions
338
- let basePos = lerpComplex(p.hypZ, p.eucZ, animationProgress);
339
-
340
- // Apply View Transformation
341
- // As we move to Euclidean, we want to reset the view to center (0,0)
342
- // effectively disabling the "infinite scroll"
343
- const effectiveViewOffset = new Complex(
344
- viewOffset.re * (1 - animationProgress),
345
- viewOffset.im * (1 - animationProgress)
346
- );
347
-
348
- p.currentZ = mobiusAdd(basePos, effectiveViewOffset);
349
- });
350
-
351
- draw();
352
- requestAnimationFrame(update);
353
- }
354
-
355
- function draw() {
356
- ctx.clearRect(0, 0, WIDTH, HEIGHT);
357
-
358
- // Draw Disk Boundary
359
- ctx.beginPath();
360
- ctx.arc(RADIUS, RADIUS, RADIUS - 1, 0, Math.PI * 2);
361
- ctx.strokeStyle = '#333';
362
- ctx.lineWidth = 2;
363
- ctx.stroke();
364
- ctx.fillStyle = '#fff';
365
- ctx.fill();
366
-
367
- // Draw Grid (Geodesics)
368
- ctx.strokeStyle = '#eee';
369
- ctx.lineWidth = 1;
370
- // Fade out grid in Euclidean mode
371
- ctx.globalAlpha = 1 - animationProgress;
372
- for(let r=0.2; r<1.0; r+=0.2) {
373
- ctx.beginPath();
374
- ctx.arc(RADIUS, RADIUS, r * RADIUS, 0, Math.PI * 2);
375
- ctx.stroke();
376
- }
377
- ctx.globalAlpha = 1.0;
378
-
379
- // Draw Points
380
- points.forEach(p => {
381
- const px = p.currentZ.re * RADIUS + RADIUS;
382
- const py = p.currentZ.im * RADIUS + RADIUS;
383
-
384
- ctx.beginPath();
385
- ctx.arc(px, py, p.type === 'rare' ? 5 : 3, 0, Math.PI * 2);
386
-
387
- if (p.type === 'majority') ctx.fillStyle = 'rgba(150, 150, 150, 0.5)';
388
- if (p.type === 'minority') ctx.fillStyle = 'rgba(0, 102, 204, 0.8)';
389
- if (p.type === 'rare') ctx.fillStyle = 'rgba(255, 0, 0, 1.0)';
390
-
391
- ctx.fill();
392
- });
393
- }
394
-
395
- // Interaction
396
- canvas.addEventListener('mousedown', e => {
397
- if (isEuclidean) return; // Disable drag in Euclidean mode
398
- isDragging = true;
399
- lastMouse = screenToComplex(e.clientX, e.clientY);
400
- canvas.style.cursor = 'grabbing';
401
- });
402
-
403
- window.addEventListener('mouseup', () => {
404
- isDragging = false;
405
- canvas.style.cursor = isEuclidean ? 'not-allowed' : 'grab';
406
- });
407
-
408
- canvas.addEventListener('mousemove', e => {
409
- if (!isDragging) return;
410
-
411
- const currentMouse = screenToComplex(e.clientX, e.clientY);
412
- const delta = currentMouse.sub(lastMouse);
413
- const move = new Complex(delta.re, delta.im);
414
-
415
- viewOffset = mobiusAdd(viewOffset, move);
416
- lastMouse = currentMouse;
417
- });
418
-
419
- // Toggle Logic
420
- toggleBtn.addEventListener('click', () => {
421
- isEuclidean = !isEuclidean;
422
-
423
- if (isEuclidean) {
424
- toggleBtn.textContent = "Switch to Hyperbolic Mode";
425
- toggleBtn.classList.add('active');
426
- statusBox.innerHTML = "<strong>Euclidean Collapse:</strong><br>Rare items are crushed.<br>Indistinguishable from Minority.";
427
- statusBox.className = "status-box collapse";
428
- canvas.style.cursor = 'not-allowed';
429
- } else {
430
- toggleBtn.textContent = "Simulate Euclidean Collapse";
431
- toggleBtn.classList.remove('active');
432
- statusBox.innerHTML = "<strong>Hyperbolic Mode:</strong><br>Space expands exponentially.<br>Rare items are distinct.";
433
- statusBox.className = "status-box expand";
434
- canvas.style.cursor = 'grab';
435
- }
436
- });
437
-
438
- // About Overlay Logic
439
- const aboutBtn = document.getElementById('aboutBtn');
440
- const aboutOverlay = document.getElementById('aboutOverlay');
441
- const closeOverlay = document.getElementById('closeOverlay');
442
-
443
- function openAbout() {
444
- aboutOverlay.classList.add('visible');
445
- }
446
-
447
- function closeAbout() {
448
- aboutOverlay.classList.remove('visible');
449
- }
450
-
451
- aboutBtn.addEventListener('click', openAbout);
452
- closeOverlay.addEventListener('click', closeAbout);
453
-
454
- // Close on click outside
455
- aboutOverlay.addEventListener('click', (e) => {
456
- if (e.target === aboutOverlay) {
457
- closeAbout();
458
- }
459
- });
460
-
461
- // Start Loop
462
- update();
463
- </script>
464
- </body>
465
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/components.json DELETED
@@ -1,22 +0,0 @@
1
- {
2
- "$schema": "https://ui.shadcn.com/schema.json",
3
- "style": "new-york",
4
- "rsc": true,
5
- "tsx": true,
6
- "tailwind": {
7
- "config": "tailwind.config.ts",
8
- "css": "src/app/globals.css",
9
- "baseColor": "neutral",
10
- "cssVariables": true,
11
- "prefix": ""
12
- },
13
- "iconLibrary": "lucide",
14
- "aliases": {
15
- "components": "@/components",
16
- "utils": "@/lib/utils",
17
- "ui": "@/components/ui",
18
- "lib": "@/lib",
19
- "hooks": "@/hooks"
20
- },
21
- "registries": {}
22
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/eslint.config.mjs DELETED
@@ -1,22 +0,0 @@
1
- import path from "path";
2
- import { fileURLToPath } from "url";
3
-
4
- import { FlatCompat } from "@eslint/eslintrc";
5
-
6
- const __filename = fileURLToPath(import.meta.url);
7
- const __dirname = path.dirname(__filename);
8
-
9
- // Bridge legacy shareable configs (like `next/core-web-vitals`) into ESLint v9 flat config.
10
- const compat = new FlatCompat({
11
- baseDirectory: __dirname,
12
- });
13
-
14
- const config = [
15
- {
16
- // Mirror Next.js defaults: never lint build artifacts.
17
- ignores: ["**/.next/**", "**/out/**", "**/node_modules/**"],
18
- },
19
- ...compat.extends("next/core-web-vitals"),
20
- ];
21
-
22
- export default config;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/next-env.d.ts DELETED
@@ -1,6 +0,0 @@
1
- /// <reference types="next" />
2
- /// <reference types="next/image-types/global" />
3
- import "./.next/dev/types/routes.d.ts";
4
-
5
- // NOTE: This file should not be edited
6
- // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
 
 
 
 
 
 
 
frontend/next.config.ts DELETED
@@ -1,24 +0,0 @@
1
- import type { NextConfig } from "next";
2
- import path from "path";
3
-
4
- const nextConfig: NextConfig = {
5
- output: "export",
6
- trailingSlash: true,
7
- // Needed for Turbopack to resolve local linked/file dependencies in a monorepo.
8
- outputFileTracingRoot: path.join(__dirname, ".."),
9
- transpilePackages: ["hyper-scatter"],
10
- images: {
11
- unoptimized: true,
12
- },
13
- // Proxy API calls to backend during development
14
- async rewrites() {
15
- return [
16
- {
17
- source: "/api/:path*",
18
- destination: "http://127.0.0.1:6262/api/:path*",
19
- },
20
- ];
21
- },
22
- };
23
-
24
- export default nextConfig;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/package-lock.json DELETED
The diff for this file is too large to render. See raw diff
 
frontend/package.json DELETED
@@ -1,51 +0,0 @@
1
- {
2
- "name": "hyperview-frontend",
3
- "version": "0.1.0",
4
- "private": true,
5
- "scripts": {
6
- "dev": "next dev",
7
- "build": "next build",
8
- "start": "next start",
9
- "lint": "eslint .",
10
- "export": "next build && cp -r out/* ../src/hyperview/server/static/"
11
- },
12
- "dependencies": {
13
- "@radix-ui/react-collapsible": "^1.1.12",
14
- "@radix-ui/react-dialog": "^1.1.15",
15
- "@radix-ui/react-dropdown-menu": "^2.1.16",
16
- "@radix-ui/react-popover": "^1.1.15",
17
- "@radix-ui/react-radio-group": "^1.3.8",
18
- "@radix-ui/react-scroll-area": "^1.2.10",
19
- "@radix-ui/react-separator": "^1.1.8",
20
- "@radix-ui/react-slot": "^1.2.4",
21
- "@radix-ui/react-toggle": "^1.1.10",
22
- "@radix-ui/react-toggle-group": "^1.1.11",
23
- "@radix-ui/react-tooltip": "^1.2.8",
24
- "@tanstack/react-virtual": "^3.10.9",
25
- "class-variance-authority": "^0.7.1",
26
- "clsx": "^2.1.1",
27
- "cmdk": "^1.1.1",
28
- "dockview": "^4.13.1",
29
- "hyper-scatter": "https://github.com/Hyper3Labs/hyper-scatter/archive/14421d24ccf128392444175a23e74627c120905c.tar.gz",
30
- "justified-layout": "^4.1.0",
31
- "lucide-react": "^0.562.0",
32
- "next": "^16.0.7",
33
- "react": "18.3.1",
34
- "react-dom": "18.3.1",
35
- "tailwind-merge": "^3.4.0",
36
- "tailwindcss-animate": "^1.0.7",
37
- "zustand": "^5.0.1"
38
- },
39
- "devDependencies": {
40
- "@types/justified-layout": "^4.1.4",
41
- "@types/node": "^22.9.0",
42
- "@types/react": "^18.3.12",
43
- "@types/react-dom": "^18.3.1",
44
- "autoprefixer": "^10.4.20",
45
- "eslint": "^9.14.0",
46
- "eslint-config-next": "15.0.3",
47
- "postcss": "^8.4.49",
48
- "tailwindcss": "^3.4.15",
49
- "typescript": "^5.6.3"
50
- }
51
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/postcss.config.mjs DELETED
@@ -1,9 +0,0 @@
1
- /** @type {import('postcss-load-config').Config} */
2
- const config = {
3
- plugins: {
4
- tailwindcss: {},
5
- autoprefixer: {},
6
- },
7
- };
8
-
9
- export default config;
 
 
 
 
 
 
 
 
 
 
frontend/src/app/globals.css DELETED
@@ -1,224 +0,0 @@
1
- @import "dockview/dist/styles/dockview.css";
2
-
3
- @tailwind base;
4
- @tailwind components;
5
- @tailwind utilities;
6
-
7
- @layer base {
8
- html,
9
- body {
10
- height: 100%;
11
- margin: 0;
12
- padding: 0;
13
- overflow: hidden;
14
- }
15
-
16
- :root {
17
- /* Rerun-inspired dark theme as default (dark-first) */
18
- --background: 215 28% 7%; /* #0d1117 */
19
- --foreground: 213 27% 92%; /* #e6edf3 */
20
-
21
- --card: 215 21% 11%; /* #161b22 */
22
- --card-foreground: 213 27% 92%;
23
-
24
- --popover: 215 21% 11%;
25
- --popover-foreground: 213 27% 92%;
26
-
27
- --primary: 212 100% 67%; /* #58a6ff */
28
- --primary-foreground: 215 28% 7%;
29
-
30
- --secondary: 215 14% 17%; /* #21262d */
31
- --secondary-foreground: 213 27% 92%;
32
-
33
- --muted: 215 14% 17%;
34
- --muted-foreground: 213 12% 58%; /* #8b949e */
35
-
36
- --accent: 215 14% 17%;
37
- --accent-foreground: 213 27% 92%;
38
-
39
- --destructive: 0 62% 50%;
40
- --destructive-foreground: 0 0% 98%;
41
-
42
- --border: 215 14% 22%; /* #30363d */
43
- --input: 215 14% 22%;
44
- --ring: 212 100% 67%;
45
-
46
- --radius: 0.375rem;
47
-
48
- /* Custom Rerun-specific tokens */
49
- --surface: 215 21% 11%; /* #161b22 */
50
- --surface-light: 215 14% 17%; /* #21262d */
51
- --surface-elevated: 215 14% 22%; /* #30363d */
52
- --border-subtle: 215 14% 17%;
53
- --text: 213 27% 92%; /* #e6edf3 */
54
- --text-muted: 213 12% 58%; /* #8b949e */
55
- --text-subtle: 215 10% 46%; /* #6e7681 */
56
- --accent-cyan: 176 60% 53%; /* #39d3cc */
57
- --accent-orange: 27 86% 59%; /* #f0883e */
58
- }
59
- }
60
-
61
- @layer base {
62
- * {
63
- @apply border-border;
64
- }
65
- body {
66
- @apply bg-background text-foreground;
67
- font-size: 12px;
68
- line-height: 16px;
69
- letter-spacing: -0.15px;
70
- font-weight: 500;
71
- font-feature-settings: "liga" 1, "calt" 1;
72
- -webkit-font-smoothing: antialiased;
73
- -moz-osx-font-smoothing: grayscale;
74
- }
75
- }
76
-
77
- /* Custom scrollbar - Rerun style: 6px bar, 2px inner margin, 2px outer margin */
78
- ::-webkit-scrollbar {
79
- width: 10px; /* 6px bar + 2px inner margin on each side */
80
- height: 10px;
81
- }
82
-
83
- ::-webkit-scrollbar-track {
84
- background: transparent;
85
- }
86
-
87
- ::-webkit-scrollbar-thumb {
88
- background: hsl(var(--muted-foreground) / 0.2);
89
- border-radius: 3px;
90
- border: 2px solid transparent;
91
- background-clip: padding-box;
92
- }
93
-
94
- ::-webkit-scrollbar-thumb:hover {
95
- background: hsl(var(--muted-foreground) / 0.35);
96
- border: 2px solid transparent;
97
- background-clip: padding-box;
98
- }
99
-
100
- /* Firefox scrollbar */
101
- * {
102
- scrollbar-width: thin;
103
- scrollbar-color: hsl(var(--muted-foreground) / 0.25) transparent;
104
- }
105
-
106
- /* Hide scrollbar for some elements */
107
- .hide-scrollbar::-webkit-scrollbar {
108
- display: none;
109
- }
110
-
111
- .hide-scrollbar {
112
- -ms-overflow-style: none;
113
- scrollbar-width: none;
114
- }
115
-
116
- /* Scroll containers inside panels should keep scrollbars inset */
117
- .panel-scroll {
118
- scrollbar-gutter: stable;
119
- }
120
-
121
- /* Dockview theme overrides (match HyperView tokens) */
122
- .hyperview-dockview {
123
- --dv-background-color: hsl(var(--background));
124
- --dv-group-view-background-color: hsl(var(--card));
125
- --dv-tabs-and-actions-container-background-color: hsl(var(--secondary));
126
- --dv-activegroup-visiblepanel-tab-background-color: hsl(var(--card));
127
- --dv-activegroup-hiddenpanel-tab-background-color: hsl(var(--secondary));
128
- --dv-inactivegroup-visiblepanel-tab-background-color: hsl(var(--secondary));
129
- --dv-inactivegroup-hiddenpanel-tab-background-color: hsl(var(--secondary));
130
- --dv-activegroup-visiblepanel-tab-color: hsl(var(--foreground));
131
- --dv-activegroup-hiddenpanel-tab-color: hsl(var(--muted-foreground));
132
- --dv-inactivegroup-visiblepanel-tab-color: hsl(var(--muted-foreground));
133
- --dv-inactivegroup-hiddenpanel-tab-color: hsl(var(--muted-foreground));
134
- --dv-tabs-and-actions-container-font-size: 12px;
135
- --dv-tabs-and-actions-container-height: 24px;
136
- --dv-tab-font-size: 12px;
137
- --dv-tabs-container-scrollbar-color: hsl(var(--muted-foreground) / 0.25);
138
- --dv-scrollbar-background-color: hsl(var(--muted-foreground) / 0.25);
139
- --dv-tab-divider-color: hsl(var(--border));
140
- --dv-separator-border: transparent;
141
- --dv-paneview-header-border-color: hsl(var(--border));
142
- --dv-sash-color: hsl(var(--border));
143
- --dv-icon-hover-background-color: hsl(var(--accent));
144
- --dv-active-sash-color: hsl(var(--primary));
145
- --dv-drag-over-background-color: hsl(var(--primary) / 0.15);
146
- --dv-drag-over-border-color: hsl(var(--primary) / 0.6);
147
- /* Remove tab margins for flush appearance */
148
- --dv-tab-margin: 0;
149
- }
150
-
151
- /* Remove any gaps between panels - Rerun-style flush layout */
152
- .hyperview-dockview .dv-groupview {
153
- border: none;
154
- }
155
-
156
- .hyperview-dockview .dv-tabs-and-actions-container {
157
- border-bottom: 1px solid hsl(var(--border));
158
- }
159
-
160
- .hyperview-dockview .dv-tab {
161
- padding: 0 8px;
162
- height: 100%;
163
- display: flex;
164
- align-items: center;
165
- line-height: 16px;
166
- }
167
-
168
- .hyperview-dockview .dv-scrollable .dv-scrollbar-horizontal {
169
- height: 6px;
170
- border-radius: 3px;
171
- }
172
-
173
- /* Hide Dockview tab scrollbars (avoid extra bar under tabs) */
174
- .hyperview-dockview .dv-tabs-container {
175
- scrollbar-width: none;
176
- }
177
-
178
- .hyperview-dockview .dv-tabs-container::-webkit-scrollbar {
179
- height: 0;
180
- }
181
-
182
- .hyperview-dockview .dv-scrollable .dv-scrollbar-horizontal {
183
- display: none;
184
- }
185
-
186
- /* Sash styling: transparent hit area with centered 1px visible line via pseudo-element */
187
- .hyperview-dockview .dv-split-view-container > .dv-sash-container > .dv-sash {
188
- /* Keep the sash itself transparent - we'll draw the line with ::after */
189
- background-color: transparent !important;
190
- }
191
-
192
- /* Horizontal sash (vertical divider line) */
193
- .hyperview-dockview .dv-split-view-container.dv-horizontal > .dv-sash-container > .dv-sash::after {
194
- content: "";
195
- position: absolute;
196
- top: 0;
197
- bottom: 0;
198
- left: 50%;
199
- transform: translateX(-50%);
200
- width: 1px;
201
- background-color: hsl(var(--border));
202
- pointer-events: none;
203
- }
204
-
205
- .hyperview-dockview .dv-split-view-container.dv-horizontal > .dv-sash-container > .dv-sash.dv-enabled:hover::after {
206
- background-color: hsl(var(--primary));
207
- }
208
-
209
- /* Vertical sash (horizontal divider line) */
210
- .hyperview-dockview .dv-split-view-container.dv-vertical > .dv-sash-container > .dv-sash::after {
211
- content: "";
212
- position: absolute;
213
- left: 0;
214
- right: 0;
215
- top: 50%;
216
- transform: translateY(-50%);
217
- height: 1px;
218
- background-color: hsl(var(--border));
219
- pointer-events: none;
220
- }
221
-
222
- .hyperview-dockview .dv-split-view-container.dv-vertical > .dv-sash-container > .dv-sash.dv-enabled:hover::after {
223
- background-color: hsl(var(--primary));
224
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/app/layout.tsx DELETED
@@ -1,26 +0,0 @@
1
- import type { Metadata } from "next";
2
- import { Inter } from "next/font/google";
3
- import "./globals.css";
4
-
5
- const inter = Inter({
6
- subsets: ["latin"],
7
- display: "swap",
8
- weight: ["400", "500"],
9
- });
10
-
11
- export const metadata: Metadata = {
12
- title: "HyperView",
13
- description: "Dataset visualization with hyperbolic embeddings",
14
- };
15
-
16
- export default function RootLayout({
17
- children,
18
- }: Readonly<{
19
- children: React.ReactNode;
20
- }>) {
21
- return (
22
- <html lang="en" className="h-full">
23
- <body className={`${inter.className} antialiased h-full`}>{children}</body>
24
- </html>
25
- );
26
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/app/page.tsx DELETED
@@ -1,277 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect, useCallback, useMemo, useRef, useState } from "react";
4
- import { Header } from "@/components";
5
- import { DockviewWorkspace, DockviewProvider } from "@/components/DockviewWorkspace";
6
- import { useStore } from "@/store/useStore";
7
- import type { Sample } from "@/types";
8
- import {
9
- fetchDataset,
10
- fetchSamples,
11
- fetchSamplesBatch,
12
- fetchLassoSelection,
13
- } from "@/lib/api";
14
-
15
- const SAMPLES_PER_PAGE = 100;
16
-
17
- export default function Home() {
18
- const {
19
- samples,
20
- totalSamples,
21
- samplesLoaded,
22
- setSamples,
23
- appendSamples,
24
- addSamplesIfMissing,
25
- setDatasetInfo,
26
- setIsLoading,
27
- isLoading,
28
- error,
29
- setError,
30
- selectedIds,
31
- isLassoSelection,
32
- selectionSource,
33
- lassoQuery,
34
- lassoSamples,
35
- lassoTotal,
36
- lassoIsLoading,
37
- setLassoResults,
38
- labelFilter,
39
- } = useStore();
40
-
41
- const [loadingMore, setLoadingMore] = useState(false);
42
- const labelFilterRef = useRef<string | null>(labelFilter ?? null);
43
-
44
- // Initial data load - runs once on mount
45
- // Store setters are stable and don't need to be in deps
46
- useEffect(() => {
47
- const loadData = async () => {
48
- setIsLoading(true);
49
- setError(null);
50
-
51
- try {
52
- // Fetch dataset info and samples in parallel
53
- const [datasetInfo, samplesRes] = await Promise.all([
54
- fetchDataset(),
55
- fetchSamples(0, SAMPLES_PER_PAGE),
56
- ]);
57
-
58
- setDatasetInfo(datasetInfo);
59
- setSamples(samplesRes.samples, samplesRes.total);
60
- } catch (err) {
61
- console.error("Failed to load data:", err);
62
- setError(err instanceof Error ? err.message : "Failed to load data");
63
- } finally {
64
- setIsLoading(false);
65
- }
66
- };
67
-
68
- loadData();
69
- // eslint-disable-next-line react-hooks/exhaustive-deps
70
- }, []);
71
-
72
- // Fetch selected samples that aren't already loaded
73
- useEffect(() => {
74
- const fetchSelectedSamples = async () => {
75
- if (isLassoSelection) return;
76
- if (selectedIds.size === 0) return;
77
- if (selectionSource === "label") return;
78
-
79
- // Find IDs that are selected but not in our samples array
80
- const loadedIds = new Set(samples.map((s) => s.id));
81
- const missingIds = Array.from(selectedIds).filter((id) => !loadedIds.has(id));
82
-
83
- if (missingIds.length === 0) return;
84
-
85
- try {
86
- const fetchedSamples = await fetchSamplesBatch(missingIds);
87
- addSamplesIfMissing(fetchedSamples);
88
- } catch (err) {
89
- console.error("Failed to fetch selected samples:", err);
90
- }
91
- };
92
-
93
- fetchSelectedSamples();
94
- }, [selectedIds, samples, addSamplesIfMissing, isLassoSelection, selectionSource]);
95
-
96
- // Refetch samples when label filter changes (non-lasso mode)
97
- useEffect(() => {
98
- if (labelFilterRef.current === labelFilter) return;
99
- if (isLassoSelection) return;
100
-
101
- labelFilterRef.current = labelFilter ?? null;
102
-
103
- let cancelled = false;
104
- const run = async () => {
105
- try {
106
- const res = await fetchSamples(0, SAMPLES_PER_PAGE, labelFilter ?? undefined);
107
- if (cancelled) return;
108
- setSamples(res.samples, res.total);
109
- } catch (err) {
110
- if (cancelled) return;
111
- console.error("Failed to load filtered samples:", err);
112
- }
113
- };
114
-
115
- run();
116
- return () => {
117
- cancelled = true;
118
- };
119
- }, [isLassoSelection, labelFilter, setSamples]);
120
-
121
- // Fetch initial lasso selection page when a new lasso query begins.
122
- useEffect(() => {
123
- if (!isLassoSelection) return;
124
- if (!lassoQuery) return;
125
- if (!lassoIsLoading) return;
126
-
127
- const abort = new AbortController();
128
-
129
- const run = async () => {
130
- try {
131
- const res = await fetchLassoSelection({
132
- layoutKey: lassoQuery.layoutKey,
133
- polygon: lassoQuery.polygon,
134
- offset: 0,
135
- limit: SAMPLES_PER_PAGE,
136
- signal: abort.signal,
137
- });
138
- if (abort.signal.aborted) return;
139
- setLassoResults(res.samples, res.total, false);
140
- } catch (err) {
141
- if (err instanceof DOMException && err.name === "AbortError") return;
142
- console.error("Failed to fetch lasso selection:", err);
143
- setLassoResults([], 0, false);
144
- }
145
- };
146
-
147
- run();
148
-
149
- return () => abort.abort();
150
- }, [isLassoSelection, lassoIsLoading, lassoQuery, setLassoResults]);
151
-
152
- // Load more samples
153
- const loadMore = useCallback(async () => {
154
- if (loadingMore) return;
155
-
156
- if (isLassoSelection) {
157
- if (!lassoQuery) return;
158
- if (lassoIsLoading) return;
159
- if (!lassoIsLoading && lassoSamples.length >= lassoTotal) return;
160
-
161
- setLoadingMore(true);
162
- try {
163
- const res = await fetchLassoSelection({
164
- layoutKey: lassoQuery.layoutKey,
165
- polygon: lassoQuery.polygon,
166
- offset: lassoSamples.length,
167
- limit: SAMPLES_PER_PAGE,
168
- });
169
- setLassoResults(res.samples, res.total, true);
170
- } catch (err) {
171
- console.error("Failed to load more lasso samples:", err);
172
- } finally {
173
- setLoadingMore(false);
174
- }
175
- return;
176
- }
177
-
178
- if (samplesLoaded >= totalSamples) return;
179
-
180
- setLoadingMore(true);
181
- try {
182
- const res = await fetchSamples(samplesLoaded, SAMPLES_PER_PAGE, labelFilter ?? undefined);
183
- appendSamples(res.samples);
184
- } catch (err) {
185
- console.error("Failed to load more samples:", err);
186
- } finally {
187
- setLoadingMore(false);
188
- }
189
- }, [
190
- loadingMore,
191
- appendSamples,
192
- isLassoSelection,
193
- lassoIsLoading,
194
- lassoQuery,
195
- lassoSamples.length,
196
- lassoTotal,
197
- samplesLoaded,
198
- totalSamples,
199
- setLassoResults,
200
- labelFilter,
201
- ]);
202
-
203
- const displayedSamples = useMemo(() => {
204
- if (isLassoSelection) return lassoSamples;
205
-
206
- // When a selection comes from the scatter plot, bring selected samples to the top
207
- // so the user immediately sees what they clicked.
208
- if (selectionSource === "scatter" && selectedIds.size > 0) {
209
- const byId = new Map<string, Sample>();
210
- for (const s of samples) byId.set(s.id, s);
211
-
212
- const pinned: Sample[] = [];
213
- for (const id of selectedIds) {
214
- const s = byId.get(id);
215
- if (s) pinned.push(s);
216
- }
217
-
218
- if (pinned.length > 0) {
219
- const rest = samples.filter((s) => !selectedIds.has(s.id));
220
- return [...pinned, ...rest];
221
- }
222
- }
223
-
224
- return samples;
225
- }, [isLassoSelection, lassoSamples, samples, selectedIds, selectionSource]);
226
-
227
- const displayedTotal = isLassoSelection ? lassoTotal : totalSamples;
228
- const displayedHasMore = isLassoSelection ? displayedSamples.length < displayedTotal : samplesLoaded < totalSamples;
229
-
230
- if (error) {
231
- return (
232
- <div className="h-screen flex flex-col bg-background">
233
- <Header />
234
- <div className="flex-1 flex items-center justify-center">
235
- <div className="text-center">
236
- <div className="text-destructive text-lg mb-2">Error</div>
237
- <div className="text-muted-foreground">{error}</div>
238
- <p className="text-muted-foreground mt-4 text-sm">
239
- Make sure the HyperView backend is running on port 6262.
240
- </p>
241
- </div>
242
- </div>
243
- </div>
244
- );
245
- }
246
-
247
- if (isLoading) {
248
- return (
249
- <div className="h-screen flex flex-col bg-background">
250
- <Header />
251
- <div className="flex-1 flex items-center justify-center">
252
- <div className="text-center">
253
- <div className="w-8 h-8 border-2 border-primary border-t-transparent rounded-full animate-spin mx-auto mb-4" />
254
- <div className="text-muted-foreground">Loading dataset...</div>
255
- </div>
256
- </div>
257
- </div>
258
- );
259
- }
260
-
261
- return (
262
- <DockviewProvider
263
- samples={displayedSamples}
264
- onLoadMore={loadMore}
265
- hasMore={displayedHasMore}
266
- >
267
- <div className="h-screen flex flex-col bg-background">
268
- <Header />
269
-
270
- {/* Main content - dockable panels */}
271
- <div className="flex-1 bg-background overflow-hidden">
272
- <DockviewWorkspace />
273
- </div>
274
- </div>
275
- </DockviewProvider>
276
- );
277
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/DockviewWorkspace.tsx DELETED
@@ -1,765 +0,0 @@
1
- "use client";
2
-
3
- import React, {
4
- useCallback,
5
- useEffect,
6
- useMemo,
7
- useState,
8
- createContext,
9
- useContext,
10
- type ReactNode,
11
- } from "react";
12
- import {
13
- DockviewReact,
14
- type DockviewApi,
15
- type DockviewReadyEvent,
16
- type IDockviewPanelProps,
17
- type IDockviewPanelHeaderProps,
18
- type IWatermarkPanelProps,
19
- themeAbyss,
20
- } from "dockview";
21
- import { Circle, Disc, Grid3X3 } from "lucide-react";
22
-
23
- import type { Geometry, Sample } from "@/types";
24
- import { useStore } from "@/store/useStore";
25
- import { findLayoutByGeometry } from "@/lib/layouts";
26
- import { ImageGrid } from "./ImageGrid";
27
- import { ScatterPanel } from "./ScatterPanel";
28
- import { ExplorerPanel } from "./ExplorerPanel";
29
- import { PlaceholderPanel } from "./PlaceholderPanel";
30
- import { HyperViewLogo } from "./icons";
31
-
32
- const LAYOUT_STORAGE_KEY = "hyperview:dockview-layout:v4";
33
-
34
- // Panel IDs
35
- const PANEL = {
36
- EXPLORER: "explorer",
37
- GRID: "grid",
38
- SCATTER_EUCLIDEAN: "scatter-euclidean",
39
- SCATTER_POINCARE: "scatter-poincare",
40
- SCATTER_DEFAULT: "scatter-default",
41
- RIGHT_PLACEHOLDER: "right-placeholder",
42
- BOTTOM_PLACEHOLDER: "bottom-placeholder",
43
- } as const;
44
-
45
- const CENTER_PANEL_IDS = [
46
- PANEL.GRID,
47
- PANEL.SCATTER_EUCLIDEAN,
48
- PANEL.SCATTER_POINCARE,
49
- PANEL.SCATTER_DEFAULT,
50
- ] as const;
51
-
52
- export const CENTER_PANEL_DEFS = [
53
- { id: PANEL.GRID, label: "Samples", icon: Grid3X3 },
54
- { id: PANEL.SCATTER_EUCLIDEAN, label: "Euclidean", icon: Circle },
55
- { id: PANEL.SCATTER_POINCARE, label: "Hyperbolic", icon: Disc },
56
- ] as const;
57
-
58
- const NON_ANCHOR_PANEL_IDS = new Set<string>([
59
- PANEL.EXPLORER,
60
- PANEL.RIGHT_PLACEHOLDER,
61
- PANEL.BOTTOM_PLACEHOLDER,
62
- ]);
63
-
64
- const DRAG_LOCKED_PANEL_IDS = new Set<string>([PANEL.EXPLORER]);
65
-
66
- const DEFAULT_CONTAINER_WIDTH = 1200;
67
- const DEFAULT_CONTAINER_HEIGHT = 800;
68
- const MIN_SIDE_PANEL_WIDTH = 120;
69
- const MIN_BOTTOM_PANEL_HEIGHT = 150;
70
-
71
- const getContainerWidth = (api?: DockviewApi | null) =>
72
- api?.width ??
73
- (typeof window === "undefined" ? DEFAULT_CONTAINER_WIDTH : window.innerWidth);
74
- const getContainerHeight = (api?: DockviewApi | null) =>
75
- api?.height ??
76
- (typeof window === "undefined" ? DEFAULT_CONTAINER_HEIGHT : window.innerHeight);
77
-
78
- const getDefaultLeftPanelWidth = (screenWidth: number) =>
79
- Math.round(Math.min(0.35 * screenWidth, 200));
80
- const getDefaultRightPanelWidth = (screenWidth: number) =>
81
- Math.round(Math.min(0.45 * screenWidth, 300));
82
- const getDefaultBottomPanelHeight = (containerHeight: number) =>
83
- Math.round(
84
- Math.min(Math.max(0.25 * containerHeight, MIN_BOTTOM_PANEL_HEIGHT), 250)
85
- );
86
- const getBottomPanelMaxHeight = (containerHeight: number) =>
87
- Math.round(
88
- Math.max(containerHeight - MIN_BOTTOM_PANEL_HEIGHT, MIN_BOTTOM_PANEL_HEIGHT)
89
- );
90
-
91
- function getCenterAnchorPanel(api: DockviewApi) {
92
- for (const id of CENTER_PANEL_IDS) {
93
- const panel = api.getPanel(id);
94
- if (panel) return panel;
95
- }
96
-
97
- const fallback = api.panels.find((panel) => !NON_ANCHOR_PANEL_IDS.has(panel.id));
98
- return fallback ?? api.activePanel;
99
- }
100
-
101
- function getZonePosition(zone: "left" | "right" | "bottom") {
102
- return { direction: zone === "bottom" ? "below" : zone };
103
- }
104
-
105
- function getCenterTabPosition(api: DockviewApi) {
106
- const anchor = getCenterAnchorPanel(api);
107
- if (!anchor) return undefined;
108
- return { referencePanel: anchor, direction: "within" as const };
109
- }
110
-
111
- // -----------------------------------------------------------------------------
112
- // Context for sharing dockview API across components
113
- // -----------------------------------------------------------------------------
114
- interface DockviewContextValue {
115
- api: DockviewApi | null;
116
- setApi: (api: DockviewApi) => void;
117
- samples: Sample[];
118
- onLoadMore: () => void;
119
- hasMore: boolean;
120
- }
121
-
122
- const DockviewContext = createContext<DockviewContextValue | null>(null);
123
-
124
- function useDockviewContext() {
125
- const ctx = useContext(DockviewContext);
126
- if (!ctx) throw new Error("useDockviewContext must be used within DockviewProvider");
127
- return ctx;
128
- }
129
-
130
- // Public hook for components like Header
131
- export function useDockviewApi() {
132
- const ctx = useContext(DockviewContext);
133
- const datasetInfo = useStore((state) => state.datasetInfo);
134
- const {
135
- leftPanelOpen,
136
- rightPanelOpen,
137
- bottomPanelOpen,
138
- setLeftPanelOpen,
139
- setRightPanelOpen,
140
- setBottomPanelOpen,
141
- } = useStore();
142
-
143
- const addPanel = useCallback(
144
- (panelId: string) => {
145
- if (!ctx?.api) return;
146
-
147
- const api = ctx.api;
148
- const position = getCenterTabPosition(api);
149
- const baseOptions = position ? { position } : {};
150
-
151
- const layouts = datasetInfo?.layouts ?? [];
152
- const euclideanLayout = findLayoutByGeometry(layouts, "euclidean");
153
- const poincareLayout = findLayoutByGeometry(layouts, "poincare");
154
-
155
- // Don't add if already exists - just focus it
156
- if (api.getPanel(panelId)) {
157
- api.getPanel(panelId)?.focus();
158
- return;
159
- }
160
-
161
- switch (panelId) {
162
- case PANEL.GRID:
163
- api.addPanel({
164
- id: PANEL.GRID,
165
- component: "grid",
166
- title: "Samples",
167
- tabComponent: "samplesTab",
168
- renderer: "always",
169
- ...baseOptions,
170
- });
171
- break;
172
-
173
- case PANEL.SCATTER_EUCLIDEAN:
174
- api.addPanel({
175
- id: PANEL.SCATTER_EUCLIDEAN,
176
- component: "scatter",
177
- title: "Euclidean",
178
- tabComponent: "euclideanTab",
179
- params: {
180
- layoutKey: euclideanLayout?.layout_key,
181
- geometry: "euclidean" as Geometry,
182
- },
183
- renderer: "always",
184
- ...baseOptions,
185
- });
186
- break;
187
-
188
- case PANEL.SCATTER_POINCARE:
189
- api.addPanel({
190
- id: PANEL.SCATTER_POINCARE,
191
- component: "scatter",
192
- title: "Hyperbolic",
193
- tabComponent: "hyperbolicTab",
194
- params: {
195
- layoutKey: poincareLayout?.layout_key,
196
- geometry: "poincare" as Geometry,
197
- },
198
- renderer: "always",
199
- ...baseOptions,
200
- });
201
- break;
202
- }
203
- },
204
- [ctx?.api, datasetInfo]
205
- );
206
-
207
- const resetLayout = useCallback(() => {
208
- localStorage.removeItem(LAYOUT_STORAGE_KEY);
209
- window.location.reload();
210
- }, []);
211
-
212
- // Toggle zone visibility
213
- const toggleZone = useCallback(
214
- (zone: "left" | "right" | "bottom") => {
215
- if (!ctx?.api) return;
216
-
217
- const api = ctx.api;
218
- const panelId =
219
- zone === "left"
220
- ? PANEL.EXPLORER
221
- : zone === "right"
222
- ? PANEL.RIGHT_PLACEHOLDER
223
- : PANEL.BOTTOM_PLACEHOLDER;
224
- const setOpen =
225
- zone === "left"
226
- ? setLeftPanelOpen
227
- : zone === "right"
228
- ? setRightPanelOpen
229
- : setBottomPanelOpen;
230
- const isOpen =
231
- zone === "left"
232
- ? leftPanelOpen
233
- : zone === "right"
234
- ? rightPanelOpen
235
- : bottomPanelOpen;
236
-
237
- const existingPanel = api.getPanel(panelId);
238
-
239
- if (isOpen && existingPanel) {
240
- existingPanel.api.close();
241
- setOpen(false);
242
- return;
243
- }
244
-
245
- if (isOpen) return;
246
-
247
- const containerWidth = getContainerWidth(api);
248
- const containerHeight = getContainerHeight(api);
249
- const position = getZonePosition(zone);
250
-
251
- let newPanel;
252
- if (zone === "left") {
253
- const targetWidth = getDefaultLeftPanelWidth(containerWidth);
254
- newPanel = api.addPanel({
255
- id: panelId,
256
- component: "explorer",
257
- title: "Explorer",
258
- position,
259
- initialWidth: targetWidth,
260
- minimumWidth: MIN_SIDE_PANEL_WIDTH,
261
- maximumWidth: targetWidth,
262
- });
263
-
264
- if (newPanel) {
265
- newPanel.group.locked = true;
266
- newPanel.group.header.hidden = true;
267
- // Explicitly set the width to ensure it's applied
268
- newPanel.api.setSize({ width: targetWidth });
269
- }
270
- } else if (zone === "right") {
271
- newPanel = api.addPanel({
272
- id: panelId,
273
- component: "placeholder",
274
- title: "Blank",
275
- position,
276
- initialWidth: getDefaultRightPanelWidth(containerWidth),
277
- minimumWidth: MIN_SIDE_PANEL_WIDTH,
278
- maximumWidth: Math.round(containerWidth * 0.65),
279
- });
280
- } else {
281
- newPanel = api.addPanel({
282
- id: panelId,
283
- component: "placeholder",
284
- title: "Blank",
285
- position,
286
- initialHeight: getDefaultBottomPanelHeight(containerHeight),
287
- minimumHeight: MIN_BOTTOM_PANEL_HEIGHT,
288
- maximumHeight: getBottomPanelMaxHeight(containerHeight),
289
- });
290
- }
291
-
292
- if (newPanel) {
293
- setOpen(true);
294
- // Activate the panel so its content renders immediately
295
- newPanel.api.setActive();
296
- }
297
- },
298
- [
299
- ctx?.api,
300
- leftPanelOpen,
301
- rightPanelOpen,
302
- bottomPanelOpen,
303
- setLeftPanelOpen,
304
- setRightPanelOpen,
305
- setBottomPanelOpen,
306
- ]
307
- );
308
-
309
- if (!ctx) return null;
310
-
311
- return {
312
- api: ctx.api,
313
- addPanel,
314
- resetLayout,
315
- toggleZone,
316
- };
317
- }
318
-
319
- // -----------------------------------------------------------------------------
320
- // Panel Components - stable references defined outside component
321
- // -----------------------------------------------------------------------------
322
- type ScatterPanelParams = {
323
- layoutKey?: string;
324
- geometry?: Geometry;
325
- };
326
-
327
- const ScatterDockPanel = React.memo(function ScatterDockPanel(
328
- props: IDockviewPanelProps<ScatterPanelParams>
329
- ) {
330
- const params = props.params ?? {};
331
- return (
332
- <ScatterPanel
333
- className="h-full"
334
- layoutKey={params.layoutKey}
335
- geometry={params.geometry}
336
- />
337
- );
338
- });
339
-
340
- // Custom tab component with icon (like Rerun's "Image and segmentation mask" tab)
341
- type TabWithIconProps = IDockviewPanelHeaderProps & {
342
- icon: React.ReactNode;
343
- };
344
-
345
- const TabWithIcon = React.memo(function TabWithIcon({ api, icon }: TabWithIconProps) {
346
- return (
347
- <div className="flex items-center gap-1 text-[12px] leading-[16px] font-medium tracking-[-0.15px]">
348
- <span className="flex-shrink-0">{icon}</span>
349
- <span className="truncate">{api.title}</span>
350
- </div>
351
- );
352
- });
353
-
354
- // Tab components for different panel types
355
- const EuclideanTab = React.memo(function EuclideanTab(props: IDockviewPanelHeaderProps) {
356
- return <TabWithIcon {...props} icon={<Circle className="h-3.5 w-3.5" />} />;
357
- });
358
-
359
- const HyperbolicTab = React.memo(function HyperbolicTab(props: IDockviewPanelHeaderProps) {
360
- return <TabWithIcon {...props} icon={<Disc className="h-3.5 w-3.5" />} />;
361
- });
362
-
363
- const SamplesTab = React.memo(function SamplesTab(props: IDockviewPanelHeaderProps) {
364
- return <TabWithIcon {...props} icon={<Grid3X3 className="h-3.5 w-3.5" />} />;
365
- });
366
-
367
- // Grid panel uses context to get samples
368
- const GridDockPanel = React.memo(function GridDockPanel() {
369
- const ctx = useDockviewContext();
370
- return (
371
- <ImageGrid
372
- samples={ctx.samples}
373
- onLoadMore={ctx.onLoadMore}
374
- hasMore={ctx.hasMore}
375
- />
376
- );
377
- });
378
-
379
- // Explorer panel for left zone
380
- const ExplorerDockPanel = React.memo(function ExplorerDockPanel() {
381
- return <ExplorerPanel />;
382
- });
383
-
384
- // Placeholder panel for right/bottom zones
385
- const PlaceholderDockPanel = React.memo(function PlaceholderDockPanel(
386
- props: IDockviewPanelProps
387
- ) {
388
- const handleClose = React.useCallback(() => {
389
- props.api.close();
390
- }, [props.api]);
391
-
392
- return <PlaceholderPanel onClose={handleClose} />;
393
- });
394
-
395
- // Watermark shown when dock is empty - just the logo, no text
396
- const Watermark = React.memo(function Watermark(_props: IWatermarkPanelProps) {
397
- return (
398
- <div className="flex items-center justify-center h-full w-full">
399
- <div className="text-muted-foreground/20">
400
- <HyperViewLogo className="w-16 h-16" />
401
- </div>
402
- </div>
403
- );
404
- });
405
-
406
- // Stable components object - never changes
407
- const COMPONENTS = {
408
- grid: GridDockPanel,
409
- scatter: ScatterDockPanel,
410
- explorer: ExplorerDockPanel,
411
- placeholder: PlaceholderDockPanel,
412
- };
413
-
414
- // Tab components with icons
415
- const TAB_COMPONENTS = {
416
- euclideanTab: EuclideanTab,
417
- hyperbolicTab: HyperbolicTab,
418
- samplesTab: SamplesTab,
419
- };
420
-
421
- // -----------------------------------------------------------------------------
422
- // Provider Component
423
- // -----------------------------------------------------------------------------
424
- interface DockviewProviderProps {
425
- children: ReactNode;
426
- samples: Sample[];
427
- onLoadMore: () => void;
428
- hasMore: boolean;
429
- }
430
-
431
- export function DockviewProvider({
432
- children,
433
- samples,
434
- onLoadMore,
435
- hasMore,
436
- }: DockviewProviderProps) {
437
- const [api, setApi] = useState<DockviewApi | null>(null);
438
-
439
- const contextValue = useMemo(
440
- () => ({
441
- api,
442
- setApi,
443
- samples,
444
- onLoadMore,
445
- hasMore,
446
- }),
447
- [api, samples, onLoadMore, hasMore]
448
- );
449
-
450
- return (
451
- <DockviewContext.Provider value={contextValue}>
452
- {children}
453
- </DockviewContext.Provider>
454
- );
455
- }
456
-
457
- function applyZonePolicies(api: DockviewApi) {
458
- const explorer = api.getPanel(PANEL.EXPLORER);
459
- if (explorer) {
460
- explorer.group.locked = true;
461
- explorer.group.header.hidden = true;
462
- }
463
-
464
- // Hide tab headers for placeholder panels
465
- const rightPlaceholder = api.getPanel(PANEL.RIGHT_PLACEHOLDER);
466
- if (rightPlaceholder) {
467
- rightPlaceholder.group.header.hidden = true;
468
- }
469
-
470
- const bottomPlaceholder = api.getPanel(PANEL.BOTTOM_PLACEHOLDER);
471
- if (bottomPlaceholder) {
472
- bottomPlaceholder.group.header.hidden = true;
473
- }
474
- }
475
-
476
- // -----------------------------------------------------------------------------
477
- // Workspace Component - the actual dockview renderer
478
- // -----------------------------------------------------------------------------
479
- export function DockviewWorkspace() {
480
- const ctx = useDockviewContext();
481
- const datasetInfo = useStore((state) => state.datasetInfo);
482
- const { setLeftPanelOpen, setRightPanelOpen, setBottomPanelOpen } = useStore();
483
-
484
- const buildDefaultLayout = useCallback(
485
- (api: DockviewApi) => {
486
- const layouts = datasetInfo?.layouts ?? [];
487
- const euclideanLayout = findLayoutByGeometry(layouts, "euclidean");
488
- const poincareLayout = findLayoutByGeometry(layouts, "poincare");
489
- const fallbackLayout = !euclideanLayout && !poincareLayout ? layouts[0] : null;
490
- const hasLayouts = layouts.length > 0;
491
-
492
- // Create the grid panel first (center zone)
493
- const gridPanel =
494
- api.getPanel(PANEL.GRID) ??
495
- api.addPanel({
496
- id: PANEL.GRID,
497
- component: "grid",
498
- title: "Samples",
499
- tabComponent: "samplesTab",
500
- renderer: "always",
501
- });
502
-
503
- let scatterPanel: typeof gridPanel | null = null;
504
-
505
- if (hasLayouts && euclideanLayout) {
506
- scatterPanel =
507
- api.getPanel(PANEL.SCATTER_EUCLIDEAN) ??
508
- api.addPanel({
509
- id: PANEL.SCATTER_EUCLIDEAN,
510
- component: "scatter",
511
- title: "Euclidean",
512
- tabComponent: "euclideanTab",
513
- params: {
514
- layoutKey: euclideanLayout.layout_key,
515
- geometry: "euclidean" as Geometry,
516
- },
517
- position: {
518
- referencePanel: gridPanel.id,
519
- direction: "right",
520
- },
521
- renderer: "always",
522
- });
523
- }
524
-
525
- if (hasLayouts && poincareLayout) {
526
- const position = scatterPanel
527
- ? { referencePanel: scatterPanel.id, direction: "within" as const }
528
- : { referencePanel: gridPanel.id, direction: "right" as const };
529
-
530
- const poincarePanel =
531
- api.getPanel(PANEL.SCATTER_POINCARE) ??
532
- api.addPanel({
533
- id: PANEL.SCATTER_POINCARE,
534
- component: "scatter",
535
- title: "Hyperbolic",
536
- tabComponent: "hyperbolicTab",
537
- params: {
538
- layoutKey: poincareLayout.layout_key,
539
- geometry: "poincare" as Geometry,
540
- },
541
- position,
542
- renderer: "always",
543
- });
544
-
545
- if (!scatterPanel) {
546
- scatterPanel = poincarePanel;
547
- }
548
- }
549
-
550
- if (!hasLayouts) {
551
- const euclideanPanel =
552
- api.getPanel(PANEL.SCATTER_EUCLIDEAN) ??
553
- api.addPanel({
554
- id: PANEL.SCATTER_EUCLIDEAN,
555
- component: "scatter",
556
- title: "Euclidean",
557
- tabComponent: "euclideanTab",
558
- params: {
559
- geometry: "euclidean" as Geometry,
560
- },
561
- position: {
562
- referencePanel: gridPanel.id,
563
- direction: "right",
564
- },
565
- renderer: "always",
566
- });
567
-
568
- api.getPanel(PANEL.SCATTER_POINCARE) ??
569
- api.addPanel({
570
- id: PANEL.SCATTER_POINCARE,
571
- component: "scatter",
572
- title: "Hyperbolic",
573
- tabComponent: "hyperbolicTab",
574
- params: {
575
- geometry: "poincare" as Geometry,
576
- },
577
- position: {
578
- referencePanel: euclideanPanel.id,
579
- direction: "within" as const,
580
- },
581
- renderer: "always",
582
- });
583
-
584
- scatterPanel = euclideanPanel;
585
- }
586
-
587
- if (fallbackLayout && !scatterPanel) {
588
- api.getPanel(PANEL.SCATTER_DEFAULT) ??
589
- api.addPanel({
590
- id: PANEL.SCATTER_DEFAULT,
591
- component: "scatter",
592
- title: "Embeddings",
593
- params: {
594
- layoutKey: fallbackLayout.layout_key,
595
- },
596
- position: {
597
- referencePanel: gridPanel.id,
598
- direction: "right",
599
- },
600
- renderer: "always",
601
- });
602
- }
603
-
604
- const containerWidth = getContainerWidth(api);
605
- const explorerPanel =
606
- api.getPanel(PANEL.EXPLORER) ??
607
- api.addPanel({
608
- id: PANEL.EXPLORER,
609
- component: "explorer",
610
- title: "Explorer",
611
- position: getZonePosition("left"),
612
- initialWidth: getDefaultLeftPanelWidth(containerWidth),
613
- minimumWidth: MIN_SIDE_PANEL_WIDTH,
614
- maximumWidth: getDefaultLeftPanelWidth(containerWidth),
615
- inactive: true,
616
- });
617
-
618
- if (explorerPanel) {
619
- explorerPanel.group.locked = true;
620
- explorerPanel.group.header.hidden = true;
621
- }
622
-
623
- setLeftPanelOpen(!!explorerPanel);
624
- setRightPanelOpen(false);
625
- setBottomPanelOpen(false);
626
- },
627
- [datasetInfo, setLeftPanelOpen, setRightPanelOpen, setBottomPanelOpen]
628
- );
629
-
630
- const onReady = useCallback(
631
- (event: DockviewReadyEvent) => {
632
- ctx.setApi(event.api);
633
-
634
- const stored = localStorage.getItem(LAYOUT_STORAGE_KEY);
635
- if (stored) {
636
- try {
637
- event.api.fromJSON(JSON.parse(stored));
638
- if (event.api.totalPanels === 0) {
639
- localStorage.removeItem(LAYOUT_STORAGE_KEY);
640
- buildDefaultLayout(event.api);
641
- }
642
-
643
- // Re-apply side-zone policies after restore (header hidden, no-drop targets, etc)
644
- applyZonePolicies(event.api);
645
-
646
- // Sync store state with restored layout
647
- setLeftPanelOpen(!!event.api.getPanel(PANEL.EXPLORER));
648
- setRightPanelOpen(!!event.api.getPanel(PANEL.RIGHT_PLACEHOLDER));
649
- setBottomPanelOpen(!!event.api.getPanel(PANEL.BOTTOM_PLACEHOLDER));
650
- return;
651
- } catch (err) {
652
- console.warn("Failed to restore dock layout, resetting.", err);
653
- localStorage.removeItem(LAYOUT_STORAGE_KEY);
654
- }
655
- }
656
-
657
- if (event.api.totalPanels === 0) {
658
- buildDefaultLayout(event.api);
659
- }
660
- },
661
- [buildDefaultLayout, ctx, setLeftPanelOpen, setRightPanelOpen, setBottomPanelOpen]
662
- );
663
-
664
- // Save layout on changes
665
- useEffect(() => {
666
- const api = ctx.api;
667
- if (!api) return;
668
-
669
- const disposable = api.onDidLayoutChange(() => {
670
- if (api.totalPanels === 0) return;
671
- const layout = api.toJSON();
672
- localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify(layout));
673
- });
674
-
675
- return () => disposable.dispose();
676
- }, [ctx.api]);
677
-
678
- // Sync panel state when panels are closed
679
- useEffect(() => {
680
- const api = ctx.api;
681
- if (!api) return;
682
-
683
- const disposable = api.onDidRemovePanel((e) => {
684
- if (e.id === PANEL.EXPLORER) setLeftPanelOpen(false);
685
- if (e.id === PANEL.RIGHT_PLACEHOLDER) setRightPanelOpen(false);
686
- if (e.id === PANEL.BOTTOM_PLACEHOLDER) setBottomPanelOpen(false);
687
- });
688
-
689
- return () => disposable.dispose();
690
- }, [ctx.api, setLeftPanelOpen, setRightPanelOpen, setBottomPanelOpen]);
691
-
692
- // When a real panel is dropped into a placeholder group, close the placeholder
693
- useEffect(() => {
694
- const api = ctx.api;
695
- if (!api) return;
696
-
697
- const disposable = api.onDidAddPanel((e) => {
698
- // Skip if the added panel is a placeholder itself
699
- if (e.id === PANEL.RIGHT_PLACEHOLDER || e.id === PANEL.BOTTOM_PLACEHOLDER) {
700
- return;
701
- }
702
-
703
- // Check if this panel was added to the same group as a placeholder
704
- const group = e.group;
705
- if (!group) return;
706
-
707
- // Find and close any placeholder panels in the same group
708
- const rightPlaceholder = api.getPanel(PANEL.RIGHT_PLACEHOLDER);
709
- const bottomPlaceholder = api.getPanel(PANEL.BOTTOM_PLACEHOLDER);
710
-
711
- if (rightPlaceholder && rightPlaceholder.group?.id === group.id) {
712
- rightPlaceholder.api.close();
713
- }
714
- if (bottomPlaceholder && bottomPlaceholder.group?.id === group.id) {
715
- bottomPlaceholder.api.close();
716
- }
717
- });
718
-
719
- return () => disposable.dispose();
720
- }, [ctx.api]);
721
-
722
- // Prevent dragging locked panels (explorer only)
723
- useEffect(() => {
724
- const api = ctx.api;
725
- if (!api) return;
726
-
727
- const disposable = api.onWillDragPanel((event) => {
728
- if (DRAG_LOCKED_PANEL_IDS.has(event.panel.id)) {
729
- event.nativeEvent.preventDefault();
730
- }
731
- });
732
-
733
- return () => disposable.dispose();
734
- }, [ctx.api]);
735
-
736
- // Rebuild layout when dataset info changes
737
- useEffect(() => {
738
- if (!ctx.api) return;
739
- if (!datasetInfo) return;
740
-
741
- const hasScatter =
742
- ctx.api.getPanel(PANEL.SCATTER_EUCLIDEAN) ||
743
- ctx.api.getPanel(PANEL.SCATTER_POINCARE) ||
744
- ctx.api.getPanel(PANEL.SCATTER_DEFAULT);
745
-
746
- if (!hasScatter) {
747
- buildDefaultLayout(ctx.api);
748
- }
749
- }, [buildDefaultLayout, datasetInfo, ctx.api]);
750
-
751
- return (
752
- <div className="h-full w-full">
753
- <DockviewReact
754
- className="dockview-theme-abyss hyperview-dockview"
755
- components={COMPONENTS}
756
- tabComponents={TAB_COMPONENTS}
757
- onReady={onReady}
758
- theme={themeAbyss}
759
- defaultRenderer="always"
760
- scrollbars="native"
761
- watermarkComponent={Watermark}
762
- />
763
- </div>
764
- );
765
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ExplorerPanel.tsx DELETED
@@ -1,181 +0,0 @@
1
- "use client";
2
-
3
- import React from "react";
4
- import { useStore } from "@/store/useStore";
5
- import { Panel } from "./Panel";
6
- import { PanelHeader } from "./PanelHeader";
7
- import { Tag, Search, ChevronDown, ChevronRight } from "lucide-react";
8
- import { cn } from "@/lib/utils";
9
- import { FALLBACK_LABEL_COLOR, MISSING_LABEL_COLOR, normalizeLabel } from "@/lib/labelColors";
10
- import { useLabelLegend } from "./useLabelLegend";
11
-
12
- interface ExplorerPanelProps {
13
- className?: string;
14
- }
15
-
16
- export function ExplorerPanel({ className }: ExplorerPanelProps) {
17
- const {
18
- datasetInfo,
19
- embeddingsByLayoutKey,
20
- activeLayoutKey,
21
- labelFilter,
22
- setLabelFilter,
23
- } = useStore();
24
- const [labelSearch, setLabelSearch] = React.useState("");
25
- const [isSearchOpen, setIsSearchOpen] = React.useState(false);
26
- const [isLabelsExpanded, setIsLabelsExpanded] = React.useState(true);
27
- const searchInputRef = React.useRef<HTMLInputElement>(null);
28
-
29
- const resolvedLayoutKey =
30
- activeLayoutKey ?? datasetInfo?.layouts?.[0]?.layout_key ?? null;
31
- const embeddings = resolvedLayoutKey
32
- ? embeddingsByLayoutKey[resolvedLayoutKey] ?? null
33
- : null;
34
-
35
- const {
36
- labelCounts,
37
- labelUniverse,
38
- distinctLabelCount,
39
- distinctColoringDisabled,
40
- labelColorMap,
41
- legendLabels,
42
- } = useLabelLegend({ datasetInfo, embeddings, labelSearch, labelFilter });
43
-
44
- const hasCounts = labelCounts.size > 0;
45
- const baseLabelCount = labelUniverse.length > 0
46
- ? labelUniverse.filter((label) => label !== "undefined").length
47
- : distinctLabelCount;
48
- const displayCount = labelSearch.trim().length > 0
49
- ? legendLabels.length
50
- : baseLabelCount;
51
-
52
- const activeLabel = labelFilter ? normalizeLabel(labelFilter) : null;
53
-
54
- // Focus search input when opened
55
- React.useEffect(() => {
56
- if (isSearchOpen && searchInputRef.current) {
57
- searchInputRef.current.focus();
58
- }
59
- }, [isSearchOpen]);
60
-
61
- const handleSearchToggle = () => {
62
- setIsSearchOpen(!isSearchOpen);
63
- if (isSearchOpen) {
64
- setLabelSearch("");
65
- }
66
- };
67
-
68
- return (
69
- <Panel className={cn("h-full flex flex-col", className)}>
70
- <PanelHeader title="Explorer" />
71
-
72
- {/* Scrollable content area */}
73
- <div className="flex-1 min-h-0 overflow-auto panel-scroll">
74
- {/* Labels section */}
75
- <div className="border-b border-border">
76
- {/* Section header - collapsible with search icon */}
77
- <div className="flex items-center h-6 px-2 bg-secondary/50">
78
- <button
79
- onClick={() => setIsLabelsExpanded(!isLabelsExpanded)}
80
- className="flex items-center gap-1.5 flex-1 min-w-0 text-left hover:bg-muted/30 rounded px-1 -ml-1"
81
- >
82
- {isLabelsExpanded ? (
83
- <ChevronDown className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
84
- ) : (
85
- <ChevronRight className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
86
- )}
87
- <Tag className="h-3 w-3 flex-shrink-0 text-muted-foreground" />
88
- <span className="text-xs text-muted-foreground truncate">
89
- Labels
90
- </span>
91
- <span className="ml-auto text-xs text-muted-foreground/50 tabular-nums">
92
- {displayCount}
93
- </span>
94
- </button>
95
-
96
- {/* Search toggle button */}
97
- <button
98
- onClick={handleSearchToggle}
99
- className={cn(
100
- "h-5 w-5 flex items-center justify-center rounded text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted/50 transition-colors ml-1",
101
- isSearchOpen && "text-muted-foreground bg-muted/50"
102
- )}
103
- title="Search labels"
104
- >
105
- <Search className="h-3 w-3" />
106
- </button>
107
- </div>
108
-
109
- {/* Search input - shown when search is toggled */}
110
- {isSearchOpen && (
111
- <div className="px-2 py-1.5 bg-secondary/30 border-b border-border/50">
112
- <input
113
- ref={searchInputRef}
114
- value={labelSearch}
115
- onChange={(e) => setLabelSearch(e.target.value)}
116
- placeholder="Filter labels..."
117
- className="w-full h-6 px-2 rounded bg-background border border-border text-[12px] leading-[16px] text-foreground placeholder:text-muted-foreground/50 outline-none focus:ring-1 focus:ring-ring focus:border-ring"
118
- />
119
- </div>
120
- )}
121
-
122
- {/* Labels list - collapsible */}
123
- {isLabelsExpanded && (
124
- <div className="py-1">
125
- {distinctColoringDisabled && (
126
- <div className="px-3 py-1 text-[10px] text-muted-foreground/60">
127
- Too many labels ({distinctLabelCount}) to color distinctly; using one color.
128
- </div>
129
- )}
130
-
131
- {legendLabels.length === 0 ? (
132
- <div className="px-3 py-2 text-[11px] text-muted-foreground/50">
133
- No labels available
134
- </div>
135
- ) : (
136
- <div className="space-y-px">
137
- {legendLabels.map((label) => {
138
- const color =
139
- label === "undefined"
140
- ? MISSING_LABEL_COLOR
141
- : labelColorMap[label] ?? FALLBACK_LABEL_COLOR;
142
- const normalized = normalizeLabel(label);
143
- const isActive = activeLabel === normalized;
144
- const isDimmed = activeLabel && !isActive;
145
-
146
- return (
147
- <button
148
- key={label}
149
- type="button"
150
- onClick={() => setLabelFilter(isActive ? null : normalized)}
151
- className={cn(
152
- "flex items-center gap-2 w-full h-6 px-3 text-[12px] leading-[16px] text-left text-muted-foreground hover:text-foreground",
153
- "hover:bg-muted/40 transition-colors",
154
- isActive && "bg-muted/60 text-foreground",
155
- isDimmed && "opacity-40"
156
- )}
157
- >
158
- <span
159
- className="w-2 h-2 rounded-full flex-shrink-0"
160
- style={{ backgroundColor: color }}
161
- />
162
- <span className="truncate flex-1" title={label}>
163
- {label}
164
- </span>
165
- {hasCounts && (
166
- <span className="text-[10px] text-muted-foreground/50 font-mono tabular-nums flex-shrink-0">
167
- {labelCounts.get(label) ?? 0}
168
- </span>
169
- )}
170
- </button>
171
- );
172
- })}
173
- </div>
174
- )}
175
- </div>
176
- )}
177
- </div>
178
- </div>
179
- </Panel>
180
- );
181
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/Header.tsx DELETED
@@ -1,244 +0,0 @@
1
- "use client";
2
-
3
- import { useStore } from "@/store/useStore";
4
- import { Button } from "@/components/ui/button";
5
- import { HyperViewLogo, DiscordIcon } from "./icons";
6
- import { CENTER_PANEL_DEFS, useDockviewApi } from "./DockviewWorkspace";
7
- import {
8
- DropdownMenu,
9
- DropdownMenuContent,
10
- DropdownMenuItem,
11
- DropdownMenuTrigger,
12
- } from "@/components/ui/dropdown-menu";
13
- import {
14
- Popover,
15
- PopoverContent,
16
- PopoverTrigger,
17
- } from "@/components/ui/popover";
18
- import {
19
- Command,
20
- CommandEmpty,
21
- CommandGroup,
22
- CommandInput,
23
- CommandItem,
24
- CommandList,
25
- } from "@/components/ui/command";
26
- import {
27
- ChevronDown,
28
- RotateCcw,
29
- Check,
30
- PanelLeft,
31
- PanelBottom,
32
- PanelRight,
33
- Settings,
34
- Search,
35
- } from "lucide-react";
36
- import { useState } from "react";
37
- import { cn } from "@/lib/utils";
38
-
39
- const PANEL_CONFIG = CENTER_PANEL_DEFS;
40
- const DISCORD_URL = process.env.NEXT_PUBLIC_DISCORD_URL ?? "https://discord.gg/Qf2pXtY4Vf";
41
-
42
- export function Header() {
43
- const { datasetInfo, leftPanelOpen, rightPanelOpen, bottomPanelOpen } = useStore();
44
- const dockview = useDockviewApi();
45
- const [datasetPickerOpen, setDatasetPickerOpen] = useState(false);
46
-
47
- const handlePanelToggle = (panelId: string) => {
48
- if (!dockview?.api) return;
49
- const panel = dockview.api.getPanel(panelId);
50
- if (panel) {
51
- panel.api.close();
52
- return;
53
- }
54
- dockview.addPanel(panelId);
55
- };
56
-
57
- // Check which panels are currently open
58
- const openPanels = new Set(
59
- PANEL_CONFIG.map((p) => p.id).filter((id) => dockview?.api?.getPanel(id))
60
- );
61
-
62
- return (
63
- <header className="h-7 min-h-[28px] bg-secondary border-b border-border flex items-center justify-between px-2">
64
- {/* Left side: Logo + View menu */}
65
- <div className="flex items-center gap-2">
66
- {/* Logo */}
67
- <div className="flex items-center justify-center h-6 w-6 text-primary">
68
- <HyperViewLogo className="h-3.5 w-3.5" />
69
- </div>
70
-
71
- {/* View dropdown */}
72
- {dockview && (
73
- <DropdownMenu>
74
- <DropdownMenuTrigger asChild>
75
- <Button
76
- variant="ghost"
77
- size="sm"
78
- className="h-6 px-2 text-[12px] leading-[16px] tracking-[-0.15px] text-muted-foreground hover:text-foreground hover:bg-muted/50"
79
- >
80
- View
81
- <ChevronDown className="ml-0.5 h-2.5 w-2.5" />
82
- </Button>
83
- </DropdownMenuTrigger>
84
- <DropdownMenuContent align="start" className="w-48">
85
- {/* Panel toggles - no section header, similar to Rerun */}
86
- {PANEL_CONFIG.map((panel) => {
87
- const Icon = panel.icon;
88
- const isOpen = openPanels.has(panel.id);
89
- return (
90
- <DropdownMenuItem
91
- key={panel.id}
92
- onClick={() => handlePanelToggle(panel.id)}
93
- className="flex items-center justify-between h-7 text-[12px] leading-[16px]"
94
- >
95
- <span className="flex items-center gap-2">
96
- <Icon className="h-3.5 w-3.5" />
97
- {panel.label}
98
- </span>
99
- {isOpen && <Check className="h-3.5 w-3.5 text-primary" />}
100
- </DropdownMenuItem>
101
- );
102
- })}
103
-
104
- {/* Spacer */}
105
- <div className="h-2" />
106
-
107
- {/* Reset layout */}
108
- <DropdownMenuItem
109
- onClick={() => dockview.resetLayout()}
110
- className="flex items-center gap-2 h-7 text-[12px] leading-[16px]"
111
- >
112
- <RotateCcw className="h-3.5 w-3.5" />
113
- Reset Layout
114
- </DropdownMenuItem>
115
- </DropdownMenuContent>
116
- </DropdownMenu>
117
- )}
118
- </div>
119
-
120
- {/* Center: Dataset picker (VS Code style command palette trigger) */}
121
- <div className="flex-1 flex justify-center px-4">
122
- <Popover open={datasetPickerOpen} onOpenChange={setDatasetPickerOpen}>
123
- <PopoverTrigger asChild>
124
- <Button
125
- variant="ghost"
126
- size="sm"
127
- role="combobox"
128
- aria-expanded={datasetPickerOpen}
129
- className="h-6 min-w-[200px] max-w-[400px] px-3 text-[12px] leading-[16px] tracking-[-0.15px] text-muted-foreground hover:text-foreground bg-muted/40 hover:bg-muted/60 border border-border/50 rounded-md justify-start gap-2"
130
- >
131
- <Search className="h-3 w-3 flex-shrink-0 opacity-50" />
132
- <span className="truncate flex-1 text-left">
133
- {datasetInfo?.name ?? "No dataset loaded"}
134
- </span>
135
- </Button>
136
- </PopoverTrigger>
137
- <PopoverContent className="w-[280px] p-0" align="center">
138
- <Command>
139
- <CommandInput
140
- placeholder="Search datasets..."
141
- className="h-6 text-[12px] leading-[16px]"
142
- />
143
- <CommandList>
144
- <CommandEmpty className="py-4 text-xs text-center">
145
- No datasets found.
146
- </CommandEmpty>
147
- <CommandGroup>
148
- {/* Currently only show the loaded dataset */}
149
- {datasetInfo && (
150
- <CommandItem
151
- value={datasetInfo.name}
152
- onSelect={() => setDatasetPickerOpen(false)}
153
- className="text-[12px] leading-[16px]"
154
- >
155
- <span className="flex-1 truncate">{datasetInfo.name}</span>
156
- <span className="text-[10px] text-muted-foreground ml-2">
157
- {datasetInfo.num_samples.toLocaleString()} samples
158
- </span>
159
- <Check className="h-3 w-3 ml-2 text-primary" />
160
- </CommandItem>
161
- )}
162
- </CommandGroup>
163
- </CommandList>
164
- </Command>
165
- </PopoverContent>
166
- </Popover>
167
- </div>
168
-
169
- {/* Right side: Discord + Panel toggles + Settings */}
170
- <div className="flex items-center gap-0.5">
171
- {/* Discord link */}
172
- <a
173
- href={DISCORD_URL}
174
- target="_blank"
175
- rel="noopener noreferrer"
176
- className="h-6 w-6 p-0 flex items-center justify-center rounded-sm text-muted-foreground hover:text-foreground hover:bg-muted/40 transition-colors"
177
- title="Join our Discord community"
178
- >
179
- <DiscordIcon className="h-3.5 w-3.5" />
180
- </a>
181
-
182
- {/* Separator */}
183
- <div className="w-px h-3 bg-border mx-1" />
184
-
185
- {/* Left panel toggle */}
186
- <Button
187
- variant="ghost"
188
- size="sm"
189
- onClick={() => dockview?.toggleZone("left")}
190
- className={cn(
191
- "h-6 w-6 p-0",
192
- leftPanelOpen
193
- ? "text-foreground bg-muted/50"
194
- : "text-muted-foreground hover:text-foreground hover:bg-muted/40"
195
- )}
196
- >
197
- <PanelLeft className="h-3.5 w-3.5" />
198
- </Button>
199
-
200
- {/* Bottom panel toggle */}
201
- <Button
202
- variant="ghost"
203
- size="sm"
204
- onClick={() => dockview?.toggleZone("bottom")}
205
- className={cn(
206
- "h-6 w-6 p-0",
207
- bottomPanelOpen
208
- ? "text-foreground bg-muted/50"
209
- : "text-muted-foreground hover:text-foreground hover:bg-muted/40"
210
- )}
211
- >
212
- <PanelBottom className="h-3.5 w-3.5" />
213
- </Button>
214
-
215
- {/* Right panel toggle */}
216
- <Button
217
- variant="ghost"
218
- size="sm"
219
- onClick={() => dockview?.toggleZone("right")}
220
- className={cn(
221
- "h-6 w-6 p-0",
222
- rightPanelOpen
223
- ? "text-foreground bg-muted/50"
224
- : "text-muted-foreground hover:text-foreground hover:bg-muted/40"
225
- )}
226
- >
227
- <PanelRight className="h-3.5 w-3.5" />
228
- </Button>
229
-
230
- {/* Separator */}
231
- <div className="w-px h-3 bg-border mx-1" />
232
-
233
- {/* Settings button */}
234
- <Button
235
- variant="ghost"
236
- size="sm"
237
- className="h-6 w-6 p-0 text-muted-foreground hover:text-foreground hover:bg-muted/40"
238
- >
239
- <Settings className="h-3.5 w-3.5" />
240
- </Button>
241
- </div>
242
- </header>
243
- );
244
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ImageGrid.tsx DELETED
@@ -1,338 +0,0 @@
1
- "use client";
2
-
3
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
- import { useVirtualizer } from "@tanstack/react-virtual";
5
- import justifiedLayout from "justified-layout";
6
- import { useStore } from "@/store/useStore";
7
- import { Panel } from "./Panel";
8
- import { CheckIcon } from "./icons";
9
- import type { Sample } from "@/types";
10
-
11
- interface ImageGridProps {
12
- samples: Sample[];
13
- onLoadMore?: () => void;
14
- hasMore?: boolean;
15
- }
16
-
17
- // Justified layout config
18
- const BOX_SPACING = 2; // Tight spacing between images
19
- const TARGET_ROW_HEIGHT = 180; // Target height for rows
20
- const DEFAULT_ASPECT_RATIO = 1; // Fallback for samples without dimensions
21
-
22
- /**
23
- * Get aspect ratio from sample, with fallback
24
- */
25
- function getAspectRatio(sample: Sample): number {
26
- if (sample.width && sample.height && sample.height > 0) {
27
- return sample.width / sample.height;
28
- }
29
- return DEFAULT_ASPECT_RATIO;
30
- }
31
-
32
- /**
33
- * Compute justified layout geometry for samples
34
- */
35
- function computeLayout(
36
- samples: Sample[],
37
- containerWidth: number
38
- ): { boxes: Array<{ width: number; height: number; top: number; left: number }>; containerHeight: number } {
39
- if (samples.length === 0 || containerWidth <= 0) {
40
- return { boxes: [], containerHeight: 0 };
41
- }
42
-
43
- const aspectRatios = samples.map(getAspectRatio);
44
-
45
- const geometry = justifiedLayout(aspectRatios, {
46
- containerWidth,
47
- containerPadding: 0,
48
- boxSpacing: BOX_SPACING,
49
- targetRowHeight: TARGET_ROW_HEIGHT,
50
- targetRowHeightTolerance: 0.25,
51
- showWidows: true, // Always show last row even if incomplete
52
- });
53
-
54
- return {
55
- boxes: geometry.boxes,
56
- containerHeight: geometry.containerHeight,
57
- };
58
- }
59
-
60
- /**
61
- * Group boxes into rows for virtualization
62
- */
63
- interface RowData {
64
- startIndex: number;
65
- endIndex: number; // exclusive
66
- top: number;
67
- height: number;
68
- }
69
-
70
- function groupIntoRows(
71
- boxes: Array<{ width: number; height: number; top: number; left: number }>
72
- ): RowData[] {
73
- if (boxes.length === 0) return [];
74
-
75
- const rows: RowData[] = [];
76
- let currentRowTop = boxes[0].top;
77
- let currentRowStart = 0;
78
- let currentRowHeight = boxes[0].height;
79
-
80
- for (let i = 1; i < boxes.length; i++) {
81
- const box = boxes[i];
82
- // New row if top position changes significantly
83
- if (Math.abs(box.top - currentRowTop) > 1) {
84
- rows.push({
85
- startIndex: currentRowStart,
86
- endIndex: i,
87
- top: currentRowTop,
88
- height: currentRowHeight,
89
- });
90
- currentRowStart = i;
91
- currentRowTop = box.top;
92
- currentRowHeight = box.height;
93
- } else {
94
- // Same row - take max height in case of slight variations
95
- currentRowHeight = Math.max(currentRowHeight, box.height);
96
- }
97
- }
98
-
99
- // Push final row
100
- rows.push({
101
- startIndex: currentRowStart,
102
- endIndex: boxes.length,
103
- top: currentRowTop,
104
- height: currentRowHeight,
105
- });
106
-
107
- return rows;
108
- }
109
-
110
- export function ImageGrid({ samples, onLoadMore, hasMore }: ImageGridProps) {
111
- const containerRef = useRef<HTMLDivElement>(null);
112
- const [containerWidth, setContainerWidth] = useState(0);
113
-
114
- const {
115
- selectedIds,
116
- isLassoSelection,
117
- selectionSource,
118
- toggleSelection,
119
- addToSelection,
120
- setHoveredId,
121
- hoveredId,
122
- labelFilter,
123
- } = useStore();
124
-
125
- // Track container width for layout computation
126
- useEffect(() => {
127
- const container = containerRef.current;
128
- if (!container) return;
129
-
130
- const updateWidth = () => {
131
- const width = container.clientWidth;
132
- if (width > 0 && width !== containerWidth) {
133
- setContainerWidth(width);
134
- }
135
- };
136
-
137
- updateWidth();
138
-
139
- const resizeObserver = new ResizeObserver(() => {
140
- requestAnimationFrame(updateWidth);
141
- });
142
- resizeObserver.observe(container);
143
-
144
- return () => resizeObserver.disconnect();
145
- }, [containerWidth]);
146
-
147
- // Compute justified layout
148
- const { boxes, containerHeight } = useMemo(
149
- () => computeLayout(samples, containerWidth),
150
- [samples, containerWidth]
151
- );
152
-
153
- // Group into rows for virtualization
154
- const rows = useMemo(() => groupIntoRows(boxes), [boxes]);
155
-
156
- // Virtualizer for rows
157
- const virtualizer = useVirtualizer({
158
- count: rows.length,
159
- getScrollElement: () => containerRef.current,
160
- estimateSize: (index) => rows[index]?.height ?? TARGET_ROW_HEIGHT,
161
- overscan: 3,
162
- getItemKey: (index) => {
163
- const row = rows[index];
164
- if (!row) return `row-${index}`;
165
- // Create stable key from sample IDs in this row
166
- const rowSamples = samples.slice(row.startIndex, row.endIndex);
167
- return rowSamples.map((s) => s.id).join("-") || `row-${index}`;
168
- },
169
- });
170
-
171
- // Load more when scrolling near bottom
172
- useEffect(() => {
173
- const container = containerRef.current;
174
- if (!container || !onLoadMore || !hasMore) return;
175
-
176
- const handleScroll = () => {
177
- const { scrollTop, scrollHeight, clientHeight } = container;
178
- if (scrollHeight - scrollTop - clientHeight < 500) {
179
- onLoadMore();
180
- }
181
- };
182
-
183
- container.addEventListener("scroll", handleScroll);
184
- return () => container.removeEventListener("scroll", handleScroll);
185
- }, [onLoadMore, hasMore]);
186
-
187
- // Reset scroll on filter change
188
- useEffect(() => {
189
- containerRef.current?.scrollTo({ top: 0 });
190
- }, [labelFilter]);
191
-
192
- // Scroll to top when scatter selection made
193
- useEffect(() => {
194
- if (isLassoSelection) return;
195
- if (selectionSource !== "scatter") return;
196
- if (selectedIds.size === 0) return;
197
-
198
- try {
199
- virtualizer.scrollToIndex(0, { align: "start" });
200
- } catch {
201
- containerRef.current?.scrollTo({ top: 0 });
202
- }
203
- }, [isLassoSelection, selectedIds, selectionSource, virtualizer]);
204
-
205
- // Handle click with selection logic
206
- const handleClick = useCallback(
207
- (sample: Sample, event: React.MouseEvent) => {
208
- if (event.metaKey || event.ctrlKey) {
209
- toggleSelection(sample.id);
210
- } else if (event.shiftKey && selectedIds.size > 0) {
211
- const selectedArray = Array.from(selectedIds);
212
- const lastSelected = selectedArray[selectedArray.length - 1];
213
- const lastIndex = samples.findIndex((s) => s.id === lastSelected);
214
- const currentIndex = samples.findIndex((s) => s.id === sample.id);
215
-
216
- if (lastIndex !== -1 && currentIndex !== -1) {
217
- const start = Math.min(lastIndex, currentIndex);
218
- const end = Math.max(lastIndex, currentIndex);
219
- const rangeIds = samples.slice(start, end + 1).map((s) => s.id);
220
- addToSelection(rangeIds);
221
- }
222
- } else {
223
- const newSet = new Set<string>();
224
- newSet.add(sample.id);
225
- useStore.getState().setSelectedIds(newSet, "grid");
226
- }
227
- },
228
- [samples, selectedIds, toggleSelection, addToSelection]
229
- );
230
-
231
- const virtualRows = virtualizer.getVirtualItems();
232
-
233
- return (
234
- <Panel>
235
- <div className="flex-1 min-h-0 overflow-hidden">
236
- <div ref={containerRef} className="panel-scroll h-full min-h-0 overflow-auto">
237
- <div
238
- style={{
239
- height: containerHeight || "100%",
240
- width: "100%",
241
- position: "relative",
242
- }}
243
- >
244
- {virtualRows.map((virtualRow) => {
245
- const row = rows[virtualRow.index];
246
- if (!row) return null;
247
-
248
- const rowSamples = samples.slice(row.startIndex, row.endIndex);
249
- const rowBoxes = boxes.slice(row.startIndex, row.endIndex);
250
-
251
- return (
252
- <div
253
- key={virtualRow.key}
254
- style={{
255
- position: "absolute",
256
- top: 0,
257
- left: 0,
258
- width: "100%",
259
- height: row.height,
260
- transform: `translateY(${row.top}px)`,
261
- }}
262
- >
263
- {rowSamples.map((sample, i) => {
264
- const box = rowBoxes[i];
265
- if (!box) return null;
266
-
267
- const isSelected = isLassoSelection ? true : selectedIds.has(sample.id);
268
- const isHovered = hoveredId === sample.id;
269
-
270
- return (
271
- <div
272
- key={sample.id}
273
- style={{
274
- position: "absolute",
275
- left: box.left,
276
- top: 0,
277
- width: box.width,
278
- height: box.height,
279
- }}
280
- className={`
281
- overflow-hidden cursor-pointer
282
- transition-shadow duration-150 ease-out
283
- ${isSelected ? "ring-2 ring-inset ring-primary" : ""}
284
- ${isHovered && !isSelected ? "ring-2 ring-inset ring-primary/50" : ""}
285
- `}
286
- onClick={(e) => handleClick(sample, e)}
287
- onMouseEnter={() => setHoveredId(sample.id)}
288
- onMouseLeave={() => setHoveredId(null)}
289
- >
290
- {/* Image container - justified layout sizes tile to preserve aspect ratio */}
291
- {/* Future: overlays (segmentations, bboxes) will be absolutely positioned here */}
292
- {sample.thumbnail ? (
293
- // eslint-disable-next-line @next/next/no-img-element
294
- <img
295
- src={`data:image/jpeg;base64,${sample.thumbnail}`}
296
- alt={sample.filename}
297
- className="w-full h-full object-cover"
298
- loading="lazy"
299
- />
300
- ) : (
301
- <div className="w-full h-full bg-muted flex items-center justify-center">
302
- <span className="text-muted-foreground text-xs">No image</span>
303
- </div>
304
- )}
305
-
306
- {/* Label badge */}
307
- {sample.label && (
308
- <div className="absolute bottom-0.5 left-0.5 right-0.5">
309
- <span
310
- className="inline-block px-1 py-0.5 text-[10px] leading-tight truncate max-w-full"
311
- style={{
312
- backgroundColor: "rgba(0,0,0,0.7)",
313
- color: "#fff",
314
- }}
315
- >
316
- {sample.label}
317
- </span>
318
- </div>
319
- )}
320
-
321
- {/* Selection indicator */}
322
- {isSelected && (
323
- <div className="absolute top-0.5 right-0.5 w-4 h-4 rounded-full bg-primary flex items-center justify-center">
324
- <CheckIcon />
325
- </div>
326
- )}
327
- </div>
328
- );
329
- })}
330
- </div>
331
- );
332
- })}
333
- </div>
334
- </div>
335
- </div>
336
- </Panel>
337
- );
338
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/Panel.tsx DELETED
@@ -1,43 +0,0 @@
1
- "use client";
2
-
3
- import { ReactNode } from "react";
4
- import { cn } from "@/lib/utils";
5
-
6
- interface PanelProps {
7
- children: ReactNode;
8
- className?: string;
9
- }
10
-
11
- /**
12
- * Base panel container with consistent Rerun-style appearance.
13
- * No borders or rounded corners - panels should be flush against each other.
14
- */
15
- export function Panel({ children, className }: PanelProps) {
16
- return (
17
- <div className={cn(
18
- "flex flex-col h-full bg-card overflow-hidden",
19
- className
20
- )}>
21
- {children}
22
- </div>
23
- );
24
- }
25
-
26
- interface PanelFooterProps {
27
- children: ReactNode;
28
- className?: string;
29
- }
30
-
31
- /**
32
- * Panel footer for keyboard shortcuts/hints.
33
- */
34
- export function PanelFooter({ children, className }: PanelFooterProps) {
35
- return (
36
- <div className={cn(
37
- "px-3 py-1 text-[11px] text-muted-foreground/70 border-t border-border bg-card font-mono",
38
- className
39
- )}>
40
- {children}
41
- </div>
42
- );
43
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/PanelHeader.tsx DELETED
@@ -1,47 +0,0 @@
1
- "use client";
2
-
3
- import { ReactNode } from "react";
4
- import { cn } from "@/lib/utils";
5
-
6
- interface PanelHeaderProps {
7
- icon?: ReactNode;
8
- title: string;
9
- subtitle?: string;
10
- children?: ReactNode; // Toolbar actions slot
11
- className?: string;
12
- }
13
-
14
- /**
15
- * Rerun-style panel header with icon, title, and optional toolbar.
16
- *
17
- * Design tokens (from Rerun):
18
- * - Title bar height: 24px
19
- * - Icon size: 14px (3.5 tailwind units)
20
- * - Icon-to-text gap: 4px (gap-1)
21
- * - Font size: 12px with -0.15px tracking
22
- * - Section header font: 11px uppercase
23
- */
24
- export function PanelHeader({ icon, title, subtitle, children, className }: PanelHeaderProps) {
25
- return (
26
- <div className={cn(
27
- // 24px height matches Rerun's title_bar_height()
28
- "h-6 min-h-[24px] flex items-center justify-between px-3 border-b border-border bg-secondary select-none",
29
- className
30
- )}>
31
- <div className="flex items-center gap-1 min-w-0">
32
- {icon && (
33
- // 14px icon (3.5 tailwind units) with 4px gap to text
34
- <span className="flex-shrink-0 w-3.5 h-3.5 text-muted-foreground">{icon}</span>
35
- )}
36
- {/* 12px font, medium weight, tight tracking */}
37
- <span className="text-[12px] leading-[16px] font-medium tracking-[-0.15px] text-foreground truncate">{title}</span>
38
- {subtitle && (
39
- <span className="text-[11px] leading-4 text-muted-foreground truncate">{subtitle}</span>
40
- )}
41
- </div>
42
- {children && (
43
- <div className="flex items-center gap-1">{children}</div>
44
- )}
45
- </div>
46
- );
47
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/PlaceholderPanel.tsx DELETED
@@ -1,39 +0,0 @@
1
- "use client";
2
-
3
- import React from "react";
4
- import { HyperViewLogo } from "./icons";
5
- import { Panel } from "./Panel";
6
- import { cn } from "@/lib/utils";
7
- import { X } from "lucide-react";
8
-
9
- interface PlaceholderPanelProps {
10
- className?: string;
11
- onClose?: () => void;
12
- }
13
-
14
- /**
15
- * Empty placeholder panel with centered HyperView logo and a close button.
16
- * Used for right and bottom zones that are reserved for future features.
17
- * The close button is always visible in the top-right corner of the panel content.
18
- */
19
- export function PlaceholderPanel({ className, onClose }: PlaceholderPanelProps) {
20
- return (
21
- <Panel className={cn("h-full w-full relative", className)}>
22
- {/* Close button always visible in top right */}
23
- {onClose && (
24
- <button
25
- onClick={onClose}
26
- className="absolute top-3 right-3 p-1.5 rounded-md text-muted-foreground/60 hover:text-muted-foreground hover:bg-muted/60 transition-colors z-10"
27
- aria-label="Close panel"
28
- >
29
- <X className="h-4 w-4" />
30
- </button>
31
- )}
32
- <div className="flex-1 flex items-center justify-center">
33
- <div className="text-muted-foreground/20">
34
- <HyperViewLogo className="w-12 h-12" />
35
- </div>
36
- </div>
37
- </Panel>
38
- );
39
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ScatterPanel.tsx DELETED
@@ -1,174 +0,0 @@
1
- "use client";
2
-
3
- import { useCallback, useEffect, useMemo, useState } from "react";
4
- import { useStore } from "@/store/useStore";
5
- import { Panel } from "./Panel";
6
- import { useHyperScatter } from "./useHyperScatter";
7
- import { useLabelLegend } from "./useLabelLegend";
8
- import type { Geometry } from "@/types";
9
- import { findLayoutByGeometry, listAvailableGeometries } from "@/lib/layouts";
10
- import { fetchEmbeddings } from "@/lib/api";
11
-
12
- interface ScatterPanelProps {
13
- className?: string;
14
- layoutKey?: string;
15
- geometry?: Geometry;
16
- }
17
-
18
- export function ScatterPanel({
19
- className = "",
20
- layoutKey,
21
- geometry,
22
- }: ScatterPanelProps) {
23
- const {
24
- datasetInfo,
25
- embeddingsByLayoutKey,
26
- setEmbeddingsForLayout,
27
- selectedIds,
28
- setSelectedIds,
29
- beginLassoSelection,
30
- hoveredId,
31
- setHoveredId,
32
- setActiveLayoutKey,
33
- labelFilter,
34
- } = useStore();
35
-
36
- const [localGeometry, setLocalGeometry] = useState<Geometry>("euclidean");
37
-
38
- // Check which geometries are available
39
- const availableGeometries = useMemo(() => {
40
- return listAvailableGeometries(datasetInfo?.layouts ?? []);
41
- }, [datasetInfo?.layouts]);
42
-
43
- useEffect(() => {
44
- if (geometry) return;
45
- if (availableGeometries.length === 0) return;
46
- if (!availableGeometries.includes(localGeometry)) {
47
- setLocalGeometry(availableGeometries[0]);
48
- }
49
- }, [availableGeometries, geometry, localGeometry]);
50
-
51
- const resolvedGeometry = geometry ?? localGeometry;
52
-
53
- const resolvedLayoutKey = useMemo(() => {
54
- if (!datasetInfo) return layoutKey ?? null;
55
-
56
- if (layoutKey) {
57
- const exists = datasetInfo.layouts.some((layout) => layout.layout_key === layoutKey);
58
- if (exists) return layoutKey;
59
- }
60
-
61
- const layout = findLayoutByGeometry(datasetInfo.layouts, resolvedGeometry);
62
- return layout?.layout_key ?? datasetInfo.layouts[0]?.layout_key ?? null;
63
- }, [datasetInfo, layoutKey, resolvedGeometry]);
64
-
65
- const embeddings = resolvedLayoutKey ? embeddingsByLayoutKey[resolvedLayoutKey] ?? null : null;
66
-
67
- useEffect(() => {
68
- if (!resolvedLayoutKey) return;
69
- setActiveLayoutKey(resolvedLayoutKey);
70
- }, [resolvedLayoutKey, setActiveLayoutKey]);
71
-
72
- useEffect(() => {
73
- if (!resolvedLayoutKey) return;
74
- if (embeddingsByLayoutKey[resolvedLayoutKey]) return;
75
-
76
- let cancelled = false;
77
-
78
- fetchEmbeddings(resolvedLayoutKey)
79
- .then((data) => {
80
- if (cancelled) return;
81
- setEmbeddingsForLayout(resolvedLayoutKey, data);
82
- })
83
- .catch((err) => {
84
- if (cancelled) return;
85
- console.error("Failed to load embeddings:", err);
86
- })
87
-
88
- return () => {
89
- cancelled = true;
90
- };
91
- }, [embeddingsByLayoutKey, resolvedLayoutKey, setEmbeddingsForLayout]);
92
-
93
- const { labelsInfo } = useLabelLegend({ datasetInfo, embeddings, labelFilter });
94
-
95
- const {
96
- canvasRef,
97
- overlayCanvasRef,
98
- containerRef,
99
- handlePointerDown,
100
- handlePointerMove,
101
- handlePointerUp,
102
- handlePointerLeave,
103
- handleDoubleClick,
104
- rendererError,
105
- } = useHyperScatter({
106
- embeddings,
107
- labelsInfo,
108
- selectedIds,
109
- hoveredId,
110
- setSelectedIds,
111
- beginLassoSelection,
112
- setHoveredId,
113
- hoverEnabled: !labelFilter,
114
- });
115
-
116
- const focusLayout = useCallback(() => {
117
- if (!resolvedLayoutKey) return;
118
- setActiveLayoutKey(resolvedLayoutKey);
119
- }, [resolvedLayoutKey, setActiveLayoutKey]);
120
-
121
- const loadingLabel = resolvedLayoutKey
122
- ? "Loading embeddings..."
123
- : "No embeddings layout available";
124
-
125
- return (
126
- <Panel className={className}>
127
- {/* Main content area - min-h-0 prevents flex overflow */}
128
- <div className="flex-1 flex min-h-0">
129
- {/* Canvas container */}
130
- <div ref={containerRef} className="flex-1 relative min-w-0">
131
- <canvas
132
- ref={canvasRef}
133
- className="absolute inset-0"
134
- style={{ zIndex: 1 }}
135
- onPointerDown={(e) => {
136
- focusLayout();
137
- handlePointerDown(e);
138
- }}
139
- onPointerMove={handlePointerMove}
140
- onPointerUp={handlePointerUp}
141
- onPointerCancel={handlePointerUp}
142
- onPointerLeave={handlePointerLeave}
143
- onDoubleClick={handleDoubleClick}
144
- onPointerEnter={focusLayout}
145
- />
146
-
147
- {/* Lasso overlay (screen-space) */}
148
- <canvas
149
- ref={overlayCanvasRef}
150
- className="absolute inset-0 pointer-events-none"
151
- style={{ zIndex: 20 }}
152
- />
153
-
154
- {/* Loading overlay */}
155
- {rendererError ? (
156
- <div className="absolute inset-0 flex items-center justify-center bg-card/85 z-10 p-6">
157
- <div className="max-w-md text-center">
158
- <div className="text-sm font-semibold text-foreground mb-2">Browser not supported</div>
159
- <div className="text-sm text-muted-foreground">{rendererError}</div>
160
- </div>
161
- </div>
162
- ) : (
163
- !embeddings && (
164
- <div className="absolute inset-0 flex items-center justify-center bg-card/80 z-10">
165
- <div className="text-muted-foreground">{loadingLabel}</div>
166
- </div>
167
- )
168
- )}
169
- </div>
170
-
171
- </div>
172
- </Panel>
173
- );
174
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/icons.tsx DELETED
@@ -1,73 +0,0 @@
1
- "use client";
2
-
3
- /**
4
- * Shared icons for HyperView UI.
5
- * Using inline SVGs for simplicity (no extra icon library dependency).
6
- */
7
-
8
- export const GridIcon = () => (
9
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-4 h-4">
10
- <rect x="3" y="3" width="7" height="7" />
11
- <rect x="14" y="3" width="7" height="7" />
12
- <rect x="3" y="14" width="7" height="7" />
13
- <rect x="14" y="14" width="7" height="7" />
14
- </svg>
15
- );
16
-
17
- export const ScatterIcon = () => (
18
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" className="w-4 h-4">
19
- <circle cx="8" cy="8" r="2" />
20
- <circle cx="16" cy="16" r="2" />
21
- <circle cx="18" cy="8" r="2" />
22
- <circle cx="6" cy="16" r="2" />
23
- <circle cx="12" cy="12" r="2" />
24
- </svg>
25
- );
26
-
27
- export const HyperViewLogo = ({ className = "w-5 h-5" }: { className?: string }) => (
28
- <svg viewBox="0 0 24 24" fill="none" className={className}>
29
- <circle cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="1.5" />
30
- <circle cx="12" cy="12" r="6" stroke="currentColor" strokeWidth="1.5" opacity="0.6" />
31
- <circle cx="12" cy="12" r="2" fill="currentColor" />
32
- </svg>
33
- );
34
-
35
- export const CheckIcon = () => (
36
- <svg className="w-3 h-3 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
37
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
38
- </svg>
39
- );
40
-
41
- /** Euclidean geometry icon - flat grid */
42
- export const EuclideanIcon = () => (
43
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.25" className="w-3.5 h-3.5">
44
- <rect x="2" y="2" width="12" height="12" rx="1" />
45
- <line x1="2" y1="8" x2="14" y2="8" />
46
- <line x1="8" y1="2" x2="8" y2="14" />
47
- </svg>
48
- );
49
-
50
- /** Poincaré disk icon - hyperbolic geometry */
51
- export const PoincareIcon = () => (
52
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.25" className="w-3.5 h-3.5">
53
- <circle cx="8" cy="8" r="6" />
54
- <ellipse cx="8" cy="8" rx="3" ry="5.5" />
55
- <ellipse cx="8" cy="8" rx="5.5" ry="3" />
56
- </svg>
57
- );
58
-
59
- /** Spherical geometry icon - for future use */
60
- export const SphericalIcon = () => (
61
- <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.25" className="w-3.5 h-3.5">
62
- <circle cx="8" cy="8" r="6" />
63
- <ellipse cx="8" cy="8" rx="6" ry="2.5" />
64
- <path d="M8 2 Q12 8 8 14" />
65
- </svg>
66
- );
67
-
68
- /** Discord icon - official simplified logo */
69
- export const DiscordIcon = ({ className = "w-4 h-4" }: { className?: string }) => (
70
- <svg viewBox="0 0 24 24" fill="currentColor" className={className}>
71
- <path d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.09.09 0 0 0-.07.03c-.18.33-.39.76-.53 1.09a16.09 16.09 0 0 0-4.8 0c-.14-.34-.35-.76-.54-1.09c-.01-.02-.04-.03-.07-.03c-1.5.26-2.93.71-4.27 1.33c-.01 0-.02.01-.03.02c-2.72 4.07-3.47 8.03-3.1 11.95c0 .02.01.04.03.05c1.8 1.32 3.53 2.12 5.24 2.65c.03.01.06 0 .07-.02c.4-.55.76-1.13 1.07-1.74c.02-.04 0-.08-.04-.09c-.57-.22-1.11-.48-1.64-.78c-.04-.02-.04-.08-.01-.11c.11-.08.22-.17.33-.25c.02-.02.05-.02.07-.01c3.44 1.57 7.15 1.57 10.55 0c.02-.01.05-.01.07.01c.11.09.22.17.33.26c.04.03.04.09-.01.11c-.52.31-1.07.56-1.64.78c-.04.01-.05.06-.04.09c.32.61.68 1.19 1.07 1.74c.03.01.06.02.09.01c1.72-.53 3.45-1.33 5.25-2.65c.02-.01.03-.03.03-.05c.44-4.53-.73-8.46-3.1-11.95c-.01-.01-.02-.02-.04-.02zM8.52 14.91c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.84 2.12-1.89 2.12zm6.97 0c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12c0 1.17-.83 2.12-1.89 2.12z"/>
72
- </svg>
73
- );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/index.ts DELETED
@@ -1,9 +0,0 @@
1
- export { ImageGrid } from "./ImageGrid";
2
- export { ScatterPanel } from "./ScatterPanel";
3
- export { DockviewWorkspace } from "./DockviewWorkspace";
4
- export { Header } from "./Header";
5
- export { Panel, PanelFooter } from "./Panel";
6
- export { PanelHeader } from "./PanelHeader";
7
- export { ExplorerPanel } from "./ExplorerPanel";
8
- export { PlaceholderPanel } from "./PlaceholderPanel";
9
- export * from "./icons";
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ui/button.tsx DELETED
@@ -1,57 +0,0 @@
1
- import * as React from "react"
2
- import { Slot } from "@radix-ui/react-slot"
3
- import { cva, type VariantProps } from "class-variance-authority"
4
-
5
- import { cn } from "@/lib/utils"
6
-
7
- const buttonVariants = cva(
8
- "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
9
- {
10
- variants: {
11
- variant: {
12
- default:
13
- "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14
- destructive:
15
- "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16
- outline:
17
- "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18
- secondary:
19
- "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20
- ghost: "hover:bg-accent hover:text-accent-foreground",
21
- link: "text-primary underline-offset-4 hover:underline",
22
- },
23
- size: {
24
- default: "h-9 px-4 py-2",
25
- sm: "h-8 rounded-md px-3 text-xs",
26
- lg: "h-10 rounded-md px-8",
27
- icon: "h-9 w-9",
28
- },
29
- },
30
- defaultVariants: {
31
- variant: "default",
32
- size: "default",
33
- },
34
- }
35
- )
36
-
37
- export interface ButtonProps
38
- extends React.ButtonHTMLAttributes<HTMLButtonElement>,
39
- VariantProps<typeof buttonVariants> {
40
- asChild?: boolean
41
- }
42
-
43
- const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
44
- ({ className, variant, size, asChild = false, ...props }, ref) => {
45
- const Comp = asChild ? Slot : "button"
46
- return (
47
- <Comp
48
- className={cn(buttonVariants({ variant, size, className }))}
49
- ref={ref}
50
- {...props}
51
- />
52
- )
53
- }
54
- )
55
- Button.displayName = "Button"
56
-
57
- export { Button, buttonVariants }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ui/collapsible.tsx DELETED
@@ -1,11 +0,0 @@
1
- "use client"
2
-
3
- import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
4
-
5
- const Collapsible = CollapsiblePrimitive.Root
6
-
7
- const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
8
-
9
- const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
10
-
11
- export { Collapsible, CollapsibleTrigger, CollapsibleContent }
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ui/command.tsx DELETED
@@ -1,153 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import { type DialogProps } from "@radix-ui/react-dialog"
5
- import { Command as CommandPrimitive } from "cmdk"
6
- import { Search } from "lucide-react"
7
-
8
- import { cn } from "@/lib/utils"
9
- import { Dialog, DialogContent } from "@/components/ui/dialog"
10
-
11
- const Command = React.forwardRef<
12
- React.ElementRef<typeof CommandPrimitive>,
13
- React.ComponentPropsWithoutRef<typeof CommandPrimitive>
14
- >(({ className, ...props }, ref) => (
15
- <CommandPrimitive
16
- ref={ref}
17
- className={cn(
18
- "flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
19
- className
20
- )}
21
- {...props}
22
- />
23
- ))
24
- Command.displayName = CommandPrimitive.displayName
25
-
26
- const CommandDialog = ({ children, ...props }: DialogProps) => {
27
- return (
28
- <Dialog {...props}>
29
- <DialogContent className="overflow-hidden p-0">
30
- <Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
31
- {children}
32
- </Command>
33
- </DialogContent>
34
- </Dialog>
35
- )
36
- }
37
-
38
- const CommandInput = React.forwardRef<
39
- React.ElementRef<typeof CommandPrimitive.Input>,
40
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
41
- >(({ className, ...props }, ref) => (
42
- <div className="flex items-center border-b px-3" cmdk-input-wrapper="">
43
- <Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
44
- <CommandPrimitive.Input
45
- ref={ref}
46
- className={cn(
47
- "flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
48
- className
49
- )}
50
- {...props}
51
- />
52
- </div>
53
- ))
54
-
55
- CommandInput.displayName = CommandPrimitive.Input.displayName
56
-
57
- const CommandList = React.forwardRef<
58
- React.ElementRef<typeof CommandPrimitive.List>,
59
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
60
- >(({ className, ...props }, ref) => (
61
- <CommandPrimitive.List
62
- ref={ref}
63
- className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
64
- {...props}
65
- />
66
- ))
67
-
68
- CommandList.displayName = CommandPrimitive.List.displayName
69
-
70
- const CommandEmpty = React.forwardRef<
71
- React.ElementRef<typeof CommandPrimitive.Empty>,
72
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
73
- >((props, ref) => (
74
- <CommandPrimitive.Empty
75
- ref={ref}
76
- className="py-6 text-center text-sm"
77
- {...props}
78
- />
79
- ))
80
-
81
- CommandEmpty.displayName = CommandPrimitive.Empty.displayName
82
-
83
- const CommandGroup = React.forwardRef<
84
- React.ElementRef<typeof CommandPrimitive.Group>,
85
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
86
- >(({ className, ...props }, ref) => (
87
- <CommandPrimitive.Group
88
- ref={ref}
89
- className={cn(
90
- "overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
91
- className
92
- )}
93
- {...props}
94
- />
95
- ))
96
-
97
- CommandGroup.displayName = CommandPrimitive.Group.displayName
98
-
99
- const CommandSeparator = React.forwardRef<
100
- React.ElementRef<typeof CommandPrimitive.Separator>,
101
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
102
- >(({ className, ...props }, ref) => (
103
- <CommandPrimitive.Separator
104
- ref={ref}
105
- className={cn("-mx-1 h-px bg-border", className)}
106
- {...props}
107
- />
108
- ))
109
- CommandSeparator.displayName = CommandPrimitive.Separator.displayName
110
-
111
- const CommandItem = React.forwardRef<
112
- React.ElementRef<typeof CommandPrimitive.Item>,
113
- React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
114
- >(({ className, ...props }, ref) => (
115
- <CommandPrimitive.Item
116
- ref={ref}
117
- className={cn(
118
- "relative flex cursor-default gap-2 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none data-[disabled=true]:pointer-events-none data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
119
- className
120
- )}
121
- {...props}
122
- />
123
- ))
124
-
125
- CommandItem.displayName = CommandPrimitive.Item.displayName
126
-
127
- const CommandShortcut = ({
128
- className,
129
- ...props
130
- }: React.HTMLAttributes<HTMLSpanElement>) => {
131
- return (
132
- <span
133
- className={cn(
134
- "ml-auto text-xs tracking-widest text-muted-foreground",
135
- className
136
- )}
137
- {...props}
138
- />
139
- )
140
- }
141
- CommandShortcut.displayName = "CommandShortcut"
142
-
143
- export {
144
- Command,
145
- CommandDialog,
146
- CommandInput,
147
- CommandList,
148
- CommandEmpty,
149
- CommandGroup,
150
- CommandItem,
151
- CommandShortcut,
152
- CommandSeparator,
153
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ui/dialog.tsx DELETED
@@ -1,122 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import * as DialogPrimitive from "@radix-ui/react-dialog"
5
- import { X } from "lucide-react"
6
-
7
- import { cn } from "@/lib/utils"
8
-
9
- const Dialog = DialogPrimitive.Root
10
-
11
- const DialogTrigger = DialogPrimitive.Trigger
12
-
13
- const DialogPortal = DialogPrimitive.Portal
14
-
15
- const DialogClose = DialogPrimitive.Close
16
-
17
- const DialogOverlay = React.forwardRef<
18
- React.ElementRef<typeof DialogPrimitive.Overlay>,
19
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
20
- >(({ className, ...props }, ref) => (
21
- <DialogPrimitive.Overlay
22
- ref={ref}
23
- className={cn(
24
- "fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
25
- className
26
- )}
27
- {...props}
28
- />
29
- ))
30
- DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31
-
32
- const DialogContent = React.forwardRef<
33
- React.ElementRef<typeof DialogPrimitive.Content>,
34
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
35
- >(({ className, children, ...props }, ref) => (
36
- <DialogPortal>
37
- <DialogOverlay />
38
- <DialogPrimitive.Content
39
- ref={ref}
40
- className={cn(
41
- "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
42
- className
43
- )}
44
- {...props}
45
- >
46
- {children}
47
- <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
48
- <X className="h-4 w-4" />
49
- <span className="sr-only">Close</span>
50
- </DialogPrimitive.Close>
51
- </DialogPrimitive.Content>
52
- </DialogPortal>
53
- ))
54
- DialogContent.displayName = DialogPrimitive.Content.displayName
55
-
56
- const DialogHeader = ({
57
- className,
58
- ...props
59
- }: React.HTMLAttributes<HTMLDivElement>) => (
60
- <div
61
- className={cn(
62
- "flex flex-col space-y-1.5 text-center sm:text-left",
63
- className
64
- )}
65
- {...props}
66
- />
67
- )
68
- DialogHeader.displayName = "DialogHeader"
69
-
70
- const DialogFooter = ({
71
- className,
72
- ...props
73
- }: React.HTMLAttributes<HTMLDivElement>) => (
74
- <div
75
- className={cn(
76
- "flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
77
- className
78
- )}
79
- {...props}
80
- />
81
- )
82
- DialogFooter.displayName = "DialogFooter"
83
-
84
- const DialogTitle = React.forwardRef<
85
- React.ElementRef<typeof DialogPrimitive.Title>,
86
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
87
- >(({ className, ...props }, ref) => (
88
- <DialogPrimitive.Title
89
- ref={ref}
90
- className={cn(
91
- "text-lg font-semibold leading-none tracking-tight",
92
- className
93
- )}
94
- {...props}
95
- />
96
- ))
97
- DialogTitle.displayName = DialogPrimitive.Title.displayName
98
-
99
- const DialogDescription = React.forwardRef<
100
- React.ElementRef<typeof DialogPrimitive.Description>,
101
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
102
- >(({ className, ...props }, ref) => (
103
- <DialogPrimitive.Description
104
- ref={ref}
105
- className={cn("text-sm text-muted-foreground", className)}
106
- {...props}
107
- />
108
- ))
109
- DialogDescription.displayName = DialogPrimitive.Description.displayName
110
-
111
- export {
112
- Dialog,
113
- DialogPortal,
114
- DialogOverlay,
115
- DialogTrigger,
116
- DialogClose,
117
- DialogContent,
118
- DialogHeader,
119
- DialogFooter,
120
- DialogTitle,
121
- DialogDescription,
122
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ui/dropdown-menu.tsx DELETED
@@ -1,201 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5
- import { Check, ChevronRight, Circle } from "lucide-react"
6
-
7
- import { cn } from "@/lib/utils"
8
-
9
- const DropdownMenu = DropdownMenuPrimitive.Root
10
-
11
- const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12
-
13
- const DropdownMenuGroup = DropdownMenuPrimitive.Group
14
-
15
- const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16
-
17
- const DropdownMenuSub = DropdownMenuPrimitive.Sub
18
-
19
- const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20
-
21
- const DropdownMenuSubTrigger = React.forwardRef<
22
- React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
23
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
24
- inset?: boolean
25
- }
26
- >(({ className, inset, children, ...props }, ref) => (
27
- <DropdownMenuPrimitive.SubTrigger
28
- ref={ref}
29
- className={cn(
30
- "flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
31
- inset && "pl-8",
32
- className
33
- )}
34
- {...props}
35
- >
36
- {children}
37
- <ChevronRight className="ml-auto" />
38
- </DropdownMenuPrimitive.SubTrigger>
39
- ))
40
- DropdownMenuSubTrigger.displayName =
41
- DropdownMenuPrimitive.SubTrigger.displayName
42
-
43
- const DropdownMenuSubContent = React.forwardRef<
44
- React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
45
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
46
- >(({ className, ...props }, ref) => (
47
- <DropdownMenuPrimitive.SubContent
48
- ref={ref}
49
- className={cn(
50
- "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
51
- className
52
- )}
53
- {...props}
54
- />
55
- ))
56
- DropdownMenuSubContent.displayName =
57
- DropdownMenuPrimitive.SubContent.displayName
58
-
59
- const DropdownMenuContent = React.forwardRef<
60
- React.ElementRef<typeof DropdownMenuPrimitive.Content>,
61
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
62
- >(({ className, sideOffset = 4, ...props }, ref) => (
63
- <DropdownMenuPrimitive.Portal>
64
- <DropdownMenuPrimitive.Content
65
- ref={ref}
66
- sideOffset={sideOffset}
67
- className={cn(
68
- "z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md",
69
- "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
70
- className
71
- )}
72
- {...props}
73
- />
74
- </DropdownMenuPrimitive.Portal>
75
- ))
76
- DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
77
-
78
- const DropdownMenuItem = React.forwardRef<
79
- React.ElementRef<typeof DropdownMenuPrimitive.Item>,
80
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
81
- inset?: boolean
82
- }
83
- >(({ className, inset, ...props }, ref) => (
84
- <DropdownMenuPrimitive.Item
85
- ref={ref}
86
- className={cn(
87
- "relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0",
88
- inset && "pl-8",
89
- className
90
- )}
91
- {...props}
92
- />
93
- ))
94
- DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
95
-
96
- const DropdownMenuCheckboxItem = React.forwardRef<
97
- React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
98
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
99
- >(({ className, children, checked, ...props }, ref) => (
100
- <DropdownMenuPrimitive.CheckboxItem
101
- ref={ref}
102
- className={cn(
103
- "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
104
- className
105
- )}
106
- checked={checked}
107
- {...props}
108
- >
109
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
110
- <DropdownMenuPrimitive.ItemIndicator>
111
- <Check className="h-4 w-4" />
112
- </DropdownMenuPrimitive.ItemIndicator>
113
- </span>
114
- {children}
115
- </DropdownMenuPrimitive.CheckboxItem>
116
- ))
117
- DropdownMenuCheckboxItem.displayName =
118
- DropdownMenuPrimitive.CheckboxItem.displayName
119
-
120
- const DropdownMenuRadioItem = React.forwardRef<
121
- React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
122
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
123
- >(({ className, children, ...props }, ref) => (
124
- <DropdownMenuPrimitive.RadioItem
125
- ref={ref}
126
- className={cn(
127
- "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
128
- className
129
- )}
130
- {...props}
131
- >
132
- <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
133
- <DropdownMenuPrimitive.ItemIndicator>
134
- <Circle className="h-2 w-2 fill-current" />
135
- </DropdownMenuPrimitive.ItemIndicator>
136
- </span>
137
- {children}
138
- </DropdownMenuPrimitive.RadioItem>
139
- ))
140
- DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
141
-
142
- const DropdownMenuLabel = React.forwardRef<
143
- React.ElementRef<typeof DropdownMenuPrimitive.Label>,
144
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
145
- inset?: boolean
146
- }
147
- >(({ className, inset, ...props }, ref) => (
148
- <DropdownMenuPrimitive.Label
149
- ref={ref}
150
- className={cn(
151
- "px-2 py-1.5 text-sm font-semibold",
152
- inset && "pl-8",
153
- className
154
- )}
155
- {...props}
156
- />
157
- ))
158
- DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
159
-
160
- const DropdownMenuSeparator = React.forwardRef<
161
- React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
162
- React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
163
- >(({ className, ...props }, ref) => (
164
- <DropdownMenuPrimitive.Separator
165
- ref={ref}
166
- className={cn("-mx-1 my-1 h-px bg-muted", className)}
167
- {...props}
168
- />
169
- ))
170
- DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
171
-
172
- const DropdownMenuShortcut = ({
173
- className,
174
- ...props
175
- }: React.HTMLAttributes<HTMLSpanElement>) => {
176
- return (
177
- <span
178
- className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
179
- {...props}
180
- />
181
- )
182
- }
183
- DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
184
-
185
- export {
186
- DropdownMenu,
187
- DropdownMenuTrigger,
188
- DropdownMenuContent,
189
- DropdownMenuItem,
190
- DropdownMenuCheckboxItem,
191
- DropdownMenuRadioItem,
192
- DropdownMenuLabel,
193
- DropdownMenuSeparator,
194
- DropdownMenuShortcut,
195
- DropdownMenuGroup,
196
- DropdownMenuPortal,
197
- DropdownMenuSub,
198
- DropdownMenuSubContent,
199
- DropdownMenuSubTrigger,
200
- DropdownMenuRadioGroup,
201
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ui/popover.tsx DELETED
@@ -1,33 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import * as PopoverPrimitive from "@radix-ui/react-popover"
5
-
6
- import { cn } from "@/lib/utils"
7
-
8
- const Popover = PopoverPrimitive.Root
9
-
10
- const PopoverTrigger = PopoverPrimitive.Trigger
11
-
12
- const PopoverAnchor = PopoverPrimitive.Anchor
13
-
14
- const PopoverContent = React.forwardRef<
15
- React.ElementRef<typeof PopoverPrimitive.Content>,
16
- React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
17
- >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
18
- <PopoverPrimitive.Portal>
19
- <PopoverPrimitive.Content
20
- ref={ref}
21
- align={align}
22
- sideOffset={sideOffset}
23
- className={cn(
24
- "z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-popover-content-transform-origin]",
25
- className
26
- )}
27
- {...props}
28
- />
29
- </PopoverPrimitive.Portal>
30
- ))
31
- PopoverContent.displayName = PopoverPrimitive.Content.displayName
32
-
33
- export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ui/radio-group.tsx DELETED
@@ -1,44 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
5
- import { Circle } from "lucide-react"
6
-
7
- import { cn } from "@/lib/utils"
8
-
9
- const RadioGroup = React.forwardRef<
10
- React.ElementRef<typeof RadioGroupPrimitive.Root>,
11
- React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
12
- >(({ className, ...props }, ref) => {
13
- return (
14
- <RadioGroupPrimitive.Root
15
- className={cn("grid gap-2", className)}
16
- {...props}
17
- ref={ref}
18
- />
19
- )
20
- })
21
- RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
22
-
23
- const RadioGroupItem = React.forwardRef<
24
- React.ElementRef<typeof RadioGroupPrimitive.Item>,
25
- React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
26
- >(({ className, ...props }, ref) => {
27
- return (
28
- <RadioGroupPrimitive.Item
29
- ref={ref}
30
- className={cn(
31
- "aspect-square h-4 w-4 rounded-full border border-primary text-primary shadow focus:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50",
32
- className
33
- )}
34
- {...props}
35
- >
36
- <RadioGroupPrimitive.Indicator className="flex items-center justify-center">
37
- <Circle className="h-3.5 w-3.5 fill-primary" />
38
- </RadioGroupPrimitive.Indicator>
39
- </RadioGroupPrimitive.Item>
40
- )
41
- })
42
- RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
43
-
44
- export { RadioGroup, RadioGroupItem }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ui/scroll-area.tsx DELETED
@@ -1,49 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5
-
6
- import { cn } from "@/lib/utils"
7
-
8
- const ScrollArea = React.forwardRef<
9
- React.ElementRef<typeof ScrollAreaPrimitive.Root>,
10
- React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
11
- >(({ className, children, ...props }, ref) => (
12
- <ScrollAreaPrimitive.Root
13
- ref={ref}
14
- className={cn("relative overflow-hidden", className)}
15
- {...props}
16
- >
17
- <ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
18
- {children}
19
- </ScrollAreaPrimitive.Viewport>
20
- <ScrollBar />
21
- <ScrollAreaPrimitive.Corner />
22
- </ScrollAreaPrimitive.Root>
23
- ))
24
- ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
25
-
26
- const ScrollBar = React.forwardRef<
27
- React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
28
- React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
29
- >(({ className, orientation = "vertical", ...props }, ref) => (
30
- <ScrollAreaPrimitive.ScrollAreaScrollbar
31
- ref={ref}
32
- orientation={orientation}
33
- className={cn(
34
- "flex touch-none select-none transition-colors",
35
- // Rerun style: 6px bar width, 2px inner/outer margin
36
- orientation === "vertical" &&
37
- "h-full w-[10px] border-l border-l-transparent p-[2px]",
38
- orientation === "horizontal" &&
39
- "h-[10px] flex-col border-t border-t-transparent p-[2px]",
40
- className
41
- )}
42
- {...props}
43
- >
44
- <ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-[3px] bg-muted-foreground/25 hover:bg-muted-foreground/40" />
45
- </ScrollAreaPrimitive.ScrollAreaScrollbar>
46
- ))
47
- ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
48
-
49
- export { ScrollArea, ScrollBar }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ui/separator.tsx DELETED
@@ -1,31 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import * as SeparatorPrimitive from "@radix-ui/react-separator"
5
-
6
- import { cn } from "@/lib/utils"
7
-
8
- const Separator = React.forwardRef<
9
- React.ElementRef<typeof SeparatorPrimitive.Root>,
10
- React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
11
- >(
12
- (
13
- { className, orientation = "horizontal", decorative = true, ...props },
14
- ref
15
- ) => (
16
- <SeparatorPrimitive.Root
17
- ref={ref}
18
- decorative={decorative}
19
- orientation={orientation}
20
- className={cn(
21
- "shrink-0 bg-border",
22
- orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
23
- className
24
- )}
25
- {...props}
26
- />
27
- )
28
- )
29
- Separator.displayName = SeparatorPrimitive.Root.displayName
30
-
31
- export { Separator }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ui/toggle-group.tsx DELETED
@@ -1,61 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
5
- import { type VariantProps } from "class-variance-authority"
6
-
7
- import { cn } from "@/lib/utils"
8
- import { toggleVariants } from "@/components/ui/toggle"
9
-
10
- const ToggleGroupContext = React.createContext<
11
- VariantProps<typeof toggleVariants>
12
- >({
13
- size: "default",
14
- variant: "default",
15
- })
16
-
17
- const ToggleGroup = React.forwardRef<
18
- React.ElementRef<typeof ToggleGroupPrimitive.Root>,
19
- React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
20
- VariantProps<typeof toggleVariants>
21
- >(({ className, variant, size, children, ...props }, ref) => (
22
- <ToggleGroupPrimitive.Root
23
- ref={ref}
24
- className={cn("flex items-center justify-center gap-1", className)}
25
- {...props}
26
- >
27
- <ToggleGroupContext.Provider value={{ variant, size }}>
28
- {children}
29
- </ToggleGroupContext.Provider>
30
- </ToggleGroupPrimitive.Root>
31
- ))
32
-
33
- ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName
34
-
35
- const ToggleGroupItem = React.forwardRef<
36
- React.ElementRef<typeof ToggleGroupPrimitive.Item>,
37
- React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
38
- VariantProps<typeof toggleVariants>
39
- >(({ className, children, variant, size, ...props }, ref) => {
40
- const context = React.useContext(ToggleGroupContext)
41
-
42
- return (
43
- <ToggleGroupPrimitive.Item
44
- ref={ref}
45
- className={cn(
46
- toggleVariants({
47
- variant: context.variant || variant,
48
- size: context.size || size,
49
- }),
50
- className
51
- )}
52
- {...props}
53
- >
54
- {children}
55
- </ToggleGroupPrimitive.Item>
56
- )
57
- })
58
-
59
- ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName
60
-
61
- export { ToggleGroup, ToggleGroupItem }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ui/toggle.tsx DELETED
@@ -1,45 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import * as TogglePrimitive from "@radix-ui/react-toggle"
5
- import { cva, type VariantProps } from "class-variance-authority"
6
-
7
- import { cn } from "@/lib/utils"
8
-
9
- const toggleVariants = cva(
10
- "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-primary data-[state=on]:text-primary-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
11
- {
12
- variants: {
13
- variant: {
14
- default: "bg-transparent",
15
- outline:
16
- "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground data-[state=on]:border-primary",
17
- },
18
- size: {
19
- default: "h-9 px-2 min-w-9",
20
- sm: "h-8 px-1.5 min-w-8",
21
- lg: "h-10 px-2.5 min-w-10",
22
- },
23
- },
24
- defaultVariants: {
25
- variant: "default",
26
- size: "default",
27
- },
28
- }
29
- )
30
-
31
- const Toggle = React.forwardRef<
32
- React.ElementRef<typeof TogglePrimitive.Root>,
33
- React.ComponentPropsWithoutRef<typeof TogglePrimitive.Root> &
34
- VariantProps<typeof toggleVariants>
35
- >(({ className, variant, size, ...props }, ref) => (
36
- <TogglePrimitive.Root
37
- ref={ref}
38
- className={cn(toggleVariants({ variant, size, className }))}
39
- {...props}
40
- />
41
- ))
42
-
43
- Toggle.displayName = TogglePrimitive.Root.displayName
44
-
45
- export { Toggle, toggleVariants }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/ui/tooltip.tsx DELETED
@@ -1,32 +0,0 @@
1
- "use client"
2
-
3
- import * as React from "react"
4
- import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5
-
6
- import { cn } from "@/lib/utils"
7
-
8
- const TooltipProvider = TooltipPrimitive.Provider
9
-
10
- const Tooltip = TooltipPrimitive.Root
11
-
12
- const TooltipTrigger = TooltipPrimitive.Trigger
13
-
14
- const TooltipContent = React.forwardRef<
15
- React.ElementRef<typeof TooltipPrimitive.Content>,
16
- React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
17
- >(({ className, sideOffset = 4, ...props }, ref) => (
18
- <TooltipPrimitive.Portal>
19
- <TooltipPrimitive.Content
20
- ref={ref}
21
- sideOffset={sideOffset}
22
- className={cn(
23
- "z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-tooltip-content-transform-origin]",
24
- className
25
- )}
26
- {...props}
27
- />
28
- </TooltipPrimitive.Portal>
29
- ))
30
- TooltipContent.displayName = TooltipPrimitive.Content.displayName
31
-
32
- export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/useHyperScatter.ts DELETED
@@ -1,614 +0,0 @@
1
- import type React from "react";
2
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
-
4
- import type { EmbeddingsData } from "@/types";
5
- import type { ScatterLabelsInfo } from "@/lib/labelLegend";
6
- import type { Dataset, GeometryMode, Modifiers, Renderer } from "hyper-scatter";
7
-
8
- type HyperScatterModule = typeof import("hyper-scatter");
9
-
10
- const MAX_LASSO_VERTS = 512;
11
-
12
- function supportsWebGL2(): boolean {
13
- try {
14
- if (typeof document === "undefined") return false;
15
- const canvas = document.createElement("canvas");
16
- return !!canvas.getContext("webgl2");
17
- } catch {
18
- return false;
19
- }
20
- }
21
-
22
- function capInterleavedXY(points: ArrayLike<number>, maxVerts: number): number[] {
23
- const n = Math.floor(points.length / 2);
24
- if (n <= maxVerts) return Array.from(points as ArrayLike<number>);
25
-
26
- const out = new Array<number>(maxVerts * 2);
27
- for (let i = 0; i < maxVerts; i++) {
28
- const src = Math.floor((i * n) / maxVerts);
29
- out[i * 2] = points[src * 2];
30
- out[i * 2 + 1] = points[src * 2 + 1];
31
- }
32
- return out;
33
- }
34
-
35
-
36
- interface UseHyperScatterArgs {
37
- embeddings: EmbeddingsData | null;
38
- labelsInfo: ScatterLabelsInfo | null;
39
- selectedIds: Set<string>;
40
- hoveredId: string | null;
41
- setSelectedIds: (ids: Set<string>, source?: "scatter" | "grid") => void;
42
- beginLassoSelection: (query: { layoutKey: string; polygon: number[] }) => void;
43
- setHoveredId: (id: string | null) => void;
44
- hoverEnabled?: boolean;
45
- }
46
-
47
- function toModifiers(e: { shiftKey: boolean; ctrlKey: boolean; altKey: boolean; metaKey: boolean }): Modifiers {
48
- return {
49
- shift: e.shiftKey,
50
- ctrl: e.ctrlKey,
51
- alt: e.altKey,
52
- meta: e.metaKey,
53
- };
54
- }
55
-
56
- function clearOverlay(canvas: HTMLCanvasElement | null): void {
57
- if (!canvas) return;
58
- const ctx = canvas.getContext("2d");
59
- if (!ctx) return;
60
- ctx.setTransform(1, 0, 0, 1, 0, 0);
61
- ctx.clearRect(0, 0, canvas.width, canvas.height);
62
- }
63
-
64
- function drawLassoOverlay(canvas: HTMLCanvasElement | null, points: number[]): void {
65
- if (!canvas) return;
66
- const ctx = canvas.getContext("2d");
67
- if (!ctx) return;
68
-
69
- clearOverlay(canvas);
70
- if (points.length < 6) return;
71
-
72
- ctx.save();
73
- ctx.lineWidth = 2;
74
- ctx.strokeStyle = "rgba(79,70,229,0.9)"; // indigo-ish
75
- ctx.fillStyle = "rgba(79,70,229,0.15)";
76
-
77
- ctx.beginPath();
78
- ctx.moveTo(points[0], points[1]);
79
- for (let i = 2; i < points.length; i += 2) {
80
- ctx.lineTo(points[i], points[i + 1]);
81
- }
82
- ctx.closePath();
83
- ctx.fill();
84
- ctx.stroke();
85
- ctx.restore();
86
- }
87
-
88
- export function useHyperScatter({
89
- embeddings,
90
- labelsInfo,
91
- selectedIds,
92
- hoveredId,
93
- setSelectedIds,
94
- beginLassoSelection,
95
- setHoveredId,
96
- hoverEnabled = true,
97
- }: UseHyperScatterArgs) {
98
- const canvasRef = useRef<HTMLCanvasElement>(null);
99
- const overlayCanvasRef = useRef<HTMLCanvasElement>(null);
100
- const containerRef = useRef<HTMLDivElement>(null);
101
-
102
- const rendererRef = useRef<Renderer | null>(null);
103
-
104
- const [rendererError, setRendererError] = useState<string | null>(null);
105
-
106
- const rafPendingRef = useRef(false);
107
-
108
- // Interaction state (refs to avoid rerender churn)
109
- const isPanningRef = useRef(false);
110
- const isLassoingRef = useRef(false);
111
- const pointerDownXRef = useRef(0);
112
- const pointerDownYRef = useRef(0);
113
- const lastPointerXRef = useRef(0);
114
- const lastPointerYRef = useRef(0);
115
- const lassoPointsRef = useRef<number[]>([]);
116
- const persistentLassoRef = useRef<number[] | null>(null);
117
-
118
- const hoveredIndexRef = useRef<number>(-1);
119
-
120
- const idToIndex = useMemo(() => {
121
- if (!embeddings) return null;
122
- const m = new Map<string, number>();
123
- for (let i = 0; i < embeddings.ids.length; i++) {
124
- m.set(embeddings.ids[i], i);
125
- }
126
- return m;
127
- }, [embeddings]);
128
-
129
- const requestRender = useCallback(() => {
130
- if (rafPendingRef.current) return;
131
- rafPendingRef.current = true;
132
-
133
- requestAnimationFrame(() => {
134
- rafPendingRef.current = false;
135
- const renderer = rendererRef.current;
136
- if (!renderer) return;
137
-
138
- try {
139
- renderer.render();
140
- } catch (err) {
141
- // Avoid an exception storm that would permanently prevent the UI from updating.
142
- console.error("hyper-scatter renderer.render() failed:", err);
143
- try {
144
- renderer.destroy();
145
- } catch {
146
- // ignore
147
- }
148
- rendererRef.current = null;
149
- setRendererError(
150
- "This browser can't render the scatter plot (WebGL2 is required). Please use Chrome/Edge/Firefox."
151
- );
152
- clearOverlay(overlayCanvasRef.current);
153
- return;
154
- }
155
-
156
- if (isLassoingRef.current) {
157
- drawLassoOverlay(overlayCanvasRef.current, lassoPointsRef.current);
158
- }
159
- });
160
- }, []);
161
-
162
- const getCanvasPos = useCallback((e: { clientX: number; clientY: number }) => {
163
- const canvas = canvasRef.current;
164
- if (!canvas) return { x: 0, y: 0 };
165
- const rect = canvas.getBoundingClientRect();
166
- return {
167
- x: e.clientX - rect.left,
168
- y: e.clientY - rect.top,
169
- };
170
- }, []);
171
-
172
- const redrawOverlay = useCallback(() => {
173
- if (!overlayCanvasRef.current) return;
174
- clearOverlay(overlayCanvasRef.current);
175
- const persistent = persistentLassoRef.current;
176
- if (persistent && persistent.length >= 6) {
177
- drawLassoOverlay(overlayCanvasRef.current, persistent);
178
- }
179
- }, []);
180
-
181
- const clearPersistentLasso = useCallback(() => {
182
- persistentLassoRef.current = null;
183
- clearOverlay(overlayCanvasRef.current);
184
- }, []);
185
-
186
- const stopInteraction = useCallback(() => {
187
- isPanningRef.current = false;
188
- isLassoingRef.current = false;
189
- lassoPointsRef.current = [];
190
- if (persistentLassoRef.current) {
191
- redrawOverlay();
192
- return;
193
- }
194
- clearOverlay(overlayCanvasRef.current);
195
- }, [redrawOverlay]);
196
-
197
- // Initialize renderer when embeddings change.
198
- useEffect(() => {
199
- if (!embeddings || !labelsInfo) return;
200
- if (!canvasRef.current || !containerRef.current) return;
201
-
202
- let cancelled = false;
203
-
204
- const init = async () => {
205
- // Clear any previous renderer errors when we attempt to re-init.
206
- setRendererError(null);
207
-
208
- if (!supportsWebGL2()) {
209
- setRendererError(
210
- "This browser doesn't support WebGL2, so the scatter plot can't be displayed. Please use Chrome/Edge/Firefox."
211
- );
212
- return;
213
- }
214
-
215
- try {
216
- const viz = (await import("hyper-scatter")) as HyperScatterModule;
217
- if (cancelled) return;
218
-
219
- const container = containerRef.current;
220
- const canvas = canvasRef.current;
221
- if (!container || !canvas) return;
222
-
223
- // Destroy existing renderer (if any)
224
- if (rendererRef.current) {
225
- rendererRef.current.destroy();
226
- rendererRef.current = null;
227
- }
228
-
229
- const rect = container.getBoundingClientRect();
230
- const width = Math.floor(rect.width);
231
- const height = Math.floor(rect.height);
232
- if (overlayCanvasRef.current) {
233
- overlayCanvasRef.current.width = Math.max(1, width);
234
- overlayCanvasRef.current.height = Math.max(1, height);
235
- overlayCanvasRef.current.style.width = `${width}px`;
236
- overlayCanvasRef.current.style.height = `${height}px`;
237
- redrawOverlay();
238
- }
239
-
240
- // Use coords from embeddings response directly
241
- const coords = embeddings.coords;
242
- const positions = new Float32Array(coords.length * 2);
243
- for (let i = 0; i < coords.length; i++) {
244
- positions[i * 2] = coords[i][0];
245
- positions[i * 2 + 1] = coords[i][1];
246
- }
247
-
248
- const geometry = embeddings.geometry as GeometryMode;
249
- const dataset: Dataset = viz.createDataset(geometry, positions, labelsInfo.categories);
250
-
251
- const opts = {
252
- width,
253
- height,
254
- devicePixelRatio: window.devicePixelRatio,
255
- pointRadius: 4,
256
- colors: labelsInfo.palette,
257
- backgroundColor: "#161b22", // Match HyperView theme: --card is #161b22
258
- };
259
-
260
- const renderer: Renderer =
261
- geometry === "euclidean" ? new viz.EuclideanWebGLCandidate() : new viz.HyperbolicWebGLCandidate();
262
-
263
- renderer.init(canvas, opts);
264
-
265
- renderer.setDataset(dataset);
266
- rendererRef.current = renderer;
267
-
268
- // Force a first render to surface WebGL2 context creation failures early.
269
- try {
270
- renderer.render();
271
- } catch (err) {
272
- console.error("hyper-scatter initial render failed:", err);
273
- rendererRef.current = null;
274
- try {
275
- renderer.destroy();
276
- } catch {
277
- // ignore
278
- }
279
- setRendererError(
280
- "This browser can't render the scatter plot (WebGL2 is required). Please use Chrome/Edge/Firefox."
281
- );
282
- return;
283
- }
284
-
285
- hoveredIndexRef.current = -1;
286
- renderer.setHovered(-1);
287
-
288
- requestRender();
289
- } catch (err) {
290
- console.error("Failed to initialize hyper-scatter renderer:", err);
291
- setRendererError(
292
- "Failed to initialize the scatter renderer in this browser. Please use Chrome/Edge/Firefox."
293
- );
294
- }
295
- };
296
-
297
- init();
298
-
299
- return () => {
300
- cancelled = true;
301
- stopInteraction();
302
- if (rendererRef.current) {
303
- rendererRef.current.destroy();
304
- rendererRef.current = null;
305
- }
306
- };
307
- }, [embeddings, labelsInfo, redrawOverlay, requestRender, stopInteraction]);
308
-
309
- // Store -> renderer sync
310
- useEffect(() => {
311
- const renderer = rendererRef.current;
312
- if (!renderer || !embeddings || !idToIndex) return;
313
-
314
- const indices = new Set<number>();
315
- for (const id of selectedIds) {
316
- const idx = idToIndex.get(id);
317
- if (typeof idx === "number") indices.add(idx);
318
- }
319
-
320
- renderer.setSelection(indices);
321
-
322
- if (!hoverEnabled) {
323
- renderer.setHovered(-1);
324
- hoveredIndexRef.current = -1;
325
- requestRender();
326
- return;
327
- }
328
-
329
- const hoveredIdx = hoveredId ? (idToIndex.get(hoveredId) ?? -1) : -1;
330
- renderer.setHovered(hoveredIdx);
331
- hoveredIndexRef.current = hoveredIdx;
332
-
333
- requestRender();
334
- }, [embeddings, hoveredId, hoverEnabled, idToIndex, requestRender, selectedIds]);
335
-
336
- // Resize handling
337
- useEffect(() => {
338
- const container = containerRef.current;
339
- if (!container) return;
340
-
341
- const resize = () => {
342
- const rect = container.getBoundingClientRect();
343
- const width = Math.floor(rect.width);
344
- const height = Math.floor(rect.height);
345
- if (!(width > 0) || !(height > 0)) return;
346
-
347
- if (overlayCanvasRef.current) {
348
- overlayCanvasRef.current.width = Math.max(1, width);
349
- overlayCanvasRef.current.height = Math.max(1, height);
350
- overlayCanvasRef.current.style.width = `${width}px`;
351
- overlayCanvasRef.current.style.height = `${height}px`;
352
- redrawOverlay();
353
- }
354
-
355
- const renderer = rendererRef.current;
356
- if (renderer) {
357
- renderer.resize(width, height);
358
- requestRender();
359
- }
360
- };
361
-
362
- resize();
363
-
364
- const ro = new ResizeObserver(resize);
365
- ro.observe(container);
366
- return () => ro.disconnect();
367
- }, [redrawOverlay, requestRender]);
368
-
369
- // Wheel zoom (native listener so we can set passive:false)
370
- useEffect(() => {
371
- const canvas = canvasRef.current;
372
- if (!canvas) return;
373
-
374
- const onWheel = (e: WheelEvent) => {
375
- const renderer = rendererRef.current;
376
- if (!renderer) return;
377
- e.preventDefault();
378
-
379
- const pos = getCanvasPos(e);
380
- const delta = -e.deltaY / 100;
381
- renderer.zoom(pos.x, pos.y, delta, toModifiers(e));
382
- requestRender();
383
- };
384
-
385
- canvas.addEventListener("wheel", onWheel, { passive: false });
386
- return () => canvas.removeEventListener("wheel", onWheel);
387
- }, [getCanvasPos, requestRender]);
388
-
389
- // Pointer interactions
390
- const handlePointerDown = useCallback(
391
- (e: React.PointerEvent<HTMLCanvasElement>) => {
392
- const renderer = rendererRef.current;
393
- if (!renderer) return;
394
-
395
- // Left button only
396
- if (typeof e.button === "number" && e.button !== 0) return;
397
-
398
- const pos = getCanvasPos(e);
399
- pointerDownXRef.current = pos.x;
400
- pointerDownYRef.current = pos.y;
401
- lastPointerXRef.current = pos.x;
402
- lastPointerYRef.current = pos.y;
403
-
404
- if (persistentLassoRef.current) {
405
- clearPersistentLasso();
406
- }
407
-
408
- // Shift-drag = lasso, otherwise pan.
409
- if (e.shiftKey) {
410
- isLassoingRef.current = true;
411
- isPanningRef.current = false;
412
- lassoPointsRef.current = [pos.x, pos.y];
413
- drawLassoOverlay(overlayCanvasRef.current, lassoPointsRef.current);
414
- } else {
415
- isPanningRef.current = true;
416
- isLassoingRef.current = false;
417
- }
418
-
419
- try {
420
- e.currentTarget.setPointerCapture(e.pointerId);
421
- } catch {
422
- // ignore
423
- }
424
-
425
- e.preventDefault();
426
- },
427
- [clearPersistentLasso, getCanvasPos]
428
- );
429
-
430
- const handlePointerMove = useCallback(
431
- (e: React.PointerEvent<HTMLCanvasElement>) => {
432
- const renderer = rendererRef.current;
433
- if (!renderer) return;
434
-
435
- const pos = getCanvasPos(e);
436
-
437
- if (isPanningRef.current) {
438
- const dx = pos.x - lastPointerXRef.current;
439
- const dy = pos.y - lastPointerYRef.current;
440
- lastPointerXRef.current = pos.x;
441
- lastPointerYRef.current = pos.y;
442
-
443
- renderer.pan(dx, dy, toModifiers(e));
444
- requestRender();
445
- return;
446
- }
447
-
448
- if (isLassoingRef.current) {
449
- const pts = lassoPointsRef.current;
450
- const lastX = pts[pts.length - 2] ?? pos.x;
451
- const lastY = pts[pts.length - 1] ?? pos.y;
452
- const ddx = pos.x - lastX;
453
- const ddy = pos.y - lastY;
454
- const distSq = ddx * ddx + ddy * ddy;
455
-
456
- // Sample at ~2px spacing
457
- if (distSq >= 4) {
458
- pts.push(pos.x, pos.y);
459
- drawLassoOverlay(overlayCanvasRef.current, pts);
460
- }
461
- return;
462
- }
463
-
464
- if (!hoverEnabled) {
465
- if (hoveredIndexRef.current !== -1) {
466
- hoveredIndexRef.current = -1;
467
- renderer.setHovered(-1);
468
- requestRender();
469
- }
470
- return;
471
- }
472
-
473
- // Hover
474
- const hit = renderer.hitTest(pos.x, pos.y);
475
- const nextIndex = hit ? hit.index : -1;
476
- if (nextIndex === hoveredIndexRef.current) return;
477
- hoveredIndexRef.current = nextIndex;
478
- renderer.setHovered(nextIndex);
479
-
480
- if (!embeddings) return;
481
- if (nextIndex >= 0 && nextIndex < embeddings.ids.length) {
482
- setHoveredId(embeddings.ids[nextIndex]);
483
- } else {
484
- setHoveredId(null);
485
- }
486
-
487
- requestRender();
488
- },
489
- [embeddings, getCanvasPos, hoverEnabled, requestRender, setHoveredId]
490
- );
491
-
492
- const handlePointerUp = useCallback(
493
- async (e: React.PointerEvent<HTMLCanvasElement>) => {
494
- const renderer = rendererRef.current;
495
- if (!renderer || !embeddings) {
496
- stopInteraction();
497
- return;
498
- }
499
-
500
- if (isLassoingRef.current) {
501
- const pts = lassoPointsRef.current.slice();
502
- persistentLassoRef.current = pts.length >= 6 ? pts : null;
503
- stopInteraction();
504
- redrawOverlay();
505
-
506
- if (pts.length >= 6) {
507
- try {
508
- const polyline = new Float32Array(pts);
509
- const result = renderer.lassoSelect(polyline);
510
-
511
- // Enter server-driven lasso mode by sending a data-space polygon.
512
- // Backend selection runs in the same coordinate system returned by /api/embeddings.
513
- const dataCoords = result.geometry?.coords;
514
- if (!dataCoords || dataCoords.length < 6) return;
515
-
516
- // Clear any existing manual selection highlights immediately.
517
- renderer.setSelection(new Set());
518
-
519
- // Cap vertex count to keep request payload + backend runtime bounded.
520
- const polygon = capInterleavedXY(dataCoords, MAX_LASSO_VERTS);
521
- if (polygon.length < 6) return;
522
-
523
- beginLassoSelection({ layoutKey: embeddings.layout_key, polygon });
524
- } catch (err) {
525
- console.error("Lasso selection failed:", err);
526
- }
527
- }
528
-
529
- requestRender();
530
- return;
531
- }
532
-
533
- // Click-to-select (scatter -> image grid)
534
- // Only treat as a click if the pointer didn't move much (otherwise it's a pan).
535
- const pos = getCanvasPos(e);
536
- const dx = pos.x - pointerDownXRef.current;
537
- const dy = pos.y - pointerDownYRef.current;
538
- const CLICK_MAX_DIST_SQ = 36; // ~6px
539
- const isClick = dx * dx + dy * dy <= CLICK_MAX_DIST_SQ;
540
-
541
- if (isClick) {
542
- const hit = renderer.hitTest(pos.x, pos.y);
543
- const idx = hit ? hit.index : -1;
544
-
545
- if (idx >= 0 && idx < embeddings.ids.length) {
546
- const id = embeddings.ids[idx];
547
-
548
- if (e.metaKey || e.ctrlKey) {
549
- const next = new Set(selectedIds);
550
- if (next.has(id)) next.delete(id);
551
- else next.add(id);
552
- setSelectedIds(next, "scatter");
553
- } else {
554
- setSelectedIds(new Set([id]), "scatter");
555
- }
556
- }
557
- }
558
-
559
- stopInteraction();
560
- requestRender();
561
- },
562
- [
563
- beginLassoSelection,
564
- embeddings,
565
- getCanvasPos,
566
- redrawOverlay,
567
- requestRender,
568
- selectedIds,
569
- setSelectedIds,
570
- stopInteraction,
571
- ]
572
- );
573
-
574
- const handlePointerLeave = useCallback(
575
- (_e: React.PointerEvent<HTMLCanvasElement>) => {
576
- const renderer = rendererRef.current;
577
- if (renderer) {
578
- hoveredIndexRef.current = -1;
579
- setHoveredId(null);
580
- renderer.setHovered(-1);
581
- requestRender();
582
- }
583
- stopInteraction();
584
- },
585
- [requestRender, setHoveredId, stopInteraction]
586
- );
587
-
588
- const handleDoubleClick = useCallback(
589
- (_e: React.MouseEvent<HTMLCanvasElement>) => {
590
- const renderer = rendererRef.current;
591
- if (!renderer) return;
592
- clearPersistentLasso();
593
- stopInteraction();
594
-
595
- renderer.setSelection(new Set());
596
- setSelectedIds(new Set<string>(), "scatter");
597
-
598
- requestRender();
599
- },
600
- [clearPersistentLasso, requestRender, setSelectedIds, stopInteraction]
601
- );
602
-
603
- return {
604
- canvasRef,
605
- overlayCanvasRef,
606
- containerRef,
607
- handlePointerDown,
608
- handlePointerMove,
609
- handlePointerUp,
610
- handlePointerLeave,
611
- handleDoubleClick,
612
- rendererError,
613
- };
614
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/components/useLabelLegend.ts DELETED
@@ -1,85 +0,0 @@
1
- import { useMemo } from "react";
2
-
3
- import type { DatasetInfo, EmbeddingsData } from "@/types";
4
- import {
5
- MAX_DISTINCT_LABEL_COLORS,
6
- buildLabelColorMap,
7
- buildLabelCounts,
8
- buildLabelUniverse,
9
- buildLabelsInfo,
10
- buildLegendLabels,
11
- getDistinctLabelCount,
12
- } from "@/lib/labelLegend";
13
-
14
- interface UseLabelLegendArgs {
15
- datasetInfo: DatasetInfo | null;
16
- embeddings: EmbeddingsData | null;
17
- labelSearch?: string;
18
- labelFilter?: string | null;
19
- }
20
-
21
- export function useLabelLegend({
22
- datasetInfo,
23
- embeddings,
24
- labelSearch = "",
25
- labelFilter = null,
26
- }: UseLabelLegendArgs) {
27
- const labelCounts = useMemo(() => buildLabelCounts(embeddings), [embeddings]);
28
-
29
- const labelUniverse = useMemo(
30
- () => buildLabelUniverse(datasetInfo?.labels ?? [], embeddings?.labels ?? null),
31
- [datasetInfo?.labels, embeddings?.labels]
32
- );
33
-
34
- const distinctLabelCount = useMemo(() => {
35
- const fromCounts = getDistinctLabelCount(labelCounts);
36
- if (fromCounts > 0) return fromCounts;
37
- let n = labelUniverse.length;
38
- if (labelUniverse.includes("undefined")) n -= 1;
39
- return n;
40
- }, [labelCounts, labelUniverse]);
41
-
42
- const distinctColoringDisabled = distinctLabelCount > MAX_DISTINCT_LABEL_COLORS;
43
-
44
- const labelsInfo = useMemo(
45
- () =>
46
- buildLabelsInfo({
47
- datasetLabels: datasetInfo?.labels ?? [],
48
- embeddings,
49
- distinctColoringDisabled,
50
- labelFilter,
51
- }),
52
- [datasetInfo?.labels, embeddings, distinctColoringDisabled, labelFilter]
53
- );
54
-
55
- const labelColorMap = useMemo(
56
- () =>
57
- buildLabelColorMap({
58
- labelsInfo,
59
- labelUniverse,
60
- distinctColoringDisabled,
61
- labelFilter,
62
- }),
63
- [labelsInfo, labelUniverse, distinctColoringDisabled, labelFilter]
64
- );
65
-
66
- const legendLabels = useMemo(
67
- () =>
68
- buildLegendLabels({
69
- labelUniverse,
70
- labelCounts,
71
- query: labelSearch,
72
- }),
73
- [labelUniverse, labelCounts, labelSearch]
74
- );
75
-
76
- return {
77
- labelCounts,
78
- labelUniverse,
79
- distinctLabelCount,
80
- distinctColoringDisabled,
81
- labelsInfo,
82
- labelColorMap,
83
- legendLabels,
84
- };
85
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/lib/api.ts DELETED
@@ -1,103 +0,0 @@
1
- import type { DatasetInfo, EmbeddingsData, Sample, SamplesResponse } from "@/types";
2
-
3
- const API_BASE = process.env.NODE_ENV === "development" ? "http://127.0.0.1:6262" : "";
4
-
5
- export async function fetchDataset(): Promise<DatasetInfo> {
6
- const res = await fetch(`${API_BASE}/api/dataset`);
7
- if (!res.ok) {
8
- throw new Error(`Failed to fetch dataset: ${res.statusText}`);
9
- }
10
- return res.json();
11
- }
12
-
13
- export async function fetchSamples(
14
- offset: number = 0,
15
- limit: number = 100,
16
- label?: string
17
- ): Promise<SamplesResponse> {
18
- const params = new URLSearchParams({
19
- offset: offset.toString(),
20
- limit: limit.toString(),
21
- });
22
- if (label) {
23
- params.set("label", label);
24
- }
25
-
26
- const res = await fetch(`${API_BASE}/api/samples?${params}`);
27
- if (!res.ok) {
28
- throw new Error(`Failed to fetch samples: ${res.statusText}`);
29
- }
30
- return res.json();
31
- }
32
-
33
- export async function fetchEmbeddings(layoutKey?: string): Promise<EmbeddingsData> {
34
- const params = new URLSearchParams();
35
- if (layoutKey) {
36
- params.set("layout_key", layoutKey);
37
- }
38
- const query = params.toString();
39
- const res = await fetch(`${API_BASE}/api/embeddings${query ? `?${query}` : ""}`);
40
- if (!res.ok) {
41
- throw new Error(`Failed to fetch embeddings: ${res.statusText}`);
42
- }
43
- return res.json();
44
- }
45
-
46
- export async function fetchSample(sampleId: string): Promise<Sample> {
47
- const res = await fetch(`${API_BASE}/api/samples/${sampleId}`);
48
- if (!res.ok) {
49
- throw new Error(`Failed to fetch sample: ${res.statusText}`);
50
- }
51
- return res.json();
52
- }
53
-
54
- export async function fetchSamplesBatch(sampleIds: string[]): Promise<Sample[]> {
55
- const res = await fetch(`${API_BASE}/api/samples/batch`, {
56
- method: "POST",
57
- headers: {
58
- "Content-Type": "application/json",
59
- },
60
- body: JSON.stringify({ sample_ids: sampleIds }),
61
- });
62
- if (!res.ok) {
63
- throw new Error(`Failed to fetch samples batch: ${res.statusText}`);
64
- }
65
- const data = await res.json();
66
- return data.samples;
67
- }
68
-
69
- export interface LassoSelectionResponse {
70
- total: number;
71
- offset: number;
72
- limit: number;
73
- sample_ids: string[];
74
- samples: Sample[];
75
- }
76
-
77
- export async function fetchLassoSelection(args: {
78
- layoutKey: string;
79
- polygon: ArrayLike<number>;
80
- offset?: number;
81
- limit?: number;
82
- includeThumbnails?: boolean;
83
- signal?: AbortSignal;
84
- }): Promise<LassoSelectionResponse> {
85
- const res = await fetch(`${API_BASE}/api/selection/lasso`, {
86
- method: "POST",
87
- headers: {
88
- "Content-Type": "application/json",
89
- },
90
- body: JSON.stringify({
91
- layout_key: args.layoutKey,
92
- polygon: Array.from(args.polygon),
93
- offset: args.offset ?? 0,
94
- limit: args.limit ?? 100,
95
- include_thumbnails: args.includeThumbnails ?? true,
96
- }),
97
- signal: args.signal,
98
- });
99
- if (!res.ok) {
100
- throw new Error(`Failed to fetch lasso selection: ${res.statusText}`);
101
- }
102
- return res.json();
103
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
frontend/src/lib/labelColors.ts DELETED
@@ -1,153 +0,0 @@
1
- const MISSING_LABEL_SENTINEL = "undefined";
2
-
3
- export const MISSING_LABEL_COLOR = "#39d3cc"; // matches --accent-cyan
4
- export const FALLBACK_LABEL_COLOR = "#8b949e"; // matches --muted-foreground
5
-
6
- // Rerun-style auto colors: distribute hues by golden ratio conjugate.
7
- // See: context/repos/rerun/crates/viewer/re_viewer_context/src/utils/color.rs
8
- const GOLDEN_RATIO_CONJUGATE = (Math.sqrt(5) - 1) / 2; // ~0.61803398875
9
-
10
- function fnv1a32(input: string): number {
11
- // 32-bit FNV-1a hash
12
- let hash = 0x811c9dc5;
13
- for (let i = 0; i < input.length; i++) {
14
- hash ^= input.charCodeAt(i);
15
- hash = Math.imul(hash, 0x01000193);
16
- }
17
- return hash >>> 0;
18
- }
19
-
20
- function clamp01(v: number): number {
21
- if (v < 0) return 0;
22
- if (v > 1) return 1;
23
- return v;
24
- }
25
-
26
- function hsvToHex(h: number, s: number, v: number): string {
27
- // h/s/v in [0, 1]
28
- const hh = ((h % 1) + 1) % 1;
29
- const ss = clamp01(s);
30
- const vv = clamp01(v);
31
-
32
- const x = hh * 6;
33
- const i = Math.floor(x);
34
- const f = x - i;
35
-
36
- const p = vv * (1 - ss);
37
- const q = vv * (1 - ss * f);
38
- const t = vv * (1 - ss * (1 - f));
39
-
40
- let r = 0;
41
- let g = 0;
42
- let b = 0;
43
-
44
- switch (i % 6) {
45
- case 0:
46
- r = vv;
47
- g = t;
48
- b = p;
49
- break;
50
- case 1:
51
- r = q;
52
- g = vv;
53
- b = p;
54
- break;
55
- case 2:
56
- r = p;
57
- g = vv;
58
- b = t;
59
- break;
60
- case 3:
61
- r = p;
62
- g = q;
63
- b = vv;
64
- break;
65
- case 4:
66
- r = t;
67
- g = p;
68
- b = vv;
69
- break;
70
- case 5:
71
- r = vv;
72
- g = p;
73
- b = q;
74
- break;
75
- }
76
-
77
- const toHex = (x: number) => {
78
- const n = Math.round(clamp01(x) * 255);
79
- return n.toString(16).padStart(2, "0");
80
- };
81
-
82
- return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
83
- }
84
-
85
- export function labelToColor(label: string): string {
86
- if (!label) return FALLBACK_LABEL_COLOR;
87
- if (label === MISSING_LABEL_SENTINEL) return MISSING_LABEL_COLOR;
88
-
89
- return colorForKey(label);
90
- }
91
-
92
- function colorForKey(key: string): string {
93
- const seed = fnv1a32(key);
94
- const val = seed & 0xffff; // map to u16
95
- const h = (val * GOLDEN_RATIO_CONJUGATE) % 1;
96
-
97
- // Match Rerun's defaults: saturation 0.85, value 0.5
98
- return hsvToHex(h, 0.85, 0.5);
99
- }
100
-
101
- function stableLabelSort(a: string, b: string): number {
102
- if (a === MISSING_LABEL_SENTINEL && b !== MISSING_LABEL_SENTINEL) return 1;
103
- if (b === MISSING_LABEL_SENTINEL && a !== MISSING_LABEL_SENTINEL) return -1;
104
- return a.localeCompare(b);
105
- }
106
-
107
- /**
108
- * Builds a deterministic, collision-free label → color mapping for the given
109
- * label universe.
110
- *
111
- * Notes:
112
- * - No modulo/cycling: if there are N labels, we produce N colors.
113
- * - Deterministic: same input set yields same mapping.
114
- * - Collision-free within the provided set via deterministic rehashing.
115
- */
116
- export function createLabelColorMap(labels: string[]): Record<string, string> {
117
- const unique = Array.from(new Set(labels.map((l) => normalizeLabel(l)))).sort(stableLabelSort);
118
-
119
- const colors: Record<string, string> = {};
120
- const used = new Set<string>();
121
-
122
- for (const label of unique) {
123
- if (label === MISSING_LABEL_SENTINEL) {
124
- colors[label] = MISSING_LABEL_COLOR;
125
- used.add(MISSING_LABEL_COLOR.toLowerCase());
126
- continue;
127
- }
128
-
129
- let attempt = 0;
130
- // Deterministic collision resolution (should be extremely rare).
131
- while (attempt < 32) {
132
- const candidate = colorForKey(attempt === 0 ? label : `${label}#${attempt}`);
133
- const normalized = candidate.toLowerCase();
134
- if (!used.has(normalized)) {
135
- colors[label] = candidate;
136
- used.add(normalized);
137
- break;
138
- }
139
- attempt++;
140
- }
141
-
142
- if (!colors[label]) {
143
- // Should never happen, but keep UI resilient.
144
- colors[label] = FALLBACK_LABEL_COLOR;
145
- }
146
- }
147
-
148
- return colors;
149
- }
150
-
151
- export function normalizeLabel(label: string | null | undefined): string {
152
- return label && label.length > 0 ? label : MISSING_LABEL_SENTINEL;
153
- }