feat: QA fixes, new APIs (assignments, rubrics, custom exams), Generation page enhancements

- Fix ELAI video generation (correct render endpoint, script splitting for 60s limit)
- Fix speaking script generation error handling and empty response display
- Add custom exam list API (GET /api/exam/custom/list)
- Add assignments REST API (list, create, get)
- Add rubrics REST API (list, create)
- Enhance Generation page: dynamic exam structures, auto-module selection, preview dialog, audio player
- Improve submit feedback with exam ID and status in toast notifications
- Fix ExamsListPage to show both custom exams and exam sessions
- Connect RubricsPage to backend API with fallback data
- Add Dockerfile, docker-compose.yml, requirements.txt for deployment
- Fix placement, grading, scoring, and auth controllers
- Add ErrorBoundary component for frontend resilience
- Add QA report and credentials documentation

Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-12 14:26:39 +04:00
parent 74c2c9f2d2
commit e066c595b9
16 changed files with 958 additions and 168 deletions

View File

@@ -583,10 +583,27 @@ function NewQuestionInline({ sectionIndex, form }: { sectionIndex: number; form:
<Button
type="button"
size="sm"
onClick={() => {
const cur = form.getValues(`sections.${sectionIndex}.question_ids`) ?? [];
const nextId = Math.floor(Math.random() * 1_000_000_000);
form.setValue(`sections.${sectionIndex}.question_ids`, [...cur, nextId]);
disabled={!stem.trim()}
onClick={async () => {
if (!stem.trim()) return;
try {
const res = await fetch("/api/exam/questions/quick-create", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("encoach_token")}`,
},
body: JSON.stringify({ stem: stem.trim(), question_type: "short_answer", skill: form.getValues(`sections.${sectionIndex}.skill`) || "reading" }),
});
const data = await res.json();
const qId = data?.question_id;
if (qId) {
const cur = form.getValues(`sections.${sectionIndex}.question_ids`) ?? [];
form.setValue(`sections.${sectionIndex}.question_ids`, [...cur, qId]);
}
} catch {
// fallback: still add with a placeholder notice
}
setStem("");
}}
>

View File

@@ -1,4 +1,5 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
@@ -22,6 +23,7 @@ import type { PendingScore } from "@/types";
import { toast } from "sonner";
export default function ScoreApprovalQueue() {
const navigate = useNavigate();
const { data, isLoading, refetch } = usePendingScores();
const release = useReleaseScore();
const reject = useRejectScore();
@@ -156,7 +158,7 @@ export default function ScoreApprovalQueue() {
<Button size="sm" variant="destructive" onClick={() => openReject(r)}>
Reject
</Button>
<Button size="sm" variant="outline">
<Button size="sm" variant="outline" onClick={() => navigate(`/admin/exam/${r.exam_id ?? r.attempt_id}/grading`)}>
View Details
</Button>
</TableCell>