блоки 2 и 3 доработки интерфейса системы тестирования
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
<p class="link-back"><a href="/tests">← к списку тестов</a></p>
|
||||
<h1 class="font-headline" style="font-size:1.35rem;margin-top:0;" id="attempt-title">Загрузка…</h1>
|
||||
<p class="text-muted" style="margin-top:0;" id="attempt-subtitle"></p>
|
||||
<p class="text-muted" id="attempt-timer" style="margin-top:0;display:none;font-weight:600;"></p>
|
||||
<p class="error-text" id="attempt-error" style="display:none;"></p>
|
||||
|
||||
<ol id="questions-list" style="padding-left:1.25rem;"></ol>
|
||||
@@ -15,6 +16,18 @@
|
||||
</div>
|
||||
|
||||
<div id="attempt-result" class="surface-card" style="display:none;margin-top:1rem;padding:1rem;"></div>
|
||||
|
||||
<dialog id="hint-modal" style="border:none;border-radius:14px;padding:0;max-width:480px;width:90%;box-shadow:0 12px 40px rgba(0,0,0,0.18);">
|
||||
<div style="padding:1rem 1.25rem;">
|
||||
<h3 id="hint-title" style="margin:0 0 0.5rem 0;font-size:1.05rem;">Подсказка</h3>
|
||||
<p id="hint-verdict" style="margin:0 0 0.5rem 0;font-weight:600;"></p>
|
||||
<p id="hint-correct" style="margin:0 0 0.5rem 0;font-size:0.9rem;color:#506965;"></p>
|
||||
<p id="hint-explanation" style="margin:0;font-size:0.95rem;line-height:1.4;"></p>
|
||||
<div class="inline-actions" style="margin-top:0.85rem;justify-content:flex-end;">
|
||||
<button type="button" class="btn btn-primary btn--sm" id="hint-close-btn">Понятно</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -24,12 +37,23 @@
|
||||
const attemptId = root.dataset.attemptId;
|
||||
const titleEl = document.getElementById('attempt-title');
|
||||
const subEl = document.getElementById('attempt-subtitle');
|
||||
const timerEl = document.getElementById('attempt-timer');
|
||||
const errEl = document.getElementById('attempt-error');
|
||||
const listEl = document.getElementById('questions-list');
|
||||
const resultEl = document.getElementById('attempt-result');
|
||||
const submitBtn = document.getElementById('submit-attempt-btn');
|
||||
const hintModal = document.getElementById('hint-modal');
|
||||
const hintTitle = document.getElementById('hint-title');
|
||||
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 = {};
|
||||
let timerHandle = null;
|
||||
let deadlineMs = null;
|
||||
|
||||
function esc(s) {
|
||||
return String(s ?? '').replace(/[&<>"']/g, (m) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]));
|
||||
@@ -51,38 +75,127 @@
|
||||
}
|
||||
selections[k] = [id];
|
||||
}
|
||||
function isImmediate() {
|
||||
return playData && playData.resultMode === 'immediate';
|
||||
}
|
||||
|
||||
function renderQuestions() {
|
||||
listEl.innerHTML = '';
|
||||
for (const q of (playData.questions || [])) {
|
||||
const qid = String(q.id);
|
||||
const isChecked = !!checked[qid];
|
||||
const li = document.createElement('li');
|
||||
li.style.marginBottom = '1.5rem';
|
||||
li.innerHTML = '<p style="margin-top:0;margin-bottom:0.5rem;">' + esc(q.text) + '</p>';
|
||||
li.dataset.qid = qid;
|
||||
let badge = '';
|
||||
if (isChecked) {
|
||||
const ok = checked[qid].isCorrect;
|
||||
badge = '<span style="margin-left:8px;font-size:0.8rem;font-weight:600;color:' +
|
||||
(ok ? '#1a7a4a' : '#b32d2d') + ';">' + (ok ? '✓ верно' : '✗ неверно') + '</span>';
|
||||
}
|
||||
li.innerHTML = '<p style="margin-top:0;margin-bottom:0.5rem;">' + esc(q.text) + badge + '</p>';
|
||||
const ul = document.createElement('ul');
|
||||
ul.style.listStyle = 'none';
|
||||
ul.style.padding = '0';
|
||||
ul.style.margin = '0';
|
||||
const correctSet = new Set((checked[qid] && checked[qid].correctOptionIds) || []);
|
||||
for (const o of (q.options || [])) {
|
||||
const oid = String(o.id);
|
||||
const row = document.createElement('li');
|
||||
row.style.marginBottom = '6px';
|
||||
const type = q.hasMultipleAnswers ? 'checkbox' : 'radio';
|
||||
const name = 'q-' + q.id;
|
||||
let mark = '';
|
||||
if (isChecked) {
|
||||
if (correctSet.has(oid)) mark = ' <span style="color:#1a7a4a;font-weight:600;">(верно)</span>';
|
||||
else if (isSelected(q.id, o.id)) mark = ' <span style="color:#b32d2d;">(ваш ответ)</span>';
|
||||
}
|
||||
row.innerHTML =
|
||||
'<label style="display:flex;align-items:flex-start;gap:8px;cursor:pointer;">' +
|
||||
'<input type="' + type + '" ' + (q.hasMultipleAnswers ? '' : ('name="' + name + '"')) + ' ' + (isSelected(q.id, o.id) ? 'checked' : '') + ' />' +
|
||||
'<span>' + esc(o.text) + '</span>' +
|
||||
'<label style="display:flex;align-items:flex-start;gap:8px;cursor:' + (isChecked ? 'default' : 'pointer') + ';' +
|
||||
(isChecked ? 'opacity:0.85;' : '') + '">' +
|
||||
'<input type="' + type + '" ' + (q.hasMultipleAnswers ? '' : ('name="' + name + '"')) + ' ' +
|
||||
(isSelected(q.id, o.id) ? 'checked' : '') + ' ' + (isChecked ? 'disabled' : '') + ' />' +
|
||||
'<span>' + esc(o.text) + mark + '</span>' +
|
||||
'</label>';
|
||||
const input = row.querySelector('input');
|
||||
input.addEventListener('change', () => {
|
||||
if (checked[qid]) return;
|
||||
toggle(q.id, o.id, q.hasMultipleAnswers);
|
||||
renderQuestions();
|
||||
});
|
||||
ul.appendChild(row);
|
||||
}
|
||||
li.appendChild(ul);
|
||||
|
||||
if (isImmediate() && !isChecked) {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'btn btn-ghost btn--sm';
|
||||
btn.textContent = 'Ответить';
|
||||
btn.style.marginTop = '0.4rem';
|
||||
const sel = selections[qid] || [];
|
||||
btn.disabled = sel.length === 0;
|
||||
btn.addEventListener('click', () => checkOne(q.id));
|
||||
li.appendChild(btn);
|
||||
}
|
||||
listEl.appendChild(li);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkOne(qid) {
|
||||
const k = String(qid);
|
||||
const sel = selections[k] || [];
|
||||
if (!sel.length) return;
|
||||
try {
|
||||
const r = await fetch('/api/tests/' + testId + '/attempts/' + attemptId + '/check', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ questionId: qid, selectedOptionIds: sel }),
|
||||
});
|
||||
const data = await r.json().catch(() => ({}));
|
||||
if (!r.ok) throw new Error(data.error || 'Не удалось проверить ответ.');
|
||||
checked[k] = data;
|
||||
renderQuestions();
|
||||
if (playData.hintsEnabled) {
|
||||
showHint(data);
|
||||
}
|
||||
} catch (e) {
|
||||
setErr(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
function showHint(data) {
|
||||
hintVerdict.textContent = data.isCorrect ? 'Верно!' : 'Неверно.';
|
||||
hintVerdict.style.color = data.isCorrect ? '#1a7a4a' : '#b32d2d';
|
||||
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');
|
||||
});
|
||||
|
||||
function startTimer(minutes) {
|
||||
if (!minutes || minutes <= 0) return;
|
||||
deadlineMs = Date.now() + minutes * 60 * 1000;
|
||||
timerEl.style.display = '';
|
||||
const tick = () => {
|
||||
const left = Math.max(0, deadlineMs - Date.now());
|
||||
const m = Math.floor(left / 60000);
|
||||
const s = Math.floor((left % 60000) / 1000);
|
||||
timerEl.textContent = 'Осталось: ' + m + ':' + String(s).padStart(2, '0');
|
||||
if (left <= 0) {
|
||||
clearInterval(timerHandle);
|
||||
submit(true);
|
||||
}
|
||||
};
|
||||
tick();
|
||||
timerHandle = setInterval(tick, 500);
|
||||
}
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const r = await fetch('/api/tests/' + testId + '/attempts/' + attemptId + '/play');
|
||||
@@ -90,22 +203,26 @@
|
||||
if (!r.ok) throw new Error(data.error || 'Не удалось открыть попытку.');
|
||||
playData = data;
|
||||
titleEl.textContent = data.testTitle || 'Прохождение теста';
|
||||
subEl.textContent = 'Порог зачёта: ' + (data.passingThreshold ?? 0) + '%.';
|
||||
const parts = ['Порог зачёта: ' + (data.passingThreshold ?? 0) + '%'];
|
||||
if (data.resultMode === 'immediate') parts.push('Результат сразу после ответа');
|
||||
if (data.hintsEnabled) parts.push('С ИИ-подсказками');
|
||||
subEl.textContent = parts.join(' · ') + '.';
|
||||
if (!Array.isArray(data.questions) || !data.questions.length) {
|
||||
setErr('В активной версии нет вопросов.');
|
||||
submitBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
renderQuestions();
|
||||
if (data.timeLimit) startTimer(Number(data.timeLimit));
|
||||
} catch (e) {
|
||||
setErr(e.message);
|
||||
submitBtn.disabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
async function submit(auto) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Отправка…';
|
||||
submitBtn.textContent = auto ? 'Время вышло, отправка…' : 'Отправка…';
|
||||
try {
|
||||
const r = await fetch('/api/tests/' + testId + '/attempts/' + attemptId + '/submit', {
|
||||
method: 'POST',
|
||||
@@ -114,6 +231,8 @@
|
||||
});
|
||||
const data = await r.json().catch(() => ({}));
|
||||
if (!r.ok) throw new Error(data.error || 'Не удалось завершить попытку.');
|
||||
if (timerHandle) clearInterval(timerHandle);
|
||||
timerEl.style.display = 'none';
|
||||
resultEl.style.display = '';
|
||||
resultEl.innerHTML =
|
||||
'<h3 style="margin-top:0;">Результат</h3>' +
|
||||
@@ -129,7 +248,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
submitBtn.addEventListener('click', submit);
|
||||
submitBtn.addEventListener('click', () => submit(false));
|
||||
load();
|
||||
})();
|
||||
</script>
|
||||
|
||||
@@ -8,16 +8,9 @@
|
||||
data-initial='{{ content | tojson | safe }}'>
|
||||
|
||||
<section class="cabinet-brick cabinet-brick--hero hero-brick">
|
||||
<div class="hero-brick__nav">
|
||||
<a href="{{ url_for('tests.tests_list_page') }}" class="link-back">← к списку</a>
|
||||
<span class="hero-brick__meta">
|
||||
<span>Автор: <b id="intro-author">Вы</b></span>
|
||||
<span class="hero-brick__sep">·</span>
|
||||
<span>Обновлён: <span id="intro-updated">—</span></span>
|
||||
<span class="hero-brick__sep">·</span>
|
||||
<span>Версия <span id="intro-version">—</span></span>
|
||||
</span>
|
||||
</div>
|
||||
<a href="{{ url_for('tests.tests_list_page') }}" class="link-back">← К тестам</a>
|
||||
<textarea id="test-title" maxlength="200" rows="1" placeholder="Название теста"
|
||||
class="hero-brick__title font-headline"></textarea>
|
||||
|
||||
<textarea id="test-title" maxlength="200" rows="1" placeholder="Название теста"
|
||||
class="hero-brick__title font-headline"></textarea>
|
||||
@@ -25,26 +18,92 @@
|
||||
<textarea id="test-description" rows="2" placeholder="Краткое описание (необязательно)"
|
||||
class="hero-brick__desc"></textarea>
|
||||
|
||||
<div class="hero-brick__chips">
|
||||
<label class="hero-brick__chip">
|
||||
<span>Порог зачёта</span>
|
||||
<input id="test-threshold" type="number" min="0" max="100" step="1" inputmode="numeric" />
|
||||
<span>%</span>
|
||||
</label>
|
||||
<span class="hero-brick__chip hero-brick__chip--readonly">
|
||||
Вопросов: <b id="q-count">0</b>
|
||||
</span>
|
||||
<label class="hero-brick__chip">
|
||||
<input id="chain-active" type="checkbox" />
|
||||
<span>Активна в каталоге</span>
|
||||
</label>
|
||||
<div class="hero-brick__meta-row">
|
||||
<span>Автор: <b id="intro-author">Вы</b></span>
|
||||
<span class="hero-brick__sep">·</span>
|
||||
<span>Обновлён: <span id="intro-updated">—</span></span>
|
||||
<span class="hero-brick__sep">·</span>
|
||||
<span>Версия <span id="intro-version">—</span></span>
|
||||
</div>
|
||||
|
||||
<div id="intro-fork-banner" class="callout callout--warning" data-fork-risk="{{ '1' if content.test.hasForkRisk else '0' }}" style="margin-top:0.85rem; display:none;">
|
||||
При сохранении будет создана новая версия теста.
|
||||
<div class="hero-brick__divider"></div>
|
||||
|
||||
<div class="hero-brick__meta-row">
|
||||
<span>Порог зачёта: <b id="threshold-mirror">—</b>%</span>
|
||||
<span class="hero-brick__sep">·</span>
|
||||
<span>Вопросов: <b id="q-count">0</b></span>
|
||||
<span class="hero-brick__sep">·</span>
|
||||
<span id="chain-active-display">Активна в каталоге</span>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
{# ── Версии ───────────────────────────────────────────────────── #}
|
||||
<details class="cabinet-disclosure cabinet-brick">
|
||||
<summary class="cabinet-disclosure__summary">
|
||||
<span class="cabinet-disclosure__summary-text">
|
||||
<span class="cabinet-disclosure__summary-title font-headline">Версии</span>
|
||||
<span class="cabinet-disclosure__summary-sub">История изменений; можно переключить активную версию</span>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="cabinet-disclosure__body">
|
||||
<ul id="versions-list" class="version-card-list"></ul>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="cabinet-disclosure cabinet-brick">
|
||||
<summary class="cabinet-disclosure__summary">
|
||||
<span class="cabinet-disclosure__summary-text">
|
||||
<span class="cabinet-disclosure__summary-title font-headline">Параметры теста</span>
|
||||
<span class="cabinet-disclosure__summary-sub">Порог, таймер, режим показа результата и подсказок</span>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="cabinet-disclosure__body">
|
||||
<div class="settings-grid">
|
||||
<label class="settings-row">
|
||||
<span class="settings-row__label">Проходной балл, %</span>
|
||||
<input id="test-threshold" type="number" min="0" max="100" step="1" inputmode="numeric"
|
||||
class="settings-row__input" />
|
||||
</label>
|
||||
|
||||
<label class="settings-row">
|
||||
<span class="settings-row__label">
|
||||
Таймер, минут
|
||||
<span class="settings-row__hint">0 или пусто — без ограничения</span>
|
||||
</span>
|
||||
<input id="test-time-limit" type="number" min="0" max="600" step="1" inputmode="numeric"
|
||||
class="settings-row__input" placeholder="—" />
|
||||
</label>
|
||||
|
||||
<fieldset class="settings-row settings-row--block">
|
||||
<legend class="settings-row__label">Когда показывать результат</legend>
|
||||
<label class="settings-radio">
|
||||
<input type="radio" name="result-mode" value="end" />
|
||||
<span>В конце теста <span class="settings-row__hint">(подсказок не будет)</span></span>
|
||||
</label>
|
||||
<label class="settings-radio">
|
||||
<input type="radio" name="result-mode" value="immediate" />
|
||||
<span>Сразу после ответа <span class="settings-row__hint">(с ИИ-подсказкой)</span></span>
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<label class="settings-row settings-row--toggle" id="test-hints-row" style="display:none;">
|
||||
<span class="settings-row__label">
|
||||
Показывать подсказку после ответа
|
||||
<span class="settings-row__hint">Краткое объяснение во всплывающем окне</span>
|
||||
</span>
|
||||
<input id="test-hints-enabled" type="checkbox" />
|
||||
</label>
|
||||
|
||||
<div class="settings-row settings-row--block" style="padding-top:0.75rem; border-top:1px solid var(--outline-variant); margin-top:0.25rem;">
|
||||
<span class="settings-row__label">Видимость в каталоге</span>
|
||||
<p class="settings-row__hint" style="margin-bottom:0.5rem;">Скрытые тесты не показываются в общем списке; ссылку по-прежнему можно открыть.</p>
|
||||
<button id="btn-toggle-visibility" class="btn btn-ghost btn--sm" type="button">Скрыть из списка</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="cabinet-disclosure cabinet-brick" open>
|
||||
<summary class="cabinet-disclosure__summary">
|
||||
<span class="cabinet-disclosure__summary-text">
|
||||
@@ -54,13 +113,14 @@
|
||||
</summary>
|
||||
<div class="cabinet-disclosure__body">
|
||||
<section class="rounded-2xl bg-brand-50/60 border border-brand-100 p-4 sm:p-5 test-detail-ai-panel">
|
||||
|
||||
{# ── Создать шаблон ──────────────────────────────────────── #}
|
||||
<div class="question-editor-block question-editor-block--first">
|
||||
<h3 class="test-detail-subsection__title" style="margin-top:0;">Генерация сетки вопросов (ИИ)</h3>
|
||||
<label class="block">
|
||||
<span class="form-label">Тема</span>
|
||||
<input id="ai-topic" type="text" class="form-input" placeholder="Например: Введение про LLM" />
|
||||
</label>
|
||||
<div class="mt-3 flex flex-wrap items-end gap-3">
|
||||
<h3 class="test-detail-subsection__title">Структура теста</h3>
|
||||
<p class="muted text-xs mb-3">
|
||||
Укажите количество вопросов и вариантов — создайте шаблон и заполните его вручную или через ИИ.
|
||||
</p>
|
||||
<div class="flex flex-wrap items-end gap-3">
|
||||
<label class="block">
|
||||
<span class="form-label">Вопросов</span>
|
||||
<input id="ai-q-count" type="number" min="1" max="30" step="1" value="7"
|
||||
@@ -71,106 +131,121 @@
|
||||
<input id="ai-o-count" type="number" min="2" max="8" step="1" value="3"
|
||||
class="form-input" style="width:90px;" />
|
||||
</label>
|
||||
<button id="ai-generate-test"
|
||||
<button id="create-template"
|
||||
class="btn btn-ghost" type="button" style="min-height:43px;">
|
||||
Сгенерировать тест (ИИ)
|
||||
<span class="material-symbols-outlined text-base" style="vertical-align:-3px;">grid_view</span>
|
||||
Создать шаблон
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Заполнить через ИИ по теме ──────────────────────────── #}
|
||||
<div class="question-editor-block">
|
||||
<h3 class="test-detail-subsection__title">Заполнить через ИИ</h3>
|
||||
<label class="block">
|
||||
<span class="form-label">Тема / промпт</span>
|
||||
<textarea id="ai-topic" rows="1" class="form-input"
|
||||
placeholder="Например: охрана труда на производстве"
|
||||
style="resize:none; overflow:hidden; font-family:inherit;"></textarea>
|
||||
</label>
|
||||
<div class="mt-2">
|
||||
<button id="ai-generate-test"
|
||||
class="btn btn-ghost" type="button" style="min-height:43px;">
|
||||
Сгенерировать вопросы (ИИ)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Проверить и улучшить ─────────────────────────────────── #}
|
||||
<div class="question-editor-block">
|
||||
<h3 class="test-detail-subsection__title">Проверить и улучшить</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button id="ai-check"
|
||||
class="btn btn-ghost" type="button" style="min-height:43px;">
|
||||
<span class="material-symbols-outlined text-base" style="vertical-align:-3px;">fact_check</span>
|
||||
Проверить тест
|
||||
</button>
|
||||
<button id="ai-improve"
|
||||
class="btn btn-ghost" type="button" style="min-height:43px;">
|
||||
<span class="material-symbols-outlined text-base" style="vertical-align:-3px;">auto_fix_high</span>
|
||||
Предложить улучшение
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ── Документ в вопросы ──────────────────────────────────── #}
|
||||
<div class="question-editor-block test-detail-subsection test-detail-subsection--import">
|
||||
<h3 class="test-detail-subsection__title">Документ в вопросы</h3>
|
||||
<p class="muted test-detail-hint" style="margin-top:0;">
|
||||
PDF, Word или текст — вставьте в черновик вопросов.
|
||||
</p>
|
||||
<label class="mt-2 inline-flex w-full items-center justify-center gap-2 px-3 py-3
|
||||
rounded-lg bg-white border border-ink-300/60 hover:border-brand-300
|
||||
text-sm cursor-pointer min-h-11">
|
||||
<span class="material-symbols-outlined text-base text-brand-600">upload_file</span>
|
||||
<span>Загрузить документ (PDF, DOCX, TXT, MD)</span>
|
||||
<label id="ai-import-dropzone"
|
||||
class="import-dropzone mt-2 flex flex-col w-full items-center justify-center gap-1
|
||||
px-4 py-5 rounded-xl bg-white border-2 border-dashed border-ink-300/70
|
||||
hover:border-brand-400 hover:bg-brand-50/40 cursor-pointer transition-colors">
|
||||
<span class="material-symbols-outlined text-2xl text-brand-400">upload_file</span>
|
||||
<span id="ai-import-dropzone-label" class="text-sm font-medium text-ink-700">Перетащите файл сюда или нажмите</span>
|
||||
<span class="text-xs text-ink-400">PDF, DOCX, TXT, MD · до 16 МБ</span>
|
||||
<input id="ai-import-file" type="file" accept=".pdf,.docx,.txt,.md" class="hidden" />
|
||||
</label>
|
||||
<p class="mt-1.5 text-xs text-ink-500">
|
||||
До 16 МБ. AI извлечёт текст и предложит черновик теста.
|
||||
</p>
|
||||
<label class="block mt-3">
|
||||
<span class="form-label">Пожелания по содержанию <span class="text-ink-400 font-normal">(необязательно)</span></span>
|
||||
<textarea id="doc-user-hint" rows="1"
|
||||
class="form-input mt-1"
|
||||
placeholder="Например: акцент на разделе 3, не делать вопросы про даты"
|
||||
style="resize:none; overflow:hidden; font-family:inherit;"></textarea>
|
||||
</label>
|
||||
<button id="doc-generate-btn"
|
||||
class="btn btn-ghost mt-2" type="button" style="min-height:43px;">
|
||||
<span class="material-symbols-outlined text-base" style="vertical-align:-3px;">auto_awesome</span>
|
||||
Сгенерировать из документа
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# ── Модалка результата импорта документа ─────────────────── #}
|
||||
<dialog id="import-modal" class="save-modal">
|
||||
<div class="save-modal__inner" style="max-width:480px; width:100%;">
|
||||
<h3 id="import-modal-title" class="font-headline text-base font-semibold mb-2"></h3>
|
||||
<div id="import-modal-body" class="text-sm text-ink-600 mb-4 max-h-64 overflow-y-auto"></div>
|
||||
<div id="import-modal-actions" class="flex gap-2 justify-end flex-wrap"></div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<p id="ai-status" class="mt-3 text-sm text-ink-500 min-h-[1.25rem]"></p>
|
||||
</section>
|
||||
|
||||
{# ── 3. Вопросы ─────────────────────────────────────────────── #}
|
||||
<section>
|
||||
<div class="flex items-center justify-between gap-2 px-1">
|
||||
<h2 class="font-semibold">Вопросы (<span id="q-count-mirror">0</span>)</h2>
|
||||
<section class="mt-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h2 class="font-semibold text-ink-900">Вопросы (<span id="q-count-mirror">0</span>)</h2>
|
||||
</div>
|
||||
<ol id="questions" class="mt-3 space-y-4"></ol>
|
||||
<div class="mt-3 flex justify-center">
|
||||
<button id="add-question"
|
||||
class="inline-flex items-center gap-1 px-3 py-2 rounded-lg
|
||||
bg-white border border-ink-300/60 hover:border-brand-300 text-sm min-h-10
|
||||
btn btn-ghost btn--sm question-editor__add-question">
|
||||
btn btn-ghost question-editor__add-question">
|
||||
<span class="material-symbols-outlined text-base">add</span>
|
||||
<span class="hidden sm:inline">Добавить вопрос</span>
|
||||
<span class="sm:hidden">Добавить</span>
|
||||
<span>Добавить вопрос</span>
|
||||
</button>
|
||||
</div>
|
||||
<ol id="questions" class="mt-3 space-y-3"></ol>
|
||||
|
||||
{# Кнопка «Сохранить» под вопросами #}
|
||||
<div class="mt-5 flex items-center gap-3">
|
||||
<button id="save-draft-inline"
|
||||
class="inline-flex items-center gap-2 px-5 py-2.5 rounded-lg
|
||||
bg-brand-600 hover:bg-brand-700 text-white font-medium min-h-11 btn btn-primary">
|
||||
<span class="material-symbols-outlined text-base">save</span>
|
||||
Сохранить
|
||||
</button>
|
||||
<button id="btn-cancel-inline"
|
||||
class="inline-flex px-3 py-2 rounded-lg text-ink-700 hover:bg-ink-100 text-sm btn btn-ghost">
|
||||
Отмена
|
||||
</button>
|
||||
<p id="save-status-inline" class="text-xs text-ink-500"></p>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="cabinet-disclosure cabinet-brick" open>
|
||||
<summary class="cabinet-disclosure__summary">
|
||||
<span class="cabinet-disclosure__summary-text">
|
||||
<span class="cabinet-disclosure__summary-title font-headline">История</span>
|
||||
<span class="cabinet-disclosure__summary-sub">Версии теста и кто проходил</span>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="cabinet-disclosure__body">
|
||||
<div class="test-detail-subsection test-detail-subsection--tight">
|
||||
<h3 class="test-detail-subsection__title">Версии</h3>
|
||||
<ul id="versions-list" class="version-card-list"></ul>
|
||||
</div>
|
||||
<div class="test-detail-subsection">
|
||||
<h3 class="test-detail-subsection__title">Прохождения</h3>
|
||||
<ul id="attempts-list" class="attempts-card-list"></ul>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="cabinet-disclosure cabinet-brick" open>
|
||||
<summary class="cabinet-disclosure__summary">
|
||||
<span class="cabinet-disclosure__summary-text">
|
||||
<span class="cabinet-disclosure__summary-title font-headline">Показ в каталоге</span>
|
||||
<span class="cabinet-disclosure__summary-sub">Видимость в списке и выдача сотрудникам</span>
|
||||
</span>
|
||||
</summary>
|
||||
<div class="cabinet-disclosure__body">
|
||||
<div class="test-detail-subsection test-detail-subsection--tight">
|
||||
<h3 class="test-detail-subsection__title">Видимость</h3>
|
||||
<p class="test-detail-hint">Скрытые тесты в общем списке не показываются; ссылку на тест по-прежнему можно открыть.</p>
|
||||
<div class="publication-visibility__actions">
|
||||
<button id="btn-toggle-visibility" class="btn btn-ghost" type="button">Скрыть из списка</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="test-detail-subsection">
|
||||
<h3 class="test-detail-subsection__title">Кому выдать</h3>
|
||||
<p class="test-detail-hint">Список с учётом поиска и фильтров; можно отметить всех на экране.</p>
|
||||
<div class="assign-toolbar">
|
||||
<input id="assign-search" class="form-input assign-toolbar__search" type="text" placeholder="Поиск: ФИО, логин" />
|
||||
<select id="assign-dept" class="form-input"><option value="__all__">Все отделы</option></select>
|
||||
<select id="assign-clinic" class="form-input">
|
||||
<option value="all">Все</option>
|
||||
<option value="with">С учёткой в модуле</option>
|
||||
<option value="without">Без учётки (создадим при назначении)</option>
|
||||
</select>
|
||||
<button id="assign-select-all" class="btn btn-ghost btn--sm" type="button">Выбрать всех</button>
|
||||
</div>
|
||||
<div id="assign-list" class="assign-list"></div>
|
||||
<div class="inline-actions" style="margin-top:0.75rem;">
|
||||
<button id="assign-submit" class="btn btn-primary" type="button">Назначить выбранных</button>
|
||||
<span id="assign-status" class="muted"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
{# Прохождения перенесены на /stats. Назначения перенесены на /assignments #}
|
||||
</div>
|
||||
|
||||
{# ── Sticky-footer: «Цепочка активна» + «Сохранить» ────────────── #}
|
||||
@@ -178,12 +253,16 @@
|
||||
pb-[env(safe-area-inset-bottom)]">
|
||||
<div class="mx-auto {% if ui_variant == 'legacy' %}max-w-2xl{% else %}max-w-6xl{% endif %} px-4 py-3
|
||||
flex items-center justify-between gap-3">
|
||||
<span class="text-xs text-ink-500 truncate">Активность цепочки и поля теста — в шапке.</span>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<a href="{{ url_for('tests.tests_list_page') }}"
|
||||
class="hidden sm:inline-flex px-3 py-2 rounded-lg text-ink-700 hover:bg-ink-100 text-sm btn btn-ghost">
|
||||
К каталогу
|
||||
</a>
|
||||
<div id="intro-fork-banner" class="callout callout--warning text-xs sm:text-sm"
|
||||
data-fork-risk="{{ '1' if content.test.hasForkRisk else '0' }}"
|
||||
style="display:none; margin:0; padding:0.4rem 0.6rem; flex:1 1 0; min-width:0; white-space:normal; word-break:break-word; line-height:1.25;">
|
||||
При сохранении будет создана новая версия теста.
|
||||
</div>
|
||||
<div class="flex items-center gap-2 ml-auto shrink-0">
|
||||
<button id="btn-cancel"
|
||||
class="inline-flex px-3 py-2 rounded-lg text-ink-700 hover:bg-ink-100 text-sm btn btn-ghost">
|
||||
Отмена
|
||||
</button>
|
||||
<button id="save-draft"
|
||||
class="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg
|
||||
bg-brand-600 hover:bg-brand-700 text-white font-medium min-h-11 btn btn-primary">
|
||||
@@ -197,12 +276,31 @@
|
||||
|
||||
{# ── Шаблон вопроса ─────────────────────────────────────────────── #}
|
||||
<template id="tpl-question">
|
||||
<li class="rounded-xl bg-white border border-ink-300/60 p-3 sm:p-4 q-item question-editor-block">
|
||||
{# Шапка карточки вопроса: номер слева, кнопки справа. #}
|
||||
<li class="relative rounded-xl bg-white border border-ink-300/60 p-4 sm:p-5 q-item" draggable="true">
|
||||
|
||||
{# Оверлей загрузки AI #}
|
||||
<div class="q-ai-overlay hidden absolute inset-0 rounded-xl z-10
|
||||
bg-white/80 backdrop-blur-[2px] flex flex-col items-center justify-center gap-2">
|
||||
<span class="q-ai-spinner inline-block w-7 h-7 rounded-full
|
||||
border-[3px] border-brand-200 border-t-brand-600 animate-spin"></span>
|
||||
<span class="text-xs text-ink-500 font-medium">Генерирую…</span>
|
||||
</div>
|
||||
|
||||
{# Шапка карточки вопроса #}
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-md
|
||||
bg-brand-50 text-brand-700 text-xs font-medium q-num">Вопрос #</span>
|
||||
<span class="inline-flex items-center gap-1">
|
||||
<button class="q-drag p-2 rounded hover:bg-ink-100 min-w-10 min-h-10 cursor-grab"
|
||||
title="Перетащить" aria-label="Перетащить" type="button">
|
||||
<span class="material-symbols-outlined text-base">drag_indicator</span>
|
||||
</button>
|
||||
<span class="inline-flex items-center px-2 py-0.5 rounded-md
|
||||
bg-brand-50 text-brand-700 text-xs font-medium q-num">Вопрос #</span>
|
||||
</span>
|
||||
<div class="flex items-center gap-0.5">
|
||||
<button class="q-clear p-2 rounded hover:bg-ink-100 min-w-10 min-h-10 text-ink-400"
|
||||
title="Очистить вопрос" aria-label="Очистить вопрос" type="button">
|
||||
<span class="material-symbols-outlined text-base">backspace</span>
|
||||
</button>
|
||||
<button class="q-up p-2 rounded hover:bg-ink-100 min-w-10 min-h-10"
|
||||
title="Выше" aria-label="Поднять выше">
|
||||
<span class="material-symbols-outlined text-base">arrow_upward</span>
|
||||
@@ -218,55 +316,80 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="question-editor-block__header">
|
||||
<h4 class="question-editor-block__title q-num">Вопрос #</h4>
|
||||
<button class="q-ai btn btn-ghost btn--sm question-editor-block__ai-btn">
|
||||
Сгенерировать вопрос (ИИ)
|
||||
</button>
|
||||
<div class="mt-2 relative">
|
||||
<textarea class="q-text w-full rounded-lg border border-ink-300 px-3 py-2
|
||||
focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20"
|
||||
rows="1" placeholder="Формулировка вопроса" maxlength="500"
|
||||
style="resize:none; overflow:hidden; font-family:inherit;"></textarea>
|
||||
<span class="q-text-counter absolute bottom-1.5 right-2 text-xs text-ink-400 pointer-events-none select-none"></span>
|
||||
</div>
|
||||
|
||||
<textarea class="q-text mt-2 w-full rounded-lg border border-ink-300 px-3 py-2
|
||||
focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20"
|
||||
rows="2" placeholder="Формулировка вопроса"></textarea>
|
||||
|
||||
<div class="mt-3 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 text-sm">
|
||||
<label class="inline-flex items-center gap-2 min-h-9">
|
||||
<input type="checkbox"
|
||||
class="q-multi rounded border-ink-300 text-brand-600 focus:ring-brand-500" />
|
||||
<span>Несколько правильных ответов</span>
|
||||
</label>
|
||||
<button class="q-ai btn btn-ghost btn--sm q-ai-btn" style="font-size:0.75rem; padding:0.3rem 0.7rem;">
|
||||
<span class="material-symbols-outlined q-ai-icon" style="font-size:0.9rem; vertical-align:-2px;">auto_fix_high</span>
|
||||
<span class="q-ai-label">Сгенерировать</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ul class="q-options mt-3 space-y-2"></ul>
|
||||
<button class="q-add-option mt-2 inline-flex items-center gap-1 px-2 py-2 rounded
|
||||
text-sm text-brand-700 hover:bg-brand-50 min-h-10 btn btn-ghost btn--sm">
|
||||
<span class="material-symbols-outlined text-base">add</span>
|
||||
Добавить вариант
|
||||
</button>
|
||||
<p class="mt-4 mb-2 text-xs text-ink-400 font-medium">Отметьте правильные варианты</p>
|
||||
<ul class="q-options space-y-2"></ul>
|
||||
<div class="mt-3 flex items-center gap-3">
|
||||
<button class="q-add-option inline-flex items-center gap-1 px-2 py-2 rounded
|
||||
text-sm text-brand-700 hover:bg-brand-50 min-h-10 btn btn-ghost btn--sm">
|
||||
<span class="material-symbols-outlined text-base">add</span>
|
||||
<span class="q-add-option-label">Добавить вариант</span>
|
||||
</button>
|
||||
<span class="q-options-count text-xs text-ink-400"></span>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
{# ── Шаблон варианта ────────────────────────────────────────────── #}
|
||||
<template id="tpl-option">
|
||||
<li class="flex items-center gap-2 opt-item question-option-row">
|
||||
{# Чекбокс «Правильный» — обёрнут в большой tap-target. #}
|
||||
<label class="inline-flex items-center justify-center w-10 h-10 shrink-0 cursor-pointer
|
||||
rounded hover:bg-ink-100" title="Правильный ответ">
|
||||
<li class="flex items-start gap-2 opt-item">
|
||||
{# Чекбокс «Правильный» — выровнен по первой строке textarea #}
|
||||
<label class="shrink-0 w-10 inline-flex items-center justify-center cursor-pointer
|
||||
rounded hover:bg-ink-100 pt-1.5" style="min-height:2.5rem;" title="Правильный ответ">
|
||||
<input type="checkbox"
|
||||
class="opt-correct w-5 h-5 rounded border-ink-300 text-brand-600 focus:ring-brand-500 question-option-row__mark" />
|
||||
class="opt-correct w-5 h-5 rounded border-ink-300 text-brand-600 focus:ring-brand-500" />
|
||||
</label>
|
||||
<input type="text"
|
||||
class="opt-text flex-1 min-w-0 rounded-lg border border-ink-300 px-3 py-2 question-option-row__text
|
||||
focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20"
|
||||
placeholder="Вариант ответа" />
|
||||
<button class="opt-delete shrink-0 w-10 h-10 inline-flex items-center justify-center
|
||||
rounded hover:bg-red-50 text-red-600 question-option-remove"
|
||||
<textarea rows="1"
|
||||
class="opt-text flex-1 min-w-0 rounded-lg border border-ink-300 px-3 py-2
|
||||
focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20"
|
||||
placeholder="Вариант ответа"
|
||||
style="resize:none; overflow:hidden; font-family:inherit; line-height:1.55;"></textarea>
|
||||
<button class="opt-delete shrink-0 w-10 inline-flex items-center justify-center
|
||||
rounded hover:bg-red-50 text-red-600 pt-1.5"
|
||||
style="min-height:2.5rem;"
|
||||
title="Удалить" aria-label="Удалить вариант">
|
||||
<span class="material-symbols-outlined text-base">close</span>
|
||||
</button>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
{# ── Модалка успешного сохранения (компактная, сверху) ──────────── #}
|
||||
<dialog id="save-modal" class="save-modal">
|
||||
<div class="save-modal__inner">
|
||||
<h3 class="text-base font-semibold mb-1" id="save-modal-title">Сохранено</h3>
|
||||
<p id="save-modal-msg" class="text-sm text-ink-700">Изменения сохранены.</p>
|
||||
<div class="mt-4 flex items-center justify-end gap-2">
|
||||
<button id="save-modal-stay" type="button"
|
||||
class="px-3 py-2 rounded-lg bg-ink-100 hover:bg-ink-200 text-sm btn btn-ghost">
|
||||
К редактору
|
||||
</button>
|
||||
<button id="save-modal-go" type="button"
|
||||
class="px-3 py-2 rounded-lg bg-brand-600 hover:bg-brand-700 text-white text-sm btn btn-primary">
|
||||
К каталогу
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
{# ── Модалка результата AI-проверки/улучшения (fullscreen на мобиле) ── #}
|
||||
<dialog id="ai-modal"
|
||||
class="m-0 p-0 w-full h-full sm:h-auto sm:max-w-3xl sm:w-full sm:max-h-[90vh]
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<span class="list-row__title">{{ t.title }}</span>
|
||||
<span class="list-row__meta">
|
||||
{{ t.author_full_name or '—' }}
|
||||
<span class="list-row__meta-tail"> · v{{ t.version }}</span>
|
||||
<span class="list-row__meta-tail"> · Версия {{ t.version }}</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -44,7 +44,7 @@
|
||||
<span class="list-row__title">{{ t.title }}</span>
|
||||
<span class="list-row__meta">
|
||||
{{ t.author_full_name or '—' }}
|
||||
<span class="list-row__meta-tail"> · v{{ t.version }} · скрыт</span>
|
||||
<span class="list-row__meta-tail"> · Версия {{ t.version }} · скрыт</span>
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
@@ -61,7 +61,7 @@
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-xl sm:text-2xl font-semibold">Каталог тестов</h1>
|
||||
<p class="mt-1 text-sm text-ink-500">Активные тесты, к которым у вас есть доступ.</p>
|
||||
<p class="mt-1 text-sm text-ink-500">Все активные тесты.</p>
|
||||
</div>
|
||||
<button id="btn-create-test"
|
||||
class="inline-flex items-center justify-center gap-2 px-4 py-3 rounded-lg
|
||||
@@ -80,13 +80,13 @@
|
||||
class="block p-4 active:bg-ink-100/40">
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<h3 class="font-semibold text-ink-900 line-clamp-2 min-w-0">{{ t.title }}</h3>
|
||||
<span class="text-xs text-ink-500 shrink-0 mt-0.5">v{{ t.version }}</span>
|
||||
<span class="text-xs text-ink-500 shrink-0 mt-0.5 whitespace-nowrap">Версия {{ t.version }}</span>
|
||||
</div>
|
||||
{% if t.description %}
|
||||
<p class="mt-1 text-sm text-ink-500 line-clamp-3">{{ t.description }}</p>
|
||||
{% endif %}
|
||||
<div class="mt-3 flex items-center justify-between gap-2 text-xs text-ink-500">
|
||||
<span class="truncate">Автор: {{ t.author_full_name or '—' }}</span>
|
||||
<span class="truncate">{{ t.author_full_name or '—' }}</span>
|
||||
<span class="inline-flex items-center gap-1 text-brand-700">
|
||||
<span class="material-symbols-outlined text-sm">edit_note</span>
|
||||
Открыть
|
||||
@@ -103,7 +103,7 @@
|
||||
{% if hidden %}
|
||||
<details class="mt-6 rounded-xl border border-ink-300/60 bg-ink-100/50 p-4">
|
||||
<summary class="cursor-pointer font-medium text-ink-700">
|
||||
Скрытые вами цепочки ({{ hidden|length }})
|
||||
Скрытые из каталога ({{ hidden|length }})
|
||||
</summary>
|
||||
<ul class="mt-3 space-y-2">
|
||||
{% for t in hidden %}
|
||||
|
||||
Reference in New Issue
Block a user