'use strict'; /* ────────────────────────────────────────────────────────────────── Practice view — random / unsolved tasks trainer. Renders a batch of N TaskCards; once all are answered (or user clicks "Завершить"), shows a summary card with score + restart. ────────────────────────────────────────────────────────────────── */ (async function () { await EP.boot(); const examKey = EP.examKey; const main = document.getElementById('ep-main'); // Per-session state let batch = null; // { strategy, session_id, tasks: [...] } let strategy = readStrategyFromUrl() || readPersistedStrategy() || 'random'; let count = readPersistedCount(); let excludeSlugs = readPersistedExclude(); // Set let topicSections = null; // { sections: [...] } — lazy-loaded let finalized = false; const results = new Map(); // taskId -> { isCorrect: 0|1|null, attempts: number } function readStrategyFromUrl() { const m = location.search.match(/[?&]strategy=(random|unsolved|weak)/); return m ? m[1] : null; } /* ── Strategy persistence (local pref) ──────────────────────── */ function readPersistedStrategy() { try { return localStorage.getItem(`exam_prep_${examKey}_practice_strategy`); } catch { return null; } } function persistStrategy(s) { try { localStorage.setItem(`exam_prep_${examKey}_practice_strategy`, s); } catch {} } function readPersistedCount() { try { const n = Number(localStorage.getItem(`exam_prep_${examKey}_practice_count`)); return (n >= 5 && n <= 30) ? n : 10; } catch { return 10; } } function persistCount(n) { try { localStorage.setItem(`exam_prep_${examKey}_practice_count`, String(n)); } catch {} } function readPersistedExclude() { try { const raw = localStorage.getItem(`exam_prep_${examKey}_practice_exclude`) || ''; return new Set(raw.split(',').filter(Boolean)); } catch { return new Set(); } } function persistExclude(set) { try { localStorage.setItem(`exam_prep_${examKey}_practice_exclude`, Array.from(set).join(',')); } catch {} } /* ── Boot: show controls and load first batch ───────────────── */ await loadBatch(); /* ── Controls bar ───────────────────────────────────────────── */ function controlsHTML() { const opts = [5, 10, 15, 20].map(n => ` `).join(''); const excludeBadge = excludeSlugs.size ? `${excludeSlugs.size}` : ''; // Exclude only applies to the difficulty-ordered random pool. Hide // for other strategies so users don't think it filters them too. const showExclude = strategy === 'random'; return `
${showExclude ? ` ` : ''}
${strategy === 'random' ? `
Задачи идут по возрастанию сложности: 1-я всегда лёгкая, к концу — сложнее.
` : ''}
`; } function renderProgress() { const el = document.getElementById('pr-progress'); if (!el || !batch) return; const total = batch.tasks.length; const answered = Array.from(results.values()).filter(r => r.isCorrect != null).length; const correct = Array.from(results.values()).filter(r => r.isCorrect === 1).length; const pct = total ? Math.round((answered / total) * 100) : 0; el.innerHTML = `
Прогресс сессии ${answered}/${total} · ${correct} верно
`; } /* ── Batch loading + render ─────────────────────────────────── */ async function loadBatch() { finalized = false; results.clear(); main.innerHTML = controlsHTML() + `

Загрузка задач…

`; if (window.lucide) lucide.createIcons(); wireControls(); try { const query = { count, strategy }; if (strategy === 'random' && excludeSlugs.size) { query.exclude = Array.from(excludeSlugs).join(','); } batch = await EP.api.getPracticeNext(examKey, query); } catch (e) { main.querySelector('.ep-empty').outerHTML = `

Не удалось загрузить задачи

${escapeHtml(e.message || String(e))}

`; if (window.lucide) lucide.createIcons(); return; } if (!batch.tasks.length) { main.querySelector('.ep-empty').outerHTML = `

Не нашлось задач по этой стратегии

Попробуйте другую — или это знак, что вы уже всё решили.

`; if (window.lucide) lucide.createIcons(); return; } renderBatch(); } function renderBatch() { // Replace empty placeholder with task list const empty = main.querySelector('.ep-empty'); const taskContainer = document.createElement('div'); taskContainer.className = 'pr-tasks'; if (empty) empty.replaceWith(taskContainer); batch.tasks.forEach((task, i) => { results.set(task.id, { isCorrect: null, attempts: 0 }); EP.TaskCard.render(taskContainer, task, { mode: 'practice', sessionId: batch.session_id, numbering: i + 1, onAttempt: ({ taskId, isCorrect, attempt }) => { if (isCorrect == null) return; // ignore solution-only events const r = results.get(taskId); if (!r) return; // We record only the FIRST attempt result toward batch score. if (r.isCorrect == null) r.isCorrect = isCorrect; r.attempts = attempt || r.attempts + 1; renderProgress(); maybeFinalize(); }, }); }); // Bottom: "Завершить" button (allow finalizing early) const finish = document.createElement('div'); finish.className = 'pr-finish-row'; finish.innerHTML = ` `; main.appendChild(finish); finish.querySelector('.pr-finish-btn').onclick = () => finalize(); renderProgress(); if (window.lucide) lucide.createIcons(); } function maybeFinalize() { const answered = Array.from(results.values()).filter(r => r.isCorrect != null).length; if (answered === batch.tasks.length && !finalized) { finalize(); } } function finalize() { if (finalized) return; finalized = true; const total = batch.tasks.length; const answered = Array.from(results.values()).filter(r => r.isCorrect != null).length; const correct = Array.from(results.values()).filter(r => r.isCorrect === 1).length; const acc = answered ? Math.round((correct / answered) * 100) : 0; const summary = document.createElement('div'); summary.className = 'pr-summary ep-card'; summary.innerHTML = `
Решено
${answered} / ${total}
Верно
${correct}
${acc}% точности
Стратегия
${strategyLabel(strategy)}
На дашборд
`; main.appendChild(summary); summary.querySelector('#pr-summary-restart').onclick = () => loadBatch(); if (window.lucide) lucide.createIcons(); summary.scrollIntoView({ behavior: 'smooth', block: 'start' }); } /* ── Wire controls (strategy toggle, count select, restart) ── */ function wireControls() { main.querySelectorAll('[data-strat]').forEach(btn => { btn.onclick = () => { if (btn.disabled) return; strategy = btn.dataset.strat; persistStrategy(strategy); loadBatch(); }; }); const sel = main.querySelector('.pr-count-select'); if (sel) { sel.onchange = () => { count = Number(sel.value) || 10; persistCount(count); }; } const restart = main.querySelector('.pr-restart'); if (restart) restart.onclick = () => loadBatch(); const excludeBtn = main.querySelector('.pr-exclude-btn'); if (excludeBtn) excludeBtn.onclick = () => openExcludeModal(); } /* ── Exclude-topics modal ───────────────────────────────────── */ async function openExcludeModal() { if (!topicSections) { try { topicSections = await EP.api.listTopics(examKey); } catch (e) { LS.toast?.('Не удалось загрузить темы', 'error'); return; } } const sections = topicSections.sections || []; const sectionsHtml = sections.map(sec => { const items = (sec.subtopics || []).map(st => { const checked = excludeSlugs.has(st.slug) ? 'checked' : ''; return ` `; }).join(''); return `
${escapeHtml(sec.title)}
${items}
`; }).join(''); const body = `

Отмеченные темы не попадут в случайный пул. Сложность распределения сохраняется.

${sectionsHtml || '
Темы не настроены
'}
`; const m = LS.modal({ title: 'Исключить темы из пула', content: body, size: 'md', actions: [ { label: 'Сбросить', onClick: () => { m.body.querySelectorAll('input[type="checkbox"]').forEach(c => c.checked = false); } }, { label: 'Отмена', onClick: () => m.close() }, { label: 'Применить', primary: true, onClick: () => { const checked = Array.from(m.body.querySelectorAll('input[type="checkbox"]:checked')).map(c => c.value); excludeSlugs = new Set(checked); persistExclude(excludeSlugs); m.close(); loadBatch(); } }, ], }); // "Все" toggle per section m.body.querySelectorAll('.pr-ex-toggle-all').forEach(btn => { btn.onclick = () => { const items = m.body.querySelector(`[data-section-items="${btn.dataset.section}"]`); if (!items) return; const boxes = items.querySelectorAll('input[type="checkbox"]'); const allChecked = Array.from(boxes).every(b => b.checked); boxes.forEach(b => b.checked = !allChecked); }; }); } function escapeHtml(s) { return String(s || '').replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c])); } function strategyLabel(s) { if (s === 'unsolved') return 'Нерешённые'; if (s === 'weak') return 'Слабые темы'; if (s === 'unsolved-fallback') return 'Нерешённые (fallback)'; return 'Случайные'; } })();