2026-09-17 21:13:41 +03:00
|
|
|
|
// АРМ курсанта: приём вызова и разговор голосом.
|
|
|
|
|
|
// Карточка КИО — lct-10, таймеры — lct-11.
|
|
|
|
|
|
|
lct-10: карточка КИО — форма, слияние правок, read-only
Фронт не хранит карточку как истину: локально живут только правки
курсанта, ещё не подтверждённые сервером. Распознанное сервером значение
перекрывает набранное, эхо своей правки её подтверждает. Иначе
автозаполнение и ручной ввод дерутся за одно поле — худший класс багов
для отладки в последнюю ночь.
Описание полей отдельно от отрисовки: по нему же карточка рисуется
read-only на мониторе, пульте и АРМ ДДС.
Сверка формы с контрактом встроена в make typecheck: все 40 полей
домена на форме, а поле, добавленное в контракт и забытое в форме,
теперь поймается само. Это пропущенный факт по вине интерфейса,
а не курсанта.
Открытый вопрос в карточке: нужно ли автозаполнение карточки из речи
вообще — полнота КИО оценивается кодом E5, и если сервер впишет адрес
сам, курсант перестанет за неё отвечать.
2026-09-17 21:16:51 +03:00
|
|
|
|
import { KioCard } from "@/features/kio-card/KioCard";
|
2026-09-17 21:18:28 +03:00
|
|
|
|
import { ModeBanner, hintsAllowed } from "@/features/mode-banner/ModeBanner";
|
lct-16 и половина lct-19: разбор, отчёт, внешний монитор, эталон
Эталонный диалог собирается кодом из фактов и чек-листа: написанный
руками, он разошёлся бы с фактами при первой же правке сценария,
и курсанта оштрафовали бы за правильный ответ.
Отчёт: метрики фактом против норматива со ссылкой, отметка E1 на каждый
недобытый факт с эталонным вопросом, расхождение самооценки — что
курсант заметил сам, чего не заметил, что отметил зря. Не заметил —
самое ценное для разбора.
Внешний монитор — не отдельное приложение, а другой режим отрисовки тех
же событий: крупный таймер опроса, ход разговора, карточка, после оценки
разбор на весь экран.
Коррекция преподавателем сохраняет автооценку рядом: видно, что
скорректировано и кем.
Найдено: все метрики весили одинаково, и курсант, не задавший ни одного
вопроса, но заполнивший карточку руками, получал 87 из 100. Предварительные
веса (полнота опроса — 4) дают 74; окончательные утверждает методист,
вопрос записан в DEBRIEF.md.
2026-09-17 21:32:12 +03:00
|
|
|
|
import { Debrief } from "@/features/debrief/Debrief";
|
2026-09-17 21:24:34 +03:00
|
|
|
|
import { SelfAssessment } from "@/features/self-assessment/SelfAssessment";
|
2026-09-17 21:18:28 +03:00
|
|
|
|
import { InterviewTimer, Timers } from "@/features/timers/Timers";
|
lct-10: карточка КИО — форма, слияние правок, read-only
Фронт не хранит карточку как истину: локально живут только правки
курсанта, ещё не подтверждённые сервером. Распознанное сервером значение
перекрывает набранное, эхо своей правки её подтверждает. Иначе
автозаполнение и ручной ввод дерутся за одно поле — худший класс багов
для отладки в последнюю ночь.
Описание полей отдельно от отрисовки: по нему же карточка рисуется
read-only на мониторе, пульте и АРМ ДДС.
Сверка формы с контрактом встроена в make typecheck: все 40 полей
домена на форме, а поле, добавленное в контракт и забытое в форме,
теперь поймается само. Это пропущенный факт по вине интерфейса,
а не курсанта.
Открытый вопрос в карточке: нужно ли автозаполнение карточки из речи
вообще — полнота КИО оценивается кодом E5, и если сервер впишет адрес
сам, курсант перестанет за неё отвечать.
2026-09-17 21:16:51 +03:00
|
|
|
|
import { display } from "@/features/kio-card/merge";
|
2026-09-17 21:13:41 +03:00
|
|
|
|
import { useCall } from "@/features/call/useCall";
|
|
|
|
|
|
import { sessionIdFromUrl } from "@/shared/api/session";
|
|
|
|
|
|
|
|
|
|
|
|
const STATUS_LABEL: Record<string, string> = {
|
|
|
|
|
|
connecting: "подключение",
|
|
|
|
|
|
open: "на связи",
|
|
|
|
|
|
reconnecting: "связь потеряна, переподключение",
|
|
|
|
|
|
closed: "отключено",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export function Call() {
|
|
|
|
|
|
const sessionId = sessionIdFromUrl();
|
|
|
|
|
|
const call = useCall(sessionId);
|
|
|
|
|
|
|
|
|
|
|
|
if (!sessionId) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<main className="page">
|
|
|
|
|
|
<h1>АРМ курсанта</h1>
|
|
|
|
|
|
<p className="warn">В адресе нет номера занятия: откройте ссылку вида /trainee?session=…</p>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<main className="page">
|
2026-09-17 21:18:28 +03:00
|
|
|
|
<ModeBanner mode={call.incoming?.mode} />
|
2026-09-17 21:13:41 +03:00
|
|
|
|
<h1>АРМ оператора 112</h1>
|
2026-09-17 21:18:28 +03:00
|
|
|
|
<InterviewTimer timers={call.timers} />
|
2026-09-17 21:13:41 +03:00
|
|
|
|
<table className="grid">
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
<tr><th>Канал</th><td className={call.status === "open" ? "" : "warn"}>{STATUS_LABEL[call.status]}</td></tr>
|
|
|
|
|
|
<tr><th>Вызов</th><td>{call.incoming ? `${call.incoming.caller_number} · сценарий ${call.incoming.scenario_id} · режим ${call.incoming.mode}` : "ожидание"}</td></tr>
|
|
|
|
|
|
<tr><th>Гарнитура</th><td>{call.micOn ? "микрофон включён" : "выключен"}</td></tr>
|
|
|
|
|
|
<tr><th>Звонящий</th><td>{call.callerSpeaking ? "говорит" : "молчит"}</td></tr>
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
|
|
|
|
<p>
|
|
|
|
|
|
{call.phase === "incoming" && !call.micOn && (
|
|
|
|
|
|
<button type="button" onClick={() => void call.answer()}>Ответить</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{call.micOn && <button type="button" onClick={call.hangup}>Завершить вызов</button>}{" "}
|
2026-09-17 21:18:28 +03:00
|
|
|
|
{call.micOn && hintsAllowed(call.incoming?.mode) && (
|
2026-09-17 21:13:41 +03:00
|
|
|
|
<button type="button" onClick={call.hint}>Подсказка</button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
|
|
{call.error && <p className="violated">{call.error}</p>}
|
|
|
|
|
|
|
2026-09-17 21:24:34 +03:00
|
|
|
|
{call.phase === "ended" && !call.selfAssessed && call.checklist.length > 0 && (
|
|
|
|
|
|
<SelfAssessment checklist={call.checklist} onSubmit={call.submitSelfAssessment} />
|
|
|
|
|
|
)}
|
lct-16 и половина lct-19: разбор, отчёт, внешний монитор, эталон
Эталонный диалог собирается кодом из фактов и чек-листа: написанный
руками, он разошёлся бы с фактами при первой же правке сценария,
и курсанта оштрафовали бы за правильный ответ.
Отчёт: метрики фактом против норматива со ссылкой, отметка E1 на каждый
недобытый факт с эталонным вопросом, расхождение самооценки — что
курсант заметил сам, чего не заметил, что отметил зря. Не заметил —
самое ценное для разбора.
Внешний монитор — не отдельное приложение, а другой режим отрисовки тех
же событий: крупный таймер опроса, ход разговора, карточка, после оценки
разбор на весь экран.
Коррекция преподавателем сохраняет автооценку рядом: видно, что
скорректировано и кем.
Найдено: все метрики весили одинаково, и курсант, не задавший ни одного
вопроса, но заполнивший карточку руками, получал 87 из 100. Предварительные
веса (полнота опроса — 4) дают 74; окончательные утверждает методист,
вопрос записан в DEBRIEF.md.
2026-09-17 21:32:12 +03:00
|
|
|
|
{call.selfAssessed && !call.report && <p>Самооценка принята, оценка считается…</p>}
|
|
|
|
|
|
{call.report && <Debrief report={call.report} />}
|
2026-09-17 21:24:34 +03:00
|
|
|
|
|
lct-10: карточка КИО — форма, слияние правок, read-only
Фронт не хранит карточку как истину: локально живут только правки
курсанта, ещё не подтверждённые сервером. Распознанное сервером значение
перекрывает набранное, эхо своей правки её подтверждает. Иначе
автозаполнение и ручной ввод дерутся за одно поле — худший класс багов
для отладки в последнюю ночь.
Описание полей отдельно от отрисовки: по нему же карточка рисуется
read-only на мониторе, пульте и АРМ ДДС.
Сверка формы с контрактом встроена в make typecheck: все 40 полей
домена на форме, а поле, добавленное в контракт и забытое в форме,
теперь поймается само. Это пропущенный факт по вине интерфейса,
а не курсанта.
Открытый вопрос в карточке: нужно ли автозаполнение карточки из речи
вообще — полнота КИО оценивается кодом E5, и если сервер впишет адрес
сам, курсант перестанет за неё отвечать.
2026-09-17 21:16:51 +03:00
|
|
|
|
<div className="columns">
|
|
|
|
|
|
<section>
|
|
|
|
|
|
<h2>Карточка информационного обмена</h2>
|
|
|
|
|
|
<KioCard
|
|
|
|
|
|
state={call.card}
|
|
|
|
|
|
required={call.incoming?.required_fields ?? []}
|
|
|
|
|
|
readOnly={!call.micOn}
|
|
|
|
|
|
onChange={call.patchKio}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<p>
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
disabled={!call.micOn || !display(call.card, "dds")}
|
|
|
|
|
|
onClick={() => call.dispatch(display(call.card, "dds") as never)}
|
|
|
|
|
|
>
|
|
|
|
|
|
Передать в ДДС
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
<section>
|
2026-09-17 21:18:28 +03:00
|
|
|
|
<h2>Нормативы</h2>
|
|
|
|
|
|
<Timers timers={call.timers} />
|
2026-09-17 21:13:41 +03:00
|
|
|
|
<h2>Разговор</h2>
|
|
|
|
|
|
<table className="grid">
|
|
|
|
|
|
<tbody>
|
|
|
|
|
|
{call.lines.map((line, index) => (
|
|
|
|
|
|
<tr key={index}>
|
|
|
|
|
|
<th>{line.speaker === "caller" ? "звонящий" : "оператор"}</th>
|
|
|
|
|
|
<td className={line.partial ? "warn" : ""}>{line.text}</td>
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
))}
|
|
|
|
|
|
{call.lines.length === 0 && <tr><td colSpan={2}>—</td></tr>}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
lct-10: карточка КИО — форма, слияние правок, read-only
Фронт не хранит карточку как истину: локально живут только правки
курсанта, ещё не подтверждённые сервером. Распознанное сервером значение
перекрывает набранное, эхо своей правки её подтверждает. Иначе
автозаполнение и ручной ввод дерутся за одно поле — худший класс багов
для отладки в последнюю ночь.
Описание полей отдельно от отрисовки: по нему же карточка рисуется
read-only на мониторе, пульте и АРМ ДДС.
Сверка формы с контрактом встроена в make typecheck: все 40 полей
домена на форме, а поле, добавленное в контракт и забытое в форме,
теперь поймается само. Это пропущенный факт по вине интерфейса,
а не курсанта.
Открытый вопрос в карточке: нужно ли автозаполнение карточки из речи
вообще — полнота КИО оценивается кодом E5, и если сервер впишет адрес
сам, курсант перестанет за неё отвечать.
2026-09-17 21:16:51 +03:00
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
2026-09-17 21:13:41 +03:00
|
|
|
|
</main>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|