Files
Learn_System/frontend/js/labs/_tasks.js
T
Maxim Dolgolyov 15282c50b3 feat(labs): Фаза1 — фреймворк учебных заданий (LabTasks)
Превращает песочницы в учебные инструменты: задание → ответ числом с допуском →
проверка/подсказка/прогресс (по образцу race.js, но переиспользуемо).
- _tasks.js: LabTasks (панель, прогресс-точки, проверка с tol, KaTeX в условии).
- Интеграция в loadTheory (одна точка): панель «Задания» дописывается в теорию,
  бейдж на кнопке теории когда задания есть.
- Данные на 5 симуляций: quadratic, trigcircle, normaldist, projectile, pendulum.
Проверка на клиенте (учебные, не оценочные). XP — отдельным инкрементом.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 10:42:17 +03:00

156 lines
9.9 KiB
JavaScript
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.
'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: 'Правило 689599.7' },
{ q: 'Какая доля попадает в ±2σ?', a: 0.95, tol: 0.03, hint: 'Правило 689599.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 = '<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px"><polyline points="20 6 9 17 4 12"/></svg>';
var ICON_X = '<svg class="ic" viewBox="0 0 24 24" style="width:14px;height:14px;vertical-align:-2px"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
var ICON_TASK = '<svg class="ic" viewBox="0 0 24 24" style="width:15px;height:15px;vertical-align:-2px"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>';
var ICON_TROPHY = '<svg class="ic" viewBox="0 0 24 24" style="width:22px;height:22px"><path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6"/><path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22"/><path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22"/><path d="M18 2H6v7a6 6 0 0 0 12 0V2z"/></svg>';
function esc(s) { return (global.LS && LS.esc) ? LS.esc(s) : String(s == null ? '' : s).replace(/[&<>"]/g, function (c) { return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[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 '<div class="lt-panel lt-done">' + ICON_TROPHY +
'<div class="lt-done-t">Все задания выполнены!</div>' +
'<div class="lt-done-s">' + total + ' из ' + total + '</div></div>';
}
var i = s.idx, t = list[i];
var dots = list.map(function (_, k) { return '<span class="lt-dot' + (s.done[k] ? ' ok' : '') + (k === i ? ' cur' : '') + '"></span>'; }).join('');
return '<div class="lt-panel">' +
'<div class="lt-head"><span class="lt-lbl">' + ICON_TASK + ' Задание ' + (i + 1) + ' из ' + total + '</span><span class="lt-dots">' + dots + '</span></div>' +
'<div class="lt-q">' + esc(t.q) + '</div>' +
(t.formula ? '<div class="lt-formula" data-formula="' + t.formula.replace(/"/g, '&quot;') + '"></div>' : '') +
'<div class="lt-row">' +
'<input class="lt-input" id="lt-in-' + simId + '" type="number" step="any" placeholder="ответ" onkeydown="if(event.key===\'Enter\')LabTasks.check(\'' + simId + '\')">' +
(t.unit ? '<span class="lt-unit">' + esc(t.unit) + '</span>' : '') +
'<button class="lt-btn" onclick="LabTasks.check(\'' + simId + '\')">Проверить</button>' +
'</div>' +
'<div class="lt-fb" id="lt-fb-' + simId + '"></div>' +
'</div>';
}
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 '<div class="lt-host" id="lt-host-' + simId + '">' + inner(simId) + '</div>';
},
/** отрисовать формулы после вставки в 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);