import { useState } from "react"; import { ConfirmationModal, Dropdown, Icon } from "@nodedc/ui-react"; import { type ObservationSessionReplayLaunch, type ObservationSessionSummary, } from "../core/observation/sessionArchive"; import { useObservationSessions } from "../core/observation/useObservationSessions"; import type { ObservationPreparationPhase, ObservationReplayOutcome, } from "../core/observation/useObservationSessions"; import "../styles/observation-sessions.css"; type SessionVisualState = "ready" | "processing" | "cold" | "error"; export function observationSessionVisualState( session: ObservationSessionSummary, { pending = false, failed = false }: { pending?: boolean; failed?: boolean } = {}, ): SessionVisualState { if (failed) return "error"; if (pending) return "processing"; if (session.preparation !== null) { if (["queued", "validating", "exporting", "finalizing"].includes( session.preparation.state, )) return "processing"; if (session.preparation.state === "ready" && session.replayable) return "ready"; return "error"; } if (session.status === "recording") return "processing"; return session.replayable ? "cold" : "error"; } export function observationSessionVisualLabel(state: SessionVisualState): string { if (state === "ready") return "Готово"; if (state === "processing") return "Обработка"; if (state === "cold") return "Подготовить"; return "Ошибка"; } const modalityLabel: Record = { "point-cloud": "облако точек", pose: "траектория", trajectory: "траектория", video: "видео", image: "изображения", depth: "глубина", telemetry: "телеметрия", }; const preparationLabel: Record = { requesting: "Запрашиваем подготовку", queued: "В очереди", validating: "Проверяем запись", exporting: "Готовим операторскую сцену", finalizing: "Завершаем подготовку", failed: "Подготовка не выполнена", cancelled: "Подготовка отменена", }; function progressCopy(phase: ObservationPreparationPhase): string { // Backend progress values are phase markers and heartbeat revisions, not a // measured fraction of bytes or frames. Presenting 0.5 as "50%" made a // healthy long export look stalled, especially after a browser reload. return preparationLabel[phase]; } function formatStartedAt(value: string): string { return new Intl.DateTimeFormat("ru-RU", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", }).format(new Date(value)); } function formatDuration(seconds: number): string { const totalSeconds = Math.max(0, Math.round(seconds)); const hours = Math.floor(totalSeconds / 3_600); const minutes = Math.floor((totalSeconds % 3_600) / 60); const remainingSeconds = totalSeconds % 60; return hours > 0 ? `${hours}:${String(minutes).padStart(2, "0")}:${String(remainingSeconds).padStart(2, "0")}` : `${minutes}:${String(remainingSeconds).padStart(2, "0")}`; } function sessionDescription(session: ObservationSessionSummary): string { const modalities = session.modalities.length ? session.modalities.map((value) => modalityLabel[value] ?? value).join(" · ") : "каналы не зафиксированы"; return `${formatStartedAt(session.startedAtUtc)} · ${formatDuration(session.durationSeconds)} · ${modalities}`; } export function ObservationSessionSelect({ limit = 100, disabled = false, blockedReason = null, onReplayBegin, onReplayAccepted, onReplaySettled, }: { limit?: number; disabled?: boolean; blockedReason?: string | null; onReplayBegin?: ( session: ObservationSessionSummary, launch: ObservationSessionReplayLaunch, ) => void | Promise; onReplayAccepted?: ( session: ObservationSessionSummary, launch: ObservationSessionReplayLaunch, ) => void | Promise; onReplaySettled?: ( session: ObservationSessionSummary, outcome: ObservationReplayOutcome, ) => void | Promise; }) { const [deleteTarget, setDeleteTarget] = useState(null); const sessions = useObservationSessions({ limit, replayEnabled: blockedReason === null, onReplayBegin, onReplayAccepted, onReplaySettled, }); const triggerCopy = sessions.replayProgress ? progressCopy(sessions.replayProgress.phase) : sessions.state === "loading" ? "Загружаем сессии…" : "Сохранённые сессии"; const presentedTriggerCopy = blockedReason ?? triggerCopy; return <> ( )} > {({ close }) => (
ИСТОРИЯ НАБЛЮДЕНИЯ Сохранённые сессии
{sessions.items.length > 0 ? (
{sessions.items.map((session) => { const pending = sessions.replayingSessionId === session.id; const deleting = sessions.deletingSessionId === session.id; const failed = sessions.failedSessionId === session.id; const visualState = observationSessionVisualState(session, { pending, failed }); return (