'use client'; import { useState } from 'react'; import { DownloadSimple, CaretDown, Warning } from '@phosphor-icons/react'; import toast from 'react-hot-toast'; import { longApi } from '@/lib/api'; interface StructureExportMenuProps { /** UniProt accession or PDB ID — enables structure exports (PDB/CIF/PSE/ChimeraX/VMD) */ identifier?: string | null; /** Docking job ID — enables docking exports (complex PDB / ligand SDF) */ dockingJobId?: string | null; } function saveBlob(blob: Blob, filename: string) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } async function downloadFile(path: string, filename: string) { const res = await longApi.get(path, { responseType: 'blob', timeout: 120_000 }); saveBlob(new Blob([res.data]), filename); } const CHIMERAX_SCRIPT_NOTE = '# ChimeraX command script — run after opening the downloaded CIF/PDB:\n' + '# open \n' + '# cartoon\n' + '# color byattribute bfactor palette 0,#ff7d45:50,#ffcb12:70,#65cbf3:90,#0053d6 target a\n\n' + 'open $1\ncartoon\ncolor byattribute bfactor palette 0,#ff7d45:50,#ffcb12:70,#65cbf3:90,#0053d6 target a\n'; const VMD_SCRIPT_NOTE = '# VMD/Tcl script — run after opening the downloaded PDB:\n' + '# vmd -e docking.tcl\n\n' + 'mol new [lindex $argv 0]\nmol delrep 0 top\nmol representation NewCartoon\nmol color Beta\nmol addrep top\n'; export function StructureExportMenu({ identifier, dockingJobId }: StructureExportMenuProps) { const [open, setOpen] = useState(false); const [busy, setBusy] = useState(null); if (!identifier && !dockingJobId) return null; const label = identifier ?? `job-${dockingJobId!.slice(0, 8)}`; const items: Array<{ id: string; label: string; hint: string; run: () => Promise; }> = []; if (identifier) { items.push( { id: 'pdb', label: 'PDB', hint: '.pdb coordinates', run: async () => { await downloadFile(`/api/structure-export/structure/${identifier}?format=pdb`, `${label}.pdb`); } }, { id: 'cif', label: 'mmCIF', hint: '.cif coordinates', run: async () => { await downloadFile(`/api/structure-export/structure/${identifier}?format=cif`, `${label}.cif`); } }, { id: 'pse', label: 'PyMOL session', hint: '.pse — cartoon + pLDDT spectrum pre-applied', run: async () => { await downloadFile(`/api/structure-export/structure/${identifier}?format=pse`, `${label}_styled.pse`); } }, { id: 'chimerax', label: 'ChimeraX bundle', hint: '.cif + command script (no fake .cxs)', run: async () => { await downloadFile(`/api/structure-export/structure/${identifier}?format=cif`, `${label}.cif`); saveBlob(new Blob([CHIMERAX_SCRIPT_NOTE], { type: 'text/plain' }), `${label}_chimerax.cxc`); } }, { id: 'vmd', label: 'VMD bundle', hint: '.pdb + tcl script (no fake state file)', run: async () => { await downloadFile(`/api/structure-export/structure/${identifier}?format=pdb`, `${label}.pdb`); saveBlob(new Blob([VMD_SCRIPT_NOTE], { type: 'text/plain' }), `${label}_vmd.tcl`); } }, ); } if (dockingJobId) { items.push( { id: 'complex', label: 'Complex PDB', hint: 'receptor + docked ligand merged', run: async () => { await downloadFile(`/api/docking/result/${dockingJobId}/complex.pdb`, `complex_${dockingJobId.slice(0, 8)}.pdb`); } }, { id: 'sdf', label: 'Ligand SDF', hint: 'docked poses only', run: async () => { await downloadFile(`/api/docking/result/${dockingJobId}/ligand.sdf`, `docked_${dockingJobId.slice(0, 8)}.sdf`); } }, ); } const handle = async (item: (typeof items)[number]) => { setOpen(false); setBusy(item.id); try { await item.run(); toast.success(`Downloaded ${item.label}`); } catch (err: unknown) { const detail = (err as { response?: { data?: Blob | unknown; status?: number } }).response; if (detail?.status === 503) toast.error('PyMOL export unavailable on this deployment'); else toast.error(`Export failed${detail?.status ? ` (HTTP ${detail.status})` : ''}`); } finally { setBusy(null); } }; return (
{open && ( <> {/* click-away layer */}
setOpen(false)} />
{items.map((item) => ( ))}
)}
); }