feat(trainer): A-добивка символов + C1 новые форматы (choice/verify/estimate)

A-добивка answerSym: суммы прогрессий → S, гипотенуза → c, катет/сторона → b
(+6 генераторов; устранена нестыковка ввода и решения).

C1 — новые форматы условий (движок + страница + 10 генераторов):
- kind 'choice' — выбор из вариантов; дистракторы из generator.distractors
  (правильный + до 3, перемешиваются детерминированно); страница рисует кнопки,
  клик = ответ, верный подсвечивается зелёным, выбранный неверный — красным.
- kind 'verify' — верно/неверно: claim (булева SimExpr) → две кнопки Верно/Неверно.
- kind 'estimate' — прикидка: ответ принимается в допуске tol (_checkEstimate).
Движок: problem.choices/tol; самопроверка choice (ровно 1 правильный); estimate
через verifyRoot + допуск. Страница: #tr-choices/#tr-choice-next, applyInputMode
переключает ввод↔варианты, renderChoices/submitChoice, advance сбрасывает.

Генераторы: ch-area-rect/ch-lin-basic/ch-pct-of/ch-pyth-hyp (выбор),
vf-frac-compare/vf-divisible/vf-pyth (обратная Пифагора)/vf-eq-root (верно/неверно),
est-product/est-percent (прикидка). Итого 199 генераторов.

