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, клавиатура, озвучка формул);