feat(lesson-ai): проверка отчёта о занятии по шаблону + история версий
Галочка «Проверить по шаблону» в окне отчёта отправляет текст модели: совпал с шаблоном — остаётся как есть (skipped), не совпал — переписывается в деловом виде (done). Обработка идёт в фоне, HTTP-запрос не ждёт модель, оригинал тьютора сохраняется в text_original. - схема: text_original/text_ai/ai_status/ai_checked_at/ai_error в lesson_reports, таблица lesson_report_versions, ensureLessonReportsTable() - настройки lesson_ai_enabled и lesson_ai_prompt (раздел sec-lesson-ai), значения только 'true'/'false' - worker.js: createLessonReportChecker (FOR UPDATE OF lr SKIP LOCKED, до 3 попыток), хук назовён notifyEvent — notify в createPhotoEnhanceWorker уже занят будильником - server.js: wakeLessonAiWorker, onLessonAiDone (версия, аудит с diff, уведомление lesson.ai.formatted, SSE lesson_report_status), маршруты /versions, /versions/:id/restore и /ai/revert - aiComplete вместо aiCorrectText: общий вызов модели с таймаутом - бэкап/восстановление: lesson_reports и lesson_report_versions в payload - фронтенд: openLessonVersions/restoreLessonVersion в admin.js, бейджи статусов в lessons.js, лейблы аудита, renderAuditPager - docs: раздел 3d в AGENTS.md и Agent Workflow, пункт в README - тесты: контракт lesson-report и настройки уведомления в api.smoketest.js
This commit is contained in:
@@ -662,6 +662,20 @@ body[data-page=settings] .page-head{align-items:flex-end}
|
||||
.lesson-modal .lesson-row{display:grid;grid-template-columns:1fr 1fr;gap:12px}
|
||||
.lesson-modal .hint{font-size:.78rem;color:var(--muted);text-align:right}
|
||||
.lesson-modal textarea{min-height:130px}
|
||||
.lesson-modal .lesson-ai-toggle{background:var(--bg);border:1px solid var(--border);border-radius:10px}
|
||||
.lesson-ai-status{font-size:.78rem;color:var(--muted);line-height:1.45;margin-top:-4px}
|
||||
.lesson-ai-status.err{color:#ef4444}
|
||||
.lesson-versions{display:flex;flex-direction:column;gap:8px;max-height:56vh;overflow-y:auto;margin:4px 0}
|
||||
.lesson-version{border:1px solid var(--border);border-radius:var(--radius);padding:10px 12px;background:var(--bg)}
|
||||
.lesson-version.current{border-color:var(--accent);background:rgba(37,99,235,.06)}
|
||||
.lesson-version-head{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
||||
.lesson-version-label{font-size:.78rem;font-weight:600}
|
||||
.lesson-version-time{font-size:.74rem;color:var(--muted);margin-left:auto}
|
||||
.lesson-version-text{font-size:.82rem;line-height:1.5;white-space:pre-wrap;word-break:break-word;margin-top:6px}
|
||||
.lesson-version-foot{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:8px}
|
||||
.lesson-version-author{font-size:.74rem;color:var(--muted)}
|
||||
.lesson-version-foot .btn-link{margin-left:auto;padding:4px 10px;font-size:.75rem}
|
||||
.lesson-item .ai-badge{margin-left:0}
|
||||
@media(max-width:560px){
|
||||
.lesson-modal .lesson-row{grid-template-columns:1fr}
|
||||
}
|
||||
|
||||
+154
-2
@@ -178,6 +178,7 @@ const NOTIFY_ICONS = {
|
||||
'backup.create': 'download',
|
||||
'system.test': 'send',
|
||||
'lesson.report': 'notebook-pen',
|
||||
'lesson.ai.formatted': 'file-check',
|
||||
};
|
||||
const NOTIFY_LEVELS = { warning: 'Предупреждение', critical: 'Важно', info: '' };
|
||||
|
||||
@@ -540,7 +541,14 @@ function ensureLessonModal() {
|
||||
placeholder="Например: разобрали циклы for и while, решали задачи на списки, домашнее задание — функции"></textarea>
|
||||
<span class="hint" id="lessonHint">0 / 5000</span>
|
||||
</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>
|
||||
<span class="toggle-text"><b>Проверить по шаблону делового сообщения</b><small>ИИ сверит текст с шаблоном из настроек и, если не совпадёт, оформит его как деловое сообщение</small></span>
|
||||
</label>
|
||||
<div class="lesson-ai-status" id="lessonAiStatus" hidden></div>
|
||||
<div class="card-foot" style="justify-content:flex-end;gap:8px">
|
||||
<button type="button" class="btn-primary ghost" id="lessonVersionsBtn" hidden>История версий</button>
|
||||
<button type="button" class="btn-primary ghost" id="lessonCancel">Отмена</button>
|
||||
<button type="submit" class="btn-primary" id="lessonSubmit">Сохранить</button>
|
||||
</div>
|
||||
@@ -556,6 +564,10 @@ function ensureLessonModal() {
|
||||
textEl.addEventListener('input', () => { hintEl.textContent = `${textEl.value.length} / 5000`; });
|
||||
modal.addEventListener('click', e => { if (e.target === modal) closeLessonModal(); });
|
||||
modal.querySelector('#lessonCancel').addEventListener('click', closeLessonModal);
|
||||
modal.querySelector('#lessonVersionsBtn').addEventListener('click', () => {
|
||||
const id = modal.querySelector('#lessonId').value;
|
||||
if (id) openLessonVersions(id);
|
||||
});
|
||||
groupSel.addEventListener('change', () => prefetchLessonForGroupDate());
|
||||
modal.querySelector('#lessonForm').addEventListener('submit', e => submitLessonModal(e));
|
||||
return modal;
|
||||
@@ -570,6 +582,7 @@ async function submitLessonModal(e) {
|
||||
const timeEl = modal.querySelector('#lessonTime');
|
||||
const textEl = modal.querySelector('#lessonText');
|
||||
const submitBtn = modal.querySelector('#lessonSubmit');
|
||||
const aiCheckEl = modal.querySelector('#lessonAiCheck');
|
||||
const groupId = groupSel.value;
|
||||
const date = dateEl.value;
|
||||
const text = textEl.value.trim();
|
||||
@@ -577,12 +590,13 @@ async function submitLessonModal(e) {
|
||||
if (!date) { showToast('Укажите дату занятия'); return; }
|
||||
if (!text) { showToast('Введите текст отчёта'); textEl.focus(); return; }
|
||||
const id = modal.querySelector('#lessonId').value;
|
||||
const aiCheck = !!aiCheckEl && aiCheckEl.checked;
|
||||
submitBtn.disabled = true;
|
||||
try {
|
||||
const res = await fetch(`${API}/api/lesson-reports${id ? '/' + id : ''}`, {
|
||||
method: id ? 'PUT' : 'POST',
|
||||
headers: hdrJson(),
|
||||
body: JSON.stringify({ group_id: Number(groupId), lesson_date: date, lesson_time: timeEl.value || null, text }),
|
||||
body: JSON.stringify({ group_id: Number(groupId), lesson_date: date, lesson_time: timeEl.value || null, text, ai_check: aiCheck }),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
@@ -596,7 +610,13 @@ async function submitLessonModal(e) {
|
||||
return;
|
||||
}
|
||||
closeLessonModal();
|
||||
showToast(id ? 'Отчёт о занятии обновлён' : 'Отчёт о занятии сохранён');
|
||||
if (data.ai_status === 'pending' || data.ai_status === 'processing') {
|
||||
showToast('Отчёт сохранён — ИИ проверяет текст по шаблону');
|
||||
} else if (data.ai_status === 'error') {
|
||||
showToast(`Отчёт сохранён, проверка по шаблону не удалась: ${data.ai_error || 'ошибка ИИ'}`);
|
||||
} else {
|
||||
showToast(id ? 'Отчёт о занятии обновлён' : 'Отчёт о занятии сохранён');
|
||||
}
|
||||
if (typeof onLessonReportSaved === 'function') onLessonReportSaved();
|
||||
} catch {
|
||||
showToast('Ошибка сети');
|
||||
@@ -633,10 +653,38 @@ function fillLessonModalFromReport(modal, r) {
|
||||
textEl.value = r.text || '';
|
||||
textEl.dispatchEvent(new Event('input'));
|
||||
}
|
||||
const aiCheck = modal.querySelector('#lessonAiCheck');
|
||||
if (aiCheck) aiCheck.checked = false;
|
||||
updateLessonAiRow(modal, r);
|
||||
modal.querySelector('#lessonModalTitle').textContent = 'Редактирование отчёта';
|
||||
modal.querySelector('#lessonSubmit').textContent = 'Обновить';
|
||||
}
|
||||
|
||||
function lessonAiStatusText(r) {
|
||||
if (!r || !r.ai_status || r.ai_status === 'none') return '';
|
||||
if (r.ai_status === 'pending') return 'ИИ проверяет текст по шаблону…';
|
||||
if (r.ai_status === 'processing') return 'ИИ оформляет текст по шаблону…';
|
||||
if (r.ai_status === 'done' && r.text_ai) return 'ИИ оформил текст по шаблону';
|
||||
if (r.ai_status === 'skipped') return 'Текст уже соответствует шаблону';
|
||||
if (r.ai_status === 'reverted') return 'Возвращена исходная версия текста';
|
||||
if (r.ai_status === 'error') return `Проверка по шаблону не удалась: ${r.ai_error || 'ошибка ИИ'}`;
|
||||
return `Статус ИИ: ${r.ai_status}`;
|
||||
}
|
||||
|
||||
function updateLessonAiRow(modal, r) {
|
||||
const row = modal.querySelector('#lessonAiRow');
|
||||
const status = modal.querySelector('#lessonAiStatus');
|
||||
const versionsBtn = modal.querySelector('#lessonVersionsBtn');
|
||||
if (row) row.hidden = false;
|
||||
const text = lessonAiStatusText(r);
|
||||
if (status) {
|
||||
status.textContent = text;
|
||||
status.hidden = !text;
|
||||
status.classList.toggle('err', !!r && r.ai_status === 'error');
|
||||
}
|
||||
if (versionsBtn) versionsBtn.hidden = !modal.querySelector('#lessonId').value;
|
||||
}
|
||||
|
||||
function resetLessonModalFields(modal) {
|
||||
modal.querySelector('#lessonId').value = '';
|
||||
modal.querySelector('#lessonDate').value = lessonTodayIso();
|
||||
@@ -644,10 +692,114 @@ function resetLessonModalFields(modal) {
|
||||
const textEl = modal.querySelector('#lessonText');
|
||||
textEl.value = '';
|
||||
textEl.dispatchEvent(new Event('input'));
|
||||
const aiCheck = modal.querySelector('#lessonAiCheck');
|
||||
if (aiCheck) aiCheck.checked = true;
|
||||
updateLessonAiRow(modal, null);
|
||||
modal.querySelector('#lessonModalTitle').textContent = 'Что прошли на занятии';
|
||||
modal.querySelector('#lessonSubmit').textContent = 'Сохранить';
|
||||
}
|
||||
|
||||
const LESSON_VERSION_LABELS = {
|
||||
manual: 'Тьютор',
|
||||
ai: 'ИИ по шаблону',
|
||||
restore: 'Восстановлено'
|
||||
};
|
||||
|
||||
let currentLessonVersionId = '';
|
||||
|
||||
function ensureLessonVersionsModal() {
|
||||
let modal = document.getElementById('lessonVersionsModal');
|
||||
if (modal) return modal;
|
||||
modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.id = 'lessonVersionsModal';
|
||||
modal.innerHTML = `
|
||||
<div class="edit-modal lesson-modal">
|
||||
<h3>История версий отчёта</h3>
|
||||
<div class="lesson-versions" id="lessonVersionsList"><div class="empty">Загрузка…</div></div>
|
||||
<div class="card-foot" style="justify-content:flex-end;gap:8px">
|
||||
<button type="button" class="btn-primary ghost" id="lessonVersionsClose">Закрыть</button>
|
||||
</div>
|
||||
</div>`;
|
||||
document.body.appendChild(modal);
|
||||
modal.addEventListener('click', e => { if (e.target === modal) closeLessonVersions(); });
|
||||
modal.querySelector('#lessonVersionsClose').addEventListener('click', closeLessonVersions);
|
||||
return modal;
|
||||
}
|
||||
|
||||
function closeLessonVersions() {
|
||||
const modal = document.getElementById('lessonVersionsModal');
|
||||
if (modal) modal.classList.remove('open');
|
||||
}
|
||||
|
||||
function lessonVersionItemHtml(v, index) {
|
||||
const label = LESSON_VERSION_LABELS[v.source] || v.source || 'Версия';
|
||||
const who = v.author_name || v.author_username || '';
|
||||
return `
|
||||
<div class="lesson-version${index === 0 ? ' current' : ''}">
|
||||
<div class="lesson-version-head">
|
||||
<span class="lesson-version-label">${esc(label)}</span>
|
||||
<span class="lesson-version-time">${esc(notifyTime(v.created_at))}</span>
|
||||
</div>
|
||||
<div class="lesson-version-text">${esc(v.text || '')}</div>
|
||||
<div class="lesson-version-foot">
|
||||
${who ? `<span class="lesson-version-author">${esc(who)}</span>` : ''}
|
||||
${index === 0 ? '<span class="lesson-badge">текущая</span>' : ''}
|
||||
${index === 0 ? '' : `<button type="button" class="btn-link" data-restore="${v.id}" data-report="${v.lesson_report_id || currentLessonVersionId || ''}">Восстановить</button>`}
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function openLessonVersions(id) {
|
||||
currentLessonVersionId = String(id || '');
|
||||
const modal = ensureLessonVersionsModal();
|
||||
const list = modal.querySelector('#lessonVersionsList');
|
||||
list.innerHTML = '<div class="empty">Загрузка…</div>';
|
||||
modal.classList.add('open');
|
||||
renderIcons();
|
||||
try {
|
||||
const res = await fetch(`${API}/api/lesson-reports/${id}/versions`, { headers: hdr() });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) { list.innerHTML = `<div class="empty">${esc(data.error || 'Не удалось загрузить историю')}</div>`; return; }
|
||||
const items = data.items || [];
|
||||
if (!items.length) { list.innerHTML = '<div class="empty">Версий пока нет</div>'; return; }
|
||||
list.innerHTML = items.map(lessonVersionItemHtml).join('');
|
||||
renderIcons();
|
||||
} catch {
|
||||
list.innerHTML = '<div class="empty">Ошибка сети</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function restoreLessonVersion(id, versionId, btn) {
|
||||
if (!confirm('Восстановить эту версию отчёта?')) return;
|
||||
if (btn) btn.disabled = true;
|
||||
try {
|
||||
const res = await fetch(`${API}/api/lesson-reports/${id}/versions/${versionId}/restore`, { method: 'POST', headers: hdr() });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) { showToast(data.error || 'Не удалось восстановить версию'); return; }
|
||||
closeLessonVersions();
|
||||
showToast('Версия отчёта восстановлена');
|
||||
if (typeof onLessonReportSaved === 'function') onLessonReportSaved();
|
||||
} catch {
|
||||
showToast('Ошибка сети');
|
||||
} finally {
|
||||
if (btn) btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('click', e => {
|
||||
const btn = e.target.closest('[data-restore]');
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
let id = btn.dataset.report || currentLessonVersionId;
|
||||
if (!id) {
|
||||
const lessonModal = document.getElementById('lessonModal');
|
||||
id = (lessonModal && lessonModal.querySelector('#lessonId').value) || '';
|
||||
}
|
||||
if (!id) return;
|
||||
restoreLessonVersion(id, btn.dataset.restore, btn);
|
||||
});
|
||||
|
||||
async function openLessonModal(opts = {}) {
|
||||
const modal = ensureLessonModal();
|
||||
const groupSel = modal.querySelector('#lessonGroup');
|
||||
|
||||
+1
-1
@@ -150,7 +150,7 @@ nav a:hover{color:var(--text)}
|
||||
|
||||
<div class="field">
|
||||
<label>Что сделал</label>
|
||||
<textarea id="descInput" placeholder="Опиши, что проходил, что делал на занятии..." required></textarea>
|
||||
<textarea id="descInput" maxlength="300" placeholder="Опиши, что проходил, что делал на занятии..." required></textarea>
|
||||
</div>
|
||||
|
||||
<input type="text" name="website" id="hpWebsite" tabindex="-1" autocomplete="off" style="display:none" value="">
|
||||
|
||||
+9
-2
@@ -82,6 +82,13 @@ const ACTION_LABELS = {
|
||||
'ai.profile.activate': 'Выбран активный профиль ИИ',
|
||||
'export.student': 'Экспорт данных ученика',
|
||||
'export.group_files': 'Экспорт файлов группы (ZIP)',
|
||||
'lesson_report.create': 'Создан отчёт о занятии',
|
||||
'lesson_report.update': 'Изменён отчёт о занятии',
|
||||
'lesson_report.delete': 'Удалён отчёт о занятии',
|
||||
'lesson_report.ai.format': 'ИИ оформил отчёт по шаблону',
|
||||
'lesson_report.ai.error': 'Ошибка проверки отчёта по шаблону',
|
||||
'lesson_report.ai.revert': 'Возвращён оригинал отчёта',
|
||||
'lesson_report.version.restore': 'Восстановлена версия отчёта',
|
||||
};
|
||||
|
||||
const AUDIT_PAGE_SIZE = 50;
|
||||
@@ -202,7 +209,7 @@ function renderTargetDetails(target) {
|
||||
<details class="audit-raw"><summary>Технические данные (JSON)</summary><pre>${esc(fullTarget(target))}</pre></details>`;
|
||||
}
|
||||
|
||||
function renderPager(container, page, totalPages, onPageChange) {
|
||||
function renderAuditPager(container, page, totalPages, onPageChange) {
|
||||
if (totalPages <= 1) { container.innerHTML = ''; return; }
|
||||
const pages = [];
|
||||
const maxVisible = 5;
|
||||
@@ -251,7 +258,7 @@ async function loadAudit() {
|
||||
<td style="color:var(--muted);white-space:nowrap">${esc(r.ip || '')}</td>
|
||||
</tr>`).join('');
|
||||
}
|
||||
renderPager(document.getElementById('auditPager'), auditPage, auditTotal, goAuditPage);
|
||||
renderAuditPager(document.getElementById('auditPager'), auditPage, auditTotal, goAuditPage);
|
||||
}
|
||||
|
||||
function goAuditPage(p) {
|
||||
|
||||
@@ -25,16 +25,31 @@ function lessonAuthorText(r) {
|
||||
return upd ? `${name} · изменён ${notifyTime(r.updated_at)}` : name;
|
||||
}
|
||||
|
||||
function lessonAiBadgeHtml(r) {
|
||||
const st = r.ai_status;
|
||||
if (!st || st === 'none') return '';
|
||||
if (st === 'pending' || st === 'processing') return `<span class="ai-badge ai-badge-ic ai-badge-processing" title="ИИ проверяет текст по шаблону"><i data-lucide="loader"></i></span>`;
|
||||
if (st === 'done' && r.text_ai) return '<span class="ai-badge ai-badge-done" title="ИИ оформил текст по шаблону">ИИ</span>';
|
||||
if (st === 'skipped') return '<span class="ai-badge ai-badge-skipped" title="Текст уже соответствует шаблону">По шаблону</span>';
|
||||
if (st === 'reverted') return '<span class="ai-badge ai-badge-reverted" title="Возвращена исходная версия текста">Оригинал</span>';
|
||||
if (st === 'error') return `<span class="ai-badge ai-badge-error" title="Проверка по шаблону не удалась: ${esc(r.ai_error || 'ошибка ИИ')}">Ошибка ИИ</span>`;
|
||||
return `<span class="ai-badge ai-badge-ic ai-badge-${esc(st)}" title="Статус ИИ: ${esc(st)}">${esc(st)}</span>`;
|
||||
}
|
||||
|
||||
function lessonItemHtml(r) {
|
||||
const isToday = String(r.lesson_date).slice(0, 10) === lessonTodayIso();
|
||||
const author = lessonAuthorText(r);
|
||||
const canRevert = !!r.text_original && r.text_original !== r.text;
|
||||
return `
|
||||
<div class="lesson-item" data-lesson="${r.id}">
|
||||
<div class="lesson-item-head">
|
||||
<span class="lesson-group">${esc(r.group_name || '')}</span>
|
||||
<span class="lesson-when"><i data-lucide="clock-3"></i>${esc(lessonWhenText(r))}</span>
|
||||
${isToday ? '<span class="lesson-badge today">сегодня</span>' : ''}
|
||||
${lessonAiBadgeHtml(r)}
|
||||
<div class="lesson-actions">
|
||||
<button type="button" class="btn-link" data-versions="${r.id}">История</button>
|
||||
${canRevert ? `<button type="button" class="btn-link" data-revert="${r.id}" title="Вернуть исходный текст тьютора">↩ Оригинал</button>` : ''}
|
||||
<button type="button" class="btn-link" data-edit="${r.id}">Редактировать</button>
|
||||
${isAdmin() ? `<button type="button" class="btn-link danger" data-del="${r.id}">Удалить</button>` : ''}
|
||||
</div>
|
||||
@@ -108,6 +123,15 @@ async function deleteLesson(id) {
|
||||
loadLessons();
|
||||
}
|
||||
|
||||
async function revertLessonAi(id) {
|
||||
if (!confirm('Вернуть исходный текст отчёта (до оформления по шаблону)?')) return;
|
||||
const res = await fetch(`${API}/api/lesson-reports/${id}/ai/revert`, { method: 'POST', headers: hdr() });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) { showToast(data.error || 'Не удалось вернуть оригинал'); return; }
|
||||
showToast('Возвращён исходный текст отчёта');
|
||||
loadLessons();
|
||||
}
|
||||
|
||||
function onLessonReportSaved() {
|
||||
loadLessons();
|
||||
}
|
||||
@@ -115,6 +139,10 @@ function onLessonReportSaved() {
|
||||
document.getElementById('lessonsList').addEventListener('click', e => {
|
||||
const edit = e.target.closest('[data-edit]');
|
||||
if (edit) { openLessonModal({ id: edit.dataset.edit }); return; }
|
||||
const versions = e.target.closest('[data-versions]');
|
||||
if (versions) { openLessonVersions(versions.dataset.versions); return; }
|
||||
const revert = e.target.closest('[data-revert]');
|
||||
if (revert) { revertLessonAi(revert.dataset.revert); return; }
|
||||
const del = e.target.closest('[data-del]');
|
||||
if (del) { deleteLesson(del.dataset.del); }
|
||||
});
|
||||
@@ -131,6 +159,26 @@ document.getElementById('searchInput').addEventListener('input', () => {
|
||||
searchTimer = setTimeout(() => { page = 1; loadLessons(); }, 350);
|
||||
});
|
||||
|
||||
let lessonLiveSource = null;
|
||||
|
||||
function connectLessonLive() {
|
||||
if (lessonLiveSource) { lessonLiveSource.close(); lessonLiveSource = null; }
|
||||
const connect = () => {
|
||||
const t = sessionStorage.getItem('authToken');
|
||||
if (!t) return;
|
||||
lessonLiveSource = new EventSource(`${API}/api/events?token=${encodeURIComponent(t)}`);
|
||||
lessonLiveSource.addEventListener('lesson_report_status', () => loadLessons());
|
||||
lessonLiveSource.onerror = () => {
|
||||
if (lessonLiveSource) { lessonLiveSource.close(); lessonLiveSource = null; }
|
||||
setTimeout(connect, 5000);
|
||||
};
|
||||
};
|
||||
connect();
|
||||
window.addEventListener('beforeunload', () => { if (lessonLiveSource) lessonLiveSource.close(); });
|
||||
}
|
||||
|
||||
connectLessonLive();
|
||||
|
||||
(async () => {
|
||||
if (!(await checkAuth())) return;
|
||||
buildSidebar(document.body.dataset.page);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const DIRTY_FIELDS = ['systemName', 'spamInterval', 'footerLeft', 'footerRight', 'aiPrompt', 'aiAutoCheck', 'shareShowMessage', 'shareShowDate', 'shareShowNames', 'shareShowPhotos', 'cookieNoticeText', 'photoResolution', 'photoQuality', 'photoEngine', 'camEnabled', 'photoAiFaceMode', 'photoAiFaceModel', 'photoAiDevicePref', 'trashPurgeDays', 'notifyEnabled', 'notifyRetentionDays'];
|
||||
const DIRTY_FIELDS = ['systemName', 'spamInterval', 'footerLeft', 'footerRight', 'aiPrompt', 'aiAutoCheck', 'lessonAiPrompt', 'lessonAiEnabled', 'shareShowMessage', 'shareShowDate', 'shareShowNames', 'shareShowPhotos', 'cookieNoticeText', 'photoResolution', 'photoQuality', 'photoEngine', 'camEnabled', 'photoAiFaceMode', 'photoAiFaceModel', 'photoAiDevicePref', 'trashPurgeDays', 'notifyEnabled', 'notifyRetentionDays'];
|
||||
const NOTIFY_IDS = [];
|
||||
const SECTION_IDS = ['sec-system', 'sec-stack', 'sec-brand', 'sec-antispam', 'sec-share', 'sec-ai', 'sec-backup', 'sec-trash', 'sec-form', 'sec-notify', 'sec-photo', 'sec-photo-ai', 'sec-bans'];
|
||||
const SECTION_IDS = ['sec-system', 'sec-stack', 'sec-brand', 'sec-antispam', 'sec-share', 'sec-ai', 'sec-lesson-ai', 'sec-backup', 'sec-trash', 'sec-form', 'sec-notify', 'sec-photo', 'sec-photo-ai', 'sec-bans'];
|
||||
const SPAM_MAX = 10080;
|
||||
const BAN_REASONS = {
|
||||
'honeypot': 'Антиспам-поле',
|
||||
@@ -206,6 +206,8 @@ async function loadSettings() {
|
||||
document.getElementById('footerRight').value = s.footer_right || '';
|
||||
document.getElementById('aiPrompt').value = s.ai_prompt || '';
|
||||
document.getElementById('aiAutoCheck').checked = s.ai_autocheck_enabled !== 'false';
|
||||
document.getElementById('lessonAiEnabled').checked = s.lesson_ai_enabled !== 'false';
|
||||
document.getElementById('lessonAiPrompt').value = s.lesson_ai_prompt || '';
|
||||
document.getElementById('shareShowMessage').checked = s.share_show_student_message === 'true';
|
||||
document.getElementById('shareShowDate').checked = s.share_show_entry_date === 'true';
|
||||
document.getElementById('shareShowNames').checked = s.share_show_student_names !== 'false';
|
||||
@@ -807,6 +809,8 @@ async function saveSettings() {
|
||||
footer_right: document.getElementById('footerRight').value.trim(),
|
||||
ai_prompt: document.getElementById('aiPrompt').value.trim(),
|
||||
ai_autocheck_enabled: document.getElementById('aiAutoCheck').checked ? 'true' : 'false',
|
||||
lesson_ai_enabled: document.getElementById('lessonAiEnabled').checked ? 'true' : 'false',
|
||||
lesson_ai_prompt: document.getElementById('lessonAiPrompt').value.trim(),
|
||||
share_show_student_message: document.getElementById('shareShowMessage').checked ? 'true' : 'false',
|
||||
share_show_entry_date: document.getElementById('shareShowDate').checked ? 'true' : 'false',
|
||||
share_show_student_names: document.getElementById('shareShowNames').checked ? 'true' : 'false',
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<a href="#sec-photo-ai" data-nav="sec-photo-ai"><i data-lucide="sparkles"></i> Фото-ИИ</a>
|
||||
<a href="#sec-notify" data-nav="sec-notify"><i data-lucide="bell"></i> Уведомления</a>
|
||||
<a href="#sec-ai" data-nav="sec-ai"><i data-lucide="bot"></i> ИИ</a>
|
||||
<a href="#sec-lesson-ai" data-nav="sec-lesson-ai"><i data-lucide="notebook-pen"></i> Шаблон отчёта</a>
|
||||
<a href="#sec-backup" data-nav="sec-backup"><i data-lucide="database"></i> Бэкапы</a>
|
||||
<a href="#sec-trash" data-nav="sec-trash"><i data-lucide="trash-2"></i> Корзина</a>
|
||||
<a href="#sec-bans" data-nav="sec-bans"><i data-lucide="shield-off"></i> Блокировки</a>
|
||||
@@ -165,6 +166,24 @@
|
||||
<div class="ai-queue" id="aiQueueInfo"></div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card" id="sec-lesson-ai">
|
||||
<div class="card-head">
|
||||
<div class="ic"><i data-lucide="notebook-pen"></i></div>
|
||||
<h3>Шаблон отчёта о занятии</h3>
|
||||
</div>
|
||||
<p class="hint">Инструкция для нейросети: она сверяет текст отчёта тьютора с шаблоном делового сообщения. Если текст уже соответствует шаблону — остаётся без изменений, иначе приводится к деловому виду. Оригинал тьютора всегда сохраняется, результат можно посмотреть в истории версий.</p>
|
||||
<label class="toggle">
|
||||
<input type="checkbox" id="lessonAiEnabled">
|
||||
<span class="toggle-track"><span class="toggle-thumb"></span></span>
|
||||
<span class="toggle-text"><b>Проверять отчёты по шаблону</b><small>Применяется, когда тьютор включил галочку в окне отчёта о занятии</small></span>
|
||||
</label>
|
||||
<div class="settings-field">
|
||||
<label for="lessonAiPrompt">Шаблон и правила</label>
|
||||
<textarea id="lessonAiPrompt" class="settings-input mono" rows="12" spellcheck="false"></textarea>
|
||||
<span class="hint">Пусто — будет подставлен шаблон по умолчанию</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card">
|
||||
<div class="card-head">
|
||||
<div class="ic"><i data-lucide="boxes"></i></div>
|
||||
|
||||
Reference in New Issue
Block a user