← Files Biological Sequence & Alignment ViewerARCHIVED FILE
src/msa/msa-rich-viewer.tsx
120 KB · Sep 30, 2026 · 23:01 UTC
import clsx from "clsx";
import {
useCallback,
useContext,
useEffect,
useMemo,
useReducer,
useRef,
useState,
type ChangeEvent,
type ComponentProps,
type ReactNode,
type SetStateAction,
type WheelEventHandler,
} from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { createArtifactStateKey } from "../artifact-state-key";
import { useModelContext, type ModelContextUpdater } from "../model-context";
import {
applySequenceDurableDocumentPatches,
createDurableAlignmentState,
SequenceDurableViewerStateContext,
} from "../persistent/durable-viewer-state";
import { resolveViewerTarget } from "../target-resolution";
import { Button } from "../ui/button";
import { RichPreviewMessage } from "../ui/status-panel";
import { WorkbenchTools } from "../ui/workbench-tools";
import type {
QueuedSequenceViewerCommand,
SequenceViewerCommandResult,
} from "../viewer-commands";
import type { SequenceViewerEditRequest } from "../viewer-operations";
import type { SequenceWorkspaceArtifactPublisher } from "../views/workbench-persistence";
import type { SequenceWorkspaceSessionClient } from "../views/workspace-sessions";
import { exportAlignmentWorkbench } from "../workbench-exports";
import {
alignmentWorkbenchReducer,
createAlignmentWorkbenchState,
parseWorkbenchSession,
serializeWorkbenchSession,
settleRestoredJobs,
} from "../workbench-state";
import { workspaceSessionDefaultName } from "../workspace-session-controls";
import { exportAlignedFasta } from "./alignment-editing";
import { createAlignmentExportParameters } from "./alignment-export-provenance";
import {
createMsaInteractionState,
createMsaRowSetKey,
msaInteractionReducer,
type MsaReferenceMode,
} from "./alignment-state";
import { AlignmentWorkbenchPanel } from "./alignment-workbench-panel";
import { computeRowMetrics } from "./analysis";
import {
MsaCellHoverDetails,
type FocusedMsaCell,
type MsaHoverCellDetails,
} from "./cell-hover-overlay";
import { inferCdsContext } from "./codon";
import { formatResiduePaletteLabel, MsaColorLegend } from "./color-legend";
import {
getCompatibleResiduePalettes,
getDefaultColorMode,
getDefaultResiduePalette,
isResiduePaletteCompatible,
type MsaColorMode,
type MsaResiduePalette,
} from "./colors";
import { getAlignmentColumnForUngappedPosition } from "./coordinate-map";
import { MsaExportActions } from "./export-actions";
import { MsaGuideTreePanel } from "./guide-tree-panel";
import { MsaMatrixRenderer } from "./matrix-renderer";
import { MsaMetadataPanel } from "./metadata-panel";
import {
getAvailableMsaMetricTracks,
getDefaultMsaMetricTracks,
MsaMetricTrackControls,
MsaMetricTracks,
} from "./metric-tracks";
import { createMsaViewerModelContext } from "./model-context";
import { MsaOverviewStrip } from "./overview-strip";
import { markMsaPreviewMilestone } from "./performance";
import {
MsaPerformanceStateBanner,
MsaProgressiveLoadingMessage,
} from "./performance-banners";
import {
buildGuideTree,
isGuideTreeResult,
type GuideTreeAlgorithm,
type GuideTreeResult,
} from "./phylogenetic-tree";
import { MsaPinnedInspector } from "./pinned-inspector";
import { classifyResidue } from "./residue-alphabet";
import { MsaRowsPanel } from "./rows-panel";
import type { MsaMotifSearchHit } from "./search";
import { MsaSearchResultsPanel } from "./search-results-panel";
import type {
MsaAnalysisScope,
MsaColumnRange,
MsaDocument,
MsaMetricTrackKey,
MsaMoleculeType,
MsaRowMetrics,
MsaRowSortDirection,
MsaRowSortKey,
MsaSearchScope,
MsaSequenceRow,
} from "./types";
import {
isAlignmentOperation,
createAlignmentWorkbenchSession,
useAlignmentWorkbenchCommands,
type AlignmentWorkbenchView,
} from "./use-alignment-workbench-commands";
import {
useMsaDerivedAnalysis,
useMsaMotifSearch,
useParsedMsaDocument,
} from "./use-msa-document";
import { useMsaViewport } from "./use-msa-viewport";
import { MSA_CELL_WIDTH_PX } from "./virtualization";
import { MsaWarningsDrawer } from "./warnings-drawer";
import {
applyAlignmentEditRequest,
realignRows,
runAlignmentAnalysis,
} from "./workbench-controller";
const DEFAULT_CELL_WIDTH_PX = MSA_CELL_WIDTH_PX;
const MIN_CELL_WIDTH_PX = 16;
const MAX_CELL_WIDTH_PX = 42;
const WINDOW_STEP = 24;
const LARGE_ALIGNMENT_ROW_THRESHOLD = 1_000;
function alignmentTreeInputKey(document: MsaDocument): string {
return document.rows
.filter(({ hidden }) => !hidden)
.map(({ alignedSequence, id }) => ({ alignedSequence, id }))
.sort((left, right) => left.id.localeCompare(right.id))
.map(({ alignedSequence, id }) => `${id}\u001f${alignedSequence}`)
.join("\u001e");
}
function resolveAlignmentRow(rows: Array<MsaSequenceRow>, selector: string) {
return resolveViewerTarget({
aliases: (row) => [row.label, row.sourceId],
id: (row) => row.id,
selector,
targets: rows,
});
}
function isMsaColorModeCompatible({
cdsEligible,
colorMode,
moleculeType,
}: {
cdsEligible: boolean;
colorMode: MsaColorMode;
moleculeType: MsaMoleculeType;
}): boolean {
const nucleicAcid =
moleculeType === "dna" ||
moleculeType === "rna" ||
moleculeType === "nucleic-acid-ambiguous";
if (colorMode === "coding-impact") {
return cdsEligible;
}
if (colorMode === "nucleotide-substitution") {
return nucleicAcid;
}
if (
colorMode === "protein-conservation" ||
colorMode === "protein-similarity"
) {
return moleculeType === "protein";
}
return true;
}
export function MsaRichViewer({
className,
command,
contents,
fileName,
onCommandResult,
publishWorkspaceArtifact,
showFileHeader = true,
sourceStateKeyOverride,
toolbarRevealed,
toolbarVisible = true,
updateModelContext,
viewerSessionId,
workspaceSessions,
}: {
className?: string;
command?: QueuedSequenceViewerCommand;
contents: string;
fileName?: string;
onCommandResult?: (
command: QueuedSequenceViewerCommand,
result: SequenceViewerCommandResult,
) => void;
publishWorkspaceArtifact?: SequenceWorkspaceArtifactPublisher;
showFileHeader?: boolean;
sourceStateKeyOverride?: string;
toolbarRevealed?: boolean;
toolbarVisible?: boolean;
updateModelContext?: ModelContextUpdater;
viewerSessionId?: string;
workspaceSessions?: SequenceWorkspaceSessionClient;
}): React.ReactElement {
return (
<MsaPreview
className={className}
command={command}
contents={contents}
filePath={fileName}
formatHintPath={fileName}
onCommandResult={onCommandResult}
publishWorkspaceArtifact={publishWorkspaceArtifact}
showFileHeader={showFileHeader}
sourceStateKeyOverride={sourceStateKeyOverride}
toolbarRevealed={toolbarRevealed}
toolbarVisible={toolbarVisible}
updateModelContext={updateModelContext}
viewerSessionId={viewerSessionId}
workspaceSessions={workspaceSessions}
/>
);
}
export function MsaPreview({
className,
command,
contents,
formatHintPath,
filePath,
onCommandResult,
publishWorkspaceArtifact,
showFileHeader = true,
sourceStateKeyOverride,
toolbarRevealed,
toolbarVisible = true,
updateModelContext,
viewerSessionId,
workspaceSessions,
}: {
className?: string;
command?: QueuedSequenceViewerCommand;
contents: string;
formatHintPath?: string;
filePath?: string;
onCommandResult?: (
command: QueuedSequenceViewerCommand,
result: SequenceViewerCommandResult,
) => void;
publishWorkspaceArtifact?: SequenceWorkspaceArtifactPublisher;
showFileHeader?: boolean;
sourceStateKeyOverride?: string;
toolbarRevealed?: boolean;
toolbarVisible?: boolean;
updateModelContext?: ModelContextUpdater;
viewerSessionId?: string;
workspaceSessions?: SequenceWorkspaceSessionClient;
}): React.ReactElement {
const documentPreviewKey = useMemo(
() =>
sourceStateKeyOverride ??
createArtifactStateKey(contents, formatHintPath ?? filePath),
[contents, filePath, formatHintPath, sourceStateKeyOverride],
);
const parsed = useParsedMsaDocument({
contents,
filePath: formatHintPath ?? filePath,
});
useEffect(() => {
markMsaPreviewMilestone("load-start");
markMsaPreviewMilestone("shell-mounted");
}, [contents, filePath, formatHintPath]);
useEffect(() => {
if (parsed.phase === "parsed" || parsed.phase === "error") {
markMsaPreviewMilestone("parsed");
}
}, [parsed.phase]);
if (parsed.phase === "parsing") {
return (
<MsaPreviewShell
className={className}
filePath={filePath}
showFileHeader={showFileHeader}
>
<MsaProgressiveLoadingMessage phase="parsing" />
</MsaPreviewShell>
);
}
if (parsed.phase === "error") {
return (
<MsaPreviewShell
className={className}
filePath={filePath}
showFileHeader={showFileHeader}
>
<div className="flex min-h-0 flex-1 items-center justify-center p-4">
<RichPreviewMessage className="max-w-md text-center">
{parsed.message}
</RichPreviewMessage>
</div>
</MsaPreviewShell>
);
}
return (
<MsaDocumentPreview
className={className}
command={command}
document={parsed.document}
filePath={filePath}
key={documentPreviewKey}
onCommandResult={onCommandResult}
publishWorkspaceArtifact={publishWorkspaceArtifact}
showFileHeader={showFileHeader}
sourceStateKeyOverride={documentPreviewKey}
toolbarRevealed={toolbarRevealed}
toolbarVisible={toolbarVisible}
updateModelContext={updateModelContext}
viewerSessionId={viewerSessionId}
workspaceSessions={workspaceSessions}
/>
);
}
function MsaDocumentPreview({
className,
command,
document: initialDocument,
filePath,
onCommandResult,
publishWorkspaceArtifact,
showFileHeader = true,
sourceStateKeyOverride,
toolbarRevealed,
toolbarVisible = true,
updateModelContext,
viewerSessionId,
workspaceSessions,
}: {
className?: string;
command?: QueuedSequenceViewerCommand;
document: MsaDocument;
filePath?: string;
onCommandResult?: (
command: QueuedSequenceViewerCommand,
result: SequenceViewerCommandResult,
) => void;
publishWorkspaceArtifact?: SequenceWorkspaceArtifactPublisher;
showFileHeader?: boolean;
sourceStateKeyOverride?: string;
toolbarRevealed?: boolean;
toolbarVisible?: boolean;
updateModelContext?: ModelContextUpdater;
viewerSessionId?: string;
workspaceSessions?: SequenceWorkspaceSessionClient;
}): React.ReactElement {
const durableViewerState = useContext(SequenceDurableViewerStateContext);
const handledCommandIdRef = useRef<string | undefined>(undefined);
const cancelledJobsRef = useRef(new Set<string>());
const restoredCheckpointRef = useRef<string | null>(null);
const restoredMotifQueryRef = useRef<string | null>(null);
const restoredHitIndexRef = useRef<number | null>(null);
const intl = useIntl();
const [workbenchState, dispatchWorkbench] = useReducer(
alignmentWorkbenchReducer,
initialDocument,
createAlignmentWorkbenchState,
);
const document = workbenchState.document;
const sourceStateKey = useMemo(
() =>
sourceStateKeyOverride ??
createArtifactStateKey(
initialDocument.rows
.map(({ id, alignedSequence }) => `${id}\u001f${alignedSequence}`)
.join("\u001e"),
filePath,
),
[filePath, initialDocument, sourceStateKeyOverride],
);
const workbenchStateRef = useRef(workbenchState);
workbenchStateRef.current = workbenchState;
const setDocument = useCallback(
(update: SetStateAction<MsaDocument>): void => {
const current = workbenchStateRef.current.document;
dispatchWorkbench({
document: typeof update === "function" ? update(current) : update,
type: "replace-alignment-view-document",
});
},
[],
);
const [workbenchTree, setWorkbenchTree] = useState<GuideTreeResult | null>(
null,
);
const treeSourceKeyRef = useRef<string | null>(null);
const [workbenchError, setWorkbenchError] = useState<string>();
const [interactionState, dispatchInteraction] = useReducer(
msaInteractionReducer,
initialDocument.rows[0]?.id ?? null,
createMsaInteractionState,
);
const {
anchorRowId,
focusedCell,
guideTree,
pinnedCell,
referenceMode,
selectedHitIndex,
} = interactionState;
const guideTreeNewick = guideTree?.newick ?? null;
const [showIdenticalAsDots, setShowIdenticalAsDots] = useState(false);
const [showAnnotationTracks, setShowAnnotationTracks] = useState(true);
const [showRnaStructureOverlays, setShowRnaStructureOverlays] =
useState(true);
const [enabledMetricTracks, setEnabledMetricTracks] = useState<
Array<MsaMetricTrackKey>
>(() => getDefaultMsaMetricTracks(initialDocument));
const [showSequenceLogoHelp, setShowSequenceLogoHelp] = useState(false);
const [rowSortKey, setRowSortKey] = useState<MsaRowSortKey>("source");
const [rowSortDirection, setRowSortDirection] =
useState<MsaRowSortDirection>("asc");
const [analysisScope, setAnalysisScope] =
useState<MsaAnalysisScope>("all-unhidden-rows");
const [searchScope, setSearchScope] = useState<MsaSearchScope>(
"currently-displayed-rows",
);
const [colorMode, setColorMode] = useState<MsaColorMode>(() =>
getDefaultColorMode(initialDocument.displayInterpretation.moleculeType),
);
const [residuePalette, setResiduePalette] =
useState<MsaResiduePalette | null>(() =>
getDefaultResiduePalette(
initialDocument.displayInterpretation.moleculeType,
),
);
const [rowFilter, setRowFilter] = useState("");
const [alignmentColumnJump, setAlignmentColumnJump] = useState("");
const [referencePositionJump, setReferencePositionJump] = useState("");
const [motifQuery, setMotifQuery] = useState("");
const [cellWidth, setCellWidth] = useState(DEFAULT_CELL_WIDTH_PX);
const [hoverCellDetails, setHoverCellDetails] =
useState<MsaHoverCellDetails>(null);
const [selectedColumnRange, setSelectedColumnRange] =
useState<MsaColumnRange | null>(null);
const setReferenceMode = (mode: MsaReferenceMode): void =>
dispatchInteraction({ mode, type: "set-reference-mode" });
const setAnchorRowId = (anchorRowId: string | null): void =>
dispatchInteraction({ anchorRowId, type: "set-anchor" });
const setFocusedCell = (cell: FocusedMsaCell): void =>
dispatchInteraction({ cell, type: "set-focus" });
const setPinnedCell = (cell: FocusedMsaCell): void =>
dispatchInteraction({ cell, type: "set-pinned" });
const setSelectedHitIndex = (
value: number | ((current: number) => number),
): void =>
dispatchInteraction({
index: typeof value === "function" ? value(selectedHitIndex) : value,
type: "set-selected-hit",
});
const columnSelectionAnchorRef = useRef<number | null>(null);
const clearSelectedColumnRange = useCallback((): void => {
setSelectedColumnRange(null);
columnSelectionAnchorRef.current = null;
}, []);
const clearSelection = useCallback((): void => {
clearSelectedColumnRange();
dispatchInteraction({ type: "clear-selection" });
setHoverCellDetails(null);
}, [clearSelectedColumnRange]);
const visibleRows = useMemo(() => {
const normalizedFilter = rowFilter.trim().toLowerCase();
return document.rows.filter((row) => {
if (row.hidden) {
return false;
}
if (normalizedFilter.length === 0) {
return true;
}
return `${row.label} ${row.description ?? ""}`
.toLowerCase()
.includes(normalizedFilter);
});
}, [document.rows, rowFilter]);
const visibleRowIds = useMemo(
() => visibleRows.map((row) => row.id),
[visibleRows],
);
const unhiddenRowIds = useMemo(
() => document.rows.filter((row) => !row.hidden).map((row) => row.id),
[document.rows],
);
const visibleRowSetKey = createMsaRowSetKey(visibleRowIds);
const unhiddenRowSetKey = createMsaRowSetKey(unhiddenRowIds);
useEffect(() => {
dispatchInteraction({
rowIds: visibleRowIds,
type: "visible-rows-changed",
});
}, [visibleRowSetKey]);
useEffect(() => {
dispatchInteraction({
rowIds: unhiddenRowIds,
type: "analysis-rows-changed",
});
setMotifQuery(restoredMotifQueryRef.current ?? "");
restoredMotifQueryRef.current = null;
}, [unhiddenRowSetKey]);
const setGuideTreeNewick = (newick: string | null): void =>
dispatchInteraction({
newick,
rowSetKey: unhiddenRowSetKey,
type: "set-guide-tree",
});
const analysisRowIds =
analysisScope === "currently-displayed-rows"
? visibleRowIds
: unhiddenRowIds;
const searchRowIds =
searchScope === "currently-displayed-rows" ? visibleRowIds : unhiddenRowIds;
const searchRowSetKey = createMsaRowSetKey(searchRowIds);
useEffect(() => {
dispatchInteraction({
index: restoredHitIndexRef.current ?? 0,
type: "set-selected-hit",
});
restoredHitIndexRef.current = null;
}, [searchRowSetKey]);
const analysisState = useMsaDerivedAnalysis({
analysisRowIds,
document,
});
const analysis = analysisState.analysis;
const referenceSequence = useMemo(() => {
if (referenceMode === "none") {
return null;
}
if (referenceMode === "anchor") {
return (
document.rows.find((row) => row.id === anchorRowId)?.alignedSequence ??
null
);
}
return analysis?.consensusSequence ?? null;
}, [analysis?.consensusSequence, anchorRowId, document.rows, referenceMode]);
const availableMetricTracks = getAvailableMsaMetricTracks({
moleculeType: document.displayInterpretation.moleculeType,
referenceAvailable: referenceSequence != null,
rnaStructureAvailable: document.rnaStructure != null,
});
const visibleMetricTracks = enabledMetricTracks.filter((track) =>
availableMetricTracks.includes(track),
);
const referenceLabel =
referenceMode === "anchor"
? (document.rows.find((row) => row.id === anchorRowId)?.label ?? "anchor")
: document.displayInterpretation.moleculeType === "protein"
? "protein_representative"
: "nucleotide_consensus";
const searchState = useMsaMotifSearch({
analysis,
document,
rawQuery: motifQuery,
searchRowIds,
});
const motifHits = searchState.hits;
const selectedHit = motifHits[selectedHitIndex] ?? null;
const rowMetricsById = useMemo(() => {
if (referenceMode === "consensus" && analysis != null) {
return analysis.rowMetricsById;
}
return Object.fromEntries(
computeRowMetrics(document.rows, referenceSequence).map((metric) => [
metric.rowId,
metric,
]),
) as Record<string, MsaRowMetrics | undefined>;
}, [analysis, document.rows, referenceMode, referenceSequence]);
const staticRowCount =
1 +
(referenceMode === "none" ? 0 : 1) +
(showAnnotationTracks ? document.annotations.length : 0) +
(document.cdsContext.applicability === "eligible" ? 2 : 0);
const {
scrollHorizontallyBy,
scrollToColumn,
scrollToRow,
setScrollContainerRef,
slice,
} = useMsaViewport({
alignedLength: document.alignedLength,
cellWidth,
rowCount: visibleRows.length,
staticRowCount,
});
const handleHorizontalWheel = useCallback<WheelEventHandler<HTMLElement>>(
(event) => {
if (event.deltaX === 0) {
return;
}
event.preventDefault();
scrollHorizontallyBy(event.deltaX);
},
[scrollHorizontallyBy],
);
useEffect(() => {
const finishColumnSelection = (): void => {
columnSelectionAnchorRef.current = null;
};
window.addEventListener("pointerup", finishColumnSelection);
window.addEventListener("pointercancel", finishColumnSelection);
return (): void => {
window.removeEventListener("pointerup", finishColumnSelection);
window.removeEventListener("pointercancel", finishColumnSelection);
};
}, []);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent): void => {
if (
event.key === "Escape" &&
(selectedColumnRange != null || pinnedCell != null)
) {
clearSelection();
}
};
window.addEventListener("keydown", handleKeyDown);
return (): void => window.removeEventListener("keydown", handleKeyDown);
}, [clearSelection, pinnedCell, selectedColumnRange]);
const renderedColumns = useMemo(
() =>
Array.from(
{ length: Math.max(0, slice.columnEnd - slice.columnStart) },
(_, offset) => slice.columnStart + offset,
),
[slice.columnEnd, slice.columnStart],
);
const visibleTrackColumns = useMemo(
() =>
Array.from(
{
length: Math.max(
0,
slice.visibleColumnEnd - slice.visibleColumnStart,
),
},
(_, offset) => slice.visibleColumnStart + offset,
),
[slice.visibleColumnEnd, slice.visibleColumnStart],
);
const displayedRows = useMemo(
() => visibleRows.slice(slice.rowStart, slice.rowEnd),
[slice.rowEnd, slice.rowStart, visibleRows],
);
const summaries = analysis?.summaries ?? [];
const visibleColumns = Math.max(1, slice.visibleColumnCount);
const isLargeAlignment =
document.rows.length >= LARGE_ALIGNMENT_ROW_THRESHOLD ||
document.rows.length * Math.max(document.alignedLength, 1) >= 250_000;
const workbenchView = useMemo<AlignmentWorkbenchView>(
() => ({
analysisScope,
cellWidth,
colorMode,
enabledMetricTracks,
referenceMode,
residuePalette,
rowFilter,
rowSortDirection,
rowSortKey,
searchScope,
selectedColumns: selectedColumnRange,
selectedRows: workbenchState.selectedRows,
showAnnotationTracks,
showIdenticalAsDots,
showRnaStructureOverlays,
showSequenceLogoHelp,
}),
[
analysisScope,
cellWidth,
colorMode,
enabledMetricTracks,
referenceMode,
residuePalette,
rowFilter,
rowSortDirection,
rowSortKey,
searchScope,
selectedColumnRange,
showAnnotationTracks,
showIdenticalAsDots,
showRnaStructureOverlays,
showSequenceLogoHelp,
workbenchState.selectedRows,
],
);
const treeInputKey = useMemo(
() => alignmentTreeInputKey(document),
[document],
);
const restoreWorkbenchView = useCallback(
(view: AlignmentWorkbenchView): void => {
setAnalysisScope(view.analysisScope as MsaAnalysisScope);
setCellWidth(view.cellWidth);
setColorMode(view.colorMode as MsaColorMode);
setReferenceMode(view.referenceMode as MsaReferenceMode);
setResiduePalette(view.residuePalette as MsaResiduePalette | null);
setRowFilter(view.rowFilter);
setSearchScope(view.searchScope as MsaSearchScope);
setSelectedColumnRange(view.selectedColumns);
dispatchWorkbench({
rowIds: view.selectedRows,
type: "select-alignment-rows",
});
setShowAnnotationTracks(view.showAnnotationTracks);
setShowIdenticalAsDots(view.showIdenticalAsDots);
setShowRnaStructureOverlays(view.showRnaStructureOverlays);
setEnabledMetricTracks(
view.enabledMetricTracks ?? getDefaultMsaMetricTracks(initialDocument),
);
setShowSequenceLogoHelp(view.showSequenceLogoHelp ?? false);
setRowSortKey(view.rowSortKey ?? "source");
setRowSortDirection(view.rowSortDirection ?? "asc");
},
[initialDocument],
);
const updateWorkbenchTree = useCallback(
(tree: GuideTreeResult | null, sourceDocument = document): void => {
treeSourceKeyRef.current =
tree == null ? null : alignmentTreeInputKey(sourceDocument);
setWorkbenchTree(tree);
setGuideTreeNewick(tree?.newick ?? null);
},
[document, unhiddenRowSetKey],
);
const restoredNativeAlignment =
durableViewerState?.restoredState?.sourceStateKey === sourceStateKey
? durableViewerState.restoredState.alignment
: undefined;
const [nativeRecoveryReady, setNativeRecoveryReady] = useState(
() => restoredNativeAlignment == null,
);
useEffect(() => {
if (restoredNativeAlignment == null || durableViewerState == null) {
setNativeRecoveryReady(true);
return;
}
const checkpointKey =
durableViewerState.client.session.sourceRevision + ":" + sourceStateKey;
if (restoredCheckpointRef.current === checkpointKey) {
setNativeRecoveryReady(true);
return;
}
try {
const session = parseWorkbenchSession(
JSON.stringify(restoredNativeAlignment.session),
);
if (
session.view.mode !== "alignment" ||
session.source.stateKey !== sourceStateKey ||
session.source.format !== initialDocument.format ||
session.source.fileName !== (filePath ?? null)
) {
throw new Error(
"The native Alignment checkpoint belongs to a different source artifact.",
);
}
const restoredDocument = applySequenceDurableDocumentPatches(
initialDocument,
restoredNativeAlignment.documentPatches,
);
if (
restoredNativeAlignment.documentPatches.length > 0 ||
restoredNativeAlignment.history.length > 0 ||
restoredNativeAlignment.future.length > 0
) {
const historicalDocuments = restoredNativeAlignment.history.map(
(entry) => ({
description: entry.description,
document: applySequenceDurableDocumentPatches(
initialDocument,
entry.patches,
),
}),
);
dispatchWorkbench({
document: historicalDocuments[0]?.document ?? initialDocument,
type: "reset-alignment-document",
});
for (let index = 1; index < historicalDocuments.length; index++) {
dispatchWorkbench({
description:
historicalDocuments[index - 1]?.description ??
"Automatically restored native alignment history.",
document: historicalDocuments[index]?.document ?? initialDocument,
type: "apply-alignment-document",
});
}
dispatchWorkbench({
description:
historicalDocuments.at(-1)?.description ??
"Automatically restored native alignment edits.",
document: restoredDocument,
type: "apply-alignment-document",
});
for (const entry of [...restoredNativeAlignment.future].reverse()) {
dispatchWorkbench({
description: entry.description,
document: applySequenceDurableDocumentPatches(
initialDocument,
entry.patches,
),
type: "apply-alignment-document",
});
}
for (
let index = 0;
index < restoredNativeAlignment.future.length;
index++
) {
dispatchWorkbench({ type: "undo-alignment-document" });
}
}
dispatchWorkbench({
state: {
artifacts: session.artifacts,
dirty: session.dirty,
jobs: settleRestoredJobs(session.jobs),
revision: session.revision,
tracks: session.tracks,
},
type: "restore-shared",
});
restoreWorkbenchView(restoredNativeAlignment.view);
dispatchInteraction({
anchorRowId: restoredNativeAlignment.anchorRowId,
type: "set-anchor",
});
restoredMotifQueryRef.current = restoredNativeAlignment.motifQuery;
restoredHitIndexRef.current = restoredNativeAlignment.selectedHitIndex;
setMotifQuery(restoredNativeAlignment.motifQuery);
setAlignmentColumnJump(restoredNativeAlignment.alignmentColumnJump);
setReferencePositionJump(restoredNativeAlignment.referencePositionJump);
for (const [kind, checkpointCell] of [
["set-focus", restoredNativeAlignment.focusedCell],
["set-pinned", restoredNativeAlignment.pinnedCell],
] as const) {
const row =
checkpointCell == null
? undefined
: restoredDocument.rows.find(
(candidate) => candidate.id === checkpointCell.rowId,
);
dispatchInteraction({
cell:
checkpointCell == null || row == null
? null
: {
column: checkpointCell.column,
row,
symbol: checkpointCell.symbol,
},
type: kind,
});
}
if (restoredNativeAlignment.guideTreeNewick != null) {
dispatchInteraction({
newick: restoredNativeAlignment.guideTreeNewick,
rowSetKey: createMsaRowSetKey(
restoredDocument.rows
.filter((row) => !row.hidden)
.map((row) => row.id),
),
type: "set-guide-tree",
});
}
dispatchInteraction({
index: restoredNativeAlignment.selectedHitIndex,
type: "set-selected-hit",
});
scrollToColumn(restoredNativeAlignment.viewport.column);
scrollToRow(restoredNativeAlignment.viewport.row);
restoredCheckpointRef.current = checkpointKey;
setWorkbenchError(undefined);
} catch (error) {
setWorkbenchError(
error instanceof Error
? error.message
: "The native Alignment checkpoint could not be restored.",
);
} finally {
setNativeRecoveryReady(true);
}
}, [
durableViewerState,
filePath,
initialDocument,
restoreWorkbenchView,
restoredNativeAlignment,
scrollToColumn,
scrollToRow,
sourceStateKey,
]);
useEffect(() => {
if (durableViewerState == null || !nativeRecoveryReady) return;
try {
durableViewerState.updateAlignment({
sourceStateKey,
state: createDurableAlignmentState({
alignmentColumnJump,
anchorRowId,
fileName: filePath,
focusedCell:
focusedCell == null
? null
: {
column: focusedCell.column,
rowId: focusedCell.row.id,
symbol: focusedCell.symbol,
},
guideTreeNewick,
initialDocument,
motifQuery,
pinnedCell:
pinnedCell == null
? null
: {
column: pinnedCell.column,
rowId: pinnedCell.row.id,
symbol: pinnedCell.symbol,
},
referencePositionJump,
selectedHitIndex,
sourceStateKey,
state: workbenchState,
view: workbenchView,
viewport: {
column: slice.visibleColumnStart,
row: slice.visibleRowStart,
},
}),
});
} catch (error) {
setWorkbenchError(
error instanceof Error
? error.message
: "The native Alignment state could not be checkpointed.",
);
}
}, [
alignmentColumnJump,
anchorRowId,
durableViewerState,
filePath,
focusedCell,
guideTreeNewick,
initialDocument,
motifQuery,
nativeRecoveryReady,
pinnedCell,
referencePositionJump,
selectedHitIndex,
slice.visibleColumnStart,
slice.visibleRowStart,
sourceStateKey,
workbenchState,
workbenchView,
]);
useEffect(() => {
if (durableViewerState == null) return;
const flushCheckpoint = (): void => {
void durableViewerState.flush().catch(() => {
// A failed native flush never resurrects a conversation-scoped MCP.
});
};
window.addEventListener("pagehide", flushCheckpoint);
return () => {
window.removeEventListener("pagehide", flushCheckpoint);
flushCheckpoint();
};
}, [durableViewerState]);
useEffect(() => {
if (workbenchTree != null && treeSourceKeyRef.current !== treeInputKey) {
treeSourceKeyRef.current = null;
setWorkbenchTree(null);
setGuideTreeNewick(null);
}
}, [treeInputKey, workbenchTree]);
useAlignmentWorkbenchCommands({
analysis,
cancelledJobsRef,
command,
dispatch: dispatchWorkbench,
documentFileName: filePath,
handledCommandIdRef,
hits: motifHits,
onCommandResult,
onRestoreView: restoreWorkbenchView,
onSetTree: updateWorkbenchTree,
selectedColumns: selectedColumnRange,
sourceStateKey,
state: workbenchState,
tree: workbenchTree,
view: workbenchView,
visibleRows,
});
useEffect(() => {
markMsaPreviewMilestone("usable-grid");
}, [document.alignedLength, document.rows.length]);
useEffect(() => {
if (analysisState.phase === "ready") {
markMsaPreviewMilestone("analysis-ready");
}
}, [analysisState.phase]);
const alignmentModelContext = createMsaViewerModelContext({
analysis,
analysisScope,
artifacts: workbenchState.artifacts,
anchorRowId,
cellWidth,
colorMode,
availableMetricTracks,
enabledMetricTracks: visibleMetricTracks,
document,
dirty: workbenchState.dirty,
filePath,
focusedCell,
guideTreeNewick,
jobs: workbenchState.jobs,
motifHits,
motifQuery,
searchPending: searchState.phase === "searching",
searchTruncated: searchState.truncated,
referenceLabel,
referenceMode,
residuePalette,
rowFilter,
rowSortDirection,
rowSortKey,
searchScope,
selectedColumnRange,
selectedRowIds: workbenchState.selectedRows,
selectedHit,
showAnnotationTracks,
showIdenticalAsDots,
showRnaStructureOverlays,
showSequenceLogoHelp:
showSequenceLogoHelp && visibleMetricTracks.includes("sequence-logo"),
slice,
sourceStateKey,
tracks: workbenchState.tracks,
tree: workbenchTree,
visibleRows,
viewerSessionId,
});
useModelContext(updateModelContext, {
...alignmentModelContext,
structuredContent: {
...alignmentModelContext.structuredContent,
toolbarVisible,
},
});
useEffect(() => {
if (selectedHit == null) {
return;
}
if (
selectedHit.alignmentStartColumn < slice.visibleColumnStart ||
selectedHit.alignmentEndColumn >= slice.visibleColumnEnd
) {
scrollToColumn(
selectedHit.alignmentStartColumn -
Math.floor(Math.max(visibleColumns, 1) / 4),
);
}
const rowIndex = visibleRows.findIndex(
(row) => row.id === selectedHit.rowId,
);
if (
rowIndex >= 0 &&
(rowIndex < slice.visibleRowStart || rowIndex >= slice.visibleRowEnd)
) {
scrollToRow(Math.max(0, rowIndex - 2));
}
}, [
scrollToColumn,
scrollToRow,
selectedHit,
slice.visibleRowEnd,
slice.visibleRowStart,
slice.visibleColumnEnd,
slice.visibleColumnStart,
visibleColumns,
visibleRows,
]);
function updateInterpretation(moleculeType: MsaMoleculeType): void {
const nucleicAcid =
moleculeType === "dna" ||
moleculeType === "rna" ||
moleculeType === "nucleic-acid-ambiguous";
setDocument((current) => ({
...current,
cdsContext: inferCdsContext({
alignedLength: current.alignedLength,
moleculeType,
rows: current.rows,
}),
displayInterpretation: {
moleculeType,
source: "user-override",
},
searchCapabilities: {
...current.searchCapabilities,
supportsAmbiguousNucleotideCodes: nucleicAcid,
supportsProteinAmbiguityCodes: moleculeType === "protein",
supportsReverseComplement: nucleicAcid,
},
}));
setResiduePalette((current) =>
isResiduePaletteCompatible(moleculeType, current)
? current
: getDefaultResiduePalette(moleculeType),
);
setColorMode((current) => {
const proteinOnly =
current === "protein-conservation" || current === "protein-similarity";
const nucleotideOnly =
current === "nucleotide-substitution" || current === "coding-impact";
if (
(proteinOnly && moleculeType !== "protein") ||
(nucleotideOnly && !nucleicAcid)
) {
return getDefaultColorMode(moleculeType);
}
return current;
});
}
function toggleRow(rowId: string): void {
applyRows(
document.rows.map((row) =>
row.id === rowId ? { ...row, hidden: !row.hidden } : row,
),
);
}
function applyRows(nextRows: MsaDocument["rows"]): void {
const nextUnhiddenRowIds = nextRows
.filter((row) => !row.hidden)
.map((row) => row.id);
setDocument((current) => ({
...current,
rawSummary: {
...current.rawSummary,
visibleSequenceCount: nextUnhiddenRowIds.length,
},
rows: nextRows,
}));
dispatchInteraction({
rowIds: nextUnhiddenRowIds,
type: "analysis-rows-changed",
});
setMotifQuery("");
}
function adjustCellWidth(delta: number): void {
setCellWidth((current) =>
Math.max(MIN_CELL_WIDTH_PX, Math.min(MAX_CELL_WIDTH_PX, current + delta)),
);
}
function resetView(): void {
const moleculeType = document.displayInterpretation.moleculeType;
dispatchInteraction({
anchorRowId: document.rows.find((row) => !row.hidden)?.id ?? null,
type: "reset",
});
setShowIdenticalAsDots(false);
setShowAnnotationTracks(true);
setShowRnaStructureOverlays(true);
setEnabledMetricTracks(getDefaultMsaMetricTracks(document));
setShowSequenceLogoHelp(false);
setRowSortKey("source");
setRowSortDirection("asc");
setAnalysisScope("all-unhidden-rows");
setSearchScope("currently-displayed-rows");
setColorMode(getDefaultColorMode(moleculeType));
setResiduePalette(getDefaultResiduePalette(moleculeType));
setRowFilter("");
setAlignmentColumnJump("");
setReferencePositionJump("");
setMotifQuery("");
setCellWidth(DEFAULT_CELL_WIDTH_PX);
scrollToColumn(0);
clearSelection();
}
function jumpToAlignmentColumn(): void {
const targetColumn = Number.parseInt(alignmentColumnJump.trim(), 10);
if (!Number.isFinite(targetColumn)) {
return;
}
scrollToColumn(Math.max(0, targetColumn - 1));
}
function jumpToReferencePosition(): void {
const targetPosition = Number.parseInt(referencePositionJump.trim(), 10);
if (!Number.isFinite(targetPosition) || referenceSequence == null) {
return;
}
const targetColumn = getAlignmentColumnForUngappedPosition(
referenceSequence,
targetPosition,
);
if (targetColumn != null) {
scrollToColumn(targetColumn);
}
}
function buildColumnRange(anchor: number, column: number): MsaColumnRange {
const start = Math.max(0, Math.min(anchor, column));
const inclusiveEnd = Math.max(anchor, column);
return {
end: Math.min(document.alignedLength, inclusiveEnd + 1),
start,
};
}
function startColumnRangeSelection(column: number): void {
const normalizedColumn = Math.max(
0,
Math.min(Math.max(0, document.alignedLength - 1), column),
);
columnSelectionAnchorRef.current = normalizedColumn;
setSelectedColumnRange(
buildColumnRange(normalizedColumn, normalizedColumn),
);
}
function extendColumnRangeSelection(column: number): void {
const anchor = columnSelectionAnchorRef.current;
if (anchor == null) {
return;
}
const normalizedColumn = Math.max(
0,
Math.min(Math.max(0, document.alignedLength - 1), column),
);
setSelectedColumnRange(buildColumnRange(anchor, normalizedColumn));
}
function updateKeyboardColumnRangeSelection(
anchorColumn: number,
nextColumn: number,
extend: boolean,
): void {
if (!extend) {
columnSelectionAnchorRef.current = null;
return;
}
const anchor = columnSelectionAnchorRef.current ?? anchorColumn;
columnSelectionAnchorRef.current = anchor;
setSelectedColumnRange(buildColumnRange(anchor, nextColumn));
}
function updateSelectedColumnRange(range: MsaColumnRange | null): void {
setSelectedColumnRange(range);
columnSelectionAnchorRef.current = null;
}
function focusSelectedColumnRange(): void {
if (selectedColumnRange == null) {
return;
}
scrollToColumn(selectedColumnRange.start);
}
useEffect(() => {
if (
command == null ||
command.action === "set_mode" ||
isAlignmentOperation(command) ||
handledCommandIdRef.current === command.commandId
) {
return;
}
let result: SequenceViewerCommandResult;
switch (command.action) {
case "clear_alignment_selection":
clearSelection();
result = {
applied: true,
message: "Cleared the alignment selection.",
};
break;
case "filter_alignment_rows": {
const nextFilter = command.query.trim();
const normalizedFilter = nextFilter.toLowerCase();
const matchingRowCount = document.rows.filter(
(row) =>
!row.hidden &&
(normalizedFilter.length === 0 ||
`${row.label} ${row.description ?? ""}`
.toLowerCase()
.includes(normalizedFilter)),
).length;
const matchingRowIds = document.rows
.filter(
(row) =>
!row.hidden &&
(normalizedFilter.length === 0 ||
`${row.label} ${row.description ?? ""}`
.toLowerCase()
.includes(normalizedFilter)),
)
.map((row) => row.id);
setRowFilter(nextFilter);
setMotifQuery("");
setSelectedHitIndex(0);
dispatchInteraction({
rowIds: matchingRowIds,
type: "visible-rows-changed",
});
result = {
applied: true,
message:
nextFilter.length === 0
? `Cleared the row filter; ${matchingRowCount} unhidden rows are displayed.`
: `Filtered the alignment to ${matchingRowCount} matching row${matchingRowCount === 1 ? "" : "s"}.`,
state: { matchingRowCount, query: nextFilter },
};
break;
}
case "select_alignment_columns": {
const start = Math.min(command.start ?? 1, command.end ?? 1);
const end = Math.max(command.start ?? 1, command.end ?? 1);
if (start < 1 || end > document.alignedLength) {
result = {
applied: false,
message: `Alignment range ${start}-${end} is outside this ${document.alignedLength}-column alignment.`,
state: { maxColumn: document.alignedLength, minColumn: 1 },
};
break;
}
updateSelectedColumnRange({ end, start: start - 1 });
scrollToColumn(start - 1);
result = {
applied: true,
message: `Selected alignment columns ${start}-${end}.`,
state: { end, start },
};
break;
}
case "focus_alignment_cell": {
const rowResolution = resolveAlignmentRow(visibleRows, command.row);
if (rowResolution.status !== "resolved") {
result = {
applied: false,
message:
rowResolution.status === "ambiguous"
? `More than one visible alignment row matched ${command.row}; retry with an exact row ID.`
: `No visible alignment row matched ${command.row ?? "the request"}.`,
state:
rowResolution.status === "ambiguous"
? {
candidateRowIds: rowResolution.candidates.map(
({ id }) => id,
),
}
: undefined,
};
break;
}
const row = rowResolution.target;
const requestedColumn = command.column ?? 1;
if (requestedColumn < 1 || requestedColumn > document.alignedLength) {
result = {
applied: false,
message: `Alignment column ${requestedColumn} is outside this ${document.alignedLength}-column alignment.`,
state: { maxColumn: document.alignedLength, minColumn: 1 },
};
break;
}
const column = requestedColumn - 1;
const cell = {
column,
row,
symbol: row.alignedSequence[column] ?? "-",
};
setFocusedCell(cell);
setPinnedCell(cell);
scrollToColumn(column);
const rowIndex = visibleRows.findIndex(
(candidate) => candidate.id === row.id,
);
if (rowIndex >= 0) {
scrollToRow(rowIndex);
}
result = {
applied: true,
message: `Focused ${row.label} at alignment column ${column + 1}.`,
state: { column: column + 1, rowId: row.id, symbol: cell.symbol },
};
break;
}
case "focus_alignment_reference_coordinate": {
if (referenceSequence == null) {
result = {
applied: false,
message:
"No active alignment reference is available. Set a consensus or anchor reference first.",
};
break;
}
const column = getAlignmentColumnForUngappedPosition(
referenceSequence,
command.coordinate,
);
if (column == null) {
result = {
applied: false,
message: `Reference coordinate ${command.coordinate} is outside the active reference sequence.`,
};
break;
}
updateSelectedColumnRange({ end: column + 1, start: column });
scrollToColumn(column);
const referenceRow =
referenceMode === "anchor"
? (document.rows.find((row) => row.id === anchorRowId) ?? null)
: null;
if (referenceRow != null) {
const cell = {
column,
row: referenceRow,
symbol: referenceRow.alignedSequence[column] ?? "-",
};
setFocusedCell(cell);
setPinnedCell(cell);
const rowIndex = visibleRows.findIndex(
(row) => row.id === referenceRow.id,
);
if (rowIndex >= 0) {
scrollToRow(rowIndex);
}
}
result = {
applied: true,
message: `Focused reference coordinate ${command.coordinate} at alignment column ${column + 1}.`,
state: {
alignmentColumn: column + 1,
coordinate: command.coordinate,
referenceMode,
referenceRowId: referenceRow?.id ?? null,
},
};
break;
}
case "navigate_alignment_search_hit": {
if (motifHits.length === 0) {
result = {
applied: false,
message: "There are no alignment search hits to navigate.",
};
break;
}
const nextIndex =
(selectedHitIndex +
(command.direction === "next" ? 1 : -1) +
motifHits.length) %
motifHits.length;
const hit = motifHits[nextIndex];
if (hit == null) {
return;
}
setSelectedHitIndex(nextIndex);
result = {
applied: true,
message: `Focused alignment search hit ${nextIndex + 1} of ${motifHits.length}.`,
state: {
alignmentEndColumn: hit.alignmentEndColumn + 1,
alignmentStartColumn: hit.alignmentStartColumn + 1,
hitCount: motifHits.length,
hitIndex: nextIndex + 1,
orientation: hit.orientation,
rowId: hit.rowId,
},
};
break;
}
case "reset_alignment_view":
resetView();
result = {
applied: true,
message: "Reset the alignment view controls.",
};
break;
case "search_alignment":
setMotifQuery(command.query ?? "");
setSelectedHitIndex(0);
result = {
applied: true,
message: `Searching the alignment for ${command.query ?? ""}.`,
state: { query: command.query ?? "" },
};
break;
case "select_alignment_rows": {
const resolutions = command.rows.map((selector) => ({
resolution: resolveAlignmentRow(document.rows, selector),
selector,
}));
const ambiguous = resolutions.find(
({ resolution }) => resolution.status === "ambiguous",
);
if (ambiguous?.resolution.status === "ambiguous") {
result = {
applied: false,
message: `More than one alignment row matched ${ambiguous.selector}; retry with exact row IDs.`,
state: {
candidateRowIds: ambiguous.resolution.candidates.map(
({ id }) => id,
),
},
};
break;
}
const missingRows = resolutions.flatMap(({ resolution, selector }) =>
resolution.status === "not-found" ? [selector] : [],
);
if (missingRows.length > 0) {
result = {
applied: false,
message:
"The alignment row selection was not changed because some requested rows were not found.",
state: { missingRows },
};
break;
}
const selectedRowIds = [
...new Set(
resolutions.flatMap(({ resolution }) =>
resolution.status === "resolved" ? [resolution.target.id] : [],
),
),
];
dispatchWorkbench({
rowIds: selectedRowIds,
type: "select-alignment-rows",
});
result = {
applied: true,
message:
selectedRowIds.length === 0
? "Cleared the alignment row selection."
: `Selected ${selectedRowIds.length} alignment row${selectedRowIds.length === 1 ? "" : "s"} for analysis, editing, or export.`,
state: { selectedRowCount: selectedRowIds.length, selectedRowIds },
};
break;
}
case "set_alignment_row_visibility": {
const rowResolutions = command.rows.map((selector) => ({
resolution: resolveAlignmentRow(document.rows, selector),
selector,
}));
const ambiguous = rowResolutions.find(
({ resolution }) => resolution.status === "ambiguous",
);
if (ambiguous?.resolution.status === "ambiguous") {
result = {
applied: false,
message: `More than one alignment row matched ${ambiguous.selector}; retry with exact row IDs.`,
state: {
candidateRowIds: ambiguous.resolution.candidates.map(
({ id }) => id,
),
},
};
break;
}
const matchedRows = rowResolutions.flatMap(({ resolution }) =>
resolution.status === "resolved" ? [resolution.target] : [],
);
if (matchedRows.length === 0) {
result = {
applied: false,
message: "None of the requested alignment rows were found.",
state: { missingRows: command.rows },
};
break;
}
const matchedRowIds = new Set(matchedRows.map(({ id }) => id));
const nextRows = document.rows.map((row) =>
matchedRowIds.has(row.id)
? { ...row, hidden: !command.visible }
: row,
);
const missingRows = rowResolutions.flatMap(
({ resolution, selector }) =>
resolution.status === "not-found" ? [selector] : [],
);
const visibleRowCount = nextRows.filter((row) => !row.hidden).length;
applyRows(nextRows);
result = {
applied: true,
message: `${command.visible ? "Showed" : "Hid"} ${matchedRows.length} alignment row${matchedRows.length === 1 ? "" : "s"}${missingRows.length === 0 ? "." : `; ${missingRows.length} requested row${missingRows.length === 1 ? " was" : "s were"} not found.`}`,
state: {
matchedRowIds: matchedRows.map(({ id }) => id),
missingRows,
visible: command.visible,
visibleRowCount,
},
};
break;
}
case "set_alignment_reference": {
const normalizedReference = command.reference?.trim() ?? "";
if (normalizedReference === "consensus") {
setReferenceMode("consensus");
result = {
applied: true,
message: "Set the alignment reference to consensus.",
state: { referenceMode: "consensus" },
};
break;
}
if (normalizedReference === "none") {
setReferenceMode("none");
result = {
applied: true,
message: "Cleared the alignment reference.",
state: { referenceMode: "none" },
};
break;
}
const rowResolution = resolveAlignmentRow(
document.rows.filter((row) => !row.hidden),
normalizedReference,
);
if (rowResolution.status !== "resolved") {
result = {
applied: false,
message:
rowResolution.status === "ambiguous"
? `More than one alignment row matched ${normalizedReference}; retry with an exact row ID.`
: `No unhidden alignment row matched ${normalizedReference}.`,
state:
rowResolution.status === "ambiguous"
? {
candidateRowIds: rowResolution.candidates.map(
({ id }) => id,
),
}
: undefined,
};
break;
}
const row = rowResolution.target;
setAnchorRowId(row.id);
setReferenceMode("anchor");
result = {
applied: true,
message: `Set ${row.label} as the alignment reference.`,
state: { referenceMode: "anchor", rowId: row.id },
};
break;
}
case "set_alignment_view_options": {
const targetMoleculeType =
command.moleculeType ?? document.displayInterpretation.moleculeType;
const targetCdsContext =
command.moleculeType == null
? document.cdsContext
: inferCdsContext({
alignedLength: document.alignedLength,
moleculeType: targetMoleculeType,
rows: document.rows,
});
const nextAvailableMetricTracks = getAvailableMsaMetricTracks({
moleculeType: targetMoleculeType,
referenceAvailable: referenceSequence != null,
rnaStructureAvailable: document.rnaStructure != null,
});
const unavailableMetricTracks =
command.enabledMetricTracks?.filter(
(track) => !nextAvailableMetricTracks.includes(track),
) ?? [];
if (unavailableMetricTracks.length > 0) {
result = {
applied: false,
message:
"Some requested metric tracks are unavailable for this alignment and active reference; no display options were changed.",
state: {
availableMetricTracks: nextAvailableMetricTracks,
unavailableMetricTracks,
},
};
break;
}
const nextEnabledMetricTracks = [
...new Set(command.enabledMetricTracks ?? enabledMetricTracks),
];
if (
command.showSequenceLogoHelp === true &&
!nextEnabledMetricTracks.includes("sequence-logo")
) {
result = {
applied: false,
message:
"Enable the sequence-logo metric track before opening its help; no display options were changed.",
};
break;
}
if (
command.colorMode != null &&
!isMsaColorModeCompatible({
cdsEligible: targetCdsContext.applicability === "eligible",
colorMode: command.colorMode,
moleculeType: targetMoleculeType,
})
) {
result = {
applied: false,
message: `Color mode ${command.colorMode} is not compatible with the ${targetMoleculeType} interpretation.`,
};
break;
}
if (
command.residuePalette != null &&
!isResiduePaletteCompatible(
targetMoleculeType,
command.residuePalette,
)
) {
result = {
applied: false,
message: `Residue palette ${command.residuePalette} is not compatible with the ${targetMoleculeType} interpretation.`,
};
break;
}
const nextColorMode =
command.colorMode ??
(isMsaColorModeCompatible({
cdsEligible: targetCdsContext.applicability === "eligible",
colorMode,
moleculeType: targetMoleculeType,
})
? colorMode
: getDefaultColorMode(targetMoleculeType));
const nextResiduePalette =
command.residuePalette ??
(isResiduePaletteCompatible(targetMoleculeType, residuePalette)
? residuePalette
: getDefaultResiduePalette(targetMoleculeType));
if (command.moleculeType != null) {
updateInterpretation(command.moleculeType);
}
if (command.analysisScope != null) {
setAnalysisScope(command.analysisScope);
}
if (command.cellWidth != null) {
setCellWidth(command.cellWidth);
}
if (command.colorMode != null) {
setColorMode(command.colorMode);
}
if (command.residuePalette != null) {
setResiduePalette(command.residuePalette);
}
if (command.searchScope != null) {
setSearchScope(command.searchScope);
}
if (command.showAnnotationTracks != null) {
setShowAnnotationTracks(command.showAnnotationTracks);
}
if (command.showIdenticalAsDots != null) {
setShowIdenticalAsDots(command.showIdenticalAsDots);
}
if (command.showRnaStructureOverlays != null) {
setShowRnaStructureOverlays(command.showRnaStructureOverlays);
}
if (command.enabledMetricTracks != null) {
setEnabledMetricTracks(nextEnabledMetricTracks);
}
if (command.showSequenceLogoHelp != null) {
setShowSequenceLogoHelp(command.showSequenceLogoHelp);
}
if (command.rowSortKey != null) {
setRowSortKey(command.rowSortKey);
}
if (command.rowSortDirection != null) {
setRowSortDirection(command.rowSortDirection);
}
result = {
applied: true,
message: "Updated the alignment display and analysis options.",
state: {
analysisScope: command.analysisScope ?? analysisScope,
cellWidth: command.cellWidth ?? cellWidth,
colorMode: nextColorMode,
availableMetricTracks: nextAvailableMetricTracks,
enabledMetricTracks: nextEnabledMetricTracks.filter((track) =>
nextAvailableMetricTracks.includes(track),
),
moleculeType: targetMoleculeType,
residuePalette: nextResiduePalette,
rowSortDirection: command.rowSortDirection ?? rowSortDirection,
rowSortKey: command.rowSortKey ?? rowSortKey,
rowSortScope: "row-manager",
searchScope: command.searchScope ?? searchScope,
showAnnotationTracks:
command.showAnnotationTracks ?? showAnnotationTracks,
showIdenticalAsDots:
command.showIdenticalAsDots ?? showIdenticalAsDots,
showRnaStructureOverlays:
command.showRnaStructureOverlays ?? showRnaStructureOverlays,
showSequenceLogoHelp:
(command.showSequenceLogoHelp ?? showSequenceLogoHelp) &&
nextEnabledMetricTracks.includes("sequence-logo"),
},
};
break;
}
case "show_all_alignment_rows": {
const nextRows = document.rows.map((row) => ({
...row,
hidden: false,
}));
applyRows(nextRows);
result = {
applied: true,
message: `Showed all ${nextRows.length} alignment rows.`,
state: { visibleRowCount: nextRows.length },
};
break;
}
case "compute_alignment_guide_tree": {
try {
const tree = buildGuideTree(
document.rows.filter((row) => !row.hidden),
"upgma",
);
updateWorkbenchTree(tree);
result = {
applied: true,
message:
"Computed an exploratory UPGMA guide tree from alignment p-distances.",
state: {
method: "UPGMA",
newick: tree.newick,
provenance:
"Uncorrected alignment p-distance; all-gap columns ignored.",
},
};
} catch (error) {
result = {
applied: false,
message:
error instanceof Error
? error.message
: "The alignment guide tree could not be computed.",
};
}
break;
}
default:
return;
}
handledCommandIdRef.current = command.commandId;
onCommandResult?.(command, result);
}, [
analysisScope,
anchorRowId,
cellWidth,
clearSelection,
colorMode,
command,
document,
enabledMetricTracks,
motifHits,
onCommandResult,
referenceMode,
referenceSequence,
residuePalette,
rowSortDirection,
rowSortKey,
scrollToColumn,
scrollToRow,
searchScope,
selectedHitIndex,
showAnnotationTracks,
showIdenticalAsDots,
showRnaStructureOverlays,
showSequenceLogoHelp,
visibleRows,
]);
function runLocalAlignmentAnalysis(
request:
| { algorithm: GuideTreeAlgorithm; analysis: "build-tree" }
| { analysis: "distance-matrix" },
): void {
const sourceDocument = document;
const id = crypto.randomUUID();
const kind =
request.analysis === "build-tree" ? "guide-tree" : "distance-matrix";
dispatchWorkbench({
job: {
id,
kind,
message: "Queued in the viewer",
parameters: request,
progress: 0,
startedAt: Date.now(),
status: "running",
},
type: "start-job",
});
window.setTimeout(() => {
if (cancelledJobsRef.current.has(id)) return;
if (workbenchStateRef.current.document !== sourceDocument) {
dispatchWorkbench({
completedAt: Date.now(),
error:
"The source alignment changed before this job completed. Run it again on the current copy.",
id,
type: "fail-job",
});
return;
}
try {
const result = runAlignmentAnalysis({
document: sourceDocument,
request,
});
if (cancelledJobsRef.current.has(id)) return;
if (workbenchStateRef.current.document !== sourceDocument) {
dispatchWorkbench({
completedAt: Date.now(),
error:
"The source alignment changed before this job completed. Run it again on the current copy.",
id,
type: "fail-job",
});
return;
}
if (request.analysis === "build-tree") {
updateWorkbenchTree(result.tree as GuideTreeResult, sourceDocument);
}
dispatchWorkbench({
completedAt: Date.now(),
id,
message: `${kind} completed.`,
result,
type: "complete-job",
});
} catch (error) {
if (cancelledJobsRef.current.has(id)) return;
dispatchWorkbench({
completedAt: Date.now(),
error: error instanceof Error ? error.message : `${kind} failed.`,
id,
type: "fail-job",
});
}
}, 0);
}
function applyLocalAlignmentEdit(request: SequenceViewerEditRequest): void {
try {
setWorkbenchError(undefined);
const change = applyAlignmentEditRequest({
document,
request,
tree: workbenchTree,
});
if ("historyOperation" in change) {
dispatchWorkbench({
type:
change.historyOperation === "undo"
? "undo-alignment-document"
: "redo-alignment-document",
});
return;
}
dispatchWorkbench({
description: change.description,
document: change.document,
type: "apply-alignment-document",
});
} catch (error) {
setWorkbenchError(
error instanceof Error ? error.message : "The alignment edit failed.",
);
}
}
function realignLocalRows(rowIds?: Array<string>): void {
const sourceDocument = document;
const id = crypto.randomUUID();
dispatchWorkbench({
job: {
id,
kind: "align",
message: "Queued in the viewer",
parameters: { rowIds: rowIds ?? null },
progress: 0,
startedAt: Date.now(),
status: "running",
},
type: "start-job",
});
window.setTimeout(() => {
if (cancelledJobsRef.current.has(id)) return;
if (workbenchStateRef.current.document !== sourceDocument) {
dispatchWorkbench({
completedAt: Date.now(),
error:
"The source alignment changed before this job completed. Run it again on the current copy.",
id,
type: "fail-job",
});
return;
}
try {
const result = realignRows({ document: sourceDocument, rowIds });
if (cancelledJobsRef.current.has(id)) return;
if (workbenchStateRef.current.document !== sourceDocument) {
dispatchWorkbench({
completedAt: Date.now(),
error:
"The source alignment changed before this job completed. Run it again on the current copy.",
id,
type: "fail-job",
});
return;
}
const content = exportAlignedFasta(result.document.rows);
dispatchWorkbench({
description: "Realigned rows in an editable copy.",
document: result.document,
type: "apply-alignment-document",
});
dispatchWorkbench({
artifact: {
content,
createdAt: Date.now(),
format: "aligned-fasta",
id: crypto.randomUUID(),
mediaType: "text/x-fasta",
name: `${basenameFromPath(filePath ?? "alignment")}.realigned.fasta`,
provenance: {
engine: result.engine,
parameters: result.parameters,
sourceRevision: workbenchState.revision,
},
},
type: "add-artifact",
});
dispatchWorkbench({
completedAt: Date.now(),
id,
message: "Alignment completed.",
result: {
alignedLength: result.document.alignedLength,
rowCount: result.document.rows.length,
warning: result.warning,
},
type: "complete-job",
});
} catch (error) {
if (cancelledJobsRef.current.has(id)) return;
dispatchWorkbench({
completedAt: Date.now(),
error: error instanceof Error ? error.message : "Alignment failed.",
id,
type: "fail-job",
});
}
}, 0);
}
function exportLocalAlignment(
format:
| "a3m"
| "aligned-fasta"
| "clustal"
| "json"
| "newick"
| "pdf"
| "stockholm"
| "svg"
| "tsv",
scope: "all" | "selection" | "visible",
): void {
try {
setWorkbenchError(undefined);
const output = exportAlignmentWorkbench({
document,
format,
name: filePath,
newick: workbenchTree?.newick,
scope,
selectedColumns: selectedColumnRange,
selectedRows: workbenchState.selectedRows,
visibleRows,
});
dispatchWorkbench({
artifact: {
...output,
createdAt: Date.now(),
id: crypto.randomUUID(),
provenance: {
engine: "sequence-viewer-alignment-export-v1",
parameters: createAlignmentExportParameters({
format,
scope,
tree: workbenchTree,
}),
sourceRevision: workbenchState.revision,
},
},
type: "add-artifact",
});
} catch (error) {
setWorkbenchError(
error instanceof Error ? error.message : "The export failed.",
);
}
}
function prepareWorkspaceSession(): string {
return serializeWorkbenchSession(
createAlignmentWorkbenchSession(
workbenchState,
workbenchView,
filePath,
sourceStateKey,
),
);
}
function saveLocalSession(): void {
try {
setWorkbenchError(undefined);
const content = prepareWorkspaceSession();
dispatchWorkbench({
artifact: {
content,
createdAt: Date.now(),
format: "sequence-viewer-session",
id: crypto.randomUUID(),
mediaType: "application/json",
name: `${basenameFromPath(filePath ?? "alignment")}.alignment-session.json`,
provenance: {
engine: "sequence-viewer-session-v1",
parameters: { mode: "alignment" },
sourceRevision: workbenchState.revision,
},
},
type: "add-artifact",
});
} catch (error) {
setWorkbenchError(
error instanceof Error
? error.message
: "The session could not be saved.",
);
}
}
function restoreLocalSession(content: string): void {
try {
setWorkbenchError(undefined);
const session = parseWorkbenchSession(content);
if (session.view.mode !== "alignment" || session.view.alignment == null) {
throw new Error("This is not an Alignment-mode workbench session.");
}
if (
session.source.fileName !== (filePath ?? null) ||
session.source.format !== document.format ||
session.source.stateKey !== sourceStateKey
) {
throw new Error(
"This session belongs to a different source artifact and was not applied.",
);
}
if (session.snapshot?.alignmentDocument != null) {
dispatchWorkbench({
description: "Restored saved alignment-copy state.",
document: session.snapshot.alignmentDocument,
type: "apply-alignment-document",
});
}
dispatchWorkbench({
state: {
artifacts: session.artifacts,
dirty: session.dirty,
jobs: settleRestoredJobs(session.jobs),
revision: workbenchState.revision,
tracks: session.tracks,
},
type: "restore-shared",
});
restoreWorkbenchView(session.view.alignment);
const restoredTree = [...session.jobs]
.reverse()
.find(
({ kind, status }) => kind === "guide-tree" && status === "completed",
)?.result?.tree;
if (isGuideTreeResult(restoredTree)) {
updateWorkbenchTree(
restoredTree,
session.snapshot?.alignmentDocument ?? document,
);
}
} catch (error) {
setWorkbenchError(
error instanceof Error
? error.message
: "The session could not be restored.",
);
}
}
const toolbarProps: MsaToolbarProps = {
anchorRowId,
alignmentColumnJump,
analysisScope,
colorMode,
document,
motifQuery,
onAnchorRowIdChange: setAnchorRowId,
onAlignmentColumnJumpChange: setAlignmentColumnJump,
onAnalysisScopeChange: setAnalysisScope,
onColorModeChange: setColorMode,
onInterpretationChange: updateInterpretation,
onJumpToAlignmentColumn: jumpToAlignmentColumn,
onJumpToReferencePosition: jumpToReferencePosition,
onMotifQueryChange: (value) => {
setMotifQuery(value);
setSelectedHitIndex(0);
},
onNextHit: () =>
setSelectedHitIndex((current) =>
motifHits.length === 0 ? 0 : (current + 1) % motifHits.length,
),
onPreviousHit: () =>
setSelectedHitIndex((current) =>
motifHits.length === 0
? 0
: (current - 1 + motifHits.length) % motifHits.length,
),
onReferenceModeChange: setReferenceMode,
onReferencePositionJumpChange: setReferencePositionJump,
onResetView: resetView,
onResiduePaletteChange: setResiduePalette,
onRowFilterChange: (value) => {
setRowFilter(value);
setMotifQuery("");
setSelectedHitIndex(0);
},
onSearchScopeChange: setSearchScope,
onShowAnnotationTracksChange: setShowAnnotationTracks,
onShowIdenticalAsDotsChange: setShowIdenticalAsDots,
onShowRnaStructureOverlaysChange: setShowRnaStructureOverlays,
onWindowBack: () =>
scrollToColumn(Math.max(0, slice.visibleColumnStart - WINDOW_STEP)),
onWindowForward: () =>
scrollToColumn(
Math.min(
Math.max(0, document.alignedLength - visibleColumns),
slice.visibleColumnStart + WINDOW_STEP,
),
),
onZoomIn: () => adjustCellWidth(4),
onZoomOut: () => adjustCellWidth(-4),
referenceMode,
referencePositionJump,
referenceSequence,
residuePalette,
rowFilter,
searchScope,
selectedHit,
selectedHitIndex,
showAnnotationTracks,
showIdenticalAsDots,
showRnaStructureOverlays,
totalHits: motifHits.length,
};
const workbenchProps: ComponentProps<typeof AlignmentWorkbenchPanel> = {
focusedCell:
focusedCell == null
? null
: { column: focusedCell.column, rowId: focusedCell.row.id },
onBuildTree: (algorithm) =>
runLocalAlignmentAnalysis({ algorithm, analysis: "build-tree" }),
onCancelJob: (jobId) => {
cancelledJobsRef.current.add(jobId);
dispatchWorkbench({ id: jobId, type: "cancel-job" });
},
onDistanceMatrix: () =>
runLocalAlignmentAnalysis({ analysis: "distance-matrix" }),
onEdit: applyLocalAlignmentEdit,
onExport: exportLocalAlignment,
onRealign: realignLocalRows,
onRedo: () => dispatchWorkbench({ type: "redo-alignment-document" }),
onRestoreSession: restoreLocalSession,
onSaveSession: saveLocalSession,
prepareWorkspaceSession,
publishWorkspaceArtifact,
workspaceSessionName: workspaceSessionDefaultName(filePath, "alignment"),
workspaceSessions,
onSelectRows: (rowIds) =>
dispatchWorkbench({ rowIds, type: "select-alignment-rows" }),
onUndo: () => dispatchWorkbench({ type: "undo-alignment-document" }),
selectedColumns: selectedColumnRange,
state: workbenchState,
tree: workbenchTree,
};
const controlsVisible = toolbarRevealed ?? toolbarVisible;
return (
<MsaPreviewShell
className={className}
filePath={filePath}
showFileHeader={showFileHeader}
>
<div className="bio-workbench-layout bio-msa-workbench-layout">
<main className="bio-workbench-main min-w-0 overflow-hidden">
<div className="flex flex-wrap items-center gap-2 border-b border-token-border px-3 py-2">
<span className="bg-token-main-surface-secondary rounded px-2 py-0.5 text-xs font-medium text-token-text-primary">
{formatMoleculeLabel(document.displayInterpretation.moleculeType)}
</span>
<span className="rounded border border-token-border px-2 py-0.5 text-xs text-token-text-secondary">
{document.format}
</span>
<span className="text-xs text-token-text-secondary">
{document.molecule.confidence === "high" ? (
<FormattedMessage
id="codex.filePreview.msa.confidence.high"
defaultMessage="high-confidence modality"
description="MSA viewer badge text for high-confidence molecule-type inference."
/>
) : (
<FormattedMessage
id="codex.filePreview.msa.confidence.caveated"
defaultMessage="{confidence}-confidence modality inference"
description="MSA viewer badge text for caveated molecule-type inference."
values={{ confidence: document.molecule.confidence }}
/>
)}
</span>
<div className="ml-auto flex flex-wrap gap-x-4 gap-y-1 text-xs text-token-text-secondary">
<span>
<FormattedMessage
id="codex.filePreview.msa.sequenceCount"
defaultMessage="{count, number} sequences"
description="Summary count of sequence rows parsed from an MSA file."
values={{ count: document.rows.length }}
/>
</span>
<span>
<FormattedMessage
id="codex.filePreview.msa.columnCount"
defaultMessage="{count, number} alignment columns"
description="Summary count of alignment columns parsed from an MSA file."
values={{ count: document.alignedLength }}
/>
</span>
<span>
{analysisState.phase === "ready" ? (
<FormattedMessage
id="codex.filePreview.msa.identity"
defaultMessage="{value}% mean identity"
description="Mean column identity summary for an MSA preview."
values={{
value: Math.round(
analysisState.analysis.meanIdentity * 100,
),
}}
/>
) : (
<FormattedMessage
id="codex.filePreview.msa.identityPending"
defaultMessage="Computing mean identity…"
description="Pending summary shown while MSA identity analysis is still running."
/>
)}
</span>
{analysisState.phase === "ready" &&
analysisState.analysis.meanConservationNormalized != null ? (
<span>
<FormattedMessage
id="codex.filePreview.msa.normalizedConservation"
defaultMessage="{value}% normalized conservation"
description="Mean normalized modality-specific conservation summary for an MSA preview."
values={{
value: Math.round(
analysisState.analysis.meanConservationNormalized * 100,
),
}}
/>
</span>
) : null}
</div>
</div>
<div
data-sequence-toolbar-controls="alignment"
hidden={!controlsVisible}
inert={!controlsVisible ? true : undefined}
>
<MsaToolbar {...toolbarProps} section="primary" />
</div>
{workbenchError == null ? null : (
<div
className="border-b border-red-500/40 bg-red-500/5 px-3 py-2 text-xs text-red-600 dark:text-red-400"
data-workbench-nonblocking="true"
role="alert"
>
{workbenchError}
</div>
)}
<MsaSearchResultsPanel
hits={motifHits}
isPending={searchState.phase === "searching"}
onSelectHit={setSelectedHitIndex}
query={motifQuery}
referenceSequence={referenceSequence}
selectedHitIndex={selectedHitIndex}
truncated={searchState.truncated}
/>
<MsaSelectedRangePanel
pinnedCell={pinnedCell}
range={selectedColumnRange}
onClear={clearSelection}
onFocus={focusSelectedColumnRange}
/>
<MsaPerformanceStateBanner
analysisMessage={
analysisState.phase === "error"
? analysisState.message
: undefined
}
fallbackMessage={
analysisState.phase === "ready"
? analysisState.fallbackMessage
: undefined
}
analysisPhase={analysisState.phase}
isLargeAlignment={isLargeAlignment}
searchPhase={searchState.phase}
/>
<MsaWarningsDrawer warnings={document.warnings} />
<MsaOverviewStrip
alignedLength={document.alignedLength}
isPending={analysisState.phase !== "ready"}
onWindowStartChange={scrollToColumn}
overviewBuckets={analysis?.overviewBuckets ?? []}
selectedColumnRange={selectedColumnRange}
onSelectedColumnRangeChange={updateSelectedColumnRange}
onHorizontalWheel={handleHorizontalWheel}
visibleColumns={visibleColumns}
windowStart={slice.visibleColumnStart}
/>
<MsaMetricTracks
analysis={analysis}
cellWidth={cellWidth}
columns={visibleTrackColumns}
document={document}
enabledTracks={enabledMetricTracks}
onHorizontalWheel={handleHorizontalWheel}
referenceSequence={referenceSequence}
slice={slice}
/>
<div
aria-label={intl.formatMessage({
id: "codex.filePreview.msa.scrollViewport",
defaultMessage: "Scrollable multiple sequence alignment matrix",
description:
"Accessible label for the horizontally and vertically scrollable MSA matrix viewport.",
})}
className="min-w-0 flex-1 overflow-auto"
ref={setScrollContainerRef}
>
<MsaMatrixRenderer
analysis={analysis}
cellWidth={cellWidth}
colorMode={colorMode}
columns={renderedColumns}
document={document}
motifHits={motifHits}
onFocusedCellChange={setFocusedCell}
onHoverCellDetailsChange={setHoverCellDetails}
onKeyboardRangeSelection={updateKeyboardColumnRangeSelection}
onPinnedCellChange={setPinnedCell}
onRangeSelectionExtend={extendColumnRangeSelection}
onRangeSelectionStart={startColumnRangeSelection}
referenceSequence={referenceSequence}
residuePalette={residuePalette}
rows={displayedRows}
selectedColumnRange={selectedColumnRange}
selectedHit={selectedHit}
showAnnotationTracks={showAnnotationTracks}
showIdenticalAsDots={showIdenticalAsDots}
showRnaStructureOverlays={showRnaStructureOverlays}
slice={slice}
summaries={summaries}
totalVisibleRows={visibleRows.length}
/>
</div>
<MsaStatusFooter
document={document}
focusedCell={focusedCell}
selectedHit={selectedHit}
visibleRowCount={visibleRows.length}
visibleRange={{
end: slice.visibleColumnEnd,
start: slice.visibleColumnStart,
}}
/>
<MsaPinnedInspector
analysis={analysis}
cell={pinnedCell}
document={document}
onClear={() => setPinnedCell(null)}
referenceSequence={referenceSequence}
/>
</main>
<aside
className="bio-workbench-sidebar"
data-sequence-toolbar-controls="alignment-inspector"
hidden={!controlsVisible}
inert={!controlsVisible ? true : undefined}
>
<WorkbenchTools
group="alignment-tools"
label="Alignment tools"
panels={[
{
id: "display",
label: "Display",
content: (
<>
<MsaToolbar {...toolbarProps} section="display" />
<MsaMetricTrackControls
document={document}
enabledTracks={enabledMetricTracks}
onEnabledTracksChange={setEnabledMetricTracks}
onSequenceLogoHelpChange={setShowSequenceLogoHelp}
referenceSequence={referenceSequence}
showSequenceLogoHelp={showSequenceLogoHelp}
/>
<MsaColorLegend
analysisPhase={analysisState.phase}
colorMode={colorMode}
document={document}
referenceMode={referenceMode}
referenceSequence={referenceSequence}
residuePalette={residuePalette}
/>
</>
),
},
{
id: "rows",
label: "Rows",
content: (
<MsaRowsPanel
document={document}
onSelectRows={workbenchProps.onSelectRows}
onSortDirectionChange={setRowSortDirection}
onSortKeyChange={setRowSortKey}
onToggleRow={toggleRow}
referenceKind={referenceMode}
rowMetricsById={rowMetricsById}
selectedRowIds={workbenchState.selectedRows}
sortDirection={rowSortDirection}
sortKey={rowSortKey}
/>
),
},
{
id: "analyze",
label: "Analyze",
content: (
<>
<AlignmentWorkbenchPanel
{...workbenchProps}
panel="analyze"
/>
<MsaGuideTreePanel
newick={guideTreeNewick}
onCompute={() =>
updateWorkbenchTree(
buildGuideTree(
document.rows.filter((row) => !row.hidden),
"upgma",
),
)
}
rows={document.rows.filter((row) => !row.hidden)}
/>
</>
),
},
{
id: "edit",
label: workbenchState.dirty ? "Edit copy •" : "Edit copy",
content: (
<AlignmentWorkbenchPanel {...workbenchProps} panel="edit" />
),
},
{
id: "export",
label: "Export",
content: (
<>
<AlignmentWorkbenchPanel
{...workbenchProps}
panel="export"
/>
<MsaExportActions
hits={motifHits}
publishWorkspaceArtifact={publishWorkspaceArtifact}
referenceLabel={referenceLabel}
referenceSequence={referenceSequence}
rows={visibleRows}
selectedColumnRange={selectedColumnRange}
serverSourceAlignedFasta={
document.format === "aligned-fasta" &&
workbenchState.revision === 0
}
sourceRevision={workbenchState.revision}
visibleColumnEnd={slice.visibleColumnEnd}
visibleColumnStart={slice.visibleColumnStart}
/>
</>
),
},
{
id: "details",
label: "Details",
content: <MsaMetadataPanel document={document} />,
},
{
id: "tasks",
label: workbenchState.jobs.some(
({ status }) => status === "running",
)
? "Tasks •"
: "Tasks",
content: (
<AlignmentWorkbenchPanel {...workbenchProps} panel="tasks" />
),
},
]}
/>
</aside>
</div>
<MsaCellHoverDetails details={hoverCellDetails} />
</MsaPreviewShell>
);
}
function MsaSelectedRangePanel({
onClear,
onFocus,
pinnedCell,
range,
}: {
onClear: () => void;
onFocus: () => void;
pinnedCell: FocusedMsaCell;
range: MsaColumnRange | null;
}): React.ReactElement | null {
if (range == null && pinnedCell == null) {
return null;
}
return (
<section className="flex flex-wrap items-center gap-2 border-b border-token-border bg-token-main-surface-primary px-3 py-2 text-xs text-token-text-secondary">
<span className="font-medium text-token-text-primary">
{range == null ? (
<FormattedMessage
id="codex.filePreview.msa.pinnedCellSelection"
defaultMessage="Pinned cell {rowLabel} column {column, number}"
description="Summary of the currently pinned MSA cell selection."
values={{
column: pinnedCell?.column == null ? 0 : pinnedCell.column + 1,
rowLabel: pinnedCell?.row.label ?? "",
}}
/>
) : (
<FormattedMessage
id="codex.filePreview.msa.selectedRange"
defaultMessage="Selected columns {start, number}-{end, number}"
description="Summary of the currently selected MSA alignment-column range."
values={{ end: range.end, start: range.start + 1 }}
/>
)}
</span>
{range == null ? null : (
<>
<span>
<FormattedMessage
id="codex.filePreview.msa.selectedRangeCount"
defaultMessage="{count, number} columns"
description="Column count for the currently selected MSA alignment-column range."
values={{ count: range.end - range.start }}
/>
</span>
<Button color="outline" size="toolbar" onClick={onFocus}>
<FormattedMessage
id="codex.filePreview.msa.focusSelectedRange"
defaultMessage="Focus range"
description="Button that moves the MSA viewport to the selected alignment-column range."
/>
</Button>
</>
)}
<Button color="outline" size="toolbar" onClick={onClear}>
<FormattedMessage
id="codex.filePreview.msa.clearSelectedRange"
defaultMessage="Clear selection"
description="Button that clears the selected MSA range or pinned cell."
/>
</Button>
<span>
<FormattedMessage
id="codex.filePreview.msa.clearSelectionShortcut"
defaultMessage="Esc"
description="Keyboard shortcut hint for clearing the active MSA selection."
/>
</span>
</section>
);
}
type MsaToolbarProps = {
anchorRowId: string | null;
alignmentColumnJump: string;
analysisScope: MsaAnalysisScope;
colorMode: MsaColorMode;
document: MsaDocument;
motifQuery: string;
onAnchorRowIdChange: (value: string) => void;
onAlignmentColumnJumpChange: (value: string) => void;
onAnalysisScopeChange: (value: MsaAnalysisScope) => void;
onColorModeChange: (value: MsaColorMode) => void;
onInterpretationChange: (value: MsaMoleculeType) => void;
onJumpToAlignmentColumn: () => void;
onJumpToReferencePosition: () => void;
onMotifQueryChange: (value: string) => void;
onNextHit: () => void;
onPreviousHit: () => void;
onReferenceModeChange: (value: MsaReferenceMode) => void;
onReferencePositionJumpChange: (value: string) => void;
onResetView: () => void;
onResiduePaletteChange: (value: MsaResiduePalette | null) => void;
onRowFilterChange: (value: string) => void;
onSearchScopeChange: (value: MsaSearchScope) => void;
onShowAnnotationTracksChange: (value: boolean) => void;
onShowIdenticalAsDotsChange: (value: boolean) => void;
onShowRnaStructureOverlaysChange: (value: boolean) => void;
onWindowBack: () => void;
onWindowForward: () => void;
onZoomIn: () => void;
onZoomOut: () => void;
referenceMode: MsaReferenceMode;
referencePositionJump: string;
referenceSequence: string | null;
residuePalette: MsaResiduePalette | null;
rowFilter: string;
searchScope: MsaSearchScope;
selectedHit: MsaMotifSearchHit | null;
selectedHitIndex: number;
showAnnotationTracks: boolean;
showIdenticalAsDots: boolean;
showRnaStructureOverlays: boolean;
totalHits: number;
};
function MsaToolbar({
anchorRowId,
alignmentColumnJump,
analysisScope,
colorMode,
document,
motifQuery,
onAnchorRowIdChange,
onAlignmentColumnJumpChange,
onAnalysisScopeChange,
onColorModeChange,
onInterpretationChange,
onJumpToAlignmentColumn,
onJumpToReferencePosition,
onMotifQueryChange,
onNextHit,
onPreviousHit,
onReferenceModeChange,
onReferencePositionJumpChange,
onResetView,
onResiduePaletteChange,
onRowFilterChange,
onSearchScopeChange,
onShowAnnotationTracksChange,
onShowIdenticalAsDotsChange,
onShowRnaStructureOverlaysChange,
onWindowBack,
onWindowForward,
onZoomIn,
onZoomOut,
referenceMode,
referencePositionJump,
referenceSequence,
residuePalette,
rowFilter,
searchScope,
selectedHit,
selectedHitIndex,
showAnnotationTracks,
showIdenticalAsDots,
showRnaStructureOverlays,
totalHits,
section,
}: MsaToolbarProps & { section: "display" | "primary" }): React.ReactElement {
const intl = useIntl();
const hasAnnotationTracks = document.annotations.length > 0;
const compatiblePalettes = getCompatibleResiduePalettes(
document.displayInterpretation.moleculeType,
);
const nucleicAcidInterpretation =
document.displayInterpretation.moleculeType === "dna" ||
document.displayInterpretation.moleculeType === "rna" ||
document.displayInterpretation.moleculeType === "nucleic-acid-ambiguous";
if (section === "primary") {
return (
<div className="bio-primary-toolbar flex flex-wrap items-center gap-2 border-b border-token-border bg-token-main-surface-primary px-3 py-2 text-xs text-token-text-secondary">
<div
aria-label="Motif search"
className="flex min-w-0 flex-wrap items-center gap-1.5"
role="group"
>
<ToolbarInput
ariaLabel={intl.formatMessage({
id: "codex.filePreview.msa.searchMotifAria",
defaultMessage: "Search MSA motif",
description: "Accessible label for searching an MSA motif.",
})}
onChange={onMotifQueryChange}
onSubmit={onNextHit}
placeholder={
document.searchCapabilities.supportsReverseComplement
? intl.formatMessage({
id: "codex.filePreview.msa.searchMotifBothStrands",
defaultMessage: "Search motif (both strands)",
description:
"Placeholder text for nucleic-acid motif search with reverse-complement matching.",
})
: intl.formatMessage({
id: "codex.filePreview.msa.searchMotif",
defaultMessage: "Search motif",
description:
"Placeholder text for motif search in an MSA viewer.",
})
}
value={motifQuery}
/>
<Button color="outline" size="toolbar" onClick={onPreviousHit}>
<FormattedMessage
id="codex.filePreview.msa.previousHit"
defaultMessage="Prev"
description="Button label for moving to the previous MSA motif-search hit."
/>
</Button>
<Button color="outline" size="toolbar" onClick={onNextHit}>
<FormattedMessage
id="codex.filePreview.msa.nextHit"
defaultMessage="Next"
description="Button label for moving to the next MSA motif-search hit."
/>
</Button>
{totalHits > 0 ? (
<span className="self-center font-medium text-token-text-primary">
<FormattedMessage
id="codex.filePreview.msa.hitSummary"
defaultMessage="{current}/{total} · {orientation}"
description="Summary of the selected MSA motif-search hit."
values={{
current: selectedHitIndex + 1,
orientation: selectedHit?.orientation ?? "",
total: totalHits,
}}
/>
</span>
) : null}
</div>
<div
aria-label="Alignment navigation"
className="flex min-w-0 flex-wrap items-center gap-1.5"
role="group"
>
<ToolbarInput
ariaLabel={intl.formatMessage({
id: "codex.filePreview.msa.filterRowsAria",
defaultMessage: "Filter MSA rows",
description:
"Accessible label for filtering visible MSA sequence rows.",
})}
onChange={onRowFilterChange}
placeholder={intl.formatMessage({
id: "codex.filePreview.msa.filterRowsPlaceholder",
defaultMessage: "Filter rows",
description:
"Placeholder text for filtering visible MSA sequence rows.",
})}
value={rowFilter}
/>
<ToolbarInput
ariaLabel={intl.formatMessage({
id: "codex.filePreview.msa.jumpAlignmentColumn",
defaultMessage: "Jump to MSA alignment column",
description:
"Accessible label for entering an alignment column to jump to in the MSA viewer.",
})}
onChange={onAlignmentColumnJumpChange}
onSubmit={onJumpToAlignmentColumn}
placeholder={intl.formatMessage({
id: "codex.filePreview.msa.jumpAlignmentColumnPlaceholder",
defaultMessage: "Alignment column",
description:
"Placeholder for the MSA alignment-column jump input.",
})}
value={alignmentColumnJump}
/>
<Button
color="outline"
size="toolbar"
onClick={onJumpToAlignmentColumn}
>
<FormattedMessage
id="codex.filePreview.msa.jumpAlignmentColumnButton"
defaultMessage="Go to column"
description="Button label for jumping to a chosen MSA alignment column."
/>
</Button>
<Button color="outline" size="toolbar" onClick={onZoomIn}>
<FormattedMessage
id="codex.filePreview.msa.zoomIn"
defaultMessage="Zoom in"
description="Button label for showing fewer MSA columns with larger visual cells."
/>
</Button>
<Button color="outline" size="toolbar" onClick={onZoomOut}>
<FormattedMessage
id="codex.filePreview.msa.zoomOut"
defaultMessage="Zoom out"
description="Button label for showing more MSA columns with denser visual cells."
/>
</Button>
</div>
</div>
);
}
return (
<div className="bio-inspector-controls space-y-4 text-xs text-token-text-secondary">
<div className="grid gap-3">
{referenceSequence == null ? null : (
<>
<ToolbarInput
ariaLabel={intl.formatMessage({
id: "codex.filePreview.msa.jumpReferenceCoordinate",
defaultMessage: "Jump to active reference coordinate",
description:
"Accessible label for entering an ungapped active-reference position to jump to.",
})}
onChange={onReferencePositionJumpChange}
onSubmit={onJumpToReferencePosition}
placeholder={intl.formatMessage({
id: "codex.filePreview.msa.jumpReferenceCoordinatePlaceholder",
defaultMessage: "Reference position",
description:
"Placeholder for the active-reference position jump input.",
})}
value={referencePositionJump}
/>
<Button
color="outline"
size="toolbar"
onClick={onJumpToReferencePosition}
>
<FormattedMessage
id="codex.filePreview.msa.jumpReferenceCoordinateButton"
defaultMessage="Go to reference"
description="Button label for jumping to an active-reference coordinate."
/>
</Button>
</>
)}
<ToolbarSelect
label="Reference"
ariaLabel={intl.formatMessage({
id: "codex.filePreview.msa.referenceMode",
defaultMessage: "MSA reference mode",
description:
"Accessible label for selecting an MSA reference mode.",
})}
onChange={(event) =>
onReferenceModeChange(event.target.value as MsaReferenceMode)
}
value={referenceMode}
>
<option value="none">
{intl.formatMessage({
id: "codex.filePreview.msa.reference.none",
defaultMessage: "No reference",
description:
"MSA reference-mode option that disables the reference row.",
})}
</option>
<option value="consensus">
{document.displayInterpretation.moleculeType === "protein"
? intl.formatMessage({
id: "codex.filePreview.msa.reference.proteinRepresentative",
defaultMessage: "Representative row",
description:
"MSA reference-mode option that shows a majority-residue representative row for proteins.",
})
: intl.formatMessage({
id: "codex.filePreview.msa.reference.consensus",
defaultMessage: "Consensus",
description:
"MSA reference-mode option that shows a nucleotide consensus sequence.",
})}
</option>
<option value="anchor">
{intl.formatMessage({
id: "codex.filePreview.msa.reference.anchor",
defaultMessage: "Anchor",
description:
"MSA reference-mode option that uses a selected sequence row.",
})}
</option>
</ToolbarSelect>
{referenceMode === "anchor" ? (
<ToolbarSelect
label="Anchor sequence"
ariaLabel={intl.formatMessage({
id: "codex.filePreview.msa.anchorRow",
defaultMessage: "MSA anchor row",
description:
"Accessible label for selecting an MSA anchor sequence.",
})}
onChange={(event) => onAnchorRowIdChange(event.target.value)}
value={anchorRowId ?? ""}
>
{document.rows.map((row) => (
<option key={row.id} value={row.id}>
{row.label}
</option>
))}
</ToolbarSelect>
) : null}
<ToolbarSelect
label="Analyze"
ariaLabel={intl.formatMessage({
id: "codex.filePreview.msa.analysisScope",
defaultMessage: "MSA analysis scope",
description:
"Accessible label for selecting which rows drive MSA analysis summaries.",
})}
onChange={(event) =>
onAnalysisScopeChange(event.target.value as MsaAnalysisScope)
}
value={analysisScope}
>
<option value="all-unhidden-rows">
{intl.formatMessage({
id: "codex.filePreview.msa.analysisScope.all",
defaultMessage: "Analyze all shown-capable rows",
description:
"MSA analysis scope option that uses all rows that have not been explicitly hidden.",
})}
</option>
<option value="currently-displayed-rows">
{intl.formatMessage({
id: "codex.filePreview.msa.analysisScope.displayed",
defaultMessage: "Analyze filtered display",
description:
"MSA analysis scope option that uses only the rows currently displayed by filters.",
})}
</option>
</ToolbarSelect>
<ToolbarSelect
label="Color by"
ariaLabel={intl.formatMessage({
id: "codex.filePreview.msa.colorMode",
defaultMessage: "MSA color mode",
description: "Accessible label for selecting an MSA color mode.",
})}
onChange={(event) =>
onColorModeChange(event.target.value as MsaColorMode)
}
value={colorMode}
>
<option value="residue">
{intl.formatMessage({
id: "codex.filePreview.msa.colorMode.residue",
defaultMessage: "Residue colors",
description:
"MSA color-mode option that colors cells by the active residue palette.",
})}
</option>
<option value="identity">
{intl.formatMessage({
id: "codex.filePreview.msa.colorMode.identity",
defaultMessage: "Identity",
description:
"MSA color-mode option that colors by per-column modal identity.",
})}
</option>
{nucleicAcidInterpretation ? (
<option value="nucleotide-substitution">
{intl.formatMessage({
id: "codex.filePreview.msa.colorMode.nucleotideSubstitution",
defaultMessage: "Transition / transversion",
description:
"MSA color-mode option that distinguishes transition and transversion substitutions from the active reference.",
})}
</option>
) : null}
{document.cdsContext.applicability === "eligible" ? (
<option value="coding-impact">
{intl.formatMessage({
id: "codex.filePreview.msa.colorMode.codingImpact",
defaultMessage: "Synonymous / nonsynonymous",
description:
"MSA color-mode option that distinguishes coding DNA synonymous and nonsynonymous codon differences from the active reference.",
})}
</option>
) : null}
{document.displayInterpretation.moleculeType === "protein" ? (
<>
<option value="protein-conservation">
{intl.formatMessage({
id: "codex.filePreview.msa.colorMode.proteinConservation",
defaultMessage: "Protein conservation",
description:
"MSA color-mode option that colors by protein relative-entropy conservation.",
})}
</option>
<option value="protein-similarity">
{intl.formatMessage({
id: "codex.filePreview.msa.colorMode.proteinSimilarity",
defaultMessage: "Protein similarity",
description:
"MSA color-mode option that colors by BLOSUM62 protein similarity.",
})}
</option>
</>
) : null}
<option value="difference">
{intl.formatMessage({
id: "codex.filePreview.msa.colorMode.differences",
defaultMessage: "Differences",
description:
"MSA color-mode option that highlights differences from the reference.",
})}
</option>
</ToolbarSelect>
{colorMode === "residue" && compatiblePalettes.length > 0 ? (
<ToolbarSelect
label="Residue palette"
ariaLabel={intl.formatMessage({
id: "codex.filePreview.msa.residuePalette",
defaultMessage: "MSA residue palette",
description:
"Accessible label for selecting a modality-specific MSA residue palette.",
})}
onChange={(event) =>
onResiduePaletteChange(event.target.value as MsaResiduePalette)
}
value={residuePalette ?? compatiblePalettes[0]}
>
{compatiblePalettes.map((palette) => (
<option key={palette} value={palette}>
{formatResiduePaletteLabel(intl, palette)}
</option>
))}
</ToolbarSelect>
) : null}
<ToolbarSelect
label="Interpret as"
ariaLabel={intl.formatMessage({
id: "codex.filePreview.msa.interpretation",
defaultMessage: "MSA molecule interpretation",
description:
"Accessible label for overriding the detected MSA molecule interpretation.",
})}
onChange={(event) =>
onInterpretationChange(event.target.value as MsaMoleculeType)
}
value={document.displayInterpretation.moleculeType}
>
<option value="dna">
{intl.formatMessage({
id: "codex.filePreview.msa.interpretation.dna",
defaultMessage: "DNA",
description:
"MSA molecule-interpretation option for DNA alignments.",
})}
</option>
<option value="rna">
{intl.formatMessage({
id: "codex.filePreview.msa.interpretation.rna",
defaultMessage: "RNA",
description:
"MSA molecule-interpretation option for RNA alignments.",
})}
</option>
<option value="protein">
{intl.formatMessage({
id: "codex.filePreview.msa.interpretation.protein",
defaultMessage: "Protein",
description:
"MSA molecule-interpretation option for protein alignments.",
})}
</option>
<option value="nucleic-acid-ambiguous">
{intl.formatMessage({
id: "codex.filePreview.msa.interpretation.ambiguous",
defaultMessage: "DNA/RNA ambiguous",
description:
"MSA molecule-interpretation option for ambiguous nucleic-acid alignments.",
})}
</option>
<option value="unknown">
{intl.formatMessage({
id: "codex.filePreview.msa.interpretation.unknown",
defaultMessage: "Unknown",
description:
"MSA molecule-interpretation option for unclassified alignments.",
})}
</option>
</ToolbarSelect>
<ToolbarCheckbox
checked={showIdenticalAsDots}
label={intl.formatMessage({
id: "codex.filePreview.msa.dotsForMatches",
defaultMessage: "Dots for matches",
description:
"MSA toolbar checkbox label for rendering identical reference matches as dots.",
})}
onChange={onShowIdenticalAsDotsChange}
/>
<ToolbarCheckbox
checked={showAnnotationTracks}
disabled={!hasAnnotationTracks}
label={
hasAnnotationTracks
? intl.formatMessage({
id: "codex.filePreview.msa.annotations",
defaultMessage: "Annotations",
description:
"MSA toolbar checkbox label for showing annotation tracks.",
})
: intl.formatMessage({
id: "codex.filePreview.msa.annotationsUnavailable",
defaultMessage: "Annotations unavailable",
description:
"MSA toolbar checkbox label indicating that the current file has no annotation tracks.",
})
}
onChange={onShowAnnotationTracksChange}
/>
<ToolbarCheckbox
checked={showRnaStructureOverlays}
disabled={document.rnaStructure == null}
label={
document.rnaStructure == null
? intl.formatMessage({
id: "codex.filePreview.msa.rnaOverlaysUnavailable",
defaultMessage: "RNA overlays unavailable",
description:
"MSA toolbar checkbox label indicating that the file has no RNA pair overlays.",
})
: intl.formatMessage({
id: "codex.filePreview.msa.rnaOverlays",
defaultMessage: "RNA structure overlays",
description:
"MSA toolbar checkbox label for showing RNA structure overlays.",
})
}
onChange={onShowRnaStructureOverlaysChange}
/>
<ToolbarSelect
label="Search in"
ariaLabel={intl.formatMessage({
id: "codex.filePreview.msa.searchScope",
defaultMessage: "MSA motif search scope",
description:
"Accessible label for selecting which rows are searched for motifs.",
})}
onChange={(event) =>
onSearchScopeChange(event.target.value as MsaSearchScope)
}
value={searchScope}
>
<option value="currently-displayed-rows">
{intl.formatMessage({
id: "codex.filePreview.msa.searchScope.displayed",
defaultMessage: "Search displayed rows",
description:
"MSA search scope option that searches only filtered/displayed rows.",
})}
</option>
<option value="all-unhidden-rows">
{intl.formatMessage({
id: "codex.filePreview.msa.searchScope.all",
defaultMessage: "Search all unhidden rows",
description:
"MSA search scope option that searches every row that has not been explicitly hidden.",
})}
</option>
</ToolbarSelect>
</div>
<div className="border-t border-token-border pt-3">
<h3 className="mb-2 font-medium text-token-text-primary">Navigation</h3>
<div className="flex flex-wrap items-center gap-2">
<Button color="outline" size="toolbar" onClick={onResetView}>
<FormattedMessage
id="codex.filePreview.msa.resetView"
defaultMessage="Reset view"
description="Button label for resetting the MSA viewer view state."
/>
</Button>
<Button color="outline" size="toolbar" onClick={onWindowBack}>
<FormattedMessage
id="codex.filePreview.msa.previousColumns"
defaultMessage="Previous columns"
description="Button label for moving the visible MSA window to earlier alignment columns."
/>
</Button>
<Button color="outline" size="toolbar" onClick={onWindowForward}>
<FormattedMessage
id="codex.filePreview.msa.nextColumns"
defaultMessage="Next columns"
description="Button label for moving the visible MSA window to later alignment columns."
/>
</Button>
</div>
</div>
</div>
);
}
function ToolbarSelect({
ariaLabel,
children,
label,
onChange,
value,
}: {
ariaLabel: string;
children: ReactNode;
label?: string;
onChange: (event: ChangeEvent<HTMLSelectElement>) => void;
value: string;
}): React.ReactElement {
return (
<label className="grid min-w-0 gap-1.5">
<span className="font-medium text-token-text-secondary">
{label ?? ariaLabel}
</span>
<select
aria-label={ariaLabel}
className="h-8 min-w-0 max-w-full rounded-md border border-token-border bg-token-input-background px-2 text-xs text-token-text-primary outline-none focus:border-token-focus-border"
onChange={onChange}
value={value}
>
{children}
</select>
</label>
);
}
function ToolbarInput({
ariaLabel,
onChange,
onSubmit,
placeholder,
value,
}: {
ariaLabel: string;
onChange: (value: string) => void;
onSubmit?: () => void;
placeholder: string;
value: string;
}): React.ReactElement {
return (
<input
aria-label={ariaLabel}
className="h-7 min-w-36 rounded-md border border-token-border bg-token-input-background px-2 text-xs text-token-text-primary outline-none placeholder:text-token-input-placeholder-foreground focus:border-token-focus-border"
onChange={(event) => onChange(event.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter" && onSubmit != null) {
event.preventDefault();
onSubmit();
}
}}
placeholder={placeholder}
value={value}
/>
);
}
function ToolbarCheckbox({
checked,
disabled = false,
label,
onChange,
}: {
checked: boolean;
disabled?: boolean;
label: string;
onChange: (value: boolean) => void;
}): React.ReactElement {
return (
<label
className={clsx(
"inline-flex items-center gap-1.5 rounded-md border border-token-border px-2 text-xs",
disabled
? "cursor-not-allowed text-token-text-tertiary"
: "text-token-text-primary",
)}
>
<input
checked={checked}
disabled={disabled}
onChange={(event) => onChange(event.target.checked)}
type="checkbox"
/>
{label}
</label>
);
}
function MsaStatusFooter({
document,
focusedCell,
selectedHit,
visibleRange,
visibleRowCount,
}: {
document: MsaDocument;
focusedCell: FocusedMsaCell;
selectedHit: MsaMotifSearchHit | null;
visibleRange: { end: number; start: number };
visibleRowCount: number;
}): React.ReactElement {
return (
<div className="flex flex-wrap gap-x-4 gap-y-1 border-t border-token-border px-3 py-2 text-xs text-token-text-secondary">
<span>
<FormattedMessage
id="codex.filePreview.msa.visibleColumns"
defaultMessage="Visible columns {start, number}-{end, number}"
description="MSA footer summary of the currently visible alignment-column range."
values={{ end: visibleRange.end, start: visibleRange.start + 1 }}
/>
</span>
<span>
<FormattedMessage
id="codex.filePreview.msa.visibleRows"
defaultMessage="{count, number} visible rows"
description="MSA footer summary of how many sequence rows are currently visible."
values={{ count: visibleRowCount }}
/>
</span>
<span>
<FormattedMessage
id="codex.filePreview.msa.parseWarnings"
defaultMessage="{count, number} parse warnings"
description="MSA footer summary of parser warning count."
values={{ count: document.warnings.length }}
/>
</span>
<span>
<FormattedMessage
id="codex.filePreview.msa.cdsMode"
defaultMessage="CDS mode: {mode}"
description="MSA footer summary of whether coding-sequence helpers are applicable."
values={{ mode: document.cdsContext.applicability }}
/>
</span>
<span className="text-token-text-tertiary">
Arrow, Home/End, and Page keys navigate; hold Shift to select columns.
</span>
{selectedHit == null ? null : (
<span>
<FormattedMessage
id="codex.filePreview.msa.matchSummary"
defaultMessage="Match {row}:{start, number}-{end, number} ({orientation})"
description="MSA footer summary of the selected motif-search match."
values={{
end: selectedHit.alignmentEndColumn + 1,
orientation: selectedHit.orientation,
row: selectedHit.rowLabel,
start: selectedHit.alignmentStartColumn + 1,
}}
/>
</span>
)}
{focusedCell == null ? null : (
<span className="ml-auto font-medium text-token-text-primary">
<FormattedMessage
id="codex.filePreview.msa.focusedCellSummary"
defaultMessage="{label} · col {column, number} · {symbol} · {residueClass}"
description="MSA footer summary for the matrix cell currently under the cursor or keyboard focus."
values={{
column: focusedCell.column + 1,
label: focusedCell.row.label,
residueClass: classifyResidue(
focusedCell.symbol,
document.displayInterpretation.moleculeType,
),
symbol: focusedCell.symbol,
}}
/>
</span>
)}
</div>
);
}
function MsaPreviewShell({
children,
className,
filePath,
showFileHeader = true,
}: {
children: ReactNode;
className?: string;
filePath?: string;
showFileHeader?: boolean;
}): React.ReactElement {
return (
<div
className={clsx(
"flex h-full min-h-0 w-full min-w-0 max-w-full flex-col overflow-x-hidden bg-token-main-surface-primary",
className,
)}
>
{showFileHeader && filePath != null ? (
<div className="border-b border-token-border px-3 py-2 text-sm font-medium text-token-text-primary">
{basenameFromPath(filePath)}
</div>
) : null}
{children}
</div>
);
}
function basenameFromPath(path: string): string {
const slashIndex = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
return slashIndex >= 0 ? path.slice(slashIndex + 1) : path;
}
function formatMoleculeLabel(moleculeType: MsaMoleculeType): string {
switch (moleculeType) {
case "dna":
return "DNA";
case "mixed":
return "Mixed";
case "nucleic-acid-ambiguous":
return "DNA/RNA ambiguous";
case "protein":
return "Protein";
case "rna":
return "RNA";
case "unknown":
return "Unknown";
}
}
SHA-256: d7c9d7c4058d75072720476575c97ba169f70f0ab552980fe20f8b225f121f5b