lct-15: самооценка курсанта до показа автооценки
Правило сервера, а не интерфейса: score.ready курсанту уходит только после self_assessment.submit, обойти его через DevTools нельзя. Преподаватель и монитор получают оценку сразу — им ждать нечего. Чек-лист для самооценки отдаётся отдельной точкой и только после конца звонка: во время разговора это содержимое подсказок, и открыть его значило бы выдать в контрольном режиме то, чего там быть не должно. Попутно подключён расчёт оценки при завершении звонка — детерминированный слой был написан в lct-12, но его никто не вызывал. Тест на отсутствие события смотрит очередь курсанта, а не ждёт из сокета: ожидание того, чего не будет, вешает прогон навсегда.
This commit is contained in:
parent
78f66cf3b5
commit
25143645d3
10 changed files with 260 additions and 2 deletions
|
|
@ -11,6 +11,7 @@ import { type Capture, startCapture } from "@/shared/audio/capture";
|
|||
import { EnergyGate } from "@/shared/audio/levels";
|
||||
import { CALLER_RATE, Playback } from "@/shared/audio/playback";
|
||||
import { type CardState, applyPatch, edit, empty as emptyCard } from "@/features/kio-card/merge";
|
||||
import type { ChecklistItem } from "@/features/self-assessment/SelfAssessment";
|
||||
import type { CallIncoming, DDSCode, ServerToTrainee, TimerSnapshot } from "@/shared/types/generated";
|
||||
|
||||
/** Правки копятся и уходят одной дельтой: 300 мс тишины — и отправка. */
|
||||
|
|
@ -33,6 +34,9 @@ export function useCall(sessionId: string | null) {
|
|||
const [callerSpeaking, setCallerSpeaking] = useState(false);
|
||||
const [micOn, setMicOn] = useState(false);
|
||||
const [card, setCard] = useState<CardState>(emptyCard);
|
||||
const [checklist, setChecklist] = useState<ChecklistItem[]>([]);
|
||||
const [selfAssessed, setSelfAssessed] = useState(false);
|
||||
const [scoreReady, setScoreReady] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const channel = useRef<ReturnType<typeof callChannel> | null>(null);
|
||||
|
|
@ -98,6 +102,15 @@ export function useCall(sessionId: string | null) {
|
|||
case "call.ended":
|
||||
setPhase("ended");
|
||||
audio.current?.ambience.stop();
|
||||
// Чек-лист открывается только после звонка: во время него это
|
||||
// содержимое подсказок.
|
||||
void fetch(`/api/sessions/${sessionId}/checklist`)
|
||||
.then((response) => (response.ok ? response.json() : []))
|
||||
.then(setChecklist)
|
||||
.catch(() => setChecklist([]));
|
||||
break;
|
||||
case "score.ready":
|
||||
setScoreReady(true);
|
||||
break;
|
||||
case "error":
|
||||
setError(event.message);
|
||||
|
|
@ -162,8 +175,14 @@ export function useCall(sessionId: string | null) {
|
|||
channel.current?.send({ type: "dds.dispatch", service });
|
||||
}, []);
|
||||
|
||||
const submitSelfAssessment = useCallback((missed: string[], comment: string) => {
|
||||
channel.current?.send({ type: "self_assessment.submit", missed, comment });
|
||||
setSelfAssessed(true);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
status, phase, incoming, lines, timers, callerSpeaking, micOn, error, card,
|
||||
answer, hangup, hint, patchKio, dispatch,
|
||||
checklist, selfAssessed, scoreReady,
|
||||
answer, hangup, hint, patchKio, dispatch, submitSelfAssessment,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
65
frontend/src/features/self-assessment/SelfAssessment.tsx
Normal file
65
frontend/src/features/self-assessment/SelfAssessment.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
// Самооценка курсанта — до показа автооценки.
|
||||
//
|
||||
// Педагогический смысл — рефлексия: курсант сверяет своё ощущение с объективной
|
||||
// оценкой, а расхождение — отдельный материал для преподавателя. Курсант,
|
||||
// не заметивший, что пропустил вопрос о пострадавших, — более важный случай,
|
||||
// чем сама ошибка (docs/product/DEBRIEF.md).
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
export interface ChecklistItem {
|
||||
id: string;
|
||||
question: string;
|
||||
}
|
||||
|
||||
export function SelfAssessment({
|
||||
checklist,
|
||||
onSubmit,
|
||||
}: {
|
||||
checklist: ChecklistItem[];
|
||||
onSubmit: (missed: string[], comment: string) => void;
|
||||
}) {
|
||||
const [missed, setMissed] = useState<string[]>([]);
|
||||
const [comment, setComment] = useState("");
|
||||
|
||||
const toggle = (id: string) =>
|
||||
setMissed((prev) => (prev.includes(id) ? prev.filter((item) => item !== id) : [...prev, id]));
|
||||
|
||||
return (
|
||||
<section>
|
||||
<h2>Самооценка</h2>
|
||||
<p>Отметьте вопросы, которые, по вашему мнению, вы пропустили. Оценка откроется после.</p>
|
||||
<table className="grid">
|
||||
<tbody>
|
||||
{checklist.map((item) => (
|
||||
<tr key={item.id}>
|
||||
<th>
|
||||
<label>
|
||||
<input type="checkbox" checked={missed.includes(item.id)} onChange={() => toggle(item.id)} />{" "}
|
||||
пропустил
|
||||
</label>
|
||||
</th>
|
||||
<td>{item.question}</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr>
|
||||
<th>Комментарий</th>
|
||||
<td>
|
||||
<input
|
||||
type="text"
|
||||
value={comment}
|
||||
placeholder="например: запнулся на адресе, растерялся"
|
||||
onChange={(event) => setComment(event.target.value)}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>
|
||||
<button type="button" onClick={() => onSubmit(missed, comment)}>
|
||||
Отправить самооценку
|
||||
</button>
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue