← Files Biological Sequence & Alignment ViewerARCHIVED FILE
src/msa/search-results-panel.tsx
7.56 KB · Sep 30, 2026 · 23:01 UTC
import clsx from "clsx";
import { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { WorkbenchDisclosure } from "../ui/workbench-disclosure";
import { getUngappedPosition } from "./coordinate-map";
import { SEQUENCE_VIEWER_LIMITS } from "../runtime-contract";
import type { MsaMotifSearchHit } from "./search";
export function MsaSearchResultsPanel({
hits,
isPending,
onSelectHit,
query,
referenceSequence,
selectedHitIndex,
truncated = false,
}: {
hits: Array<MsaMotifSearchHit>;
isPending: boolean;
onSelectHit: (index: number) => void;
query: string;
referenceSequence: string | null;
selectedHitIndex: number;
truncated?: boolean;
}): React.ReactElement | null {
const intl = useIntl();
const [page, setPage] = useState(0);
const pageSize = SEQUENCE_VIEWER_LIMITS.ui.searchHitPageSize;
const pageCount = Math.max(1, Math.ceil(hits.length / pageSize));
useEffect(() => {
setPage(Math.min(pageCount - 1, Math.floor(selectedHitIndex / pageSize)));
}, [pageCount, selectedHitIndex]);
const trimmedQuery = query.trim();
if (trimmedQuery.length === 0) {
return null;
}
return (
<section className="border-b border-token-border bg-token-main-surface-primary px-3 py-2 text-xs text-token-text-secondary">
<WorkbenchDisclosure
defaultExpanded
id="alignment.search-results"
label={intl.formatMessage(
{
id: "codex.filePreview.msa.searchResultsSummary",
defaultMessage: "Motif search results · {count, number} hits",
description: "Summary label for the MSA motif search result panel.",
},
{ count: hits.length },
)}
summaryClassName="cursor-interaction font-medium text-token-text-primary select-none"
>
<div className="mt-2">
{truncated ? (
<div className="mb-2 rounded-md bg-amber-500/10 px-2 py-1 text-amber-700 dark:text-amber-300">
Results reached the bounded search limit. Refine the motif to
inspect additional matches.
</div>
) : null}
{isPending ? (
<div className="bg-token-main-surface-secondary rounded-md px-2 py-1 text-token-text-tertiary">
<FormattedMessage
id="codex.filePreview.msa.searchResultsPending"
defaultMessage="Processing motif matches…"
description="Pending label in the MSA motif search result panel."
/>
</div>
) : hits.length === 0 ? (
<div className="bg-token-main-surface-secondary rounded-md px-2 py-1 text-token-text-tertiary">
<FormattedMessage
id="codex.filePreview.msa.searchResultsEmpty"
defaultMessage="No rows matched “{query}”."
description="Empty-state label in the MSA motif search result panel."
values={{ query: trimmedQuery }}
/>
</div>
) : (
<div className="max-h-44 overflow-auto rounded-md border border-token-border">
{hits
.slice(page * pageSize, (page + 1) * pageSize)
.map((hit, pageIndex) => {
const index = page * pageSize + pageIndex;
return (
<SearchResultButton
hit={hit}
index={index}
isSelected={index === selectedHitIndex}
key={`${hit.rowId}:${hit.alignmentStartColumn}:${hit.alignmentEndColumn}:${hit.orientation}`}
onSelectHit={onSelectHit}
referenceSequence={referenceSequence}
/>
);
})}
</div>
)}
{pageCount <= 1 || isPending ? null : (
<div className="mt-2 flex items-center justify-end gap-2">
<button
disabled={page === 0}
onClick={() => setPage((value) => Math.max(0, value - 1))}
type="button"
>
Previous
</button>
<span>
{page + 1}/{pageCount}
</span>
<button
disabled={page >= pageCount - 1}
onClick={() =>
setPage((value) => Math.min(pageCount - 1, value + 1))
}
type="button"
>
Next
</button>
</div>
)}
</div>
</WorkbenchDisclosure>
</section>
);
}
function SearchResultButton({
hit,
index,
isSelected,
onSelectHit,
referenceSequence,
}: {
hit: MsaMotifSearchHit;
index: number;
isSelected: boolean;
onSelectHit: (index: number) => void;
referenceSequence: string | null;
}): React.ReactElement {
const intl = useIntl();
const referenceStart =
referenceSequence == null
? null
: getUngappedPosition(referenceSequence, hit.alignmentStartColumn);
const referenceEnd =
referenceSequence == null
? null
: getUngappedPosition(referenceSequence, hit.alignmentEndColumn);
return (
<button
aria-label={intl.formatMessage(
{
id: "codex.filePreview.msa.searchResultButton",
defaultMessage:
"Select motif hit {row} alignment columns {start} to {end}",
description:
"Accessible label for selecting one MSA motif search result.",
},
{
end: hit.alignmentEndColumn + 1,
row: hit.rowLabel,
start: hit.alignmentStartColumn + 1,
},
)}
className={clsx(
"grid w-full cursor-interaction grid-cols-[minmax(0,1.4fr)_auto_auto_auto_auto] gap-3 border-b border-token-border px-2 py-1 text-left last:border-b-0",
isSelected
? "bg-token-main-surface-secondary text-token-text-primary"
: "hover:bg-token-main-surface-secondary",
)}
onClick={() => onSelectHit(index)}
type="button"
>
<span className="truncate font-medium">{hit.rowLabel}</span>
<span>
<FormattedMessage
id="codex.filePreview.msa.searchResultColumns"
defaultMessage="cols {start, number}-{end, number}"
description="Alignment-column range label for one MSA search hit."
values={{
end: hit.alignmentEndColumn + 1,
start: hit.alignmentStartColumn + 1,
}}
/>
</span>
<span>
<FormattedMessage
id="codex.filePreview.msa.searchResultUngapped"
defaultMessage="ungapped {start, number}-{end, number}"
description="Ungapped row-coordinate range label for one MSA search hit."
values={{
end: hit.ungappedEndPosition,
start: hit.ungappedStartPosition,
}}
/>
</span>
<span>{hit.orientation}</span>
<span>
{referenceStart == null || referenceEnd == null ? (
<FormattedMessage
id="codex.filePreview.msa.searchResultReferenceUnavailable"
defaultMessage="reference —"
description="Reference-coordinate fallback label for one MSA search hit."
/>
) : (
<FormattedMessage
id="codex.filePreview.msa.searchResultReference"
defaultMessage="reference {start, number}-{end, number}"
description="Active-reference coordinate range label for one MSA search hit."
values={{
end: referenceEnd,
start: referenceStart,
}}
/>
)}
</span>
</button>
);
}
SHA-256: 18df3ef095920fd5a016f792d874af62b33ab2edc9c7948995177e0a9e366f4e