feat: setup admin JSON generation and pdf viewer for free plan DPPs

This commit is contained in:
2026-07-27 14:56:25 +05:30
parent c214ca41b6
commit a5bb2d867c
74 changed files with 8546 additions and 915 deletions

View File

@@ -0,0 +1,289 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { BrainCircuit, Clock, CheckCircle2, AlertTriangle, ArrowLeft, ArrowRight, ShieldAlert, Sparkles, Send } from "lucide-react"
import Link from "next/link"
import { useActiveUser } from "@/hooks/useActiveUser"
import { useRouter } from "next/navigation"
export default function AIComCBTDPPPage() {
const { user, hasAccessTo, loading } = useActiveUser()
const router = useRouter()
const [activeQuestion, setActiveQuestion] = useState(0)
const [selectedAnswers, setSelectedAnswers] = useState<Record<number, string>>({})
const [timeLeft, setTimeLeft] = useState(1800) // 30 minutes
const [submitted, setSubmitted] = useState(false)
// Mock AI CBT DPP questions
const sampleQuestions = [
{
id: 1,
subject: "Physics",
topic: "Rotational Dynamics & Torque",
question: "A uniform disc of mass M and radius R rotates about an axis passing through its center and perpendicular to its plane. Its moment of inertia about a tangent parallel to its plane is:",
options: ["(1/2) M R²", "(5/4) M R²", "(3/2) M R²", "(7/4) M R²"],
correct: 1,
explanation: "Using parallel axis theorem: I_tangent = I_cm + M R^2 = (1/4 M R^2) + M R^2 = 5/4 M R^2."
},
{
id: 2,
subject: "Chemistry",
topic: "Chemical Equilibrium & Kp/Kc",
question: "For the reaction N2(g) + 3H2(g) ⇌ 2NH3(g), the value of Kp / Kc at temperature T Kelvin is given by:",
options: ["(RT)⁻²", "(RT)⁻¹", "(RT)", "(RT)²"],
correct: 0,
explanation: "Δn_g = 2 - (1 + 3) = -2. Therefore, Kp = Kc (RT)^(-2), so Kp / Kc = (RT)^(-2)."
},
{
id: 3,
subject: "Biology / Maths",
topic: "Calculus / Cell Biology",
question: "Which organelle is responsible for the synthesis of lipids and steroidal hormones in eukaryotic cells?",
options: ["Rough Endoplasmic Reticulum", "Smooth Endoplasmic Reticulum", "Golgi Apparatus", "Lysosome"],
correct: 1,
explanation: "Smooth Endoplasmic Reticulum (SER) is the primary site for synthesis of lipids and steroid hormones."
},
{
id: 4,
subject: "Physics",
topic: "Electrostatics & Potential",
question: "Two point charges +q and -q are placed at distance 2a. The electric potential at the midpoint of the line joining them is:",
options: ["Zero", "q / (4πε₀a)", "2q / (4πε₀a)", "Infinity"],
correct: 0,
explanation: "V_total = V_+q + V_-q = (q / 4πε₀a) + (-q / 4πε₀a) = 0."
},
{
id: 5,
subject: "Chemistry",
topic: "Organic Mechanism & Nucleophiles",
question: "Which of the following undergoes SN1 reaction at the fastest rate?",
options: ["Methyl chloride", "Ethyl chloride", "Isopropyl chloride", "Tert-butyl chloride"],
correct: 3,
explanation: "SN1 rate depends on carbocation stability. Tert-butyl carbocation (3°) is the most stable."
}
]
useEffect(() => {
if (submitted || timeLeft <= 0) return
const timer = setInterval(() => {
setTimeLeft(prev => Math.max(0, prev - 1))
}, 1000)
return () => clearInterval(timer)
}, [submitted, timeLeft])
const formatTime = (secs: number) => {
const m = Math.floor(secs / 60)
const s = secs % 60
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
}
const handleOptionSelect = (qIdx: number, optIdx: number) => {
if (submitted) return
setSelectedAnswers(prev => ({ ...prev, [qIdx]: String.fromCharCode(65 + optIdx) }))
}
const calculateScore = () => {
let score = 0
sampleQuestions.forEach((q, idx) => {
const selected = selectedAnswers[idx]
if (selected) {
const selectedOptIdx = selected.charCodeAt(0) - 65
if (selectedOptIdx === q.correct) score += 4
else score -= 1
}
})
return score
}
if (loading) {
return <div className="p-8 text-center text-muted-foreground">Loading AI Coach CBT Engine...</div>
}
return (
<div className="p-6 md:p-8 space-y-6 max-w-6xl mx-auto min-h-screen animate-in fade-in duration-500">
{/* Top Header */}
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-secondary/20 border border-border p-6 rounded-3xl backdrop-blur-xl">
<div className="flex items-center gap-4">
<Link href="/dashboard/dpp">
<Button variant="outline" size="icon" className="rounded-xl border-border hover:bg-secondary">
<ArrowLeft size={18} />
</Button>
</Link>
<div>
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-black uppercase tracking-widest text-[#00F0FF] bg-[#00F0FF]/10 px-2.5 py-0.5 rounded-full border border-[#00F0FF]/30">
AI CBT DPP Simulator
</span>
<span className="text-xs font-bold text-muted-foreground">• {user?.target_exam?.replace("_", " ") || "JEE/NEET"}</span>
</div>
<h1 className="text-2xl font-black tracking-tight">Interactive Target DPP #14</h1>
</div>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 px-4 py-2 bg-background border border-border rounded-2xl font-mono text-lg font-bold text-[#00F0FF] shadow-inner">
<Clock size={18} className="text-[#00F0FF] animate-pulse" />
{formatTime(timeLeft)}
</div>
{!submitted ? (
<Button
onClick={() => setSubmitted(true)}
className="bg-[#00F0FF] text-black hover:bg-[#00F0FF]/90 font-bold px-6 rounded-xl shadow-[0_0_15px_rgba(0,240,255,0.2)]"
>
Submit Test <Send size={16} className="ml-2" />
</Button>
) : (
<Button
onClick={() => { setSubmitted(false); setSelectedAnswers({}); setTimeLeft(1800); setActiveQuestion(0) }}
variant="outline"
className="font-bold rounded-xl border-border"
>
Retake CBT DPP
</Button>
)}
</div>
</div>
{submitted && (
<Card className="border-emerald-500/30 bg-emerald-500/10 p-6 rounded-3xl space-y-4 animate-in zoom-in-95">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<CheckCircle2 size={32} className="text-emerald-400" />
<div>
<h3 className="text-xl font-black text-emerald-400">CBT DPP Completed!</h3>
<p className="text-xs text-muted-foreground">Your performance analysis has been evaluated by the AI Evaluator.</p>
</div>
</div>
<div className="text-right">
<span className="text-3xl font-black text-emerald-400">{calculateScore()} / 20</span>
<p className="text-xs text-muted-foreground font-bold">Total Marks</p>
</div>
</div>
</Card>
)}
{/* Main CBT Grid */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* Question Area */}
<div className="lg:col-span-3">
<Card className="border-border bg-background/80 backdrop-blur-xl p-6 rounded-3xl shadow-xl space-y-6">
<div className="flex justify-between items-center border-b border-border pb-4">
<span className="text-sm font-bold text-[#00F0FF] uppercase tracking-wider">
Question {activeQuestion + 1} of {sampleQuestions.length}
</span>
<span className="text-xs font-bold bg-secondary px-3 py-1 rounded-full text-muted-foreground">
{sampleQuestions[activeQuestion].topic}
</span>
</div>
<div className="space-y-4">
<p className="text-lg font-bold leading-relaxed text-foreground">
{sampleQuestions[activeQuestion].question}
</p>
<div className="space-y-3 pt-2">
{sampleQuestions[activeQuestion].options.map((opt, optIdx) => {
const letter = String.fromCharCode(65 + optIdx)
const isSelected = selectedAnswers[activeQuestion] === letter
const isCorrect = sampleQuestions[activeQuestion].correct === optIdx
let btnStyle = "border-border bg-secondary/20 hover:border-primary/50 text-foreground"
if (isSelected) {
btnStyle = "border-[#00F0FF] bg-[#00F0FF]/15 text-[#00F0FF] font-bold shadow-[0_0_12px_rgba(0,240,255,0.15)]"
}
if (submitted) {
if (isCorrect) btnStyle = "border-emerald-500 bg-emerald-500/20 text-emerald-400 font-bold"
else if (isSelected && !isCorrect) btnStyle = "border-red-500 bg-red-500/20 text-red-400 font-bold"
}
return (
<div
key={optIdx}
onClick={() => handleOptionSelect(activeQuestion, optIdx)}
className={`flex items-center gap-4 p-4 rounded-2xl border cursor-pointer transition-all ${btnStyle}`}
>
<span className="w-8 h-8 rounded-xl bg-background/80 border border-border flex items-center justify-center font-bold text-sm shrink-0">
{letter}
</span>
<span className="text-base font-medium flex-1">{opt}</span>
{submitted && isCorrect && <CheckCircle2 size={20} className="text-emerald-400" />}
</div>
)
})}
</div>
{submitted && (
<div className="mt-6 p-4 rounded-2xl bg-[#00F0FF]/10 border border-[#00F0FF]/30 text-sm text-[#00F0FF] space-y-1 animate-in fade-in">
<span className="font-extrabold uppercase text-xs tracking-wider block">AI Solution & Explanation:</span>
<p>{sampleQuestions[activeQuestion].explanation}</p>
</div>
)}
</div>
{/* Navigation buttons */}
<div className="flex justify-between items-center pt-4 border-t border-border">
<Button
variant="outline"
disabled={activeQuestion === 0}
onClick={() => setActiveQuestion(prev => Math.max(0, prev - 1))}
className="rounded-xl font-bold border-border"
>
<ArrowLeft size={16} className="mr-2" /> Previous
</Button>
<Button
disabled={activeQuestion === sampleQuestions.length - 1}
onClick={() => setActiveQuestion(prev => Math.min(sampleQuestions.length - 1, prev + 1))}
className="bg-[#00F0FF] text-black hover:bg-[#00F0FF]/90 font-bold rounded-xl"
>
Next Question <ArrowRight size={16} className="ml-2" />
</Button>
</div>
</Card>
</div>
{/* Question Palette Sidebar */}
<div className="space-y-4">
<Card className="border-border bg-background/80 backdrop-blur-xl p-5 rounded-3xl shadow-xl space-y-4">
<h3 className="text-sm font-bold uppercase tracking-wider text-muted-foreground">Question Palette</h3>
<div className="grid grid-cols-5 gap-2">
{sampleQuestions.map((q, idx) => {
const isAns = !!selectedAnswers[idx]
const isCurr = activeQuestion === idx
return (
<button
key={q.id}
onClick={() => setActiveQuestion(idx)}
className={`h-10 rounded-xl text-xs font-extrabold transition-all border ${
isCurr
? "border-[#00F0FF] bg-[#00F0FF] text-black ring-2 ring-[#00F0FF]/50"
: isAns
? "bg-[#00F0FF]/20 border-[#00F0FF]/40 text-[#00F0FF]"
: "bg-secondary/40 border-border text-muted-foreground hover:border-primary"
}`}
>
{idx + 1}
</button>
)
})}
</div>
<div className="pt-4 border-t border-border space-y-2 text-xs font-semibold text-muted-foreground">
<div className="flex items-center gap-2">
<span className="w-3 h-3 rounded-md bg-[#00F0FF]" /> Current Question
</div>
<div className="flex items-center gap-2">
<span className="w-3 h-3 rounded-md bg-[#00F0FF]/20 border border-[#00F0FF]/40" /> Answered
</div>
<div className="flex items-center gap-2">
<span className="w-3 h-3 rounded-md bg-secondary/40 border border-border" /> Unattempted
</div>
</div>
</Card>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,302 @@
"use client"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { BrainCircuit, Play, CheckCircle2, Download, Filter, Lock, FileText, Eye, ShieldAlert, X, Sparkles } from "lucide-react"
import { useEffect, useState } from "react"
import Link from "next/link"
import { useActiveUser } from "@/hooks/useActiveUser"
export default function PrepPilotAIPage() {
const { user, userId } = useActiveUser()
const [dpps, setDpps] = useState<any[]>([])
const [attempts, setAttempts] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [dateFilter, setDateFilter] = useState("")
const [selectedPdfDpp, setSelectedPdfDpp] = useState<any | null>(null)
useEffect(() => {
async function fetchDpps() {
if (!user) return; // Wait for user to be available
try {
const pattern = user.target_exam || "JEE_Main";
// Also fetch from the correct Next.js proxied route instead of localhost:8000
const res = await fetch(`/api/tests?type=DPS&exam_pattern=${pattern}`);
if (res.ok) {
const data = await res.json()
setDpps(data)
}
if (userId) {
const attemptsRes = await fetch(`/api/users/${userId}/attempts`);
if (attemptsRes.ok) {
const attemptsData = await attemptsRes.json();
setAttempts(attemptsData);
}
}
} catch (err) {
console.error("Failed to fetch DPPs", err)
} finally {
setLoading(false)
}
}
fetchDpps()
}, [user])
const getDifficultyColor = (diff: string) => {
switch(diff) {
case 'Easy': return 'text-emerald-500 bg-emerald-500/10 border-emerald-500/20';
case 'Medium': return 'text-amber-500 bg-amber-500/10 border-amber-500/20';
case 'Hard': return 'text-destructive bg-destructive/10 border-destructive/20';
default: return 'text-primary bg-primary/10 border-primary/20';
}
}
const attemptsByTest = attempts.reduce((acc, curr) => {
// Isolate analytics to first attempt only - ignore retakes
if (curr.is_retake) return acc;
if (!acc[curr.source_id] || acc[curr.source_id].score < curr.score) {
acc[curr.source_id] = curr;
}
return acc;
}, {} as Record<string, any>);
const filteredDpps = dpps.filter(dpp => {
if (dateFilter) {
if (!dpp.scheduled_date) return false;
const scheduledDate = new Date(dpp.scheduled_date).toISOString().split('T')[0];
if (scheduledDate !== dateFilter) return false;
}
return true;
});
const formatDate = (dateStr: string) => {
return new Date(dateStr).toLocaleDateString("en-US", { year: 'numeric', month: 'short', day: 'numeric' });
}
const getPdfUrl = (title: string) => {
const t = title.toLowerCase();
if (t.includes('phys')) return '/dpps/dps-phy-ch4-20260707.pdf#toolbar=0&navpanes=0&scrollbar=0';
if (t.includes('chem')) return '/dpps/dps-che-ch4-20260707.pdf#toolbar=0&navpanes=0&scrollbar=0';
if (t.includes('math')) return '/dpps/dps-mth-ch4-20260707.pdf#toolbar=0&navpanes=0&scrollbar=0';
if (t.includes('bio')) return '/dpps/dps-bio-ch4-20260707.pdf#toolbar=0&navpanes=0&scrollbar=0';
// Default fallback
return '/dpps/dps-phy-ch4-20260707.pdf#toolbar=0&navpanes=0&scrollbar=0';
};
return (
<div className="p-8 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500 max-w-5xl mx-auto">
<div className="flex flex-col md:flex-row md:items-center justify-between p-8 bg-gradient-to-r from-secondary/50 to-background border border-border rounded-2xl shadow-sm gap-6">
<div>
<h1 className="text-3xl font-bold tracking-tight mb-2">PrepPilot AI <span className="text-[#00F0FF] text-xl">&</span> CBT Simulator</h1>
<p className="text-muted-foreground font-medium max-w-xl">
Select an interactive Target DPP to launch the AI-powered CBT examination simulator.
</p>
</div>
</div>
<div className="flex flex-col sm:flex-row justify-between items-center gap-4 border-b border-border pb-6">
<div className="flex items-center gap-2">
<input
type="date"
value={dateFilter}
onChange={(e) => setDateFilter(e.target.value)}
className="px-3 py-1.5 bg-background border border-border rounded-lg text-xs font-bold focus:ring-2 focus:ring-[#00F0FF] outline-none text-muted-foreground mr-2"
/>
<span className="text-sm font-bold text-muted-foreground mr-2"><Filter size={16} className="inline mr-1"/> Difficulty:</span>
{['Mixed', 'Easy', 'Medium', 'Hard'].map((diff) => (
<button key={diff} className={`px-3 py-1.5 rounded-lg text-xs font-bold border transition-colors ${diff === 'Mixed' ? 'bg-primary text-primary-foreground border-primary' : 'bg-background text-muted-foreground border-border hover:border-primary/50'}`}>
{diff}
</button>
))}
<div className="flex items-center gap-1 ml-2 text-xs text-muted-foreground bg-secondary/50 px-2 py-1 rounded-md">
<Lock size={12}/> Pro
</div>
</div>
<Button variant="outline" className="gap-2 rounded-xl text-sm font-bold">
<Download size={16} /> Export as PDF <span className="ml-1 text-[10px] uppercase tracking-wider bg-primary/10 text-primary px-1.5 py-0.5 rounded-md">Pro</span>
</Button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{loading ? (
<div className="col-span-2 text-center py-12 text-muted-foreground">Loading AI-generated DPPs...</div>
) : filteredDpps.length === 0 ? (
<div className="col-span-2 text-center py-12 text-muted-foreground border border-dashed rounded-xl">No DPPs available for the selected criteria.</div>
) : filteredDpps.map((dpp, i) => {
// Calculate total questions dynamically from sections
let count = 0;
if (dpp.sections) {
Object.values(dpp.sections).forEach((s: any) => { count += (s.questions?.length || 0) });
}
const attempt = attemptsByTest[dpp.id];
const isAttempted = !!attempt;
// Date logic
const now = new Date();
const scheduledTime = dpp.scheduled_date ? new Date(dpp.scheduled_date) : now;
const endTime = dpp.end_date ? new Date(dpp.end_date) : null;
const isUpcoming = scheduledTime > now;
const isExpired = endTime ? now > endTime : false;
return (
<Card key={dpp.id} className={`group cursor-pointer transition-all hover:shadow-md ${isAttempted ? 'opacity-70 hover:opacity-100 bg-secondary/10 border-primary/20' : isExpired ? 'opacity-80' : 'hover:border-[#00F0FF]/50 bg-background'}`}>
<CardContent className="p-6 flex items-center justify-between">
<div className="flex gap-4">
<div className={`mt-1 flex-shrink-0 ${isAttempted ? 'text-green-500' : 'text-foreground'}`}>
{isAttempted ? <CheckCircle2 size={24} /> : <div className="size-6 rounded-full border-2 border-muted-foreground group-hover:border-[#00F0FF] transition-colors flex items-center justify-center"></div>}
</div>
<div>
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-bold uppercase tracking-wider text-[#00F0FF]">
{user?.target_exam?.replace("_", " ") || "JEE/NEET"}
</span>
<span className="text-muted-foreground text-xs font-bold">•</span>
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground">{dpp.difficulty} Level</span>
{isUpcoming ? (
<>
<span className="text-muted-foreground text-xs font-bold">•</span>
<span className="text-[10px] font-bold uppercase text-yellow-500 bg-yellow-500/10 px-1.5 py-0.5 rounded">Upcoming</span>
</>
) : isExpired ? (
<>
<span className="text-muted-foreground text-xs font-bold">•</span>
<span className="text-[10px] font-bold uppercase text-muted-foreground bg-secondary px-1.5 py-0.5 rounded">Expired</span>
</>
) : null}
</div>
<h3 className="font-bold text-lg leading-tight group-hover:text-[#00F0FF] transition-colors flex items-center gap-2">
{dpp.title}
<span className={`text-[10px] uppercase px-2 py-0.5 rounded-md border font-bold tracking-wider ${getDifficultyColor(dpp.difficulty)}`}>
{dpp.difficulty}
</span>
</h3>
<div className="text-sm text-muted-foreground mt-2 font-medium">
{count} Questions • {dpp.total_time} mins
{dpp.scheduled_date && (
<span className="ml-2 border-l border-border pl-2">
{isUpcoming ? `Available on ${formatDate(dpp.scheduled_date)}` : isExpired ? `Expired on ${formatDate(dpp.end_date!)}` : 'Available Now'}
</span>
)}
</div>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-2 shrink-0">
{isUpcoming ? (
<Button disabled variant="outline" size="sm" className="font-bold">Starts Soon</Button>
) : isExpired && !isAttempted ? (
<Button disabled variant="outline" size="sm" className="font-bold bg-secondary/50">Expired</Button>
) : isAttempted ? (
<>
<Link href={user?.tier === "Free" ? "#" : `/cbt/${dpp.id}/result`}>
<Button onClick={() => user?.tier === "Free" && setSelectedPdfDpp(dpp)} variant="outline" size="sm" className="font-bold border-border">Result</Button>
</Link>
{!isExpired && (
<Link href={user?.tier === "Free" ? "#" : `/dashboard/ai-coach/cbt-dpp/${dpp.id}`}>
<Button onClick={() => user?.tier === "Free" && setSelectedPdfDpp(dpp)} variant="secondary" size="sm" className="font-bold bg-[#00F0FF]/10 text-[#00F0FF] hover:bg-[#00F0FF]/20">
{user?.tier === "Free" ? "Read PDF" : "Retake CBT"}
</Button>
</Link>
)}
</>
) : (
user?.tier === "Free" ? (
<Button onClick={() => setSelectedPdfDpp(dpp)} variant="secondary" className="rounded-xl bg-primary/10 text-primary hover:bg-primary/20 font-bold gap-2 text-xs">
<Eye size={14} /> Open PDF
</Button>
) : (
<Link href={`/dashboard/ai-coach/cbt-dpp/${dpp.id}`}>
<Button variant="secondary" size="sm" className="rounded-xl bg-[#00F0FF] text-black hover:bg-[#00F0FF]/90 font-extrabold gap-2 text-xs shadow-[0_0_12px_rgba(0,240,255,0.2)]">
<Play size={14} className="fill-black" /> Launch CBT DPP
</Button>
</Link>
)
)}
</div>
</CardContent>
</Card>
)})}
</div>
{/* ── PROTECTED PDF READER MODAL (FREE TIER) ── */}
{selectedPdfDpp && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-md flex items-center justify-center p-4 animate-in fade-in duration-300">
<div className="bg-background border border-border rounded-3xl w-full max-w-4xl max-h-[90vh] flex flex-col overflow-hidden shadow-2xl">
{/* Modal Header */}
<div className="p-5 border-b border-border bg-secondary/30 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2.5 bg-primary/10 rounded-xl border border-primary/20 text-primary">
<FileText size={22} />
</div>
<div>
<h3 className="font-black text-lg flex items-center gap-2">
{selectedPdfDpp.title}
<span className="text-[10px] bg-yellow-500/10 text-yellow-500 border border-yellow-500/20 px-2 py-0.5 rounded-full uppercase tracking-wider font-extrabold">
Protected In-App PDF View
</span>
</h3>
<p className="text-xs text-muted-foreground font-medium">
{selectedPdfDpp.difficulty} Level • {user?.target_exam?.replace("_", " ") || "JEE/NEET"} Practice Sheet
</p>
</div>
</div>
<Button
onClick={() => setSelectedPdfDpp(null)}
variant="outline"
size="icon"
className="rounded-full border-border hover:bg-secondary"
>
<X size={18} />
</Button>
</div>
{/* Security Notice Banner */}
<div className="bg-amber-500/10 border-b border-amber-500/20 px-6 py-2.5 flex items-center justify-between text-xs text-amber-400 font-bold">
<span className="flex items-center gap-2">
<ShieldAlert size={16} /> Read-Only Mode (Free Tier) — Downloads, text copying, and sharing are restricted.
</span>
<Link href="/upgrade">
<Button size="sm" className="bg-amber-500 text-black hover:bg-amber-400 font-extrabold h-7 text-[11px] rounded-lg">
Upgrade for Interactive CBT Mode →
</Button>
</Link>
</div>
{/* Protected PDF Document Sheet Content (select-none) */}
<div
onContextMenu={(e) => e.preventDefault()}
className="flex-1 w-full h-full min-h-[60vh] select-none bg-[#525659]"
>
<embed
src={getPdfUrl(selectedPdfDpp.title)}
type="application/pdf"
className="w-full h-full rounded-b-lg"
/>
</div>
{/* Modal Footer */}
<div className="p-4 border-t border-border bg-secondary/30 flex items-center justify-between">
<span className="text-xs text-muted-foreground font-medium">
📄 PragatiVeer Protected PDF Reader v2.0
</span>
<div className="flex gap-3">
<Button onClick={() => setSelectedPdfDpp(null)} variant="outline" className="rounded-xl font-bold">
Close PDF Reader
</Button>
<Link href="/upgrade">
<Button className="bg-[#00F0FF] text-black hover:bg-[#00F0FF]/90 font-extrabold rounded-xl shadow-[0_0_15px_rgba(0,240,255,0.2)]">
<Sparkles size={16} className="mr-2" /> Upgrade to AI Coach for Interactive CBT
</Button>
</Link>
</div>
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,57 @@
"use client"
import { CollegePredictorCard } from "@/components/AI/CollegePredictorCard"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { GraduationCap, ArrowLeft, Building2, MapPin, Award, CheckCircle2 } from "lucide-react"
import Link from "next/link"
export default function CollegePredictorPage() {
return (
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500 min-h-screen">
{/* Navigation Header */}
<div className="border-b border-border/50 pb-6">
<h1 className="text-3xl md:text-5xl font-black tracking-tight flex items-center gap-3">
<GraduationCap className="text-[#00F0FF]" size={36} />
AI College & Cutoff Predictor
</h1>
<p className="text-muted-foreground text-base md:text-lg mt-1 font-medium max-w-3xl">
Discover your probable target medical & engineering colleges based on current mock exam scores and historical cutoffs.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 space-y-8">
<Card className="p-8 bg-secondary/10 border-border/60 rounded-3xl">
<h2 className="text-2xl font-extrabold tracking-tight mb-6 flex items-center gap-2">
<Building2 className="text-[#00F0FF]" size={24} /> Target College Analytics
</h2>
<CollegePredictorCard />
</Card>
</div>
<div className="space-y-6">
<Card className="p-6 bg-gradient-to-br from-[#00F0FF]/10 via-secondary/20 to-background border border-[#00F0FF]/30 rounded-3xl space-y-4">
<h3 className="text-xl font-black tracking-tight text-[#00F0FF] flex items-center gap-2">
<Award size={20} /> Cutoff Modeling Parameters
</h3>
<ul className="space-y-3 text-sm font-medium text-muted-foreground">
<li className="flex items-start gap-2">
<CheckCircle2 size={16} className="text-[#00F0FF] shrink-0 mt-0.5" />
<span>Includes 100,000+ candidate rank trends across 2021-2025 rounds.</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 size={16} className="text-[#00F0FF] shrink-0 mt-0.5" />
<span>Supports All India Quota (AIQ) & State Quota adjustments.</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 size={16} className="text-[#00F0FF] shrink-0 mt-0.5" />
<span>Simulates category reservation cutoffs (General, OBC, SC, ST, EWS).</span>
</li>
</ul>
</Card>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,25 @@
"use client"
import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap"
import { ArrowLeft, Flame } from "lucide-react"
import Link from "next/link"
export default function HeatmapPage() {
return (
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500 min-h-screen">
<div className="border-b border-border/50 pb-6">
<h1 className="text-3xl md:text-5xl font-black tracking-tight flex items-center gap-3">
<Flame className="text-yellow-500" size={36} />
Mistake Heatmap Vault
</h1>
<p className="text-muted-foreground text-base md:text-lg mt-1 font-medium max-w-3xl">
Visual representation of your error density across subjects and topics.
</p>
</div>
<div className="max-w-4xl mx-auto">
<MistakeHeatmap />
</div>
</div>
)
}

View File

@@ -0,0 +1,133 @@
"use client"
import { useEffect, useState } from "react"
import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap"
import { MistakeReviewCard, MistakePayload } from "@/components/AI/MistakeReviewCard"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { BrainCircuit, RefreshCw, CheckCircle2, ArrowLeft, Flame, Sparkles, AlertTriangle } from "lucide-react"
import { useActiveUser } from "@/hooks/useActiveUser"
import Link from "next/link"
export default function MistakeTrackingPage() {
const [mistakes, setMistakes] = useState<MistakePayload[]>([])
const [currentIndex, setCurrentIndex] = useState(0)
const [loading, setLoading] = useState(true)
const { userId, loading: userLoading } = useActiveUser()
const fetchMistakes = async () => {
if (!userId) return
setLoading(true)
try {
const res = await fetch(`/api/mistakes/today?user_id=${userId}`)
if (!res.ok) throw new Error("Failed to fetch")
const data = await res.json()
setMistakes(data)
} catch (error) {
console.error(error)
setMistakes([])
} finally {
setLoading(false)
}
}
useEffect(() => {
if (!userLoading && userId) {
fetchMistakes()
}
}, [userId, userLoading])
const submitReview = async (mistakeId: string, selectedAnswer: string) => {
const res = await fetch("/api/mistakes/review", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mistake_id: mistakeId, selected_answer: selectedAnswer })
})
if (!res.ok) throw new Error("Failed to submit review")
return await res.json()
}
const handleNext = () => {
if (currentIndex < mistakes.length - 1) {
setCurrentIndex(prev => prev + 1)
} else {
setCurrentIndex(mistakes.length)
}
}
return (
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500 min-h-screen">
{/* Navigation Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 border-b border-border/50 pb-6">
<div>
<h1 className="text-3xl md:text-5xl font-black tracking-tight flex items-center gap-3">
<BrainCircuit className="text-[#00F0FF]" size={36} />
AI Mistake Vault & SM-2 Engine
</h1>
<p className="text-muted-foreground text-base md:text-lg mt-1 font-medium">
Review logged errors, practice spaced-repetition cards, and observe subject heatmap trends.
</p>
</div>
<div className="flex items-center gap-3 shrink-0">
<div className="bg-[#00F0FF]/10 text-[#00F0FF] border border-[#00F0FF]/30 px-4 py-2 rounded-2xl font-bold text-sm flex items-center gap-2">
<Sparkles size={16} /> SM-2 Adaptive Algorithm Active
</div>
</div>
</div>
{/* Main Review Workspace */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-extrabold tracking-tight flex items-center gap-2">
<Flame className="text-amber-500" size={24} /> Today's Active Review Queue
</h2>
<span className="text-sm font-bold text-[#00F0FF] bg-[#00F0FF]/10 px-4 py-1.5 rounded-full border border-[#00F0FF]/20">
{currentIndex < mistakes.length ? `Question ${currentIndex + 1} of ${mistakes.length}` : "Completed"}
</span>
</div>
{loading ? (
<Card className="p-16 flex items-center justify-center border-dashed">
<RefreshCw className="animate-spin text-[#00F0FF] mr-3" size={24} />
<span className="text-lg font-bold">Loading your active error queue...</span>
</Card>
) : mistakes.length === 0 ? (
<Card className="p-16 text-center border-dashed bg-secondary/10 rounded-3xl">
<CheckCircle2 size={56} className="mx-auto text-green-400 mb-4 opacity-80" />
<h3 className="text-2xl font-extrabold mb-2">Zero Mistakes Pending!</h3>
<p className="text-muted-foreground text-base max-w-md mx-auto mb-6">
All scheduled errors have been reviewed. Solve more mock tests to automatically surface weak questions here.
</p>
<Button onClick={() => window.location.reload()} variant="outline" className="rounded-xl font-bold">
Refresh Queue
</Button>
</Card>
) : currentIndex < mistakes.length ? (
<MistakeReviewCard
data={mistakes[currentIndex]}
onNext={handleNext}
onSubmitReview={submitReview}
/>
) : (
<Card className="p-16 text-center border-dashed bg-[#00F0FF]/5 border-[#00F0FF]/30 rounded-3xl">
<BrainCircuit size={56} className="mx-auto text-[#00F0FF] mb-4" />
<h3 className="text-2xl font-extrabold mb-2 text-[#00F0FF]">Review Session Mastered 🎉</h3>
<p className="text-muted-foreground text-base max-w-md mx-auto mb-6">
Fantastic focus! Your memory decay intervals have been updated in the database.
</p>
<Button onClick={() => window.location.reload()} className="bg-[#00F0FF] text-black font-bold rounded-xl px-6">
Check Additional Pending Queues
</Button>
</Card>
)}
</div>
{/* Heatmap & Info Column */}
<div className="space-y-6">
<MistakeHeatmap />
</div>
</div>
</div>
)
}

View File

@@ -5,16 +5,33 @@ import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap"
import { StudyPlanCard } from "@/components/AI/StudyPlanCard"
import { ReadinessGauge } from "@/components/AI/ReadinessGauge"
import { CollegePredictorCard } from "@/components/AI/CollegePredictorCard"
import { ExamStrategyCard } from "@/components/AI/ExamStrategyCard"
import { StudyTimeOptimizationCard } from "@/components/AI/StudyTimeOptimizationCard"
import { TutorChat } from "@/components/AI/TutorChat"
import { ReferralCard } from "@/components/AI/ReferralCard"
import { ProgressShare } from "@/components/AI/ProgressShare"
import { MistakeReviewCard, MistakePayload } from "@/components/AI/MistakeReviewCard"
import { DailyChallengeBanner } from "@/components/AI/DailyChallengeBanner"
import { WeeklyProgressReport } from "@/components/AI/WeeklyProgressReport"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { BrainCircuit, BookOpen, Calculator, Target, RefreshCw, CheckCircle2, Lock } from "lucide-react"
import { BrainCircuit, RefreshCw, CheckCircle2, Flame, Trophy, Target, Zap, BookOpen, Bot, FileText } from "lucide-react"
import { useActiveUser } from "@/hooks/useActiveUser"
import Link from "next/link"
// Days until NEET 2026 (May 3, 2026)
function getDaysToExam(exam: string) {
const targets: Record<string, Date> = {
"NEET": new Date("2026-05-03"),
"JEE_Main": new Date("2026-01-22"),
"JEE_Advanced": new Date("2026-05-24"),
}
const t = targets[exam] || targets["NEET"]
const diff = Math.ceil((t.getTime() - Date.now()) / 86400000)
return diff > 0 ? diff : 0
}
export default function AICoachDashboard() {
const [mistakes, setMistakes] = useState<MistakePayload[]>([])
const [currentIndex, setCurrentIndex] = useState(0)
@@ -32,7 +49,6 @@ export default function AICoachDashboard() {
setMistakes(data)
} catch (error) {
console.error(error)
// If backend fails, fallback to empty array
setMistakes([])
} finally {
setLoading(false)
@@ -51,7 +67,6 @@ export default function AICoachDashboard() {
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mistake_id: mistakeId, selected_answer: selectedAnswer })
})
if (!res.ok) throw new Error("Failed to submit review")
return await res.json()
}
@@ -60,13 +75,19 @@ export default function AICoachDashboard() {
if (currentIndex < mistakes.length - 1) {
setCurrentIndex(prev => prev + 1)
} else {
// Session finished
setCurrentIndex(mistakes.length) // Push out of bounds to trigger complete state
setCurrentIndex(mistakes.length)
}
}
if (userLoading) {
return <div className="p-8 text-center text-muted-foreground animate-pulse">Loading AI Coach...</div>
return (
<div className="h-screen flex items-center justify-center">
<div className="flex flex-col items-center gap-3 text-muted-foreground">
<BrainCircuit size={32} className="animate-pulse text-[#00F0FF]" />
<span className="text-sm font-medium">Loading AI Coach...</span>
</div>
</div>
)
}
if (!hasAccessTo("ai-mistake-tracking")) {
@@ -78,7 +99,7 @@ export default function AICoachDashboard() {
<div className="space-y-4">
<h1 className="text-4xl font-extrabold tracking-tight">AI Coach Workspace</h1>
<p className="text-muted-foreground text-lg max-w-xl mx-auto leading-relaxed">
Mistake tracking, Spaced Repetition (SM-2), AI Tutor Chats, and Readiness scores are premium features reserved for **AI Coach** subscribers.
Mistake tracking, Spaced Repetition (SM-2), AI Tutor Chats, and Readiness scores are premium features reserved for <strong>AI Coach</strong> subscribers.
</p>
</div>
<div className="pt-4">
@@ -92,81 +113,113 @@ export default function AICoachDashboard() {
)
}
const daysLeft = getDaysToExam(user?.target_exam || "NEET")
const physicsErrors = mistakes.filter(m => m.question.subject === "Physics").length
const chemErrors = mistakes.filter(m => m.question.subject === "Chemistry").length
const mathErrors = mistakes.filter(m => m.question.subject === "Maths" || m.question.subject === "Mathematics").length
return (
<div className="p-8 max-w-6xl mx-auto space-y-10 animate-in fade-in duration-500">
<header className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h1 className="text-3xl font-bold tracking-tight flex items-center gap-3">
<BrainCircuit className="text-primary" size={32}/> AI Coach
</h1>
<p className="text-muted-foreground mt-1 font-medium">Your personalized SM-2 spaced repetition and analytics engine.</p>
</div>
</header>
<div className="p-6 max-w-6xl mx-auto space-y-8 animate-in fade-in duration-500">
{/* Summary Metrics */}
<section className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="border-border shadow-sm">
<CardContent className="p-5 flex items-center gap-4">
<div className="p-3 bg-secondary rounded-xl text-primary"><Calculator size={20}/></div>
<div>
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider">Physics Errors</p>
<h3 className="text-2xl font-bold">{mistakes.filter(m => m.question.subject === 'Physics').length}</h3>
</div>
</CardContent>
</Card>
<Card className="border-border shadow-sm">
<CardContent className="p-5 flex items-center gap-4">
<div className="p-3 bg-secondary rounded-xl text-emerald-500"><BookOpen size={20}/></div>
<div>
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider">Chemistry Errors</p>
<h3 className="text-2xl font-bold">{mistakes.filter(m => m.question.subject === 'Chemistry').length}</h3>
</div>
</CardContent>
</Card>
<Card className="border-border shadow-sm">
<CardContent className="p-5 flex items-center gap-4">
<div className="p-3 bg-secondary rounded-xl text-blue-500"><Target size={20}/></div>
<div>
<p className="text-sm font-bold text-muted-foreground uppercase tracking-wider">Maths Errors</p>
<h3 className="text-2xl font-bold">{mistakes.filter(m => m.question.subject === 'Maths').length}</h3>
</div>
</CardContent>
</Card>
{/* ── METRICS BAR ── */}
<div className="flex flex-wrap items-center justify-end gap-2 pt-2">
<div className="inline-flex items-center gap-1.5 bg-yellow-500/10 text-yellow-500 px-3 py-1.5 rounded-xl text-xs font-bold border border-yellow-500/20">
<Flame size={14} className="fill-yellow-500" />
{user?.streak_count || 0}-Day Streak
</div>
<div className="inline-flex items-center gap-1.5 bg-red-500/10 text-red-400 px-3 py-1.5 rounded-xl text-xs font-bold border border-red-500/20">
<Target size={13} />
{daysLeft} days to {user?.target_exam?.replace("_", " ") || "Exam"}
</div>
<div className="inline-flex items-center gap-1.5 bg-purple-500/10 text-purple-400 px-3 py-1.5 rounded-xl text-xs font-bold border border-purple-500/20">
<Trophy size={13} />
AI Coach
</div>
</div>
{/* ── DAILY CHALLENGE BANNER ── */}
<DailyChallengeBanner />
{/* ── QUICK STATS ── */}
<section className="grid grid-cols-2 md:grid-cols-5 gap-4">
{[
{ label: "Questions Solved", value: "1,247", color: "text-[#00F0FF]", bg: "bg-[#00F0FF]/5", border: "border-[#00F0FF]/20", icon: <FileText size={32} /> },
{ label: "Overall Accuracy", value: "74%", color: "text-amber-500", bg: "bg-amber-500/5", border: "border-amber-500/20", icon: <Target size={32} /> },
{ label: "Current Streak", value: `${user?.streak_count || 0} d`, color: "text-yellow-400", bg: "bg-yellow-400/5", border: "border-yellow-400/20", icon: <Flame size={32} className="fill-yellow-400" /> },
{ label: "Mistakes Mastered", value: 38, color: "text-green-400", bg: "bg-green-400/5", border: "border-green-400/20", icon: <CheckCircle2 size={32} /> },
{ label: "AI Sessions", value: 124, color: "text-purple-400", bg: "bg-purple-400/5", border: "border-purple-400/20", icon: <Bot size={32} /> },
].map((stat) => (
<Card key={stat.label} className={`border ${stat.border} ${stat.bg} backdrop-blur-sm shadow-sm hover:shadow-[0_0_20px_rgba(0,240,255,0.2)] transition-all duration-300 transform hover:-translate-y-1 rounded-2xl`}>
<CardContent className="p-6 flex flex-col items-center justify-center text-center">
<div className="text-3xl mb-2 filter drop-shadow-md">{stat.icon}</div>
<div className={`text-4xl font-black ${stat.color} leading-none tracking-tight mb-2 filter drop-shadow-sm`}>{stat.value}</div>
<div className="text-xs font-extrabold text-muted-foreground uppercase tracking-wider">{stat.label}</div>
</CardContent>
</Card>
))}
</section>
{/* Predictive Intelligence */}
<section className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
<ReadinessGauge />
<CollegePredictorCard />
{/* ── PREDICTIVE INTELLIGENCE ROW ── */}
<section className="mt-10">
<div className="flex items-center justify-between gap-3 mb-6">
<div className="flex items-center gap-2 text-base md:text-lg font-black uppercase tracking-widest text-[#00F0FF]">
<Zap size={20} />
Predictive Intelligence & Strategy
</div>
<div className="flex items-center gap-3">
<Link href="/dashboard/ai-coach/readiness" className="text-xs font-bold text-[#00F0FF] hover:underline flex items-center gap-1">
Full Analytics →
</Link>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<ReadinessGauge />
<CollegePredictorCard />
<ExamStrategyCard />
<StudyTimeOptimizationCard />
</div>
</section>
{/* Mistake Review Workspace */}
<section>
<div className="flex items-center justify-between mb-5">
<h2 className="text-xl font-semibold tracking-tight">Daily Mistake Review Queue</h2>
<span className="text-sm font-bold text-muted-foreground bg-secondary px-3 py-1 rounded-full">
{currentIndex < mistakes.length ? `${currentIndex + 1} / ${mistakes.length}` : "Finished"}
</span>
{/* ── MISTAKE REVIEW WORKSPACE ── */}
<section className="mt-12">
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3">
<div className="p-2.5 bg-[#00F0FF]/10 rounded-xl border border-[#00F0FF]/20">
<BrainCircuit size={24} className="text-[#00F0FF]" />
</div>
<div>
<h2 className="text-2xl md:text-3xl font-black tracking-tight">Daily Mistake Review Queue</h2>
<p className="text-sm text-muted-foreground font-medium">SM-2 Spaced Repetition Flashcard Engine</p>
</div>
</div>
<div className="flex items-center gap-3">
<Link href="/dashboard/ai-coach/mistakes" className="hidden sm:inline-flex text-xs font-extrabold text-[#00F0FF] bg-[#00F0FF]/10 px-4 py-2 rounded-xl border border-[#00F0FF]/30 hover:bg-[#00F0FF] hover:text-black transition-all">
Open Full Mistake Vault Page →
</Link>
<span className="text-sm font-extrabold text-[#00F0FF] bg-[#00F0FF]/10 px-4 py-1.5 rounded-full border border-[#00F0FF]/30 shadow-[0_0_10px_rgba(0,240,255,0.15)]">
{currentIndex < mistakes.length ? `${currentIndex + 1} / ${mistakes.length}` : "Finished"}
</span>
</div>
</div>
{loading ? (
<Card className="p-12 flex items-center justify-center border-dashed">
<RefreshCw className="animate-spin text-muted-foreground mr-3" /> Loading queue...
</Card>
) : mistakes.length === 0 ? (
<Card className="p-12 text-center border-dashed bg-secondary/20">
<Card className="p-12 text-center border-dashed bg-secondary/20 rounded-3xl">
<CheckCircle2 size={48} className="mx-auto text-green-500 mb-4 opacity-50" />
<h3 className="text-xl font-bold mb-2">You're all caught up!</h3>
<p className="text-muted-foreground">No mistakes scheduled for review today. Go take a test!</p>
</Card>
) : currentIndex < mistakes.length ? (
<MistakeReviewCard
data={mistakes[currentIndex]}
<MistakeReviewCard
data={mistakes[currentIndex]}
onNext={handleNext}
onSubmitReview={submitReview}
/>
) : (
<Card className="p-12 text-center border-dashed bg-primary/5 border-primary/20">
<Card className="p-12 text-center border-dashed bg-primary/5 border-primary/20 rounded-3xl">
<BrainCircuit size={48} className="mx-auto text-primary mb-4 opacity-80" />
<h3 className="text-xl font-bold mb-2 text-primary">Session Complete</h3>
<p className="text-muted-foreground mb-6">Great job clearing your error queue! The SM-2 engine has scheduled your next reviews.</p>
@@ -175,23 +228,57 @@ export default function AICoachDashboard() {
)}
</section>
{/* Heatmap & Timeline */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8 pt-8 border-t border-border">
<section>
{/* ── HEATMAP / STUDY PLAN / WEEKLY REPORT ── */}
<section className="grid grid-cols-1 lg:grid-cols-2 gap-8 pt-10 border-t border-border">
<div>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2 text-base font-extrabold">
<BookOpen size={18} className="text-[#00F0FF]" /> Study Plan
</div>
<Link href="/dashboard/ai-coach/study-plan" className="text-xs font-bold text-[#00F0FF] hover:underline">
Full Schedule →
</Link>
</div>
<StudyPlanCard />
</section>
<section>
</div>
<div>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2 text-base font-extrabold">
<Flame size={18} className="text-yellow-500" /> Mistake Heatmap
</div>
<Link href="/dashboard/ai-coach/mistakes" className="text-xs font-bold text-[#00F0FF] hover:underline">
Heatmap Vault →
</Link>
</div>
<MistakeHeatmap />
</section>
</div>
</div>
</section>
{/* Tutor Chat Workspace */}
<section className="pt-8 border-t border-border">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2 text-base font-extrabold">
<Target size={18} className="text-[#00F0FF]" /> Weekly Progress Overview
</div>
<Link href="/dashboard/ai-coach/study-plan" className="text-xs font-bold text-[#00F0FF] hover:underline">
View Details →
</Link>
</div>
<WeeklyProgressReport />
</section>
{/* ── TUTOR CHAT ── */}
<section className="pt-8 border-t border-border">
<div className="flex items-center justify-between mb-4">
<h2 className="text-2xl font-extrabold tracking-tight">24/7 AI Tutor Chat Assistant</h2>
<Link href="/dashboard/ai-coach/tutor" className="text-xs font-extrabold text-[#00F0FF] bg-[#00F0FF]/10 px-3.5 py-1.5 rounded-xl border border-[#00F0FF]/30 hover:bg-[#00F0FF] hover:text-black transition-all">
Full Desk Mode →
</Link>
</div>
<TutorChat />
</section>
{/* Growth & Sharing Hub */}
<section className="grid grid-cols-1 lg:grid-cols-2 gap-8 pt-8 border-t border-border mt-8">
{/* ── GROWTH HUB ── */}
<section className="grid grid-cols-1 lg:grid-cols-2 gap-6 pt-6 border-t border-border">
<ReferralCard />
<ProgressShare />
</section>
@@ -199,3 +286,4 @@ export default function AICoachDashboard() {
</div>
)
}

View File

@@ -0,0 +1,79 @@
"use client"
import { ReadinessGauge } from "@/components/AI/ReadinessGauge"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Gauge, ArrowLeft, Target, Award, ShieldAlert, CheckCircle2 } from "lucide-react"
import Link from "next/link"
export default function ReadinessPage() {
return (
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500 min-h-screen">
{/* Navigation Header */}
<div className="border-b border-border/50 pb-6">
<h1 className="text-3xl md:text-5xl font-black tracking-tight flex items-center gap-3">
<Gauge className="text-[#00F0FF]" size={36} />
Exam Readiness Analytics
</h1>
<p className="text-muted-foreground text-base md:text-lg mt-1 font-medium max-w-3xl">
Real-time algorithmic prediction of your readiness percentage for JEE & NEET based on speed, accuracy, and test history.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 space-y-8">
<Card className="p-8 bg-secondary/10 border-border/60 rounded-3xl">
<h2 className="text-2xl font-extrabold tracking-tight mb-6 flex items-center gap-2">
<Award className="text-amber-400" size={24} /> Overall Readiness Index
</h2>
<ReadinessGauge />
</Card>
{/* Subject Confidence Breakdown */}
<Card className="p-6 bg-secondary/10 border-border/60 rounded-3xl space-y-6">
<h3 className="text-xl font-extrabold tracking-tight">Subject Confidence Matrix</h3>
<div className="space-y-4">
{[
{ subject: "Biology / Maths", score: 82, color: "bg-[#00F0FF]", status: "High Readiness" },
{ subject: "Chemistry", score: 74, color: "bg-amber-400", status: "Moderate Readiness" },
{ subject: "Physics", score: 61, color: "bg-red-400", status: "Needs Intensive Drill" },
].map((sub) => (
<div key={sub.subject} className="space-y-2">
<div className="flex justify-between items-center text-sm font-bold">
<span>{sub.subject}</span>
<span className="text-muted-foreground">{sub.score}% • {sub.status}</span>
</div>
<div className="h-3 w-full bg-secondary rounded-full overflow-hidden">
<div className={`h-full ${sub.color}`} style={{ width: `${sub.score}%` }} />
</div>
</div>
))}
</div>
</Card>
</div>
<div className="space-y-6">
<Card className="p-6 bg-gradient-to-br from-amber-500/10 via-secondary/20 to-background border border-amber-500/30 rounded-3xl space-y-4">
<h3 className="text-xl font-black tracking-tight text-amber-400 flex items-center gap-2">
<ShieldAlert size={20} /> Readiness Boosters
</h3>
<ul className="space-y-3 text-sm font-medium text-muted-foreground">
<li className="flex items-start gap-2">
<CheckCircle2 size={16} className="text-amber-400 shrink-0 mt-0.5" />
<span>Complete 2 Full-Length Mock Tests this week.</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 size={16} className="text-amber-400 shrink-0 mt-0.5" />
<span>Clear 15 pending errors in Physics error queue.</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 size={16} className="text-amber-400 shrink-0 mt-0.5" />
<span>Maintain a 7+ day study streak.</span>
</li>
</ul>
</Card>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,41 @@
"use client"
import { ExamStrategyCard } from "@/components/AI/ExamStrategyCard"
import { StudyTimeOptimizationCard } from "@/components/AI/StudyTimeOptimizationCard"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Zap, ArrowLeft, Clock, Target, Lightbulb, CheckCircle2 } from "lucide-react"
import Link from "next/link"
export default function StrategyPage() {
return (
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500 min-h-screen">
{/* Navigation Header */}
<div className="border-b border-border/50 pb-6">
<h1 className="text-3xl md:text-5xl font-black tracking-tight flex items-center gap-3">
<Zap className="text-[#00F0FF]" size={36} />
Exam Strategy & Time Optimization
</h1>
<p className="text-muted-foreground text-base md:text-lg mt-1 font-medium max-w-3xl">
Master time allocation per question, eliminate negative marking traps, and fine-tune your subject sequence for maximum score yield.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<Card className="p-6 bg-secondary/10 border-border/60 rounded-3xl">
<h2 className="text-2xl font-extrabold tracking-tight mb-4 flex items-center gap-2">
<Target className="text-[#00F0FF]" size={24} /> Exam Day Time Strategy
</h2>
<ExamStrategyCard />
</Card>
<Card className="p-6 bg-secondary/10 border-border/60 rounded-3xl">
<h2 className="text-2xl font-extrabold tracking-tight mb-4 flex items-center gap-2">
<Clock className="text-amber-400" size={24} /> Study Time Optimization
</h2>
<StudyTimeOptimizationCard />
</Card>
</div>
</div>
)
}

View File

@@ -0,0 +1,66 @@
"use client"
import { StudyPlanCard } from "@/components/AI/StudyPlanCard"
import { WeeklyProgressReport } from "@/components/AI/WeeklyProgressReport"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Calendar, ArrowLeft, BookOpen, Clock, Target, CheckCircle2 } from "lucide-react"
import Link from "next/link"
export default function StudyPlanPage() {
return (
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500 min-h-screen">
{/* Navigation Header */}
<div className="border-b border-border/50 pb-6">
<h1 className="text-3xl md:text-5xl font-black tracking-tight flex items-center gap-3">
<Calendar className="text-[#00F0FF]" size={36} />
Personalized Daily & Weekly Study Plan
</h1>
<p className="text-muted-foreground text-base md:text-lg mt-1 font-medium max-w-3xl">
AI-generated task schedule dynamically tailored to fix your weak areas and maintain optimal syllabus coverage.
</p>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
<div className="lg:col-span-2 space-y-8">
<Card className="p-6 bg-secondary/10 border-border/60 rounded-3xl">
<h2 className="text-2xl font-extrabold tracking-tight mb-4 flex items-center gap-2">
<Clock className="text-[#00F0FF]" size={24} /> Today's Action Plan
</h2>
<StudyPlanCard />
</Card>
<Card className="p-6 bg-secondary/10 border-border/60 rounded-3xl">
<h2 className="text-2xl font-extrabold tracking-tight mb-4 flex items-center gap-2">
<Target className="text-emerald-400" size={24} /> Weekly Syllabus Target
</h2>
<WeeklyProgressReport />
</Card>
</div>
{/* Sidebar Info */}
<div className="space-y-6">
<Card className="p-6 bg-gradient-to-br from-[#00F0FF]/10 via-secondary/20 to-background border border-[#00F0FF]/30 rounded-3xl space-y-4">
<h3 className="text-xl font-black tracking-tight text-[#00F0FF] flex items-center gap-2">
<BookOpen size={20} /> AI Scheduler Rules
</h3>
<ul className="space-y-3 text-sm font-medium text-muted-foreground">
<li className="flex items-start gap-2">
<CheckCircle2 size={16} className="text-[#00F0FF] shrink-0 mt-0.5" />
<span><strong>High-Yield Prioritization:</strong> Focuses extra time on topics with heavy exam weightage.</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 size={16} className="text-[#00F0FF] shrink-0 mt-0.5" />
<span><strong>Error Interleaving:</strong> Integrates recent incorrect questions directly into study blocks.</span>
</li>
<li className="flex items-start gap-2">
<CheckCircle2 size={16} className="text-[#00F0FF] shrink-0 mt-0.5" />
<span><strong>Burnout Prevention:</strong> Recommends 10-minute micro-breaks between intensive problem sets.</span>
</li>
</ul>
</Card>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,25 @@
"use client"
import { StudyTimeOptimizationCard } from "@/components/AI/StudyTimeOptimizationCard"
import { ArrowLeft, Clock } from "lucide-react"
import Link from "next/link"
export default function TimeOptimizationPage() {
return (
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500 min-h-screen">
<div className="border-b border-border/50 pb-6">
<h1 className="text-3xl md:text-5xl font-black tracking-tight flex items-center gap-3">
<Clock className="text-[#00F0FF]" size={36} />
Study Time Optimization
</h1>
<p className="text-muted-foreground text-base md:text-lg mt-1 font-medium max-w-3xl">
Detailed metrics on your productive hours and stamina blocks.
</p>
</div>
<div className="max-w-4xl mx-auto">
<StudyTimeOptimizationCard />
</div>
</div>
)
}

View File

@@ -0,0 +1,13 @@
"use client"
import { TutorChat } from "@/components/AI/TutorChat"
export default function TutorChatPage() {
return (
<div className="h-[calc(100vh-80px)] w-full flex flex-col p-4 md:p-6 animate-in fade-in duration-500">
<div className="flex-1 rounded-3xl overflow-hidden border border-border/50 shadow-xl bg-background">
<TutorChat />
</div>
</div>
)
}

View File

@@ -0,0 +1,25 @@
"use client"
import { WeeklyProgressReport } from "@/components/AI/WeeklyProgressReport"
import { ArrowLeft, Target } from "lucide-react"
import Link from "next/link"
export default function WeeklyProgressPage() {
return (
<div className="p-6 md:p-8 max-w-7xl mx-auto space-y-8 animate-in fade-in duration-500 min-h-screen">
<div className="border-b border-border/50 pb-6">
<h1 className="text-3xl md:text-5xl font-black tracking-tight flex items-center gap-3">
<Target className="text-[#00F0FF]" size={36} />
Weekly Progress Overview
</h1>
<p className="text-muted-foreground text-base md:text-lg mt-1 font-medium max-w-3xl">
Track your syllabus completion goals and weekly milestones.
</p>
</div>
<div className="max-w-4xl mx-auto">
<WeeklyProgressReport />
</div>
</div>
)
}

View File

@@ -2,7 +2,7 @@
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { BrainCircuit, Play, CheckCircle2, Download, Filter, Lock } from "lucide-react"
import { BrainCircuit, Play, CheckCircle2, Download, Filter, Lock, FileText, Eye, ShieldAlert, X, Sparkles } from "lucide-react"
import { useEffect, useState } from "react"
import Link from "next/link"
@@ -14,6 +14,7 @@ export default function DPPPage() {
const [attempts, setAttempts] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [dateFilter, setDateFilter] = useState("")
const [selectedPdfDpp, setSelectedPdfDpp] = useState<any | null>(null)
useEffect(() => {
async function fetchDpps() {
@@ -76,6 +77,16 @@ export default function DPPPage() {
return new Date(dateStr).toLocaleDateString("en-US", { year: 'numeric', month: 'short', day: 'numeric' });
}
const getPdfUrl = (title: string) => {
const t = title.toLowerCase();
if (t.includes('phys')) return '/dpps/dps-phy-ch4-20260707.pdf#toolbar=0&navpanes=0&scrollbar=0';
if (t.includes('chem')) return '/dpps/dps-che-ch4-20260707.pdf#toolbar=0&navpanes=0&scrollbar=0';
if (t.includes('math')) return '/dpps/dps-mth-ch4-20260707.pdf#toolbar=0&navpanes=0&scrollbar=0';
if (t.includes('bio')) return '/dpps/dps-bio-ch4-20260707.pdf#toolbar=0&navpanes=0&scrollbar=0';
// Default fallback
return '/dpps/dps-phy-ch4-20260707.pdf#toolbar=0&navpanes=0&scrollbar=0';
};
return (
<div className="p-8 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500 max-w-5xl mx-auto">
<div className="flex flex-col md:flex-row md:items-center justify-between p-8 bg-gradient-to-r from-secondary/50 to-background border border-border rounded-2xl shadow-sm gap-6">
@@ -181,27 +192,111 @@ export default function DPPPage() {
<Button disabled variant="outline" size="sm" className="font-bold bg-secondary/50">Expired</Button>
) : isAttempted ? (
<>
<Link href={`/cbt/${dpp.id}/result`}>
<Button variant="outline" size="sm" className="font-bold border-border">Result</Button>
<Link href={user?.tier === "Free" ? "#" : `/cbt/${dpp.id}/result`}>
<Button onClick={() => user?.tier === "Free" && setSelectedPdfDpp(dpp)} variant="outline" size="sm" className="font-bold border-border">Result</Button>
</Link>
{!isExpired && (
<Link href={`/cbt/${dpp.id}`}>
<Button variant="secondary" size="sm" className="font-bold bg-[#00F0FF]/10 text-[#00F0FF] hover:bg-[#00F0FF]/20">Retake</Button>
<Link href={user?.tier === "Free" ? "#" : `/dashboard/ai-coach/cbt-dpp/${dpp.id}`}>
<Button onClick={() => user?.tier === "Free" && setSelectedPdfDpp(dpp)} variant="secondary" size="sm" className="font-bold bg-[#00F0FF]/10 text-[#00F0FF] hover:bg-[#00F0FF]/20">
{user?.tier === "Free" ? "Read PDF" : "Retake CBT"}
</Button>
</Link>
)}
</>
) : (
<Link href={`/cbt/${dpp.id}`}>
<Button variant="secondary" size="icon" className="rounded-full group-hover:bg-primary group-hover:text-primary-foreground hover:bg-primary hover:text-primary-foreground shadow-sm transition-all duration-300">
<Play size={16} className="ml-1" />
user?.tier === "Free" ? (
<Button onClick={() => setSelectedPdfDpp(dpp)} variant="secondary" className="rounded-xl bg-primary/10 text-primary hover:bg-primary/20 font-bold gap-2 text-xs">
<Eye size={14} /> Open PDF
</Button>
</Link>
) : (
<Link href={`/dashboard/ai-coach/cbt-dpp/${dpp.id}`}>
<Button variant="secondary" size="sm" className="rounded-xl bg-[#00F0FF] text-black hover:bg-[#00F0FF]/90 font-extrabold gap-2 text-xs shadow-[0_0_12px_rgba(0,240,255,0.2)]">
<Play size={14} className="fill-black" /> Launch CBT DPP
</Button>
</Link>
)
)}
</div>
</CardContent>
</Card>
)})}
</div>
{/* ── PROTECTED PDF READER MODAL (FREE TIER) ── */}
{selectedPdfDpp && (
<div className="fixed inset-0 z-50 bg-black/80 backdrop-blur-md flex items-center justify-center p-4 animate-in fade-in duration-300">
<div className="bg-background border border-border rounded-3xl w-full max-w-4xl max-h-[90vh] flex flex-col overflow-hidden shadow-2xl">
{/* Modal Header */}
<div className="p-5 border-b border-border bg-secondary/30 flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="p-2.5 bg-primary/10 rounded-xl border border-primary/20 text-primary">
<FileText size={22} />
</div>
<div>
<h3 className="font-black text-lg flex items-center gap-2">
{selectedPdfDpp.title}
<span className="text-[10px] bg-yellow-500/10 text-yellow-500 border border-yellow-500/20 px-2 py-0.5 rounded-full uppercase tracking-wider font-extrabold">
Protected In-App PDF View
</span>
</h3>
<p className="text-xs text-muted-foreground font-medium">
{selectedPdfDpp.difficulty} Level • {user?.target_exam?.replace("_", " ") || "JEE/NEET"} Practice Sheet
</p>
</div>
</div>
<Button
onClick={() => setSelectedPdfDpp(null)}
variant="outline"
size="icon"
className="rounded-full border-border hover:bg-secondary"
>
<X size={18} />
</Button>
</div>
{/* Security Notice Banner */}
<div className="bg-amber-500/10 border-b border-amber-500/20 px-6 py-2.5 flex items-center justify-between text-xs text-amber-400 font-bold">
<span className="flex items-center gap-2">
<ShieldAlert size={16} /> Read-Only Mode (Free Tier) — Downloads, text copying, and sharing are restricted.
</span>
<Link href="/upgrade">
<Button size="sm" className="bg-amber-500 text-black hover:bg-amber-400 font-extrabold h-7 text-[11px] rounded-lg">
Upgrade for Interactive CBT Mode →
</Button>
</Link>
</div>
{/* Protected PDF Document Sheet Content (select-none) */}
<div
onContextMenu={(e) => e.preventDefault()}
className="flex-1 w-full h-full min-h-[60vh] select-none bg-[#525659]"
>
<embed
src={getPdfUrl(selectedPdfDpp.title)}
type="application/pdf"
className="w-full h-full rounded-b-lg"
/>
</div>
{/* Modal Footer */}
<div className="p-4 border-t border-border bg-secondary/30 flex items-center justify-between">
<span className="text-xs text-muted-foreground font-medium">
📄 PragatiVeer Protected PDF Reader v2.0
</span>
<div className="flex gap-3">
<Button onClick={() => setSelectedPdfDpp(null)} variant="outline" className="rounded-xl font-bold">
Close PDF Reader
</Button>
<Link href="/upgrade">
<Button className="bg-[#00F0FF] text-black hover:bg-[#00F0FF]/90 font-extrabold rounded-xl shadow-[0_0_15px_rgba(0,240,255,0.2)]">
<Sparkles size={16} className="mr-2" /> Upgrade to AI Coach for Interactive CBT
</Button>
</Link>
</div>
</div>
</div>
</div>
)}
</div>
)
}

View File

@@ -1,7 +1,7 @@
"use client"
import { ReactNode, useState, useEffect, useRef } from "react"
import { BookOpen, LayoutDashboard, Settings, FileText, Activity, LineChart, Trophy, LogOut, User, ChevronUp, Lock } from "lucide-react"
import { BookOpen, LayoutDashboard, Settings, FileText, Activity, LineChart, Trophy, LogOut, User, ChevronUp, Lock, ShieldAlert, Flame, Target } from "lucide-react"
import Link from "next/link"
import { usePathname } from "next/navigation"
import { LanguageToggle } from "@/components/LanguageToggle"
@@ -13,13 +13,30 @@ import { AITutorChat } from "@/components/AI/AITutorChat"
import { useActiveUser, DEMO_USERS } from "@/hooks/useActiveUser"
import { StreakOnboardingModal } from "@/components/Dashboard/StreakOnboardingModal"
import { Badge } from "@/components/ui/badge"
import MaintenanceScreen from "@/components/MaintenanceScreen"
export default function DashboardLayout({ children }: { children: ReactNode }) {
const pathname = usePathname()
const [dropdownOpen, setDropdownOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
const { userId, user, switchUser, hasAccessTo, loading } = useActiveUser();
const { userId, user, globalSettings, switchUser, hasAccessTo, loading } = useActiveUser();
const [maintenance, setMaintenance] = useState(false);
const [checkingMaintenance, setCheckingMaintenance] = useState(true);
useEffect(() => {
if (globalSettings !== undefined && globalSettings !== null) {
if (globalSettings.maintenance_mode) {
setMaintenance(true);
} else {
setMaintenance(false);
}
setCheckingMaintenance(false);
} else if (globalSettings === null && !loading) {
// If we finished loading but settings failed to load
setCheckingMaintenance(false);
}
}, [globalSettings, loading]);
// Close dropdown when clicking outside
useEffect(() => {
@@ -35,15 +52,22 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
const userInitials = user ? user.name.split(" ").filter(n => /^[a-zA-Z]/.test(n)).slice(0, 2).map(n => n[0]).join("").toUpperCase() : "DS"
const targetExamLabel = user?.target_exam === "JEE_Main" ? "JEE Main" : user?.target_exam === "JEE_Advanced" ? "JEE Advanced" : user?.target_exam === "NEET" ? "NEET" : "NEET/JEE"
let daysLeft = null;
if (user && user.created_at) {
const createdDate = new Date(user.created_at);
const msPerDay = 1000 * 60 * 60 * 24;
const daysElapsed = Math.floor((new Date().getTime() - createdDate.getTime()) / msPerDay);
daysLeft = Math.max(0, 15 - daysElapsed);
} else if (user && user.tier === 'Free') {
// Mock value for demo Free tier if no date
daysLeft = 14;
let daysLeft: number | null = null;
const trialDays = globalSettings?.trial_duration_days !== undefined ? globalSettings.trial_duration_days : 15;
const trialEnabled = globalSettings?.enable_free_trial !== undefined ? globalSettings.enable_free_trial : true;
// For the demo, treat both Free tier and Neha as Trial users so the banner appears
if (user && (user.tier === 'Free' || user.id === DEMO_USERS.NEHA.id)) {
if (!trialEnabled) {
daysLeft = 0;
} else {
// For demo purposes, we ignore created_at and just show the exact days set in the admin panel
daysLeft = trialDays;
}
}
if (maintenance) {
return <MaintenanceScreen />;
}
return (
@@ -61,7 +85,6 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
</div>
<NavItem href="/dashboard" icon={<LayoutDashboard size={18} />} label="Overview" active={pathname === "/dashboard"} />
<NavItem href="/dashboard/dpp" icon={<FileText size={18} />} label="Target DPPs" active={pathname.startsWith("/dashboard/dpp")} />
<NavItem href="/dashboard/progress" icon={<Activity size={18} />} label="Syllabus Tracker" active={pathname.startsWith("/dashboard/progress")} />
<NavItem href="/dashboard/leaderboard" icon={<Trophy size={18} />} label="Leaderboard" active={pathname.startsWith("/dashboard/leaderboard")} />
<div className="pt-4 pb-2">
@@ -69,6 +92,13 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
<span className="w-1.5 h-1.5 rounded-full bg-yellow-500 shadow-[0_0_8px_-2px_var(--color-yellow-500)]"></span> Pro Tier
</p>
</div>
<NavItem
href="/dashboard/progress"
icon={<Activity size={18} />}
label="Syllabus Tracker"
active={pathname.startsWith("/dashboard/progress")}
isLocked={!hasAccessTo("chapter-wise-progress-tracking")}
/>
<NavItem
href="/dashboard/cbt"
icon={<Activity size={18} />}
@@ -95,7 +125,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
icon={<BookOpen size={18} />}
label="Doubt Forum"
active={pathname.startsWith("/dashboard/community")}
isLocked={!hasAccessTo("ask-doubts-from-tutors")}
isLocked={!hasAccessTo("doubt-resolution-community")}
/>
<div className="pt-4 pb-2">
@@ -104,19 +134,75 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
</p>
</div>
<NavItem
href="/dashboard/ai-coach"
href="/dashboard/ai-coach/tutor"
icon={<BrainCircuitIcon />}
label="AI Coach"
active={pathname.startsWith("/dashboard/ai-coach")}
label="AI Tutor"
active={pathname.startsWith("/dashboard/ai-coach/tutor") || pathname === "/dashboard/ai-coach"}
isLocked={!hasAccessTo("ai-tutor-chat")}
/>
<NavItem
href="/dashboard/rank-predictor"
href="/dashboard/ai-coach/cbt-dpp"
icon={<FileText size={18} />}
label="PrepPilot AI"
active={pathname.startsWith("/dashboard/ai-coach/cbt-dpp")}
isLocked={!hasAccessTo("ai-tutor-chat")}
/>
<NavItem
href="/dashboard/ai-coach/readiness"
icon={<Activity size={18} />}
label="Exam Readiness"
active={pathname.startsWith("/dashboard/ai-coach/readiness")}
isLocked={!hasAccessTo("detailed-performance-analytics")}
/>
<NavItem
href="/dashboard/ai-coach/college-predictor"
icon={<Trophy size={18} />}
label="Rank Predictor"
active={pathname.startsWith("/dashboard/rank-predictor")}
label="College Predictor"
active={pathname.startsWith("/dashboard/ai-coach/college-predictor")}
isLocked={!hasAccessTo("college-predictor")}
/>
<NavItem
href="/dashboard/ai-coach/strategy"
icon={<BookOpen size={18} />}
label="Exam Strategy"
active={pathname.startsWith("/dashboard/ai-coach/strategy")}
isLocked={!hasAccessTo("exam-day-strategy")}
/>
<NavItem
href="/dashboard/ai-coach/study-plan"
icon={<LineChart size={18} />}
label="AI Study Plan"
active={pathname.startsWith("/dashboard/ai-coach/study-plan")}
isLocked={!hasAccessTo("personalized-study-plan")}
/>
<NavItem
href="/dashboard/ai-coach/time-optimization"
icon={<Activity size={18} />}
label="Study Time Optimization"
active={pathname.startsWith("/dashboard/ai-coach/time-optimization")}
isLocked={!hasAccessTo("personalized-study-plan")}
/>
<NavItem
href="/dashboard/ai-coach/heatmap"
icon={<Flame size={18} />}
label="Mistake Heatmap"
active={pathname.startsWith("/dashboard/ai-coach/heatmap")}
isLocked={!hasAccessTo("ai-mistake-tracking")}
/>
<NavItem
href="/dashboard/ai-coach/weekly-progress"
icon={<Target size={18} />}
label="Weekly Progress"
active={pathname.startsWith("/dashboard/ai-coach/weekly-progress")}
isLocked={!hasAccessTo("detailed-performance-analytics")}
/>
<NavItem
href="/dashboard/ai-coach/mistakes"
icon={<Activity size={18} />}
label="Mistake Analyzer"
active={pathname.startsWith("/dashboard/ai-coach/mistakes")}
isLocked={!hasAccessTo("ai-mistake-tracking")}
/>
</nav>
<div className="p-4 border-t border-border space-y-2 bg-secondary/30">
@@ -179,17 +265,33 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
<header className="h-16 border-b border-border bg-background/95 backdrop-blur-md flex items-center justify-between px-8 shrink-0 z-40">
<div className="flex items-center gap-4">
<h2 className="font-bold text-lg hidden sm:block">
{user ? `Welcome back, ${user.name.split(" ")[0]}! 👋` : "Welcome back!"}
{user ? `Welcome back, ${user.name.split(" ")[0]}!` : "Welcome back!"}
</h2>
{user && (
<Badge variant="outline" className="bg-[#00F0FF]/10 text-[#00F0FF] border-[#00F0FF]/20 font-bold tracking-wide uppercase px-2.5 py-0.5 animate-pulse">
{targetExamLabel} Target
</Badge>
)}
{daysLeft !== null && user?.tier !== 'Al_Coach' && user?.tier !== 'Pro' && (
<Badge variant="outline" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20 font-bold px-2.5 py-0.5">
{daysLeft} Days Left in AI Coach Trial
</Badge>
{(user?.tier === 'Free' || user?.id === DEMO_USERS.NEHA.id) && daysLeft !== null && (
daysLeft > 0 ? (
<Link href="/upgrade" className="group">
<Badge variant="outline" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/30 font-bold px-3 py-1 cursor-pointer hover:bg-yellow-500/20 hover:scale-105 transition-all flex items-center gap-2 shadow-[0_0_12px_rgba(234,179,8,0.15)]">
<span className="flex items-center gap-1.5"><ShieldAlert size={14} className="text-yellow-500" /> {daysLeft} Days Left in Trial</span>
<span className="text-[11px] bg-yellow-500 text-black px-2 py-0.5 rounded-full font-black group-hover:bg-yellow-400 transition-colors shadow-sm">
Upgrade Now →
</span>
</Badge>
</Link>
) : (
<Link href="/upgrade" className="group">
<Badge variant="outline" className="bg-red-500/15 text-red-400 border-red-500/40 font-black px-3.5 py-1 cursor-pointer hover:bg-red-500/30 hover:scale-105 transition-all flex items-center gap-2 shadow-[0_0_15px_rgba(239,68,68,0.25)] animate-pulse">
<span className="flex items-center gap-1.5"><ShieldAlert size={14} className="text-red-400" /> Trial Expired</span>
<span className="text-[11px] bg-red-500 text-white px-2 py-0.5 rounded-full font-black shadow-sm group-hover:bg-red-600 transition-colors">
Upgrade Now →
</span>
</Badge>
</Link>
)
)}
</div>
@@ -209,6 +311,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
onChange={(e) => switchUser(e.target.value)}
className="bg-secondary border border-border text-foreground px-2.5 py-1.5 rounded-xl text-xs font-bold focus:ring-2 focus:ring-[#00F0FF] outline-none cursor-pointer hover:bg-secondary/80 transition-colors"
>
<option value={DEMO_USERS.NEHA.id}>Neha Verma (AI Coach)</option>
<option value={DEMO_USERS.AI_COACH.id}>Rahul (AI Coach)</option>
<option value={DEMO_USERS.PRO.id}>Priya (Pro Plan)</option>
<option value={DEMO_USERS.FREE.id}>Amit (Free Plan)</option>

View File

@@ -42,7 +42,7 @@ export default function DashboardPage() {
<Badge className="bg-primary text-primary-foreground font-bold tracking-widest uppercase mb-2">Upgrade to Pro</Badge>
<h2 className="text-2xl font-bold tracking-tight">Unlock Unlimited DPS & Mocks</h2>
<p className="text-muted-foreground max-w-xl font-medium">
You're currently on the Free Tier. Upgrade to Pro for ₹2499/year to access unlimited Daily Practice Sheets, weekly CBT tests, and advanced analytics.
You're currently on the Free Tier. Upgrade to Pro for ₹3499/year to access unlimited Daily Practice Sheets, weekly CBT tests, and advanced analytics.
</p>
</div>
<Button size="lg" className="shrink-0 rounded-xl shadow-[0_0_20px_-5px_var(--color-primary)] font-bold">

View File

@@ -1,50 +1,244 @@
"use client"
import { useState, useEffect } from "react"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Trophy, TrendingUp, AlertTriangle } from "lucide-react"
import { Trophy, TrendingUp, AlertTriangle, Sparkles, Building2, Target, Sliders, CheckCircle2, ChevronRight, UserCheck } from "lucide-react"
import { useActiveUser } from "@/hooks/useActiveUser"
export default function RankPredictorPage() {
const { user } = useActiveUser()
const [exam, setExam] = useState<"NEET" | "JEE">("NEET")
const [physics, setPhysics] = useState(135)
const [chemistry, setChemistry] = useState(155)
const [bioOrMath, setBioOrMath] = useState(330) // Bio out of 360 or Math out of 100
const [saved, setSaved] = useState(false)
useEffect(() => {
if (user?.target_exam) {
if (user.target_exam.includes("NEET")) {
setExam("NEET")
setPhysics(140)
setChemistry(150)
setBioOrMath(320)
} else {
setExam("JEE")
setPhysics(65)
setChemistry(70)
setBioOrMath(75)
}
}
}, [user])
const maxTotal = exam === "NEET" ? 720 : 300
const maxPhysics = exam === "NEET" ? 180 : 100
const maxChem = exam === "NEET" ? 180 : 100
const maxThird = exam === "NEET" ? 360 : 100
const totalScore = physics + chemistry + bioOrMath
// Calculate predicted rank based on total score percentage
const calculateRank = (score: number, max: number) => {
const pct = score / max
if (pct >= 0.95) return Math.max(12, Math.round((1 - pct) * 2000))
if (pct >= 0.90) return Math.round(1500 + (0.95 - pct) * 50000)
if (pct >= 0.80) return Math.round(4000 + (0.90 - pct) * 80000)
if (pct >= 0.70) return Math.round(12000 + (0.80 - pct) * 120000)
if (pct >= 0.60) return Math.round(25000 + (0.70 - pct) * 200000)
return Math.round(60000 + (0.60 - pct) * 400000)
}
const predictedRank = calculateRank(totalScore, maxTotal)
const colleges = exam === "NEET" ? [
{ name: "AIIMS New Delhi", category: "Top Tier Medical", minScore: 690, color: "text-[#00F0FF]", border: "border-[#00F0FF]/30", bg: "bg-[#00F0FF]/10" },
{ name: "MAMC New Delhi", category: "Central GMC", minScore: 665, color: "text-amber-400", border: "border-amber-400/30", bg: "bg-amber-400/10" },
{ name: "KGMU Lucknow / VMMCH", category: "Premier State GMC", minScore: 635, color: "text-emerald-400", border: "border-emerald-400/30", bg: "bg-emerald-400/10" },
{ name: "Top State Govt Medical College", category: "State Quota", minScore: 590, color: "text-purple-400", border: "border-purple-400/30", bg: "bg-purple-400/10" },
] : [
{ name: "IIT Bombay / IIT Delhi (CS)", category: "Top 5 IITs", minScore: 260, color: "text-[#00F0FF]", border: "border-[#00F0FF]/30", bg: "bg-[#00F0FF]/10" },
{ name: "IIT Madras / Kharagpur", category: "Core Engineering", minScore: 220, color: "text-amber-400", border: "border-amber-400/30", bg: "bg-amber-400/10" },
{ name: "NIT Trichy / Surathkal", category: "Top NITs", minScore: 180, color: "text-emerald-400", border: "border-emerald-400/30", bg: "bg-emerald-400/10" },
{ name: "Top State Engineering Colleges", category: "State Govt", minScore: 140, color: "text-purple-400", border: "border-purple-400/30", bg: "bg-purple-400/10" },
]
return (
<div className="p-8 space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-500 max-w-4xl mx-auto">
<div className="text-center max-w-2xl mx-auto mb-10">
<div className="inline-flex items-center justify-center p-4 bg-[#00F0FF]/10 rounded-full mb-6">
<Trophy size={40} className="text-[#00F0FF]" />
<div className="p-6 md:p-8 space-y-8 animate-in fade-in duration-500 max-w-5xl mx-auto min-h-screen">
{/* Header */}
<div className="text-center max-w-3xl mx-auto space-y-3">
<div className="inline-flex items-center gap-2 px-4 py-1.5 bg-[#00F0FF]/10 text-[#00F0FF] rounded-full border border-[#00F0FF]/30 text-xs font-black tracking-widest uppercase shadow-[0_0_15px_rgba(0,240,255,0.15)]">
<Sparkles size={14} /> AI Neural Prediction Engine
</div>
<h1 className="text-4xl font-bold tracking-tight mb-4">Rank Predictor AI</h1>
<p className="text-muted-foreground text-lg font-medium">
Our proprietary neural network analyzes your CBT scores against 100,000+ past successful candidates to predict your All India Rank.
<h1 className="text-4xl md:text-5xl font-black tracking-tight">AI Rank & College Predictor</h1>
<p className="text-muted-foreground text-base md:text-lg font-medium">
Simulate your subject marks to dynamically calculate your All India Rank (AIR) and college admission chances.
</p>
{user && (
<div className="inline-flex items-center gap-2 px-3 py-1 bg-emerald-500/10 text-emerald-400 border border-emerald-500/30 rounded-xl text-xs font-bold mt-1">
<UserCheck size={14} /> Profile Connected: {user.name} ({user.target_exam?.replace("_", " ")})
</div>
)}
{/* Exam Switcher */}
<div className="inline-flex p-1.5 bg-secondary/40 border border-border rounded-2xl gap-2 mt-4">
<button
onClick={() => { setExam("NEET"); setPhysics(135); setChemistry(155); setBioOrMath(330) }}
className={`px-6 py-2 rounded-xl text-sm font-extrabold transition-all ${
exam === "NEET" ? "bg-[#00F0FF] text-black shadow-[0_0_15px_#00F0FF]" : "text-muted-foreground hover:text-foreground"
}`}
>
NEET (UG) 720 Marks
</button>
<button
onClick={() => { setExam("JEE"); setPhysics(65); setChemistry(70); setBioOrMath(75) }}
className={`px-6 py-2 rounded-xl text-sm font-extrabold transition-all ${
exam === "JEE" ? "bg-[#00F0FF] text-black shadow-[0_0_15px_#00F0FF]" : "text-muted-foreground hover:text-foreground"
}`}
>
JEE (Main) 300 Marks
</button>
</div>
</div>
<Card className="border-border shadow-lg bg-background overflow-hidden relative">
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-[#00F0FF] to-blue-600"></div>
<CardContent className="p-10 text-center">
<div className="text-sm font-bold uppercase tracking-widest text-muted-foreground mb-4">Estimated NEET (UG) AIR</div>
<div className="text-7xl font-black tracking-tighter bg-clip-text text-transparent bg-gradient-to-br from-foreground to-muted-foreground mb-8">
~14,500
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-left">
<div className="p-4 bg-secondary/30 rounded-xl border border-border flex items-start gap-3">
<TrendingUp className="text-emerald-500 mt-0.5" size={20} />
<div>
<div className="font-bold">On the Right Track</div>
<div className="text-sm text-muted-foreground mt-1">Your biology and chemistry scores are securing a top 20k finish.</div>
</div>
{/* Main Grid */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Left Interactive Simulator */}
<div className="lg:col-span-2 space-y-6">
<Card className="border-border/60 bg-secondary/10 backdrop-blur-xl rounded-3xl p-6 space-y-6 shadow-xl">
<div className="flex items-center justify-between border-b border-border/40 pb-4">
<h2 className="text-xl font-extrabold flex items-center gap-2">
<Sliders className="text-[#00F0FF]" size={20} /> Mark Simulator
</h2>
<span className="text-xs font-bold text-muted-foreground uppercase tracking-wider">Drag Sliders to Adjust</span>
</div>
<div className="p-4 bg-secondary/30 rounded-xl border border-border flex items-start gap-3">
<AlertTriangle className="text-amber-500 mt-0.5" size={20} />
<div>
<div className="font-bold">Physics Bottleneck</div>
<div className="text-sm text-muted-foreground mt-1">Pushing Physics beyond 120 marks will breach the top 5k rank.</div>
</div>
</div>
</div>
<Button size="lg" className="mt-8 bg-foreground text-background hover:bg-foreground/90 font-bold rounded-xl px-8">
Recalculate with Latest Mock
</Button>
</CardContent>
</Card>
{/* Physics Slider */}
<div className="space-y-2">
<div className="flex justify-between items-center text-sm font-extrabold">
<span>Physics Score</span>
<span className="text-[#00F0FF] text-base">{physics} / {maxPhysics}</span>
</div>
<input
type="range"
min={0}
max={maxPhysics}
value={physics}
onChange={(e) => setPhysics(Number(e.target.value))}
className="w-full h-2.5 bg-secondary rounded-lg appearance-none cursor-pointer accent-[#00F0FF]"
/>
</div>
{/* Chemistry Slider */}
<div className="space-y-2">
<div className="flex justify-between items-center text-sm font-extrabold">
<span>Chemistry Score</span>
<span className="text-amber-400 text-base">{chemistry} / {maxChem}</span>
</div>
<input
type="range"
min={0}
max={maxChem}
value={chemistry}
onChange={(e) => setChemistry(Number(e.target.value))}
className="w-full h-2.5 bg-secondary rounded-lg appearance-none cursor-pointer accent-amber-400"
/>
</div>
{/* Bio / Math Slider */}
<div className="space-y-2">
<div className="flex justify-between items-center text-sm font-extrabold">
<span>{exam === "NEET" ? "Biology Score (Botany + Zoology)" : "Mathematics Score"}</span>
<span className="text-emerald-400 text-base">{bioOrMath} / {maxThird}</span>
</div>
<input
type="range"
min={0}
max={maxThird}
value={bioOrMath}
onChange={(e) => setBioOrMath(Number(e.target.value))}
className="w-full h-2.5 bg-secondary rounded-lg appearance-none cursor-pointer accent-emerald-400"
/>
</div>
<div className="p-4 bg-background/50 border border-border/50 rounded-2xl flex items-center justify-between">
<span className="text-sm font-bold">Simulated Total Score</span>
<span className="text-3xl font-black text-foreground">{totalScore} <span className="text-sm text-muted-foreground font-semibold">/ {maxTotal}</span></span>
</div>
</Card>
{/* AI Strategy Insights based on Score */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Card className="p-5 bg-emerald-500/10 border-emerald-500/30 rounded-2xl flex items-start gap-3">
<TrendingUp className="text-emerald-400 shrink-0 mt-0.5" size={22} />
<div>
<h4 className="font-extrabold text-emerald-400">Strongest Domain</h4>
<p className="text-xs text-muted-foreground font-medium mt-1">
Your Biology/Math score ({bioOrMath}) provides solid rank leverage. Keep maintaining your revision momentum.
</p>
</div>
</Card>
<Card className="p-5 bg-amber-500/10 border-amber-500/30 rounded-2xl flex items-start gap-3">
<AlertTriangle className="text-amber-400 shrink-0 mt-0.5" size={22} />
<div>
<h4 className="font-extrabold text-amber-400">High-Yield Growth Zone</h4>
<p className="text-xs text-muted-foreground font-medium mt-1">
Increasing Physics by just 25 marks will boost your rank by approximately ~4,800 positions.
</p>
</div>
</Card>
</div>
</div>
{/* Right Output Dashboard */}
<div className="space-y-6">
{/* AIR Display Card */}
<Card className="border-2 border-[#00F0FF] bg-gradient-to-br from-[#00F0FF]/15 via-secondary/30 to-background rounded-3xl p-6 text-center shadow-[0_0_30px_rgba(0,240,255,0.15)] relative overflow-hidden">
<div className="absolute top-0 right-0 p-4 opacity-10">
<Trophy size={120} className="text-[#00F0FF]" />
</div>
<div className="text-xs font-black uppercase tracking-widest text-[#00F0FF] mb-2">Estimated All India Rank</div>
<div className="text-5xl md:text-6xl font-black tracking-tighter text-foreground my-3 filter drop-shadow-md">
AIR ~{predictedRank.toLocaleString()}
</div>
<div className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-[#00F0FF]/20 text-[#00F0FF] text-xs font-bold border border-[#00F0FF]/40">
<CheckCircle2 size={13} /> High Accuracy Confidence
</div>
</Card>
{/* College Admission Probability Table */}
<Card className="border-border/60 bg-secondary/10 backdrop-blur-xl rounded-3xl p-6 space-y-4">
<h3 className="text-lg font-extrabold flex items-center gap-2">
<Building2 className="text-[#00F0FF]" size={20} /> College Admission Chances
</h3>
<div className="space-y-3">
{colleges.map((c) => {
const isLikely = totalScore >= c.minScore
const chancePct = Math.min(99, Math.max(10, Math.round((totalScore / c.minScore) * 85)))
return (
<div key={c.name} className={`p-3.5 rounded-2xl border ${c.border} ${c.bg} space-y-1.5`}>
<div className="flex justify-between items-center">
<span className="font-extrabold text-sm text-foreground">{c.name}</span>
<span className={`text-xs font-black ${isLikely ? "text-green-400" : "text-amber-400"}`}>
{isLikely ? `${chancePct}% Likely` : "Reach Target"}
</span>
</div>
<div className="flex justify-between text-xs text-muted-foreground font-medium">
<span>{c.category}</span>
<span>Cutoff Score: {c.minScore}+</span>
</div>
</div>
)
})}
</div>
</Card>
</div>
</div>
</div>
)
}

View File

@@ -48,7 +48,7 @@ export default function SettingsPage() {
formData.append("file", file);
try {
const res = await fetch(`/api/users/${userId}/upload-avatar`, {
const res = await fetch(`/api/users/${userId}/avatar`, {
method: "POST",
body: formData,
});