You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
440 lines
24 KiB
440 lines
24 KiB
{% extends "base.html" %} |
|
{% block title %}{{ content.test.title }} — редактор{% endblock %} |
|
|
|
{% block content %} |
|
<div id="editor-root" |
|
class="space-y-4 sm:space-y-5 pb-24 {% if ui_variant == 'legacy' %}test-detail-page test-detail-page--with-fixed-actions{% endif %}" |
|
data-test-id="{{ test_id }}" |
|
data-initial='{{ content | tojson | safe }}'> |
|
|
|
<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="Название теста" |
|
class="hero-brick__title font-headline"></textarea> |
|
|
|
<textarea id="test-description" rows="2" placeholder="Краткое описание (необязательно)" |
|
class="hero-brick__desc"></textarea> |
|
|
|
<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 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>Время: <b id="editor-hero-time-val">—</b></span> |
|
<span class="hero-brick__sep">·</span> |
|
<span>Результат: <b id="editor-hero-result-val">—</b></span> |
|
<span class="hero-brick__sep">·</span> |
|
<span>Подсказки: <b id="editor-hero-hints-val">—</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> |
|
</label> |
|
<label class="settings-radio"> |
|
<input type="radio" name="result-mode" value="immediate" /> |
|
<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"> |
|
<span class="settings-row__label">Шаблон структуры вопросов</span> |
|
<p class="settings-row__hint" style="margin-bottom:0.5rem;"> |
|
Задаёт сетку для автосборки пустых вопросов и для генерации через ИИ. При смене чисел список вопросов пересобирается (с подтверждением, если уже есть текст). |
|
</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" |
|
class="form-input" style="width:90px;" /> |
|
</label> |
|
<label class="block"> |
|
<span class="form-label">Вариантов</span> |
|
<input id="ai-o-count" type="number" min="2" max="8" step="1" value="3" |
|
class="form-input" style="width:90px;" /> |
|
</label> |
|
</div> |
|
<div class="mt-3 grid gap-2 sm:grid-cols-3 items-end"> |
|
<label class="inline-flex items-center gap-2 min-h-9"> |
|
<input id="template-global-multi" type="checkbox" |
|
class="rounded border-ink-300 text-brand-600 focus:ring-brand-500" /> |
|
<span class="text-sm">Несколько правильных ответов (все вопросы)</span> |
|
</label> |
|
<label class="block"> |
|
<span class="form-label">Правильных: от</span> |
|
<input id="template-min-correct" type="number" min="1" max="8" step="1" value="1" |
|
class="form-input" style="width:90px;" /> |
|
</label> |
|
<label class="block"> |
|
<span class="form-label">до</span> |
|
<input id="template-max-correct" type="number" min="1" max="8" step="1" value="1" |
|
class="form-input" style="width:90px;" /> |
|
</label> |
|
</div> |
|
</div> |
|
|
|
<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"> |
|
<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"> |
|
<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">Заполнить через ИИ</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> |
|
<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> |
|
<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> |
|
<p id="doc-progress" class="mt-2 text-xs text-ink-500 min-h-[1rem]"></p> |
|
</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> |
|
|
|
</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"> |
|
<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 question-editor__add-question"> |
|
<span class="material-symbols-outlined text-base">add</span> |
|
<span>Добавить вопрос</span> |
|
</button> |
|
</div> |
|
|
|
</section> |
|
</div> |
|
</details> |
|
|
|
{# Прохождения перенесены на /stats. Назначения перенесены на /assignments #} |
|
</div> |
|
|
|
{# ── Sticky-footer: «Цепочка активна» + «Сохранить» ────────────── #} |
|
<div class="fixed bottom-0 inset-x-0 z-30 bg-white/95 backdrop-blur border-t border-ink-300/60 |
|
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"> |
|
<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"> |
|
<span class="material-symbols-outlined text-base">save</span> |
|
Сохранить |
|
</button> |
|
</div> |
|
</div> |
|
<p id="save-status" class="mx-auto {% if ui_variant == 'legacy' %}max-w-2xl{% else %}max-w-6xl{% endif %} px-4 pb-2 text-xs text-ink-500"></p> |
|
</div> |
|
|
|
{# ── Шаблон вопроса ─────────────────────────────────────────────── #} |
|
<template id="tpl-question"> |
|
<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 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> |
|
</button> |
|
<button class="q-down p-2 rounded hover:bg-ink-100 min-w-10 min-h-10" |
|
title="Ниже" aria-label="Опустить ниже"> |
|
<span class="material-symbols-outlined text-base">arrow_downward</span> |
|
</button> |
|
<button class="q-delete p-2 rounded hover:bg-red-50 text-red-600 min-w-10 min-h-10" |
|
title="Удалить" aria-label="Удалить вопрос"> |
|
<span class="material-symbols-outlined text-base">delete</span> |
|
</button> |
|
</div> |
|
</div> |
|
|
|
<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> |
|
|
|
<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> |
|
|
|
<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> |
|
|
|
<div class="mt-4 pt-3 border-t border-ink-200/80 q-hint-block"> |
|
<label class="block"> |
|
<span class="text-xs text-ink-600 font-medium mb-1 block">Подсказка при прохождении</span> |
|
<textarea class="q-hint form-input w-full rounded-lg border border-ink-300 px-3 py-2 text-sm |
|
focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20" |
|
rows="2" maxlength="8000" |
|
placeholder="Краткий текст подсказки (если в параметрах теста включены подсказки и режим «Сразу после ответа»)" |
|
style="resize:none; overflow:hidden; font-family:inherit;"></textarea> |
|
</label> |
|
<p class="mt-1.5 text-xs text-ink-400 leading-snug">Необязательно. Показывается участнику во всплывающем окне после ответа на вопрос.</p> |
|
</div> |
|
</li> |
|
</template> |
|
|
|
{# ── Шаблон варианта ────────────────────────────────────────────── #} |
|
<template id="tpl-option"> |
|
<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" /> |
|
</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> |
|
<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] |
|
sm:rounded-2xl sm:m-auto bg-white backdrop:bg-black/50"> |
|
<div class="flex flex-col h-full sm:max-h-[90vh]"> |
|
<div class="flex items-center justify-between gap-3 px-4 sm:px-5 py-3 border-b border-ink-300/60"> |
|
<h3 id="ai-modal-title" class="text-lg font-semibold truncate">AI</h3> |
|
<button id="ai-modal-close" |
|
class="p-2 rounded hover:bg-ink-100 min-w-10 min-h-10" |
|
aria-label="Закрыть"> |
|
<span class="material-symbols-outlined">close</span> |
|
</button> |
|
</div> |
|
<div id="ai-modal-body" class="flex-1 overflow-y-auto px-4 sm:px-5 py-4"></div> |
|
<div id="ai-modal-actions" |
|
class="px-4 sm:px-5 py-3 border-t border-ink-300/60 |
|
flex items-center justify-end gap-2 flex-wrap |
|
pb-[max(env(safe-area-inset-bottom),0.75rem)]"></div> |
|
</div> |
|
</dialog> |
|
{% endblock %} |
|
|
|
{% block scripts %} |
|
<script src="{{ url_for('static', filename='js/editor.js') }}"></script> |
|
{% endblock %}
|
|
|