testingwebapp fixes, weeek tasks 2948-2958

This commit is contained in:
Константин Лебединский
2026-05-04 21:29:23 +05:00
parent 0229bc250b
commit 1ea83aa6b4
9 changed files with 1035 additions and 214 deletions
+91 -13
View File
@@ -7,6 +7,8 @@
data-test-id="{{ test_id }}"
data-initial='{{ content | tojson | safe }}'>
<div id="editor-gen-toast" class="editor-gen-toast" role="status" aria-live="polite" hidden></div>
<section class="cabinet-brick cabinet-brick--hero hero-brick">
<a href="{{ url_for('tests.tests_list_page') }}" class="link-back">К тестам</a>
<textarea id="test-title" maxlength="200" rows="1" placeholder="Название теста"
@@ -164,6 +166,10 @@
placeholder="Например: охрана труда на производстве"
style="resize:none; overflow:hidden; font-family:inherit;"></textarea>
</label>
<label class="mt-2 inline-flex items-start gap-2 text-sm text-ink-700 cursor-pointer select-none">
<input type="checkbox" id="ai-keep-title" class="mt-1 rounded border-ink-300 text-brand-600 focus:ring-brand-500" />
<span>Не менять название теста в редакторе после генерации (оставить текущее)</span>
</label>
<div class="mt-2">
<button id="ai-generate-test"
class="btn btn-ghost" type="button" style="min-height:43px;">
@@ -185,7 +191,18 @@
<p class="text-xs text-ink-500 leading-snug mb-3">
<span class="font-medium text-ink-600">Предложить улучшение</span> — ИИ предложит правки по каждому вопросу (было → стало); вы отметите, что применить к черновику.
</p>
<div class="flex flex-wrap gap-2">
<div class="flex flex-col sm:flex-row sm:flex-wrap sm:items-end gap-2">
<label class="flex flex-col gap-1 min-w-[12rem] flex-1">
<span class="text-xs font-medium text-ink-600">Область улучшения</span>
<select id="ai-improve-focus"
class="form-input text-sm py-2 rounded-lg border border-ink-300 bg-white">
<option value="all">Всё: вопросы и варианты</option>
<option value="questions">Только формулировки вопросов</option>
<option value="distractors">Только неверные варианты (дистракторы)</option>
<option value="options">Все варианты ответа (без смены верности)</option>
</select>
</label>
<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>
@@ -196,13 +213,14 @@
<span class="material-symbols-outlined text-base" style="vertical-align:-3px;">auto_fix_high</span>
Предложить улучшение
</button>
</div>
</div>
</div>
{# ── Документ в вопросы ──────────────────────────────────── #}
<div class="question-editor-block test-detail-subsection test-detail-subsection--import">
<h3 class="test-detail-subsection__title">Документ в вопросы</h3>
<p class="text-xs text-ink-500 leading-snug mb-2">
<p class="text-xs text-ink-500 leading-snug mb-2 text-center">
<span class="font-medium text-ink-600">Сгенерировать из документа</span> — из файла извлекается текст; ИИ составляет вопросы по содержанию и шаблону из «Параметров» (число вопросов, вариантов, несколько верных и т.д.), с учётом поля «Пожелания», если оно заполнено. Перед заменой откроется предпросмотр: «Применить» подставит черновик вместо текущих вопросов; дальше сохраните тест на сервер — подсказки и версии ведут себя так же, как при генерации по теме.
</p>
<label id="ai-import-dropzone"
@@ -210,10 +228,15 @@
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" />
<span id="ai-import-dropzone-label" class="text-sm font-medium text-ink-700 text-center block w-full">Перетащите файлы сюда или нажмите</span>
<span class="text-xs text-ink-400 text-center block w-full">PDF, DOCX, TXT, MD · до 5 за раз · до 16 МБ · повторный выбор добавляет к уже загруженным; полный сброс — «Сбросить загрузку».</span>
<input id="ai-import-file" type="file" accept=".pdf,.docx,.txt,.md" multiple class="hidden" />
</label>
<div class="mt-2 flex flex-wrap gap-2">
<button type="button" id="ai-import-clear" class="btn btn-ghost btn--sm text-sm" style="min-height:36px;">
Сбросить загрузку
</button>
</div>
<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"
@@ -226,7 +249,6 @@
<span class="material-symbols-outlined text-base" style="vertical-align:-3px;">auto_awesome</span>
Сгенерировать из документа
</button>
<p id="doc-progress" class="mt-2 text-xs text-ink-500 min-h-[1rem]"></p>
</div>
{# ── Модалка результата импорта документа ─────────────────── #}
@@ -238,7 +260,6 @@
</div>
</dialog>
<p id="ai-status" class="editor-generation-panel__status text-sm text-ink-500"></p>
</section>
</div>
@@ -257,7 +278,7 @@
<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">
<div class="mt-3 flex justify-center flex-wrap gap-2">
<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
@@ -265,6 +286,14 @@
<span class="material-symbols-outlined text-base">add</span>
<span>Добавить вопрос</span>
</button>
<button id="add-question-ai" type="button"
class="inline-flex items-center gap-1 px-3 py-2 rounded-lg
bg-brand-50 border border-brand-200 hover:border-brand-400 text-sm min-h-10
btn btn-ghost question-editor__add-question-ai"
title="Добавляет пустой блок и вызывает ИИ для одного нового вопроса с вариантами">
<span class="material-symbols-outlined text-base">auto_awesome</span>
<span>Новый вопрос (ИИ)</span>
</button>
</div>
</section>
@@ -396,11 +425,26 @@
<input type="checkbox"
class="opt-correct w-5 h-5 rounded border-ink-300 text-brand-600 focus:ring-brand-500" />
</label>
<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>
<div class="opt-text-wrap relative flex-1 min-w-0 self-start">
<textarea rows="1"
class="opt-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"
placeholder="Вариант ответа"
style="resize:none; overflow:hidden; font-family:inherit; line-height:1.55;"></textarea>
<div class="opt-ai-overlay hidden absolute inset-0 rounded-lg z-[5]
bg-white/85 backdrop-blur-[2px] flex flex-col items-center justify-center gap-1
border border-ink-200/80 shadow-sm">
<span class="inline-block w-6 h-6 rounded-full
border-[3px] border-brand-200 border-t-brand-600 animate-spin"></span>
<span class="text-xs text-ink-600 font-medium px-2 text-center leading-snug">Улучшаю…</span>
</div>
</div>
<button type="button" class="opt-ai shrink-0 inline-flex items-center justify-center
rounded hover:bg-brand-50 text-brand-700 px-1.5 pt-1.5"
style="min-height:2.5rem; min-width:2.25rem;"
title="Улучшить только этот вариант (ИИ)" aria-label="Улучшить вариант ИИ">
<span class="material-symbols-outlined text-base">auto_fix_high</span>
</button>
<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;"
@@ -428,6 +472,40 @@
</div>
</dialog>
{# ── Выбор режима ИИ для непустого вопроса (улучшить / дистракторы) ── #}
<dialog id="dlg-q-ai-mode" class="save-modal" style="max-width: 24rem; width: calc(100% - 2rem);">
<div class="save-modal__inner">
<h3 class="text-base font-semibold text-ink-900 mb-1">Что сделать с вопросом?</h3>
<p class="text-xs text-ink-500 mb-3">Выберите, что должен сделать ИИ.</p>
<div class="flex flex-col gap-2">
<button type="button" id="q-ai-mode-distractors"
class="px-3 py-2.5 rounded-lg border border-ink-200 text-left text-sm
hover:bg-brand-50 hover:border-brand-200 transition-colors">
<span class="font-medium text-ink-800">Добавить дистракторы</span>
<span class="block text-ink-500 text-xs mt-0.5">Заполнить пустые поля вариантов</span>
</button>
<button type="button" id="q-ai-mode-question"
class="px-3 py-2.5 rounded-lg border border-ink-200 text-left text-sm
hover:bg-brand-50 hover:border-brand-200 transition-colors">
<span class="font-medium text-ink-800">Улучшить только вопрос</span>
<span class="block text-ink-500 text-xs mt-0.5">Переформулировать текст вопроса</span>
</button>
<button type="button" id="q-ai-mode-options"
class="px-3 py-2.5 rounded-lg border border-ink-200 text-left text-sm
hover:bg-brand-50 hover:border-brand-200 transition-colors">
<span class="font-medium text-ink-800">Улучшить только варианты</span>
<span class="block text-ink-500 text-xs mt-0.5">Тексты ответов, без смены верности</span>
</button>
</div>
<div class="mt-4 flex justify-end">
<button type="button" id="q-ai-mode-cancel"
class="px-3 py-2 rounded-lg text-ink-600 hover:bg-ink-100 text-sm">
Отмена
</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]
+81 -4
View File
@@ -19,16 +19,37 @@
{% block content %}
<section class="legacy-list-shell">
<h1 class="font-headline legacy-list-title">Тесты</h1>
<div class="legacy-list-toolbar">
<button id="btn-create-test" class="btn btn-ghost" type="button">
<div class="legacy-list-toolbar legacy-list-toolbar--wrap flex flex-wrap items-center gap-3">
<div class="flex flex-col sm:flex-row flex-1 min-w-0 gap-2 sm:gap-3">
<label class="flex flex-col gap-1 flex-1 min-w-[12rem] max-w-md">
<span class="text-xs font-medium text-ink-600">Поиск по названию</span>
<input id="catalog-search" type="search" autocomplete="off" placeholder="Начните вводить…"
class="rounded-lg border border-ink-300 px-3 py-2 text-sm w-full
focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20" />
</label>
<label class="flex flex-col gap-1 w-full sm:w-52 shrink-0">
<span class="text-xs font-medium text-ink-600">Автор</span>
<select id="catalog-author"
class="rounded-lg border border-ink-300 px-3 py-2 text-sm w-full bg-white
focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20">
<option value="">Все авторы</option>
</select>
</label>
</div>
<button id="btn-create-test" class="btn btn-ghost shrink-0" type="button">
Создать
</button>
</div>
<p id="catalog-filter-empty" class="text-sm text-ink-500 mt-2 hidden" role="status"></p>
{% if visible %}
<ul class="list-stack" aria-label="Тесты в общем списке">
{% for t in visible %}
<li class="list-row list-row--split">
<li class="list-row list-row--split list-row--catalog"
data-catalog-row
data-title-lower="{{ (t.title or '')|lower }}"
data-author-id="{{ t.created_by or '' }}"
data-author-name="{{ (t.author_full_name or '—')|e }}">
<div class="list-row__main">
<a href="{{ url_for('tests.tests_editor_page', test_id=t.id) }}" class="list-row__link">
<span class="list-row__title">{{ t.title }}</span>
@@ -55,7 +76,11 @@
<h2 class="font-headline legacy-list-subtitle">Скрытые вами из списка</h2>
<ul class="list-stack" aria-label="Скрытые тесты автора">
{% for t in hidden %}
<li class="list-row list-row--split list-row--hidden">
<li class="list-row list-row--split list-row--hidden list-row--catalog"
data-catalog-row
data-title-lower="{{ (t.title or '')|lower }}"
data-author-id="{{ t.created_by or '' }}"
data-author-name="{{ (t.author_full_name or '—')|e }}">
<div class="list-row__main">
<a href="{{ url_for('tests.tests_editor_page', test_id=t.id) }}" class="list-row__link">
<span class="list-row__title">{{ t.title }}</span>
@@ -124,6 +149,58 @@ class="m-0 p-0 w-full sm:w-full sm:max-w-md
const dlg = document.getElementById('dlg-create');
const titleEl = document.getElementById('new-test-title');
const descEl = document.getElementById('new-test-desc');
const catalogSearch = document.getElementById('catalog-search');
const catalogAuthor = document.getElementById('catalog-author');
const catalogEmpty = document.getElementById('catalog-filter-empty');
(function initCatalogFilter() {
if (!catalogSearch || !catalogAuthor) return;
const rows = Array.from(document.querySelectorAll('[data-catalog-row]'));
const byAuthor = new Map();
rows.forEach((row) => {
const id = (row.dataset.authorId || '').trim();
const name = (row.dataset.authorName || '').trim() || '—';
if (id && !byAuthor.has(id)) byAuthor.set(id, name);
});
const sorted = Array.from(byAuthor.entries()).sort((a, b) => a[1].localeCompare(b[1], 'ru'));
sorted.forEach(([id, name]) => {
const opt = document.createElement('option');
opt.value = id;
opt.textContent = name;
catalogAuthor.appendChild(opt);
});
function applyFilter() {
const q = (catalogSearch.value || '').trim().toLowerCase();
const author = (catalogAuthor.value || '').trim();
let visibleCount = 0;
rows.forEach((row) => {
const title = row.dataset.titleLower || '';
const aid = (row.dataset.authorId || '').trim();
const matchQ = !q || title.includes(q);
const matchA = !author || aid === author;
const show = matchQ && matchA;
row.style.display = show ? '' : 'none';
if (show) visibleCount += 1;
});
if (catalogEmpty) {
if (rows.length && visibleCount === 0) {
catalogEmpty.textContent = 'Ничего не найдено — измените запрос или фильтр.';
catalogEmpty.classList.remove('hidden');
} else {
catalogEmpty.textContent = '';
catalogEmpty.classList.add('hidden');
}
}
}
let t = null;
catalogSearch.addEventListener('input', () => {
clearTimeout(t);
t = setTimeout(applyFilter, 120);
});
catalogAuthor.addEventListener('change', applyFilter);
})();
document.getElementById('btn-create-test').addEventListener('click', () => {
titleEl.value = '';