File size: 3,729 Bytes
f0992bc
 
8321d59
349f22c
f0992bc
8321d59
 
f0992bc
 
 
 
 
 
 
8321d59
f0992bc
8321d59
 
 
f0992bc
 
 
 
 
 
8321d59
 
f0992bc
8321d59
 
f0992bc
 
 
 
 
 
 
8321d59
f0992bc
 
 
 
 
 
 
 
 
 
 
 
 
 
8321d59
 
 
 
 
 
 
 
 
 
 
f0992bc
 
 
 
 
 
 
 
 
 
8321d59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0992bc
 
 
 
 
 
 
8321d59
 
 
f0992bc
 
 
 
 
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
'use client';

import { useRef, useState } from 'react';
import { GitMerge as GitCompareArrows, CircleNotch as LoaderCircle } from '@phosphor-icons/react';
import { runPairwiseAlignment } from '@/lib/api';
import { ClaySegmented } from '@/components/ui';
import { PairwiseResultDisplay } from '@/components/alignment/PairwiseResultDisplay';
import type { BlastHitSummary, PairwiseAlignResult } from '@/types/pipeline';

interface PairwiseAlignViewProps {
  hit: BlastHitSummary;
  querySequence?: string;
}

type AlignMode = 'global' | 'local';

function getStoredAlignMode(): AlignMode {
  if (typeof window === 'undefined') return 'global';
  return sessionStorage.getItem('blast_align_mode') === 'local' ? 'local' : 'global';
}

export function PairwiseAlignView({ hit, querySequence }: PairwiseAlignViewProps) {
  const [result, setResult] = useState<PairwiseAlignResult | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [mode, setMode] = useState<AlignMode>(getStoredAlignMode);
  const hasRun = useRef(false);

  const run = async (m: AlignMode) => {
    if (!querySequence) return;
    setLoading(true);
    setError(null);
    setResult(null);
    try {
      const res = await runPairwiseAlignment({
        hit_accession: hit.accession,
        query_sequence: querySequence,
        mode: m,
      });
      setResult(res);
    } catch (e: any) {
      const detail = e?.response?.data?.detail;
      setError(
        typeof detail === 'string' && detail
          ? detail
          : 'Pairwise alignment failed. Please try again.',
      );
    } finally {
      setLoading(false);
    }
  };

  const handleRun = () => {
    hasRun.current = true;
    run(mode);
  };

  const handleModeChange = (m: AlignMode) => {
    setMode(m);
    sessionStorage.setItem('blast_align_mode', m);
    if (hasRun.current) run(m);
  };

  if (!querySequence) {
    return (
      <p className="mt-2 text-xs text-text-muted">
        Query sequence is not available in this view, so pairwise re-alignment is disabled.
      </p>
    );
  }

  return (
    <div className="mt-3 pt-3 border-t border-glass-border">
      <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
        <div className="flex items-center gap-2 flex-wrap">
          <button
            onClick={handleRun}
            disabled={loading}
            className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg bg-accent-cyan/10 text-accent-cyan text-xs font-medium hover:bg-accent-cyan/20 transition border border-accent-cyan/20 disabled:opacity-60"
          >
            {loading ? (
              <LoaderCircle className="w-3.5 h-3.5 animate-spin" />
            ) : (
              <GitCompareArrows className="w-3.5 h-3.5" />
            )}
            {result ? 'Re-align pair' : 'Align pair (full sequences)'}
          </button>
          <span className="text-xs text-text-muted">
            vs <code className="font-mono">{hit.accession}</code>
          </span>
        </div>
        <ClaySegmented
          size="sm"
          options={[
            { value: 'global', label: 'Global (NW)' },
            { value: 'local', label: 'Local (SW)' },
          ]}
          value={mode}
          onChange={handleModeChange}
        />
      </div>

      {error && (
        <div className="mt-3 rounded-lg bg-error/10 border border-error/30 px-3 py-2 text-xs text-error">
          <strong>Alignment failed:</strong> {error}
        </div>
      )}

      {result && (
        <div className="mt-4">
          <PairwiseResultDisplay result={result} subjectLabel={hit.accession} />
        </div>
      )}
    </div>
  );
}