// Разбор после звонка — главный учебный момент занятия. // // Ни одной отметки без обоснования: у каждой есть код, факт и норматив. // По каждому пропущенному пункту видно, каким был эталонный вопрос // (docs/product/DEBRIEF.md). import type { SessionReport } from "@/shared/types/generated"; import { Radar } from "./Radar"; export function Debrief({ report, big = false }: { report: SessionReport; big?: boolean }) { const questions = new Map(report.reference_questions.map((item) => [item.checklist_id, item.question])); const diff = report.self_assessment_diff; // Поля со значениями по умолчанию приходят из контракта необязательными. const unnoticed = diff?.unnoticed ?? []; const noticed = diff?.noticed ?? []; const overcautious = diff?.overcautious ?? []; const notes = report.notes ?? []; const passedMetrics = Math.max(0, report.metrics.length - report.failed_metrics); const improvementPoints = report.metrics.filter((metric) => !metric.passed).slice(0, 3); const metricTitles = new Map(report.metrics.map((metric) => [metric.key, metric.title])); const coaching = report.ai_coaching?.status === "ready" ? report.ai_coaching.recommendations ?? [] : []; return (

Результаты занятия

Итог{report.passed ? "Зачёт" : "Не зачтено"} Критерии: выполнено {passedMetrics} из {report.metrics.length} Допустимо не выполнить: {report.criteria.allowed_errors}
Ваша итоговая оценка{report.score_final.toFixed(0)} из 100 {report.overridden_by && Изменено: {report.overridden_by}}
Автоматическая оценка{report.score_auto.toFixed(0)} из 100 {report.overridden_by ? "до корректировки преподавателя" : "расчёт по критериям занятия"}
Упражнение{report.exercise === "dds" ? "ДДС" : report.exercise === "card" ? "КИО" : report.exercise === "call" ? "Вызов 112" : "112 → ДДС"} {report.attempt ? `Попытка ${report.attempt}` : "Завершённое упражнение"}
Служебные сведения

Идентификатор сценария: {report.scenario_id} · номер занятия: {report.session_id}

{report.passed ? "Что получилось" : "На чём сосредоточиться"} {report.passed ? (

Курсант выполнил критерии зачёта. Разберите сильные решения и закрепите порядок действий.

) : improvementPoints.length ? ( <>

Начните с этих критериев — по ним нужно разобрать решение курсанта:

) : (

Зачёт не получен. Сверьте отметки и факты ниже с ходом занятия.

)} {coaching.length > 0 && <>

Рекомендации локальной модели (на балл не влияют):

}

Скачать отчёт: таблица (CSV) {" · "}документ (PDF)

{report.passed ? "Зачёт" : "Не зачтено"}: на разборе {report.failed_metrics} пунктов; допустимо пропустить {report.criteria.allowed_errors}. {report.exercise === "dds" && <>На первичное решение ДДС отводится {` ${report.criteria.decision_time_limit_seconds} с`}} {report.exercise === "dds" && <>; норматив отработки карточки ДДС — {` ${report.criteria.dds_card_work_time_limit_seconds ?? 180} с`}} {report.exercise === "card" && <>Норматив заполнения КИО — {` ${report.criteria.card_fill_time_limit_seconds ?? 180} с`}} {report.exercise && report.exercise !== "dds" && <> ; грамматика описания КИО {report.criteria.require_correct_grammar ? "входит" : "не входит"} в оценку }.

{Object.keys(report.criteria.score_weights ?? {}).length > 0 &&

Преподаватель переопределил веса: {Object.entries(report.criteria.score_weights ?? {}) .map(([key, weight]) => `${key} — ${weight}`).join("; ")}.

} {Boolean(report.card_results?.length) &&

Карточки занятия ({report.card_results!.length})

{report.card_results!.map((card, index) =>
Карточка {index + 1}: {card.title || card.scenario_id} · {card.score_auto.toFixed(0)} / 100 {card.address ? ` · ${card.address}` : ""} {card.managed_service &&

Ответственная служба: {card.managed_service}

}

Ответ диспетчера: {card.reply_text || "не внесён"}

{card.metrics.some((metric) => !metric.passed || (metric.credit != null && metric.credit < 1)) &&
    {card.metrics.filter((metric) => !metric.passed || (metric.credit != null && metric.credit < 1)).map((metric) =>
  • {metric.title}: {metric.fact} · норматив {metric.norm} {metric.credit != null && ` · вклад ${Math.round(metric.credit * 100)}%`}
  • )}
} {card.findings.map((finding, findingIndex) =>

{finding.code}: {finding.summary}{finding.fact ? ` — ${finding.fact}` : ""}

)}
)}
}

Разбор по критериям

Сравните, что сделал курсант, с ожидаемым порядком действий.

{report.metrics.map((metric) => ( ))}
{metric.title} {metric.passed ? "Выполнено" : "Разобрать"} {metric.fact} · ожидалось: {metric.norm} {metric.ref && ({metric.ref})}
{report.metrics.some((metric) => metric.weight != null || metric.credit != null) &&
Как критерии повлияли на балл
    {report.metrics.filter((metric) => metric.weight != null || metric.credit != null).map((metric) =>
  • {metric.title}: вес {metric.weight ?? 1} {metric.credit != null && ` · вклад в оценку ${Math.round(metric.credit * 100)}%`}
  • )}
}

Отметки

{report.findings.map((finding, index) => ( ))} {report.findings.length === 0 && }
{finding.code} {finding.summary}
{finding.fact}{finding.norm && ` · ${finding.norm}`}
Замечаний по ходу занятия нет.

Компетенции

{report.missed_checklist.length > 0 && ( <>

Пропущенные вопросы

{report.missed_checklist.map((id) => ( ))}
{id} эталонный вопрос: «{questions.get(id)}»
)} {diff && ( <>

Самооценка

{report.self_assessment?.comment && ( )}
Не заметил, что пропустил {unnoticed.map((id) => questions.get(id) ?? id).join("; ") || "—"}
Заметил сам {noticed.map((id) => questions.get(id) ?? id).join("; ") || "—"}
Отметил зря — спрашивал {overcautious.map((id) => questions.get(id) ?? id).join("; ") || "—"}
Комментарий курсанта{report.self_assessment.comment}
)} {report.hints_used.length > 0 && ( <>

Использованные подсказки

{report.hints_used.map((hint, index) => ( ))}
{hint.checklist_id}{hint.question}
)}
Весь ход разговора ({report.transcript.length} реплик)

Отметки и комментарии преподавателя привязаны к соответствующим репликам.

{report.transcript.map((line) => { const marks = report.findings.filter((finding) => finding.transcript_ref === line.ref); const note = notes.find((item) => item.transcript_ref === line.ref); return ( ); })}
{line.speaker === "caller" ? "звонящий" : "оператор"} {line.text} {marks.map((mark, index) => (
{mark.code}: {mark.summary}
))} {note &&
пометка преподавателя: {note.text}
}
); }