feat(generation): rebuild Generation Page with full AI workflows

- Rebuild GenerationPage.tsx from static placeholder to production-parity
  exam generation wizard with all 4 IELTS modules (Reading, Listening,
  Writing, Speaking) plus Level and Industry
- Add per-module config: timer, CEFR difficulty tags, access type,
  entities, approval workflow, rubric, grading system, shuffling
- Reading: AI passage generation, 5 exercise types (MCQ, Fill Blanks,
  Write Blanks, True/False, Paragraph Match), categories/types
- Listening: 4 section types, AI context generation, TTS audio generation
- Writing: Task 1/2, AI instruction generation, word limits, marks
- Speaking: 3 parts, AI script generation, avatar video generation
  with 7 avatar options
- Wire ExamStructuresPage to real CRUD API (list/create/delete)
- Add backend exam_structure model and controller (/api/exam-structures)
- Enhance ai_controller with 5 specialized generation handlers
  (passage, exercises, writing instructions, speaking script,
  listening context)
- Add POST /api/exam/generation/submit for exam creation workflow
- Fix media.service avatar video endpoint alignment
- All 12 API tests passed, browser-verified with real OpenAI calls

Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-11 14:21:40 +04:00
parent f1c4953a63
commit 140ca7408d
11 changed files with 2134 additions and 103 deletions

View File

@@ -2,21 +2,144 @@ import { api } from "@/lib/api-client";
import type { ExamModule } from "@/types";
export interface GenerationParams {
title: string;
title?: string;
label?: string;
entity_id?: number;
subject_id?: number;
topic_id?: number;
difficulty?: string;
count?: number;
topic?: string;
passage_length?: string;
task_type?: string;
part?: string;
question_count?: number;
}
export interface PassageGenerationParams {
topic?: string;
difficulty?: string;
word_count?: number;
category?: string;
type?: string;
}
export interface ExerciseConfig {
passage_index: number;
exercise_types: string[];
count_per_type?: number;
}
export interface ModuleConfig {
module: ExamModule;
timer_minutes: number;
difficulty: string[];
access_type: string;
entities?: number[];
approval_workflow?: string;
rubric_criteria_group?: string;
rubric_criteria?: string;
grading_system?: string;
shuffling_enabled: boolean;
passages?: PassageConfig[];
sections?: SectionConfig[];
tasks?: TaskConfig[];
speaking_parts?: SpeakingPartConfig[];
}
export interface PassageConfig {
index: number;
category?: string;
type?: string;
divider?: string;
text?: string;
exercise_types: string[];
exercises?: unknown[];
}
export interface SectionConfig {
type: string;
category?: string;
divider?: string;
audio_context?: string;
audio_url?: string;
exercise_types: string[];
exercises?: unknown[];
}
export interface TaskConfig {
index: number;
category?: string;
type?: string;
divider?: string;
instructions?: string;
word_limit: number;
marks: number;
images?: string[];
}
export interface SpeakingPartConfig {
type: string;
category?: string;
divider?: string;
script?: string;
video_url?: string;
avatar_id?: string;
marks: number;
topics?: string[];
}
export const generationService = {
async generate(module: ExamModule, params: GenerationParams): Promise<{ exam_id: number; exercises: unknown[] }> {
generate(module: ExamModule, params: GenerationParams): Promise<{ questions: unknown[] }> {
return api.post(`/exam/${module}/generate`, params);
},
async generateFromScratch(module: ExamModule, params: GenerationParams): Promise<{ exam_id: number; exercises: unknown[] }> {
return api.post(`/exam/${module}/generate/scratch`, params);
saveGenerated(module: ExamModule, data: unknown): Promise<{ saved: number; module: string }> {
const payload = Array.isArray(data) ? { questions: data } : data;
return api.post(`/exam/${module}/generate/save`, payload);
},
generatePassage(params: PassageGenerationParams): Promise<{ passage: string; title?: string }> {
return api.post("/exam/reading/generate", {
...params,
generate_passage: true,
});
},
generateExercises(module: ExamModule, config: ExerciseConfig & { passage_text?: string }): Promise<{ questions: unknown[] }> {
return api.post(`/exam/${module}/generate`, {
...config,
generate_exercises: true,
});
},
generateWritingInstructions(params: { topic?: string; difficulty?: string; task_type?: string }): Promise<{ instructions: string }> {
return api.post("/exam/writing/generate", {
...params,
generate_instructions: true,
});
},
generateSpeakingScript(params: { topics?: string[]; difficulty?: string; part?: string }): Promise<{ script: string }> {
return api.post("/exam/speaking/generate", {
...params,
generate_script: true,
});
},
generateListeningContext(params: { topic?: string; section_type?: string }): Promise<{ context: string }> {
return api.post("/exam/listening/generate", {
...params,
generate_context: true,
});
},
submitExam(data: {
title: string;
label: string;
modules: Record<string, unknown>;
skip_approval?: boolean;
}): Promise<{ exam_id: number; status: string; template_id?: number }> {
return api.post("/exam/generation/submit", data);
},
};

View File

@@ -1,15 +1,31 @@
import { api } from "@/lib/api-client";
export interface Avatar {
id: number | string;
name: string;
thumbnail?: string;
voice?: string;
gender?: string;
}
export const mediaService = {
async generateListeningAudio(data: { text: string; voice_id?: string }): Promise<{ audio_url: string }> {
generateListeningAudio(data: { text: string; voice_id?: string }): Promise<{ audio_url: string; audio_base64?: string; content_type?: string }> {
return api.post("/exam/listening/media", data);
},
async generateSpeakingVideo(data: { text: string; avatar_id?: number }): Promise<{ video_url: string; job_id: string }> {
generateSpeakingAudio(data: { text: string; voice_id?: string }): Promise<{ audio_url: string; audio_base64?: string }> {
return api.post("/exam/speaking/media", data);
},
async getAvatars(): Promise<{ id: number; name: string; thumbnail: string; voice: string }[]> {
getAvatars(): Promise<Avatar[]> {
return api.get("/exam/avatars");
},
createAvatarVideo(data: { script: string; avatar_id: string; title?: string }): Promise<{ video_id: string; status: string }> {
return api.post("/exam/avatar/video", data);
},
getVideoStatus(videoId: string): Promise<{ status: string; video_url?: string; progress?: number }> {
return api.get(`/exam/avatar/video/${videoId}`);
},
};