4b0d56ff0e
Этап 1 миграции TestingWebApp на целевой стек (Python/Flask/Jinja),
БД остаётся clinic_tests.
E1.0 — База Flask-приложения: SQLAlchemy/psycopg2 пул, Flask sessions,
фабрика create_app, blueprint main с / и /health, base.html в стиле
кабинета HR (Tailwind CDN + Manrope + Material Symbols), 404/500.
E1.1 — Auth + /api/me: Flask sessions (signed cookie) вместо JWT,
bcrypt + Werkzeug, опц. HR_AUTH=1 с UPSERT в clinic_tests.users по
staff_id. UI /login, JSON /api/auth/{login,logout,me}, декораторы
@login_required / @require_role.
E1.2 — Тесты: список + редактор. 10 эндпоинтов, сервисы test_draft,
test_access, test_chain, ai_editor, llm_client, draft_validator,
editor_content. UI /tests (каталог + создание) и /tests/<id>/edit
(редактор с AI). Полный мобильный UX (аккордеоны/drag-n-drop) — в E1.7.
E1.3 — Импорт документов: pypdf + python-docx, эндпоинт
POST /api/tests/import/document, кнопка «Импорт документа» в
AI-панели редактора, лимит 16 МБ.
E1.8 — AI v2: страница /settings (статус ENV-ключа + ping),
ai/generate-by-title (без сетки), ai/check (рецензия), ai/improve
(массовое было→стало с чекбоксами). Унифицированный ответ AI-ошибок:
{ error, code, settingsUrl }.
Docker:
- docker-compose.dev.yml: добавлены DATABASE_URL, HR_AUTH/HR_DATABASE_URL,
DEEPSEEK_API_KEY/OPENAI_API_KEY/LLM_BASE_URL/LLM_MODEL и сеть postgres
для testing-flask.
Документация:
- docs/migration-final.md — двух-этапный план (Этап 1: унификация
стека внутри TestingWebApp; Этап 2: слияние с tgFlaskForm).
- docs/migration-final-inventory.md — карта 22 эндпоинтов Express.
Made-with: Cursor
192 lines
9.4 KiB
HTML
192 lines
9.4 KiB
HTML
{% extends "base.html" %}
|
||
{% block title %}{{ content.test.title }} — редактор{% endblock %}
|
||
|
||
{% block content %}
|
||
<div id="editor-root"
|
||
data-test-id="{{ test_id }}"
|
||
data-initial='{{ content | tojson | safe }}'>
|
||
<!-- Шапка: название/описание/проходной балл -->
|
||
<section class="rounded-2xl bg-white shadow-sm border border-ink-300/60 p-5">
|
||
<div class="flex items-start justify-between gap-3 flex-wrap">
|
||
<div class="flex-1 min-w-[260px]">
|
||
<label class="block">
|
||
<span class="text-xs font-medium text-ink-500 uppercase">Название</span>
|
||
<input id="test-title" type="text" maxlength="200"
|
||
class="mt-1 w-full rounded-lg border border-ink-300 px-3 py-2 text-lg font-semibold
|
||
focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20" />
|
||
</label>
|
||
<label class="block mt-3">
|
||
<span class="text-xs font-medium text-ink-500 uppercase">Описание</span>
|
||
<textarea id="test-description" rows="2"
|
||
class="mt-1 w-full rounded-lg border border-ink-300 px-3 py-2
|
||
focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20"></textarea>
|
||
</label>
|
||
</div>
|
||
<div class="w-44">
|
||
<label class="block">
|
||
<span class="text-xs font-medium text-ink-500 uppercase">Проходной балл, %</span>
|
||
<input id="test-threshold" type="number" min="0" max="100" step="1"
|
||
class="mt-1 w-full rounded-lg border border-ink-300 px-3 py-2
|
||
focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20" />
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- AI-панель -->
|
||
<section class="mt-4 rounded-2xl bg-brand-50/60 border border-brand-100 p-4">
|
||
<div class="flex items-center gap-2">
|
||
<span class="material-symbols-outlined text-brand-600">auto_awesome</span>
|
||
<h2 class="font-semibold text-brand-700">AI-помощник</h2>
|
||
</div>
|
||
<p class="mt-1 text-sm text-ink-700">
|
||
Сгенерировать вопросы по текущей сетке (число вопросов и вариантов берётся из таблицы ниже).
|
||
</p>
|
||
<div class="mt-3 flex flex-wrap gap-2 items-center">
|
||
<button id="ai-generate-test"
|
||
class="inline-flex items-center gap-2 px-3 py-2 rounded-lg
|
||
bg-brand-600 hover:bg-brand-700 text-white text-sm">
|
||
<span class="material-symbols-outlined text-base">stars</span>
|
||
Сгенерировать по сетке
|
||
</button>
|
||
<button id="ai-generate-by-title"
|
||
class="inline-flex items-center gap-2 px-3 py-2 rounded-lg
|
||
bg-white border border-brand-300/60 text-brand-700 hover:bg-brand-50 text-sm">
|
||
<span class="material-symbols-outlined text-base">edit_note</span>
|
||
Сгенерировать по названию
|
||
</button>
|
||
<button id="ai-check"
|
||
class="inline-flex items-center gap-2 px-3 py-2 rounded-lg
|
||
bg-white border border-ink-300/60 hover:border-brand-300 text-sm">
|
||
<span class="material-symbols-outlined text-base">fact_check</span>
|
||
Проверить тест
|
||
</button>
|
||
<button id="ai-improve"
|
||
class="inline-flex items-center gap-2 px-3 py-2 rounded-lg
|
||
bg-white border border-ink-300/60 hover:border-brand-300 text-sm">
|
||
<span class="material-symbols-outlined text-base">tune</span>
|
||
Улучшить тест
|
||
</button>
|
||
<label class="inline-flex items-center gap-2 px-3 py-2 rounded-lg
|
||
bg-white border border-ink-300/60 hover:border-brand-300 text-sm cursor-pointer">
|
||
<span class="material-symbols-outlined text-base text-brand-600">upload_file</span>
|
||
<span>Импорт документа</span>
|
||
<input id="ai-import-file" type="file" accept=".pdf,.docx,.txt,.md" class="hidden" />
|
||
</label>
|
||
<span id="ai-status" class="text-sm text-ink-500"></span>
|
||
</div>
|
||
<p class="mt-2 text-xs text-ink-500">
|
||
Поддерживаются PDF, DOCX, TXT, MD (до 16 МБ). AI извлечёт текст и предложит черновик теста.
|
||
</p>
|
||
</section>
|
||
|
||
<!-- Список вопросов -->
|
||
<section class="mt-4">
|
||
<div class="flex items-center justify-between gap-2 px-1">
|
||
<h2 class="font-semibold">Вопросы (<span id="q-count">0</span>)</h2>
|
||
<button id="add-question"
|
||
class="inline-flex items-center gap-1 px-3 py-1.5 rounded-lg
|
||
bg-white border border-ink-300/60 hover:border-brand-300 text-sm">
|
||
<span class="material-symbols-outlined text-base">add</span>
|
||
Добавить вопрос
|
||
</button>
|
||
</div>
|
||
<ol id="questions" class="mt-3 space-y-3"></ol>
|
||
</section>
|
||
|
||
<!-- Footer: сохранение / активность цепочки -->
|
||
<section class="sticky bottom-0 z-20 mt-6 -mx-4 px-4 py-3
|
||
bg-white/90 backdrop-blur border-t border-ink-300/60
|
||
flex items-center justify-between gap-2 flex-wrap">
|
||
<label class="inline-flex items-center gap-2 text-sm">
|
||
<input id="chain-active" type="checkbox"
|
||
class="rounded border-ink-300 text-brand-600 focus:ring-brand-500" />
|
||
<span>Цепочка активна (виден в каталоге)</span>
|
||
</label>
|
||
<div class="flex items-center gap-2">
|
||
<span id="save-status" class="text-sm text-ink-500"></span>
|
||
<a href="{{ url_for('tests.tests_list_page') }}"
|
||
class="px-3 py-2 rounded-lg text-ink-700 hover:bg-ink-100 text-sm">К каталогу</a>
|
||
<button id="save-draft"
|
||
class="inline-flex items-center gap-2 px-4 py-2 rounded-lg
|
||
bg-brand-600 hover:bg-brand-700 text-white">
|
||
<span class="material-symbols-outlined text-base">save</span>
|
||
Сохранить
|
||
</button>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
|
||
<!-- Шаблон вопроса -->
|
||
<template id="tpl-question">
|
||
<li class="rounded-xl bg-white border border-ink-300/60 p-4 q-item">
|
||
<div class="flex items-start justify-between gap-2">
|
||
<span class="text-xs uppercase tracking-wide text-ink-500 q-num">Вопрос #</span>
|
||
<div class="flex items-center gap-1">
|
||
<button class="q-up p-1 rounded hover:bg-ink-100" title="Выше">
|
||
<span class="material-symbols-outlined text-base">arrow_upward</span>
|
||
</button>
|
||
<button class="q-down p-1 rounded hover:bg-ink-100" title="Ниже">
|
||
<span class="material-symbols-outlined text-base">arrow_downward</span>
|
||
</button>
|
||
<button class="q-delete p-1 rounded hover:bg-red-50 text-red-600" title="Удалить">
|
||
<span class="material-symbols-outlined text-base">delete</span>
|
||
</button>
|
||
</div>
|
||
</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-2 flex items-center justify-between gap-2 flex-wrap text-sm">
|
||
<label class="inline-flex items-center gap-2">
|
||
<input type="checkbox" class="q-multi rounded border-ink-300 text-brand-600 focus:ring-brand-500" />
|
||
<span>Несколько правильных ответов</span>
|
||
</label>
|
||
<button class="q-ai inline-flex items-center gap-1 px-2 py-1 rounded-lg
|
||
bg-brand-50 hover:bg-brand-100 text-brand-700">
|
||
<span class="material-symbols-outlined text-base">auto_awesome</span>
|
||
AI: вопрос/переформулировать
|
||
</button>
|
||
</div>
|
||
|
||
<ul class="q-options mt-3 space-y-2"></ul>
|
||
<div class="mt-2">
|
||
<button class="q-add-option inline-flex items-center gap-1 text-sm text-brand-700 hover:underline">
|
||
<span class="material-symbols-outlined text-base">add</span> Добавить вариант
|
||
</button>
|
||
</div>
|
||
</li>
|
||
</template>
|
||
|
||
<!-- Модалка результата AI-проверки/улучшения -->
|
||
<dialog id="ai-modal" class="rounded-2xl p-0 max-w-3xl w-full backdrop:bg-black/40">
|
||
<div class="p-5">
|
||
<div class="flex items-center justify-between gap-3">
|
||
<h3 id="ai-modal-title" class="text-lg font-semibold">AI</h3>
|
||
<button id="ai-modal-close" class="p-1 rounded hover:bg-ink-100">
|
||
<span class="material-symbols-outlined">close</span>
|
||
</button>
|
||
</div>
|
||
<div id="ai-modal-body" class="mt-3 max-h-[70vh] overflow-y-auto"></div>
|
||
<div id="ai-modal-actions" class="mt-4 flex items-center justify-end gap-2"></div>
|
||
</div>
|
||
</dialog>
|
||
|
||
<template id="tpl-option">
|
||
<li class="flex items-center gap-2 opt-item">
|
||
<input type="checkbox" class="opt-correct rounded border-ink-300 text-brand-600 focus:ring-brand-500" />
|
||
<input type="text" class="opt-text flex-1 rounded-lg border border-ink-300 px-3 py-1.5
|
||
focus:border-brand-500 focus:ring-2 focus:ring-brand-500/20"
|
||
placeholder="Вариант ответа" />
|
||
<button class="opt-delete p-1 rounded hover:bg-red-50 text-red-600" title="Удалить">
|
||
<span class="material-symbols-outlined text-base">close</span>
|
||
</button>
|
||
</li>
|
||
</template>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script src="{{ url_for('static', filename='js/editor.js') }}"></script>
|
||
{% endblock %}
|