'use strict'; /* ────────────────────────────────────────────────────────────────── Exam 9 — Math 2025 renderer Variants loaded into window.VARIANTS by /js/exam9/variants/vNN.js ────────────────────────────────────────────────────────────────── */ const STORAGE_KEY = 'exam9_progress_v1'; let currentVariant = null; let katexLoaded = false; let variantTests = {}; // { variantNum: testId } — populated by /api/exam9/variants let userRole = null; // populated by LS.getUser() let teacherClasses = null; // lazy-loaded from /api/classes /* ── KaTeX bootstrap ────────────────────────────────────────────── */ function onKatexLoad() { katexLoaded = true; if (currentVariant !== null) runKatex(document.getElementById('ex-main')); } function runKatex(el) { if (!katexLoaded || !el) return; try { renderMathInElement(el, { delimiters: [ { left: '$$', right: '$$', display: true }, { left: '$', right: '$', display: false }, ], throwOnError: false, }); } catch {} } /* ── Progress in localStorage ───────────────────────────────────── */ function loadProgress() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'); } catch { return {}; } } function saveProgress(p) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(p)); } catch {} } function markSolutionViewed(variantNum, taskIdx) { const p = loadProgress(); p[variantNum] = p[variantNum] || []; if (!p[variantNum].includes(taskIdx)) { p[variantNum].push(taskIdx); saveProgress(p); } } /* ── Variant picker ─────────────────────────────────────────────── */ function buildGrid() { const grid = document.getElementById('variant-grid'); const progress = loadProgress(); grid.innerHTML = ''; Object.keys(VARIANTS).sort((a, b) => Number(a) - Number(b)).forEach(n => { const v = VARIANTS[n]; const total = (v.tasks || []).length; const viewed = (progress[n] || []).length; let cls = ''; if (viewed === total && total > 0) cls = ' done'; else if (viewed > 0) cls = ' partial'; const isActive = Number(n) === currentVariant ? ' active' : ''; const btn = document.createElement('button'); btn.className = 'vg-btn' + cls + isActive; btn.textContent = n; btn.title = `${v.label}${viewed === total ? ' ✓' : viewed > 0 ? ` (${viewed}/${total})` : ''}`; btn.onclick = () => { selectVariant(Number(n)); closePicker(); }; grid.appendChild(btn); }); } function togglePicker() { const overlay = document.getElementById('picker-overlay'); const btn = document.getElementById('picker-btn'); if (overlay.classList.contains('visible')) closePicker(); else { buildGrid(); overlay.classList.add('visible'); btn.classList.add('open'); document.addEventListener('keydown', onEsc); } } function closePicker() { document.getElementById('picker-overlay').classList.remove('visible'); document.getElementById('picker-btn').classList.remove('open'); document.removeEventListener('keydown', onEsc); } function onOverlayClick(e) { if (e.target === document.getElementById('picker-overlay')) closePicker(); } function onEsc(e) { if (e.key === 'Escape') closePicker(); } /* ── Task rendering ─────────────────────────────────────────────── */ function buildOpts(opts) { const isLong = opts.some(([, t]) => t.length > 40 && !t.startsWith('$')); const cls = isLong ? 'opts-vertical' : 'opts'; return `
` + opts.map(([l, t]) => `${l})${t}` ).join('') + `
`; } const SOL_ICON_CLOSED = ``; function renderVariant(num) { const main = document.getElementById('ex-main'); const v = VARIANTS[num]; if (!v) { main.innerHTML = '
Вариант не найден
'; return; } const isTeacher = userRole === 'teacher' || userRole === 'admin'; const testId = variantTests[num]; const assignBtn = isTeacher ? `
${testId ? '' : 'Этот вариант ещё не импортирован в банк (только нечётные)'}
` : ''; main.innerHTML = `
${v.label}${v.tasks.length} заданий
` + assignBtn + v.tasks.map((t, i) => `
${i + 1}
Задание ${i + 1}
${t.text}
${t.figure ? `
${t.figure}
` : ''} ${t.opts ? buildOpts(t.opts) : ''}
${t.sol ? `
${t.sol}
` : ''}
` ).join(''); runKatex(main); } function toggleSol(btn, variantNum, taskIdx) { const panel = btn.nextElementSibling; const open = panel.classList.contains('visible'); panel.classList.toggle('visible', !open); btn.classList.toggle('open', !open); btn.querySelector('span').textContent = open ? 'Показать решение' : 'Скрыть решение'; if (!open) { if (!panel.dataset.k) { runKatex(panel); panel.dataset.k = '1'; } markSolutionViewed(variantNum, taskIdx); } } function selectVariant(num) { currentVariant = num; document.getElementById('picker-label').textContent = VARIANTS[num].label; document.querySelectorAll('.vg-btn').forEach(b => { b.classList.toggle('active', Number(b.textContent) === num); }); renderVariant(num); // Persist last opened variant try { localStorage.setItem('exam9_last_variant', String(num)); } catch {} window.scrollTo({ top: 0, behavior: 'smooth' }); } /* ── Assignment modal ───────────────────────────────────────────── */ async function loadTeacherClasses() { if (teacherClasses) return teacherClasses; try { const list = await LS.api('/api/classes'); teacherClasses = Array.isArray(list) ? list : []; } catch { teacherClasses = []; } return teacherClasses; } function escapeHtml(s) { return String(s || '').replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c])); } async function openAssignModal(variantNum) { if (!variantTests[variantNum]) return; const testId = variantTests[variantNum]; // Build body with shared LS.modal helper const classes = await loadTeacherClasses(); const classesHtml = classes.length ? classes.map(c => ` `).join('') : '
У вас пока нет классов. Создайте класс на странице «Классы».
'; const body = `
${classesHtml}
`; const m = LS.modal({ title: `Назначить «Вариант ${variantNum}» как ДЗ`, content: body, size: 'sm', actions: [ { label: 'Отмена', onClick: () => m.close() }, { label: 'Назначить', primary: true, onClick: async () => { const checked = Array.from(m.body.querySelectorAll('input[name="cls"]:checked')).map(el => Number(el.value)); if (!checked.length) { m.setError('Выберите хотя бы один класс'); return; } const btns = m.root.querySelectorAll('.ls-mod-btn'); btns.forEach(b => b.disabled = true); btns[1].textContent = 'Назначаю…'; try { const r = await LS.api('/api/assignments/bulk', { method: 'POST', body: { title: `Экзамен 9 — Вариант ${variantNum}`, class_ids: checked, mode: 'exam', count: 10, test_id: testId, deadline: m.body.querySelector('#ax-deadline').value || null, is_homework: 1, }, }); LS.toast(`Назначено в ${r.count || checked.length} класс(ах)`, 'success'); m.close(); } catch (e) { m.setError(e.message || 'Не удалось создать задание'); btns.forEach(b => b.disabled = false); btns[1].textContent = 'Назначить'; } }, }, ], }); } /* ── Boot ───────────────────────────────────────────────────────── */ (async function boot() { const keys = Object.keys(VARIANTS); if (!keys.length) { document.getElementById('ex-main').innerHTML = '
Варианты не загружены
'; return; } // Load user role + variant-to-test map (parallel) const user = (typeof LS !== 'undefined') ? LS.getUser?.() : null; userRole = user?.role || null; if (userRole === 'teacher' || userRole === 'admin') { try { const r = await LS.api('/api/exam9/variants'); variantTests = r.variants || {}; } catch { variantTests = {}; } } // Resume last opened variant or open first one let initial = Number(keys[0]); try { const last = Number(localStorage.getItem('exam9_last_variant')); if (last && VARIANTS[last]) initial = last; } catch {} selectVariant(initial); })();