feat(trainer): P1 — темы/навыки, +8 генераторов, подробные пошаговые решения

- таксономия тема→навык (topics/byTopic), метаданные topic/order/subject/grade
- 13 генераторов в 3 темах: Уравнения (+a(x+b)=c(x+d), (ax+b)/c=d), Пропорции (3), Проценты (3)
- проценты как compute-задачи: текстовый prompt + проверка подстановкой (latex уравнения скрыт)
- подробные объяснения: каждый шаг расписан словами + шаг «Проверка» (подстановка корня)
- UI: вкладки тем + чипы навыков, бейджи мастерства, авто-выбор первой неосвоенной темы/навыка
- движок: exprToLatex чинит отрицательные множители (7·(−5)), поле kind, нумерованные шаги решения
- смоуки 238/238 (движок) + 19/19 (страница); план: P1 отмечен DONE

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-25 13:29:44 +03:00
parent c370eaa803
commit 20b8ce2c5b
4 changed files with 314 additions and 99 deletions
+15 -4
View File
@@ -134,6 +134,12 @@
var s = _latex(node);
return _prec(node) < minPrec ? '\\left(' + s + '\\right)' : s;
}
// Операнд умножения: отрицательное/унарное/сумму берём в скобки, иначе
// соседство схлопнет смысл (7*(-5) -> «7-5», 6*(x+1) -> «6x+1»).
function _mulOperand(node) {
if (_isNeg(node) || _prec(node) < 2) return '\\left(' + _latex(node) + '\\right)';
return _latex(node);
}
function _latex(node) {
switch (node.k) {
case 'num': return fmtNum(node.v);
@@ -168,8 +174,8 @@
return base + '^{' + _latex(node.b) + '}';
}
if (op === '*') {
var sep = (node.b.k === 'num') ? ' \\cdot ' : ''; // ·число; иначе соседство (7x, 6(x+1))
return _wrapL(node.a, 2) + sep + _wrapL(node.b, 2);
var sep = (node.b.k === 'num' && node.b.v >= 0) ? ' \\cdot ' : ''; // знак · между числами; иначе соседство
return _mulOperand(node.a) + sep + _mulOperand(node.b);
}
if (op === '%') return _wrapL(node.a, 2) + ' \\bmod ' + _wrapL(node.b, 3);
// + или - (схлопываем a + (-b) -> a - b)
@@ -233,12 +239,17 @@
var lhsExpr = render(gen.lhs, env);
var rhsExpr = render(gen.rhs, env);
var sEnv = assign(env, { ans: answer });
var ll = exprToLatex(lhsExpr), rl = exprToLatex(rhsExpr);
// compute-задача (проценты): показываем текстовый prompt из display, а
// уравнение lhs=rhs служит лишь для проверки → latex уравнения не строим.
var isCompute = gen.kind === 'compute';
var ll = isCompute ? null : exprToLatex(lhsExpr);
var rl = isCompute ? null : exprToLatex(rhsExpr);
var problem = {
genId: gen.id,
skill: gen.skill,
skill: gen.skill || gen.id, // ключ прогресса = id генератора, если skill не задан
title: gen.title,
kind: gen.kind || 'solve',
lhsExpr: lhsExpr,
rhsExpr: rhsExpr,
display: prettyMath(render(gen.display || (gen.lhs + ' = ' + gen.rhs), env)),