feat(analysis): implement real-time code analysis with async processing and status tracking

This commit is contained in:
cfngc4594 2025-06-21 17:04:52 +08:00
parent ab598459a2
commit e83a1165da
7 changed files with 278 additions and 116 deletions

View File

@ -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';

View File

@ -142,10 +142,20 @@ model Submission {
updatedAt DateTime @updatedAt
}
enum AnalysisStatus {
PENDING
QUEUED
PROCESSING
COMPLETED
FAILED
}
model CodeAnalysis {
id String @id @default(cuid())
submissionId String @unique
status AnalysisStatus @default(PENDING)
timeComplexity String?
spaceComplexity String?

View File

@ -15,8 +15,21 @@ export const analyzeCode = async ({
content,
submissionId,
}: analyzeCodeProps) => {
const analysis = await prisma.codeAnalysis.create({
data: {
submissionId,
status: "QUEUED"
},
});
try {
const result = await generateText({
await prisma.codeAnalysis.update({
where: { id: analysis.id },
data: {
status: "PROCESSING"
},
});
await generateText({
model: deepseek("deepseek-chat"),
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,
feedback,
}) => {
const codeAnalysis = await prisma.codeAnalysis.create({
await prisma.codeAnalysis.update({
where: {
id: analysis.id
},
data: {
submissionId,
status: "COMPLETED",
timeComplexity,
spaceComplexity,
overallScore,
@ -127,20 +143,21 @@ export const analyzeCode = async ({
feedback,
},
});
return {
success: true,
message: "Code analysis saved successfully",
data: codeAnalysis,
};
},
}),
},
toolChoice: { type: "tool", toolName: "saveCodeAnalysis" },
});
return result;
} catch (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");
}
};

View File

@ -5,8 +5,11 @@ import {
AnalyzeComplexityResponseSchema,
Complexity,
} from "@/types/complexity";
import prisma from "@/lib/prisma";
import { openai } from "@/lib/ai";
import { auth } from "@/lib/auth";
import { CoreMessage, generateText } from "ai";
import { CodeAnalysis } from "@/generated/client";
export const analyzeComplexity = async (
content: string
@ -62,3 +65,23 @@ export const analyzeComplexity = async (
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;
};

View File

@ -120,10 +120,14 @@ export const judge = async (
},
});
await analyzeCode({
content,
submissionId: submission.id,
});
const executeAnalyzeCode = async () => {
await analyzeCode({
content,
submissionId: submission.id,
});
}
executeAnalyzeCode()
// Upload code to the container
const tarStream = createTarStream(

View 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&apos;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>
);
};

View File

@ -1,24 +1,10 @@
import {
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";
import { AnalysisCard } from "@/features/problems/analysis/components/card";
interface AnalysisContentProps {
submissionId: string | undefined;
}
export const AnalysisContent = async ({
submissionId,
}: AnalysisContentProps) => {
export const AnalysisContent = ({ submissionId }: AnalysisContentProps) => {
if (!submissionId) {
return (
<div className="p-4 text-center text-muted-foreground">
@ -27,90 +13,5 @@ export const AnalysisContent = async ({
);
}
const codeAnalysisData = await prisma.codeAnalysis.findUnique({
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>
);
return <AnalysisCard submissionId={submissionId} />;
};