From d07cb2a434ca64ade5346609d7baaa9017bfef04 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Thu, 25 Jun 2026 16:10:28 +0300 Subject: [PATCH] =?UTF-8?q?feat(trainer):=20=D1=83=D1=80=D0=BE=D0=B2=D0=BD?= =?UTF-8?q?=D0=B8=20=D1=81=D0=BB=D0=BE=D0=B6=D0=BD=D0=BE=D1=81=D1=82=D0=B8?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=B4=D0=B0=D0=BD=D0=B8=D0=B9=20(=D0=9B=D1=91?= =?UTF-8?q?=D0=B3=D0=BA=D0=B8=D0=B9/=D0=A1=D1=80=D0=B5=D0=B4=D0=BD=D0=B8?= =?UTF-8?q?=D0=B9/=D0=A1=D0=BB=D0=BE=D0=B6=D0=BD=D1=8B=D0=B9=20+=20=D0=90?= =?UTF-8?q?=D0=B2=D1=82=D0=BE)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - движок: instantiate(gen,{level}) масштабирует диапазоны pick (_scaleRange): L2=база, L1 меньше магнитуды/меньше отрицательных, L3 шире → сложнее; универсально для всех генераторов (корень-вперёд + самопроверка держат корректность), opt-out gen.noScale; generateBatch прокидывает level - страница: контрол «Сложность: Авто / Лёгкий / Средний / Сложный» в рабочей зоне; «Авто» поднимает уровень с серией верных (streak≥2→2, ≥4→3, ошибка→1); скрыт для текстовых задач из банка - смоук движка 682/682 (T18: 36 ген × L1/L2/L3, L3 шире L1, L2==база), страница 34/34; эмодзи/eval 0 Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/js/trainer/_trainer_engine.js | 24 +++++++++++++++-- frontend/trainer.html | 36 +++++++++++++++++++++++++- plans/ai-trainer/ROADMAP_V2.md | 8 ++++++ 3 files changed, 65 insertions(+), 3 deletions(-) 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) для параметрики; производительность.