Add test submission feature for LLM checks without creating submissions; update API and UI components to support new functionality, enhancing task evaluation for teachers and challenge authors. Update localization for test check messages in English and Russian.

This commit is contained in:
2025-12-10 12:41:03 +03:00
parent 173954f685
commit ec79dd58aa
6 changed files with 408 additions and 0 deletions

View File

@@ -19,6 +19,7 @@ import {
useGetTaskQuery,
useCreateTaskMutation,
useUpdateTaskMutation,
useTestSubmissionMutation,
} from '../../__data__/api/api'
import { URLs } from '../../__data__/urls'
import { LoadingSpinner } from '../../components/LoadingSpinner'
@@ -35,11 +36,15 @@ export const TaskFormPage: React.FC = () => {
})
const [createTask, { isLoading: isCreating }] = useCreateTaskMutation()
const [updateTask, { isLoading: isUpdating }] = useUpdateTaskMutation()
const [testSubmission, { isLoading: isTesting }] = useTestSubmissionMutation()
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [hiddenInstructions, setHiddenInstructions] = useState('')
const [showDescPreview, setShowDescPreview] = useState(false)
const [testAnswer, setTestAnswer] = useState('')
const [testStatus, setTestStatus] = useState<string | null>(null)
const [testFeedback, setTestFeedback] = useState<string | null>(null)
useEffect(() => {
if (task) {
@@ -106,6 +111,58 @@ export const TaskFormPage: React.FC = () => {
}
}
const handleTestSubmit = async () => {
if (!task || !id) {
return
}
if (!testAnswer.trim()) {
toaster.create({
title: t('challenge.admin.common.validation.error'),
description: t('challenge.admin.tasks.test.validation.fill.answer'),
type: 'error',
})
return
}
setTestStatus(null)
setTestFeedback(null)
try {
const dummyUserId = task.creator?.sub || task.id
const result = await testSubmission({
userId: dummyUserId,
taskId: task.id,
result: testAnswer.trim(),
isTest: true,
}).unwrap()
setTestStatus(result.status)
setTestFeedback(result.feedback ?? null)
toaster.create({
title: t('challenge.admin.common.success'),
description: t('challenge.admin.tasks.test.success'),
type: 'success',
})
} catch (err: unknown) {
const isForbidden =
err &&
typeof err === 'object' &&
'status' in err &&
(err as { status?: number }).status === 403
toaster.create({
title: t('challenge.admin.common.error'),
description: isForbidden
? t('challenge.admin.tasks.test.forbidden')
: t('challenge.admin.tasks.test.error'),
type: 'error',
})
}
}
if (isEdit && isLoadingTask) {
return <LoadingSpinner message={t('challenge.admin.tasks.loading')} />
}
@@ -309,6 +366,57 @@ export const TaskFormPage: React.FC = () => {
</Box>
)}
{/* Test submission (LLM check) */}
{isEdit && task && (
<Box p={4} bg="teal.50" borderRadius="md" borderWidth="1px" borderColor="teal.200">
<Text fontWeight="bold" mb={2} color="teal.900">
{t('challenge.admin.tasks.test.title')}
</Text>
<Text fontSize="sm" mb={3} color="teal.800">
{t('challenge.admin.tasks.test.description')}
</Text>
<Field.Root>
<Field.Label>{t('challenge.admin.tasks.test.field.answer')}</Field.Label>
<Textarea
value={testAnswer}
onChange={(e) => setTestAnswer(e.target.value)}
placeholder={t('challenge.admin.tasks.test.field.answer.placeholder')}
rows={6}
fontFamily="monospace"
disabled={isTesting}
/>
<Field.HelperText>
{t('challenge.admin.tasks.test.field.answer.helper')}
</Field.HelperText>
</Field.Root>
<HStack mt={3} align="flex-start" justify="space-between" gap={4}>
<Button
onClick={handleTestSubmit}
colorPalette="teal"
disabled={isTesting || !testAnswer.trim()}
>
{t('challenge.admin.tasks.test.button.run')}
</Button>
{(testStatus || testFeedback) && (
<Box flex="1" p={3} bg="white" borderRadius="md" borderWidth="1px" borderColor="teal.100">
{testStatus && (
<Text fontSize="sm" fontWeight="medium" mb={1}>
{t(`challenge.admin.tasks.test.status.${testStatus}`)}
</Text>
)}
{testFeedback && (
<Text fontSize="sm" whiteSpace="pre-wrap">
{testFeedback}
</Text>
)}
</Box>
)}
</HStack>
</Box>
)}
{/* Actions */}
<HStack gap={3} justify="flex-end">
<Button variant="outline" onClick={() => navigate(URLs.tasks)} disabled={isLoading}>