Spaces:
Sleeping
Sleeping
yassinekolsi commited on
Commit ·
f2bd2d1
1
Parent(s): 2b2f173
fix: remove /dashboard prefix, fix theme toggle position, fix 3D viewer, remove langflow
Browse files- langflow/bioflow_dti_pipeline.json +0 -171
- run_langflow.py +0 -21
- ui/app/api/molecules/[id]/sdf/route.ts +38 -15
- ui/app/dashboard/data/data-view.tsx +0 -118
- ui/app/dashboard/data/page.tsx +0 -29
- ui/app/dashboard/discovery/page.tsx +0 -281
- ui/app/dashboard/error.tsx +0 -79
- ui/app/dashboard/explorer/chart.tsx +0 -56
- ui/app/dashboard/explorer/components.tsx +0 -100
- ui/app/dashboard/explorer/page.tsx +0 -59
- ui/app/dashboard/layout.tsx +0 -21
- ui/app/dashboard/molecules-2d/_components/Smiles2DViewer.tsx +0 -119
- ui/app/dashboard/molecules-2d/page.tsx +0 -223
- ui/app/dashboard/molecules-3d/_components/Molecule3DViewer.tsx +0 -191
- ui/app/dashboard/molecules-3d/page.tsx +0 -264
- ui/app/dashboard/page.tsx +0 -170
- ui/app/dashboard/proteins-3d/_components/ProteinViewer.tsx +0 -243
- ui/app/dashboard/proteins-3d/page.tsx +0 -228
- ui/app/dashboard/settings/page.tsx +0 -193
- ui/app/dashboard/visualization/page.tsx +0 -581
- ui/app/dashboard/workflow/page.tsx +0 -762
- ui/components/sidebar.tsx +26 -28
langflow/bioflow_dti_pipeline.json
DELETED
|
@@ -1,171 +0,0 @@
|
|
| 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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
run_langflow.py
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python
|
| 2 |
-
"""Run BioFlow Orchestrator (Langflow) server."""
|
| 3 |
-
|
| 4 |
-
import subprocess
|
| 5 |
-
import os
|
| 6 |
-
|
| 7 |
-
def main():
|
| 8 |
-
print("Starting BioFlow Orchestrator (Langflow)...")
|
| 9 |
-
|
| 10 |
-
# Get the directory where this script is located
|
| 11 |
-
script_dir = os.path.dirname(os.path.abspath(__file__))
|
| 12 |
-
os.chdir(script_dir)
|
| 13 |
-
|
| 14 |
-
# Path to langflow executable in .venv
|
| 15 |
-
langflow_exe = os.path.join(script_dir, ".venv", "Scripts", "langflow.exe")
|
| 16 |
-
|
| 17 |
-
# Run langflow
|
| 18 |
-
subprocess.run([langflow_exe, "run", "--host", "0.0.0.0", "--port", "7860"])
|
| 19 |
-
|
| 20 |
-
if __name__ == "__main__":
|
| 21 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/app/api/molecules/[id]/sdf/route.ts
CHANGED
|
@@ -1,31 +1,54 @@
|
|
| 1 |
import { NextResponse } from 'next/server';
|
| 2 |
import { API_CONFIG } from '@/config/api.config';
|
| 3 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
export async function GET(
|
| 5 |
_request: Request,
|
| 6 |
{ params }: { params: Promise<{ id: string }> }
|
| 7 |
) {
|
| 8 |
const { id } = await params;
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
try {
|
| 11 |
-
//
|
| 12 |
const moleculeRes = await fetch(`${API_CONFIG.baseUrl}/api/molecules/${id}`);
|
| 13 |
|
| 14 |
-
if (
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
}
|
| 18 |
-
throw new Error(`Backend returned ${moleculeRes.status}`);
|
| 19 |
-
}
|
| 20 |
-
|
| 21 |
-
const molecule = await moleculeRes.json();
|
| 22 |
-
|
| 23 |
-
if (!molecule.pubchemCid) {
|
| 24 |
-
return NextResponse.json({ error: 'No PubChem CID available for this molecule' }, { status: 400 });
|
| 25 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
// Fetch 3D SDF from PubChem
|
| 28 |
-
const pubchemUrl = `https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/${
|
| 29 |
|
| 30 |
const response = await fetch(pubchemUrl, {
|
| 31 |
next: { revalidate: 3600 },
|
|
@@ -33,7 +56,7 @@ export async function GET(
|
|
| 33 |
|
| 34 |
if (!response.ok) {
|
| 35 |
// Try 2D SDF if 3D is not available
|
| 36 |
-
const pubchemUrl2D = `https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/${
|
| 37 |
const response2D = await fetch(pubchemUrl2D, {
|
| 38 |
next: { revalidate: 3600 },
|
| 39 |
});
|
|
@@ -63,7 +86,7 @@ export async function GET(
|
|
| 63 |
},
|
| 64 |
});
|
| 65 |
} catch (error) {
|
| 66 |
-
console.error('Error fetching SDF:', error);
|
| 67 |
return NextResponse.json(
|
| 68 |
{ error: `Failed to fetch SDF: ${error instanceof Error ? error.message : 'Unknown error'}` },
|
| 69 |
{ status: 500 }
|
|
|
|
| 1 |
import { NextResponse } from 'next/server';
|
| 2 |
import { API_CONFIG } from '@/config/api.config';
|
| 3 |
|
| 4 |
+
// Mock PubChem CIDs for common molecules when backend is unavailable
|
| 5 |
+
const MOCK_PUBCHEM_CIDS: Record<string, number> = {
|
| 6 |
+
caffeine: 2519,
|
| 7 |
+
aspirin: 2244,
|
| 8 |
+
ibuprofen: 3672,
|
| 9 |
+
acetaminophen: 1983,
|
| 10 |
+
morphine: 5288826,
|
| 11 |
+
penicillin: 5904,
|
| 12 |
+
insulin: 16129672,
|
| 13 |
+
glucose: 5793,
|
| 14 |
+
ethanol: 702,
|
| 15 |
+
water: 962,
|
| 16 |
+
};
|
| 17 |
+
|
| 18 |
export async function GET(
|
| 19 |
_request: Request,
|
| 20 |
{ params }: { params: Promise<{ id: string }> }
|
| 21 |
) {
|
| 22 |
const { id } = await params;
|
| 23 |
+
const moleculeId = id.toLowerCase();
|
| 24 |
+
|
| 25 |
+
let pubchemCid: number | null = null;
|
| 26 |
|
| 27 |
try {
|
| 28 |
+
// Try to get molecule info from backend
|
| 29 |
const moleculeRes = await fetch(`${API_CONFIG.baseUrl}/api/molecules/${id}`);
|
| 30 |
|
| 31 |
+
if (moleculeRes.ok) {
|
| 32 |
+
const molecule = await moleculeRes.json();
|
| 33 |
+
pubchemCid = molecule.pubchemCid;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
}
|
| 35 |
+
} catch (error) {
|
| 36 |
+
console.error('Error fetching SDF:', error);
|
| 37 |
+
// Backend unavailable, use mock data
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
// Fallback to mock CID if backend didn't provide one
|
| 41 |
+
if (!pubchemCid) {
|
| 42 |
+
pubchemCid = MOCK_PUBCHEM_CIDS[moleculeId] || null;
|
| 43 |
+
}
|
| 44 |
|
| 45 |
+
if (!pubchemCid) {
|
| 46 |
+
return NextResponse.json({ error: 'No PubChem CID available for this molecule' }, { status: 400 });
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
try {
|
| 50 |
// Fetch 3D SDF from PubChem
|
| 51 |
+
const pubchemUrl = `https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/${pubchemCid}/record/SDF?record_type=3d`;
|
| 52 |
|
| 53 |
const response = await fetch(pubchemUrl, {
|
| 54 |
next: { revalidate: 3600 },
|
|
|
|
| 56 |
|
| 57 |
if (!response.ok) {
|
| 58 |
// Try 2D SDF if 3D is not available
|
| 59 |
+
const pubchemUrl2D = `https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/${pubchemCid}/record/SDF`;
|
| 60 |
const response2D = await fetch(pubchemUrl2D, {
|
| 61 |
next: { revalidate: 3600 },
|
| 62 |
});
|
|
|
|
| 86 |
},
|
| 87 |
});
|
| 88 |
} catch (error) {
|
| 89 |
+
console.error('Error fetching SDF from PubChem:', error);
|
| 90 |
return NextResponse.json(
|
| 91 |
{ error: `Failed to fetch SDF: ${error instanceof Error ? error.message : 'Unknown error'}` },
|
| 92 |
{ status: 500 }
|
ui/app/dashboard/data/data-view.tsx
DELETED
|
@@ -1,118 +0,0 @@
|
|
| 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"
|
| 7 |
-
import { Button } from "@/components/ui/button"
|
| 8 |
-
import { Card, CardContent } from "@/components/ui/card"
|
| 9 |
-
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
| 10 |
-
import { Tabs, TabsContent,TabsList, TabsTrigger } from "@/components/ui/tabs"
|
| 11 |
-
import { Dataset, Statistics } from "@/types/data"
|
| 12 |
-
|
| 13 |
-
interface DataViewProps {
|
| 14 |
-
datasets: Dataset[];
|
| 15 |
-
stats: Statistics | null;
|
| 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" },
|
| 22 |
-
{ label: "Proteins", value: stats?.proteins ?? "—", icon: Database, color: "text-emerald-500" },
|
| 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">
|
| 29 |
-
{statCards.map((stat, i) => (
|
| 30 |
-
<Card key={i}>
|
| 31 |
-
<CardContent className="p-6">
|
| 32 |
-
<div className="flex justify-between items-start mb-2">
|
| 33 |
-
<div className="text-sm font-medium text-muted-foreground">{stat.label}</div>
|
| 34 |
-
<div className={`text-lg ${stat.color}`}><stat.icon className="h-5 w-5" /></div>
|
| 35 |
-
</div>
|
| 36 |
-
<div className="text-2xl font-bold">{stat.value}</div>
|
| 37 |
-
</CardContent>
|
| 38 |
-
</Card>
|
| 39 |
-
))}
|
| 40 |
-
</div>
|
| 41 |
-
|
| 42 |
-
<Tabs defaultValue="datasets">
|
| 43 |
-
<TabsList>
|
| 44 |
-
<TabsTrigger value="datasets">Datasets</TabsTrigger>
|
| 45 |
-
<TabsTrigger value="upload">Upload</TabsTrigger>
|
| 46 |
-
<TabsTrigger value="processing">Processing</TabsTrigger>
|
| 47 |
-
</TabsList>
|
| 48 |
-
<TabsContent value="datasets" className="space-y-4">
|
| 49 |
-
<SectionHeader title="Your Datasets" icon={<Folder className="h-5 w-5 text-primary" />} />
|
| 50 |
-
<Card>
|
| 51 |
-
<Table>
|
| 52 |
-
<TableHeader>
|
| 53 |
-
<TableRow>
|
| 54 |
-
<TableHead>Name</TableHead>
|
| 55 |
-
<TableHead>Type</TableHead>
|
| 56 |
-
<TableHead>Items</TableHead>
|
| 57 |
-
<TableHead>Size</TableHead>
|
| 58 |
-
<TableHead>Updated</TableHead>
|
| 59 |
-
<TableHead className="text-right">Actions</TableHead>
|
| 60 |
-
</TableRow>
|
| 61 |
-
</TableHeader>
|
| 62 |
-
<TableBody>
|
| 63 |
-
{datasets.length === 0 && (
|
| 64 |
-
<TableRow>
|
| 65 |
-
<TableCell colSpan={6} className="text-center text-muted-foreground text-sm">No datasets found.</TableCell>
|
| 66 |
-
</TableRow>
|
| 67 |
-
)}
|
| 68 |
-
{datasets.map((ds, i) => (
|
| 69 |
-
<TableRow key={i}>
|
| 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>
|
| 77 |
-
<TableCell>{ds.size}</TableCell>
|
| 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>
|
| 87 |
-
))}
|
| 88 |
-
</TableBody>
|
| 89 |
-
</Table>
|
| 90 |
-
</Card>
|
| 91 |
-
</TabsContent>
|
| 92 |
-
<TabsContent value="upload">
|
| 93 |
-
<SectionHeader title="Upload New Data" icon={<Upload className="h-5 w-5 text-primary" />} />
|
| 94 |
-
<Card>
|
| 95 |
-
<CardContent className="p-12">
|
| 96 |
-
<div className="border-2 border-dashed rounded-lg p-12 flex flex-col items-center justify-center text-center space-y-4 hover:bg-accent/50 transition-colors cursor-pointer">
|
| 97 |
-
<div className="h-16 w-16 rounded-full bg-primary/10 flex items-center justify-center text-primary">
|
| 98 |
-
<CloudUpload className="h-8 w-8" />
|
| 99 |
-
</div>
|
| 100 |
-
<div>
|
| 101 |
-
<div className="text-lg font-semibold">Click to upload or drag and drop</div>
|
| 102 |
-
<div className="text-sm text-muted-foreground">CSV, SDF, FASTA, or JSON (max 50MB)</div>
|
| 103 |
-
</div>
|
| 104 |
-
</div>
|
| 105 |
-
</CardContent>
|
| 106 |
-
</Card>
|
| 107 |
-
</TabsContent>
|
| 108 |
-
<TabsContent value="processing">
|
| 109 |
-
<Card>
|
| 110 |
-
<CardContent className="p-12 text-center text-muted-foreground">
|
| 111 |
-
No active processing tasks.
|
| 112 |
-
</CardContent>
|
| 113 |
-
</Card>
|
| 114 |
-
</TabsContent>
|
| 115 |
-
</Tabs>
|
| 116 |
-
</div>
|
| 117 |
-
)
|
| 118 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/app/dashboard/data/page.tsx
DELETED
|
@@ -1,29 +0,0 @@
|
|
| 1 |
-
import { Database, Loader2 } from "lucide-react"
|
| 2 |
-
import { Suspense } from 'react'
|
| 3 |
-
|
| 4 |
-
import { PageHeader } from "@/components/page-header"
|
| 5 |
-
import { getData } from "@/lib/data-service"
|
| 6 |
-
|
| 7 |
-
import { DataView } from "./data-view"
|
| 8 |
-
|
| 9 |
-
export default async function DataPage() {
|
| 10 |
-
const { datasets, stats } = await getData()
|
| 11 |
-
|
| 12 |
-
return (
|
| 13 |
-
<div className="space-y-8 animate-in fade-in duration-500">
|
| 14 |
-
<PageHeader
|
| 15 |
-
title="Data Management"
|
| 16 |
-
subtitle="Upload, manage, and organize your datasets"
|
| 17 |
-
icon={<Database className="h-8 w-8" />}
|
| 18 |
-
/>
|
| 19 |
-
|
| 20 |
-
<Suspense fallback={
|
| 21 |
-
<div className="flex h-[400px] w-full items-center justify-center">
|
| 22 |
-
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
| 23 |
-
</div>
|
| 24 |
-
}>
|
| 25 |
-
<DataView datasets={datasets} stats={stats} />
|
| 26 |
-
</Suspense>
|
| 27 |
-
</div>
|
| 28 |
-
)
|
| 29 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/app/dashboard/discovery/page.tsx
DELETED
|
@@ -1,281 +0,0 @@
|
|
| 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"
|
| 7 |
-
import { Button } from "@/components/ui/button"
|
| 8 |
-
import { Card, CardContent } from "@/components/ui/card"
|
| 9 |
-
import { Label } from "@/components/ui/label"
|
| 10 |
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
| 11 |
-
import { Tabs, TabsContent,TabsList, TabsTrigger } from "@/components/ui/tabs"
|
| 12 |
-
import { Textarea } from "@/components/ui/textarea"
|
| 13 |
-
|
| 14 |
-
interface Candidate {
|
| 15 |
-
name?: string;
|
| 16 |
-
smiles?: string;
|
| 17 |
-
content?: string;
|
| 18 |
-
score: number;
|
| 19 |
-
mw?: number | string;
|
| 20 |
-
logp?: number | string;
|
| 21 |
-
}
|
| 22 |
-
|
| 23 |
-
interface DiscoveryResult {
|
| 24 |
-
candidates: Candidate[];
|
| 25 |
-
query: string;
|
| 26 |
-
search_type: string;
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
export default function DiscoveryPage() {
|
| 30 |
-
const [query, setQuery] = React.useState("")
|
| 31 |
-
const [searchType, setSearchType] = React.useState("similarity")
|
| 32 |
-
const [database, setDatabase] = React.useState("all")
|
| 33 |
-
const [isSearching, setIsSearching] = React.useState(false)
|
| 34 |
-
const [step, setStep] = React.useState(0)
|
| 35 |
-
const [jobId, setJobId] = React.useState<string | null>(null)
|
| 36 |
-
const [results, setResults] = React.useState<DiscoveryResult | null>(null)
|
| 37 |
-
const [error, setError] = React.useState<string | null>(null)
|
| 38 |
-
|
| 39 |
-
const handleSearch = async () => {
|
| 40 |
-
setIsSearching(true)
|
| 41 |
-
setStep(1)
|
| 42 |
-
setError(null)
|
| 43 |
-
setResults(null)
|
| 44 |
-
|
| 45 |
-
try {
|
| 46 |
-
// Start discovery pipeline
|
| 47 |
-
const response = await fetch('/api/discovery', {
|
| 48 |
-
method: 'POST',
|
| 49 |
-
headers: { 'Content-Type': 'application/json' },
|
| 50 |
-
body: JSON.stringify({
|
| 51 |
-
query,
|
| 52 |
-
searchType,
|
| 53 |
-
database,
|
| 54 |
-
limit: 10,
|
| 55 |
-
}),
|
| 56 |
-
})
|
| 57 |
-
|
| 58 |
-
const data = await response.json()
|
| 59 |
-
|
| 60 |
-
if (data.job_id) {
|
| 61 |
-
setJobId(data.job_id)
|
| 62 |
-
setStep(2)
|
| 63 |
-
|
| 64 |
-
// Poll for results
|
| 65 |
-
pollForResults(data.job_id)
|
| 66 |
-
} else if (data.candidates) {
|
| 67 |
-
// Direct result (sync mode)
|
| 68 |
-
setResults({ candidates: data.candidates, query, search_type: searchType })
|
| 69 |
-
setStep(4)
|
| 70 |
-
setIsSearching(false)
|
| 71 |
-
}
|
| 72 |
-
} catch (err) {
|
| 73 |
-
console.error("Discovery error:", err)
|
| 74 |
-
setError(`Discovery failed: ${err instanceof Error ? err.message : 'Unknown error'}. Ensure the backend is running.`)
|
| 75 |
-
setIsSearching(false)
|
| 76 |
-
setStep(0)
|
| 77 |
-
}
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
const pollForResults = async (id: string) => {
|
| 81 |
-
const maxAttempts = 30
|
| 82 |
-
let attempts = 0
|
| 83 |
-
|
| 84 |
-
const poll = async () => {
|
| 85 |
-
try {
|
| 86 |
-
const response = await fetch(`/api/discovery?jobId=${id}`)
|
| 87 |
-
const data = await response.json()
|
| 88 |
-
|
| 89 |
-
// Update step based on current_step
|
| 90 |
-
if (data.current_step === "encode") setStep(2)
|
| 91 |
-
else if (data.current_step === "search") setStep(3)
|
| 92 |
-
else if (data.current_step === "predict") setStep(3)
|
| 93 |
-
|
| 94 |
-
if (data.status === "completed") {
|
| 95 |
-
setResults(data.result)
|
| 96 |
-
setStep(4)
|
| 97 |
-
setIsSearching(false)
|
| 98 |
-
return
|
| 99 |
-
} else if (data.status === "failed") {
|
| 100 |
-
setError(data.error || "Pipeline failed")
|
| 101 |
-
setIsSearching(false)
|
| 102 |
-
return
|
| 103 |
-
}
|
| 104 |
-
|
| 105 |
-
attempts++
|
| 106 |
-
if (attempts < maxAttempts) {
|
| 107 |
-
setTimeout(poll, 1000)
|
| 108 |
-
} else {
|
| 109 |
-
setError("Pipeline timeout")
|
| 110 |
-
setIsSearching(false)
|
| 111 |
-
}
|
| 112 |
-
} catch (err) {
|
| 113 |
-
setError("Failed to get status")
|
| 114 |
-
setIsSearching(false)
|
| 115 |
-
}
|
| 116 |
-
}
|
| 117 |
-
|
| 118 |
-
poll()
|
| 119 |
-
}
|
| 120 |
-
|
| 121 |
-
const steps = [
|
| 122 |
-
{ name: "Input", status: step > 0 ? "done" : "active" },
|
| 123 |
-
{ name: "Encode", status: step > 1 ? "done" : (step === 1 ? "active" : "pending") },
|
| 124 |
-
{ name: "Search", status: step > 2 ? "done" : (step === 2 ? "active" : "pending") },
|
| 125 |
-
{ name: "Predict", status: step > 3 ? "done" : (step === 3 ? "active" : "pending") },
|
| 126 |
-
{ name: "Results", status: step === 4 ? "active" : "pending" },
|
| 127 |
-
]
|
| 128 |
-
|
| 129 |
-
const candidates = results?.candidates || []
|
| 130 |
-
|
| 131 |
-
return (
|
| 132 |
-
<div className="space-y-8 animate-in fade-in duration-500">
|
| 133 |
-
<PageHeader
|
| 134 |
-
title="Drug Discovery"
|
| 135 |
-
subtitle="Search for drug candidates with AI-powered analysis"
|
| 136 |
-
icon={<Microscope className="h-8 w-8" />}
|
| 137 |
-
/>
|
| 138 |
-
|
| 139 |
-
<Card>
|
| 140 |
-
<div className="p-4 border-b font-semibold">Search Query</div>
|
| 141 |
-
<CardContent className="p-6">
|
| 142 |
-
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
| 143 |
-
<div className="md:col-span-3">
|
| 144 |
-
<Textarea
|
| 145 |
-
placeholder="Enter a natural language query, SMILES string, or FASTA sequence..."
|
| 146 |
-
className="min-h-[120px] font-mono"
|
| 147 |
-
value={query}
|
| 148 |
-
onChange={(e) => setQuery(e.target.value)}
|
| 149 |
-
/>
|
| 150 |
-
</div>
|
| 151 |
-
<div className="space-y-4">
|
| 152 |
-
<div className="space-y-2">
|
| 153 |
-
<Label>Search Type</Label>
|
| 154 |
-
<Select value={searchType} onValueChange={setSearchType}>
|
| 155 |
-
<SelectTrigger>
|
| 156 |
-
<SelectValue placeholder="Select type" />
|
| 157 |
-
</SelectTrigger>
|
| 158 |
-
<SelectContent>
|
| 159 |
-
<SelectItem value="similarity">Similarity</SelectItem>
|
| 160 |
-
<SelectItem value="binding">Binding Affinity</SelectItem>
|
| 161 |
-
<SelectItem value="properties">Properties</SelectItem>
|
| 162 |
-
</SelectContent>
|
| 163 |
-
</Select>
|
| 164 |
-
</div>
|
| 165 |
-
<div className="space-y-2">
|
| 166 |
-
<Label>Database</Label>
|
| 167 |
-
<Select value={database} onValueChange={setDatabase}>
|
| 168 |
-
<SelectTrigger>
|
| 169 |
-
<SelectValue placeholder="Select database" />
|
| 170 |
-
</SelectTrigger>
|
| 171 |
-
<SelectContent>
|
| 172 |
-
<SelectItem value="all">All</SelectItem>
|
| 173 |
-
<SelectItem value="drugbank">DrugBank</SelectItem>
|
| 174 |
-
<SelectItem value="chembl">ChEMBL</SelectItem>
|
| 175 |
-
<SelectItem value="zinc">ZINC</SelectItem>
|
| 176 |
-
</SelectContent>
|
| 177 |
-
</Select>
|
| 178 |
-
</div>
|
| 179 |
-
<Button
|
| 180 |
-
className="w-full"
|
| 181 |
-
onClick={handleSearch}
|
| 182 |
-
disabled={isSearching || !query}
|
| 183 |
-
>
|
| 184 |
-
{isSearching ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : <Search className="mr-2 h-4 w-4" />}
|
| 185 |
-
{isSearching ? "Running..." : "Search"}
|
| 186 |
-
</Button>
|
| 187 |
-
</div>
|
| 188 |
-
</div>
|
| 189 |
-
</CardContent>
|
| 190 |
-
</Card>
|
| 191 |
-
|
| 192 |
-
<div className="space-y-4">
|
| 193 |
-
<SectionHeader title="Pipeline Status" icon={<ArrowRight className="h-5 w-5 text-muted-foreground" />} />
|
| 194 |
-
|
| 195 |
-
<div className="relative">
|
| 196 |
-
<div className="absolute left-0 top-1/2 w-full h-0.5 bg-muted -z-10 transform -translate-y-1/2"></div>
|
| 197 |
-
<div className="flex justify-between items-center w-full px-4">
|
| 198 |
-
{steps.map((s, i) => (
|
| 199 |
-
<div key={i} className="flex flex-col items-center gap-2 bg-background px-2">
|
| 200 |
-
<div className={`h-8 w-8 rounded-full flex items-center justify-center border-2 transition-colors ${
|
| 201 |
-
s.status === 'done' ? 'bg-primary border-primary text-primary-foreground' :
|
| 202 |
-
s.status === 'active' ? 'border-primary text-primary animate-pulse' : 'border-muted text-muted-foreground bg-background'
|
| 203 |
-
}`}>
|
| 204 |
-
{s.status === 'done' ? <CheckCircle2 className="h-5 w-5" /> : <Circle className="h-5 w-5" />}
|
| 205 |
-
</div>
|
| 206 |
-
<span className={`text-sm font-medium ${s.status === 'pending' ? 'text-muted-foreground' : 'text-foreground'}`}>
|
| 207 |
-
{s.name}
|
| 208 |
-
</span>
|
| 209 |
-
</div>
|
| 210 |
-
))}
|
| 211 |
-
</div>
|
| 212 |
-
</div>
|
| 213 |
-
</div>
|
| 214 |
-
|
| 215 |
-
{error && (
|
| 216 |
-
<Card className="border-amber-500 bg-amber-50 dark:bg-amber-950">
|
| 217 |
-
<CardContent className="p-4 text-amber-700 dark:text-amber-300">
|
| 218 |
-
⚠️ {error}
|
| 219 |
-
</CardContent>
|
| 220 |
-
</Card>
|
| 221 |
-
)}
|
| 222 |
-
|
| 223 |
-
{step === 4 && candidates.length > 0 && (
|
| 224 |
-
<div className="space-y-4 animate-in slide-in-from-bottom-4 duration-500">
|
| 225 |
-
<SectionHeader title="Results" icon={<CheckCircle2 className="h-5 w-5 text-green-500" />} />
|
| 226 |
-
|
| 227 |
-
<Tabs defaultValue="candidates">
|
| 228 |
-
<TabsList>
|
| 229 |
-
<TabsTrigger value="candidates">Top Candidates ({candidates.length})</TabsTrigger>
|
| 230 |
-
<TabsTrigger value="analysis">Property Analysis</TabsTrigger>
|
| 231 |
-
<TabsTrigger value="evidence">Evidence</TabsTrigger>
|
| 232 |
-
</TabsList>
|
| 233 |
-
<TabsContent value="candidates" className="space-y-4">
|
| 234 |
-
{candidates.map((candidate, i) => (
|
| 235 |
-
<Card key={i}>
|
| 236 |
-
<CardContent className="p-4 flex items-center justify-between">
|
| 237 |
-
<div>
|
| 238 |
-
<div className="font-bold text-lg">
|
| 239 |
-
{candidate.name || `Candidate ${i + 1}`}
|
| 240 |
-
</div>
|
| 241 |
-
<div className="text-sm text-muted-foreground font-mono mb-1">
|
| 242 |
-
{candidate.smiles || candidate.content || "N/A"}
|
| 243 |
-
</div>
|
| 244 |
-
<div className="flex gap-4 text-sm text-muted-foreground">
|
| 245 |
-
<span>MW: {candidate.mw ?? "N/A"}</span>
|
| 246 |
-
<span>LogP: {candidate.logp ?? "N/A"}</span>
|
| 247 |
-
</div>
|
| 248 |
-
</div>
|
| 249 |
-
<div className="text-right">
|
| 250 |
-
<div className="text-sm text-muted-foreground">Score</div>
|
| 251 |
-
<div className={`text-xl font-bold ${
|
| 252 |
-
candidate.score >= 0.9 ? 'text-green-600' :
|
| 253 |
-
candidate.score >= 0.8 ? 'text-green-500' : 'text-amber-500'
|
| 254 |
-
}`}>
|
| 255 |
-
{typeof candidate.score === 'number' ? candidate.score.toFixed(2) : candidate.score}
|
| 256 |
-
</div>
|
| 257 |
-
</div>
|
| 258 |
-
</CardContent>
|
| 259 |
-
</Card>
|
| 260 |
-
))}
|
| 261 |
-
</TabsContent>
|
| 262 |
-
<TabsContent value="analysis">
|
| 263 |
-
<Card>
|
| 264 |
-
<CardContent className="p-12 text-center text-muted-foreground">
|
| 265 |
-
Chart visualization would go here (using Recharts).
|
| 266 |
-
</CardContent>
|
| 267 |
-
</Card>
|
| 268 |
-
</TabsContent>
|
| 269 |
-
<TabsContent value="evidence">
|
| 270 |
-
<Card>
|
| 271 |
-
<CardContent className="p-12 text-center text-muted-foreground">
|
| 272 |
-
Evidence graph visualization would go here.
|
| 273 |
-
</CardContent>
|
| 274 |
-
</Card>
|
| 275 |
-
</TabsContent>
|
| 276 |
-
</Tabs>
|
| 277 |
-
</div>
|
| 278 |
-
)}
|
| 279 |
-
</div>
|
| 280 |
-
)
|
| 281 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/app/dashboard/error.tsx
DELETED
|
@@ -1,79 +0,0 @@
|
|
| 1 |
-
"use client";
|
| 2 |
-
|
| 3 |
-
import { useEffect } from "react";
|
| 4 |
-
import Link from "next/link";
|
| 5 |
-
|
| 6 |
-
import { RefreshCcw, Home, AlertCircle } from "lucide-react";
|
| 7 |
-
|
| 8 |
-
import { Button } from "@/components/ui/button";
|
| 9 |
-
import {
|
| 10 |
-
Empty,
|
| 11 |
-
EmptyContent,
|
| 12 |
-
EmptyDescription,
|
| 13 |
-
EmptyHeader,
|
| 14 |
-
EmptyTitle,
|
| 15 |
-
} from "@/components/ui/empty";
|
| 16 |
-
|
| 17 |
-
export default function Error({
|
| 18 |
-
error,
|
| 19 |
-
reset,
|
| 20 |
-
}: {
|
| 21 |
-
error: Error & { digest?: string };
|
| 22 |
-
reset: () => void;
|
| 23 |
-
}) {
|
| 24 |
-
useEffect(() => {
|
| 25 |
-
console.error(error);
|
| 26 |
-
}, [error]);
|
| 27 |
-
|
| 28 |
-
return (
|
| 29 |
-
<div className="flex w-full items-center justify-center p-4">
|
| 30 |
-
<div className="flex min-h-screen items-center border-x">
|
| 31 |
-
<div className="relative">
|
| 32 |
-
<div className="absolute inset-x-0 top-0 h-px bg-border" />
|
| 33 |
-
<Empty>
|
| 34 |
-
<EmptyHeader>
|
| 35 |
-
<div className="flex justify-center mb-4">
|
| 36 |
-
<div className="p-4 rounded-full bg-destructive/10">
|
| 37 |
-
<AlertCircle className="h-12 w-12 text-destructive" />
|
| 38 |
-
</div>
|
| 39 |
-
</div>
|
| 40 |
-
<EmptyTitle className="font-black font-mono text-5xl md:text-7xl">
|
| 41 |
-
ERREUR
|
| 42 |
-
</EmptyTitle>
|
| 43 |
-
<EmptyDescription className="text-lg max-w-md mx-auto">
|
| 44 |
-
Désolé, une erreur inattendue est survenue lors de la navigation.
|
| 45 |
-
{error.digest && (
|
| 46 |
-
<span className="block mt-2 text-xs font-mono text-muted-foreground">
|
| 47 |
-
ID digest: {error.digest}
|
| 48 |
-
</span>
|
| 49 |
-
)}
|
| 50 |
-
</EmptyDescription>
|
| 51 |
-
</EmptyHeader>
|
| 52 |
-
<EmptyContent>
|
| 53 |
-
<div className="flex flex-wrap items-center justify-center gap-4">
|
| 54 |
-
<Button
|
| 55 |
-
onClick={() => reset()}
|
| 56 |
-
size="lg"
|
| 57 |
-
className="rounded-full px-8"
|
| 58 |
-
>
|
| 59 |
-
<RefreshCcw className="mr-2 h-4 w-4" /> Réessayer
|
| 60 |
-
</Button>
|
| 61 |
-
<Button
|
| 62 |
-
asChild
|
| 63 |
-
variant="outline"
|
| 64 |
-
size="lg"
|
| 65 |
-
className="rounded-full px-8"
|
| 66 |
-
>
|
| 67 |
-
<Link href="/">
|
| 68 |
-
<Home className="mr-2 h-4 w-4" /> Accueil
|
| 69 |
-
</Link>
|
| 70 |
-
</Button>
|
| 71 |
-
</div>
|
| 72 |
-
</EmptyContent>
|
| 73 |
-
</Empty>
|
| 74 |
-
<div className="absolute inset-x-0 bottom-0 h-px bg-border" />
|
| 75 |
-
</div>
|
| 76 |
-
</div>
|
| 77 |
-
</div>
|
| 78 |
-
);
|
| 79 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/app/dashboard/explorer/chart.tsx
DELETED
|
@@ -1,56 +0,0 @@
|
|
| 1 |
-
"use client"
|
| 2 |
-
|
| 3 |
-
import { AlertCircle } from "lucide-react"
|
| 4 |
-
import { Cell,ResponsiveContainer, Scatter, ScatterChart, Tooltip, XAxis, YAxis, ZAxis } from "recharts"
|
| 5 |
-
|
| 6 |
-
import { Card, CardContent } from "@/components/ui/card"
|
| 7 |
-
import { DataPoint } from "@/types/explorer"
|
| 8 |
-
|
| 9 |
-
export function ExplorerChart({ data }: { data: DataPoint[] }) {
|
| 10 |
-
if (!data || data.length === 0) {
|
| 11 |
-
return (
|
| 12 |
-
<div className="flex h-full items-center justify-center text-muted-foreground">
|
| 13 |
-
<AlertCircle className="mr-2 h-4 w-4" />
|
| 14 |
-
No data available for this configuration.
|
| 15 |
-
</div>
|
| 16 |
-
)
|
| 17 |
-
}
|
| 18 |
-
|
| 19 |
-
return (
|
| 20 |
-
<Card className="h-[500px] overflow-hidden bg-gradient-to-br from-card to-secondary/30">
|
| 21 |
-
<CardContent className="p-4 h-full relative">
|
| 22 |
-
<ResponsiveContainer width="100%" height="100%">
|
| 23 |
-
<ScatterChart margin={{ top: 20, right: 20, bottom: 20, left: 20 }}>
|
| 24 |
-
<XAxis type="number" dataKey="x" name="PC1" stroke="currentColor" fontSize={12} tickLine={false} axisLine={{ strokeOpacity: 0.2 }} />
|
| 25 |
-
<YAxis type="number" dataKey="y" name="PC2" stroke="currentColor" fontSize={12} tickLine={false} axisLine={{ strokeOpacity: 0.2 }} />
|
| 26 |
-
<ZAxis type="number" dataKey="z" range={[50, 400]} />
|
| 27 |
-
<Tooltip
|
| 28 |
-
cursor={{ strokeDasharray: '3 3' }}
|
| 29 |
-
content={({ active, payload }) => {
|
| 30 |
-
if (active && payload && payload.length) {
|
| 31 |
-
const point = payload[0].payload as DataPoint;
|
| 32 |
-
return (
|
| 33 |
-
<div className="bg-popover border border-border p-3 rounded-lg shadow-xl text-sm z-50">
|
| 34 |
-
<p className="font-bold mb-1 text-primary">{point.name}</p>
|
| 35 |
-
<div className="grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-muted-foreground">
|
| 36 |
-
<span>X:</span> <span className="text-foreground">{Number(point.x).toFixed(2)}</span>
|
| 37 |
-
<span>Y:</span> <span className="text-foreground">{Number(point.y).toFixed(2)}</span>
|
| 38 |
-
<span>Affinity:</span> <span className="text-foreground">{Number(point.affinity).toFixed(2)}</span>
|
| 39 |
-
</div>
|
| 40 |
-
</div>
|
| 41 |
-
)
|
| 42 |
-
}
|
| 43 |
-
return null;
|
| 44 |
-
}}
|
| 45 |
-
/>
|
| 46 |
-
<Scatter name="Molecules" data={data} fill="#8884d8" animationDuration={1000}>
|
| 47 |
-
{data.map((entry, index) => (
|
| 48 |
-
<Cell key={`cell-${index}`} fill={entry.color} fillOpacity={0.7} className="hover:opacity-100 transition-opacity duration-200" />
|
| 49 |
-
))}
|
| 50 |
-
</Scatter>
|
| 51 |
-
</ScatterChart>
|
| 52 |
-
</ResponsiveContainer>
|
| 53 |
-
</CardContent>
|
| 54 |
-
</Card>
|
| 55 |
-
)
|
| 56 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/app/dashboard/explorer/components.tsx
DELETED
|
@@ -1,100 +0,0 @@
|
|
| 1 |
-
"use client"
|
| 2 |
-
|
| 3 |
-
import { RefreshCw, SlidersHorizontal } from "lucide-react"
|
| 4 |
-
import { useRouter, useSearchParams } from "next/navigation"
|
| 5 |
-
import * as React from "react"
|
| 6 |
-
|
| 7 |
-
import { Button } from "@/components/ui/button"
|
| 8 |
-
import { Card, CardContent,CardHeader, CardTitle } from "@/components/ui/card"
|
| 9 |
-
import { Label } from "@/components/ui/label"
|
| 10 |
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
| 11 |
-
|
| 12 |
-
export function ExplorerControls() {
|
| 13 |
-
const router = useRouter()
|
| 14 |
-
const searchParams = useSearchParams()
|
| 15 |
-
const [isPending, startTransition] = React.useTransition()
|
| 16 |
-
|
| 17 |
-
const dataset = searchParams.get("dataset") || "DrugBank"
|
| 18 |
-
const visualization = searchParams.get("view") || "UMAP"
|
| 19 |
-
const colorBy = searchParams.get("colorBy") || "Activity"
|
| 20 |
-
|
| 21 |
-
const createQueryString = React.useCallback(
|
| 22 |
-
(name: string, value: string) => {
|
| 23 |
-
const params = new URLSearchParams(searchParams.toString())
|
| 24 |
-
params.set(name, value)
|
| 25 |
-
return params.toString()
|
| 26 |
-
},
|
| 27 |
-
[searchParams]
|
| 28 |
-
)
|
| 29 |
-
|
| 30 |
-
const handleUpdate = (name: string, value: string) => {
|
| 31 |
-
startTransition(() => {
|
| 32 |
-
router.push(`?${createQueryString(name, value)}`, { scroll: false })
|
| 33 |
-
})
|
| 34 |
-
}
|
| 35 |
-
|
| 36 |
-
return (
|
| 37 |
-
<Card className="h-full border-l-4 border-l-primary/50">
|
| 38 |
-
<CardHeader>
|
| 39 |
-
<CardTitle className="flex items-center gap-2 text-lg">
|
| 40 |
-
<SlidersHorizontal className="h-5 w-5" />
|
| 41 |
-
Controls
|
| 42 |
-
</CardTitle>
|
| 43 |
-
</CardHeader>
|
| 44 |
-
<CardContent className="space-y-6">
|
| 45 |
-
<div className="space-y-2">
|
| 46 |
-
<Label htmlFor="dataset">Dataset</Label>
|
| 47 |
-
<Select value={dataset} onValueChange={(v) => handleUpdate("dataset", v)}>
|
| 48 |
-
<SelectTrigger id="dataset">
|
| 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>
|
| 58 |
-
<div className="space-y-2">
|
| 59 |
-
<Label htmlFor="visualization">Algorithm</Label>
|
| 60 |
-
<Select value={visualization} onValueChange={(v) => handleUpdate("view", v)}>
|
| 61 |
-
<SelectTrigger id="visualization">
|
| 62 |
-
<SelectValue placeholder="Select algorithm" />
|
| 63 |
-
</SelectTrigger>
|
| 64 |
-
<SelectContent>
|
| 65 |
-
<SelectItem value="UMAP">UMAP</SelectItem>
|
| 66 |
-
<SelectItem value="t-SNE">t-SNE</SelectItem>
|
| 67 |
-
<SelectItem value="PCA">PCA</SelectItem>
|
| 68 |
-
</SelectContent>
|
| 69 |
-
</Select>
|
| 70 |
-
</div>
|
| 71 |
-
<div className="space-y-2">
|
| 72 |
-
<Label htmlFor="colorBy">Color Mapping</Label>
|
| 73 |
-
<Select value={colorBy} onValueChange={(v) => handleUpdate("colorBy", v)}>
|
| 74 |
-
<SelectTrigger id="colorBy">
|
| 75 |
-
<SelectValue placeholder="Select color metric" />
|
| 76 |
-
</SelectTrigger>
|
| 77 |
-
<SelectContent>
|
| 78 |
-
<SelectItem value="Activity">Binding Affinity</SelectItem>
|
| 79 |
-
<SelectItem value="MW">Molecular Weight</SelectItem>
|
| 80 |
-
<SelectItem value="LogP">LogP</SelectItem>
|
| 81 |
-
<SelectItem value="Cluster">Cluster ID</SelectItem>
|
| 82 |
-
</SelectContent>
|
| 83 |
-
</Select>
|
| 84 |
-
</div>
|
| 85 |
-
|
| 86 |
-
<div className="pt-4">
|
| 87 |
-
<Button
|
| 88 |
-
variant="secondary"
|
| 89 |
-
className="w-full"
|
| 90 |
-
disabled={isPending}
|
| 91 |
-
onClick={() => handleUpdate("refresh", Date.now().toString())}
|
| 92 |
-
>
|
| 93 |
-
<RefreshCw className={`mr-2 h-4 w-4 ${isPending ? "animate-spin" : ""}`} />
|
| 94 |
-
{isPending ? "Updating..." : "Regenerate View"}
|
| 95 |
-
</Button>
|
| 96 |
-
</div>
|
| 97 |
-
</CardContent>
|
| 98 |
-
</Card>
|
| 99 |
-
)
|
| 100 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/app/dashboard/explorer/page.tsx
DELETED
|
@@ -1,59 +0,0 @@
|
|
| 1 |
-
import { Loader2 } from "lucide-react"
|
| 2 |
-
import { Suspense } from "react"
|
| 3 |
-
|
| 4 |
-
import { getExplorerPoints } from "@/lib/explorer-service"
|
| 5 |
-
|
| 6 |
-
import { ExplorerChart } from "./chart"
|
| 7 |
-
import { ExplorerControls } from "./components"
|
| 8 |
-
|
| 9 |
-
interface ExplorerPageProps {
|
| 10 |
-
searchParams: Promise<{ [key: string]: string | string[] | undefined }>
|
| 11 |
-
}
|
| 12 |
-
|
| 13 |
-
function pickParam(v: string | string[] | undefined): string | undefined {
|
| 14 |
-
if (Array.isArray(v)) return v[0]
|
| 15 |
-
return v
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
export default async function ExplorerPage({ searchParams }: ExplorerPageProps) {
|
| 19 |
-
// Await searchParams as required by Next.js 16/15
|
| 20 |
-
const params = await searchParams
|
| 21 |
-
|
| 22 |
-
const dataset = pickParam(params.dataset) || "DrugBank"
|
| 23 |
-
const view = pickParam(params.view) || "UMAP"
|
| 24 |
-
const colorBy = pickParam(params.colorBy) || "Activity"
|
| 25 |
-
|
| 26 |
-
const { points: data } = await getExplorerPoints(dataset, view, colorBy)
|
| 27 |
-
|
| 28 |
-
return (
|
| 29 |
-
<div className="container mx-auto p-6 space-y-6">
|
| 30 |
-
<div className="flex flex-col space-y-2">
|
| 31 |
-
<h1 className="text-3xl font-bold tracking-tight">Data Explorer</h1>
|
| 32 |
-
<p className="text-muted-foreground">
|
| 33 |
-
Visualize binding affinity landscapes and model predictions in 3D space using dimensionality reduction.
|
| 34 |
-
</p>
|
| 35 |
-
</div>
|
| 36 |
-
|
| 37 |
-
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
| 38 |
-
<div className="lg:col-span-1">
|
| 39 |
-
<Suspense fallback={<div>Loading controls...</div>}>
|
| 40 |
-
<ExplorerControls />
|
| 41 |
-
</Suspense>
|
| 42 |
-
</div>
|
| 43 |
-
|
| 44 |
-
<div className="lg:col-span-3">
|
| 45 |
-
<Suspense
|
| 46 |
-
key={`${dataset}-${view}-${colorBy}`}
|
| 47 |
-
fallback={
|
| 48 |
-
<div className="h-[500px] flex items-center justify-center border rounded-lg bg-muted/10">
|
| 49 |
-
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
| 50 |
-
</div>
|
| 51 |
-
}
|
| 52 |
-
>
|
| 53 |
-
<ExplorerChart data={data} />
|
| 54 |
-
</Suspense>
|
| 55 |
-
</div>
|
| 56 |
-
</div>
|
| 57 |
-
</div>
|
| 58 |
-
)
|
| 59 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/app/dashboard/layout.tsx
DELETED
|
@@ -1,21 +0,0 @@
|
|
| 1 |
-
import type { ReactNode } from 'react';
|
| 2 |
-
import {
|
| 3 |
-
SidebarInset,
|
| 4 |
-
SidebarProvider,
|
| 5 |
-
} from "@/components/animate-ui/components/radix/sidebar";
|
| 6 |
-
import { AppSidebar } from "@/components/sidebar";
|
| 7 |
-
|
| 8 |
-
export default function DashboardLayout({ children }: { children: ReactNode }) {
|
| 9 |
-
return (
|
| 10 |
-
<SidebarProvider>
|
| 11 |
-
<AppSidebar />
|
| 12 |
-
<SidebarInset>
|
| 13 |
-
<main className="flex-1 overflow-y-auto bg-background p-8">
|
| 14 |
-
<div className="mx-auto max-w-7xl">
|
| 15 |
-
{children}
|
| 16 |
-
</div>
|
| 17 |
-
</main>
|
| 18 |
-
</SidebarInset>
|
| 19 |
-
</SidebarProvider>
|
| 20 |
-
);
|
| 21 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/app/dashboard/molecules-2d/_components/Smiles2DViewer.tsx
DELETED
|
@@ -1,119 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { AlertCircle } from 'lucide-react';
|
| 4 |
-
import { useCallback, useEffect, useId, useRef, useState } from 'react';
|
| 5 |
-
import SmilesDrawer from 'smiles-drawer';
|
| 6 |
-
|
| 7 |
-
import { Card, CardContent } from '@/components/ui/card';
|
| 8 |
-
import { Skeleton } from '@/components/ui/skeleton';
|
| 9 |
-
|
| 10 |
-
interface Smiles2DViewerProps {
|
| 11 |
-
smiles: string;
|
| 12 |
-
width?: number;
|
| 13 |
-
height?: number;
|
| 14 |
-
className?: string;
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
export function Smiles2DViewer({
|
| 18 |
-
smiles,
|
| 19 |
-
width = 400,
|
| 20 |
-
height = 300,
|
| 21 |
-
className,
|
| 22 |
-
}: Smiles2DViewerProps) {
|
| 23 |
-
const canvasRef = useRef<HTMLCanvasElement>(null);
|
| 24 |
-
const canvasId = useId();
|
| 25 |
-
const [error, setError] = useState<string | null>(null);
|
| 26 |
-
const [isLoading, setIsLoading] = useState(true);
|
| 27 |
-
|
| 28 |
-
const drawMolecule = useCallback(async () => {
|
| 29 |
-
if (!canvasRef.current || !smiles) {
|
| 30 |
-
setIsLoading(false);
|
| 31 |
-
return;
|
| 32 |
-
}
|
| 33 |
-
|
| 34 |
-
setIsLoading(true);
|
| 35 |
-
setError(null);
|
| 36 |
-
|
| 37 |
-
try {
|
| 38 |
-
const drawer = new SmilesDrawer.SmiDrawer({
|
| 39 |
-
width,
|
| 40 |
-
height,
|
| 41 |
-
bondThickness: 1.5,
|
| 42 |
-
bondLength: 15,
|
| 43 |
-
shortBondLength: 0.85,
|
| 44 |
-
bondSpacing: 4,
|
| 45 |
-
atomVisualization: 'default',
|
| 46 |
-
isomeric: true,
|
| 47 |
-
debug: false,
|
| 48 |
-
terminalCarbons: false,
|
| 49 |
-
explicitHydrogens: false,
|
| 50 |
-
compactDrawing: true,
|
| 51 |
-
fontSizeLarge: 11,
|
| 52 |
-
fontSizeSmall: 8,
|
| 53 |
-
padding: 20,
|
| 54 |
-
});
|
| 55 |
-
|
| 56 |
-
try {
|
| 57 |
-
await new Promise<void>((resolve, reject) => {
|
| 58 |
-
(drawer as any).draw(
|
| 59 |
-
smiles,
|
| 60 |
-
`[id="${canvasId}"]`,
|
| 61 |
-
'light',
|
| 62 |
-
() => resolve(),
|
| 63 |
-
(drawError: Error) => reject(drawError)
|
| 64 |
-
);
|
| 65 |
-
});
|
| 66 |
-
} catch (drawError) {
|
| 67 |
-
throw drawError;
|
| 68 |
-
}
|
| 69 |
-
|
| 70 |
-
setIsLoading(false);
|
| 71 |
-
} catch (err) {
|
| 72 |
-
console.error('SMILES drawing error:', err);
|
| 73 |
-
setError(
|
| 74 |
-
err instanceof Error
|
| 75 |
-
? `Invalid SMILES: ${err.message}`
|
| 76 |
-
: 'Failed to render molecule structure'
|
| 77 |
-
);
|
| 78 |
-
setIsLoading(false);
|
| 79 |
-
}
|
| 80 |
-
}, [smiles, width, height]);
|
| 81 |
-
|
| 82 |
-
useEffect(() => {
|
| 83 |
-
drawMolecule();
|
| 84 |
-
}, [drawMolecule]);
|
| 85 |
-
|
| 86 |
-
if (error) {
|
| 87 |
-
return (
|
| 88 |
-
<Card className={`border-destructive bg-destructive/10 ${className}`}>
|
| 89 |
-
<CardContent className="flex items-center gap-3 p-4">
|
| 90 |
-
<AlertCircle className="size-5 text-destructive" />
|
| 91 |
-
<div className="flex flex-col">
|
| 92 |
-
<span className="text-sm font-medium text-destructive">
|
| 93 |
-
Visualization Error
|
| 94 |
-
</span>
|
| 95 |
-
<span className="text-xs text-muted-foreground">{error}</span>
|
| 96 |
-
</div>
|
| 97 |
-
</CardContent>
|
| 98 |
-
</Card>
|
| 99 |
-
);
|
| 100 |
-
}
|
| 101 |
-
|
| 102 |
-
return (
|
| 103 |
-
<div className={`relative ${className}`}>
|
| 104 |
-
{isLoading && (
|
| 105 |
-
<Skeleton
|
| 106 |
-
className="absolute inset-0"
|
| 107 |
-
style={{ width, height }}
|
| 108 |
-
/>
|
| 109 |
-
)}
|
| 110 |
-
<canvas
|
| 111 |
-
ref={canvasRef}
|
| 112 |
-
id={canvasId}
|
| 113 |
-
width={width}
|
| 114 |
-
height={height}
|
| 115 |
-
className={`bg-background rounded-lg ${isLoading ? 'opacity-0' : 'opacity-100'} transition-opacity duration-200`}
|
| 116 |
-
/>
|
| 117 |
-
</div>
|
| 118 |
-
);
|
| 119 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/app/dashboard/molecules-2d/page.tsx
DELETED
|
@@ -1,223 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { Check, Copy, Search } from 'lucide-react';
|
| 4 |
-
import dynamic from 'next/dynamic';
|
| 5 |
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
| 6 |
-
|
| 7 |
-
import { Badge } from '@/components/ui/badge';
|
| 8 |
-
import { Button } from '@/components/ui/button';
|
| 9 |
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
| 10 |
-
import { Input } from '@/components/ui/input';
|
| 11 |
-
import { ScrollArea } from '@/components/ui/scroll-area';
|
| 12 |
-
import { Separator } from '@/components/ui/separator';
|
| 13 |
-
import { Skeleton } from '@/components/ui/skeleton';
|
| 14 |
-
import { getMolecules } from '@/lib/visualization-api';
|
| 15 |
-
import type { Molecule } from '@/lib/visualization-types';
|
| 16 |
-
|
| 17 |
-
// Dynamic import of the 2D viewer to prevent SSR issues
|
| 18 |
-
const Smiles2DViewer = dynamic(
|
| 19 |
-
() =>
|
| 20 |
-
import('./_components/Smiles2DViewer').then((mod) => mod.Smiles2DViewer),
|
| 21 |
-
{
|
| 22 |
-
ssr: false,
|
| 23 |
-
loading: () => <Skeleton className="size-[400px]" />,
|
| 24 |
-
}
|
| 25 |
-
);
|
| 26 |
-
|
| 27 |
-
export default function Molecules2DPage() {
|
| 28 |
-
const [molecules, setMolecules] = useState<Molecule[]>([]);
|
| 29 |
-
const [selectedMolecule, setSelectedMolecule] = useState<Molecule | null>(null);
|
| 30 |
-
const [searchQuery, setSearchQuery] = useState('');
|
| 31 |
-
const [isLoading, setIsLoading] = useState(true);
|
| 32 |
-
const [error, setError] = useState<string | null>(null);
|
| 33 |
-
const [copied, setCopied] = useState(false);
|
| 34 |
-
|
| 35 |
-
useEffect(() => {
|
| 36 |
-
const loadMolecules = async () => {
|
| 37 |
-
try {
|
| 38 |
-
setIsLoading(true);
|
| 39 |
-
const data = await getMolecules();
|
| 40 |
-
setMolecules(data);
|
| 41 |
-
if (data.length > 0) {
|
| 42 |
-
setSelectedMolecule(data[0]);
|
| 43 |
-
}
|
| 44 |
-
} catch (err) {
|
| 45 |
-
setError(
|
| 46 |
-
err instanceof Error ? err.message : 'Failed to load molecules'
|
| 47 |
-
);
|
| 48 |
-
} finally {
|
| 49 |
-
setIsLoading(false);
|
| 50 |
-
}
|
| 51 |
-
};
|
| 52 |
-
loadMolecules();
|
| 53 |
-
}, []);
|
| 54 |
-
|
| 55 |
-
const filteredMolecules = useMemo(() => {
|
| 56 |
-
if (!searchQuery.trim()) return molecules;
|
| 57 |
-
const query = searchQuery.toLowerCase();
|
| 58 |
-
return molecules.filter(
|
| 59 |
-
(m) =>
|
| 60 |
-
m.name.toLowerCase().includes(query) ||
|
| 61 |
-
m.smiles.toLowerCase().includes(query)
|
| 62 |
-
);
|
| 63 |
-
}, [molecules, searchQuery]);
|
| 64 |
-
|
| 65 |
-
const handleCopySmiles = useCallback(async () => {
|
| 66 |
-
if (!selectedMolecule) return;
|
| 67 |
-
try {
|
| 68 |
-
await navigator.clipboard.writeText(selectedMolecule.smiles);
|
| 69 |
-
setCopied(true);
|
| 70 |
-
setTimeout(() => setCopied(false), 2000);
|
| 71 |
-
} catch {
|
| 72 |
-
// Fallback for older browsers
|
| 73 |
-
const textArea = document.createElement('textarea');
|
| 74 |
-
textArea.value = selectedMolecule.smiles;
|
| 75 |
-
document.body.appendChild(textArea);
|
| 76 |
-
textArea.select();
|
| 77 |
-
document.execCommand('copy');
|
| 78 |
-
document.body.removeChild(textArea);
|
| 79 |
-
setCopied(true);
|
| 80 |
-
setTimeout(() => setCopied(false), 2000);
|
| 81 |
-
}
|
| 82 |
-
}, [selectedMolecule]);
|
| 83 |
-
|
| 84 |
-
if (error) {
|
| 85 |
-
return (
|
| 86 |
-
<div className="flex h-full items-center justify-center">
|
| 87 |
-
<Card className="max-w-md">
|
| 88 |
-
<CardHeader>
|
| 89 |
-
<CardTitle className="text-destructive">Error</CardTitle>
|
| 90 |
-
<CardDescription>{error}</CardDescription>
|
| 91 |
-
</CardHeader>
|
| 92 |
-
</Card>
|
| 93 |
-
</div>
|
| 94 |
-
);
|
| 95 |
-
}
|
| 96 |
-
|
| 97 |
-
return (
|
| 98 |
-
<div className="flex h-full gap-6 p-6">
|
| 99 |
-
{/* Left Panel - Molecule List */}
|
| 100 |
-
<Card className="w-80 shrink-0">
|
| 101 |
-
<CardHeader className="pb-3">
|
| 102 |
-
<CardTitle className="text-lg">Molecules</CardTitle>
|
| 103 |
-
<CardDescription>Select a molecule to visualize</CardDescription>
|
| 104 |
-
</CardHeader>
|
| 105 |
-
<CardContent className="pb-3">
|
| 106 |
-
<div className="relative">
|
| 107 |
-
<Search className="absolute left-2.5 top-2.5 size-4 text-muted-foreground" />
|
| 108 |
-
<Input
|
| 109 |
-
placeholder="Search molecules..."
|
| 110 |
-
className="pl-8"
|
| 111 |
-
value={searchQuery}
|
| 112 |
-
onChange={(e) => setSearchQuery(e.target.value)}
|
| 113 |
-
/>
|
| 114 |
-
</div>
|
| 115 |
-
</CardContent>
|
| 116 |
-
<Separator />
|
| 117 |
-
<ScrollArea className="h-[calc(100vh-280px)]">
|
| 118 |
-
<div className="p-2">
|
| 119 |
-
{isLoading ? (
|
| 120 |
-
<div className="space-y-2">
|
| 121 |
-
{[1, 2, 3].map((i) => (
|
| 122 |
-
<Skeleton key={i} className="h-16 w-full" />
|
| 123 |
-
))}
|
| 124 |
-
</div>
|
| 125 |
-
) : filteredMolecules.length === 0 ? (
|
| 126 |
-
<p className="p-4 text-center text-sm text-muted-foreground">
|
| 127 |
-
No molecules found
|
| 128 |
-
</p>
|
| 129 |
-
) : (
|
| 130 |
-
<div className="space-y-1">
|
| 131 |
-
{filteredMolecules.map((molecule) => (
|
| 132 |
-
<button
|
| 133 |
-
key={molecule.id}
|
| 134 |
-
onClick={() => setSelectedMolecule(molecule)}
|
| 135 |
-
className={`w-full rounded-lg p-3 text-left transition-colors hover:bg-accent ${
|
| 136 |
-
selectedMolecule?.id === molecule.id
|
| 137 |
-
? 'bg-accent'
|
| 138 |
-
: 'bg-transparent'
|
| 139 |
-
}`}
|
| 140 |
-
>
|
| 141 |
-
<div className="font-medium">{molecule.name}</div>
|
| 142 |
-
<div className="truncate text-xs text-muted-foreground">
|
| 143 |
-
{molecule.smiles}
|
| 144 |
-
</div>
|
| 145 |
-
</button>
|
| 146 |
-
))}
|
| 147 |
-
</div>
|
| 148 |
-
)}
|
| 149 |
-
</div>
|
| 150 |
-
</ScrollArea>
|
| 151 |
-
</Card>
|
| 152 |
-
|
| 153 |
-
{/* Right Panel - Visualization */}
|
| 154 |
-
<div className="flex flex-1 flex-col gap-6">
|
| 155 |
-
{selectedMolecule ? (
|
| 156 |
-
<>
|
| 157 |
-
{/* Molecule Info Card */}
|
| 158 |
-
<Card>
|
| 159 |
-
<CardHeader className="pb-3">
|
| 160 |
-
<div className="flex items-start justify-between">
|
| 161 |
-
<div>
|
| 162 |
-
<CardTitle>{selectedMolecule.name}</CardTitle>
|
| 163 |
-
<CardDescription>
|
| 164 |
-
{selectedMolecule.description}
|
| 165 |
-
</CardDescription>
|
| 166 |
-
</div>
|
| 167 |
-
<Badge variant="outline">
|
| 168 |
-
PubChem: {selectedMolecule.pubchemCid}
|
| 169 |
-
</Badge>
|
| 170 |
-
</div>
|
| 171 |
-
</CardHeader>
|
| 172 |
-
<CardContent>
|
| 173 |
-
<div className="flex items-center gap-2">
|
| 174 |
-
<code className="flex-1 truncate rounded bg-muted px-2 py-1 text-sm">
|
| 175 |
-
{selectedMolecule.smiles}
|
| 176 |
-
</code>
|
| 177 |
-
<Button
|
| 178 |
-
variant="outline"
|
| 179 |
-
size="sm"
|
| 180 |
-
onClick={handleCopySmiles}
|
| 181 |
-
className="shrink-0"
|
| 182 |
-
>
|
| 183 |
-
{copied ? (
|
| 184 |
-
<>
|
| 185 |
-
<Check className="mr-1 size-4" />
|
| 186 |
-
Copied
|
| 187 |
-
</>
|
| 188 |
-
) : (
|
| 189 |
-
<>
|
| 190 |
-
<Copy className="mr-1 size-4" />
|
| 191 |
-
Copy SMILES
|
| 192 |
-
</>
|
| 193 |
-
)}
|
| 194 |
-
</Button>
|
| 195 |
-
</div>
|
| 196 |
-
</CardContent>
|
| 197 |
-
</Card>
|
| 198 |
-
|
| 199 |
-
{/* 2D Structure Card */}
|
| 200 |
-
<Card className="flex-1">
|
| 201 |
-
<CardHeader>
|
| 202 |
-
<CardTitle className="text-lg">2D Structure</CardTitle>
|
| 203 |
-
</CardHeader>
|
| 204 |
-
<CardContent className="flex items-center justify-center">
|
| 205 |
-
<Smiles2DViewer
|
| 206 |
-
smiles={selectedMolecule.smiles}
|
| 207 |
-
width={500}
|
| 208 |
-
height={400}
|
| 209 |
-
/>
|
| 210 |
-
</CardContent>
|
| 211 |
-
</Card>
|
| 212 |
-
</>
|
| 213 |
-
) : (
|
| 214 |
-
<div className="flex flex-1 items-center justify-center">
|
| 215 |
-
<p className="text-muted-foreground">
|
| 216 |
-
Select a molecule to view its structure
|
| 217 |
-
</p>
|
| 218 |
-
</div>
|
| 219 |
-
)}
|
| 220 |
-
</div>
|
| 221 |
-
</div>
|
| 222 |
-
);
|
| 223 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/app/dashboard/molecules-3d/_components/Molecule3DViewer.tsx
DELETED
|
@@ -1,191 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { AlertCircle, RotateCcw } from 'lucide-react';
|
| 4 |
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
| 5 |
-
|
| 6 |
-
import { Button } from '@/components/ui/button';
|
| 7 |
-
import { Card, CardContent } from '@/components/ui/card';
|
| 8 |
-
import {
|
| 9 |
-
Select,
|
| 10 |
-
SelectContent,
|
| 11 |
-
SelectItem,
|
| 12 |
-
SelectTrigger,
|
| 13 |
-
SelectValue,
|
| 14 |
-
} from '@/components/ui/select';
|
| 15 |
-
import { Skeleton } from '@/components/ui/skeleton';
|
| 16 |
-
import type { MoleculeRepresentation } from '@/lib/visualization-types';
|
| 17 |
-
|
| 18 |
-
interface Molecule3DViewerProps {
|
| 19 |
-
sdfData: string;
|
| 20 |
-
width?: number;
|
| 21 |
-
height?: number;
|
| 22 |
-
className?: string;
|
| 23 |
-
initialRepresentation?: MoleculeRepresentation;
|
| 24 |
-
}
|
| 25 |
-
|
| 26 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 27 |
-
type $3Dmol = any;
|
| 28 |
-
|
| 29 |
-
export function Molecule3DViewer({
|
| 30 |
-
sdfData,
|
| 31 |
-
width = 400,
|
| 32 |
-
height = 400,
|
| 33 |
-
className,
|
| 34 |
-
initialRepresentation = 'stick',
|
| 35 |
-
}: Molecule3DViewerProps) {
|
| 36 |
-
const containerRef = useRef<HTMLDivElement>(null);
|
| 37 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 38 |
-
const viewerRef = useRef<any>(null);
|
| 39 |
-
const [error, setError] = useState<string | null>(null);
|
| 40 |
-
const [isLoading, setIsLoading] = useState(true);
|
| 41 |
-
const [representation, setRepresentation] =
|
| 42 |
-
useState<MoleculeRepresentation>(initialRepresentation);
|
| 43 |
-
const [$3Dmol, set$3Dmol] = useState<$3Dmol | null>(null);
|
| 44 |
-
|
| 45 |
-
// Load 3Dmol.js dynamically
|
| 46 |
-
useEffect(() => {
|
| 47 |
-
const load3Dmol = async () => {
|
| 48 |
-
try {
|
| 49 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 50 |
-
const module = await import('3dmol') as any;
|
| 51 |
-
set$3Dmol(module);
|
| 52 |
-
} catch (err) {
|
| 53 |
-
console.error('Failed to load 3Dmol:', err);
|
| 54 |
-
setError('Failed to load 3D visualization library');
|
| 55 |
-
setIsLoading(false);
|
| 56 |
-
}
|
| 57 |
-
};
|
| 58 |
-
load3Dmol();
|
| 59 |
-
}, []);
|
| 60 |
-
|
| 61 |
-
const getStyleForRepresentation = useCallback(
|
| 62 |
-
(rep: MoleculeRepresentation) => {
|
| 63 |
-
switch (rep) {
|
| 64 |
-
case 'stick':
|
| 65 |
-
return { stick: { radius: 0.15 } };
|
| 66 |
-
case 'sphere':
|
| 67 |
-
return { sphere: { scale: 0.3 } };
|
| 68 |
-
case 'line':
|
| 69 |
-
return { line: { linewidth: 2 } };
|
| 70 |
-
case 'cartoon':
|
| 71 |
-
return { cartoon: {} };
|
| 72 |
-
default:
|
| 73 |
-
return { stick: { radius: 0.15 } };
|
| 74 |
-
}
|
| 75 |
-
},
|
| 76 |
-
[]
|
| 77 |
-
);
|
| 78 |
-
|
| 79 |
-
const initViewer = useCallback(() => {
|
| 80 |
-
if (!containerRef.current || !$3Dmol || !sdfData) return;
|
| 81 |
-
|
| 82 |
-
setIsLoading(true);
|
| 83 |
-
setError(null);
|
| 84 |
-
|
| 85 |
-
try {
|
| 86 |
-
// Clean up existing viewer
|
| 87 |
-
if (viewerRef.current) {
|
| 88 |
-
viewerRef.current.removeAllModels();
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
// Create viewer - use 'white' to avoid crash on some systems
|
| 92 |
-
const viewer = $3Dmol.createViewer(containerRef.current, {
|
| 93 |
-
backgroundColor: 'white',
|
| 94 |
-
});
|
| 95 |
-
viewerRef.current = viewer;
|
| 96 |
-
|
| 97 |
-
// Add molecule
|
| 98 |
-
viewer.addModel(sdfData, 'sdf');
|
| 99 |
-
viewer.setStyle({}, getStyleForRepresentation(representation));
|
| 100 |
-
viewer.zoomTo();
|
| 101 |
-
viewer.render();
|
| 102 |
-
|
| 103 |
-
setIsLoading(false);
|
| 104 |
-
} catch (err) {
|
| 105 |
-
console.error('3D viewer error:', err);
|
| 106 |
-
setError(
|
| 107 |
-
err instanceof Error
|
| 108 |
-
? `Visualization error: ${err.message}`
|
| 109 |
-
: 'Failed to render 3D structure'
|
| 110 |
-
);
|
| 111 |
-
setIsLoading(false);
|
| 112 |
-
}
|
| 113 |
-
}, [$3Dmol, sdfData, representation, getStyleForRepresentation]);
|
| 114 |
-
|
| 115 |
-
useEffect(() => {
|
| 116 |
-
initViewer();
|
| 117 |
-
}, [$3Dmol, sdfData, initViewer]);
|
| 118 |
-
|
| 119 |
-
// Update representation
|
| 120 |
-
useEffect(() => {
|
| 121 |
-
if (!viewerRef.current) return;
|
| 122 |
-
try {
|
| 123 |
-
viewerRef.current.setStyle({}, getStyleForRepresentation(representation));
|
| 124 |
-
viewerRef.current.render();
|
| 125 |
-
} catch (err) {
|
| 126 |
-
console.error('Style update error:', err);
|
| 127 |
-
}
|
| 128 |
-
}, [representation, getStyleForRepresentation]);
|
| 129 |
-
|
| 130 |
-
const handleResetCamera = useCallback(() => {
|
| 131 |
-
if (!viewerRef.current) return;
|
| 132 |
-
viewerRef.current.zoomTo();
|
| 133 |
-
viewerRef.current.render();
|
| 134 |
-
}, []);
|
| 135 |
-
|
| 136 |
-
if (error) {
|
| 137 |
-
return (
|
| 138 |
-
<Card className={`border-destructive bg-destructive/10 ${className}`}>
|
| 139 |
-
<CardContent className="flex items-center gap-3 p-4">
|
| 140 |
-
<AlertCircle className="size-5 text-destructive" />
|
| 141 |
-
<div className="flex flex-col">
|
| 142 |
-
<span className="text-sm font-medium text-destructive">
|
| 143 |
-
3D Visualization Error
|
| 144 |
-
</span>
|
| 145 |
-
<span className="text-xs text-muted-foreground">{error}</span>
|
| 146 |
-
</div>
|
| 147 |
-
</CardContent>
|
| 148 |
-
</Card>
|
| 149 |
-
);
|
| 150 |
-
}
|
| 151 |
-
|
| 152 |
-
return (
|
| 153 |
-
<div className={`flex flex-col gap-3 ${className}`}>
|
| 154 |
-
<div className="flex items-center gap-2">
|
| 155 |
-
<Select
|
| 156 |
-
value={representation}
|
| 157 |
-
onValueChange={(value) =>
|
| 158 |
-
setRepresentation(value as MoleculeRepresentation)
|
| 159 |
-
}
|
| 160 |
-
>
|
| 161 |
-
<SelectTrigger className="w-32">
|
| 162 |
-
<SelectValue placeholder="Style" />
|
| 163 |
-
</SelectTrigger>
|
| 164 |
-
<SelectContent>
|
| 165 |
-
<SelectItem value="stick">Stick</SelectItem>
|
| 166 |
-
<SelectItem value="sphere">Sphere</SelectItem>
|
| 167 |
-
<SelectItem value="line">Line</SelectItem>
|
| 168 |
-
</SelectContent>
|
| 169 |
-
</Select>
|
| 170 |
-
<Button variant="outline" size="sm" onClick={handleResetCamera}>
|
| 171 |
-
<RotateCcw className="mr-1 size-4" />
|
| 172 |
-
Reset
|
| 173 |
-
</Button>
|
| 174 |
-
</div>
|
| 175 |
-
|
| 176 |
-
<div className="relative">
|
| 177 |
-
{isLoading && (
|
| 178 |
-
<Skeleton
|
| 179 |
-
className="absolute inset-0 z-10"
|
| 180 |
-
style={{ width, height }}
|
| 181 |
-
/>
|
| 182 |
-
)}
|
| 183 |
-
<div
|
| 184 |
-
ref={containerRef}
|
| 185 |
-
style={{ width, height }}
|
| 186 |
-
className={`rounded-lg border bg-background ${isLoading ? 'opacity-0' : 'opacity-100'} transition-opacity duration-200`}
|
| 187 |
-
/>
|
| 188 |
-
</div>
|
| 189 |
-
</div>
|
| 190 |
-
);
|
| 191 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/app/dashboard/molecules-3d/page.tsx
DELETED
|
@@ -1,264 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { Check, Copy, Search } from 'lucide-react';
|
| 4 |
-
import dynamic from 'next/dynamic';
|
| 5 |
-
import { useCallback, useEffect, useMemo, useState } from 'react';
|
| 6 |
-
|
| 7 |
-
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
| 8 |
-
import { Badge } from '@/components/ui/badge';
|
| 9 |
-
import { Button } from '@/components/ui/button';
|
| 10 |
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
| 11 |
-
import { Input } from '@/components/ui/input';
|
| 12 |
-
import { ScrollArea } from '@/components/ui/scroll-area';
|
| 13 |
-
import { Separator } from '@/components/ui/separator';
|
| 14 |
-
import { Skeleton } from '@/components/ui/skeleton';
|
| 15 |
-
import { getMolecules, getMoleculeSDF } from '@/lib/visualization-api';
|
| 16 |
-
import type { Molecule } from '@/lib/visualization-types';
|
| 17 |
-
|
| 18 |
-
// Dynamic import of the 3D viewer to prevent SSR issues
|
| 19 |
-
const Molecule3DViewer = dynamic(
|
| 20 |
-
() =>
|
| 21 |
-
import('./_components/Molecule3DViewer').then((mod) => mod.Molecule3DViewer),
|
| 22 |
-
{
|
| 23 |
-
ssr: false,
|
| 24 |
-
loading: () => <Skeleton className="size-[400px]" />,
|
| 25 |
-
}
|
| 26 |
-
);
|
| 27 |
-
|
| 28 |
-
export default function Molecules3DPage() {
|
| 29 |
-
const [molecules, setMolecules] = useState<Molecule[]>([]);
|
| 30 |
-
const [selectedMolecule, setSelectedMolecule] = useState<Molecule | null>(null);
|
| 31 |
-
const [sdfData, setSdfData] = useState<string | null>(null);
|
| 32 |
-
const [searchQuery, setSearchQuery] = useState('');
|
| 33 |
-
const [isLoading, setIsLoading] = useState(true);
|
| 34 |
-
const [isSdfLoading, setIsSdfLoading] = useState(false);
|
| 35 |
-
const [error, setError] = useState<string | null>(null);
|
| 36 |
-
const [sdfError, setSdfError] = useState<string | null>(null);
|
| 37 |
-
const [copied, setCopied] = useState(false);
|
| 38 |
-
|
| 39 |
-
// Load molecules list
|
| 40 |
-
useEffect(() => {
|
| 41 |
-
const loadMolecules = async () => {
|
| 42 |
-
try {
|
| 43 |
-
setIsLoading(true);
|
| 44 |
-
const data = await getMolecules();
|
| 45 |
-
setMolecules(data);
|
| 46 |
-
if (data.length > 0) {
|
| 47 |
-
setSelectedMolecule(data[0]);
|
| 48 |
-
}
|
| 49 |
-
} catch (err) {
|
| 50 |
-
setError(
|
| 51 |
-
err instanceof Error ? err.message : 'Failed to load molecules'
|
| 52 |
-
);
|
| 53 |
-
} finally {
|
| 54 |
-
setIsLoading(false);
|
| 55 |
-
}
|
| 56 |
-
};
|
| 57 |
-
loadMolecules();
|
| 58 |
-
}, []);
|
| 59 |
-
|
| 60 |
-
// Load SDF when molecule changes
|
| 61 |
-
useEffect(() => {
|
| 62 |
-
if (!selectedMolecule) {
|
| 63 |
-
setSdfData(null);
|
| 64 |
-
return;
|
| 65 |
-
}
|
| 66 |
-
|
| 67 |
-
const loadSdf = async () => {
|
| 68 |
-
try {
|
| 69 |
-
setIsSdfLoading(true);
|
| 70 |
-
setSdfError(null);
|
| 71 |
-
const data = await getMoleculeSDF(selectedMolecule.id);
|
| 72 |
-
setSdfData(data);
|
| 73 |
-
} catch (err) {
|
| 74 |
-
setSdfError(
|
| 75 |
-
err instanceof Error ? err.message : 'Failed to load 3D structure'
|
| 76 |
-
);
|
| 77 |
-
setSdfData(null);
|
| 78 |
-
} finally {
|
| 79 |
-
setIsSdfLoading(false);
|
| 80 |
-
}
|
| 81 |
-
};
|
| 82 |
-
loadSdf();
|
| 83 |
-
}, [selectedMolecule]);
|
| 84 |
-
|
| 85 |
-
const filteredMolecules = useMemo(() => {
|
| 86 |
-
if (!searchQuery.trim()) return molecules;
|
| 87 |
-
const query = searchQuery.toLowerCase();
|
| 88 |
-
return molecules.filter(
|
| 89 |
-
(m) =>
|
| 90 |
-
m.name.toLowerCase().includes(query) ||
|
| 91 |
-
m.smiles.toLowerCase().includes(query)
|
| 92 |
-
);
|
| 93 |
-
}, [molecules, searchQuery]);
|
| 94 |
-
|
| 95 |
-
const handleCopySmiles = useCallback(async () => {
|
| 96 |
-
if (!selectedMolecule) return;
|
| 97 |
-
try {
|
| 98 |
-
await navigator.clipboard.writeText(selectedMolecule.smiles);
|
| 99 |
-
setCopied(true);
|
| 100 |
-
setTimeout(() => setCopied(false), 2000);
|
| 101 |
-
} catch {
|
| 102 |
-
const textArea = document.createElement('textarea');
|
| 103 |
-
textArea.value = selectedMolecule.smiles;
|
| 104 |
-
document.body.appendChild(textArea);
|
| 105 |
-
textArea.select();
|
| 106 |
-
document.execCommand('copy');
|
| 107 |
-
document.body.removeChild(textArea);
|
| 108 |
-
setCopied(true);
|
| 109 |
-
setTimeout(() => setCopied(false), 2000);
|
| 110 |
-
}
|
| 111 |
-
}, [selectedMolecule]);
|
| 112 |
-
|
| 113 |
-
if (error) {
|
| 114 |
-
return (
|
| 115 |
-
<div className="flex h-full items-center justify-center">
|
| 116 |
-
<Card className="max-w-md">
|
| 117 |
-
<CardHeader>
|
| 118 |
-
<CardTitle className="text-destructive">Error</CardTitle>
|
| 119 |
-
<CardDescription>{error}</CardDescription>
|
| 120 |
-
</CardHeader>
|
| 121 |
-
</Card>
|
| 122 |
-
</div>
|
| 123 |
-
);
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
return (
|
| 127 |
-
<div className="flex h-full gap-6 p-6">
|
| 128 |
-
{/* Left Panel - Molecule List */}
|
| 129 |
-
<Card className="w-80 shrink-0">
|
| 130 |
-
<CardHeader className="pb-3">
|
| 131 |
-
<CardTitle className="text-lg">Molecules</CardTitle>
|
| 132 |
-
<CardDescription>Select a molecule for 3D view</CardDescription>
|
| 133 |
-
</CardHeader>
|
| 134 |
-
<CardContent className="pb-3">
|
| 135 |
-
<div className="relative">
|
| 136 |
-
<Search className="absolute left-2.5 top-2.5 size-4 text-muted-foreground" />
|
| 137 |
-
<Input
|
| 138 |
-
placeholder="Search molecules..."
|
| 139 |
-
className="pl-8"
|
| 140 |
-
value={searchQuery}
|
| 141 |
-
onChange={(e) => setSearchQuery(e.target.value)}
|
| 142 |
-
/>
|
| 143 |
-
</div>
|
| 144 |
-
</CardContent>
|
| 145 |
-
<Separator />
|
| 146 |
-
<ScrollArea className="h-[calc(100vh-280px)]">
|
| 147 |
-
<div className="p-2">
|
| 148 |
-
{isLoading ? (
|
| 149 |
-
<div className="space-y-2">
|
| 150 |
-
{[1, 2, 3].map((i) => (
|
| 151 |
-
<Skeleton key={i} className="h-16 w-full" />
|
| 152 |
-
))}
|
| 153 |
-
</div>
|
| 154 |
-
) : filteredMolecules.length === 0 ? (
|
| 155 |
-
<p className="p-4 text-center text-sm text-muted-foreground">
|
| 156 |
-
No molecules found
|
| 157 |
-
</p>
|
| 158 |
-
) : (
|
| 159 |
-
<div className="space-y-1">
|
| 160 |
-
{filteredMolecules.map((molecule) => (
|
| 161 |
-
<button
|
| 162 |
-
key={molecule.id}
|
| 163 |
-
onClick={() => setSelectedMolecule(molecule)}
|
| 164 |
-
className={`w-full rounded-lg p-3 text-left transition-colors hover:bg-accent ${
|
| 165 |
-
selectedMolecule?.id === molecule.id
|
| 166 |
-
? 'bg-accent'
|
| 167 |
-
: 'bg-transparent'
|
| 168 |
-
}`}
|
| 169 |
-
>
|
| 170 |
-
<div className="font-medium">{molecule.name}</div>
|
| 171 |
-
<div className="truncate text-xs text-muted-foreground">
|
| 172 |
-
{molecule.smiles}
|
| 173 |
-
</div>
|
| 174 |
-
</button>
|
| 175 |
-
))}
|
| 176 |
-
</div>
|
| 177 |
-
)}
|
| 178 |
-
</div>
|
| 179 |
-
</ScrollArea>
|
| 180 |
-
</Card>
|
| 181 |
-
|
| 182 |
-
{/* Right Panel - 3D Visualization */}
|
| 183 |
-
<div className="flex flex-1 flex-col gap-6">
|
| 184 |
-
{selectedMolecule ? (
|
| 185 |
-
<>
|
| 186 |
-
{/* Molecule Info Card */}
|
| 187 |
-
<Card>
|
| 188 |
-
<CardHeader className="pb-3">
|
| 189 |
-
<div className="flex items-start justify-between">
|
| 190 |
-
<div>
|
| 191 |
-
<CardTitle>{selectedMolecule.name}</CardTitle>
|
| 192 |
-
<CardDescription>
|
| 193 |
-
{selectedMolecule.description}
|
| 194 |
-
</CardDescription>
|
| 195 |
-
</div>
|
| 196 |
-
<Badge variant="outline">
|
| 197 |
-
PubChem: {selectedMolecule.pubchemCid}
|
| 198 |
-
</Badge>
|
| 199 |
-
</div>
|
| 200 |
-
</CardHeader>
|
| 201 |
-
<CardContent>
|
| 202 |
-
<div className="flex items-center gap-2">
|
| 203 |
-
<code className="flex-1 truncate rounded bg-muted px-2 py-1 text-sm">
|
| 204 |
-
{selectedMolecule.smiles}
|
| 205 |
-
</code>
|
| 206 |
-
<Button
|
| 207 |
-
variant="outline"
|
| 208 |
-
size="sm"
|
| 209 |
-
onClick={handleCopySmiles}
|
| 210 |
-
className="shrink-0"
|
| 211 |
-
>
|
| 212 |
-
{copied ? (
|
| 213 |
-
<>
|
| 214 |
-
<Check className="mr-1 size-4" />
|
| 215 |
-
Copied
|
| 216 |
-
</>
|
| 217 |
-
) : (
|
| 218 |
-
<>
|
| 219 |
-
<Copy className="mr-1 size-4" />
|
| 220 |
-
Copy SMILES
|
| 221 |
-
</>
|
| 222 |
-
)}
|
| 223 |
-
</Button>
|
| 224 |
-
</div>
|
| 225 |
-
</CardContent>
|
| 226 |
-
</Card>
|
| 227 |
-
|
| 228 |
-
{/* 3D Structure Card */}
|
| 229 |
-
<Card className="flex-1">
|
| 230 |
-
<CardHeader>
|
| 231 |
-
<CardTitle className="text-lg">3D Structure</CardTitle>
|
| 232 |
-
<CardDescription>
|
| 233 |
-
Rotate: click + drag • Zoom: scroll • Pan: right-click + drag
|
| 234 |
-
</CardDescription>
|
| 235 |
-
</CardHeader>
|
| 236 |
-
<CardContent className="flex items-center justify-center">
|
| 237 |
-
{isSdfLoading ? (
|
| 238 |
-
<Skeleton className="size-[400px]" />
|
| 239 |
-
) : sdfError ? (
|
| 240 |
-
<Alert variant="destructive" className="max-w-md">
|
| 241 |
-
<AlertTitle>Failed to load 3D structure</AlertTitle>
|
| 242 |
-
<AlertDescription>{sdfError}</AlertDescription>
|
| 243 |
-
</Alert>
|
| 244 |
-
) : sdfData ? (
|
| 245 |
-
<Molecule3DViewer sdfData={sdfData} width={500} height={400} />
|
| 246 |
-
) : (
|
| 247 |
-
<p className="text-muted-foreground">
|
| 248 |
-
No 3D structure available
|
| 249 |
-
</p>
|
| 250 |
-
)}
|
| 251 |
-
</CardContent>
|
| 252 |
-
</Card>
|
| 253 |
-
</>
|
| 254 |
-
) : (
|
| 255 |
-
<div className="flex flex-1 items-center justify-center">
|
| 256 |
-
<p className="text-muted-foreground">
|
| 257 |
-
Select a molecule to view its 3D structure
|
| 258 |
-
</p>
|
| 259 |
-
</div>
|
| 260 |
-
)}
|
| 261 |
-
</div>
|
| 262 |
-
</div>
|
| 263 |
-
);
|
| 264 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/app/dashboard/page.tsx
DELETED
|
@@ -1,170 +0,0 @@
|
|
| 1 |
-
"use client"
|
| 2 |
-
|
| 3 |
-
import { ArrowUp, Database, FileText, Search, Sparkles, Zap } from "lucide-react"
|
| 4 |
-
import Link from "next/link"
|
| 5 |
-
|
| 6 |
-
import { SectionHeader } from "@/components/page-header"
|
| 7 |
-
import { Badge } from "@/components/ui/badge"
|
| 8 |
-
import { Button } from "@/components/ui/button"
|
| 9 |
-
import { Card, CardContent } from "@/components/ui/card"
|
| 10 |
-
import { Separator } from "@/components/ui/separator"
|
| 11 |
-
|
| 12 |
-
export default function DashboardHome() {
|
| 13 |
-
return (
|
| 14 |
-
<div className="space-y-8 animate-in fade-in duration-500">
|
| 15 |
-
{/* Hero Section */}
|
| 16 |
-
<div className="flex flex-col lg:flex-row gap-6">
|
| 17 |
-
<div className="flex-1 rounded-2xl bg-gradient-to-br from-primary/10 via-background to-background p-8 border">
|
| 18 |
-
<Badge variant="secondary" className="mb-4">New • BioFlow 2.0</Badge>
|
| 19 |
-
<h1 className="text-4xl font-bold tracking-tight mb-4">AI-Powered Drug Discovery</h1>
|
| 20 |
-
<p className="text-lg text-muted-foreground mb-6 max-w-xl">
|
| 21 |
-
Run discovery pipelines, predict binding, and surface evidence in one streamlined workspace.
|
| 22 |
-
</p>
|
| 23 |
-
<div className="flex gap-2 mb-6">
|
| 24 |
-
<Badge variant="outline" className="bg-primary/5 border-primary/20 text-primary">Model-aware search</Badge>
|
| 25 |
-
<Badge variant="outline" className="bg-green-500/10 border-green-500/20 text-green-700 dark:text-green-400">Evidence-linked</Badge>
|
| 26 |
-
<Badge variant="outline" className="bg-amber-500/10 border-amber-500/20 text-amber-700 dark:text-amber-400">Fast iteration</Badge>
|
| 27 |
-
</div>
|
| 28 |
-
|
| 29 |
-
<div className="flex gap-4">
|
| 30 |
-
<Link href="/dashboard/discovery">
|
| 31 |
-
<Button size="lg" className="font-semibold">
|
| 32 |
-
Start Discovery
|
| 33 |
-
</Button>
|
| 34 |
-
</Link>
|
| 35 |
-
<Link href="/dashboard/explorer">
|
| 36 |
-
<Button size="lg" variant="outline">
|
| 37 |
-
Explore Data
|
| 38 |
-
</Button>
|
| 39 |
-
</Link>
|
| 40 |
-
</div>
|
| 41 |
-
</div>
|
| 42 |
-
|
| 43 |
-
<div className="lg:w-[350px]">
|
| 44 |
-
<Card className="h-full">
|
| 45 |
-
<CardContent className="p-6 flex flex-col justify-between h-full">
|
| 46 |
-
<div>
|
| 47 |
-
<div className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-2">Today</div>
|
| 48 |
-
<div className="text-4xl font-bold mb-2">156 Discoveries</div>
|
| 49 |
-
<div className="text-sm text-green-600 font-medium flex items-center gap-1">
|
| 50 |
-
<ArrowUp className="h-4 w-4" />
|
| 51 |
-
+12% vs last week
|
| 52 |
-
</div>
|
| 53 |
-
</div>
|
| 54 |
-
|
| 55 |
-
<Separator className="my-4" />
|
| 56 |
-
|
| 57 |
-
<div className="space-y-2">
|
| 58 |
-
<div className="flex items-center justify-between text-sm">
|
| 59 |
-
<span className="flex items-center gap-2">
|
| 60 |
-
<span className="h-2 w-2 rounded-full bg-primary"></span>
|
| 61 |
-
Discovery
|
| 62 |
-
</span>
|
| 63 |
-
<span className="font-mono font-medium">64</span>
|
| 64 |
-
</div>
|
| 65 |
-
<div className="flex items-center justify-between text-sm">
|
| 66 |
-
<span className="flex items-center gap-2">
|
| 67 |
-
<span className="h-2 w-2 rounded-full bg-green-500"></span>
|
| 68 |
-
Prediction
|
| 69 |
-
</span>
|
| 70 |
-
<span className="font-mono font-medium">42</span>
|
| 71 |
-
</div>
|
| 72 |
-
<div className="flex items-center justify-between text-sm">
|
| 73 |
-
<span className="flex items-center gap-2">
|
| 74 |
-
<span className="h-2 w-2 rounded-full bg-amber-500"></span>
|
| 75 |
-
Evidence
|
| 76 |
-
</span>
|
| 77 |
-
<span className="font-mono font-medium">50</span>
|
| 78 |
-
</div>
|
| 79 |
-
</div>
|
| 80 |
-
</CardContent>
|
| 81 |
-
</Card>
|
| 82 |
-
</div>
|
| 83 |
-
</div>
|
| 84 |
-
|
| 85 |
-
{/* Metrics Row */}
|
| 86 |
-
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
| 87 |
-
{[
|
| 88 |
-
{ label: "Molecules", value: "12.5M", icon: "🧪", change: "+2.3%", color: "text-blue-500" },
|
| 89 |
-
{ label: "Proteins", value: "847K", icon: "🧬", change: "+1.8%", color: "text-cyan-500" },
|
| 90 |
-
{ label: "Papers", value: "1.2M", icon: "📚", change: "+5.2%", color: "text-emerald-500" },
|
| 91 |
-
{ label: "Discoveries", value: "156", icon: "✨", change: "+12%", color: "text-amber-500" }
|
| 92 |
-
].map((metric, i) => (
|
| 93 |
-
<Card key={i}>
|
| 94 |
-
<CardContent className="p-6">
|
| 95 |
-
<div className="flex justify-between items-start mb-2">
|
| 96 |
-
<div className="text-sm font-medium text-muted-foreground">{metric.label}</div>
|
| 97 |
-
<div className="text-lg">{metric.icon}</div>
|
| 98 |
-
</div>
|
| 99 |
-
<div className="text-2xl font-bold mb-1">{metric.value}</div>
|
| 100 |
-
<div className="text-xs font-medium flex items-center gap-1 text-green-500">
|
| 101 |
-
<ArrowUp className="h-3 w-3" />
|
| 102 |
-
{metric.change}
|
| 103 |
-
</div>
|
| 104 |
-
</CardContent>
|
| 105 |
-
</Card>
|
| 106 |
-
))}
|
| 107 |
-
</div>
|
| 108 |
-
|
| 109 |
-
{/* Quick Actions */}
|
| 110 |
-
<div className="pt-4">
|
| 111 |
-
<SectionHeader title="Quick Actions" icon={<Zap className="h-5 w-5 text-amber-500" />} />
|
| 112 |
-
|
| 113 |
-
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
| 114 |
-
<Link href="/dashboard/discovery" className="block">
|
| 115 |
-
<Card className="hover:bg-accent/50 transition-colors cursor-pointer h-full">
|
| 116 |
-
<CardContent className="p-6 flex flex-col items-center text-center gap-3">
|
| 117 |
-
<div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-primary">
|
| 118 |
-
<Search className="h-5 w-5" />
|
| 119 |
-
</div>
|
| 120 |
-
<div>
|
| 121 |
-
<div className="font-semibold">New Discovery</div>
|
| 122 |
-
<div className="text-sm text-muted-foreground">Start a pipeline</div>
|
| 123 |
-
</div>
|
| 124 |
-
</CardContent>
|
| 125 |
-
</Card>
|
| 126 |
-
</Link>
|
| 127 |
-
<Link href="/dashboard/explorer" className="block">
|
| 128 |
-
<Card className="hover:bg-accent/50 transition-colors cursor-pointer h-full">
|
| 129 |
-
<CardContent className="p-6 flex flex-col items-center text-center gap-3">
|
| 130 |
-
<div className="h-10 w-10 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-500">
|
| 131 |
-
<Database className="h-5 w-5" />
|
| 132 |
-
</div>
|
| 133 |
-
<div>
|
| 134 |
-
<div className="font-semibold">Browse Data</div>
|
| 135 |
-
<div className="text-sm text-muted-foreground">Explore datasets</div>
|
| 136 |
-
</div>
|
| 137 |
-
</CardContent>
|
| 138 |
-
</Card>
|
| 139 |
-
</Link>
|
| 140 |
-
<Link href="/dashboard/data" className="block">
|
| 141 |
-
<Card className="hover:bg-accent/50 transition-colors cursor-pointer h-full">
|
| 142 |
-
<CardContent className="p-6 flex flex-col items-center text-center gap-3">
|
| 143 |
-
<div className="h-10 w-10 rounded-full bg-purple-500/10 flex items-center justify-center text-purple-500">
|
| 144 |
-
<FileText className="h-5 w-5" />
|
| 145 |
-
</div>
|
| 146 |
-
<div>
|
| 147 |
-
<div className="font-semibold">Training</div>
|
| 148 |
-
<div className="text-sm text-muted-foreground">Train new models</div>
|
| 149 |
-
</div>
|
| 150 |
-
</CardContent>
|
| 151 |
-
</Card>
|
| 152 |
-
</Link>
|
| 153 |
-
<Link href="/dashboard/settings" className="block">
|
| 154 |
-
<Card className="hover:bg-accent/50 transition-colors cursor-pointer h-full">
|
| 155 |
-
<CardContent className="p-6 flex flex-col items-center text-center gap-3">
|
| 156 |
-
<div className="h-10 w-10 rounded-full bg-slate-500/10 flex items-center justify-center text-slate-500">
|
| 157 |
-
<Sparkles className="h-5 w-5" />
|
| 158 |
-
</div>
|
| 159 |
-
<div>
|
| 160 |
-
<div className="font-semibold">View Insights</div>
|
| 161 |
-
<div className="text-sm text-muted-foreground">Check predictions</div>
|
| 162 |
-
</div>
|
| 163 |
-
</CardContent>
|
| 164 |
-
</Card>
|
| 165 |
-
</Link>
|
| 166 |
-
</div>
|
| 167 |
-
</div>
|
| 168 |
-
</div>
|
| 169 |
-
)
|
| 170 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/app/dashboard/proteins-3d/_components/ProteinViewer.tsx
DELETED
|
@@ -1,243 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { AlertCircle, RotateCcw } from 'lucide-react';
|
| 4 |
-
import { useCallback, useEffect, useRef, useState } from 'react';
|
| 5 |
-
|
| 6 |
-
import { Button } from '@/components/ui/button';
|
| 7 |
-
import { Card, CardContent } from '@/components/ui/card';
|
| 8 |
-
import {
|
| 9 |
-
Select,
|
| 10 |
-
SelectContent,
|
| 11 |
-
SelectItem,
|
| 12 |
-
SelectTrigger,
|
| 13 |
-
SelectValue,
|
| 14 |
-
} from '@/components/ui/select';
|
| 15 |
-
import { Skeleton } from '@/components/ui/skeleton';
|
| 16 |
-
import { getProteinPdbUrl } from '@/lib/visualization-api';
|
| 17 |
-
import type { ProteinRepresentation } from '@/lib/visualization-types';
|
| 18 |
-
|
| 19 |
-
interface ProteinViewerProps {
|
| 20 |
-
pdbId: string;
|
| 21 |
-
width?: number;
|
| 22 |
-
height?: number;
|
| 23 |
-
className?: string;
|
| 24 |
-
initialRepresentation?: ProteinRepresentation;
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 28 |
-
type $3Dmol = any;
|
| 29 |
-
|
| 30 |
-
export function ProteinViewer({
|
| 31 |
-
pdbId,
|
| 32 |
-
width = 500,
|
| 33 |
-
height = 500,
|
| 34 |
-
className,
|
| 35 |
-
initialRepresentation = 'cartoon',
|
| 36 |
-
}: ProteinViewerProps) {
|
| 37 |
-
const containerRef = useRef<HTMLDivElement>(null);
|
| 38 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 39 |
-
const viewerRef = useRef<any>(null);
|
| 40 |
-
const [error, setError] = useState<string | null>(null);
|
| 41 |
-
const [isLoading, setIsLoading] = useState(true);
|
| 42 |
-
const [representation, setRepresentation] =
|
| 43 |
-
useState<ProteinRepresentation>(initialRepresentation);
|
| 44 |
-
const [$3Dmol, set$3Dmol] = useState<$3Dmol | null>(null);
|
| 45 |
-
const [pdbData, setPdbData] = useState<string | null>(null);
|
| 46 |
-
|
| 47 |
-
// Load 3Dmol.js dynamically
|
| 48 |
-
useEffect(() => {
|
| 49 |
-
const load3Dmol = async () => {
|
| 50 |
-
try {
|
| 51 |
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 52 |
-
const module = await import('3dmol') as any;
|
| 53 |
-
set$3Dmol(module);
|
| 54 |
-
} catch (err) {
|
| 55 |
-
console.error('Failed to load 3Dmol:', err);
|
| 56 |
-
setError('Failed to load 3D visualization library');
|
| 57 |
-
setIsLoading(false);
|
| 58 |
-
}
|
| 59 |
-
};
|
| 60 |
-
load3Dmol();
|
| 61 |
-
}, []);
|
| 62 |
-
|
| 63 |
-
// Fetch PDB data when pdbId changes
|
| 64 |
-
useEffect(() => {
|
| 65 |
-
if (!pdbId) return;
|
| 66 |
-
|
| 67 |
-
const fetchPdb = async () => {
|
| 68 |
-
try {
|
| 69 |
-
setIsLoading(true);
|
| 70 |
-
setError(null);
|
| 71 |
-
const pdbUrl = getProteinPdbUrl(pdbId);
|
| 72 |
-
const response = await fetch(pdbUrl);
|
| 73 |
-
|
| 74 |
-
if (!response.ok) {
|
| 75 |
-
throw new Error(`Failed to fetch PDB: ${response.statusText}`);
|
| 76 |
-
}
|
| 77 |
-
|
| 78 |
-
const data = await response.text();
|
| 79 |
-
setPdbData(data);
|
| 80 |
-
} catch (err) {
|
| 81 |
-
console.error('Failed to fetch PDB:', err);
|
| 82 |
-
setError(
|
| 83 |
-
err instanceof Error ? err.message : 'Failed to load PDB data'
|
| 84 |
-
);
|
| 85 |
-
setIsLoading(false);
|
| 86 |
-
}
|
| 87 |
-
};
|
| 88 |
-
fetchPdb();
|
| 89 |
-
}, [pdbId]);
|
| 90 |
-
|
| 91 |
-
const getStyleForRepresentation = useCallback(
|
| 92 |
-
(rep: ProteinRepresentation) => {
|
| 93 |
-
switch (rep) {
|
| 94 |
-
case 'cartoon':
|
| 95 |
-
return { cartoon: { color: 'spectrum' } };
|
| 96 |
-
case 'ball-and-stick':
|
| 97 |
-
return { stick: { radius: 0.15 }, sphere: { scale: 0.25 } };
|
| 98 |
-
case 'surface':
|
| 99 |
-
return { surface: {} }; // Surface handled via addSurface
|
| 100 |
-
case 'ribbon':
|
| 101 |
-
return { cartoon: { style: 'trace', color: 'spectrum' } };
|
| 102 |
-
default:
|
| 103 |
-
return { cartoon: { color: 'spectrum' } };
|
| 104 |
-
}
|
| 105 |
-
},
|
| 106 |
-
[]
|
| 107 |
-
);
|
| 108 |
-
|
| 109 |
-
const initViewer = useCallback(() => {
|
| 110 |
-
if (!containerRef.current || !$3Dmol || !pdbData) return;
|
| 111 |
-
|
| 112 |
-
setIsLoading(true);
|
| 113 |
-
setError(null);
|
| 114 |
-
|
| 115 |
-
try {
|
| 116 |
-
// Clean up existing viewer
|
| 117 |
-
if (viewerRef.current) {
|
| 118 |
-
viewerRef.current.removeAllModels();
|
| 119 |
-
}
|
| 120 |
-
|
| 121 |
-
// Create viewer
|
| 122 |
-
const viewer = $3Dmol.createViewer(containerRef.current, {
|
| 123 |
-
backgroundColor: '#0a0a0a',
|
| 124 |
-
});
|
| 125 |
-
viewerRef.current = viewer;
|
| 126 |
-
|
| 127 |
-
// Add protein structure
|
| 128 |
-
viewer.addModel(pdbData, 'pdb');
|
| 129 |
-
|
| 130 |
-
if (representation === 'surface') {
|
| 131 |
-
viewer.setStyle({}, { cartoon: { color: 'spectrum' } }); // Base style
|
| 132 |
-
viewer.addSurface($3Dmol.SurfaceType.VDW, {
|
| 133 |
-
opacity: 0.85,
|
| 134 |
-
color: 'spectrum',
|
| 135 |
-
}, {}, {});
|
| 136 |
-
} else {
|
| 137 |
-
viewer.setStyle({}, getStyleForRepresentation(representation));
|
| 138 |
-
}
|
| 139 |
-
|
| 140 |
-
viewer.zoomTo();
|
| 141 |
-
viewer.render();
|
| 142 |
-
|
| 143 |
-
setIsLoading(false);
|
| 144 |
-
} catch (err) {
|
| 145 |
-
console.error('3D viewer error:', err);
|
| 146 |
-
setError(
|
| 147 |
-
err instanceof Error
|
| 148 |
-
? `Visualization error: ${err.message}`
|
| 149 |
-
: 'Failed to render 3D structure'
|
| 150 |
-
);
|
| 151 |
-
setIsLoading(false);
|
| 152 |
-
}
|
| 153 |
-
}, [$3Dmol, pdbData, representation, getStyleForRepresentation]);
|
| 154 |
-
|
| 155 |
-
useEffect(() => {
|
| 156 |
-
initViewer();
|
| 157 |
-
}, [$3Dmol, pdbData, initViewer]);
|
| 158 |
-
|
| 159 |
-
// Update representation
|
| 160 |
-
useEffect(() => {
|
| 161 |
-
if (!viewerRef.current || !pdbData || !$3Dmol) return;
|
| 162 |
-
try {
|
| 163 |
-
viewerRef.current.removeAllSurfaces();
|
| 164 |
-
|
| 165 |
-
if (representation === 'surface') {
|
| 166 |
-
viewerRef.current.setStyle({}, { cartoon: { color: 'spectrum', opacity: 0.5 } });
|
| 167 |
-
viewerRef.current.addSurface($3Dmol.SurfaceType.VDW, {
|
| 168 |
-
opacity: 0.85,
|
| 169 |
-
color: 'spectrum',
|
| 170 |
-
}, {}, {});
|
| 171 |
-
} else {
|
| 172 |
-
viewerRef.current.setStyle({}, getStyleForRepresentation(representation));
|
| 173 |
-
}
|
| 174 |
-
|
| 175 |
-
viewerRef.current.render();
|
| 176 |
-
} catch (err) {
|
| 177 |
-
console.error('Style update error:', err);
|
| 178 |
-
}
|
| 179 |
-
}, [representation, getStyleForRepresentation, pdbData, $3Dmol]);
|
| 180 |
-
|
| 181 |
-
const handleResetCamera = useCallback(() => {
|
| 182 |
-
if (!viewerRef.current) return;
|
| 183 |
-
viewerRef.current.zoomTo();
|
| 184 |
-
viewerRef.current.render();
|
| 185 |
-
}, []);
|
| 186 |
-
|
| 187 |
-
if (error) {
|
| 188 |
-
return (
|
| 189 |
-
<Card className={`border-destructive bg-destructive/10 ${className}`}>
|
| 190 |
-
<CardContent className="flex items-center gap-3 p-4">
|
| 191 |
-
<AlertCircle className="size-5 text-destructive" />
|
| 192 |
-
<div className="flex flex-col">
|
| 193 |
-
<span className="text-sm font-medium text-destructive">
|
| 194 |
-
Protein Visualization Error
|
| 195 |
-
</span>
|
| 196 |
-
<span className="text-xs text-muted-foreground">{error}</span>
|
| 197 |
-
</div>
|
| 198 |
-
</CardContent>
|
| 199 |
-
</Card>
|
| 200 |
-
);
|
| 201 |
-
}
|
| 202 |
-
|
| 203 |
-
return (
|
| 204 |
-
<div className={`flex flex-col gap-3 ${className}`}>
|
| 205 |
-
<div className="flex items-center gap-2">
|
| 206 |
-
<Select
|
| 207 |
-
value={representation}
|
| 208 |
-
onValueChange={(value) =>
|
| 209 |
-
setRepresentation(value as ProteinRepresentation)
|
| 210 |
-
}
|
| 211 |
-
>
|
| 212 |
-
<SelectTrigger className="w-40">
|
| 213 |
-
<SelectValue placeholder="Representation" />
|
| 214 |
-
</SelectTrigger>
|
| 215 |
-
<SelectContent>
|
| 216 |
-
<SelectItem value="cartoon">Cartoon</SelectItem>
|
| 217 |
-
<SelectItem value="ball-and-stick">Ball & Stick</SelectItem>
|
| 218 |
-
<SelectItem value="surface">Surface</SelectItem>
|
| 219 |
-
<SelectItem value="ribbon">Ribbon</SelectItem>
|
| 220 |
-
</SelectContent>
|
| 221 |
-
</Select>
|
| 222 |
-
<Button variant="outline" size="sm" onClick={handleResetCamera}>
|
| 223 |
-
<RotateCcw className="mr-1 size-4" />
|
| 224 |
-
Reset
|
| 225 |
-
</Button>
|
| 226 |
-
</div>
|
| 227 |
-
|
| 228 |
-
<div className="relative">
|
| 229 |
-
{isLoading && (
|
| 230 |
-
<Skeleton
|
| 231 |
-
className="absolute inset-0 z-10"
|
| 232 |
-
style={{ width, height }}
|
| 233 |
-
/>
|
| 234 |
-
)}
|
| 235 |
-
<div
|
| 236 |
-
ref={containerRef}
|
| 237 |
-
style={{ width, height }}
|
| 238 |
-
className={`rounded-lg border bg-background ${isLoading ? 'opacity-0' : 'opacity-100'} transition-opacity duration-200`}
|
| 239 |
-
/>
|
| 240 |
-
</div>
|
| 241 |
-
</div>
|
| 242 |
-
);
|
| 243 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/app/dashboard/proteins-3d/page.tsx
DELETED
|
@@ -1,228 +0,0 @@
|
|
| 1 |
-
'use client';
|
| 2 |
-
|
| 3 |
-
import { ExternalLink, Search } from 'lucide-react';
|
| 4 |
-
import dynamic from 'next/dynamic';
|
| 5 |
-
import { useEffect, useMemo, useState } from 'react';
|
| 6 |
-
|
| 7 |
-
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
| 8 |
-
import { Badge } from '@/components/ui/badge';
|
| 9 |
-
import { Button } from '@/components/ui/button';
|
| 10 |
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
| 11 |
-
import { Input } from '@/components/ui/input';
|
| 12 |
-
import { ScrollArea } from '@/components/ui/scroll-area';
|
| 13 |
-
import { Separator } from '@/components/ui/separator';
|
| 14 |
-
import { Skeleton } from '@/components/ui/skeleton';
|
| 15 |
-
import { getProteins, getProteinPdbUrl } from '@/lib/visualization-api';
|
| 16 |
-
import type { Protein } from '@/lib/visualization-types';
|
| 17 |
-
|
| 18 |
-
// Dynamic import of the protein viewer to prevent SSR issues
|
| 19 |
-
const ProteinViewer = dynamic(
|
| 20 |
-
() =>
|
| 21 |
-
import('./_components/ProteinViewer').then((mod) => mod.ProteinViewer),
|
| 22 |
-
{
|
| 23 |
-
ssr: false,
|
| 24 |
-
loading: () => <Skeleton className="size-[500px]" />,
|
| 25 |
-
}
|
| 26 |
-
);
|
| 27 |
-
|
| 28 |
-
export default function Proteins3DPage() {
|
| 29 |
-
const [proteins, setProteins] = useState<Protein[]>([]);
|
| 30 |
-
const [selectedProtein, setSelectedProtein] = useState<Protein | null>(null);
|
| 31 |
-
const [searchQuery, setSearchQuery] = useState('');
|
| 32 |
-
const [isLoading, setIsLoading] = useState(true);
|
| 33 |
-
const [error, setError] = useState<string | null>(null);
|
| 34 |
-
|
| 35 |
-
// Load proteins list
|
| 36 |
-
useEffect(() => {
|
| 37 |
-
const loadProteins = async () => {
|
| 38 |
-
try {
|
| 39 |
-
setIsLoading(true);
|
| 40 |
-
const data = await getProteins();
|
| 41 |
-
setProteins(data);
|
| 42 |
-
if (data.length > 0) {
|
| 43 |
-
setSelectedProtein(data[0]);
|
| 44 |
-
}
|
| 45 |
-
} catch (err) {
|
| 46 |
-
setError(
|
| 47 |
-
err instanceof Error ? err.message : 'Failed to load proteins'
|
| 48 |
-
);
|
| 49 |
-
} finally {
|
| 50 |
-
setIsLoading(false);
|
| 51 |
-
}
|
| 52 |
-
};
|
| 53 |
-
loadProteins();
|
| 54 |
-
}, []);
|
| 55 |
-
|
| 56 |
-
const filteredProteins = useMemo(() => {
|
| 57 |
-
if (!searchQuery.trim()) return proteins;
|
| 58 |
-
const query = searchQuery.toLowerCase();
|
| 59 |
-
return proteins.filter(
|
| 60 |
-
(p) =>
|
| 61 |
-
p.name.toLowerCase().includes(query) ||
|
| 62 |
-
p.pdbId.toLowerCase().includes(query) ||
|
| 63 |
-
p.description?.toLowerCase().includes(query)
|
| 64 |
-
);
|
| 65 |
-
}, [proteins, searchQuery]);
|
| 66 |
-
|
| 67 |
-
if (error) {
|
| 68 |
-
return (
|
| 69 |
-
<div className="flex h-full items-center justify-center">
|
| 70 |
-
<Card className="max-w-md">
|
| 71 |
-
<CardHeader>
|
| 72 |
-
<CardTitle className="text-destructive">Error</CardTitle>
|
| 73 |
-
<CardDescription>{error}</CardDescription>
|
| 74 |
-
</CardHeader>
|
| 75 |
-
</Card>
|
| 76 |
-
</div>
|
| 77 |
-
);
|
| 78 |
-
}
|
| 79 |
-
|
| 80 |
-
return (
|
| 81 |
-
<div className="flex h-full gap-6 p-6">
|
| 82 |
-
{/* Left Panel - Protein List */}
|
| 83 |
-
<Card className="w-80 shrink-0">
|
| 84 |
-
<CardHeader className="pb-3">
|
| 85 |
-
<CardTitle className="text-lg">Proteins</CardTitle>
|
| 86 |
-
<CardDescription>Select a protein to visualize</CardDescription>
|
| 87 |
-
</CardHeader>
|
| 88 |
-
<CardContent className="pb-3">
|
| 89 |
-
<div className="relative">
|
| 90 |
-
<Search className="absolute left-2.5 top-2.5 size-4 text-muted-foreground" />
|
| 91 |
-
<Input
|
| 92 |
-
placeholder="Search proteins..."
|
| 93 |
-
className="pl-8"
|
| 94 |
-
value={searchQuery}
|
| 95 |
-
onChange={(e) => setSearchQuery(e.target.value)}
|
| 96 |
-
/>
|
| 97 |
-
</div>
|
| 98 |
-
</CardContent>
|
| 99 |
-
<Separator />
|
| 100 |
-
<ScrollArea className="h-[calc(100vh-280px)]">
|
| 101 |
-
<div className="p-2">
|
| 102 |
-
{isLoading ? (
|
| 103 |
-
<div className="space-y-2">
|
| 104 |
-
{[1, 2, 3].map((i) => (
|
| 105 |
-
<Skeleton key={i} className="h-16 w-full" />
|
| 106 |
-
))}
|
| 107 |
-
</div>
|
| 108 |
-
) : filteredProteins.length === 0 ? (
|
| 109 |
-
<p className="p-4 text-center text-sm text-muted-foreground">
|
| 110 |
-
No proteins found
|
| 111 |
-
</p>
|
| 112 |
-
) : (
|
| 113 |
-
<div className="space-y-1">
|
| 114 |
-
{filteredProteins.map((protein) => (
|
| 115 |
-
<button
|
| 116 |
-
key={protein.id}
|
| 117 |
-
onClick={() => setSelectedProtein(protein)}
|
| 118 |
-
className={`w-full rounded-lg p-3 text-left transition-colors hover:bg-accent ${
|
| 119 |
-
selectedProtein?.id === protein.id
|
| 120 |
-
? 'bg-accent'
|
| 121 |
-
: 'bg-transparent'
|
| 122 |
-
}`}
|
| 123 |
-
>
|
| 124 |
-
<div className="flex items-center gap-2">
|
| 125 |
-
<span className="font-medium">{protein.name}</span>
|
| 126 |
-
<Badge variant="secondary" className="text-xs">
|
| 127 |
-
{protein.pdbId}
|
| 128 |
-
</Badge>
|
| 129 |
-
</div>
|
| 130 |
-
{protein.description && (
|
| 131 |
-
<div className="mt-1 truncate text-xs text-muted-foreground">
|
| 132 |
-
{protein.description}
|
| 133 |
-
</div>
|
| 134 |
-
)}
|
| 135 |
-
</button>
|
| 136 |
-
))}
|
| 137 |
-
</div>
|
| 138 |
-
)}
|
| 139 |
-
</div>
|
| 140 |
-
</ScrollArea>
|
| 141 |
-
</Card>
|
| 142 |
-
|
| 143 |
-
{/* Right Panel - 3D Visualization */}
|
| 144 |
-
<div className="flex flex-1 flex-col gap-6">
|
| 145 |
-
{selectedProtein ? (
|
| 146 |
-
<>
|
| 147 |
-
{/* Protein Info Card */}
|
| 148 |
-
<Card>
|
| 149 |
-
<CardHeader className="pb-3">
|
| 150 |
-
<div className="flex items-start justify-between">
|
| 151 |
-
<div>
|
| 152 |
-
<CardTitle>{selectedProtein.name}</CardTitle>
|
| 153 |
-
<CardDescription>
|
| 154 |
-
{selectedProtein.description}
|
| 155 |
-
</CardDescription>
|
| 156 |
-
</div>
|
| 157 |
-
<div className="flex items-center gap-2">
|
| 158 |
-
<Badge variant="outline">PDB: {selectedProtein.pdbId}</Badge>
|
| 159 |
-
<Button
|
| 160 |
-
variant="outline"
|
| 161 |
-
size="sm"
|
| 162 |
-
asChild
|
| 163 |
-
>
|
| 164 |
-
<a
|
| 165 |
-
href={getProteinPdbUrl(selectedProtein.id)}
|
| 166 |
-
target="_blank"
|
| 167 |
-
rel="noopener noreferrer"
|
| 168 |
-
>
|
| 169 |
-
<ExternalLink className="mr-1 size-4" />
|
| 170 |
-
Open PDB
|
| 171 |
-
</a>
|
| 172 |
-
</Button>
|
| 173 |
-
</div>
|
| 174 |
-
</div>
|
| 175 |
-
</CardHeader>
|
| 176 |
-
<CardContent>
|
| 177 |
-
<div className="flex gap-4 text-sm text-muted-foreground">
|
| 178 |
-
<a
|
| 179 |
-
href={`https://www.rcsb.org/structure/${selectedProtein.pdbId}`}
|
| 180 |
-
target="_blank"
|
| 181 |
-
rel="noopener noreferrer"
|
| 182 |
-
className="flex items-center gap-1 hover:text-foreground"
|
| 183 |
-
>
|
| 184 |
-
<ExternalLink className="size-3" />
|
| 185 |
-
View on RCSB PDB
|
| 186 |
-
</a>
|
| 187 |
-
<a
|
| 188 |
-
href={`https://www.uniprot.org/uniprotkb?query=${selectedProtein.name}`}
|
| 189 |
-
target="_blank"
|
| 190 |
-
rel="noopener noreferrer"
|
| 191 |
-
className="flex items-center gap-1 hover:text-foreground"
|
| 192 |
-
>
|
| 193 |
-
<ExternalLink className="size-3" />
|
| 194 |
-
Search UniProt
|
| 195 |
-
</a>
|
| 196 |
-
</div>
|
| 197 |
-
</CardContent>
|
| 198 |
-
</Card>
|
| 199 |
-
|
| 200 |
-
{/* 3D Structure Card */}
|
| 201 |
-
<Card className="flex-1">
|
| 202 |
-
<CardHeader>
|
| 203 |
-
<CardTitle className="text-lg">3D Structure</CardTitle>
|
| 204 |
-
<CardDescription>
|
| 205 |
-
Rotate: click + drag • Zoom: scroll • Pan: right-click + drag
|
| 206 |
-
</CardDescription>
|
| 207 |
-
</CardHeader>
|
| 208 |
-
<CardContent className="flex items-center justify-center">
|
| 209 |
-
<ProteinViewer
|
| 210 |
-
key={selectedProtein.pdbId}
|
| 211 |
-
pdbId={selectedProtein.pdbId}
|
| 212 |
-
width={600}
|
| 213 |
-
height={500}
|
| 214 |
-
/>
|
| 215 |
-
</CardContent>
|
| 216 |
-
</Card>
|
| 217 |
-
</>
|
| 218 |
-
) : (
|
| 219 |
-
<div className="flex flex-1 items-center justify-center">
|
| 220 |
-
<p className="text-muted-foreground">
|
| 221 |
-
Select a protein to view its 3D structure
|
| 222 |
-
</p>
|
| 223 |
-
</div>
|
| 224 |
-
)}
|
| 225 |
-
</div>
|
| 226 |
-
</div>
|
| 227 |
-
);
|
| 228 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/app/dashboard/settings/page.tsx
DELETED
|
@@ -1,193 +0,0 @@
|
|
| 1 |
-
"use client"
|
| 2 |
-
|
| 3 |
-
import { Brain, Save,Settings } from "lucide-react"
|
| 4 |
-
|
| 5 |
-
import { PageHeader, SectionHeader } from "@/components/page-header"
|
| 6 |
-
import { Button } from "@/components/ui/button"
|
| 7 |
-
import { Card, CardContent, CardDescription,CardHeader, CardTitle } from "@/components/ui/card"
|
| 8 |
-
import { Label } from "@/components/ui/label"
|
| 9 |
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
| 10 |
-
import { Slider } from "@/components/ui/slider"
|
| 11 |
-
import { Switch } from "@/components/ui/switch"
|
| 12 |
-
import { Tabs, TabsContent,TabsList, TabsTrigger } from "@/components/ui/tabs"
|
| 13 |
-
|
| 14 |
-
export default function SettingsPage() {
|
| 15 |
-
return (
|
| 16 |
-
<div className="space-y-8 animate-in fade-in duration-500">
|
| 17 |
-
<PageHeader
|
| 18 |
-
title="Settings"
|
| 19 |
-
subtitle="Configure models, databases, and preferences"
|
| 20 |
-
icon={<Settings className="h-8 w-8 text-primary" />}
|
| 21 |
-
/>
|
| 22 |
-
|
| 23 |
-
<Tabs defaultValue="models" className="w-full">
|
| 24 |
-
<TabsList className="w-full justify-start overflow-x-auto">
|
| 25 |
-
<TabsTrigger value="models">Models</TabsTrigger>
|
| 26 |
-
<TabsTrigger value="database">Database</TabsTrigger>
|
| 27 |
-
<TabsTrigger value="api">API Keys</TabsTrigger>
|
| 28 |
-
<TabsTrigger value="appearance">Appearance</TabsTrigger>
|
| 29 |
-
<TabsTrigger value="system">System</TabsTrigger>
|
| 30 |
-
</TabsList>
|
| 31 |
-
<TabsContent value="models" className="space-y-6 mt-6">
|
| 32 |
-
<SectionHeader title="Model Configuration" icon={<Brain className="h-5 w-5 text-primary" />} />
|
| 33 |
-
|
| 34 |
-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
| 35 |
-
<Card>
|
| 36 |
-
<CardHeader>
|
| 37 |
-
<CardTitle>Embedding Models</CardTitle>
|
| 38 |
-
<CardDescription>Configure models used for molecular and protein embeddings</CardDescription>
|
| 39 |
-
</CardHeader>
|
| 40 |
-
<CardContent className="space-y-4">
|
| 41 |
-
<div className="space-y-2">
|
| 42 |
-
<Label htmlFor="mol-encoder">Molecule Encoder</Label>
|
| 43 |
-
<Select defaultValue="MolCLR">
|
| 44 |
-
<SelectTrigger id="mol-encoder">
|
| 45 |
-
<SelectValue placeholder="Select molecule encoder" />
|
| 46 |
-
</SelectTrigger>
|
| 47 |
-
<SelectContent>
|
| 48 |
-
<SelectItem value="MolCLR">MolCLR (Recommended)</SelectItem>
|
| 49 |
-
<SelectItem value="ChemBERTa">ChemBERTa</SelectItem>
|
| 50 |
-
<SelectItem value="GraphMVP">GraphMVP</SelectItem>
|
| 51 |
-
<SelectItem value="MolBERT">MolBERT</SelectItem>
|
| 52 |
-
</SelectContent>
|
| 53 |
-
</Select>
|
| 54 |
-
</div>
|
| 55 |
-
<div className="space-y-2">
|
| 56 |
-
<Label htmlFor="prot-encoder">Protein Encoder</Label>
|
| 57 |
-
<Select defaultValue="ESM-2">
|
| 58 |
-
<SelectTrigger id="prot-encoder">
|
| 59 |
-
<SelectValue placeholder="Select protein encoder" />
|
| 60 |
-
</SelectTrigger>
|
| 61 |
-
<SelectContent>
|
| 62 |
-
<SelectItem value="ESM-2">ESM-2 (Recommended)</SelectItem>
|
| 63 |
-
<SelectItem value="ProtTrans">ProtTrans</SelectItem>
|
| 64 |
-
<SelectItem value="UniRep">UniRep</SelectItem>
|
| 65 |
-
<SelectItem value="SeqVec">SeqVec</SelectItem>
|
| 66 |
-
</SelectContent>
|
| 67 |
-
</Select>
|
| 68 |
-
</div>
|
| 69 |
-
</CardContent>
|
| 70 |
-
</Card>
|
| 71 |
-
|
| 72 |
-
<Card>
|
| 73 |
-
<CardHeader>
|
| 74 |
-
<CardTitle>Prediction Heads</CardTitle>
|
| 75 |
-
<CardDescription>Configure downstream task predictors</CardDescription>
|
| 76 |
-
</CardHeader>
|
| 77 |
-
<CardContent className="space-y-4">
|
| 78 |
-
<div className="space-y-2">
|
| 79 |
-
<Label htmlFor="binding">Binding Predictor</Label>
|
| 80 |
-
<Select defaultValue="DrugBAN">
|
| 81 |
-
<SelectTrigger id="binding">
|
| 82 |
-
<SelectValue placeholder="Select predictor" />
|
| 83 |
-
</SelectTrigger>
|
| 84 |
-
<SelectContent>
|
| 85 |
-
<SelectItem value="DrugBAN">DrugBAN (Recommended)</SelectItem>
|
| 86 |
-
<SelectItem value="DeepDTA">DeepDTA</SelectItem>
|
| 87 |
-
<SelectItem value="GraphDTA">GraphDTA</SelectItem>
|
| 88 |
-
<SelectItem value="Custom">Custom</SelectItem>
|
| 89 |
-
</SelectContent>
|
| 90 |
-
</Select>
|
| 91 |
-
</div>
|
| 92 |
-
<div className="space-y-2">
|
| 93 |
-
<Label htmlFor="property">Property Predictor</Label>
|
| 94 |
-
<Select defaultValue="ADMET-AI">
|
| 95 |
-
<SelectTrigger id="property">
|
| 96 |
-
<SelectValue placeholder="Select predictor" />
|
| 97 |
-
</SelectTrigger>
|
| 98 |
-
<SelectContent>
|
| 99 |
-
<SelectItem value="ADMET-AI">ADMET-AI (Recommended)</SelectItem>
|
| 100 |
-
<SelectItem value="ChemProp">ChemProp</SelectItem>
|
| 101 |
-
<SelectItem value="Custom">Custom</SelectItem>
|
| 102 |
-
</SelectContent>
|
| 103 |
-
</Select>
|
| 104 |
-
</div>
|
| 105 |
-
</CardContent>
|
| 106 |
-
</Card>
|
| 107 |
-
</div>
|
| 108 |
-
|
| 109 |
-
<Card>
|
| 110 |
-
<CardHeader>
|
| 111 |
-
<CardTitle>LLM Settings</CardTitle>
|
| 112 |
-
<CardDescription>Configure language models for evidence retrieval and reasoning</CardDescription>
|
| 113 |
-
</CardHeader>
|
| 114 |
-
<CardContent className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
| 115 |
-
<div className="space-y-2">
|
| 116 |
-
<Label htmlFor="llm-provider">LLM Provider</Label>
|
| 117 |
-
<Select defaultValue="OpenAI">
|
| 118 |
-
<SelectTrigger id="llm-provider">
|
| 119 |
-
<SelectValue placeholder="Select provider" />
|
| 120 |
-
</SelectTrigger>
|
| 121 |
-
<SelectContent>
|
| 122 |
-
<SelectItem value="OpenAI">OpenAI</SelectItem>
|
| 123 |
-
<SelectItem value="Anthropic">Anthropic</SelectItem>
|
| 124 |
-
<SelectItem value="Local">Local (Ollama)</SelectItem>
|
| 125 |
-
<SelectItem value="Azure">Azure OpenAI</SelectItem>
|
| 126 |
-
</SelectContent>
|
| 127 |
-
</Select>
|
| 128 |
-
</div>
|
| 129 |
-
<div className="space-y-2">
|
| 130 |
-
<Label htmlFor="llm-model">Model</Label>
|
| 131 |
-
<Select defaultValue="GPT-4o">
|
| 132 |
-
<SelectTrigger id="llm-model">
|
| 133 |
-
<SelectValue placeholder="Select model" />
|
| 134 |
-
</SelectTrigger>
|
| 135 |
-
<SelectContent>
|
| 136 |
-
<SelectItem value="GPT-4o">GPT-4o</SelectItem>
|
| 137 |
-
<SelectItem value="GPT-4-turbo">GPT-4-turbo</SelectItem>
|
| 138 |
-
<SelectItem value="Claude 3.5">Claude 3.5 Sonnet</SelectItem>
|
| 139 |
-
<SelectItem value="Llama 3">Llama 3.1 70B</SelectItem>
|
| 140 |
-
</SelectContent>
|
| 141 |
-
</Select>
|
| 142 |
-
</div>
|
| 143 |
-
<div className="col-span-1 md:col-span-2 space-y-4">
|
| 144 |
-
<div className="space-y-2">
|
| 145 |
-
<div className="flex items-center justify-between">
|
| 146 |
-
<Label>Temperature: 0.7</Label>
|
| 147 |
-
<span className="text-xs text-muted-foreground">Creativity vs Precision</span>
|
| 148 |
-
</div>
|
| 149 |
-
<Slider defaultValue={[0.7]} max={1} step={0.1} />
|
| 150 |
-
</div>
|
| 151 |
-
<div className="flex items-center space-x-2">
|
| 152 |
-
<Switch id="stream" defaultChecked />
|
| 153 |
-
<Label htmlFor="stream">Stream Responses</Label>
|
| 154 |
-
</div>
|
| 155 |
-
</div>
|
| 156 |
-
</CardContent>
|
| 157 |
-
</Card>
|
| 158 |
-
</TabsContent>
|
| 159 |
-
|
| 160 |
-
<TabsContent value="appearance">
|
| 161 |
-
<Card>
|
| 162 |
-
<CardContent className="p-12 text-center text-muted-foreground">
|
| 163 |
-
Theme settings coming soon.
|
| 164 |
-
</CardContent>
|
| 165 |
-
</Card>
|
| 166 |
-
</TabsContent>
|
| 167 |
-
|
| 168 |
-
<TabsContent value="database">
|
| 169 |
-
<Card>
|
| 170 |
-
<CardContent className="p-12 text-center text-muted-foreground">
|
| 171 |
-
Database connection settings.
|
| 172 |
-
</CardContent>
|
| 173 |
-
</Card>
|
| 174 |
-
</TabsContent>
|
| 175 |
-
|
| 176 |
-
<TabsContent value="api">
|
| 177 |
-
<Card>
|
| 178 |
-
<CardContent className="p-12 text-center text-muted-foreground">
|
| 179 |
-
API Key configuration.
|
| 180 |
-
</CardContent>
|
| 181 |
-
</Card>
|
| 182 |
-
</TabsContent>
|
| 183 |
-
</Tabs>
|
| 184 |
-
|
| 185 |
-
<div className="fixed bottom-6 right-6">
|
| 186 |
-
<Button size="lg" className="shadow-2xl">
|
| 187 |
-
<Save className="mr-2 h-4 w-4" />
|
| 188 |
-
Save Changes
|
| 189 |
-
</Button>
|
| 190 |
-
</div>
|
| 191 |
-
</div>
|
| 192 |
-
)
|
| 193 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/app/dashboard/visualization/page.tsx
DELETED
|
@@ -1,581 +0,0 @@
|
|
| 1 |
-
"use client"
|
| 2 |
-
|
| 3 |
-
import * as React from "react"
|
| 4 |
-
import { Download, ExternalLink, Filter, Loader2, RotateCcw, Search, ZoomIn, ZoomOut } from "lucide-react"
|
| 5 |
-
|
| 6 |
-
import { Button } from "@/components/ui/button"
|
| 7 |
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
| 8 |
-
import { Input } from "@/components/ui/input"
|
| 9 |
-
import { Label } from "@/components/ui/label"
|
| 10 |
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
| 11 |
-
import { Slider } from "@/components/ui/slider"
|
| 12 |
-
import { Badge } from "@/components/ui/badge"
|
| 13 |
-
import { ScrollArea } from "@/components/ui/scroll-area"
|
| 14 |
-
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
| 15 |
-
|
| 16 |
-
// Types
|
| 17 |
-
interface EmbeddingPoint {
|
| 18 |
-
id: string
|
| 19 |
-
x: number
|
| 20 |
-
y: number
|
| 21 |
-
z: number
|
| 22 |
-
label: string
|
| 23 |
-
content: string
|
| 24 |
-
modality: string
|
| 25 |
-
source: string
|
| 26 |
-
score?: number
|
| 27 |
-
metadata?: Record<string, unknown>
|
| 28 |
-
}
|
| 29 |
-
|
| 30 |
-
interface SearchResult {
|
| 31 |
-
id: string
|
| 32 |
-
content: string
|
| 33 |
-
score: number
|
| 34 |
-
modality: string
|
| 35 |
-
source: string
|
| 36 |
-
evidence_links?: Array<{
|
| 37 |
-
source: string
|
| 38 |
-
identifier: string
|
| 39 |
-
url: string
|
| 40 |
-
label: string
|
| 41 |
-
}>
|
| 42 |
-
citation?: string
|
| 43 |
-
}
|
| 44 |
-
|
| 45 |
-
// 3D Canvas Component using CSS transforms (no Three.js dependency)
|
| 46 |
-
function Scatter3DCanvas({
|
| 47 |
-
points,
|
| 48 |
-
selectedPoint,
|
| 49 |
-
onSelectPoint,
|
| 50 |
-
rotation,
|
| 51 |
-
zoom,
|
| 52 |
-
}: {
|
| 53 |
-
points: EmbeddingPoint[]
|
| 54 |
-
selectedPoint: EmbeddingPoint | null
|
| 55 |
-
onSelectPoint: (point: EmbeddingPoint | null) => void
|
| 56 |
-
rotation: { x: number; y: number }
|
| 57 |
-
zoom: number
|
| 58 |
-
}) {
|
| 59 |
-
const containerRef = React.useRef<HTMLDivElement>(null)
|
| 60 |
-
|
| 61 |
-
// Color by modality
|
| 62 |
-
const getColor = (modality: string) => {
|
| 63 |
-
switch (modality) {
|
| 64 |
-
case "text": return "#3b82f6" // blue
|
| 65 |
-
case "molecule": return "#22c55e" // green
|
| 66 |
-
case "protein": return "#f59e0b" // amber
|
| 67 |
-
default: return "#8b5cf6" // purple
|
| 68 |
-
}
|
| 69 |
-
}
|
| 70 |
-
|
| 71 |
-
// Project 3D to 2D with rotation
|
| 72 |
-
const project = (point: EmbeddingPoint) => {
|
| 73 |
-
const rad = Math.PI / 180
|
| 74 |
-
const cosX = Math.cos(rotation.x * rad)
|
| 75 |
-
const sinX = Math.sin(rotation.x * rad)
|
| 76 |
-
const cosY = Math.cos(rotation.y * rad)
|
| 77 |
-
const sinY = Math.sin(rotation.y * rad)
|
| 78 |
-
|
| 79 |
-
// Rotate around Y axis
|
| 80 |
-
let x = point.x * cosY - point.z * sinY
|
| 81 |
-
let z = point.x * sinY + point.z * cosY
|
| 82 |
-
|
| 83 |
-
// Rotate around X axis
|
| 84 |
-
const y = point.y * cosX - z * sinX
|
| 85 |
-
z = point.y * sinX + z * cosX
|
| 86 |
-
|
| 87 |
-
// Simple perspective projection
|
| 88 |
-
const perspective = 500
|
| 89 |
-
const scale = perspective / (perspective + z * 50)
|
| 90 |
-
|
| 91 |
-
return {
|
| 92 |
-
x: 250 + x * 100 * zoom * scale,
|
| 93 |
-
y: 250 - y * 100 * zoom * scale,
|
| 94 |
-
scale,
|
| 95 |
-
z,
|
| 96 |
-
}
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
// Sort points by z for proper rendering order
|
| 100 |
-
const sortedPoints = [...points]
|
| 101 |
-
.map(p => ({ ...p, projected: project(p) }))
|
| 102 |
-
.sort((a, b) => a.projected.z - b.projected.z)
|
| 103 |
-
|
| 104 |
-
return (
|
| 105 |
-
<div
|
| 106 |
-
ref={containerRef}
|
| 107 |
-
className="relative w-full h-[500px] bg-gradient-to-br from-slate-900 to-slate-800 rounded-lg overflow-hidden"
|
| 108 |
-
style={{ perspective: "500px" }}
|
| 109 |
-
>
|
| 110 |
-
{/* Axis lines */}
|
| 111 |
-
<svg className="absolute inset-0 w-full h-full pointer-events-none opacity-20">
|
| 112 |
-
<line x1="50" y1="250" x2="450" y2="250" stroke="#fff" strokeWidth="1" />
|
| 113 |
-
<line x1="250" y1="50" x2="250" y2="450" stroke="#fff" strokeWidth="1" />
|
| 114 |
-
<text x="460" y="255" fill="#fff" fontSize="12">X</text>
|
| 115 |
-
<text x="255" y="40" fill="#fff" fontSize="12">Y</text>
|
| 116 |
-
</svg>
|
| 117 |
-
|
| 118 |
-
{/* Points */}
|
| 119 |
-
{sortedPoints.map((point) => {
|
| 120 |
-
const { x, y, scale } = point.projected
|
| 121 |
-
const size = Math.max(6, 12 * scale)
|
| 122 |
-
const isSelected = selectedPoint?.id === point.id
|
| 123 |
-
|
| 124 |
-
return (
|
| 125 |
-
<button
|
| 126 |
-
key={point.id}
|
| 127 |
-
className="absolute rounded-full transition-all duration-150 cursor-pointer hover:ring-2 hover:ring-white/50"
|
| 128 |
-
style={{
|
| 129 |
-
left: x - size / 2,
|
| 130 |
-
top: y - size / 2,
|
| 131 |
-
width: size,
|
| 132 |
-
height: size,
|
| 133 |
-
backgroundColor: getColor(point.modality),
|
| 134 |
-
opacity: 0.5 + scale * 0.5,
|
| 135 |
-
transform: isSelected ? "scale(1.5)" : "scale(1)",
|
| 136 |
-
boxShadow: isSelected ? `0 0 20px ${getColor(point.modality)}` : "none",
|
| 137 |
-
zIndex: Math.floor(scale * 100),
|
| 138 |
-
}}
|
| 139 |
-
onClick={() => onSelectPoint(isSelected ? null : point)}
|
| 140 |
-
title={point.label}
|
| 141 |
-
/>
|
| 142 |
-
)
|
| 143 |
-
})}
|
| 144 |
-
|
| 145 |
-
{/* Legend */}
|
| 146 |
-
<div className="absolute bottom-4 left-4 flex gap-4 text-xs text-white/70">
|
| 147 |
-
<div className="flex items-center gap-1">
|
| 148 |
-
<div className="w-3 h-3 rounded-full bg-blue-500" />
|
| 149 |
-
<span>Text</span>
|
| 150 |
-
</div>
|
| 151 |
-
<div className="flex items-center gap-1">
|
| 152 |
-
<div className="w-3 h-3 rounded-full bg-green-500" />
|
| 153 |
-
<span>Molecule</span>
|
| 154 |
-
</div>
|
| 155 |
-
<div className="flex items-center gap-1">
|
| 156 |
-
<div className="w-3 h-3 rounded-full bg-amber-500" />
|
| 157 |
-
<span>Protein</span>
|
| 158 |
-
</div>
|
| 159 |
-
</div>
|
| 160 |
-
</div>
|
| 161 |
-
)
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
// Evidence Panel Component
|
| 165 |
-
function EvidencePanel({ result }: { result: SearchResult | null }) {
|
| 166 |
-
if (!result) {
|
| 167 |
-
return (
|
| 168 |
-
<Card className="h-full">
|
| 169 |
-
<CardHeader>
|
| 170 |
-
<CardTitle className="text-lg">Evidence Panel</CardTitle>
|
| 171 |
-
<CardDescription>Select a point to view details</CardDescription>
|
| 172 |
-
</CardHeader>
|
| 173 |
-
<CardContent>
|
| 174 |
-
<div className="text-center text-muted-foreground py-8">
|
| 175 |
-
Click on a point in the 3D view to see its evidence trail
|
| 176 |
-
</div>
|
| 177 |
-
</CardContent>
|
| 178 |
-
</Card>
|
| 179 |
-
)
|
| 180 |
-
}
|
| 181 |
-
|
| 182 |
-
return (
|
| 183 |
-
<Card className="h-full">
|
| 184 |
-
<CardHeader className="pb-3">
|
| 185 |
-
<div className="flex items-center justify-between">
|
| 186 |
-
<CardTitle className="text-lg">Evidence</CardTitle>
|
| 187 |
-
<Badge variant="outline">{result.modality}</Badge>
|
| 188 |
-
</div>
|
| 189 |
-
<CardDescription>Score: {result.score.toFixed(3)}</CardDescription>
|
| 190 |
-
</CardHeader>
|
| 191 |
-
<CardContent>
|
| 192 |
-
<ScrollArea className="h-[400px] pr-4">
|
| 193 |
-
{/* Content */}
|
| 194 |
-
<div className="space-y-4">
|
| 195 |
-
<div>
|
| 196 |
-
<Label className="text-xs text-muted-foreground">Content</Label>
|
| 197 |
-
<p className="text-sm mt-1 bg-muted/50 p-3 rounded-lg">
|
| 198 |
-
{result.content.slice(0, 300)}
|
| 199 |
-
{result.content.length > 300 && "..."}
|
| 200 |
-
</p>
|
| 201 |
-
</div>
|
| 202 |
-
|
| 203 |
-
{/* Source */}
|
| 204 |
-
<div>
|
| 205 |
-
<Label className="text-xs text-muted-foreground">Source</Label>
|
| 206 |
-
<Badge className="mt-1">{result.source}</Badge>
|
| 207 |
-
</div>
|
| 208 |
-
|
| 209 |
-
{/* Citation */}
|
| 210 |
-
{result.citation && (
|
| 211 |
-
<div>
|
| 212 |
-
<Label className="text-xs text-muted-foreground">Citation</Label>
|
| 213 |
-
<p className="text-sm mt-1 italic">{result.citation}</p>
|
| 214 |
-
</div>
|
| 215 |
-
)}
|
| 216 |
-
|
| 217 |
-
{/* Evidence Links */}
|
| 218 |
-
{result.evidence_links && result.evidence_links.length > 0 && (
|
| 219 |
-
<div>
|
| 220 |
-
<Label className="text-xs text-muted-foreground">External Links</Label>
|
| 221 |
-
<div className="mt-2 space-y-2">
|
| 222 |
-
{result.evidence_links.map((link, idx) => (
|
| 223 |
-
<a
|
| 224 |
-
key={idx}
|
| 225 |
-
href={link.url}
|
| 226 |
-
target="_blank"
|
| 227 |
-
rel="noopener noreferrer"
|
| 228 |
-
className="flex items-center gap-2 text-sm text-primary hover:underline"
|
| 229 |
-
>
|
| 230 |
-
<ExternalLink className="h-3 w-3" />
|
| 231 |
-
{link.label}
|
| 232 |
-
</a>
|
| 233 |
-
))}
|
| 234 |
-
</div>
|
| 235 |
-
</div>
|
| 236 |
-
)}
|
| 237 |
-
</div>
|
| 238 |
-
</ScrollArea>
|
| 239 |
-
</CardContent>
|
| 240 |
-
</Card>
|
| 241 |
-
)
|
| 242 |
-
}
|
| 243 |
-
|
| 244 |
-
// Export functions
|
| 245 |
-
function exportToCSV(results: SearchResult[]) {
|
| 246 |
-
const headers = ["id", "content", "score", "modality", "source", "citation"]
|
| 247 |
-
const rows = results.map(r => [
|
| 248 |
-
r.id,
|
| 249 |
-
`"${r.content.replace(/"/g, '""')}"`,
|
| 250 |
-
r.score,
|
| 251 |
-
r.modality,
|
| 252 |
-
r.source,
|
| 253 |
-
r.citation || "",
|
| 254 |
-
])
|
| 255 |
-
|
| 256 |
-
const csv = [headers.join(","), ...rows.map(r => r.join(","))].join("\n")
|
| 257 |
-
const blob = new Blob([csv], { type: "text/csv" })
|
| 258 |
-
const url = URL.createObjectURL(blob)
|
| 259 |
-
const a = document.createElement("a")
|
| 260 |
-
a.href = url
|
| 261 |
-
a.download = `bioflow_results_${Date.now()}.csv`
|
| 262 |
-
a.click()
|
| 263 |
-
URL.revokeObjectURL(url)
|
| 264 |
-
}
|
| 265 |
-
|
| 266 |
-
function exportToJSON(results: SearchResult[]) {
|
| 267 |
-
const json = JSON.stringify(results, null, 2)
|
| 268 |
-
const blob = new Blob([json], { type: "application/json" })
|
| 269 |
-
const url = URL.createObjectURL(blob)
|
| 270 |
-
const a = document.createElement("a")
|
| 271 |
-
a.href = url
|
| 272 |
-
a.download = `bioflow_results_${Date.now()}.json`
|
| 273 |
-
a.click()
|
| 274 |
-
URL.revokeObjectURL(url)
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
function exportToFASTA(results: SearchResult[]) {
|
| 278 |
-
const fasta = results
|
| 279 |
-
.filter(r => r.modality === "protein")
|
| 280 |
-
.map(r => `>${r.id}\n${r.content}`)
|
| 281 |
-
.join("\n\n")
|
| 282 |
-
|
| 283 |
-
if (!fasta) {
|
| 284 |
-
alert("No protein sequences to export")
|
| 285 |
-
return
|
| 286 |
-
}
|
| 287 |
-
|
| 288 |
-
const blob = new Blob([fasta], { type: "text/plain" })
|
| 289 |
-
const url = URL.createObjectURL(blob)
|
| 290 |
-
const a = document.createElement("a")
|
| 291 |
-
a.href = url
|
| 292 |
-
a.download = `bioflow_proteins_${Date.now()}.fasta`
|
| 293 |
-
a.click()
|
| 294 |
-
URL.revokeObjectURL(url)
|
| 295 |
-
}
|
| 296 |
-
|
| 297 |
-
// Main Visualization Page
|
| 298 |
-
export default function VisualizationPage() {
|
| 299 |
-
const [isLoading, setIsLoading] = React.useState(false)
|
| 300 |
-
const [query, setQuery] = React.useState("")
|
| 301 |
-
const [points, setPoints] = React.useState<EmbeddingPoint[]>([])
|
| 302 |
-
const [results, setResults] = React.useState<SearchResult[]>([])
|
| 303 |
-
const [selectedPoint, setSelectedPoint] = React.useState<EmbeddingPoint | null>(null)
|
| 304 |
-
const [selectedResult, setSelectedResult] = React.useState<SearchResult | null>(null)
|
| 305 |
-
const [rotation, setRotation] = React.useState({ x: 15, y: 30 })
|
| 306 |
-
const [zoom, setZoom] = React.useState(1)
|
| 307 |
-
const [modalityFilter, setModalityFilter] = React.useState("all")
|
| 308 |
-
const [isDragging, setIsDragging] = React.useState(false)
|
| 309 |
-
const [lastMousePos, setLastMousePos] = React.useState({ x: 0, y: 0 })
|
| 310 |
-
|
| 311 |
-
// Handle mouse drag for rotation
|
| 312 |
-
const handleMouseDown = (e: React.MouseEvent) => {
|
| 313 |
-
setIsDragging(true)
|
| 314 |
-
setLastMousePos({ x: e.clientX, y: e.clientY })
|
| 315 |
-
}
|
| 316 |
-
|
| 317 |
-
const handleMouseMove = (e: React.MouseEvent) => {
|
| 318 |
-
if (!isDragging) return
|
| 319 |
-
const dx = e.clientX - lastMousePos.x
|
| 320 |
-
const dy = e.clientY - lastMousePos.y
|
| 321 |
-
setRotation(prev => ({
|
| 322 |
-
x: prev.x + dy * 0.5,
|
| 323 |
-
y: prev.y + dx * 0.5,
|
| 324 |
-
}))
|
| 325 |
-
setLastMousePos({ x: e.clientX, y: e.clientY })
|
| 326 |
-
}
|
| 327 |
-
|
| 328 |
-
const handleMouseUp = () => {
|
| 329 |
-
setIsDragging(false)
|
| 330 |
-
}
|
| 331 |
-
|
| 332 |
-
// Search and load embeddings
|
| 333 |
-
const handleSearch = async () => {
|
| 334 |
-
if (!query.trim()) return
|
| 335 |
-
|
| 336 |
-
setIsLoading(true)
|
| 337 |
-
try {
|
| 338 |
-
const response = await fetch("/api/search", {
|
| 339 |
-
method: "POST",
|
| 340 |
-
headers: { "Content-Type": "application/json" },
|
| 341 |
-
body: JSON.stringify({
|
| 342 |
-
query,
|
| 343 |
-
top_k: 50,
|
| 344 |
-
use_mmr: true,
|
| 345 |
-
}),
|
| 346 |
-
})
|
| 347 |
-
|
| 348 |
-
const data = await response.json()
|
| 349 |
-
|
| 350 |
-
if (data.results) {
|
| 351 |
-
// Convert to points with random 3D positions (in real use, these would come from UMAP/t-SNE)
|
| 352 |
-
const newPoints: EmbeddingPoint[] = data.results.map((r: SearchResult, idx: number) => {
|
| 353 |
-
// Create pseudo-3D positions based on score and index
|
| 354 |
-
const angle = (idx / data.results.length) * Math.PI * 2
|
| 355 |
-
const radius = 1 - r.score
|
| 356 |
-
return {
|
| 357 |
-
id: r.id || `point-${idx}`,
|
| 358 |
-
x: Math.cos(angle) * radius + (Math.random() - 0.5) * 0.3,
|
| 359 |
-
y: r.score * 2 - 1 + (Math.random() - 0.5) * 0.2,
|
| 360 |
-
z: Math.sin(angle) * radius + (Math.random() - 0.5) * 0.3,
|
| 361 |
-
label: r.content.slice(0, 50) + "...",
|
| 362 |
-
content: r.content,
|
| 363 |
-
modality: r.modality,
|
| 364 |
-
source: r.source,
|
| 365 |
-
score: r.score,
|
| 366 |
-
}
|
| 367 |
-
})
|
| 368 |
-
|
| 369 |
-
setPoints(newPoints)
|
| 370 |
-
setResults(data.results)
|
| 371 |
-
}
|
| 372 |
-
} catch (err) {
|
| 373 |
-
console.error("Search failed:", err)
|
| 374 |
-
} finally {
|
| 375 |
-
setIsLoading(false)
|
| 376 |
-
}
|
| 377 |
-
}
|
| 378 |
-
|
| 379 |
-
// Filter points by modality
|
| 380 |
-
const filteredPoints = modalityFilter === "all"
|
| 381 |
-
? points
|
| 382 |
-
: points.filter(p => p.modality === modalityFilter)
|
| 383 |
-
|
| 384 |
-
// Handle point selection
|
| 385 |
-
const handleSelectPoint = (point: EmbeddingPoint | null) => {
|
| 386 |
-
setSelectedPoint(point)
|
| 387 |
-
if (point) {
|
| 388 |
-
const result = results.find(r => r.id === point.id || r.content === point.content)
|
| 389 |
-
setSelectedResult(result || null)
|
| 390 |
-
} else {
|
| 391 |
-
setSelectedResult(null)
|
| 392 |
-
}
|
| 393 |
-
}
|
| 394 |
-
|
| 395 |
-
return (
|
| 396 |
-
<div className="container mx-auto p-6 space-y-6">
|
| 397 |
-
{/* Header */}
|
| 398 |
-
<div className="flex flex-col space-y-2">
|
| 399 |
-
<h1 className="text-3xl font-bold tracking-tight">3D Embedding Explorer</h1>
|
| 400 |
-
<p className="text-muted-foreground">
|
| 401 |
-
Visualize and explore multimodal embeddings in 3D space. Search, filter, and examine evidence trails.
|
| 402 |
-
</p>
|
| 403 |
-
</div>
|
| 404 |
-
|
| 405 |
-
{/* Search Bar */}
|
| 406 |
-
<Card>
|
| 407 |
-
<CardContent className="pt-6">
|
| 408 |
-
<div className="flex gap-4">
|
| 409 |
-
<div className="flex-1">
|
| 410 |
-
<Input
|
| 411 |
-
placeholder="Search for molecules, proteins, or literature..."
|
| 412 |
-
value={query}
|
| 413 |
-
onChange={(e) => setQuery(e.target.value)}
|
| 414 |
-
onKeyDown={(e) => e.key === "Enter" && handleSearch()}
|
| 415 |
-
/>
|
| 416 |
-
</div>
|
| 417 |
-
<Button onClick={handleSearch} disabled={isLoading}>
|
| 418 |
-
{isLoading ? (
|
| 419 |
-
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
| 420 |
-
) : (
|
| 421 |
-
<Search className="h-4 w-4 mr-2" />
|
| 422 |
-
)}
|
| 423 |
-
Search
|
| 424 |
-
</Button>
|
| 425 |
-
</div>
|
| 426 |
-
</CardContent>
|
| 427 |
-
</Card>
|
| 428 |
-
|
| 429 |
-
{/* Main Content */}
|
| 430 |
-
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
| 431 |
-
{/* Controls */}
|
| 432 |
-
<Card className="lg:col-span-1">
|
| 433 |
-
<CardHeader>
|
| 434 |
-
<CardTitle className="text-lg">Controls</CardTitle>
|
| 435 |
-
</CardHeader>
|
| 436 |
-
<CardContent className="space-y-6">
|
| 437 |
-
{/* Modality Filter */}
|
| 438 |
-
<div className="space-y-2">
|
| 439 |
-
<Label>Filter by Modality</Label>
|
| 440 |
-
<Select value={modalityFilter} onValueChange={setModalityFilter}>
|
| 441 |
-
<SelectTrigger>
|
| 442 |
-
<SelectValue />
|
| 443 |
-
</SelectTrigger>
|
| 444 |
-
<SelectContent>
|
| 445 |
-
<SelectItem value="all">All</SelectItem>
|
| 446 |
-
<SelectItem value="text">Text</SelectItem>
|
| 447 |
-
<SelectItem value="molecule">Molecule</SelectItem>
|
| 448 |
-
<SelectItem value="protein">Protein</SelectItem>
|
| 449 |
-
</SelectContent>
|
| 450 |
-
</Select>
|
| 451 |
-
</div>
|
| 452 |
-
|
| 453 |
-
{/* Zoom */}
|
| 454 |
-
<div className="space-y-2">
|
| 455 |
-
<Label>Zoom: {zoom.toFixed(1)}x</Label>
|
| 456 |
-
<div className="flex items-center gap-2">
|
| 457 |
-
<ZoomOut className="h-4 w-4 text-muted-foreground" />
|
| 458 |
-
<Slider
|
| 459 |
-
value={[zoom]}
|
| 460 |
-
onValueChange={([v]) => setZoom(v)}
|
| 461 |
-
min={0.5}
|
| 462 |
-
max={3}
|
| 463 |
-
step={0.1}
|
| 464 |
-
className="flex-1"
|
| 465 |
-
/>
|
| 466 |
-
<ZoomIn className="h-4 w-4 text-muted-foreground" />
|
| 467 |
-
</div>
|
| 468 |
-
</div>
|
| 469 |
-
|
| 470 |
-
{/* Rotation controls */}
|
| 471 |
-
<div className="space-y-2">
|
| 472 |
-
<Label>Rotation X: {rotation.x.toFixed(0)}°</Label>
|
| 473 |
-
<Slider
|
| 474 |
-
value={[rotation.x]}
|
| 475 |
-
onValueChange={([v]) => setRotation(prev => ({ ...prev, x: v }))}
|
| 476 |
-
min={-180}
|
| 477 |
-
max={180}
|
| 478 |
-
step={1}
|
| 479 |
-
/>
|
| 480 |
-
</div>
|
| 481 |
-
|
| 482 |
-
<div className="space-y-2">
|
| 483 |
-
<Label>Rotation Y: {rotation.y.toFixed(0)}°</Label>
|
| 484 |
-
<Slider
|
| 485 |
-
value={[rotation.y]}
|
| 486 |
-
onValueChange={([v]) => setRotation(prev => ({ ...prev, y: v }))}
|
| 487 |
-
min={-180}
|
| 488 |
-
max={180}
|
| 489 |
-
step={1}
|
| 490 |
-
/>
|
| 491 |
-
</div>
|
| 492 |
-
|
| 493 |
-
{/* Reset */}
|
| 494 |
-
<Button
|
| 495 |
-
variant="outline"
|
| 496 |
-
className="w-full"
|
| 497 |
-
onClick={() => {
|
| 498 |
-
setRotation({ x: 15, y: 30 })
|
| 499 |
-
setZoom(1)
|
| 500 |
-
}}
|
| 501 |
-
>
|
| 502 |
-
<RotateCcw className="h-4 w-4 mr-2" />
|
| 503 |
-
Reset View
|
| 504 |
-
</Button>
|
| 505 |
-
|
| 506 |
-
{/* Export */}
|
| 507 |
-
<div className="space-y-2">
|
| 508 |
-
<Label>Export Results</Label>
|
| 509 |
-
<div className="flex gap-2">
|
| 510 |
-
<Button
|
| 511 |
-
size="sm"
|
| 512 |
-
variant="outline"
|
| 513 |
-
onClick={() => exportToCSV(results)}
|
| 514 |
-
disabled={results.length === 0}
|
| 515 |
-
>
|
| 516 |
-
CSV
|
| 517 |
-
</Button>
|
| 518 |
-
<Button
|
| 519 |
-
size="sm"
|
| 520 |
-
variant="outline"
|
| 521 |
-
onClick={() => exportToJSON(results)}
|
| 522 |
-
disabled={results.length === 0}
|
| 523 |
-
>
|
| 524 |
-
JSON
|
| 525 |
-
</Button>
|
| 526 |
-
<Button
|
| 527 |
-
size="sm"
|
| 528 |
-
variant="outline"
|
| 529 |
-
onClick={() => exportToFASTA(results)}
|
| 530 |
-
disabled={results.length === 0}
|
| 531 |
-
>
|
| 532 |
-
FASTA
|
| 533 |
-
</Button>
|
| 534 |
-
</div>
|
| 535 |
-
</div>
|
| 536 |
-
|
| 537 |
-
{/* Stats */}
|
| 538 |
-
<div className="pt-4 border-t">
|
| 539 |
-
<div className="text-sm text-muted-foreground space-y-1">
|
| 540 |
-
<p>Points: {filteredPoints.length}</p>
|
| 541 |
-
<p>Results: {results.length}</p>
|
| 542 |
-
</div>
|
| 543 |
-
</div>
|
| 544 |
-
</CardContent>
|
| 545 |
-
</Card>
|
| 546 |
-
|
| 547 |
-
{/* 3D View */}
|
| 548 |
-
<div
|
| 549 |
-
className="lg:col-span-2"
|
| 550 |
-
onMouseDown={handleMouseDown}
|
| 551 |
-
onMouseMove={handleMouseMove}
|
| 552 |
-
onMouseUp={handleMouseUp}
|
| 553 |
-
onMouseLeave={handleMouseUp}
|
| 554 |
-
>
|
| 555 |
-
<Card>
|
| 556 |
-
<CardHeader className="pb-2">
|
| 557 |
-
<CardTitle className="text-lg">Embedding Space</CardTitle>
|
| 558 |
-
<CardDescription>
|
| 559 |
-
Drag to rotate • Click points for details
|
| 560 |
-
</CardDescription>
|
| 561 |
-
</CardHeader>
|
| 562 |
-
<CardContent className="p-4">
|
| 563 |
-
<Scatter3DCanvas
|
| 564 |
-
points={filteredPoints}
|
| 565 |
-
selectedPoint={selectedPoint}
|
| 566 |
-
onSelectPoint={handleSelectPoint}
|
| 567 |
-
rotation={rotation}
|
| 568 |
-
zoom={zoom}
|
| 569 |
-
/>
|
| 570 |
-
</CardContent>
|
| 571 |
-
</Card>
|
| 572 |
-
</div>
|
| 573 |
-
|
| 574 |
-
{/* Evidence Panel */}
|
| 575 |
-
<div className="lg:col-span-1">
|
| 576 |
-
<EvidencePanel result={selectedResult} />
|
| 577 |
-
</div>
|
| 578 |
-
</div>
|
| 579 |
-
</div>
|
| 580 |
-
)
|
| 581 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/app/dashboard/workflow/page.tsx
DELETED
|
@@ -1,762 +0,0 @@
|
|
| 1 |
-
"use client"
|
| 2 |
-
|
| 3 |
-
import * as React from "react"
|
| 4 |
-
import {
|
| 5 |
-
ArrowRight,
|
| 6 |
-
Check,
|
| 7 |
-
ChevronDown,
|
| 8 |
-
ChevronRight,
|
| 9 |
-
Download,
|
| 10 |
-
Loader2,
|
| 11 |
-
Play,
|
| 12 |
-
Plus,
|
| 13 |
-
Settings,
|
| 14 |
-
Sparkles,
|
| 15 |
-
Trash2,
|
| 16 |
-
Upload,
|
| 17 |
-
X,
|
| 18 |
-
} from "lucide-react"
|
| 19 |
-
|
| 20 |
-
import { Button } from "@/components/ui/button"
|
| 21 |
-
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
| 22 |
-
import { Input } from "@/components/ui/input"
|
| 23 |
-
import { Label } from "@/components/ui/label"
|
| 24 |
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
| 25 |
-
import { Textarea } from "@/components/ui/textarea"
|
| 26 |
-
import { Badge } from "@/components/ui/badge"
|
| 27 |
-
import { ScrollArea } from "@/components/ui/scroll-area"
|
| 28 |
-
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
|
| 29 |
-
import { Slider } from "@/components/ui/slider"
|
| 30 |
-
import { Switch } from "@/components/ui/switch"
|
| 31 |
-
import { Progress } from "@/components/ui/progress"
|
| 32 |
-
|
| 33 |
-
// Types
|
| 34 |
-
interface WorkflowStep {
|
| 35 |
-
id: string
|
| 36 |
-
type: "generate" | "validate" | "rank"
|
| 37 |
-
name: string
|
| 38 |
-
config: Record<string, unknown>
|
| 39 |
-
status: "pending" | "running" | "completed" | "error"
|
| 40 |
-
result?: unknown
|
| 41 |
-
error?: string
|
| 42 |
-
}
|
| 43 |
-
|
| 44 |
-
interface WorkflowResult {
|
| 45 |
-
candidates: Array<{
|
| 46 |
-
smiles: string
|
| 47 |
-
name: string
|
| 48 |
-
validation: {
|
| 49 |
-
is_valid: boolean
|
| 50 |
-
checks: Record<string, boolean>
|
| 51 |
-
properties: Record<string, number>
|
| 52 |
-
}
|
| 53 |
-
score: number
|
| 54 |
-
}>
|
| 55 |
-
steps_completed: number
|
| 56 |
-
total_time_ms: number
|
| 57 |
-
}
|
| 58 |
-
|
| 59 |
-
// Step Configuration Components
|
| 60 |
-
function GenerateStepConfig({
|
| 61 |
-
config,
|
| 62 |
-
onChange,
|
| 63 |
-
}: {
|
| 64 |
-
config: Record<string, unknown>
|
| 65 |
-
onChange: (config: Record<string, unknown>) => void
|
| 66 |
-
}) {
|
| 67 |
-
return (
|
| 68 |
-
<div className="space-y-4">
|
| 69 |
-
<div className="space-y-2">
|
| 70 |
-
<Label>Mode</Label>
|
| 71 |
-
<Select
|
| 72 |
-
value={(config.mode as string) || "text"}
|
| 73 |
-
onValueChange={(v) => onChange({ ...config, mode: v })}
|
| 74 |
-
>
|
| 75 |
-
<SelectTrigger>
|
| 76 |
-
<SelectValue />
|
| 77 |
-
</SelectTrigger>
|
| 78 |
-
<SelectContent>
|
| 79 |
-
<SelectItem value="text">Text-to-Molecule</SelectItem>
|
| 80 |
-
<SelectItem value="mutate">Mutation-Based</SelectItem>
|
| 81 |
-
<SelectItem value="scaffold">Scaffold-Based</SelectItem>
|
| 82 |
-
</SelectContent>
|
| 83 |
-
</Select>
|
| 84 |
-
</div>
|
| 85 |
-
|
| 86 |
-
<div className="space-y-2">
|
| 87 |
-
<Label>Prompt / SMILES</Label>
|
| 88 |
-
<Textarea
|
| 89 |
-
placeholder={
|
| 90 |
-
config.mode === "text"
|
| 91 |
-
? "Describe the molecule you want to generate..."
|
| 92 |
-
: config.mode === "mutate"
|
| 93 |
-
? "Enter a SMILES string to mutate..."
|
| 94 |
-
: "Enter a scaffold SMILES..."
|
| 95 |
-
}
|
| 96 |
-
value={(config.prompt as string) || ""}
|
| 97 |
-
onChange={(e) => onChange({ ...config, prompt: e.target.value })}
|
| 98 |
-
className="h-24"
|
| 99 |
-
/>
|
| 100 |
-
</div>
|
| 101 |
-
|
| 102 |
-
<div className="space-y-2">
|
| 103 |
-
<Label>Number to Generate: {config.num_candidates || 5}</Label>
|
| 104 |
-
<Slider
|
| 105 |
-
value={[Number(config.num_candidates) || 5]}
|
| 106 |
-
onValueChange={([v]) => onChange({ ...config, num_candidates: v })}
|
| 107 |
-
min={1}
|
| 108 |
-
max={20}
|
| 109 |
-
step={1}
|
| 110 |
-
/>
|
| 111 |
-
</div>
|
| 112 |
-
</div>
|
| 113 |
-
)
|
| 114 |
-
}
|
| 115 |
-
|
| 116 |
-
function ValidateStepConfig({
|
| 117 |
-
config,
|
| 118 |
-
onChange,
|
| 119 |
-
}: {
|
| 120 |
-
config: Record<string, unknown>
|
| 121 |
-
onChange: (config: Record<string, unknown>) => void
|
| 122 |
-
}) {
|
| 123 |
-
const checks = (config.checks as string[]) || ["lipinski", "admet", "qed", "alerts"]
|
| 124 |
-
|
| 125 |
-
const toggleCheck = (check: string) => {
|
| 126 |
-
const newChecks = checks.includes(check)
|
| 127 |
-
? checks.filter((c) => c !== check)
|
| 128 |
-
: [...checks, check]
|
| 129 |
-
onChange({ ...config, checks: newChecks })
|
| 130 |
-
}
|
| 131 |
-
|
| 132 |
-
return (
|
| 133 |
-
<div className="space-y-4">
|
| 134 |
-
<Label>Validation Checks</Label>
|
| 135 |
-
<div className="grid grid-cols-2 gap-2">
|
| 136 |
-
{[
|
| 137 |
-
{ id: "lipinski", label: "Lipinski Rule of 5" },
|
| 138 |
-
{ id: "admet", label: "ADMET Properties" },
|
| 139 |
-
{ id: "qed", label: "QED Score" },
|
| 140 |
-
{ id: "alerts", label: "Structural Alerts" },
|
| 141 |
-
].map((check) => (
|
| 142 |
-
<div
|
| 143 |
-
key={check.id}
|
| 144 |
-
className={`flex items-center gap-2 p-2 rounded-lg border cursor-pointer transition-colors ${
|
| 145 |
-
checks.includes(check.id)
|
| 146 |
-
? "border-primary bg-primary/10"
|
| 147 |
-
: "border-border hover:border-primary/50"
|
| 148 |
-
}`}
|
| 149 |
-
onClick={() => toggleCheck(check.id)}
|
| 150 |
-
>
|
| 151 |
-
<div
|
| 152 |
-
className={`w-4 h-4 rounded border flex items-center justify-center ${
|
| 153 |
-
checks.includes(check.id) ? "bg-primary border-primary" : "border-muted-foreground"
|
| 154 |
-
}`}
|
| 155 |
-
>
|
| 156 |
-
{checks.includes(check.id) && <Check className="h-3 w-3 text-primary-foreground" />}
|
| 157 |
-
</div>
|
| 158 |
-
<span className="text-sm">{check.label}</span>
|
| 159 |
-
</div>
|
| 160 |
-
))}
|
| 161 |
-
</div>
|
| 162 |
-
|
| 163 |
-
<div className="flex items-center justify-between">
|
| 164 |
-
<Label>Strict Mode</Label>
|
| 165 |
-
<Switch
|
| 166 |
-
checked={Boolean(config.strict)}
|
| 167 |
-
onCheckedChange={(v) => onChange({ ...config, strict: v })}
|
| 168 |
-
/>
|
| 169 |
-
</div>
|
| 170 |
-
</div>
|
| 171 |
-
)
|
| 172 |
-
}
|
| 173 |
-
|
| 174 |
-
function RankStepConfig({
|
| 175 |
-
config,
|
| 176 |
-
onChange,
|
| 177 |
-
}: {
|
| 178 |
-
config: Record<string, unknown>
|
| 179 |
-
onChange: (config: Record<string, unknown>) => void
|
| 180 |
-
}) {
|
| 181 |
-
const weights = (config.weights as Record<string, number>) || {
|
| 182 |
-
qed: 0.3,
|
| 183 |
-
validity: 0.3,
|
| 184 |
-
mw: 0.2,
|
| 185 |
-
logp: 0.2,
|
| 186 |
-
}
|
| 187 |
-
|
| 188 |
-
const updateWeight = (key: string, value: number) => {
|
| 189 |
-
onChange({ ...config, weights: { ...weights, [key]: value } })
|
| 190 |
-
}
|
| 191 |
-
|
| 192 |
-
return (
|
| 193 |
-
<div className="space-y-4">
|
| 194 |
-
<Label>Ranking Weights</Label>
|
| 195 |
-
|
| 196 |
-
{Object.entries({
|
| 197 |
-
qed: "QED Score",
|
| 198 |
-
validity: "Validity",
|
| 199 |
-
mw: "Molecular Weight",
|
| 200 |
-
logp: "LogP",
|
| 201 |
-
}).map(([key, label]) => (
|
| 202 |
-
<div key={key} className="space-y-1">
|
| 203 |
-
<div className="flex justify-between text-sm">
|
| 204 |
-
<span>{label}</span>
|
| 205 |
-
<span className="text-muted-foreground">{(weights[key] || 0).toFixed(2)}</span>
|
| 206 |
-
</div>
|
| 207 |
-
<Slider
|
| 208 |
-
value={[weights[key] || 0]}
|
| 209 |
-
onValueChange={([v]) => updateWeight(key, v)}
|
| 210 |
-
min={0}
|
| 211 |
-
max={1}
|
| 212 |
-
step={0.05}
|
| 213 |
-
/>
|
| 214 |
-
</div>
|
| 215 |
-
))}
|
| 216 |
-
|
| 217 |
-
<div className="space-y-2">
|
| 218 |
-
<Label>Top K: {config.top_k || 5}</Label>
|
| 219 |
-
<Slider
|
| 220 |
-
value={[Number(config.top_k) || 5]}
|
| 221 |
-
onValueChange={([v]) => onChange({ ...config, top_k: v })}
|
| 222 |
-
min={1}
|
| 223 |
-
max={20}
|
| 224 |
-
step={1}
|
| 225 |
-
/>
|
| 226 |
-
</div>
|
| 227 |
-
</div>
|
| 228 |
-
)
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
// Step Card Component
|
| 232 |
-
function StepCard({
|
| 233 |
-
step,
|
| 234 |
-
index,
|
| 235 |
-
onUpdate,
|
| 236 |
-
onRemove,
|
| 237 |
-
isLast,
|
| 238 |
-
}: {
|
| 239 |
-
step: WorkflowStep
|
| 240 |
-
index: number
|
| 241 |
-
onUpdate: (step: WorkflowStep) => void
|
| 242 |
-
onRemove: () => void
|
| 243 |
-
isLast: boolean
|
| 244 |
-
}) {
|
| 245 |
-
const [isExpanded, setIsExpanded] = React.useState(true)
|
| 246 |
-
|
| 247 |
-
const getStepIcon = () => {
|
| 248 |
-
switch (step.type) {
|
| 249 |
-
case "generate":
|
| 250 |
-
return <Sparkles className="h-4 w-4" />
|
| 251 |
-
case "validate":
|
| 252 |
-
return <Check className="h-4 w-4" />
|
| 253 |
-
case "rank":
|
| 254 |
-
return <Settings className="h-4 w-4" />
|
| 255 |
-
}
|
| 256 |
-
}
|
| 257 |
-
|
| 258 |
-
const getStatusBadge = () => {
|
| 259 |
-
switch (step.status) {
|
| 260 |
-
case "pending":
|
| 261 |
-
return <Badge variant="outline">Pending</Badge>
|
| 262 |
-
case "running":
|
| 263 |
-
return <Badge className="bg-blue-500">Running</Badge>
|
| 264 |
-
case "completed":
|
| 265 |
-
return <Badge className="bg-green-500">Completed</Badge>
|
| 266 |
-
case "error":
|
| 267 |
-
return <Badge variant="destructive">Error</Badge>
|
| 268 |
-
}
|
| 269 |
-
}
|
| 270 |
-
|
| 271 |
-
return (
|
| 272 |
-
<div className="relative">
|
| 273 |
-
<Card className={step.status === "running" ? "border-blue-500 shadow-lg" : ""}>
|
| 274 |
-
<CardHeader className="py-3">
|
| 275 |
-
<div className="flex items-center justify-between">
|
| 276 |
-
<div className="flex items-center gap-3">
|
| 277 |
-
<div className="flex items-center justify-center w-8 h-8 rounded-full bg-primary/10 text-primary">
|
| 278 |
-
{index + 1}
|
| 279 |
-
</div>
|
| 280 |
-
<div className="flex items-center gap-2">
|
| 281 |
-
{getStepIcon()}
|
| 282 |
-
<span className="font-medium">{step.name}</span>
|
| 283 |
-
</div>
|
| 284 |
-
{getStatusBadge()}
|
| 285 |
-
</div>
|
| 286 |
-
<div className="flex items-center gap-2">
|
| 287 |
-
<Button
|
| 288 |
-
size="sm"
|
| 289 |
-
variant="ghost"
|
| 290 |
-
onClick={() => setIsExpanded(!isExpanded)}
|
| 291 |
-
>
|
| 292 |
-
{isExpanded ? (
|
| 293 |
-
<ChevronDown className="h-4 w-4" />
|
| 294 |
-
) : (
|
| 295 |
-
<ChevronRight className="h-4 w-4" />
|
| 296 |
-
)}
|
| 297 |
-
</Button>
|
| 298 |
-
<Button
|
| 299 |
-
size="sm"
|
| 300 |
-
variant="ghost"
|
| 301 |
-
onClick={onRemove}
|
| 302 |
-
className="text-destructive hover:text-destructive"
|
| 303 |
-
>
|
| 304 |
-
<Trash2 className="h-4 w-4" />
|
| 305 |
-
</Button>
|
| 306 |
-
</div>
|
| 307 |
-
</div>
|
| 308 |
-
</CardHeader>
|
| 309 |
-
|
| 310 |
-
{isExpanded && (
|
| 311 |
-
<CardContent className="pt-0">
|
| 312 |
-
{step.type === "generate" && (
|
| 313 |
-
<GenerateStepConfig
|
| 314 |
-
config={step.config}
|
| 315 |
-
onChange={(config) => onUpdate({ ...step, config })}
|
| 316 |
-
/>
|
| 317 |
-
)}
|
| 318 |
-
{step.type === "validate" && (
|
| 319 |
-
<ValidateStepConfig
|
| 320 |
-
config={step.config}
|
| 321 |
-
onChange={(config) => onUpdate({ ...step, config })}
|
| 322 |
-
/>
|
| 323 |
-
)}
|
| 324 |
-
{step.type === "rank" && (
|
| 325 |
-
<RankStepConfig
|
| 326 |
-
config={step.config}
|
| 327 |
-
onChange={(config) => onUpdate({ ...step, config })}
|
| 328 |
-
/>
|
| 329 |
-
)}
|
| 330 |
-
|
| 331 |
-
{step.error && (
|
| 332 |
-
<div className="mt-4 p-3 bg-destructive/10 text-destructive rounded-lg text-sm">
|
| 333 |
-
{step.error}
|
| 334 |
-
</div>
|
| 335 |
-
)}
|
| 336 |
-
</CardContent>
|
| 337 |
-
)}
|
| 338 |
-
</Card>
|
| 339 |
-
|
| 340 |
-
{/* Arrow connector */}
|
| 341 |
-
{!isLast && (
|
| 342 |
-
<div className="flex justify-center py-2">
|
| 343 |
-
<ArrowRight className="h-6 w-6 text-muted-foreground" />
|
| 344 |
-
</div>
|
| 345 |
-
)}
|
| 346 |
-
</div>
|
| 347 |
-
)
|
| 348 |
-
}
|
| 349 |
-
|
| 350 |
-
// Results Display
|
| 351 |
-
function WorkflowResults({ result }: { result: WorkflowResult | null }) {
|
| 352 |
-
if (!result) return null
|
| 353 |
-
|
| 354 |
-
return (
|
| 355 |
-
<Card>
|
| 356 |
-
<CardHeader>
|
| 357 |
-
<div className="flex items-center justify-between">
|
| 358 |
-
<CardTitle className="text-lg">Results</CardTitle>
|
| 359 |
-
<Badge variant="outline">
|
| 360 |
-
{result.total_time_ms.toFixed(0)}ms
|
| 361 |
-
</Badge>
|
| 362 |
-
</div>
|
| 363 |
-
<CardDescription>
|
| 364 |
-
{result.candidates.length} candidates generated • {result.steps_completed} steps completed
|
| 365 |
-
</CardDescription>
|
| 366 |
-
</CardHeader>
|
| 367 |
-
<CardContent>
|
| 368 |
-
<ScrollArea className="h-[300px]">
|
| 369 |
-
<div className="space-y-3">
|
| 370 |
-
{result.candidates.map((candidate, idx) => (
|
| 371 |
-
<div
|
| 372 |
-
key={idx}
|
| 373 |
-
className="p-3 rounded-lg border bg-card"
|
| 374 |
-
>
|
| 375 |
-
<div className="flex items-start justify-between">
|
| 376 |
-
<div className="space-y-1">
|
| 377 |
-
<div className="flex items-center gap-2">
|
| 378 |
-
<span className="font-medium">{candidate.name}</span>
|
| 379 |
-
<Badge variant={candidate.validation.is_valid ? "default" : "destructive"}>
|
| 380 |
-
{candidate.validation.is_valid ? "Valid" : "Invalid"}
|
| 381 |
-
</Badge>
|
| 382 |
-
</div>
|
| 383 |
-
<code className="text-xs text-muted-foreground block max-w-md truncate">
|
| 384 |
-
{candidate.smiles}
|
| 385 |
-
</code>
|
| 386 |
-
</div>
|
| 387 |
-
<div className="text-right">
|
| 388 |
-
<div className="text-lg font-bold">{candidate.score.toFixed(3)}</div>
|
| 389 |
-
<span className="text-xs text-muted-foreground">Score</span>
|
| 390 |
-
</div>
|
| 391 |
-
</div>
|
| 392 |
-
|
| 393 |
-
{/* Properties */}
|
| 394 |
-
<div className="mt-2 flex gap-4 text-xs text-muted-foreground">
|
| 395 |
-
{Object.entries(candidate.validation.properties).map(([key, value]) => (
|
| 396 |
-
<span key={key}>
|
| 397 |
-
{key}: {typeof value === "number" ? value.toFixed(2) : value}
|
| 398 |
-
</span>
|
| 399 |
-
))}
|
| 400 |
-
</div>
|
| 401 |
-
</div>
|
| 402 |
-
))}
|
| 403 |
-
</div>
|
| 404 |
-
</ScrollArea>
|
| 405 |
-
</CardContent>
|
| 406 |
-
</Card>
|
| 407 |
-
)
|
| 408 |
-
}
|
| 409 |
-
|
| 410 |
-
// Main Workflow Builder Page
|
| 411 |
-
export default function WorkflowBuilderPage() {
|
| 412 |
-
const [steps, setSteps] = React.useState<WorkflowStep[]>([
|
| 413 |
-
{
|
| 414 |
-
id: "gen-1",
|
| 415 |
-
type: "generate",
|
| 416 |
-
name: "Generate Molecules",
|
| 417 |
-
config: { mode: "text", prompt: "", num_candidates: 5 },
|
| 418 |
-
status: "pending",
|
| 419 |
-
},
|
| 420 |
-
{
|
| 421 |
-
id: "val-1",
|
| 422 |
-
type: "validate",
|
| 423 |
-
name: "Validate Candidates",
|
| 424 |
-
config: { checks: ["lipinski", "admet", "qed", "alerts"], strict: false },
|
| 425 |
-
status: "pending",
|
| 426 |
-
},
|
| 427 |
-
{
|
| 428 |
-
id: "rank-1",
|
| 429 |
-
type: "rank",
|
| 430 |
-
name: "Rank & Select",
|
| 431 |
-
config: { weights: { qed: 0.3, validity: 0.3, mw: 0.2, logp: 0.2 }, top_k: 5 },
|
| 432 |
-
status: "pending",
|
| 433 |
-
},
|
| 434 |
-
])
|
| 435 |
-
const [isRunning, setIsRunning] = React.useState(false)
|
| 436 |
-
const [progress, setProgress] = React.useState(0)
|
| 437 |
-
const [result, setResult] = React.useState<WorkflowResult | null>(null)
|
| 438 |
-
const [workflowName, setWorkflowName] = React.useState("My Discovery Workflow")
|
| 439 |
-
|
| 440 |
-
// Add a new step
|
| 441 |
-
const addStep = (type: WorkflowStep["type"]) => {
|
| 442 |
-
const id = `${type}-${Date.now()}`
|
| 443 |
-
const newStep: WorkflowStep = {
|
| 444 |
-
id,
|
| 445 |
-
type,
|
| 446 |
-
name: type === "generate" ? "Generate Molecules" : type === "validate" ? "Validate Candidates" : "Rank & Select",
|
| 447 |
-
config: type === "generate"
|
| 448 |
-
? { mode: "text", prompt: "", num_candidates: 5 }
|
| 449 |
-
: type === "validate"
|
| 450 |
-
? { checks: ["lipinski", "admet", "qed", "alerts"], strict: false }
|
| 451 |
-
: { weights: { qed: 0.3, validity: 0.3, mw: 0.2, logp: 0.2 }, top_k: 5 },
|
| 452 |
-
status: "pending",
|
| 453 |
-
}
|
| 454 |
-
setSteps([...steps, newStep])
|
| 455 |
-
}
|
| 456 |
-
|
| 457 |
-
// Update a step
|
| 458 |
-
const updateStep = (updatedStep: WorkflowStep) => {
|
| 459 |
-
setSteps(steps.map((s) => (s.id === updatedStep.id ? updatedStep : s)))
|
| 460 |
-
}
|
| 461 |
-
|
| 462 |
-
// Remove a step
|
| 463 |
-
const removeStep = (id: string) => {
|
| 464 |
-
setSteps(steps.filter((s) => s.id !== id))
|
| 465 |
-
}
|
| 466 |
-
|
| 467 |
-
// Run the workflow
|
| 468 |
-
const runWorkflow = async () => {
|
| 469 |
-
setIsRunning(true)
|
| 470 |
-
setProgress(0)
|
| 471 |
-
setResult(null)
|
| 472 |
-
|
| 473 |
-
// Reset all step statuses
|
| 474 |
-
setSteps(steps.map((s) => ({ ...s, status: "pending", result: undefined, error: undefined })))
|
| 475 |
-
|
| 476 |
-
try {
|
| 477 |
-
// Build workflow config from steps
|
| 478 |
-
const generateStep = steps.find((s) => s.type === "generate")
|
| 479 |
-
const validateStep = steps.find((s) => s.type === "validate")
|
| 480 |
-
const rankStep = steps.find((s) => s.type === "rank")
|
| 481 |
-
|
| 482 |
-
if (!generateStep) {
|
| 483 |
-
throw new Error("Workflow must include a generate step")
|
| 484 |
-
}
|
| 485 |
-
|
| 486 |
-
// Mark generate as running
|
| 487 |
-
setSteps((prev) =>
|
| 488 |
-
prev.map((s) => (s.id === generateStep.id ? { ...s, status: "running" } : s))
|
| 489 |
-
)
|
| 490 |
-
setProgress(10)
|
| 491 |
-
|
| 492 |
-
// Call the workflow API
|
| 493 |
-
const response = await fetch("/api/agents/workflow", {
|
| 494 |
-
method: "POST",
|
| 495 |
-
headers: { "Content-Type": "application/json" },
|
| 496 |
-
body: JSON.stringify({
|
| 497 |
-
query: generateStep.config.prompt || "drug-like molecule",
|
| 498 |
-
num_candidates: generateStep.config.num_candidates || 5,
|
| 499 |
-
top_k: rankStep?.config.top_k || 5,
|
| 500 |
-
}),
|
| 501 |
-
})
|
| 502 |
-
|
| 503 |
-
// Simulate step progression
|
| 504 |
-
setProgress(30)
|
| 505 |
-
setSteps((prev) =>
|
| 506 |
-
prev.map((s) =>
|
| 507 |
-
s.id === generateStep.id
|
| 508 |
-
? { ...s, status: "completed" }
|
| 509 |
-
: s.type === "validate"
|
| 510 |
-
? { ...s, status: "running" }
|
| 511 |
-
: s
|
| 512 |
-
)
|
| 513 |
-
)
|
| 514 |
-
|
| 515 |
-
await new Promise((r) => setTimeout(r, 500))
|
| 516 |
-
setProgress(60)
|
| 517 |
-
|
| 518 |
-
if (validateStep) {
|
| 519 |
-
setSteps((prev) =>
|
| 520 |
-
prev.map((s) =>
|
| 521 |
-
s.id === validateStep.id
|
| 522 |
-
? { ...s, status: "completed" }
|
| 523 |
-
: s.type === "rank"
|
| 524 |
-
? { ...s, status: "running" }
|
| 525 |
-
: s
|
| 526 |
-
)
|
| 527 |
-
)
|
| 528 |
-
}
|
| 529 |
-
|
| 530 |
-
await new Promise((r) => setTimeout(r, 500))
|
| 531 |
-
setProgress(90)
|
| 532 |
-
|
| 533 |
-
const data = await response.json()
|
| 534 |
-
|
| 535 |
-
if (!response.ok) {
|
| 536 |
-
throw new Error(data.detail || "Workflow failed")
|
| 537 |
-
}
|
| 538 |
-
|
| 539 |
-
// Mark all as completed
|
| 540 |
-
setSteps((prev) => prev.map((s) => ({ ...s, status: "completed" })))
|
| 541 |
-
setProgress(100)
|
| 542 |
-
|
| 543 |
-
// Transform API result to WorkflowResult
|
| 544 |
-
// API returns: success, status, steps_completed, total_steps, execution_time_ms, top_candidates, all_outputs, errors
|
| 545 |
-
const workflowResult: WorkflowResult = {
|
| 546 |
-
candidates: (data.top_candidates || []).map((c: any) => ({
|
| 547 |
-
smiles: c.smiles || '',
|
| 548 |
-
name: c.name || `Candidate ${c.rank || 0}`,
|
| 549 |
-
validation: c.validation || { is_valid: true, checks: {}, properties: {} },
|
| 550 |
-
score: c.score || 0,
|
| 551 |
-
})),
|
| 552 |
-
steps_completed: data.steps_completed || steps.length,
|
| 553 |
-
total_time_ms: data.execution_time_ms || 0,
|
| 554 |
-
}
|
| 555 |
-
|
| 556 |
-
setResult(workflowResult)
|
| 557 |
-
} catch (err) {
|
| 558 |
-
console.error("Workflow error:", err)
|
| 559 |
-
setSteps((prev) =>
|
| 560 |
-
prev.map((s) =>
|
| 561 |
-
s.status === "running"
|
| 562 |
-
? { ...s, status: "error", error: String(err) }
|
| 563 |
-
: s
|
| 564 |
-
)
|
| 565 |
-
)
|
| 566 |
-
} finally {
|
| 567 |
-
setIsRunning(false)
|
| 568 |
-
}
|
| 569 |
-
}
|
| 570 |
-
|
| 571 |
-
// Export workflow config
|
| 572 |
-
const exportWorkflow = () => {
|
| 573 |
-
const config = {
|
| 574 |
-
name: workflowName,
|
| 575 |
-
steps: steps.map((s) => ({
|
| 576 |
-
type: s.type,
|
| 577 |
-
name: s.name,
|
| 578 |
-
config: s.config,
|
| 579 |
-
})),
|
| 580 |
-
}
|
| 581 |
-
const json = JSON.stringify(config, null, 2)
|
| 582 |
-
const blob = new Blob([json], { type: "application/json" })
|
| 583 |
-
const url = URL.createObjectURL(blob)
|
| 584 |
-
const a = document.createElement("a")
|
| 585 |
-
a.href = url
|
| 586 |
-
a.download = `${workflowName.replace(/\s+/g, "_").toLowerCase()}.json`
|
| 587 |
-
a.click()
|
| 588 |
-
URL.revokeObjectURL(url)
|
| 589 |
-
}
|
| 590 |
-
|
| 591 |
-
// Import workflow config
|
| 592 |
-
const importWorkflow = () => {
|
| 593 |
-
const input = document.createElement("input")
|
| 594 |
-
input.type = "file"
|
| 595 |
-
input.accept = ".json"
|
| 596 |
-
input.onchange = async (e) => {
|
| 597 |
-
const file = (e.target as HTMLInputElement).files?.[0]
|
| 598 |
-
if (!file) return
|
| 599 |
-
|
| 600 |
-
const text = await file.text()
|
| 601 |
-
try {
|
| 602 |
-
const config = JSON.parse(text)
|
| 603 |
-
setWorkflowName(config.name || "Imported Workflow")
|
| 604 |
-
setSteps(
|
| 605 |
-
config.steps.map((s: Record<string, unknown>, idx: number) => ({
|
| 606 |
-
id: `${s.type}-${idx}`,
|
| 607 |
-
type: s.type,
|
| 608 |
-
name: s.name,
|
| 609 |
-
config: s.config,
|
| 610 |
-
status: "pending",
|
| 611 |
-
}))
|
| 612 |
-
)
|
| 613 |
-
} catch (err) {
|
| 614 |
-
console.error("Failed to import workflow:", err)
|
| 615 |
-
}
|
| 616 |
-
}
|
| 617 |
-
input.click()
|
| 618 |
-
}
|
| 619 |
-
|
| 620 |
-
return (
|
| 621 |
-
<div className="container mx-auto p-6 space-y-6">
|
| 622 |
-
{/* Header */}
|
| 623 |
-
<div className="flex items-center justify-between">
|
| 624 |
-
<div className="space-y-1">
|
| 625 |
-
<h1 className="text-3xl font-bold tracking-tight">Workflow Builder</h1>
|
| 626 |
-
<p className="text-muted-foreground">
|
| 627 |
-
Design and execute drug discovery pipelines with visual configuration
|
| 628 |
-
</p>
|
| 629 |
-
</div>
|
| 630 |
-
<div className="flex items-center gap-2">
|
| 631 |
-
<Button variant="outline" onClick={importWorkflow}>
|
| 632 |
-
<Upload className="h-4 w-4 mr-2" />
|
| 633 |
-
Import
|
| 634 |
-
</Button>
|
| 635 |
-
<Button variant="outline" onClick={exportWorkflow}>
|
| 636 |
-
<Download className="h-4 w-4 mr-2" />
|
| 637 |
-
Export
|
| 638 |
-
</Button>
|
| 639 |
-
<Button onClick={runWorkflow} disabled={isRunning || steps.length === 0}>
|
| 640 |
-
{isRunning ? (
|
| 641 |
-
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
| 642 |
-
) : (
|
| 643 |
-
<Play className="h-4 w-4 mr-2" />
|
| 644 |
-
)}
|
| 645 |
-
Run Workflow
|
| 646 |
-
</Button>
|
| 647 |
-
</div>
|
| 648 |
-
</div>
|
| 649 |
-
|
| 650 |
-
{/* Workflow Name */}
|
| 651 |
-
<Card>
|
| 652 |
-
<CardContent className="pt-6">
|
| 653 |
-
<div className="flex items-center gap-4">
|
| 654 |
-
<Label className="whitespace-nowrap">Workflow Name:</Label>
|
| 655 |
-
<Input
|
| 656 |
-
value={workflowName}
|
| 657 |
-
onChange={(e) => setWorkflowName(e.target.value)}
|
| 658 |
-
className="max-w-md"
|
| 659 |
-
/>
|
| 660 |
-
</div>
|
| 661 |
-
</CardContent>
|
| 662 |
-
</Card>
|
| 663 |
-
|
| 664 |
-
{/* Progress */}
|
| 665 |
-
{isRunning && (
|
| 666 |
-
<Card>
|
| 667 |
-
<CardContent className="pt-6">
|
| 668 |
-
<div className="space-y-2">
|
| 669 |
-
<div className="flex justify-between text-sm">
|
| 670 |
-
<span>Running workflow...</span>
|
| 671 |
-
<span>{progress}%</span>
|
| 672 |
-
</div>
|
| 673 |
-
<Progress value={progress} />
|
| 674 |
-
</div>
|
| 675 |
-
</CardContent>
|
| 676 |
-
</Card>
|
| 677 |
-
)}
|
| 678 |
-
|
| 679 |
-
{/* Main Content */}
|
| 680 |
-
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
| 681 |
-
{/* Pipeline Editor */}
|
| 682 |
-
<div className="lg:col-span-2 space-y-4">
|
| 683 |
-
<Card>
|
| 684 |
-
<CardHeader>
|
| 685 |
-
<CardTitle>Pipeline Steps</CardTitle>
|
| 686 |
-
<CardDescription>
|
| 687 |
-
Configure each step of your discovery workflow
|
| 688 |
-
</CardDescription>
|
| 689 |
-
</CardHeader>
|
| 690 |
-
<CardContent className="space-y-4">
|
| 691 |
-
{steps.length === 0 ? (
|
| 692 |
-
<div className="text-center py-8 text-muted-foreground">
|
| 693 |
-
No steps added yet. Click "Add Step" to get started.
|
| 694 |
-
</div>
|
| 695 |
-
) : (
|
| 696 |
-
steps.map((step, idx) => (
|
| 697 |
-
<StepCard
|
| 698 |
-
key={step.id}
|
| 699 |
-
step={step}
|
| 700 |
-
index={idx}
|
| 701 |
-
onUpdate={updateStep}
|
| 702 |
-
onRemove={() => removeStep(step.id)}
|
| 703 |
-
isLast={idx === steps.length - 1}
|
| 704 |
-
/>
|
| 705 |
-
))
|
| 706 |
-
)}
|
| 707 |
-
|
| 708 |
-
{/* Add Step Buttons */}
|
| 709 |
-
<div className="flex gap-2 pt-4 border-t">
|
| 710 |
-
<Button
|
| 711 |
-
variant="outline"
|
| 712 |
-
size="sm"
|
| 713 |
-
onClick={() => addStep("generate")}
|
| 714 |
-
>
|
| 715 |
-
<Plus className="h-4 w-4 mr-1" />
|
| 716 |
-
Add Generate
|
| 717 |
-
</Button>
|
| 718 |
-
<Button
|
| 719 |
-
variant="outline"
|
| 720 |
-
size="sm"
|
| 721 |
-
onClick={() => addStep("validate")}
|
| 722 |
-
>
|
| 723 |
-
<Plus className="h-4 w-4 mr-1" />
|
| 724 |
-
Add Validate
|
| 725 |
-
</Button>
|
| 726 |
-
<Button
|
| 727 |
-
variant="outline"
|
| 728 |
-
size="sm"
|
| 729 |
-
onClick={() => addStep("rank")}
|
| 730 |
-
>
|
| 731 |
-
<Plus className="h-4 w-4 mr-1" />
|
| 732 |
-
Add Rank
|
| 733 |
-
</Button>
|
| 734 |
-
</div>
|
| 735 |
-
</CardContent>
|
| 736 |
-
</Card>
|
| 737 |
-
</div>
|
| 738 |
-
|
| 739 |
-
{/* Results Panel */}
|
| 740 |
-
<div className="lg:col-span-1">
|
| 741 |
-
<WorkflowResults result={result} />
|
| 742 |
-
|
| 743 |
-
{!result && (
|
| 744 |
-
<Card>
|
| 745 |
-
<CardHeader>
|
| 746 |
-
<CardTitle className="text-lg">Results</CardTitle>
|
| 747 |
-
<CardDescription>
|
| 748 |
-
Run the workflow to see results
|
| 749 |
-
</CardDescription>
|
| 750 |
-
</CardHeader>
|
| 751 |
-
<CardContent>
|
| 752 |
-
<div className="text-center py-8 text-muted-foreground">
|
| 753 |
-
Configure your pipeline and click "Run Workflow" to execute
|
| 754 |
-
</div>
|
| 755 |
-
</CardContent>
|
| 756 |
-
</Card>
|
| 757 |
-
)}
|
| 758 |
-
</div>
|
| 759 |
-
</div>
|
| 760 |
-
</div>
|
| 761 |
-
)
|
| 762 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui/components/sidebar.tsx
CHANGED
|
@@ -59,105 +59,105 @@ import { useIsMobile } from '@/hooks/use-mobile';
|
|
| 59 |
const navMain = [
|
| 60 |
{
|
| 61 |
title: 'Home',
|
| 62 |
-
url: '/
|
| 63 |
icon: Home,
|
| 64 |
isActive: true,
|
| 65 |
},
|
| 66 |
{
|
| 67 |
title: 'Visualization',
|
| 68 |
-
url: '/
|
| 69 |
icon: FlaskConical,
|
| 70 |
items: [
|
| 71 |
{
|
| 72 |
title: 'Molecules 2D',
|
| 73 |
-
url: '/
|
| 74 |
},
|
| 75 |
{
|
| 76 |
title: 'Molecules 3D',
|
| 77 |
-
url: '/
|
| 78 |
},
|
| 79 |
{
|
| 80 |
title: 'Proteins 3D',
|
| 81 |
-
url: '/
|
| 82 |
},
|
| 83 |
],
|
| 84 |
},
|
| 85 |
{
|
| 86 |
title: 'Discovery',
|
| 87 |
-
url: '/
|
| 88 |
icon: Microscope,
|
| 89 |
items: [
|
| 90 |
{
|
| 91 |
title: 'Drug Discovery',
|
| 92 |
-
url: '/
|
| 93 |
},
|
| 94 |
{
|
| 95 |
title: 'Molecule Search',
|
| 96 |
-
url: '/
|
| 97 |
},
|
| 98 |
],
|
| 99 |
},
|
| 100 |
{
|
| 101 |
title: 'Explorer',
|
| 102 |
-
url: '/
|
| 103 |
icon: Dna,
|
| 104 |
items: [
|
| 105 |
{
|
| 106 |
title: 'Embeddings',
|
| 107 |
-
url: '/
|
| 108 |
},
|
| 109 |
{
|
| 110 |
title: '3D Visualization',
|
| 111 |
-
url: '/
|
| 112 |
},
|
| 113 |
{
|
| 114 |
title: 'Predictions',
|
| 115 |
-
url: '/
|
| 116 |
},
|
| 117 |
],
|
| 118 |
},
|
| 119 |
{
|
| 120 |
title: 'Workflows',
|
| 121 |
-
url: '/
|
| 122 |
icon: Sparkles,
|
| 123 |
items: [
|
| 124 |
{
|
| 125 |
title: 'Builder',
|
| 126 |
-
url: '/
|
| 127 |
},
|
| 128 |
{
|
| 129 |
title: 'Templates',
|
| 130 |
-
url: '/
|
| 131 |
},
|
| 132 |
],
|
| 133 |
},
|
| 134 |
{
|
| 135 |
title: 'Data',
|
| 136 |
-
url: '/
|
| 137 |
icon: BarChart2,
|
| 138 |
items: [
|
| 139 |
{
|
| 140 |
title: 'Datasets',
|
| 141 |
-
url: '/
|
| 142 |
},
|
| 143 |
{
|
| 144 |
title: 'Analytics',
|
| 145 |
-
url: '/
|
| 146 |
},
|
| 147 |
],
|
| 148 |
},
|
| 149 |
{
|
| 150 |
title: 'Settings',
|
| 151 |
-
url: '/
|
| 152 |
icon: Settings,
|
| 153 |
items: [
|
| 154 |
{
|
| 155 |
title: 'General',
|
| 156 |
-
url: '/
|
| 157 |
},
|
| 158 |
{
|
| 159 |
title: 'Models',
|
| 160 |
-
url: '/
|
| 161 |
},
|
| 162 |
],
|
| 163 |
},
|
|
@@ -180,7 +180,7 @@ export function AppSidebar() {
|
|
| 180 |
<SidebarMenu>
|
| 181 |
<SidebarMenuItem>
|
| 182 |
<SidebarMenuButton size="lg" asChild>
|
| 183 |
-
<Link href="/
|
| 184 |
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
| 185 |
<Dna className="size-4" />
|
| 186 |
</div>
|
|
@@ -192,11 +192,6 @@ export function AppSidebar() {
|
|
| 192 |
</SidebarMenuButton>
|
| 193 |
</SidebarMenuItem>
|
| 194 |
</SidebarMenu>
|
| 195 |
-
{/* Theme Toggle */}
|
| 196 |
-
<div className="group-data-[collapsible=icon]:hidden px-2">
|
| 197 |
-
<ThemeToggle />
|
| 198 |
-
</div>
|
| 199 |
-
{/* App Header */}
|
| 200 |
</SidebarHeader>
|
| 201 |
|
| 202 |
<SidebarContent>
|
|
@@ -258,7 +253,10 @@ export function AppSidebar() {
|
|
| 258 |
|
| 259 |
{/* Status Section */}
|
| 260 |
<SidebarGroup className="group-data-[collapsible=icon]:hidden mt-auto">
|
| 261 |
-
<SidebarGroupLabel
|
|
|
|
|
|
|
|
|
|
| 262 |
<div className="px-3 py-2">
|
| 263 |
<div className="rounded-lg border bg-muted/50 p-3">
|
| 264 |
<div className="flex items-center gap-2 mb-2">
|
|
|
|
| 59 |
const navMain = [
|
| 60 |
{
|
| 61 |
title: 'Home',
|
| 62 |
+
url: '/',
|
| 63 |
icon: Home,
|
| 64 |
isActive: true,
|
| 65 |
},
|
| 66 |
{
|
| 67 |
title: 'Visualization',
|
| 68 |
+
url: '/molecules-2d',
|
| 69 |
icon: FlaskConical,
|
| 70 |
items: [
|
| 71 |
{
|
| 72 |
title: 'Molecules 2D',
|
| 73 |
+
url: '/molecules-2d',
|
| 74 |
},
|
| 75 |
{
|
| 76 |
title: 'Molecules 3D',
|
| 77 |
+
url: '/molecules-3d',
|
| 78 |
},
|
| 79 |
{
|
| 80 |
title: 'Proteins 3D',
|
| 81 |
+
url: '/proteins-3d',
|
| 82 |
},
|
| 83 |
],
|
| 84 |
},
|
| 85 |
{
|
| 86 |
title: 'Discovery',
|
| 87 |
+
url: '/discovery',
|
| 88 |
icon: Microscope,
|
| 89 |
items: [
|
| 90 |
{
|
| 91 |
title: 'Drug Discovery',
|
| 92 |
+
url: '/discovery',
|
| 93 |
},
|
| 94 |
{
|
| 95 |
title: 'Molecule Search',
|
| 96 |
+
url: '/discovery#search',
|
| 97 |
},
|
| 98 |
],
|
| 99 |
},
|
| 100 |
{
|
| 101 |
title: 'Explorer',
|
| 102 |
+
url: '/explorer',
|
| 103 |
icon: Dna,
|
| 104 |
items: [
|
| 105 |
{
|
| 106 |
title: 'Embeddings',
|
| 107 |
+
url: '/explorer',
|
| 108 |
},
|
| 109 |
{
|
| 110 |
title: '3D Visualization',
|
| 111 |
+
url: '/visualization',
|
| 112 |
},
|
| 113 |
{
|
| 114 |
title: 'Predictions',
|
| 115 |
+
url: '/explorer#predictions',
|
| 116 |
},
|
| 117 |
],
|
| 118 |
},
|
| 119 |
{
|
| 120 |
title: 'Workflows',
|
| 121 |
+
url: '/workflow',
|
| 122 |
icon: Sparkles,
|
| 123 |
items: [
|
| 124 |
{
|
| 125 |
title: 'Builder',
|
| 126 |
+
url: '/workflow',
|
| 127 |
},
|
| 128 |
{
|
| 129 |
title: 'Templates',
|
| 130 |
+
url: '/workflow#templates',
|
| 131 |
},
|
| 132 |
],
|
| 133 |
},
|
| 134 |
{
|
| 135 |
title: 'Data',
|
| 136 |
+
url: '/data',
|
| 137 |
icon: BarChart2,
|
| 138 |
items: [
|
| 139 |
{
|
| 140 |
title: 'Datasets',
|
| 141 |
+
url: '/data',
|
| 142 |
},
|
| 143 |
{
|
| 144 |
title: 'Analytics',
|
| 145 |
+
url: '/data#analytics',
|
| 146 |
},
|
| 147 |
],
|
| 148 |
},
|
| 149 |
{
|
| 150 |
title: 'Settings',
|
| 151 |
+
url: '/settings',
|
| 152 |
icon: Settings,
|
| 153 |
items: [
|
| 154 |
{
|
| 155 |
title: 'General',
|
| 156 |
+
url: '/settings',
|
| 157 |
},
|
| 158 |
{
|
| 159 |
title: 'Models',
|
| 160 |
+
url: '/settings#models',
|
| 161 |
},
|
| 162 |
],
|
| 163 |
},
|
|
|
|
| 180 |
<SidebarMenu>
|
| 181 |
<SidebarMenuItem>
|
| 182 |
<SidebarMenuButton size="lg" asChild>
|
| 183 |
+
<Link href="/">
|
| 184 |
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
|
| 185 |
<Dna className="size-4" />
|
| 186 |
</div>
|
|
|
|
| 192 |
</SidebarMenuButton>
|
| 193 |
</SidebarMenuItem>
|
| 194 |
</SidebarMenu>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 195 |
</SidebarHeader>
|
| 196 |
|
| 197 |
<SidebarContent>
|
|
|
|
| 253 |
|
| 254 |
{/* Status Section */}
|
| 255 |
<SidebarGroup className="group-data-[collapsible=icon]:hidden mt-auto">
|
| 256 |
+
<SidebarGroupLabel className="flex items-center justify-between">
|
| 257 |
+
<span>System Status</span>
|
| 258 |
+
<ThemeToggle />
|
| 259 |
+
</SidebarGroupLabel>
|
| 260 |
<div className="px-3 py-2">
|
| 261 |
<div className="rounded-lg border bg-muted/50 p-3">
|
| 262 |
<div className="flex items-center gap-2 mb-2">
|