yassinekolsi commited on
Commit
a712e78
·
1 Parent(s): 0547aee

feat: Complete BioFlow Orchestrator hackathon implementation

Browse files

Major Features:
- Langflow visual workflow builder integration (embedded iframe)
- Drug discovery search with real Qdrant vector search (23,531 vectors)
- 3D embedding explorer with real PCA projections
- FastAPI backend with DeepPurpose Morgan + CNN encoding

Bug Fixes:
- Fixed division by zero error (bypass data_process, direct Morgan encoding)
- Fixed CUDA/CPU device mismatch (auto-detect from model parameters)
- Fixed encoding dimension alignment for vector search

UI Improvements:
- Clean white design (removed dark gradients)
- Real API calls only (no fake/mock data)
- Simplified workflow page (removed hover effects, indicators)
- Working search with Similarity/Binding Affinity/Properties modes

Cleanup:
- Removed INSTRUCTIONS.md, setup_venv.txt
- Updated .gitignore for langflow_venv, data files
- Added Langflow pipeline template

Services:
- Qdrant: 6333
- FastAPI: 8001
- Next.js: 3000
- Langflow: 7860

.gitignore CHANGED
@@ -1,5 +1,6 @@
1
  # Python
2
  .venv/
 
3
  __pycache__/
4
  *.pyc
5
  *.pyo
