feat(exam-prep F8): слабые темы на дашборде + strategy=weak в тренажёре

This commit is contained in:
Maxim Dolgolyov
2026-05-29 11:38:55 +03:00
parent fe7d44aa83
commit 54b8d06c61
4 changed files with 182 additions and 9 deletions
+34 -2
View File
@@ -104,9 +104,9 @@
</div>
</div>
<div class="ep-card" style="opacity:.7">
<div class="ep-card" id="dh-weak">
<h3>Слабые темы</h3>
<div class="ep-card-hint">Топ-3 темы с худшей точностью появятся после F6 (тегирование) и F8.</div>
<div id="dh-weak-list"><div class="ep-empty" style="padding:20px"><span class="ep-skel" style="width:200px;height:1em">&nbsp;</span></div></div>
</div>
`;
@@ -120,6 +120,7 @@
renderRecent(dash.recent_attempts, track.exam_key);
renderHeatmap(dash.heatmap);
renderMocks(dash.recent_mocks, track.exam_key);
renderWeakTopics(dash.weak_topics || [], track.exam_key);
if (window.lucide) lucide.createIcons();
}
@@ -397,6 +398,37 @@ function formatRuDate(iso) {
return `${d} ${months[m - 1]} ${y}`;
}
function renderWeakTopics(items, examKey) {
const el = document.getElementById('dh-weak-list');
if (!el) return;
if (!items.length) {
el.innerHTML = `<div class="ep-empty" style="padding:20px">
<i data-lucide="check-circle-2"></i>
<p>Слабых тем не выявлено. Решите больше задач, чтобы увидеть приоритеты.</p>
</div>`;
return;
}
el.innerHTML = `
<div class="dh-weak-rows">
${items.map(w => {
const tasksLeft = Math.max(0, w.total_tasks - w.solved_tasks);
return `<a class="dh-weak-row" href="/exam-prep/${examKey}/topics/${encodeURIComponent(w.slug)}">
<div class="dh-weak-info">
<div class="dh-weak-title">${escapeHtml(w.title)}</div>
<div class="dh-weak-meta">${w.correct}/${w.attempts} попыток · ${tasksLeft} задач не взято</div>
</div>
<div class="dh-weak-accuracy">${w.accuracy}%</div>
<div class="dh-weak-cta">Прокачать <i data-lucide="arrow-right"></i></div>
</a>`;
}).join('')}
</div>
<div class="ep-cta-row" style="margin-top:14px">
<a class="ep-btn ep-btn-primary" href="/exam-prep/${examKey}/practice?strategy=weak">
<i data-lucide="target"></i> Тренировать слабые темы
</a>
</div>`;
}
function renderMocks(items, examKey) {
const el = document.getElementById('dh-mocks-list');
if (!el) return;
+16 -3
View File
@@ -13,11 +13,16 @@
// Per-session state
let batch = null; // { strategy, session_id, tasks: [...] }
let strategy = readPersistedStrategy() || 'random';
let strategy = readStrategyFromUrl() || readPersistedStrategy() || 'random';
let count = readPersistedCount();
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; }
@@ -52,7 +57,8 @@
<button class="pr-strategy-btn ${strategy === 'unsolved' ? 'active' : ''}" data-strat="unsolved">
<i data-lucide="circle-dashed"></i> Только нерешённые
</button>
<button class="pr-strategy-btn disabled" disabled title="Появится в F8 после тегирования тем">
<button class="pr-strategy-btn ${strategy === 'weak' ? 'active' : ''}" data-strat="weak"
title="Топ-3 темы с худшей точностью (нужно ≥3 попыток в теме)">
<i data-lucide="target"></i> Слабые темы
</button>
</div>
@@ -192,7 +198,7 @@
</div>
<div class="pr-summary-stat">
<div class="ep-stat-label">Стратегия</div>
<div class="ep-stat-value" style="font-size:1.1rem">${strategy === 'unsolved' ? 'Нерешённые' : 'Случайные'}</div>
<div class="ep-stat-value" style="font-size:1.1rem">${strategyLabel(strategy)}</div>
</div>
</div>
<div class="ep-cta-row">
@@ -233,4 +239,11 @@
function escapeHtml(s) {
return String(s || '').replace(/[&<>"']/g, c => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;' }[c]));
}
function strategyLabel(s) {
if (s === 'unsolved') return 'Нерешённые';
if (s === 'weak') return 'Слабые темы';
if (s === 'unsolved-fallback') return 'Нерешённые (fallback)';
return 'Случайные';
}
})();