lct-39: пауза занятия с пульта, таймеры и очередь ДДС стоят на время простоя
This commit is contained in:
parent
b4420cea16
commit
b77d65c82e
23 changed files with 410 additions and 20 deletions
|
|
@ -50,6 +50,7 @@ export function useCall(sessionId: string | null) {
|
|||
const [scoreReady, setScoreReady] = useState(false);
|
||||
const { report, error: reportError } = useSessionReport(sessionId, scoreReady);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [paused, setPaused] = useState(false);
|
||||
|
||||
const channel = useRef<ReturnType<typeof callChannel> | null>(null);
|
||||
const capture = useRef<Capture | null>(null);
|
||||
|
|
@ -153,6 +154,9 @@ export function useCall(sessionId: string | null) {
|
|||
case "score.ready":
|
||||
setScoreReady(true);
|
||||
break;
|
||||
case "session.paused":
|
||||
setPaused(event.paused);
|
||||
break;
|
||||
case "error":
|
||||
setError(event.message);
|
||||
break;
|
||||
|
|
@ -253,7 +257,7 @@ export function useCall(sessionId: string | null) {
|
|||
|
||||
return {
|
||||
resolve,
|
||||
status, phase, incoming, briefing, cardSubmitted, lines, timers, callerSpeaking, micOn, error, card,
|
||||
status, phase, incoming, briefing, cardSubmitted, lines, timers, callerSpeaking, micOn, error, paused, card,
|
||||
checklist, selfAssessed, scoreReady, report, reportError,
|
||||
answer, hangup, hint, patchKio, dispatch, submitCard, submitSelfAssessment, sendText,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -63,7 +63,9 @@ export function useControl(sessionId: string | null) {
|
|||
);
|
||||
|
||||
const stop = useCallback(() => channel.current?.socket.send({ type: "session.stop" }), []);
|
||||
const pause = useCallback(() => channel.current?.socket.send({ type: "session.pause" }), []);
|
||||
const resume = useCallback(() => channel.current?.socket.send({ type: "session.resume" }), []);
|
||||
const playReference = useCallback(() => channel.current?.socket.send({ type: "reference.play" }), []);
|
||||
|
||||
return { status, openSessionId, start, note, inject, stop, playReference };
|
||||
return { status, openSessionId, start, note, inject, stop, pause, resume, playReference };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export interface ObservedSession {
|
|||
hintsUsed: number;
|
||||
notes: Map<string, string>;
|
||||
ended: boolean;
|
||||
paused: boolean;
|
||||
report: SessionReport | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
|
@ -45,6 +46,7 @@ export function useObserve(sessionId: string | null): ObservedSession {
|
|||
const [hintsUsed, setHintsUsed] = useState(0);
|
||||
const [notes, setNotes] = useState<Map<string, string>>(new Map());
|
||||
const [ended, setEnded] = useState(false);
|
||||
const [paused, setPaused] = useState(false);
|
||||
// Сессия, для которой отчёт уже стоит просить. Привязка к id, а не флаг:
|
||||
// при переключении истории старый флаг не должен дёрнуть запрос новой сессии.
|
||||
const [reportFor, setReportFor] = useState<string | null>(null);
|
||||
|
|
@ -65,6 +67,7 @@ export function useObserve(sessionId: string | null): ObservedSession {
|
|||
setHintsUsed(0);
|
||||
setNotes(new Map());
|
||||
setEnded(false);
|
||||
setPaused(false);
|
||||
setError(null);
|
||||
if (!sessionId) return;
|
||||
let active = true;
|
||||
|
|
@ -88,6 +91,7 @@ export function useObserve(sessionId: string | null): ObservedSession {
|
|||
setTimers(event.timers);
|
||||
setHintsUsed(event.hints_used ?? 0);
|
||||
setEnded(event.ended ?? false);
|
||||
setPaused(event.paused ?? false);
|
||||
if (event.ended) loadReport();
|
||||
break;
|
||||
case "kio.state":
|
||||
|
|
@ -112,6 +116,9 @@ export function useObserve(sessionId: string | null): ObservedSession {
|
|||
setEnded(true);
|
||||
loadReport();
|
||||
break;
|
||||
case "session.paused":
|
||||
setPaused(event.paused);
|
||||
break;
|
||||
case "error":
|
||||
setError(event.message);
|
||||
if (event.code === "session_not_found") loadReport();
|
||||
|
|
@ -127,5 +134,5 @@ export function useObserve(sessionId: string | null): ObservedSession {
|
|||
return () => { active = false; channel.close(); };
|
||||
}, [sessionId, queries]);
|
||||
|
||||
return { status, title, mode, trainee, card, requiredFields, transcript, timers, hintsUsed, notes, ended, report, error };
|
||||
return { status, title, mode, trainee, card, requiredFields, transcript, timers, hintsUsed, notes, ended, paused, report, error };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -233,6 +233,7 @@ export function Dds() {
|
|||
const [checkText, setCheckText] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [ended, setEnded] = useState(false);
|
||||
const [paused, setPaused] = useState(false);
|
||||
// Разбор просим только после `score.ready`; новая карточка снимает флаг.
|
||||
const [scoreReady, setScoreReady] = useState(false);
|
||||
const { report, loading: reportLoading, error: reportError, refetch: refetchReport } =
|
||||
|
|
@ -252,6 +253,7 @@ export function Dds() {
|
|||
if (event.type === "station.state") {
|
||||
const snapshot = event.snapshot as DdsSnapshot;
|
||||
setStation(snapshot);
|
||||
setPaused(snapshot.paused ?? false);
|
||||
setZone(snapshot.zone_decision ?? null);
|
||||
if (snapshot.card_id == null && (snapshot.pending_cards_count ?? 0) > 0) {
|
||||
activeCardId.current = null;
|
||||
|
|
@ -298,6 +300,7 @@ export function Dds() {
|
|||
} else if (event.type === "timer.tick") setTimers(event.timers);
|
||||
else if (event.type === "error") { setError(event.message); setDialing(false); setAdvancePending(false); }
|
||||
else if (event.type === "session.ended") { setEnded(true); setDialing(false); setAdvancePending(false); setPane(null); setView("registry"); }
|
||||
else if (event.type === "session.paused") setPaused(event.paused);
|
||||
else if (event.type === "score.ready") setScoreReady(true);
|
||||
else if (event.type === "phone.report") {
|
||||
setLastReport(event);
|
||||
|
|
@ -368,6 +371,7 @@ export function Dds() {
|
|||
АРМ {role}: {isConnected ? "на связи" : status}
|
||||
</span>
|
||||
{error && <div className="dds-alert" role="alert">Действие не принято: {error}</div>}
|
||||
{paused && !ended && <div className="dds-alert dds-paused">Пауза, ждите преподавателя</div>}
|
||||
{ended && <div className="dds-alert dds-done">Занятие завершено. Оценка появится после расчёта.</div>}
|
||||
{(ended || scoreReady) && <section className="dds-panel" aria-live="polite">
|
||||
<div className="dds-panel-title"><h2>Итог упражнения</h2></div>
|
||||
|
|
|
|||
|
|
@ -695,7 +695,15 @@ export function Instructor() {
|
|||
{sessionId && session.title && (
|
||||
<p>
|
||||
{session.ended ? "занятие завершено" : (
|
||||
<button type="button" onClick={control.stop}>Завершить занятие</button>
|
||||
<>
|
||||
<button type="button" onClick={control.stop}>Завершить занятие</button>{" "}
|
||||
{session.paused ? (
|
||||
<button type="button" onClick={control.resume}>Возобновить</button>
|
||||
) : (
|
||||
<button type="button" onClick={control.pause}>Пауза</button>
|
||||
)}
|
||||
{session.paused && <small className="ref" role="status">Занятие на паузе — время не идёт</small>}
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ export function Call() {
|
|||
placeholder="Ваша реплика…" onChange={(event) => setTextTurn(event.target.value)} />
|
||||
<button type="submit" disabled={call.status !== "open" || !textTurn.trim() || call.cardSubmitted}>Отправить</button>
|
||||
</form>
|
||||
{call.paused && <p role="status" className="state-warn">Пауза, ждите преподавателя</p>}
|
||||
{call.error && <p role="alert" className="state-violated">{call.error}</p>}
|
||||
</section>
|
||||
<div className="arm-card-wrap">
|
||||
|
|
@ -190,6 +191,7 @@ export function Call() {
|
|||
: "Ожидайте входящего вызова"}</span>
|
||||
</div>
|
||||
|
||||
{call.paused && <p className="state-warn arm-message" role="status">Пауза, ждите преподавателя</p>}
|
||||
{call.error && <p className="violated arm-message" role="alert">{call.error}</p>}
|
||||
|
||||
<div className="arm-card-wrap">
|
||||
|
|
|
|||
|
|
@ -585,6 +585,17 @@ export interface SessionEnded {
|
|||
/** Режим сессии. Меняет доступность подсказок и протоколирование, но не поведение звонящего (docs/product/MODES.md). */
|
||||
export type SessionMode = "training" | "exam" | "self";
|
||||
|
||||
/** Остановить время занятия; карточку курсанта команда не меняет (lct-39). */
|
||||
export interface SessionPause {
|
||||
type: "session.pause";
|
||||
}
|
||||
|
||||
/** Пульт остановил или возобновил время занятия (lct-39). */
|
||||
export interface SessionPaused {
|
||||
type: "session.paused";
|
||||
paused: boolean;
|
||||
}
|
||||
|
||||
/** Единица истории: из отчётов складываются профиль, дельта попыток, аналитика. */
|
||||
export interface SessionReport {
|
||||
session_id: string;
|
||||
|
|
@ -611,6 +622,11 @@ export interface SessionReport {
|
|||
score_final: number;
|
||||
overridden_by?: string | null;
|
||||
override_comment?: string | null;
|
||||
total_paused_ms?: number;
|
||||
}
|
||||
|
||||
export interface SessionResume {
|
||||
type: "session.resume";
|
||||
}
|
||||
|
||||
/** Обязательно при подключении: монитор в классе включают посреди занятия. */
|
||||
|
|
@ -631,6 +647,7 @@ export interface SessionSnapshot {
|
|||
timers: Array<TimerSnapshot>;
|
||||
hints_used?: number;
|
||||
ended?: boolean;
|
||||
paused?: boolean;
|
||||
}
|
||||
|
||||
export interface SessionStop {
|
||||
|
|
@ -667,6 +684,7 @@ export interface StationSnapshot {
|
|||
pending_cards_count?: number;
|
||||
next_arrival_in_seconds?: number | null;
|
||||
max_waiting_cards?: number;
|
||||
paused?: boolean;
|
||||
}
|
||||
|
||||
/** Состояние АРМ ДДС после каждой отметки: что стоит и что доступно дальше. */
|
||||
|
|
@ -775,15 +793,15 @@ export interface ZoneDecision {
|
|||
|
||||
// Союзы по каналам — смотри docs/arch/CONTRACT.md#каналы
|
||||
|
||||
export type ServerToTrainee = CallIncoming | CardBriefing | TextTurnAccepted | CallStarted | SttPartial | SttFinal | CallerUtterance | TtsBegin | TtsEnd | TtsCancel | BgStart | BgStop | EraData | KioPatchOut | HintShown | TimerTick | CallEnded | ScoreReady | ErrorEvent;
|
||||
export type ServerToTrainee = CallIncoming | CardBriefing | TextTurnAccepted | CallStarted | SttPartial | SttFinal | CallerUtterance | TtsBegin | TtsEnd | TtsCancel | BgStart | BgStop | EraData | KioPatchOut | HintShown | TimerTick | CallEnded | ScoreReady | SessionPaused | ErrorEvent;
|
||||
|
||||
export type TraineeToServer = CallAnswer | CardSubmit | TextTurn | KioPatchIn | HintRequest | SelfAssessmentSubmit | DdsDispatch | CallResolve | CallHangup | CallbackDial;
|
||||
|
||||
export type ServerToObserver = SessionSnapshot | TranscriptAppend | KioState | TimerTick | ModeSet | HintShown | BgStart | BgStop | CallerUtterance | SessionEnded | ScoreReady | InstructorNoteShown | ReferenceStarted | ErrorEvent;
|
||||
export type ServerToObserver = SessionSnapshot | TranscriptAppend | KioState | TimerTick | ModeSet | HintShown | BgStart | BgStop | CallerUtterance | SessionEnded | ScoreReady | InstructorNoteShown | ReferenceStarted | SessionPaused | ErrorEvent;
|
||||
|
||||
export type InstructorToServer = ScenarioStart | DirectorInject | ReferencePlay | InstructorNoteAdd | ScoreOverride | SessionStop;
|
||||
export type InstructorToServer = ScenarioStart | DirectorInject | ReferencePlay | InstructorNoteAdd | ScoreOverride | SessionStop | SessionPause | SessionResume;
|
||||
|
||||
export type ServerToStation = CardReceived | StationState | PhoneLine | PhoneReport | TimerTick | SessionEnded | ScoreReady | CommandAck | ErrorEvent;
|
||||
export type ServerToStation = CardReceived | StationState | PhoneLine | PhoneReport | TimerTick | SessionEnded | ScoreReady | CommandAck | SessionPaused | ErrorEvent;
|
||||
|
||||
export type StationToServer = CardAck | CardBounce | ServiceStatusSet | CrewSelect | PhoneDial | PhoneBrief | PhoneCheck | PhoneHangup | StationFinish | CardReply | CardNext | CardOpen | ZoneDecision | CrewDispatched | CrewArrived;
|
||||
|
||||
|
|
@ -835,6 +853,9 @@ export type EventName =
|
|||
| "score.ready"
|
||||
| "self_assessment.submit"
|
||||
| "session.ended"
|
||||
| "session.pause"
|
||||
| "session.paused"
|
||||
| "session.resume"
|
||||
| "session.snapshot"
|
||||
| "session.stop"
|
||||
| "station.finish"
|
||||
|
|
|
|||
|
|
@ -349,6 +349,7 @@ button:disabled { opacity: 0.5; cursor: default; }
|
|||
.dds-status-action button { padding: 4px 8px; }
|
||||
.dds-alert { padding: 10px 12px; margin: 0 0 12px; color: var(--violated); background: #fde8e8; border: 1px solid var(--violated); }
|
||||
.dds-done { color: var(--ok); background: #e7f4e9; border-color: var(--ok); }
|
||||
.dds-paused { color: var(--warn); background: #fdf3d8; border-color: var(--warn); }
|
||||
.dds-phone > label { display: block; margin: 14px 12px 5px; font-size: 13px; font-weight: 600; }
|
||||
.dds-phone > select { display: block; width: calc(100% - 24px); margin: 0 12px; padding: 7px; box-sizing: border-box; }
|
||||
.dds-dial { display: block; margin: 12px; width: calc(100% - 24px); text-align: center; background: #23577e; border-color: #23577e; color: white; font-weight: 600; }
|
||||
|
|
|
|||
Loading…
Reference in a new issue