Files
Learn_System/frontend/trainer.html
T
Maxim Dolgolyov f0af2079c3 fix(trainer): сложность = структура задачи, а не масштаб чисел
Пользователь верно заметил: масштабирование чисел (больше/меньше) — не настоящая сложность. Настоящая = больше действий, скобки, дроби, переменная в обеих частях.

- генераторы размечены структурным 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) <noreply@anthropic.com>
2026-06-25 16:24:31 +03:00

1176 lines
71 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Тренажёр — LearnSpace</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml"/>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&display=swap" rel="stylesheet"/>
<link rel="stylesheet" href="/css/ls.css"/>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css"/>
<script src="https://unpkg.com/lucide@0.469.0/dist/umd/lucide.min.js"></script>
<style>
/* ═══════════════ Тренажёр — «рабочий лист в клетку» ═══════════════
Фон — ambient-клетка (материал предмета: тетрадь в клетку). Задача —
чистая карточка-герой с акцентной «решающей» полосой. Акцент индиго→
фиолет, успех — изумруд, мастерство — золото. Логика/классы не менялись. */
:root {
--ink: #1b1f38; --ink-soft: #5b6378; --ink-faint: #98a1b8;
--card: #ffffff;
--g1: #6366f1; --g2: #8b5cf6;
--accent-ink: #4338ca; --accent-soft: #eef0ff;
--ok: #10b981; --ok-ink: #047857; --ok-soft: #dcfce7;
--bad: #ef4444; --bad-soft: #fee2e2; --warn: #d97706;
--gold: #f59e0b;
--r-lg: 24px;
--sh: 0 16px 40px rgba(27,31,56,.09), 0 2px 6px rgba(27,31,56,.04);
--sh-lg: 0 30px 70px rgba(27,31,56,.20);
--ease: cubic-bezier(.22,.61,.36,1);
}
.sb-content {
background-color: #f5f6fb;
background-image:
radial-gradient(1000px 600px at 86% -10%, rgba(139,92,246,.10), transparent 60%),
radial-gradient(820px 560px at 2% -6%, rgba(99,102,241,.09), transparent 55%),
linear-gradient(rgba(99,102,241,.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(99,102,241,.05) 1px, transparent 1px);
background-size: 100% 100%, 100% 100%, 26px 26px, 26px 26px;
background-attachment: fixed;
}
.tr-wrap { max-width: 740px; margin: 0 auto; padding: 34px 20px 90px; }
@keyframes trUp { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
.tr-head, .tr-overall, .tr-mode, .tr-subjects, .tr-topbar, .tr-skillpanel, .tr-card { animation: trUp .5s var(--ease) both; }
.tr-overall { animation-delay: .04s; } .tr-mode { animation-delay: .06s; }
.tr-subjects { animation-delay: .08s; } .tr-topbar { animation-delay: .1s; } .tr-skillpanel { animation-delay: .13s; } .tr-card { animation-delay: .16s; }
/* ── фильтр по предмету (Алгебра / Геометрия) ── */
.tr-subjects { display: flex; gap: 7px; margin-bottom: 14px; }
.tr-subjects:empty { display: none; }
.tr-subbtn { font: inherit; font-size: .86rem; font-weight: 800; cursor: pointer; padding: 8px 18px; border-radius: 99px; border: 1px solid rgba(99,102,241,.2); background: #fff; color: var(--ink-soft); transition: .16s var(--ease); }
.tr-subbtn:hover { border-color: var(--g1); color: var(--accent-ink); }
.tr-subbtn.on { color: #fff; border-color: transparent; background: linear-gradient(135deg, var(--g1), var(--g2)); box-shadow: 0 8px 20px rgba(99,102,241,.3); }
.tr-head { margin-bottom: 20px; }
.tr-h1 {
font-family: 'Manrope', sans-serif; font-weight: 800; font-size: clamp(1.6rem, 4.4vw, 2.1rem);
letter-spacing: -.02em; color: var(--ink); margin: 0 0 6px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
}
.tr-sub { color: var(--ink-soft); font-size: .95rem; line-height: 1.5; }
.tr-pill {
display: inline-block; font-size: .66rem; font-weight: 800; text-transform: uppercase; letter-spacing: .06em;
padding: 5px 11px; border-radius: 99px; color: #fff; vertical-align: middle;
background: linear-gradient(135deg, var(--g1), var(--g2)); box-shadow: 0 6px 16px rgba(99,102,241,.30);
}
/* ── выбор темы (вкладки) ── */
.tr-topbar { margin: 18px 0 14px; }
.tr-nav-eyebrow { display: block; font-family: 'Manrope', sans-serif; font-size: .7rem; font-weight: 800; text-transform: uppercase; letter-spacing: .08em; color: var(--ink-faint); margin-bottom: 9px; }
.tr-topics { display: flex; flex-wrap: wrap; gap: 8px; margin: 0; }
.tr-grade { display: inline-flex; align-items: center; justify-content: center; margin-left: 7px; font-family: 'Manrope', sans-serif; font-size: .6rem; font-weight: 800; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 99px; background: rgba(99,102,241,.14); color: var(--accent-ink); }
.tr-chip.on .tr-grade { background: rgba(255,255,255,.26); color: #fff; }
/* ── панель навыков выбранной темы ── */
.tr-skillpanel { background: rgba(99,102,241,.05); border: 1px solid rgba(99,102,241,.12); border-radius: 16px; padding: 13px 15px; margin-bottom: 22px; }
.tr-skillpanel-hd { font-family: 'Manrope', sans-serif; font-size: .74rem; font-weight: 800; color: var(--accent-ink); margin-bottom: 10px; }
.tr-chip {
font: inherit; font-size: .86rem; font-weight: 700; cursor: pointer; color: var(--ink-soft);
padding: 9px 16px; border-radius: 99px; border: 1px solid rgba(99,102,241,.18);
background: rgba(255,255,255,.7); backdrop-filter: blur(6px); transition: .18s var(--ease);
}
.tr-chip:hover { border-color: var(--g1); color: var(--accent-ink); transform: translateY(-1px); }
.tr-chip.on { color: #fff; border-color: transparent; background: linear-gradient(135deg, var(--g1), var(--g2)); box-shadow: 0 8px 20px rgba(99,102,241,.34); }
/* ── карточка задачи (герой) ── */
.tr-card {
position: relative; overflow: hidden; background: var(--card);
border: 1px solid rgba(99,102,241,.12); border-radius: var(--r-lg);
box-shadow: var(--sh); transition: box-shadow .3s var(--ease), transform .3s var(--ease);
}
.tr-card.tr-correct { box-shadow: 0 22px 56px rgba(16,185,129,.30); animation: trPop .5s var(--ease); }
.tr-card.tr-wrong { animation: trShake .42s var(--ease); }
@keyframes trPop { 0% { transform: scale(1); } 32% { transform: scale(1.014); } 100% { transform: scale(1); } }
@keyframes trShake { 0%,100% { transform: translateX(0); } 18% { transform: translateX(-7px); } 38% { transform: translateX(6px); } 58% { transform: translateX(-4px); } 78% { transform: translateX(2px); } }
/* ── «сцена»-герой: уравнение крупно на ярком градиенте ── */
.tr-stage {
position: relative; overflow: hidden; text-align: center; color: #fff;
padding: 32px 28px 36px; transition: background .35s var(--ease);
background: linear-gradient(135deg, #4f46e5 0%, #6d3aed 52%, #8b5cf6 100%);
}
.tr-stage::before {
content: ''; position: absolute; inset: 0; pointer-events: none; opacity: .55;
background-image:
radial-gradient(440px 220px at 80% -25%, rgba(255,255,255,.20), transparent 60%),
linear-gradient(rgba(255,255,255,.07) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,.07) 1px, transparent 1px);
background-size: 100% 100%, 22px 22px, 22px 22px;
}
.tr-stage > * { position: relative; }
.tr-card.tr-correct .tr-stage { background: linear-gradient(135deg, #059669, #10b981 58%, #34d399); }
.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;
}
.tr-eq {
font-family: 'Cambria Math', 'Times New Roman', Georgia, serif;
font-size: clamp(1.9rem, 5.6vw, 2.7rem); font-weight: 600; letter-spacing: .01em;
color: #fff; text-align: center; padding: 2px 0; user-select: none; text-shadow: 0 2px 14px rgba(0,0,0,.18);
}
.tr-inrow { display: flex; gap: 10px; align-items: stretch; max-width: 440px; margin: 0 auto; }
#tr-eqx { font-family: 'Cambria Math', serif; font-size: 1.5rem; font-weight: 600; color: var(--accent-ink); align-self: center; padding-left: 4px; }
.tr-input {
flex: 1; min-width: 0; font: inherit; font-size: 1.2rem; font-weight: 600; text-align: center; color: var(--ink);
padding: 13px 16px; border-radius: 14px; border: 2px solid rgba(99,102,241,.22); background: #fff; outline: none; transition: .18s var(--ease);
}
.tr-input::placeholder { color: var(--ink-faint); font-weight: 500; }
.tr-input:focus { border-color: var(--g1); box-shadow: 0 0 0 4px rgba(99,102,241,.16); }
.tr-input:disabled { background: #f3f4fb; color: var(--ink-soft); }
.tr-btn {
font: inherit; font-weight: 700; cursor: pointer; border: none; border-radius: 14px;
padding: 13px 22px; transition: .18s var(--ease); display: inline-flex; align-items: center; gap: 7px;
}
.tr-btn .ic { width: 17px; height: 17px; }
.tr-primary { color: #fff; background: linear-gradient(135deg, var(--g1), var(--g2)); box-shadow: 0 10px 24px rgba(99,102,241,.34); }
.tr-primary:hover { transform: translateY(-2px); box-shadow: 0 14px 30px rgba(99,102,241,.42); }
.tr-primary:active { transform: translateY(0); }
.tr-ghost { background: rgba(99,102,241,.08); color: var(--accent-ink); }
.tr-ghost:hover { background: rgba(99,102,241,.15); }
.tr-feedback {
width: fit-content; margin: 20px auto 2px; min-height: 28px; padding: 7px 16px; border-radius: 99px;
font-weight: 700; font-size: 1rem; display: flex; align-items: center; justify-content: center; gap: 9px; transition: .2s var(--ease);
}
.tr-feedback:empty { padding: 0; min-height: 0; }
.tr-feedback .ic { width: 19px; height: 19px; }
.tr-feedback.ok { color: var(--ok-ink); background: var(--ok-soft); }
.tr-feedback.bad { color: #b91c1c; background: var(--bad-soft); }
.tr-feedback.warn { color: var(--warn); background: #fef3c7; font-weight: 600; }
.tr-actions { display: flex; flex-wrap: wrap; gap: 9px; justify-content: center; margin-top: 16px; }
/* ── мат-клавиатура + live-превью (P8) ── */
.tr-keypad { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; max-width: 440px; margin: 10px auto 0; }
.tr-key { font: inherit; font-size: .95rem; font-weight: 700; font-family: 'Cambria Math', serif; cursor: pointer; min-width: 40px; padding: 7px 10px; border-radius: 10px; border: 1px solid rgba(99,102,241,.18); background: rgba(255,255,255,.8); color: var(--accent-ink); transition: .14s var(--ease); }
.tr-key:hover { border-color: var(--g1); background: var(--accent-soft); transform: translateY(-1px); }
.tr-key:active { transform: translateY(0); }
.tr-key .ic { width: 16px; height: 16px; }
.tr-preview { text-align: center; margin: 12px auto 0; color: var(--ink-soft); }
.tr-preview:empty { display: none; }
.tr-preview .katex { font-size: 1.12em; }
/* ── пошаговое решение / репетитор (P7) ── */
.tr-steps { display: flex; flex-direction: column; gap: 8px; margin-bottom: 16px; }
.tr-steps:empty { display: none; }
.tr-step-line { display: flex; align-items: center; gap: 12px; padding: 10px 14px; border-radius: 12px; background: linear-gradient(180deg, #f4fbf7, #ecf9f1); border: 1px solid rgba(16,185,129,.22); animation: trUp .25s var(--ease) both; }
.tr-step-ic { flex-shrink: 0; width: 22px; height: 22px; border-radius: 50%; background: var(--ok); color: #fff; display: inline-flex; align-items: center; justify-content: center; }
.tr-step-ic .ic { width: 14px; height: 14px; }
.tr-step-tex { font-family: 'Cambria Math', serif; font-size: 1.12rem; color: var(--ink); }
.tr-solution {
margin-top: 22px; padding: 18px 20px; border-radius: 16px;
background: linear-gradient(180deg, #fbfbff, #f4f5fd); border: 1px solid rgba(99,102,241,.14);
animation: trUp .35s var(--ease) both;
}
.tr-solution h4 { margin: 0 0 12px; font-family: 'Manrope', sans-serif; font-size: .74rem; text-transform: uppercase; letter-spacing: .07em; color: var(--accent-ink); font-weight: 800; }
.tr-step { color: #334155; padding: 12px 0; }
.tr-step + .tr-step { border-top: 1px dashed rgba(99,102,241,.2); }
.tr-step-note { display: block; color: var(--ink-soft); font-family: 'Manrope', sans-serif; font-size: .92rem; line-height: 1.6; margin-bottom: 7px; }
.tr-step-math { display: block; font-family: 'Cambria Math', serif; font-size: 1.18rem; color: var(--ink); margin-left: 30px; }
.tr-step-n { display: inline-flex; align-items: center; justify-content: center; width: 22px; height: 22px; border-radius: 50%; background: linear-gradient(135deg, var(--g1), var(--g2)); color: #fff; font-family: 'Manrope', sans-serif; font-size: .72rem; font-weight: 800; margin-right: 9px; vertical-align: 1px; }
.tr-eq .katex-display { margin: 0; }
.tr-eq .katex { font-size: 1.12em; color: #fff; }
/* текстовый prompt (проценты/упрощение) — компактнее уравнения, на сцене белым */
.tr-eq.tr-eq-text { font-family: 'Manrope', sans-serif; font-weight: 600; font-size: clamp(1.15rem, 3.4vw, 1.6rem); line-height: 1.45; color: #fff; }
/* выбор навыка внутри темы (внутри панели) */
.tr-skills { display: flex; flex-wrap: wrap; gap: 7px; margin: 0; }
.tr-skills .tr-skill {
font: inherit; font-size: .85rem; font-weight: 600; cursor: pointer; font-family: 'Cambria Math', 'Times New Roman', serif;
padding: 7px 13px; border-radius: 11px; border: 1px solid rgba(99,102,241,.16); background: rgba(255,255,255,.7);
color: var(--ink-soft); transition: .16s var(--ease); display: inline-flex; align-items: center;
}
.tr-skills .tr-skill:hover { border-color: var(--g1); color: var(--accent-ink); transform: translateY(-1px); }
.tr-skills .tr-skill.on { background: var(--accent-soft); border-color: var(--g1); color: var(--accent-ink); }
.tr-pool-info { font-size: .82rem; color: var(--ink-soft); align-self: center; margin-right: 6px; }
#tr-gen-btn { border-style: dashed; color: var(--accent-ink); }
/* бейджи прогресса на чипах */
.tr-badge { display: inline-flex; margin-left: 7px; color: var(--ok); vertical-align: middle; }
.tr-badge .ic { width: 14px; height: 14px; }
.tr-chip.on .tr-badge { color: #fff; }
.tr-badge-n { margin-left: 7px; font-size: .7rem; font-weight: 800; color: var(--ink-faint); background: rgba(99,102,241,.12); border-radius: 99px; padding: 1px 7px; }
.tr-chip.on .tr-badge-n { color: #fff; background: rgba(255,255,255,.24); }
/* ── общий прогресс (лёгкая геймификация) ── */
.tr-overall { display: inline-flex; align-items: center; gap: 8px; color: var(--accent-ink); font-size: .82rem; font-weight: 700; margin: 0 0 14px; padding: 6px 13px; border-radius: 99px; background: rgba(99,102,241,.08); }
.tr-overall:empty { display: none; }
/* ── модалка ── */
.tr-modal { position: fixed; inset: 0; z-index: 50; background: rgba(20,22,45,.55); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; padding: 20px; animation: trFade .2s ease; }
@keyframes trFade { from { opacity: 0; } to { opacity: 1; } }
.tr-modal-card { background: #fff; border-radius: 20px; max-width: 920px; width: 100%; max-height: 86vh; overflow: auto; box-shadow: var(--sh-lg); animation: trUp .3s var(--ease) both; }
.tr-modal-head { display: flex; align-items: center; justify-content: space-between; padding: 18px 22px; border-bottom: 1px solid rgba(99,102,241,.12); font-weight: 800; font-family: 'Manrope', sans-serif; font-size: 1.06rem; color: var(--ink); position: sticky; top: 0; background: #fff; z-index: 1; }
.tr-modal-x { background: none; border: none; cursor: pointer; color: var(--ink-soft); padding: 5px; border-radius: 9px; transition: .15s; }
.tr-modal-x:hover { background: rgba(99,102,241,.1); color: var(--ink); }
.tr-modal-x .ic { width: 18px; height: 18px; }
#tr-an-body, #tr-tch-body { padding: 20px 22px; }
.tr-an-picker { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; }
.tr-an-cls { font: inherit; font-size: .85rem; font-weight: 600; cursor: pointer; padding: 8px 14px; border-radius: 99px; border: 1px solid rgba(99,102,241,.2); background: #fff; color: var(--ink-soft); transition: .15s; }
.tr-an-cls:hover, .tr-an-cls.on { border-color: var(--g1); color: var(--accent-ink); background: var(--accent-soft); }
.tr-an-empty { color: var(--ink-faint); padding: 24px; text-align: center; }
.tr-hm-wrap { overflow-x: auto; border-radius: 12px; border: 1px solid rgba(99,102,241,.12); }
table.tr-hm { border-collapse: collapse; font-size: .8rem; width: 100%; }
table.tr-hm th, table.tr-hm td { border: 1px solid rgba(99,102,241,.1); padding: 7px 9px; text-align: center; white-space: nowrap; }
table.tr-hm th { background: #f6f7fd; color: var(--ink-soft); font-weight: 700; position: sticky; top: 0; }
.tr-hm-name { text-align: left !important; font-weight: 600; color: var(--ink); background: #f6f7fd; position: sticky; left: 0; }
.tr-hm-none { color: #cbd5e1; }
.tr-hm-cell { font-weight: 700; color: #334155; }
.tr-hm-cell .ic { width: 14px; height: 14px; color: #fff; }
.tr-hm-sum { font-weight: 800; color: var(--accent-ink); background: var(--accent-soft); }
/* форма авторинга задачи */
.tr-form { display: flex; flex-direction: column; gap: 13px; }
.tr-form label { display: flex; flex-direction: column; gap: 5px; font-size: .85rem; font-weight: 700; color: var(--ink-soft); }
.tr-form input, .tr-form textarea { font: inherit; padding: 10px 12px; border: 1px solid rgba(99,102,241,.22); border-radius: 11px; outline: none; resize: vertical; color: var(--ink); transition: .15s; }
.tr-form input:focus, .tr-form textarea:focus { border-color: var(--g1); box-shadow: 0 0 0 3px rgba(99,102,241,.14); }
.tr-form-row { display: flex; gap: 10px; flex-wrap: wrap; }
.tr-form-row label { flex: 1; min-width: 110px; }
.tr-form-hint { font-size: .8rem; color: var(--ink-soft); line-height: 1.5; }
.tr-form-err { color: #dc2626; font-size: .85rem; font-weight: 600; min-height: 18px; }
/* ── режим (умная тренировка) ── */
.tr-mode { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; flex-wrap: wrap; }
.tr-mode-btn {
font: inherit; font-size: .85rem; font-weight: 700; cursor: pointer; display: inline-flex; align-items: center; gap: 7px;
padding: 9px 15px; border-radius: 99px; border: 1px solid rgba(99,102,241,.18); background: rgba(255,255,255,.7); color: var(--ink-soft); transition: .16s var(--ease);
}
.tr-mode-btn .ic { width: 16px; height: 16px; }
.tr-mode-btn:hover { border-color: var(--g1); color: var(--accent-ink); transform: translateY(-1px); }
.tr-mode-btn.on { color: #fff; border-color: transparent; background: linear-gradient(135deg, var(--g1), var(--g2)); box-shadow: 0 8px 20px rgba(99,102,241,.32); }
.tr-session { font-size: .82rem; font-weight: 800; color: var(--accent-ink); padding: 4px 12px; border-radius: 99px; background: rgba(99,102,241,.1); }
.tr-session:empty { display: none; }
/* конструктор — отдельный (янтарный) цвет, только админ */
.tr-admin-btn { color: #fff; border-color: transparent; background: linear-gradient(135deg, #f59e0b, #f97316); box-shadow: 0 8px 20px rgba(245,158,11,.32); }
.tr-admin-btn:hover { color: #fff; border-color: transparent; background: linear-gradient(135deg, #f59e0b, #f97316); transform: translateY(-1px); }
/* ── итог сессии ── */
.tr-summary {
position: relative; overflow: hidden; background: #fff; border: 1px solid rgba(99,102,241,.1); border-radius: var(--r-lg);
padding: 30px 26px; box-shadow: var(--sh); text-align: center; animation: trPop .5s var(--ease);
}
.tr-summary::before { content: ''; position: absolute; left: 0; right: 0; top: 0; height: 6px; background: linear-gradient(90deg, var(--gold), var(--g2)); }
.tr-sum-h { margin: 6px 0 18px; font-family: 'Manrope', sans-serif; font-weight: 800; font-size: 1.35rem; color: var(--ink); }
.tr-sum-row { display: inline-flex; flex-direction: column; align-items: center; margin: 0 18px 12px; }
.tr-sum-row b { font-size: 1.9rem; font-weight: 800; font-family: 'Manrope', sans-serif; line-height: 1.1; background: linear-gradient(135deg, var(--g1), var(--g2)); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
.tr-sum-row span { font-size: .72rem; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .06em; font-weight: 700; }
.tr-sum-weak { margin: 10px 0 20px; color: var(--warn); font-weight: 600; font-size: .92rem; }
.tr-sum-weak.tr-sum-good { color: var(--ok-ink); }
/* ── статистика ── */
.tr-stats { display: flex; gap: 28px; justify-content: center; margin: 24px 0 4px; }
.tr-stat { text-align: center; }
.tr-stat b { display: block; font-size: 1.7rem; font-weight: 800; font-family: 'Manrope', sans-serif; line-height: 1.1; background: linear-gradient(135deg, var(--g1), var(--g2)); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
.tr-stat span { font-size: .72rem; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .06em; font-weight: 700; }
.tr-note { margin-top: 26px; text-align: center; color: var(--ink-faint); font-size: .78rem; line-height: 1.5; }
@media (max-width: 560px) {
.tr-wrap { padding: 22px 14px 70px; }
.tr-card { padding: 26px 18px 24px; }
.tr-inrow { flex-wrap: wrap; }
.tr-inrow .tr-btn { width: 100%; justify-content: center; }
.tr-stats { gap: 22px; }
}
@media (prefers-reduced-motion: reduce) {
* { animation-duration: .001ms !important; transition-duration: .001ms !important; }
}
</style>
</head>
<body>
<div class="app-layout">
<aside class="sidebar" id="app-sidebar"></aside>
<main class="sb-content">
<div class="tr-wrap">
<div class="tr-head">
<h1 class="tr-h1">Тренажёр<span class="tr-pill" id="tr-subject">Алгебра · 78 класс</span></h1>
<div class="tr-sub">Задачи генерируются автоматически и проверяются мгновенно. Решай по одной — бесконечно.</div>
</div>
<div class="tr-overall" id="tr-overall"></div>
<div class="tr-mode">
<button class="tr-mode-btn on" id="tr-smart-btn" type="button">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3l1.8 4.6L18.5 9l-4.7 1.4L12 15l-1.8-4.6L5.5 9l4.7-1.4z"/><path d="M19 14l.7 1.8L21.5 16.5l-1.8.7L19 19l-.7-1.8L16.5 16.5l1.8-.7z"/></svg>
Умная тренировка
</button>
<span class="tr-session" id="tr-session"></span>
<button class="tr-mode-btn" id="tr-analytics-btn" type="button" style="display:none;margin-left:auto">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><rect x="7" y="10" width="3" height="7"/><rect x="13" y="6" width="3" height="11"/></svg>
Аналитика класса
</button>
<button class="tr-mode-btn tr-admin-btn" id="tr-builder-btn" type="button" style="display:none">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 4V2M15 16v-2M8 9h2M20 9h2M17.8 11.8 19 13M15 9h.01M17.8 6.2 19 5M3 21l9-9M12.2 6.2 11 5"/></svg>
Конструктор
</button>
</div>
<div class="tr-modal" id="tr-analytics" style="display:none">
<div class="tr-modal-card">
<div class="tr-modal-head">
<span>Аналитика класса</span>
<button class="tr-modal-x" id="tr-an-close" type="button" aria-label="Закрыть">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
</button>
</div>
<div id="tr-an-body"></div>
</div>
</div>
<div class="tr-modal" id="tr-teacher" style="display:none">
<div class="tr-modal-card" style="max-width:560px">
<div class="tr-modal-head">
<span id="tr-tch-title">Своя задача</span>
<button class="tr-modal-x" id="tr-tch-close" type="button" aria-label="Закрыть">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>
</button>
</div>
<div id="tr-tch-body"></div>
</div>
</div>
<div class="tr-subjects" id="tr-subjects"></div>
<div class="tr-topbar">
<span class="tr-nav-eyebrow">Тема</span>
<div class="tr-topics" id="tr-topics"></div>
</div>
<div class="tr-skillpanel">
<div class="tr-skillpanel-hd" id="tr-skillpanel-hd">Навыки</div>
<div class="tr-skills" id="tr-skills"></div>
</div>
<div class="tr-card">
<div class="tr-stage">
<div class="tr-skill" id="tr-skill"></div>
<div class="tr-eq" id="tr-eq"></div>
</div>
<div class="tr-work">
<div class="tr-difficulty" id="tr-difficulty"></div>
<div id="tr-answerbox">
<div class="tr-inrow">
<span class="tr-eqx" id="tr-eqx">x =</span>
<input class="tr-input" id="tr-input" type="text" inputmode="text" autocomplete="off"
placeholder="ответ" aria-label="Ваш ответ"/>
<button class="tr-btn tr-primary" id="tr-check" type="button">Проверить</button>
</div>
<div class="tr-keypad" id="tr-keypad"></div>
<div class="tr-preview" id="tr-preview"></div>
<div class="tr-feedback" id="tr-feedback"></div>
</div>
<div id="tr-stepbox" style="display:none">
<div class="tr-steps" id="tr-steps"></div>
<div class="tr-inrow">
<input class="tr-input" id="tr-stepin" type="text" autocomplete="off"
placeholder="следующий шаг, напр. 3x = 15" aria-label="Следующий шаг"/>
<button class="tr-btn tr-primary" id="tr-stepcheck" type="button">Шаг</button>
</div>
<div class="tr-keypad" id="tr-keypad2"></div>
<div class="tr-preview" id="tr-prev2"></div>
<div class="tr-feedback" id="tr-stepfb"></div>
</div>
<div class="tr-actions">
<button class="tr-btn tr-ghost" id="tr-step-toggle" type="button" style="display:none">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01"/></svg>
Решить по шагам
</button>
<button class="tr-btn tr-ghost" id="tr-hint" type="button">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6M10 22h4M12 2a7 7 0 0 0-4 12.7c.6.5 1 1.3 1 2.1h6c0-.8.4-1.6 1-2.1A7 7 0 0 0 12 2Z"/></svg>
Подсказка
</button>
<button class="tr-btn tr-ghost" id="tr-solve" type="button">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2zM22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg>
Решение
</button>
<button class="tr-btn tr-ghost" id="tr-skip" type="button">
<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 4 12 8-12 8z"/><path d="M20 4v16"/></svg>
Другая
</button>
</div>
<div class="tr-solution" id="tr-solution" style="display:none"></div>
</div>
</div>
<div class="tr-summary" id="tr-summary" style="display:none"></div>
<div class="tr-stats">
<div class="tr-stat"><b id="tr-solved">0</b><span>решено</span></div>
<div class="tr-stat"><b id="tr-streak">0</b><span>серия</span></div>
</div>
<div class="tr-note" id="tr-note"></div>
</div>
</main>
</div>
<script src="/js/api.js"></script>
<script src="/js/sidebar.js"></script>
<script src="/js/notifications.js"></script>
<script src="/js/mobile.js"></script>
<!-- безопасный вычислитель + ядро тренажёра (тот же путь, что lab/sim-builder) -->
<script src="/js/labs/_sim_expr.js"></script>
<script src="/js/trainer/_trainer_engine.js"></script>
<script src="/js/trainer/generators.js"></script>
<script src="/js/trainer/adaptive.js"></script>
<!-- KaTeX для рендера уравнений и шагов решения -->
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script>
(function () {
'use strict';
// initPage редиректит на /login, если не авторизован
if (typeof LS === 'undefined') return;
var ip = LS.initPage();
if (!ip) return;
// Фича-гейт: «Тренажёр» можно отключить в админке (feature_trainer_enabled).
// Сайдбар/доступ скрываются через FEATURE_HREFS; здесь — редирект при прямом заходе.
// Админ имеет доступ всегда — он управляет модулями.
if (LS.loadFeatures && !ip.isAdmin) {
LS.loadFeatures().then(function (feats) {
if (feats && feats.trainer === false) { if (LS.toast) LS.toast('Тренажёр отключён', 'warn'); location.href = '/dashboard'; }
}).catch(function () {});
}
var TE = window.TrainerEngine, TG = window.TrainerGenerators, TA = window.TrainerAdaptive;
var gens = TG.list();
var ordered = gens; // прогрессия = порядок объявления (темы по order, навыки по order)
var ICON = {
ok: '<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>',
bad: '<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18M6 6l12 12"/></svg>',
star: '<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>'
};
function $(id) { return document.getElementById(id); }
function esc(s) { return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); }
function randSeed() { return (Math.random() * 2147483647) | 0; }
function fmt(v) { return TE.prettyMath(String(v)); }
// KaTeX-рендер выражения → html-строка или null (мягкий фолбэк на текст)
function kat(latex, display) {
if (window.katex && latex) {
try { return window.katex.renderToString(latex, { displayMode: !!display, throwOnError: false }); }
catch (e) {}
}
return null;
}
function setMath(el, latex, fallbackText, display) {
var h = kat(latex, display);
if (h) el.innerHTML = h; else el.textContent = fallbackText;
}
var topics = (TG.topics ? TG.topics() : [{ key: null, label: 'Задачи' }]).concat([{ key: 'word', label: 'Текстовые задачи', word: true }]);
var isTeacher = !!(ip && ip.isTeacher);
var isAdmin = !!(ip && ip.isAdmin);
var curSubject = 'algebra'; // фильтр предмета (Алгебра/Геометрия)
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) {
if (topicKey === 'custom') return customGens;
return TG.byTopic ? TG.byTopic(topicKey) : gens;
}
function isWord() { return curTopic === 'word'; }
function currentSkill() { return (cur && cur.kind === 'word') ? (cur.skill || 'word-linear') : skillKey(curGen); }
// ── пул текстовых задач (Уровень 1, LLM + серверная проверка) ──
var wordPool = [], wordIdx = 0, wordLoading = false;
function toWordProblem(p) {
return {
kind: 'word', skill: p.skill || 'word-linear', title: 'Текстовая задача',
display: p.story, latex: null,
lhsExpr: p.lhsExpr, rhsExpr: p.rhsExpr, answerVar: p.answerVar || 'x', answer: p.answer,
solution: (p.solution || []).map(function (st) {
var tex = st.tex || '';
return { note: st.note || '', tex: tex ? TE.prettyMath(tex) : '', latex: tex ? TE.exprToLatex(tex) : null };
})
};
}
function loadWordPool(done) {
if (!LS.practicePool) { wordPool = []; if (done) done(); return; }
wordLoading = true; renderSkills();
LS.practicePool('word-linear').then(function (r) {
wordPool = ((r && r.problems) || []).map(toWordProblem); wordIdx = 0;
}).catch(function () { wordPool = []; }).then(function () {
wordLoading = false; renderSkills(); if (done) done();
});
}
function serveWordProblem() {
var eq = $('tr-eq'); eq.classList.add('tr-eq-text');
$('tr-solution').style.display = 'none'; $('tr-solution').innerHTML = '';
var fb = $('tr-feedback'); fb.className = 'tr-feedback'; fb.textContent = '';
if (!wordPool.length) {
cur = null;
$('tr-skill').textContent = 'Текстовые задачи';
eq.textContent = wordLoading ? 'Загрузка…' : (isTeacher ? 'Банк пуст. Нажмите «Сгенерировать задачу».' : 'Здесь появятся текстовые задачи.');
$('tr-input').disabled = true; setMode(false);
return;
}
cur = wordPool[wordIdx % wordPool.length]; wordIdx++;
$('tr-skill').textContent = cur.title;
setMath(eq, null, cur.display, true); // условие как текст
applyInputMode();
var inp = $('tr-input'); inp.value = ''; inp.disabled = false;
var card = $('tr-card'); if (card) { card.classList.remove('tr-correct'); card.classList.remove('tr-wrong'); }
var pv = $('tr-preview'); if (pv) pv.innerHTML = '';
setMode(false); inp.focus();
setStepMode(false); // текстовые задачи — без пошагового режима
}
function genWordProblem() {
var gb = $('tr-gen-btn'); if (gb) { gb.disabled = true; gb.textContent = 'Генерирую…'; }
LS.practiceGenerate('word-linear').then(function (r) {
if (r && r.ok && r.problem) {
wordPool.unshift(toWordProblem(r.problem)); wordIdx = 0;
if (LS.toast) LS.toast('Задача добавлена (проверена за ' + r.attempts + ' попыт.)', 'success');
serveWordProblem();
}
renderSkills();
}).catch(function () {
if (LS.toast) LS.toast('Не удалось сгенерировать (LLM-провайдер не настроен?)', 'error');
renderSkills();
});
}
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 → строка прогресса с сервера
// адаптивная сессия
var smart = true, GOAL = 10;
var sessAnswered = 0, sessEvents = [], reviewQ = [], summaryShown = false;
// пошаговый режим (P7)
var stepMode = false, stepPref = false, stepList = [];
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 topicVisible(t) { return !!(t && (t.word || t.custom || (t.subject || 'algebra') === curSubject)); }
function renderTopics() {
$('tr-topics').innerHTML = topics.map(function (t, i) {
if (!topicVisible(t)) return '';
var done = topicMastered(t.key) ? '<span class="tr-badge" title="Тема освоена">' + ICON.star + '</span>' : '';
var gr = t.grade ? '<span class="tr-grade" title="' + t.grade + ' класс">' + t.grade + '</span>' : '';
return '<button class="tr-chip' + (t.key === curTopic ? ' on' : '') + '" type="button" data-ti="' + i + '">' + esc(t.label) + gr + done + '</button>';
}).join('');
}
function presentSubjects() {
var seen = {}, out = [];
topics.forEach(function (t) { if (t.subject && !seen[t.subject]) { seen[t.subject] = 1; out.push(t.subject); } });
return out;
}
function renderSubjects() {
var el = $('tr-subjects'); if (!el) return;
var subs = presentSubjects();
if (subs.length <= 1) { el.innerHTML = ''; return; }
var LBL = { algebra: 'Алгебра', geometry: 'Геометрия' };
el.innerHTML = subs.map(function (s) {
return '<button class="tr-subbtn' + (s === curSubject ? ' on' : '') + '" type="button" data-sub="' + s + '">' + esc(LBL[s] || s) + '</button>';
}).join('');
}
function skillPanelHeader() {
var hd = $('tr-skillpanel-hd'); if (!hd) return;
var t = topics.filter(function (x) { return x.key === curTopic; })[0];
hd.textContent = (curTopic === 'word') ? 'Банк текстовых задач'
: (curTopic === 'custom') ? (isAdmin ? 'Мои генераторы' : 'Авторские задачи')
: ('Навыки темы «' + (t ? t.label : '') + '»');
}
function renderSkills() {
skillPanelHeader();
if (isWord()) {
var tb = isTeacher
? '<button class="tr-skill" id="tr-gen-btn" type="button">+ ИИ-задача</button>'
+ '<button class="tr-skill" id="tr-author-btn" type="button">Своя задача</button>'
+ '<button class="tr-skill" id="tr-assign-btn" type="button">Выдать классу</button>'
: '';
$('tr-skills').innerHTML = '<span class="tr-pool-info">' + (wordLoading ? 'Загрузка…' : ('Задач в банке: ' + wordPool.length)) + '</span>' + tb;
var gb = $('tr-gen-btn'); if (gb) gb.addEventListener('click', genWordProblem);
var ab = $('tr-author-btn'); if (ab) ab.addEventListener('click', openAuthor);
var asg = $('tr-assign-btn'); if (asg) asg.addEventListener('click', openAssign);
return;
}
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('');
}
function setMode(done) {
answered = done;
$('tr-check').textContent = done ? 'Дальше' : 'Проверить';
var sc = $('tr-stepcheck'); if (sc) sc.textContent = done ? 'Дальше' : 'Шаг';
}
// ── Сложность = СТРУКТУРА задачи (какой вариант-генератор внутри темы),
// а не масштаб чисел: ур.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') ? (' · ур.' + levelOf(curGen)) : '';
el.innerHTML = '<span class="tr-diff-label">Сложность</span>' + opts.map(function (o) {
var lbl = (o[0] === 'auto') ? ('Авто' + autoLvl) : o[1];
return '<button class="tr-diff-btn' + (String(diffMode) === String(o[0]) ? ' on' : '') + '" type="button" data-d="' + o[0] + '">' + lbl + '</button>';
}).join('');
}
// общие эффекты «задача решена» (из обычного ответа и из пошагового режима)
function onSolved() {
solved++; streak++;
var card = $('tr-card'); if (card) card.classList.add('tr-correct');
recordAnswer(true); submitAttempt(true);
setMode(true); updateStats();
}
// ── мат-клавиатура + live-превью (P8) ──
var KEYS = [
{ t: '(', ins: '(' }, { t: ')', ins: ')' }, { t: 'x', ins: 'x' },
{ t: '/', ins: '/' }, { t: '^', ins: '^' }, { t: '√', ins: 'sqrt(' }, { t: ';', ins: '; ' },
{ bksp: true }
];
function buildKeypad(container, inputId, previewId) {
if (!container) return;
container.innerHTML = KEYS.map(function (k, i) {
if (k.bksp) return '<button class="tr-key" type="button" data-k="' + i + '" aria-label="Стереть"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 4H8L2 12l6 8h13a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2z"/><path d="m18 9-6 6M12 9l6 6"/></svg></button>';
return '<button class="tr-key" type="button" data-k="' + i + '">' + esc(k.t) + '</button>';
}).join('');
container.addEventListener('click', function (e) {
var b = e.target.closest('.tr-key'); if (!b) return;
var inp = $(inputId); if (!inp || inp.disabled) return;
var k = KEYS[+b.getAttribute('data-k')];
if (k.bksp) backspaceAt(inp); else insertAt(inp, k.ins);
renderPreview(inp, $(previewId));
});
}
function insertAt(inp, text) {
var s = inp.selectionStart, e = inp.selectionEnd, v = inp.value;
if (s == null || e == null) { inp.value = v + text; inp.focus(); return; }
inp.value = v.slice(0, s) + text + v.slice(e);
var pos = s + text.length; inp.focus();
try { inp.setSelectionRange(pos, pos); } catch (err) {}
}
function backspaceAt(inp) {
var s = inp.selectionStart, e = inp.selectionEnd, v = inp.value, pos;
if (s == null) { inp.value = v.slice(0, -1); inp.focus(); return; }
if (s !== e) { inp.value = v.slice(0, s) + v.slice(e); pos = s; }
else if (s > 0) { inp.value = v.slice(0, s - 1) + v.slice(s); pos = s - 1; }
else { inp.focus(); return; }
inp.focus(); try { inp.setSelectionRange(pos, pos); } catch (err) {}
}
function renderPreview(inp, prev) {
if (!prev) return;
var raw = (inp.value || '').trim();
if (!raw) { prev.innerHTML = ''; return; }
var latex = TE.exprToLatex(raw);
prev.innerHTML = latex ? (kat(latex, false) || '') : '';
}
// ── пошаговое решение / репетитор (P7) ──
function canStep() { return !!(cur && cur.kind === 'solve'); }
function setStepMode(on) {
stepMode = !!(on && canStep());
var ab = $('tr-answerbox'), sb = $('tr-stepbox');
if (ab) ab.style.display = stepMode ? 'none' : '';
if (sb) sb.style.display = stepMode ? '' : 'none';
var tog = $('tr-step-toggle'); if (tog) tog.classList.toggle('on', stepMode);
if (stepMode) {
stepList = []; renderSteps();
var fb = $('tr-stepfb'); fb.className = 'tr-feedback'; fb.textContent = '';
var si = $('tr-stepin'); si.value = ''; si.disabled = false;
$('tr-prev2').innerHTML = '';
setMode(false);
si.focus();
}
}
function renderSteps() {
$('tr-steps').innerHTML = stepList.map(function (s) {
var latex = TE.exprToLatex(s);
var math = latex ? (kat(latex, false) || esc(TE.prettyMath(s))) : esc(TE.prettyMath(s));
return '<div class="tr-step-line"><span class="tr-step-ic">' + ICON.ok + '</span><span class="tr-step-tex">' + math + '</span></div>';
}).join('');
}
function checkStepNow() {
if (answered) { advance(); return; }
var inp = $('tr-stepin'), fb = $('tr-stepfb');
var r = TE.checkStep(cur, inp.value);
if (!r.ok) {
fb.className = 'tr-feedback ' + (r.status === 'wrong' ? 'bad' : 'warn');
fb.innerHTML = (r.status === 'wrong' ? ICON.bad + ' ' : '') + esc(r.message);
return;
}
stepList.push(inp.value.trim());
renderSteps();
inp.value = ''; $('tr-prev2').innerHTML = '';
if (r.status === 'solved') {
fb.className = 'tr-feedback ok'; fb.innerHTML = ICON.ok + ' <span>Готово!</span>';
inp.disabled = true; onSolved();
} else {
fb.className = 'tr-feedback ok'; fb.innerHTML = ICON.ok + ' Верный шаг — продолжай.';
inp.focus();
}
}
// Префикс «x =» и подсказка ввода зависят от типа задачи.
function applyInputMode() {
var k = cur && cur.kind;
var multi = (k === 'roots' || k === 'simplify' || k === 'inequality');
var eqx = $('tr-eqx'); if (eqx) eqx.style.display = multi ? 'none' : '';
$('tr-input').placeholder = (k === 'roots') ? 'корни через ;'
: (k === 'simplify') ? 'упрощённое выражение'
: (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 = { '<': '<', '>': '>', '<=': '≤', '>=': '≥' };
function answerLabel() {
if (cur.kind === 'roots' && cur.answers) return 'Корни: ' + cur.answers.map(fmt).join('; ');
if (cur.kind === 'simplify') return '= ' + (cur.answerExpr ? fmt(cur.answerExpr) : '');
if (cur.kind === 'inequality' && cur.answerRel) return (cur.answerVar || 'x') + ' ' + (REL_SYM[cur.answerRel.op] || cur.answerRel.op) + ' ' + fmt(cur.answerRel.bound);
return 'x = ' + fmt(cur.answer);
}
function isLabelKind() { return cur.kind === 'roots' || cur.kind === 'simplify' || cur.kind === 'inequality'; }
function updateStats() { $('tr-solved').textContent = solved; $('tr-streak').textContent = streak; }
function stepHtml(st, n) {
if (!st) return '';
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>'; }
return '<div class="tr-step">' + note + math + '</div>';
}
function newProblem() {
if (isWord()) { serveWordProblem(); return; }
curGen = chooseGen() || curGen; // структурный вариант по уровню/закреплению
if (curGen && curGen.topic) curTopic = curGen.topic;
// strict:false + несколько попыток на случай редкой неудачи с ограничениями
cur = null;
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 (проценты/упрощение) — другим шрифтом
setMath(eq, cur.latex, cur.display, true);
applyInputMode();
var inp = $('tr-input');
inp.value = ''; inp.disabled = false;
var fb = $('tr-feedback'); fb.className = 'tr-feedback'; fb.textContent = '';
$('tr-solution').style.display = 'none'; $('tr-solution').innerHTML = '';
var card = $('tr-card'); if (card) { card.classList.remove('tr-correct'); card.classList.remove('tr-wrong'); }
var pv = $('tr-preview'); if (pv) pv.innerHTML = '';
setMode(false);
inp.focus();
setStepMode(stepPref); // сохраняем выбор «по шагам» между задачами (для kind solve)
}
// фоновая отправка попытки на сервер (прогресс/мастерство)
function submitAttempt(correct) {
if (!LS.practiceSubmit) return;
LS.practiceSubmit(currentSkill(), correct).then(function (r) {
if (r && r.progress) { prog[r.progress.skill] = r.progress; renderSkills(); renderTopics(); updateOverall(); }
}).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 revealSolution() {
var s = $('tr-solution');
s.innerHTML = solutionHtml('Решение');
s.style.display = 'block';
}
// ── адаптивная сессия ──
function updateSession() {
$('tr-session').textContent = smart ? ('Сессия: ' + Math.min(sessAnswered, GOAL) + ' / ' + GOAL) : '';
}
function pickNext(lastSkill) {
if (!TA) return;
var last = (lastSkill !== undefined) ? lastSkill : (curGen ? skillKey(curGen) : null);
var id = TA.nextSkill({ ordered: ordered, progress: prog, queue: reviewQ, answered: sessAnswered, last: last });
var g = id ? gens.filter(function (x) { return skillKey(x) === id; })[0] : null;
if (g) { curGen = g; curTopic = g.topic; if (g.subject) curSubject = g.subject; renderSubjects(); renderTopics(); renderSkills(); }
}
function recordAnswer(correct) {
var sk = currentSkill();
sessEvents.push({ skill: sk, correct: correct });
sessAnswered++;
if (TA) reviewQ = correct ? TA.onCorrect(reviewQ, sk) : TA.onWrong(reviewQ, sk, sessAnswered);
updateSession();
}
function advance() {
if (smart && sessAnswered >= GOAL && !summaryShown) { showSummary(); return; }
if (isWord()) { serveWordProblem(); return; } // банк — без адаптивного подбора
if (smart && diffMode === 'auto' && !pinned) pickNext(); // кросс-тематический адаптив — только в Авто
newProblem();
}
function showSummary() {
summaryShown = true;
var st = TA ? TA.sessionStats(sessEvents) : { total: sessAnswered, correct: solved, accuracy: 0, skills: [], weak: [] };
var weak = st.weak.map(function (s) { var g = gens.filter(function (x) { return skillKey(x) === s; })[0]; return g ? g.title : s; });
$('tr-summary').innerHTML =
'<h3 class="tr-sum-h">Итог сессии</h3>' +
'<div class="tr-sum-row"><b>' + st.correct + ' / ' + st.total + '</b><span>верно</span></div>' +
'<div class="tr-sum-row"><b>' + st.accuracy + '%</b><span>точность</span></div>' +
'<div class="tr-sum-row"><b>' + st.skills.length + '</b><span>навыков</span></div>' +
(weak.length ? '<div class="tr-sum-weak">Стоит повторить: ' + esc(weak.join(', ')) + '</div>'
: '<div class="tr-sum-weak tr-sum-good">Отличная сессия — без ошибок!</div>') +
'<button class="tr-btn tr-primary" id="tr-sum-go" type="button">Продолжить</button>';
$('tr-card').style.display = 'none';
$('tr-summary').style.display = 'block';
$('tr-sum-go').addEventListener('click', function () {
$('tr-summary').style.display = 'none';
$('tr-card').style.display = '';
sessAnswered = 0; sessEvents = []; summaryShown = false;
updateSession();
if (smart) pickNext();
newProblem();
});
}
// «Решение» до ответа = сдаться (засчитывается как неверно один раз)
function revealAnswer(giveUp) {
revealSolution();
if (giveUp && !answered) {
streak = 0;
$('tr-input').disabled = true;
var si = $('tr-stepin'); if (si) si.disabled = true;
var fb = $('tr-feedback'); fb.className = 'tr-feedback';
if (isLabelKind()) fb.textContent = 'Ответ: ' + answerLabel();
else setMath(fb, 'x = ' + cur.answer, 'Ответ: x = ' + fmt(cur.answer), false);
setMode(true);
recordAnswer(false); submitAttempt(false);
updateStats();
}
}
function check() {
if (answered) { advance(); return; }
var r = TE.checkStudentAnswer(cur, $('tr-input').value);
var fb = $('tr-feedback');
if (r.reason === 'empty' || r.reason === 'parse' || r.reason === 'nan') {
fb.className = 'tr-feedback warn'; fb.textContent = r.message; return; // не решено, можно поправить ввод
}
$('tr-input').disabled = true;
setMode(true);
if (r.ok) {
fb.className = 'tr-feedback ok';
var lbl = isLabelKind() ? esc(answerLabel())
: (kat('x = ' + cur.answer, false) || esc('x = ' + fmt(cur.answer)));
fb.innerHTML = ICON.ok + ' <span>Верно!</span>&nbsp;' + lbl;
$('tr-input').disabled = true;
onSolved();
} else {
streak = 0;
fb.className = 'tr-feedback bad'; fb.innerHTML = ICON.bad + ' Неверно. Разбери решение и реши похожую.';
$('tr-card').classList.add('tr-wrong');
recordAnswer(false); submitAttempt(false);
revealSolution();
}
updateStats();
}
// ── общий прогресс (лёгкая геймификация) ──
function updateOverall() {
var solvedTotal = 0, mastered = 0;
for (var k in prog) {
if (k === '__ms' || !Object.prototype.hasOwnProperty.call(prog, k)) continue;
var p = prog[k]; if (!p) continue;
solvedTotal += (p.solved || 0);
if (p.mastered) mastered++;
}
var el = $('tr-overall');
if (el) el.textContent = solvedTotal ? ('Освоено навыков: ' + mastered + ' из ' + gens.length + ' · решено всего: ' + solvedTotal) : '';
}
// ── учительская аналитика класса ──
var _anClasses = [], _anCur = null;
function skillTitle(id) { var g = TG.get ? TG.get(id) : null; return g ? g.title : id; }
function anPicker() {
if (_anClasses.length <= 1) return '';
return '<div class="tr-an-picker">' + _anClasses.map(function (c) {
return '<button class="tr-an-cls' + (c.id === _anCur ? ' on' : '') + '" type="button" data-cid="' + c.id + '">' + esc(c.name || ('Класс ' + c.id)) + '</button>';
}).join('') + '</div>';
}
function renderHeatmap(data) {
if (!data.skills || !data.skills.length) return '<div class="tr-an-empty">Пока нет данных — ученики ещё не решали задачи.</div>';
var head = '<tr><th>Ученик</th>' + data.skills.map(function (s) { return '<th title="' + esc(s) + '">' + esc(skillTitle(s)) + '</th>'; }).join('') + '</tr>';
var rows = data.students.map(function (st) {
var cells = data.skills.map(function (s) {
var c = st.perSkill[s];
if (!c) return '<td class="tr-hm-none">—</td>';
if (c.mastered) return '<td class="tr-hm-cell" style="background:#16a34a" title="освоено">' + ICON.star + '</td>';
var bg = c.accuracy >= 70 ? '#bbf7d0' : c.accuracy >= 40 ? '#fef9c3' : '#fecaca';
return '<td class="tr-hm-cell" style="background:' + bg + '" title="' + c.solved + ' из ' + c.attempts + '">' + c.accuracy + '%</td>';
}).join('');
return '<tr><td class="tr-hm-name">' + esc(st.name) + '</td>' + cells + '</tr>';
}).join('');
var sumCells = data.skills.map(function (s) {
var ps = data.perSkill.filter(function (x) { return x.skill === s; })[0];
return '<td class="tr-hm-sum">' + (ps ? ps.accuracy + '%' : '') + '</td>';
}).join('');
return '<div class="tr-hm-wrap"><table class="tr-hm">' + head + rows + '<tr><td class="tr-hm-name">Класс</td>' + sumCells + '</tr></table></div>';
}
function showStats(classId) {
_anCur = classId;
$('tr-an-body').innerHTML = anPicker() + '<div class="tr-an-empty">Загрузка…</div>';
LS.practiceClassStats(classId).then(function (data) {
$('tr-an-body').innerHTML = anPicker() + renderHeatmap(data);
}).catch(function () {
$('tr-an-body').innerHTML = anPicker() + '<div class="tr-an-empty">Не удалось загрузить аналитику.</div>';
});
}
function openAnalytics() {
$('tr-analytics').style.display = 'flex';
$('tr-an-body').innerHTML = '<div class="tr-an-empty">Загрузка…</div>';
(LS.getClasses ? LS.getClasses() : Promise.resolve([])).then(function (r) {
var list = Array.isArray(r) ? r : (r && (r.classes || r.items)) || [];
_anClasses = list;
if (!list.length) { $('tr-an-body').innerHTML = '<div class="tr-an-empty">У вас пока нет классов.</div>'; return; }
showStats(list[0].id);
}).catch(function () { $('tr-an-body').innerHTML = '<div class="tr-an-empty">Не удалось загрузить классы.</div>'; });
}
// ── авторинг своей задачи (учитель) ──
function openAuthor() {
$('tr-tch-title').textContent = 'Своя задача';
$('tr-tch-body').innerHTML =
'<div class="tr-form">' +
'<label>Условие<textarea id="tr-f-story" rows="3" placeholder="Текст задачи словами"></textarea></label>' +
'<div class="tr-form-row">' +
'<label>Левая часть<input id="tr-f-lhs" placeholder="2*x + 1"></label>' +
'<label>Правая часть<input id="tr-f-rhs" placeholder="7"></label>' +
'<label>Ответ x<input id="tr-f-ans" placeholder="3"></label>' +
'</div>' +
'<div class="tr-form-hint">Сервер проверит подстановкой: при этом x левая часть должна равняться правой.</div>' +
'<div class="tr-form-err" id="tr-f-err"></div>' +
'<button class="tr-btn tr-primary" id="tr-f-save" type="button">Проверить и добавить</button>' +
'</div>';
$('tr-teacher').style.display = 'flex';
$('tr-f-save').addEventListener('click', submitAuthor);
}
function submitAuthor() {
var data = { topic: 'word-linear', story: $('tr-f-story').value, lhs: $('tr-f-lhs').value, rhs: $('tr-f-rhs').value, answer: Number($('tr-f-ans').value) };
var err = $('tr-f-err'); err.textContent = '';
var btn = $('tr-f-save'); btn.disabled = true; btn.textContent = 'Проверяю…';
LS.practiceAuthor(data).then(function (r) {
if (r && r.ok && r.problem) {
wordPool.unshift(toWordProblem(r.problem)); wordIdx = 0;
if (LS.toast) LS.toast('Задача добавлена в банк', 'success');
$('tr-teacher').style.display = 'none';
if (isWord()) { renderSkills(); serveWordProblem(); }
} else { err.textContent = 'Не удалось добавить.'; btn.disabled = false; btn.textContent = 'Проверить и добавить'; }
}).catch(function () {
err.textContent = 'Проверка не прошла: при этом x левая часть не равна правой. Исправьте уравнение или ответ.';
btn.disabled = false; btn.textContent = 'Проверить и добавить';
});
}
// ── выдать тему классу (учитель) ──
function openAssign() {
$('tr-tch-title').textContent = 'Выдать классу';
$('tr-tch-body').innerHTML = '<div class="tr-an-empty">Загрузка классов…</div>';
$('tr-teacher').style.display = 'flex';
(LS.getClasses ? LS.getClasses() : Promise.resolve([])).then(function (r) {
var list = Array.isArray(r) ? r : (r && (r.classes || r.items)) || [];
if (!list.length) { $('tr-tch-body').innerHTML = '<div class="tr-an-empty">У вас пока нет классов.</div>'; return; }
$('tr-tch-body').innerHTML = '<div class="tr-form-hint">Ученики выбранного класса получат уведомление со ссылкой на тренажёр.</div>' +
'<div class="tr-an-picker" id="tr-assign-list">' + list.map(function (c) {
return '<button class="tr-an-cls" type="button" data-cid="' + c.id + '">' + esc(c.name || ('Класс ' + c.id)) + '</button>';
}).join('') + '</div>';
$('tr-assign-list').addEventListener('click', function (e) {
var b = e.target.closest('.tr-an-cls'); if (!b) return;
b.disabled = true;
LS.practiceAssign(+b.getAttribute('data-cid'), 'word-linear', 'Текстовые задачи').then(function (res) {
if (LS.toast) LS.toast('Выдано классу (' + ((res && res.notified) || 0) + ' ученикам)', 'success');
$('tr-teacher').style.display = 'none';
}).catch(function () { if (LS.toast) LS.toast('Не удалось выдать классу', 'error'); b.disabled = false; });
});
}).catch(function () { $('tr-tch-body').innerHTML = '<div class="tr-an-empty">Не удалось загрузить классы.</div>'; });
}
// ── события ──
$('tr-tch-close').addEventListener('click', function () { $('tr-teacher').style.display = 'none'; });
$('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);
pinned = null; // выбор уровня снимает закрепление конкретного навыка
renderDifficulty();
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;
curSubject = b.getAttribute('data-sub');
renderSubjects();
var ct = topics.filter(function (x) { return x.key === curTopic; })[0];
if (ct && topicVisible(ct)) { renderTopics(); return; } // текущая тема видна — просто перерисовать
var first = topics.filter(function (x) { return (x.subject || 'algebra') === curSubject; })[0];
if (first) {
curTopic = first.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-an-close').addEventListener('click', function () { $('tr-analytics').style.display = 'none'; });
$('tr-analytics').addEventListener('click', function (e) { if (e.target === $('tr-analytics')) $('tr-analytics').style.display = 'none'; });
$('tr-an-body').addEventListener('click', function (e) {
var b = e.target.closest('.tr-an-cls'); if (!b) return;
showStats(+b.getAttribute('data-cid'));
});
$('tr-topics').addEventListener('click', function (e) {
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; }
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; } }
renderSkills(); newProblem();
});
$('tr-skills').addEventListener('click', function (e) {
var b = e.target.closest('.tr-skill'); if (!b) return;
var ss = skillsOf(curTopic);
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;
$('tr-smart-btn').classList.toggle('on', smart);
updateSession();
});
$('tr-check').addEventListener('click', check);
$('tr-skip').addEventListener('click', newProblem);
$('tr-hint').addEventListener('click', function () {
if (!cur) return;
if (stepMode) {
var sol = cur.solution || [];
var idx = Math.min(stepList.length, Math.max(0, sol.length - 1));
var st = sol[idx];
var fb = $('tr-stepfb'); fb.className = 'tr-feedback warn';
fb.innerHTML = 'Подсказка: ' + (st && st.latex ? (kat(st.latex, false) || esc(st.tex || '')) : esc((st && (st.tex || st.note)) || ('x = ' + fmt(cur.answer))));
return;
}
var s = $('tr-solution');
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(); } });
// P8 — мат-клавиатуры + live-превью
buildKeypad($('tr-keypad'), 'tr-input', 'tr-preview');
buildKeypad($('tr-keypad2'), 'tr-stepin', 'tr-prev2');
$('tr-input').addEventListener('input', function () { renderPreview($('tr-input'), $('tr-preview')); });
$('tr-stepin').addEventListener('input', function () { renderPreview($('tr-stepin'), $('tr-prev2')); });
// P7 — пошаговый режим
$('tr-step-toggle').addEventListener('click', function () { stepPref = !stepMode; setStepMode(!stepMode); });
$('tr-stepcheck').addEventListener('click', checkStepNow);
$('tr-stepin').addEventListener('keydown', function (e) { if (e.key === 'Enter') { e.preventDefault(); checkStepNow(); } });
$('tr-note').textContent = gens.length + ' навыков в ' + topics.length + ' темах · умная тренировка ведёт от простого к сложному и возвращает ошибки · прогресс сохраняется.';
// загрузка прогресса → старт (умный режим: адаптивный первый навык)
function boot() {
for (var ti = 0; ti < topics.length; ti++) {
if (!topicMastered(topics[ti].key)) { curTopic = topics[ti].key; break; }
}
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; } }
if (smart) pickNext(null); // адаптивный первый навык (last=null — можно взять текущий)
renderSubjects(); renderTopics(); renderSkills(); updateSession(); updateOverall(); newProblem();
if (isTeacher) $('tr-analytics-btn').style.display = '';
if (isAdmin) $('tr-builder-btn').style.display = '';
}
Promise.all([
LS.practiceProgressList ? LS.practiceProgressList().catch(function () { return null; }) : Promise.resolve(null),
LS.practiceGenList ? LS.practiceGenList().catch(function () { return null; }) : Promise.resolve(null)
]).then(function (res) {
var pr = res[0], cgr = res[1];
if (pr && pr.progress) pr.progress.forEach(function (row) { prog[row.skill] = row; });
if (cgr && cgr.generators && cgr.generators.length) {
customGens = cgr.generators;
topics.push({ key: 'custom', label: isAdmin ? 'Мои генераторы' : 'Авторские', custom: true });
}
boot();
}).catch(boot);
})();
</script>
</body>
</html>