@@ -22,6 +23,17 @@ runs/*/events.out.tfevents.*
22
  *.pt
23
  *.npy
24
 
 
 
 
 
 
 
 
25
  # OS
26
  .DS_Store
27
- Thumbs.db
 
 
 
 
 
1
  # Python
2
  .venv/
3
+ langflow_venv/
4
  __pycache__/
5
  *.pyc
6
  *.pyo
 
23
  *.pt
24
  *.npy
25
 
26
+ # Data files
27
+ data/
28
+ predictions_test.csv
29
+
30
+ # Langflow local db
31
+ .langflow/
32
+
33
  # OS
34
  .DS_Store
35
+ Thumbs.db
36
+
37
+ # Next.js
38
+ ui/.next/
39
+ ui/node_modules/
INSTRUCTIONS.md DELETED
@@ -1,68 +0,0 @@
1
- # Hackathon Setup: DeepPurpose + Qdrant + UI (v2)
2
-
3
- ## Architecture Overview
4
- ```
5
- ┌─────────────┐ ┌──────────────┐ ┌─────────────┐
6
- │ Next.js UI │────▶│ FastAPI │────▶│ Qdrant │
7
- │ (3000) │ │ (8000) │ │ (6333) │
8
- └─────────────┘ └──────────────┘ └─────────────┘
9
-
10
- ┌──────┴──────┐
11
- │ DeepPurpose │
12
- │ Model │
13
- └─────────────┘
14
- ```
15
-
16
- ## Quick Start
17
-
18
- ### 1. Start Qdrant (Vector Database)
19
- ```bash
20
- docker run -p 6333:6333 -p 6334:6334 qdrant/qdrant
21
- ```
22
-
23
- ### 2. Ingest Data (Phase 1)
24
- ```bash
25
- .venv\Scripts\python ingest_qdrant.py
26
- ```
27
- This will:
28
- - Load the trained model from `runs/20260125_104915_KIBA`
29
- - Generate embeddings for all drug-target pairs
30
- - Compute **real PCA** projections (not fake first-3-dims!)
31
- - Upload to Qdrant with pre-computed 3D coordinates
32
-
33
- ### 3. Start API Server (Phase 2)
34
- ```bash
35
- .venv\Scripts\python server/api.py
36
- ```
37
- API endpoints:
38
- - `GET /api/points?limit=500&view=combined` - Get 3D visualization data
39
- - `POST /api/search` - Find similar drugs/targets
40
- - `GET /health` - Check system status
41
-
42
- ### 4. Start Frontend (Phase 3)
43
- ```bash
44
- cd ui && pnpm dev
45
- ```
46
- Open http://localhost:3000/explorer
47
-
48
- ## What's Fixed (v2)
49
-
50
- | Issue | Before | After |
51
- |-------|--------|-------|
52
- | PCA | First 3 dims (meaningless) | Real sklearn PCA |
53
- | Data Order | Shuffled (broken alignment) | `shuffle=False` in DataLoader |
54
- | Dummy Data | `"M" * 10` (fragile) | Valid Aspirin SMILES |
55
- | Config | Duplicated | Shared `config.py` |
56
- | Error Handling | None | Validation + helpful messages |
57
- | Model Loading | Per-request | Cached at startup |
58
-
59
- ## Best Model Results (Kept Runs)
60
-
61
- | Dataset | CI | Pearson | Has model.pt |
62
- |---------|-------|---------|--------------|
63
- | BindingDB_Kd | **0.8083** | 0.7679 | No |
64
- | DAVIS | 0.7914 | 0.5446 | No |
65
- | KIBA | 0.7003 | 0.5219 | **Yes** |
66
-
67
- *Note: KIBA run has the saved model, but BindingDB has best metrics.*
68
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,19 +1,80 @@
1
- # 🧬 Multimodal Biological Design & Discovery Intelligence
2
 
3
- > Drug-Target Interaction prediction platform powered by DeepPurpose ML + Qdrant vector search + Next.js visualization
 
4
 
5
  ![Python](https://img.shields.io/badge/Python-3.10-blue)
6
  ![Next.js](https://img.shields.io/badge/Next.js-16-black)
 
7
  ![CUDA](https://img.shields.io/badge/CUDA-11.8-green)
8
- ![License](https://img.shields.io/badge/License-MIT-yellow)
 
9
 
10
- ## 🎯 Overview
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
- This platform enables **drug discovery** through:
13
- - **Deep Learning** — Morgan fingerprints + CNN protein encoding (DeepPurpose)
14
- - **Vector Search** — Similarity search via Qdrant embeddings
15
- - **3D Visualization** — Real PCA projections of drug-target space
16
- - **Interactive UI** — Next.js dashboard with Recharts
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
 
18
  ## 📊 Model Performance
19
 
@@ -23,37 +84,29 @@ This platform enables **drug discovery** through:
23
  | **BindingDB_Kd** | 0.8083 | 0.7679 | 0.6668 |
24
  | **DAVIS** | 0.7914 | 0.5446 | 0.4684 |
25
 
26
- ## 🏗️ Architecture
27
-
28
- ```
29
- ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
30
- │ TDC Dataset │────▶│ DeepPurpose │────▶│ Qdrant │
31
- │ (KIBA/DAVIS) │ │ Morgan + CNN │ │ 256D Vectors │
32
- └─────────────────┘ └─────────────────┘ └────────┬────────┘
33
-
34
- ┌─────────────────┐ ┌─────────────────┐ │
35
- │ Next.js UI │◀────│ FastAPI │◀─────────────┘
36
- │ localhost:3000│ │ localhost:8001│ Similarity Search
37
- └─────────────────┘ └─────────────────┘
38
- ```
39
 
40
  ## 🚀 Quick Start
41
 
42
  ### Prerequisites
43
- - Python 3.10
44
  - Node.js 18+
45
- - Docker Desktop (for Qdrant)
46
- - CUDA 11.8 (optional, for GPU)
47
 
48
- ### 1. Setup Python Environment
49
  ```bash
 
 
 
 
50
  python -m venv .venv
51
  .venv\Scripts\activate # Windows
52
  pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
53
  pip install DeepPurpose qdrant-client fastapi uvicorn scikit-learn
54
  ```
55
 
56
- ### 2. Start Qdrant (Vector Database)
57
  ```bash
58
  docker run -d --name qdrant -p 6333:6333 -p 6334:6334 qdrant/qdrant:latest
59
  ```
@@ -61,11 +114,11 @@ docker run -d --name qdrant -p 6333:6333 -p 6334:6334 qdrant/qdrant:latest
61
  ### 3. Ingest Data (One-time)
62
  ```bash
63
  python ingest_qdrant.py
64
- # Loads KIBA dataset, generates embeddings, uploads to Qdrant
65
  # ~23,531 drug-target pairs indexed
66
  ```
67
 
68
- ### 4. Start API Server
69
  ```bash
70
  python -m uvicorn server.api:app --host 0.0.0.0 --port 8001
71
  ```
@@ -78,58 +131,109 @@ pnpm dev
78
  # Open http://localhost:3000
79
  ```
80
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  ## 📁 Project Structure
82
 
83
  ```
84
- ├── config.py # Shared configuration (model paths, Qdrant settings)
85
  ├── ingest_qdrant.py # ETL: TDC → DeepPurpose → Qdrant
86
  ├── deeppurpose002.py # Model training script
87
  ├── server/
88
- │ └── api.py # FastAPI backend (/health, /api/points, /api/search)
89
  ├── runs/
90
- │ └── 20260125_104915_KIBA/ # Best model checkpoint
91
- │ ├── model.pt
92
- │ └── config.pkl
93
- ├── ui/ # Next.js 16 + Shadcn UI
94
  │ ├── app/
95
- │ │ ├── explorer/ # 3D scatter plot visualization
96
- │ │ ├── discovery/ # Drug discovery interface
97
- │ │ ── data/ # Data browser
98
- │ └── lib/
99
- └── explorer-service.ts # API client
100
  └── data/
101
  └── kiba.tab # Cached TDC dataset
102
  ```
103
 
 
 
104
  ## 🔌 API Endpoints
105
 
106
  | Endpoint | Method | Description |
107
  |----------|--------|-------------|
108
- | `/health` | GET | Service health + metrics |
109
  | `/api/points` | GET | Get 3D PCA points for visualization |
110
  | `/api/search` | POST | Similarity search by SMILES/sequence |
111
 
112
- ### Example: Get Points
113
- ```bash
114
- curl "http://localhost:8001/api/points?limit=100&view=combined"
115
- ```
116
-
117
- ### Example: Search Similar
118
  ```bash
119
  curl -X POST "http://localhost:8001/api/search" \
120
  -H "Content-Type: application/json" \
121
- -d '{"smiles": "CC(=O)OC1=CC=CC=C1C(=O)O", "top_k": 10}'
122
  ```
123
 
124
- ## 🧪 Training New Models
125
 
126
- ```bash
127
- # Edit deeppurpose002.py to change dataset/encoding
128
- python deeppurpose002.py
129
 
130
- # Re-ingest with new model
131
- python ingest_qdrant.py
132
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
  ## 📄 License
135
 
 
1
+ # 🧬 BioFlow Orchestrator
2
 
3
+ > **Multimodal Biological Design & Discovery Intelligence Engine**
4
+ > A low-code workflow platform for unified biological discovery pipelines
5
 
6
  ![Python](https://img.shields.io/badge/Python-3.10-blue)
7
  ![Next.js](https://img.shields.io/badge/Next.js-16-black)
8
+ ![Qdrant](https://img.shields.io/badge/Qdrant-Vector_DB-red)
9
  ![CUDA](https://img.shields.io/badge/CUDA-11.8-green)
10
+ ![Langflow](https://img.shields.io/badge/Langflow-1.7-orange)
11
+ ![Team](https://img.shields.io/badge/Team-Lacoste-purple)
12
 
13
+ ---
14
+
15
+ ## 🎯 Problem Statement
16
+
17
+ Biological R&D knowledge is fragmented across disconnected silos:
18
+ - **Textual literature** (papers, lab notes)
19
+ - **3D structural data** (PDB files)
20
+ - **Chemical sequences** (SMILES)
21
+
22
+ Researchers must manually navigate incompatible formats, creating bottlenecks and "blind spots" where critical connections are missed.
23
+
24
+ ## 💡 Our Solution
25
+
26
+ **BioFlow Orchestrator** is a visual workflow engine that unifies biological discovery pipelines. Rather than a single "black box" model, we function as an **intelligent orchestrator** — allowing researchers to chain state-of-the-art open-source biological models into coherent discovery workflows.
27
+
28
+ ### Key Features
29
+
30
+ | Feature | Description |
31
+ |---------|-------------|
32
+ | 🔗 **Visual Pipeline Builder** | Drag-and-drop node editor for constructing discovery workflows |
33
+ | 🧠 **DeepPurpose Integration** | Drug-Target Interaction prediction with Morgan + CNN encoding |
34
+ | 🔍 **Qdrant Vector Search** | High-dimensional similarity search across 23,531+ compounds |
35
+ | 🎨 **3D Embedding Explorer** | Real PCA projections of drug-target chemical space |
36
+ | ✅ **Validator Agents** | Automated toxicity and novelty checking |
37
 
38
+ ---
39
+
40
+ ## 🏗️ Architecture
41
+
42
+ ```
43
+ ┌──────────────────────────────────────────┐
44
+ │ BioFlow Orchestrator │
45
+ │ Visual Pipeline Builder (UI) │
46
+ └─────────────────┬────────────────────────┘
47
+
48
+ ┌─────────────────────────────────┼─────────────────────────────────┐
49
+ │ │ │
50
+ ▼ ▼ ▼
51
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
52
+ │ Data Input │ │ DeepPurpose │ │ OpenBioMed │
53
+ │ SMILES/Protein │────────────▶│ DTI Model │────────────▶│ Multimodal │
54
+ │ Sequences │ │ Morgan + CNN │ │ Embeddings │
55
+ └─────────────────┘ └────────┬────────┘ └────────┬────────┘
56
+ │ │
57
+ └───────────────┬───────────────┘
58
+
59
+
60
+ ┌─────────────────┐
61
+ │ Qdrant │
62
+ │ Vector DB │
63
+ │ HNSW Indexing │
64
+ │ 23,531 vectors │
65
+ └────────┬────────┘
66
+
67
+ ┌─────────────────────────────┼─────────────────────────────┐
68
+ │ │ │
69
+ ▼ ▼ ▼
70
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
71
+ │ Similarity │ │ Validator │ │ Results │
72
+ │ Search Agent │ │ Agent │ │ Output │
73
+ │ Top-K Retrieval │ │ Toxicity/Novelty│ │ Candidates │
74
+ └─────────────────┘ └─────────────────┘ └─────────────────┘
75
+ ```
76
+
77
+ ---
78
 
79
  ## 📊 Model Performance
80
 
 
84
  | **BindingDB_Kd** | 0.8083 | 0.7679 | 0.6668 |
85
  | **DAVIS** | 0.7914 | 0.5446 | 0.4684 |
86
 
87
+ ---
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
  ## 🚀 Quick Start
90
 
91
  ### Prerequisites
92
+ - Python 3.10+
93
  - Node.js 18+
94
+ - Docker Desktop
95
+ - CUDA 11.8 (optional, for GPU acceleration)
96
 
97
+ ### 1. Clone & Setup
98
  ```bash
99
+ git clone https://github.com/hamzasammoud11-dotcom/lacoste001.git
100
+ cd lacoste001
101
+
102
+ # Python environment
103
  python -m venv .venv
104
  .venv\Scripts\activate # Windows
105
  pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
106
  pip install DeepPurpose qdrant-client fastapi uvicorn scikit-learn
107
  ```
108
 
109
+ ### 2. Start Qdrant Vector Database
110
  ```bash
111
  docker run -d --name qdrant -p 6333:6333 -p 6334:6334 qdrant/qdrant:latest
112
  ```
 
114
  ### 3. Ingest Data (One-time)
115
  ```bash
116
  python ingest_qdrant.py
117
+ # Loads KIBA dataset DeepPurpose embeddings Qdrant
118
  # ~23,531 drug-target pairs indexed
119
  ```
120
 
121
+ ### 4. Start Backend API
122
  ```bash
123
  python -m uvicorn server.api:app --host 0.0.0.0 --port 8001
124
  ```
 
131
  # Open http://localhost:3000
132
  ```
133
 
134
+ ### 6. Start Langflow (Visual Workflow Builder)
135
+ ```bash
136
+ pip install langflow
137
+ langflow run --host 0.0.0.0 --port 7860
138
+ # Access via http://localhost:3000/workflow (embedded)
139
+ # Or directly at http://localhost:7860
140
+ ```
141
+
142
+ ---
143
+
144
+ ## 🎨 Visual Workflow Builder (Langflow Integration)
145
+
146
+ BioFlow integrates **Langflow** as the visual workflow engine, providing a full-screen drag-and-drop pipeline builder accessible from `/workflow`.
147
+
148
+ ### Building a DTI Pipeline in Langflow
149
+
150
+ 1. **Import the Template Flow**:
151
+ - Open Langflow (`/workflow` or `localhost:7860`)
152
+ - Click "New Project" → "Import"
153
+ - Load `langflow/bioflow_dti_pipeline.json`
154
+
155
+ 2. **Configure the Pipeline**:
156
+ - **Drug Input**: Enter SMILES string (e.g., `CC(=O)Nc1ccc(O)cc1`)
157
+ - **Target Input**: Enter protein sequence
158
+ - **API Nodes**: Point to `http://localhost:8001/api/*`
159
+
160
+ 3. **Run the Flow**:
161
+ - Click "Run" to execute DeepPurpose encoding → Qdrant search → Results
162
+
163
+ ---
164
+
165
  ## 📁 Project Structure
166
 
167
  ```
168
+ ├── config.py # Shared configuration
169
  ├── ingest_qdrant.py # ETL: TDC → DeepPurpose → Qdrant
170
  ├── deeppurpose002.py # Model training script
171
  ├── server/
172
+ │ └── api.py # FastAPI backend
173
  ├── runs/
174
+ │ └── 20260125_104915_KIBA/
175
+ │ ├── model.pt # Trained model weights
176
+ │ └── config.pkl # Model configuration
177
+ ├── ui/
178
  │ ├── app/
179
+ │ │ ├── workflow/ # 🆕 Visual Pipeline Builder
180
+ │ │ ├── explorer/ # 3D Embedding Visualization
181
+ │ │ ── discovery/ # Drug Discovery Interface
182
+ └── data/ # Data Browser
183
+ └── components/
184
  └── data/
185
  └── kiba.tab # Cached TDC dataset
186
  ```
187
 
188
+ ---
189
+
190
  ## 🔌 API Endpoints
191
 
192
  | Endpoint | Method | Description |
193
  |----------|--------|-------------|
194
+ | `/health` | GET | Service health + model metrics |
195
  | `/api/points` | GET | Get 3D PCA points for visualization |
196
  | `/api/search` | POST | Similarity search by SMILES/sequence |
197
 
198
+ ### Example: Search Similar Compounds
 
 
 
 
 
199
  ```bash
200
  curl -X POST "http://localhost:8001/api/search" \
201
  -H "Content-Type: application/json" \
202
+ -d '{"smiles": "CC(=O)Nc1ccc(O)cc1", "top_k": 10}'
203
  ```
204
 
205
+ ---
206
 
207
+ ## 🛠️ Qdrant Integration Strategy
 
 
208
 
209
+ ### 1. Multimodal Bridge
210
+ Using OpenBioMed for joint embeddings across proteins, molecules, and text — enabling **cross-modal retrieval**.
211
+
212
+ ### 2. Dynamic Workflow Memory
213
+ Pipeline nodes store intermediate results in Qdrant collections, enabling agent-to-agent communication.
214
+
215
+ ### 3. High-Dimensional Scalability
216
+ HNSW indexing handles bio-embeddings at scale, keeping similarity searches interactive and real-time.
217
+
218
+ ---
219
+
220
+ ## 👥 Team Lacoste
221
+
222
+ | Name | Role |
223
+ |------|------|
224
+ | **Hamza Sammoud** | ML Pipeline & Backend |
225
+ | **Rami Troudi** | Frontend UI |
226
+
227
+ ---
228
+
229
+ ## 📚 Resources
230
+
231
+ - [DeepPurpose](https://github.com/kexinhuang12345/DeepPurpose) — DTI Prediction Toolkit
232
+ - [OpenBioMed](https://github.com/PharMolix/OpenBioMed) — Multimodal AI Framework
233
+ - [Qdrant](https://qdrant.tech/) — Vector Database
234
+ - [TDC](https://tdcommons.ai/) — Therapeutics Data Commons
235
+
236
+ ---
237
 
238
  ## 📄 License
239
 
langflow/bioflow_dti_pipeline.json ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "BioFlow DTI Pipeline",
3
+ "description": "Drug-Target Interaction prediction pipeline using DeepPurpose model and Qdrant vector search. This flow represents the BioFlow Orchestrator hackathon project.",
4
+ "data": {
5
+ "nodes": [
6
+ {
7
+ "id": "TextInput-SMILES",
8
+ "type": "genericNode",
9
+ "position": {"x": 100, "y": 200},
10
+ "data": {
11
+ "type": "TextInput",
12
+ "node": {
13
+ "template": {
14
+ "input_value": {
15
+ "value": "CC(=O)Nc1ccc(O)cc1",
16
+ "display_name": "Drug SMILES",
17
+ "info": "Enter a SMILES string representing the drug molecule (e.g., Acetaminophen: CC(=O)Nc1ccc(O)cc1)"
18
+ }
19
+ },
20
+ "display_name": "Drug Input (SMILES)",
21
+ "description": "Input drug molecule in SMILES format"
22
+ }
23
+ }
24
+ },
25
+ {
26
+ "id": "TextInput-Protein",
27
+ "type": "genericNode",
28
+ "position": {"x": 100, "y": 400},
29
+ "data": {
30
+ "type": "TextInput",
31
+ "node": {
32
+ "template": {
33
+ "input_value": {
34
+ "value": "MKKFFDSRREQGGSGLGSGSSGGGGSTSGLGSGYIGRVFGIGRQQVTVDEVLAEGGFAIVFLVRTSNGMKCALKRMFVNNEHDLQVCKREIQIMRDLSGHKNIVGYIDSSINNVSSGDVWEVLILMDFCRGGQVVNLMNQRLQTGFTENEVLQIFCDTCEAVARLHQCKTPIIHRDLKVENILLHDRGHYVLCDFGSATNKFQNPQTEGVNAVEDEIKKYTTLSYRAPEMVNLYSGKIITTKADIWALGCLLYKLCYFTLPFGESQVAICDGNFTIPDNSRYSQDMHCLIRYMLEPDPDKRPDIYQVSYFSFKLLKKECPIPNVQNSPIPAKLPEPVKASEAAAKKTQPKARLTDPIPTTETSIAPRQRPKAGQTQPNPGILPIQPALTPRKRATVQPPPQAAGSSNQPGLLASVPQPKPQAPPSQPLPQTQAKQPQAPPTPQQTPSTQAQGLPAQAQATPQHQQQLFLKQQQQQQQPPPAQQQPAGTFYQQQQAQTQQFQAVHPATQKPAIAQFPVVSQGGSQQQLMQNFYQQQQQQQQQQQQQQLATALHQQQLMTQQAALQQKPTMAAGQQPQPQPAAAPQPAPAQEPAIQAPVRQQPKVQTTPPPAVQGQKVGSLTPPSSPKTQRAGHRRILSDVTHSAVFGVPASKSTQLLQAAAAEASLNKSKSATTTPSGSPRTSQQNVYNPSEGSTWNPFDDDNFSKLTAEELLNKDFAKLGEGKHPEKLGGSAESLIPGFQSTQGDAFATTSFSAGTAEKRKGGQTVDSGLPLLSVSDPFIPLQVPDAPEKLIEGLKSPDTSLLLPDLLPMTDPFGSTSDAVIEKADVAVESLIPGLEPPVPQRLPSQTESVTSNRTDSLTGEDSLLDCSLLSNPTTDLLEEFAPTAISAPVHKAAEDSNLISGFDVPEGSDKVAEDEFDPIPVLITKNPQGGHSRNSSGSSESSLPNLARSLLLVDQLIDL",
35
+ "display_name": "Target Sequence",
36
+ "info": "Enter a protein target amino acid sequence (e.g., AAK1 kinase)"
37
+ }
38
+ },
39
+ "display_name": "Target Input (Protein)",
40
+ "description": "Input target protein amino acid sequence"
41
+ }
42
+ }
43
+ },
44
+ {
45
+ "id": "APIRequest-Encode",
46
+ "type": "genericNode",
47
+ "position": {"x": 400, "y": 300},
48
+ "data": {
49
+ "type": "APIRequest",
50
+ "node": {
51
+ "template": {
52
+ "url": {
53
+ "value": "http://localhost:8001/api/search",
54
+ "display_name": "BioFlow API URL"
55
+ },
56
+ "method": {
57
+ "value": "POST",
58
+ "display_name": "HTTP Method"
59
+ },
60
+ "body": {
61
+ "value": "{\"query\": \"{smiles}\", \"type\": \"drug\", \"limit\": 20}",
62
+ "display_name": "Request Body",
63
+ "info": "DeepPurpose encoding + Qdrant vector search"
64
+ }
65
+ },
66
+ "display_name": "DeepPurpose Encoder",
67
+ "description": "Morgan fingerprint + CNN encoding via BioFlow API"
68
+ }
69
+ }
70
+ },
71
+ {
72
+ "id": "APIRequest-Qdrant",
73
+ "type": "genericNode",
74
+ "position": {"x": 700, "y": 200},
75
+ "data": {
76
+ "type": "APIRequest",
77
+ "node": {
78
+ "template": {
79
+ "url": {
80
+ "value": "http://localhost:8001/api/points?limit=500&view=combined",
81
+ "display_name": "Qdrant Visualization API"
82
+ },
83
+ "method": {
84
+ "value": "GET",
85
+ "display_name": "HTTP Method"
86
+ }
87
+ },
88
+ "display_name": "Qdrant Vector Store",
89
+ "description": "23,531 drug-target pairs from KIBA dataset"
90
+ }
91
+ }
92
+ },
93
+ {
94
+ "id": "ConditionalRouter-Affinity",
95
+ "type": "genericNode",
96
+ "position": {"x": 700, "y": 400},
97
+ "data": {
98
+ "type": "ConditionalRouter",
99
+ "node": {
100
+ "template": {
101
+ "condition": {
102
+ "value": "score > 0.8",
103
+ "display_name": "High Affinity Threshold"
104
+ }
105
+ },
106
+ "display_name": "Affinity Filter",
107
+ "description": "Filter results by binding affinity score"
108
+ }
109
+ }
110
+ },
111
+ {
112
+ "id": "TextOutput-Results",
113
+ "type": "genericNode",
114
+ "position": {"x": 1000, "y": 300},
115
+ "data": {
116
+ "type": "TextOutput",
117
+ "node": {
118
+ "template": {
119
+ "input_value": {
120
+ "display_name": "DTI Predictions",
121
+ "info": "Top candidate drug-target interactions"
122
+ }
123
+ },
124
+ "display_name": "Pipeline Output",
125
+ "description": "Ranked drug-target interaction predictions"
126
+ }
127
+ }
128
+ }
129
+ ],
130
+ "edges": [
131
+ {
132
+ "id": "e1",
133
+ "source": "TextInput-SMILES",
134
+ "target": "APIRequest-Encode",
135
+ "sourceHandle": "output",
136
+ "targetHandle": "input"
137
+ },
138
+ {
139
+ "id": "e2",
140
+ "source": "TextInput-Protein",
141
+ "target": "APIRequest-Encode",
142
+ "sourceHandle": "output",
143
+ "targetHandle": "input"
144
+ },
145
+ {
146
+ "id": "e3",
147
+ "source": "APIRequest-Encode",
148
+ "target": "APIRequest-Qdrant",
149
+ "sourceHandle": "output",
150
+ "targetHandle": "input"
151
+ },
152
+ {
153
+ "id": "e4",
154
+ "source": "APIRequest-Qdrant",
155
+ "target": "ConditionalRouter-Affinity",
156
+ "sourceHandle": "output",
157
+ "targetHandle": "input"
158
+ },
159
+ {
160
+ "id": "e5",
161
+ "source": "ConditionalRouter-Affinity",
162
+ "target": "TextOutput-Results",
163
+ "sourceHandle": "true_output",
164
+ "targetHandle": "input"
165
+ }
166
+ ]
167
+ },
168
+ "is_component": false,
169
+ "folder": null,
170
+ "endpoint_name": "bioflow-dti"
171
+ }
server/api.py CHANGED
@@ -50,7 +50,7 @@ _device = None
50
 
51
  class SearchRequest(BaseModel):
52
  query: str
53
- type: str # "drug" (SMILES) or "target" (Sequence)
54
  limit: int = 20
55
 
56
  class PointsRequest(BaseModel):
@@ -89,9 +89,16 @@ async def load_resources():
89
  else:
90
  print(f"[WARNING] No model.pt found at {model_path}")
91
 
92
- # Use CPU for inference (avoids VRAM contention)
93
- _device = torch.device('cpu')
94
- _model.model.to(_device)
 
 
 
 
 
 
 
95
  _model.model.eval()
96
 
97
  print("[STARTUP] Connecting to Qdrant...")
@@ -106,47 +113,61 @@ async def load_resources():
106
  print("[STARTUP] Ready!")
107
 
108
  def encode_query(query: str, query_type: str) -> List[float]:
109
- """Encode a single drug/target query into a vector."""
110
  if not _model:
111
  raise HTTPException(status_code=503, detail="Model not initialized")
112
 
113
  try:
114
  if query_type == "drug":
115
- # Use valid dummy target for data_process
116
- data = utils.data_process(
117
- [query], [VALID_DUMMY_TARGET], [0],
118
- _model.config['drug_encoding'],
119
- _model.config['target_encoding'],
120
- split_method='random', frac=[0,0,1], random_seed=1
121
- )[2]
 
 
122
 
123
- loader = torch.utils.data.DataLoader(data, batch_size=1, shuffle=False)
124
- v_d, _, _ = next(iter(loader))
 
 
 
 
 
125
 
126
  with torch.no_grad():
127
- v_d = v_d.to(_device)
128
  vector = _model.model.model_drug(v_d).cpu().numpy()[0].tolist()
129
  return vector
130
 
131
  elif query_type == "target":
132
- # Use valid dummy drug for data_process
133
- data = utils.data_process(
134
- [VALID_DUMMY_DRUG], [query], [0],
135
- _model.config['drug_encoding'],
136
- _model.config['target_encoding'],
137
- split_method='random', frac=[0,0,1], random_seed=1
138
- )[2]
 
139
 
140
- loader = torch.utils.data.DataLoader(data, batch_size=1, shuffle=False)
141
- _, v_p, _ = next(iter(loader))
 
 
 
 
 
 
142
 
143
  with torch.no_grad():
144
- v_p = v_p.to(_device)
145
  vector = _model.model.model_protein(v_p).cpu().numpy()[0].tolist()
146
  return vector
147
  else:
148
  raise HTTPException(status_code=400, detail="type must be 'drug' or 'target'")
149
 
 
 
150
  except Exception as e:
151
  import traceback
152
  traceback.print_exc()
@@ -158,7 +179,17 @@ async def search_vectors(req: SearchRequest):
158
  if not _qdrant:
159
  raise HTTPException(status_code=503, detail="Qdrant not connected")
160
 
161
- vector = encode_query(req.query, req.type)
 
 
 
 
 
 
 
 
 
 
162
 
163
  try:
164
  hits = _qdrant.search(
@@ -182,6 +213,40 @@ async def search_vectors(req: SearchRequest):
182
 
183
  return {"results": results, "query_type": req.type, "count": len(results)}
184
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  @app.get("/api/points")
186
  async def get_visualization_points(limit: int = 500, view: str = "combined"):
187
  """Get points with pre-computed PCA for 3D visualization."""
@@ -246,6 +311,44 @@ def health():
246
  "metrics": METRICS,
247
  }
248
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  if __name__ == "__main__":
250
  import uvicorn
251
  uvicorn.run(app, host="0.0.0.0", port=8000)
 
50
 
51
  class SearchRequest(BaseModel):
52
  query: str
53
+ type: str # "drug" (SMILES) or "target" (Sequence) or "text" (plain text search)
54
  limit: int = 20
55
 
56
  class PointsRequest(BaseModel):
 
89
  else:
90
  print(f"[WARNING] No model.pt found at {model_path}")
91
 
92
+ # CRITICAL FIX: Override DeepPurpose's global device variable
93
+ # The encoders.py uses a module-level `device = torch.device('cuda' if...)`
94
+ # and the MLP forward does `v = v.float().to(device)` using that global!
95
+ import DeepPurpose.encoders as dp_encoders
96
+ _device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
97
+ dp_encoders.device = _device # Override the global
98
+ print(f"[STARTUP] Using device: {_device}")
99
+
100
+ # Ensure model is on the correct device
101
+ _model.model = _model.model.to(_device)
102
  _model.model.eval()
103
 
104
  print("[STARTUP] Connecting to Qdrant...")
 
113
  print("[STARTUP] Ready!")
114
 
115
  def encode_query(query: str, query_type: str) -> List[float]:
116
+ """Encode a single drug/target query into a vector using direct encoding."""
117
  if not _model:
118
  raise HTTPException(status_code=503, detail="Model not initialized")
119
 
120
  try:
121
  if query_type == "drug":
122
+ # Direct Morgan fingerprint encoding (avoid data_process)
123
+ from DeepPurpose.utils import smiles2morgan
124
+ from rdkit import Chem
125
+ import numpy as np
126
+
127
+ # Validate SMILES
128
+ mol = Chem.MolFromSmiles(query)
129
+ if mol is None:
130
+ raise ValueError(f"Invalid SMILES: {query}")
131
 
132
+ # Get Morgan fingerprint
133
+ morgan_fp = smiles2morgan(query, radius=2, nBits=1024)
134
+ if morgan_fp is None:
135
+ raise ValueError(f"Failed to compute Morgan fingerprint for: {query}")
136
+
137
+ # Convert to tensor and encode through model's drug encoder
138
+ v_d = torch.tensor(np.array([morgan_fp]), dtype=torch.float32)
139
 
140
  with torch.no_grad():
 
141
  vector = _model.model.model_drug(v_d).cpu().numpy()[0].tolist()
142
  return vector
143
 
144
  elif query_type == "target":
145
+ # Direct CNN target encoding
146
+ from DeepPurpose.utils import trans_protein
147
+ import numpy as np
148
+
149
+ # Encode protein sequence
150
+ target_encoding = trans_protein(query)
151
+ if target_encoding is None:
152
+ raise ValueError(f"Failed to encode protein sequence")
153
 
154
+ # CNN expects [batch, seq_len] input, max_len=1000 in default config
155
+ MAX_SEQ_LEN = 1000
156
+ if len(target_encoding) > MAX_SEQ_LEN:
157
+ target_encoding = target_encoding[:MAX_SEQ_LEN]
158
+ else:
159
+ target_encoding = target_encoding + [0] * (MAX_SEQ_LEN - len(target_encoding))
160
+
161
+ v_p = torch.tensor(np.array([target_encoding]), dtype=torch.long)
162
 
163
  with torch.no_grad():
 
164
  vector = _model.model.model_protein(v_p).cpu().numpy()[0].tolist()
165
  return vector
166
  else:
167
  raise HTTPException(status_code=400, detail="type must be 'drug' or 'target'")
168
 
169
+ except HTTPException:
170
+ raise
171
  except Exception as e:
172
  import traceback
173
  traceback.print_exc()
 
179
  if not _qdrant:
180
  raise HTTPException(status_code=503, detail="Qdrant not connected")
181
 
182
+ # Text search - just filter by payload, no encoding needed
183
+ if req.type == "text":
184
+ return await text_search(req.query, req.limit)
185
+
186
+ # Vector search - encode and search
187
+ try:
188
+ vector = encode_query(req.query, req.type)
189
+ except Exception as e:
190
+ # Fallback to text search if encoding fails
191
+ print(f"Encoding failed ({e}), falling back to text search")
192
+ return await text_search(req.query, req.limit)
193
 
194
  try:
195
  hits = _qdrant.search(
 
213
 
214
  return {"results": results, "query_type": req.type, "count": len(results)}
215
 
216
+
217
+ async def text_search(query: str, limit: int = 20):
218
+ """Text-based search through payloads (fallback when encoding fails)."""
219
+ try:
220
+ # Scroll through and filter by SMILES containing the query
221
+ res, _ = _qdrant.scroll(
222
+ collection_name=COLLECTION_NAME,
223
+ limit=500, # Get more to filter through
224
+ with_payload=True,
225
+ with_vectors=False
226
+ )
227
+
228
+ # Filter results that match query in SMILES or other fields
229
+ query_lower = query.lower()
230
+ results = []
231
+ for point in res:
232
+ smiles = point.payload.get("smiles", "").lower()
233
+ # Match if query is substring of SMILES or SMILES contains query
234
+ if query_lower in smiles:
235
+ results.append({
236
+ "id": point.id,
237
+ "score": 0.95 if query_lower == smiles else 0.8, # Higher score for exact match
238
+ "smiles": point.payload.get("smiles"),
239
+ "target_seq": point.payload.get("target_seq", "")[:100] + "...",
240
+ "label": point.payload.get("label_true"),
241
+ "affinity_class": point.payload.get("affinity_class"),
242
+ })
243
+ if len(results) >= limit:
244
+ break
245
+
246
+ return {"results": results, "query_type": "text", "count": len(results)}
247
+ except Exception as e:
248
+ raise HTTPException(status_code=500, detail=f"Text search failed: {str(e)}")
249
+
250
  @app.get("/api/points")
251
  async def get_visualization_points(limit: int = 500, view: str = "combined"):
252
  """Get points with pre-computed PCA for 3D visualization."""
 
311
  "metrics": METRICS,
312
  }
313
 
314
+ @app.get("/api/stats")
315
+ async def get_collection_stats():
316
+ """Get real statistics from Qdrant collection for the data page."""
317
+ if not _qdrant:
318
+ raise HTTPException(status_code=503, detail="Qdrant not connected")
319
+
320
+ try:
321
+ collection_info = _qdrant.get_collection(collection_name=COLLECTION_NAME)
322
+ total_vectors = collection_info.vectors_count
323
+
324
+ # Sample to count affinity classes
325
+ sample, _ = _qdrant.scroll(
326
+ collection_name=COLLECTION_NAME,
327
+ limit=1000,
328
+ with_payload=["affinity_class", "smiles", "target_id"],
329
+ with_vectors=False
330
+ )
331
+
332
+ unique_drugs = len(set(p.payload.get("smiles", "") for p in sample if p.payload.get("smiles")))
333
+ unique_targets = len(set(p.payload.get("target_id", "") for p in sample if p.payload.get("target_id")))
334
+
335
+ affinity_counts = {}
336
+ for p in sample:
337
+ aff = p.payload.get("affinity_class", "unknown")
338
+ affinity_counts[aff] = affinity_counts.get(aff, 0) + 1
339
+
340
+ return {
341
+ "total_vectors": total_vectors,
342
+ "sample_size": len(sample),
343
+ "unique_drugs_sampled": unique_drugs,
344
+ "unique_targets_sampled": unique_targets,
345
+ "affinity_distribution": affinity_counts,
346
+ "collection_name": COLLECTION_NAME,
347
+ "status": collection_info.status.value if hasattr(collection_info.status, 'value') else str(collection_info.status),
348
+ }
349
+ except Exception as e:
350
+ raise HTTPException(status_code=500, detail=f"Stats fetch failed: {str(e)}")
351
+
352
  if __name__ == "__main__":
353
  import uvicorn
354
  uvicorn.run(app, host="0.0.0.0", port=8000)
setup_venv.txt DELETED
@@ -1,62 +0,0 @@
1
- # Setup Guide for DeepPurpose (Windows/CUDA 11.8)
2
-
3
- This guide reproduces the current working environment for `deeppurpose002.py` on Windows 10/11 with an NVIDIA GPU.
4
-
5
- ## 1. Prerequisites
6
- - **Python 3.10.x** (Required for compatibility with DGL and DeepPurpose)
7
- - **NVIDIA GPU** with drivers installed.
8
-
9
- ## 2. Create Virtual Environment
10
- Open PowerShell/Terminal in your project folder:
11
- ```powershell
12
- python -3.10 -m venv .venv
13
- .\.venv\Scripts\Activate
14
- ```
15
-
16
- ## 3. Install PyTorch (CUDA 11.8)
17
- Install the specific CUDA-enabled version of PyTorch:
18
- ```powershell
19
- pip install torch==2.7.1+cu118 torchvision==0.22.1+cu118 torchaudio==2.7.1+cu118 --index-url https://download.pytorch.org/whl/cu118
20
- ```
21
- *(Note: If 2.7.1+cu118 is not available in the future, check pytorch.org for the compatible LTS version matching DGL)*
22
-
23
- ## 4. Install Deep Graph Library (DGL)
24
- DGL must match the CUDA version:
25
- ```powershell
26
- pip install dgl==2.2.1+cu118 -f https://data.dgl.ai/wheels/cu118/repo.html
27
- pip install dgllife
28
- ```
29
-
30
- ## 5. Install Core Dependencies
31
- DeepPurpose and PyTDC (Therapeutics Data Commons):
32
- ```powershell
33
- pip install DeepPurpose PyTDC
34
- pip install git+https://github.com/bp-kelley/descriptastorus
35
- pip install pandas numpy matplotlib prettytable tensorboard lifelines scikit-learn
36
- ```
37
-
38
- ## 6. Windows-Specific Fixes (Crucial)
39
-
40
- ### Fix A: PyTDC Dependency Issues (`tiledbsoma`, `cellxgene_census`)
41
- PyTDC tries to install `cellxgene_census`, which depends on `tiledbsoma`. On Windows, `tiledbsoma` often fails to build.
42
- **Solution:** If `pip install PyTDC` fails, manually install dependencies excluding the problematic ones, or create mock files if the code doesn't actually use them (PyTDC generally works without them for standard datasets like DAVIS/KIBA).
43
-
44
- ### Fix B: DGL GraphBolt DLL Error
45
- DGL 2.x includes "GraphBolt" which requires C++ DLLs that may be missing or incompatible on Windows.
46
- **Error:** `FileNotFoundError: ... graphbolt_pytorch_X.X.X.dll`
47
- **Solution:**
48
- 1. **Code Fix (Recommended):** Add this line to the **very top** of your Python script (before `import torch` or `import dgl`):
49
- ```python
50
- import os
51
- os.environ["DGL_DISABLE_GRAPHBOLT"] = "1"
52
- ```
53
- 2. **Library Fix (Manual):** If the error persists, edit `.venv\Lib\site-packages\dgl\graphbolt\__init__.py` and comment out all imports to prevent it from loading.
54
-
55
- ## 7. Running the Script
56
- ```powershell
57
- python deeppurpose002.py
58
- ```
59
- To force usage of a specific GPU encoding:
60
- ```powershell
61
- python deeppurpose002.py --drug_enc DGL_GCN --target_enc CNN --epochs 10
62
- ```
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ui/app/data/data-view.tsx CHANGED
@@ -1,6 +1,7 @@
1
  "use client"
2
 
3
- import { CloudUpload, Database, Download, Eye, FileText, Folder, HardDrive, Trash2,Upload } from "lucide-react"
 
4
 
5
  import { SectionHeader } from "@/components/page-header"
6
  import { Badge } from "@/components/ui/badge"
@@ -16,6 +17,8 @@ interface DataViewProps {
16
  }
17
 
18
  export function DataView({ datasets, stats }: DataViewProps) {
 
 
19
  const statCards = [
20
  { label: "Datasets", value: stats?.datasets?.toString() ?? "—", icon: Folder, color: "text-blue-500" },
21
  { label: "Molecules", value: stats?.molecules ?? "—", icon: FileText, color: "text-cyan-500" },
@@ -23,6 +26,16 @@ export function DataView({ datasets, stats }: DataViewProps) {
23
  { label: "Storage Used", value: stats?.storage ?? "—", icon: HardDrive, color: "text-amber-500" },
24
  ]
25
 
 
 
 
 
 
 
 
 
 
 
26
  return (
27
  <div className="space-y-8">
28
  <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
@@ -70,7 +83,7 @@ export function DataView({ datasets, stats }: DataViewProps) {
70
  <TableCell className="font-medium">{ds.name}</TableCell>
71
  <TableCell>
72
  <div className="flex items-center gap-2">
73
- <Badge variant={ds.type === 'Molecules' ? 'default' : 'secondary'}>{ds.type}</Badge>
74
  </div>
75
  </TableCell>
76
  <TableCell>{ds.count}</TableCell>
@@ -78,9 +91,38 @@ export function DataView({ datasets, stats }: DataViewProps) {
78
  <TableCell>{ds.updated}</TableCell>
79
  <TableCell className="text-right">
80
  <div className="flex justify-end gap-2">
81
- <Button size="icon" variant="ghost" className="h-8 w-8"><Eye className="h-4 w-4" /></Button>
82
- <Button size="icon" variant="ghost" className="h-8 w-8"><Download className="h-4 w-4" /></Button>
83
- <Button size="icon" variant="ghost" className="h-8 w-8 text-destructive hover:text-destructive"><Trash2 className="h-4 w-4" /></Button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  </div>
85
  </TableCell>
86
  </TableRow>
 
1
  "use client"
2
 
3
+ import { CloudUpload, Database, Download, Eye, FileText, Folder, HardDrive, Trash2, Upload, ExternalLink } from "lucide-react"
4
+ import { useRouter } from "next/navigation"
5
 
6
  import { SectionHeader } from "@/components/page-header"
7
  import { Badge } from "@/components/ui/badge"
 
17
  }
18
 
19
  export function DataView({ datasets, stats }: DataViewProps) {
20
+ const router = useRouter()
21
+
22
  const statCards = [
23
  { label: "Datasets", value: stats?.datasets?.toString() ?? "—", icon: Folder, color: "text-blue-500" },
24
  { label: "Molecules", value: stats?.molecules ?? "—", icon: FileText, color: "text-cyan-500" },
 
26
  { label: "Storage Used", value: stats?.storage ?? "—", icon: HardDrive, color: "text-amber-500" },
27
  ]
28
 
29
+ const handleView = (dataset: Dataset) => {
30
+ // Navigate to explorer with this dataset's data
31
+ router.push(`/explorer?dataset=${encodeURIComponent(dataset.name)}`)
32
+ }
33
+
34
+ const handleDownload = (dataset: Dataset) => {
35
+ // For KIBA/DAVIS, these are from TDC - provide info
36
+ alert(`Dataset: ${dataset.name}\n\nThis dataset is loaded from Therapeutics Data Commons (TDC).\n\nTo download raw data, visit: https://tdcommons.ai/\n\nOr access via Python:\nfrom tdc.multi_pred import DTI\ndata = DTI(name='${dataset.name.includes('KIBA') ? 'KIBA' : 'DAVIS'}')`)
37
+ }
38
+
39
  return (
40
  <div className="space-y-8">
41
  <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
 
83
  <TableCell className="font-medium">{ds.name}</TableCell>
84
  <TableCell>
85
  <div className="flex items-center gap-2">
86
+ <Badge variant={ds.type === 'Drug-Target' ? 'default' : 'secondary'}>{ds.type}</Badge>
87
  </div>
88
  </TableCell>
89
  <TableCell>{ds.count}</TableCell>
 
91
  <TableCell>{ds.updated}</TableCell>
92
  <TableCell className="text-right">
93
  <div className="flex justify-end gap-2">
94
+ <Button
95
+ size="icon"
96
+ variant="ghost"
97
+ className="h-8 w-8"
98
+ onClick={() => handleView(ds)}
99
+ title="View in Explorer"
100
+ >
101
+ <Eye className="h-4 w-4" />
102
+ </Button>
103
+ <Button
104
+ size="icon"
105
+ variant="ghost"
106
+ className="h-8 w-8"
107
+ onClick={() => handleDownload(ds)}
108
+ title="Download Info"
109
+ >
110
+ <Download className="h-4 w-4" />
111
+ </Button>
112
+ <a
113
+ href="https://tdcommons.ai/"
114
+ target="_blank"
115
+ rel="noopener noreferrer"
116
+ >
117
+ <Button
118
+ size="icon"
119
+ variant="ghost"
120
+ className="h-8 w-8"
121
+ title="View on TDC"
122
+ >
123
+ <ExternalLink className="h-4 w-4" />
124
+ </Button>
125
+ </a>
126
  </div>
127
  </TableCell>
128
  </TableRow>
ui/app/discovery/page.tsx CHANGED
@@ -1,6 +1,6 @@
1
  "use client"
2
 
3
- import { ArrowRight,CheckCircle2, Circle, Loader2, Microscope, Search } from "lucide-react"
4
  import * as React from "react"
5
 
6
  import { PageHeader, SectionHeader } from "@/components/page-header"
@@ -11,22 +11,89 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
11
  import { Tabs, TabsContent,TabsList, TabsTrigger } from "@/components/ui/tabs"
12
  import { Textarea } from "@/components/ui/textarea"
13
 
 
 
 
 
 
 
 
 
 
 
 
14
  export default function DiscoveryPage() {
15
  const [query, setQuery] = React.useState("")
 
16
  const [isSearching, setIsSearching] = React.useState(false)
17
  const [step, setStep] = React.useState(0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
 
19
- const handleSearch = () => {
 
 
20
  setIsSearching(true)
21
  setStep(1)
 
 
22
 
23
- // Simulate pipeline
24
- setTimeout(() => setStep(2), 1500)
25
- setTimeout(() => setStep(3), 3000)
26
- setTimeout(() => {
27
- setStep(4)
28
- setIsSearching(false)
29
- }, 4500)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  }
31
 
32
  const steps = [
@@ -41,7 +108,7 @@ export default function DiscoveryPage() {
41
  <div className="space-y-8 animate-in fade-in duration-500">
42
  <PageHeader
43
  title="Drug Discovery"
44
- subtitle="Search for drug candidates with AI-powered analysis"
45
  icon={<Microscope className="h-8 w-8" />}
46
  />
47
 
@@ -51,7 +118,13 @@ export default function DiscoveryPage() {
51
  <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
52
  <div className="md:col-span-3">
53
  <Textarea
54
- placeholder="Enter a natural language query, SMILES string, or FASTA sequence..."
 
 
 
 
 
 
55
  className="min-h-[120px] font-mono"
56
  value={query}
57
  onChange={(e) => setQuery(e.target.value)}
@@ -60,28 +133,26 @@ export default function DiscoveryPage() {
60
  <div className="space-y-4">
61
  <div className="space-y-2">
62
  <Label>Search Type</Label>
63
- <Select defaultValue="Similarity">
64
  <SelectTrigger>
65
  <SelectValue placeholder="Select type" />
66
  </SelectTrigger>
67
  <SelectContent>
68
- <SelectItem value="Similarity">Similarity</SelectItem>
69
- <SelectItem value="Binding Affinity">Binding Affinity</SelectItem>
70
- <SelectItem value="Properties">Properties</SelectItem>
71
  </SelectContent>
72
  </Select>
73
  </div>
74
  <div className="space-y-2">
75
  <Label>Database</Label>
76
- <Select defaultValue="All">
77
  <SelectTrigger>
78
  <SelectValue placeholder="Select database" />
79
  </SelectTrigger>
80
  <SelectContent>
81
- <SelectItem value="All">All</SelectItem>
82
- <SelectItem value="DrugBank">DrugBank</SelectItem>
83
- <SelectItem value="ChEMBL">ChEMBL</SelectItem>
84
- <SelectItem value="ZINC">ZINC</SelectItem>
85
  </SelectContent>
86
  </Select>
87
  </div>
@@ -91,13 +162,28 @@ export default function DiscoveryPage() {
91
  disabled={isSearching || !query}
92
  >
93
  {isSearching ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Search className="mr-2 h-4 w-4" />}
94
- {isSearching ? "Running..." : "Search"}
95
  </Button>
96
  </div>
97
  </div>
98
  </CardContent>
99
  </Card>
100
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  <div className="space-y-4">
102
  <SectionHeader title="Pipeline Status" icon={<ArrowRight className="h-5 w-5 text-muted-foreground" />} />
103
 
@@ -121,63 +207,61 @@ export default function DiscoveryPage() {
121
  </div>
122
  </div>
123
 
124
- {step === 4 && (
125
  <div className="space-y-4 animate-in slide-in-from-bottom-4 duration-500">
126
- <SectionHeader title="Results" icon={<CheckCircle2 className="h-5 w-5 text-green-500" />} />
127
 
128
  <Tabs defaultValue="candidates">
129
  <TabsList>
130
  <TabsTrigger value="candidates">Top Candidates</TabsTrigger>
131
- <TabsTrigger value="analysis">Property Analysis</TabsTrigger>
132
- <TabsTrigger value="evidence">Evidence</TabsTrigger>
133
  </TabsList>
134
  <TabsContent value="candidates" className="space-y-4">
135
- {[
136
- { name: "Candidate A", score: 0.95, mw: "342.4", logp: "2.1" },
137
- { name: "Candidate B", score: 0.89, mw: "298.3", logp: "1.8" },
138
- { name: "Candidate C", score: 0.82, mw: "415.5", logp: "3.2" },
139
- { name: "Candidate D", score: 0.76, mw: "267.3", logp: "1.5" },
140
- { name: "Candidate E", score: 0.71, mw: "389.4", logp: "2.8" },
141
- ].map((candidate, i) => (
142
- <Card key={i}>
143
  <CardContent className="p-4 flex items-center justify-between">
144
- <div>
145
- <div className="font-bold text-lg">{candidate.name}</div>
146
- <div className="flex gap-4 text-sm text-muted-foreground">
147
- <span>MW: {candidate.mw}</span>
148
- <span>LogP: {candidate.logp}</span>
 
 
149
  </div>
150
  </div>
151
  <div className="text-right">
152
- <div className="text-sm text-muted-foreground">Score</div>
153
  <div className={`text-xl font-bold ${
154
- candidate.score >= 0.9 ? 'text-green-600' :
155
- candidate.score >= 0.8 ? 'text-green-500' : 'text-amber-500'
156
  }`}>
157
- {candidate.score}
158
  </div>
159
  </div>
160
  </CardContent>
161
  </Card>
162
  ))}
163
  </TabsContent>
164
- <TabsContent value="analysis">
165
  <Card>
166
- <CardContent className="p-12 text-center text-muted-foreground">
167
- Chart visualization would go here (using Recharts).
168
- </CardContent>
169
- </Card>
170
- </TabsContent>
171
- <TabsContent value="evidence">
172
- <Card>
173
- <CardContent className="p-12 text-center text-muted-foreground">
174
- Evidence graph visualization would go here.
175
  </CardContent>
176
  </Card>
177
  </TabsContent>
178
  </Tabs>
179
  </div>
180
  )}
 
 
 
 
 
 
 
 
181
  </div>
182
  )
183
  }
 
1
  "use client"
2
 
3
+ import { ArrowRight,CheckCircle2, Circle, Loader2, Microscope, Search, AlertCircle } from "lucide-react"
4
  import * as React from "react"
5
 
6
  import { PageHeader, SectionHeader } from "@/components/page-header"
 
11
  import { Tabs, TabsContent,TabsList, TabsTrigger } from "@/components/ui/tabs"
12
  import { Textarea } from "@/components/ui/textarea"
13
 
14
+ const API_BASE = "http://localhost:8001";
15
+
16
+ interface SearchResult {
17
+ id: string;
18
+ score: number;
19
+ smiles: string;
20
+ target_seq: string;
21
+ label: number;
22
+ affinity_class: string;
23
+ }
24
+
25
  export default function DiscoveryPage() {
26
  const [query, setQuery] = React.useState("")
27
+ const [searchType, setSearchType] = React.useState("Similarity")
28
  const [isSearching, setIsSearching] = React.useState(false)
29
  const [step, setStep] = React.useState(0)
30
+ const [results, setResults] = React.useState<SearchResult[]>([])
31
+ const [error, setError] = React.useState<string | null>(null)
32
+
33
+ // Map UI search type to API type
34
+ const getApiType = (uiType: string, query: string): string => {
35
+ // If it looks like SMILES (contains chemistry chars), use drug encoding
36
+ const looksLikeSmiles = /^[A-Za-z0-9@+\-\[\]\(\)\\\/=#$.]+$/.test(query.trim())
37
+ // If it looks like protein sequence (all caps amino acids)
38
+ const looksLikeProtein = /^[ACDEFGHIKLMNPQRSTVWY]+$/i.test(query.trim()) && query.length > 20
39
+
40
+ if (uiType === "Similarity" || uiType === "Binding Affinity") {
41
+ if (looksLikeSmiles && !looksLikeProtein) return "drug"
42
+ if (looksLikeProtein) return "target"
43
+ return "text" // Fallback to text search
44
+ }
45
+ return "text"
46
+ }
47
 
48
+ const handleSearch = async () => {
49
+ if (!query.trim()) return;
50
+
51
  setIsSearching(true)
52
  setStep(1)
53
+ setError(null)
54
+ setResults([])
55
 
56
+ try {
57
+ // Step 1: Input received
58
+ setStep(1)
59
+
60
+ // Step 2: Determine type and encode
61
+ await new Promise(r => setTimeout(r, 300))
62
+ setStep(2)
63
+
64
+ const apiType = getApiType(searchType, query)
65
+
66
+ // Step 3: Actually search Qdrant via our API
67
+ const response = await fetch(`${API_BASE}/api/search`, {
68
+ method: 'POST',
69
+ headers: { 'Content-Type': 'application/json' },
70
+ body: JSON.stringify({
71
+ query: query.trim(),
72
+ type: apiType,
73
+ limit: 10
74
+ })
75
+ });
76
+
77
+ setStep(3)
78
+
79
+ if (!response.ok) {
80
+ const errData = await response.json().catch(() => ({}));
81
+ throw new Error(errData.detail || `API error: ${response.status}`);
82
+ }
83
+
84
+ const data = await response.json();
85
+
86
+ // Step 4: Process results
87
+ await new Promise(r => setTimeout(r, 200))
88
+ setStep(4)
89
+ setResults(data.results || [])
90
+
91
+ } catch (err) {
92
+ setError(err instanceof Error ? err.message : 'Search failed');
93
+ setStep(0)
94
+ } finally {
95
+ setIsSearching(false)
96
+ }
97
  }
98
 
99
  const steps = [
 
108
  <div className="space-y-8 animate-in fade-in duration-500">
109
  <PageHeader
110
  title="Drug Discovery"
111
+ subtitle="Search for drug candidates using DeepPurpose + Qdrant"
112
  icon={<Microscope className="h-8 w-8" />}
113
  />
114
 
 
118
  <div className="grid grid-cols-1 md:grid-cols-4 gap-6">
119
  <div className="md:col-span-3">
120
  <Textarea
121
+ placeholder={
122
+ searchType === "Similarity"
123
+ ? "Enter SMILES string (e.g., CC(=O)Nc1ccc(O)cc1 for Acetaminophen)"
124
+ : searchType === "Binding Affinity"
125
+ ? "Enter protein sequence (amino acids, e.g., MKKFFD...)"
126
+ : "Enter drug name or keyword to search"
127
+ }
128
  className="min-h-[120px] font-mono"
129
  value={query}
130
  onChange={(e) => setQuery(e.target.value)}
 
133
  <div className="space-y-4">
134
  <div className="space-y-2">
135
  <Label>Search Type</Label>
136
+ <Select value={searchType} onValueChange={setSearchType}>
137
  <SelectTrigger>
138
  <SelectValue placeholder="Select type" />
139
  </SelectTrigger>
140
  <SelectContent>
141
+ <SelectItem value="Similarity">Similarity (Drug SMILES)</SelectItem>
142
+ <SelectItem value="Binding Affinity">Binding Affinity (Protein)</SelectItem>
143
+ <SelectItem value="Properties">Properties (Text Search)</SelectItem>
144
  </SelectContent>
145
  </Select>
146
  </div>
147
  <div className="space-y-2">
148
  <Label>Database</Label>
149
+ <Select defaultValue="KIBA">
150
  <SelectTrigger>
151
  <SelectValue placeholder="Select database" />
152
  </SelectTrigger>
153
  <SelectContent>
154
+ <SelectItem value="KIBA">KIBA (23.5K pairs)</SelectItem>
155
+ <SelectItem value="DAVIS">DAVIS Kinase</SelectItem>
 
 
156
  </SelectContent>
157
  </Select>
158
  </div>
 
162
  disabled={isSearching || !query}
163
  >
164
  {isSearching ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Search className="mr-2 h-4 w-4" />}
165
+ {isSearching ? "Searching Qdrant..." : "Search"}
166
  </Button>
167
  </div>
168
  </div>
169
  </CardContent>
170
  </Card>
171
 
172
+ {error && (
173
+ <Card className="border-destructive">
174
+ <CardContent className="p-4 flex items-center gap-3 text-destructive">
175
+ <AlertCircle className="h-5 w-5" />
176
+ <div>
177
+ <div className="font-medium">Search Failed</div>
178
+ <div className="text-sm">{error}</div>
179
+ <div className="text-xs mt-1 text-muted-foreground">
180
+ Make sure the API server is running: python -m uvicorn server.api:app --port 8001
181
+ </div>
182
+ </div>
183
+ </CardContent>
184
+ </Card>
185
+ )}
186
+
187
  <div className="space-y-4">
188
  <SectionHeader title="Pipeline Status" icon={<ArrowRight className="h-5 w-5 text-muted-foreground" />} />
189
 
 
207
  </div>
208
  </div>
209
 
210
+ {step === 4 && results.length > 0 && (
211
  <div className="space-y-4 animate-in slide-in-from-bottom-4 duration-500">
212
+ <SectionHeader title={`Results (${results.length} from Qdrant)`} icon={<CheckCircle2 className="h-5 w-5 text-green-500" />} />
213
 
214
  <Tabs defaultValue="candidates">
215
  <TabsList>
216
  <TabsTrigger value="candidates">Top Candidates</TabsTrigger>
217
+ <TabsTrigger value="details">Raw Data</TabsTrigger>
 
218
  </TabsList>
219
  <TabsContent value="candidates" className="space-y-4">
220
+ {results.map((result, i) => (
221
+ <Card key={result.id}>
 
 
 
 
 
 
222
  <CardContent className="p-4 flex items-center justify-between">
223
+ <div className="flex-1">
224
+ <div className="font-mono text-sm font-medium">
225
+ {result.smiles?.slice(0, 50)}{result.smiles?.length > 50 ? '...' : ''}
226
+ </div>
227
+ <div className="flex gap-4 text-sm text-muted-foreground mt-1">
228
+ <span>Affinity: {result.affinity_class}</span>
229
+ <span>Label: {result.label?.toFixed(2)}</span>
230
  </div>
231
  </div>
232
  <div className="text-right">
233
+ <div className="text-sm text-muted-foreground">Similarity</div>
234
  <div className={`text-xl font-bold ${
235
+ result.score >= 0.9 ? 'text-green-600' :
236
+ result.score >= 0.7 ? 'text-green-500' : 'text-amber-500'
237
  }`}>
238
+ {result.score.toFixed(3)}
239
  </div>
240
  </div>
241
  </CardContent>
242
  </Card>
243
  ))}
244
  </TabsContent>
245
+ <TabsContent value="details">
246
  <Card>
247
+ <CardContent className="p-4">
248
+ <pre className="text-xs overflow-auto max-h-[400px] bg-muted p-4 rounded">
249
+ {JSON.stringify(results, null, 2)}
250
+ </pre>
 
 
 
 
 
251
  </CardContent>
252
  </Card>
253
  </TabsContent>
254
  </Tabs>
255
  </div>
256
  )}
257
+
258
+ {step === 4 && results.length === 0 && !error && (
259
+ <Card>
260
+ <CardContent className="p-8 text-center text-muted-foreground">
261
+ No similar compounds found in Qdrant.
262
+ </CardContent>
263
+ </Card>
264
+ )}
265
  </div>
266
  )
267
  }
ui/app/explorer/components.tsx CHANGED
@@ -49,9 +49,9 @@ export function ExplorerControls() {
49
  <SelectValue placeholder="Select dataset" />
50
  </SelectTrigger>
51
  <SelectContent>
52
- <SelectItem value="DrugBank">DrugBank</SelectItem>
53
- <SelectItem value="ChEMBL">ChEMBL</SelectItem>
54
- <SelectItem value="ZINC">ZINC</SelectItem>
55
  </SelectContent>
56
  </Select>
57
  </div>
 
49
  <SelectValue placeholder="Select dataset" />
50
  </SelectTrigger>
51
  <SelectContent>
52
+ <SelectItem value="KIBA">KIBA (23.5K)</SelectItem>
53
+ <SelectItem value="DAVIS">DAVIS Kinase</SelectItem>
54
+ <SelectItem value="BindingDB">BindingDB Kd</SelectItem>
55
  </SelectContent>
56
  </Select>
57
  </div>
ui/app/workflow/components/custom-nodes.tsx ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { memo } from 'react';
4
+ import { Handle, Position, NodeProps } from 'reactflow';
5
+ import { Database, FlaskConical, FileSearch, Shield, Brain, Dna, FileText } from 'lucide-react';
6
+
7
+ // Base node wrapper with consistent styling
8
+ const NodeWrapper = ({
9
+ children,
10
+ color,
11
+ label,
12
+ icon: Icon,
13
+ status = 'idle'
14
+ }: {
15
+ children?: React.ReactNode;
16
+ color: string;
17
+ label: string;
18
+ icon: React.ElementType;
19
+ status?: 'idle' | 'running' | 'complete' | 'error';
20
+ }) => {
21
+ const statusColors = {
22
+ idle: 'border-gray-600',
23
+ running: 'border-yellow-500 animate-pulse',
24
+ complete: 'border-green-500',
25
+ error: 'border-red-500'
26
+ };
27
+
28
+ return (
29
+ <div className={`
30
+ min-w-[180px] rounded-xl border-2 ${statusColors[status]}
31
+ bg-gray-900/95 backdrop-blur shadow-2xl overflow-hidden
32
+ `}>
33
+ <div className={`px-3 py-2 ${color} flex items-center gap-2`}>
34
+ <Icon className="w-4 h-4 text-white" />
35
+ <span className="text-sm font-semibold text-white">{label}</span>
36
+ </div>
37
+ <div className="p-3 text-xs text-gray-300">
38
+ {children}
39
+ </div>
40
+ </div>
41
+ );
42
+ };
43
+
44
+ // Data Input Node
45
+ export const DataInputNode = memo(({ data }: NodeProps) => (
46
+ <>
47
+ <NodeWrapper color="bg-blue-600" label="Data Input" icon={FileText} status={data.status}>
48
+ <div className="space-y-1">
49
+ <div className="text-gray-400">Type: <span className="text-white">{data.inputType || 'SMILES'}</span></div>
50
+ <div className="text-gray-400 truncate max-w-[150px]">
51
+ Input: <span className="text-white font-mono">{data.input || 'CC(=O)O...'}</span>
52
+ </div>
53
+ </div>
54
+ </NodeWrapper>
55
+ <Handle type="source" position={Position.Right} className="w-3 h-3 !bg-blue-500" />
56
+ </>
57
+ ));
58
+ DataInputNode.displayName = 'DataInputNode';
59
+
60
+ // DeepPurpose Generator Node
61
+ export const GeneratorNode = memo(({ data }: NodeProps) => (
62
+ <>
63
+ <Handle type="target" position={Position.Left} className="w-3 h-3 !bg-purple-500" />
64
+ <NodeWrapper color="bg-purple-600" label="DeepPurpose DTI" icon={FlaskConical} status={data.status}>
65
+ <div className="space-y-1">
66
+ <div className="text-gray-400">Model: <span className="text-white">Morgan + CNN</span></div>
67
+ <div className="text-gray-400">Encoding: <span className="text-green-400">256D</span></div>
68
+ {data.prediction && (
69
+ <div className="text-gray-400">Affinity: <span className="text-emerald-400">{data.prediction}</span></div>
70
+ )}
71
+ </div>
72
+ </NodeWrapper>
73
+ <Handle type="source" position={Position.Right} className="w-3 h-3 !bg-purple-500" />
74
+ </>
75
+ ));
76
+ GeneratorNode.displayName = 'GeneratorNode';
77
+
78
+ // Qdrant Storage Node
79
+ export const QdrantNode = memo(({ data }: NodeProps) => (
80
+ <>
81
+ <Handle type="target" position={Position.Left} className="w-3 h-3 !bg-orange-500" />
82
+ <NodeWrapper color="bg-orange-600" label="Qdrant Vector DB" icon={Database} status={data.status}>
83
+ <div className="space-y-1">
84
+ <div className="text-gray-400">Collection: <span className="text-white">bio_discovery</span></div>
85
+ <div className="text-gray-400">Vectors: <span className="text-cyan-400">{data.vectorCount || '23,531'}</span></div>
86
+ <div className="text-gray-400">Index: <span className="text-white">HNSW</span></div>
87
+ </div>
88
+ </NodeWrapper>
89
+ <Handle type="source" position={Position.Right} className="w-3 h-3 !bg-orange-500" />
90
+ </>
91
+ ));
92
+ QdrantNode.displayName = 'QdrantNode';
93
+
94
+ // Similarity Search Node
95
+ export const SearchNode = memo(({ data }: NodeProps) => (
96
+ <>
97
+ <Handle type="target" position={Position.Left} className="w-3 h-3 !bg-cyan-500" />
98
+ <NodeWrapper color="bg-cyan-600" label="Similarity Search" icon={FileSearch} status={data.status}>
99
+ <div className="space-y-1">
100
+ <div className="text-gray-400">Top-K: <span className="text-white">{data.topK || 10}</span></div>
101
+ <div className="text-gray-400">Metric: <span className="text-white">Cosine</span></div>
102
+ {data.results && (
103
+ <div className="text-gray-400">Found: <span className="text-green-400">{data.results} matches</span></div>
104
+ )}
105
+ </div>
106
+ </NodeWrapper>
107
+ <Handle type="source" position={Position.Right} className="w-3 h-3 !bg-cyan-500" />
108
+ </>
109
+ ));
110
+ SearchNode.displayName = 'SearchNode';
111
+
112
+ // Validator Node
113
+ export const ValidatorNode = memo(({ data }: NodeProps) => (
114
+ <>
115
+ <Handle type="target" position={Position.Left} className="w-3 h-3 !bg-green-500" />
116
+ <NodeWrapper color="bg-green-600" label="Validator Agent" icon={Shield} status={data.status}>
117
+ <div className="space-y-1">
118
+ <div className="text-gray-400">Toxicity: <span className={data.toxicity === 'Low' ? 'text-green-400' : 'text-red-400'}>{data.toxicity || 'Checking...'}</span></div>
119
+ <div className="text-gray-400">Novelty: <span className="text-white">{data.novelty || 'Pending'}</span></div>
120
+ <div className="text-gray-400">Score: <span className="text-yellow-400">{data.score || '—'}</span></div>
121
+ </div>
122
+ </NodeWrapper>
123
+ <Handle type="source" position={Position.Right} className="w-3 h-3 !bg-green-500" />
124
+ </>
125
+ ));
126
+ ValidatorNode.displayName = 'ValidatorNode';
127
+
128
+ // OpenBioMed Multimodal Node
129
+ export const MultimodalNode = memo(({ data }: NodeProps) => (
130
+ <>
131
+ <Handle type="target" position={Position.Left} className="w-3 h-3 !bg-pink-500" />
132
+ <NodeWrapper color="bg-pink-600" label="OpenBioMed" icon={Brain} status={data.status}>
133
+ <div className="space-y-1">
134
+ <div className="text-gray-400">Mode: <span className="text-white">{data.mode || 'Cross-Modal'}</span></div>
135
+ <div className="text-gray-400">Modalities: <span className="text-white">Protein + Text</span></div>
136
+ <div className="text-gray-400">Embeddings: <span className="text-pink-400">768D</span></div>
137
+ </div>
138
+ </NodeWrapper>
139
+ <Handle type="source" position={Position.Right} className="w-3 h-3 !bg-pink-500" />
140
+ </>
141
+ ));
142
+ MultimodalNode.displayName = 'MultimodalNode';
143
+
144
+ // Output/Results Node
145
+ export const OutputNode = memo(({ data }: NodeProps) => (
146
+ <>
147
+ <Handle type="target" position={Position.Left} className="w-3 h-3 !bg-emerald-500" />
148
+ <NodeWrapper color="bg-emerald-600" label="Results" icon={Dna} status={data.status}>
149
+ <div className="space-y-1">
150
+ <div className="text-gray-400">Candidates: <span className="text-white">{data.candidates || 0}</span></div>
151
+ <div className="text-gray-400">Top Match: <span className="text-emerald-400">{data.topMatch || '—'}</span></div>
152
+ <div className="text-gray-400">Confidence: <span className="text-yellow-400">{data.confidence || '—'}</span></div>
153
+ </div>
154
+ </NodeWrapper>
155
+ </>
156
+ ));
157
+ OutputNode.displayName = 'OutputNode';
158
+
159
+ export const nodeTypes = {
160
+ dataInput: DataInputNode,
161
+ generator: GeneratorNode,
162
+ qdrant: QdrantNode,
163
+ search: SearchNode,
164
+ validator: ValidatorNode,
165
+ multimodal: MultimodalNode,
166
+ output: OutputNode,
167
+ };
ui/app/workflow/layout.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { usePathname } from 'next/navigation';
4
+
5
+ // Workflow has its own layout that bypasses the main sidebar
6
+ // This gives Langflow the full viewport for proper UX
7
+ export default function WorkflowLayout({
8
+ children,
9
+ }: {
10
+ children: React.ReactNode;
11
+ }) {
12
+ return (
13
+ <div className="fixed inset-0 z-50 bg-background">
14
+ {children}
15
+ </div>
16
+ );
17
+ }
ui/app/workflow/page.tsx ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+ import { ArrowLeft, Dna, ExternalLink, Loader2, RefreshCw } from 'lucide-react';
5
+ import Link from 'next/link';
6
+
7
+ export default function WorkflowPage() {
8
+ const [isLoading, setIsLoading] = useState(true);
9
+ const [langflowStatus, setLangflowStatus] = useState<'checking' | 'online' | 'offline'>('checking');
10
+
11
+ const LANGFLOW_URL = 'http://localhost:7860';
12
+
13
+ useEffect(() => {
14
+ const checkLangflow = async () => {
15
+ try {
16
+ const img = new Image();
17
+ img.onload = () => setLangflowStatus('online');
18
+ img.onerror = () => {
19
+ fetch(`${LANGFLOW_URL}/api/v1/version`, { mode: 'no-cors' })
20
+ .then(() => setLangflowStatus('online'))
21
+ .catch(() => setLangflowStatus('offline'));
22
+ };
23
+ img.src = `${LANGFLOW_URL}/favicon.ico?t=${Date.now()}`;
24
+
25
+ setTimeout(() => {
26
+ if (langflowStatus === 'checking') {
27
+ setLangflowStatus('online');
28
+ }
29
+ }, 2000);
30
+ } catch {
31
+ setLangflowStatus('offline');
32
+ }
33
+ };
34
+
35
+ checkLangflow();
36
+
37
+ const interval = setInterval(() => {
38
+ if (langflowStatus === 'offline') {
39
+ checkLangflow();
40
+ }
41
+ }, 5000);
42
+
43
+ return () => clearInterval(interval);
44
+ }, [langflowStatus]);
45
+
46
+ const handleRetry = () => {
47
+ setLangflowStatus('checking');
48
+ setIsLoading(true);
49
+ };
50
+
51
+ // Clean white loading state
52
+ if (langflowStatus === 'checking') {
53
+ return (
54
+ <div className="flex flex-col items-center justify-center h-full w-full bg-background">
55
+ <Loader2 className="w-10 h-10 animate-spin text-primary mb-4" />
56
+ <p className="text-muted-foreground">Connecting to Langflow...</p>
57
+ </div>
58
+ );
59
+ }
60
+
61
+ // Clean white offline state
62
+ if (langflowStatus === 'offline') {
63
+ return (
64
+ <div className="flex flex-col h-full w-full bg-background">
65
+ {/* Clean header */}
66
+ <div className="flex items-center gap-4 px-4 py-3 border-b">
67
+ <Link
68
+ href="/"
69
+ className="flex items-center gap-2 text-muted-foreground"
70
+ >
71
+ <ArrowLeft className="w-4 h-4" />
72
+ Back to BioFlow
73
+ </Link>
74
+ <div className="flex items-center gap-2">
75
+ <Dna className="w-5 h-5 text-primary" />
76
+ <span className="font-semibold">BioFlow</span>
77
+ <span className="text-muted-foreground">/</span>
78
+ <span className="text-muted-foreground">Workflow Builder</span>
79
+ </div>
80
+ </div>
81
+
82
+ {/* Offline message */}
83
+ <div className="flex-1 flex items-center justify-center p-8">
84
+ <div className="max-w-lg text-center">
85
+ <p className="text-muted-foreground mb-4">Langflow not running</p>
86
+ <code className="block bg-muted text-foreground p-4 rounded-lg font-mono text-sm mb-4">
87
+ langflow run --host 0.0.0.0 --port 7860
88
+ </code>
89
+ <p className="text-xs text-muted-foreground mb-4">
90
+ Note: Run in a separate terminal/venv to avoid dependency conflicts
91
+ </p>
92
+ <button
93
+ onClick={handleRetry}
94
+ className="px-4 py-2 bg-primary text-primary-foreground rounded-lg"
95
+ >
96
+ Retry
97
+ </button>
98
+ </div>
99
+ </div>
100
+ </div>
101
+ );
102
+ }
103
+
104
+ // Clean white header with Langflow iframe
105
+ return (
106
+ <div className="h-full w-full flex flex-col bg-background">
107
+ {/* Clean minimal header */}
108
+ <div className="flex items-center justify-between px-4 py-2 border-b bg-background">
109
+ <div className="flex items-center gap-4">
110
+ <Link
111
+ href="/"
112
+ className="flex items-center gap-2 text-muted-foreground"
113
+ >
114
+ <ArrowLeft className="w-4 h-4" />
115
+ Back to BioFlow
116
+ </Link>
117
+
118
+ <div className="h-6 w-px bg-border" />
119
+
120
+ <div className="flex items-center gap-2">
121
+ <Dna className="w-5 h-5 text-primary" />
122
+ <span className="font-semibold">BioFlow</span>
123
+ <span className="text-muted-foreground">/</span>
124
+ <span className="text-muted-foreground">Workflow Builder</span>
125
+ </div>
126
+ </div>
127
+
128
+ <a
129
+ href={LANGFLOW_URL}
130
+ target="_blank"
131
+ rel="noopener noreferrer"
132
+ className="flex items-center gap-2 px-3 py-1.5 border hover:bg-muted rounded-lg text-sm transition-colors"
133
+ >
134
+ <ExternalLink className="w-4 h-4" />
135
+ Open in New Tab
136
+ </a>
137
+ </div>
138
+
139
+ {/* Full viewport Langflow iframe */}
140
+ <div className="flex-1 relative">
141
+ {isLoading && (
142
+ <div className="absolute inset-0 flex items-center justify-center bg-background z-10">
143
+ <div className="flex flex-col items-center">
144
+ <Loader2 className="w-10 h-10 animate-spin text-primary mb-3" />
145
+ <p className="text-muted-foreground">Loading Langflow...</p>
146
+ </div>
147
+ </div>
148
+ )}
149
+ <iframe
150
+ src={LANGFLOW_URL}
151
+ className="w-full h-full border-0"
152
+ onLoad={() => setIsLoading(false)}
153
+ title="Langflow Workflow Builder"
154
+ allow="clipboard-write; clipboard-read"
155
+ />
156
+ </div>
157
+ </div>
158
+ );
159
+ }
ui/components/sidebar.tsx CHANGED
@@ -16,6 +16,7 @@ import {
16
  Sparkles,
17
  Terminal,
18
  User,
 
19
  } from 'lucide-react';
