docs: миграция на tgFlaskForm и производительность Flask; контур flask_app; UI без лишних описаний

Made-with: Cursor
This commit is contained in:
Константин Лебединский
2026-04-27 18:43:51 +05:00
parent 47279c72e3
commit b3e3757a92
20 changed files with 680 additions and 115 deletions
+5 -78
View File
@@ -91,7 +91,6 @@ export default function TestDetail() {
const [assignClinic, setAssignClinic] = useState('all');
const [assignPeople, setAssignPeople] = useState([]);
const [assignDepts, setAssignDepts] = useState([]);
const [assignSource, setAssignSource] = useState('');
const [assignSelected, setAssignSelected] = useState(() => new Set());
const [assignMsg, setAssignMsg] = useState('');
const [assignErr, setAssignErr] = useState(null);
@@ -198,7 +197,6 @@ export default function TestDetail() {
}
setAssignPeople(r.people || []);
setAssignDepts(r.departments || []);
setAssignSource(r.source || '');
setAssignSelected(new Set());
} catch (e) {
if (!cancelled) {
@@ -261,8 +259,8 @@ export default function TestDetail() {
});
setAssignMsg(
out.count != null
? `Создано назначение на ${out.count} сотр. (dev).`
: 'Назначение в БД создано (dev).'
? `Назначено: ${out.count} сотр.`
: 'Назначение сохранено.'
);
setAssignSelected(new Set());
} catch (e) {
@@ -295,11 +293,7 @@ export default function TestDetail() {
questions,
}),
});
setDraftStatus(
out.forked
? 'Создана новая версия (вилка) и применён черновик'
: 'Черновик применён на месте'
);
setDraftStatus(out.forked ? 'Сохранено как новая версия' : 'Сохранено');
load();
} catch (e) {
setDraftStatus(e.message);
@@ -582,15 +576,6 @@ export default function TestDetail() {
<p className="text-muted" style={{ marginTop: 0, marginBottom: '0.4rem', fontSize: 14 }}>
{formatTestAuthorLabel(user, t?.createdBy, t?.authorFullName)}
</p>
<p className="muted" style={{ marginTop: 0, marginBottom: '0.5rem' }}>
Режим сотрудника: одна цепочка одна активная версия (v{t?.version ?? '—'}
{t?.activeVersionId && (
<span className="code-inline" style={{ marginLeft: 6, fontSize: '0.8rem' }}>
· {String(t.activeVersionId).slice(0, 8)}
</span>
)}
)
</p>
{t?.description && (
<p className="text-muted" style={{ marginTop: 0, marginBottom: '1rem' }}>
{t.description}
@@ -604,11 +589,6 @@ export default function TestDetail() {
Активная версия недоступна. Обратитесь к автору теста.
</div>
)}
<p className="text-muted" style={{ marginTop: '1.25rem' }}>
Пройдите тест в{' '}
<Link to="/tests">каталоге</Link> в строке с названием слева откроется карточка, справа
кнопка «Пройти».
</p>
</div>
);
}
@@ -645,13 +625,6 @@ export default function TestDetail() {
{aiTestBusy ? 'Генерация…' : 'Сгенерировать тест (ИИ)'}
</button>
</div>
<p className="text-muted" style={{ marginTop: 0, marginBottom: '0.75rem', fontSize: 13 }}>
Заполняет все вопросы и варианты по <strong>текущей структуре</strong> (число карточек
вопросов и вариантов в каждой задайте кнопками «+ вопрос» / «+ вариант»). Верные ответы
отмечает модель. Нужен ключ в backend:{' '}
<code className="code-inline">DEEPSEEK_API_KEY</code> или{' '}
<code className="code-inline">OPENAI_API_KEY</code>.
</p>
{test?.description && (
<p className="text-muted" style={{ marginTop: 0, marginBottom: '1rem' }}>
{test.description}
@@ -668,17 +641,13 @@ export default function TestDetail() {
{test?.chainActive === false && (
<div className="callout callout--warning" role="status" style={{ marginTop: '0.75rem' }}>
Эта цепочка <strong>не показывается</strong> в верхнем списке на «Тесты».
Карточку всегда можно открыть из раздела <strong>«Скрытые вами из списка»</strong>{' '}
на той же странице или по закладке с адресом карточки. Снова включить
отображение в блоке «Публикация»; данные не удаляются.
Скрыт из общего списка.
</div>
)}
{chain?.hasAnyAttempt && (
<div className="callout callout--warning" role="status" style={{ marginTop: '0.75rem' }}>
Уже были попытки по этой цепочке. Сохранение черновика с вопросами создаст{' '}
<strong>новую версию</strong> (см. V.1V.3); старая останется в истории.
При сохранении будет создана новая версия теста.
</div>
)}
{data && attemptsErr && (
@@ -689,10 +658,6 @@ export default function TestDetail() {
</div>
<AccSection title="История версий" defaultOpen={false}>
<p className="text-muted" style={{ marginTop: 0, marginBottom: '0.75rem' }}>
Активная та, по которой сейчас идут новые попытки. Можно вручную сделать
активной другую строку.
</p>
<div className="surface-card" style={{ padding: 0, overflow: 'hidden' }}>
<table className="table-cabinet">
<thead>
@@ -735,20 +700,10 @@ export default function TestDetail() {
</tbody>
</table>
</div>
{hasAttempts && (
<p className="text-muted" style={{ marginTop: '0.75rem', marginBottom: 0 }}>
По цепочке есть зафиксированные прогоны разбор идёт по той версии, с
которой проходили.
</p>
)}
</AccSection>
{attemptsList != null && attemptsList.length > 0 && (
<AccSection title="Прогоны и разбор" defaultOpen={false}>
<p className="text-muted" style={{ marginTop: 0, marginBottom: '0.75rem' }}>
Попытки по всем версиям цепочки. Подробный разбор вариантов для завершённых
прогонов.
</p>
{attemptsErr && (
<p className="error-text" role="alert">
{attemptsErr}
@@ -813,10 +768,6 @@ export default function TestDetail() {
)}
<AccSection title="Публикация (видимость в списке)" defaultOpen={false}>
<p className="text-muted" style={{ marginTop: 0 }}>
Деактивация убирает тест из общего списка; карточка открывается по прямой
ссылке.
</p>
<div className="inline-actions" style={{ marginTop: '0.5rem' }}>
{test?.chainActive !== false ? (
<button
@@ -841,12 +792,6 @@ export default function TestDetail() {
</AccSection>
<AccSection title="Импорт из файла" defaultOpen={false}>
<p className="text-muted" style={{ marginTop: 0, marginBottom: '0.5rem' }}>
PDF, DOCX, TXT/MD, до 10 МБ. Текст извлекается на сервере; при{' '}
<code className="code-inline">DEEPSEEK_API_KEY</code> или{' '}
<code className="code-inline">OPENAI_API_KEY</code> в backend строится черновик теста
(см. <code className="code-inline">generation.draft</code>).
</p>
<div className="inline-actions" style={{ marginBottom: '0.5rem' }}>
<input
type="file"
@@ -909,12 +854,6 @@ export default function TestDetail() {
</AccSection>
<AccSection title="Содержание: название, порог, вопросы" defaultOpen>
<p className="text-muted" style={{ marginTop: 0, marginBottom: '0.75rem' }}>
Редактируется активная версия. Сохранение отправляет вопросы на сервер; при уже
существующих попытках по цепочке создаётся новая версия с копией контента.
Порог зачёта в таблице <code className="code-inline">tests</code>, на все
версии цепочки.
</p>
<div className="draft-block">
<label className="form-label" htmlFor="draft-title">
Название
@@ -938,9 +877,6 @@ export default function TestDetail() {
<label className="form-label" htmlFor="draft-pass" style={{ marginTop: '0.75rem' }}>
Порог зачёта, %
</label>
<p className="text-muted" style={{ marginTop: 0, marginBottom: 6, fontSize: 13 }}>
Минимум правильных ответов (в процентах) для «зачёта»; по умолчанию в БД 70.
</p>
<input
id="draft-pass"
type="number"
@@ -977,10 +913,6 @@ export default function TestDetail() {
{aiQBusy === qi ? '…' : 'Сгенерировать вопрос (ИИ)'}
</button>
</div>
<p className="text-muted" style={{ marginTop: 0, marginBottom: 6, fontSize: 12 }}>
Пустое поле вопрос и все варианты по числу кнопок; непустое сформулировать
нормальным языком (13 предложения).
</p>
<textarea
id={`qtext-${q.key}`}
className="form-input"
@@ -1113,11 +1045,6 @@ export default function TestDetail() {
{assignmentUi && data && (
<AccSection title="Назначение сотрудникам" defaultOpen={false}>
<p className="text-muted" style={{ marginTop: 0, marginBottom: '0.75rem' }}>
Сотрудники из HR; при отсутствии учётки в модуле она создаётся по{' '}
<code className="code-inline">staff_id</code>. Источник данных:{' '}
<strong>{assignSource === 'hr' ? 'HR (все, с отделами)' : 'только clinic_tests'}</strong>.
</p>
{assignErr && (
<p className="error-text" role="alert">
{assignErr}