Full React 18 + TypeScript + Vite frontend with: - 90+ pages (admin, student, teacher, public) - shadcn/ui component library with 50+ components - JWT authentication with role-based access control - TanStack React Query for server state management - 30+ API service modules - AI-powered features (coaching, grading, generation) - Adaptive learning UI (diagnostics, proficiency, plans) - Institutional LMS management (courses, batches, timetable) - Communication suite (discussions, announcements, DMs) - Full CRUD with validation and confirmation dialogs Made-with: Cursor
101 lines
3.4 KiB
TypeScript
101 lines
3.4 KiB
TypeScript
import { useEffect } from "react";
|
|
import { useMutation } from "@tanstack/react-query";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Sparkles, Loader2 } from "lucide-react";
|
|
import { analyticsService } from "@/services/analytics.service";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
|
|
interface Props {
|
|
onAccept: (marks: number, feedback: string) => void;
|
|
submissionId?: number;
|
|
submissionText?: string;
|
|
rubricId?: number;
|
|
}
|
|
|
|
const DEFAULT_TEXT =
|
|
"Sample submission for AI grading suggestion. Replace by passing submissionText when integrating with real submissions.";
|
|
|
|
export default function AiGradingAssistant({
|
|
onAccept,
|
|
submissionId = 1,
|
|
submissionText = DEFAULT_TEXT,
|
|
rubricId,
|
|
}: Props) {
|
|
const { toast } = useToast();
|
|
|
|
const gradeMutation = useMutation({
|
|
mutationFn: () =>
|
|
analyticsService.getGradingSuggestion({
|
|
submission_id: submissionId,
|
|
text: submissionText,
|
|
...(rubricId !== undefined ? { rubric_id: rubricId } : {}),
|
|
}),
|
|
onError: (err: Error) => {
|
|
toast({
|
|
variant: "destructive",
|
|
title: "Could not load AI grade",
|
|
description: err.message || "Try again later.",
|
|
});
|
|
},
|
|
});
|
|
|
|
useEffect(() => {
|
|
gradeMutation.mutate();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- refetch when inputs change
|
|
}, [submissionId, submissionText, rubricId]);
|
|
|
|
const data = gradeMutation.data;
|
|
const marks = data ? Math.round(data.overall_score) : 0;
|
|
const feedbackBlock = data
|
|
? [
|
|
data.feedback,
|
|
data.suggestions?.length
|
|
? `Suggestions:\n${data.suggestions.map((s) => `• ${s}`).join("\n")}`
|
|
: "",
|
|
]
|
|
.filter(Boolean)
|
|
.join("\n\n")
|
|
: "";
|
|
|
|
return (
|
|
<div className="rounded-lg border bg-muted/30 p-4 space-y-3">
|
|
<p className="text-xs font-semibold text-primary flex items-center gap-1">
|
|
<Sparkles className="h-3 w-3" /> AI Suggested Grade
|
|
</p>
|
|
{gradeMutation.isPending ? (
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground py-2">
|
|
<Loader2 className="h-4 w-4 animate-spin text-primary" /> Analyzing submission...
|
|
</div>
|
|
) : gradeMutation.isError ? (
|
|
<p className="text-sm text-destructive">Could not load a suggestion. Check the console or try again.</p>
|
|
) : data ? (
|
|
<>
|
|
<div>
|
|
<p className="text-sm text-muted-foreground">Suggested marks</p>
|
|
<p className="text-2xl font-bold">{marks}/100</p>
|
|
</div>
|
|
<div>
|
|
<p className="text-sm text-muted-foreground mb-1">Suggested feedback</p>
|
|
<p className="text-sm whitespace-pre-wrap">{feedbackBlock}</p>
|
|
</div>
|
|
{data.scores && Object.keys(data.scores).length > 0 && (
|
|
<div className="space-y-1">
|
|
<p className="text-sm text-muted-foreground">Rubric scores</p>
|
|
<ul className="text-xs text-muted-foreground space-y-0.5">
|
|
{Object.entries(data.scores).map(([k, v]) => (
|
|
<li key={k}>
|
|
{k}: {v}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
<Button size="sm" onClick={() => onAccept(marks, feedbackBlock)}>
|
|
<Sparkles className="h-3.5 w-3.5 mr-1" /> Accept AI Grade
|
|
</Button>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|