diff --git a/prisma/migrations/20250620033028_add_code_analysis_table/migration.sql b/prisma/migrations/20250620033028_add_code_analysis_table/migration.sql new file mode 100644 index 0000000..c176e0a --- /dev/null +++ b/prisma/migrations/20250620033028_add_code_analysis_table/migration.sql @@ -0,0 +1,23 @@ +-- CreateTable +CREATE TABLE "CodeAnalysis" ( + "id" TEXT NOT NULL, + "submissionId" TEXT NOT NULL, + "timeComplexity" TEXT, + "spaceComplexity" TEXT, + "overallScore" INTEGER, + "styleScore" INTEGER, + "readabilityScore" INTEGER, + "efficiencyScore" INTEGER, + "correctnessScore" INTEGER, + "feedback" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "CodeAnalysis_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "CodeAnalysis_submissionId_key" ON "CodeAnalysis"("submissionId"); + +-- AddForeignKey +ALTER TABLE "CodeAnalysis" ADD CONSTRAINT "CodeAnalysis_submissionId_fkey" FOREIGN KEY ("submissionId") REFERENCES "Submission"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 17488cd..2065fea 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -130,6 +130,7 @@ model Submission { memoryUsage Int? testcaseResults TestcaseResult[] + codeAnalysis CodeAnalysis? userId String problemId String @@ -141,6 +142,27 @@ model Submission { updatedAt DateTime @updatedAt } +model CodeAnalysis { + id String @id @default(cuid()) + submissionId String @unique + + timeComplexity String? + spaceComplexity String? + + overallScore Int? + styleScore Int? + readabilityScore Int? + efficiencyScore Int? + correctnessScore Int? + + feedback String? + + submission Submission @relation(fields: [submissionId], references: [id], onDelete: Cascade) + + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + model Testcase { id String @id @default(cuid()) expectedOutput String diff --git a/src/app/(app)/problems/[problemId]/page.tsx b/src/app/(app)/problems/[problemId]/page.tsx index a0bb4e9..1ee7a32 100644 --- a/src/app/(app)/problems/[problemId]/page.tsx +++ b/src/app/(app)/problems/[problemId]/page.tsx @@ -1,11 +1,12 @@ import { TestcasePanel } from "@/features/problems/testcase/panel"; -import { BotPanel } from "@/features/problems/bot/components/panel"; +// import { BotPanel } from "@/features/problems/bot/components/panel"; import { CodePanel } from "@/features/problems/code/components/panel"; import { DetailPanel } from "@/features/problems/detail/components/panel"; import { SolutionPanel } from "@/features/problems/solution/components/panel"; import { SubmissionPanel } from "@/features/problems/submission/components/panel"; import { DescriptionPanel } from "@/features/problems/description/components/panel"; import { ProblemFlexLayout } from "@/features/problems/components/problem-flexlayout"; +import { AnalysisPanel } from "@/features/problems/analysis/components/panel"; interface ProblemPageProps { params: Promise<{ problemId: string }>; @@ -28,7 +29,7 @@ export default async function ProblemPage({ detail: , code: , testcase: , - bot: , + bot: , }; return ( diff --git a/src/app/actions/analyze-code.ts b/src/app/actions/analyze-code.ts new file mode 100644 index 0000000..868afb6 --- /dev/null +++ b/src/app/actions/analyze-code.ts @@ -0,0 +1,144 @@ +import { z } from "zod"; +import prisma from "@/lib/prisma"; +import { generateText, tool } from "ai"; +import { deepseek } from "@ai-sdk/deepseek"; +import { Complexity } from "@/types/complexity"; + +interface analyzeCodeProps { + content: string; + submissionId: string; +} + +export const analyzeCode = async ({ + content, + submissionId, +}: analyzeCodeProps) => { + try { + const result = 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. + +**Time/Space Complexity MUST be one of these values:** +- O(1) +- O(logN) +- O(√N) +- O(N) +- O(NlogN) +- O(N^2) +- O(2^N) +- O(N!) + +--- + +**Scoring Guidelines:** + +**Overall Score (0-100):** Represents the comprehensive quality of the code, encompassing all other metrics. +- **90-100 (Exceptional):** Highly optimized, perfectly readable, robust, and well-structured. +- **70-89 (Good):** Minor areas for improvement, generally readable, efficient, and functional. +- **50-69 (Acceptable):** Noticeable areas for improvement in style, readability, or efficiency; may have minor bugs. +- **30-49 (Poor):** Significant issues in multiple areas; code is difficult to understand or maintain. +- **0-29 (Unacceptable):** Major flaws, likely non-functional, extremely difficult to maintain, or fundamentally incorrect. + +**Code Style Score (0-100):** Adherence to common coding conventions (e.g., consistent indentation, naming conventions, proper use of whitespace, clear file organization). +- **90-100 (Flawless):** Adheres to all best practices, making the code visually clean and consistent. +- **70-89 (Good):** Minor style inconsistencies, but generally well-formatted. +- **50-69 (Acceptable):** Noticeable style issues affecting consistency and readability. +- **30-49 (Poor):** Significant style deviations, making the code messy and hard to read. +- **0-29 (Unacceptable):** Extremely poor style, rendering the code almost unreadable due to formatting issues. + +**Readability Score (0-100):** How easy the code is to understand (e.g., clear variable/function names, concise logic, effective comments, modularity). +- **90-100 (Exceptional):** Exceptionally clear and easy to understand, even for complex logic; excellent use of comments and modular design. +- **70-89 (Good):** Generally clear, minor effort required for understanding; good variable names and some helpful comments. +- **50-69 (Acceptable):** Some parts are hard to follow; could benefit from better naming, more comments, or clearer logic. +- **30-49 (Poor):** Difficult to understand without significant effort; cryptic names, missing comments, or convoluted logic. +- **0-29 (Unacceptable):** Almost impossible to decipher; requires extensive time to understand basic functionality. + +**Efficiency Score (0-100):** How well the code utilizes computational resources (time and space complexity, optimized algorithms, unnecessary computations). +- **90-100 (Optimal):** Achieves optimal time and space complexity; highly optimized algorithm with no redundant operations. +- **70-89 (Good):** Good efficiency, slight room for minor optimization; chosen algorithm is appropriate but could be subtly refined. +- **50-69 (Acceptable):** Acceptable efficiency, but clear areas for significant improvement in algorithm choice or implementation. +- **30-49 (Poor):** Inefficient, uses excessive resources; likely to cause performance issues on larger inputs. +- **0-29 (Unacceptable):** Extremely inefficient; likely to time out or crash on even small to medium-sized inputs. + +**Correctness Score (0-100):** Whether the code produces the correct output for all valid inputs and handles edge cases appropriately. +- **90-100 (Flawless):** Produces correct output for all specified requirements, including comprehensive handling of edge cases and invalid inputs. +- **70-89 (Mostly Correct):** Mostly correct, with minor bugs or issues in specific edge cases; may not handle all invalid inputs gracefully. +- **50-69 (Acceptable):** Contains noticeable bugs, fails on some common inputs, or has significant limitations in handling edge cases. +- **30-49 (Many Bugs):** Produces incorrect output frequently; fails on many common inputs. +- **0-29 (Completely Incorrect):** Non-functional or fundamentally incorrect; fails on most inputs. + +--- + +**Important Considerations for Scoring:** +- **Objectivity:** Base your scores strictly on the provided definitions and common best practices, not on personal preference. +- **Justification:** For each score, especially those below 90, provide clear and concise justifications in the 'feedback' field, explaining *why* a particular score was given and *how* it can be improved. +- **Actionable Feedback:** Ensure your feedback is actionable, offering specific, practical suggestions for improvement rather than vague statements. + +--- + +**Your response MUST call the 'saveCodeAnalysis' tool with the following:** +- Time Complexity (choose from the list above) +- Space Complexity (choose from the list above) +- Overall Score (0-100) +- Code Style Score (0-100) +- Readability Score (0-100) +- Efficiency Score (0-100) +- Correctness Score (0-100) +- Feedback (detailed, actionable suggestions) + +**DO NOT return plain text—only call the tool!**`, + messages: [{ role: "user", content: content }], + tools: { + saveCodeAnalysis: tool({ + description: + "Stores the AI's code analysis results into the database.", + parameters: z.object({ + timeComplexity: Complexity.optional(), + spaceComplexity: Complexity.optional(), + overallScore: z.number().int().min(0).max(100).optional(), + styleScore: z.number().int().min(0).max(100).optional(), + readabilityScore: z.number().int().min(0).max(100).optional(), + efficiencyScore: z.number().int().min(0).max(100).optional(), + correctnessScore: z.number().int().min(0).max(100).optional(), + feedback: z.string().optional(), + }), + execute: async ({ + timeComplexity, + spaceComplexity, + overallScore, + styleScore, + readabilityScore, + efficiencyScore, + correctnessScore, + feedback, + }) => { + const codeAnalysis = await prisma.codeAnalysis.create({ + data: { + submissionId, + timeComplexity, + spaceComplexity, + overallScore, + styleScore, + readabilityScore, + efficiencyScore, + correctnessScore, + 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); + throw new Error("Failed to analyze code"); + } +}; diff --git a/src/app/actions/analyze.ts b/src/app/actions/analyze.ts index cd362d7..48839ec 100644 --- a/src/app/actions/analyze.ts +++ b/src/app/actions/analyze.ts @@ -1,6 +1,60 @@ -import { Complexity } from "@/types/complexity"; +import { + AnalyzeComplexityResponse, + AnalyzeComplexityResponseSchema, + Complexity, +} from "@/types/complexity"; +import { deepseek } from "@ai-sdk/deepseek"; +import { CoreMessage, generateText } from "ai"; -export const analyzeComplexity = async (content: string) => { - console.log("🚀 ~ analyzeComplexity ~ content:", content); - return { time: Complexity.Enum["O(N)"], space: Complexity.Enum["O(1)"] }; +export const analyzeComplexity = async ( + content: string +): Promise => { + const prompt = ` + Analyze the time and space complexity of the following programming code snippet. + Determine the Big O notation from this list: ${Complexity.options.join(", ")}. + Provide your response as a JSON object with one key: + 1. "time": A string representing the time complexity (e.g., "O(N)", "O(logN)"). + 2. "space": A string representing the space complexity (e.g., "O(N)", "O(1)"). + + Code to analyze: + \`\`\` + ${content} + \`\`\` + + Respond ONLY with the JSON object. Do not include any other text or markdown formmating like \`\`\`json before or after the object. + `; + + const messages: CoreMessage[] = [{ role: "user", content: prompt }]; + + let text; + try { + const response = await generateText({ + model: deepseek("deepseek-chat"), + messages: messages, + }); + text = response.text; + } catch (error) { + console.error("Error generating text:", error); + throw new Error("Failed to generate response from LLM"); + } + + let llmResponseJson; + try { + const cleanedText = text.trim(); + llmResponseJson = JSON.parse(cleanedText); + } catch (error) { + console.error("Failed to parse LLM response as JSON:", error); + console.error("LLM raw output:", text); + throw new Error("Invalid JSON response from LLM"); + } + + const validationResult = + AnalyzeComplexityResponseSchema.safeParse(llmResponseJson); + + if (!validationResult.success) { + console.error("Zod validation failed:", validationResult.error.format()); + throw new Error("Response validation failed"); + } + + return validationResult.data; }; diff --git a/src/app/actions/judge.ts b/src/app/actions/judge.ts index f416c0e..902a9c3 100644 --- a/src/app/actions/judge.ts +++ b/src/app/actions/judge.ts @@ -7,6 +7,7 @@ import { compile } from "./compile"; import { auth, signIn } from "@/lib/auth"; import { revalidatePath } from "next/cache"; import { Language, Status } from "@/generated/client"; +import { analyzeCode } from "@/app/actions/analyze-code"; import { createContainer, createTarStream, prepareEnvironment } from "./docker"; export const judge = async ( @@ -119,6 +120,11 @@ export const judge = async ( }, }); + await analyzeCode({ + content, + submissionId: submission.id, + }); + // Upload code to the container const tarStream = createTarStream( getFileNameForLanguage(language), diff --git a/src/features/problems/analysis/components/panel.tsx b/src/features/problems/analysis/components/panel.tsx new file mode 100644 index 0000000..cdb393c --- /dev/null +++ b/src/features/problems/analysis/components/panel.tsx @@ -0,0 +1,113 @@ +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, + CardFooter, +} from "@/components/ui/card"; +import prisma from "@/lib/prisma"; +import { ChartDataPoint, CodeAnalysisRadarChart } from "./radar-chart"; + +export const description = "A server component to fetch code analysis data."; + +interface AnalysisPanelProps { + submissionId: string | undefined; +} + +export const AnalysisPanel = async ({ submissionId }: AnalysisPanelProps) => { + if (!submissionId) { + return ( +
+ No submission ID provided. +
+ ); + } + + 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} +

+
+ + + ); +}; diff --git a/src/features/problems/analysis/components/radar-chart.tsx b/src/features/problems/analysis/components/radar-chart.tsx new file mode 100644 index 0000000..cd38479 --- /dev/null +++ b/src/features/problems/analysis/components/radar-chart.tsx @@ -0,0 +1,45 @@ +"use client"; + +import { + PolarAngleAxis, + PolarGrid, + PolarRadiusAxis, + Radar, + RadarChart, +} from "recharts"; +import { + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart"; + +export interface ChartDataPoint { + kind: string; + score: number; + fullMark: number; +} + +interface CodeAnalysisRadarChartProps { + chartData: ChartDataPoint[]; +} + +export function CodeAnalysisRadarChart({ + chartData, +}: CodeAnalysisRadarChartProps) { + return ( + + + } /> + + + + + + + ); +}