'use client'; import { useState } from 'react'; import { motion } from 'framer-motion'; import { CircleNotch as LoaderCircle } from '@phosphor-icons/react'; import { fadeUp } from '@/lib/animations'; import { runAlignment, type AlignmentMethod } from '@/lib/api'; import { extractErrorMessage } from '@/lib/errors'; import type { AlignmentResult } from '@/lib/api'; import { useAuditTrail } from '@/hooks/useAuditTrail'; import PhyloTreeViewer from '@/components/phylo/PhyloTreeViewer'; import { ConservationTrack } from '@/components/alignment/ConservationTrack'; import { AlignmentStatsBar } from '@/components/alignment/AlignmentStatsBar'; import { AlignmentBlock } from '@/components/alignment/AlignmentBlock'; import { computeAlignmentStats, parseAlignedFasta } from '@/lib/alignment-stats'; import { BackButton, CriticalButton, ClaySegmented, FlatTextarea, PageHeader } from '@/components/ui'; const SAMPLE_PROTEIN = `>p53_human MEEPQSDPSVEPPLSQETFSDLWKLLPENNVLSPLPSQAMDDLMLSPDDIEQWFTEDPGPDEAPRMPEAAPPVAPAPAAPTPAAPAPAPSWPLSSSVPSQKTYQGSYGFRLGFLHSGTAKSVTCTYSPALNKMFCQLAKTCPVQLWVDSTPPPGTRVRAMAIYKQSQHMTEVVRRCPHHERCSDSDGLAPPQHLIRVEGNLRVEYLDDRNTFRHSVVVPYEPPEVGSDCTTIHYNYMCNSSCMGGMNRRPILTIITLEDSSGNLLGRNSFEVRVCACPGRDRRTEEENLRKKGEPHHELPPGSTKRALPNNTSSSPQPKKKPLDGEYFTLQIRGRERFEMFRELNEALELKDAQAGKEPGGSRAHSSHLKSKKGQSTSRHKKLMFKTEGPDSD >p53_mouse MEEPQSDPSIEPPLSQETFSDLWKLLPENNVLSPLPSQAVDDLMLSPDDLAQWFTEDPGPDEAPRMSEAAPPAAPAPAAPTPAAPAPAPSWPLSSFVPSQKTYQGNYGFHLGFLQSGTAKSVMCTYSPPLNKLFCQLAKTCPVQLWVSATPPAGSRVRAMAIYKKSQHMTEVVRRCPHHERCSDSSDGLAPPQHLIRVEGNLRAEYLDDRNTFRHSIVVPYEPPEVGSDCTTIHYNYMCNSSCMGGMNRRPILTIITLEDSSGNLLGRDSFEVRVCACPGRDRRTEEENFKKKEPCPEPPPGSTRALGSTSTSSPTPKKKPLDGEYFTLKIRGRERFEMFRELNEALELKDAHATEEPFGGSRAHSSHLKSKKGQSTSRHKKFKKTADPSS`; const SAMPLE_DNA = `>seq_human AGCTAGCTAGCTAGCTAGCTAGCTAGCTAGCT >seq_chimp AGCTAGCTAGCTAGCTAGCCAGCTAGCTAGCT`; function parseFasta(text: string): { headers: string[]; sequences: string[] } { const lines = text.split('\n'); const headers: string[] = []; const sequences: string[] = []; let currentHeader = ''; let currentSeq = ''; for (const line of lines) { if (line.startsWith('>')) { if (currentHeader) { headers.push(currentHeader); sequences.push(currentSeq); } currentHeader = line.slice(1).trim(); currentSeq = ''; } else if (currentHeader) { currentSeq += line.trim(); } } if (currentHeader) { headers.push(currentHeader); sequences.push(currentSeq); } return { headers, sequences }; } function validateFasta(text: string): string | null { if (!text.trim()) return 'Enter sequences in FASTA format'; const { headers, sequences } = parseFasta(text); if (headers.length < 2) return 'Provide at least 2 sequences in FASTA format (each starting with >)'; const uniqueSeqs = new Set(sequences.map(s => s.toUpperCase().replace(/[^A-Z]/g, ''))); if (uniqueSeqs.size < 2) return 'Sequences are identical — provide different sequences for alignment'; for (let i = 0; i < sequences.length; i++) { const clean = sequences[i].replace(/[^A-Za-z]/g, ''); if (clean.length < 4) return `Sequence "${headers[i]}" is too short (min 4 residues)`; } return null; } const METHOD_LABELS: Record = { clustalo: 'Clustal Omega', muscle: 'MUSCLE', kalign: 'Kalign', mafft: 'MAFFT', tcoffee: 'T-Coffee', }; const METHOD_OPTIONS = (Object.entries(METHOD_LABELS) as [AlignmentMethod, string][]).map(([value, label]) => ({ value, label })); export default function AlignmentPage() { const [input, setInput] = useState(''); const [stype, setStype] = useState('protein'); const [method, setMethod] = useState('clustalo'); const [loading, setLoading] = useState(false); const [result, setResult] = useState(null); const [error, setError] = useState(null); const audit = useAuditTrail(); const handleSubmit = async () => { const validationError = validateFasta(input); if (validationError) { setError(validationError); return; } const seqCount = parseFasta(input).headers.length; const inputSummary = `type:${stype},seqs:${seqCount}`; audit.emitStarted('alignment_run', METHOD_LABELS[method], inputSummary); setLoading(true); setError(null); setResult(null); try { const res = await runAlignment(input, stype, method); setResult(res); audit.emitSuccess('alignment_run', METHOD_LABELS[method], inputSummary, `job_id:${res?.job_id ?? ''},stype:${res?.stype ?? ''}`); } catch (err: unknown) { const errMsg = extractErrorMessage(err, 'Alignment failed'); audit.emitFailed('alignment_run', METHOD_LABELS[method], inputSummary, errMsg); setError(errMsg); } finally { setLoading(false); } }; const handleSample = () => { setInput(stype === 'protein' ? SAMPLE_PROTEIN : SAMPLE_DNA); setError(null); setResult(null); }; return (
{ setStype(v); setError(null); setResult(null); }} /> { setMethod(v as AlignmentMethod); setError(null); setResult(null); }} /> setInput(e.target.value)} placeholder={`Paste 2+ sequences in FASTA format...\n\n>sequence_1\nMEEPQSDPSVEPPLSQETFSDLWKLLPENN\n>sequence_2\nMEEPQSDPSIEPPLSQETFSDLWKLLPENN`} className="w-full h-48 font-mono text-sm text-text-primary" />
{loading ? : null} {loading ? 'Aligning...' : 'Align'}
{error && (

{error}

)} {loading && !result && (

Running {METHOD_LABELS[method]} on EBI servers...

)} {result && (() => { const alignedSeqs = parseAlignedFasta(result.aln_fasta).seqs; return (

Alignment (FASTA) — {METHOD_LABELS[result.method as AlignmentMethod] ?? result.method ?? 'Clustal Omega'}

{result.phylotree && (
)} {alignedSeqs.length >= 2 && (
)}
); })()}
); }