'use client'; import { useState } from 'react'; import { motion } from 'framer-motion'; import { CaretDown as ChevronDown, CaretUp as ChevronUp, ArrowSquareOut as ExternalLink, Download, ChartScatter as Scatter, Target } from '@phosphor-icons/react'; import type { BlastHitSummary } from '@/types/pipeline'; import { AlignmentView } from './AlignmentView'; import { PairwiseAlignView } from './PairwiseAlignView'; import { downloadTsv } from '@/lib/export-utils'; import { fadeUp, stagger, cardHover } from '@/lib/animations'; import { confidenceBand, formatEvalue, coverageColor } from '@/lib/confidence'; interface BlastPanelProps { hits: BlastHitSummary[] | undefined | null; count: number; source?: string; queryLength?: number; querySequence?: string; /** Full-length subject sequences keyed by hit accession (e.g. from the MSA). */ fullSequences?: Record; } /** Looks like a protein (not a clean nucleotide alphabet) -> BLOSUM scoring. */ function looksLikeProtein(seq: string): boolean { const cleaned = seq.toUpperCase().replace(/[^A-Za-z]/g, ''); if (!cleaned) return false; return cleaned.split('').some(ch => !'ACGTUN'.includes(ch)); } export function BlastPanel({ hits, count, source, querySequence, fullSequences }: BlastPanelProps) { const [expanded, setExpanded] = useState(null); const safeHits = hits ?? []; /** Best available subject sequence for a hit: the full-length MSA copy when * present, otherwise the aligned segment (gaps stripped). */ const subjectSequence = (hit: BlastHitSummary): string => { const full = fullSequences?.[hit.accession] ?? fullSequences?.[hit.accession.replace(/\.\d+$/, '')]; if (full) return full; return (hit.hit_alignment || hit.midline || '').replace(/-/g, ''); }; return (

BLAST Hits ({count} found{source ? ` via ${source}` : ''})

{safeHits.length === 0 ? (
No hits to display
) : safeHits.map((hit, i) => { const band = confidenceBand(hit.evalue); const isExpanded = expanded === i; return ( {isExpanded && ( )} ); })}
); }