bugfix
This commit is contained in:
@@ -31,6 +31,13 @@
|
||||
const aiTopicEl = $('#ai-topic');
|
||||
const aiQCountEl = $('#ai-q-count');
|
||||
const aiOCountEl = $('#ai-o-count');
|
||||
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');
|
||||
const versionsListEl = $('#versions-list');
|
||||
@@ -56,6 +63,34 @@
|
||||
let baselineDraftKey = '';
|
||||
let dirtyCheckQueued = false;
|
||||
|
||||
function getTemplateCorrectRange(optionsCount, hasMultipleAnswers) {
|
||||
const maxOpt = Math.max(2, Number(optionsCount || 2));
|
||||
const rawMin = Math.max(1, Number(templateMinCorrectEl?.value || 1) || 1);
|
||||
const rawMax = Math.max(1, Number(templateMaxCorrectEl?.value || 1) || 1);
|
||||
if (!hasMultipleAnswers) return { minCorrect: 1, maxCorrect: 1 };
|
||||
const minCorrect = Math.min(maxOpt, rawMin);
|
||||
const maxCorrect = Math.max(minCorrect, Math.min(maxOpt, rawMax));
|
||||
return { minCorrect, maxCorrect };
|
||||
}
|
||||
|
||||
function syncTemplateRangeUi() {
|
||||
const hasMulti = !!(templateGlobalMultiEl && templateGlobalMultiEl.checked);
|
||||
const maxOpt = Math.min(MAX_OPTIONS, Math.max(2, Number(aiOCountEl?.value || 3) || 3));
|
||||
const range = getTemplateCorrectRange(maxOpt, hasMulti);
|
||||
if (templateMinCorrectEl) {
|
||||
templateMinCorrectEl.min = '1';
|
||||
templateMinCorrectEl.max = String(maxOpt);
|
||||
templateMinCorrectEl.value = String(range.minCorrect);
|
||||
templateMinCorrectEl.disabled = !hasMulti;
|
||||
}
|
||||
if (templateMaxCorrectEl) {
|
||||
templateMaxCorrectEl.min = '1';
|
||||
templateMaxCorrectEl.max = String(maxOpt);
|
||||
templateMaxCorrectEl.value = String(range.maxCorrect);
|
||||
templateMaxCorrectEl.disabled = !hasMulti;
|
||||
}
|
||||
}
|
||||
|
||||
function currentDraftKey() {
|
||||
return JSON.stringify(collectPayload());
|
||||
}
|
||||
@@ -379,15 +414,22 @@
|
||||
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;
|
||||
scheduleDirtyCheck();
|
||||
});
|
||||
});
|
||||
if (hintsEl) {
|
||||
hintsEl.checked = !!initial.test.hintsEnabled;
|
||||
hintsEl.addEventListener('change', scheduleDirtyCheck);
|
||||
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';
|
||||
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)));
|
||||
@@ -452,10 +494,29 @@
|
||||
}
|
||||
|
||||
function collectShape() {
|
||||
return $$('#questions .q-item').map((li) => ({
|
||||
optionsCount: Math.max(2, $$('.opt-item', li).length || 4),
|
||||
hasMultipleAnswers: $('.q-multi', li).checked,
|
||||
}));
|
||||
return $$('#questions .q-item').map((li) => {
|
||||
const optionsCount = Math.max(2, $$('.opt-item', li).length || 4);
|
||||
const hasMultipleAnswers = $('.q-multi', li).checked;
|
||||
const range = getTemplateCorrectRange(optionsCount, hasMultipleAnswers);
|
||||
return {
|
||||
optionsCount,
|
||||
hasMultipleAnswers,
|
||||
minCorrect: hasMultipleAnswers ? range.minCorrect : 1,
|
||||
maxCorrect: hasMultipleAnswers ? range.maxCorrect : 1,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function saveCurrentDraftQuietly() {
|
||||
const r = await fetch(`/api/tests/${TEST_ID}/draft`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(collectPayload()),
|
||||
});
|
||||
const data = await r.json().catch(() => ({}));
|
||||
if (!r.ok) throw new Error(data.error || 'Не удалось сохранить черновик перед генерацией подсказок.');
|
||||
resetBaselineDraft();
|
||||
return data;
|
||||
}
|
||||
|
||||
// ─── actions ───────────────────────────────────────────────────────
|
||||
@@ -520,9 +581,10 @@
|
||||
if (saveModal) saveModal.showModal();
|
||||
return;
|
||||
}
|
||||
const skipped = Number(gd.skipped || 0);
|
||||
const tail = gd.failed
|
||||
? ` Подсказки: ${gd.generated} создано, ${gd.failed} не удалось.`
|
||||
: ` Подсказки созданы (${gd.generated}).`;
|
||||
? ` Подсказки: ${gd.generated} создано, ${gd.failed} не удалось${skipped ? `, пропущено ${skipped}` : ''}.`
|
||||
: ` Подсказки созданы (${gd.generated})${skipped ? `, пропущено ${skipped}` : ''}.`;
|
||||
if (saveMsg) saveMsg.textContent = msg + tail;
|
||||
} else {
|
||||
if (saveMsg) saveMsg.textContent = msg;
|
||||
@@ -561,11 +623,15 @@
|
||||
}
|
||||
const nQ = Math.min(30, Math.max(1, Number(aiQCountEl?.value || 7) || 7));
|
||||
const nO = Math.min(8, Math.max(2, Number(aiOCountEl?.value || 3) || 3));
|
||||
const globalMulti = !!(templateGlobalMultiEl && templateGlobalMultiEl.checked);
|
||||
const globalRange = getTemplateCorrectRange(nO, globalMulti);
|
||||
const shape = Array.from({ length: nQ }, () => ({
|
||||
optionsCount: nO,
|
||||
hasMultipleAnswers: false,
|
||||
hasMultipleAnswers: globalMulti,
|
||||
minCorrect: globalMulti ? globalRange.minCorrect : 1,
|
||||
maxCorrect: globalMulti ? globalRange.maxCorrect : 1,
|
||||
}));
|
||||
aiStatusEl.textContent = 'Генерируем…';
|
||||
aiStatusEl.textContent = 'Генерируем структуру и вопросы…';
|
||||
try {
|
||||
const r = await fetch(`/api/tests/${TEST_ID}/ai/generate-test`, {
|
||||
method: 'POST',
|
||||
@@ -589,6 +655,25 @@
|
||||
renumber();
|
||||
scheduleDirtyCheck();
|
||||
aiStatusEl.textContent = `Готово: ${draft.questions?.length || 0} вопросов.`;
|
||||
const hintsEl = document.getElementById('test-hints-enabled');
|
||||
const modeEl = document.querySelector('input[name="result-mode"]:checked');
|
||||
if (hintsEl && hintsEl.checked && modeEl && modeEl.value === 'immediate') {
|
||||
aiStatusEl.textContent = 'Сохраняем черновик…';
|
||||
try {
|
||||
await saveCurrentDraftQuietly();
|
||||
aiStatusEl.textContent = 'Генерируем вопросы… затем подсказки…';
|
||||
const hr = await fetch(`/api/tests/${TEST_ID}/ai/hints/generate`, { method: 'POST' });
|
||||
const hd = await hr.json().catch(() => ({}));
|
||||
if (hr.ok) {
|
||||
const skipped = Number(hd.skipped || 0);
|
||||
aiStatusEl.textContent = skipped
|
||||
? `Готово: вопросы + подсказки (${hd.generated}, пропущено ${skipped}).`
|
||||
: `Готово: вопросы + подсказки (${hd.generated}).`;
|
||||
}
|
||||
} catch (_) {
|
||||
// Оставляем базовый статус готовности вопросов.
|
||||
}
|
||||
}
|
||||
setTimeout(() => (aiStatusEl.textContent = ''), 4000);
|
||||
} catch (e) {
|
||||
aiStatusEl.textContent = '';
|
||||
@@ -662,16 +747,29 @@
|
||||
docGenerateBtn.disabled = true;
|
||||
docGenerateBtn.textContent = 'Генерируем…';
|
||||
aiStatusEl.textContent = 'Генерируем тест из документа…';
|
||||
if (docProgressEl) docProgressEl.textContent = 'Шаг 1/3: подготовка шаблона…';
|
||||
try {
|
||||
const nQ = Math.min(30, Math.max(1, Number(aiQCountEl?.value || 7) || 7));
|
||||
const nO = Math.min(8, Math.max(2, Number(aiOCountEl?.value || 3) || 3));
|
||||
const globalMulti = !!(templateGlobalMultiEl && templateGlobalMultiEl.checked);
|
||||
const globalRange = getTemplateCorrectRange(nO, globalMulti);
|
||||
const shape = Array.from({ length: nQ }, () => ({
|
||||
optionsCount: nO,
|
||||
hasMultipleAnswers: globalMulti,
|
||||
minCorrect: globalMulti ? globalRange.minCorrect : 1,
|
||||
maxCorrect: globalMulti ? globalRange.maxCorrect : 1,
|
||||
}));
|
||||
if (docProgressEl) docProgressEl.textContent = 'Шаг 2/3: генерация вопросов…';
|
||||
const r = await fetch('/api/tests/generate-from-extracted', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ extractedText: _extractedText, userHint }),
|
||||
body: JSON.stringify({ extractedText: _extractedText, userHint, shape }),
|
||||
});
|
||||
const data = await r.json();
|
||||
if (!r.ok) throw new Error(data.error || 'Ошибка генерации.');
|
||||
const g = data.generation || {};
|
||||
aiStatusEl.textContent = '';
|
||||
if (docProgressEl) docProgressEl.textContent = 'Шаг 3/3: подготовка к применению…';
|
||||
|
||||
if (!g.available) {
|
||||
openImportModal(
|
||||
@@ -741,6 +839,7 @@
|
||||
docGenerateBtn.disabled = false;
|
||||
docGenerateBtn.innerHTML = '<span class="material-symbols-outlined text-base" style="vertical-align:-3px;">auto_awesome</span> Сгенерировать из документа';
|
||||
}
|
||||
if (docProgressEl) setTimeout(() => { docProgressEl.textContent = ''; }, 2500);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1393,6 +1492,8 @@
|
||||
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(
|
||||
@@ -1405,9 +1506,9 @@
|
||||
for (let qi = 0; qi < qCount; qi++) {
|
||||
const opts = [];
|
||||
for (let oi = 0; oi < oCount; oi++) {
|
||||
opts.push({ text: '', isCorrect: oi === 0 });
|
||||
opts.push({ text: '', isCorrect: oi < range.minCorrect });
|
||||
}
|
||||
questionsEl.appendChild(renderQuestion({ text: '', hasMultipleAnswers: false, options: opts }));
|
||||
questionsEl.appendChild(renderQuestion({ text: '', hasMultipleAnswers: globalMulti, options: opts }));
|
||||
}
|
||||
renumber();
|
||||
scheduleDirtyCheck();
|
||||
@@ -1416,6 +1517,39 @@
|
||||
});
|
||||
}
|
||||
|
||||
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 || 'Не удалось сгенерировать подсказки.');
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (generateHintsBtn) {
|
||||
generateHintsBtn.addEventListener('click', generateHintsForCurrentTest);
|
||||
}
|
||||
|
||||
if (templateGlobalMultiEl) templateGlobalMultiEl.addEventListener('change', syncTemplateRangeUi);
|
||||
if (templateMinCorrectEl) templateMinCorrectEl.addEventListener('input', syncTemplateRangeUi);
|
||||
if (templateMaxCorrectEl) templateMaxCorrectEl.addEventListener('input', syncTemplateRangeUi);
|
||||
if (aiOCountEl) aiOCountEl.addEventListener('input', syncTemplateRangeUi);
|
||||
syncTemplateRangeUi();
|
||||
|
||||
Promise.all([
|
||||
fetch(`/api/tests/${TEST_ID}/versions`).then((r) => r.json()).catch(() => null),
|
||||
fetch(`/api/tests/${TEST_ID}/attempts`).then((r) => r.json()).catch(() => null),
|
||||
|
||||
Reference in New Issue
Block a user