diff --git a/prisma/migrations/20250621081920_add_analysis_status/migration.sql b/prisma/migrations/20250621081920_add_analysis_status/migration.sql new file mode 100644 index 0000000..9cc758f --- /dev/null +++ b/prisma/migrations/20250621081920_add_analysis_status/migration.sql @@ -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'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2065fea..6fa0abd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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? diff --git a/src/app/actions/analyze-code.ts b/src/app/actions/analyze-code.ts index 7ff2d59..b312bdf 100644 --- a/src/app/actions/analyze-code.ts +++ b/src/app/actions/analyze-code.ts @@ -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"); } }; diff --git a/src/app/actions/analyze.ts b/src/app/actions/analyze.ts index 1fb6c90..c35f8e9 100644 --- a/src/app/actions/analyze.ts +++ b/src/app/actions/analyze.ts @@ -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 => { + 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; +}; diff --git a/src/app/actions/judge.ts b/src/app/actions/judge.ts index 902a9c3..ebdf805 100644 --- a/src/app/actions/judge.ts +++ b/src/app/actions/judge.ts @@ -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( diff --git a/src/features/problems/analysis/components/card.tsx b/src/features/problems/analysis/components/card.tsx new file mode 100644 index 0000000..53b6e43 --- /dev/null +++ b/src/features/problems/analysis/components/card.tsx @@ -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(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 ( + + + + + + +
+ +

Analyzing your code...

+
+
+
+ ); + } + + if (analysis.status === "FAILED") { + return ( + + + + Code Analysis + + + + + + Analysis Failed + + We couldn't analyze your code. Please try again later. + + + + + ); + } + + if (analysis.status !== "COMPLETED") { + return ( + + + + Code Analysis + + + Preparing your detailed evaluation + + + +
+
+ + +
+
+

+ Processing your submission +

+

+ This may take a few moments... +

+
+
+
+
+ ); + } + + // 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 ( + + + + Code Analysis + + + Detailed evaluation of your code submission + + + + + + + +
+
+ Overall Score + + {analysis.overallScore ?? "N/A"} + /100 + +
+
+
+
+
+ +
+

Feedback

+

+ {analysis.feedback} +

+
+ + + ); +}; diff --git a/src/features/problems/analysis/components/content.tsx b/src/features/problems/analysis/components/content.tsx index 077029d..8b27660 100644 --- a/src/features/problems/analysis/components/content.tsx +++ b/src/features/problems/analysis/components/content.tsx @@ -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 (
@@ -27,90 +13,5 @@ export const AnalysisContent = async ({ ); } - const codeAnalysisData = await prisma.codeAnalysis.findUnique({ - where: { - submissionId: submissionId, - }, - }); - - if (!codeAnalysisData) { - return ( -
- No analysis data found for this submission. -
- ); - } - - // 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 ( - - - - Code Analysis - - - Detailed evaluation of your code submission - - - - - - - -
-
- Overall Score - - {codeAnalysisData.overallScore ?? "N/A"} - /100 - -
-
-
-
-
- -
-

Feedback

-

- {codeAnalysisData.feedback} -

-
- - - ); + return ; };