← Files Biological Sequence & Alignment ViewerARCHIVED FILE

src/sequence/sequence-workbench-panel.tsx

37.4 KB · Sep 30, 2026 · 23:01 UTC

↓ Download file

import { useMemo, useState, type ChangeEvent, type ReactNode } from "react";

import { WorkbenchTools } from "../ui/workbench-tools";
import { WorkbenchDisclosure } from "../ui/workbench-disclosure";

import type {
  SequenceViewerAnalysisRequest,
  SequenceViewerEditRequest,
} from "../viewer-operations";
import type {
  PreparedSequenceWorkspaceArtifact,
  SequenceWorkspaceArtifactPublisher,
} from "../views/workbench-persistence";
import type { SequenceWorkspaceSessionClient } from "../views/workspace-sessions";
import type { SequenceWorkspaceTrackBrowserClient } from "../views/workspace-tracks";
import type {
  SequenceWorkbenchState,
  WorkbenchArtifact,
} from "../workbench-state";
import { WorkspacePublishButton } from "../workspace-publish-button";
import { WorkspaceSessionSaveButton } from "../workspace-session-controls";
import { getGeneticCode, SUPPORTED_GENETIC_CODE_IDS } from "./genetic-code";
import { getSelectionLength, getSelectionSegments } from "./selection";
import type { SequenceTrackFormat } from "./tracks";
import type {
  SequenceFeature,
  SequenceRecord,
  SequenceSelection,
} from "./types";
import { WorkspaceTrackBrowserButton } from "./workspace-track-browser-dialog";

type WorkbenchTab = "analyze" | "edit" | "export" | "history" | "tracks";

