RaeDare2 RaeDare2 commited on
Commit
3a0ae3e
·
1 Parent(s): f5a9683

feat: add protein 2D/3D visualization components

Browse files

- Add Smiles2DViewer for 2D molecule visualization using smiles-drawer
- Add ProteinViewer for 3D protein visualization using 3Dmol.js
- Add visualization-api.ts for fetching molecule/protein data
- Add visualization-types.ts for type definitions
- Install smiles-drawer and 3dmol dependencies

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

ui/components/visualization/index.ts ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ export { Smiles2DViewer } from './smiles-2d-viewer';
2
+ export { ProteinViewer } from './protein-viewer';
ui/components/visualization/protein-viewer.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/components/visualization/smiles-2d-viewer.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/lib/visualization-api.ts ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Molecule, Protein } from './visualization-types';
2
+
3
+ const API_BASE = '/api';
4
+
5
+ // Generic fetch helper with error handling
6
+ async function fetchApi<T>(url: string): Promise<T> {
7
+ const response = await fetch(url);
8
+ if (!response.ok) {
9
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
10
+ throw new Error(error.error || `HTTP ${response.status}`);
11
+ }
12
+ return response.json();
13
+ }
14
+
15
+ async function fetchText(url: string): Promise<string> {
16
+ const response = await fetch(url);
17
+ if (!response.ok) {
18
+ const error = await response.json().catch(() => ({ error: 'Unknown error' }));
19
+ throw new Error(error.error || `HTTP ${response.status}`);
20
+ }
21
+ return response.text();
22
+ }
23
+
24
+ // Molecule API
25
+ export async function getMolecules(): Promise<Molecule[]> {
26
+ return fetchApi<Molecule[]>(`${API_BASE}/molecules`);
27
+ }
28
+
29
+ export async function getMolecule(id: string): Promise<Molecule> {
30
+ return fetchApi<Molecule>(`${API_BASE}/molecules/${id}`);
31
+ }
32
+
33
+ export async function getMoleculeSDF(id: string): Promise<string> {
34
+ return fetchText(`${API_BASE}/molecules/${id}/sdf`);
35
+ }
36
+
37
+ // Protein API
38
+ export async function getProteins(): Promise<Protein[]> {
39
+ return fetchApi<Protein[]>(`${API_BASE}/proteins`);
40
+ }
41
+
42
+ export async function getProtein(id: string): Promise<Protein> {
43
+ return fetchApi<Protein>(`${API_BASE}/proteins/${id}`);
44
+ }
45
+
46
+ export async function getProteinPDB(id: string): Promise<string> {
47
+ return fetchText(`${API_BASE}/proteins/${id}/pdb`);
48
+ }
49
+
50
+ // URL builders for direct links
51
+ 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
+ }
ui/lib/visualization-types.ts ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // Molecule types
2
+ export type Molecule = {
3
+ id: string;
4
+ name: string;
5
+ smiles: string;
6
+ pubchemCid: number;
7
+ description?: string;
8
+ };
9
+
10
+ // Protein types
11
+ export type Protein = {
12
+ id: string;
13
+ pdbId: string;
14
+ name: string;
15
+ description?: string;
16
+ };
17
+
18
+ // Viewer representation types
19
+ export type MoleculeRepresentation = 'stick' | 'sphere' | 'line' | 'cartoon';
20
+ export type ProteinRepresentation = 'cartoon' | 'ball-and-stick' | 'surface' | 'ribbon';
ui/package.json CHANGED
@@ -10,6 +10,7 @@
10
  "type-check": "tsc --noEmit"
11
  },
12
  "dependencies": {
 
13
  "@floating-ui/react": "^0.27.16",
14
  "@radix-ui/react-avatar": "^1.1.11",
15
  "@radix-ui/react-collapsible": "^1.1.12",
@@ -32,6 +33,7 @@
32
  "react-dom": "^19.2.3",
33
  "reactflow": "^11.11.4",
34
  "recharts": "^3.7.0",
 
35
  "tailwind-merge": "^3.4.0",
36
  "zod": "^4.3.6"
37
  },
 
10
  "type-check": "tsc --noEmit"
11
  },
12
  "dependencies": {
13
+ "3dmol": "^2.5.4",
14
  "@floating-ui/react": "^0.27.16",
15
  "@radix-ui/react-avatar": "^1.1.11",
16
  "@radix-ui/react-collapsible": "^1.1.12",
 
33
  "react-dom": "^19.2.3",
34
  "reactflow": "^11.11.4",
35
  "recharts": "^3.7.0",
36
+ "smiles-drawer": "^2.1.7",
37
  "tailwind-merge": "^3.4.0",
38
  "zod": "^4.3.6"
39
  },
