b5916e7f3b
- Шапка-пилюля теперь УНИВЕРСАЛЬНАЯ и не мигает: всегда «Математика · 5–9 класс» (не переключается на «Алгебра»; предмет выбирается сегментом Алгебра/Геометрия) - Системы 2 ур-ний — ПОЛНОЕ решение методом сложения (6 шагов): уравнять коэффициенты при x, вычесть (исключить x → coefY·y=rhsY), найти y, подставить, найти x, ответ-пара. Коэффициенты 2..4 / |коэф|≥2 — без «1x» в шагах - Аудит решений по ВСЕМ темам: 7 «тонких» (1 шаг) генераторов (simp-like/expand, pow-mult/pow, sq-sum/diff, diff-sq) развёрнуты в 2 шага (правило → итог) - смоук T21: у каждого из 60 генераторов решение ≥2 шагов; движок 1214/1214, страница 42/42; эмодзи 0 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1289 lines
81 KiB
HTML
1289 lines
81 KiB
HTML
<!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">Математика · 5–9 класс</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, '&').replace(/</g, '<').replace(/>/g, '>'); }
|
||
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;
|
||
}
|
||
var SUBJ_LBL = { algebra: 'Алгебра', geometry: 'Геометрия' };
|
||
// подпись в шапке — УНИВЕРСАЛЬНАЯ: «Математика · 5–9 класс» (предмет выбирается
|
||
// отдельным сегментом Алгебра/Геометрия, шапка его не дублирует).
|
||
function updateSubjectPill() {
|
||
var pill = $('tr-subject'); if (!pill) return;
|
||
var gr = topics.filter(function (t) { return t.grade; }).map(function (t) { return t.grade; });
|
||
if (!gr.length) { pill.textContent = 'Математика'; return; }
|
||
var lo = Math.min.apply(null, gr), hi = Math.max.apply(null, gr);
|
||
pill.textContent = 'Математика · ' + (lo === hi ? (lo + ' класс') : (lo + '–' + hi + ' класс'));
|
||
}
|
||
function renderSubjects() {
|
||
updateSubjectPill();
|
||
var el = $('tr-subjects'); if (!el) return;
|
||
var subs = presentSubjects();
|
||
if (subs.length <= 1) { el.innerHTML = ''; return; }
|
||
el.innerHTML = subs.map(function (s) {
|
||
return '<button class="tr-subbtn' + (s === curSubject ? ' on' : '') + '" type="button" data-sub="' + s + '">' + esc(SUBJ_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 scope = skillsOf(curTopic); if (!scope || !scope.length) scope = ordered;
|
||
var id = TA.nextSkill({ ordered: scope, 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> ' + 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>
|