diff --git a/.gitignore b/.gitignore
index bfa36b2..467becb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,4 @@ node_modules/
.env*.local
.DS_Store
.vercel
+*.tsbuildinfo
diff --git a/app/(dashboard)/essay-grader/[id]/page.tsx b/app/(dashboard)/essay-grader/[id]/page.tsx
new file mode 100644
index 0000000..eb6091c
--- /dev/null
+++ b/app/(dashboard)/essay-grader/[id]/page.tsx
@@ -0,0 +1,132 @@
+import { createServerClient } from "@supabase/ssr"
+import { cookies } from "next/headers"
+import { notFound } from "next/navigation"
+import Link from "next/link"
+import { ArrowLeft, CheckCircle2, XCircle, Lightbulb } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "@/components/ui/card"
+
+export default async function EssayEvaluationPage({
+ params
+}: {
+ params: { id: string }
+}) {
+ const cookieStore = await cookies()
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ get(name) { return cookieStore.get(name)?.value },
+ },
+ }
+ )
+
+ const { data: { user } } = await supabase.auth.getUser()
+
+ if (!user) {
+ return notFound()
+ }
+
+ const { data: evaluation, error } = await supabase
+ .from('essay_evaluations')
+ .select('*')
+ .eq('id', params.id)
+ .eq('user_id', user.id)
+ .single()
+
+ if (error || !evaluation) {
+ return notFound()
+ }
+
+ const feedback = evaluation.feedback
+
+ return (
+
+
+
+
+
+
+
Evaluation Results
+
+ Graded on {new Date(evaluation.created_at).toLocaleDateString()}
+
+
+
+
+
+
+
+ Estimated Grade
+
+
+ {feedback.grade}
+
+
+
+
+
+ Strengths & Weaknesses
+
+
+
+
+ Pros
+
+
+ {feedback.pros.map((pro: string, i: number) => (
+ - {pro}
+ ))}
+
+
+
+
+ Cons
+
+
+ {feedback.cons.map((con: string, i: number) => (
+ - {con}
+ ))}
+
+
+
+
+
+
+
+
+
+ Actionable Suggestions
+
+ Specific feedback to improve your essay.
+
+
+ {feedback.suggestions.map((suggestion: any, i: number) => (
+
+
+ "{suggestion.quote}"
+
+
+ {suggestion.comment}
+
+
+ ))}
+
+
+
+
+
+ Original Essay
+
+
+
+ {evaluation.content}
+
+
+
+
+ )
+}
diff --git a/app/(dashboard)/essay-grader/page.tsx b/app/(dashboard)/essay-grader/page.tsx
new file mode 100644
index 0000000..cbf418f
--- /dev/null
+++ b/app/(dashboard)/essay-grader/page.tsx
@@ -0,0 +1,166 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import { useRouter } from "next/navigation"
+import { createBrowserClient } from "@supabase/ssr"
+import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Textarea } from "@/components/ui/textarea"
+import { Loader2, FileSignature, CheckCircle } from "lucide-react"
+
+export default function EssayGraderPage() {
+ const router = useRouter()
+ const [content, setContent] = useState("")
+ const [rubric, setRubric] = useState("")
+ const [loading, setLoading] = useState(false)
+ const [evaluations, setEvaluations] = useState([])
+ const [fetching, setFetching] = useState(true)
+
+ const supabase = createBrowserClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
+ )
+
+ useEffect(() => {
+ fetchEvaluations()
+ }, [])
+
+ async function fetchEvaluations() {
+ setFetching(true)
+ const { data: { user } } = await supabase.auth.getUser()
+ if (user) {
+ const { data, error } = await supabase
+ .from('essay_evaluations')
+ .select('id, grade, created_at, content')
+ .eq('user_id', user.id)
+ .order('created_at', { ascending: false })
+
+ if (!error && data) {
+ setEvaluations(data)
+ }
+ }
+ setFetching(false)
+ }
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault()
+ if (!content.trim() || loading) return
+
+ setLoading(true)
+
+ try {
+ const res = await fetch("/api/essays/grade", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ content, rubric }),
+ })
+
+ if (res.ok) {
+ const data = await res.json()
+ if (data.id) {
+ router.push(`/essay-grader/${data.id}`)
+ }
+ } else {
+ const errorData = await res.json()
+ alert(errorData.error || "Failed to evaluate essay.")
+ }
+ } catch (err) {
+ console.error(err)
+ alert("Connection error.")
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+
AI Essay Grader
+
+ Paste your essay and an optional rubric to receive an instant grade, strengths, weaknesses, and actionable feedback.
+
+
+
+
+
+
+
+ New Evaluation
+ Submit a new essay for AI grading.
+
+
+
+
+
+
+
Past Evaluations
+ {fetching ? (
+
+
+
+ ) : evaluations.length === 0 ? (
+
+
+
+
+ You haven't graded any essays yet.
+
+
+
+ ) : (
+
+ {evaluations.map((evalData) => (
+ router.push(`/essay-grader/${evalData.id}`)}>
+
+
+ {evalData.content.substring(0, 30)}...
+
+ {evalData.grade}
+
+
+
+ {new Date(evalData.created_at).toLocaleDateString()}
+
+
+
+ ))}
+
+ )}
+
+
+
+ )
+}
diff --git a/app/api/essays/grade/route.ts b/app/api/essays/grade/route.ts
new file mode 100644
index 0000000..1079cb5
--- /dev/null
+++ b/app/api/essays/grade/route.ts
@@ -0,0 +1,68 @@
+import { NextRequest, NextResponse } from "next/server"
+import { createServerClient } from "@supabase/ssr"
+import { cookies } from "next/headers"
+import { gradeEssay } from "@/lib/ai"
+import { consumeCredit } from "@/lib/credits"
+
+export const maxDuration = 60 // Extended duration for long essay evaluations
+
+export async function POST(req: NextRequest) {
+ try {
+ const cookieStore = await cookies()
+ const supabase = createServerClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
+ {
+ cookies: {
+ get(name) { return cookieStore.get(name)?.value },
+ },
+ }
+ )
+
+ const { data: { user } } = await supabase.auth.getUser()
+
+ if (!user) {
+ return NextResponse.json({ error: "Unauthorized" }, { status: 401 })
+ }
+
+ const creditCheck = await consumeCredit(user.id, "ai_chat")
+ if (!creditCheck.allowed) {
+ return NextResponse.json({ error: "Insufficient credits" }, { status: 403 })
+ }
+
+ const { content, rubric } = await req.json()
+
+ if (!content || content.trim().length < 50) {
+ return NextResponse.json({ error: "Essay content is too short." }, { status: 400 })
+ }
+
+ // Call Gemini to grade the essay
+ const feedback = await gradeEssay(content, rubric)
+
+ // Save to database
+ const { data: evaluation, error: dbError } = await supabase
+ .from("essay_evaluations")
+ .insert({
+ user_id: user.id,
+ content: content,
+ rubric: rubric || null,
+ grade: feedback.grade,
+ feedback: feedback
+ })
+ .select("id")
+ .single()
+
+ if (dbError || !evaluation) {
+ console.error("[Essay Grader DB Error]", dbError)
+ return NextResponse.json({ error: "Failed to save evaluation." }, { status: 500 })
+ }
+
+ return NextResponse.json({ id: evaluation.id })
+ } catch (error: any) {
+ console.error("[Essay Grader Error]", error)
+ return NextResponse.json(
+ { error: error.message || "Failed to grade essay" },
+ { status: 500 }
+ )
+ }
+}
diff --git a/lib/ai.ts b/lib/ai.ts
index 42c4ba0..f6babb6 100644
--- a/lib/ai.ts
+++ b/lib/ai.ts
@@ -782,4 +782,61 @@ ${message}
Answer as EduPilot Guide:`
return callAIWithFallback(prompt)
+}
+
+// =========================
+// Essay Grader
+// =========================
+
+export interface EssayFeedback {
+ grade: string
+ pros: string[]
+ cons: string[]
+ suggestions: { quote: string; comment: string }[]
+}
+
+export async function gradeEssay(essay: string, rubric?: string): Promise {
+ const prompt = `You are a strict but fair expert academic evaluator. You are reviewing a student's essay.
+${rubric ? `Here is the grading rubric or assignment prompt to base your evaluation on:\n"""\n${rubric}\n"""\n` : `Use standard academic writing standards for college-level essays (clear thesis, good structure, strong arguments, proper grammar).\n`}
+Here is the student's essay:
+"""
+${essay.slice(0, 15000)}
+"""
+
+Evaluate the essay and provide structured feedback.
+Return ONLY valid JSON in this exact shape:
+{
+ "grade": "Estimated Letter Grade (e.g., A-, B+, C)",
+ "pros": ["Strength 1", "Strength 2"],
+ "cons": ["Weakness 1", "Weakness 2"],
+ "suggestions": [
+ { "quote": "exact quote from the essay with a flaw", "comment": "how to improve this specific part" }
+ ]
+}
+
+Rules:
+- Return ONLY JSON.
+- No markdown formatting outside the JSON, no backticks.
+- suggestions array should have 2 to 5 items.`
+
+ const raw = await callAIWithFallback(prompt)
+ const cleaned = cleanJsonText(raw)
+
+ try {
+ const parsed = JSON.parse(cleaned)
+ return {
+ grade: String(parsed.grade || "N/A"),
+ pros: Array.isArray(parsed.pros) ? parsed.pros.map(String) : [],
+ cons: Array.isArray(parsed.cons) ? parsed.cons.map(String) : [],
+ suggestions: Array.isArray(parsed.suggestions)
+ ? parsed.suggestions.map((s: any) => ({
+ quote: String(s?.quote || ""),
+ comment: String(s?.comment || ""),
+ }))
+ : [],
+ }
+ } catch (error) {
+ console.error("[gradeEssay] Invalid AI JSON:", raw)
+ throw new Error("AI returned invalid evaluation format. Please try again.")
+ }
}
\ No newline at end of file
diff --git a/proxy.ts b/proxy.ts
index aa2aa1e..efa3d0d 100644
--- a/proxy.ts
+++ b/proxy.ts
@@ -14,6 +14,8 @@ const PROTECTED_ROUTES = [
"/analytics",
"/time-tracking",
"/marketplace",
+ "/document-chat",
+ "/essay-grader",
]
export async function proxy(req: NextRequest) {
diff --git a/supabase/migration-essay-grader.sql b/supabase/migration-essay-grader.sql
new file mode 100644
index 0000000..14399cf
--- /dev/null
+++ b/supabase/migration-essay-grader.sql
@@ -0,0 +1,25 @@
+-- Create a table to store essay evaluations
+create table if not exists essay_evaluations (
+ id uuid primary key default gen_random_uuid(),
+ user_id uuid references auth.users(id) on delete cascade not null,
+ rubric text,
+ content text not null,
+ grade text,
+ feedback jsonb,
+ created_at timestamp with time zone default timezone('utc'::text, now()) not null
+);
+
+-- Enable RLS for essay_evaluations
+alter table essay_evaluations enable row level security;
+
+create policy "Users can view their own essay evaluations"
+ on essay_evaluations for select
+ using (auth.uid() = user_id);
+
+create policy "Users can insert their own essay evaluations"
+ on essay_evaluations for insert
+ with check (auth.uid() = user_id);
+
+create policy "Users can delete their own essay evaluations"
+ on essay_evaluations for delete
+ using (auth.uid() = user_id);