From 277bddf1fd9da5576a478dedd860dd16d161ea30 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Thu, 25 Jun 2026 15:06:46 +0300 Subject: [PATCH] =?UTF-8?q?feat(trainer):=20P7=20=D0=BF=D0=BE=D1=88=D0=B0?= =?UTF-8?q?=D0=B3=D0=BE=D0=B2=D0=BE=D0=B5=20=D1=80=D0=B5=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20(=D1=80=D0=B5=D0=BF=D0=B5=D1=82=D0=B8=D1=82?= =?UTF-8?q?=D0=BE=D1=80)=20+=20P8=20=D0=BC=D0=B0=D1=82-=D0=BA=D0=BB=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=B0=D1=82=D1=83=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - движок checkStep(problem, line): шаг = равносильное уравнение (держится во всех корнях И не выполняется в не-корнях) → ловит арифметику, потерю корня, тождество; статусы equivalent/solved/wrong/identity/parse - страница: тумблер «Решить по шагам» (kind solve), ввод и проверка каждого шага, список принятых шагов (KaTeX + галочка), подсказка следующего шага, завершение по solved-форме; общий onSolved; stepPref между задачами - P8: экранная мат-клавиатура (( ) x / ^ √ ; ⌫, вставка в курсор, без либ) + live-превью KaTeX; для поля ответа и поля шага - ROADMAP_V2: P7+P8 → DONE; смоук движка 300/300 (T14 checkStep), страница 33/33 (шаг-сценарии) Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/js/trainer/_trainer_engine.js | 59 +++++++++ frontend/trainer.html | 177 +++++++++++++++++++++++-- plans/ai-trainer/ROADMAP_V2.md | 89 +++++++++++++ 3 files changed, 315 insertions(+), 10 deletions(-) create mode 100644 plans/ai-trainer/ROADMAP_V2.md diff --git a/frontend/js/trainer/_trainer_engine.js b/frontend/js/trainer/_trainer_engine.js index 8487af5..4128db4 100644 --- a/frontend/js/trainer/_trainer_engine.js +++ b/frontend/js/trainer/_trainer_engine.js @@ -413,11 +413,70 @@ return { ok: se.ok, reason: se.ok ? null : (se.reason || 'wrong'), value: raw, message: se.ok ? 'Верно!' : 'Пока неверно.' }; } + /* ── Пошаговое решение (репетитор): проверка одного шага-равенства ── + Шаг = равносильное уравнение (то же множество корней). Идея без решения + уравнений: уравнение L=R равносильно исходному ⟺ выполняется во ВСЕХ корнях + и НЕ выполняется в точках-не-корнях (то есть сужает x именно до корней). + Ловит арифметику (не держится в корне), потерю корня и тождество «0=0». */ + function _splitEq(s) { + var i = String(s).indexOf('='); + if (i <= 0 || i >= s.length - 1) return null; + if (s.indexOf('=', i + 1) !== -1) return null; // нет цепочек a=b=c и составных ==,<=,>= + return [s.slice(0, i).trim(), s.slice(i + 1).trim()]; + } + function _isConst(c, v) { + var e1 = {}, e2 = {}; e1[v] = 1.3; e2[v] = 2.7; + return Math.abs(c.fn(e1) - c.fn(e2)) < 1e-9; + } + function _isVarOnly(s, v) { return String(s).replace(/\s+/g, '') === v; } + function _isSolvedForm(lhs, rhs, v, roots) { + var cl = SE().compile(lhs), cr = SE().compile(rhs); + if (cl.error || cr.error) return false; + var lv = _isVarOnly(lhs, v), rv = _isVarOnly(rhs, v); + if (lv && _isConst(cr, v)) { var a = cr.fn({}); return roots.some(function (r) { return Math.abs(a - r) <= 1e-6; }); } + if (rv && _isConst(cl, v)) { var b = cl.fn({}); return roots.some(function (r) { return Math.abs(b - r) <= 1e-6; }); } + return false; + } + function checkStep(problem, line) { + var raw = String(line == null ? '' : line).trim(); + if (!raw) return { ok: false, status: 'empty', message: 'Введите шаг — равенство со знаком «=».' }; + var parts = _splitEq(raw); + if (!parts) return { ok: false, status: 'parse', message: 'Шаг — это одно равенство со знаком «=».' }; + var cl = SE().compile(parts[0]), cr = SE().compile(parts[1]); + if (cl.error || cr.error) return { ok: false, status: 'parse', message: 'Не понял выражение в шаге.' }; + + var v = problem.answerVar || 'x'; + var roots = (problem.answers && problem.answers.length) ? problem.answers : [problem.answer]; + + // держится во всех корнях? + for (var i = 0; i < roots.length; i++) { + var env = {}; env[v] = roots[i]; + var L = cl.fn(env), R = cr.fn(env); + if (Math.abs(L - R) > 1e-7 * Math.max(1, Math.abs(L), Math.abs(R))) + return { ok: false, status: 'wrong', message: 'Не равносильно: при ' + v + ' = ' + fmtNum(roots[i]) + ' равенство не выполняется.' }; + } + // сужает x до корней? (в не-корнях должно НЕ выполняться) + var total = 0, holds = 0; + for (var j = 0; j < _EQUIV_PTS.length; j++) { + var x = _EQUIV_PTS[j]; + if (roots.some(function (r) { return Math.abs(x - r) < 1e-6; })) continue; + total++; var e2 = {}; e2[v] = x; + var L2 = cl.fn(e2), R2 = cr.fn(e2); + if (Math.abs(L2 - R2) <= 1e-7 * Math.max(1, Math.abs(L2), Math.abs(R2))) holds++; + } + if (total > 0 && holds === total) + return { ok: false, status: 'identity', message: 'Это тождество — верно при любом ' + v + ' и не приближает к ответу.' }; + + var done = _isSolvedForm(parts[0], parts[1], v, roots); + return { ok: true, status: done ? 'solved' : 'equivalent', message: done ? 'Готово!' : 'Верный шаг.' }; + } + global.TrainerEngine = { instantiate: instantiate, generateBatch: generateBatch, verifyRoot: verifyRoot, checkStudentAnswer: checkStudentAnswer, + checkStep: checkStep, makeRng: makeRng, // мелочи наружу для билдера/тестов render: render, diff --git a/frontend/trainer.html b/frontend/trainer.html index a4a6759..3ca215f 100644 --- a/frontend/trainer.html +++ b/frontend/trainer.html @@ -120,7 +120,25 @@ .tr-feedback.bad { color: #b91c1c; background: var(--bad-soft); } .tr-feedback.warn { color: var(--warn); background: #fef3c7; font-weight: 600; } - .tr-actions { display: flex; flex-wrap: wrap; gap: 9px; justify-content: center; margin-top: 14px; } + .tr-actions { display: flex; flex-wrap: wrap; gap: 9px; justify-content: center; margin-top: 16px; } + + /* ── мат-клавиатура + live-превью (P8) ── */ + .tr-keypad { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; max-width: 440px; margin: 10px auto 0; } + .tr-key { font: inherit; font-size: .95rem; font-weight: 700; font-family: 'Cambria Math', serif; cursor: pointer; min-width: 40px; padding: 7px 10px; border-radius: 10px; border: 1px solid rgba(99,102,241,.18); background: rgba(255,255,255,.8); color: var(--accent-ink); transition: .14s var(--ease); } + .tr-key:hover { border-color: var(--g1); background: var(--accent-soft); transform: translateY(-1px); } + .tr-key:active { transform: translateY(0); } + .tr-key .ic { width: 16px; height: 16px; } + .tr-preview { text-align: center; margin: 12px auto 0; color: var(--ink-soft); } + .tr-preview:empty { display: none; } + .tr-preview .katex { font-size: 1.12em; } + + /* ── пошаговое решение / репетитор (P7) ── */ + .tr-steps { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; } + .tr-steps:empty { display: none; } + .tr-step-line { display: flex; align-items: center; gap: 12px; padding: 10px 14px; border-radius: 12px; background: linear-gradient(180deg, #f4fbf7, #ecf9f1); border: 1px solid rgba(16,185,129,.22); animation: trUp .25s var(--ease) both; } + .tr-step-ic { flex-shrink: 0; width: 22px; height: 22px; border-radius: 50%; background: var(--ok); color: #fff; display: inline-flex; align-items: center; justify-content: center; } + .tr-step-ic .ic { width: 14px; height: 14px; } + .tr-step-tex { font-family: 'Cambria Math', serif; font-size: 1.12rem; color: var(--ink); } .tr-solution { margin-top: 22px; padding: 18px 20px; border-radius: 16px; @@ -293,16 +311,35 @@
-
- x = - - +
+
+ x = + + +
+
+
+
-
+
+ '; + return ''; + }).join(''); + container.addEventListener('click', function (e) { + var b = e.target.closest('.tr-key'); if (!b) return; + var inp = $(inputId); if (!inp || inp.disabled) return; + var k = KEYS[+b.getAttribute('data-k')]; + if (k.bksp) backspaceAt(inp); else insertAt(inp, k.ins); + renderPreview(inp, $(previewId)); + }); + } + function insertAt(inp, text) { + var s = inp.selectionStart, e = inp.selectionEnd, v = inp.value; + if (s == null || e == null) { inp.value = v + text; inp.focus(); return; } + inp.value = v.slice(0, s) + text + v.slice(e); + var pos = s + text.length; inp.focus(); + try { inp.setSelectionRange(pos, pos); } catch (err) {} + } + function backspaceAt(inp) { + var s = inp.selectionStart, e = inp.selectionEnd, v = inp.value, pos; + if (s == null) { inp.value = v.slice(0, -1); inp.focus(); return; } + if (s !== e) { inp.value = v.slice(0, s) + v.slice(e); pos = s; } + else if (s > 0) { inp.value = v.slice(0, s - 1) + v.slice(s); pos = s - 1; } + else { inp.focus(); return; } + inp.focus(); try { inp.setSelectionRange(pos, pos); } catch (err) {} + } + function renderPreview(inp, prev) { + if (!prev) return; + var raw = (inp.value || '').trim(); + if (!raw) { prev.innerHTML = ''; return; } + var latex = TE.exprToLatex(raw); + prev.innerHTML = latex ? (kat(latex, false) || '') : ''; + } + + // ── пошаговое решение / репетитор (P7) ── + function canStep() { return !!(cur && cur.kind === 'solve'); } + function setStepMode(on) { + stepMode = !!(on && canStep()); + var ab = $('tr-answerbox'), sb = $('tr-stepbox'); + if (ab) ab.style.display = stepMode ? 'none' : ''; + if (sb) sb.style.display = stepMode ? '' : 'none'; + var tog = $('tr-step-toggle'); if (tog) tog.classList.toggle('on', stepMode); + if (stepMode) { + stepList = []; renderSteps(); + var fb = $('tr-stepfb'); fb.className = 'tr-feedback'; fb.textContent = ''; + var si = $('tr-stepin'); si.value = ''; si.disabled = false; + $('tr-prev2').innerHTML = ''; + setMode(false); + si.focus(); + } + } + function renderSteps() { + $('tr-steps').innerHTML = stepList.map(function (s) { + var latex = TE.exprToLatex(s); + var math = latex ? (kat(latex, false) || esc(TE.prettyMath(s))) : esc(TE.prettyMath(s)); + return '
' + ICON.ok + '' + math + '
'; + }).join(''); + } + function checkStepNow() { + if (answered) { advance(); return; } + var inp = $('tr-stepin'), fb = $('tr-stepfb'); + var r = TE.checkStep(cur, inp.value); + if (!r.ok) { + fb.className = 'tr-feedback ' + (r.status === 'wrong' ? 'bad' : 'warn'); + fb.innerHTML = (r.status === 'wrong' ? ICON.bad + ' ' : '') + esc(r.message); + return; + } + stepList.push(inp.value.trim()); + renderSteps(); + inp.value = ''; $('tr-prev2').innerHTML = ''; + if (r.status === 'solved') { + fb.className = 'tr-feedback ok'; fb.innerHTML = ICON.ok + ' Готово!'; + inp.disabled = true; onSolved(); + } else { + fb.className = 'tr-feedback ok'; fb.innerHTML = ICON.ok + ' Верный шаг — продолжай.'; + inp.focus(); + } } // Префикс «x =» и подсказка ввода зависят от типа задачи. function applyInputMode() { @@ -509,6 +646,7 @@ var multi = (k === 'roots' || k === 'simplify'); var eqx = $('tr-eqx'); if (eqx) eqx.style.display = multi ? 'none' : ''; $('tr-input').placeholder = (k === 'roots') ? 'корни через ;' : (k === 'simplify') ? 'упрощённое выражение' : 'ответ'; + var tog = $('tr-step-toggle'); if (tog) tog.style.display = canStep() ? '' : 'none'; } // Текст ответа в фидбеке/раскрытии — по типу задачи. function answerLabel() { @@ -546,8 +684,10 @@ var fb = $('tr-feedback'); fb.className = 'tr-feedback'; fb.textContent = ''; $('tr-solution').style.display = 'none'; $('tr-solution').innerHTML = ''; var card = $('tr-card'); if (card) { card.classList.remove('tr-correct'); card.classList.remove('tr-wrong'); } + var pv = $('tr-preview'); if (pv) pv.innerHTML = ''; setMode(false); inp.focus(); + setStepMode(stepPref); // сохраняем выбор «по шагам» между задачами (для kind solve) } // фоновая отправка попытки на сервер (прогресс/мастерство) @@ -622,6 +762,7 @@ if (giveUp && !answered) { streak = 0; $('tr-input').disabled = true; + var si = $('tr-stepin'); if (si) si.disabled = true; var fb = $('tr-feedback'); fb.className = 'tr-feedback'; if (cur.kind === 'roots' || cur.kind === 'simplify') fb.textContent = 'Ответ: ' + answerLabel(); else setMath(fb, 'x = ' + cur.answer, 'Ответ: x = ' + fmt(cur.answer), false); @@ -641,13 +782,12 @@ $('tr-input').disabled = true; setMode(true); if (r.ok) { - solved++; streak++; fb.className = 'tr-feedback ok'; - $('tr-card').classList.add('tr-correct'); var lbl = (cur.kind === 'roots' || cur.kind === 'simplify') ? esc(answerLabel()) : (kat('x = ' + cur.answer, false) || esc('x = ' + fmt(cur.answer))); fb.innerHTML = ICON.ok + ' Верно! ' + lbl; - recordAnswer(true); submitAttempt(true); + $('tr-input').disabled = true; + onSolved(); } else { streak = 0; fb.className = 'tr-feedback bad'; fb.innerHTML = ICON.bad + ' Неверно. Разбери решение и реши похожую.'; @@ -814,12 +954,29 @@ $('tr-skip').addEventListener('click', newProblem); $('tr-hint').addEventListener('click', function () { if (!cur) return; + if (stepMode) { + var sol = cur.solution || []; + var idx = Math.min(stepList.length, Math.max(0, sol.length - 1)); + var st = sol[idx]; + var fb = $('tr-stepfb'); fb.className = 'tr-feedback warn'; + fb.innerHTML = 'Подсказка: ' + (st && st.latex ? (kat(st.latex, false) || esc(st.tex || '')) : esc((st && (st.tex || st.note)) || ('x = ' + fmt(cur.answer)))); + return; + } var s = $('tr-solution'); s.innerHTML = '

Подсказка

' + stepHtml((cur.solution || [])[0] || { note: '', tex: 'x = ' + fmt(cur.answer), latex: null }, 1); s.style.display = 'block'; }); $('tr-solve').addEventListener('click', function () { if (cur) revealAnswer(true); }); $('tr-input').addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); check(); } }); + // P8 — мат-клавиатуры + live-превью + buildKeypad($('tr-keypad'), 'tr-input', 'tr-preview'); + buildKeypad($('tr-keypad2'), 'tr-stepin', 'tr-prev2'); + $('tr-input').addEventListener('input', function () { renderPreview($('tr-input'), $('tr-preview')); }); + $('tr-stepin').addEventListener('input', function () { renderPreview($('tr-stepin'), $('tr-prev2')); }); + // P7 — пошаговый режим + $('tr-step-toggle').addEventListener('click', function () { stepPref = !stepMode; setStepMode(!stepMode); }); + $('tr-stepcheck').addEventListener('click', checkStepNow); + $('tr-stepin').addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); checkStepNow(); } }); $('tr-note').textContent = gens.length + ' навыков в ' + topics.length + ' темах · умная тренировка ведёт от простого к сложному и возвращает ошибки · прогресс сохраняется.'; diff --git a/plans/ai-trainer/ROADMAP_V2.md b/plans/ai-trainer/ROADMAP_V2.md new file mode 100644 index 0000000..38ef027 --- /dev/null +++ b/plans/ai-trainer/ROADMAP_V2.md @@ -0,0 +1,89 @@ +# ИИ-Тренажёр — Roadmap v2 (Phase 7+) + +**Контекст.** P0–P6 готовы (`PLAN.md`): движок параметрических генераторов + SimExpr- +верификатор (+несколько корней, эквивалентность сэмплингом), 17 генераторов / 5 тем, +умная тренировка с интервальным повторением, LLM-задачи с серверной проверкой, пул, +авторинг/раздача учителем, аналитика-тепловая карта, визуальный редизайн. + +**Цель v2.** Превратить «проверяльщик ответов» в **репетитора** (ведёт по шагам, +объясняет ошибки) и расширить **охват** (классы 5–9, ЦТ) и **вовлечение**. + +**Инвариант (не нарушать).** Выражения — только SimExpr (⛔ без eval). Любая задача +проходит проверку подстановкой/сэмплингом; неверная ученику не попадает. Тексты +экранируются. Каждая фаза — со смоуками/тестами и коммитом. + +--- + +## P7 — Пошаговое решение (репетитор) ⭐ — DONE +**Сделано:** движок `TE.checkStep(problem, line)` — шаг = равносильное уравнение +(держится во всех корнях И не выполняется в не-корнях → ловит арифметику, потерю +корня, тождество `0=0`); статусы equivalent/solved/wrong/identity/parse. Страница: +тумблер «Решить по шагам» (для kind solve), ввод шагов с проверкой каждого, список +принятых шагов (KaTeX + зелёная галочка), подсказка следующего шага, завершение по +solved-форме `x=c` → общий `onSolved` (засчитывается как решение). stepPref хранит выбор +между задачами. Смоук движка T14 + страницы шаг-сценарий. + +Ученик решает по шагам, движок проверяет КАЖДЫЙ шаг. +- Режим «по шагам»: ввод следующей строки преобразования → проверка эквивалентности + предыдущему (reuse `_sampleEquiv`; для уравнений — сохранение множества корней) + + прогресс к ответу. +- Подсказка следующего шага; «застрял» → раскрыть шаг. Guided-вариант: заполнить + пропуски в данных шагах. +- **Ценность:** глубочайшая педагогика, уникальное отличие от «answer-checker». + +## P8 — Математический ввод ⭐ — DONE +**Сделано:** лёгкая экранная мат-клавиатура (`( ) x / ^ √ ; ⌫`, вставка в позицию +курсора, без библиотек) под полем ответа И под полем шага; live-превью KaTeX введённого +(показывается только при валидном разборе через `exprToLatex`). Переиспользуется и для +ответа, и для пошагового ввода. + +Удобный ввод дробей/степеней/корней (моб. + выражения). +- Лёгкая экранная мат-клавиатура (свои кнопки `/ ^ √ ( ) ± x`), live-превью KaTeX + введённого. Без тяжёлых библиотек. +- Синергия с P7 (ввод шагов) и с multi-root / simplify. + +## P9 — Разбор ошибок + сократические подсказки (LLM) +«Почему неверно» и подсказки, не выдавая ответ сразу. +- Правиловая детекция типовых ошибок (потерян знак, забыл поделить, арифметика) для + linear/quadratic — по разнице ответа ученика с «ответом при типичной ошибке». +- LLM-фолбэк «объясни мою ошибку» / «подскажи» через Квантик-ассистента + (`callLLMFailover`) — только ОБЪЯСНЕНИЯ (безопасно, не генерация задач). +- 3 уровня подсказок (намёк → шаг → решение). + +## P10 — Контент 5–9 классов + ЦТ +Расширить охват и связать с подготовкой к ЦТ/ЦЭ. +- Новые темы: арифметика/дроби/десятичные (5–6), степени, формулы сокр. умножения, + разложение на множители, **линейные неравенства** (новый тип ответа: парсинг и + нормализация отношения `x>3`/`x≤−2`), системы 2 лин. уравнений, линейная функция (k,b), + текстовые семьи (движение/работа/смеси) параметрически. +- Дерево навыков по таксономии exam-prep ЦТ (связь с готовым модулем экзамена). + +## P11 — Геймификация + карта навыков +Вовлечение через существующую инфраструктуру. +- XP/монеты/достижения (Квантик-геймификация) за решения/серии/мастерство; учёт + kill-switch геймификации. +- Карта-дерево навыков (визуализация прогресса) на странице/дашборде. +- Дневная цель + календарь серий. + +## P12 — Задания и журнал +Учительский рабочий процесс поверх раздачи. +- Задание: темы/навыки + цель (N решено / мастерство) + дедлайн → ученики видят, + прогресс трекается; учитель видит выполнение и результаты; интеграция с journal/homework. +- Апгрейд текущего `assign` (уведомление) до отслеживаемого задания (таблица). + +## P13 — Конструктор генераторов + управление пулом +Учитель создаёт ПАРАМЕТРИЧЕСКИЕ генераторы (не только одиночные задачи). +- Визуальный билдер: диапазоны `pick`, формулы `derive`, шаблоны `lhs/rhs`, ответ, + шаги решения + live-превью + валидация (отложенный «полный P4»). +- Управление пулом (ревью/правка/удаление), генерация по теме урока/§ учебника. + +--- + +## Сквозное +Тесты/смоуки на каждую фазу; доступность (ARIA, клавиатура, озвучка формул); +офлайн-режим (PWA) для параметрики; производительность. + +## Рекомендация +Начать с **P7 + P8** (репетитор + мат-ввод — сильная синергия, наибольший скачок +качества обучения), затем **P9** (разбор ошибок) — вместе дают эффект «личный +репетитор». Параллельный быстрый выигрыш по охвату — **P10** (неравенства/системы/ЦТ).