Spaces:
Sleeping
Sleeping
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 +2 -0
- ui/components/visualization/protein-viewer.tsx +243 -0
- ui/components/visualization/smiles-2d-viewer.tsx +119 -0
- ui/lib/visualization-api.ts +57 -0
- ui/lib/visualization-types.ts +20 -0
- ui/package.json +2 -0
- ui/pnpm-lock.yaml +58 -0
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
|