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 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
- // Get molecule info from backend
12
  const moleculeRes = await fetch(`${API_CONFIG.baseUrl}/api/molecules/${id}`);
13
 
14
- if (!moleculeRes.ok) {
15
- if (moleculeRes.status === 404) {
16
- return NextResponse.json({ error: 'Molecule not found' }, { status: 404 });
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/${molecule.pubchemCid}/record/SDF?record_type=3d`;
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/${molecule.pubchemCid}/record/SDF`;
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: '/dashboard',
63
  icon: Home,
64
  isActive: true,
65
  },
66
  {
67
  title: 'Visualization',
68
- url: '/dashboard/molecules-2d',
69
  icon: FlaskConical,
70
  items: [
71
  {
72
  title: 'Molecules 2D',
73
- url: '/dashboard/molecules-2d',
74
  },
75
  {
76
  title: 'Molecules 3D',
77
- url: '/dashboard/molecules-3d',
78
  },
79
  {
80
  title: 'Proteins 3D',
81
- url: '/dashboard/proteins-3d',
82
  },
83
  ],
84
  },
85
  {
86
  title: 'Discovery',
87
- url: '/dashboard/discovery',
88
  icon: Microscope,
89
  items: [
90
  {
91
  title: 'Drug Discovery',
92
- url: '/dashboard/discovery',
93
  },
94
  {
95
  title: 'Molecule Search',
96
- url: '/dashboard/discovery#search',
97
  },
98
  ],
99
  },
100
  {
101
  title: 'Explorer',
102
- url: '/dashboard/explorer',
103
  icon: Dna,
104
  items: [
105
  {
106
  title: 'Embeddings',
107
- url: '/dashboard/explorer',
108
  },
109
  {
110
  title: '3D Visualization',
111
- url: '/dashboard/visualization',
112
  },
113
  {
114
  title: 'Predictions',
115
- url: '/dashboard/explorer#predictions',
116
  },
117
  ],
118
  },
119
  {
120
  title: 'Workflows',
121
- url: '/dashboard/workflow',
122
  icon: Sparkles,
123
  items: [
124
  {
125
  title: 'Builder',
126
- url: '/dashboard/workflow',
127
  },
128
  {
129
  title: 'Templates',
130
- url: '/dashboard/workflow#templates',
131
  },
132
  ],
133
  },
134
  {
135
  title: 'Data',
136
- url: '/dashboard/data',
137
  icon: BarChart2,
138
  items: [
139
  {
140
  title: 'Datasets',
141
- url: '/dashboard/data',
142
  },
143
  {
144
  title: 'Analytics',
145
- url: '/dashboard/data#analytics',
146
  },
147
  ],
148
  },
149
  {
150
  title: 'Settings',
151
- url: '/dashboard/settings',
152
  icon: Settings,
153
  items: [
154
  {
155
  title: 'General',
156
- url: '/dashboard/settings',
157
  },
158
  {
159
  title: 'Models',
160
- url: '/dashboard/settings#models',
161
  },
162
  ],
163
  },
@@ -180,7 +180,7 @@ export function AppSidebar() {
180
  <SidebarMenu>
181
  <SidebarMenuItem>
182
  <SidebarMenuButton size="lg" asChild>
183
- <Link href="/dashboard">
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>System Status</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">