ui/pnpm-lock.yaml CHANGED
@@ -8,6 +8,9 @@ importers:
8
 
9
  .:
10
  dependencies:
 
 
 
11
  '@floating-ui/react':
12
  specifier: ^0.27.16
13
  version: 0.27.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
@@ -74,6 +77,9 @@ importers:
74
  recharts:
75
  specifier: ^3.7.0
76
  version: 3.7.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1)
 
 
 
77
  tailwind-merge:
78
  specifier: ^3.4.0
79
  version: 3.4.0
@@ -129,6 +135,10 @@ importers:
129
 
130
  packages:
131
 
 
 
 
 
132
  '@alloc/quick-lru@5.2.0':
133
  resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
134
  engines: {node: '>=10'}
@@ -2076,6 +2086,9 @@ packages:
2076
  resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
2077
  engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
2078
 
 
 
 
2079
  class-variance-authority@0.7.1:
2080
  resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
2081
 
@@ -2879,6 +2892,9 @@ packages:
2879
  resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
2880
  engines: {node: '>=12'}
2881
 
 
 
 
2882
  ipaddr.js@1.9.1:
2883
  resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
2884
  engines: {node: '>= 0.10'}
@@ -3348,6 +3364,9 @@ packages:
3348
  resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
3349
  engines: {node: '>= 0.6'}
3350
 
 
 
 
3351
  next@16.1.4:
3352
  resolution: {integrity: sha512-gKSecROqisnV7Buen5BfjmXAm7Xlpx9o2ueVQRo5DxQcjC8d330dOM1xiGWc2k3Dcnz0In3VybyRPOsudwgiqQ==}
3353
  engines: {node: '>=20.9.0'}
@@ -3470,6 +3489,12 @@ packages:
3470
  package-manager-detector@1.6.0:
3471
  resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
3472
 
 
 
 
 
 
 
3473
  parent-module@1.0.1:
3474
  resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
3475
  engines: {node: '>=6'}
@@ -3825,6 +3850,9 @@ packages:
3825
  sisteransi@1.0.5:
3826
  resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
3827
 
 
 
 
3828
  source-map-js@1.2.1:
3829
  resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
3830
  engines: {node: '>=0.10.0'}
@@ -4071,6 +4099,9 @@ packages:
4071
  peerDependencies:
4072
  browserslist: '>= 4.21.0'
4073
 
 
 
 
4074
  uri-js@4.4.1:
4075
  resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
4076
 
@@ -4223,6 +4254,13 @@ packages:
4223
 
4224
  snapshots:
4225
 
 
 
 
 
 
 
 
4226
  '@alloc/quick-lru@5.2.0': {}
4227
 
4228
  '@antfu/ni@25.0.0':
@@ -6267,6 +6305,8 @@ snapshots:
6267
 
6268
  chalk@5.6.2: {}
6269
 
 
 
6270
  class-variance-authority@0.7.1:
6271
  dependencies:
6272
  clsx: 2.1.1
@@ -7184,6 +7224,8 @@ snapshots:
7184
 
7185
  internmap@2.0.3: {}
7186
 
 
 
7187
  ipaddr.js@1.9.1: {}
7188
 
7189
  is-array-buffer@3.0.5:
@@ -7579,6 +7621,10 @@ snapshots:
7579
 
7580
  negotiator@1.0.0: {}
7581
 
 
 
 
 
7582
  next@16.1.4(@babel/core@7.28.6)(react-dom@19.2.3(react@19.2.3))(react@19.2.3):
7583
  dependencies:
7584
  '@next/env': 16.1.4
@@ -7730,6 +7776,10 @@ snapshots:
7730
 
7731
  package-manager-detector@1.6.0: {}
7732
 
 
 
 
 
7733
  parent-module@1.0.1:
7734
  dependencies:
7735
  callsites: 3.1.0
@@ -8242,6 +8292,10 @@ snapshots:
8242
 
8243
  sisteransi@1.0.5: {}
8244
 
 
 
 
 
8245
  source-map-js@1.2.1: {}
8246
 
8247
  source-map@0.6.1: {}
@@ -8524,6 +8578,10 @@ snapshots:
8524
  escalade: 3.2.0
8525
  picocolors: 1.1.1
8526
 
 
 
 
 
8527
  uri-js@4.4.1:
8528
  dependencies:
8529
  punycode: 2.3.1
 
8
 
9
  .:
10
  dependencies:
11
+ 3dmol:
12
+ specifier: ^2.5.4
13
+ version: 2.5.4
14
  '@floating-ui/react':
15
  specifier: ^0.27.16
