'use client'; import { useMemo } from 'react'; import { DownloadSimple as Download, ChartScatter as Scatter } from '@phosphor-icons/react'; import type { PairwiseAlignResult } from '@/types/pipeline'; import { downloadText, downloadTsv } from '@/lib/export-utils'; import { computeAlignmentStats } from '@/lib/alignment-stats'; import { AlignmentStatsBar } from '@/components/alignment/AlignmentStatsBar'; const DOTPLOT_MATRICES = ['blosum62', 'blosum50', 'blosum45', 'pam30', 'pam70', 'pam250']; function dotplotScoringFor(matrix: string): string { const m = matrix.toLowerCase(); return DOTPLOT_MATRICES.includes(m) ? m : 'identity'; } const BLOCK_SIZE = 60; interface Column { q: string; h: string; qPos: number; hPos: number; match: boolean; gap: boolean; } function buildColumns(result: PairwiseAlignResult): Column[] { const a = result.aligned_query; const b = result.aligned_hit; const cols: Column[] = []; let qPos = result.query_start; let hPos = result.hit_start; for (let i = 0; i < a.length; i++) { const q = a[i]; const h = b[i]; cols.push({ q, h, qPos, hPos, match: q !== '-' && h !== '-' && q === h, gap: q === '-' || h === '-', }); if (q !== '-') qPos++; if (h !== '-') hPos++; } return cols; } function charClass(c: string, match: boolean): string { if (c === '-') return 'text-text-muted/25'; return match ? 'text-accent-cyan' : 'text-text-primary'; } function CoverageStrip({ label, start, end, length, }: { label: string; start: number; end: number; length: number; }) { const covered = length > 0 ? ((end - start + 1) / length) * 100 : 0; const left = length > 0 ? ((start - 1) / length) * 100 : 0; return (
{label}
{Math.round(covered)}%
); } function CoverageNote({ result }: { result: PairwiseAlignResult }) { if (result.alignment_length === 0) { return (

No significant {result.mode} alignment detected — the two sequences show no local similarity.

); } const queryOnlyCoversPart = result.query_start > 1 || result.query_end < result.query_length; const hitOnlyCoversPart = result.hit_start > 1 || result.hit_end < result.hit_length; if (!queryOnlyCoversPart && !hitOnlyCoversPart) return null; return (

{queryOnlyCoversPart && ( <> Alignment covers residues {result.query_start}–{result.query_end} of a {result.query_length}-residue query. )} {queryOnlyCoversPart && hitOnlyCoversPart && ·} {hitOnlyCoversPart && ( <> Residues {result.hit_start}–{result.hit_end} of a {result.hit_length}-residue subject. )}

); } function AlignmentBlocks({ result }: { result: PairwiseAlignResult }) { const cols = useMemo(() => buildColumns(result), [result]); const blocks = useMemo(() => { const out: Column[][] = []; for (let i = 0; i < cols.length; i += BLOCK_SIZE) out.push(cols.slice(i, i + BLOCK_SIZE)); return out; }, [cols]); if (cols.length === 0) return null; return (
{blocks.map((block, bi) => (
Q{block[0].qPos} {block.map((c, i) => ( {c.q} ))}
{block.map((c, i) => ( {c.match ? '|' : ' '} ))}
S{block[0].hPos} {block.map((c, i) => ( {c.h} ))}
))}
); } function Legend() { return (
Match Mismatch Gap | Identical position
); } export function PairwiseResultDisplay({ result, queryLabel = 'Query', subjectLabel = 'Subject', }: { result: PairwiseAlignResult; queryLabel?: string; subjectLabel?: string; }) { const stats = useMemo( () => computeAlignmentStats([result.aligned_query, result.aligned_hit]), [result], ); if (result.alignment_length === 0) { return ; } const exportFasta = () => { const fasta = `>${queryLabel}\n${result.aligned_query.replace(/-/g, '')}\n>${subjectLabel}\n${result.aligned_hit.replace(/-/g, '')}`; downloadText(fasta, `${result.mode}_alignment.fasta`); }; const exportTsv = () => { downloadTsv( ['Field', 'Value'], [ ['Mode', result.mode], ['Matrix', result.matrix], ['Score', String(result.score)], ['Matched', String(stats.matched)], ['Mismatched', String(stats.mismatched)], ['Gapped columns', String(stats.gapped)], ['Gap characters', String(result.gaps_total)], ['Alignment length', String(stats.length)], ['Identity (%)', String(result.pct_identity)], ['Identical residues', String(result.identity)], ['Query start', String(result.query_start)], ['Query end', String(result.query_end)], ['Query length', String(result.query_length)], ['Subject start', String(result.hit_start)], ['Subject end', String(result.hit_end)], ['Subject length', String(result.hit_length)], ], `${result.mode}_alignment_stats.tsv`, ); }; return (
Score: {result.score} Identity: {result.pct_identity}% ({result.identity}/{result.alignment_length}) Matrix: {result.matrix.toUpperCase()} Mode: {result.mode} {result.mode === 'global' ? 'Needleman-Wunsch' : 'Smith-Waterman'} Dot plot
{queryLabel} {result.query_start}–{result.query_end} · {result.query_length} aa
{subjectLabel} {result.hit_start}–{result.hit_end} · {result.hit_length} aa
); }