export function SequenceWorkbenchPanel({
  browseWorkspaceTracks,
  contextPanels = [],
  geneticCodeId,
  onAddAnnotation,
  onAlignRecords,
  onCancelJob,
  onEdit,
  onExport,
  onGeneticCodeChange,
  onDeleteAnnotation,
  onImportTrack,
  onLoadTrack,
  onRedo,
  onRemoveTrack,
  onRunAnalysis,
  onRestoreSession,
  onSaveSession,
  prepareWorkspaceSession,
  publishWorkspaceArtifact,
  onSelectFeature,
  onUndo,
  onUpdateAnnotation,
  record,
  recordCount,
  selection,
  selectedFeature,
  state,
  workspaceSessionName,
  workspaceSessions,
}: {
  browseWorkspaceTracks?: SequenceWorkspaceTrackBrowserClient;
  contextPanels?: Array<{ id: string; label: string; content: ReactNode }>;
  geneticCodeId: number;
  onAddAnnotation: (input: { label: string; type: string }) => void;
  onAlignRecords: () => void;
  onCancelJob: (jobId: string) => void;
  onEdit: (request: SequenceViewerEditRequest) => void;
  onExport: (
    format:
      | "bed"
      | "csv"
      | "embl"
      | "fasta"
      | "fastq"
      | "genbank"
      | "gff3"
      | "gtf"
      | "json"
      | "pdf"
      | "svg"
      | "tsv"
      | "vcf",
    scope: "all" | "selection" | "visible",
  ) => void;
  onGeneticCodeChange: (id: number) => void;
  onDeleteAnnotation: (featureId: string) => void;
  onImportTrack: (trackId: string) => void;
  onLoadTrack: (input: {
    bytes: Uint8Array;
    displayName: string;
    format: SequenceTrackFormat;
    indexBytes?: Uint8Array;
    indexFormat?: "bai" | "csi";
    referenceContents?: string;
    referenceFileName?: string;
  }) => void;
  onRedo: () => void;
  onRemoveTrack: (trackId: string) => void;
  onRunAnalysis: (request: SequenceViewerAnalysisRequest) => void;
  onRestoreSession: (content: string) => void;
  onSaveSession: () => void;
  prepareWorkspaceSession?: () => string;
  publishWorkspaceArtifact?: SequenceWorkspaceArtifactPublisher;
  onSelectFeature: (feature: SequenceFeature) => void;
  onUndo: () => void;
  onUpdateAnnotation: (input: {
    feature: SequenceFeature;
    label: string;
    type: string;
  }) => void;
  record: SequenceRecord;
  recordCount: number;
  selection?: SequenceSelection;
  selectedFeature?: SequenceFeature;
  state: SequenceWorkbenchState;
  workspaceSessionName?: string;
  workspaceSessions?: SequenceWorkspaceSessionClient;
}): React.ReactElement {
  const [editSequence, setEditSequence] = useState("");
  const [annotationLabel, setAnnotationLabel] = useState("");
  const [annotationType, setAnnotationType] = useState("misc_feature");
  const [fileError, setFileError] = useState<string>();
  const [featureFilter, setFeatureFilter] = useState("");
  const [includePartialOrfs, setIncludePartialOrfs] = useState(false);
  const [orfMinimumAminoAcids, setOrfMinimumAminoAcids] = useState(30);
  const [orfStrands, setOrfStrands] = useState<"+" | "-" | "both">("both");
  const [primerMaximumPairs, setPrimerMaximumPairs] = useState(10);
  const [primerMaximumProductLength, setPrimerMaximumProductLength] =
    useState(1_500);
  const [primerMinimumProductLength, setPrimerMinimumProductLength] =
    useState(80);
  const [restrictionEnzymes, setRestrictionEnzymes] = useState("");
  const [sessionError, setSessionError] = useState<string>();
  const activeSelection =
    selection?.recordId === record.id ? selection : undefined;
  const runningJobs = state.jobs.filter(({ status }) => status === "running");
  const latestJob = state.jobs.at(-1);
  const filteredFeatures = useMemo(() => {
    const query = featureFilter.trim().toLowerCase();
    if (query.length === 0) return record.features;
    return record.features.filter((feature) =>
      [
        feature.id,
        feature.label,
        feature.type,
        ...Object.entries(feature.qualifiers).flatMap(([key, value]) => [
          key,
          ...(Array.isArray(value) ? value : [value]),
        ]),
      ].some((value) => value?.toLowerCase().includes(query)),
    );
  }, [featureFilter, record.features]);
  const actions = useMemo(
    () =>
      [
        ["analyze", "Analyze"],
        ["edit", state.dirty ? "Edit copy •" : "Edit copy"],
        [
          "tracks",
          `Tracks${state.tracks.length === 0 ? "" : ` ${state.tracks.length}`}`,
        ],
        ["export", "Export"],
        [
          "history",
          `Tasks${runningJobs.length === 0 ? "" : ` ${runningJobs.length}`}`,
        ],
      ] as Array<[WorkbenchTab, string]>,
    [runningJobs.length, state.dirty, state.tracks.length],
  );

  async function loadTrack(
    event: ChangeEvent<HTMLInputElement>,
  ): Promise<void> {
    const files = Array.from(event.target.files ?? []);
    event.target.value = "";
    if (files.length === 0) return;
    try {
      if (
        files.reduce((sum, file) => sum + file.size, 0) >
        16 * 1_024 * 1_024
      ) {
        throw new Error(
          "Interactive track uploads are limited to 16 MiB total.",
        );
      }
      const file = files.find(({ name }) => inferTrackFormat(name) != null);
      if (file == null)
        throw new Error("No supported evidence track was selected.");
      const format = inferTrackFormat(file.name);
      if (format == null) {
        throw new Error(
          "Choose a BED, GFF3, GTF, VCF, SAM, BAM, or CRAM file.",
        );
      }
      setFileError(undefined);
      if (format === "cram") {
        const index = files.find(({ name }) =>
          name.toLowerCase().endsWith(".crai"),
        );
        const reference = files.find(({ name }) => isReferenceFileName(name));
        if (index == null || reference == null) {
          throw new Error(
            "CRAM loading requires the CRAM, its CRAI index, and a matching FASTA reference in the same chooser.",
          );
        }
        onLoadTrack({
          bytes: new Uint8Array(await file.arrayBuffer()),
          displayName: file.name,
          format,
          indexBytes: new Uint8Array(await index.arrayBuffer()),
          referenceContents: await reference.text(),
          referenceFileName: reference.name,
        });
        return;
      }
      if (format === "bam") {
        const index = files.find(({ name }) => /\.(?:bai|csi)$/iu.test(name));
        onLoadTrack({
          bytes: new Uint8Array(await file.arrayBuffer()),
          displayName: file.name,
          format,
          indexBytes:
            index == null
              ? undefined
              : new Uint8Array(await index.arrayBuffer()),
          indexFormat: index?.name.toLowerCase().endsWith(".csi")
            ? "csi"
            : "bai",
        });
        return;
      }
      onLoadTrack({
        bytes: new Uint8Array(await file.arrayBuffer()),
        displayName: file.name,
        format,
      });
    } catch (error) {
      setFileError(
        error instanceof Error
          ? error.message
          : "The track could not be loaded.",
      );
    }
  }

  async function restoreSession(
    event: ChangeEvent<HTMLInputElement>,
  ): Promise<void> {
    const file = event.target.files?.[0];
    event.target.value = "";
    if (file == null) return;
    try {
      if (file.size > 512 * 1_024) {
        throw new Error("Session files are limited to 512 KiB.");
      }
      setSessionError(undefined);
      onRestoreSession(await file.text());
    } catch (error) {
      setSessionError(
        error instanceof Error
          ? error.message
          : "The session could not be restored.",
      );
    }
  }

  const panelContent: Record<WorkbenchTab, ReactNode> = {
    analyze: (
      <div className="space-y-3">
        <label className="flex flex-wrap items-center gap-2 text-sm text-token-text-secondary">
          Genetic code
          <select
            aria-label="Genetic code"
            className={inputClass}
            onChange={(event) =>
              onGeneticCodeChange(Number(event.target.value))
            }
            value={geneticCodeId}
          >
            {SUPPORTED_GENETIC_CODE_IDS.map((id) => (
              <option key={id} value={id}>
                {id} · {getGeneticCode(id)?.name}
              </option>
            ))}
          </select>
        </label>
        <div className="flex flex-wrap gap-2">
          <AnalysisButton
            label="Statistics"
            onClick={() => onRunAnalysis({ analysis: "statistics" })}
          />
          <AnalysisButton
            label="Six-frame translation"
            onClick={() => onRunAnalysis({ analysis: "translate" })}
          />
          <AnalysisButton
            label="Find ORFs"
            onClick={() =>
              onRunAnalysis({
                analysis: "find-orfs",
                includePartial: includePartialOrfs,
                minAminoAcids: orfMinimumAminoAcids,
                strands: orfStrands,
              })
            }
          />
          <AnalysisButton
            label="Restriction digest"
            onClick={() =>
              onRunAnalysis({
                analysis: "restriction-analysis",
                enzymes: parseRestrictionEnzymes(restrictionEnzymes),
              })
            }
          />
          <AnalysisButton
            label="Design primers"
            onClick={() =>
              onRunAnalysis({
                analysis: "design-primers",
                maxPairs: primerMaximumPairs,
                maxProductLength: primerMaximumProductLength,
                minProductLength: primerMinimumProductLength,
              })
            }
          />
          <button
            className={actionClass}
            disabled={recordCount < 2}
            onClick={onAlignRecords}
            type="button"
          >
            Align {recordCount.toLocaleString()} records
          </button>
        </div>
        <p className="text-xs text-token-text-secondary">
          Primer design and built-in alignment are exploratory. Results include
          engine provenance and explicit limitations.
        </p>
        <WorkbenchDisclosure
          className="rounded-md border border-token-border bg-token-main-surface-primary p-2"
          id="sequence.analysis-parameters"
          label="Advanced analysis parameters"
          summaryClassName="cursor-pointer text-sm font-medium text-token-text-primary"
        >
          <div className="mt-3 grid gap-3">
            <fieldset className="space-y-2">
              <legend className="text-sm font-medium text-token-text-primary">
                ORFs
              </legend>
              <label className="flex items-center gap-2 text-xs text-token-text-secondary">
                Minimum amino acids
                <input
                  aria-label="Minimum ORF amino acids"
                  className={inputClass}
                  min="1"
                  onChange={(event) =>
                    setOrfMinimumAminoAcids(
                      Math.max(1, Number(event.target.value)),
                    )
                  }
                  type="number"
                  value={orfMinimumAminoAcids}
                />
              </label>
              <label className="flex items-center gap-2 text-xs text-token-text-secondary">
                Strands
                <select
                  aria-label="ORF strands"
                  className={inputClass}
                  onChange={(event) =>
                    setOrfStrands(event.target.value as "+" | "-" | "both")
                  }
                  value={orfStrands}
                >
                  <option value="both">Both</option>
                  <option value="+">Forward</option>
                  <option value="-">Reverse</option>
                </select>
              </label>
              <label className="flex items-center gap-2 text-xs text-token-text-secondary">
                <input
                  checked={includePartialOrfs}
                  onChange={(event) =>
                    setIncludePartialOrfs(event.target.checked)
                  }
                  type="checkbox"
                />
                Include partial ORFs
              </label>
            </fieldset>
            <fieldset className="space-y-2">
              <legend className="text-sm font-medium text-token-text-primary">
                Primer pairs
              </legend>
              <label className="flex items-center gap-2 text-xs text-token-text-secondary">
                Minimum product
                <input
                  aria-label="Minimum primer product length"
                  className={inputClass}
                  min="20"
                  onChange={(event) =>
                    setPrimerMinimumProductLength(
                      Math.min(
                        primerMaximumProductLength,
                        Math.max(20, Number(event.target.value)),
                      ),
                    )
                  }
                  type="number"
                  value={primerMinimumProductLength}
                />
              </label>
              <label className="flex items-center gap-2 text-xs text-token-text-secondary">
                Maximum product
                <input
                  aria-label="Maximum primer product length"
                  className={inputClass}
                  min="20"
                  onChange={(event) =>
                    setPrimerMaximumProductLength(
                      Math.max(
                        primerMinimumProductLength,
                        Number(event.target.value),
                      ),
                    )
                  }
                  type="number"
                  value={primerMaximumProductLength}
                />
              </label>
              <label className="flex items-center gap-2 text-xs text-token-text-secondary">
                Maximum pairs
                <input
                  aria-label="Maximum primer pairs"
                  className={inputClass}
                  max="50"
                  min="1"
                  onChange={(event) =>
                    setPrimerMaximumPairs(
                      Math.min(50, Math.max(1, Number(event.target.value))),
                    )
                  }
                  type="number"
                  value={primerMaximumPairs}
                />
              </label>
            </fieldset>
            <fieldset className="space-y-2">
              <legend className="text-sm font-medium text-token-text-primary">
                Restriction digest
              </legend>
              <label className="block text-xs text-token-text-secondary">
                Enzymes (comma separated)
                <input
                  aria-label="Restriction enzymes"
                  className={`${inputClass} mt-1 w-full`}
                  onChange={(event) =>
                    setRestrictionEnzymes(event.target.value)
                  }
                  placeholder="EcoRI, BamHI; blank = common set"
                  value={restrictionEnzymes}
                />
              </label>
            </fieldset>
          </div>
        </WorkbenchDisclosure>
      </div>
    ),
    edit: (
      <div className="grid gap-4">
        <p className="text-sm text-token-text-secondary">
          Edits are staged in a reversible copy.
        </p>
        <div className="space-y-2">
          <h3 className="text-sm font-semibold text-token-text-primary">
            Edit selected sequence
          </h3>
          <textarea
            aria-label="Replacement or inserted sequence"
            className={`${inputClass} min-h-16 w-full font-mono`}
            onChange={(event) => setEditSequence(event.target.value)}
            placeholder="Paste bases or residues"
            value={editSequence}
          />
          <div className="flex flex-wrap gap-2">
            <button
              className={actionClass}
              disabled={editSequence.trim().length === 0}
              onClick={() => {
                onEdit({
                  coordinate: activeSelection?.start ?? 1,
                  operation: "insert-sequence",
                  sequence: editSequence,
                });
                setEditSequence("");
              }}
              type="button"
            >
              Insert before {activeSelection?.start ?? 1}
            </button>
            <button
              className={actionClass}
              disabled={
                activeSelection == null ||
                activeSelection.segments != null ||
                editSequence.trim().length === 0
              }
              onClick={() => {
                if (activeSelection == null) return;
                onEdit({
                  end: activeSelection.end,
                  operation: "replace-sequence-range",
                  sequence: editSequence,
                  start: activeSelection.start,
                });
                setEditSequence("");
              }}
              type="button"
            >
              Replace selection
            </button>
            <button
              className={actionClass}
              disabled={
                activeSelection == null || activeSelection.segments != null
              }
              onClick={() => {
                if (activeSelection == null) return;
                onEdit({
                  end: activeSelection.end,
                  operation: "delete-sequence-range",
                  start: activeSelection.start,
                });
              }}
              type="button"
            >
              Delete selection
            </button>
          </div>
          {activeSelection?.segments == null ? null : (
            <p className="text-xs text-token-text-secondary">
              Discontinuous selections can be inspected and exported. Select a
              contiguous range before editing or annotating.
            </p>
          )}
        </div>
        <div className="space-y-2">
          <h3 className="text-sm font-semibold text-token-text-primary">
            Annotate selection
          </h3>
          <div className="flex gap-2">
            <input
              aria-label="Annotation type"
              className={inputClass}
              onChange={(event) => setAnnotationType(event.target.value)}
              value={annotationType}
            />
            <input
              aria-label="Annotation label"
              className={inputClass}
              onChange={(event) => setAnnotationLabel(event.target.value)}
              placeholder="Label"
              value={annotationLabel}
            />
          </div>
          <button
            className={actionClass}
            disabled={
              activeSelection == null ||
              activeSelection.segments != null ||
              annotationLabel.trim().length === 0
            }
            onClick={() => {
              onAddAnnotation({
                label: annotationLabel.trim(),
                type: annotationType.trim() || "misc_feature",
              });
              setAnnotationLabel("");
            }}
            type="button"
          >
            Add annotation
          </button>
          <button
            className={actionClass}
            disabled={
              selectedFeature == null || annotationLabel.trim().length === 0
            }
            onClick={() => {
              if (selectedFeature == null) return;
              onUpdateAnnotation({
                feature: selectedFeature,
                label: annotationLabel.trim(),
                type: annotationType.trim() || selectedFeature.type,
              });
              setAnnotationLabel("");
            }}
            type="button"
          >
            Update selected feature
          </button>
          <div className="flex gap-2 pt-2">
            <button
              className={actionClass}
              disabled={state.history.length === 0}
              onClick={onUndo}
              type="button"
            >
              Undo
            </button>
            <button
              className={actionClass}
              disabled={state.future.length === 0}
              onClick={onRedo}
              type="button"
            >
              Redo
            </button>
          </div>
        </div>
        <div className="space-y-2">
          <div className="flex flex-wrap items-center gap-2">
            <h3 className="text-sm font-semibold text-token-text-primary">
              Features and annotations
            </h3>
            <input
              aria-label="Filter features"
              className={inputClass}
              onChange={(event) => setFeatureFilter(event.target.value)}
              placeholder="Filter type, label, qualifier"
              value={featureFilter}
            />
            <span className="text-xs text-token-text-secondary">
              {filteredFeatures.length.toLocaleString()} of{" "}
              {record.features.length.toLocaleString()}
            </span>
          </div>
          <div className="max-h-44 overflow-auto rounded border border-token-border bg-token-main-surface-primary">
            {filteredFeatures.slice(0, 500).map((feature) => (
              <div
                className={`flex items-center gap-2 border-b border-token-border px-2 py-1.5 text-sm last:border-b-0 ${selectedFeature?.id === feature.id ? "bg-emerald-500/10" : ""}`}
                key={feature.id}
              >
                <button
                  className="min-w-0 flex-1 text-left"
                  onClick={() => onSelectFeature(feature)}
                  type="button"
                >
                  <span className="font-medium text-token-text-primary">
                    {feature.label ?? feature.type}
                  </span>{" "}
                  <span className="text-token-text-secondary">
                    {feature.type} · {feature.start.toLocaleString()}–
                    {feature.end.toLocaleString()} · {feature.strand}
                    {feature.segments != null && feature.segments.length > 1
                      ? ` · ${feature.segments.length.toLocaleString()} exons`
                      : null}
                  </span>
                </button>
                <button
                  aria-label={`Delete ${feature.label ?? feature.type}`}
                  className={actionClass}
                  onClick={() => onDeleteAnnotation(feature.id)}
                  type="button"
                >
                  Delete
                </button>
              </div>
            ))}
            {filteredFeatures.length === 0 ? (
              <p className="p-2 text-sm text-token-text-secondary">
                No features match this filter.
              </p>
            ) : null}
          </div>
          {filteredFeatures.length > 500 ? (
            <p className="text-xs text-token-text-secondary">
              Showing the first 500 matches; use the model query tool to page
              the full feature set.
            </p>
          ) : null}
        </div>
      </div>
    ),
    tracks: (
      <div className="space-y-3">
        <div className="flex flex-wrap gap-2">
          <label className="hover:bg-token-main-surface-secondary inline-flex cursor-pointer items-center rounded-md border border-token-border bg-token-main-surface-primary px-2.5 py-1.5 text-sm font-medium text-token-text-primary">
            Load local evidence track
            <input
              accept=".bai,.bam,.bed,.cram,.crai,.csi,.fa,.fasta,.fna,.gff,.gff3,.gtf,.sam,.vcf"
              className="sr-only"
              multiple
              onChange={(event) => void loadTrack(event)}
              type="file"
            />
          </label>
          <WorkspaceTrackBrowserButton browser={browseWorkspaceTracks} />
        </div>
        {fileError == null ? null : (
          <p role="alert" className="text-sm text-red-500">
            {fileError}
          </p>
        )}
        <div className="grid gap-2">
          {state.tracks.map((track) => (
            <div
              className="rounded border border-token-border bg-token-main-surface-primary p-2"
              key={track.id}
            >
              <div className="flex items-start justify-between gap-2">
                <div>
                  <div className="text-sm font-medium text-token-text-primary">
                    {track.name}
                  </div>
                  <div className="text-xs text-token-text-secondary">
                    {track.format.toUpperCase()} ·{" "}
                    {track.summary.itemCount.toLocaleString()} items · mapping{" "}
                    {track.mapping.status}
                  </div>
                </div>
                <button
                  aria-label={`Remove ${track.name}`}
                  className={actionClass}
                  onClick={() => onRemoveTrack(track.id)}
                  type="button"
                >
                  Remove
                </button>
              </div>
              {track.kind === "annotations" ? (
                <button
                  className={`${actionClass} mt-2`}
                  onClick={() => onImportTrack(track.id)}
                  type="button"
                >
                  Import into editable copy
                </button>
              ) : null}
            </div>
          ))}
          {state.tracks.length === 0 ? (
            <p className="text-sm text-token-text-secondary">
              Load GFF/GTF/BED annotations, VCF variants, SAM reads, BAM +
              BAI/CSI, or CRAM + CRAI + reference FASTA. Indexed read bundles
              load only the first bounded regional window; the model can request
              exact workspace regions with <code>sequence.load_track</code>.
            </p>
          ) : null}
        </div>
      </div>
    ),
    export: (
      <div className="space-y-3">
        <div className="flex flex-wrap gap-2">
          {(
            [
              "fasta",
              "genbank",
              "embl",
              "gff3",
              "gtf",
              "bed",
              "csv",
              "tsv",
              "vcf",
              "svg",
              "pdf",
              "json",
            ] as const
          ).map((format) => (
            <button
              className={actionClass}
              key={format}
              onClick={() =>
                onExport(format, activeSelection == null ? "all" : "selection")
              }
              type="button"
            >
              {format.toUpperCase()}
            </button>
          ))}
          <button className={actionClass} onClick={onSaveSession} type="button">
            Save session
          </button>
          {workspaceSessions == null ||
          prepareWorkspaceSession == null ||
          workspaceSessionName == null ? null : (
            <WorkspaceSessionSaveButton
              className={actionClass}
              client={workspaceSessions}
              defaultName={workspaceSessionName}
              prepare={prepareWorkspaceSession}
            />
          )}
          <label className={actionClass}>
            Restore session
            <input
              accept=".json"
              className="sr-only"
              onChange={(event) => void restoreSession(event)}
              type="file"
            />
          </label>
        </div>
        {sessionError == null ? null : (
          <p className="text-sm text-red-500" role="alert">
            {sessionError}
          </p>
        )}
        <ArtifactList
          artifacts={state.artifacts}
          nativeSourceSafe={
            !state.dirty &&
            state.history.length === 0 &&
            state.tracks.length === 0
          }
          publisher={publishWorkspaceArtifact}
        />
      </div>
    ),
    history: (
      <div className="grid gap-3">
        <div>
          <h3 className="text-sm font-semibold text-token-text-primary">
            Tasks
          </h3>
          <div className="mt-2 space-y-2">
            {state.jobs
              .slice(-10)
              .reverse()
              .map((job) => (
                <div
                  className="rounded border border-token-border bg-token-main-surface-primary p-2 text-sm"
                  key={job.id}
                >
                  <div className="flex items-center justify-between gap-2">
                    <span className="font-medium">{job.kind}</span>
                    <span className="text-token-text-secondary">
                      {job.status}
                    </span>
                  </div>
                  <div className="mt-1 text-xs text-token-text-secondary">
                    {job.message}
                  </div>
                  {job.status === "running" ? (
                    <button
                      className={`${actionClass} mt-2`}
                      onClick={() => onCancelJob(job.id)}
                      type="button"
                    >
                      Cancel
                    </button>
                  ) : null}
                </div>
              ))}
            {state.jobs.length === 0 ? (
              <p className="text-sm text-token-text-secondary">
                No analyses have run yet.
              </p>
            ) : null}
          </div>
        </div>
        <div>
          <h3 className="text-sm font-semibold text-token-text-primary">
            Latest result
          </h3>
          <pre className="mt-2 max-h-48 overflow-auto rounded border border-token-border bg-token-main-surface-primary p-2 text-xs whitespace-pre-wrap text-token-text-secondary">
            {latestJob?.result == null
              ? "No completed result."
              : JSON.stringify(latestJob.result, null, 2).slice(0, 8_000)}
          </pre>
        </div>
      </div>
    ),
  };

  return (
    <section
      aria-label="Sequence workbench"
      className="bio-sequence-workbench min-w-0"
      data-testid="sequence-workbench"
    >
      {activeSelection == null ? null : (
        <div
          aria-label="Selected sequence actions"
          className="flex flex-wrap items-center gap-2 border-b border-token-border px-3 py-3"
        >
          <span className="text-sm font-medium text-token-text-primary">
            {getSelectionSegments(activeSelection)
              .map(
                ({ end, start }) =>
                  `${start.toLocaleString()}–${end.toLocaleString()}`,
              )
              .join(" + ")}{" "}
            selected · {getSelectionLength(activeSelection).toLocaleString()}{" "}
            residues
          </span>
          <button
            className={actionClass}
            disabled={activeSelection.segments != null}
            onClick={() =>
              onRunAnalysis({
                analysis: "translate",
                end: activeSelection.end,
                start: activeSelection.start,
              })
            }
            type="button"
          >
            Translate
          </button>
          <button
            className={actionClass}
            disabled={activeSelection.segments != null}
            onClick={() =>
              onEdit({
                end: activeSelection.end,
                operation: "reverse-complement-range",
                start: activeSelection.start,
              })
            }
            type="button"
          >
            Reverse complement
          </button>
          <button
            className={actionClass}
            onClick={() => onExport("fasta", "selection")}
            type="button"
          >
            Export FASTA
          </button>
        </div>
      )}
      <WorkbenchTools
        group="sequence-tools"
        label="Sequence tools"
        panels={[
          ...contextPanels,
          ...actions.map(([id, label]) => ({
            id,
            label,
            content: panelContent[id],
          })),
        ]}
      />
      {latestJob == null ? null : (
        <div
          className="border-t border-token-border px-3 py-2 text-sm text-token-text-secondary"
          role="status"
        >
          <span className="font-medium text-token-text-primary">
            {latestJob.kind}
          </span>
          {" · "}
          {latestJob.status}
          {latestJob.message == null ? "" : ` · ${latestJob.message}`}
        </div>
      )}
    </section>
  );
}

