Spaces:
Running
Running
File size: 8,458 Bytes
f0b48d6 6c03381 f0b48d6 349f22c f0b48d6 cc7ed3f 13df1a1 f0b48d6 b3c481f fe5ce87 bac1801 c83d9b9 ec313db 4de1e1a 6c03381 bac1801 aa498ec f0b48d6 cc7ed3f f0b48d6 cc7ed3f f0b48d6 b3c481f f0b48d6 aa498ec b3c481f cc7ed3f f0b48d6 cc7ed3f f0b48d6 cc7ed3f 13df1a1 b3c481f cc7ed3f b3c481f f0b48d6 aa498ec f0b48d6 6c03381 cc7ed3f 6c03381 f0b48d6 cc7ed3f 6c03381 f0b48d6 aa498ec 6c03381 f0b48d6 aa498ec f0b48d6 aa498ec f0b48d6 6c03381 f0b48d6 6c03381 f0b48d6 150c960 f0b48d6 150c960 f0b48d6 cc7ed3f f0b48d6 bac1801 4de1e1a bac1801 150c960 cc7ed3f ec313db bac1801 6c03381 bac1801 6c03381 bac1801 f0b48d6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | '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<AlignmentMethod, string> = {
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<AlignmentMethod>('clustalo');
const [loading, setLoading] = useState(false);
const [result, setResult] = useState<AlignmentResult | null>(null);
const [error, setError] = useState<string | null>(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 (
<div className="max-w-3xl">
<BackButton />
<PageHeader
title="Multiple Sequence Alignment"
subtitle="Align two or more protein or DNA sequences with a choice of EBI methods."
/>
<motion.div variants={fadeUp} initial={{ y: 24 }} animate="show" className="data-card p-5 mb-6 space-y-4">
<ClaySegmented
options={[
{ value: 'protein', label: 'Protein' },
{ value: 'dna', label: 'DNA' },
] as { value: string; label: string }[]}
value={stype}
onChange={(v) => { setStype(v); setError(null); setResult(null); }}
/>
<ClaySegmented
options={METHOD_OPTIONS as { value: string; label: string }[]}
value={method}
onChange={(v) => { setMethod(v as AlignmentMethod); setError(null); setResult(null); }}
/>
<FlatTextarea
value={input}
onChange={(e) => 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"
/>
<div className="flex gap-3">
<button
onClick={handleSample}
className="text-sm text-accent-cyan hover:text-accent-cyan/80 underline"
>
Load sample {stype === 'protein' ? 'p53 (human vs mouse)' : 'DNA sequences'}
</button>
<div className="flex-1" />
<CriticalButton onClick={handleSubmit} disabled={loading || !input.trim()}>
{loading ? <LoaderCircle className="w-4 h-4 animate-spin" /> : null}
{loading ? 'Aligning...' : 'Align'}
</CriticalButton>
</div>
</motion.div>
{error && (
<motion.div variants={fadeUp} initial={{ y: 24 }} animate="show" className="glass-card p-4 border border-error/20">
<p className="text-sm text-error">{error}</p>
</motion.div>
)}
{loading && !result && (
<motion.div variants={fadeUp} initial={{ y: 24 }} animate="show" className="glass-card p-8 text-center">
<LoaderCircle className="w-6 h-6 animate-spin text-accent-cyan mx-auto mb-3" />
<p className="text-sm text-text-secondary">Running {METHOD_LABELS[method]} on EBI servers...</p>
</motion.div>
)}
{result && (() => {
const alignedSeqs = parseAlignedFasta(result.aln_fasta).seqs;
return (
<motion.div variants={fadeUp} initial={{ y: 24 }} animate="show" className="space-y-4">
<div className="data-card p-5">
<h3 className="text-sm font-semibold text-text-primary mb-3">Alignment (FASTA) — {METHOD_LABELS[result.method as AlignmentMethod] ?? result.method ?? 'Clustal Omega'}</h3>
<AlignmentStatsBar stats={computeAlignmentStats(alignedSeqs)} className="mb-3" />
<AlignmentBlock alnFasta={result.aln_fasta} className="max-h-80" />
</div>
{result.phylotree && (
<div className="data-card p-5">
<PhyloTreeViewer newick={result.phylotree} />
</div>
)}
{alignedSeqs.length >= 2 && (
<div className="data-card p-5">
<ConservationTrack alignedSeqs={alignedSeqs} />
</div>
)}
</motion.div>
);
})()}
</div>
);
}
|