20
  import Link from 'next/link';
21
  import { usePathname } from 'next/navigation';
@@ -60,6 +61,21 @@ const navMain = [
60
  icon: Home,
61
  isActive: true,
62
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  {
64
  title: 'Discovery',
65
  url: '/discovery',
 
16
  Sparkles,
17
  Terminal,
18
  User,
19
+ Workflow,
20
  } from 'lucide-react';
21
  import Link from 'next/link';
22
  import { usePathname } from 'next/navigation';
 
61
  icon: Home,
62
  isActive: true,
63
  },
64
+ {
65
+ title: 'Workflow',
66
+ url: '/workflow',
67
+ icon: Workflow,
68
+ items: [
69
+ {
70
+ title: 'Pipeline Builder',
71
+ url: '/workflow',
72
+ },
73
+ {
74
+ title: 'Templates',
75
+ url: '/workflow#templates',
76
+ },
77
+ ],
78
+ },
79
  {
80
  title: 'Discovery',
81
  url: '/discovery',
ui/lib/data-service.ts CHANGED
@@ -1,19 +1,73 @@
1
  import { DataResponse } from "@/types/data";
2
 
 
 
3
  export async function getData(): Promise<DataResponse> {
4
- // Mock data simulation - replacing actual database/API call
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  const datasets = [
6
- { name: "DrugBank Compounds", type: "Molecules", count: "12,450", size: "45.2 MB", updated: "2024-01-15" },
7
- { name: "ChEMBL Kinase Inhibitors", type: "Molecules", count: "8,234", size: "32.1 MB", updated: "2024-01-10" },
8
- { name: "Custom Protein Targets", type: "Proteins", count: "1,245", size: "78.5 MB", updated: "2024-01-08" },
 
 
 
 
 
 
 
 
 
 
 
9
  ];
10
 
11
  const stats = {
12
- datasets: 5,
13
- molecules: "24.5K",
14
- proteins: "1.2K",
15
- storage: "156 MB",
16
  };
17
 
18
- return Promise.resolve({ datasets, stats });
19
  }
 
1
  import { DataResponse } from "@/types/data";
2
 
3
+ const API_BASE = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8001";
4
+
5
  export async function getData(): Promise<DataResponse> {
6
+ try {
7
+ // Fetch real stats from our Qdrant-backed API
8
+ const response = await fetch(`${API_BASE}/api/stats`, {
9
+ next: { revalidate: 60 },
10
+ cache: 'no-store',
11
+ });
12
+
13
+ if (response.ok) {
14
+ const apiStats = await response.json();
15
+
16
+ // Only show the 2 REAL datasets we have in /data folder
17
+ const datasets = [
18
+ {
19
+ name: "KIBA Dataset",
20
+ type: "Drug-Target",
21
+ count: apiStats.total_vectors?.toLocaleString() || "23,531",
22
+ size: "94.1 MB",
23
+ updated: new Date().toISOString().split('T')[0],
24
+ },
25
+ {
26
+ name: "DAVIS Kinase",
27
+ type: "Drug-Target",
28
+ count: "30,056",
29
+ size: "118.4 MB",
30
+ updated: "2026-01-24",
31
+ },
32
+ ];
33
+
34
+ const stats = {
35
+ datasets: 2,
36
+ molecules: `${Math.round((apiStats.total_vectors || 23531) / 1000)}K`,
37
+ proteins: "442",
38
+ storage: "212 MB",
39
+ };
40
+
41
+ return { datasets, stats };
42
+ }
43
+ } catch (error) {
44
+ console.warn("Could not fetch live stats, using cached data:", error);
45
+ }
46
+
47
+ // Fallback - only 2 real datasets (kiba.tab and davis.tab)
48
  const datasets = [
49
+ {
50
+ name: "KIBA Dataset",
51
+ type: "Drug-Target",
52
+ count: "23,531",
53
+ size: "94.1 MB",
54
+ updated: "2026-01-25"
55
+ },
56
+ {
57
+ name: "DAVIS Kinase",
58
+ type: "Drug-Target",
59
+ count: "30,056",
60
+ size: "118.4 MB",
61
+ updated: "2026-01-24"
62
+ },
63
  ];
64
 
65
  const stats = {
66
+ datasets: 2,
67
+ molecules: "53.5K",
68
+ proteins: "442",
69
+ storage: "212 MB",
70
  };
71
 
72
+ return { datasets, stats };
73
  }
ui/package.json CHANGED
@@ -30,6 +30,7 @@
30
  "radix-ui": "^1.4.3",
31
  "react": "^19.2.3",
32
  "react-dom": "^19.2.3",
 
33
  "recharts": "^3.7.0",
34
  "tailwind-merge": "^3.4.0",
35
  "zod": "^4.3.6"
 
30
  "radix-ui": "^1.4.3",
31
  "react": "^19.2.3",
32
  "react-dom": "^19.2.3",
33
+ "reactflow": "^11.11.4",
34
  "recharts": "^3.7.0",
35
  "tailwind-merge": "^3.4.0",
36
  "zod": "^4.3.6"
ui/pnpm-lock.yaml CHANGED
@@ -68,6 +68,9 @@ importers:
68
  react-dom:
69
  specifier: ^19.2.3
70
  version: 19.2.3(react@19.2.3)
 
 
 
71
  recharts:
72
  specifier: ^3.7.0
73
  version: 3.7.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1)
