'use strict'; /* ────────────────────────────────────────────────────────────────── Variants view — port of the old /exam9 browser onto API + DB. Same UX as before: pick a variant from a grid overlay, then read conditions + reveal solutions. Progress (which variants have all solutions opened) is per-user via /api/exam-prep/attempts. ────────────────────────────────────────────────────────────────── */ (async function () { await EP.boot(); const examKey = EP.examKey; // Optional ?v=N in URL: open that variant initially const initialVariantFromQuery = (() => { const m = location.search.match(/[?&]v=(\d+)/); return m ? Number(m[1]) : null; })(); let variants = []; // [{ n, label, total, solved, viewed_sol }] let currentN = null; let currentTasks = null; // cache: { [variantN]: tasks[] } const tasksCache = new Map(); /* ── Load variants list ─────────────────────────────────────── */ try { const r = await EP.api.listVariants(examKey); variants = r.variants || []; } catch (e) { showError(`Не удалось загрузить варианты: ${e.message || e}`); return; } if (!variants.length) { showError('Варианты не найдены'); return; } /* ── DOM refs ───────────────────────────────────────────────── */ const main = document.getElementById('ep-main'); const pickerBtn = document.getElementById('vp-btn'); const pickerLabel = document.getElementById('vp-label'); const pickerOver = document.getElementById('vp-overlay'); const pickerGrid = document.getElementById('vp-grid'); /* ── Picker overlay ─────────────────────────────────────────── */ function buildGrid() { pickerGrid.innerHTML = variants.map(v => { let cls = ''; if (v.total > 0 && v.viewed_sol === v.total) cls = ' done'; else if (v.viewed_sol > 0) cls = ' partial'; const active = v.n === currentN ? ' active' : ''; const title = v.viewed_sol === v.total ? `${v.label} (все решения открыты)` : `${v.label} (${v.viewed_sol}/${v.total} решений открыто)`; return ``; }).join(''); pickerGrid.querySelectorAll('button[data-n]').forEach(b => { b.onclick = () => { selectVariant(Number(b.dataset.n)); closePicker(); }; }); } function openPicker() { buildGrid(); pickerOver.classList.add('visible'); pickerBtn.classList.add('open'); document.addEventListener('keydown', onEsc); } function closePicker() { pickerOver.classList.remove('visible'); pickerBtn.classList.remove('open'); document.removeEventListener('keydown', onEsc); } function onEsc(e) { if (e.key === 'Escape') closePicker(); } function onOverlayClick(e) { if (e.target === pickerOver) closePicker(); } pickerBtn.onclick = () => { if (pickerOver.classList.contains('visible')) closePicker(); else openPicker(); }; pickerOver.onclick = onOverlayClick; document.getElementById('vp-close').onclick = closePicker; /* ── Variant rendering ──────────────────────────────────────── */ async function selectVariant(n) { currentN = n; pickerLabel.textContent = `Вариант ${n}`; try { localStorage.setItem(`exam_prep_${examKey}_last_variant`, String(n)); } catch {} if (!tasksCache.has(n)) { main.innerHTML = `

Загрузка варианта ${n}…

`; if (window.lucide) lucide.createIcons(); try { const r = await EP.api.getVariant(examKey, n); tasksCache.set(n, r.tasks || []); } catch (e) { showError(`Не удалось загрузить вариант ${n}: ${e.message || e}`); return; } } currentTasks = tasksCache.get(n); renderVariant(n, currentTasks); window.scrollTo({ top: 0, behavior: 'smooth' }); } function renderVariant(n, tasks) { main.innerHTML = `
Вариант ${n}${tasks.length} заданий
` + tasks.map((t, i) => `
${t.idx}
Задание ${t.idx}
${t.text}
${t.figure ? `
${t.figure}
` : ''} ${t.opts ? buildOpts(t.opts) : ''}
${t.solution ? `
${t.solution}
` : ''}
`).join(''); main.querySelectorAll('.vp-sol-btn').forEach(btn => { btn.onclick = () => toggleSol(btn, n, Number(btn.dataset.i)); }); EP.katex.run(main); } function buildOpts(opts) { const isLong = opts.some(([, txt]) => txt.length > 40 && !txt.startsWith('$')); const cls = isLong ? 'vp-opts vp-opts-vertical' : 'vp-opts'; return `
` + opts.map(([l, t]) => `${l})${t}` ).join('') + '
'; } async function toggleSol(btn, n, i) { const panel = btn.nextElementSibling; const wasOpen = panel.classList.contains('visible'); panel.classList.toggle('visible', !wasOpen); btn.classList.toggle('open', !wasOpen); btn.querySelector('span').textContent = wasOpen ? 'Показать решение' : 'Скрыть решение'; if (!wasOpen) { if (!panel.dataset.k) { EP.katex.run(panel); panel.dataset.k = '1'; } // Persist "solution viewed" exactly once per (user, task) if (!panel.dataset.logged) { panel.dataset.logged = '1'; const taskId = currentTasks[i]?.id; if (taskId) { EP.api.saveAttempt({ exam_task_id: taskId, user_answer: null, is_correct: null, mode: 'variant', solution_viewed: 1, }).catch(() => { // Silent: progress sync is best-effort panel.dataset.logged = ''; }); // Local optimistic update of picker grid const v = variants.find(v => v.n === n); if (v && panel.dataset.firstView !== '1') { panel.dataset.firstView = '1'; v.viewed_sol = Math.min(v.viewed_sol + 1, v.total); } } } } } function showError(msg) { document.getElementById('ep-main').innerHTML = `

${escapeHtml(msg)}

`; if (window.lucide) lucide.createIcons(); } function escapeHtml(s) { return String(s || '').replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c])); } /* ── Pick the initial variant ───────────────────────────────── */ let initial = variants[0].n; if (initialVariantFromQuery && variants.some(v => v.n === initialVariantFromQuery)) { initial = initialVariantFromQuery; } else { try { const last = Number(localStorage.getItem(`exam_prep_${examKey}_last_variant`)); if (last && variants.some(v => v.n === last)) initial = last; } catch {} } selectVariant(initial); })();