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 571a08d0f7
commit 82ec3debcc
38 changed files with 2324 additions and 243 deletions

View File

@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useMemo } from "react";
import { useParams } from "react-router-dom";
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
@@ -12,6 +12,23 @@ import { useGenerateOutline, useGenerateChapterContent, usePublishWorkbench } fr
import { useToast } from "@/hooks/use-toast";
import type { WorkbenchGeneratedOutline, WorkbenchGeneratedChapter } from "@/types/courseware";
function sanitizeHtml(dirty: string): string {
const div = document.createElement("div");
div.textContent = "";
const parser = new DOMParser();
const doc = parser.parseFromString(dirty, "text/html");
const scripts = doc.querySelectorAll("script, iframe, object, embed, link[rel=import]");
scripts.forEach((el) => el.remove());
doc.querySelectorAll("*").forEach((el) => {
for (const attr of Array.from(el.attributes)) {
if (attr.name.startsWith("on") || attr.value.trim().toLowerCase().startsWith("javascript:")) {
el.removeAttribute(attr.name);
}
}
});
return doc.body.innerHTML;
}
type Complexity = "beginner" | "intermediate" | "advanced";
export default function AiWorkbench() {
@@ -160,7 +177,7 @@ export default function AiWorkbench() {
<CardDescription>Review the detailed content before publishing.</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="prose prose-sm max-w-none p-4 rounded-lg border bg-muted/30" dangerouslySetInnerHTML={{ __html: generatedContent.content }} />
<div className="prose prose-sm max-w-none p-4 rounded-lg border bg-muted/30" dangerouslySetInnerHTML={{ __html: sanitizeHtml(generatedContent.content) }} />
{generatedContent.exercises.length > 0 && (
<div className="space-y-2">
<h3 className="font-medium">Exercises ({generatedContent.exercises.length})</h3>