diff --git a/src/components/ai/AiAlertBanner.tsx b/src/components/ai/AiAlertBanner.tsx index e14ec37..f60a9ad 100644 --- a/src/components/ai/AiAlertBanner.tsx +++ b/src/components/ai/AiAlertBanner.tsx @@ -8,12 +8,13 @@ export default function AiAlertBanner() { const [dismissedIds, setDismissedIds] = useState>(() => new Set()); const [errorDismissed, setErrorDismissed] = useState(false); - const { data: alerts, isLoading, isError, error } = useQuery({ + const { data: resp, isLoading, isError, error } = useQuery({ queryKey: ["ai", "alerts"], queryFn: () => analyticsService.getAlerts(), }); - const visible = alerts?.filter((a) => !dismissedIds.has(a.id)) ?? []; + const alerts = resp?.alerts ?? []; + const visible = alerts.filter((a, i) => !dismissedIds.has(String(i))); if (isLoading) { return ( @@ -43,7 +44,7 @@ export default function AiAlertBanner() { if (isError && errorDismissed) return null; - if (!alerts?.length) { + if (!alerts.length) { return (
@@ -56,8 +57,8 @@ export default function AiAlertBanner() { return (
- {visible.map((alert) => ( -
+ {visible.map((alert, idx) => ( +

@@ -69,7 +70,7 @@ export default function AiAlertBanner() { variant="ghost" size="icon" className="h-7 w-7 shrink-0" - onClick={() => setDismissedIds((prev) => new Set(prev).add(alert.id))} + onClick={() => setDismissedIds((prev) => new Set(prev).add(String(idx)))} > diff --git a/src/components/ai/AiAssistantDrawer.tsx b/src/components/ai/AiAssistantDrawer.tsx index 05ccaf7..7e52fc4 100644 --- a/src/components/ai/AiAssistantDrawer.tsx +++ b/src/components/ai/AiAssistantDrawer.tsx @@ -26,7 +26,7 @@ export default function AiAssistantDrawer() { mutationFn: (message: string) => coachingService.chat({ message, context: { page: location.pathname } }), onSuccess: (data) => { - setMessages((prev) => [...prev, { role: "ai", text: data.message }]); + setMessages((prev) => [...prev, { role: "ai", text: data.reply }]); }, onError: (err: Error) => { toast({ diff --git a/src/components/ai/AiBatchOptimizer.tsx b/src/components/ai/AiBatchOptimizer.tsx index 7a10079..ed222ba 100644 --- a/src/components/ai/AiBatchOptimizer.tsx +++ b/src/components/ai/AiBatchOptimizer.tsx @@ -25,6 +25,8 @@ export default function AiBatchOptimizer({ batchId }: Props) { }, }); + type OptResult = Awaited>; + const handleOpen = () => { if (batchId == null) { toast({ @@ -39,9 +41,23 @@ export default function AiBatchOptimizer({ batchId }: Props) { mutation.mutate(batchId); }; + const applyMutation = useMutation({ + mutationFn: () => analyticsService.applyBatchOptimization(batchId!, mutation.data?.optimized ?? []), + onSuccess: (res) => { + toast({ title: "Suggestion Applied", description: `${res.applied} optimization(s) saved successfully.` }); + setOpen(false); + }, + onError: (err: Error) => { + toast({ + variant: "destructive", + title: "Apply failed", + description: err.message || "Could not apply batch optimization.", + }); + }, + }); + const handleApply = () => { - toast({ title: "Suggestion Applied", description: "Batch split recommendation has been saved successfully." }); - setOpen(false); + applyMutation.mutate(); }; const onOpenChange = (next: boolean) => { @@ -49,9 +65,10 @@ export default function AiBatchOptimizer({ batchId }: Props) { if (!next) mutation.reset(); }; - const suggestions = mutation.data ?? []; - const showResults = !mutation.isPending && !mutation.isError && suggestions.length > 0; - const showEmpty = !mutation.isPending && !mutation.isError && mutation.isSuccess && suggestions.length === 0; + const optData = mutation.data as OptResult | undefined; + const hasSuggestions = !!optData?.summary; + const showResults = !mutation.isPending && !mutation.isError && hasSuggestions; + const showEmpty = !mutation.isPending && !mutation.isError && mutation.isSuccess && !hasSuggestions; return ( <> @@ -71,20 +88,28 @@ export default function AiBatchOptimizer({ batchId }: Props) {

) : mutation.isError ? (

Something went wrong. Try again.

- ) : showResults ? ( + ) : showResults && optData ? (
-
- {suggestions.map((s, i) => ( -
-

{s.impact} impact

-

{s.suggestion}

- {s.details ?

{s.details}

: null} -
- ))} +
+

{optData.impact} impact

+

{optData.summary}

+ {Array.isArray(optData.optimized) && optData.optimized.length > 0 && ( +
+ {optData.optimized.map((item, i) => ( +
+ {typeof item === "object" && item !== null ? JSON.stringify(item) : String(item)} +
+ ))} +
+ )}
- + - )} -
+
+ +

{result.answer}

+
+ {result.suggestions?.length > 0 && ( +
+

Related queries

+ {result.suggestions.map((s, i) => ( + + ))}
+ )} + {result.related_actions?.map((a, i) => ( + ))}
) : ( diff --git a/src/components/ai/AiStudyCoach.tsx b/src/components/ai/AiStudyCoach.tsx index 5b4f7cc..c5c9689 100644 --- a/src/components/ai/AiStudyCoach.tsx +++ b/src/components/ai/AiStudyCoach.tsx @@ -29,8 +29,11 @@ export default function AiStudyCoach() { suggestMutation.mutate(); }; - const suggestions = suggestMutation.data?.suggestions ?? []; - const planTips = suggestMutation.data?.study_plan_tips ?? []; + const d = suggestMutation.data; + const suggestions = d ? [d.suggestion, ...(d.focus_areas ?? []).map((a: string) => `Focus area: ${a}`)].filter(Boolean) : []; + const planTips = d?.daily_plan?.length + ? d.daily_plan.map((p: { activity: string; duration_min: number; skill: string }) => `${p.activity} (${p.duration_min}min — ${p.skill})`) + : d?.motivation ? [d.motivation] : []; return ( diff --git a/src/components/ai/AiTipBanner.tsx b/src/components/ai/AiTipBanner.tsx index 4e51ef3..3484e70 100644 --- a/src/components/ai/AiTipBanner.tsx +++ b/src/components/ai/AiTipBanner.tsx @@ -50,7 +50,7 @@ export default function AiTipBanner({ context = "dashboard", variant = "tip", di ); } - if (!data.content?.trim() && !data.title?.trim()) { + if (!data.tip?.trim()) { return (
@@ -62,14 +62,16 @@ export default function AiTipBanner({ context = "dashboard", variant = "tip", di ); } + const label = data.category && data.category !== "general" + ? `AI ${data.category.charAt(0).toUpperCase() + data.category.slice(1)} Tip` + : `AI ${variant === "tip" ? "Tip" : variant === "insight" ? "Insight" : "Recommendation"}`; + return (
- - {data.title?.trim() || `AI ${variant === "tip" ? "Tip" : variant === "insight" ? "Insight" : "Recommendation"}`} - -

{data.content}

+ {label} +

{data.tip}

{dismissible && (
)} @@ -128,9 +129,9 @@ export default function AiWritingHelper({ text, task_type = "ielts_writing" }: P

Estimated band / assessment

-

{mutation.data.feedback}

- {mutation.data.improved ? ( -

{mutation.data.improved}

+

{mutation.data.tips?.join(" ") ?? ""}

+ {mutation.data.improved_text ? ( +

{mutation.data.improved_text}

) : null}
)} diff --git a/src/hooks/queries/useAiCourse.ts b/src/hooks/queries/useAiCourse.ts index c76e139..9e1b006 100644 --- a/src/hooks/queries/useAiCourse.ts +++ b/src/hooks/queries/useAiCourse.ts @@ -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"] }); }, diff --git a/src/hooks/queries/useExamSession.ts b/src/hooks/queries/useExamSession.ts index 541bb6f..5532891 100644 --- a/src/hooks/queries/useExamSession.ts +++ b/src/hooks/queries/useExamSession.ts @@ -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 }), }); } diff --git a/src/pages/ExamPage.tsx b/src/pages/ExamPage.tsx index 3c933bf..0bd5858 100644 --- a/src/pages/ExamPage.tsx +++ b/src/pages/ExamPage.tsx @@ -7,7 +7,7 @@ import AiTipBanner from "@/components/ai/AiTipBanner"; export default function ExamPage() { return (
- + diff --git a/src/pages/GrammarPage.tsx b/src/pages/GrammarPage.tsx index aaa06f3..1536bb5 100644 --- a/src/pages/GrammarPage.tsx +++ b/src/pages/GrammarPage.tsx @@ -28,7 +28,7 @@ export default function GrammarPage() {

Master grammar rules essential for IELTS.

- +
diff --git a/src/pages/PaymentRecordPage.tsx b/src/pages/PaymentRecordPage.tsx index 3de5279..ddb2454 100644 --- a/src/pages/PaymentRecordPage.tsx +++ b/src/pages/PaymentRecordPage.tsx @@ -52,7 +52,7 @@ export default function PaymentRecordPage() {
- + @@ -61,7 +61,7 @@ export default function PaymentRecordPage() { - + diff --git a/src/pages/RecordPage.tsx b/src/pages/RecordPage.tsx index 7da0b1b..1217957 100644 --- a/src/pages/RecordPage.tsx +++ b/src/pages/RecordPage.tsx @@ -21,9 +21,9 @@ export default function RecordPage() {

Browse assignment and exam attempt history.

- + - +