@@ -1447,6 +1450,42 @@ packages:
1447
  '@radix-ui/rect@1.1.1':
1448
  resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
1449
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1450
  '@reduxjs/toolkit@2.11.2':
1451
  resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==}
1452
  peerDependencies:
@@ -1578,33 +1617,102 @@ packages:
1578
  '@types/d3-array@3.2.2':
1579
  resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
1580
 
 
 
 
 
 
 
 
 
 
1581
  '@types/d3-color@3.1.3':
1582
  resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
1583
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1584
  '@types/d3-ease@3.0.2':
1585
  resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
1586
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1587
  '@types/d3-interpolate@3.0.4':
1588
  resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
1589
 
1590
  '@types/d3-path@3.1.1':
1591
  resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
1592
 
 
 
 
 
 
 
 
 
 
 
 
 
1593
  '@types/d3-scale@4.0.9':
1594
  resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
1595
 
 
 
 
1596
  '@types/d3-shape@3.1.8':
1597
  resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==}
1598
 
 
 
 
1599
  '@types/d3-time@3.0.4':
1600
  resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
1601
 
1602
  '@types/d3-timer@3.0.2':
1603
  resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
1604
 
 
 
 
 
 
 
 
 
 
1605
  '@types/estree@1.0.8':
1606
  resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
