RaedAddala RaedAddala commited on
Commit
af68ace
·
1 Parent(s): 3a0ae3e

feat: add complete visualization system from feat/raed-ui

Browse files

- Add dashboard layout with sidebar navigation
- Add molecules 2D page with SMILES visualization (smiles-drawer)
- Add molecules 3D page with 3Dmol.js viewer
- Add proteins 3D page with PDB structure viewer
- Add API routes for molecules and proteins (mock data)
- Add scroll-area and alert UI components
- Add theme toggle component
- Update sidebar with visualization menu items
- Update routes to use /dashboard prefix

Co-authored-by: RaedAddala <raed.addala@example.com>

ui/app/api/_mock/molecules.ts ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type Molecule = {
2
+ id: string;
3
+ name: string;
4
+ smiles: string;
5
+ pubchemCid: number;
6
+ description?: string;
7
+ };
8
+
9
+ export const molecules: Molecule[] = [
10
+ {
11
+ id: 'caffeine',
12
+ name: 'Caffeine',
13
+ smiles: 'CN1C=NC2=C1C(=O)N(C(=O)N2C)C',
14
+ pubchemCid: 2519,
15
+ description: 'Xanthine alkaloid found in coffee and tea',
16
+ },
17
+ {
18
+ id: 'aspirin',
19
+ name: 'Aspirin',
20
+ smiles: 'CC(=O)OC1=CC=CC=C1C(=O)O',
21
+ pubchemCid: 2244,
22
+ description: 'Acetylsalicylic acid - common pain reliever',
23
+ },
24
+ {
25
+ id: 'ibuprofen',
26
+ name: 'Ibuprofen',
27
+ smiles: 'CC(C)CC1=CC=C(C=C1)C(C)C(=O)O',
28
+ pubchemCid: 3672,
29
+ description: 'Non-steroidal anti-inflammatory drug (NSAID)',
30
+ },
31
+ ];
ui/app/api/_mock/proteins.ts ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type Protein = {
2
+ id: string;
3
+ pdbId: string;
4
+ name: string;
5
+ description?: string;
6
+ };
7
+
8
+ export const proteins: Protein[] = [
9
+ {
10
+ id: '1CRN',
11
+ pdbId: '1CRN',
12
+ name: 'Crambin',
13
+ description: 'Small hydrophobic protein from Crambe abyssinica seeds',
14
+ },
15
+ {
16
+ id: '1UBQ',
17
+ pdbId: '1UBQ',
18
+ name: 'Ubiquitin',
19
+ description: 'Regulatory protein found in most eukaryotic cells',
20
+ },
21
+ {
22
+ id: '4HHB',
23
+ pdbId: '4HHB',
24
+ name: 'Hemoglobin',
25
+ description: 'Iron-containing oxygen-transport protein in red blood cells',
26
+ },
27
+ ];
ui/app/api/molecules/[id]/route.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server';
2
+
3
+ import { molecules } from '../../_mock/molecules';
4
+
5
+ export async function GET(
6
+ _request: Request,
7
+ { params }: { params: Promise<{ id: string }> }
8
+ ) {
9
+ const { id } = await params;
10
+ const molecule = molecules.find((m) => m.id === id);
11
+
12
+ if (!molecule) {
13
+ return NextResponse.json({ error: 'Molecule not found' }, { status: 404 });
14
+ }
15
+
16
+ return NextResponse.json(molecule);
17
+ }
ui/app/api/molecules/[id]/sdf/route.ts ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server';
2
+
3
+ import { molecules } from '../../../_mock/molecules';
4
+
5
+ export async function GET(
6
+ _request: Request,
7
+ { params }: { params: Promise<{ id: string }> }
8
+ ) {
9
+ const { id } = await params;
10
+ const molecule = molecules.find((m) => m.id === id);
11
+
12
+ if (!molecule) {
13
+ return NextResponse.json({ error: 'Molecule not found' }, { status: 404 });
14
+ }
15
+
16
+ try {
17
+ const pubchemUrl = `https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/${molecule.pubchemCid}/record/SDF?record_type=3d`;
18
+ const response = await fetch(pubchemUrl, {
19
+ next: { revalidate: 3600 }, // Cache for 1 hour
20
+ });
21
+
22
+ if (!response.ok) {
23
+ // Try 2D SDF as fallback if 3D is not available
24
+ const pubchemUrl2D = `https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/${molecule.pubchemCid}/record/SDF`;
25
+ const response2D = await fetch(pubchemUrl2D, {
26
+ next: { revalidate: 3600 },
27
+ });
28
+
29
+ if (!response2D.ok) {
30
+ return NextResponse.json(
31
+ { error: 'Failed to fetch SDF from PubChem' },
32
+ { status: 502 }
33
+ );
34
+ }
35
+
36
+ const sdfText = await response2D.text();
37
+ return new NextResponse(sdfText, {
38
+ headers: {
39
+ 'Content-Type': 'chemical/x-mdl-sdfile',
40
+ 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
41
+ },
42
+ });
43
+ }
44
+
45
+ const sdfText = await response.text();
46
+
47
+ return new NextResponse(sdfText, {
48
+ headers: {
49
+ 'Content-Type': 'chemical/x-mdl-sdfile',
50
+ 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
51
+ },
52
+ });
53
+ } catch (error) {
54
+ console.error('Error fetching SDF:', error);
55
+ return NextResponse.json(
56
+ { error: 'Failed to fetch SDF data' },
57
+ { status: 500 }
58
+ );
59
+ }
60
+ }
ui/app/api/molecules/route.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server';
2
+
3
+ import { molecules } from '../_mock/molecules';
4
+
5
+ export async function GET() {
6
+ return NextResponse.json(molecules);
7
+ }
ui/app/api/proteins/[id]/pdb/route.ts ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server';
2
+
3
+ import { proteins } from '../../../_mock/proteins';
4
+
5
+ export async function GET(
6
+ _request: Request,
7
+ { params }: { params: Promise<{ id: string }> }
8
+ ) {
9
+ const { id } = await params;
10
+ const protein = proteins.find((p) => p.id === id || p.pdbId === id);
11
+
12
+ if (!protein) {
13
+ return NextResponse.json({ error: 'Protein not found' }, { status: 404 });
14
+ }
15
+
16
+ try {
17
+ const rcsbUrl = `https://files.rcsb.org/download/${protein.pdbId}.pdb`;
18
+
19
+ const response = await fetch(rcsbUrl, {
20
+ next: { revalidate: 3600 }, // Cache for 1 hour
21
+ });
22
+
23
+ if (!response.ok) {
24
+ return NextResponse.json(
25
+ { error: `Failed to fetch PDB from RCSB: ${response.statusText}` },
26
+ { status: 502 }
27
+ );
28
+ }
29
+
30
+ const pdbText = await response.text();
31
+
32
+ return new NextResponse(pdbText, {
33
+ headers: {
34
+ 'Content-Type': 'text/plain',
35
+ 'Cache-Control': 'public, s-maxage=3600, stale-while-revalidate=86400',
36
+ },
37
+ });
38
+ } catch (error) {
39
+ console.error('Error fetching PDB:', error);
40
+ return NextResponse.json(
41
+ { error: 'Failed to fetch PDB data' },
42
+ { status: 500 }
43
+ );
44
+ }
45
+ }
ui/app/api/proteins/[id]/route.ts ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server';
2
+
3
+ import { proteins } from '../../_mock/proteins';
4
+
5
+ export async function GET(
6
+ _request: Request,
7
+ { params }: { params: Promise<{ id: string }> }
8
+ ) {
9
+ const { id } = await params;
10
+ const protein = proteins.find((p) => p.id === id || p.pdbId === id);
11
+
12
+ if (!protein) {
13
+ return NextResponse.json({ error: 'Protein not found' }, { status: 404 });
14
+ }
15
+
16
+ return NextResponse.json(protein);
17
+ }
ui/app/api/proteins/route.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import { NextResponse } from 'next/server';
2
+
3
+ import { proteins } from '../_mock/proteins';
4
+
5
+ export async function GET() {
6
+ return NextResponse.json(proteins);
7
+ }
ui/app/dashboard/layout.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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">
14
+ {children}
15
+ </main>
16
+ </SidebarInset>
17
+ </SidebarProvider>
18
+ );
19
+ }
ui/app/dashboard/molecules-2d/_components/Smiles2DViewer.tsx ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, canvasId]);
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 ADDED
@@ -0,0 +1,223 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
92
+ const viewer = $3Dmol.createViewer(containerRef.current, {
93
+ backgroundColor: 'transparent',
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 ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import { ArrowUp, Database, FileText, Search, Sparkles, Zap } from "lucide-react"
4
+ import Link from "next/link"
5
+
6
+ import { Badge } from "@/components/ui/badge"
7
+ import { Button } from "@/components/ui/button"
8
+ import { Card, CardContent } from "@/components/ui/card"
9
+ import { Separator } from "@/components/ui/separator"
10
+
11
+ export default function DashboardHome() {
12
+ return (
13
+ <div className="space-y-8 animate-in fade-in duration-500 p-8">
14
+ {/* Hero Section */}
15
+ <div className="flex flex-col lg:flex-row gap-6">
16
+ <div className="flex-1 rounded-2xl bg-gradient-to-br from-primary/10 via-background to-background p-8 border">
17
+ <Badge variant="secondary" className="mb-4">New • BioFlow 2.0</Badge>
18
+ <h1 className="text-4xl font-bold tracking-tight mb-4">AI-Powered Drug Discovery</h1>
19
+ <p className="text-lg text-muted-foreground mb-6 max-w-xl">
20
+ Run discovery pipelines, predict binding, and surface evidence in one streamlined workspace.
21
+ </p>
22
+ <div className="flex gap-2 mb-6">
23
+ <Badge variant="outline" className="bg-primary/5 border-primary/20 text-primary">Model-aware search</Badge>
24
+ <Badge variant="outline" className="bg-green-500/10 border-green-500/20 text-green-700 dark:text-green-400">Evidence-linked</Badge>
25
+ <Badge variant="outline" className="bg-amber-500/10 border-amber-500/20 text-amber-700 dark:text-amber-400">Fast iteration</Badge>
26
+ </div>
27
+
28
+ <div className="flex gap-4">
29
+ <Link href="/dashboard/discovery">
30
+ <Button size="lg" className="font-semibold">
31
+ Start Discovery
32
+ </Button>
33
+ </Link>
34
+ <Link href="/dashboard/explorer">
35
+ <Button size="lg" variant="outline">
36
+ Explore Data
37
+ </Button>
38
+ </Link>
39
+ </div>
40
+ </div>
41
+
42
+ <div className="lg:w-[350px]">
43
+ <Card className="h-full">
44
+ <CardContent className="p-6 flex flex-col justify-between h-full">
45
+ <div>
46
+ <div className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-2">Today</div>
47
+ <div className="text-4xl font-bold mb-2">156 Discoveries</div>
48
+ <div className="text-sm text-green-600 font-medium flex items-center gap-1">
49
+ <ArrowUp className="h-4 w-4" />
50
+ +12% vs last week
51
+ </div>
52
+ </div>
53
+
54
+ <Separator className="my-4" />
55
+
56
+ <div className="space-y-2">
57
+ <div className="flex items-center justify-between text-sm">
58
+ <span className="flex items-center gap-2">
59
+ <span className="h-2 w-2 rounded-full bg-primary"></span>
60
+ Discovery
61
+ </span>
62
+ <span className="font-mono font-medium">64</span>
63
+ </div>
64
+ <div className="flex items-center justify-between text-sm">
65
+ <span className="flex items-center gap-2">
66
+ <span className="h-2 w-2 rounded-full bg-green-500"></span>
67
+ Prediction
68
+ </span>
69
+ <span className="font-mono font-medium">42</span>
70
+ </div>
71
+ <div className="flex items-center justify-between text-sm">
72
+ <span className="flex items-center gap-2">
73
+ <span className="h-2 w-2 rounded-full bg-amber-500"></span>
74
+ Evidence
75
+ </span>
76
+ <span className="font-mono font-medium">50</span>
77
+ </div>
78
+ </div>
79
+ </CardContent>
80
+ </Card>
81
+ </div>
82
+ </div>
83
+
84
+ {/* Metrics Row */}
85
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
86
+ {[
87
+ { label: "Molecules", value: "12.5M", icon: "🧪", change: "+2.3%", color: "text-blue-500" },
88
+ { label: "Proteins", value: "847K", icon: "🧬", change: "+1.8%", color: "text-cyan-500" },
89
+ { label: "Papers", value: "1.2M", icon: "📚", change: "+5.2%", color: "text-emerald-500" },
90
+ { label: "Discoveries", value: "156", icon: "✨", change: "+12%", color: "text-amber-500" }
91
+ ].map((metric, i) => (
92
+ <Card key={i}>
93
+ <CardContent className="p-6">
94
+ <div className="flex justify-between items-start mb-2">
95
+ <div className="text-sm font-medium text-muted-foreground">{metric.label}</div>
96
+ <div className="text-lg">{metric.icon}</div>
97
+ </div>
98
+ <div className="text-2xl font-bold mb-1">{metric.value}</div>
99
+ <div className="text-xs font-medium flex items-center gap-1 text-green-500">
100
+ <ArrowUp className="h-3 w-3" />
101
+ {metric.change}
102
+ </div>
103
+ </CardContent>
104
+ </Card>
105
+ ))}
106
+ </div>
107
+
108
+ {/* Quick Actions */}
109
+ <div className="pt-4">
110
+ <div className="flex items-center gap-2 mb-4">
111
+ <Zap className="h-5 w-5 text-amber-500" />
112
+ <h2 className="text-xl font-semibold">Quick Actions</h2>
113
+ </div>
114
+
115
+ <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
116
+ <Link href="/dashboard/molecules-2d" className="block">
117
+ <Card className="hover:bg-accent/50 transition-colors cursor-pointer h-full">
118
+ <CardContent className="p-6 flex flex-col items-center text-center gap-3">
119
+ <div className="h-10 w-10 rounded-full bg-primary/10 flex items-center justify-center text-primary">
120
+ <Search className="h-5 w-5" />
121
+ </div>
122
+ <div>
123
+ <div className="font-semibold">Molecules 2D</div>
124
+ <div className="text-sm text-muted-foreground">View 2D structures</div>
125
+ </div>
126
+ </CardContent>
127
+ </Card>
128
+ </Link>
129
+ <Link href="/dashboard/molecules-3d" className="block">
130
+ <Card className="hover:bg-accent/50 transition-colors cursor-pointer h-full">
131
+ <CardContent className="p-6 flex flex-col items-center text-center gap-3">
132
+ <div className="h-10 w-10 rounded-full bg-blue-500/10 flex items-center justify-center text-blue-500">
133
+ <Database className="h-5 w-5" />
134
+ </div>
135
+ <div>
136
+ <div className="font-semibold">Molecules 3D</div>
137
+ <div className="text-sm text-muted-foreground">View 3D structures</div>
138
+ </div>
139
+ </CardContent>
140
+ </Card>
141
+ </Link>
142
+ <Link href="/dashboard/proteins-3d" className="block">
143
+ <Card className="hover:bg-accent/50 transition-colors cursor-pointer h-full">
144
+ <CardContent className="p-6 flex flex-col items-center text-center gap-3">
145
+ <div className="h-10 w-10 rounded-full bg-purple-500/10 flex items-center justify-center text-purple-500">
146
+ <FileText className="h-5 w-5" />
147
+ </div>
148
+ <div>
149
+ <div className="font-semibold">Proteins 3D</div>
150
+ <div className="text-sm text-muted-foreground">View protein structures</div>
151
+ </div>
152
+ </CardContent>
153
+ </Card>
154
+ </Link>
155
+ <Link href="/dashboard/discovery" className="block">
156
+ <Card className="hover:bg-accent/50 transition-colors cursor-pointer h-full">
157
+ <CardContent className="p-6 flex flex-col items-center text-center gap-3">
158
+ <div className="h-10 w-10 rounded-full bg-slate-500/10 flex items-center justify-center text-slate-500">
159
+ <Sparkles className="h-5 w-5" />
160
+ </div>
161
+ <div>
162
+ <div className="font-semibold">Discovery</div>
163
+ <div className="text-sm text-muted-foreground">Run predictions</div>
164
+ </div>
165
+ </CardContent>
166
+ </Card>
167
+ </Link>
168
+ </div>
169
+ </div>
170
+ </div>
171
+ )
172
+ }
ui/app/dashboard/proteins-3d/_components/ProteinViewer.tsx ADDED
@@ -0,0 +1,243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 { 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 { getProteins, getProteinPdbUrl } from '@/lib/visualization-api';
15
+ import type { Protein } from '@/lib/visualization-types';
16
+
17
+ // Dynamic import of the protein viewer to prevent SSR issues
18
+ const ProteinViewer = dynamic(
19
+ () =>
20
+ import('./_components/ProteinViewer').then((mod) => mod.ProteinViewer),
21
+ {
22
+ ssr: false,
23
+ loading: () => <Skeleton className="size-[500px]" />,
24
+ }
25
+ );
26
+
27
+ export default function Proteins3DPage() {
28
+ const [proteins, setProteins] = useState<Protein[]>([]);
29
+ const [selectedProtein, setSelectedProtein] = useState<Protein | null>(null);
30
+ const [searchQuery, setSearchQuery] = useState('');
31
+ const [isLoading, setIsLoading] = useState(true);
32
+ const [error, setError] = useState<string | null>(null);
33
+
34
+ // Load proteins list
35
+ useEffect(() => {
36
+ const loadProteins = async () => {
37
+ try {
38
+ setIsLoading(true);
39
+ const data = await getProteins();
40
+ setProteins(data);
41
+ if (data.length > 0) {
42
+ setSelectedProtein(data[0]);
43
+ }
44
+ } catch (err) {
45
+ setError(
46
+ err instanceof Error ? err.message : 'Failed to load proteins'
47
+ );
48
+ } finally {
49
+ setIsLoading(false);
50
+ }
51
+ };
52
+ loadProteins();
53
+ }, []);
54
+
55
+ const filteredProteins = useMemo(() => {
56
+ if (!searchQuery.trim()) return proteins;
57
+ const query = searchQuery.toLowerCase();
58
+ return proteins.filter(
59
+ (p) =>
60
+ p.name.toLowerCase().includes(query) ||
61
+ p.pdbId.toLowerCase().includes(query) ||
62
+ p.description?.toLowerCase().includes(query)
63
+ );
64
+ }, [proteins, searchQuery]);
65
+
66
+ if (error) {
67
+ return (
68
+ <div className="flex h-full items-center justify-center">
69
+ <Card className="max-w-md">
70
+ <CardHeader>
71
+ <CardTitle className="text-destructive">Error</CardTitle>
72
+ <CardDescription>{error}</CardDescription>
73
+ </CardHeader>
74
+ </Card>
75
+ </div>
76
+ );
77
+ }
78
+
79
+ return (
80
+ <div className="flex h-full gap-6 p-6">
81
+ {/* Left Panel - Protein List */}
82
+ <Card className="w-80 shrink-0">
83
+ <CardHeader className="pb-3">
84
+ <CardTitle className="text-lg">Proteins</CardTitle>
85
+ <CardDescription>Select a protein to visualize</CardDescription>
86
+ </CardHeader>
87
+ <CardContent className="pb-3">
88
+ <div className="relative">
89
+ <Search className="absolute left-2.5 top-2.5 size-4 text-muted-foreground" />
90
+ <Input
91
+ placeholder="Search proteins..."
92
+ className="pl-8"
93
+ value={searchQuery}
94
+ onChange={(e) => setSearchQuery(e.target.value)}
95
+ />
96
+ </div>
97
+ </CardContent>
98
+ <Separator />
99
+ <ScrollArea className="h-[calc(100vh-280px)]">
100
+ <div className="p-2">
101
+ {isLoading ? (
102
+ <div className="space-y-2">
103
+ {[1, 2, 3].map((i) => (
104
+ <Skeleton key={i} className="h-16 w-full" />
105
+ ))}
106
+ </div>
107
+ ) : filteredProteins.length === 0 ? (
108
+ <p className="p-4 text-center text-sm text-muted-foreground">
109
+ No proteins found
110
+ </p>
111
+ ) : (
112
+ <div className="space-y-1">
113
+ {filteredProteins.map((protein) => (
114
+ <button
115
+ key={protein.id}
116
+ onClick={() => setSelectedProtein(protein)}
117
+ className={`w-full rounded-lg p-3 text-left transition-colors hover:bg-accent ${
118
+ selectedProtein?.id === protein.id
119
+ ? 'bg-accent'
120
+ : 'bg-transparent'
121
+ }`}
122
+ >
123
+ <div className="flex items-center gap-2">
124
+ <span className="font-medium">{protein.name}</span>
125
+ <Badge variant="secondary" className="text-xs">
126
+ {protein.pdbId}
127
+ </Badge>
128
+ </div>
129
+ {protein.description && (
130
+ <div className="mt-1 truncate text-xs text-muted-foreground">
131
+ {protein.description}
132
+ </div>
133
+ )}
134
+ </button>
135
+ ))}
136
+ </div>
137
+ )}
138
+ </div>
139
+ </ScrollArea>
140
+ </Card>
141
+
142
+ {/* Right Panel - 3D Visualization */}
143
+ <div className="flex flex-1 flex-col gap-6">
144
+ {selectedProtein ? (
145
+ <>
146
+ {/* Protein Info Card */}
147
+ <Card>
148
+ <CardHeader className="pb-3">
149
+ <div className="flex items-start justify-between">
150
+ <div>
151
+ <CardTitle>{selectedProtein.name}</CardTitle>
152
+ <CardDescription>
153
+ {selectedProtein.description}
154
+ </CardDescription>
155
+ </div>
156
+ <div className="flex items-center gap-2">
157
+ <Badge variant="outline">PDB: {selectedProtein.pdbId}</Badge>
158
+ <Button
159
+ variant="outline"
160
+ size="sm"
161
+ asChild
162
+ >
163
+ <a
164
+ href={getProteinPdbUrl(selectedProtein.id)}
165
+ target="_blank"
166
+ rel="noopener noreferrer"
167
+ >
168
+ <ExternalLink className="mr-1 size-4" />
169
+ Open PDB
170
+ </a>
171
+ </Button>
172
+ </div>
173
+ </div>
174
+ </CardHeader>
175
+ <CardContent>
176
+ <div className="flex gap-4 text-sm text-muted-foreground">
177
+ <a
178
+ href={`https://www.rcsb.org/structure/${selectedProtein.pdbId}`}
179
+ target="_blank"
180
+ rel="noopener noreferrer"
181
+ className="flex items-center gap-1 hover:text-foreground"
182
+ >
183
+ <ExternalLink className="size-3" />
184
+ View on RCSB PDB
185
+ </a>
186
+ <a
187
+ href={`https://www.uniprot.org/uniprotkb?query=${selectedProtein.name}`}
188
+ target="_blank"
189
+ rel="noopener noreferrer"
190
+ className="flex items-center gap-1 hover:text-foreground"
191
+ >
192
+ <ExternalLink className="size-3" />
193
+ Search UniProt
194
+ </a>
195
+ </div>
196
+ </CardContent>
197
+ </Card>
198
+
199
+ {/* 3D Structure Card */}
200
+ <Card className="flex-1">
201
+ <CardHeader>
202
+ <CardTitle className="text-lg">3D Structure</CardTitle>
203
+ <CardDescription>
204
+ Rotate: click + drag • Zoom: scroll • Pan: right-click + drag
205
+ </CardDescription>
206
+ </CardHeader>
207
+ <CardContent className="flex items-center justify-center">
208
+ <ProteinViewer
209
+ key={selectedProtein.pdbId}
210
+ pdbId={selectedProtein.pdbId}
211
+ width={600}
212
+ height={500}
213
+ />
214
+ </CardContent>
215
+ </Card>
216
+ </>
217
+ ) : (
218
+ <div className="flex flex-1 items-center justify-center">
219
+ <p className="text-muted-foreground">
220
+ Select a protein to view its 3D structure
221
+ </p>
222
+ </div>
223
+ )}
224
+ </div>
225
+ </div>
226
+ );
227
+ }
ui/app/visualization/page.tsx ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+
5
+ import { PageHeader } from '@/components/page-header';
6
+ import { Button } from '@/components/ui/button';
7
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
8
+ import { Input } from '@/components/ui/input';
9
+ import { Label } from '@/components/ui/label';
10
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
11
+ import { ProteinViewer } from '@/components/visualization/protein-viewer';
12
+ import { Smiles2DViewer } from '@/components/visualization/smiles-2d-viewer';
13
+
14
+ // Example molecules
15
+ const EXAMPLE_SMILES = [
16
+ { name: 'Aspirin', smiles: 'CC(=O)OC1=CC=CC=C1C(=O)O' },
17
+ { name: 'Caffeine', smiles: 'CN1C=NC2=C1C(=O)N(C(=O)N2C)C' },
18
+ { name: 'Ibuprofen', smiles: 'CC(C)CC1=CC=C(C=C1)C(C)C(=O)O' },
19
+ { name: 'Paracetamol', smiles: 'CC(=O)NC1=CC=C(C=C1)O' },
20
+ ];
21
+
22
+ // Example proteins (PDB IDs)
23
+ const EXAMPLE_PROTEINS = [
24
+ { name: 'Hemoglobin', pdbId: '1HHO' },
25
+ { name: 'Insulin', pdbId: '1ZNI' },
26
+ { name: 'Lysozyme', pdbId: '1LYZ' },
27
+ { name: 'Green Fluorescent Protein', pdbId: '1EMA' },
28
+ ];
29
+
30
+ export default function VisualizationPage() {
31
+ const [customSmiles, setCustomSmiles] = useState('');
32
+ const [customPdbId, setCustomPdbId] = useState('');
33
+ const [activeSmiles, setActiveSmiles] = useState(EXAMPLE_SMILES[0].smiles);
34
+ const [activePdbId, setActivePdbId] = useState(EXAMPLE_PROTEINS[0].pdbId);
35
+
36
+ return (
37
+ <div className="container mx-auto p-6 space-y-6">
38
+ <PageHeader
39
+ title="Molecular Visualization"
40
+ subtitle="View 2D molecule structures and 3D protein models"
41
+ breadcrumbs={[
42
+ { label: 'Home', href: '/' },
43
+ { label: 'Visualization' },
44
+ ]}
45
+ />
46
+
47
+ <Tabs defaultValue="molecules" className="space-y-6">
48
+ <TabsList>
49
+ <TabsTrigger value="molecules">2D Molecules</TabsTrigger>
50
+ <TabsTrigger value="proteins">3D Proteins</TabsTrigger>
51
+ </TabsList>
52
+
53
+ <TabsContent value="molecules" className="space-y-6">
54
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
55
+ {/* Controls */}
56
+ <Card>
57
+ <CardHeader>
58
+ <CardTitle>Molecule Selection</CardTitle>
59
+ </CardHeader>
60
+ <CardContent className="space-y-4">
61
+ <div className="space-y-2">
62
+ <Label>Example Molecules</Label>
63
+ <div className="flex flex-wrap gap-2">
64
+ {EXAMPLE_SMILES.map((mol) => (
65
+ <Button
66
+ key={mol.name}
67
+ variant={activeSmiles === mol.smiles ? 'default' : 'outline'}
68
+ size="sm"
69
+ onClick={() => setActiveSmiles(mol.smiles)}
70
+ >
71
+ {mol.name}
72
+ </Button>
73
+ ))}
74
+ </div>
75
+ </div>
76
+
77
+ <div className="space-y-2">
78
+ <Label htmlFor="custom-smiles">Custom SMILES</Label>
79
+ <div className="flex gap-2">
80
+ <Input
81
+ id="custom-smiles"
82
+ placeholder="Enter SMILES string..."
83
+ value={customSmiles}
84
+ onChange={(e) => setCustomSmiles(e.target.value)}
85
+ />
86
+ <Button
87
+ onClick={() => customSmiles && setActiveSmiles(customSmiles)}
88
+ disabled={!customSmiles}
89
+ >
90
+ View
91
+ </Button>
92
+ </div>
93
+ </div>
94
+
95
+ <div className="p-3 bg-muted rounded-lg">
96
+ <Label className="text-xs text-muted-foreground">Current SMILES</Label>
97
+ <code className="block text-sm font-mono mt-1 break-all">
98
+ {activeSmiles}
99
+ </code>
100
+ </div>
101
+ </CardContent>
102
+ </Card>
103
+
104
+ {/* Viewer */}
105
+ <Card>
106
+ <CardHeader>
107
+ <CardTitle>2D Structure</CardTitle>
108
+ </CardHeader>
109
+ <CardContent className="flex justify-center">
110
+ <Smiles2DViewer
111
+ smiles={activeSmiles}
112
+ width={400}
113
+ height={300}
114
+ />
115
+ </CardContent>
116
+ </Card>
117
+ </div>
118
+ </TabsContent>
119
+
120
+ <TabsContent value="proteins" className="space-y-6">
121
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
122
+ {/* Controls */}
123
+ <Card>
124
+ <CardHeader>
125
+ <CardTitle>Protein Selection</CardTitle>
126
+ </CardHeader>
127
+ <CardContent className="space-y-4">
128
+ <div className="space-y-2">
129
+ <Label>Example Proteins</Label>
130
+ <div className="flex flex-wrap gap-2">
131
+ {EXAMPLE_PROTEINS.map((protein) => (
132
+ <Button
133
+ key={protein.pdbId}
134
+ variant={activePdbId === protein.pdbId ? 'default' : 'outline'}
135
+ size="sm"
136
+ onClick={() => setActivePdbId(protein.pdbId)}
137
+ >
138
+ {protein.name}
139
+ </Button>
140
+ ))}
141
+ </div>
142
+ </div>
143
+
144
+ <div className="space-y-2">
145
+ <Label htmlFor="custom-pdb">Custom PDB ID</Label>
146
+ <div className="flex gap-2">
147
+ <Input
148
+ id="custom-pdb"
149
+ placeholder="Enter PDB ID (e.g., 1HHO)..."
150
+ value={customPdbId}
151
+ onChange={(e) => setCustomPdbId(e.target.value.toUpperCase())}
152
+ maxLength={4}
153
+ />
154
+ <Button
155
+ onClick={() => customPdbId && setActivePdbId(customPdbId)}
156
+ disabled={!customPdbId || customPdbId.length !== 4}
157
+ >
158
+ View
159
+ </Button>
160
+ </div>
161
+ </div>
162
+
163
+ <div className="p-3 bg-muted rounded-lg">
164
+ <Label className="text-xs text-muted-foreground">Current PDB ID</Label>
165
+ <code className="block text-sm font-mono mt-1">
166
+ {activePdbId}
167
+ </code>
168
+ </div>
169
+ </CardContent>
170
+ </Card>
171
+
172
+ {/* Viewer */}
173
+ <Card>
174
+ <CardHeader>
175
+ <CardTitle>3D Structure</CardTitle>
176
+ </CardHeader>
177
+ <CardContent className="flex justify-center">
178
+ <ProteinViewer
179
+ pdbId={activePdbId}
180
+ width={500}
181
+ height={400}
182
+ />
183
+ </CardContent>
184
+ </Card>
185
+ </div>
186
+ </TabsContent>
187
+ </Tabs>
188
+ </div>
189
+ );
190
+ }
ui/components/sidebar.tsx CHANGED
@@ -9,6 +9,7 @@ import {
9
  ChevronsUpDown,
10
  CreditCard,
11
  Dna,
 
12
  Home,
13
  LogOut,
14
  Microscope,
@@ -16,7 +17,6 @@ import {
16
  Sparkles,
17
  Terminal,
18
  User,
19
- Workflow,
20
  } from 'lucide-react';
21
  import Link from 'next/link';
22
  import { usePathname } from 'next/navigation';
@@ -52,87 +52,92 @@ import {
52
  CollapsibleTrigger,
53
  } from '@/components/animate-ui/primitives/radix/collapsible';
54
  import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
 
55
  import { useIsMobile } from '@/hooks/use-mobile';
56
 
57
  const navMain = [
58
  {
59
  title: 'Home',
60
- url: '/',
61
  icon: Home,
62
  isActive: true,
63
  },
64
  {
65
- title: 'Workflow',
66
- url: '/workflow',
67
- icon: Workflow,
68
  items: [
69
  {
70
- title: 'Pipeline Builder',
71
- url: '/workflow',
72
  },
73
  {
74
- title: 'Templates',
75
- url: '/workflow#templates',
 
 
 
 
76
  },
77
  ],
78
  },
79
  {
80
  title: 'Discovery',
81
- url: '/discovery',
82
  icon: Microscope,
83
  items: [
84
  {
85
  title: 'Drug Discovery',
86
- url: '/discovery',
87
  },
88
  {
89
  title: 'Molecule Search',
90
- url: '/discovery#search',
91
  },
92
  ],
93
  },
94
  {
95
  title: 'Explorer',
96
- url: '/explorer',
97
  icon: Dna,
98
  items: [
99
  {
100
  title: 'Embeddings',
101
- url: '/explorer',
102
  },
103
  {
104
  title: 'Predictions',
105
- url: '/explorer#predictions',
106
  },
107
  ],
108
  },
109
  {
110
  title: 'Data',
111
- url: '/data',
112
  icon: BarChart2,
113
  items: [
114
  {
115
  title: 'Datasets',
116
- url: '/data',
117
  },
118
  {
119
  title: 'Analytics',
120
- url: '/data#analytics',
121
  },
122
  ],
123
  },
124
  {
125
  title: 'Settings',
126
- url: '/settings',
127
  icon: Settings,
128
  items: [
129
  {
130
  title: 'General',
131
- url: '/settings',
132
  },
133
  {
134
  title: 'Models',
135
- url: '/settings#models',
136
  },
137
  ],
138
  },
@@ -155,7 +160,7 @@ export function AppSidebar() {
155
  <SidebarMenu>
156
  <SidebarMenuItem>
157
  <SidebarMenuButton size="lg" asChild>
158
- <Link href="/">
159
  <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
160
  <Dna className="size-4" />
161
  </div>
@@ -167,6 +172,10 @@ export function AppSidebar() {
167
  </SidebarMenuButton>
168
  </SidebarMenuItem>
169
  </SidebarMenu>
 
 
 
 
170
  {/* App Header */}
171
  </SidebarHeader>
172
 
@@ -177,7 +186,7 @@ export function AppSidebar() {
177
  <SidebarMenu>
178
  {navMain.map((item) => {
179
  const isActive = pathname === item.url || pathname?.startsWith(item.url + '/');
180
-
181
  if (!item.items || item.items.length === 0) {
182
  return (
183
  <SidebarMenuItem key={item.title}>
 
9
  ChevronsUpDown,
10
  CreditCard,
11
  Dna,
12
+ FlaskConical,
13
  Home,
14
  LogOut,
15
  Microscope,
 
17
  Sparkles,
18
  Terminal,
19
  User,
 
20
  } from 'lucide-react';
21
  import Link from 'next/link';
22
  import { usePathname } from 'next/navigation';
 
52
  CollapsibleTrigger,
53
  } from '@/components/animate-ui/primitives/radix/collapsible';
54
  import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
55
+ import { ThemeToggle } from '@/components/theme-toggle';
56
  import { useIsMobile } from '@/hooks/use-mobile';
57
 
58
  const navMain = [
59
  {
60
  title: 'Home',
61
+ url: '/dashboard',
62
  icon: Home,
63
  isActive: true,
64
  },
65
  {
66
+ title: 'Visualization',
67
+ url: '/dashboard/molecules-2d',
68
+ icon: FlaskConical,
69
  items: [
70
  {
71
+ title: 'Molecules 2D',
72
+ url: '/dashboard/molecules-2d',
73
  },
74
  {
75
+ title: 'Molecules 3D',
76
+ url: '/dashboard/molecules-3d',
77
+ },
78
+ {
79
+ title: 'Proteins 3D',
80
+ url: '/dashboard/proteins-3d',
81
  },
82
  ],
83
  },
84
  {
85
  title: 'Discovery',
86
+ url: '/dashboard/discovery',
87
  icon: Microscope,
88
  items: [
89
  {
90
  title: 'Drug Discovery',
91
+ url: '/dashboard/discovery',
92
  },
93
  {
94
  title: 'Molecule Search',
95
+ url: '/dashboard/discovery#search',
96
  },
97
  ],
98
  },
99
  {
100
  title: 'Explorer',
101
+ url: '/dashboard/explorer',
102
  icon: Dna,
103
  items: [
104
  {
105
  title: 'Embeddings',
106
+ url: '/dashboard/explorer',
107
  },
108
  {
109
  title: 'Predictions',
110
+ url: '/dashboard/explorer#predictions',
111
  },
112
  ],
113
  },
114
  {
115
  title: 'Data',
116
+ url: '/dashboard/data',
117
  icon: BarChart2,
118
  items: [
119
  {
120
  title: 'Datasets',
121
+ url: '/dashboard/data',
122
  },
123
  {
124
  title: 'Analytics',
125
+ url: '/dashboard/data#analytics',
126
  },
127
  ],
128
  },
129
  {
130
  title: 'Settings',
131
+ url: '/dashboard/settings',
132
  icon: Settings,
133
  items: [
134
  {
135
  title: 'General',
136
+ url: '/dashboard/settings',
137
  },
138
  {
139
  title: 'Models',
140
+ url: '/dashboard/settings#models',
141
  },
142
  ],
143
  },
 
160
  <SidebarMenu>
161
  <SidebarMenuItem>
162
  <SidebarMenuButton size="lg" asChild>
163
+ <Link href="/dashboard">
164
  <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-primary text-primary-foreground">
165
  <Dna className="size-4" />
166
  </div>
 
172
  </SidebarMenuButton>
173
  </SidebarMenuItem>
174
  </SidebarMenu>
175
+ {/* Theme Toggle */}
176
+ <div className="group-data-[collapsible=icon]:hidden px-2">
177
+ <ThemeToggle />
178
+ </div>
179
  {/* App Header */}
180
  </SidebarHeader>
181
 
 
186
  <SidebarMenu>
187
  {navMain.map((item) => {
188
  const isActive = pathname === item.url || pathname?.startsWith(item.url + '/');
189
+
190
  if (!item.items || item.items.length === 0) {
191
  return (
192
  <SidebarMenuItem key={item.title}>
ui/components/theme-toggle.tsx ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Moon, Sun, Laptop } from "lucide-react"
5
+ import { useTheme } from "next-themes"
6
+
7
+ import { Button } from "@/components/ui/button"
8
+ import {
9
+ DropdownMenu,
10
+ DropdownMenuContent,
11
+ DropdownMenuItem,
12
+ DropdownMenuTrigger,
13
+ } from "@/components/animate-ui/components/radix/dropdown-menu"
14
+
15
+ export function ThemeToggle() {
16
+ const { setTheme, theme } = useTheme()
17
+
18
+ return (
19
+ <DropdownMenu>
20
+ <DropdownMenuTrigger asChild>
21
+ <Button variant="ghost" size="icon" className="h-9 w-9">
22
+ <Sun className="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
23
+ <Moon className="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
24
+ <span className="sr-only">Toggle theme</span>
25
+ </Button>
26
+ </DropdownMenuTrigger>
27
+ <DropdownMenuContent align="end">
28
+ <DropdownMenuItem onClick={() => setTheme("light")}>
29
+ <Sun className="mr-2 h-4 w-4" />
30
+ <span>Light</span>
31
+ </DropdownMenuItem>
32
+ <DropdownMenuItem onClick={() => setTheme("dark")}>
33
+ <Moon className="mr-2 h-4 w-4" />
34
+ <span>Dark</span>
35
+ </DropdownMenuItem>
36
+ <DropdownMenuItem onClick={() => setTheme("system")}>
37
+ <Laptop className="mr-2 h-4 w-4" />
38
+ <span>System</span>
39
+ </DropdownMenuItem>
40
+ </DropdownMenuContent>
41
+ </DropdownMenu>
42
+ )
43
+ }
ui/components/ui/alert.tsx ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-card text-card-foreground",
12
+ destructive:
13
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ function Alert({
23
+ className,
24
+ variant,
25
+ ...props
26
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
27
+ return (
28
+ <div
29
+ data-slot="alert"
30
+ role="alert"
31
+ className={cn(alertVariants({ variant }), className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38
+ return (
39
+ <div
40
+ data-slot="alert-title"
41
+ className={cn(
42
+ "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ )
48
+ }
49
+
50
+ function AlertDescription({
51
+ className,
52
+ ...props
53
+ }: React.ComponentProps<"div">) {
54
+ return (
55
+ <div
56
+ data-slot="alert-description"
57
+ className={cn(
58
+ "text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
59
+ className
60
+ )}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ export { Alert, AlertTitle, AlertDescription }
ui/components/ui/scroll-area.tsx ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function ScrollArea({
9
+ className,
10
+ children,
11
+ ...props
12
+ }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
13
+ return (
14
+ <ScrollAreaPrimitive.Root
15
+ data-slot="scroll-area"
16
+ className={cn("relative", className)}
17
+ {...props}
18
+ >
19
+ <ScrollAreaPrimitive.Viewport
20
+ data-slot="scroll-area-viewport"
21
+ className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
22
+ >
23
+ {children}
24
+ </ScrollAreaPrimitive.Viewport>
25
+ <ScrollBar />
26
+ <ScrollAreaPrimitive.Corner />
27
+ </ScrollAreaPrimitive.Root>
28
+ )
29
+ }
30
+
31
+ function ScrollBar({
32
+ className,
33
+ orientation = "vertical",
34
+ ...props
35
+ }: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
36
+ return (
37
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
38
+ data-slot="scroll-area-scrollbar"
39
+ orientation={orientation}
40
+ className={cn(
41
+ "flex touch-none p-px transition-colors select-none",
42
+ orientation === "vertical" &&
43
+ "h-full w-2.5 border-l border-l-transparent",
44
+ orientation === "horizontal" &&
45
+ "h-2.5 flex-col border-t border-t-transparent",
46
+ className
47
+ )}
48
+ {...props}
49
+ >
50
+ <ScrollAreaPrimitive.ScrollAreaThumb
51
+ data-slot="scroll-area-thumb"
52
+ className="bg-border relative flex-1 rounded-full"
53
+ />
54
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
55
+ )
56
+ }
57
+
58
+ export { ScrollArea, ScrollBar }
ui/lib/visualization-api.ts CHANGED
@@ -52,6 +52,7 @@ export function getMoleculeSdfUrl(id: string): string {
52
  return `${API_BASE}/molecules/${id}/sdf`;
53
  }
54
 
55
- export function getProteinPdbUrl(id: string): string {
56
- return `${API_BASE}/proteins/${id}/pdb`;
 
57
  }
 
52
  return `${API_BASE}/molecules/${id}/sdf`;
53
  }
54
 
55
+ export function getProteinPdbUrl(pdbId: string): string {
56
+ // Fetch directly from RCSB PDB for now
57
+ return `https://files.rcsb.org/download/${pdbId.toUpperCase()}.pdb`;
58
  }
ui/package.json CHANGED
@@ -16,6 +16,7 @@
16
  "@radix-ui/react-collapsible": "^1.1.12",
17
  "@radix-ui/react-dropdown-menu": "^2.1.16",
18
  "@radix-ui/react-label": "^2.1.8",
 
19
  "@radix-ui/react-select": "^2.2.6",
20
  "@radix-ui/react-separator": "^1.1.8",
21
  "@radix-ui/react-slider": "^1.3.6",
@@ -28,6 +29,7 @@
28
  "lucide-react": "^0.563.0",
29
  "motion": "^12.29.2",
30
  "next": "^16.1.4",
 
31
  "radix-ui": "^1.4.3",
32
  "react": "^19.2.3",
33
  "react-dom": "^19.2.3",
 
16
  "@radix-ui/react-collapsible": "^1.1.12",
17
  "@radix-ui/react-dropdown-menu": "^2.1.16",
18
  "@radix-ui/react-label": "^2.1.8",
19
+ "@radix-ui/react-scroll-area": "^1.2.10",
20
  "@radix-ui/react-select": "^2.2.6",
21
  "@radix-ui/react-separator": "^1.1.8",
22
  "@radix-ui/react-slider": "^1.3.6",
 
29
  "lucide-react": "^0.563.0",
30
  "motion": "^12.29.2",
31
  "next": "^16.1.4",
32
+ "next-themes": "^0.4.6",
33
  "radix-ui": "^1.4.3",
34
  "react": "^19.2.3",
35
  "react-dom": "^19.2.3",
ui/pnpm-lock.yaml CHANGED
@@ -26,6 +26,9 @@ importers:
26
  '@radix-ui/react-label':
27
  specifier: ^2.1.8
28
  version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
 
 
 
29
  '@radix-ui/react-select':
30
  specifier: ^2.2.6
31
  version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -62,6 +65,9 @@ importers:
62
  next:
63
  specifier: ^16.1.4
64
  version: 16.1.4(@babel/core@7.28.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
 
 
 
65
  radix-ui:
66
  specifier: ^1.4.3
67
  version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -3367,6 +3373,12 @@ packages:
3367
  netcdfjs@3.0.0:
3368
  resolution: {integrity: sha512-LOvT8KkC308qtpUkcBPiCMBtii7ZQCN6LxcVheWgyUeZ6DQWcpSRFV9dcVXLj/2eHZ/bre9tV5HTH4Sf93vrFw==}
3369
 
 
 
 
 
 
 
3370
  next@16.1.4:
3371
  resolution: {integrity: sha512-gKSecROqisnV7Buen5BfjmXAm7Xlpx9o2ueVQRo5DxQcjC8d330dOM1xiGWc2k3Dcnz0In3VybyRPOsudwgiqQ==}
3372
  engines: {node: '>=20.9.0'}
@@ -7625,6 +7637,11 @@ snapshots:
7625
  dependencies:
7626
  iobuffer: 5.4.0
7627
 
 
 
 
 
 
7628
  next@16.1.4(@babel/core@7.28.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
7629
  dependencies:
7630
  '@next/env': 16.1.4
 
26
  '@radix-ui/react-label':
27
  specifier: ^2.1.8
28
  version: 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
29
+ '@radix-ui/react-scroll-area':
30
+ specifier: ^1.2.10
31
+ version: 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
32
  '@radix-ui/react-select':
33
  specifier: ^2.2.6
34
  version: 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
 
65
  next:
66
  specifier: ^16.1.4
67
  version: 16.1.4(@babel/core@7.28.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
68
+ next-themes:
69
+ specifier: ^0.4.6
70
+ version: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
71
  radix-ui:
72
  specifier: ^1.4.3
73
  version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.9))(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
 
3373
  netcdfjs@3.0.0:
3374
  resolution: {integrity: sha512-LOvT8KkC308qtpUkcBPiCMBtii7ZQCN6LxcVheWgyUeZ6DQWcpSRFV9dcVXLj/2eHZ/bre9tV5HTH4Sf93vrFw==}
3375
 
3376
+ next-themes@0.4.6:
3377
+ resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
3378
+ peerDependencies:
3379
+ react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
3380
+ react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
3381
+
3382
  next@16.1.4:
3383
  resolution: {integrity: sha512-gKSecROqisnV7Buen5BfjmXAm7Xlpx9o2ueVQRo5DxQcjC8d330dOM1xiGWc2k3Dcnz0In3VybyRPOsudwgiqQ==}
3384
  engines: {node: '>=20.9.0'}
 
7637
  dependencies:
7638
  iobuffer: 5.4.0
7639
 
7640
+ next-themes@0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
7641
+ dependencies:
7642
+ react: 19.2.3
7643
+ react-dom: 19.2.3(react@19.2.3)
7644
+
7645
  next@16.1.4(@babel/core@7.28.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
7646
  dependencies:
7647
  '@next/env': 16.1.4
ui/types/smiles-drawer.d.ts ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ declare module 'smiles-drawer' {
2
+ interface SmiDrawerOptions {
3
+ width?: number;
4
+ height?: number;
5
+ bondThickness?: number;
6
+ bondLength?: number;
7
+ shortBondLength?: number;
8
+ bondSpacing?: number;
9
+ atomVisualization?: 'default' | 'balls' | 'none';
10
+ isomeric?: boolean;
11
+ debug?: boolean;
12
+ terminalCarbons?: boolean;
13
+ explicitHydrogens?: boolean;
14
+ compactDrawing?: boolean;
15
+ fontSizeLarge?: number;
16
+ fontSizeSmall?: number;
17
+ padding?: number;
18
+ }
19
+
20
+ class SmiDrawer {
21
+ constructor(options?: SmiDrawerOptions);
22
+ draw(
23
+ smiles: string,
24
+ target: string | HTMLCanvasElement,
25
+ theme?: 'light' | 'dark',
26
+ onSuccess?: () => void,
27
+ onError?: (error: Error) => void
28
+ ): void;
29
+ }
30
+
31
+ export { SmiDrawer };
32
+ export default { SmiDrawer };
33
+ }