init + api use
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
import {
|
||||
Box,
|
||||
Heading,
|
||||
Button,
|
||||
Input,
|
||||
Textarea,
|
||||
VStack,
|
||||
HStack,
|
||||
Text,
|
||||
Flex,
|
||||
Stack,
|
||||
Field,
|
||||
Tabs,
|
||||
} from '@chakra-ui/react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import {
|
||||
useGetTaskQuery,
|
||||
useCreateTaskMutation,
|
||||
useUpdateTaskMutation,
|
||||
} from '../../__data__/api/api'
|
||||
import { URLs } from '../../__data__/urls'
|
||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||
import { ErrorAlert } from '../../components/ErrorAlert'
|
||||
import { toaster } from '../../components/ui/toaster'
|
||||
|
||||
export const TaskFormPage: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const navigate = useNavigate()
|
||||
const isEdit = !!id
|
||||
|
||||
const { data: task, isLoading: isLoadingTask, error: loadError } = useGetTaskQuery(id!, {
|
||||
skip: !id,
|
||||
})
|
||||
const [createTask, { isLoading: isCreating }] = useCreateTaskMutation()
|
||||
const [updateTask, { isLoading: isUpdating }] = useUpdateTaskMutation()
|
||||
|
||||
const [title, setTitle] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [hiddenInstructions, setHiddenInstructions] = useState('')
|
||||
const [showDescPreview, setShowDescPreview] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (task) {
|
||||
setTitle(task.title)
|
||||
setDescription(task.description)
|
||||
setHiddenInstructions(task.hiddenInstructions || '')
|
||||
}
|
||||
}, [task])
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
if (!title.trim() || !description.trim()) {
|
||||
toaster.create({
|
||||
title: 'Ошибка валидации',
|
||||
description: 'Заполните обязательные поля',
|
||||
type: 'error',
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (isEdit && id) {
|
||||
await updateTask({
|
||||
id,
|
||||
data: {
|
||||
title: title.trim(),
|
||||
description: description.trim(),
|
||||
hiddenInstructions: hiddenInstructions.trim() || undefined,
|
||||
},
|
||||
}).unwrap()
|
||||
toaster.create({
|
||||
title: 'Успешно',
|
||||
description: 'Задание обновлено',
|
||||
type: 'success',
|
||||
})
|
||||
} else {
|
||||
await createTask({
|
||||
title: title.trim(),
|
||||
description: description.trim(),
|
||||
hiddenInstructions: hiddenInstructions.trim() || undefined,
|
||||
}).unwrap()
|
||||
toaster.create({
|
||||
title: 'Успешно',
|
||||
description: 'Задание создано',
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
navigate(URLs.tasks)
|
||||
} catch (err: any) {
|
||||
toaster.create({
|
||||
title: 'Ошибка',
|
||||
description: err?.data?.error?.message || 'Не удалось сохранить задание',
|
||||
type: 'error',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (isEdit && isLoadingTask) {
|
||||
return <LoadingSpinner message="Загрузка задания..." />
|
||||
}
|
||||
|
||||
if (isEdit && loadError) {
|
||||
return <ErrorAlert message="Не удалось загрузить задание" />
|
||||
}
|
||||
|
||||
const isLoading = isCreating || isUpdating
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Heading mb={6}>{isEdit ? 'Редактировать задание' : 'Создать задание'}</Heading>
|
||||
|
||||
<Box
|
||||
as="form"
|
||||
onSubmit={handleSubmit}
|
||||
bg="white"
|
||||
p={6}
|
||||
borderRadius="lg"
|
||||
boxShadow="sm"
|
||||
borderWidth="1px"
|
||||
borderColor="gray.200"
|
||||
>
|
||||
<VStack gap={6} align="stretch">
|
||||
{/* Title */}
|
||||
<Field.Root required>
|
||||
<Field.Label>Название задания</Field.Label>
|
||||
<Input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Введите название задания"
|
||||
maxLength={255}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<Field.HelperText>Максимум 255 символов</Field.HelperText>
|
||||
</Field.Root>
|
||||
|
||||
{/* Description with Markdown */}
|
||||
<Field.Root required>
|
||||
<Field.Label>Описание (Markdown)</Field.Label>
|
||||
<Tabs.Root
|
||||
value={showDescPreview ? 'preview' : 'editor'}
|
||||
onValueChange={(e) => setShowDescPreview(e.value === 'preview')}
|
||||
>
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="editor">Редактор</Tabs.Trigger>
|
||||
<Tabs.Trigger value="preview">Превью</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="editor" pt={4}>
|
||||
<Textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="# Заголовок задания Описание задания в формате Markdown..."
|
||||
rows={15}
|
||||
fontFamily="monospace"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="preview" pt={4}>
|
||||
<Box
|
||||
p={4}
|
||||
borderWidth="1px"
|
||||
borderColor="gray.200"
|
||||
borderRadius="md"
|
||||
minH="300px"
|
||||
bg="gray.50"
|
||||
>
|
||||
{description ? (
|
||||
<Box className="markdown-preview">
|
||||
<ReactMarkdown>{description}</ReactMarkdown>
|
||||
</Box>
|
||||
) : (
|
||||
<Text color="gray.400" fontStyle="italic">
|
||||
Предпросмотр появится здесь...
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
<Field.HelperText>Используйте Markdown для форматирования текста</Field.HelperText>
|
||||
</Field.Root>
|
||||
|
||||
{/* Hidden Instructions */}
|
||||
<Field.Root>
|
||||
<Box bg="purple.50" p={4} borderRadius="md" borderWidth="1px" borderColor="purple.200">
|
||||
<HStack mb={2}>
|
||||
<Text fontWeight="bold" color="purple.800">
|
||||
🔒 Скрытые инструкции для LLM
|
||||
</Text>
|
||||
</HStack>
|
||||
<Text fontSize="sm" color="purple.700" mb={3}>
|
||||
Эти инструкции будут переданы LLM при проверке решений студентов. Студенты их не
|
||||
увидят.
|
||||
</Text>
|
||||
<Textarea
|
||||
value={hiddenInstructions}
|
||||
onChange={(e) => setHiddenInstructions(e.target.value)}
|
||||
placeholder="Например: Проверь, что сложность алгоритма O(n log n). Код должен обрабатывать edge cases..."
|
||||
rows={6}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<Field.HelperText>
|
||||
Опционально. Используйте для тонкой настройки проверки LLM.
|
||||
</Field.HelperText>
|
||||
</Box>
|
||||
</Field.Root>
|
||||
|
||||
{/* Meta info for edit mode */}
|
||||
{isEdit && task && (
|
||||
<Box p={4} bg="gray.50" borderRadius="md">
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
<strong>Создано:</strong>{' '}
|
||||
{new Date(task.createdAt).toLocaleString('ru-RU')}
|
||||
</Text>
|
||||
{task.creator && (
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
<strong>Автор:</strong> {task.creator.preferred_username}
|
||||
</Text>
|
||||
)}
|
||||
{task.updatedAt !== task.createdAt && (
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
<strong>Обновлено:</strong>{' '}
|
||||
{new Date(task.updatedAt).toLocaleString('ru-RU')}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<HStack gap={3} justify="flex-end">
|
||||
<Button variant="outline" onClick={() => navigate(URLs.tasks)} disabled={isLoading}>
|
||||
Отмена
|
||||
</Button>
|
||||
<Button type="submit" colorPalette="teal" loading={isLoading}>
|
||||
{isEdit ? 'Сохранить изменения' : 'Создать задание'}
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user