Add Submission Details Page and Update Localization for Submissions Overview
This commit is contained in:
@@ -177,6 +177,12 @@
|
|||||||
"challenge.admin.submissions.details.solution": "User solution:",
|
"challenge.admin.submissions.details.solution": "User solution:",
|
||||||
"challenge.admin.submissions.details.feedback": "LLM feedback:",
|
"challenge.admin.submissions.details.feedback": "LLM feedback:",
|
||||||
"challenge.admin.submissions.details.close": "Close",
|
"challenge.admin.submissions.details.close": "Close",
|
||||||
|
"challenge.admin.submissions.details.not.found": "Attempt not found",
|
||||||
|
"challenge.admin.submissions.overview.title": "Overall participant progress",
|
||||||
|
"challenge.admin.submissions.overview.description": "Below is an overview of all participants and chains. Select a user above to see their individual attempts.",
|
||||||
|
"challenge.admin.submissions.overview.table.user": "Participant",
|
||||||
|
"challenge.admin.submissions.overview.table.chain": "Chain",
|
||||||
|
"challenge.admin.submissions.overview.table.progress": "Progress",
|
||||||
"challenge.admin.layout.title": "Challenge Admin",
|
"challenge.admin.layout.title": "Challenge Admin",
|
||||||
"challenge.admin.layout.nav.dashboard": "Dashboard",
|
"challenge.admin.layout.nav.dashboard": "Dashboard",
|
||||||
"challenge.admin.layout.nav.detailed.stats": "Detailed Statistics",
|
"challenge.admin.layout.nav.detailed.stats": "Detailed Statistics",
|
||||||
|
|||||||
@@ -177,6 +177,7 @@
|
|||||||
"challenge.admin.submissions.details.solution": "Решение пользователя:",
|
"challenge.admin.submissions.details.solution": "Решение пользователя:",
|
||||||
"challenge.admin.submissions.details.feedback": "Обратная связь от LLM:",
|
"challenge.admin.submissions.details.feedback": "Обратная связь от LLM:",
|
||||||
"challenge.admin.submissions.details.close": "Закрыть",
|
"challenge.admin.submissions.details.close": "Закрыть",
|
||||||
|
"challenge.admin.submissions.details.not.found": "Попытка не найдена",
|
||||||
"challenge.admin.submissions.overview.title": "Общий прогресс по участникам",
|
"challenge.admin.submissions.overview.title": "Общий прогресс по участникам",
|
||||||
"challenge.admin.submissions.overview.description": "Ниже — сводка по прогрессу всех участников и цепочек. Выберите пользователя выше, чтобы просмотреть его отдельные попытки.",
|
"challenge.admin.submissions.overview.description": "Ниже — сводка по прогрессу всех участников и цепочек. Выберите пользователя выше, чтобы просмотреть его отдельные попытки.",
|
||||||
"challenge.admin.submissions.overview.table.user": "Участник",
|
"challenge.admin.submissions.overview.table.user": "Участник",
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export const URLs = {
|
|||||||
|
|
||||||
// Submissions
|
// Submissions
|
||||||
submissions: makeUrl('/submissions'),
|
submissions: makeUrl('/submissions'),
|
||||||
|
submissionDetails: (userId: string, submissionId: string) => makeUrl(`/submissions/${userId}/${submissionId}`),
|
||||||
|
submissionDetailsPath: makeUrl('/submissions/:userId/:submissionId'),
|
||||||
|
|
||||||
// External links
|
// External links
|
||||||
challengePlayer: navs['link.challenge'] || '/challenge',
|
challengePlayer: navs['link.challenge'] || '/challenge',
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { ChainsListPage } from './pages/chains/ChainsListPage'
|
|||||||
import { ChainFormPage } from './pages/chains/ChainFormPage'
|
import { ChainFormPage } from './pages/chains/ChainFormPage'
|
||||||
import { UsersPage } from './pages/users/UsersPage'
|
import { UsersPage } from './pages/users/UsersPage'
|
||||||
import { SubmissionsPage } from './pages/submissions/SubmissionsPage'
|
import { SubmissionsPage } from './pages/submissions/SubmissionsPage'
|
||||||
|
import { SubmissionDetailsPage } from './pages/submissions/SubmissionDetailsPage'
|
||||||
import { URLs } from './__data__/urls'
|
import { URLs } from './__data__/urls'
|
||||||
|
|
||||||
const PageWrapper = ({ children }: React.PropsWithChildren) => (
|
const PageWrapper = ({ children }: React.PropsWithChildren) => (
|
||||||
@@ -120,6 +121,14 @@ export const Dashboard = () => {
|
|||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path={URLs.submissionDetailsPath}
|
||||||
|
element={
|
||||||
|
<PageWrapper>
|
||||||
|
<SubmissionDetailsPage />
|
||||||
|
</PageWrapper>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Routes>
|
</Routes>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
235
src/pages/submissions/SubmissionDetailsPage.tsx
Normal file
235
src/pages/submissions/SubmissionDetailsPage.tsx
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom'
|
||||||
|
import { Box, Heading, Text, Button, HStack, VStack } from '@chakra-ui/react'
|
||||||
|
import ReactMarkdown from 'react-markdown'
|
||||||
|
import { useGetUserSubmissionsQuery } from '../../__data__/api/api'
|
||||||
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
|
import { ErrorAlert } from '../../components/ErrorAlert'
|
||||||
|
import { StatusBadge } from '../../components/StatusBadge'
|
||||||
|
import type { ChallengeTask, ChallengeUser } from '../../types/challenge'
|
||||||
|
import { URLs } from '../../__data__/urls'
|
||||||
|
|
||||||
|
export const SubmissionDetailsPage: React.FC = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { userId, submissionId } = useParams<{ userId: string; submissionId: string }>()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
// Получаем submissions для конкретного пользователя
|
||||||
|
const { data: submissions, isLoading, error } = useGetUserSubmissionsQuery(
|
||||||
|
{ userId: userId!, taskId: undefined },
|
||||||
|
{ skip: !userId }
|
||||||
|
)
|
||||||
|
|
||||||
|
const submission = submissions?.find((s) => s.id === submissionId)
|
||||||
|
|
||||||
|
const handleBack = () => {
|
||||||
|
navigate(URLs.submissions)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <LoadingSpinner message={t('challenge.admin.submissions.loading')} />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <ErrorAlert message={t('challenge.admin.submissions.load.error')} onRetry={handleBack} />
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!submission) {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Button variant="ghost" onClick={handleBack} mb={4}>
|
||||||
|
← {t('challenge.admin.common.close')}
|
||||||
|
</Button>
|
||||||
|
<ErrorAlert
|
||||||
|
message={t('challenge.admin.submissions.details.not.found')}
|
||||||
|
onRetry={handleBack}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const rawUser = submission.user as ChallengeUser | string | undefined
|
||||||
|
const rawTask = submission.task as ChallengeTask | string | undefined
|
||||||
|
|
||||||
|
const userNickname =
|
||||||
|
rawUser && typeof rawUser === 'object' && 'nickname' in rawUser
|
||||||
|
? rawUser.nickname ?? ''
|
||||||
|
: typeof rawUser === 'string'
|
||||||
|
? rawUser
|
||||||
|
: ''
|
||||||
|
|
||||||
|
const taskTitle =
|
||||||
|
rawTask && typeof rawTask === 'object' && 'title' in rawTask
|
||||||
|
? rawTask.title ?? ''
|
||||||
|
: typeof rawTask === 'string'
|
||||||
|
? rawTask
|
||||||
|
: ''
|
||||||
|
|
||||||
|
const taskDescription =
|
||||||
|
rawTask && typeof rawTask === 'object' && 'description' in rawTask
|
||||||
|
? rawTask.description ?? ''
|
||||||
|
: ''
|
||||||
|
|
||||||
|
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 getCheckTimeValue = () => {
|
||||||
|
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 (
|
||||||
|
<Box>
|
||||||
|
{/* Header with back button */}
|
||||||
|
<HStack mb={6}>
|
||||||
|
<Button variant="ghost" onClick={handleBack}>
|
||||||
|
← {t('challenge.admin.common.close')}
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<Heading mb={6}>
|
||||||
|
{t('challenge.admin.submissions.details.title')} #{submission.attemptNumber}
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<VStack gap={6} align="stretch">
|
||||||
|
{/* Meta */}
|
||||||
|
<Box
|
||||||
|
p={6}
|
||||||
|
bg="white"
|
||||||
|
borderRadius="lg"
|
||||||
|
boxShadow="sm"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor="gray.200"
|
||||||
|
>
|
||||||
|
<HStack mb={4} justify="space-between">
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="sm" color="gray.600" mb={1}>
|
||||||
|
{t('challenge.admin.submissions.details.user')}
|
||||||
|
</Text>
|
||||||
|
<Text fontWeight="bold">{userNickname}</Text>
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fontSize="sm" color="gray.600" mb={1}>
|
||||||
|
{t('challenge.admin.submissions.details.status')}
|
||||||
|
</Text>
|
||||||
|
<StatusBadge status={submission.status} />
|
||||||
|
</Box>
|
||||||
|
</HStack>
|
||||||
|
|
||||||
|
<VStack align="stretch" gap={2}>
|
||||||
|
<Text fontSize="sm" color="gray.600">
|
||||||
|
<strong>{t('challenge.admin.submissions.details.submitted')}</strong>{' '}
|
||||||
|
{formatDate(submission.submittedAt)}
|
||||||
|
</Text>
|
||||||
|
{submission.checkedAt && (
|
||||||
|
<>
|
||||||
|
<Text fontSize="sm" color="gray.600">
|
||||||
|
<strong>{t('challenge.admin.submissions.details.checked')}</strong>{' '}
|
||||||
|
{formatDate(submission.checkedAt)}
|
||||||
|
</Text>
|
||||||
|
<Text fontSize="sm" color="gray.600">
|
||||||
|
<strong>{t('challenge.admin.submissions.details.check.time')}</strong>{' '}
|
||||||
|
{t('challenge.admin.submissions.check.time', { time: getCheckTimeValue() })}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Task */}
|
||||||
|
<Box
|
||||||
|
p={6}
|
||||||
|
bg="white"
|
||||||
|
borderRadius="lg"
|
||||||
|
boxShadow="sm"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor="gray.200"
|
||||||
|
>
|
||||||
|
<Text fontWeight="bold" mb={4}>
|
||||||
|
{t('challenge.admin.submissions.details.task')} {taskTitle}
|
||||||
|
</Text>
|
||||||
|
<Box
|
||||||
|
p={4}
|
||||||
|
bg="gray.50"
|
||||||
|
borderRadius="md"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor="gray.200"
|
||||||
|
maxH="400px"
|
||||||
|
overflowY="auto"
|
||||||
|
>
|
||||||
|
<ReactMarkdown>{taskDescription}</ReactMarkdown>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Solution */}
|
||||||
|
<Box
|
||||||
|
p={6}
|
||||||
|
bg="white"
|
||||||
|
borderRadius="lg"
|
||||||
|
boxShadow="sm"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor="gray.200"
|
||||||
|
>
|
||||||
|
<Text fontWeight="bold" mb={4}>
|
||||||
|
{t('challenge.admin.submissions.details.solution')}
|
||||||
|
</Text>
|
||||||
|
<Box
|
||||||
|
p={4}
|
||||||
|
bg="blue.50"
|
||||||
|
borderRadius="md"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor="blue.200"
|
||||||
|
maxH="500px"
|
||||||
|
overflowY="auto"
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
fontFamily="monospace"
|
||||||
|
fontSize="sm"
|
||||||
|
whiteSpace="pre-wrap"
|
||||||
|
wordBreak="break-word"
|
||||||
|
>
|
||||||
|
{submission.result}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Feedback */}
|
||||||
|
{submission.feedback && (
|
||||||
|
<Box
|
||||||
|
p={6}
|
||||||
|
bg="white"
|
||||||
|
borderRadius="lg"
|
||||||
|
boxShadow="sm"
|
||||||
|
borderWidth="1px"
|
||||||
|
borderColor="gray.200"
|
||||||
|
>
|
||||||
|
<Text fontWeight="bold" mb={4}>
|
||||||
|
{t('challenge.admin.submissions.details.feedback')}
|
||||||
|
</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>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import { useNavigate } from 'react-router-dom'
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Heading,
|
Heading,
|
||||||
@@ -10,23 +11,16 @@ import {
|
|||||||
HStack,
|
HStack,
|
||||||
VStack,
|
VStack,
|
||||||
Select,
|
Select,
|
||||||
DialogRoot,
|
|
||||||
DialogContent,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogBody,
|
|
||||||
DialogFooter,
|
|
||||||
DialogActionTrigger,
|
|
||||||
Progress,
|
Progress,
|
||||||
Grid,
|
Grid,
|
||||||
createListCollection,
|
createListCollection,
|
||||||
} from '@chakra-ui/react'
|
} from '@chakra-ui/react'
|
||||||
import ReactMarkdown from 'react-markdown'
|
|
||||||
import { useGetSystemStatsV2Query, useGetUserSubmissionsQuery } from '../../__data__/api/api'
|
import { useGetSystemStatsV2Query, useGetUserSubmissionsQuery } from '../../__data__/api/api'
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { ErrorAlert } from '../../components/ErrorAlert'
|
import { ErrorAlert } from '../../components/ErrorAlert'
|
||||||
import { EmptyState } from '../../components/EmptyState'
|
import { EmptyState } from '../../components/EmptyState'
|
||||||
import { StatusBadge } from '../../components/StatusBadge'
|
import { StatusBadge } from '../../components/StatusBadge'
|
||||||
|
import { URLs } from '../../__data__/urls'
|
||||||
import type {
|
import type {
|
||||||
ActiveParticipant,
|
ActiveParticipant,
|
||||||
ChallengeSubmission,
|
ChallengeSubmission,
|
||||||
@@ -37,12 +31,12 @@ import type {
|
|||||||
|
|
||||||
export const SubmissionsPage: React.FC = () => {
|
export const SubmissionsPage: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
const { data: stats, isLoading: isStatsLoading, error: statsError, refetch: refetchStats } =
|
const { data: stats, isLoading: isStatsLoading, error: statsError, refetch: refetchStats } =
|
||||||
useGetSystemStatsV2Query(undefined)
|
useGetSystemStatsV2Query(undefined)
|
||||||
|
|
||||||
const [searchQuery, setSearchQuery] = useState('')
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
const [statusFilter, setStatusFilter] = useState<SubmissionStatus | 'all'>('all')
|
const [statusFilter, setStatusFilter] = useState<SubmissionStatus | 'all'>('all')
|
||||||
const [selectedSubmission, setSelectedSubmission] = useState<ChallengeSubmission | null>(null)
|
|
||||||
const [selectedUserId, setSelectedUserId] = useState<string | null>(null)
|
const [selectedUserId, setSelectedUserId] = useState<string | null>(null)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -369,7 +363,7 @@ export const SubmissionsPage: React.FC = () => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
colorPalette="teal"
|
colorPalette="teal"
|
||||||
onClick={() => setSelectedSubmission(submission)}
|
onClick={() => navigate(URLs.submissionDetails(selectedUserId!, submission.id))}
|
||||||
>
|
>
|
||||||
{t('challenge.admin.submissions.button.details')}
|
{t('challenge.admin.submissions.button.details')}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -381,187 +375,7 @@ export const SubmissionsPage: React.FC = () => {
|
|||||||
</Table.Root>
|
</Table.Root>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Submission Details Modal */}
|
|
||||||
<SubmissionDetailsModal
|
|
||||||
submission={selectedSubmission}
|
|
||||||
isOpen={!!selectedSubmission}
|
|
||||||
onClose={() => setSelectedSubmission(null)}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SubmissionDetailsModalProps {
|
|
||||||
submission: ChallengeSubmission | null
|
|
||||||
isOpen: boolean
|
|
||||||
onClose: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const SubmissionDetailsModal: React.FC<SubmissionDetailsModalProps> = ({
|
|
||||||
submission,
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
if (!submission) return null
|
|
||||||
|
|
||||||
const rawUser = submission.user as ChallengeUser | string | undefined
|
|
||||||
const rawTask = submission.task as ChallengeTask | string | undefined
|
|
||||||
|
|
||||||
const userNickname =
|
|
||||||
rawUser && typeof rawUser === 'object' && 'nickname' in rawUser
|
|
||||||
? (rawUser.nickname ?? '')
|
|
||||||
: typeof rawUser === 'string'
|
|
||||||
? rawUser
|
|
||||||
: ''
|
|
||||||
|
|
||||||
const taskTitle =
|
|
||||||
rawTask && typeof rawTask === 'object' && 'title' in rawTask
|
|
||||||
? (rawTask.title ?? '')
|
|
||||||
: typeof rawTask === 'string'
|
|
||||||
? rawTask
|
|
||||||
: ''
|
|
||||||
|
|
||||||
const taskDescription =
|
|
||||||
rawTask && typeof rawTask === 'object' && 'description' in rawTask
|
|
||||||
? (rawTask.description ?? '')
|
|
||||||
: ''
|
|
||||||
|
|
||||||
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 getCheckTimeValue = () => {
|
|
||||||
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 maxH="90vh" overflowY="auto">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>
|
|
||||||
{t('challenge.admin.submissions.details.title')} #{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}>
|
|
||||||
{t('challenge.admin.submissions.details.user')}
|
|
||||||
</Text>
|
|
||||||
<Text fontWeight="bold">{userNickname}</Text>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Text fontSize="sm" color="gray.600" mb={1}>
|
|
||||||
{t('challenge.admin.submissions.details.status')}
|
|
||||||
</Text>
|
|
||||||
<StatusBadge status={submission.status} />
|
|
||||||
</Box>
|
|
||||||
</HStack>
|
|
||||||
|
|
||||||
<VStack align="stretch" gap={2}>
|
|
||||||
<Text fontSize="sm" color="gray.600">
|
|
||||||
<strong>{t('challenge.admin.submissions.details.submitted')}</strong> {formatDate(submission.submittedAt)}
|
|
||||||
</Text>
|
|
||||||
{submission.checkedAt && (
|
|
||||||
<>
|
|
||||||
<Text fontSize="sm" color="gray.600">
|
|
||||||
<strong>{t('challenge.admin.submissions.details.checked')}</strong> {formatDate(submission.checkedAt)}
|
|
||||||
</Text>
|
|
||||||
<Text fontSize="sm" color="gray.600">
|
|
||||||
<strong>{t('challenge.admin.submissions.details.check.time')}</strong> {t('challenge.admin.submissions.check.time', { time: getCheckTimeValue() })}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</VStack>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Task */}
|
|
||||||
<Box>
|
|
||||||
<Text fontWeight="bold" mb={2}>
|
|
||||||
{t('challenge.admin.submissions.details.task')} {taskTitle}
|
|
||||||
</Text>
|
|
||||||
<Box
|
|
||||||
p={4}
|
|
||||||
bg="gray.50"
|
|
||||||
borderRadius="md"
|
|
||||||
borderWidth="1px"
|
|
||||||
borderColor="gray.200"
|
|
||||||
maxH="200px"
|
|
||||||
overflowY="auto"
|
|
||||||
>
|
|
||||||
<ReactMarkdown>{taskDescription}</ReactMarkdown>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Solution */}
|
|
||||||
<Box>
|
|
||||||
<Text fontWeight="bold" mb={2}>
|
|
||||||
{t('challenge.admin.submissions.details.solution')}
|
|
||||||
</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}>
|
|
||||||
{t('challenge.admin.submissions.details.feedback')}
|
|
||||||
</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}>
|
|
||||||
{t('challenge.admin.submissions.details.close')}
|
|
||||||
</Button>
|
|
||||||
</DialogActionTrigger>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</DialogRoot>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user