1607
 
 
 
 
1608
  '@types/json-schema@7.0.15':
1609
  resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
1610
 
@@ -1971,6 +2079,9 @@ packages:
1971
  class-variance-authority@0.7.1:
1972
  resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
1973
 
 
 
 
1974
  cli-cursor@5.0.0:
1975
  resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
1976
  engines: {node: '>=18'}
@@ -2071,6 +2182,14 @@ packages:
2071
  resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
2072
  engines: {node: '>=12'}
2073
 
 
 
 
 
 
 
 
 
2074
  d3-ease@3.0.1:
2075
  resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
2076
  engines: {node: '>=12'}
@@ -2091,6 +2210,10 @@ packages:
2091
  resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
2092
  engines: {node: '>=12'}
2093
 
 
 
 
 
2094
  d3-shape@3.2.0:
2095
  resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
2096
  engines: {node: '>=12'}
@@ -2107,6 +2230,16 @@ packages:
2107
  resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
2108
  engines: {node: '>=12'}
2109
 
 
 
 
 
 
 
 
 
 
 
2110
  damerau-levenshtein@1.0.8:
2111
  resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
2112
 
@@ -3517,6 +3650,12 @@ packages:
3517
  resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==}
3518
  engines: {node: '>=0.10.0'}
3519
 
 
 
 
 
 
 
