Files
Learn_System/frontend/trainer.html
T
Maxim Dolgolyov 03c6ebfdce feat(trainer): страница — ввод систем (пара) + подсказки-разбор ошибок (C1)
Подключение на странице (после редизайна-премиум-консоли):
- applyInputMode: kind system в multi (скрыт префикс «x =») + placeholder «напр. x = 2; y = 3»
- answerLabel/isLabelKind: для системы показываем пару «x = 2,  y = 3»
- check(): на неверном ответе зовём TE.analyzeMistake -> адресная подсказка (не разделил на коэффициент / перепутан знак / арифметика), не выдавая ответ; иначе общий текст
- смоук страницы 40/40 (системы: пара принята, префикс скрыт; неверный ответ -> разбор-подсказка)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:55:43 +03:00

1276 lines
80 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>
/* ═══════════════════════════════════════════════════════════════════
ТРЕНАЖЁР — премиум-консоль. Двухпанельный «рабочий стол»: слева панель
прогресса/навигации, справа кинематографичная карточка-задача. Индиго→
фиолет, изумруд — успех, золото — мастерство. Мягкая глубина (clay),
стекло, aurora-фон с тетрадной клеткой. Шрифт Manrope, математика serif.
⛔ Логика/ID/инжектируемые классы НЕ менялись — только визуальный слой. */
:root {
--ink: #181b34; --ink-soft: #565d77; --ink-faint: #98a0bb;
--card: #ffffff;
--g1: #6366f1; --g2: #8b5cf6; --g0: #4f46e5;
--accent-ink: #4338ca; --accent-soft: #eef0ff;
--ok: #10b981; --ok-ink: #047857; --ok-soft: #dcfce7;
--bad: #ef4444; --bad-soft: #fee2e2; --warn: #d97706;
--gold: #f59e0b;
--line: rgba(99,102,241,.14);
--r-lg: 26px; --r-md: 18px;
--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);
--sh-1: 0 1px 0 rgba(255,255,255,.8) inset, 0 2px 6px rgba(27,31,56,.05), 0 18px 40px -24px rgba(80,70,200,.30);
--sh-2: 0 1px 0 rgba(255,255,255,.7) inset, 0 8px 20px -10px rgba(27,31,56,.14), 0 34px 70px -34px rgba(80,70,200,.45);
--ease: cubic-bezier(.22,.61,.36,1);
}
.sb-content {
background-color: #eef0f8;
background-image:
radial-gradient(1100px 680px at 90% -12%, rgba(139,92,246,.15), transparent 58%),
radial-gradient(900px 620px at -6% 4%, rgba(99,102,241,.14), transparent 55%),
radial-gradient(760px 600px at 60% 118%, rgba(56,189,248,.08), transparent 60%),
linear-gradient(rgba(99,102,241,.045) 1px, transparent 1px),
linear-gradient(90deg, rgba(99,102,241,.045) 1px, transparent 1px);
background-size: 100% 100%, 100% 100%, 100% 100%, 28px 28px, 28px 28px;
background-attachment: fixed;
}
.tr-wrap { max-width: 1240px; margin: 0 auto; padding: 22px 28px 90px; font-family: 'Manrope', system-ui, sans-serif; color: var(--ink); }
@keyframes trUp { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
@keyframes trPop { 0% { transform: scale(1); } 32% { transform: scale(1.012); } 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); } }
@keyframes trFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes ringIn { from { stroke-dashoffset: 339.292; } }
/* ═══════════ верхняя панель (стекло, плавающая) ═══════════ */
.tr-topbar2 {
position: sticky; top: 14px; z-index: 30;
display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
padding: 13px 16px 13px 20px; margin-bottom: 22px;
background: rgba(255,255,255,.72); backdrop-filter: blur(18px) saturate(1.4);
border: 1px solid rgba(255,255,255,.7); border-radius: 20px; box-shadow: var(--sh-1);
animation: trUp .5s var(--ease) both;
}
.tr-brand { display: flex; align-items: center; gap: 13px; min-width: 0; }
.tr-brand-mark { width: 42px; height: 42px; border-radius: 13px; background: linear-gradient(135deg, var(--g0), var(--g2)); box-shadow: 0 10px 22px rgba(99,102,241,.4); display: grid; place-items: center; color: #fff; flex: none; }
.tr-brand-mark .ic { width: 22px; height: 22px; }
.tr-h1 { font-family: 'Manrope', sans-serif; font-weight: 800; font-size: 1.34rem; letter-spacing: -.02em; margin: 0; line-height: 1.1; color: var(--ink); }
.tr-brand-sub { display: flex; align-items: center; gap: 8px; margin-top: 3px; }
.tr-pill {
display: inline-block; font-size: .64rem; font-weight: 800; text-transform: uppercase; letter-spacing: .06em;
padding: 4px 10px; border-radius: 99px; color: #fff;
background: linear-gradient(135deg, var(--g1), var(--g2)); box-shadow: 0 6px 14px rgba(99,102,241,.3);
}
.tr-modes { display: flex; align-items: center; gap: 9px; margin-left: auto; flex-wrap: wrap; }
/* умная тренировка — премиальный тумблер-капсула */
.tr-smart {
display: inline-flex; align-items: center; gap: 11px; cursor: pointer; user-select: none;
padding: 7px 8px 7px 15px; border-radius: 99px; border: 1px solid var(--line);
background: #fff; box-shadow: var(--sh-1); transition: .18s var(--ease);
}
.tr-smart:hover { border-color: var(--g1); }
.tr-smart:focus-visible { outline: none; border-color: var(--g1); box-shadow: 0 0 0 4px rgba(99,102,241,.18); }
.tr-smart-txt { font-size: .82rem; font-weight: 800; color: var(--ink); display: inline-flex; align-items: center; gap: 7px; }
.tr-smart-txt .ic { width: 16px; height: 16px; color: var(--g1); }
.tr-switch { width: 42px; height: 24px; border-radius: 99px; background: rgba(99,102,241,.18); position: relative; transition: .2s var(--ease); flex: none; }
.tr-switch::after { content: ''; position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: 50%; background: #fff; box-shadow: 0 2px 6px rgba(0,0,0,.25); transition: .2s var(--ease); }
.tr-smart.on .tr-switch { background: linear-gradient(135deg, var(--g1), var(--g2)); box-shadow: 0 6px 14px rgba(99,102,241,.4); }
.tr-smart.on .tr-switch::after { left: 21px; }
.tr-session { display: inline-flex; align-items: center; font-size: .8rem; font-weight: 800; color: var(--accent-ink); padding: 8px 14px; border-radius: 99px; background: rgba(99,102,241,.1); }
.tr-session:empty { display: none; }
.tr-mode-btn {
font: inherit; font-size: .8rem; font-weight: 800; cursor: pointer; display: inline-flex; align-items: center; gap: 7px;
padding: 9px 15px; border-radius: 99px; border: 1px solid var(--line); background: #fff; color: var(--ink-soft);
box-shadow: var(--sh-1); 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-admin-btn { color: #fff; border-color: transparent; background: linear-gradient(135deg, #f59e0b, #f97316); box-shadow: 0 8px 18px rgba(245,158,11,.32); }
.tr-admin-btn:hover { color: #fff; transform: translateY(-1px); }
/* ═══════════ двухпанельная сетка ═══════════ */
.tr-grid { display: grid; grid-template-columns: 312px minmax(0, 1fr); gap: 22px; align-items: start; }
.tr-rail { position: sticky; top: 96px; display: flex; flex-direction: column; gap: 16px; animation: trUp .55s var(--ease) both; animation-delay: .05s; }
.tr-main { display: flex; flex-direction: column; gap: 18px; min-width: 0; animation: trUp .55s var(--ease) both; animation-delay: .1s; }
.tr-panel { background: var(--card); border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--sh-1); }
/* секция «Предмет» скрывается, когда фильтр предметов пуст (один предмет) */
.tr-rail-sec:has(#tr-subjects:empty) { display: none; }
/* ── панель прогресса (кольцо мастерства) ── */
.tr-progress-card { padding: 20px; }
.tr-ring-row { display: flex; align-items: center; gap: 16px; }
.tr-ring { width: 92px; height: 92px; flex: none; position: relative; }
.tr-ring #tr-ring-arc { animation: ringIn 1s var(--ease); }
.tr-ring-core { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.tr-ring-core b { font-size: 1.15rem; font-weight: 800; line-height: 1; color: var(--ink); }
.tr-ring-den { font-size: .7em; color: var(--ink-faint); }
.tr-ring-core span { font-size: .56rem; font-weight: 800; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-faint); margin-top: 2px; }
.tr-progress-meta b { display: block; font-weight: 800; font-size: 1.02rem; color: var(--ink); }
.tr-progress-meta span { font-size: .82rem; color: var(--ink-soft); line-height: 1.45; }
.tr-overall { display: none; } /* свёрнут в кольцо; элемент сохранён для JS */
.tr-stat-tiles { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 16px; }
.tr-tile { padding: 13px; border-radius: var(--r-md); background: linear-gradient(180deg, #fbfbff, #f3f4fd); border: 1px solid var(--line); text-align: center; }
.tr-tile b { display: block; font-size: 1.7rem; font-weight: 800; line-height: 1; background: linear-gradient(135deg, var(--g1), var(--g2)); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
.tr-tile.gold b { background: linear-gradient(135deg, var(--gold), #f97316); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; }
.tr-tile span { display: block; margin-top: 5px; font-size: .64rem; font-weight: 800; text-transform: uppercase; letter-spacing: .06em; color: var(--ink-faint); }
/* ── секции рейла ── */
.tr-rail-sec { padding: 16px 18px 18px; }
.tr-rail-hd { font-family: 'Manrope', sans-serif; font-size: .68rem; font-weight: 800; text-transform: uppercase; letter-spacing: .09em; color: var(--ink-faint); margin: 0 0 12px; display: flex; align-items: center; gap: 7px; }
.tr-rail-hd .ic { width: 14px; height: 14px; color: var(--g1); }
/* сегмент-контрол предмета */
.tr-subjects { display: flex; gap: 5px; padding: 4px; border-radius: 14px; background: rgba(99,102,241,.08); }
.tr-subjects:empty { display: none; }
.tr-subbtn { flex: 1; font: inherit; font-size: .82rem; font-weight: 800; cursor: pointer; padding: 8px 12px; border-radius: 10px; border: none; background: transparent; color: var(--ink-soft); transition: .16s var(--ease); }
.tr-subbtn:hover { color: var(--accent-ink); }
.tr-subbtn.on { color: var(--accent-ink); background: #fff; box-shadow: 0 4px 12px rgba(27,31,56,.1); }
/* список тем (вертикальная навигация) */
.tr-topics { display: flex; flex-direction: column; gap: 5px; }
.tr-chip {
font: inherit; font-family: 'Manrope', sans-serif; font-size: .9rem; font-weight: 700; cursor: pointer; color: var(--ink-soft); text-align: left;
display: flex; align-items: center; gap: 9px; width: 100%; padding: 11px 13px; border-radius: 13px;
border: 1px solid transparent; background: transparent; transition: .16s var(--ease);
}
.tr-chip::before { content: ''; width: 7px; height: 7px; border-radius: 50%; background: rgba(99,102,241,.3); flex: none; transition: .16s var(--ease); }
.tr-chip:hover { background: rgba(99,102,241,.07); color: var(--accent-ink); }
.tr-chip:hover::before { background: var(--g1); transform: scale(1.25); }
.tr-chip.on { color: #fff; border-color: transparent; background: linear-gradient(135deg, var(--g1), var(--g2)); box-shadow: 0 10px 22px -8px rgba(99,102,241,.6); }
.tr-chip.on::before { background: #fff; }
.tr-grade { display: inline-flex; align-items: center; justify-content: center; font-family: 'Manrope', sans-serif; font-size: .58rem; font-weight: 800; min-width: 17px; height: 17px; padding: 0 4px; border-radius: 99px; background: rgba(99,102,241,.14); color: var(--accent-ink); }
/* первый мета-элемент чипа (класс/звезда) — к правому краю; если есть оба — рядом */
.tr-chip > .tr-grade, .tr-chip > .tr-badge { margin-left: auto; }
.tr-chip > .tr-grade ~ .tr-badge { margin-left: 6px; }
.tr-chip.on .tr-grade { background: rgba(255,255,255,.26); color: #fff; }
.tr-badge { display: inline-flex; color: var(--ok); }
.tr-badge .ic { width: 15px; height: 15px; }
.tr-chip.on .tr-badge { color: #fff; }
.tr-badge-n { font-size: .66rem; 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-skillbar { padding: 14px 18px; }
.tr-skillpanel-hd { font-family: 'Manrope', sans-serif; font-size: .68rem; font-weight: 800; text-transform: uppercase; letter-spacing: .09em; color: var(--ink-faint); margin-bottom: 11px; }
.tr-skills { display: flex; flex-wrap: wrap; gap: 7px; margin: 0; }
.tr-skills .tr-skill {
font: inherit; font-size: .84rem; font-weight: 700; cursor: pointer; font-family: 'Cambria Math', 'Times New Roman', serif;
padding: 8px 14px; border-radius: 11px; border: 1px solid var(--line); background: rgba(255,255,255,.7);
color: var(--ink-soft); transition: .16s var(--ease); display: inline-flex; align-items: center; gap: 6px;
}
.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); box-shadow: 0 6px 14px -6px rgba(99,102,241,.4); }
.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-card {
position: relative; overflow: hidden; background: var(--card); border: 1px solid var(--line);
border-radius: var(--r-lg); box-shadow: var(--sh-2); transition: box-shadow .3s var(--ease), transform .3s var(--ease);
}
.tr-card.tr-correct { box-shadow: 0 30px 70px -28px rgba(16,185,129,.6); animation: trPop .5s var(--ease); }
.tr-card.tr-wrong { animation: trShake .42s var(--ease); }
/* сцена-герой */
.tr-stage {
position: relative; overflow: hidden; text-align: center; color: #fff;
padding: 30px 30px 38px; 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: .6;
background-image:
radial-gradient(520px 260px at 82% -30%, rgba(255,255,255,.28), transparent 60%),
radial-gradient(420px 320px at 8% 130%, rgba(0,0,0,.18), 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%, 100% 100%, 24px 24px, 24px 24px;
}
.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-skill {
color: rgba(255,255,255,.82); font-family: 'Manrope', sans-serif; font-size: .7rem; font-weight: 800;
text-transform: uppercase; letter-spacing: .12em; margin-bottom: 16px;
}
.tr-eq {
font-family: 'Cambria Math', 'Times New Roman', Georgia, serif;
font-size: clamp(2rem, 4.2vw, 3rem); font-weight: 600; letter-spacing: .01em;
color: #fff; text-align: center; padding: 2px 0; user-select: none; text-shadow: 0 2px 18px rgba(0,0,0,.22);
}
.tr-eq .katex-display { margin: 0; }
.tr-eq .katex { font-size: 1.16em; color: #fff; }
/* текстовый prompt (проценты/упрощение) — компактнее уравнения, на сцене белым */
.tr-eq.tr-eq-text { font-family: 'Manrope', sans-serif; font-weight: 600; font-size: clamp(1.2rem, 3vw, 1.7rem); line-height: 1.45; color: #fff; }
.tr-work { padding: 24px 28px 28px; }
/* ── уровни сложности ── */
.tr-difficulty { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: center; margin-bottom: 22px; }
.tr-diff-label { font-size: .68rem; font-weight: 800; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .07em; margin-right: 4px; }
.tr-diff-btn { font: inherit; font-size: .8rem; font-weight: 800; cursor: pointer; padding: 6px 14px; border-radius: 99px; border: 1px solid var(--line); 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 8px 16px -6px rgba(99,102,241,.5); }
/* строка ответа */
.tr-inrow { display: flex; gap: 10px; align-items: stretch; max-width: 460px; margin: 0 auto; }
#tr-eqx { font-family: 'Cambria Math', serif; font-size: 1.55rem; 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.25rem; font-weight: 700; text-align: center; color: var(--ink);
padding: 14px 16px; border-radius: 15px; border: 2px solid rgba(99,102,241,.22); background: #fbfbff; outline: none; transition: .18s var(--ease);
}
.tr-input::placeholder { color: var(--ink-faint); font-weight: 500; }
.tr-input:focus { border-color: var(--g1); background: #fff; 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: 800; cursor: pointer; border: none; border-radius: 15px; padding: 14px 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 12px 26px -8px rgba(99,102,241,.7); }
.tr-primary:hover { transform: translateY(-2px); box-shadow: 0 16px 32px -8px rgba(99,102,241,.8); }
.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-keypad { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; max-width: 460px; margin: 12px auto 0; }
.tr-key { font: inherit; font-size: .98rem; font-weight: 700; font-family: 'Cambria Math', serif; cursor: pointer; min-width: 42px; padding: 8px 11px; border-radius: 11px; border: 1px solid var(--line); background: linear-gradient(180deg, #fff, #f4f5fd); color: var(--accent-ink); box-shadow: 0 2px 0 rgba(99,102,241,.12); transition: .12s var(--ease); }
.tr-key:hover { border-color: var(--g1); background: var(--accent-soft); transform: translateY(-1px); }
.tr-key:active { transform: translateY(1px); box-shadow: none; }
.tr-key .ic { width: 16px; height: 16px; }
.tr-preview { text-align: center; margin: 14px auto 0; color: var(--ink-soft); }
.tr-preview:empty { display: none; }
.tr-preview .katex { font-size: 1.14em; }
.tr-feedback { width: fit-content; margin: 20px auto 2px; min-height: 28px; padding: 8px 18px; border-radius: 99px; font-weight: 800; 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; margin: 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: 700; }
.tr-actions { display: flex; flex-wrap: wrap; gap: 9px; justify-content: center; margin-top: 18px; }
/* пошаговое решение / репетитор (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: 11px 15px; border-radius: 13px; background: linear-gradient(180deg, #f4fbf7, #ecf9f1); border: 1px solid rgba(16,185,129,.22); animation: trUp .25s var(--ease) both; }
.tr-step-ic { flex: none; width: 23px; height: 23px; 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.14rem; color: var(--ink); }
/* решение — нумерованная лента */
.tr-solution { margin-top: 22px; padding: 20px 22px; border-radius: var(--r-md); background: linear-gradient(180deg, #fbfbff, #f4f5fd); border: 1px solid var(--line); animation: trUp .35s var(--ease) both; }
.tr-solution h4 { margin: 0 0 14px; font-family: 'Manrope', sans-serif; font-size: .72rem; text-transform: uppercase; letter-spacing: .08em; color: var(--accent-ink); font-weight: 800; }
.tr-step { color: #334155; padding: 13px 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.2rem; color: var(--ink); margin-left: 31px; }
.tr-step-n { display: inline-flex; align-items: center; justify-content: center; width: 23px; height: 23px; 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-summary { position: relative; overflow: hidden; background: #fff; border: 1px solid var(--line); border-radius: var(--r-lg); padding: 32px 28px; box-shadow: var(--sh-2); 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 20px; font-family: 'Manrope', sans-serif; font-weight: 800; font-size: 1.4rem; color: var(--ink); }
.tr-sum-row { display: inline-flex; flex-direction: column; align-items: center; margin: 0 20px 12px; }
.tr-sum-row b { font-size: 2rem; 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: .7rem; color: var(--ink-faint); text-transform: uppercase; letter-spacing: .06em; font-weight: 800; }
.tr-sum-weak { margin: 10px 0 22px; color: var(--warn); font-weight: 700; font-size: .92rem; }
.tr-sum-weak.tr-sum-good { color: var(--ok-ink); }
.tr-note { text-align: center; color: var(--ink-faint); font-size: .78rem; line-height: 1.55; padding: 0 10px; }
/* ── модалка (аналитика / авторинг) ── */
.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; }
.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 var(--line); 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: 700; 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 var(--line); }
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: 700; 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; }
/* ── адаптив ── */
@media (max-width: 1080px) {
.tr-grid { grid-template-columns: 1fr; }
.tr-rail { position: static; }
.tr-progress-card .tr-stat-tiles { grid-template-columns: 1fr 1fr 1fr 1fr; }
.tr-topics { flex-direction: row; flex-wrap: wrap; }
.tr-chip { width: auto; }
.tr-chip > .tr-grade, .tr-chip > .tr-badge { margin-left: 0; }
}
@media (max-width: 620px) {
.tr-wrap { padding: 16px 14px 70px; }
.tr-topbar2 { top: 8px; padding: 12px; }
.tr-h1 { font-size: 1.16rem; }
.tr-modes { gap: 7px; }
.tr-work { padding: 20px 16px 22px; }
.tr-stage { padding: 26px 18px 30px; }
.tr-progress-card .tr-stat-tiles { grid-template-columns: 1fr 1fr; }
.tr-inrow { flex-wrap: wrap; }
.tr-inrow .tr-btn { width: 100%; justify-content: center; }
}
@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-topbar2">
<div class="tr-brand">
<span class="tr-brand-mark"><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></span>
<div>
<h1 class="tr-h1">Тренажёр</h1>
<div class="tr-brand-sub"><span class="tr-pill" id="tr-subject">Алгебра · 78 класс</span></div>
</div>
</div>
<div class="tr-modes">
<div class="tr-smart on" id="tr-smart-btn" role="switch" aria-checked="true" tabindex="0">
<span class="tr-smart-txt">
<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>
Умная тренировка
</span>
<span class="tr-switch"></span>
</div>
<span class="tr-session" id="tr-session"></span>
<button class="tr-mode-btn" id="tr-analytics-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="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>
<!-- ════ двухпанельная сетка ════ -->
<div class="tr-grid">
<!-- ── ЛЕВЫЙ РЕЙЛ ── -->
<aside class="tr-rail">
<div class="tr-panel tr-progress-card">
<div class="tr-ring-row">
<div class="tr-ring">
<svg width="92" height="92" viewBox="0 0 120 120">
<circle cx="60" cy="60" r="54" fill="none" stroke="rgba(99,102,241,.14)" stroke-width="11"/>
<circle id="tr-ring-arc" cx="60" cy="60" r="54" fill="none" stroke="url(#trrg)" stroke-width="11" stroke-linecap="round" stroke-dasharray="339.292" stroke-dashoffset="339.292" transform="rotate(-90 60 60)"/>
<defs><linearGradient id="trrg" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#6366f1"/><stop offset="1" stop-color="#8b5cf6"/></linearGradient></defs>
</svg>
<div class="tr-ring-core"><b><span id="tr-ring-num">0</span><span class="tr-ring-den" id="tr-ring-den"></span></b><span>навыков</span></div>
</div>
<div class="tr-progress-meta">
<b>Мастерство</b>
<span>Осваивай навыки — кольцо растёт с каждой освоенной темой.</span>
<div class="tr-overall" id="tr-overall"></div>
</div>
</div>
<div class="tr-stat-tiles">
<div class="tr-tile"><b id="tr-solved">0</b><span>решено</span></div>
<div class="tr-tile gold"><b id="tr-streak">0</b><span>серия</span></div>
</div>
</div>
<div class="tr-panel tr-rail-sec" id="tr-subjects-sec">
<div class="tr-rail-hd"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 7h18M3 12h18M3 17h18"/></svg>Предмет</div>
<div class="tr-subjects" id="tr-subjects"></div>
</div>
<div class="tr-panel tr-rail-sec">
<div class="tr-rail-hd"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20M4 4.5A2.5 2.5 0 0 1 6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5z"/></svg>Темы</div>
<div class="tr-topics" id="tr-topics"></div>
</div>
</aside>
<!-- ── ОСНОВНАЯ ОБЛАСТЬ ── -->
<section class="tr-main">
<div class="tr-panel tr-skillbar">
<div class="tr-skillpanel-hd" id="tr-skillpanel-hd">Навыки</div>
<div class="tr-skills" id="tr-skills"></div>
</div>
<div class="tr-card" id="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-note" id="tr-note"></div>
</section>
</div>
<!-- модалки (position:fixed — местоположение в потоке не важно) -->
<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>
</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' || k === 'system');
var eqx = $('tr-eqx'); if (eqx) eqx.style.display = multi ? 'none' : '';
$('tr-input').placeholder = (k === 'roots') ? 'корни через ;'
: (k === 'simplify') ? 'упрощённое выражение'
: (k === 'inequality') ? ('напр. ' + (cur.answerVar || 'x') + ' < 3')
: (k === 'system') ? 'напр. x = 2; y = 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);
if (cur.kind === 'system' && cur.pair) return (cur.answerVars || ['x', 'y']).map(function (v) { return v + ' = ' + fmt(cur.pair[v]); }).join(', ');
return 'x = ' + fmt(cur.answer);
}
function isLabelKind() { return cur.kind === 'roots' || cur.kind === 'simplify' || cur.kind === 'inequality' || cur.kind === 'system'; }
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;
// репетитор (C1): адресная подсказка по типовой ошибке, не выдавая ответ
var diag = TE.analyzeMistake ? TE.analyzeMistake(cur, r.value) : null;
var msg = diag ? diag.hint : 'Неверно. Разбери решение и реши похожую.';
fb.className = 'tr-feedback bad'; fb.innerHTML = ICON.bad + ' ' + esc(msg);
$('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++;
}
// премиум: кольцо мастерства в рейле (отражает уже посчитанные mastered/total)
var _total = gens.length || 1, _arc = $('tr-ring-arc');
if (_arc) { var _C = 339.292, _f = Math.max(0, Math.min(1, mastered / _total)); _arc.style.strokeDashoffset = (_C * (1 - _f)).toFixed(1); }
var _rn = $('tr-ring-num'); if (_rn) _rn.textContent = mastered;
var _rd = $('tr-ring-den'); if (_rd) _rd.textContent = '/' + _total;
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>