Спринт 4: AI-помощник на базе DeepSeek
- Страница /settings: ввод и проверка API ключа DeepSeek - POST /api/llm/generate — генерация вопросов по названию теста - POST /api/llm/improve — улучшение формулировки вопроса + ответов (модал с галочками) - POST /api/llm/distractors — генерация дистракторов - POST /api/llm/review — рецензия теста + кнопка «Предложить вариант» - POST /api/llm/improve_all — улучшение всего теста с постатейным сравнением - Миграция 004: таблица settings (key-value) - Шапка приложения с навигацией на /settings Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 80 }}>
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 600, margin: '40px auto', padding: '0 24px' }}>
|
||||
<Title level={2}>Настройки</Title>
|
||||
|
||||
<Card title="AI-помощник (DeepSeek)">
|
||||
<Text type="secondary" style={{ display: 'block', marginBottom: 16 }}>
|
||||
Введите API ключ DeepSeek для активации AI-функций при создании и редактировании
|
||||
тестов. Ключ хранится только на сервере.
|
||||
</Text>
|
||||
|
||||
<Form layout="vertical" onFinish={handleSave} initialValues={{ api_key: setting?.value ?? '' }}>
|
||||
<Form.Item
|
||||
name="api_key"
|
||||
label="API ключ DeepSeek"
|
||||
>
|
||||
<Input.Password
|
||||
placeholder="sk-..."
|
||||
visibilityToggle
|
||||
style={{ fontFamily: 'monospace' }}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item style={{ marginBottom: 0 }}>
|
||||
<Space wrap>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={saveMutation.isPending}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => checkMutation.mutate()}
|
||||
loading={checkMutation.isPending}
|
||||
disabled={!setting?.value}
|
||||
>
|
||||
Проверить подключение
|
||||
</Button>
|
||||
<Button onClick={() => navigate('/')}>
|
||||
На главную
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
{saveMutation.isSuccess && (
|
||||
<Alert
|
||||
type="success"
|
||||
message="Ключ сохранён"
|
||||
showIcon
|
||||
style={{ marginTop: 16 }}
|
||||
closable
|
||||
/>
|
||||
)}
|
||||
|
||||
{checkResult && (
|
||||
<Alert
|
||||
type={checkResult.ok ? 'success' : 'error'}
|
||||
message={checkResult.message}
|
||||
icon={
|
||||
checkResult.ok ? (
|
||||
<CheckCircleOutlined />
|
||||
) : (
|
||||
<CloseCircleOutlined />
|
||||
)
|
||||
}
|
||||
showIcon
|
||||
style={{ marginTop: 16 }}
|
||||
closable
|
||||
onClose={() => setCheckResult(null)}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user