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
82 lines
3.0 KiB
TypeScript
82 lines
3.0 KiB
TypeScript
import { useState } from "react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { AlertTriangle, X, Sparkles, Loader2 } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { analyticsService } from "@/services/analytics.service";
|
|
|
|
export default function AiAlertBanner() {
|
|
const [dismissedIds, setDismissedIds] = useState<Set<string>>(() => new Set());
|
|
const [errorDismissed, setErrorDismissed] = useState(false);
|
|
|
|
const { data: resp, isLoading, isError, error } = useQuery({
|
|
queryKey: ["ai", "alerts"],
|
|
queryFn: () => analyticsService.getAlerts(),
|
|
});
|
|
|
|
const alerts = resp?.alerts ?? [];
|
|
const visible = alerts.filter((a, i) => !dismissedIds.has(String(i)));
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="rounded-lg border border-warning/30 bg-warning/10 p-4 flex items-center gap-3">
|
|
<Loader2 className="h-5 w-5 animate-spin text-warning shrink-0" />
|
|
<p className="text-sm text-muted-foreground">Loading alerts…</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isError && !errorDismissed) {
|
|
return (
|
|
<div className="rounded-lg border border-warning/30 bg-warning/10 p-4 flex items-start gap-3">
|
|
<AlertTriangle className="h-5 w-5 text-warning shrink-0 mt-0.5" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium flex items-center gap-1">
|
|
<Sparkles className="h-3 w-3 text-primary" /> Alerts unavailable
|
|
</p>
|
|
<p className="text-sm text-muted-foreground mt-1">{error instanceof Error ? error.message : "Could not load alerts."}</p>
|
|
</div>
|
|
<Button variant="ghost" size="icon" className="h-7 w-7 shrink-0" onClick={() => setErrorDismissed(true)}>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isError && errorDismissed) return null;
|
|
|
|
if (!alerts.length) {
|
|
return (
|
|
<div className="rounded-lg border border-muted bg-muted/20 p-4 flex items-start gap-3">
|
|
<Sparkles className="h-5 w-5 text-muted-foreground shrink-0 mt-0.5" />
|
|
<p className="text-sm text-muted-foreground">No AI alerts right now.</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!visible.length) return null;
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{visible.map((alert, idx) => (
|
|
<div key={idx} className="rounded-lg border border-warning/30 bg-warning/10 p-4 flex items-start gap-3">
|
|
<AlertTriangle className="h-5 w-5 text-warning shrink-0 mt-0.5" />
|
|
<div className="flex-1">
|
|
<p className="text-sm font-medium flex items-center gap-1">
|
|
<Sparkles className="h-3 w-3 text-primary" /> {alert.title}
|
|
</p>
|
|
<p className="text-sm text-muted-foreground mt-1">{alert.description}</p>
|
|
</div>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="h-7 w-7 shrink-0"
|
|
onClick={() => setDismissedIds((prev) => new Set(prev).add(String(idx)))}
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|