feat(lesson-ai): новый шаблон промпта + предупреждение о дубле отчёта

- заменён дефолтный промпт редактуры отчёта о занятии на формат
  «сообщение тьютора»: связный текст от 3-го лица, 1 абзац 2–4
  предложения, начало «На занятии ребята …», практика «В конце занятия …»,
  без группы/даты/времени и markdown, обращение на «вы» убрано, «они» → «каждый»
- синхронизированы три копии промпта: LESSON_AI_DEFAULT_PROMPT в server.js
  и сид lesson_ai_prompt в db/init.sql и db/migration.sql; добавлен пример
  «плохо → хорошо» с запретом переносить факты примера
- дубль отчёта в модалке создания больше не автозаполняет форму: показывается
  баннер #lessonDup с кнопкой перехода к существующему отчёту
- проверка на дубль срабатывает и при смене даты (#lessonDate), защищена
  счётчиком поколений lessonDupSeq от гонки при быстрой смене группы/даты
- стили .lesson-dup в public/admin.css, уточнена подсказка в карточке
  шаблона отчёта на public/settings.html
- DDL не менялся: ни таблиц, ни колонок, ни индексов
This commit is contained in:
dev
2026-10-04 00:54:23 +03:00
parent b931c0a760
commit 2363d099d2
6 changed files with 132 additions and 42 deletions
+33 -8
View File
@@ -541,6 +541,10 @@ function ensureLessonModal() {
placeholder="Например: разобрали циклы for и while, решали задачи на списки, домашнее задание — функции"></textarea>
<span class="hint" id="lessonHint">0 / 5000</span>
</div>
<div class="lesson-dup" id="lessonDup" hidden>
<span>За эту группу и дату отчёт уже есть — создать второй нельзя.</span>
<button type="button" class="btn-link" id="lessonDupGo">Перейти к существующему отчёту</button>
</div>
<label class="toggle lesson-ai-toggle" id="lessonAiRow">
<input type="checkbox" id="lessonAiCheck" checked>
<span class="toggle-track"><span class="toggle-thumb"></span></span>
@@ -569,6 +573,13 @@ function ensureLessonModal() {
if (id) openLessonVersions(id);
});
groupSel.addEventListener('change', () => prefetchLessonForGroupDate());
modal.querySelector('#lessonDate').addEventListener('change', () => prefetchLessonForGroupDate());
modal.querySelector('#lessonDupGo').addEventListener('click', () => {
const id = modal.querySelector('#lessonDup').dataset.id;
if (!id) return;
closeLessonModal();
openLessonModal({ id });
});
modal.querySelector('#lessonForm').addEventListener('submit', e => submitLessonModal(e));
return modal;
}
@@ -601,9 +612,8 @@ async function submitLessonModal(e) {
const data = await res.json().catch(() => ({}));
if (!res.ok) {
if (res.status === 409 && data.id) {
closeLessonModal();
openLessonModal({ id: data.id });
showToast('За эту дату отчёт уже есть — открыт для редактирования');
showLessonDuplicate(modal, data.id);
showToast('За эту группу и дату отчёт уже есть — новый создать нельзя');
return;
}
showToast(data.error || 'Не удалось сохранить отчёт');
@@ -625,22 +635,35 @@ async function submitLessonModal(e) {
}
}
let lessonDupSeq = 0;
async function prefetchLessonForGroupDate() {
const modal = document.getElementById('lessonModal');
if (!modal || !modal.classList.contains('open')) return;
if (modal.querySelector('#lessonId').value) return;
const groupId = modal.querySelector('#lessonGroup').value;
const date = modal.querySelector('#lessonDate').value;
if (!groupId || !date) return;
if (!groupId || !date) { showLessonDuplicate(modal, null); return; }
const seq = ++lessonDupSeq;
try {
const q = `group_id=${encodeURIComponent(groupId)}&date_from=${encodeURIComponent(date)}&date_to=${encodeURIComponent(date)}&limit=1`;
const res = await fetch(`${API}/api/lesson-reports?${q}`, { headers: hdr() });
if (!res.ok) return;
if (seq !== lessonDupSeq) return;
if (!res.ok) { showLessonDuplicate(modal, null); return; }
const data = await res.json();
if (seq !== lessonDupSeq) return;
const item = (data.items || [])[0];
if (!item) return;
fillLessonModalFromReport(modal, item);
} catch {}
showLessonDuplicate(modal, item ? item.id : null);
} catch {
if (seq === lessonDupSeq) showLessonDuplicate(modal, null);
}
}
function showLessonDuplicate(modal, existingId) {
const row = modal.querySelector('#lessonDup');
if (!row) return;
row.dataset.id = existingId ? String(existingId) : '';
row.hidden = !existingId;
}
function fillLessonModalFromReport(modal, r) {
@@ -686,7 +709,9 @@ function updateLessonAiRow(modal, r) {
}
function resetLessonModalFields(modal) {
lessonDupSeq++;
modal.querySelector('#lessonId').value = '';
showLessonDuplicate(modal, null);
modal.querySelector('#lessonDate').value = lessonTodayIso();
modal.querySelector('#lessonTime').value = lessonNowTime();
const textEl = modal.querySelector('#lessonText');