16
  version: 0.27.16(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
 
77
  recharts:
78
  specifier: ^3.7.0
79
  version: 3.7.0(@types/react@19.2.9)(react-dom@19.2.3(react@19.2.3))(react-is@16.13.1)(react@19.2.3)(redux@5.0.1)
80
+ smiles-drawer:
81
+ specifier: ^2.1.7
82
+ version: 2.1.7
83
  tailwind-merge:
84
  specifier: ^3.4.0
85
  version: 3.4.0
 
135
 
136
  packages:
137
 
138
+ 3dmol@2.5.4:
139
+ resolution: {integrity: sha512-stbHw2c2T12bABmFyBrhJL4tufw+hUjvhmJthOxAxKVDwAtAZSI17Kue8VNxaHewf5oRydPo6W62BlWrbrNa6Q==}
140
+ engines: {node: '>=16.16.0', npm: '>=8.11'}
141
+
142
  '@alloc/quick-lru@5.2.0':
143
  resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
144
  engines: {node: '>=10'}
 
2086
  resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==}
2087
  engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
2088
 
2089
+ chroma-js@2.6.0:
2090
+ resolution: {integrity: sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A==}
2091
+
2092
  class-variance-authority@0.7.1:
2093
  resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
2094
 
 
2892
  resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
2893
  engines: {node: '>=12'}
2894
 
2895
+ iobuffer@5.4.0:
2896
+ resolution: {integrity: sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==}
2897
+
2898
  ipaddr.js@1.9.1:
2899
  resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==}
2900
  engines: {node: '>= 0.10'}
 
3364
  resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
3365
  engines: {node: '>= 0.6'}
3366
 
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'}
 
3489
  package-manager-detector@1.6.0:
3490
  resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
3491
 
3492
+ pako@1.0.11:
3493
+ resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
3494
+
3495
+ pako@2.1.0:
3496
+ resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
3497
+
3498
  parent-module@1.0.1:
3499
  resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
3500
  engines: {node: '>=6'}
 
3850
  sisteransi@1.0.5:
3851
  resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
3852
 
3853
+ smiles-drawer@2.1.7:
3854
+ resolution: {integrity: sha512-gApm5tsWrAYDkjbGYQb5OhwIyHvtM2kIO40DfATaOV0DPm0wA63yn4Ow7us27BT49lDdU9busCOPN9fpyonzaA==}
3855
+
3856
  source-map-js@1.2.1:
3857
  resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
3858
  engines: {node: '>=0.10.0'}
 
4099
  peerDependencies:
4100
  browserslist: '>= 4.21.0'
4101
 
4102
+ upng-js@2.1.0:
4103
+ resolution: {integrity: sha512-d3xzZzpMP64YkjP5pr8gNyvBt7dLk/uGI67EctzDuVp4lCZyVMo0aJO6l/VDlgbInJYDY6cnClLoBp29eKWI6g==}
4104
+
4105
  uri-js@4.4.1:
4106
  resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
4107
 
 
4254
 
4255
  snapshots:
4256
 
4257
+ 3dmol@2.5.4:
4258
+ dependencies:
4259
+ iobuffer: 5.4.0
4260
+ netcdfjs: 3.0.0
4261
+ pako: 2.1.0
4262
+ upng-js: 2.1.0
4263
+
4264
  '@alloc/quick-lru@5.2.0': {}
4265
 
4266
  '@antfu/ni@25.0.0':
 
6305
 
6306
  chalk@5.6.2: {}
6307
 
6308
+ chroma-js@2.6.0: {}
6309
+
6310
  class-variance-authority@0.7.1:
6311
  dependencies:
6312
  clsx: 2.1.1
 
7224
 
7225
  internmap@2.0.3: {}
7226
 
7227
+ iobuffer@5.4.0: {}
7228
+
7229
  ipaddr.js@1.9.1: {}
7230
 
7231
  is-array-buffer@3.0.5:
 
7621
 
7622
  negotiator@1.0.0: {}
7623
 
7624
+ netcdfjs@3.0.0:
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
 
7776
 
7777
  package-manager-detector@1.6.0: {}
7778
 
7779
+ pako@1.0.11: {}
7780
+
7781
+ pako@2.1.0: {}
7782
+
7783
  parent-module@1.0.1:
7784
  dependencies:
7785
  callsites: 3.1.0
 
8292
 
8293
  sisteransi@1.0.5: {}
8294
 
8295
+ smiles-drawer@2.1.7:
8296
+ dependencies:
8297
+ chroma-js: 2.6.0
8298
+
8299
  source-map-js@1.2.1: {}
8300
 
8301
  source-map@0.6.1: {}
 
8578
  escalade: 3.2.0
8579
  picocolors: 1.1.1
8580
 
8581
+ upng-js@2.1.0:
8582
+ dependencies:
8583
+ pako: 1.0.11
8584
+
8585
  uri-js@4.4.1:
8586
  dependencies:
8587
  punycode: 2.3.1