mirror of
https://github.com/massbug/judge4c.git
synced 2025-07-03 23:30:50 +00:00
feat(analysis): implement real-time code analysis with async processing and status tracking
This commit is contained in:
parent
ab598459a2
commit
e83a1165da
@ -0,0 +1,5 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "AnalysisStatus" AS ENUM ('PENDING', 'QUEUED', 'PROCESSING', 'COMPLETED', 'FAILED');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "CodeAnalysis" ADD COLUMN "status" "AnalysisStatus" NOT NULL DEFAULT 'PENDING';
|
@ -142,10 +142,20 @@ model Submission {
|
|||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AnalysisStatus {
|
||||||
|
PENDING
|
||||||
|
QUEUED
|
||||||
|
PROCESSING
|
||||||
|
COMPLETED
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
|
|
||||||
model CodeAnalysis {
|
model CodeAnalysis {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
submissionId String @unique
|
submissionId String @unique
|
||||||
|
|
||||||
|
status AnalysisStatus @default(PENDING)
|
||||||
|
|
||||||
timeComplexity String?
|
timeComplexity String?
|
||||||
spaceComplexity String?
|
spaceComplexity String?
|
||||||
|
|
||||||
|
@ -15,8 +15,21 @@ export const analyzeCode = async ({
|
|||||||
content,
|
content,
|
||||||
submissionId,
|
submissionId,
|
||||||
}: analyzeCodeProps) => {
|
}: analyzeCodeProps) => {
|
||||||
|
const analysis = await prisma.codeAnalysis.create({
|
||||||
|
data: {
|
||||||
|
submissionId,
|
||||||
|
status: "QUEUED"
|
||||||
|
},
|
||||||
|
});
|
||||||
try {
|
try {
|
||||||
const result = await generateText({
|
await prisma.codeAnalysis.update({
|
||||||
|
where: { id: analysis.id },
|
||||||
|
data: {
|
||||||
|
status: "PROCESSING"
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await generateText({
|
||||||
model: deepseek("deepseek-chat"),
|
model: deepseek("deepseek-chat"),
|
||||||
system: `You are an AI assistant that rigorously analyzes code for time and space complexity, and assesses overall code quality.
|
system: `You are an AI assistant that rigorously analyzes code for time and space complexity, and assesses overall code quality.
|
||||||
|
|
||||||
@ -114,9 +127,12 @@ export const analyzeCode = async ({
|
|||||||
correctnessScore,
|
correctnessScore,
|
||||||
feedback,
|
feedback,
|
||||||
}) => {
|
}) => {
|
||||||
const codeAnalysis = await prisma.codeAnalysis.create({
|
await prisma.codeAnalysis.update({
|
||||||
|
where: {
|
||||||
|
id: analysis.id
|
||||||
|
},
|
||||||
data: {
|
data: {
|
||||||
submissionId,
|
status: "COMPLETED",
|
||||||
timeComplexity,
|
timeComplexity,
|
||||||
spaceComplexity,
|
spaceComplexity,
|
||||||
overallScore,
|
overallScore,
|
||||||
@ -127,20 +143,21 @@ export const analyzeCode = async ({
|
|||||||
feedback,
|
feedback,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: "Code analysis saved successfully",
|
|
||||||
data: codeAnalysis,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
toolChoice: { type: "tool", toolName: "saveCodeAnalysis" },
|
toolChoice: { type: "tool", toolName: "saveCodeAnalysis" },
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error analyzing code:", error);
|
console.error("Error analyzing code:", error);
|
||||||
|
await prisma.codeAnalysis.update({
|
||||||
|
where: {
|
||||||
|
id: analysis.id
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: "FAILED"
|
||||||
|
},
|
||||||
|
});
|
||||||
throw new Error("Failed to analyze code");
|
throw new Error("Failed to analyze code");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -5,8 +5,11 @@ import {
|
|||||||
AnalyzeComplexityResponseSchema,
|
AnalyzeComplexityResponseSchema,
|
||||||
Complexity,
|
Complexity,
|
||||||
} from "@/types/complexity";
|
} from "@/types/complexity";
|
||||||
|
import prisma from "@/lib/prisma";
|
||||||
import { openai } from "@/lib/ai";
|
import { openai } from "@/lib/ai";
|
||||||
|
import { auth } from "@/lib/auth";
|
||||||
import { CoreMessage, generateText } from "ai";
|
import { CoreMessage, generateText } from "ai";
|
||||||
|
import { CodeAnalysis } from "@/generated/client";
|
||||||
|
|
||||||
export const analyzeComplexity = async (
|
export const analyzeComplexity = async (
|
||||||
content: string
|
content: string
|
||||||
@ -62,3 +65,23 @@ export const analyzeComplexity = async (
|
|||||||
|
|
||||||
return validationResult.data;
|
return validationResult.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getAnalysis = async (submissionId: string):Promise<CodeAnalysis> => {
|
||||||
|
const session = await auth();
|
||||||
|
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
throw new Error(
|
||||||
|
"Authentication required: Please log in to submit code for analysis"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const analysis = await prisma.codeAnalysis.findUnique({
|
||||||
|
where: { submissionId: submissionId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!analysis) {
|
||||||
|
throw new Error("Analysis not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
return analysis;
|
||||||
|
};
|
||||||
|
@ -120,10 +120,14 @@ export const judge = async (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await analyzeCode({
|
const executeAnalyzeCode = async () => {
|
||||||
content,
|
await analyzeCode({
|
||||||
submissionId: submission.id,
|
content,
|
||||||
});
|
submissionId: submission.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
executeAnalyzeCode()
|
||||||
|
|
||||||
// Upload code to the container
|
// Upload code to the container
|
||||||
const tarStream = createTarStream(
|
const tarStream = createTarStream(
|
||||||
|
202
src/features/problems/analysis/components/card.tsx
Normal file
202
src/features/problems/analysis/components/card.tsx
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
"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";
|
||||||
|
|
||||||
|
interface AnalysisCardProps {
|
||||||
|
submissionId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACTIVE_STATUSES: AnalysisStatus[] = ["PENDING", "QUEUED", "PROCESSING"];
|
||||||
|
const FINAL_STATUSES: AnalysisStatus[] = ["COMPLETED", "FAILED"];
|
||||||
|
|
||||||
|
export const AnalysisCard = ({ submissionId }: AnalysisCardProps) => {
|
||||||
|
const [analysis, setAnalysis] = useState<CodeAnalysis | null>(null);
|
||||||
|
|
||||||
|
const fetchAnalysis = useCallback(() => {
|
||||||
|
getAnalysis(submissionId)
|
||||||
|
.then((analysis) => {
|
||||||
|
setAnalysis(analysis);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
toast.error("Analysis Update Failed", {
|
||||||
|
description: error.message || "Failed to fetch analysis data.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [submissionId]);
|
||||||
|
|
||||||
|
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" />
|
||||||
|
<p className="text-muted-foreground">Analyzing your code...</p>
|
||||||
|
</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">
|
||||||
|
Code Analysis
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-6">
|
||||||
|
<Alert variant="destructive">
|
||||||
|
<TerminalIcon className="h-4 w-4" />
|
||||||
|
<AlertTitle>Analysis Failed</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
We couldn't analyze your code. Please try again later.
|
||||||
|
</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">
|
||||||
|
Code Analysis
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-muted-foreground">
|
||||||
|
Preparing your detailed evaluation
|
||||||
|
</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">
|
||||||
|
Processing your submission
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground/60">
|
||||||
|
This may take a few moments...
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transform the data into a format suitable for the RadarChart
|
||||||
|
const chartData: ChartDataPoint[] = [
|
||||||
|
{
|
||||||
|
kind: "overall",
|
||||||
|
score: analysis.overallScore ?? 0,
|
||||||
|
fullMark: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "style",
|
||||||
|
score: analysis.styleScore ?? 0,
|
||||||
|
fullMark: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "readability",
|
||||||
|
score: analysis.readabilityScore ?? 0,
|
||||||
|
fullMark: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "efficiency",
|
||||||
|
score: analysis.efficiencyScore ?? 0,
|
||||||
|
fullMark: 100,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
kind: "correctness",
|
||||||
|
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">
|
||||||
|
Code Analysis
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription className="text-muted-foreground">
|
||||||
|
Detailed evaluation of your code submission
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<CodeAnalysisRadarChart chartData={chartData} />
|
||||||
|
</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">
|
||||||
|
<span className="text-muted-foreground">Overall Score</span>
|
||||||
|
<span className="text-primary">
|
||||||
|
{analysis.overallScore ?? "N/A"}
|
||||||
|
<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">
|
||||||
|
<h3 className="font-medium mb-2 text-foreground">Feedback</h3>
|
||||||
|
<p className="whitespace-pre-wrap leading-relaxed">
|
||||||
|
{analysis.feedback}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
@ -1,24 +1,10 @@
|
|||||||
import {
|
import { AnalysisCard } from "@/features/problems/analysis/components/card";
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardDescription,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
CardFooter,
|
|
||||||
} from "@/components/ui/card";
|
|
||||||
import prisma from "@/lib/prisma";
|
|
||||||
import {
|
|
||||||
ChartDataPoint,
|
|
||||||
CodeAnalysisRadarChart,
|
|
||||||
} from "@/features/problems/analysis/components/radar-chart";
|
|
||||||
|
|
||||||
interface AnalysisContentProps {
|
interface AnalysisContentProps {
|
||||||
submissionId: string | undefined;
|
submissionId: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AnalysisContent = async ({
|
export const AnalysisContent = ({ submissionId }: AnalysisContentProps) => {
|
||||||
submissionId,
|
|
||||||
}: AnalysisContentProps) => {
|
|
||||||
if (!submissionId) {
|
if (!submissionId) {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 text-center text-muted-foreground">
|
<div className="p-4 text-center text-muted-foreground">
|
||||||
@ -27,90 +13,5 @@ export const AnalysisContent = async ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const codeAnalysisData = await prisma.codeAnalysis.findUnique({
|
return <AnalysisCard submissionId={submissionId} />;
|
||||||
where: {
|
|
||||||
submissionId: submissionId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!codeAnalysisData) {
|
|
||||||
return (
|
|
||||||
<div className="p-4 text-center text-muted-foreground">
|
|
||||||
No analysis data found for this submission.
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transform the data into a format suitable for the RadarChart
|
|
||||||
const chartData: ChartDataPoint[] = [
|
|
||||||
{
|
|
||||||
kind: "overall",
|
|
||||||
score: codeAnalysisData.overallScore ?? 0,
|
|
||||||
fullMark: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "style",
|
|
||||||
score: codeAnalysisData.styleScore ?? 0,
|
|
||||||
fullMark: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "readability",
|
|
||||||
score: codeAnalysisData.readabilityScore ?? 0,
|
|
||||||
fullMark: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "efficiency",
|
|
||||||
score: codeAnalysisData.efficiencyScore ?? 0,
|
|
||||||
fullMark: 100,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
kind: "correctness",
|
|
||||||
score: codeAnalysisData.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">
|
|
||||||
Code Analysis
|
|
||||||
</CardTitle>
|
|
||||||
<CardDescription className="text-muted-foreground">
|
|
||||||
Detailed evaluation of your code submission
|
|
||||||
</CardDescription>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent className="p-0">
|
|
||||||
<CodeAnalysisRadarChart chartData={chartData} />
|
|
||||||
</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">
|
|
||||||
<span className="text-muted-foreground">Overall Score</span>
|
|
||||||
<span className="text-primary">
|
|
||||||
{codeAnalysisData.overallScore ?? "N/A"}
|
|
||||||
<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: `${codeAnalysisData.overallScore ?? 0}%`,
|
|
||||||
transitionProperty: "width",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="text-muted-foreground bg-muted/40 p-4 rounded-lg w-full border">
|
|
||||||
<h3 className="font-medium mb-2 text-foreground">Feedback</h3>
|
|
||||||
<p className="whitespace-pre-wrap leading-relaxed">
|
|
||||||
{codeAnalysisData.feedback}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user