lct-16 и половина lct-19: разбор, отчёт, внешний монитор, эталон

Эталонный диалог собирается кодом из фактов и чек-листа: написанный
руками, он разошёлся бы с фактами при первой же правке сценария,
и курсанта оштрафовали бы за правильный ответ.

Отчёт: метрики фактом против норматива со ссылкой, отметка E1 на каждый
недобытый факт с эталонным вопросом, расхождение самооценки — что
курсант заметил сам, чего не заметил, что отметил зря. Не заметил —
самое ценное для разбора.

Внешний монитор — не отдельное приложение, а другой режим отрисовки тех
же событий: крупный таймер опроса, ход разговора, карточка, после оценки
разбор на весь экран.

Коррекция преподавателем сохраняет автооценку рядом: видно, что
скорректировано и кем.

Найдено: все метрики весили одинаково, и курсант, не задавший ни одного
вопроса, но заполнивший карточку руками, получал 87 из 100. Предварительные
веса (полнота опроса — 4) дают 74; окончательные утверждает методист,
вопрос записан в DEBRIEF.md.
This commit is contained in:
Ivan Gerasimov 2026-09-17 21:32:12 +03:00
commit fadfa3e479
20 changed files with 781 additions and 9 deletions

View file

@ -3,6 +3,7 @@
import { KioCard } from "@/features/kio-card/KioCard";
import { ModeBanner, hintsAllowed } from "@/features/mode-banner/ModeBanner";
import { Debrief } from "@/features/debrief/Debrief";
import { SelfAssessment } from "@/features/self-assessment/SelfAssessment";
import { InterviewTimer, Timers } from "@/features/timers/Timers";
import { display } from "@/features/kio-card/merge";
@ -58,9 +59,8 @@ export function Call() {
{call.phase === "ended" && !call.selfAssessed && call.checklist.length > 0 && (
<SelfAssessment checklist={call.checklist} onSubmit={call.submitSelfAssessment} />
)}
{call.selfAssessed && (
<p>{call.scoreReady ? "Оценка готова — разбор открывает преподаватель." : "Самооценка принята, оценка считается…"}</p>
)}
{call.selfAssessed && !call.report && <p>Самооценка принята, оценка считается…</p>}
{call.report && <Debrief report={call.report} />}
<div className="columns">
<section>

View file

@ -0,0 +1,74 @@
// Внешний монитор: группа смотрит, как идёт занятие, и разбирает вместе с ним.
//
// Это не отдельное приложение, а другой режим отрисовки тех же событий: крупный
// шрифт, высокий контраст, минимум деталей. Никакого взаимодействия — экран висит
// и показывает (docs/arch/FRONTEND.md).
import { Debrief } from "@/features/debrief/Debrief";
import { KioCard } from "@/features/kio-card/KioCard";
import { ModeBanner } from "@/features/mode-banner/ModeBanner";
import { InterviewTimer } from "@/features/timers/Timers";
import { useObserve } from "@/features/observe/useObserve";
import { sessionIdFromUrl } from "@/shared/api/session";
export function Wall() {
const sessionId = sessionIdFromUrl();
const session = useObserve(sessionId);
if (!sessionId) {
return (
<main className="page wall">
<h1>Внешний монитор</h1>
<p className="warn">Откройте ссылку вида /wall?session=…</p>
</main>
);
}
if (session.report) {
return (
<main className="page wall">
<ModeBanner mode={session.mode} />
<h1>{session.title} · разбор</h1>
<Debrief report={session.report} big />
</main>
);
}
return (
<main className="page wall">
<ModeBanner mode={session.mode} />
<h1>
{session.title ?? "Ожидание занятия"}
{session.trainee && ` · ${session.trainee}`}
</h1>
<InterviewTimer timers={session.timers} big />
<div className="columns">
<section>
<h2>Ход разговора</h2>
<table className="grid">
<tbody>
{session.transcript.slice(-12).map((line) => (
<tr key={line.ref}>
<th>{line.speaker === "caller" ? "звонящий" : "оператор"}</th>
<td>
{line.text}
{session.notes.has(line.ref) && (
<div className="ref">пометка: {session.notes.get(line.ref)}</div>
)}
</td>
</tr>
))}
{session.transcript.length === 0 && <tr><td colSpan={2}>—</td></tr>}
</tbody>
</table>
<p className="ref">подсказок использовано: {session.hintsUsed}</p>
</section>
<section>
<h2>Карточка</h2>
<KioCard state={session.card} required={session.requiredFields} readOnly />
</section>
</div>
</main>
);
}