From 71b6180ab9a81ebbea08c2de0f19540e1aa983d9 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Date: Wed, 10 Dec 2025 00:25:25 +0300 Subject: [PATCH] Add Submission Details Page and Update Localization for Submissions Overview --- locales/en.json | 6 + locales/ru.json | 1 + src/__data__/urls.ts | 2 + src/dashboard.tsx | 9 + .../submissions/SubmissionDetailsPage.tsx | 235 ++++++++++++++++++ src/pages/submissions/SubmissionsPage.tsx | 194 +-------------- 6 files changed, 257 insertions(+), 190 deletions(-) create mode 100644 src/pages/submissions/SubmissionDetailsPage.tsx diff --git a/locales/en.json b/locales/en.json index 04d47f4..24ebb71 100644 --- a/locales/en.json +++ b/locales/en.json @@ -177,6 +177,12 @@ "challenge.admin.submissions.details.solution": "User solution:", "challenge.admin.submissions.details.feedback": "LLM feedback:", "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.nav.dashboard": "Dashboard", "challenge.admin.layout.nav.detailed.stats": "Detailed Statistics", diff --git a/locales/ru.json b/locales/ru.json index 3042bfd..e8f6343 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -177,6 +177,7 @@ "challenge.admin.submissions.details.solution": "Решение пользователя:", "challenge.admin.submissions.details.feedback": "Обратная связь от LLM:", "challenge.admin.submissions.details.close": "Закрыть", + "challenge.admin.submissions.details.not.found": "Попытка не найдена", "challenge.admin.submissions.overview.title": "Общий прогресс по участникам", "challenge.admin.submissions.overview.description": "Ниже — сводка по прогрессу всех участников и цепочек. Выберите пользователя выше, чтобы просмотреть его отдельные попытки.", "challenge.admin.submissions.overview.table.user": "Участник", diff --git a/src/__data__/urls.ts b/src/__data__/urls.ts index d0df8a5..3462cb7 100644 --- a/src/__data__/urls.ts +++ b/src/__data__/urls.ts @@ -34,6 +34,8 @@ export const URLs = { // Submissions submissions: makeUrl('/submissions'), + submissionDetails: (userId: string, submissionId: string) => makeUrl(`/submissions/${userId}/${submissionId}`), + submissionDetailsPath: makeUrl('/submissions/:userId/:submissionId'), // External links challengePlayer: navs['link.challenge'] || '/challenge', diff --git a/src/dashboard.tsx b/src/dashboard.tsx index 351b0a5..d388b18 100644 --- a/src/dashboard.tsx +++ b/src/dashboard.tsx @@ -10,6 +10,7 @@ import { ChainsListPage } from './pages/chains/ChainsListPage' import { ChainFormPage } from './pages/chains/ChainFormPage' import { UsersPage } from './pages/users/UsersPage' import { SubmissionsPage } from './pages/submissions/SubmissionsPage' +import { SubmissionDetailsPage } from './pages/submissions/SubmissionDetailsPage' import { URLs } from './__data__/urls' const PageWrapper = ({ children }: React.PropsWithChildren) => ( @@ -120,6 +121,14 @@ export const Dashboard = () => { } /> + + + + } + /> ) } diff --git a/src/pages/submissions/SubmissionDetailsPage.tsx b/src/pages/submissions/SubmissionDetailsPage.tsx new file mode 100644 index 0000000..d3b80ee --- /dev/null +++ b/src/pages/submissions/SubmissionDetailsPage.tsx @@ -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 + } + + if (error) { + return + } + + if (!submission) { + return ( + + + + + ) + } + + 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 ( + + {/* Header with back button */} + + + + + + {t('challenge.admin.submissions.details.title')} #{submission.attemptNumber} + + + + {/* Meta */} + + + + + {t('challenge.admin.submissions.details.user')} + + {userNickname} + + + + {t('challenge.admin.submissions.details.status')} + + + + + + + + {t('challenge.admin.submissions.details.submitted')}{' '} + {formatDate(submission.submittedAt)} + + {submission.checkedAt && ( + <> + + {t('challenge.admin.submissions.details.checked')}{' '} + {formatDate(submission.checkedAt)} + + + {t('challenge.admin.submissions.details.check.time')}{' '} + {t('challenge.admin.submissions.check.time', { time: getCheckTimeValue() })} + + + )} + + + + {/* Task */} + + + {t('challenge.admin.submissions.details.task')} {taskTitle} + + + {taskDescription} + + + + {/* Solution */} + + + {t('challenge.admin.submissions.details.solution')} + + + + {submission.result} + + + + + {/* Feedback */} + {submission.feedback && ( + + + {t('challenge.admin.submissions.details.feedback')} + + + {submission.feedback} + + + )} + + + ) +} + diff --git a/src/pages/submissions/SubmissionsPage.tsx b/src/pages/submissions/SubmissionsPage.tsx index a1ccadb..27b1014 100644 --- a/src/pages/submissions/SubmissionsPage.tsx +++ b/src/pages/submissions/SubmissionsPage.tsx @@ -1,5 +1,6 @@ import React, { useState } from 'react' import { useTranslation } from 'react-i18next' +import { useNavigate } from 'react-router-dom' import { Box, Heading, @@ -10,23 +11,16 @@ import { HStack, VStack, Select, - DialogRoot, - DialogContent, - DialogHeader, - DialogTitle, - DialogBody, - DialogFooter, - DialogActionTrigger, Progress, Grid, createListCollection, } from '@chakra-ui/react' -import ReactMarkdown from 'react-markdown' import { useGetSystemStatsV2Query, useGetUserSubmissionsQuery } 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 { URLs } from '../../__data__/urls' import type { ActiveParticipant, ChallengeSubmission, @@ -37,12 +31,12 @@ import type { export const SubmissionsPage: React.FC = () => { const { t } = useTranslation() + const navigate = useNavigate() const { data: stats, isLoading: isStatsLoading, error: statsError, refetch: refetchStats } = useGetSystemStatsV2Query(undefined) const [searchQuery, setSearchQuery] = useState('') const [statusFilter, setStatusFilter] = useState('all') - const [selectedSubmission, setSelectedSubmission] = useState(null) const [selectedUserId, setSelectedUserId] = useState(null) const { @@ -369,7 +363,7 @@ export const SubmissionsPage: React.FC = () => { size="sm" variant="ghost" colorPalette="teal" - onClick={() => setSelectedSubmission(submission)} + onClick={() => navigate(URLs.submissionDetails(selectedUserId!, submission.id))} > {t('challenge.admin.submissions.button.details')} @@ -381,187 +375,7 @@ export const SubmissionsPage: React.FC = () => { )} - - {/* Submission Details Modal */} - setSelectedSubmission(null)} - /> ) } -interface SubmissionDetailsModalProps { - submission: ChallengeSubmission | null - isOpen: boolean - onClose: () => void -} - -const SubmissionDetailsModal: React.FC = ({ - 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 ( - !e.open && onClose()} size="xl"> - - - - {t('challenge.admin.submissions.details.title')} #{submission.attemptNumber} - - - - - {/* Meta */} - - - - - {t('challenge.admin.submissions.details.user')} - - {userNickname} - - - - {t('challenge.admin.submissions.details.status')} - - - - - - - - {t('challenge.admin.submissions.details.submitted')} {formatDate(submission.submittedAt)} - - {submission.checkedAt && ( - <> - - {t('challenge.admin.submissions.details.checked')} {formatDate(submission.checkedAt)} - - - {t('challenge.admin.submissions.details.check.time')} {t('challenge.admin.submissions.check.time', { time: getCheckTimeValue() })} - - - )} - - - - {/* Task */} - - - {t('challenge.admin.submissions.details.task')} {taskTitle} - - - {taskDescription} - - - - {/* Solution */} - - - {t('challenge.admin.submissions.details.solution')} - - - - {submission.result} - - - - - {/* Feedback */} - {submission.feedback && ( - - - {t('challenge.admin.submissions.details.feedback')} - - - {submission.feedback} - - - )} - - - - - - - - - - ) -} -