function AnalysisButton({
  label,
  onClick,
}: {
  label: string;
  onClick: () => void;
}): React.ReactElement {
  return (
    <button className={actionClass} onClick={onClick} type="button">
      {label}
    </button>
  );
}

function ArtifactList({
  artifacts,
  nativeSourceSafe,
  publisher,
}: {
  artifacts: Array<WorkbenchArtifact>;
  nativeSourceSafe: boolean;
  publisher?: SequenceWorkspaceArtifactPublisher;
}): React.ReactElement {
  if (artifacts.length === 0)
    return (
      <p className="text-sm text-token-text-secondary">
        Exports and generated alignments appear here with their provenance.
      </p>
    );
  return (
    <div className="grid gap-2">
      {artifacts
        .slice(-10)
        .reverse()
        .map((artifact) => (
          <div
            className="flex items-center justify-between gap-2 rounded border border-token-border bg-token-main-surface-primary p-2"
            key={artifact.id}
          >
            <div className="min-w-0">
              <div className="truncate text-sm font-medium">
                {artifact.name}
              </div>
              <div className="text-xs text-token-text-secondary">
                {artifact.format} · {artifact.provenance.engine}
              </div>
            </div>
            <div className="flex items-center gap-1">
              {publisher == null ? (
                <span
                  className="text-xs text-token-text-secondary"
                  role="status"
                >
                  Workspace export is unavailable for this viewer.
                </span>
              ) : (
                <WorkspacePublishButton
                  artifact={preparedWorkspaceArtifact(
                    artifact,
                    publisher,
                    nativeSourceSafe,
                  )}
                  className={actionClass}
                  publisher={publisher}
                />
              )}
            </div>
          </div>
        ))}
    </div>
  );
}

