UI bugfixes with boss
This commit is contained in:
@@ -22,12 +22,6 @@
|
||||
const btnFinish = document.getElementById('attempt-finish');
|
||||
const resultEl = document.getElementById('attempt-result');
|
||||
|
||||
const hintModal = document.getElementById('hint-modal');
|
||||
const hintVerdict = document.getElementById('hint-verdict');
|
||||
const hintCorrect = document.getElementById('hint-correct');
|
||||
const hintExplanation = document.getElementById('hint-explanation');
|
||||
const hintCloseBtn = document.getElementById('hint-close-btn');
|
||||
|
||||
let playData = null;
|
||||
const selections = {};
|
||||
const checked = {};
|
||||
@@ -65,6 +59,41 @@
|
||||
return playData && playData.resultMode === 'immediate';
|
||||
}
|
||||
|
||||
function formatAttemptMeta(d) {
|
||||
const qs = d.questions || [];
|
||||
const n = qs.length;
|
||||
const tl = d.timeLimit;
|
||||
const timestr = !tl || Number(tl) <= 0 ? 'без ограничения' : `${tl} мин`;
|
||||
const res = d.resultMode === 'immediate' ? 'сразу' : 'в конце';
|
||||
const hint = d.resultMode !== 'immediate'
|
||||
? 'недоступны'
|
||||
: (d.hintsEnabled ? 'вкл' : 'выкл');
|
||||
const th = d.passingThreshold ?? 0;
|
||||
return `Порог: ${th}% · Вопросов: ${n} · Время: ${timestr} · Результат: ${res} · Подсказки: ${hint}`;
|
||||
}
|
||||
|
||||
function renderFeedbackPanel(data) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'attempt-feedback-panel';
|
||||
const ok = data.isCorrect;
|
||||
const verdict = document.createElement('p');
|
||||
verdict.className = `attempt-feedback-verdict ${ok ? 'attempt-feedback-verdict--ok' : 'attempt-feedback-verdict--bad'}`;
|
||||
verdict.textContent = ok ? 'Верно!' : 'Неверно.';
|
||||
wrap.appendChild(verdict);
|
||||
const correct = (data.correctOptionTexts || []).join('; ');
|
||||
if (correct) {
|
||||
const p = document.createElement('p');
|
||||
p.className = 'attempt-feedback-correct';
|
||||
p.textContent = `Правильный ответ: ${correct}`;
|
||||
wrap.appendChild(p);
|
||||
}
|
||||
const exp = document.createElement('p');
|
||||
exp.className = 'attempt-feedback-explanation';
|
||||
exp.textContent = data.explanation || 'Объяснение недоступно.';
|
||||
wrap.appendChild(exp);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
function stepAnswered(q) {
|
||||
const k = String(q.id);
|
||||
if (isImmediate()) return !!checked[k];
|
||||
@@ -151,6 +180,10 @@
|
||||
}
|
||||
card.appendChild(ul);
|
||||
|
||||
if (isChk && isImmediate() && playData.hintsEnabled) {
|
||||
card.appendChild(renderFeedbackPanel(checked[qid]));
|
||||
}
|
||||
|
||||
if (isImmediate() && !isChk) {
|
||||
const wrap = document.createElement('div');
|
||||
wrap.className = 'attempt-answer-actions';
|
||||
@@ -204,29 +237,11 @@
|
||||
if (!r.ok) throw new Error(data.error || 'Не удалось проверить ответ.');
|
||||
checked[k] = data;
|
||||
renderStep();
|
||||
if (playData.hintsEnabled) {
|
||||
showHint(data);
|
||||
}
|
||||
} catch (e) {
|
||||
setErr(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function showHint(data) {
|
||||
hintVerdict.textContent = data.isCorrect ? 'Верно!' : 'Неверно.';
|
||||
hintVerdict.className = `attempt-hint-verdict ${data.isCorrect ? 'attempt-hint-verdict--ok' : 'attempt-hint-verdict--bad'}`;
|
||||
const correct = (data.correctOptionTexts || []).join('; ');
|
||||
hintCorrect.textContent = correct ? (`Правильный ответ: ${correct}`) : '';
|
||||
hintExplanation.textContent = data.explanation || 'Объяснение недоступно.';
|
||||
if (typeof hintModal.showModal === 'function') hintModal.showModal();
|
||||
else hintModal.setAttribute('open', '');
|
||||
}
|
||||
|
||||
hintCloseBtn.addEventListener('click', () => {
|
||||
if (typeof hintModal.close === 'function') hintModal.close();
|
||||
else hintModal.removeAttribute('open');
|
||||
});
|
||||
|
||||
btnPrev.addEventListener('click', () => {
|
||||
if (currentIdx <= 0) return;
|
||||
currentIdx -= 1;
|
||||
@@ -243,9 +258,14 @@
|
||||
btnFinish.addEventListener('click', () => submit(false));
|
||||
|
||||
function startTimer(minutes) {
|
||||
if (!minutes || minutes <= 0) return;
|
||||
deadlineMs = Date.now() + minutes * 60 * 1000;
|
||||
if (!timerEl) return;
|
||||
timerEl.style.display = '';
|
||||
if (!minutes || minutes <= 0) {
|
||||
deadlineMs = null;
|
||||
timerEl.textContent = 'без ограничения';
|
||||
return;
|
||||
}
|
||||
deadlineMs = Date.now() + minutes * 60 * 1000;
|
||||
const tick = () => {
|
||||
const left = Math.max(0, deadlineMs - Date.now());
|
||||
const m = Math.floor(left / 60000);
|
||||
@@ -267,10 +287,7 @@
|
||||
if (!r.ok) throw new Error(data.error || 'Не удалось открыть попытку.');
|
||||
playData = data;
|
||||
titleEl.textContent = data.testTitle || 'Прохождение теста';
|
||||
const parts = [`Порог зачёта ${data.passingThreshold ?? 0}%`];
|
||||
if (data.resultMode === 'immediate') parts.push('обратная связь после каждого ответа');
|
||||
if (data.hintsEnabled) parts.push('с подсказками');
|
||||
subEl.textContent = parts.join(' · ');
|
||||
subEl.textContent = formatAttemptMeta(data);
|
||||
if (!Array.isArray(data.questions) || !data.questions.length) {
|
||||
setErr('В активной версии нет вопросов.');
|
||||
btnNext.disabled = true;
|
||||
@@ -279,7 +296,7 @@
|
||||
}
|
||||
currentIdx = 0;
|
||||
renderStep();
|
||||
if (data.timeLimit) startTimer(Number(data.timeLimit));
|
||||
startTimer(Number(data.timeLimit));
|
||||
} catch (e) {
|
||||
setErr(e.message);
|
||||
btnFinish.disabled = true;
|
||||
|
||||
@@ -34,9 +34,6 @@
|
||||
const templateGlobalMultiEl = $('#template-global-multi');
|
||||
const templateMinCorrectEl = $('#template-min-correct');
|
||||
const templateMaxCorrectEl = $('#template-max-correct');
|
||||
const hintsStatusEl = $('#hints-status');
|
||||
const hintsActionsEl = $('#test-hints-actions');
|
||||
const generateHintsBtn = $('#btn-generate-hints');
|
||||
const docProgressEl = $('#doc-progress');
|
||||
const introUpdatedEl = $('#intro-updated');
|
||||
const introForkBannerEl = $('#intro-fork-banner');
|
||||
@@ -365,14 +362,36 @@
|
||||
m.textContent = v;
|
||||
}
|
||||
|
||||
function syncEditorHeroExtra() {
|
||||
const timeVal = document.getElementById('editor-hero-time-val');
|
||||
const resVal = document.getElementById('editor-hero-result-val');
|
||||
const hintsVal = document.getElementById('editor-hero-hints-val');
|
||||
if (!timeVal || !resVal || !hintsVal) return;
|
||||
const tlEl = document.getElementById('test-time-limit');
|
||||
const raw = tlEl && tlEl.value !== '' ? Number(tlEl.value) : 0;
|
||||
timeVal.textContent = (!raw || raw <= 0) ? 'без ограничения' : `${raw} мин`;
|
||||
const mode = document.querySelector('input[name="result-mode"]:checked');
|
||||
const imm = mode && mode.value === 'immediate';
|
||||
resVal.textContent = imm ? 'сразу' : 'в конце';
|
||||
const hintsCheckbox = document.getElementById('test-hints-enabled');
|
||||
if (!imm) hintsVal.textContent = 'недоступны';
|
||||
else if (hintsCheckbox && hintsCheckbox.checked) hintsVal.textContent = 'вкл';
|
||||
else hintsVal.textContent = 'выкл';
|
||||
}
|
||||
|
||||
function syncHeroMetaRow() {
|
||||
syncThresholdMirror();
|
||||
syncEditorHeroExtra();
|
||||
}
|
||||
|
||||
function loadInitial() {
|
||||
titleEl.value = initial.test.title || '';
|
||||
descEl.value = initial.test.description || '';
|
||||
autoResize(titleEl);
|
||||
autoResize(descEl);
|
||||
if (thresholdEl) {
|
||||
thresholdEl.addEventListener('input', syncThresholdMirror);
|
||||
thresholdEl.addEventListener('change', syncThresholdMirror);
|
||||
thresholdEl.addEventListener('input', syncHeroMetaRow);
|
||||
thresholdEl.addEventListener('change', syncHeroMetaRow);
|
||||
}
|
||||
if (titleEl && titleEl.tagName === 'TEXTAREA') {
|
||||
titleEl.addEventListener('input', () => {
|
||||
@@ -396,7 +415,7 @@
|
||||
if (docUserHint) docUserHint.addEventListener('input', () => autoResize(docUserHint));
|
||||
thresholdEl.value =
|
||||
initial.test.passingThreshold == null ? '' : Number(initial.test.passingThreshold);
|
||||
syncThresholdMirror();
|
||||
syncHeroMetaRow();
|
||||
|
||||
const timeLimitEl = document.getElementById('test-time-limit');
|
||||
const hintsEl = document.getElementById('test-hints-enabled');
|
||||
@@ -405,7 +424,10 @@
|
||||
|
||||
if (timeLimitEl) {
|
||||
timeLimitEl.value = initial.test.timeLimit == null ? '' : Number(initial.test.timeLimit);
|
||||
timeLimitEl.addEventListener('input', scheduleDirtyCheck);
|
||||
timeLimitEl.addEventListener('input', () => {
|
||||
scheduleDirtyCheck();
|
||||
syncEditorHeroExtra();
|
||||
});
|
||||
}
|
||||
const initMode = (initial.test.resultMode === 'immediate') ? 'immediate' : 'end';
|
||||
resultModeRadios.forEach((r) => {
|
||||
@@ -414,8 +436,8 @@
|
||||
const mode = document.querySelector('input[name="result-mode"]:checked');
|
||||
const isImmediate = mode && mode.value === 'immediate';
|
||||
if (hintsRow) hintsRow.style.display = isImmediate ? '' : 'none';
|
||||
if (hintsActionsEl) hintsActionsEl.style.display = (isImmediate && hintsEl && hintsEl.checked) ? '' : 'none';
|
||||
if (hintsEl && !isImmediate) hintsEl.checked = false;
|
||||
syncHeroMetaRow();
|
||||
scheduleDirtyCheck();
|
||||
});
|
||||
});
|
||||
@@ -424,12 +446,11 @@
|
||||
hintsEl.addEventListener('change', () => {
|
||||
const mode = document.querySelector('input[name="result-mode"]:checked');
|
||||
const isImmediate = mode && mode.value === 'immediate';
|
||||
if (hintsActionsEl) hintsActionsEl.style.display = (isImmediate && hintsEl.checked) ? '' : 'none';
|
||||
syncHeroMetaRow();
|
||||
scheduleDirtyCheck();
|
||||
});
|
||||
}
|
||||
if (hintsRow) hintsRow.style.display = (initMode === 'immediate') ? '' : 'none';
|
||||
if (hintsActionsEl) hintsActionsEl.style.display = (initMode === 'immediate' && hintsEl && hintsEl.checked) ? '' : 'none';
|
||||
|
||||
questionsEl.innerHTML = '';
|
||||
(initial.questions || []).forEach((q) => questionsEl.appendChild(renderQuestion(q)));
|
||||
@@ -519,6 +540,60 @@
|
||||
return data;
|
||||
}
|
||||
|
||||
function applyServerTestState(test) {
|
||||
if (!test || typeof test !== 'object') return;
|
||||
if (typeof test.title === 'string' && titleEl) titleEl.value = test.title;
|
||||
if (typeof test.description === 'string' && descEl) descEl.value = test.description;
|
||||
if (thresholdEl && test.passingThreshold != null) {
|
||||
thresholdEl.value = Number(test.passingThreshold);
|
||||
}
|
||||
|
||||
const timeLimitEl = document.getElementById('test-time-limit');
|
||||
if (timeLimitEl) timeLimitEl.value = test.timeLimit == null ? '' : Number(test.timeLimit);
|
||||
|
||||
const hintsEl = document.getElementById('test-hints-enabled');
|
||||
const hintsRow = document.getElementById('test-hints-row');
|
||||
const mode = (test.resultMode === 'immediate') ? 'immediate' : 'end';
|
||||
const modeEl = document.querySelector(`input[name="result-mode"][value="${mode}"]`);
|
||||
if (modeEl) modeEl.checked = true;
|
||||
if (hintsRow) hintsRow.style.display = (mode === 'immediate') ? '' : 'none';
|
||||
if (hintsEl) {
|
||||
hintsEl.checked = !!test.hintsEnabled && mode === 'immediate';
|
||||
}
|
||||
|
||||
autoResize(titleEl);
|
||||
autoResize(descEl);
|
||||
syncHeroMetaRow();
|
||||
}
|
||||
|
||||
async function refreshMetaAfterSave() {
|
||||
const [v, s, e] = await Promise.all([
|
||||
fetch(`/api/tests/${TEST_ID}/versions`).then((r) => r.json()).catch(() => null),
|
||||
fetch(`/api/tests/${TEST_ID}/summary`).then((r) => r.json()).catch(() => null),
|
||||
fetch(`/api/tests/${TEST_ID}/editor`).then((r) => r.json()).catch(() => null),
|
||||
]);
|
||||
|
||||
if (v && Array.isArray(v.versions)) {
|
||||
renderVersions(v.versions);
|
||||
hasForkRisk = hasForkRisk || (v.versions.length > 1);
|
||||
if (typeof v.hasAttempts === 'boolean') {
|
||||
hasAnyAttempts = hasAnyAttempts || v.hasAttempts;
|
||||
hasForkRisk = hasForkRisk || v.hasAttempts;
|
||||
}
|
||||
}
|
||||
if (s && s.test) {
|
||||
if (introUpdatedEl) introUpdatedEl.textContent = fmtDt(s.test.updated_at || s.test.updatedAt);
|
||||
const versionEl = document.getElementById('intro-version');
|
||||
if (versionEl && s.test.version != null) versionEl.textContent = s.test.version;
|
||||
if (typeof s.test.hasAttempts === 'boolean') {
|
||||
hasAnyAttempts = hasAnyAttempts || s.test.hasAttempts;
|
||||
hasForkRisk = hasForkRisk || s.test.hasAttempts;
|
||||
}
|
||||
}
|
||||
if (e && e.test) applyServerTestState(e.test);
|
||||
updateForkBanner();
|
||||
}
|
||||
|
||||
async function refreshHintsInForm() {
|
||||
const r = await fetch(`/api/tests/${TEST_ID}/editor`);
|
||||
const data = await r.json().catch(() => ({}));
|
||||
@@ -572,6 +647,7 @@
|
||||
});
|
||||
if (r2.ok) chainActive = chainActiveEl.checked;
|
||||
}
|
||||
await refreshMetaAfterSave();
|
||||
resetBaselineDraft();
|
||||
const msg = data.forked
|
||||
? 'Сохранено. Создана новая версия — у теста есть попытки прохождения.'
|
||||
@@ -587,23 +663,36 @@
|
||||
const sr = await fetch(`/api/tests/${TEST_ID}/ai/hints/status`);
|
||||
const st = await sr.json().catch(() => ({}));
|
||||
if (sr.ok && Number(st.missing) > 0) {
|
||||
saveStatusEl.textContent = `Создаём ИИ-подсказки (${st.missing} из ${st.total})…`;
|
||||
const gr = await fetch(`/api/tests/${TEST_ID}/ai/hints/generate`, { method: 'POST' });
|
||||
const gd = await gr.json().catch(() => ({}));
|
||||
if (!gr.ok) {
|
||||
saveStatusEl.textContent = '';
|
||||
alert(gd.error || 'Не удалось сгенерировать подсказки.');
|
||||
if (saveMsg) saveMsg.textContent = msg + ' (часть подсказок не создана)';
|
||||
if (saveModal) saveModal.showModal();
|
||||
return;
|
||||
const okGen = confirm(
|
||||
`Подсказок пока нет или заполнены не все: не хватает ${st.missing} из ${st.total}.\n\n`
|
||||
+ 'Сгенерировать недостающие подсказки через ИИ?',
|
||||
);
|
||||
if (okGen) {
|
||||
saveStatusEl.textContent = `Создаём ИИ-подсказки (${st.missing} из ${st.total})…`;
|
||||
const gr = await fetch(`/api/tests/${TEST_ID}/ai/hints/generate`, { method: 'POST' });
|
||||
const gd = await gr.json().catch(() => ({}));
|
||||
if (!gr.ok) {
|
||||
saveStatusEl.textContent = '';
|
||||
alert(gd.error || 'Не удалось сгенерировать подсказки.');
|
||||
if (saveMsg) saveMsg.textContent = msg + ' (подсказки не созданы)';
|
||||
if (saveModal) saveModal.showModal();
|
||||
return;
|
||||
}
|
||||
const skipped = Number(gd.skipped || 0);
|
||||
const tail = gd.failed
|
||||
? ` Подсказки: ${gd.generated} создано, ${gd.failed} не удалось${skipped ? `, пропущено ${skipped}` : ''}.`
|
||||
: ` Подсказки созданы (${gd.generated})${skipped ? `, пропущено ${skipped}` : ''}.`;
|
||||
if (saveMsg) saveMsg.textContent = msg + tail;
|
||||
try {
|
||||
await refreshHintsInForm();
|
||||
} catch (_) {
|
||||
/* не блокируем успех сохранения */
|
||||
}
|
||||
} else if (saveMsg) {
|
||||
saveMsg.textContent = msg;
|
||||
}
|
||||
const skipped = Number(gd.skipped || 0);
|
||||
const tail = gd.failed
|
||||
? ` Подсказки: ${gd.generated} создано, ${gd.failed} не удалось${skipped ? `, пропущено ${skipped}` : ''}.`
|
||||
: ` Подсказки созданы (${gd.generated})${skipped ? `, пропущено ${skipped}` : ''}.`;
|
||||
if (saveMsg) saveMsg.textContent = msg + tail;
|
||||
} else {
|
||||
if (saveMsg) saveMsg.textContent = msg;
|
||||
} else if (saveMsg) {
|
||||
saveMsg.textContent = msg;
|
||||
}
|
||||
} catch (err) {
|
||||
if (saveMsg) saveMsg.textContent = msg + ' (ИИ-подсказки не созданы)';
|
||||
@@ -972,18 +1061,6 @@
|
||||
const cancelBtn = document.getElementById('btn-cancel');
|
||||
if (cancelBtn) cancelBtn.addEventListener('click', doCancel);
|
||||
|
||||
const cancelBtnInline = document.getElementById('btn-cancel-inline');
|
||||
if (cancelBtnInline) cancelBtnInline.addEventListener('click', doCancel);
|
||||
|
||||
// Кнопка «Сохранить» под вопросами — дублирует основную
|
||||
const saveDraftInlineBtn = document.getElementById('save-draft-inline');
|
||||
const saveStatusInlineEl = document.getElementById('save-status-inline');
|
||||
if (saveDraftInlineBtn) {
|
||||
saveDraftInlineBtn.addEventListener('click', () => {
|
||||
document.getElementById('save-draft')?.click();
|
||||
});
|
||||
}
|
||||
|
||||
function openModal(title, bodyHtml, actions) {
|
||||
modalTitle.textContent = title;
|
||||
modalBody.innerHTML = bodyHtml;
|
||||
@@ -1205,6 +1282,19 @@
|
||||
}
|
||||
});
|
||||
|
||||
/** Перемешивает строки вариантов в DOM; чекбоксы «верный» остаются при своих полях. */
|
||||
function shuffleQuestionOptionsDom(qNode) {
|
||||
const optsEl = $('.q-options', qNode);
|
||||
if (!optsEl) return;
|
||||
const rows = Array.from(optsEl.querySelectorAll('.opt-item'));
|
||||
if (rows.length < 2) return;
|
||||
for (let i = rows.length - 1; i > 0; i -= 1) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[rows[i], rows[j]] = [rows[j], rows[i]];
|
||||
}
|
||||
rows.forEach((el) => optsEl.appendChild(el));
|
||||
}
|
||||
|
||||
async function aiGenerateQuestion(node) {
|
||||
const qTextEl = $('.q-text', node);
|
||||
const qText = qTextEl.value.trim();
|
||||
@@ -1278,6 +1368,7 @@
|
||||
dIdx++;
|
||||
}
|
||||
});
|
||||
shuffleQuestionOptionsDom(node);
|
||||
}
|
||||
|
||||
syncOptionInputTypes(node);
|
||||
@@ -1507,73 +1598,69 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Создать шаблон ────────────────────────────────────────────
|
||||
const createTemplateBtn = $('#create-template');
|
||||
if (createTemplateBtn) {
|
||||
createTemplateBtn.addEventListener('click', () => {
|
||||
const qCount = Math.min(30, Math.max(1, parseInt($('#ai-q-count').value || '7', 10)));
|
||||
const oCount = Math.min(MAX_OPTIONS, Math.max(2, parseInt($('#ai-o-count').value || '3', 10)));
|
||||
const globalMulti = !!(templateGlobalMultiEl && templateGlobalMultiEl.checked);
|
||||
const range = getTemplateCorrectRange(oCount, globalMulti);
|
||||
const existing = $$('#questions .q-item').length;
|
||||
if (existing > 0) {
|
||||
const ok = confirm(
|
||||
`Создать шаблон: ${qCount} вопросов × ${oCount} вариантов?\n` +
|
||||
'Текущие вопросы будут заменены.'
|
||||
);
|
||||
if (!ok) return;
|
||||
}
|
||||
questionsEl.innerHTML = '';
|
||||
for (let qi = 0; qi < qCount; qi++) {
|
||||
const opts = [];
|
||||
for (let oi = 0; oi < oCount; oi++) {
|
||||
opts.push({ text: '', isCorrect: oi < range.minCorrect });
|
||||
}
|
||||
questionsEl.appendChild(renderQuestion({ text: '', hasMultipleAnswers: globalMulti, options: opts }));
|
||||
}
|
||||
renumber();
|
||||
scheduleDirtyCheck();
|
||||
// Прокручиваем к первому вопросу
|
||||
questionsEl.firstElementChild?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
// ─── Автосоздание шаблона ──────────────────────────────────────
|
||||
let templateRebuildTimer = null;
|
||||
let lastAppliedTemplateKey = '';
|
||||
|
||||
function hasMeaningfulQuestions() {
|
||||
return $$('#questions .q-item').some((node) => {
|
||||
const qText = ($('.q-text', node)?.value || '').trim();
|
||||
if (qText) return true;
|
||||
return $$('.opt-text', node).some((o) => (o.value || '').trim());
|
||||
});
|
||||
}
|
||||
|
||||
async function generateHintsForCurrentTest() {
|
||||
if (!generateHintsBtn) return;
|
||||
generateHintsBtn.disabled = true;
|
||||
if (hintsStatusEl) hintsStatusEl.textContent = 'Сохраняем текущие изменения…';
|
||||
try {
|
||||
await saveCurrentDraftQuietly();
|
||||
if (hintsStatusEl) hintsStatusEl.textContent = 'Генерируем подсказки…';
|
||||
const r = await fetch(`/api/tests/${TEST_ID}/ai/hints/generate`, { method: 'POST' });
|
||||
const data = await r.json().catch(() => ({}));
|
||||
if (!r.ok) throw new Error(data.error || 'Не удалось сгенерировать подсказки.');
|
||||
try {
|
||||
await refreshHintsInForm();
|
||||
} catch (_) {
|
||||
// Статус покажем как успешный; пользователь может перезагрузить страницу.
|
||||
}
|
||||
const skipped = Number(data.skipped || 0);
|
||||
if (hintsStatusEl) {
|
||||
hintsStatusEl.textContent = data.failed
|
||||
? `Создано ${data.generated}, ошибок ${data.failed}${skipped ? `, пропущено ${skipped}` : ''}.`
|
||||
: `Подсказки созданы: ${data.generated}${skipped ? `, пропущено ${skipped}` : ''}.`;
|
||||
}
|
||||
} catch (e) {
|
||||
if (hintsStatusEl) hintsStatusEl.textContent = e.message || 'Ошибка генерации подсказок.';
|
||||
} finally {
|
||||
generateHintsBtn.disabled = false;
|
||||
function buildTemplateFromControls({ askConfirm } = { askConfirm: true }) {
|
||||
const qCount = Math.min(30, Math.max(1, parseInt(aiQCountEl?.value || '7', 10)));
|
||||
const oCount = Math.min(MAX_OPTIONS, Math.max(2, parseInt(aiOCountEl?.value || '3', 10)));
|
||||
const globalMulti = !!(templateGlobalMultiEl && templateGlobalMultiEl.checked);
|
||||
const range = getTemplateCorrectRange(oCount, globalMulti);
|
||||
const key = JSON.stringify({ qCount, oCount, globalMulti, min: range.minCorrect, max: range.maxCorrect });
|
||||
if (key === lastAppliedTemplateKey) return;
|
||||
|
||||
if (askConfirm && hasMeaningfulQuestions()) {
|
||||
const ok = confirm(
|
||||
`Обновить шаблон: ${qCount} вопросов × ${oCount} вариантов?\n` +
|
||||
'Текущие вопросы будут заменены.'
|
||||
);
|
||||
if (!ok) return;
|
||||
}
|
||||
|
||||
questionsEl.innerHTML = '';
|
||||
for (let qi = 0; qi < qCount; qi++) {
|
||||
const opts = [];
|
||||
for (let oi = 0; oi < oCount; oi++) {
|
||||
opts.push({ text: '', isCorrect: oi < range.minCorrect });
|
||||
}
|
||||
questionsEl.appendChild(renderQuestion({ text: '', hasMultipleAnswers: globalMulti, options: opts }));
|
||||
}
|
||||
renumber();
|
||||
scheduleDirtyCheck();
|
||||
lastAppliedTemplateKey = key;
|
||||
}
|
||||
|
||||
if (generateHintsBtn) {
|
||||
generateHintsBtn.addEventListener('click', generateHintsForCurrentTest);
|
||||
function scheduleTemplateRebuild() {
|
||||
if (templateRebuildTimer) clearTimeout(templateRebuildTimer);
|
||||
templateRebuildTimer = setTimeout(() => buildTemplateFromControls({ askConfirm: true }), 200);
|
||||
}
|
||||
|
||||
if (templateGlobalMultiEl) templateGlobalMultiEl.addEventListener('change', syncTemplateRangeUi);
|
||||
if (templateMinCorrectEl) templateMinCorrectEl.addEventListener('input', syncTemplateRangeUi);
|
||||
if (templateMaxCorrectEl) templateMaxCorrectEl.addEventListener('input', syncTemplateRangeUi);
|
||||
if (aiOCountEl) aiOCountEl.addEventListener('input', syncTemplateRangeUi);
|
||||
if (templateGlobalMultiEl) templateGlobalMultiEl.addEventListener('change', () => {
|
||||
syncTemplateRangeUi();
|
||||
scheduleTemplateRebuild();
|
||||
});
|
||||
if (templateMinCorrectEl) templateMinCorrectEl.addEventListener('change', () => {
|
||||
syncTemplateRangeUi();
|
||||
scheduleTemplateRebuild();
|
||||
});
|
||||
if (templateMaxCorrectEl) templateMaxCorrectEl.addEventListener('change', () => {
|
||||
syncTemplateRangeUi();
|
||||
scheduleTemplateRebuild();
|
||||
});
|
||||
if (aiOCountEl) aiOCountEl.addEventListener('change', () => {
|
||||
syncTemplateRangeUi();
|
||||
scheduleTemplateRebuild();
|
||||
});
|
||||
if (aiQCountEl) aiQCountEl.addEventListener('change', scheduleTemplateRebuild);
|
||||
syncTemplateRangeUi();
|
||||
|
||||
Promise.all([
|
||||
|
||||
Reference in New Issue
Block a user