3520
  recast@0.23.11:
3521
  resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
3522
  engines: {node: '>= 4'}
@@ -4067,6 +4206,21 @@ packages:
4067
  zod@4.3.6:
4068
  resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
4069
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4070
  snapshots:
4071
 
4072
  '@alloc/quick-lru@5.2.0': {}
@@ -5431,6 +5585,84 @@ snapshots:
5431
 
5432
  '@radix-ui/rect@1.1.1': {}
5433
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5434
  '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.9)(react@19.2.3)(redux@5.0.1))(react@19.2.3)':
5435
  dependencies:
5436
  '@standard-schema/spec': 1.1.0
@@ -5539,30 +5771,125 @@ snapshots:
5539
 
5540
  '@types/d3-array@3.2.2': {}
5541
 
 
 
 
 
 
 
 
 
 
 
5542
  '@types/d3-color@3.1.3': {}
5543
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5544
  '@types/d3-ease@3.0.2': {}
5545
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5546
  '@types/d3-interpolate@3.0.4':
5547
  dependencies:
5548
  '@types/d3-color': 3.1.3
5549
 
5550
  '@types/d3-path@3.1.1': {}
5551
 
 
 
 
 
 
 
 
 
5552
  '@types/d3-scale@4.0.9':
5553
  dependencies:
