From f0af2079c3b2b49f795caa39564615f10980328b Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Thu, 25 Jun 2026 16:24:31 +0300 Subject: [PATCH] =?UTF-8?q?fix(trainer):=20=D1=81=D0=BB=D0=BE=D0=B6=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=3D=20=D1=81=D1=82=D1=80=D1=83=D0=BA?= =?UTF-8?q?=D1=82=D1=83=D1=80=D0=B0=20=D0=B7=D0=B0=D0=B4=D0=B0=D1=87=D0=B8?= =?UTF-8?q?,=20=D0=B0=20=D0=BD=D0=B5=20=D0=BC=D0=B0=D1=81=D1=88=D1=82?= =?UTF-8?q?=D0=B0=D0=B1=20=D1=87=D0=B8=D1=81=D0=B5=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Пользователь верно заметил: масштабирование чисел (больше/меньше) — не настоящая сложность. Настоящая = больше действий, скобки, дроби, переменная в обеих частях. - генераторы размечены структурным level 1-3 (generators.js, LEVELS): напр. Уравнения ax+b=c (1) -> a(x+b)=c (2) -> a(x+b)=c(x+d) (3); Степени: вычислить -> произведение -> степень степени - контрол сложности выбирает ВАРИАНТ-генератор нужного уровня в теме (pickByLevel с клампом к доступным), а не масштабирует числа - клик по чипу навыка закрепляет конкретный вариант (pinned); Авто = адаптивный подбор (умная тренировка от простого к сложному) + показ ур.N текущего - кросс-тематический адаптив pickNext — только в Авто без закрепления - движковое _scaleRange/level оставлено как capability (T18), страница его НЕ использует - смоук движка 682/682, страница 36/36 (Сложный->ген ур.3, Лёгкий->ур.1); эмодзи/eval 0 Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/js/trainer/generators.js | 32 +++++++++++++++++++ frontend/trainer.html | 52 ++++++++++++++++++++++++------- plans/ai-trainer/ROADMAP_V2.md | 22 ++++++++----- 3 files changed, 87 insertions(+), 19 deletions(-) diff --git a/frontend/js/trainer/generators.js b/frontend/js/trainer/generators.js index 855525e..a03a57d 100644 --- a/frontend/js/trainer/generators.js +++ b/frontend/js/trainer/generators.js @@ -594,6 +594,38 @@ ]; + // Структурная сложность генератора (1 — простейшая форма, 3 — больше действий / + // скобки / дроби / переменная в обеих частях). Определяет, какой ВАРИАНТ внутри + // темы даётся на выбранном уровне сложности (не просто масштаб чисел). + var LEVELS = { + // Уравнения: ax+b=c → скобки/обе части/дроби → (ax+b)/c=d + 'lin-basic': 1, 'lin-paren': 2, 'lin-both-sides': 2, 'lin-frac-denom': 2, + 'lin-coef-frac': 2, 'lin-paren-both': 3, 'lin-frac-eq': 3, + // Пропорции + 'prop-x-right': 1, 'prop-x-left': 1, 'prop-x-denom': 2, + // Проценты + 'pct-of': 1, 'pct-what': 2, 'pct-whole': 2, + // Упрощение + 'simp-like': 1, 'simp-expand': 2, + // Степени + 'pow-eval': 1, 'pow-mult': 2, 'pow-pow': 3, + // Формулы сокр. умножения + 'sq-sum': 2, 'sq-diff': 2, 'diff-sq': 3, + // Неравенства (смена знака — сложнее) + 'ineq-lt': 1, 'ineq-ge': 1, 'ineq-flip': 3, + // Квадратные + 'quad-diff': 2, 'quad-factored': 3, + // Прогрессии + 'prog-arith-term': 2, 'prog-geom-term': 3, + // Геометрия — Углы + 'ang-adjacent': 1, 'ang-triangle': 2, 'ang-exterior': 2, + // Геометрия — Пифагор + 'pyth-hyp': 2, 'pyth-leg': 3, + // Геометрия — Площади + 'area-square': 1, 'area-rect': 1, 'area-triangle': 2 + }; + GENERATORS.forEach(function (g) { g.level = LEVELS[g.id] || 1; }); + function get(id) { for (var i = 0; i < GENERATORS.length; i++) if (GENERATORS[i].id === id) return GENERATORS[i]; return null; diff --git a/frontend/trainer.html b/frontend/trainer.html index 614f6a6..bdfbce9 100644 --- a/frontend/trainer.html +++ b/frontend/trainer.html @@ -488,7 +488,8 @@ var isTeacher = !!(ip && ip.isTeacher); var isAdmin = !!(ip && ip.isAdmin); var curSubject = 'algebra'; // фильтр предмета (Алгебра/Геометрия) - var diffMode = 'auto'; // уровень сложности: 'auto' | 1 | 2 | 3 + var diffMode = 'auto'; // уровень сложности: 'auto' | 1 | 2 | 3 (= структурный вариант) + var pinned = null; // закреплённый навык (id) при явном клике по чипу var customGens = []; // пользовательские генераторы (P13), тема «Авторские» function skillKey(g) { return g.skill || g.id; } function skillsOf(topicKey) { @@ -635,15 +636,35 @@ $('tr-check').textContent = done ? 'Дальше' : 'Проверить'; var sc = $('tr-stepcheck'); if (sc) sc.textContent = done ? 'Дальше' : 'Шаг'; } - // уровень сложности: ручной (1/2/3) или «Авто» — растёт с серией верных в сессии - function currentLevel() { - if (diffMode === 1 || diffMode === 2 || diffMode === 3) return diffMode; - return streak >= 4 ? 3 : streak >= 2 ? 2 : 1; + // ── Сложность = СТРУКТУРА задачи (какой вариант-генератор внутри темы), + // а не масштаб чисел: ур.1 — простейшая форма, ур.3 — больше действий / + // скобки / дроби / переменная в обеих частях ── + function levelOf(g) { return (g && g.level) || 1; } + function genById(id) { + var i; + for (i = 0; i < gens.length; i++) if (skillKey(gens[i]) === id) return gens[i]; + for (i = 0; i < customGens.length; i++) if (skillKey(customGens[i]) === id) return customGens[i]; + return null; + } + // выбрать генератор нужного структурного уровня в теме (кламп к доступным уровням) + function pickByLevel(topicKey, level) { + var ss = skillsOf(topicKey); if (!ss.length) return null; + var lv = ss.map(levelOf); + var L = Math.max(Math.min.apply(null, lv), Math.min(Math.max.apply(null, lv), level)); + var at = ss.filter(function (g) { return levelOf(g) === L; }); + if (!at.length) at = ss; + return at[Math.floor(Math.random() * at.length)] || at[0]; + } + // какой генератор давать: закреплённый навык > ручной уровень > текущий (адаптив/выбор) + function chooseGen() { + if (pinned) { var g = genById(pinned); if (g && g.topic === curTopic) return g; pinned = null; } + if (diffMode === 1 || diffMode === 2 || diffMode === 3) { var bl = pickByLevel(curTopic, diffMode); if (bl) return bl; } + return curGen; } function renderDifficulty() { var el = $('tr-difficulty'); if (!el) return; var opts = [['auto', 'Авто'], [1, 'Лёгкий'], [2, 'Средний'], [3, 'Сложный']]; - var autoLvl = (diffMode === 'auto') ? (' · ур.' + currentLevel()) : ''; + var autoLvl = (diffMode === 'auto') ? (' · ур.' + levelOf(curGen)) : ''; el.innerHTML = 'Сложность' + opts.map(function (o) { var lbl = (o[0] === 'auto') ? ('Авто' + autoLvl) : o[1]; return ''; @@ -781,12 +802,14 @@ function newProblem() { if (isWord()) { serveWordProblem(); return; } + curGen = chooseGen() || curGen; // структурный вариант по уровню/закреплению + if (curGen && curGen.topic) curTopic = curGen.topic; // strict:false + несколько попыток на случай редкой неудачи с ограничениями cur = null; - var lvl = currentLevel(); - for (var i = 0; i < 6 && !cur; i++) cur = TE.instantiate(curGen, { seed: randSeed(), strict: false, level: lvl }); + for (var i = 0; i < 6 && !cur; i++) cur = TE.instantiate(curGen, { seed: randSeed(), strict: false }); if (!cur) { $('tr-eq').textContent = 'Не удалось сгенерировать задачу'; return; } + renderSkills(); // подсветить активный навык (мог смениться вместе с уровнем) $('tr-skill').textContent = curGen.title; var eq = $('tr-eq'); eq.classList.toggle('tr-eq-text', !cur.latex); // текстовый prompt (проценты/упрощение) — другим шрифтом @@ -842,7 +865,7 @@ function advance() { if (smart && sessAnswered >= GOAL && !summaryShown) { showSummary(); return; } if (isWord()) { serveWordProblem(); return; } // банк — без адаптивного подбора - if (smart) pickNext(); + if (smart && diffMode === 'auto' && !pinned) pickNext(); // кросс-тематический адаптив — только в Авто newProblem(); } function showSummary() { @@ -1039,8 +1062,11 @@ var b = e.target.closest('.tr-diff-btn'); if (!b) return; var d = b.getAttribute('data-d'); diffMode = (d === 'auto') ? 'auto' : (+d); + pinned = null; // выбор уровня снимает закрепление конкретного навыка renderDifficulty(); - if (cur && cur.kind !== 'word') newProblem(); // свежая задача на выбранном уровне + if (cur && cur.kind === 'word') return; + if (diffMode === 'auto' && smart) pickNext(); // вернуть адаптивный подбор + newProblem(); // chooseGen возьмёт навык нужного структурного уровня }); $('tr-subjects').addEventListener('click', function (e) { var b = e.target.closest('.tr-subbtn'); if (!b) return; @@ -1066,6 +1092,7 @@ var b = e.target.closest('.tr-chip'); if (!b) return; var t = topics[+b.getAttribute('data-ti')]; if (!t) return; curTopic = t.key; + pinned = null; // смена темы снимает закрепление навыка if (t.subject) { curSubject = t.subject; renderSubjects(); } renderTopics(); if (t.word) { renderSkills(); loadWordPool(function () { serveWordProblem(); }); return; } @@ -1078,8 +1105,9 @@ $('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(); + var g = ss[+b.getAttribute('data-si')]; if (!g) return; + curGen = g; pinned = skillKey(g); diffMode = 'auto'; // явный выбор навыка → закрепить + renderDifficulty(); renderSkills(); newProblem(); }); $('tr-smart-btn').addEventListener('click', function () { smart = !smart; diff --git a/plans/ai-trainer/ROADMAP_V2.md b/plans/ai-trainer/ROADMAP_V2.md index 6328050..e2f6b6a 100644 --- a/plans/ai-trainer/ROADMAP_V2.md +++ b/plans/ai-trainer/ROADMAP_V2.md @@ -111,13 +111,21 @@ LLM-задач (P3) из UI, генерация по теме урока. --- -## Уровни сложности — DONE -Движок: `instantiate(gen,{level})` масштабирует диапазоны `pick` (`_scaleRange`): -L2 = база, L1 — меньше магнитуды/меньше отрицательных (легче), L3 — шире (сложнее). -Универсально для всех генераторов (корень-вперёд + самопроверка держат корректность), -opt-out `gen.noScale`. Страница: контрол **Сложность: Авто / Лёгкий / Средний / -Сложный**; «Авто» поднимает уровень с серией верных в сессии (streak≥2→2, ≥4→3, ошибка→1). -Смоук движка T18 (36 ген × L1/L2/L3 материализуются; L3 шире L1; L2==база). +## Уровни сложности — DONE (структурные) +**Сложность = СТРУКТУРА задачи, а не масштаб чисел.** Каждый генератор размечен +`level` 1–3 (в `generators.js`, `LEVELS`): ур.1 — простейшая форма, ур.3 — больше +действий / скобки / дроби / переменная в обеих частях. Пример (Уравнения): +`ax+b=c` → `a(x+b)=c` → `a(x+b)=c(x+d)`; Степени: вычислить → произведение → степень +степени. Контрол **Авто / Лёгкий / Средний / Сложный** выбирает ВАРИАНТ-генератор +нужного уровня внутри текущей темы (`pickByLevel` с клампом к доступным уровням); +клик по чипу навыка — закрепляет конкретный (`pinned`); «Авто» = адаптивный подбор +(умная тренировка ведёт от простого к сложному по `order`) и показывает `ур.N` текущего. +Кросс-тематический адаптив (`pickNext`) работает только в Авто без закрепления. +Смоук страницы: Сложный→генератор ур.3, Лёгкий→ур.1. + +(Движок дополнительно умеет числовое масштабирование `instantiate(gen,{level})` +через `_scaleRange` — capability для билдера/будущего, смоук T18; страница его НЕ +использует, т.к. числа ≠ сложность.) ## Сквозное Тесты/смоуки на каждую фазу; доступность (ARIA, клавиатура, озвучка формул);