diff --git a/frontend/js/trainer/_trainer_engine.js b/frontend/js/trainer/_trainer_engine.js
index 62bd98a..87b039c 100644
--- a/frontend/js/trainer/_trainer_engine.js
+++ b/frontend/js/trainer/_trainer_engine.js
@@ -68,6 +68,24 @@
}
function randInt(rng, lo, hi) { return lo + Math.floor(rng() * (hi - lo + 1)); }
+ /* ── Уровни сложности: масштабирование диапазона pick ──
+ level 2 — базовый (как задано); 1 — легче (меньше магнитуды, меньше
+ отрицательных); 3 — сложнее (шире магнитуды). Универсально для всех
+ генераторов; корректность держит «корень-вперёд» + самопроверка. */
+ function _scaleRange(r, level) {
+ var lo = r[0], hi = r[1];
+ if (!level || level === 2) return [lo, hi];
+ if (level === 1) {
+ var nlo = lo < 0 ? Math.ceil(lo / 2) : lo;
+ var nhi = hi > 0 ? Math.max(nlo + 1, Math.round(hi / 2)) : hi;
+ return [nlo, nhi];
+ }
+ var elo = lo < 0 ? Math.floor(lo * 1.8) : lo;
+ var ehi = Math.round(hi * 1.8);
+ if (ehi <= elo) ehi = elo + 1;
+ return [elo, ehi];
+ }
+
/* ── Кэш компиляции выражений (рендеренные строки часто повторяются) ── */
var _cache = Object.create(null);
function compileExpr(src) {
@@ -245,8 +263,10 @@
for (var attempt = 0; attempt < maxTries; attempt++) {
var env = {};
var pk = gen.pick || {}, k;
+ var lvl = opts.level;
for (k in pk) if (Object.prototype.hasOwnProperty.call(pk, k)) {
- env[k] = randInt(rng, pk[k][0], pk[k][1]);
+ var rk = (lvl && !gen.noScale) ? _scaleRange(pk[k], lvl) : pk[k];
+ env[k] = randInt(rng, rk[0], rk[1]);
}
if (gen.constraint && !truthy(evalExpr(gen.constraint, env))) continue;
@@ -354,7 +374,7 @@
var out = [], seen = Object.create(null);
var guard = n * 20 + 50;
while (out.length < n && guard-- > 0) {
- var p = instantiate(gen, { rng: rng, strict: opts.strict, maxTries: opts.maxTries });
+ var p = instantiate(gen, { rng: rng, strict: opts.strict, maxTries: opts.maxTries, level: opts.level });
if (!p) break;
if (seen[p.display]) continue;
seen[p.display] = 1;
diff --git a/frontend/trainer.html b/frontend/trainer.html
index de984dc..614f6a6 100644
--- a/frontend/trainer.html
+++ b/frontend/trainer.html
@@ -110,6 +110,13 @@
.tr-card.tr-wrong .tr-stage { background: linear-gradient(135deg, #dc2626, #ef4444 58%, #fb7185); }
.tr-work { padding: 24px 26px 28px; }
+ /* ── уровни сложности ── */
+ .tr-difficulty { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; justify-content: center; margin-bottom: 18px; }
+ .tr-diff-label { font-size: .72rem; font-weight: 800; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .06em; margin-right: 2px; }
+ .tr-diff-btn { font: inherit; font-size: .8rem; font-weight: 700; cursor: pointer; padding: 5px 13px; border-radius: 99px; border: 1px solid rgba(99,102,241,.2); background: #fff; color: var(--ink-soft); transition: .14s var(--ease); }
+ .tr-diff-btn:hover { border-color: var(--g1); color: var(--accent-ink); }
+ .tr-diff-btn.on { color: #fff; border-color: transparent; background: linear-gradient(135deg, var(--g1), var(--g2)); box-shadow: 0 6px 14px rgba(99,102,241,.26); }
+
#tr-skill {
color: rgba(255,255,255,.75); font-family: 'Manrope', sans-serif; font-size: .72rem; font-weight: 800;
text-transform: uppercase; letter-spacing: .1em; margin-bottom: 14px;
@@ -359,6 +366,8 @@
+
+
x =
@@ -479,6 +488,7 @@
var isTeacher = !!(ip && ip.isTeacher);
var isAdmin = !!(ip && ip.isAdmin);
var curSubject = 'algebra'; // фильтр предмета (Алгебра/Геометрия)
+ var diffMode = 'auto'; // уровень сложности: 'auto' | 1 | 2 | 3
var customGens = []; // пользовательские генераторы (P13), тема «Авторские»
function skillKey(g) { return g.skill || g.id; }
function skillsOf(topicKey) {
@@ -625,6 +635,20 @@
$('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;
+ }
+ function renderDifficulty() {
+ var el = $('tr-difficulty'); if (!el) return;
+ var opts = [['auto', 'Авто'], [1, 'Лёгкий'], [2, 'Средний'], [3, 'Сложный']];
+ var autoLvl = (diffMode === 'auto') ? (' · ур.' + currentLevel()) : '';
+ el.innerHTML = 'Сложность' + opts.map(function (o) {
+ var lbl = (o[0] === 'auto') ? ('Авто' + autoLvl) : o[1];
+ return '';
+ }).join('');
+ }
// общие эффекты «задача решена» (из обычного ответа и из пошагового режима)
function onSolved() {
solved++; streak++;
@@ -730,6 +754,8 @@
: (k === 'inequality') ? ('напр. ' + (cur.answerVar || 'x') + ' < 3')
: 'ответ';
var tog = $('tr-step-toggle'); if (tog) tog.style.display = canStep() ? '' : 'none';
+ var df = $('tr-difficulty'); if (df) df.style.display = (k === 'word') ? 'none' : '';
+ renderDifficulty();
}
// Текст ответа в фидбеке/раскрытии — по типу задачи.
var REL_SYM = { '<': '<', '>': '>', '<=': '≤', '>=': '≥' };
@@ -757,7 +783,8 @@
if (isWord()) { serveWordProblem(); return; }
// strict:false + несколько попыток на случай редкой неудачи с ограничениями
cur = null;
- for (var i = 0; i < 6 && !cur; i++) cur = TE.instantiate(curGen, { seed: randSeed(), strict: false });
+ var lvl = currentLevel();
+ for (var i = 0; i < 6 && !cur; i++) cur = TE.instantiate(curGen, { seed: randSeed(), strict: false, level: lvl });
if (!cur) { $('tr-eq').textContent = 'Не удалось сгенерировать задачу'; return; }
$('tr-skill').textContent = curGen.title;
@@ -1008,6 +1035,13 @@
$('tr-teacher').addEventListener('click', function (e) { if (e.target === $('tr-teacher')) $('tr-teacher').style.display = 'none'; });
$('tr-analytics-btn').addEventListener('click', openAnalytics);
$('tr-builder-btn').addEventListener('click', function () { location.href = '/trainer-builder'; });
+ $('tr-difficulty').addEventListener('click', function (e) {
+ var b = e.target.closest('.tr-diff-btn'); if (!b) return;
+ var d = b.getAttribute('data-d');
+ diffMode = (d === 'auto') ? 'auto' : (+d);
+ renderDifficulty();
+ if (cur && cur.kind !== 'word') newProblem(); // свежая задача на выбранном уровне
+ });
$('tr-subjects').addEventListener('click', function (e) {
var b = e.target.closest('.tr-subbtn'); if (!b) return;
curSubject = b.getAttribute('data-sub');
diff --git a/plans/ai-trainer/ROADMAP_V2.md b/plans/ai-trainer/ROADMAP_V2.md
index 3dd1dec..6328050 100644
--- a/plans/ai-trainer/ROADMAP_V2.md
+++ b/plans/ai-trainer/ROADMAP_V2.md
@@ -111,6 +111,14 @@ 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==база).
+
## Сквозное
Тесты/смоуки на каждую фазу; доступность (ARIA, клавиатура, озвучка формул);
офлайн-режим (PWA) для параметрики; производительность.