feat(assistant): генерация тестов в банк вопросов (фича 5/6)
Учитель: режим «Тест в банк» в Квантике — тема/текст превращается ИИ в вопросы с выбором ответа, ревью в чате (варианты, верный подсвечен, пояснение), кнопка «Сохранить в банк» (выбор предмета + тема) создаёт их через POST /questions. Бэкенд: questionsFromText (по образцу flashcardsFromText, надёжный парс JSON с починкой обрезанного) + роут POST /assistant/questions (requireRole teacher/admin, fcLimiter). Клиент: LS.assistantQuestions. Виджет: режим quiz только для учителя + makeQuiz (рендер и сохранение через createQuestion/getSubjects). Проверено на живом шлюзе: 5 валидных вопросов, верный индекс в диапазоне. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -528,7 +528,7 @@
|
||||
}
|
||||
var FB_UP = '<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7 10v11"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>';
|
||||
var FB_DOWN = '<svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="transform:rotate(180deg)"><path d="M7 10v11"/><path d="M15 5.88 14 10h5.83a2 2 0 0 1 1.92 2.56l-2.33 8A2 2 0 0 1 17.5 22H4a2 2 0 0 1-2-2v-8a2 2 0 0 1 2-2h2.76a2 2 0 0 0 1.79-1.11L12 2a3.13 3.13 0 0 1 3 3.88Z"/></svg>';
|
||||
var MODE_PH = { answer: 'Спроси что угодно: «объясни…», «как…»', hint: 'Задача, по которой нужна подсказка…', check: 'Вставь своё решение — проверю…', draw: 'Опиши картинку: «кот-учёный, плоская иллюстрация»' };
|
||||
var MODE_PH = { answer: 'Спроси что угодно: «объясни…», «как…»', hint: 'Задача, по которой нужна подсказка…', check: 'Вставь своё решение — проверю…', draw: 'Опиши картинку: «кот-учёный, плоская иллюстрация»', quiz: 'Тема или текст — сгенерирую вопросы для банка' };
|
||||
function openAsk(prefill) {
|
||||
var sel = _lastSel, pc = getPageContext();
|
||||
var noun = pc && pc.kind === 'lesson' ? 'этот урок' : 'этот параграф';
|
||||
@@ -541,10 +541,12 @@
|
||||
var sug = (_role === 'teacher' || _role === 'admin') ? TEACHER_SUGGESTIONS : SUGGESTIONS;
|
||||
var chips = '<div class="asst-chips">' + ctxBtns +
|
||||
sug.map(function (q) { return '<button class="asst-chip" type="button">' + esc(q) + '</button>'; }).join('') + '</div>';
|
||||
var isTch = (_role === 'teacher' || _role === 'admin');
|
||||
var modes = '<div class="asst-modes">' +
|
||||
'<button class="asst-mode on" data-m="answer">Ответ</button>' +
|
||||
'<button class="asst-mode" data-m="hint">Подсказка</button>' +
|
||||
'<button class="asst-mode" data-m="check">Проверить решение</button>' +
|
||||
(isTch ? '<button class="asst-mode" data-m="quiz">Тест в банк</button>' : '') +
|
||||
'<button class="asst-mode" data-m="draw">Нарисовать</button></div>';
|
||||
openBubble(
|
||||
'<div class="asst-name"><span class="asst-name-face">' + faceSVG('happy') + '</span>Спроси Квантика' +
|
||||
@@ -637,6 +639,7 @@
|
||||
q = (q || '').trim();
|
||||
if (q.length < 2) return;
|
||||
if (mode === 'draw') return drawInChat(q, chatEl);
|
||||
if (mode === 'quiz') return makeQuiz(q, chatEl);
|
||||
// стриминг недоступен (старый кэш api.js / нет ReadableStream) — обычный путь
|
||||
if (!LS.assistantAskStream || typeof ReadableStream === 'undefined') return sendNonStream(q, context, chatEl, mode);
|
||||
|
||||
@@ -711,6 +714,7 @@
|
||||
q = (q || '').trim();
|
||||
if (q.length < 2) return;
|
||||
if (mode === 'draw') return drawInChat(q, chatEl);
|
||||
if (mode === 'quiz') return makeQuiz(q, chatEl);
|
||||
var history = _chat.slice(-6);
|
||||
_chat.push({ role: 'user', content: q });
|
||||
var u = msgEl('user'); u.textContent = q; chatEl.appendChild(u);
|
||||
@@ -784,6 +788,63 @@
|
||||
}).catch(function () { note.innerHTML = '<div class="asst-rich">Не удалось сделать карточки. Попробуй позже.</div>'; });
|
||||
}
|
||||
|
||||
/* ── «Тест в банк» (учитель): модель → вопросы → банк вопросов ─────────── */
|
||||
function makeQuiz(topic, chatEl) {
|
||||
topic = (topic || '').trim();
|
||||
var note = msgEl('assistant');
|
||||
note.innerHTML = '<div class="asst-rich">Составляю тестовые вопросы…</div>';
|
||||
chatEl.appendChild(note); chatEl.scrollTop = chatEl.scrollHeight;
|
||||
Promise.all([
|
||||
LS.assistantQuestions(topic, 5),
|
||||
(LS.getSubjects ? LS.getSubjects() : Promise.resolve([])).catch(function () { return []; }),
|
||||
]).then(function (res) {
|
||||
var qs = (res[0] && res[0].questions) || [];
|
||||
var subjects = Array.isArray(res[1]) ? res[1] : ((res[1] && res[1].subjects) || []);
|
||||
if (!qs.length) { note.innerHTML = '<div class="asst-rich">Не получилось сгенерировать вопросы. Уточни тему и попробуй ещё.</div>'; return; }
|
||||
note.remove();
|
||||
var wrap = msgEl('assistant'); wrap.style.maxWidth = '100%';
|
||||
var box = document.createElement('div'); box.className = 'asst-rich'; wrap.appendChild(box);
|
||||
var head = document.createElement('div'); head.style.cssText = 'font-weight:800;margin-bottom:6px'; head.textContent = 'Вопросы (' + qs.length + ') — проверь и сохрани:'; box.appendChild(head);
|
||||
qs.forEach(function (it, i) {
|
||||
var qd = document.createElement('div'); qd.style.cssText = 'margin:8px 0;padding:8px 10px;border:1px solid var(--border,#e2e8f0);border-radius:10px';
|
||||
var qt = document.createElement('div'); qt.style.cssText = 'font-weight:700;margin-bottom:4px'; qt.appendChild(document.createTextNode((i + 1) + '. '));
|
||||
var qr = document.createElement('span'); qt.appendChild(qr); renderRich(qr, it.q); qd.appendChild(qt);
|
||||
(it.options || []).forEach(function (op, oi) {
|
||||
var li = document.createElement('div'); li.style.cssText = 'padding:2px 0 2px 14px;font-size:.84rem' + (oi === it.correct ? ';color:#059652;font-weight:700' : '');
|
||||
var os = document.createElement('span'); renderRich(os, op); li.appendChild(os);
|
||||
if (oi === it.correct) { var okm = document.createElement('span'); okm.textContent = ' — верно'; okm.style.color = '#059652'; li.appendChild(okm); }
|
||||
qd.appendChild(li);
|
||||
});
|
||||
if (it.explanation) { var ex = document.createElement('div'); ex.style.cssText = 'margin-top:4px;font-size:.8rem;color:#8a94a6'; ex.appendChild(document.createTextNode('Пояснение: ')); var exs = document.createElement('span'); renderRich(exs, it.explanation); ex.appendChild(exs); qd.appendChild(ex); }
|
||||
box.appendChild(qd);
|
||||
});
|
||||
var bar = document.createElement('div'); bar.style.cssText = 'display:flex;gap:6px;flex-wrap:wrap;align-items:center;margin-top:8px';
|
||||
var sel = document.createElement('select'); sel.style.cssText = 'padding:6px 8px;border:1px solid var(--border,#e2e8f0);border-radius:8px;font:inherit;font-size:.82rem';
|
||||
sel.innerHTML = '<option value="">Предмет…</option>' + subjects.map(function (s) { return '<option value="' + esc(s.slug) + '">' + esc(s.name || s.slug) + '</option>'; }).join('');
|
||||
var topicIn = document.createElement('input'); topicIn.type = 'text'; topicIn.placeholder = 'Тема (необязательно)'; topicIn.style.cssText = 'padding:6px 8px;border:1px solid var(--border,#e2e8f0);border-radius:8px;font:inherit;font-size:.82rem;flex:1;min-width:110px';
|
||||
var saveB = document.createElement('button'); saveB.className = 'asst-chip'; saveB.type = 'button'; saveB.textContent = 'Сохранить в банк';
|
||||
var st = document.createElement('span'); st.style.cssText = 'font-size:.8rem;color:#8a94a6';
|
||||
bar.appendChild(sel); bar.appendChild(topicIn); bar.appendChild(saveB); bar.appendChild(st); box.appendChild(bar);
|
||||
saveB.addEventListener('click', function () {
|
||||
var slug = sel.value; if (!slug) { st.textContent = 'Выбери предмет'; return; }
|
||||
saveB.disabled = true; st.textContent = 'Сохраняю…';
|
||||
var topicName = topicIn.value.trim() || (topic.length <= 60 ? topic : '');
|
||||
var done = 0;
|
||||
qs.reduce(function (p, it) {
|
||||
return p.then(function () {
|
||||
return LS.createQuestion({ subject_slug: slug, topic_name: topicName || undefined, type: 'single', text: it.q, explanation: it.explanation || undefined, difficulty: 1, options: (it.options || []).map(function (t, i) { return { text: t, is_correct: i === it.correct }; }) }).then(function () { done++; }).catch(function () {});
|
||||
});
|
||||
}, Promise.resolve()).then(function () {
|
||||
st.innerHTML = 'Сохранено ' + done + ' из ' + qs.length + '. <a class="asst-ans-link" href="/question-bank">Открыть банк вопросов</a>';
|
||||
saveB.style.display = 'none'; sel.disabled = true; topicIn.disabled = true;
|
||||
});
|
||||
});
|
||||
chatEl.appendChild(wrap); chatEl.scrollTop = chatEl.scrollHeight;
|
||||
}).catch(function (e) {
|
||||
note.innerHTML = '<div class="asst-rich">' + ((e && e.data && e.data.error) ? esc(e.data.error) : 'Не удалось сгенерировать вопросы.') + '</div>';
|
||||
});
|
||||
}
|
||||
|
||||
/* ── Ф2: онбординг-тур по разделам ───────────────────────────────────── */
|
||||
var TOUR = [
|
||||
{ sel: '#app-sidebar a[href="/dashboard"]', title: 'Дашборд', text: 'Главная: твой прогресс, активность и питомец.' },
|
||||
|
||||
Reference in New Issue
Block a user