2025-06-21 09:04:52 +00:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
Card,
|
|
|
|
|
CardContent,
|
|
|
|
|
CardDescription,
|
|
|
|
|
CardHeader,
|
|
|
|
|
CardTitle,
|
|
|
|
|
CardFooter,
|
|
|
|
|
} from "@/components/ui/card";
|
|
|
|
|
import { toast } from "sonner";
|
|
|
|
|
import { Skeleton } from "@/components/ui/skeleton";
|
|
|
|
|
import { getAnalysis } from "@/app/actions/analyze";
|
|
|
|
|
import { Loader2Icon, TerminalIcon } from "lucide-react";
|
|
|
|
|
import { useCallback, useEffect, useState } from "react";
|
|
|
|
|
import {
|
|
|
|
|
ChartDataPoint,
|
|
|
|
|
CodeAnalysisRadarChart,
|
|
|
|
|
} from "@/features/problems/analysis/components/radar-chart";
|
|
|
|
|
import type { AnalysisStatus, CodeAnalysis } from "@/generated/client";
|
|
|
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
2026-05-13 07:10:06 +00:00
|
|
|
import { useTranslations } from "next-intl";
|
2025-06-21 09:04:52 +00:00
|
|
|
|
|
|
|
|
interface AnalysisCardProps {
|
|
|
|
|
submissionId: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ACTIVE_STATUSES: AnalysisStatus[] = ["PENDING", "QUEUED", "PROCESSING"];
|
|
|
|
|
const FINAL_STATUSES: AnalysisStatus[] = ["COMPLETED", "FAILED"];
|
|
|
|
|
|
|
|
|
|
export const AnalysisCard = ({ submissionId }: AnalysisCardProps) => {
|
2026-05-13 07:10:06 +00:00
|
|
|
const t = useTranslations("AnalysisCard");
|
2025-06-21 09:04:52 +00:00
|
|
|
const [analysis, setAnalysis] = useState<CodeAnalysis | null>(null);
|
|
|
|
|
|
|
|
|
|
const fetchAnalysis = useCallback(() => {
|
|
|
|
|
getAnalysis(submissionId)
|
|
|
|
|
.then((analysis) => {
|
|
|
|
|
setAnalysis(analysis);
|
|
|
|
|
})
|
|
|
|
|
.catch((error) => {
|
2026-05-13 07:10:06 +00:00
|
|
|
toast.error(t("UpdateFailed"), {
|
|
|
|
|
description: error.message || t("UpdateFailedDescription"),
|
2025-06-21 09:04:52 +00:00
|
|
|
});
|
|
|
|
|
});
|
2026-05-13 07:10:06 +00:00
|
|
|
}, [submissionId, t]);
|
2025-06-21 09:04:52 +00:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!analysis) {
|
|
|
|
|
fetchAnalysis();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const interval = setInterval(() => {
|
|
|
|
|
if (!analysis || ACTIVE_STATUSES.includes(analysis.status)) {
|
|
|
|
|
fetchAnalysis();
|
|
|
|
|
} else if (FINAL_STATUSES.includes(analysis.status)) {
|
|
|
|
|
clearInterval(interval);
|
|
|
|
|
}
|
|
|
|
|
}, 5000);
|
|
|
|
|
|
|
|
|
|
return () => clearInterval(interval);
|
|
|
|
|
}, [analysis, fetchAnalysis]);
|
|
|
|
|
|
|
|
|
|
if (!analysis) {
|
|
|
|
|
return (
|
|
|
|
|
<Card className="w-full max-w-2xl mx-auto shadow-lg rounded-xl overflow-hidden border-0 bg-background/50 backdrop-blur-sm">
|
|
|
|
|
<CardHeader className="items-center pb-2 space-y-1 px-6 pt-6">
|
|
|
|
|
<Skeleton className="h-8 w-48" />
|
|
|
|
|
<Skeleton className="h-4 w-64" />
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="p-6">
|
|
|
|
|
<div className="flex flex-col items-center justify-center space-y-4 min-h-64">
|
|
|
|
|
<Loader2Icon className="h-8 w-8 animate-spin text-primary" />
|
2026-05-13 07:10:06 +00:00
|
|
|
<p className="text-muted-foreground">{t("Analyzing")}</p>
|
2025-06-21 09:04:52 +00:00
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (analysis.status === "FAILED") {
|
|
|
|
|
return (
|
|
|
|
|
<Card className="w-full max-w-2xl mx-auto shadow-lg rounded-xl overflow-hidden border-0 bg-background/50 backdrop-blur-sm">
|
|
|
|
|
<CardHeader className="items-center pb-2 space-y-1 px-6 pt-6">
|
|
|
|
|
<CardTitle className="text-2xl font-bold bg-gradient-to-r from-primary to-foreground bg-clip-text text-transparent">
|
2026-05-13 07:10:06 +00:00
|
|
|
{t("Title")}
|
2025-06-21 09:04:52 +00:00
|
|
|
</CardTitle>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="p-6">
|
|
|
|
|
<Alert variant="destructive">
|
|
|
|
|
<TerminalIcon className="h-4 w-4" />
|
2026-05-13 07:10:06 +00:00
|
|
|
<AlertTitle>{t("FailedTitle")}</AlertTitle>
|
2025-06-21 09:04:52 +00:00
|
|
|
<AlertDescription>
|
2026-05-13 07:10:06 +00:00
|
|
|
{t("FailedDescription")}
|
2025-06-21 09:04:52 +00:00
|
|
|
</AlertDescription>
|
|
|
|
|
</Alert>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (analysis.status !== "COMPLETED") {
|
|
|
|
|
return (
|
|
|
|
|
<Card className="w-full max-w-2xl mx-auto shadow-lg rounded-xl overflow-hidden border-0 bg-background/50 backdrop-blur-sm">
|
|
|
|
|
<CardHeader className="items-center pb-2 space-y-1 px-6 pt-6">
|
|
|
|
|
<CardTitle className="text-2xl font-bold bg-gradient-to-r from-primary to-foreground bg-clip-text text-transparent">
|
2026-05-13 07:10:06 +00:00
|
|
|
{t("Title")}
|
2025-06-21 09:04:52 +00:00
|
|
|
</CardTitle>
|
|
|
|
|
<CardDescription className="text-muted-foreground">
|
2026-05-13 07:10:06 +00:00
|
|
|
{t("PreparingDescription")}
|
2025-06-21 09:04:52 +00:00
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="p-6">
|
|
|
|
|
<div className="flex flex-col items-center justify-center space-y-4 min-h-64">
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<Loader2Icon className="h-8 w-8 animate-spin text-primary" />
|
|
|
|
|
<span className="absolute inset-0 rounded-full bg-primary/10 animate-ping"></span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="space-y-2 text-center">
|
|
|
|
|
<p className="text-muted-foreground">
|
2026-05-13 07:10:06 +00:00
|
|
|
{t("Processing")}
|
2025-06-21 09:04:52 +00:00
|
|
|
</p>
|
|
|
|
|
<p className="text-sm text-muted-foreground/60">
|
2026-05-13 07:10:06 +00:00
|
|
|
{t("ProcessingHint")}
|
2025-06-21 09:04:52 +00:00
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</CardContent>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Transform the data into a format suitable for the RadarChart
|
|
|
|
|
const chartData: ChartDataPoint[] = [
|
|
|
|
|
{
|
2026-05-13 07:10:06 +00:00
|
|
|
kind: t("Kinds.Overall"),
|
2025-06-21 09:04:52 +00:00
|
|
|
score: analysis.overallScore ?? 0,
|
|
|
|
|
fullMark: 100,
|
|
|
|
|
},
|
|
|
|
|
{
|
2026-05-13 07:10:06 +00:00
|
|
|
kind: t("Kinds.Style"),
|
2025-06-21 09:04:52 +00:00
|
|
|
score: analysis.styleScore ?? 0,
|
|
|
|
|
fullMark: 100,
|
|
|
|
|
},
|
|
|
|
|
{
|
2026-05-13 07:10:06 +00:00
|
|
|
kind: t("Kinds.Readability"),
|
2025-06-21 09:04:52 +00:00
|
|
|
score: analysis.readabilityScore ?? 0,
|
|
|
|
|
fullMark: 100,
|
|
|
|
|
},
|
|
|
|
|
{
|
2026-05-13 07:10:06 +00:00
|
|
|
kind: t("Kinds.Efficiency"),
|
2025-06-21 09:04:52 +00:00
|
|
|
score: analysis.efficiencyScore ?? 0,
|
|
|
|
|
fullMark: 100,
|
|
|
|
|
},
|
|
|
|
|
{
|
2026-05-13 07:10:06 +00:00
|
|
|
kind: t("Kinds.Correctness"),
|
2025-06-21 09:04:52 +00:00
|
|
|
score: analysis.correctnessScore ?? 0,
|
|
|
|
|
fullMark: 100,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Card className="w-full max-w-2xl mx-auto shadow-lg rounded-xl overflow-hidden border-0 bg-background/50 backdrop-blur-sm animate-fade-in">
|
|
|
|
|
<CardHeader className="items-center pb-2 space-y-1 px-6 pt-6">
|
|
|
|
|
<CardTitle className="text-2xl font-bold bg-gradient-to-r from-primary to-foreground bg-clip-text text-transparent">
|
2026-05-13 07:10:06 +00:00
|
|
|
{t("Title")}
|
2025-06-21 09:04:52 +00:00
|
|
|
</CardTitle>
|
|
|
|
|
<CardDescription className="text-muted-foreground">
|
2026-05-13 07:10:06 +00:00
|
|
|
{t("CompletedDescription")}
|
2025-06-21 09:04:52 +00:00
|
|
|
</CardDescription>
|
|
|
|
|
</CardHeader>
|
|
|
|
|
<CardContent className="p-0">
|
2026-05-13 07:10:06 +00:00
|
|
|
<CodeAnalysisRadarChart chartData={chartData} radarName={t("Score")} />
|
2025-06-21 09:04:52 +00:00
|
|
|
</CardContent>
|
|
|
|
|
|
|
|
|
|
<CardFooter className="flex-col items-start gap-4 p-6 pt-0">
|
|
|
|
|
<div className="w-full space-y-3">
|
|
|
|
|
<div className="flex justify-between text-sm font-medium">
|
2026-05-13 07:10:06 +00:00
|
|
|
<span className="text-muted-foreground">{t("OverallScore")}</span>
|
2025-06-21 09:04:52 +00:00
|
|
|
<span className="text-primary">
|
2026-05-13 07:10:06 +00:00
|
|
|
{analysis.overallScore ?? t("NotAvailable")}
|
2025-06-21 09:04:52 +00:00
|
|
|
<span className="text-muted-foreground">/100</span>
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="relative h-2.5 w-full overflow-hidden rounded-full bg-muted">
|
|
|
|
|
<div
|
|
|
|
|
className="h-full bg-gradient-to-r from-primary to-purple-500 rounded-full transition-all duration-700 ease-out"
|
|
|
|
|
style={{
|
|
|
|
|
width: `${analysis.overallScore ?? 0}%`,
|
|
|
|
|
transitionProperty: "width",
|
|
|
|
|
}}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="text-muted-foreground bg-muted/40 p-4 rounded-lg w-full border">
|
2026-05-13 07:10:06 +00:00
|
|
|
<h3 className="font-medium mb-2 text-foreground">{t("Feedback")}</h3>
|
2025-06-21 09:04:52 +00:00
|
|
|
<p className="whitespace-pre-wrap leading-relaxed">
|
|
|
|
|
{analysis.feedback}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</CardFooter>
|
|
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
};
|