'use client'; import { useState, useEffect, useRef, useMemo } from 'react'; import { motion } from 'framer-motion'; import { CircleNotch as LoaderCircle, Rocket as Send } from '@phosphor-icons/react'; import { fadeUp, stagger } from '@/lib/animations'; import { predictStructure, getPredictionStatus, type PredictionResult } from '@/lib/api'; import { extractErrorMessage } from '@/lib/errors'; import { useAuditTrail } from '@/hooks/useAuditTrail'; import { DockingViewer } from '@/components/DockingViewer'; import { BackButton, PageHeader, CriticalButton, FlatInput } from '@/components/ui'; function PredictResultDisplay({ pdb, result, seqLen }: { pdb: string; result: PredictionResult; seqLen: number }) { const pdbUrl = useMemo(() => { const blob = new Blob([pdb], { type: 'text/plain' }); return URL.createObjectURL(blob); }, [pdb]); useEffect(() => { return () => URL.revokeObjectURL(pdbUrl); }, [pdbUrl]); return (

Predicted Structure

{result.mean_plddt != null && ( Mean pLDDT: {result.mean_plddt.toFixed(1)} )} {result.ptm != null && ( pTM: {result.ptm.toFixed(3)} )} {seqLen} residues

Download

); } const EXAMPLES = [ { label: 'Villin headpiece (35 res)', seq: 'MDYKDDDDKLEIRELHEEAAKEFAKEIAAALPAAEA' }, { label: 'Trp-cage (20 res)', seq: 'NLYIQWLKDGGPSSGRPPPS' }, ]; export default function PredictStructurePage() { const [sequence, setSequence] = useState(''); const [jobTitle, setJobTitle] = useState(''); const [jobId, setJobId] = useState(null); 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 seqLen = cleanSeq.length; const handleSubmit = async () => { if (!cleanSeq || seqLen < 10) return; audit.emitStarted('structure_predict', 'ESMFold', `len:${seqLen}`); setLoading(true); setError(null); setResult(null); setProgress(0); try { const res = await predictStructure(cleanSeq, jobTitle || `ESMFold ${seqLen} res`); setJobId(res.job_id); setProgress(10); } catch (err: unknown) { const msg = extractErrorMessage(err, 'Prediction failed'); audit.emitFailed('structure_predict', 'ESMFold', `len:${seqLen}`, msg); setError(msg); setLoading(false); } }; useEffect(() => { if (!jobId) return; let elapsed = 0; pollRef.current = setInterval(async () => { elapsed += 2; setProgress(Math.min(10 + (elapsed / 120) * 80, 90)); try { const res = await getPredictionStatus(jobId); if (res.status === 'complete') { if (pollRef.current) clearInterval(pollRef.current); setResult(res); setLoading(false); setProgress(100); audit.emitSuccess('structure_predict', 'ESMFold', `len:${seqLen}`, `plddt:${res.mean_plddt}`); } else if (res.status === 'failed') { if (pollRef.current) clearInterval(pollRef.current); setError(res.error || 'Prediction failed'); setLoading(false); audit.emitFailed('structure_predict', 'ESMFold', `len:${seqLen}`, res.error || 'unknown'); } } catch { // keep polling } }, 2000); return () => { if (pollRef.current) clearInterval(pollRef.current); }; }, [jobId, seqLen, audit]); return (
setJobTitle(e.target.value)} placeholder="Job title (optional)" className="w-full mb-3" />