+ {contextHolder}
+
{onBack && (
-
}
- onClick={onBack}
- style={{ marginBottom: 16 }}
- >
+
} onClick={onBack} style={{ marginBottom: 16 }}>
{backLabel}
)}
@@ -129,6 +363,31 @@ export default function TestForm({
+ {/* ── AI: кнопки ── */}
+
prev.title !== cur.title}
+ >
+ {({ getFieldValue }) => (
+
+ }
+ onClick={handleGenerate}
+ loading={generateLoading}
+ disabled={!getFieldValue('title')?.trim()}
+ >
+ Сгенерировать с AI
+
+ }
+ onClick={handleReview}
+ >
+ Проверить тест
+
+
+ )}
+
+
{/* ── Вопросы ── */}
7 ? (
- removeQuestion(qName)}
- />
- ) : null
+
+ }
+ loading={improveState.loading && improveState.qIndex === index}
+ onClick={() => handleImprove(index)}
+ title="Улучшить формулировку"
+ >
+ Улучшить
+
+ }
+ loading={distractorsLoading[index]}
+ onClick={() => handleDistractors(index)}
+ title="Добавить дистракторы"
+ >
+ Дистракторы
+
+ {questionFields.length > 7 && (
+ removeQuestion(qName)}
+ />
+ )}
+
}
style={{ marginBottom: 16 }}
>
@@ -263,6 +542,327 @@ export default function TestForm({
+
+ {/* ── Modal: Превью сгенерированных вопросов ── */}
+ Сгенерированные вопросы>}
+ open={previewOpen}
+ onCancel={handleGenerateClose}
+ footer={null}
+ width={640}
+ >
+ {generateLoading && (
+
+
+
AI генерирует вопросы...
+
+ )}
+
+ {!generateLoading && previewQuestions && (
+ <>
+
+ Сгенерировано {previewQuestions.length} вопросов. Нажмите «Применить», чтобы
+ заменить вопросы в форме.
+
+
+ {previewQuestions.map((q, i) => (
+
+
+ {i + 1}. {q.text}
+
+
+ {q.answers.map((a, ai) => (
+ -
+ {a.text} {a.is_correct && '✓'}
+
+ ))}
+
+
+ ))}
+
+
+
+
+
+
+ >
+ )}
+
+
+ {/* ── Modal: Улучшение вопроса ── */}
+
+ Улучшение вопроса{' '}
+ {improveState.qIndex !== null ? improveState.qIndex + 1 : ''}
+ >
+ }
+ open={improveState.open}
+ onCancel={() => setImproveState((prev) => ({ ...prev, open: false }))}
+ width={680}
+ footer={
+ improveState.improved ? (
+
+
+
+
+ ) : null
+ }
+ >
+ {improveState.loading && (
+
+
+
AI улучшает формулировки...
+
+ )}
+
+ {!improveState.loading && improveState.improved && improveState.original && (
+ <>
+ {/* Вопрос */}
+
+
Текст вопроса
+
+
+
+
+ Текущая
+
+
{improveState.original.text}
+
+
+
+ AI предлагает
+
+
{improveState.improved.question}
+
+
+
+ setImproveState((prev) => ({ ...prev, applyQuestion: e.target.checked }))
+ }
+ >
+ Применить
+
+
+
+
+ {/* Ответы */}
+ Варианты ответов
+
+ {improveState.original.answers.map((answer, i) => (
+
+
+
+
+ Вариант {i + 1}{answer.is_correct ? ' (правильный ✓)' : ''}
+
+
{answer.text}
+
+
+
+ AI предлагает
+
+
+ {improveState.improved.answers[i] ?? answer.text}
+
+
+
+
{
+ const next = [...improveState.applyAnswers]
+ next[i] = e.target.checked
+ setImproveState((prev) => ({ ...prev, applyAnswers: next }))
+ }}
+ >
+ Применить
+
+
+ ))}
+
+ >
+ )}
+
+
+ {/* ── Modal: Проверка теста + улучшение всего теста ── */}
+ Предложения по улучшению теста>
+ : <> Рекомендации AI>
+ }
+ open={reviewOpen}
+ onCancel={() => { setReviewOpen(false); setImproveAllData(null) }}
+ width={700}
+ footer={
+ improveAllData ? (
+
+
+
+
+
+ ) : (
+
+ {!reviewLoading && reviewText && (
+ }
+ loading={improveAllLoading}
+ onClick={handleImproveAll}
+ >
+ Предложить вариант
+
+ )}
+
+
+ )
+ }
+ >
+ {/* Режим 1: рекомендации */}
+ {!improveAllData && (
+ reviewLoading ? (
+
+
+
AI анализирует тест...
+
+ ) : improveAllLoading ? (
+
+
+
AI готовит улучшенный вариант...
+
+ ) : (
+ {reviewText}
+ )
+ )}
+
+ {/* Режим 2: сравнение старого и нового */}
+ {improveAllData && (
+
+ {improveAllData.original.map((origQ, qi) => (
+
+ {/* Заголовок вопроса */}
+
+
+
+ Вопрос {qi + 1} — текущий
+
+
{origQ.text}
+
+ AI предлагает
+
+
+ {improveAllData.improved[qi]?.question ?? origQ.text}
+
+
+
{
+ const next = [...improveAllData.applyQuestions]
+ next[qi] = e.target.checked
+ setImproveAllData((prev) => prev && { ...prev, applyQuestions: next })
+ }}
+ >
+ Применить
+
+
+
+ {/* Ответы */}
+ {origQ.answers.map((origA, ai) => (
+
+
+
+ Вариант {ai + 1}{origA.is_correct ? ' (правильный ✓)' : ''}
+
+
{origA.text}
+
AI предлагает
+
+ {improveAllData.improved[qi]?.answers[ai] ?? origA.text}
+
+
+
{
+ const next = improveAllData.applyAnswers.map((row) => [...row])
+ next[qi][ai] = e.target.checked
+ setImproveAllData((prev) => prev && { ...prev, applyAnswers: next })
+ }}
+ >
+ Применить
+
+
+ ))}
+
+ ))}
+
+ )}
+
)
}
diff --git a/frontend/src/pages/Settings/index.tsx b/frontend/src/pages/Settings/index.tsx
new file mode 100644
index 0000000..e3f5d86
--- /dev/null
+++ b/frontend/src/pages/Settings/index.tsx
@@ -0,0 +1,124 @@
+import { CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
+import { Alert, Button, Card, Form, Input, Space, Spin, Typography } from 'antd'
+import { useState } from 'react'
+import { useNavigate } from 'react-router-dom'
+
+import llmApi from '../../api/llm'
+import settingsApi from '../../api/settings'
+
+const { Title, Text } = Typography
+
+const API_KEY = 'deepseek_api_key'
+
+export default function Settings() {
+ const navigate = useNavigate()
+ const queryClient = useQueryClient()
+ const [checkResult, setCheckResult] = useState<{ ok: boolean; message: string } | null>(null)
+
+ const { data: setting, isLoading } = useQuery({
+ queryKey: ['settings', API_KEY],
+ queryFn: () => settingsApi.get(API_KEY),
+ })
+
+ const saveMutation = useMutation({
+ mutationFn: (value: string) => settingsApi.update(API_KEY, value || null),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ['settings', API_KEY] })
+ setCheckResult(null)
+ },
+ })
+
+ const checkMutation = useMutation({
+ mutationFn: () => llmApi.check(),
+ onSuccess: (data) => setCheckResult(data),
+ })
+
+ const handleSave = (values: { api_key: string }) => {
+ saveMutation.mutate(values.api_key)
+ }
+
+ if (isLoading) {
+ return (
+