Смоук v41 99634 проверки (choice: ≥2 варианта, ровно 1 правильный; estimate:
приём в допуске + отказ далёкого; рендер фигур; шаги→LaTeX); figures-смоук
19289/3180 на 53 геом-генераторах. Inline trainer.html парсится.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-29 16:41:07 +03:00
parent fa034fee7c
commit 382974461a
3 changed files with 262 additions and 8 deletions
+44
View File
@@ -330,6 +330,30 @@
answer = pair[avs[0]]; // запасной одиночный ответ
}
// ── новые форматы условий (Ф C1) ──
// choice — выбор из вариантов; verify — верно/неверно; estimate — ответ в допуске
var choices = null, tol = null;
if (kind === 'choice') {
var corr = answer;
var pool = [corr];
(gen.distractors || []).forEach(function (d) {
var v = evalExpr(d, env);
if (gen.integerAnswer) v = Math.round(v);
if (isFinite(v) && pool.every(function (x) { return Math.abs(x - v) > 1e-9; })) pool.push(v);
});
var opts = pool.slice(0, 4); // правильный + до 3 дистракторов
for (var oi = opts.length - 1; oi > 0; oi--) { // детерминированное перемешивание (rng)
var oj = Math.floor(rng() * (oi + 1)), tmp = opts[oi]; opts[oi] = opts[oj]; opts[oj] = tmp;
}
choices = opts.map(function (v) { return { label: fmtNum(v), correct: Math.abs(v - corr) < 1e-9 }; });
} else if (kind === 'verify') {
var claimTrue = truthy(evalExpr(gen.claim, env));
answer = claimTrue ? 1 : 0;
choices = [{ label: 'Верно', correct: claimTrue }, { label: 'Неверно', correct: !claimTrue }];
} else if (kind === 'estimate') {
tol = (gen.tol != null) ? Math.abs(evalExpr(String(gen.tol), env)) : Math.max(1, Math.abs(answer) * 0.05);
}
var lhsExpr = render(gen.lhs || 'x', env);
var rhsExpr = render(gen.rhs || 'x', env);
var sEnv = assign(env, { ans: answer });
@@ -372,6 +396,8 @@
figure: gen.figure || null, // спека чертежа (данные) — рисует TrainerFigures по params
figurePrompt: gen.figurePrompt || null, // краткое условие для режима «читать с чертежа»
answerSym: gen.answerSym || null, // обозначение искомой величины (P/S/C/d/…) — только для показа
choices: choices, // [{label, correct}] для kind choice/verify
tol: tol, // допуск для kind estimate
lhsExpr: lhsExpr,
rhsExpr: rhsExpr,
// система: по умолчанию показываем сами уравнения; но если задан gen.display
@@ -418,6 +444,12 @@
okSelf = _origIneqHolds(lhsExpr, rhsExpr, gen.dispOp || '<', answerVar, inside) &&
!_origIneqHolds(lhsExpr, rhsExpr, gen.dispOp || '<', answerVar, outside);
why = 'неравенство не согласовано с ответом';
} else if (kind === 'choice') {
okSelf = !!choices && choices.length >= 2 && choices.filter(function (c) { return c.correct; }).length === 1;
why = 'некорректный набор вариантов выбора';
} else if (kind === 'verify') {
okSelf = !!choices && choices.length === 2;
why = 'verify без вариантов';
} else if (kind === 'system') {
okSelf = !!(system && system.length) && system.every(function (e) {
var L = evalExpr(e.lhs, pair), R = evalExpr(e.rhs, pair);
@@ -469,6 +501,7 @@
if (problem.kind === 'roots') return _checkMultiRoot(problem, raw);
if (problem.kind === 'inequality') return _checkInequality(problem, raw);
if (problem.kind === 'system') return _checkSystem(problem, raw);
if (problem.kind === 'estimate') return _checkEstimate(problem, raw);
var c = SE().compile(raw);
if (c.error) {
@@ -582,6 +615,17 @@
return { ok: true, reason: null, message: 'Верно!' };
}
/* Оценка/прикидка: ответ верен, если попадает в допуск tol вокруг истинного значения. */
function _checkEstimate(problem, raw) {
var c = SE().compile(raw);
if (c.error) return { ok: false, reason: 'parse', value: null, message: 'Не понял ответ: ' + c.error };
var val = c.fn({});
if (!isFinite(val)) return { ok: false, reason: 'nan', value: val, message: 'Это не число.' };
var tol = (typeof problem.tol === 'number' && isFinite(problem.tol)) ? problem.tol : 0;
var ok = Math.abs(val - problem.answer) <= tol + 1e-9;
return { ok: ok, reason: ok ? null : 'wrong', value: val, message: ok ? 'Верно (в допуске)!' : 'Пока неверно.' };
}
/* Упрощение: ответ-выражение проверяем на эквивалентность сэмплингом. */
function _checkEquiv(problem, raw) {
var c = SE().compile(raw);
+157 -6
View File
@@ -547,7 +547,7 @@
/* гипотенуза по катетам (пифагорова тройка m,n) */
{
id: 'pyth-hyp', topic: 'g-pyth', order: 1, subject: 'geometry', grade: 8, kind: 'compute',
title: 'Гипотенуза (Пифагор)',
title: 'Гипотенуза (Пифагор)', answerSym: 'c',
figure: { type: 'right-triangle', a: 'a', b: 'b', c: 'c', unknown: 'c' },
figurePrompt: 'Найдите гипотенузу прямоугольного треугольника (отмечена «?»).',
pick: { m: [2, 5], n: [1, 4] }, constraint: 'm > n',
@@ -563,7 +563,7 @@
/* катет по гипотенузе и катету */
{
id: 'pyth-leg', topic: 'g-pyth', order: 2, subject: 'geometry', grade: 8, kind: 'compute',
title: 'Катет (Пифагор)',
title: 'Катет (Пифагор)', answerSym: 'b',
figure: { type: 'right-triangle', a: 'a', b: 'b', c: 'c', unknown: 'b' },
figurePrompt: 'Найдите неизвестный катет (отмечен «?»).',
pick: { m: [2, 5], n: [1, 4] }, constraint: 'm > n',
@@ -2088,7 +2088,7 @@
/* сумма n членов арифметической прогрессии */
{
id: 'prog-arith-sum', topic: 'progressions', order: 3, subject: 'algebra', grade: 9, kind: 'compute',
title: 'Сумма арифм. прогрессии',
title: 'Сумма арифм. прогрессии', answerSym: 'S',
pick: { a: [-8, 12], d: [-6, 6], n: [4, 12] }, require: 'd != 0',
derive: { an: 'a + (n - 1)*d', sum: 'n*(a + an)/2' },
lhs: 'x', rhs: '{n}*({a} + {an})/2', display: 'Арифметическая прогрессия: a₁ = {a}, d = {d}. Найдите сумму первых {n} членов.',
@@ -2173,7 +2173,7 @@
/* сумма n членов геометрической прогрессии */
{
id: 'prog-geom-sum', topic: 'progressions', order: 9, subject: 'algebra', grade: 9, kind: 'compute',
title: 'Сумма геом. прогрессии',
title: 'Сумма геом. прогрессии', answerSym: 'S',
pick: { b: [1, 4], q: [2, 3], n: [2, 5] },
derive: { qn: 'q^n', sum: 'b*(q^n - 1)/(q - 1)' },
lhs: 'x', rhs: '{b}*({q}^{n} - 1)/({q} - 1)', display: 'Геометрическая прогрессия: b₁ = {b}, q = {q}. Найдите сумму первых {n} членов.',
@@ -2187,7 +2187,7 @@
/* текстовая задача (ряды кресел) */
{
id: 'prog-arith-word', topic: 'progressions', order: 10, subject: 'algebra', grade: 9, kind: 'compute',
title: 'Задача (ряды кресел)',
title: 'Задача (ряды кресел)', answerSym: 'S',
pick: { a: [10, 20], d: [2, 5], n: [5, 12] },
derive: { an: 'a + (n - 1)*d', sum: 'n*(a + an)/2' },
lhs: 'x', rhs: '{n}*({a} + {an})/2', display: 'В первом ряду зала {a} кресел, в каждом следующем на {d} больше. Сколько всего кресел в {n} рядах?',
@@ -2659,7 +2659,7 @@
/* найти сторону по площади (обратная) */
{
id: 'area-rect-inverse', topic: 'g-area', order: 7, subject: 'geometry', grade: 8, kind: 'compute',
title: 'Сторона по площади',
title: 'Сторона по площади', answerSym: 'b',
figure: { type: 'rectangle', w: 'a', h: 'b', unknown: 'h', area: 'S' },
figurePrompt: 'Найдите неизвестную сторону прямоугольника.',
pick: { a: [2, 16], b: [2, 16] }, derive: { S: 'a*b', val: 'b' },
@@ -3036,6 +3036,153 @@
{ note: 'Циферблат — 360°, каждый час — 30°. В {H}:00 между стрелками {ang0}°.', tex: '' },
{ note: 'Берём меньший угол (не больше 180°).', tex: 'x = {ans}' }
]
},
/* ═══════════════════════════════════════════════════════════════════════
V4 C1 — новые форматы условий: choice (выбор), verify (верно/неверно),
estimate (прикидка в допуске). Разнообразие подачи во всех темах.
═══════════════════════════════════════════════════════════════════════ */
/* ── Выбор ответа ── */
/* площадь прямоугольника — выбор */
{
id: 'ch-area-rect', topic: 'g-area', order: 10, subject: 'geometry', grade: 8, kind: 'choice',
title: 'Площадь — выбор ответа',
figure: { type: 'rectangle', w: 'a', h: 'b' },
pick: { a: [3, 16], b: [3, 16] }, constraint: 'a != b',
derive: { val: 'a*b' }, answer: 'val', distractors: ['2*(a + b)', 'a + b', '2*a*b'], integerAnswer: true,
display: 'Чему равна площадь прямоугольника со сторонами {a} и {b}? Выберите ответ.',
solution: [
{ note: 'Площадь прямоугольника — произведение сторон (а не периметр!).', tex: 'S = {a} * {b}' },
{ note: 'Считаем.', tex: 'S = {val}' }
]
},
/* корень линейного уравнения — выбор */
{
id: 'ch-lin-basic', topic: 'linear-eq', order: 15, subject: 'algebra', grade: 7, kind: 'choice',
title: 'Корень уравнения — выбор',
pick: { a: [2, 9], b: [1, 20], root: [-9, 9] }, require: 'root != 0',
derive: { c: 'a*root + b' }, answer: 'root', distractors: ['-root', 'c - b', 'root + a'], integerAnswer: true,
display: 'Чему равен корень уравнения {a}x + {b} = {c}? Выберите ответ.',
solution: [
{ note: 'Переносим {b} вправо и делим на {a}.', tex: 'x = ({c} - {b}) / {a}' },
{ note: 'Получаем корень.', tex: 'x = {root}' }
]
},
/* процент от числа — выбор */
{
id: 'ch-pct-of', topic: 'percents', order: 10, subject: 'algebra', grade: 6, kind: 'choice',
title: 'Проценты — выбор ответа',
pick: { pidx: [2, 10], abase: [1, 15] },
derive: { p: 'pidx*5', a: 'abase*20', val: 'pidx*abase' }, answer: 'val',
distractors: ['p', 'val*2', 'a - val'], integerAnswer: true,
display: 'Сколько будет {p}% от числа {a}? Выберите ответ.',
solution: [
{ note: 'Процент — сотая доля: умножаем число на {p} и делим на 100.', tex: 'x = {a} * {p} / 100' },
{ note: 'Считаем.', tex: 'x = {val}' }
]
},
/* гипотенуза — выбор */
{
id: 'ch-pyth-hyp', topic: 'g-pyth', order: 7, subject: 'geometry', grade: 8, kind: 'choice',
title: 'Гипотенуза — выбор ответа',
figure: { type: 'right-triangle', a: 'a', b: 'b', c: 'c', unknown: 'c' },
pick: { m: [2, 5], n: [1, 4] }, constraint: 'm > n',
derive: { a: 'm*m - n*n', b: '2*m*n', c: 'm*m + n*n' }, answer: 'c',
distractors: ['a + b', 'c - 1', 'c + 2'], integerAnswer: true,
display: 'Чему равна гипотенуза прямоугольного треугольника с катетами {a} и {b}? Выберите ответ.',
solution: [
{ note: 'По теореме Пифагора c = √(a² + b²) (не сумма катетов!).', tex: 'c = sqrt({a}^2 + {b}^2)' },
{ note: 'Считаем.', tex: 'c = {c}' }
]
},
/* ── Верно / неверно ── */
/* сравнение дробей — верно/неверно */
{
id: 'vf-frac-compare', topic: 'fractions', order: 9, subject: 'algebra', grade: 6, kind: 'verify',
title: 'Сравнение дробей (верно?)',
pick: { a: [1, 7], b: [2, 9], c: [1, 7], d: [2, 9] }, constraint: 'a < b && c < d && a*d != c*b',
claim: 'a*d > c*b',
display: 'Верно ли, что {a}/{b} > {c}/{d}?',
solution: [
{ note: 'Сравниваем перекрёстные произведения: {a}·{d} и {c}·{b}.', tex: '' },
{ note: 'Первая дробь больше, если {a}·{d} больше {c}·{b}.', tex: '' }
]
},
/* делимость — верно/неверно */
{
id: 'vf-divisible', topic: 'gcd-lcm', order: 7, subject: 'algebra', grade: 5, kind: 'verify',
title: 'Делимость (верно?)',
pick: { N: [10, 99], k: [2, 9] }, derive: { rem: 'mod(N, k)' },
claim: 'mod(N, k) == 0',
display: 'Верно ли, что {N} делится нацело на {k}?',
solution: [
{ note: 'Остаток от деления {N} на {k} равен {rem}.', tex: '' },
{ note: 'Делится нацело только если остаток равен нулю.', tex: '' }
]
},
/* прямоугольный ли треугольник — верно/неверно (обратная т. Пифагора) */
{
id: 'vf-pyth', topic: 'g-pyth', order: 8, subject: 'geometry', grade: 8, kind: 'verify',
title: 'Прямоугольный ли? (верно?)',
pick: { m: [2, 5], n: [1, 4], yes: [0, 1] }, constraint: 'm > n',
derive: { a: 'm*m - n*n', b: '2*m*n', c: 'yes*(m*m + n*n) + (1 - yes)*(m*m + n*n + 1)', sumSq: '(m*m - n*n)^2 + (2*m*n)^2', cSq: '(yes*(m*m + n*n) + (1 - yes)*(m*m + n*n + 1))^2' },
claim: 'a*a + b*b == c*c',
display: 'Верно ли, что треугольник со сторонами {a}, {b} и {c} прямоугольный?',
solution: [
{ note: 'Проверяем обратную теорему Пифагора: {a}² + {b}² должно равняться {c}².', tex: '' },
{ note: '{a}² + {b}² = {sumSq}, а {c}² = {cSq}.', tex: '' }
]
},
/* проверка корня — верно/неверно */
{
id: 'vf-eq-root', topic: 'linear-eq', order: 16, subject: 'algebra', grade: 7, kind: 'verify',
title: 'Корень ли это? (верно?)',
pick: { a: [2, 6], b: [1, 12], r: [-6, 6], yes: [0, 1] }, require: 'r != 0',
derive: { c: 'yes*(a*r + b) + (1 - yes)*(a*r + b + 1)', lhsval: 'a*r + b' },
claim: 'a*r + b == c',
display: 'Верно ли, что x = {r} — корень уравнения {a}x + {b} = {c}?',
solution: [
{ note: 'Подставим x = {r}: {a}·({r}) + {b} = {lhsval}.', tex: '' },
{ note: 'Сравниваем с правой частью {c}.', tex: '' }
]
},
/* ── Прикидка (ответ в допуске) ── */
/* прикидка произведения */
{
id: 'est-product', topic: 'applied', order: 12, subject: 'algebra', grade: 6, kind: 'estimate',
title: 'Прикидка произведения',
pick: { a: [12, 89], b: [12, 89] }, derive: { val: 'a*b' }, tol: 'a*b*0.12',
lhs: 'x', rhs: '{a}*{b}', display: 'Оцените (приближённо) произведение {a} · {b}. Допускается близкий ответ.',
answerVar: 'x', answer: 'val',
solution: [
{ note: 'Округлим множители до десятков и перемножим. Точное значение:', tex: 'x = {a} * {b}' },
{ note: 'Получаем.', tex: 'x = {val}' }
]
},
/* прикидка процента */
{
id: 'est-percent', topic: 'percents', order: 11, subject: 'algebra', grade: 6, kind: 'estimate',
title: 'Прикидка процента',
pick: { p: [5, 95], a: [20, 400] }, derive: { val: 'p*a/100' }, tol: '(p*a/100)*0.12 + 1',
lhs: 'x', rhs: '{p}*{a}/100', display: 'Оцените (приближённо) {p}% от {a}. Допускается близкий ответ.',
answerVar: 'x', answer: 'val',
solution: [
{ note: 'Точное значение {p}% от {a}:', tex: 'x = {p} * {a} / 100' },
{ note: 'Подойдёт близкая прикидка.', tex: 'x = {val}' }
]
}
];
@@ -3110,6 +3257,10 @@
'ang-alt-exterior': 2, 'ang-coint-exterior': 2, 'ang-parallel-twostep': 3, 'ang-alt-solve': 3,
'ang-bisector': 1, 'ang-complementary': 1, 'ang-right-acute': 1, 'ang-parallelogram': 2,
'ang-polygon-missing': 3, 'ang-triangle-ratio': 3, 'ang-clock': 2,
// V4 C1 — новые форматы (выбор/верно-неверно/прикидка)
'ch-area-rect': 1, 'ch-lin-basic': 1, 'ch-pct-of': 2, 'ch-pyth-hyp': 2,
'vf-frac-compare': 2, 'vf-divisible': 1, 'vf-pyth': 3, 'vf-eq-root': 2,
'est-product': 2, 'est-percent': 2,
'pyth-perimeter': 3, 'pyth-distance': 3, 'pyth-rect-diagonal': 2, 'pyth-space-diagonal': 3,
'area-rect-inverse': 2, 'area-l-shape': 3, 'area-sector': 3,
'poly-diagonals': 2, 'poly-find-n': 3, 'poly-exterior-sum': 2,
+61 -2
View File
@@ -237,6 +237,15 @@
.tr-fm-btn:hover { border-color: var(--g1); color: var(--accent-ink); }
.tr-fm-btn.on { color: #fff; border-color: transparent; background: linear-gradient(135deg, var(--g1), var(--g2)); box-shadow: 0 8px 16px -6px rgba(99,102,241,.5); }
/* ── варианты выбора (kind choice / verify) ── */
.tr-choices { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; max-width: 460px; margin: 0 auto 4px; }
.tr-choice-btn { font: inherit; font-size: 1.15rem; font-weight: 700; cursor: pointer; padding: 14px 16px; border-radius: 15px; border: 2px solid rgba(99,102,241,.22); background: #fbfbff; color: var(--ink); transition: .14s var(--ease); }
.tr-choice-btn:hover:not(:disabled) { border-color: var(--g1); background: #fff; transform: translateY(-1px); }
.tr-choice-btn:disabled { cursor: default; }
.tr-choice-btn.right { border-color: transparent; color: #fff; background: linear-gradient(135deg, #059669, #10b981); }
.tr-choice-btn.wrongpick { border-color: transparent; color: #fff; background: linear-gradient(135deg, #dc2626, #ef4444); }
#tr-choice-next { display: block; margin: 0 auto 4px; }
/* строка ответа */
.tr-inrow { display: flex; gap: 10px; align-items: stretch; max-width: 460px; margin: 0 auto; }
#tr-eqx { font-family: 'Cambria Math', serif; font-size: 1.55rem; font-weight: 600; color: var(--accent-ink); align-self: center; padding-left: 4px; }
@@ -459,12 +468,14 @@
<div class="tr-difficulty" id="tr-difficulty"></div>
<div id="tr-answerbox">
<div class="tr-inrow">
<div class="tr-inrow" id="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-choices" id="tr-choices" style="display:none"></div>
<button class="tr-btn tr-primary" id="tr-choice-next" type="button" style="display:none">Дальше</button>
<div class="tr-keypad" id="tr-keypad"></div>
<div class="tr-preview" id="tr-preview"></div>
<div class="tr-feedback" id="tr-feedback"></div>
@@ -924,20 +935,62 @@
function applyInputMode() {
var k = cur && cur.kind;
var multi = (k === 'roots' || k === 'simplify' || k === 'inequality' || k === 'system');
var isChoice = (k === 'choice' || k === 'verify'); // ответ кнопками-вариантами
var eqx = $('tr-eqx');
if (eqx) {
eqx.style.display = multi ? 'none' : '';
eqx.style.display = (multi || isChoice) ? 'none' : '';
eqx.textContent = ((cur && (cur.answerSym || cur.answerVar)) || 'x') + ' ='; // P =/S =/x = по смыслу задачи
}
// переключение «ввод текста» ↔ «варианты выбора»
var inrow = $('tr-inrow'), kp = $('tr-keypad'), pv = $('tr-preview'), ch = $('tr-choices'), nx = $('tr-choice-next');
if (inrow) inrow.style.display = isChoice ? 'none' : '';
if (kp) kp.style.display = isChoice ? 'none' : '';
if (pv) pv.style.display = isChoice ? 'none' : '';
if (ch) ch.style.display = isChoice ? '' : 'none';
if (nx) nx.style.display = 'none';
$('tr-input').placeholder = (k === 'roots') ? 'корни через ;'
: (k === 'simplify') ? 'упрощённое выражение'
: (k === 'inequality') ? ('напр. ' + (cur.answerVar || 'x') + ' < 3')
: (k === 'system') ? 'напр. x = 2; y = 3'
: (k === 'estimate') ? 'примерно…'
: 'ответ';
var tog = $('tr-step-toggle'); if (tog) tog.style.display = canStep() ? '' : 'none';
var df = $('tr-difficulty'); if (df) df.style.display = (k === 'word') ? 'none' : '';
renderDifficulty();
}
// Варианты выбора (kind choice/verify): рисуем кнопки; клик = ответ.
function renderChoices() {
var box = $('tr-choices'); if (!box) return;
if (!cur || !cur.choices) { box.innerHTML = ''; return; }
box.innerHTML = cur.choices.map(function (c, i) {
return '<button class="tr-choice-btn" type="button" data-ci="' + i + '">' + esc(c.label) + '</button>';
}).join('');
}
function submitChoice(idx) {
if (answered || !cur || !cur.choices) return;
var c = cur.choices[idx]; if (!c) return;
var fb = $('tr-feedback');
setMode(true);
// показать раскраску вариантов
var btns = $('tr-choices').querySelectorAll('.tr-choice-btn');
for (var i = 0; i < btns.length; i++) {
btns[i].disabled = true;
if (cur.choices[i].correct) btns[i].classList.add('right');
else if (i === idx) btns[i].classList.add('wrongpick');
}
if (c.correct) {
fb.className = 'tr-feedback ok'; fb.innerHTML = ICON.ok + ' <span>Верно!</span>';
onSolved();
} else {
streak = 0; lastWrong = true;
fb.className = 'tr-feedback bad'; fb.innerHTML = ICON.bad + ' Неверно.';
$('tr-card').classList.add('tr-wrong');
recordAnswer(false); submitAttempt(false);
revealSolution();
}
var nx = $('tr-choice-next'); if (nx) nx.style.display = '';
updateStats();
}
// Текст ответа в фидбеке/раскрытии — по типу задачи.
var REL_SYM = { '<': '<', '>': '>', '<=': '≤', '>=': '≥' };
function answerLabel() {
@@ -976,6 +1029,7 @@
renderFigure(cur); // чертёж для геометрии (иначе скрыт)
renderFigureToggle(); // переключатель «Текст / На чертеже» (если уместен)
applyInputMode();
renderChoices(); // варианты выбора (kind choice/verify)
var inp = $('tr-input');
inp.value = ''; inp.disabled = false;
var fb = $('tr-feedback'); fb.className = 'tr-feedback'; fb.textContent = '';
@@ -1297,6 +1351,11 @@
updateSession();
});
$('tr-check').addEventListener('click', check);
$('tr-choices').addEventListener('click', function (e) {
var b = e.target.closest('.tr-choice-btn'); if (!b) return;
submitChoice(+b.getAttribute('data-ci'));
});
$('tr-choice-next').addEventListener('click', advance);
$('tr-skip').addEventListener('click', newProblem);
// ИИ-репетитор: после неверного ответа — разбор ошибки; иначе — наводящая подсказка.
// Опирается на известный ответ + шаги (движок), ИИ только ОБЪЯСНЯЕТ. Недоступен ИИ → решение.