feat(trainer): ИИ-тренажёр — генераторы задач + SimExpr-верификатор, прогресс, фича-флаг
- движок _trainer_engine.js: instantiate/generateBatch/verifyRoot/checkStudentAnswer/exprToLatex - 5 генераторов уравнений 7 класса (generators.js), приём «корень-вперёд» → целые ответы - страница /trainer: KaTeX-рендер, чипы-темы, мгновенная проверка, подсказка/решение, авто-выбор навыка - прогресс practice_progress (мигр.081) + /api/practice/progress|attempt + LS.practiceProgressList/Submit - фича-флаг trainer: тумблер в админке (Модули), requireFeature, FEATURE_HREFS (скрытие сайдбара+редирект), MODULE_CATALOG - fix: подключён Lucide CDN на странице (иначе иконки сайдбара пустые) - тесты practice.test.js (10/10); план развития plans/ai-trainer/PLAN.md Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -993,6 +993,7 @@ const MODULE_CATALOG = [
|
||||
{ key: 'crossword', name: 'Кроссворд', url: '/crossword', desc: 'Игра-кроссворд по терминам предметов; закрепляет понятия, даёт XP.' },
|
||||
{ key: 'hangman', name: 'Виселица', url: '/hangman', desc: 'Игра «Виселица» по терминам предметов; закрепляет слова, даёт XP.' },
|
||||
{ key: 'quantik', name: 'Квантик: Законы Мира', url: '/quantik', desc: 'Физическая игра-головоломка: уровни на 2D-механике, звёзды и прогресс.' },
|
||||
{ key: 'trainer', name: 'Тренажёр', url: '/trainer', desc: 'ИИ-тренажёр: бесконечные сгенерированные задачи по темам с мгновенной проверкой ответа и прогрессом по навыкам.' },
|
||||
{ key: 'live_quiz', name: 'Live-викторина', url: '/live-quiz', desc: 'Викторина в реальном времени: учитель запускает, ученики отвечают одновременно.' },
|
||||
{ key: 'sitemap', name: 'Путеводитель', url: '/sitemap', desc: 'Карта-обзор всех разделов платформы со ссылками.' },
|
||||
{ key: 'sim_builder', name: 'Конструктор симуляций', url: '/sim-builder', desc: 'Авторинг 2D-симуляций (учитель/админ).' },
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
'use strict';
|
||||
/* Practice progress (ИИ-тренажёр, Фаза 0).
|
||||
*
|
||||
* Прогресс ученика по навыкам тренажёра. Навык = skill генератора; задачи
|
||||
* генерируются и проверяются на клиенте (детерминированно, подстановкой), а
|
||||
* сервер хранит только агрегаты. На каждую попытку клиент шлёт { skill, correct };
|
||||
* сервер делает upsert: solved/attempts, текущая и лучшая серия, флаг mastered.
|
||||
*
|
||||
* Стиль следует gameController / customSimController: node:sqlite db.prepare,
|
||||
* auth-only (роутер ставит authMiddleware), валидация входа без исполнения,
|
||||
* статусы 400. Прогресс всегда принадлежит req.user — проверка владения не нужна.
|
||||
*/
|
||||
const db = require('../db/db');
|
||||
|
||||
const MAX_SKILL = 120; // длина skill (TEXT)
|
||||
const MASTERY_STREAK = 5; // серия верных подряд для «освоено»
|
||||
|
||||
/* GET /api/practice/progress — прогресс текущего ученика по всем навыкам. */
|
||||
function listProgress(req, res) {
|
||||
const uid = req.user.id;
|
||||
const rows = db.prepare(`
|
||||
SELECT skill, solved, attempts, cur_streak, best_streak, mastered, updated_at
|
||||
FROM practice_progress
|
||||
WHERE user_id = ?
|
||||
ORDER BY updated_at DESC, id DESC
|
||||
`).all(uid);
|
||||
res.json({ progress: rows, masteryStreak: MASTERY_STREAK });
|
||||
}
|
||||
|
||||
/* POST /api/practice/attempt body: { skill, correct }
|
||||
* Upsert агрегата попытки. Валидация: skill строка ≤120; correct — boolean.
|
||||
* НИЧЕГО не исполняет (skill — лишь ключ). */
|
||||
function submitAttempt(req, res) {
|
||||
const uid = req.user.id;
|
||||
const b = req.body || {};
|
||||
|
||||
const skill = typeof b.skill === 'string' ? b.skill.trim() : '';
|
||||
if (!skill) return res.status(400).json({ error: 'skill обязателен' });
|
||||
if (skill.length > MAX_SKILL) return res.status(400).json({ error: `skill длиннее ${MAX_SKILL} символов` });
|
||||
if (typeof b.correct !== 'boolean') return res.status(400).json({ error: 'correct должно быть boolean' });
|
||||
|
||||
const correct = b.correct;
|
||||
const existing = db.prepare(
|
||||
'SELECT id, solved, attempts, cur_streak, best_streak, mastered FROM practice_progress WHERE user_id = ? AND skill = ?'
|
||||
).get(uid, skill);
|
||||
|
||||
if (!existing) {
|
||||
const curStreak = correct ? 1 : 0;
|
||||
db.prepare(`
|
||||
INSERT INTO practice_progress (user_id, skill, solved, attempts, cur_streak, best_streak, mastered, updated_at)
|
||||
VALUES (?, ?, ?, 1, ?, ?, ?, datetime('now'))
|
||||
`).run(uid, skill, correct ? 1 : 0, curStreak, curStreak, curStreak >= MASTERY_STREAK ? 1 : 0);
|
||||
} else {
|
||||
const curStreak = correct ? (existing.cur_streak + 1) : 0;
|
||||
const bestStreak = Math.max(existing.best_streak || 0, curStreak);
|
||||
const mastered = (existing.mastered || (curStreak >= MASTERY_STREAK)) ? 1 : 0;
|
||||
db.prepare(`
|
||||
UPDATE practice_progress
|
||||
SET solved = solved + ?, attempts = attempts + 1,
|
||||
cur_streak = ?, best_streak = ?, mastered = ?, updated_at = datetime('now')
|
||||
WHERE id = ?
|
||||
`).run(correct ? 1 : 0, curStreak, bestStreak, mastered, existing.id);
|
||||
}
|
||||
|
||||
const row = db.prepare(
|
||||
'SELECT skill, solved, attempts, cur_streak, best_streak, mastered, updated_at FROM practice_progress WHERE user_id = ? AND skill = ?'
|
||||
).get(uid, skill);
|
||||
res.json({ ok: true, progress: row, masteryStreak: MASTERY_STREAK });
|
||||
}
|
||||
|
||||
module.exports = { listProgress, submitAttempt };
|
||||
Reference in New Issue
Block a user