import { useEffect, useMemo, useState } from "react"; import { useParams } from "react-router-dom"; import { useGradingQueue, useStudentResponse, useGradingRubric, useAIGradeSuggestion, useSubmitGrade, } from "@/hooks/queries/useGrading"; import type { GradingQueueItem } from "@/types"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table"; import { Badge } from "@/components/ui/badge"; import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet"; import { Label } from "@/components/ui/label"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { Skeleton } from "@/components/ui/skeleton"; import { Slider } from "@/components/ui/slider"; import { AlertTriangle } from "lucide-react"; const WRITING_CRITERIA = ["Task Achievement", "Coherence", "Lexical Resource", "Grammatical Range"]; const SPEAKING_CRITERIA = ["Fluency", "Lexical Resource", "Grammatical Range", "Pronunciation"]; export default function GradingQueue() { const { examId: examIdParam } = useParams(); const examId = Number(examIdParam); const { data: queue = [], isLoading } = useGradingQueue({ exam_id: examId }); const filtered = useMemo(() => { if (!Number.isFinite(examId) || examId <= 0) return queue; return queue.filter((row) => row.exam_id === examId); }, [queue, examId]); const [open, setOpen] = useState(false); const [active, setActive] = useState(null); const skill = active?.skill ?? "writing"; const attemptId = active?.id ?? 0; const responseQ = useStudentResponse(attemptId, skill); const rubricQ = useGradingRubric(attemptId, skill); const aiSuggest = useAIGradeSuggestion(); const submitGrade = useSubmitGrade(); const [scores, setScores] = useState>({}); const criteriaNames = skill === "speaking" ? SPEAKING_CRITERIA : WRITING_CRITERIA; const openRow = (row: GradingQueueItem) => { setActive(row); setScores({}); setOpen(true); }; useEffect(() => { if (!open || !rubricQ.data) return; const names = skill === "speaking" ? SPEAKING_CRITERIA : WRITING_CRITERIA; const list = rubricQ.data; const next: Record = {}; list.forEach((c) => { next[c.name] = typeof c.score === "number" ? c.score : 0; }); names.forEach((n) => { if (next[n] === undefined) next[n] = 0; }); setScores(next); }, [open, rubricQ.data, skill]); const handleAISuggest = () => { if (!active) return; aiSuggest.mutate( { attemptId: active.id, skill }, { onSuccess: (data) => { setScores((prev) => { const next = { ...prev }; data.criteria_scores.forEach((c) => { next[c.name] = c.score; }); return next; }); }, }, ); }; const handleSubmit = () => { if (!active) return; submitGrade.mutate( { attempt_id: active.id, skill, criteria_scores: criteriaNames.map((name) => ({ name, score: scores[name] ?? 0 })), }, { onSuccess: () => { setOpen(false); setActive(null); }, }, ); }; return (

Grading queue

Exam {examIdParam}

Pending submissions {isLoading ? ( ) : ( Student Name Exam Title Skill Submitted At Status Action {filtered.map((row) => ( {row.student_name} {row.exam_title} {row.skill} {new Date(row.submitted_at).toLocaleString()} {row.status} ))}
)}
Grade response
{responseQ.isLoading ? ( ) : responseQ.data ? (

{responseQ.data.prompt_text}

{responseQ.data.type === "writing" && responseQ.data.text ? (
{responseQ.data.text}
) : null} {responseQ.data.type === "speaking" ? (
Audio response placeholder
) : null}
) : null}

Rubric

{criteriaNames.map((name) => (
0–9
setScores((s) => ({ ...s, [name]: v[0] ?? 0 }))} />

{scores[name] ?? 0}

))}
{aiSuggest.isError ? (

Suggestion could not be loaded.

) : null}
); }