// Пульт преподавателя. // // Компоновка от того, по чему его оценит действующий преподаватель: задать // сценарий и не вмешиваться, видеть работу курсанта вживую, получить готовый // разбор, показать группе (docs/arch/FRONTEND.md). import { useEffect, useRef, useState } from "react"; import { Debrief } from "@/features/debrief/Debrief"; import { Director } from "@/features/instructor/Director"; import { useControl } from "@/features/instructor/useControl"; import { KioCard } from "@/features/kio-card/KioCard"; import { StandStatus } from "@/features/health/StandStatus"; import { ModeBanner } from "@/features/mode-banner/ModeBanner"; import { useObserve } from "@/features/observe/useObserve"; import { InterviewTimer, Timers } from "@/features/timers/Timers"; import { useScenarios, useSessions } from "@/shared/api/http"; import { sessionIdFromUrl } from "@/shared/api/session"; import type { SessionMode } from "@/shared/types/generated"; const MODES: { value: SessionMode; label: string }[] = [ { value: "training", label: "тренировочный" }, { value: "exam", label: "контрольный" }, { value: "self", label: "самостоятельный" }, ]; export function Instructor() { const [sessionId, setSessionId] = useState(sessionIdFromUrl()); const [scenarioId, setScenarioId] = useState(""); const [mode, setMode] = useState("training"); const [trainee, setTrainee] = useState(""); const [noteFor, setNoteFor] = useState(null); // Запускается только занятие, созданное этой кнопкой. Открытая из истории // ссылка на идущее занятие не должна запускать его заново и сбрасывать. const toStart = useRef(null); const [noteText, setNoteText] = useState(""); const scenarios = useScenarios(); const history = useSessions(); const session = useObserve(sessionId); const control = useControl(sessionId); useEffect(() => { if (scenarios.data?.length && !scenarioId) setScenarioId(scenarios.data[0].id); }, [scenarios.data, scenarioId]); function startLesson() { const id = crypto.randomUUID(); // Номер занятия живёт в адресе: монитор и АРМ курсанта открываются ссылкой. window.history.replaceState(null, "", `/instructor?session=${id}`); toStart.current = id; setSessionId(id); } // Занятие запускается, когда канал управления открылся. useEffect(() => { if (!sessionId || control.status !== "open" || toStart.current !== sessionId) return; toStart.current = null; control.start(scenarioId, mode, trainee || "Курсант"); }, [sessionId, control, scenarioId, mode, trainee]); const links = sessionId ? { trainee: `${location.origin}/trainee?session=${sessionId}`, wall: `${location.origin}/wall?session=${sessionId}`, dds: `${location.origin}/dds?session=${sessionId}&role=dds_01`, } : null; return (

Рабочее место преподавателя

Занятие

Сценарий
Режим {MODES.map((item) => ( ))}
Курсант setTrainee(event.target.value)} />

{sessionId && (

{session.ended ? "занятие завершено" : ( )}

)} {sessionId && !session.ended && ( )} {links && (
АРМ курсантаоткрыть
Внешний монитороткрыть
АРМ ДДСоткрыть
)}

Ход занятия

{!sessionId &&

Занятие не запущено.

} {sessionId && ( <>

канал: {session.status} · подсказок использовано: {session.hintsUsed}

Транскрипт — клик по реплике добавляет пометку

{session.transcript.map((line) => ( setNoteFor(line.ref)} className="clickable"> ))} {session.transcript.length === 0 && }
{line.speaker === "caller" ? "звонящий" : "оператор"} {line.text} {session.notes.has(line.ref) && (
пометка: {session.notes.get(line.ref)}
)} {noteFor === line.ref && (
setNoteText(event.target.value)} />{" "}
)}
—

Нормативы

Карточка курсанта

{/* Только чтение: преподаватель управляет ситуацией, а не работой обучаемого. */} )}

История группы

профили курсантов

{history.data?.slice(0, 15).map((item) => ( ))} {!history.data?.length && }
{item.mode} · попытка {item.attempt} {item.scenario_id}
занятий ещё не было
{session.report && }
); }