c0442d6803
LAB_TASKS расширен: waves, circuit, radioactive, heatengine, hydrostatics, isoprocess, probability, emfield, geometry, photosynthesis, celldivision (+ ранее quadratic/trigcircle/normaldist/projectile/pendulum) — итого 17. Только валидные single-concept id (мульти-модули molphys/chemistry пропущены). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
191 lines
13 KiB
JavaScript
191 lines
13 KiB
JavaScript
'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' },
|
||
],
|
||
waves: [
|
||
{ q: 'Скорость волны v = λ·f. Если λ = 2 м, f = 3 Гц, то v = ? (м/с)', a: 6, tol: 0.01, hint: 'Просто перемножь' },
|
||
],
|
||
circuit: [
|
||
{ q: 'Закон Ома I = U/R. При U = 12 В, R = 4 Ом ток I = ? (А)', a: 3, tol: 0.01, hint: 'I = U / R' },
|
||
],
|
||
radioactive: [
|
||
{ q: 'Через сколько периодов полураспада останется 25% вещества?', a: 2, tol: 0.01, hint: 'Каждый период — вдвое: 100→50→25' },
|
||
],
|
||
heatengine: [
|
||
{ q: 'КПД идеального цикла Карно при T_хол = 300 К, T_гор = 600 К? (долей)', a: 0.5, tol: 0.02, formula: '\\eta = 1 - T_{х}/T_{г}', hint: '1 − 300/600' },
|
||
],
|
||
hydrostatics: [
|
||
{ q: 'Давление на глубине 2 м в воде (ρ=1000, g=10)? В килопаскалях (кПа).', a: 20, tol: 0.5, formula: 'p = \\rho g h', hint: 'p = 1000·10·2 Па, переведи в кПа' },
|
||
],
|
||
isoprocess: [
|
||
{ q: 'Изотермический процесс идеального газа: чему равно ΔU? (Дж)', a: 0, tol: 0.01, hint: 'T = const → внутренняя энергия не меняется' },
|
||
],
|
||
probability: [
|
||
{ q: 'Вероятность выпадения орла у честной монеты? (дробью)', a: 0.5, tol: 0.02, hint: '1 из 2 равновероятных исходов' },
|
||
{ q: 'Вероятность выпадения шестёрки на кубике?', a: 0.1667, tol: 0.02, hint: '1 из 6' },
|
||
],
|
||
emfield: [
|
||
{ q: 'Сила Кулона ∝ 1/r². Во сколько раз изменится сила, если расстояние удвоить?', a: 0.25, tol: 0.01, hint: '1 / 2² = 1/4' },
|
||
],
|
||
geometry: [
|
||
{ q: 'Чему равна сумма углов треугольника? (в градусах)', a: 180, tol: 0.5, hint: 'Постоянна для любого треугольника' },
|
||
],
|
||
photosynthesis: [
|
||
{ q: 'Сколько молекул CO₂ нужно для синтеза одной молекулы глюкозы (цикл Кальвина)?', a: 6, tol: 0.01, hint: '6 CO₂ + 6 H₂O → C₆H₁₂O₆ + 6 O₂' },
|
||
],
|
||
celldivision: [
|
||
{ q: 'Сколько хромосом в дочерней клетке после митоза, если у материнской 46?', a: 46, tol: 0.01, hint: 'Митоз сохраняет набор' },
|
||
{ q: 'А после мейоза?', a: 23, tol: 0.01, hint: 'Мейоз уменьшает набор вдвое' },
|
||
],
|
||
};
|
||
|
||
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 ({ '&': '&', '<': '<', '>': '>', '"': '"' })[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, '"') + '"></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);
|