feat(trainer): P7 пошаговое решение (репетитор) + P8 мат-клавиатура
- движок 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
+167
-10
@@ -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 @@
|
||||
<div class="tr-skill" id="tr-skill"></div>
|
||||
<div class="tr-eq" id="tr-eq">—</div>
|
||||
|
||||
<div class="tr-inrow">
|
||||
<span class="tr-eqx" id="tr-eqx">x =</span>
|
||||
<input class="tr-input" id="tr-input" type="text" inputmode="text" autocomplete="off"
|
||||
placeholder="ответ" aria-label="Ваш ответ"/>
|
||||
<button class="tr-btn tr-primary" id="tr-check" type="button">Проверить</button>
|
||||
<div id="tr-answerbox">
|
||||
<div class="tr-inrow">
|
||||
<span class="tr-eqx" id="tr-eqx">x =</span>
|
||||
<input class="tr-input" id="tr-input" type="text" inputmode="text" autocomplete="off"
|
||||
placeholder="ответ" aria-label="Ваш ответ"/>
|
||||
<button class="tr-btn tr-primary" id="tr-check" type="button">Проверить</button>
|
||||
</div>
|
||||
<div class="tr-keypad" id="tr-keypad"></div>
|
||||
<div class="tr-preview" id="tr-preview"></div>
|
||||
<div class="tr-feedback" id="tr-feedback"></div>
|
||||
</div>
|
||||
|
||||
<div class="tr-feedback" id="tr-feedback"></div>
|
||||
<div id="tr-stepbox" style="display:none">
|
||||
<div class="tr-steps" id="tr-steps"></div>
|
||||
<div class="tr-inrow">
|
||||
<input class="tr-input" id="tr-stepin" type="text" autocomplete="off"
|
||||
placeholder="следующий шаг, напр. 3x = 15" aria-label="Следующий шаг"/>
|
||||
<button class="tr-btn tr-primary" id="tr-stepcheck" type="button">Шаг</button>
|
||||
</div>
|
||||
<div class="tr-keypad" id="tr-keypad2"></div>
|
||||
<div class="tr-preview" id="tr-prev2"></div>
|
||||
<div class="tr-feedback" id="tr-stepfb"></div>
|
||||
</div>
|
||||
|
||||
<div class="tr-actions">
|
||||
<button class="tr-btn tr-ghost" id="tr-step-toggle" type="button" style="display:none">
|
||||
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg>
|
||||
Решить по шагам
|
||||
</button>
|
||||
<button class="tr-btn tr-ghost" id="tr-hint" type="button">
|
||||
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6M10 22h4M12 2a7 7 0 0 0-4 12.7c.6.5 1 1.3 1 2.1h6c0-.8.4-1.6 1-2.1A7 7 0 0 0 12 2Z"/></svg>
|
||||
Подсказка
|
||||
@@ -435,7 +472,9 @@
|
||||
applyInputMode();
|
||||
var inp = $('tr-input'); inp.value = ''; inp.disabled = false;
|
||||
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(false); // текстовые задачи — без пошагового режима
|
||||
}
|
||||
function genWordProblem() {
|
||||
var gb = $('tr-gen-btn'); if (gb) { gb.disabled = true; gb.textContent = 'Генерирую…'; }
|
||||
@@ -462,6 +501,8 @@
|
||||
// адаптивная сессия
|
||||
var smart = true, GOAL = 10;
|
||||
var sessAnswered = 0, sessEvents = [], reviewQ = [], summaryShown = false;
|
||||
// пошаговый режим (P7)
|
||||
var stepMode = false, stepPref = false, stepList = [];
|
||||
|
||||
function topicMastered(topicKey) {
|
||||
var ss = skillsOf(topicKey);
|
||||
@@ -502,6 +543,102 @@
|
||||
function setMode(done) {
|
||||
answered = done;
|
||||
$('tr-check').textContent = done ? 'Дальше' : 'Проверить';
|
||||
var sc = $('tr-stepcheck'); if (sc) sc.textContent = done ? 'Дальше' : 'Шаг';
|
||||
}
|
||||
// общие эффекты «задача решена» (из обычного ответа и из пошагового режима)
|
||||
function onSolved() {
|
||||
solved++; streak++;
|
||||
var card = $('tr-card'); if (card) card.classList.add('tr-correct');
|
||||
recordAnswer(true); submitAttempt(true);
|
||||
setMode(true); updateStats();
|
||||
}
|
||||
|
||||
// ── мат-клавиатура + live-превью (P8) ──
|
||||
var KEYS = [
|
||||
{ t: '(', ins: '(' }, { t: ')', ins: ')' }, { t: 'x', ins: 'x' },
|
||||
{ t: '/', ins: '/' }, { t: '^', ins: '^' }, { t: '√', ins: 'sqrt(' }, { t: ';', ins: '; ' },
|
||||
{ bksp: true }
|
||||
];
|
||||
function buildKeypad(container, inputId, previewId) {
|
||||
if (!container) return;
|
||||
container.innerHTML = KEYS.map(function (k, i) {
|
||||
if (k.bksp) return '<button class="tr-key" type="button" data-k="' + i + '" aria-label="Стереть"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 4H8L2 12l6 8h13a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"/><path d="m18 9-6 6M12 9l6 6"/></svg></button>';
|
||||
return '<button class="tr-key" type="button" data-k="' + i + '">' + esc(k.t) + '</button>';
|
||||
}).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 '<div class="tr-step-line"><span class="tr-step-ic">' + ICON.ok + '</span><span class="tr-step-tex">' + math + '</span></div>';
|
||||
}).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 + ' <span>Готово!</span>';
|
||||
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 + ' <span>Верно!</span> ' + 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 = '<h4>Подсказка</h4>' + 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 + ' темах · умная тренировка ведёт от простого к сложному и возвращает ошибки · прогресс сохраняется.';
|
||||
|
||||
|
||||
@@ -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** (неравенства/системы/ЦТ).
|
||||
Reference in New Issue
Block a user