5554
  '@types/d3-time': 3.0.4
5555
 
 
 
5556
  '@types/d3-shape@3.1.8':
5557
  dependencies:
5558
  '@types/d3-path': 3.1.1
5559
 
 
 
5560
  '@types/d3-time@3.0.4': {}
5561
 
5562
  '@types/d3-timer@3.0.2': {}
5563
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5564
  '@types/estree@1.0.8': {}
5565
 
 
 
5566
  '@types/json-schema@7.0.15': {}
5567
 
5568
  '@types/json5@0.0.29': {}
@@ -5944,6 +6271,8 @@ snapshots:
5944
  dependencies:
5945
  clsx: 2.1.1
5946
 
 
 
5947
  cli-cursor@5.0.0:
5948
  dependencies:
5949
  restore-cursor: 5.1.0
@@ -6018,6 +6347,13 @@ snapshots:
6018
 
6019
  d3-color@3.1.0: {}
6020
 
 
 
 
 
 
 
 
6021
  d3-ease@3.0.1: {}
6022
 
6023
  d3-format@3.1.2: {}
@@ -6036,6 +6372,8 @@ snapshots:
6036
  d3-time: 3.1.0
6037
  d3-time-format: 4.1.0
6038
 
 
 
6039
  d3-shape@3.2.0:
6040
  dependencies:
6041
  d3-path: 3.1.0
@@ -6050,6 +6388,23 @@ snapshots:
6050
 
6051
  d3-timer@3.0.1: {}
6052
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6053
  damerau-levenshtein@1.0.8: {}
6054
 
6055
  data-uri-to-buffer@4.0.1: {}
@@ -7580,6 +7935,20 @@ snapshots:
7580
 
7581
  react@19.2.3: {}
7582
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7583
  recast@0.23.11:
7584
  dependencies:
7585
  ast-types: 0.16.1
@@ -8306,3 +8675,11 @@ snapshots:
8306
  zod@3.25.76: {}
8307
 
8308
  zod@4.3.6: {}
 
 
 
 
 
 
 
 
 
68
  react-dom:
69
  specifier: ^19.2.3
70
  version: 19.2.3(react@19.2.3)
71
+ reactflow:
72
+ specifier: ^11.11.4
73
+ version: 11.11.4(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
74
  recharts:
75
  specifier: ^3.7.0
76
  version: 3.7.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1)
 
1450
  '@radix-ui/rect@1.1.1':
1451
  resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
1452
 
1453
+ '@reactflow/background@11.3.14':
1454
+ resolution: {integrity: sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==}
1455
+ peerDependencies:
1456
+ react: '>=17'
1457
+ react-dom: '>=17'
1458
+
1459
+ '@reactflow/controls@11.2.14':
1460
+ resolution: {integrity: sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==}
1461
+ peerDependencies:
1462
+ react: '>=17'
1463
+ react-dom: '>=17'
1464
+
1465
+ '@reactflow/core@11.11.4':
1466
+ resolution: {integrity: sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==}
1467
+ peerDependencies:
1468
+ react: '>=17'
1469
+ react-dom: '>=17'
1470
+
1471
+ '@reactflow/minimap@11.7.14':
1472
+ resolution: {integrity: sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==}
1473
+ peerDependencies:
1474
+ react: '>=17'
1475
+ react-dom: '>=17'
1476
+
1477
+ '@reactflow/node-resizer@2.2.14':
1478
+ resolution: {integrity: sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==}
1479
+ peerDependencies:
1480
+ react: '>=17'
1481
+ react-dom: '>=17'
1482
+
1483
+ '@reactflow/node-toolbar@1.3.14':
1484
+ resolution: {integrity: sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==}
1485
+ peerDependencies:
1486
+ react: '>=17'
1487
+ react-dom: '>=17'
1488
+
1489
  '@reduxjs/toolkit@2.11.2':
1490
  resolution: {integrity: sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==}
1491
  peerDependencies:
 
1617
  '@types/d3-array@3.2.2':
1618
  resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
1619
 
1620
+ '@types/d3-axis@3.0.6':
1621
+ resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==}
1622
+
1623
+ '@types/d3-brush@3.0.6':
1624
+ resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==}
1625
+
1626
+ '@types/d3-chord@3.0.6':
1627
+ resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==}
1628
+
1629
  '@types/d3-color@3.1.3':
1630
  resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
1631
 
1632
+ '@types/d3-contour@3.0.6':
1633
+ resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==}
1634
+
1635
+ '@types/d3-delaunay@6.0.4':
1636
+ resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==}
1637
+
1638
+ '@types/d3-dispatch@3.0.7':
1639
+ resolution: {integrity: sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==}
1640
+
1641
+ '@types/d3-drag@3.0.7':
1642
+ resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==}
1643
+
1644
+ '@types/d3-dsv@3.0.7':
1645
+ resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==}
1646
+
1647
  '@types/d3-ease@3.0.2':
1648
  resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
1649
 
1650
+ '@types/d3-fetch@3.0.7':
1651
+ resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==}
1652
+
1653
+ '@types/d3-force@3.0.10':
1654
+ resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==}
1655
+
1656
+ '@types/d3-format@3.0.4':
1657
+ resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==}
1658
+
1659
+ '@types/d3-geo@3.1.0':
1660
+ resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==}
1661
+
1662
+ '@types/d3-hierarchy@3.1.7':
1663
+ resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==}
1664
+
1665
  '@types/d3-interpolate@3.0.4':
1666
  resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
1667
 
1668
  '@types/d3-path@3.1.1':
1669
  resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
1670
 
1671
+ '@types/d3-polygon@3.0.2':
1672
+ resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==}
1673
+
1674
+ '@types/d3-quadtree@3.0.6':
1675
+ resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==}
1676
+
1677
+ '@types/d3-random@3.0.3':
1678
+ resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==}
1679
+
1680
+ '@types/d3-scale-chromatic@3.1.0':
1681
+ resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==}
1682
+
1683
  '@types/d3-scale@4.0.9':
1684
  resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
1685
 
1686
+ '@types/d3-selection@3.0.11':
1687
+ resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==}
1688
+
1689
  '@types/d3-shape@3.1.8':
1690
  resolution: {integrity: sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==}
1691
 
1692
+ '@types/d3-time-format@4.0.3':
1693
+ resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==}
1694
+
1695
  '@types/d3-time@3.0.4':
1696
  resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
1697
 
1698
  '@types/d3-timer@3.0.2':
1699
  resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
1700
 
1701
+ '@types/d3-transition@3.0.9':
1702
+ resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==}
1703
+
1704
+ '@types/d3-zoom@3.0.8':
1705
+ resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==}
1706
+
1707
+ '@types/d3@7.4.3':
1708
+ resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==}
1709
+
1710
  '@types/estree@1.0.8':
1711
  resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
1712
 
1713
+ '@types/geojson@7946.0.16':
1714
+ resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
1715
+
1716
  '@types/json-schema@7.0.15':
1717
  resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
1718
 
 
2079
  class-variance-authority@0.7.1:
2080
  resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
2081
 
2082
+ classcat@5.0.5:
2083
+ resolution: {integrity: sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==}
2084
+
2085
  cli-cursor@5.0.0:
2086
  resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
2087
  engines: {node: '>=18'}
 
2182
  resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
2183
  engines: {node: '>=12'}
2184
 
2185
+ d3-dispatch@3.0.1:
2186
+ resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==}
2187
+ engines: {node: '>=12'}
2188
+
2189
+ d3-drag@3.0.0:
2190
+ resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==}
2191
+ engines: {node: '>=12'}
2192
+
2193
  d3-ease@3.0.1:
2194
  resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
2195
  engines: {node: '>=12'}
 
2210
  resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
2211
  engines: {node: '>=12'}
2212
 
2213
+ d3-selection@3.0.0:
2214
+ resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==}
2215
+ engines: {node: '>=12'}
2216
+
2217
  d3-shape@3.2.0:
