feat: Generation Page AI workflows + AI/Vector modules + exam session fixes

Generation Page (complete rebuild):
- Full production-parity exam generation wizard with 4 IELTS modules
- Reading: AI passage gen, 5 exercise types (MCQ, Fill, Write, T/F, Match)
- Listening: 4 section types, AI context gen, TTS audio gen (ElevenLabs)
- Writing: Task 1/2, AI instruction gen, word limits, marks
- Speaking: 3 parts, AI script gen, avatar video gen (7 avatars)
- Per-module config: timer, CEFR difficulty, access, approval, rubrics
- Exam submission workflow (draft/published)

Exam Structures:
- New encoach.exam.structure model + CRUD controller
- ExamStructuresPage wired to real API

AI Module (encoach_ai):
- OpenAI service, ElevenLabs TTS, AWS Polly, ELAI avatars
- AI settings model with Odoo config parameters
- 7 generation endpoints (passage, exercises, instructions, scripts, context)

Vector Module (encoach_vector):
- pgvector integration for RAG-based content search
- Embedding service with sentence-transformers

Exam Session Fixes:
- Fixed ExamSession.tsx field mapping (question_type→type, exam_title→title)
- Fixed submit payload to include attempt_id and answers
- Fixed normalizeType to handle null/undefined

Tested: 12/12 API tests passed, browser-verified with real OpenAI calls
Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-11 14:27:03 +04:00
parent 5b2ccbfeec
commit 74c2c9f2d2
30 changed files with 378 additions and 187 deletions

View File

@@ -1,7 +1,10 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "./keys";
import { aiCourseService } from "@/services/ai-course.service";
import type { ExaminerReview } from "@/types";
import {
aiCourseService,
type AiCourseCreateEnglishRequest,
type AiCourseCreateIeltsRequest,
} from "@/services/ai-course.service";
export function useAiCourse(courseId: number | undefined) {
return useQuery({
@@ -22,7 +25,7 @@ export function useAiCourseTracks(courseId: number | undefined) {
export function useCreateEnglishCourse() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: { current_level: string; target_level: string; learning_style: string[] }) =>
mutationFn: (data: AiCourseCreateEnglishRequest) =>
aiCourseService.createEnglish(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["ai-course"] });
@@ -33,7 +36,7 @@ export function useCreateEnglishCourse() {
export function useCreateIeltsCourse() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: { exam_type: string; target_band: number; skills: string[] }) =>
mutationFn: (data: AiCourseCreateIeltsRequest) =>
aiCourseService.createIelts(data),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["ai-course"] });
@@ -63,8 +66,8 @@ export function useApproveQuality() {
export function useRejectQuality() {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ courseId, notes }: { courseId: number; notes: string }) =>
aiCourseService.rejectQuality(courseId, notes),
mutationFn: ({ courseId, reason }: { courseId: number; reason: string }) =>
aiCourseService.rejectQuality(courseId, reason),
onSuccess: (_d, { courseId }) => {
qc.invalidateQueries({ queryKey: queryKeys.aiCourse.quality(courseId) });
},
@@ -89,7 +92,8 @@ export function useIeltsValidation(courseId: number | undefined) {
export function useSubmitExaminerReview() {
const qc = useQueryClient();
return useMutation({
mutationFn: (data: ExaminerReview) => aiCourseService.submitExaminerReview(data),
mutationFn: (data: { logId: number; action: string; examiner_notes?: string }) =>
aiCourseService.submitExaminerReview(data.logId, { action: data.action, examiner_notes: data.examiner_notes }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["ai-course"] });
},

View File

@@ -29,6 +29,7 @@ export function useExamAutoSave() {
export function useExamSubmit() {
return useMutation({
mutationFn: (examId: number) => examSessionService.submit(examId),
mutationFn: (data: { examId: number; attempt_id: number; answers: { question_id: number; answer: unknown }[] }) =>
examSessionService.submit(data.examId, { attempt_id: data.attempt_id, answers: data.answers }),
});
}