diff --git a/frontend/js/labs/_tasks.js b/frontend/js/labs/_tasks.js new file mode 100644 index 0000000..26c5001 --- /dev/null +++ b/frontend/js/labs/_tasks.js @@ -0,0 +1,155 @@ +'use strict'; +/* LabTasks — лёгкий фреймворк учебных заданий для симуляций (Фаза 1 плана). + * Превращает «песочницу» в учебный инструмент: задание → ответ числом с допуском → + * проверка/подсказка/прогресс. Данные — LAB_TASKS[simId] = [{q, formula?, a, tol, unit?, hint?}]. + * Рендерится панелью в теорию (loadTheory дописывает LabTasks.mountHtml). Ответы + * проверяются на клиенте (учебные, не оценочные) — без обращения к серверу. */ +(function (global) { + + /* ── Данные заданий (наполняется по симуляциям) ── */ + var LAB_TASKS = { + quadratic: [ + { q: 'Найди дискриминант уравнения ниже.', formula: 'x^2 - 5x + 6 = 0', a: 1, tol: 0.01, hint: 'D = b² − 4ac' }, + { q: 'Сколько действительных корней у уравнения x² + 2x + 5 = 0?', a: 0, tol: 0.01, hint: 'Если D < 0 — корней нет' }, + { q: 'Чему равна абсцисса вершины параболы y = x² − 4x + 1?', a: 2, tol: 0.01, hint: 'x вершины = −b / (2a)' }, + ], + trigcircle: [ + { q: 'Чему равен sin 30°? (десятичной дробью)', a: 0.5, tol: 0.02, hint: 'sin — это ордината (y) точки на окружности' }, + { q: 'Чему равен cos 90°?', a: 0, tol: 0.02, hint: 'cos — это абсцисса (x) точки' }, + { q: 'Чему равен tg 45°?', a: 1, tol: 0.02, hint: 'tg = sin / cos' }, + ], + normaldist: [ + { q: 'Какая доля значений попадает в интервал ±1σ? (например 0.68)', a: 0.68, tol: 0.03, hint: 'Правило 68–95–99.7' }, + { q: 'Какая доля попадает в ±2σ?', a: 0.95, tol: 0.03, hint: 'Правило 68–95–99.7' }, + ], + projectile: [ + { q: 'При каком угле броска дальность максимальна (без сопротивления)? В градусах.', a: 45, tol: 1, hint: 'Дальность ∝ sin(2α), максимум при 2α = 90°' }, + ], + pendulum: [ + { q: 'Период математического маятника длиной 1 м при g ≈ 9.8 м/с²? В секундах.', a: 2.0, tol: 0.15, formula: 'T = 2\\pi\\sqrt{L/g}', hint: 'Подставь L = 1, g = 9.8' }, + ], + }; + + var ICON_CHECK = ''; + var ICON_X = ''; + var ICON_TASK = ''; + var ICON_TROPHY = ''; + + function esc(s) { return (global.LS && LS.esc) ? LS.esc(s) : String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]; }); } + + /* состояние прохождения (сбрасывается при перезагрузке — задел под persist в Фазе 2) */ + var state = {}; + function st(simId) { + if (!state[simId]) state[simId] = { idx: 0, done: LAB_TASKS[simId].map(function () { return false; }) }; + return state[simId]; + } + + function ensureStyle() { + if (document.getElementById('lt-style')) return; + var s = document.createElement('style'); s.id = 'lt-style'; + s.textContent = [ + '.lt-host{margin-top:18px;}', + '.lt-panel{border:1.5px solid rgba(155,93,229,.25);border-radius:14px;padding:14px 15px;background:rgba(155,93,229,.05);}', + '.lt-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px;}', + '.lt-lbl{display:inline-flex;align-items:center;gap:6px;font-size:.74rem;font-weight:800;letter-spacing:.04em;text-transform:uppercase;color:var(--violet,#9B5DE5);}', + '.lt-dots{display:inline-flex;gap:5px;}', + '.lt-dot{width:7px;height:7px;border-radius:50%;background:rgba(155,93,229,.25);}', + '.lt-dot.ok{background:var(--green,#06D664);}', + '.lt-dot.cur{box-shadow:0 0 0 2px rgba(155,93,229,.35);}', + '.lt-q{font-size:.9rem;line-height:1.5;color:var(--text,#0f172a);margin-bottom:8px;}', + '.lt-formula{margin:6px 0 10px;}', + '.lt-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;}', + '.lt-input{width:120px;padding:8px 11px;border:1.5px solid var(--border-h,#cbd5e1);border-radius:9px;font:inherit;font-size:.88rem;background:var(--surface,#fff);color:var(--text,#0f172a);}', + '.lt-unit{font-size:.82rem;color:var(--text-3,#56687A);}', + '.lt-btn{padding:8px 16px;border:none;border-radius:9px;background:var(--violet,#9B5DE5);color:#fff;font:700 .82rem Manrope,sans-serif;cursor:pointer;}', + '.lt-btn:hover{background:#8347d4;}', + '.lt-fb{margin-top:9px;font-size:.84rem;font-weight:600;min-height:1px;}', + '.lt-fb.ok{color:var(--green,#06A552);}', + '.lt-fb.err{color:var(--pink,#E23C8E);}', + '.lt-fb.warn{color:var(--amber,#D9831A);}', + '.lt-done{text-align:center;padding:20px 14px;}', + '.lt-done .ic{color:var(--violet,#9B5DE5);}', + '.lt-done-t{font-weight:800;font-size:.95rem;margin-top:6px;}', + '.lt-done-s{font-size:.8rem;color:var(--text-3,#56687A);margin-top:2px;}', + '#theory-toggle.lt-has{position:relative;}', + '#theory-toggle.lt-has::after{content:"";position:absolute;top:5px;right:5px;width:7px;height:7px;border-radius:50%;background:var(--violet,#9B5DE5);box-shadow:0 0 0 2px var(--bg,#EEF2FF);}', + ].join(''); + document.head.appendChild(s); + } + + function inner(simId) { + var list = LAB_TASKS[simId]; if (!list) return ''; + var s = st(simId); + var total = list.length, solved = s.done.filter(Boolean).length; + if (solved === total) { + return '
' + ICON_TROPHY + + '
Все задания выполнены!
' + + '
' + total + ' из ' + total + '
'; + } + var i = s.idx, t = list[i]; + var dots = list.map(function (_, k) { return ''; }).join(''); + return '
' + + '
' + ICON_TASK + ' Задание ' + (i + 1) + ' из ' + total + '' + dots + '
' + + '
' + esc(t.q) + '
' + + (t.formula ? '
' : '') + + '
' + + '' + + (t.unit ? '' + esc(t.unit) + '' : '') + + '' + + '
' + + '
' + + '
'; + } + + function renderFormulas(host) { + if (!global.katex) return; + host.querySelectorAll('.lt-formula[data-formula]').forEach(function (d) { + try { katex.render(d.dataset.formula, d, { displayMode: true, throwOnError: false }); } + catch (e) { d.textContent = d.dataset.formula; } + }); + } + function rerender(simId) { + var host = document.getElementById('lt-host-' + simId); + if (!host) return; + host.innerHTML = inner(simId); + renderFormulas(host); + } + + global.LabTasks = { + has: function (simId) { return !!(LAB_TASKS[simId] && LAB_TASKS[simId].length); }, + /** HTML панели заданий для вставки в теорию */ + mountHtml: function (simId) { + if (!this.has(simId)) return ''; + ensureStyle(); + return '
' + inner(simId) + '
'; + }, + /** отрисовать формулы после вставки в DOM */ + afterMount: function (simId) { + var host = document.getElementById('lt-host-' + simId); + if (host) renderFormulas(host); + }, + check: function (simId) { + var list = LAB_TASKS[simId]; if (!list) return; + var s = st(simId), t = list[s.idx]; + var inp = document.getElementById('lt-in-' + simId); + var fb = document.getElementById('lt-fb-' + simId); + if (!inp || !fb) return; + var v = parseFloat(String(inp.value || '').replace(',', '.')); + if (isNaN(v)) { fb.className = 'lt-fb warn'; fb.textContent = 'Введи число'; return; } + if (Math.abs(v - t.a) <= (t.tol || 0)) { + s.done[s.idx] = true; + fb.className = 'lt-fb ok'; fb.innerHTML = ICON_CHECK + ' Верно!'; + setTimeout(function () { + // перейти к следующему невыполненному + var next = -1; + for (var k = 1; k <= list.length; k++) { var j = (s.idx + k) % list.length; if (!s.done[j]) { next = j; break; } } + if (next !== -1) s.idx = next; + rerender(simId); + }, 650); + } else { + fb.className = 'lt-fb err'; fb.innerHTML = ICON_X + ' Не совсем.' + (t.hint ? ' Подсказка: ' + esc(t.hint) : ''); + } + }, + }; + +})(window); diff --git a/frontend/js/labs/lab-glue.js b/frontend/js/labs/lab-glue.js index b32c547..d472611 100644 --- a/frontend/js/labs/lab-glue.js +++ b/frontend/js/labs/lab-glue.js @@ -242,7 +242,15 @@ const SIMS = [ const _rm = window.LabRegistry ? window.LabRegistry.get(simId) : null; const t = (_rm && _rm.theory) ? _rm.theory : THEORY[simId]; const el = document.getElementById('theory-content'); - if (!t) { el.innerHTML = '
Теория для этой симуляции пока не добавлена
'; return; } + // Фаза 1: панель учебных заданий (если есть для этой симуляции) + const tasksHtml = (window.LabTasks && LabTasks.has(simId)) ? LabTasks.mountHtml(simId) : ''; + const tbtn = document.getElementById('theory-toggle'); + if (tbtn) tbtn.classList.toggle('lt-has', !!tasksHtml); + if (!t) { + el.innerHTML = '
Теория для этой симуляции пока не добавлена
' + tasksHtml; + if (tasksHtml) LabTasks.afterMount(simId); + return; + } let html = `
${LS.icon('book-open',16)} ${t.title}
`; for (const s of t.sections) { html += '
'; @@ -252,12 +260,13 @@ const SIMS = [ if (s.vars) html += `
${s.vars.map(([v,d]) => `
${v} — ${d}
`).join('')}
`; html += '
'; } - el.innerHTML = html; - // render KaTeX formulas + el.innerHTML = html + tasksHtml; + // render KaTeX formulas (теория) el.querySelectorAll('.tp-formula[data-formula]').forEach(div => { try { katex.render(div.dataset.formula, div, { displayMode: true, throwOnError: false }); } catch(e) { div.textContent = div.dataset.formula; } }); + if (tasksHtml) LabTasks.afterMount(simId); } /* ── Контент-движок, Фаза 5: чип «Связано с программой» ────────────────── diff --git a/frontend/lab.html b/frontend/lab.html index 35f6351..792e624 100644 --- a/frontend/lab.html +++ b/frontend/lab.html @@ -430,6 +430,7 @@ +