feat(trainer): инлайн-KaTeX в текст-условиях (дроби/уравнения через $...$)

Текстовые условия (verify/choice/compute) показывали математику плоским текстом
(«Верно ли, что 1/5 > 4/6?»). Теперь:
- renderMixed(text): сегменты в $...$ рендерятся KaTeX через exprToLatex (с un-pretty
  ·×→*, −→-, ÷→/, т.к. prettyMath уже косметит display), остальное экранируется;
  showStatement использует его для текст-условий (полные уравнения solve/roots/…
  по-прежнему идут целым latex).
- В дисплеях обёрнута математика $...$: vf-frac-compare, vf-eq-root, ch-lin-basic,
  frac-add-unlike/mult/reduce/to-decimal/of-number/of-whole-inverse/add-same.

Дроби теперь видны как настоящие \frac, уравнения — как KaTeX. Проверка:
все $-сегменты 220/220 рендерятся (0 null); смоук v41 99634; inline парсится.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-06-29 19:01:13 +03:00
parent 382974461a
commit 27d5308a04
2 changed files with 34 additions and 12 deletions
+10 -10
View File
@@ -858,7 +858,7 @@
title: 'Часть от числа', title: 'Часть от числа',
pick: { n: [2, 6], a: [1, 5], mfac: [2, 9] }, require: 'a < n', pick: { n: [2, 6], a: [1, 5], mfac: [2, 9] }, require: 'a < n',
derive: { m: 'n*mfac', val: 'mfac*a' }, derive: { m: 'n*mfac', val: 'mfac*a' },
lhs: 'x', rhs: '{m}*{a}/{n}', display: 'Найдите {a}/{n} от числа {m}.', lhs: 'x', rhs: '{m}*{a}/{n}', display: 'Найдите ${a}/{n}$ от числа {m}.',
answerVar: 'x', answer: 'val', integerAnswer: true, answerVar: 'x', answer: 'val', integerAnswer: true,
solution: [ solution: [
{ note: 'Чтобы найти часть от числа, умножаем число на числитель и делим на знаменатель:', tex: 'x = {m}*{a}/{n}' }, { note: 'Чтобы найти часть от числа, умножаем число на числитель и делим на знаменатель:', tex: 'x = {m}*{a}/{n}' },
@@ -872,7 +872,7 @@
title: 'Сложение дробей', title: 'Сложение дробей',
pick: { n: [3, 9], a: [1, 6], b: [1, 6] }, require: 'a + b < 2*n', pick: { n: [3, 9], a: [1, 6], b: [1, 6] }, require: 'a + b < 2*n',
derive: { val: '(a + b)/n' }, derive: { val: '(a + b)/n' },
lhs: 'x', rhs: '({a} + {b})/{n}', display: 'Вычислите {a}/{n} + {b}/{n}. Ответ запишите дробью (например 3/4) или числом.', lhs: 'x', rhs: '({a} + {b})/{n}', display: 'Вычислите ${a}/{n} + {b}/{n}$. Ответ запишите дробью (например 3/4) или числом.',
answerVar: 'x', answer: 'val', answerVar: 'x', answer: 'val',
solution: [ solution: [
{ note: 'При одинаковом знаменателе складываем числители, знаменатель оставляем:', tex: 'x = ({a} + {b})/{n}' }, { note: 'При одинаковом знаменателе складываем числители, знаменатель оставляем:', tex: 'x = ({a} + {b})/{n}' },
@@ -2278,7 +2278,7 @@
pick: { p: [1, 7], q: [2, 9], g: [2, 9] }, constraint: 'p < q', pick: { p: [1, 7], q: [2, 9], g: [2, 9] }, constraint: 'p < q',
derive: { a: 'p*g', b: 'q*g', val: 'p/q' }, require: 'gcd(p, q) == 1 && b <= 90', derive: { a: 'p*g', b: 'q*g', val: 'p/q' }, require: 'gcd(p, q) == 1 && b <= 90',
factorize: [{ name: 'aFac', of: 'a' }, { name: 'bFac', of: 'b' }], factorize: [{ name: 'aFac', of: 'a' }, { name: 'bFac', of: 'b' }],
lhs: 'x', rhs: '{a}/{b}', display: 'Сократите дробь {a}/{b}. Ответ запишите несократимой дробью (например 2/3).', lhs: 'x', rhs: '{a}/{b}', display: 'Сократите дробь ${a}/{b}$. Ответ запишите несократимой дробью (например 2/3).',
answerVar: 'x', answer: 'val', answerVar: 'x', answer: 'val',
solution: [ solution: [
{ note: 'Разложим числитель и знаменатель: {a} = {aFac}, {b} = {bFac}.', tex: '' }, { note: 'Разложим числитель и знаменатель: {a} = {aFac}, {b} = {bFac}.', tex: '' },
@@ -2293,7 +2293,7 @@
title: 'Сложение дробей (разные знам.)', title: 'Сложение дробей (разные знам.)',
pick: { m: [2, 6], n: [2, 6], a: [1, 5], b: [1, 5] }, constraint: 'm != n && a < m && b < n', pick: { m: [2, 6], n: [2, 6], a: [1, 5], b: [1, 5] }, constraint: 'm != n && a < m && b < n',
derive: { val: 'a/m + b/n', L: 'lcm(m, n)', num: 'a*(lcm(m, n)/m) + b*(lcm(m, n)/n)' }, require: 'val < 2', derive: { val: 'a/m + b/n', L: 'lcm(m, n)', num: 'a*(lcm(m, n)/m) + b*(lcm(m, n)/n)' }, require: 'val < 2',
lhs: 'x', rhs: '{a}/{m} + {b}/{n}', display: 'Вычислите {a}/{m} + {b}/{n}. Ответ запишите дробью (например 5/6) или числом.', lhs: 'x', rhs: '{a}/{m} + {b}/{n}', display: 'Вычислите ${a}/{m} + {b}/{n}$. Ответ запишите дробью (например 5/6) или числом.',
answerVar: 'x', answer: 'val', answerVar: 'x', answer: 'val',
solution: [ solution: [
{ note: 'Приводим дроби к общему знаменателю НОК({m}, {n}) = {L}.', tex: 'x = {num} / {L}' }, { note: 'Приводим дроби к общему знаменателю НОК({m}, {n}) = {L}.', tex: 'x = {num} / {L}' },
@@ -2307,7 +2307,7 @@
title: 'Умножение дробей', title: 'Умножение дробей',
pick: { a: [1, 5], m: [2, 6], b: [1, 5], n: [2, 6] }, constraint: 'a < m && b < n', pick: { a: [1, 5], m: [2, 6], b: [1, 5], n: [2, 6] }, constraint: 'a < m && b < n',
derive: { val: '(a*b)/(m*n)', num: 'a*b', den: 'm*n' }, derive: { val: '(a*b)/(m*n)', num: 'a*b', den: 'm*n' },
lhs: 'x', rhs: '({a}*{b})/({m}*{n})', display: 'Вычислите {a}/{m} · {b}/{n}. Ответ запишите дробью.', lhs: 'x', rhs: '({a}*{b})/({m}*{n})', display: 'Вычислите ${a}/{m} * {b}/{n}$. Ответ запишите дробью.',
answerVar: 'x', answer: 'val', answerVar: 'x', answer: 'val',
solution: [ solution: [
{ note: 'Перемножаем числители и знаменатели.', tex: 'x = ({a} * {b}) / ({m} * {n})' }, { note: 'Перемножаем числители и знаменатели.', tex: 'x = ({a} * {b}) / ({m} * {n})' },
@@ -2335,7 +2335,7 @@
title: 'Число по его части', title: 'Число по его части',
pick: { n: [2, 6], a: [1, 5], whole: [2, 9] }, constraint: 'a < n', pick: { n: [2, 6], a: [1, 5], whole: [2, 9] }, constraint: 'a < n',
derive: { m: 'whole*n', val: 'a*whole' }, derive: { m: 'whole*n', val: 'a*whole' },
lhs: 'x', rhs: '{val}*{n}/{a}', display: '{a}/{n} некоторого числа равны {val}. Найдите это число.', lhs: 'x', rhs: '{val}*{n}/{a}', display: '${a}/{n}$ некоторого числа равны {val}. Найдите это число.',
answerVar: 'x', answer: 'm', integerAnswer: true, answerVar: 'x', answer: 'm', integerAnswer: true,
solution: [ solution: [
{ note: 'Если {a}/{n} числа равны {val}, то одна {n}-я равна {val} ÷ {a}, а всё число — в {n} раз больше.', tex: 'x = {val} * {n} / {a}' }, { note: 'Если {a}/{n} числа равны {val}, то одна {n}-я равна {val} ÷ {a}, а всё число — в {n} раз больше.', tex: 'x = {val} * {n} / {a}' },
@@ -2350,7 +2350,7 @@
pick: { a: [1, 9], bi: [0, 4] }, pick: { a: [1, 9], bi: [0, 4] },
derive: { b: 'bi == 0 ? 2 : (bi == 1 ? 4 : (bi == 2 ? 5 : (bi == 3 ? 8 : 10)))', val: 'a / (bi == 0 ? 2 : (bi == 1 ? 4 : (bi == 2 ? 5 : (bi == 3 ? 8 : 10))))' }, derive: { b: 'bi == 0 ? 2 : (bi == 1 ? 4 : (bi == 2 ? 5 : (bi == 3 ? 8 : 10)))', val: 'a / (bi == 0 ? 2 : (bi == 1 ? 4 : (bi == 2 ? 5 : (bi == 3 ? 8 : 10))))' },
require: 'a < b', require: 'a < b',
lhs: 'x', rhs: '{a}/{b}', display: 'Запишите дробь {a}/{b} в виде десятичной дроби.', lhs: 'x', rhs: '{a}/{b}', display: 'Запишите дробь ${a}/{b}$ в виде десятичной дроби.',
answerVar: 'x', answer: 'val', answerVar: 'x', answer: 'val',
solution: [ solution: [
{ note: 'Знаменатель {b} — делитель степени десяти, поэтому дробь — конечная десятичная.', tex: 'x = {a} / {b}' }, { note: 'Знаменатель {b} — делитель степени десяти, поэтому дробь — конечная десятичная.', tex: 'x = {a} / {b}' },
@@ -3065,7 +3065,7 @@
title: 'Корень уравнения — выбор', title: 'Корень уравнения — выбор',
pick: { a: [2, 9], b: [1, 20], root: [-9, 9] }, require: 'root != 0', 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, derive: { c: 'a*root + b' }, answer: 'root', distractors: ['-root', 'c - b', 'root + a'], integerAnswer: true,
display: 'Чему равен корень уравнения {a}x + {b} = {c}? Выберите ответ.', display: 'Чему равен корень уравнения ${a}x + {b} = {c}$? Выберите ответ.',
solution: [ solution: [
{ note: 'Переносим {b} вправо и делим на {a}.', tex: 'x = ({c} - {b}) / {a}' }, { note: 'Переносим {b} вправо и делим на {a}.', tex: 'x = ({c} - {b}) / {a}' },
{ note: 'Получаем корень.', tex: 'x = {root}' } { note: 'Получаем корень.', tex: 'x = {root}' }
@@ -3109,7 +3109,7 @@
title: 'Сравнение дробей (верно?)', title: 'Сравнение дробей (верно?)',
pick: { a: [1, 7], b: [2, 9], c: [1, 7], d: [2, 9] }, constraint: 'a < b && c < d && a*d != c*b', 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', claim: 'a*d > c*b',
display: 'Верно ли, что {a}/{b} > {c}/{d}?', display: 'Верно ли, что ${a}/{b} > {c}/{d}$?',
solution: [ solution: [
{ note: 'Сравниваем перекрёстные произведения: {a}·{d} и {c}·{b}.', tex: '' }, { note: 'Сравниваем перекрёстные произведения: {a}·{d} и {c}·{b}.', tex: '' },
{ note: 'Первая дробь больше, если {a}·{d} больше {c}·{b}.', tex: '' } { note: 'Первая дробь больше, если {a}·{d} больше {c}·{b}.', tex: '' }
@@ -3150,7 +3150,7 @@
pick: { a: [2, 6], b: [1, 12], r: [-6, 6], yes: [0, 1] }, require: 'r != 0', 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' }, derive: { c: 'yes*(a*r + b) + (1 - yes)*(a*r + b + 1)', lhsval: 'a*r + b' },
claim: 'a*r + b == c', claim: 'a*r + b == c',
display: 'Верно ли, что x = {r} — корень уравнения {a}x + {b} = {c}?', display: 'Верно ли, что $x = {r}$ — корень уравнения ${a}x + {b} = {c}$?',
solution: [ solution: [
{ note: 'Подставим x = {r}: {a}·({r}) + {b} = {lhsval}.', tex: '' }, { note: 'Подставим x = {r}: {a}·({r}) + {b} = {lhsval}.', tex: '' },
{ note: 'Сравниваем с правой частью {c}.', tex: '' } { note: 'Сравниваем с правой частью {c}.', tex: '' }
+24 -2
View File
@@ -619,12 +619,34 @@
if (svg) { box.innerHTML = svg; box.style.display = ''; } if (svg) { box.innerHTML = svg; box.style.display = ''; }
else { box.innerHTML = ''; box.style.display = 'none'; } else { box.innerHTML = ''; box.style.display = 'none'; }
} }
// Смешанный текст «проза + математика»: фрагменты в $...$ рендерятся KaTeX
// (через exprToLatex), остальное экранируется. Так формулы видны и в текст-условиях
// (проценты/дроби/verify/choice), где нет единого latex уравнения.
function renderMixed(text) {
var parts = String(text == null ? '' : text).split('$');
var html = '';
for (var i = 0; i < parts.length; i++) {
if (i % 2 === 1) { // нечётные сегменты — математика
// prettyMath уже мог заменить * → ·, − и т.п.; возвращаем к виду для SimExpr-парсера
var src = parts[i].replace(/[·×]/g, '*').replace(//g, '-').replace(/÷/g, '/');
var lx = TE.exprToLatex(src);
html += lx ? (kat(lx, false) || esc(parts[i])) : esc(parts[i]);
} else {
html += esc(parts[i]);
}
}
return html;
}
// Условие: полный текст ИЛИ краткий промпт (числа читаются с чертежа). // Условие: полный текст ИЛИ краткий промпт (числа читаются с чертежа).
function showStatement(problem) { function showStatement(problem) {
var eq = $('tr-eq'); if (!eq || !problem) return; var eq = $('tr-eq'); if (!eq || !problem) return;
var useFig = figureMode && problem.figure && problem.figurePrompt; var useFig = figureMode && problem.figure && problem.figurePrompt;
if (useFig) { eq.classList.add('tr-eq-text'); setMath(eq, null, problem.figurePrompt, true); } var text = useFig ? problem.figurePrompt : problem.display;
else { eq.classList.toggle('tr-eq-text', !problem.latex); setMath(eq, problem.latex, problem.display, true); } var latex = useFig ? null : problem.latex;
eq.classList.toggle('tr-eq-text', !latex);
if (latex) { setMath(eq, latex, text, true); return; } // целое уравнение (solve/roots/…)
if (text && text.indexOf('$') !== -1) eq.innerHTML = renderMixed(text); // текст с инлайн-формулами
else eq.textContent = text;
} }
// Переключатель «Текст / На чертеже» — только для задач с чертежом и кратким условием. // Переключатель «Текст / На чертеже» — только для задач с чертежом и кратким условием.
function renderFigureToggle() { function renderFigureToggle() {