'use client'; import { useState, useEffect, useRef, useMemo } from 'react'; import { motion } from 'framer-motion'; import { CircleNotch as LoaderCircle, CheckCircle as Check, Warning as AlertTriangle, XCircle as X, Funnel, Dna } from '@phosphor-icons/react'; import { fadeUp, stagger } from '@/lib/animations'; import { extractErrorMessage } from '@/lib/errors'; import { useAuditTrail } from '@/hooks/useAuditTrail'; import { DockingViewer } from '@/components/DockingViewer'; import { BackButton, PageHeader, CriticalButton, FlatInput } from '@/components/ui'; import { runStructurePrep, runStructurePrepSequence, getStructurePrepStatus, type StructurePrepResult } from '@/lib/api'; type PipelineStatus = StructurePrepResult; type InputMode = 'pdb_id' | 'sequence'; const STEP_LABELS: Record = { fetching: 'Fetching structure', predicting_structure: 'Predicting structure (ESMFold)', analyzing: 'Detecting chain health', repairing: 'SWISS-MODEL repair', cleaning: 'PyMOL cleanup', running_fpocket: 'fpocket (local)', running_castp: 'CASTp (remote)', complete: 'Complete', }; function StatusBadge({ step, target }: { step: string; target: string }) { const steps = Object.keys(STEP_LABELS); const currentIdx = steps.indexOf(step); const targetIdx = steps.indexOf(target); if (step === 'complete' || currentIdx > targetIdx) { return ; } if (currentIdx === targetIdx) { return ; } return
; } export default function StructurePrepPage() { const [mode, setMode] = useState('pdb_id'); const [pdbId, setPdbId] = useState(''); const [sequence, setSequence] = useState(''); const [probeRadius, setProbeRadius] = useState(1.4); const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [progress, setProgress] = useState(0); const audit = useAuditTrail(); const pollRef = useRef | null>(null); const cleanSeq = sequence.replace(/[^A-Za-z]/g, '').toUpperCase(); const pdbUrl = useMemo(() => { if (!result?.cleaned_pdb) return null; const blob = new Blob([result.cleaned_pdb], { type: 'text/plain' }); return URL.createObjectURL(blob); }, [result?.cleaned_pdb]); useEffect(() => () => { if (pdbUrl) URL.revokeObjectURL(pdbUrl); }, [pdbUrl]); const canSubmit = mode === 'pdb_id' ? pdbId.trim().length > 0 : cleanSeq.length >= 10; const handleRun = async () => { const label = mode === 'pdb_id' ? `pdb:${pdbId.trim().toUpperCase()}` : `seq:${cleanSeq.length}aa`; audit.emitStarted('structure_prep', 'Pipeline', label); setLoading(true); setError(null); setResult(null); setProgress(0); try { const res = mode === 'pdb_id' ? await runStructurePrep(pdbId.trim().toUpperCase(), probeRadius) : await runStructurePrepSequence(cleanSeq, probeRadius); pollForResults(res.job_id); } catch (err: unknown) { const msg = extractErrorMessage(err, 'Pipeline failed'); audit.emitFailed('structure_prep', 'Pipeline', label, msg); setError(msg); setLoading(false); } }; const pollForResults = (jobId: string) => { let elapsed = 0; pollRef.current = setInterval(async () => { elapsed += 2; setProgress(Math.min(10 + (elapsed / 180) * 80, 90)); try { const data = await getStructurePrepStatus(jobId); if (data.status === 'complete') { if (pollRef.current) clearInterval(pollRef.current); setResult(data); setLoading(false); setProgress(100); audit.emitSuccess('structure_prep', 'Pipeline', `job:${jobId}`, `${data.fpocket_pockets.length} fpocket + ${data.castp_pockets.length} castp pockets`); } else if (data.status === 'failed') { if (pollRef.current) clearInterval(pollRef.current); setError(data.error || 'Pipeline failed'); setLoading(false); audit.emitFailed('structure_prep', 'Pipeline', `job:${jobId}`, data.error || 'unknown'); } else { setResult(data); } } catch { // keep polling } }, 2000); }; return (
{mode === 'pdb_id' ? (
{ setPdbId(e.target.value); setResult(null); setError(null); }} onKeyDown={(e) => e.key === 'Enter' && handleRun()} placeholder="PDB ID (e.g. 1TIM, 4HHB, 1FME)" className="flex-1" /> {loading ? : } Run Pipeline
) : (