'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 (
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 (