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:
+76
-26
@@ -75,11 +75,24 @@
|
||||
background: #f8fafc; border: 1px solid rgba(148,163,184,0.22);
|
||||
}
|
||||
.tr-solution h4 { margin: 0 0 10px; font-size: .82rem; text-transform: uppercase; letter-spacing: .04em; color: #64748b; }
|
||||
.tr-step { font-size: 1.05rem; color: #334155; padding: 7px 0; display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; }
|
||||
.tr-step { color: #334155; padding: 11px 0; }
|
||||
.tr-step + .tr-step { border-top: 1px dashed rgba(148,163,184,0.28); }
|
||||
.tr-step-note { color: #64748b; font-family: 'Manrope', sans-serif; font-size: .85rem; }
|
||||
.tr-step-math { font-family: 'Cambria Math', serif; }
|
||||
.tr-step-note { display: block; color: #475569; font-family: 'Manrope', sans-serif; font-size: .92rem; line-height: 1.55; margin-bottom: 6px; }
|
||||
.tr-step-math { display: block; font-family: 'Cambria Math', serif; font-size: 1.15rem; margin-left: 28px; }
|
||||
.tr-step-n { display: inline-flex; align-items: center; justify-content: center; width: 21px; height: 21px; border-radius: 50%; background: #e0e7ff; color: #4f46e5; font-family: 'Manrope', sans-serif; font-size: .72rem; font-weight: 800; margin-right: 8px; vertical-align: 1px; }
|
||||
.tr-eq .katex-display { margin: 0; }
|
||||
/* текстовый prompt (проценты) — компактнее уравнения */
|
||||
.tr-eq.tr-eq-text { font-family: 'Manrope', sans-serif; font-weight: 600; font-size: clamp(1.1rem, 3.4vw, 1.55rem); line-height: 1.4; color: #1e293b; }
|
||||
|
||||
/* выбор навыка внутри темы */
|
||||
.tr-skills { display: flex; flex-wrap: wrap; gap: 7px; margin: -8px 0 22px; }
|
||||
.tr-skill {
|
||||
font: inherit; font-size: .85rem; font-weight: 600; cursor: pointer; font-family: 'Cambria Math', 'Times New Roman', serif;
|
||||
padding: 6px 12px; border-radius: 10px; border: 1px solid rgba(148,163,184,0.3); background: #fff; color: #475569; transition: .15s;
|
||||
display: inline-flex; align-items: center;
|
||||
}
|
||||
.tr-skill:hover { border-color: #818cf8; color: #4338ca; }
|
||||
.tr-skill.on { background: #eef2ff; border-color: #818cf8; color: #4338ca; }
|
||||
|
||||
/* бейджи прогресса на чипах */
|
||||
.tr-badge { display: inline-flex; margin-left: 7px; color: #16a34a; vertical-align: middle; }
|
||||
@@ -108,6 +121,7 @@
|
||||
</div>
|
||||
|
||||
<div class="tr-topics" id="tr-topics"></div>
|
||||
<div class="tr-skills" id="tr-skills"></div>
|
||||
|
||||
<div class="tr-card">
|
||||
<div class="tr-skill" id="tr-skill"></div>
|
||||
@@ -205,21 +219,38 @@
|
||||
if (h) el.innerHTML = h; else el.textContent = fallbackText;
|
||||
}
|
||||
|
||||
var curGen = gens[0];
|
||||
var topics = TG.topics ? TG.topics() : [{ key: null, label: 'Задачи' }];
|
||||
function skillKey(g) { return g.skill || g.id; }
|
||||
function skillsOf(topicKey) { return TG.byTopic ? TG.byTopic(topicKey) : gens; }
|
||||
|
||||
var curTopic = topics[0] ? topics[0].key : null;
|
||||
var curGen = skillsOf(curTopic)[0] || gens[0];
|
||||
var cur = null;
|
||||
var solved = 0, streak = 0;
|
||||
var answered = false; // задача закрыта (верно/решение показано) → «Проверить» становится «Дальше»
|
||||
var prog = {}; // skill → строка прогресса с сервера
|
||||
|
||||
function chipBadge(skill) {
|
||||
var p = prog[skill];
|
||||
function topicMastered(topicKey) {
|
||||
var ss = skillsOf(topicKey);
|
||||
return ss.length > 0 && ss.every(function (g) { var p = prog[skillKey(g)]; return p && p.mastered; });
|
||||
}
|
||||
function skillBadge(g) {
|
||||
var p = prog[skillKey(g)];
|
||||
if (p && p.mastered) return '<span class="tr-badge" title="Освоено">' + ICON.star + '</span>';
|
||||
if (p && p.solved) return '<span class="tr-badge-n">' + p.solved + '</span>';
|
||||
return '';
|
||||
}
|
||||
|
||||
function renderTopics() {
|
||||
$('tr-topics').innerHTML = gens.map(function (g, i) {
|
||||
return '<button class="tr-chip' + (g === curGen ? ' on' : '') + '" type="button" data-i="' + i + '">' + esc(g.title) + chipBadge(g.skill) + '</button>';
|
||||
$('tr-topics').innerHTML = topics.map(function (t, i) {
|
||||
var done = topicMastered(t.key) ? '<span class="tr-badge" title="Тема освоена">' + ICON.star + '</span>' : '';
|
||||
return '<button class="tr-chip' + (t.key === curTopic ? ' on' : '') + '" type="button" data-ti="' + i + '">' + esc(t.label) + done + '</button>';
|
||||
}).join('');
|
||||
}
|
||||
function renderSkills() {
|
||||
var ss = skillsOf(curTopic);
|
||||
$('tr-skills').innerHTML = ss.map(function (g, i) {
|
||||
return '<button class="tr-skill' + (g === curGen ? ' on' : '') + '" type="button" data-si="' + i + '">' + esc(g.title) + skillBadge(g) + '</button>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
@@ -229,9 +260,11 @@
|
||||
}
|
||||
function updateStats() { $('tr-solved').textContent = solved; $('tr-streak').textContent = streak; }
|
||||
|
||||
function stepHtml(st) {
|
||||
function stepHtml(st, n) {
|
||||
if (!st) return '';
|
||||
var note = st.note ? '<span class="tr-step-note">' + esc(st.note) + '</span>' : '';
|
||||
var num = '<span class="tr-step-n">' + n + '</span>';
|
||||
var note = st.note ? '<span class="tr-step-note">' + num + esc(st.note) + '</span>'
|
||||
: '<span class="tr-step-note">' + num + '</span>';
|
||||
var math = '';
|
||||
if (st.latex) { var h = kat(st.latex, false); math = '<span class="tr-step-math">' + (h || esc(st.tex || '')) + '</span>'; }
|
||||
else if (st.tex) { math = '<span class="tr-step-math">' + esc(st.tex) + '</span>'; }
|
||||
@@ -245,7 +278,9 @@
|
||||
if (!cur) { $('tr-eq').textContent = 'Не удалось сгенерировать задачу'; return; }
|
||||
|
||||
$('tr-skill').textContent = curGen.title;
|
||||
setMath($('tr-eq'), cur.latex, cur.display, true);
|
||||
var eq = $('tr-eq');
|
||||
eq.classList.toggle('tr-eq-text', !cur.latex); // текстовый prompt (проценты) — другим шрифтом
|
||||
setMath(eq, cur.latex, cur.display, true);
|
||||
var inp = $('tr-input');
|
||||
inp.value = ''; inp.disabled = false;
|
||||
var fb = $('tr-feedback'); fb.className = 'tr-feedback'; fb.textContent = '';
|
||||
@@ -257,15 +292,19 @@
|
||||
// фоновая отправка попытки на сервер (прогресс/мастерство)
|
||||
function submitAttempt(correct) {
|
||||
if (!LS.practiceSubmit) return;
|
||||
LS.practiceSubmit(curGen.skill, correct).then(function (r) {
|
||||
if (r && r.progress) { prog[r.progress.skill] = r.progress; renderTopics(); }
|
||||
LS.practiceSubmit(skillKey(curGen), correct).then(function (r) {
|
||||
if (r && r.progress) { prog[r.progress.skill] = r.progress; renderSkills(); renderTopics(); }
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
function solutionHtml(title) {
|
||||
var steps = (cur.solution || []).map(function (st, i) { return stepHtml(st, i + 1); }).join('');
|
||||
return '<h4>' + title + '</h4>' + (steps || '<div class="tr-step"><span class="tr-step-math">x = ' + esc(fmt(cur.answer)) + '</span></div>');
|
||||
}
|
||||
|
||||
function revealAnswer(giveUp) {
|
||||
var s = $('tr-solution');
|
||||
var steps = (cur.solution || []).map(stepHtml).join('');
|
||||
s.innerHTML = '<h4>Решение</h4>' + (steps || '<div class="tr-step">x = ' + esc(fmt(cur.answer)) + '</div>');
|
||||
s.innerHTML = solutionHtml('Решение');
|
||||
s.style.display = 'block';
|
||||
if (giveUp) {
|
||||
streak = 0;
|
||||
@@ -303,31 +342,42 @@
|
||||
// ── события ──
|
||||
$('tr-topics').addEventListener('click', function (e) {
|
||||
var b = e.target.closest('.tr-chip'); if (!b) return;
|
||||
curGen = gens[+b.getAttribute('data-i')] || gens[0];
|
||||
renderTopics();
|
||||
newProblem();
|
||||
var t = topics[+b.getAttribute('data-ti')]; if (!t) return;
|
||||
curTopic = t.key;
|
||||
var ss = skillsOf(curTopic);
|
||||
// первый неосвоенный навык темы, иначе первый
|
||||
curGen = ss[0] || curGen;
|
||||
for (var i = 0; i < ss.length; i++) { var p = prog[skillKey(ss[i])]; if (!(p && p.mastered)) { curGen = ss[i]; break; } }
|
||||
renderTopics(); renderSkills(); newProblem();
|
||||
});
|
||||
$('tr-skills').addEventListener('click', function (e) {
|
||||
var b = e.target.closest('.tr-skill'); if (!b) return;
|
||||
var ss = skillsOf(curTopic);
|
||||
curGen = ss[+b.getAttribute('data-si')] || curGen;
|
||||
renderSkills(); newProblem();
|
||||
});
|
||||
$('tr-check').addEventListener('click', check);
|
||||
$('tr-skip').addEventListener('click', newProblem);
|
||||
$('tr-hint').addEventListener('click', function () {
|
||||
if (!cur) return;
|
||||
var s = $('tr-solution');
|
||||
s.innerHTML = '<h4>Подсказка</h4>' + stepHtml((cur.solution || [])[0] || { note: '', tex: 'x = ' + fmt(cur.answer), latex: null });
|
||||
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(); } });
|
||||
|
||||
$('tr-note').textContent = 'Прототип: ' + gens.length + ' генераторов · ответ проверяется подстановкой (5, x=5, 10/2, 2+3) · прогресс сохраняется.';
|
||||
$('tr-note').textContent = gens.length + ' навыков в ' + topics.length + ' темах · ответ проверяется подстановкой (5, x=5, 10/2, 2+3) · прогресс сохраняется.';
|
||||
|
||||
// загрузка прогресса → старт (авто-выбор первого неосвоенного навыка)
|
||||
// загрузка прогресса → старт (авто-выбор первой неосвоенной темы и навыка)
|
||||
function boot() {
|
||||
for (var i = 0; i < gens.length; i++) {
|
||||
var p = prog[gens[i].skill];
|
||||
if (!(p && p.mastered)) { curGen = gens[i]; break; }
|
||||
for (var ti = 0; ti < topics.length; ti++) {
|
||||
if (!topicMastered(topics[ti].key)) { curTopic = topics[ti].key; break; }
|
||||
}
|
||||
renderTopics();
|
||||
newProblem();
|
||||
var ss = skillsOf(curTopic);
|
||||
curGen = ss[0] || gens[0];
|
||||
for (var si = 0; si < ss.length; si++) { var p = prog[skillKey(ss[si])]; if (!(p && p.mastered)) { curGen = ss[si]; break; } }
|
||||
renderTopics(); renderSkills(); newProblem();
|
||||
}
|
||||
(LS.practiceProgressList ? LS.practiceProgressList() : Promise.resolve(null))
|
||||
.then(function (r) { if (r && r.progress) r.progress.forEach(function (row) { prog[row.skill] = row; }); })
|
||||
|
||||
Reference in New Issue
Block a user