feat(exam9): link tasks to textbook + difficulty-ordered random + topic exclusion

Practice (random) now picks tasks by ascending difficulty so the first
slot is always level 1 and the session ramps up. Adds ?exclude= to drop
specific subtopics from the random pool, with a per-section checkbox
modal in the UI.

Each task carries a topic_ref (textbook chapter + paragraph) shown as
a 'Учить тему · §N' button next to the solution, deep-linking to the
right section of /textbook/<slug>. Mapping seeded for all 15 math9
subtopics in migration 028.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-29 14:55:47 +03:00
parent 441321c598
commit 3cc52e21b0
5 changed files with 386 additions and 9 deletions
+24 -2
View File
@@ -34,6 +34,24 @@
return String(s || '').replace(/[&<>"']/g, c => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', '"':'&quot;', "'":'&#39;' }[c]));
}
/* topic_ref → "Учить тему" deep-link to the textbook chapter/paragraph.
ref = { slug, paragraph, title }. Paragraph is null for hub links. */
function buildRefLink(ref) {
if (!ref || !ref.slug) return '';
const href = ref.paragraph
? `/textbook/${encodeURIComponent(ref.slug)}#sec-p${ref.paragraph}`
: `/textbook/${encodeURIComponent(ref.slug)}`;
const label = ref.paragraph ? `§${ref.paragraph}` : 'учебник';
const title = ref.title ? `Учить тему: ${escapeHtml(ref.title)}` : 'Перейти к материалу';
return `<a class="tc-ref-btn" href="${href}" target="_blank" rel="noopener" title="${title}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
</svg>
<span>Учить тему · ${escapeHtml(label)}</span>
</a>`;
}
function buildOptsBlock(taskId, opts) {
const isLong = opts.some(([, t]) => t.length > 40 && !t.startsWith('$'));
const cls = isLong ? 'tc-opts tc-opts-vertical' : 'tc-opts';
@@ -101,11 +119,15 @@
</div>` + verdictSlot;
}
const refLink = buildRefLink(task.topic_ref);
const solBlock = (showSol && task.solution) ? `
<div class="tc-sol-wrap">
<button class="tc-sol-btn" data-tc-sol>${ICONS.chev}<span>Показать решение</span></button>
<div class="tc-sol-row">
<button class="tc-sol-btn" data-tc-sol>${ICONS.chev}<span>Показать решение</span></button>
${refLink}
</div>
<div class="tc-sol-panel" data-tc-sol-panel>${task.solution}</div>
</div>` : '';
</div>` : (refLink ? `<div class="tc-sol-wrap"><div class="tc-sol-row">${refLink}</div></div>` : '');
card.innerHTML = `
<div class="tc-head">