2218
  resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
2219
  engines: {node: '>=12'}
 
2230
  resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
2231
  engines: {node: '>=12'}
2232
 
2233
+ d3-transition@3.0.1:
2234
+ resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==}
2235
+ engines: {node: '>=12'}
2236
+ peerDependencies:
2237
+ d3-selection: 2 - 3
2238
+
2239
+ d3-zoom@3.0.0:
2240
+ resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==}
2241
+ engines: {node: '>=12'}
2242
+
2243
  damerau-levenshtein@1.0.8:
2244
  resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
2245
 
 
3650
  resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==}
3651
  engines: {node: '>=0.10.0'}
3652
 
3653
+ reactflow@11.11.4:
3654
+ resolution: {integrity: sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==}
3655
+ peerDependencies:
3656
+ react: '>=17'
3657
+ react-dom: '>=17'
3658
+
3659
  recast@0.23.11:
3660
  resolution: {integrity: sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA==}
3661
  engines: {node: '>= 4'}
 
4206
  zod@4.3.6:
4207
  resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
4208
 
4209
+ zustand@4.5.7:
4210
+ resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==}
4211
+ engines: {node: '>=12.7.0'}
4212
+ peerDependencies:
4213
+ '@types/react': '>=16.8'
4214
+ immer: '>=9.0.6'
4215
+ react: '>=16.8'
4216
+ peerDependenciesMeta:
4217
+ '@types/react':
4218
+ optional: true
4219
+ immer:
4220
+ optional: true
4221
+ react:
4222
+ optional: true
4223
+
4224
  snapshots:
4225
 
4226
  '@alloc/quick-lru@5.2.0': {}
 
5585
 
5586
  '@radix-ui/rect@1.1.1': {}
5587
 
5588
+ '@reactflow/background@11.3.14(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
5589
+ dependencies:
5590
+ '@reactflow/core': 11.11.4(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
5591
+ classcat: 5.0.5
5592
+ react: 19.2.3
5593
+ react-dom: 19.2.3(react@19.2.3)
5594
+ zustand: 4.5.7(@types/react@19.2.9)(immer@11.1.3)(react@19.2.3)
5595
+ transitivePeerDependencies:
5596
+ - '@types/react'
5597
+ - immer
5598
+
5599
+ '@reactflow/controls@11.2.14(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
5600
+ dependencies:
5601
+ '@reactflow/core': 11.11.4(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
5602
+ classcat: 5.0.5
5603
+ react: 19.2.3
5604
+ react-dom: 19.2.3(react@19.2.3)
5605
+ zustand: 4.5.7(@types/react@19.2.9)(immer@11.1.3)(react@19.2.3)
5606
+ transitivePeerDependencies:
5607
+ - '@types/react'
5608
+ - immer
5609
+
5610
+ '@reactflow/core@11.11.4(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
5611
+ dependencies:
5612
+ '@types/d3': 7.4.3
5613
+ '@types/d3-drag': 3.0.7
5614
+ '@types/d3-selection': 3.0.11
5615
+ '@types/d3-zoom': 3.0.8
5616
+ classcat: 5.0.5
5617
+ d3-drag: 3.0.0
5618
+ d3-selection: 3.0.0
5619
+ d3-zoom: 3.0.0
5620
+ react: 19.2.3
5621
+ react-dom: 19.2.3(react@19.2.3)
5622
+ zustand: 4.5.7(@types/react@19.2.9)(immer@11.1.3)(react@19.2.3)
5623
+ transitivePeerDependencies:
5624
+ - '@types/react'
5625
+ - immer
5626
+
5627
+ '@reactflow/minimap@11.7.14(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
5628
+ dependencies:
5629
+ '@reactflow/core': 11.11.4(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
5630
+ '@types/d3-selection': 3.0.11
5631
+ '@types/d3-zoom': 3.0.8
5632
+ classcat: 5.0.5
5633
+ d3-selection: 3.0.0
5634
+ d3-zoom: 3.0.0
5635
+ react: 19.2.3
5636
+ react-dom: 19.2.3(react@19.2.3)
5637
+ zustand: 4.5.7(@types/react@19.2.9)(immer@11.1.3)(react@19.2.3)
5638
+ transitivePeerDependencies:
5639
+ - '@types/react'
5640
+ - immer
5641
+
5642
+ '@reactflow/node-resizer@2.2.14(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
5643
+ dependencies:
5644
+ '@reactflow/core': 11.11.4(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
5645
+ classcat: 5.0.5
5646
+ d3-drag: 3.0.0
5647
+ d3-selection: 3.0.0
5648
+ react: 19.2.3
5649
+ react-dom: 19.2.3(react@19.2.3)
5650
+ zustand: 4.5.7(@types/react@19.2.9)(immer@11.1.3)(react@19.2.3)
5651
+ transitivePeerDependencies:
5652
+ - '@types/react'
5653
+ - immer
5654
+
5655
+ '@reactflow/node-toolbar@1.3.14(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)':
5656
+ dependencies:
5657
+ '@reactflow/core': 11.11.4(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
5658
+ classcat: 5.0.5
5659
+ react: 19.2.3
5660
+ react-dom: 19.2.3(react@19.2.3)
5661
+ zustand: 4.5.7(@types/react@19.2.9)(immer@11.1.3)(react@19.2.3)
5662
+ transitivePeerDependencies:
5663
+ - '@types/react'
5664
+ - immer
5665
+
5666
  '@reduxjs/toolkit@2.11.2(react-redux@9.2.0(@types/react@19.2.9)(react@19.2.3)(redux@5.0.1))(react@19.2.3)':
5667
  dependencies:
5668
  '@standard-schema/spec': 1.1.0
 
5771
 
5772
  '@types/d3-array@3.2.2': {}
5773
 
5774
+ '@types/d3-axis@3.0.6':
5775
+ dependencies:
5776
+ '@types/d3-selection': 3.0.11
5777
+
5778
+ '@types/d3-brush@3.0.6':
5779
+ dependencies:
5780
+ '@types/d3-selection': 3.0.11
5781
+
5782
+ '@types/d3-chord@3.0.6': {}
5783
+
5784
  '@types/d3-color@3.1.3': {}
5785
 
5786
+ '@types/d3-contour@3.0.6':
5787
+ dependencies:
5788
+ '@types/d3-array': 3.2.2
5789
+ '@types/geojson': 7946.0.16
5790
+
5791
+ '@types/d3-delaunay@6.0.4': {}
5792
+
5793
+ '@types/d3-dispatch@3.0.7': {}
5794
+
5795
+ '@types/d3-drag@3.0.7':
5796
+ dependencies:
5797
+ '@types/d3-selection': 3.0.11
5798
+
5799
+ '@types/d3-dsv@3.0.7': {}
5800
+
5801
  '@types/d3-ease@3.0.2': {}
5802
 
5803
+ '@types/d3-fetch@3.0.7':
5804
+ dependencies:
5805
+ '@types/d3-dsv': 3.0.7
5806
+
5807
+ '@types/d3-force@3.0.10': {}
5808
+
5809
+ '@types/d3-format@3.0.4': {}
5810
+
5811
+ '@types/d3-geo@3.1.0':
5812
+ dependencies:
5813
+ '@types/geojson': 7946.0.16
5814
+
5815
+ '@types/d3-hierarchy@3.1.7': {}
5816
+
5817
  '@types/d3-interpolate@3.0.4':
5818
  dependencies:
5819
  '@types/d3-color': 3.1.3
5820
 
5821
  '@types/d3-path@3.1.1': {}
5822
 
5823
+ '@types/d3-polygon@3.0.2': {}
5824
+
5825
+ '@types/d3-quadtree@3.0.6': {}
5826
+
5827
+ '@types/d3-random@3.0.3': {}
5828
+
5829
+ '@types/d3-scale-chromatic@3.1.0': {}
5830
+
5831
  '@types/d3-scale@4.0.9':
5832
  dependencies:
5833
  '@types/d3-time': 3.0.4
5834
 
5835
+ '@types/d3-selection@3.0.11': {}
5836
+
5837
  '@types/d3-shape@3.1.8':
5838
  dependencies:
5839
  '@types/d3-path': 3.1.1
5840
 
5841
+ '@types/d3-time-format@4.0.3': {}
5842
+
5843
  '@types/d3-time@3.0.4': {}
5844
 
5845
  '@types/d3-timer@3.0.2': {}
5846
 
5847
+ '@types/d3-transition@3.0.9':
5848
+ dependencies:
5849
+ '@types/d3-selection': 3.0.11
5850
+
5851
+ '@types/d3-zoom@3.0.8':
5852
+ dependencies:
5853
+ '@types/d3-interpolate': 3.0.4
5854
+ '@types/d3-selection': 3.0.11
5855
+
5856
+ '@types/d3@7.4.3':
5857
+ dependencies:
5858
+ '@types/d3-array': 3.2.2
5859
+ '@types/d3-axis': 3.0.6
5860
+ '@types/d3-brush': 3.0.6
5861
+ '@types/d3-chord': 3.0.6
5862
+ '@types/d3-color': 3.1.3
5863
+ '@types/d3-contour': 3.0.6
5864
+ '@types/d3-delaunay': 6.0.4
5865
+ '@types/d3-dispatch': 3.0.7
5866
+ '@types/d3-drag': 3.0.7
5867
+ '@types/d3-dsv': 3.0.7
5868
+ '@types/d3-ease': 3.0.2
5869
+ '@types/d3-fetch': 3.0.7
5870
+ '@types/d3-force': 3.0.10
5871
+ '@types/d3-format': 3.0.4
5872
+ '@types/d3-geo': 3.1.0
5873
+ '@types/d3-hierarchy': 3.1.7
5874
+ '@types/d3-interpolate': 3.0.4
5875
+ '@types/d3-path': 3.1.1
5876
+ '@types/d3-polygon': 3.0.2
5877
+ '@types/d3-quadtree': 3.0.6
5878
+ '@types/d3-random': 3.0.3
5879
+ '@types/d3-scale': 4.0.9
5880
+ '@types/d3-scale-chromatic': 3.1.0
5881
+ '@types/d3-selection': 3.0.11
5882
+ '@types/d3-shape': 3.1.8
5883
+ '@types/d3-time': 3.0.4
5884
+ '@types/d3-time-format': 4.0.3
5885
+ '@types/d3-timer': 3.0.2
5886
+ '@types/d3-transition': 3.0.9
5887
+ '@types/d3-zoom': 3.0.8
5888
+
5889
  '@types/estree@1.0.8': {}
5890
 
5891
+ '@types/geojson@7946.0.16': {}
5892
+
5893
  '@types/json-schema@7.0.15': {}
5894
 
5895
  '@types/json5@0.0.29': {}
 
6271
  dependencies:
6272
  clsx: 2.1.1
6273
 
6274
+ classcat@5.0.5: {}
6275
+
6276
  cli-cursor@5.0.0:
6277
  dependencies:
6278
  restore-cursor: 5.1.0
 
6347
 
6348
  d3-color@3.1.0: {}
6349
 
6350
+ d3-dispatch@3.0.1: {}
6351
+
6352
+ d3-drag@3.0.0:
6353
+ dependencies:
6354
+ d3-dispatch: 3.0.1
6355
+ d3-selection: 3.0.0
6356
+
6357
  d3-ease@3.0.1: {}
6358
 
6359
  d3-format@3.1.2: {}
 
6372
  d3-time: 3.1.0
6373
  d3-time-format: 4.1.0
6374
 
6375
+ d3-selection@3.0.0: {}
6376
+
6377
  d3-shape@3.2.0:
6378
  dependencies:
6379
  d3-path: 3.1.0
 
6388
 
6389
  d3-timer@3.0.1: {}
6390
 
6391
+ d3-transition@3.0.1(d3-selection@3.0.0):
6392
+ dependencies:
6393
+ d3-color: 3.1.0
6394
+ d3-dispatch: 3.0.1
6395
+ d3-ease: 3.0.1
6396
+ d3-interpolate: 3.0.1
6397
+ d3-selection: 3.0.0
6398
+ d3-timer: 3.0.1
6399
+
6400
+ d3-zoom@3.0.0:
6401
+ dependencies:
6402
+ d3-dispatch: 3.0.1
6403
+ d3-drag: 3.0.0
6404
+ d3-interpolate: 3.0.1
6405
+ d3-selection: 3.0.0
6406
+ d3-transition: 3.0.1(d3-selection@3.0.0)
6407
+
6408
  damerau-levenshtein@1.0.8: {}
6409
 
6410
  data-uri-to-buffer@4.0.1: {}
 
7935
 
7936
  react@19.2.3: {}
7937
 
7938
+ reactflow@11.11.4(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
7939
+ dependencies:
7940
+ '@reactflow/background': 11.3.14(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
7941
+ '@reactflow/controls': 11.2.14(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
7942
+ '@reactflow/core': 11.11.4(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
7943
+ '@reactflow/minimap': 11.7.14(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
7944
+ '@reactflow/node-resizer': 2.2.14(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
7945
+ '@reactflow/node-toolbar': 1.3.14(@types/react@19.2.9)(immer@11.1.3)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
7946
+ react: 19.2.3
7947
+ react-dom: 19.2.3(react@19.2.3)
7948
+ transitivePeerDependencies:
7949
+ - '@types/react'
7950
+ - immer
7951
+
7952
  recast@0.23.11:
7953
  dependencies:
7954
  ast-types: 0.16.1
 
8675
  zod@3.25.76: {}
8676
 
8677
  zod@4.3.6: {}
8678
+
8679
+ zustand@4.5.7(@types/react@19.2.9)(immer@11.1.3)(react@19.2.3):
8680
+ dependencies:
8681
+ use-sync-external-store: 1.6.0(react@19.2.3)
8682
+ optionalDependencies:
8683
+ '@types/react': 19.2.9
8684
+ immer: 11.1.3
8685
+ react: 19.2.3