Files
TestingWebApp/flask_app/app/templates/tests/editor.html
T
Константин Лебединский 4b0d56ff0e feat(flask): E1.0–E1.3, E1.8 — миграция на Python/Flask + AI v2
Этап 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
2026-04-27 23:29:26 +05:00

192 lines
9.4 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% 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 %}