init + api use
This commit is contained in:
340
src/pages/submissions/SubmissionsPage.tsx
Normal file
340
src/pages/submissions/SubmissionsPage.tsx
Normal file
@@ -0,0 +1,340 @@
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
Box,
|
||||
Heading,
|
||||
Table,
|
||||
Input,
|
||||
Text,
|
||||
Button,
|
||||
HStack,
|
||||
VStack,
|
||||
Select,
|
||||
DialogRoot,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogBody,
|
||||
DialogFooter,
|
||||
DialogActionTrigger,
|
||||
createListCollection,
|
||||
} from '@chakra-ui/react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import { useGetAllSubmissionsQuery } from '../../__data__/api/api'
|
||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||
import { ErrorAlert } from '../../components/ErrorAlert'
|
||||
import { EmptyState } from '../../components/EmptyState'
|
||||
import { StatusBadge } from '../../components/StatusBadge'
|
||||
import type { ChallengeSubmission, SubmissionStatus, ChallengeTask, ChallengeUser } from '../../types/challenge'
|
||||
|
||||
export const SubmissionsPage: React.FC = () => {
|
||||
const { data: submissions, isLoading, error, refetch } = useGetAllSubmissionsQuery()
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState<SubmissionStatus | 'all'>('all')
|
||||
const [selectedSubmission, setSelectedSubmission] = useState<ChallengeSubmission | null>(null)
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingSpinner message="Загрузка попыток..." />
|
||||
}
|
||||
|
||||
if (error || !submissions) {
|
||||
return <ErrorAlert message="Не удалось загрузить список попыток" onRetry={refetch} />
|
||||
}
|
||||
|
||||
const filteredSubmissions = submissions.filter((submission) => {
|
||||
const user = submission.user as ChallengeUser
|
||||
const task = submission.task as ChallengeTask
|
||||
|
||||
const matchesSearch =
|
||||
user.nickname.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
task.title.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
|
||||
const matchesStatus = statusFilter === 'all' || submission.status === statusFilter
|
||||
|
||||
return matchesSearch && matchesStatus
|
||||
})
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleString('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
const getCheckTime = (submission: ChallengeSubmission) => {
|
||||
if (!submission.checkedAt) return '—'
|
||||
const submitted = new Date(submission.submittedAt).getTime()
|
||||
const checked = new Date(submission.checkedAt).getTime()
|
||||
const diff = Math.round((checked - submitted) / 1000)
|
||||
return `${diff} сек`
|
||||
}
|
||||
|
||||
const statusOptions = createListCollection({
|
||||
items: [
|
||||
{ label: 'Все статусы', value: 'all' },
|
||||
{ label: 'Принято', value: 'accepted' },
|
||||
{ label: 'Доработка', value: 'needs_revision' },
|
||||
{ label: 'Проверяется', value: 'in_progress' },
|
||||
{ label: 'Ожидает', value: 'pending' },
|
||||
],
|
||||
})
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Heading mb={6}>Попытки решений</Heading>
|
||||
|
||||
{/* Filters */}
|
||||
{submissions.length > 0 && (
|
||||
<HStack mb={4} gap={4}>
|
||||
<Input
|
||||
placeholder="Поиск по пользователю или заданию..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
maxW="400px"
|
||||
/>
|
||||
<Select.Root
|
||||
collection={statusOptions}
|
||||
value={[statusFilter]}
|
||||
onValueChange={(e) => setStatusFilter(e.value[0] as SubmissionStatus | 'all')}
|
||||
maxW="200px"
|
||||
>
|
||||
<Select.Trigger>
|
||||
<Select.ValueText placeholder="Статус" />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{statusOptions.items.map((option) => (
|
||||
<Select.Item key={option.value} item={option}>
|
||||
{option.label}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</HStack>
|
||||
)}
|
||||
|
||||
{filteredSubmissions.length === 0 && submissions.length === 0 ? (
|
||||
<EmptyState title="Нет попыток" description="Попытки появятся после отправки решений" />
|
||||
) : filteredSubmissions.length === 0 ? (
|
||||
<EmptyState title="Ничего не найдено" description="Попробуйте изменить фильтры" />
|
||||
) : (
|
||||
<Box bg="white" borderRadius="lg" boxShadow="sm" borderWidth="1px" borderColor="gray.200" overflowX="auto">
|
||||
<Table.Root size="sm">
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.ColumnHeader>Пользователь</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>Задание</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>Статус</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>Попытка</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>Дата отправки</Table.ColumnHeader>
|
||||
<Table.ColumnHeader>Время проверки</Table.ColumnHeader>
|
||||
<Table.ColumnHeader textAlign="right">Действия</Table.ColumnHeader>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
{filteredSubmissions.map((submission) => {
|
||||
const user = submission.user as ChallengeUser
|
||||
const task = submission.task as ChallengeTask
|
||||
|
||||
return (
|
||||
<Table.Row key={submission.id}>
|
||||
<Table.Cell fontWeight="medium">{user.nickname}</Table.Cell>
|
||||
<Table.Cell>{task.title}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<StatusBadge status={submission.status} />
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
#{submission.attemptNumber}
|
||||
</Text>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
{formatDate(submission.submittedAt)}
|
||||
</Text>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
{getCheckTime(submission)}
|
||||
</Text>
|
||||
</Table.Cell>
|
||||
<Table.Cell textAlign="right">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
colorPalette="teal"
|
||||
onClick={() => setSelectedSubmission(submission)}
|
||||
>
|
||||
Детали
|
||||
</Button>
|
||||
</Table.Cell>
|
||||
</Table.Row>
|
||||
)
|
||||
})}
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Submission Details Modal */}
|
||||
<SubmissionDetailsModal
|
||||
submission={selectedSubmission}
|
||||
isOpen={!!selectedSubmission}
|
||||
onClose={() => setSelectedSubmission(null)}
|
||||
/>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
interface SubmissionDetailsModalProps {
|
||||
submission: ChallengeSubmission | null
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const SubmissionDetailsModal: React.FC<SubmissionDetailsModalProps> = ({
|
||||
submission,
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
if (!submission) return null
|
||||
|
||||
const user = submission.user as ChallengeUser
|
||||
const task = submission.task as ChallengeTask
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
return new Date(dateStr).toLocaleString('ru-RU', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
const getCheckTime = () => {
|
||||
if (!submission.checkedAt) return null
|
||||
const submitted = new Date(submission.submittedAt).getTime()
|
||||
const checked = new Date(submission.checkedAt).getTime()
|
||||
return ((checked - submitted) / 1000).toFixed(2)
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogRoot open={isOpen} onOpenChange={(e) => !e.open && onClose()} size="xl">
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Детали попытки #{submission.attemptNumber}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogBody>
|
||||
<VStack gap={6} align="stretch">
|
||||
{/* Meta */}
|
||||
<Box>
|
||||
<HStack mb={4} justify="space-between">
|
||||
<Box>
|
||||
<Text fontSize="sm" color="gray.600" mb={1}>
|
||||
Пользователь
|
||||
</Text>
|
||||
<Text fontWeight="bold">{user.nickname}</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text fontSize="sm" color="gray.600" mb={1}>
|
||||
Статус
|
||||
</Text>
|
||||
<StatusBadge status={submission.status} />
|
||||
</Box>
|
||||
</HStack>
|
||||
|
||||
<VStack align="stretch" gap={2}>
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
<strong>Отправлено:</strong> {formatDate(submission.submittedAt)}
|
||||
</Text>
|
||||
{submission.checkedAt && (
|
||||
<>
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
<strong>Проверено:</strong> {formatDate(submission.checkedAt)}
|
||||
</Text>
|
||||
<Text fontSize="sm" color="gray.600">
|
||||
<strong>Время проверки:</strong> {getCheckTime()} сек
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</VStack>
|
||||
</Box>
|
||||
|
||||
{/* Task */}
|
||||
<Box>
|
||||
<Text fontWeight="bold" mb={2}>
|
||||
Задание: {task.title}
|
||||
</Text>
|
||||
<Box
|
||||
p={4}
|
||||
bg="gray.50"
|
||||
borderRadius="md"
|
||||
borderWidth="1px"
|
||||
borderColor="gray.200"
|
||||
maxH="200px"
|
||||
overflowY="auto"
|
||||
>
|
||||
<ReactMarkdown>{task.description}</ReactMarkdown>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Solution */}
|
||||
<Box>
|
||||
<Text fontWeight="bold" mb={2}>
|
||||
Решение пользователя:
|
||||
</Text>
|
||||
<Box
|
||||
p={4}
|
||||
bg="blue.50"
|
||||
borderRadius="md"
|
||||
borderWidth="1px"
|
||||
borderColor="blue.200"
|
||||
maxH="300px"
|
||||
overflowY="auto"
|
||||
>
|
||||
<Text
|
||||
fontFamily="monospace"
|
||||
fontSize="sm"
|
||||
whiteSpace="pre-wrap"
|
||||
wordBreak="break-word"
|
||||
>
|
||||
{submission.result}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Feedback */}
|
||||
{submission.feedback && (
|
||||
<Box>
|
||||
<Text fontWeight="bold" mb={2}>
|
||||
Обратная связь от LLM:
|
||||
</Text>
|
||||
<Box
|
||||
p={4}
|
||||
bg={submission.status === 'accepted' ? 'green.50' : 'red.50'}
|
||||
borderRadius="md"
|
||||
borderWidth="1px"
|
||||
borderColor={submission.status === 'accepted' ? 'green.200' : 'red.200'}
|
||||
>
|
||||
<Text>{submission.feedback}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</VStack>
|
||||
</DialogBody>
|
||||
<DialogFooter>
|
||||
<DialogActionTrigger asChild>
|
||||
<Button variant="outline" onClick={onClose}>
|
||||
Закрыть
|
||||
</Button>
|
||||
</DialogActionTrigger>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</DialogRoot>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user