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:
@@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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}`);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user