feat: connect academic pages to backend APIs
- Generation page: exam structure dropdown loads from backend - Generation page: selecting structure auto-selects its modules - Rubrics page: loads rubrics from backend API with fallback to demo data Made-with: Cursor
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -52,6 +52,7 @@ import {
|
|||||||
import AiTipBanner from "@/components/ai/AiTipBanner";
|
import AiTipBanner from "@/components/ai/AiTipBanner";
|
||||||
import { generationService } from "@/services/generation.service";
|
import { generationService } from "@/services/generation.service";
|
||||||
import { mediaService, type Avatar } from "@/services/media.service";
|
import { mediaService, type Avatar } from "@/services/media.service";
|
||||||
|
import { examsService } from "@/services/exams.service";
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
|
||||||
type ModuleKey = "reading" | "listening" | "writing" | "speaking" | "level" | "industry";
|
type ModuleKey = "reading" | "listening" | "writing" | "speaking" | "level" | "industry";
|
||||||
@@ -73,6 +74,7 @@ const MODULES: ModuleInfo[] = [
|
|||||||
{ key: "industry", label: "Industry", icon: <Briefcase className="h-4 w-4" />, color: "text-amber-700", bgColor: "bg-amber-50 border-amber-200" },
|
{ key: "industry", label: "Industry", icon: <Briefcase className="h-4 w-4" />, color: "text-amber-700", bgColor: "bg-amber-50 border-amber-200" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const MODULE_KEYS: ModuleKey[] = MODULES.map((m) => m.key);
|
||||||
const CEFR_LEVELS = ["A1", "A2", "B1", "B2", "C1", "C2"];
|
const CEFR_LEVELS = ["A1", "A2", "B1", "B2", "C1", "C2"];
|
||||||
|
|
||||||
const READING_EXERCISE_TYPES = [
|
const READING_EXERCISE_TYPES = [
|
||||||
@@ -197,6 +199,12 @@ export default function GenerationPage() {
|
|||||||
const [moduleStates, setModuleStates] = useState<Record<string, ModuleState>>({});
|
const [moduleStates, setModuleStates] = useState<Record<string, ModuleState>>({});
|
||||||
const [previewOpen, setPreviewOpen] = useState(false);
|
const [previewOpen, setPreviewOpen] = useState(false);
|
||||||
|
|
||||||
|
const structuresQ = useQuery({
|
||||||
|
queryKey: ["exam-structures"],
|
||||||
|
queryFn: () => examsService.listStructures({}),
|
||||||
|
});
|
||||||
|
const structures = structuresQ.data?.items ?? [];
|
||||||
|
|
||||||
const getModuleState = useCallback((mod: ModuleKey): ModuleState => {
|
const getModuleState = useCallback((mod: ModuleKey): ModuleState => {
|
||||||
return moduleStates[mod] ?? defaultModuleState(mod);
|
return moduleStates[mod] ?? defaultModuleState(mod);
|
||||||
}, [moduleStates]);
|
}, [moduleStates]);
|
||||||
@@ -990,13 +998,27 @@ export default function GenerationPage() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<Label>Exam Structure</Label>
|
<Label>Exam Structure</Label>
|
||||||
<Select value={examStructure} onValueChange={setExamStructure}>
|
<Select value={examStructure} onValueChange={(val) => {
|
||||||
|
setExamStructure(val);
|
||||||
|
const s = structures.find((st) => String(st.id) === val);
|
||||||
|
if (s) {
|
||||||
|
const mods = (s as Record<string, unknown>).modules as string[] | undefined;
|
||||||
|
if (Array.isArray(mods) && mods.length) {
|
||||||
|
const next = new Set<ModuleKey>();
|
||||||
|
mods.forEach((m) => { if (MODULE_KEYS.includes(m as ModuleKey)) next.add(m as ModuleKey); });
|
||||||
|
setSelectedModules(next);
|
||||||
|
if (next.size > 0) setActiveModule([...next][0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}>
|
||||||
<SelectTrigger><SelectValue placeholder="Select an exam structure" /></SelectTrigger>
|
<SelectTrigger><SelectValue placeholder="Select an exam structure" /></SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="standard_ielts">Standard IELTS Academic</SelectItem>
|
{structures.map((s) => (
|
||||||
<SelectItem value="corporate">Corporate English Assessment</SelectItem>
|
<SelectItem key={s.id} value={String(s.id)}>{s.name}</SelectItem>
|
||||||
<SelectItem value="hospitality">Hospitality English Test</SelectItem>
|
))}
|
||||||
<SelectItem value="medical">Medical English Proficiency</SelectItem>
|
{structures.length === 0 && (
|
||||||
|
<SelectItem value="_none" disabled>No structures available</SelectItem>
|
||||||
|
)}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -8,11 +9,21 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from
|
|||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Search, Plus } from "lucide-react";
|
import { Search, Plus, Loader2 } from "lucide-react";
|
||||||
import AiTipBanner from "@/components/ai/AiTipBanner";
|
import AiTipBanner from "@/components/ai/AiTipBanner";
|
||||||
import AiCreationAssistant from "@/components/ai/AiCreationAssistant";
|
import AiCreationAssistant from "@/components/ai/AiCreationAssistant";
|
||||||
|
import { examsService } from "@/services/exams.service";
|
||||||
|
|
||||||
const rubrics = [
|
interface RubricItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
levels: string[];
|
||||||
|
criteria: number;
|
||||||
|
created: string;
|
||||||
|
skill?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FALLBACK_RUBRICS: RubricItem[] = [
|
||||||
{ id: 1, name: "IELTS Writing Task 2", levels: ["A1","A2","B1","B2","C1","C2"], criteria: 4, created: "2025-01-05" },
|
{ id: 1, name: "IELTS Writing Task 2", levels: ["A1","A2","B1","B2","C1","C2"], criteria: 4, created: "2025-01-05" },
|
||||||
{ id: 2, name: "Speaking Fluency", levels: ["A1","A2","B1","B2","C1","C2"], criteria: 3, created: "2025-01-10" },
|
{ id: 2, name: "Speaking Fluency", levels: ["A1","A2","B1","B2","C1","C2"], criteria: 3, created: "2025-01-10" },
|
||||||
{ id: 3, name: "Reading Comprehension", levels: ["A1","A2","B1","B2","C1"], criteria: 5, created: "2025-02-01" },
|
{ id: 3, name: "Reading Comprehension", levels: ["A1","A2","B1","B2","C1"], criteria: 5, created: "2025-02-01" },
|
||||||
@@ -26,6 +37,13 @@ const rubricGroups = [
|
|||||||
export default function RubricsPage() {
|
export default function RubricsPage() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
|
const rubricsQ = useQuery({
|
||||||
|
queryKey: ["rubrics"],
|
||||||
|
queryFn: () => examsService.listRubrics({}),
|
||||||
|
});
|
||||||
|
const backendRubrics = (rubricsQ.data?.items ?? []) as RubricItem[];
|
||||||
|
const rubrics = backendRubrics.length > 0 ? backendRubrics : FALLBACK_RUBRICS;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
Reference in New Issue
Block a user