feat(trainer): P2 — умная тренировка, интервальное повторение, итог сессии
- adaptive.js (TrainerAdaptive): nextSkill (in-session повтор → серверный due → прогрессия → удержание), onWrong/onCorrect (очередь повторения), sessionStats - умная тренировка на странице (тумблер, по умолч. вкл): авто-подбор навыка от простого к сложному, возврат ошибок - сессия из 10 задач + экран «Итог сессии» (верно/точность/навыки/стоит повторить); неверный ответ авто-показывает решение - сервер: SR-поля box+due_at на practice_progress (мигр.082, Leitner 0/1/3/7/16/30 дн), listProgress отдаёт box/due_at/due - смоуки: adaptive 12/12, страница 23/23, practice.test.js 11/11 (+SR box/due); план P2 → DONE Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,12 +14,17 @@ const db = require('../db/db');
|
||||
|
||||
const MAX_SKILL = 120; // длина skill (TEXT)
|
||||
const MASTERY_STREAK = 5; // серия верных подряд для «освоено»
|
||||
// Интервалы повторения (дни) по уровню Leitner-коробки box 0..5.
|
||||
const INTERVAL_DAYS = [0, 1, 3, 7, 16, 30];
|
||||
|
||||
/* GET /api/practice/progress — прогресс текущего ученика по всем навыкам. */
|
||||
/* GET /api/practice/progress — прогресс текущего ученика по всем навыкам.
|
||||
* `due` (0/1) — навык пора повторить (срок прошёл или не назначен). */
|
||||
function listProgress(req, res) {
|
||||
const uid = req.user.id;
|
||||
const rows = db.prepare(`
|
||||
SELECT skill, solved, attempts, cur_streak, best_streak, mastered, updated_at
|
||||
SELECT skill, solved, attempts, cur_streak, best_streak, mastered, box, due_at,
|
||||
CASE WHEN due_at IS NULL OR due_at <= datetime('now') THEN 1 ELSE 0 END AS due,
|
||||
updated_at
|
||||
FROM practice_progress
|
||||
WHERE user_id = ?
|
||||
ORDER BY updated_at DESC, id DESC
|
||||
@@ -41,15 +46,20 @@ function submitAttempt(req, res) {
|
||||
|
||||
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 = ?'
|
||||
'SELECT id, solved, attempts, cur_streak, best_streak, mastered, box FROM practice_progress WHERE user_id = ? AND skill = ?'
|
||||
).get(uid, skill);
|
||||
|
||||
// Leitner: верно → box+1 (до 5), неверно → 0. Срок = сейчас + интервал(box).
|
||||
const prevBox = existing ? (existing.box || 0) : 0;
|
||||
const box = correct ? Math.min(prevBox + 1, 5) : 0;
|
||||
const dueMod = '+' + INTERVAL_DAYS[box] + ' days';
|
||||
|
||||
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);
|
||||
INSERT INTO practice_progress (user_id, skill, solved, attempts, cur_streak, best_streak, mastered, box, due_at, updated_at)
|
||||
VALUES (?, ?, ?, 1, ?, ?, ?, ?, datetime('now', ?), datetime('now'))
|
||||
`).run(uid, skill, correct ? 1 : 0, curStreak, curStreak, curStreak >= MASTERY_STREAK ? 1 : 0, box, dueMod);
|
||||
} else {
|
||||
const curStreak = correct ? (existing.cur_streak + 1) : 0;
|
||||
const bestStreak = Math.max(existing.best_streak || 0, curStreak);
|
||||
@@ -57,14 +67,18 @@ function submitAttempt(req, res) {
|
||||
db.prepare(`
|
||||
UPDATE practice_progress
|
||||
SET solved = solved + ?, attempts = attempts + 1,
|
||||
cur_streak = ?, best_streak = ?, mastered = ?, updated_at = datetime('now')
|
||||
cur_streak = ?, best_streak = ?, mastered = ?, box = ?, due_at = datetime('now', ?),
|
||||
updated_at = datetime('now')
|
||||
WHERE id = ?
|
||||
`).run(correct ? 1 : 0, curStreak, bestStreak, mastered, existing.id);
|
||||
`).run(correct ? 1 : 0, curStreak, bestStreak, mastered, box, dueMod, 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);
|
||||
const row = db.prepare(`
|
||||
SELECT skill, solved, attempts, cur_streak, best_streak, mastered, box, due_at,
|
||||
CASE WHEN due_at IS NULL OR due_at <= datetime('now') THEN 1 ELSE 0 END AS due,
|
||||
updated_at
|
||||
FROM practice_progress WHERE user_id = ? AND skill = ?
|
||||
`).get(uid, skill);
|
||||
res.json({ ok: true, progress: row, masteryStreak: MASTERY_STREAK });
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- 082: SR-поля тренажёра (интервальное повторение по навыкам, Фаза 2).
|
||||
--
|
||||
-- К practice_progress добавляем Leitner-«коробку» и срок следующего показа:
|
||||
-- box — уровень 0..5 (выше = увереннее освоено, реже повторяем).
|
||||
-- due_at — когда навык снова стоит показать (datetime). NULL = «как можно скорее».
|
||||
-- На верный ответ box растёт и срок отодвигается; на ошибку box сбрасывается в 0
|
||||
-- и срок = сейчас (навык всплывёт первым при следующем заходе). Адаптивный
|
||||
-- подборщик на клиенте показывает «просроченные» навыки (due_at <= now) раньше.
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
|
||||
ALTER TABLE practice_progress ADD COLUMN box INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE practice_progress ADD COLUMN due_at TEXT;
|
||||
Reference in New Issue
Block a user