function preparedWorkspaceArtifact(
  artifact: WorkbenchArtifact,
  publisher: SequenceWorkspaceArtifactPublisher,
  nativeSourceSafe: boolean,
): PreparedSequenceWorkspaceArtifact {
  const useNativeRichGeneration =
    publisher.supportsNativeRichGeneration === true &&
    nativeSourceSafe &&
    artifact.provenance.sourceRevision === 0 &&
    artifact.provenance.parameters.scope === "all" &&
    artifact.format !== "bed" &&
    artifact.format !== "csv" &&
    artifact.format !== "gff3" &&
    artifact.format !== "gtf" &&
    artifact.format !== "vcf" &&
    artifact.format !== "tsv" &&
    artifact.format !== "json";
  return {
    ...(useNativeRichGeneration
      ? {
          serverGeneration: {
            compression: "none" as const,
            kind: "native-rich" as const,
          },
        }
      : { content: artifact.content }),
    format: artifact.format as PreparedSequenceWorkspaceArtifact["format"],
    mediaType: artifact.mediaType,
    name: artifact.name,
    provenance: artifact.provenance,
  };
}

function parseRestrictionEnzymes(value: string): Array<string> | undefined {
  const enzymes = value
    .split(",")
    .map((item) => item.trim())
    .filter(Boolean);
  return enzymes.length === 0 ? undefined : enzymes;
}

