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:
@@ -1,32 +1,31 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Sparkles, TrendingUp, AlertTriangle, Trophy, Loader2 } from "lucide-react";
|
||||
import { analyticsService } from "@/services/analytics.service";
|
||||
import type { AiInsight } from "@/types";
|
||||
import { Sparkles, TrendingUp, AlertTriangle, Info, Loader2 } from "lucide-react";
|
||||
import { analyticsService, type AiInsightItem } from "@/services/analytics.service";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
const EMPTY_PAYLOAD: Record<string, unknown> = {};
|
||||
|
||||
function insightIcon(type: AiInsight["type"]) {
|
||||
switch (type) {
|
||||
case "positive":
|
||||
return Trophy;
|
||||
case "warning":
|
||||
function insightIcon(severity: AiInsightItem["severity"]) {
|
||||
switch (severity) {
|
||||
case "critical":
|
||||
return AlertTriangle;
|
||||
default:
|
||||
case "warning":
|
||||
return TrendingUp;
|
||||
default:
|
||||
return Info;
|
||||
}
|
||||
}
|
||||
|
||||
function insightColor(type: AiInsight["type"]) {
|
||||
switch (type) {
|
||||
case "positive":
|
||||
return "text-primary";
|
||||
function insightColor(severity: AiInsightItem["severity"]) {
|
||||
switch (severity) {
|
||||
case "critical":
|
||||
return "text-destructive";
|
||||
case "warning":
|
||||
return "text-warning";
|
||||
default:
|
||||
return "text-success";
|
||||
return "text-primary";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,10 +50,10 @@ export default function AiInsightsPanel({ data = EMPTY_PAYLOAD }: Props) {
|
||||
|
||||
useEffect(() => {
|
||||
mutation.mutate(data);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- refetch when serialized payload changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [payloadKey]);
|
||||
|
||||
const items = mutation.data ?? [];
|
||||
const items = mutation.data?.insights ?? [];
|
||||
|
||||
return (
|
||||
<Card className="border-0 shadow-sm">
|
||||
@@ -79,19 +78,19 @@ export default function AiInsightsPanel({ data = EMPTY_PAYLOAD }: Props) {
|
||||
)}
|
||||
{!mutation.isPending && items.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{items.map((item) => {
|
||||
const Icon = insightIcon(item.type);
|
||||
const color = insightColor(item.type);
|
||||
{items.map((item, idx) => {
|
||||
const Icon = insightIcon(item.severity);
|
||||
const color = insightColor(item.severity);
|
||||
return (
|
||||
<div key={item.id} className="rounded-lg border bg-muted/30 p-4">
|
||||
<div key={idx} className="rounded-lg border bg-muted/30 p-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Icon className={`h-4 w-4 ${color}`} />
|
||||
<span className="text-sm font-semibold">{item.title}</span>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{item.description}</p>
|
||||
{item.metric != null && item.value != null && (
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
{item.metric}: {item.value}
|
||||
{item.recommendation && (
|
||||
<p className="text-xs text-muted-foreground mt-2 italic">
|
||||
{item.recommendation}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user