@teselagen/ove 0.8.39 → 0.8.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,810 @@
1
+ /* Copyright (C) 2018 TeselaGen Biotechnology, Inc. */
2
+ import React, {
3
+ useEffect,
4
+ useCallback,
5
+ useReducer,
6
+ useRef,
7
+ useState
8
+ } from "react";
9
+ import {
10
+ Button,
11
+ InputGroup,
12
+ NumericInput,
13
+ Popover,
14
+ Position,
15
+ Switch,
16
+ TextArea,
17
+ Tooltip
18
+ } from "@blueprintjs/core";
19
+ import { InfoHelper, TgHTMLSelect } from "@teselagen/ui";
20
+ import {
21
+ findApproxMatches,
22
+ findSequenceMatches,
23
+ getFeatureToColorMap
24
+ } from "@teselagen/sequence-utils";
25
+ import { debounce } from "lodash-es";
26
+ import { getSingular } from "../utils/annotationTypes";
27
+ import { MAX_MATCHES_DISPLAYED } from "../constants/findToolConstants";
28
+ import { getGapMap } from "./getGapMap";
29
+ import { scrollToAlignmentSelection, updateCaretPosition } from "./utils";
30
+ import "./style.css";
31
+ import "../FindBar/style.css";
32
+
33
+ const MATCH_COLOR = "gold";
34
+ const CURRENT_MATCH_COLOR = "green";
35
+ const MISMATCH_COLOR = "red";
36
+ const ANNOTATION_TYPES = ["features", "parts", "primers"];
37
+
38
+ const initialSearchState = {
39
+ searchText: "",
40
+ matches: [],
41
+ currentMatchIndex: 0,
42
+ searched: false,
43
+ featureMatches: [],
44
+ dnaOrAA: "DNA",
45
+ ambiguousOrLiteral: "LITERAL",
46
+ mismatchesAllowed: 0
47
+ };
48
+
49
+ function searchReducer(state, action) {
50
+ switch (action.type) {
51
+ case "SET_SEARCH_TEXT":
52
+ return { ...state, searchText: action.payload };
53
+ case "SET_MATCHES":
54
+ return {
55
+ ...state,
56
+ matches: action.payload.matches,
57
+ currentMatchIndex: action.payload.currentMatchIndex
58
+ };
59
+ case "SET_CURRENT_MATCH_INDEX":
60
+ return { ...state, currentMatchIndex: action.payload };
61
+ case "SET_SEARCHED":
62
+ return { ...state, searched: action.payload };
63
+ case "SEARCH_COMPLETE":
64
+ return {
65
+ ...state,
66
+ matches: action.payload.matches,
67
+ currentMatchIndex: action.payload.currentMatchIndex,
68
+ searched: action.payload.searched
69
+ };
70
+ case "SET_FEATURE_MATCHES":
71
+ return { ...state, featureMatches: action.payload };
72
+ case "SET_DNA_OR_AA":
73
+ return { ...state, dnaOrAA: action.payload };
74
+ case "SET_AMBIGUOUS_OR_LITERAL":
75
+ return { ...state, ambiguousOrLiteral: action.payload };
76
+ case "SET_MISMATCHES_ALLOWED":
77
+ return { ...state, mismatchesAllowed: Math.max(0, action.payload) };
78
+ case "RESET":
79
+ return { ...initialSearchState };
80
+ default:
81
+ return state;
82
+ }
83
+ }
84
+
85
+ export function AlignmentSearchBar(props) {
86
+ const { alignmentTracks = [], setSearchMatchLayers } = props;
87
+
88
+ const [searchState, dispatch] = useReducer(searchReducer, initialSearchState);
89
+ const {
90
+ searchText,
91
+ matches,
92
+ currentMatchIndex,
93
+ searched,
94
+ featureMatches,
95
+ dnaOrAA,
96
+ ambiguousOrLiteral,
97
+ mismatchesAllowed
98
+ } = searchState;
99
+
100
+ const debouncedSearch = useRef(
101
+ debounce((text, search, featureSearch) => {
102
+ search(text);
103
+ featureSearch(text);
104
+ }, 50)
105
+ ).current;
106
+
107
+ useEffect(() => {
108
+ return () => {
109
+ debouncedSearch.cancel();
110
+ };
111
+ }, [debouncedSearch]);
112
+
113
+ const [highlightAll, setHighlightAll] = useState(false);
114
+ const [isExpanded, setIsExpanded] = useState(false);
115
+ const [isOpen, setIsOpen] = useState(false);
116
+ const [isPopoverOpen, setIsPopoverOpen] = useState(false);
117
+
118
+ const handleToggleExpanded = useCallback(() => {
119
+ setIsExpanded(prev => {
120
+ const next = !prev;
121
+ if (!next) setIsPopoverOpen(true);
122
+ return next;
123
+ });
124
+ }, [setIsPopoverOpen]);
125
+
126
+ useEffect(() => {
127
+ dispatch({ type: "RESET" });
128
+ if (setSearchMatchLayers) setSearchMatchLayers([]);
129
+ }, [setSearchMatchLayers]);
130
+
131
+ const buildMatchLayers = useCallback(
132
+ (allMatches, activeIndex) => {
133
+ if (!setSearchMatchLayers) return;
134
+ if (!allMatches.length) {
135
+ setSearchMatchLayers([]);
136
+ return;
137
+ }
138
+ const makeMismatchLayers = match =>
139
+ (match.mismatchAlignmentPositions || []).map(pos => ({
140
+ start: pos,
141
+ end: pos,
142
+ color: MISMATCH_COLOR,
143
+ className: "veSearchMismatch",
144
+ ignoreGaps: true,
145
+ hideCarets: true
146
+ }));
147
+
148
+ const layers = highlightAll
149
+ ? allMatches.flatMap((match, i) => [
150
+ {
151
+ start: match.alignmentStart,
152
+ end: match.alignmentEnd,
153
+ color: i === activeIndex ? CURRENT_MATCH_COLOR : MATCH_COLOR,
154
+ className:
155
+ i === activeIndex ? "veSearchLayerActive" : "veSearchLayer",
156
+ ignoreGaps: true
157
+ },
158
+ ...makeMismatchLayers(match)
159
+ ])
160
+ : [
161
+ {
162
+ start: allMatches[activeIndex].alignmentStart,
163
+ end: allMatches[activeIndex].alignmentEnd,
164
+ color: CURRENT_MATCH_COLOR,
165
+ className: "veSearchLayerActive",
166
+ ignoreGaps: true
167
+ },
168
+ ...makeMismatchLayers(allMatches[activeIndex])
169
+ ];
170
+ setSearchMatchLayers(layers);
171
+ },
172
+ [setSearchMatchLayers, highlightAll]
173
+ );
174
+
175
+ const navigateTo = useCallback(
176
+ (allMatches, index) => {
177
+ const match = allMatches[index];
178
+ if (!match) return;
179
+ updateCaretPosition({
180
+ start: match.alignmentStart,
181
+ end: match.alignmentEnd
182
+ });
183
+ setTimeout(() => {
184
+ scrollToAlignmentSelection();
185
+ }, 0);
186
+ buildMatchLayers(allMatches, index);
187
+ },
188
+ [buildMatchLayers]
189
+ );
190
+
191
+ const runSearch = useCallback(
192
+ text => {
193
+ const query = text.trim();
194
+ if (!query) {
195
+ dispatch({
196
+ type: "SEARCH_COMPLETE",
197
+ payload: { matches: [], currentMatchIndex: 0, searched: false }
198
+ });
199
+ if (setSearchMatchLayers) setSearchMatchLayers([]);
200
+ return;
201
+ }
202
+
203
+ const allMatches = [];
204
+ alignmentTracks.slice(0, 1).forEach((track, trackIndex) => {
205
+ const rawSeq = track.sequenceData?.sequence || "";
206
+ const alignedSeq = track.alignmentData?.sequence || "";
207
+ const gapMap = getGapMap(alignedSeq);
208
+ const gapOffset = n => gapMap[n] ?? gapMap[gapMap.length - 1] ?? 0;
209
+
210
+ let seqMatches = [];
211
+ if (
212
+ dnaOrAA === "DNA" &&
213
+ ambiguousOrLiteral === "LITERAL" &&
214
+ mismatchesAllowed > 0
215
+ ) {
216
+ const approxMatches = findApproxMatches(
217
+ query.toLowerCase(),
218
+ rawSeq.toLowerCase(),
219
+ mismatchesAllowed,
220
+ false
221
+ );
222
+ seqMatches = approxMatches.map(m => ({
223
+ start: m.index,
224
+ end: m.index + m.match.length - 1,
225
+ mismatchPositions: m.mismatchPositions
226
+ }));
227
+ } else {
228
+ seqMatches = findSequenceMatches(rawSeq, query, {
229
+ isCircular: false,
230
+ isAmbiguous: ambiguousOrLiteral === "AMBIGUOUS",
231
+ isProteinSearch: dnaOrAA !== "DNA",
232
+ searchReverseStrand: dnaOrAA === "DNA"
233
+ });
234
+ }
235
+
236
+ const hitsToProcess =
237
+ query.length < 2 ? seqMatches.slice(0, 1) : seqMatches;
238
+ hitsToProcess.forEach(({ start, end, mismatchPositions }) => {
239
+ const alignmentStart = start + gapOffset(start);
240
+ const alignmentEnd = end + gapOffset(end);
241
+ const mismatchAlignmentPositions = (mismatchPositions || []).map(
242
+ p => {
243
+ const absPos = start + p;
244
+ return absPos + gapOffset(absPos);
245
+ }
246
+ );
247
+ allMatches.push({
248
+ trackIndex,
249
+ alignmentStart,
250
+ alignmentEnd,
251
+ mismatchAlignmentPositions
252
+ });
253
+ });
254
+ });
255
+
256
+ const results = query.length < 2 ? allMatches.slice(0, 1) : allMatches;
257
+
258
+ dispatch({
259
+ type: "SEARCH_COMPLETE",
260
+ payload: { matches: results, currentMatchIndex: 0, searched: true }
261
+ });
262
+
263
+ if (results.length) {
264
+ navigateTo(results, 0);
265
+ } else {
266
+ if (setSearchMatchLayers) setSearchMatchLayers([]);
267
+ }
268
+ },
269
+ [
270
+ alignmentTracks,
271
+ navigateTo,
272
+ dnaOrAA,
273
+ ambiguousOrLiteral,
274
+ mismatchesAllowed,
275
+ setSearchMatchLayers
276
+ ]
277
+ );
278
+
279
+ const runFeatureSearch = useCallback(
280
+ text => {
281
+ const query = text.trim().toLowerCase();
282
+ if (!query) {
283
+ dispatch({ type: "SET_FEATURE_MATCHES", payload: [] });
284
+ return;
285
+ }
286
+
287
+ const allMatches = [];
288
+ alignmentTracks.slice(0, 1).forEach((track, trackIndex) => {
289
+ const { sequenceData, alignmentData } = track;
290
+ const alignedSeq = alignmentData?.sequence || "";
291
+ const gapMap = getGapMap(alignedSeq);
292
+ const gapOffset = n => gapMap[n] ?? gapMap[gapMap.length - 1] ?? 0;
293
+ const trackName =
294
+ alignmentData?.name || sequenceData?.name || sequenceData?.id || "";
295
+
296
+ ANNOTATION_TYPES.forEach(type => {
297
+ const anns = sequenceData?.[type];
298
+ if (!anns) return;
299
+ const annsArray = Array.isArray(anns) ? anns : Object.values(anns);
300
+ annsArray.forEach(ann => {
301
+ if (!ann.name) return;
302
+ if (ann.name.toLowerCase().includes(query)) {
303
+ const alignmentStart = ann.start + gapOffset(ann.start);
304
+ const alignmentEnd = ann.end + gapOffset(ann.end);
305
+ allMatches.push({
306
+ trackIndex,
307
+ trackName,
308
+ type,
309
+ annotation: ann,
310
+ alignmentStart,
311
+ alignmentEnd
312
+ });
313
+ }
314
+ });
315
+ });
316
+ });
317
+
318
+ dispatch({ type: "SET_FEATURE_MATCHES", payload: allMatches });
319
+ },
320
+ [alignmentTracks]
321
+ );
322
+
323
+ const goToPrev = useCallback(() => {
324
+ if (!matches.length) return;
325
+ const newIndex =
326
+ currentMatchIndex === 0 ? matches.length - 1 : currentMatchIndex - 1;
327
+ dispatch({ type: "SET_CURRENT_MATCH_INDEX", payload: newIndex });
328
+ navigateTo(matches, newIndex);
329
+ }, [matches, currentMatchIndex, navigateTo]);
330
+
331
+ const goToNext = useCallback(() => {
332
+ if (!matches.length) return;
333
+ const newIndex =
334
+ currentMatchIndex === matches.length - 1 ? 0 : currentMatchIndex + 1;
335
+ dispatch({ type: "SET_CURRENT_MATCH_INDEX", payload: newIndex });
336
+ navigateTo(matches, newIndex);
337
+ }, [matches, currentMatchIndex, navigateTo]);
338
+ const handleKeyDown = useCallback(
339
+ e => {
340
+ if (e.key === "Escape") {
341
+ setIsOpen(false);
342
+ }
343
+ if (e.key === "Enter") {
344
+ if (e.shiftKey) {
345
+ goToPrev();
346
+ } else {
347
+ goToNext();
348
+ }
349
+ e.preventDefault();
350
+ e.stopPropagation();
351
+ }
352
+ },
353
+ [goToPrev, goToNext]
354
+ );
355
+ // Re-run both searches when search options change (only if a search has been performed)
356
+ useEffect(() => {
357
+ if (!searched || !searchText.trim()) return;
358
+ runSearch(searchText);
359
+ runFeatureSearch(searchText);
360
+ }, [
361
+ dnaOrAA,
362
+ ambiguousOrLiteral,
363
+ mismatchesAllowed,
364
+ runSearch,
365
+ runFeatureSearch,
366
+ searched,
367
+ searchText
368
+ ]);
369
+
370
+ // Disable and reset highlightAll when query is too short
371
+ useEffect(() => {
372
+ if (searchText.trim().length < 1) setHighlightAll(false);
373
+ }, [searchText]);
374
+
375
+ // Rebuild layers when highlightAll toggles without re-searching
376
+ const prevHighlightAll = useRef(highlightAll);
377
+ useEffect(() => {
378
+ if (prevHighlightAll.current !== highlightAll) {
379
+ prevHighlightAll.current = highlightAll;
380
+ if (matches.length) buildMatchLayers(matches, currentMatchIndex);
381
+ }
382
+ }, [highlightAll, matches, currentMatchIndex, buildMatchLayers]);
383
+
384
+ const hasMatches = matches.length > 0;
385
+
386
+ const handleChange = useCallback(
387
+ e => {
388
+ const value = e.target.value;
389
+ dispatch({ type: "SET_SEARCH_TEXT", payload: value });
390
+ debouncedSearch(value, runSearch, runFeatureSearch);
391
+ },
392
+ [debouncedSearch, runSearch, runFeatureSearch]
393
+ );
394
+
395
+ const handleFeatureClick = useCallback(featureMatch => {
396
+ updateCaretPosition({
397
+ start: featureMatch.alignmentStart,
398
+ end: featureMatch.alignmentEnd
399
+ });
400
+ setTimeout(() => {
401
+ scrollToAlignmentSelection();
402
+ }, 0);
403
+ }, []);
404
+
405
+ const matchCounter = (
406
+ <span
407
+ style={{
408
+ marginRight: 3,
409
+ color: "lightgrey",
410
+ fontSize: "0.9em",
411
+ whiteSpace: "nowrap"
412
+ }}
413
+ >
414
+ {hasMatches ? currentMatchIndex + 1 : 0}/{matches.length}
415
+ </span>
416
+ );
417
+
418
+ const inlineNavEl = (
419
+ <span style={{ display: "flex", alignItems: "center" }}>
420
+ {!isExpanded && (
421
+ <Popover
422
+ autoFocus={false}
423
+ enforceFocus={false}
424
+ isOpen={isPopoverOpen}
425
+ onInteraction={setIsPopoverOpen}
426
+ position={Position.TOP}
427
+ content={
428
+ <div
429
+ className="ve-find-options-popover"
430
+ style={{
431
+ display: "flex",
432
+ flexDirection: "column",
433
+ paddingLeft: 20,
434
+ paddingBottom: 10,
435
+ paddingTop: 10,
436
+ paddingRight: 20,
437
+ gap: 6
438
+ }}
439
+ >
440
+ <FindOptionsPanel
441
+ dnaOrAA={dnaOrAA}
442
+ ambiguousOrLiteral={ambiguousOrLiteral}
443
+ mismatchesAllowed={mismatchesAllowed}
444
+ searchText={searchText}
445
+ matches={matches}
446
+ dispatch={dispatch}
447
+ highlightAll={highlightAll}
448
+ setHighlightAll={setHighlightAll}
449
+ isExpanded={isExpanded}
450
+ onToggleExpanded={handleToggleExpanded}
451
+ />
452
+ </div>
453
+ }
454
+ target={<Button minimal icon="wrench" data-tip="Options" />}
455
+ />
456
+ )}
457
+ {matchCounter}
458
+ <Button
459
+ minimal
460
+ small
461
+ icon="caret-left"
462
+ data-tip="Previous"
463
+ disabled={!hasMatches}
464
+ onClick={goToPrev}
465
+ />
466
+ <Button
467
+ minimal
468
+ small
469
+ icon="caret-right"
470
+ data-tip="Next"
471
+ disabled={!hasMatches}
472
+ onClick={goToNext}
473
+ />
474
+ <Button
475
+ minimal
476
+ small
477
+ data-tip="Close (Esc)"
478
+ icon="small-cross"
479
+ onClick={() => setIsOpen(false)}
480
+ />
481
+ </span>
482
+ );
483
+
484
+ const expandedNavEl = (
485
+ <span style={{ display: "flex", alignItems: "center" }}>
486
+ {matchCounter}
487
+ <Button
488
+ minimal
489
+ small
490
+ icon="caret-up"
491
+ disabled={!hasMatches}
492
+ onClick={goToPrev}
493
+ />
494
+ <Button
495
+ minimal
496
+ small
497
+ icon="caret-down"
498
+ disabled={!hasMatches}
499
+ onClick={goToNext}
500
+ />
501
+ </span>
502
+ );
503
+
504
+ if (!isOpen) {
505
+ return (
506
+ <div>
507
+ <Button
508
+ minimal
509
+ small
510
+ intent="primary"
511
+ icon="search"
512
+ rightIcon="caret-right"
513
+ data-tip="Search"
514
+ onClick={() => setIsOpen(true)}
515
+ />
516
+ </div>
517
+ );
518
+ }
519
+
520
+ // Annotation results popup: only shown when feature matches exist
521
+ const annotationPopoverOpen = searched && featureMatches.length > 0;
522
+
523
+ const inputEl = (
524
+ <InputGroup
525
+ className="tg-find-tool-input alignment-search-bar"
526
+ leftIcon="search"
527
+ placeholder="Search..."
528
+ autoFocus
529
+ value={searchText}
530
+ onChange={handleChange}
531
+ onKeyDown={handleKeyDown}
532
+ rightElement={inlineNavEl}
533
+ />
534
+ );
535
+
536
+ return (
537
+ <div style={{ position: "relative" }}>
538
+ {!isExpanded && (
539
+ <Popover
540
+ autoFocus={false}
541
+ enforceFocus={false}
542
+ modifiers={{
543
+ arrow: false
544
+ }}
545
+ position={Position.BOTTOM}
546
+ isOpen={annotationPopoverOpen}
547
+ content={
548
+ <AnnotationResultsComp
549
+ featureMatches={featureMatches}
550
+ onClickMatch={handleFeatureClick}
551
+ />
552
+ }
553
+ target={inputEl}
554
+ />
555
+ )}
556
+
557
+ {isExpanded && (
558
+ <div
559
+ style={{
560
+ position: "absolute",
561
+ top: 0,
562
+ left: 0,
563
+ padding: 10,
564
+ paddingBottom: 25,
565
+ display: "flex",
566
+ alignItems: "flex-start",
567
+ gap: 10,
568
+ zIndex: 50000,
569
+ background: "white",
570
+ boxShadow: "0 2px 8px rgba(0,0,0,0.2)",
571
+ borderRadius: 3
572
+ }}
573
+ >
574
+ <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
575
+ <TextArea
576
+ autoFocus
577
+ placeholder="Search sequences and annotations..."
578
+ value={searchText}
579
+ onChange={handleChange}
580
+ onKeyDown={handleKeyDown}
581
+ style={{ resize: "vertical", width: 350, height: 190 }}
582
+ />
583
+ {annotationPopoverOpen && (
584
+ <AnnotationResultsComp
585
+ featureMatches={featureMatches}
586
+ onClickMatch={handleFeatureClick}
587
+ />
588
+ )}
589
+ </div>
590
+ <div style={{ display: "flex", flexDirection: "column", gap: 5 }}>
591
+ {expandedNavEl}
592
+ <FindOptionsPanel
593
+ dnaOrAA={dnaOrAA}
594
+ ambiguousOrLiteral={ambiguousOrLiteral}
595
+ mismatchesAllowed={mismatchesAllowed}
596
+ searchText={searchText}
597
+ matches={matches}
598
+ dispatch={dispatch}
599
+ highlightAll={highlightAll}
600
+ setHighlightAll={setHighlightAll}
601
+ isExpanded={isExpanded}
602
+ onToggleExpanded={handleToggleExpanded}
603
+ />
604
+ </div>
605
+ <Button
606
+ minimal
607
+ style={{ position: "absolute", bottom: 0, right: 0 }}
608
+ onClick={() => setIsOpen(false)}
609
+ icon="cross"
610
+ />
611
+ </div>
612
+ )}
613
+ </div>
614
+ );
615
+ }
616
+
617
+ function AnnotationResultsComp({ featureMatches, onClickMatch }) {
618
+ const byType = {};
619
+ ANNOTATION_TYPES.forEach(type => {
620
+ byType[type] = [];
621
+ });
622
+ featureMatches.forEach(match => {
623
+ if (byType[match.type]) {
624
+ byType[match.type].push(match);
625
+ }
626
+ });
627
+
628
+ const featureColorMap = getFeatureToColorMap({ includeHidden: true });
629
+
630
+ return (
631
+ <div className="veAnnotationFindMatches">
632
+ {ANNOTATION_TYPES.map(type => {
633
+ const anns = byType[type];
634
+ if (!anns.length) return null;
635
+ const showing = anns.slice(0, 10);
636
+ return (
637
+ <div key={type}>
638
+ <div className="veAnnotationFoundType">
639
+ {anns.length} {getSingular(type)} match
640
+ {anns.length > 1 ? "es" : null}
641
+ {anns.length > 10 ? ` (only showing 10)` : null}:
642
+ </div>
643
+ <div>
644
+ {showing.map((match, i) => {
645
+ const { annotation } = match;
646
+ const annotationColor =
647
+ type === "parts"
648
+ ? "#ac68cc"
649
+ : annotation.color || featureColorMap[annotation.type];
650
+ return (
651
+ <div
652
+ key={i}
653
+ onClick={() => onClickMatch(match)}
654
+ className="veAnnotationFoundResult"
655
+ >
656
+ <div style={{ display: "flex", alignItems: "center" }}>
657
+ <div
658
+ style={{
659
+ background: annotationColor,
660
+ height: 15,
661
+ width: 15,
662
+ marginRight: 3
663
+ }}
664
+ />
665
+ {annotation.name}
666
+ </div>
667
+ <div className="veAnnotationFoundResultRange">
668
+ {annotation.start + 1}-{annotation.end + 1}
669
+ </div>
670
+ </div>
671
+ );
672
+ })}
673
+ </div>
674
+ </div>
675
+ );
676
+ })}
677
+ </div>
678
+ );
679
+ }
680
+
681
+ function FindOptionsPanel({
682
+ dnaOrAA,
683
+ ambiguousOrLiteral,
684
+ mismatchesAllowed,
685
+ searchText,
686
+ matches,
687
+ dispatch,
688
+ highlightAll,
689
+ setHighlightAll,
690
+ isExpanded,
691
+ onToggleExpanded
692
+ }) {
693
+ return (
694
+ <>
695
+ <TgHTMLSelect
696
+ options={[
697
+ { label: "DNA", value: "DNA" },
698
+ { label: "Amino Acids", value: "AA" }
699
+ ]}
700
+ value={dnaOrAA}
701
+ onChange={e =>
702
+ dispatch({ type: "SET_DNA_OR_AA", payload: e.target.value })
703
+ }
704
+ />
705
+ <div style={{ display: "flex" }}>
706
+ <TgHTMLSelect
707
+ options={[
708
+ { label: "Literal", value: "LITERAL" },
709
+ { label: "Ambiguous", value: "AMBIGUOUS" }
710
+ ]}
711
+ value={ambiguousOrLiteral}
712
+ onChange={e =>
713
+ dispatch({
714
+ type: "SET_AMBIGUOUS_OR_LITERAL",
715
+ payload: e.target.value
716
+ })
717
+ }
718
+ />
719
+ <InfoHelper style={{ marginLeft: 10 }}>
720
+ <div>
721
+ Ambiguous substitutions:
722
+ <div style={{ display: "flex", fontSize: 12 }}>
723
+ <div style={{ marginRight: 20 }}>
724
+ <div style={{ fontSize: 14, marginBottom: 4, marginTop: 5 }}>
725
+ DNA:
726
+ </div>
727
+ <div>M: AC</div>
728
+ <div>R: AG</div>
729
+ <div>W: AT</div>
730
+ <div>S: CG</div>
731
+ <div>Y: CT</div>
732
+ <div>K: GT</div>
733
+ <div>V: ACG</div>
734
+ <div>H: ACT</div>
735
+ <div>D: AGT</div>
736
+ <div>B: CGT</div>
737
+ <div>X: GATC</div>
738
+ <div>N: GATC</div>
739
+ <div>*: any</div>
740
+ </div>
741
+ <div>
742
+ <div style={{ fontSize: 14, marginBottom: 4, marginTop: 5 }}>
743
+ AA:
744
+ </div>
745
+ <div>B: ND</div>
746
+ <div>J: IL</div>
747
+ <div>X: ACDEFGHIKLMNPQRSTVWY</div>
748
+ <div>Z: QE</div>
749
+ <div>*: any</div>
750
+ </div>
751
+ </div>
752
+ </div>
753
+ </InfoHelper>
754
+ </div>
755
+ <div
756
+ style={{
757
+ marginTop: "8px",
758
+ display: "flex",
759
+ flexDirection: "row",
760
+ gap: "3px",
761
+ alignItems: "center"
762
+ }}
763
+ >
764
+ <label>Mismatches Allowed:</label>
765
+ <NumericInput
766
+ min={0}
767
+ max={10}
768
+ className="tg-mismatches-allowed-input"
769
+ style={{ width: "60px" }}
770
+ value={mismatchesAllowed}
771
+ disabled={dnaOrAA !== "DNA" || ambiguousOrLiteral !== "LITERAL"}
772
+ onValueChange={value =>
773
+ dispatch({
774
+ type: "SET_MISMATCHES_ALLOWED",
775
+ payload: Number.parseInt(value, 10) || 0
776
+ })
777
+ }
778
+ />
779
+ <InfoHelper style={{ marginLeft: 10 }}>
780
+ <div>
781
+ Number of mismatches allowed when searching DNA sequences with
782
+ literal matching.
783
+ <br />
784
+ <br />
785
+ Higher values may slow down search performance.
786
+ </div>
787
+ </InfoHelper>
788
+ </div>
789
+ <Switch
790
+ checked={highlightAll}
791
+ onChange={() => setHighlightAll(v => !v)}
792
+ disabled={
793
+ searchText.trim().length < 2 || matches.length > MAX_MATCHES_DISPLAYED
794
+ }
795
+ >
796
+ <Tooltip
797
+ disabled={matches.length <= MAX_MATCHES_DISPLAYED}
798
+ content={`Disabled because there are >${MAX_MATCHES_DISPLAYED} matches`}
799
+ >
800
+ Highlight All
801
+ </Tooltip>
802
+ </Switch>
803
+ <Switch checked={isExpanded} onChange={onToggleExpanded}>
804
+ Expanded
805
+ </Switch>
806
+ </>
807
+ );
808
+ }
809
+
810
+ export default AlignmentSearchBar;