function inferTrackFormat(fileName: string): SequenceTrackFormat | null {
  const normalized = fileName.toLowerCase();
  if (normalized.endsWith(".bam")) return "bam";
  if (normalized.endsWith(".bed")) return "bed";
  if (normalized.endsWith(".cram")) return "cram";
  if (normalized.endsWith(".gff") || normalized.endsWith(".gff3"))
    return "gff3";
  if (normalized.endsWith(".gtf")) return "gtf";
  if (normalized.endsWith(".sam")) return "sam";
  if (normalized.endsWith(".vcf")) return "vcf";
  return null;
}

function isReferenceFileName(fileName: string): boolean {
  return /\.(?:fa|fasta|fna)$/iu.test(fileName);
}

const actionClass =
  "rounded-md border border-token-border bg-token-main-surface-primary px-2.5 py-1 text-sm font-medium text-token-text-secondary hover:bg-token-main-surface-secondary hover:text-token-text-primary disabled:cursor-not-allowed disabled:opacity-40";
const inputClass =
  "min-w-0 max-w-full rounded-md border border-token-border bg-token-input-background px-2 py-1 text-sm text-token-text-primary outline-none focus:border-token-focus-border";

SHA-256: aca3ac89a2024ecf9d6b32c6801ca376da5a284ab6407f71bddf4ac540578a2c