feat: complete comprehensive zero-state metrics across all dashboard pages for new accounts
This commit is contained in:
@@ -73,12 +73,25 @@ def health_check():
|
||||
|
||||
# --- ACCESS CONTROL DEPENDENCY ---
|
||||
|
||||
def get_effective_tier(user: models.User, session: Session) -> models.TierEnum:
|
||||
if user.tier in (models.TierEnum.AI_COACH, models.TierEnum.PRO):
|
||||
return user.tier
|
||||
# Check if trial is active
|
||||
now = datetime.utcnow()
|
||||
if user.trial_end and user.trial_end > now:
|
||||
settings = session.get(models.SystemSettings, "global")
|
||||
if settings and settings.enable_free_trial:
|
||||
return settings.trial_tier
|
||||
return models.TierEnum.AI_COACH
|
||||
return user.tier
|
||||
|
||||
def require_tier(allowed_tiers: List[models.TierEnum]):
|
||||
def tier_checker(user_id: UUID, session: Session = Depends(get_session)):
|
||||
user = session.get(models.User, user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found.")
|
||||
if user.tier not in allowed_tiers:
|
||||
effective_tier = get_effective_tier(user, session)
|
||||
if effective_tier not in allowed_tiers:
|
||||
raise HTTPException(
|
||||
status_code=403,
|
||||
detail=f"Access denied. Requires one of: {[t.value for t in allowed_tiers]}"
|
||||
@@ -597,7 +610,8 @@ async def ai_tutor_chat_stream(request: ChatRequest, session: Session = Depends(
|
||||
user = session.get(models.User, request.user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Student profile not found.")
|
||||
if user.tier != models.TierEnum.AI_COACH:
|
||||
effective_tier = get_effective_tier(user, session)
|
||||
if effective_tier != models.TierEnum.AI_COACH:
|
||||
raise HTTPException(status_code=403, detail="Access denied. Gated to AI_COACH tier.")
|
||||
|
||||
return StreamingResponse(
|
||||
@@ -826,6 +840,11 @@ def get_leaderboard(
|
||||
user_accuracy = "0%"
|
||||
user_streak = 1
|
||||
if requesting_user:
|
||||
if str(requesting_user.id) in DEMO_USER_UUIDS:
|
||||
user_score = 9250
|
||||
user_accuracy = "88.5%"
|
||||
user_streak = requesting_user.streak_count or 25
|
||||
else:
|
||||
user_streak = max(1, requesting_user.streak_count)
|
||||
user_attempts = session.exec(
|
||||
select(models.Attempt).where(
|
||||
@@ -837,6 +856,9 @@ def get_leaderboard(
|
||||
user_score = sum(a.score for a in user_attempts) + (user_streak * 50)
|
||||
avg_acc = sum(a.accuracy for a in user_attempts) / len(user_attempts)
|
||||
user_accuracy = f"{round(avg_acc, 1)}%"
|
||||
else:
|
||||
user_score = 0
|
||||
user_accuracy = "0%"
|
||||
|
||||
# Build student entries
|
||||
entries = []
|
||||
|
||||
@@ -17,7 +17,19 @@ def calculate_readiness_score(user_id: UUID, session: Session) -> dict:
|
||||
).all()
|
||||
|
||||
if not attempts:
|
||||
return {"score": 0, "status": "Not enough data"}
|
||||
return {
|
||||
"score": 0,
|
||||
"status": "Not enough data",
|
||||
"prediction": "Start practice to calculate",
|
||||
"target_type": "Unranked",
|
||||
"breakdown": {
|
||||
"accuracy": 0,
|
||||
"speed": 0,
|
||||
"consistency": 0,
|
||||
"recovery": 0,
|
||||
"coverage": 0,
|
||||
}
|
||||
}
|
||||
|
||||
# 1. Accuracy (40% Weight)
|
||||
avg_accuracy = sum(a.accuracy for a in attempts) / len(attempts)
|
||||
|
||||
@@ -5,6 +5,8 @@ import { Card, CardContent } from "@/components/ui/card"
|
||||
import { GraduationCap, Building2, Award, CheckCircle2, TrendingUp, MapPin, Users, BarChart2 } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
const CUTOFF_TRENDS = [
|
||||
{ year: "2021", rank: 3800 },
|
||||
{ year: "2022", rank: 3500 },
|
||||
@@ -14,6 +16,8 @@ const CUTOFF_TRENDS = [
|
||||
]
|
||||
|
||||
export default function CollegePredictorPage() {
|
||||
const { userId } = useActiveUser()
|
||||
const isDemo = isDemoUser(userId)
|
||||
const maxRank = Math.max(...CUTOFF_TRENDS.map(d => d.rank))
|
||||
|
||||
return (
|
||||
@@ -41,10 +45,10 @@ export default function CollegePredictorPage() {
|
||||
{/* Summary KPI Strip */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ label: "Predicted Rank", value: "~4,200", icon: <TrendingUp size={18} className="text-[#00F0FF]" />, color: "border-[#00F0FF]/20 bg-[#00F0FF]/5" },
|
||||
{ label: "Target Colleges", value: "12", icon: <Building2 size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
||||
{ label: "Reach Colleges", value: "5", icon: <MapPin size={18} className="text-purple-400" />, color: "border-purple-400/20 bg-purple-400/5" },
|
||||
{ label: "Safe Colleges", value: "18", icon: <CheckCircle2 size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
||||
{ label: "Predicted Rank", value: isDemo ? "~4,200" : "--", icon: <TrendingUp size={18} className="text-[#00F0FF]" />, color: "border-[#00F0FF]/20 bg-[#00F0FF]/5" },
|
||||
{ label: "Target Colleges", value: isDemo ? "12" : "0", icon: <Building2 size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
||||
{ label: "Reach Colleges", value: isDemo ? "5" : "0", icon: <MapPin size={18} className="text-purple-400" />, color: "border-purple-400/20 bg-purple-400/5" },
|
||||
{ label: "Safe Colleges", value: isDemo ? "18" : "0", icon: <CheckCircle2 size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
||||
].map((kpi) => (
|
||||
<Card key={kpi.label} className={`rounded-2xl border ${kpi.color} hover:scale-[1.02] transition-transform`}>
|
||||
<CardContent className="p-5 flex flex-col gap-2">
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"use client"
|
||||
|
||||
import { MistakeHeatmap } from "@/components/AI/MistakeHeatmap"
|
||||
import { Flame, AlertTriangle, TrendingDown, BookOpen, ArrowRight, BarChart2, Target } from "lucide-react"
|
||||
import { Flame, AlertTriangle, TrendingDown, BookOpen, ArrowRight, BarChart2, Target, CheckCircle2 } from "lucide-react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import Link from "next/link"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
const CRITICAL_TOPICS = [
|
||||
const DEMO_CRITICAL_TOPICS = [
|
||||
{ name: "Rotational Dynamics", rate: "80%", rateNum: 80, subject: "Physics" },
|
||||
{ name: "Optics", rate: "60%", rateNum: 60, subject: "Physics" },
|
||||
{ name: "Electromagnetism", rate: "30%", rateNum: 30, subject: "Physics" },
|
||||
@@ -13,14 +14,26 @@ const CRITICAL_TOPICS = [
|
||||
{ name: "Thermodynamics", rate: "25%", rateNum: 25, subject: "Physics" },
|
||||
]
|
||||
|
||||
const SUMMARY_STATS = [
|
||||
const DEMO_SUMMARY_STATS = [
|
||||
{ label: "Total Errors (Last 5 Tests)", value: "87", icon: <AlertTriangle size={18} className="text-red-400" />, color: "border-red-400/20 bg-red-400/5" },
|
||||
{ label: "Critical Topics", value: "4", icon: <Flame size={18} className="text-orange-400" />, color: "border-orange-400/20 bg-orange-400/5" },
|
||||
{ label: "Improving Topics", value: "3", icon: <TrendingDown size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
||||
{ label: "Avg Error Rate", value: "38%", icon: <Target size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
||||
]
|
||||
|
||||
const ZERO_SUMMARY_STATS = [
|
||||
{ label: "Total Errors (Last 5 Tests)", value: "0", icon: <AlertTriangle size={18} className="text-muted-foreground" />, color: "border-white/10 bg-white/5" },
|
||||
{ label: "Critical Topics", value: "0", icon: <Flame size={18} className="text-muted-foreground" />, color: "border-white/10 bg-white/5" },
|
||||
{ label: "Improving Topics", value: "0", icon: <TrendingDown size={18} className="text-muted-foreground" />, color: "border-white/10 bg-white/5" },
|
||||
{ label: "Avg Error Rate", value: "0%", icon: <Target size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
||||
]
|
||||
|
||||
export default function HeatmapPage() {
|
||||
const { userId } = useActiveUser()
|
||||
const isDemo = isDemoUser(userId)
|
||||
const summaryStats = isDemo ? DEMO_SUMMARY_STATS : ZERO_SUMMARY_STATS
|
||||
const criticalTopics = isDemo ? DEMO_CRITICAL_TOPICS : []
|
||||
|
||||
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">
|
||||
{/* Page Header */}
|
||||
@@ -45,7 +58,7 @@ export default function HeatmapPage() {
|
||||
|
||||
{/* KPI Strip */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{SUMMARY_STATS.map((kpi) => (
|
||||
{summaryStats.map((kpi) => (
|
||||
<Card key={kpi.label} className={`rounded-2xl border ${kpi.color} hover:scale-[1.02] transition-transform`}>
|
||||
<CardContent className="p-5 flex flex-col gap-2">
|
||||
{kpi.icon}
|
||||
@@ -96,7 +109,16 @@ export default function HeatmapPage() {
|
||||
<BarChart2 size={15} className="text-red-400" /> Priority Topics by Error Rate
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{CRITICAL_TOPICS.map((t) => (
|
||||
{criticalTopics.length === 0 ? (
|
||||
<div className="p-5 rounded-xl bg-white/5 border border-border/40 text-center space-y-1.5">
|
||||
<CheckCircle2 size={24} className="mx-auto text-green-400 opacity-80" />
|
||||
<p className="text-xs font-bold text-foreground">Clean Error Log</p>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
No mistake hotspots detected yet. Practice questions to see priority areas here.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
criticalTopics.map((t) => (
|
||||
<div key={t.name}>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<div>
|
||||
@@ -114,7 +136,8 @@ export default function HeatmapPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -54,6 +54,7 @@ export default function MistakeTrackingPage() {
|
||||
|
||||
const completedCount = Math.min(currentIndex, mistakes.length)
|
||||
const progressPct = mistakes.length > 0 ? Math.round((completedCount / mistakes.length) * 100) : 0
|
||||
const avgErrorRate = mistakes.length > 0 ? "38%" : "0%"
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-8 space-y-8 max-w-7xl mx-auto animate-in fade-in duration-500 min-h-screen">
|
||||
@@ -84,7 +85,7 @@ export default function MistakeTrackingPage() {
|
||||
{ label: "In Today's Queue", value: mistakes.length.toString(), icon: <Flame size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
||||
{ label: "Reviewed This Session", value: completedCount.toString(), icon: <CheckCircle2 size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
||||
{ label: "Session Progress", value: `${progressPct}%`, icon: <Target size={18} className="text-[#00F0FF]" />, color: "border-[#00F0FF]/20 bg-[#00F0FF]/5" },
|
||||
{ label: "Avg Error Rate", value: "38%", icon: <TrendingDown size={18} className="text-red-400" />, color: "border-red-400/20 bg-red-400/5" },
|
||||
{ label: "Avg Error Rate", value: avgErrorRate, icon: <TrendingDown size={18} className="text-red-400" />, color: "border-red-400/20 bg-red-400/5" },
|
||||
].map(kpi => (
|
||||
<Card key={kpi.label} className={`rounded-2xl border ${kpi.color}`}>
|
||||
<CardContent className="p-4 flex flex-col gap-2">
|
||||
|
||||
@@ -16,7 +16,7 @@ import { WeeklyProgressReport } from "@/components/AI/WeeklyProgressReport"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { BrainCircuit, RefreshCw, CheckCircle2, Flame, Trophy, Target, Zap, BookOpen, Bot, FileText } from "lucide-react"
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
import Link from "next/link"
|
||||
|
||||
|
||||
@@ -36,8 +36,10 @@ export default function AICoachDashboard() {
|
||||
const [mistakes, setMistakes] = useState<MistakePayload[]>([])
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [stats, setStats] = useState<any>(null)
|
||||
|
||||
const { userId, user, hasAccessTo, loading: userLoading } = useActiveUser();
|
||||
const isDemo = isDemoUser(userId);
|
||||
|
||||
const fetchMistakes = async () => {
|
||||
if (!userId) return
|
||||
@@ -58,6 +60,10 @@ export default function AICoachDashboard() {
|
||||
useEffect(() => {
|
||||
if (!userLoading && userId) {
|
||||
fetchMistakes()
|
||||
fetch(`/api/users/${userId}/stats`)
|
||||
.then(res => res.ok ? res.json() : null)
|
||||
.then(data => setStats(data))
|
||||
.catch(() => setStats(null))
|
||||
}
|
||||
}, [userId, userLoading])
|
||||
|
||||
@@ -114,9 +120,11 @@ 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
|
||||
const questionsSolved = isDemo ? "1,247" : (stats?.total_questions_attempted ?? 0).toLocaleString()
|
||||
const overallAccuracy = isDemo ? "74%" : `${stats?.overall_accuracy ?? 0}%`
|
||||
const mistakesMastered = isDemo ? 38 : 0
|
||||
const aiSessions = isDemo ? 124 : 0
|
||||
const currentStreakDays = `${user?.streak_count || (isDemo ? 12 : 1)} d`
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-6xl mx-auto space-y-8 animate-in fade-in duration-500">
|
||||
@@ -125,7 +133,7 @@ export default function AICoachDashboard() {
|
||||
<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
|
||||
{user?.streak_count || 1}-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} />
|
||||
@@ -143,11 +151,11 @@ export default function AICoachDashboard() {
|
||||
{/* ── 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} /> },
|
||||
{ label: "Questions Solved", value: questionsSolved, color: "text-[#00F0FF]", bg: "bg-[#00F0FF]/5", border: "border-[#00F0FF]/20", icon: <FileText size={32} /> },
|
||||
{ label: "Overall Accuracy", value: overallAccuracy, color: "text-amber-500", bg: "bg-amber-500/5", border: "border-amber-500/20", icon: <Target size={32} /> },
|
||||
{ label: "Current Streak", value: currentStreakDays, 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: mistakesMastered, color: "text-green-400", bg: "bg-green-400/5", border: "border-green-400/20", icon: <CheckCircle2 size={32} /> },
|
||||
{ label: "AI Sessions", value: aiSessions, 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 transition-all duration-300 transform hover:-translate-y-1 rounded-2xl`}>
|
||||
<CardContent className="p-6 flex flex-col items-center justify-center text-center">
|
||||
|
||||
@@ -5,8 +5,9 @@ import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Gauge, TrendingUp, Target, Award, ShieldAlert, CheckCircle2, BookOpen, Zap, BarChart2, Clock } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
const SCORE_HISTORY = [
|
||||
const DEMO_SCORE_HISTORY = [
|
||||
{ week: "Wk 1", score: 54 },
|
||||
{ week: "Wk 2", score: 60 },
|
||||
{ week: "Wk 3", score: 63 },
|
||||
@@ -15,8 +16,20 @@ const SCORE_HISTORY = [
|
||||
{ week: "Wk 6", score: 78 },
|
||||
]
|
||||
|
||||
const ZERO_SCORE_HISTORY = [
|
||||
{ week: "Wk 1", score: 0 },
|
||||
{ week: "Wk 2", score: 0 },
|
||||
{ week: "Wk 3", score: 0 },
|
||||
{ week: "Wk 4", score: 0 },
|
||||
{ week: "Wk 5", score: 0 },
|
||||
{ week: "Wk 6", score: 0 },
|
||||
]
|
||||
|
||||
export default function ReadinessPage() {
|
||||
const maxScore = Math.max(...SCORE_HISTORY.map(d => d.score))
|
||||
const { userId } = useActiveUser()
|
||||
const isDemo = isDemoUser(userId)
|
||||
const scoreHistory = isDemo ? DEMO_SCORE_HISTORY : ZERO_SCORE_HISTORY
|
||||
const maxScore = Math.max(1, ...scoreHistory.map(d => d.score))
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-8 space-y-8 max-w-7xl mx-auto animate-in fade-in duration-500 min-h-screen">
|
||||
@@ -43,16 +56,16 @@ export default function ReadinessPage() {
|
||||
{/* KPI Strip */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ label: "Overall Score", value: "72 / 100", trend: "+5 this week", trendUp: true, icon: <Gauge size={20} className="text-[#00F0FF]" /> },
|
||||
{ label: "Accuracy Rate", value: "88%", trend: "+3% vs last week", trendUp: true, icon: <Target size={20} className="text-amber-400" /> },
|
||||
{ label: "Avg Speed / Q", value: "45 sec", trend: "↓ Needs work", trendUp: false, icon: <Clock size={20} className="text-red-400" /> },
|
||||
{ label: "Syllabus Covered", value: "64%", trend: "+8% this month", trendUp: true, icon: <BookOpen size={20} className="text-emerald-400" /> },
|
||||
{ label: "Overall Score", value: isDemo ? "72 / 100" : "0 / 100", trend: isDemo ? "+5 this week" : "Baseline", trendUp: true, icon: <Gauge size={20} className="text-[#00F0FF]" /> },
|
||||
{ label: "Accuracy Rate", value: isDemo ? "88%" : "0%", trend: isDemo ? "+3% vs last week" : "0 attempts", trendUp: isDemo, icon: <Target size={20} className="text-amber-400" /> },
|
||||
{ label: "Avg Speed / Q", value: isDemo ? "45 sec" : "-- sec", trend: isDemo ? "↓ Needs work" : "Not tested", trendUp: false, icon: <Clock size={20} className="text-red-400" /> },
|
||||
{ label: "Syllabus Covered", value: isDemo ? "64%" : "0%", trend: isDemo ? "+8% this month" : "Day 1", trendUp: true, icon: <BookOpen size={20} className="text-emerald-400" /> },
|
||||
].map((kpi) => (
|
||||
<Card key={kpi.label} className="border-border/60 bg-secondary/10 rounded-2xl hover:border-[#00F0FF]/30 transition-colors">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
{kpi.icon}
|
||||
<span className={`text-xs font-bold px-2 py-0.5 rounded-full ${kpi.trendUp ? "bg-green-500/10 text-green-400" : "bg-red-500/10 text-red-400"}`}>
|
||||
<span className={`text-xs font-bold px-2 py-0.5 rounded-full ${kpi.trendUp ? "bg-green-500/10 text-green-400" : "bg-white/5 text-muted-foreground"}`}>
|
||||
{kpi.trend}
|
||||
</span>
|
||||
</div>
|
||||
@@ -70,7 +83,9 @@ export default function ReadinessPage() {
|
||||
<div className="flex items-center gap-2 mb-5">
|
||||
<Award className="text-amber-400" size={22} />
|
||||
<h2 className="text-xl font-extrabold tracking-tight">Composite Readiness Index</h2>
|
||||
<span className="ml-auto text-xs font-bold text-[#00F0FF] bg-cyan-50 dark:bg-[#00F0FF]/10 border border-cyan-300/60 dark:border-[#00F0FF]/20 px-2.5 py-1 rounded-full">Updated Daily</span>
|
||||
<span className="ml-auto text-xs font-bold text-[#00F0FF] bg-cyan-50 dark:bg-[#00F0FF]/10 border border-cyan-300/60 dark:border-[#00F0FF]/20 px-2.5 py-1 rounded-full">
|
||||
{isDemo ? "Updated Daily" : "Day 1 Initializing"}
|
||||
</span>
|
||||
</div>
|
||||
<ReadinessGauge />
|
||||
</Card>
|
||||
@@ -83,9 +98,9 @@ export default function ReadinessPage() {
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ subject: "Biology / Maths", score: 82, color: "bg-[#00F0FF]", status: "High Readiness", statusColor: "text-[#00F0FF]" },
|
||||
{ subject: "Chemistry", score: 74, color: "bg-amber-400", status: "Moderate Readiness", statusColor: "text-amber-400" },
|
||||
{ subject: "Physics", score: 61, color: "bg-red-400", status: "Needs Intensive Drill", statusColor: "text-red-400" },
|
||||
{ subject: "Biology / Maths", score: isDemo ? 82 : 0, color: "bg-[#00F0FF]", status: isDemo ? "High Readiness" : "Not Started", statusColor: isDemo ? "text-[#00F0FF]" : "text-muted-foreground" },
|
||||
{ subject: "Chemistry", score: isDemo ? 74 : 0, color: "bg-amber-400", status: isDemo ? "Moderate Readiness" : "Not Started", statusColor: isDemo ? "text-amber-400" : "text-muted-foreground" },
|
||||
{ subject: "Physics", score: isDemo ? 61 : 0, color: "bg-red-400", status: isDemo ? "Needs Intensive Drill" : "Not Started", statusColor: isDemo ? "text-red-400" : "text-muted-foreground" },
|
||||
].map((sub) => (
|
||||
<div key={sub.subject} className="space-y-2">
|
||||
<div className="flex justify-between items-center">
|
||||
@@ -107,12 +122,18 @@ export default function ReadinessPage() {
|
||||
<h3 className="text-lg font-extrabold tracking-tight">6-Week Readiness Trend</h3>
|
||||
</div>
|
||||
<div className="flex items-end gap-3 h-28">
|
||||
{SCORE_HISTORY.map((d, i) => (
|
||||
{scoreHistory.map((d, i) => (
|
||||
<div key={d.week} className="flex flex-col items-center gap-1 flex-1">
|
||||
<span className="text-xs font-black text-muted-foreground">{d.score}</span>
|
||||
<div
|
||||
className={`w-full rounded-t-md transition-all duration-700 ${i === SCORE_HISTORY.length - 1 ? "bg-gradient-to-t from-[#00F0FF]/60 to-[#00F0FF]" : "bg-gradient-to-t from-white/10 to-white/25"}`}
|
||||
style={{ height: `${(d.score / maxScore) * 80}px` }}
|
||||
className={`w-full rounded-t-md transition-all duration-700 ${
|
||||
!isDemo
|
||||
? "bg-white/5 border border-white/10"
|
||||
: i === scoreHistory.length - 1
|
||||
? "bg-gradient-to-t from-[#00F0FF]/60 to-[#00F0FF]"
|
||||
: "bg-gradient-to-t from-white/10 to-white/25"
|
||||
}`}
|
||||
style={{ height: isDemo ? `${(d.score / maxScore) * 80}px` : "4px" }}
|
||||
/>
|
||||
<span className="text-xs text-muted-foreground font-semibold">{d.week}</span>
|
||||
</div>
|
||||
@@ -130,19 +151,19 @@ export default function ReadinessPage() {
|
||||
<ul className="space-y-3.5 text-sm font-semibold text-muted-foreground">
|
||||
<li className="flex items-start gap-2.5">
|
||||
<CheckCircle2 size={16} className="text-amber-400 shrink-0 mt-0.5" />
|
||||
<span>Complete 2 full-length mock tests this week to boost consistency score.</span>
|
||||
<span>{isDemo ? "Complete 2 full-length mock tests this week to boost consistency score." : "Complete your first diagnostic test to calculate your initial readiness baseline."}</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 your Physics queue to gain ~4 readiness points.</span>
|
||||
<span>{isDemo ? "Clear 15 pending errors in your Physics queue to gain ~4 readiness points." : "Attempt daily DPPs to populate your question error log."}</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 streak — consistency adds 6% to your readiness index.</span>
|
||||
<span>Maintain daily streak — consistency adds directly to your readiness algorithm.</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<CheckCircle2 size={16} className="text-amber-400 shrink-0 mt-0.5" />
|
||||
<span>Reduce avg time-per-question below 40s with daily timed DPP sessions.</span>
|
||||
<span>Target under 45s per question in timed test series practice.</span>
|
||||
</li>
|
||||
</ul>
|
||||
</Card>
|
||||
|
||||
@@ -4,14 +4,25 @@ import { ExamStrategyCard } from "@/components/AI/ExamStrategyCard"
|
||||
import { StudyTimeOptimizationCard } from "@/components/AI/StudyTimeOptimizationCard"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Zap, Clock, Target, Lightbulb, CheckCircle2, AlertTriangle, TrendingUp } from "lucide-react"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
const NEG_MARKING_DATA = [
|
||||
const DEMO_NEG_MARKING_DATA = [
|
||||
{ subject: "Physics", attempts: 30, negMarks: 12, accuracy: 60 },
|
||||
{ subject: "Chemistry", attempts: 35, negMarks: 7, accuracy: 80 },
|
||||
{ subject: "Maths", attempts: 28, negMarks: 10, accuracy: 64 },
|
||||
{ subject: "Maths / Biology", attempts: 28, negMarks: 10, accuracy: 64 },
|
||||
]
|
||||
|
||||
const ZERO_NEG_MARKING_DATA = [
|
||||
{ subject: "Physics", attempts: 0, negMarks: 0, accuracy: 0 },
|
||||
{ subject: "Chemistry", attempts: 0, negMarks: 0, accuracy: 0 },
|
||||
{ subject: "Maths / Biology", attempts: 0, negMarks: 0, accuracy: 0 },
|
||||
]
|
||||
|
||||
export default function StrategyPage() {
|
||||
const { userId } = useActiveUser()
|
||||
const isDemo = isDemoUser(userId)
|
||||
const negMarkingData = isDemo ? DEMO_NEG_MARKING_DATA : ZERO_NEG_MARKING_DATA
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-8 space-y-8 max-w-7xl mx-auto animate-in fade-in duration-500 min-h-screen">
|
||||
{/* Page Header */}
|
||||
@@ -37,10 +48,10 @@ export default function StrategyPage() {
|
||||
{/* KPI Strip */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ label: "Time Per Question", value: "45 sec", note: "Target: 38 sec", icon: <Clock size={18} className="text-red-400" />, color: "border-red-400/20 bg-red-400/5" },
|
||||
{ label: "Negative Marks Lost", value: "−29", note: "Last mock test", icon: <AlertTriangle size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
||||
{ label: "Attempted Questions", value: "93 / 120", note: "+7 vs last test", icon: <Target size={18} className="text-[#00F0FF]" />, color: "border-[#00F0FF]/20 bg-[#00F0FF]/5" },
|
||||
{ label: "Score Yield", value: "214", note: "Predicted score", icon: <TrendingUp size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
||||
{ label: "Time Per Question", value: isDemo ? "45 sec" : "-- sec", note: isDemo ? "Target: 38 sec" : "Not calibrated", icon: <Clock size={18} className="text-red-400" />, color: "border-red-400/20 bg-red-400/5" },
|
||||
{ label: "Negative Marks Lost", value: isDemo ? "−29" : "0", note: isDemo ? "Last mock test" : "0 negative marks", icon: <AlertTriangle size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
||||
{ label: "Attempted Questions", value: isDemo ? "93 / 120" : "0 / 120", note: isDemo ? "+7 vs last test" : "0 attempted", icon: <Target size={18} className="text-[#00F0FF]" />, color: "border-[#00F0FF]/20 bg-[#00F0FF]/5" },
|
||||
{ label: "Score Yield", value: isDemo ? "214" : "0", note: isDemo ? "Predicted score" : "Baseline", icon: <TrendingUp size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
||||
].map((kpi) => (
|
||||
<Card key={kpi.label} className={`rounded-2xl border ${kpi.color} hover:scale-[1.02] transition-transform`}>
|
||||
<CardContent className="p-5 flex flex-col gap-2">
|
||||
@@ -77,10 +88,12 @@ export default function StrategyPage() {
|
||||
<div className="flex items-center gap-2 mb-5">
|
||||
<AlertTriangle size={20} className="text-amber-400" />
|
||||
<h3 className="text-lg font-extrabold tracking-tight">Negative Marking Analysis</h3>
|
||||
<span className="ml-auto text-xs text-muted-foreground font-semibold">Last Mock Test · Aug 1</span>
|
||||
<span className="ml-auto text-xs text-muted-foreground font-semibold">
|
||||
{isDemo ? "Last Mock Test · Aug 1" : "Day 1 Initializing"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
{NEG_MARKING_DATA.map((row) => (
|
||||
{negMarkingData.map((row) => (
|
||||
<div key={row.subject} className="grid grid-cols-4 gap-4 items-center p-3 rounded-xl bg-white/5 border border-border/40">
|
||||
<div className="font-black text-sm">{row.subject}</div>
|
||||
<div className="text-center">
|
||||
@@ -88,17 +101,19 @@ export default function StrategyPage() {
|
||||
<div className="text-[11px] text-muted-foreground font-semibold">Attempted</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-sm font-black text-red-400">−{row.negMarks}</div>
|
||||
<div className="text-sm font-black text-red-400">{row.negMarks === 0 ? "0" : `−${row.negMarks}`}</div>
|
||||
<div className="text-[11px] text-muted-foreground font-semibold">Neg. Marks</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex justify-between text-xs font-bold mb-1">
|
||||
<span className="text-muted-foreground">Accuracy</span>
|
||||
<span className={row.accuracy >= 75 ? "text-green-400" : row.accuracy >= 60 ? "text-amber-400" : "text-red-400"}>{row.accuracy}%</span>
|
||||
<span className={row.accuracy >= 75 ? "text-green-400" : row.accuracy >= 60 ? "text-amber-400" : row.accuracy === 0 ? "text-muted-foreground" : "text-red-400"}>
|
||||
{row.accuracy}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 w-full bg-secondary rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${row.accuracy >= 75 ? "bg-green-400" : row.accuracy >= 60 ? "bg-amber-400" : "bg-red-400"}`}
|
||||
className={`h-full rounded-full ${row.accuracy >= 75 ? "bg-green-400" : row.accuracy >= 60 ? "bg-amber-400" : isDemo ? "bg-red-400" : "bg-white/10"}`}
|
||||
style={{ width: `${row.accuracy}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -107,7 +122,11 @@ export default function StrategyPage() {
|
||||
))}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-4 font-medium pl-1">
|
||||
<span className="font-black text-amber-400">Insight:</span> You're losing ~29 marks to wrong answers. Switching to "skip and revisit" for uncertain Physics questions could recover 15–20 marks.
|
||||
<span className="font-black text-amber-400">Insight:</span> {isDemo ? (
|
||||
<>You're losing ~29 marks to wrong answers. Switching to "skip and revisit" for uncertain Physics questions could recover 15–20 marks.</>
|
||||
) : (
|
||||
<>Complete your first mock test or timed DPP to calculate your negative marking profile and fine-tune your subject pacing.</>
|
||||
)}
|
||||
</p>
|
||||
</Card>
|
||||
|
||||
|
||||
@@ -4,8 +4,13 @@ import { StudyPlanCard } from "@/components/AI/StudyPlanCard"
|
||||
import { WeeklyProgressReport } from "@/components/AI/WeeklyProgressReport"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Calendar, BookOpen, Clock, Target, CheckCircle2, TrendingUp, Flame } from "lucide-react"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
export default function StudyPlanPage() {
|
||||
const { user, userId } = useActiveUser()
|
||||
const isDemo = isDemoUser(userId)
|
||||
const streakCount = user?.streak_count || (isDemo ? 12 : 1)
|
||||
|
||||
return (
|
||||
<div className="p-6 md:p-8 space-y-8 max-w-7xl mx-auto animate-in fade-in duration-500 min-h-screen">
|
||||
{/* Page Header */}
|
||||
@@ -31,10 +36,10 @@ export default function StudyPlanPage() {
|
||||
{/* KPI Strip */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ label: "Tasks Today", value: "5", sub: "2 completed", icon: <CheckCircle2 size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
||||
{ label: "Study Hours Today", value: "3.8 hrs", sub: "Target: 5 hrs", icon: <Clock size={18} className="text-[#00F0FF]" />, color: "border-[#00F0FF]/20 bg-[#00F0FF]/5" },
|
||||
{ label: "Syllabus Coverage", value: "64%", sub: "+8% this week", icon: <Target size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
||||
{ label: "Current Streak", value: "12 Days", sub: "Keep it up!", icon: <Flame size={18} className="text-orange-400" />, color: "border-orange-400/20 bg-orange-400/5" },
|
||||
{ label: "Tasks Today", value: isDemo ? "5" : "3", sub: isDemo ? "2 completed" : "0 completed", icon: <CheckCircle2 size={18} className="text-green-400" />, color: "border-green-400/20 bg-green-400/5" },
|
||||
{ label: "Study Hours Today", value: isDemo ? "3.8 hrs" : "0.0 hrs", sub: "Target: 5 hrs", icon: <Clock size={18} className="text-[#00F0FF]" />, color: "border-[#00F0FF]/20 bg-[#00F0FF]/5" },
|
||||
{ label: "Syllabus Coverage", value: isDemo ? "64%" : "0%", sub: isDemo ? "+8% this week" : "Day 1 Baseline", icon: <Target size={18} className="text-amber-400" />, color: "border-amber-400/20 bg-amber-400/5" },
|
||||
{ label: "Current Streak", value: `${streakCount} Day${streakCount === 1 ? '' : 's'}`, sub: isDemo ? "Keep it up!" : "Day 1 active", icon: <Flame size={18} className="text-orange-400" />, color: "border-orange-400/20 bg-orange-400/5" },
|
||||
].map((kpi) => (
|
||||
<Card key={kpi.label} className={`rounded-2xl border ${kpi.color} hover:scale-[1.02] transition-transform`}>
|
||||
<CardContent className="p-5 flex flex-col gap-2">
|
||||
|
||||
@@ -4,8 +4,9 @@ import { StudyTimeOptimizationCard } from "@/components/AI/StudyTimeOptimization
|
||||
import { Clock, Zap, Moon, Sun, AlertCircle, ArrowRight, BarChart2 } from "lucide-react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import Link from "next/link"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
const INSIGHTS = [
|
||||
const DEMO_INSIGHTS = [
|
||||
{
|
||||
icon: <Sun size={16} className="text-yellow-400" />,
|
||||
title: "Peak Cognitive Window",
|
||||
@@ -26,7 +27,7 @@ const INSIGHTS = [
|
||||
},
|
||||
]
|
||||
|
||||
const HOURLY_EFFICIENCY = [
|
||||
const DEMO_HOURLY_EFFICIENCY = [
|
||||
{ slot: "6–9 AM", pct: 60, label: "Morning" },
|
||||
{ slot: "9–12 PM", pct: 72, label: "Mid-Morning" },
|
||||
{ slot: "12–2 PM", pct: 55, label: "Post-Lunch dip" },
|
||||
@@ -37,7 +38,36 @@ const HOURLY_EFFICIENCY = [
|
||||
{ slot: "11 PM+", pct: 32, label: "Avoid" },
|
||||
]
|
||||
|
||||
const ZERO_HOURLY_EFFICIENCY = [
|
||||
{ slot: "6–9 AM", pct: 0, label: "Pending" },
|
||||
{ slot: "9–12 PM", pct: 0, label: "Pending" },
|
||||
{ slot: "12–2 PM", pct: 0, label: "Pending" },
|
||||
{ slot: "2–5 PM", pct: 0, label: "Calibrating", highlight: false },
|
||||
{ slot: "5–7 PM", pct: 0, label: "Pending" },
|
||||
{ slot: "7–9 PM", pct: 0, label: "Pending" },
|
||||
{ slot: "9–11 PM", pct: 0, label: "Pending" },
|
||||
{ slot: "11 PM+", pct: 0, label: "Pending" },
|
||||
]
|
||||
|
||||
export default function TimeOptimizationPage() {
|
||||
const { userId } = useActiveUser()
|
||||
const isDemo = isDemoUser(userId)
|
||||
const hourlySlots = isDemo ? DEMO_HOURLY_EFFICIENCY : ZERO_HOURLY_EFFICIENCY
|
||||
const insights = isDemo ? DEMO_INSIGHTS : [
|
||||
{
|
||||
icon: <Sun size={16} className="text-yellow-400" />,
|
||||
title: "Peak Window Calibrating",
|
||||
desc: "Complete 3 test sessions at different times of day to establish your circadian peak focus hours.",
|
||||
color: "border-yellow-400/20 bg-yellow-400/5",
|
||||
},
|
||||
{
|
||||
icon: <Moon size={16} className="text-indigo-400" />,
|
||||
title: "Consistency Booster",
|
||||
desc: "Start with a 30-minute daily review routine to jumpstart your spaced repetition learning schedule.",
|
||||
color: "border-indigo-400/20 bg-indigo-400/5",
|
||||
},
|
||||
]
|
||||
|
||||
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">
|
||||
{/* Page Header */}
|
||||
@@ -63,10 +93,10 @@ export default function TimeOptimizationPage() {
|
||||
{/* KPI Strip */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ label: "Peak Efficiency", value: "95%", sub: "2–5 PM window", color: "border-yellow-400/20 bg-yellow-400/5", icon: <Sun size={18} className="text-yellow-400" /> },
|
||||
{ label: "Study Hrs Today", value: "3.8 h", sub: "Target: 6 h", color: "border-[#00F0FF]/20 bg-[#00F0FF]/5", icon: <Clock size={18} className="text-[#00F0FF]" /> },
|
||||
{ label: "Morning Block", value: "60%", sub: "6–9 AM", color: "border-indigo-400/20 bg-indigo-400/5", icon: <Moon size={18} className="text-indigo-400" /> },
|
||||
{ label: "Evening Block", value: "74%", sub: "7–9 PM", color: "border-green-400/20 bg-green-400/5", icon: <BarChart2 size={18} className="text-green-400" /> },
|
||||
{ label: "Peak Efficiency", value: isDemo ? "95%" : "--", sub: isDemo ? "2–5 PM window" : "Calibrating", color: "border-yellow-400/20 bg-yellow-400/5", icon: <Sun size={18} className="text-yellow-400" /> },
|
||||
{ label: "Study Hrs Today", value: isDemo ? "3.8 h" : "0.0 h", sub: "Target: 6 h", color: "border-[#00F0FF]/20 bg-[#00F0FF]/5", icon: <Clock size={18} className="text-[#00F0FF]" /> },
|
||||
{ label: "Morning Block", value: isDemo ? "60%" : "0%", sub: "6–9 AM", color: "border-indigo-400/20 bg-indigo-400/5", icon: <Moon size={18} className="text-indigo-400" /> },
|
||||
{ label: "Evening Block", value: isDemo ? "74%" : "0%", sub: "7–9 PM", color: "border-green-400/20 bg-green-400/5", icon: <BarChart2 size={18} className="text-green-400" /> },
|
||||
].map(kpi => (
|
||||
<Card key={kpi.label} className={`rounded-2xl border ${kpi.color}`}>
|
||||
<CardContent className="p-4 flex flex-col gap-2">
|
||||
@@ -96,12 +126,12 @@ export default function TimeOptimizationPage() {
|
||||
<CardContent className="p-5 space-y-3">
|
||||
<h3 className="text-sm font-black uppercase tracking-wider text-muted-foreground">Time Slot Breakdown</h3>
|
||||
<div className="space-y-2">
|
||||
{HOURLY_EFFICIENCY.map(slot => (
|
||||
{hourlySlots.map(slot => (
|
||||
<div key={slot.slot} className={`flex items-center gap-3 p-2 rounded-lg ${slot.highlight ? "bg-yellow-400/10 border border-yellow-400/20" : "bg-white/5"}`}>
|
||||
<div className="w-16 text-xs font-bold text-muted-foreground shrink-0">{slot.slot}</div>
|
||||
<div className="flex-1 h-1.5 bg-secondary rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full rounded-full ${slot.pct >= 80 ? (slot.highlight ? "bg-yellow-400" : "bg-green-400") : slot.pct >= 60 ? "bg-[#00F0FF]" : "bg-red-400"}`}
|
||||
className={`h-full rounded-full ${slot.pct >= 80 ? (slot.highlight ? "bg-yellow-400" : "bg-green-400") : slot.pct >= 60 ? "bg-[#00F0FF]" : isDemo ? "bg-red-400" : "bg-white/10"}`}
|
||||
style={{ width: `${slot.pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
@@ -115,7 +145,7 @@ export default function TimeOptimizationPage() {
|
||||
|
||||
{/* AI Insights */}
|
||||
<div className="space-y-3">
|
||||
{INSIGHTS.map(insight => (
|
||||
{insights.map(insight => (
|
||||
<Card key={insight.title} className={`rounded-2xl border ${insight.color}`}>
|
||||
<CardContent className="p-4 flex items-start gap-3">
|
||||
<div className="mt-0.5 shrink-0">{insight.icon}</div>
|
||||
|
||||
@@ -4,22 +4,42 @@ import { WeeklyProgressReport } from "@/components/AI/WeeklyProgressReport"
|
||||
import { Target, TrendingUp, Calendar, Zap, ArrowRight, BookOpen, CheckCircle2 } from "lucide-react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import Link from "next/link"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
const STATS = [
|
||||
const DEMO_STATS = [
|
||||
{ label: "Avg. Accuracy", value: "85%", delta: "+6%", positive: true },
|
||||
{ label: "Days Active", value: "7 / 7", delta: "Perfect week", positive: true },
|
||||
{ label: "Questions Solved", value: "214", delta: "+38 vs last week", positive: true },
|
||||
{ label: "Topics Covered", value: "12", delta: "3 remaining", positive: false },
|
||||
]
|
||||
|
||||
const SYLLABUS_PROGRESS = [
|
||||
const ZERO_STATS = [
|
||||
{ label: "Avg. Accuracy", value: "0%", delta: "Baseline", positive: true },
|
||||
{ label: "Days Active", value: "0 / 7", delta: "Day 1 Active", positive: true },
|
||||
{ label: "Questions Solved", value: "0", delta: "0 vs last week", positive: true },
|
||||
{ label: "Topics Covered", value: "0", delta: "0 completed", positive: false },
|
||||
]
|
||||
|
||||
const DEMO_SYLLABUS_PROGRESS = [
|
||||
{ subject: "Physics", completed: 42, total: 60, percentage: 70, color: "bg-blue-500", text: "text-blue-500" },
|
||||
{ subject: "Chemistry", completed: 35, total: 55, percentage: 63, color: "bg-amber-500", text: "text-amber-500" },
|
||||
{ subject: "Mathematics", completed: 50, total: 65, percentage: 76, color: "bg-purple-500", text: "text-purple-500" },
|
||||
{ subject: "Biology", completed: 20, total: 40, percentage: 50, color: "bg-emerald-500", text: "text-emerald-500" },
|
||||
]
|
||||
|
||||
const ZERO_SYLLABUS_PROGRESS = [
|
||||
{ subject: "Physics", completed: 0, total: 60, percentage: 0, color: "bg-blue-500", text: "text-blue-500" },
|
||||
{ subject: "Chemistry", completed: 0, total: 55, percentage: 0, color: "bg-amber-500", text: "text-amber-500" },
|
||||
{ subject: "Mathematics", completed: 0, total: 65, percentage: 0, color: "bg-purple-500", text: "text-purple-500" },
|
||||
{ subject: "Biology", completed: 0, total: 40, percentage: 0, color: "bg-emerald-500", text: "text-emerald-500" },
|
||||
]
|
||||
|
||||
export default function WeeklyProgressPage() {
|
||||
const { userId } = useActiveUser()
|
||||
const isDemo = isDemoUser(userId)
|
||||
const stats = isDemo ? DEMO_STATS : ZERO_STATS
|
||||
const syllabusProgress = isDemo ? DEMO_SYLLABUS_PROGRESS : ZERO_SYLLABUS_PROGRESS
|
||||
|
||||
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">
|
||||
{/* Page Header */}
|
||||
@@ -46,7 +66,7 @@ export default function WeeklyProgressPage() {
|
||||
|
||||
{/* KPI Stats Row */}
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{STATS.map((stat) => (
|
||||
{stats.map((stat) => (
|
||||
<Card key={stat.label} className="bg-secondary/20 border-border/60 rounded-2xl">
|
||||
<CardContent className="p-4">
|
||||
<p className="text-xs font-bold uppercase tracking-wider text-muted-foreground mb-1">{stat.label}</p>
|
||||
@@ -80,7 +100,7 @@ export default function WeeklyProgressPage() {
|
||||
</h2>
|
||||
<Card className="bg-secondary/10 border-border/60 rounded-2xl">
|
||||
<CardContent className="p-5 space-y-4">
|
||||
{SYLLABUS_PROGRESS.map((item) => (
|
||||
{syllabusProgress.map((item) => (
|
||||
<div key={item.subject} className="space-y-1.5">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="font-bold">{item.subject}</span>
|
||||
@@ -107,10 +127,14 @@ export default function WeeklyProgressPage() {
|
||||
<h3 className="text-sm font-black uppercase tracking-wider text-[#00F0FF]">Weekly Goals</h3>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-start gap-2 p-2 rounded-lg bg-white/5">
|
||||
{isDemo ? (
|
||||
<CheckCircle2 size={16} className="text-green-400 mt-0.5 shrink-0" />
|
||||
) : (
|
||||
<div className="w-4 h-4 rounded-full border-2 border-muted-foreground mt-0.5 shrink-0" />
|
||||
)}
|
||||
<div>
|
||||
<div className="text-xs font-bold">Complete 200 Questions</div>
|
||||
<div className="text-[10px] text-muted-foreground">Goal Met (214/200)</div>
|
||||
<div className="text-[10px] text-muted-foreground">{isDemo ? "Goal Met (214/200)" : "Pending (0/200)"}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 p-2 rounded-lg bg-white/5 opacity-70">
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { LineChart as LineChartIcon, Activity, BrainCircuit } from "lucide-react"
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, BarChart, Bar, Cell } from 'recharts'
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
export default function AnalyticsPage() {
|
||||
const trendData = [
|
||||
const DEMO_TREND_DATA = [
|
||||
{ name: 'Week 1', score: 120 },
|
||||
{ name: 'Week 2', score: 135 },
|
||||
{ name: 'Week 3', score: 130 },
|
||||
@@ -14,8 +14,16 @@ export default function AnalyticsPage() {
|
||||
{ name: 'Week 6', score: 185 },
|
||||
]
|
||||
|
||||
// For heatmap-like visualization (using a barchart with different colors)
|
||||
const heatmapData = [
|
||||
const ZERO_TREND_DATA = [
|
||||
{ name: 'Week 1', score: 0 },
|
||||
{ name: 'Week 2', score: 0 },
|
||||
{ name: 'Week 3', score: 0 },
|
||||
{ name: 'Week 4', score: 0 },
|
||||
{ name: 'Week 5', score: 0 },
|
||||
{ name: 'Week 6', score: 0 },
|
||||
]
|
||||
|
||||
const DEMO_HEATMAP_DATA = [
|
||||
{ topic: 'Kinematics', accuracy: 95 },
|
||||
{ topic: 'Optics', accuracy: 72 },
|
||||
{ topic: 'Thermodynamics', accuracy: 45 },
|
||||
@@ -24,7 +32,23 @@ export default function AnalyticsPage() {
|
||||
{ topic: 'Inorganic', accuracy: 65 },
|
||||
]
|
||||
|
||||
const ZERO_HEATMAP_DATA = [
|
||||
{ topic: 'Kinematics', accuracy: 0 },
|
||||
{ topic: 'Optics', accuracy: 0 },
|
||||
{ topic: 'Thermodynamics', accuracy: 0 },
|
||||
{ topic: 'Calculus', accuracy: 0 },
|
||||
{ topic: 'Organic Chem', accuracy: 0 },
|
||||
{ topic: 'Inorganic', accuracy: 0 },
|
||||
]
|
||||
|
||||
export default function AnalyticsPage() {
|
||||
const { userId } = useActiveUser()
|
||||
const isDemo = isDemoUser(userId)
|
||||
const trendData = isDemo ? DEMO_TREND_DATA : ZERO_TREND_DATA
|
||||
const heatmapData = isDemo ? DEMO_HEATMAP_DATA : ZERO_HEATMAP_DATA
|
||||
|
||||
const getColor = (accuracy: number) => {
|
||||
if (accuracy === 0) return '#374151' // neutral gray
|
||||
if (accuracy >= 80) return '#10b981' // emerald-500
|
||||
if (accuracy >= 60) return '#f59e0b' // amber-500
|
||||
return '#ef4444' // red-500
|
||||
@@ -50,7 +74,9 @@ export default function AnalyticsPage() {
|
||||
<Card className="col-span-1 lg:col-span-2 border-border shadow-sm bg-background">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2"><LineChartIcon className="text-[#00F0FF]" size={20} /> Projected Score Trajectory</CardTitle>
|
||||
<CardDescription>Your mock scores over the last 6 months projected towards the final exam.</CardDescription>
|
||||
<CardDescription>
|
||||
{isDemo ? "Your mock scores over the last 6 months projected towards the final exam." : "Your mock test trajectory will plot here as you complete tests."}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="h-[350px] mt-4">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
@@ -75,7 +101,11 @@ export default function AnalyticsPage() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-foreground/80 leading-relaxed font-medium">
|
||||
Our models detect a consistent <strong>14% accuracy drop</strong> in physics questions involving Calculus. We are adjusting your upcoming DPPs to focus heavily on integration applications in kinematics.
|
||||
{isDemo ? (
|
||||
<>Our models detect a consistent <strong>14% accuracy drop</strong> in physics questions involving Calculus. We are adjusting your upcoming DPPs to focus heavily on integration applications in kinematics.</>
|
||||
) : (
|
||||
<>Complete your first diagnostic test or DPP to unlock personalized AI diagnoses and focus area recommendations. Our neural models will begin analyzing your accuracy curves as soon as you attempt questions.</>
|
||||
)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardFooter } from "@/components/ui/card"
|
||||
import { MessageSquare, ThumbsUp, ChevronUp, Search, Image as ImageIcon, CheckCircle, Lock, X, Send } from "lucide-react"
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
interface Doubt {
|
||||
id: string
|
||||
@@ -22,7 +22,8 @@ interface Doubt {
|
||||
|
||||
export default function CommunityPage() {
|
||||
const { user, hasAccessTo, loading } = useActiveUser()
|
||||
const currentUserName = user?.name || "Neha Verma"
|
||||
const isDemo = isDemoUser(user?.id)
|
||||
const currentUserName = user?.name || (isDemo ? "Neha Verma" : "Student")
|
||||
const currentUserInitials = currentUserName.substring(0, 2).toUpperCase()
|
||||
const [activeFilter, setActiveFilter] = useState<'all' | 'my-doubts' | 'solved' | 'unsolved'>('all')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
@@ -58,7 +59,7 @@ export default function CommunityPage() {
|
||||
isSolved: false,
|
||||
hasModeratorReply: false
|
||||
},
|
||||
{
|
||||
...(isDemo ? [{
|
||||
id: "3",
|
||||
author: currentUserName,
|
||||
avatar: currentUserInitials,
|
||||
@@ -71,7 +72,7 @@ export default function CommunityPage() {
|
||||
replies: 8,
|
||||
isSolved: true,
|
||||
hasModeratorReply: true
|
||||
}
|
||||
}] : [])
|
||||
]
|
||||
|
||||
const filteredDoubts = mockDoubts.filter(d => {
|
||||
@@ -244,9 +245,16 @@ export default function CommunityPage() {
|
||||
</Card>
|
||||
))}
|
||||
{filteredDoubts.length === 0 && (
|
||||
<div className="text-center py-12 text-muted-foreground">
|
||||
<MessageSquare size={48} className="mx-auto mb-4 opacity-20" />
|
||||
<p className="font-medium">No doubts found matching your criteria.</p>
|
||||
<div className="text-center py-12 text-muted-foreground bg-secondary/10 border border-dashed border-border rounded-2xl p-6">
|
||||
<MessageSquare size={48} className="mx-auto mb-3 opacity-30 text-[#00F0FF]" />
|
||||
<p className="font-bold text-foreground mb-1">
|
||||
{activeFilter === 'my-doubts' ? "No Doubts Asked Yet" : "No doubts found matching your criteria."}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground max-w-sm mx-auto">
|
||||
{activeFilter === 'my-doubts'
|
||||
? "You haven't asked any doubts yet. Click 'Ask a Doubt' above to get answers from mentors and peers."
|
||||
: "Try selecting another filter or clearing your search query."}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { ChevronDown, ChevronRight, CheckCircle2, Circle, Clock, Activity, Target } from "lucide-react"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
type ChapterStatus = 'Mastered' | 'Practiced' | 'In Progress' | 'Not Started'
|
||||
|
||||
@@ -34,8 +35,7 @@ const getStatusIcon = (status: ChapterStatus) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default function ProgressTrackerPage() {
|
||||
const [subjects, setSubjects] = useState<Subject[]>([
|
||||
const DEMO_SUBJECTS: Subject[] = [
|
||||
{
|
||||
name: "Physics",
|
||||
completionPct: 42,
|
||||
@@ -82,7 +82,7 @@ export default function ProgressTrackerPage() {
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Mathematics",
|
||||
name: "Mathematics / Biology",
|
||||
completionPct: 22,
|
||||
chapters: [
|
||||
{
|
||||
@@ -95,7 +95,78 @@ export default function ProgressTrackerPage() {
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
]
|
||||
|
||||
const ZERO_SUBJECTS: Subject[] = [
|
||||
{
|
||||
name: "Physics",
|
||||
completionPct: 0,
|
||||
chapters: [
|
||||
{
|
||||
name: "Kinematics",
|
||||
isExpanded: true,
|
||||
topics: [
|
||||
{ name: "Motion in One Dimension", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||
{ name: "Projectile Motion", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||
{ name: "Relative Velocity", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Laws of Motion",
|
||||
isExpanded: false,
|
||||
topics: [
|
||||
{ name: "Newton's First Law", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||
{ name: "Friction", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Thermodynamics",
|
||||
isExpanded: false,
|
||||
topics: [
|
||||
{ name: "First Law", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||
{ name: "Carnot Engine", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Chemistry",
|
||||
completionPct: 0,
|
||||
chapters: [
|
||||
{
|
||||
name: "Atomic Structure",
|
||||
isExpanded: false,
|
||||
topics: [
|
||||
{ name: "Bohr Model", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||
{ name: "Quantum Numbers", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Mathematics / Biology",
|
||||
completionPct: 0,
|
||||
chapters: [
|
||||
{
|
||||
name: "Core Foundation",
|
||||
isExpanded: false,
|
||||
topics: [
|
||||
{ name: "Limits / Cell Structure", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||
{ name: "Derivatives / Genetics", status: "Not Started", accuracy: 0, questionsAttempted: 0 },
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
export default function ProgressTrackerPage() {
|
||||
const { userId } = useActiveUser()
|
||||
const isDemo = isDemoUser(userId)
|
||||
const [subjects, setSubjects] = useState<Subject[]>(isDemo ? DEMO_SUBJECTS : ZERO_SUBJECTS)
|
||||
|
||||
useEffect(() => {
|
||||
setSubjects(isDemo ? DEMO_SUBJECTS : ZERO_SUBJECTS)
|
||||
}, [isDemo])
|
||||
|
||||
const toggleChapter = (subjectIndex: number, chapterIndex: number) => {
|
||||
const newSubjects = [...subjects]
|
||||
|
||||
@@ -4,31 +4,40 @@ import { useState, useEffect } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Trophy, TrendingUp, AlertTriangle, Sparkles, Building2, Target, Sliders, CheckCircle2, ChevronRight, UserCheck } from "lucide-react"
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
export default function RankPredictorPage() {
|
||||
const { user } = useActiveUser()
|
||||
const isDemo = isDemoUser(user?.id)
|
||||
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 [physics, setPhysics] = useState(0)
|
||||
const [chemistry, setChemistry] = useState(0)
|
||||
const [bioOrMath, setBioOrMath] = useState(0)
|
||||
const [saved, setSaved] = useState(false)
|
||||
const [initialized, setInitialized] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.target_exam) {
|
||||
if (user.target_exam.includes("NEET")) {
|
||||
setExam("NEET")
|
||||
if (!user || initialized) return
|
||||
const isNeet = user.target_exam ? user.target_exam.includes("NEET") : true
|
||||
setExam(isNeet ? "NEET" : "JEE")
|
||||
|
||||
if (isDemo) {
|
||||
if (isNeet) {
|
||||
setPhysics(140)
|
||||
setChemistry(150)
|
||||
setBioOrMath(320)
|
||||
} else {
|
||||
setExam("JEE")
|
||||
setPhysics(65)
|
||||
setChemistry(70)
|
||||
setBioOrMath(75)
|
||||
}
|
||||
} else {
|
||||
setPhysics(0)
|
||||
setChemistry(0)
|
||||
setBioOrMath(0)
|
||||
}
|
||||
}, [user])
|
||||
setInitialized(true)
|
||||
}, [user, isDemo, initialized])
|
||||
|
||||
const maxTotal = exam === "NEET" ? 720 : 300
|
||||
const maxPhysics = exam === "NEET" ? 180 : 100
|
||||
@@ -83,7 +92,18 @@ export default function RankPredictorPage() {
|
||||
{/* 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) }}
|
||||
onClick={() => {
|
||||
setExam("NEET")
|
||||
if (isDemo) {
|
||||
setPhysics(135)
|
||||
setChemistry(155)
|
||||
setBioOrMath(330)
|
||||
} else {
|
||||
setPhysics(0)
|
||||
setChemistry(0)
|
||||
setBioOrMath(0)
|
||||
}
|
||||
}}
|
||||
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"
|
||||
}`}
|
||||
@@ -91,7 +111,18 @@ export default function RankPredictorPage() {
|
||||
NEET (UG) 720 Marks
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { setExam("JEE"); setPhysics(65); setChemistry(70); setBioOrMath(75) }}
|
||||
onClick={() => {
|
||||
setExam("JEE")
|
||||
if (isDemo) {
|
||||
setPhysics(65)
|
||||
setChemistry(70)
|
||||
setBioOrMath(75)
|
||||
} else {
|
||||
setPhysics(0)
|
||||
setChemistry(0)
|
||||
setBioOrMath(0)
|
||||
}
|
||||
}}
|
||||
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"
|
||||
}`}
|
||||
@@ -175,7 +206,9 @@ export default function RankPredictorPage() {
|
||||
<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.
|
||||
{totalScore === 0
|
||||
? "Baseline Calibrating: Solve your first DPP or CBT to benchmark your subject strengths."
|
||||
: `Your Biology/Math score (${bioOrMath}) provides solid rank leverage. Keep maintaining your revision momentum.`}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -185,7 +218,9 @@ export default function RankPredictorPage() {
|
||||
<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.
|
||||
{totalScore === 0
|
||||
? "Zero Baseline: Adjust the sliders above or take practice tests to see rank impact in real time."
|
||||
: "Increasing Physics by just 25 marks will boost your rank by approximately ~4,800 positions."}
|
||||
</p>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -202,10 +237,10 @@ export default function RankPredictorPage() {
|
||||
</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()}
|
||||
{totalScore === 0 ? "AIR --" : `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
|
||||
<CheckCircle2 size={13} /> {totalScore === 0 ? "Calibrating · Adjust Sliders" : "High Accuracy Confidence"}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
@@ -215,6 +250,12 @@ export default function RankPredictorPage() {
|
||||
<Building2 className="text-[#00F0FF]" size={20} /> College Admission Chances
|
||||
</h3>
|
||||
|
||||
{totalScore === 0 ? (
|
||||
<div className="p-4 rounded-2xl bg-secondary/20 border border-dashed border-border text-center text-xs text-muted-foreground space-y-1">
|
||||
<p className="font-bold text-foreground">No Marks Simulated</p>
|
||||
<p>Drag the subject sliders on the left or complete mock tests to view college probability predictions.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{colleges.map((c) => {
|
||||
const isLikely = totalScore >= c.minScore
|
||||
@@ -235,6 +276,7 @@ export default function RankPredictorPage() {
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,40 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Flame, Gift, Star, Trophy, Target, ArrowRight, CheckCircle2 } from "lucide-react"
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
export default function StreaksPage() {
|
||||
const { user } = useActiveUser()
|
||||
const isDemo = isDemoUser(user?.id)
|
||||
const currentStreak = user?.streak_count ?? 1
|
||||
const bestStreak = Math.max(currentStreak, user?.longest_streak ?? 1)
|
||||
const bestStreak = isDemo ? (user?.longest_streak ?? 45) : Math.max(currentStreak, user?.longest_streak ?? 1)
|
||||
const [streakPoints, setStreakPoints] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (!user?.id) return
|
||||
if (isDemo) {
|
||||
setStreakPoints(currentStreak * 50)
|
||||
return
|
||||
}
|
||||
async function loadStats() {
|
||||
try {
|
||||
const res = await fetch(`/api/users/${user?.id}/stats`)
|
||||
if (res.ok) {
|
||||
const stats = await res.json()
|
||||
if (!stats.is_new_user) {
|
||||
setStreakPoints(currentStreak * 50)
|
||||
} else {
|
||||
setStreakPoints(0)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
setStreakPoints(0)
|
||||
}
|
||||
}
|
||||
loadStats()
|
||||
}, [user?.id, isDemo, currentStreak])
|
||||
|
||||
const rewards = [
|
||||
{ streak: 7, title: "Consistent Learner", reward: "1 Free Premium DPP", unlocked: currentStreak >= 7 },
|
||||
@@ -39,7 +66,11 @@ export default function StreaksPage() {
|
||||
<Flame size={64} className="text-orange-500 mb-4 animate-pulse" />
|
||||
<h2 className="text-6xl font-black text-foreground mb-2">{currentStreak} <span className="text-2xl text-muted-foreground">Days</span></h2>
|
||||
<p className="text-sm font-bold text-orange-500">CURRENT STREAK</p>
|
||||
<p className="text-xs text-muted-foreground mt-4 max-w-xs">Complete a Daily Practice Problem (DPP) or CBT Exam every day to keep your streak alive.</p>
|
||||
<p className="text-xs text-muted-foreground mt-4 max-w-xs">
|
||||
{streakPoints === 0
|
||||
? "Complete your first Daily Practice Problem (DPP) or CBT Exam today to start earning streak points."
|
||||
: "Complete a Daily Practice Problem (DPP) or CBT Exam every day to keep your streak alive."}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@@ -57,7 +88,10 @@ export default function StreaksPage() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground mb-1">Total Streak Points Earned</p>
|
||||
<p className="text-3xl font-bold text-[#00F0FF]">{(currentStreak * 50).toLocaleString()}</p>
|
||||
<p className="text-3xl font-bold text-[#00F0FF]">{streakPoints.toLocaleString()}</p>
|
||||
{streakPoints === 0 && (
|
||||
<p className="text-xs text-muted-foreground mt-1">Take a test to activate consistency bonus points</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { useState, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { CheckCircle2, Clock, SkipForward, BrainCircuit } from "lucide-react"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
type PlanState = 'PENDING' | 'DONE' | 'SKIPPED';
|
||||
|
||||
@@ -16,14 +17,30 @@ interface PlanItem {
|
||||
status: PlanState;
|
||||
}
|
||||
|
||||
const INITIAL_PLAN: PlanItem[] = [
|
||||
const DEMO_PLAN: PlanItem[] = [
|
||||
{ id: '1', title: 'Revise Thermodynamics Concepts', duration: '45 min', type: 'Theory', status: 'PENDING' },
|
||||
{ id: '2', title: 'Solve 15 Rotational Dynamics DPPs', duration: '30 min', type: 'Practice', status: 'PENDING' },
|
||||
{ id: '3', title: 'Review Mistakes in Optics (T-3)', duration: '20 min', type: 'Review', status: 'PENDING' },
|
||||
];
|
||||
|
||||
const NEW_USER_PLAN: PlanItem[] = [
|
||||
{ id: '1', title: 'Take Baseline Diagnostic CBT Test', duration: '45 min', type: 'Diagnostic', status: 'PENDING' },
|
||||
{ id: '2', title: 'Attempt Today\'s 15 DPP Practice Questions', duration: '30 min', type: 'Practice', status: 'PENDING' },
|
||||
{ id: '3', title: 'Calibrate Target Exam & Daily Time Slot', duration: '15 min', type: 'Setup', status: 'PENDING' },
|
||||
];
|
||||
|
||||
export function AdaptiveStudyPlan() {
|
||||
const [items, setItems] = useState<PlanItem[]>(INITIAL_PLAN);
|
||||
const { user } = useActiveUser();
|
||||
const isDemo = isDemoUser(user?.id);
|
||||
const [items, setItems] = useState<PlanItem[]>(DEMO_PLAN);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDemo) {
|
||||
setItems(DEMO_PLAN);
|
||||
} else {
|
||||
setItems(NEW_USER_PLAN);
|
||||
}
|
||||
}, [isDemo]);
|
||||
|
||||
const updateStatus = (id: string, newStatus: PlanState) => {
|
||||
setItems(items.map(item => item.id === id ? { ...item, status: newStatus } : item));
|
||||
@@ -48,7 +65,9 @@ export function AdaptiveStudyPlan() {
|
||||
<Clock size={14} className="text-primary" /> Today: {totalTime} min
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Dynamically generated based on your weak areas.</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isDemo ? "Dynamically generated based on your weak areas." : "Day 1 Onboarding Plan · Calibrating AI recommendations based on your initial performance."}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 p-6">
|
||||
<div className="relative space-y-6 before:absolute before:inset-0 before:ml-5 before:-translate-x-px md:before:mx-auto md:before:translate-x-0 before:h-full before:w-0.5 before:bg-gradient-to-b before:from-transparent before:via-border before:to-transparent">
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Flame, Info } from "lucide-react"
|
||||
import { Flame, Info, CheckCircle2 } from "lucide-react"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
const HEATMAP_DATA = [
|
||||
{ topic: "Rotational Dynamics", attempts: [80, 90, 40, 20, 10] }, // 10 is critical error rate
|
||||
@@ -10,14 +13,26 @@ const HEATMAP_DATA = [
|
||||
{ topic: "Modern Physics", attempts: [10, 5, 2, 0, 0] }
|
||||
];
|
||||
|
||||
const ZERO_HEATMAP_DATA = [
|
||||
{ topic: "Physics (Core Mechanics)", attempts: [0, 0, 0, 0, 0] },
|
||||
{ topic: "Chemistry (Physical & Organic)", attempts: [0, 0, 0, 0, 0] },
|
||||
{ topic: "Mathematics / Biology", attempts: [0, 0, 0, 0, 0] },
|
||||
];
|
||||
|
||||
export function MistakeHeatmap() {
|
||||
const { userId } = useActiveUser();
|
||||
const isDemo = isDemoUser(userId);
|
||||
|
||||
const getColor = (errorRate: number) => {
|
||||
if (errorRate === 0) return 'bg-white/5 border-white/10 text-muted-foreground';
|
||||
if (errorRate >= 50) return 'bg-red-500/80 border-red-500 shadow-[0_0_10px_-2px_rgba(239,68,68,0.5)]'; // Critical
|
||||
if (errorRate >= 30) return 'bg-amber-500/60 border-amber-500'; // Warning
|
||||
if (errorRate >= 10) return 'bg-yellow-500/30 border-yellow-500/50'; // Moderate
|
||||
return 'bg-green-500/20 border-green-500/30'; // Good
|
||||
};
|
||||
|
||||
const rows = isDemo ? HEATMAP_DATA : ZERO_HEATMAP_DATA;
|
||||
|
||||
return (
|
||||
<Card className="border-border bg-background shadow-sm h-full min-h-[540px] flex flex-col justify-between">
|
||||
<CardHeader className="pb-4">
|
||||
@@ -28,7 +43,9 @@ export function MistakeHeatmap() {
|
||||
</CardTitle>
|
||||
<Info size={16} className="text-muted-foreground" />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Topic-wise error rate across last 5 tests.</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isDemo ? "Topic-wise error rate across last 5 tests." : "Topic-wise error rate across your recent test sessions."}
|
||||
</p>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 p-4 md:p-6 flex flex-col justify-between">
|
||||
<div className="w-full">
|
||||
@@ -46,7 +63,7 @@ export function MistakeHeatmap() {
|
||||
|
||||
{/* Data rows */}
|
||||
<div className="space-y-3">
|
||||
{HEATMAP_DATA.map((row, idx) => (
|
||||
{rows.map((row, idx) => (
|
||||
<div key={idx} className="flex items-center group">
|
||||
<div className="w-32 sm:w-36 md:w-40 shrink-0 text-xs sm:text-sm md:text-base font-bold truncate pr-2 group-hover:text-primary transition-colors" title={row.topic}>
|
||||
{row.topic}
|
||||
@@ -65,11 +82,22 @@ export function MistakeHeatmap() {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{!isDemo && (
|
||||
<div className="mt-6 p-4 rounded-xl bg-secondary/30 border border-border/50 text-center space-y-1">
|
||||
<div className="flex items-center justify-center gap-1.5 text-xs font-bold text-green-400">
|
||||
<CheckCircle2 size={14} /> Clean Slate · 0 Errors Recorded
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
As you attempt DPPs and CBT mock tests, questions you get wrong will automatically be tracked and heatmapped here.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div className="mt-6 pt-4 border-t border-border/50 flex flex-wrap items-center justify-center gap-x-4 gap-y-2 text-xs font-bold text-muted-foreground">
|
||||
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-green-500/20 border border-green-500/30"></div> {"< 10%"}</div>
|
||||
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-green-500/20 border border-green-500/30"></div> {"0-10% (Optimal)"}</div>
|
||||
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-yellow-500/30 border border-yellow-500/50"></div> {"10-30%"}</div>
|
||||
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-amber-500/60 border border-amber-500"></div> {"30-50%"}</div>
|
||||
<div className="flex items-center gap-1.5"><div className="size-3 rounded-sm bg-red-500/80 border border-red-500"></div> {"> 50%"}</div>
|
||||
@@ -78,3 +106,4 @@ export function MistakeHeatmap() {
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,12 +5,13 @@ import { Card, CardContent } from "@/components/ui/card"
|
||||
import { TrendingUp, Flame, RefreshCw } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
export function ReadinessGauge() {
|
||||
const [data, setData] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const { userId, loading: userLoading } = useActiveUser();
|
||||
const isDemo = isDemoUser(userId);
|
||||
|
||||
useEffect(() => {
|
||||
if (userLoading || !userId) return;
|
||||
@@ -22,7 +23,7 @@ export function ReadinessGauge() {
|
||||
const json = await res.json()
|
||||
setData(json)
|
||||
} catch (err) {
|
||||
console.warn("Using mock fallback data for Readiness Gauge.")
|
||||
if (isDemo) {
|
||||
setData({
|
||||
score: 72,
|
||||
breakdown: {
|
||||
@@ -33,12 +34,24 @@ export function ReadinessGauge() {
|
||||
coverage: 14
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setData({
|
||||
score: 0,
|
||||
breakdown: {
|
||||
accuracy: 0,
|
||||
speed: 0,
|
||||
consistency: 0,
|
||||
recovery: 0,
|
||||
coverage: 0
|
||||
}
|
||||
})
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchReadiness()
|
||||
}, [userId, userLoading])
|
||||
}, [userId, userLoading, isDemo])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -49,16 +62,10 @@ export function ReadinessGauge() {
|
||||
}
|
||||
|
||||
const score = data?.score || 0
|
||||
const breakdown = data?.breakdown || {}
|
||||
const isZero = score === 0 && !isDemo
|
||||
const circumference = 2 * Math.PI * 45 // r=45
|
||||
const strokeDashoffset = circumference - (score / 100) * circumference
|
||||
|
||||
const getColor = (s: number) => {
|
||||
if (s >= 75) return "text-green-500"
|
||||
if (s >= 50) return "text-yellow-500"
|
||||
return "text-red-500"
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
|
||||
@@ -67,10 +74,12 @@ export function ReadinessGauge() {
|
||||
<div className="absolute -top-1/2 -right-1/4 w-48 h-48 bg-[radial-gradient(circle,rgba(94,196,184,0.06)_0%,transparent_70%)] rounded-full pointer-events-none" />
|
||||
|
||||
<div className="flex justify-between items-start mb-4 relative z-10">
|
||||
<span className="text-[11px] font-bold text-muted-foreground uppercase tracking-widest">Updated daily • Today</span>
|
||||
<span className="flex items-center gap-1 text-xs font-bold text-green-500 bg-green-500/10 px-2.5 py-0.5 rounded-full">
|
||||
<span className="text-[11px] font-bold text-muted-foreground uppercase tracking-widest">
|
||||
{isZero ? "Algorithmic Baseline" : "Updated daily • Today"}
|
||||
</span>
|
||||
<span className={`flex items-center gap-1 text-xs font-bold px-2.5 py-0.5 rounded-full ${isZero ? "text-muted-foreground bg-white/5" : "text-green-500 bg-green-500/10"}`}>
|
||||
<TrendingUp size={14} className="stroke-[2.5px]" />
|
||||
+5 this week
|
||||
{isZero ? "Day 1 Baseline" : "+5 this week"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -91,7 +100,7 @@ export function ReadinessGauge() {
|
||||
strokeWidth="6"
|
||||
fill="transparent"
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
strokeDashoffset={isZero ? circumference : strokeDashoffset}
|
||||
strokeLinecap="round"
|
||||
className="transition-all duration-1500 ease-out"
|
||||
/>
|
||||
@@ -105,19 +114,31 @@ export function ReadinessGauge() {
|
||||
{/* Quick metric grid beside ring */}
|
||||
<div className="flex-1 grid grid-cols-2 gap-2.5 w-full">
|
||||
<div className="bg-white/5 rounded-lg p-2.5">
|
||||
<div className="text-base font-black flex items-baseline gap-1">88% <span className="text-xs text-green-500">↑</span></div>
|
||||
<div className="text-base font-black flex items-baseline gap-1">
|
||||
{isZero ? "0%" : "88%"}
|
||||
{!isZero && <span className="text-xs text-green-500">↑</span>}
|
||||
</div>
|
||||
<div className="text-xs font-bold text-muted-foreground">Accuracy</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-lg p-2.5">
|
||||
<div className="text-base font-black flex items-baseline gap-1">45s <span className="text-xs text-red-500">↓</span></div>
|
||||
<div className="text-base font-black flex items-baseline gap-1">
|
||||
{isZero ? "-- s" : "45s"}
|
||||
{!isZero && <span className="text-xs text-red-500">↓</span>}
|
||||
</div>
|
||||
<div className="text-xs font-bold text-muted-foreground">Speed/Q</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-lg p-2.5">
|
||||
<div className="text-base font-black flex items-baseline gap-1">High <span className="text-xs text-green-500">↑</span></div>
|
||||
<div className="text-base font-black flex items-baseline gap-1">
|
||||
{isZero ? "0%" : "High"}
|
||||
{!isZero && <span className="text-xs text-green-500">↑</span>}
|
||||
</div>
|
||||
<div className="text-xs font-bold text-muted-foreground">Consistency</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-lg p-2.5">
|
||||
<div className="text-base font-black flex items-baseline gap-1">64% <span className="text-xs text-green-500">↑</span></div>
|
||||
<div className="text-base font-black flex items-baseline gap-1">
|
||||
{isZero ? "0%" : "64%"}
|
||||
{!isZero && <span className="text-xs text-green-500">↑</span>}
|
||||
</div>
|
||||
<div className="text-xs font-bold text-muted-foreground">Syllabus</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -126,12 +147,12 @@ export function ReadinessGauge() {
|
||||
{/* Subject Bars */}
|
||||
<div className="space-y-2 mb-4 relative z-10">
|
||||
{[
|
||||
{ name: "Physics", val: 68, color: "#00F0FF" },
|
||||
{ name: "Chemistry", val: 74, color: "#F59E0B" },
|
||||
{ name: "Biology", val: 82, color: "#10B981" },
|
||||
{ name: "Physics", val: isZero ? 0 : 68, color: "#00F0FF" },
|
||||
{ name: "Chemistry", val: isZero ? 0 : 74, color: "#F59E0B" },
|
||||
{ name: "Biology / Maths", val: isZero ? 0 : 82, color: "#10B981" },
|
||||
].map(sub => (
|
||||
<div key={sub.name} className="flex items-center gap-3">
|
||||
<span className="text-xs font-bold text-muted-foreground w-20 shrink-0">{sub.name}</span>
|
||||
<span className="text-xs font-bold text-muted-foreground w-24 shrink-0 truncate">{sub.name}</span>
|
||||
<div className="flex-1 h-2 rounded-full bg-white/10 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-1000"
|
||||
@@ -147,15 +168,20 @@ export function ReadinessGauge() {
|
||||
<div className="flex items-center justify-between px-3 py-2 bg-white/5 rounded-lg mb-4 relative z-10 text-xs font-bold">
|
||||
<span className="text-muted-foreground">National Avg</span>
|
||||
<span className="text-muted-foreground">58 / 100</span>
|
||||
<span className="text-green-500">You: +{score - 58} above avg</span>
|
||||
<span className={isZero ? "text-muted-foreground" : "text-green-500"}>
|
||||
{isZero ? "Pending test data" : `You: +${score - 58} above avg`}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto px-3.5 py-3 bg-[#00F0FF]/10 border border-[#00F0FF]/20 rounded-lg text-xs md:text-sm text-[#00F0FF] font-bold flex items-center gap-2 relative z-10">
|
||||
<Flame size={18} className="fill-[#00F0FF]/20 shrink-0" />
|
||||
Your speed has dropped slightly. Focus on time-bound practice today.
|
||||
{isZero
|
||||
? "Complete your first diagnostic test to establish your personalized readiness score."
|
||||
: "Your speed has dropped slightly. Focus on time-bound practice today."}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"use client"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Flame, Star, Clock, TrendingUp } from "lucide-react"
|
||||
import { Flame, Star, Clock, TrendingUp, Sparkles } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
const SLOTS = [
|
||||
const DEMO_SLOTS = [
|
||||
{ label: "6 AM", h: 30, score: 52, peak: false },
|
||||
{ label: "9 AM", h: 55, score: 68, peak: false },
|
||||
{ label: "12 PM", h: 65, score: 74, peak: false },
|
||||
@@ -15,9 +16,25 @@ const SLOTS = [
|
||||
{ label: "11 PM", h: 32, score: 48, peak: false },
|
||||
]
|
||||
|
||||
const ZERO_SLOTS = [
|
||||
{ label: "6 AM", h: 0, score: 0, peak: false },
|
||||
{ label: "9 AM", h: 0, score: 0, peak: false },
|
||||
{ label: "12 PM", h: 0, score: 0, peak: false },
|
||||
{ label: "2 PM", h: 0, score: 0, peak: false },
|
||||
{ label: "3 PM", h: 0, score: 0, peak: false },
|
||||
{ label: "5 PM", h: 0, score: 0, peak: false },
|
||||
{ label: "7 PM", h: 0, score: 0, peak: false },
|
||||
{ label: "9 PM", h: 0, score: 0, peak: false },
|
||||
{ label: "11 PM", h: 0, score: 0, peak: false },
|
||||
]
|
||||
|
||||
export function StudyTimeOptimizationCard() {
|
||||
const { userId } = useActiveUser()
|
||||
const isDemo = isDemoUser(userId)
|
||||
const [hovered, setHovered] = useState<number | null>(null)
|
||||
const maxH = Math.max(...SLOTS.map(s => s.h))
|
||||
|
||||
const slots = isDemo ? DEMO_SLOTS : ZERO_SLOTS
|
||||
const maxH = Math.max(1, ...slots.map(s => s.h))
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -29,11 +46,11 @@ export function StudyTimeOptimizationCard() {
|
||||
<Star size={15} className="fill-yellow-500 text-yellow-500" />
|
||||
<span className="text-sm font-bold text-yellow-500">Peak Window:</span>
|
||||
<span className="text-sm font-bold bg-yellow-500/10 border border-yellow-500/20 px-2.5 py-0.5 rounded-full">
|
||||
2 PM – 5 PM
|
||||
{isDemo ? "2 PM – 5 PM" : "Calibrating..."}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-1.5 text-xs font-bold text-green-400 bg-green-400/10 border border-green-400/20 px-2.5 py-1 rounded-full">
|
||||
<TrendingUp size={11} /> 95% efficiency at peak
|
||||
<div className={`ml-auto flex items-center gap-1.5 text-xs font-bold px-2.5 py-1 rounded-full border ${isDemo ? "text-green-400 bg-green-400/10 border-green-400/20" : "text-muted-foreground bg-white/5 border-white/10"}`}>
|
||||
<TrendingUp size={11} /> {isDemo ? "95% efficiency at peak" : "0% baseline"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -58,7 +75,7 @@ export function StudyTimeOptimizationCard() {
|
||||
/>
|
||||
))}
|
||||
<div className="flex items-end gap-1 h-full absolute inset-0">
|
||||
{SLOTS.map((slot, i) => (
|
||||
{slots.map((slot, i) => (
|
||||
<div
|
||||
key={slot.label}
|
||||
className="flex-1 flex flex-col items-center justify-end h-full cursor-pointer relative group/bar"
|
||||
@@ -69,7 +86,9 @@ export function StudyTimeOptimizationCard() {
|
||||
{hovered === i && (
|
||||
<div className="absolute bottom-full mb-1.5 left-1/2 -translate-x-1/2 bg-background border border-border rounded-lg px-2.5 py-1.5 text-xs font-bold whitespace-nowrap shadow-lg z-10 pointer-events-none">
|
||||
<div className="text-foreground">{slot.label}</div>
|
||||
<div className={slot.peak ? "text-yellow-400" : "text-[#00F0FF]"}>Score: {slot.score}%</div>
|
||||
<div className={slot.peak ? "text-yellow-400" : "text-[#00F0FF]"}>
|
||||
Score: {slot.score}%
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
@@ -78,9 +97,11 @@ export function StudyTimeOptimizationCard() {
|
||||
? "bg-gradient-to-t from-yellow-600/80 to-yellow-400 shadow-[0_0_14px_rgba(234,179,8,0.5)]"
|
||||
: hovered === i
|
||||
? "bg-gradient-to-t from-[#00F0FF]/70 to-[#00F0FF]"
|
||||
: "bg-gradient-to-t from-[#00F0FF]/20 to-[#00F0FF]/50"
|
||||
: isDemo
|
||||
? "bg-gradient-to-t from-[#00F0FF]/20 to-[#00F0FF]/50"
|
||||
: "bg-white/5 border border-white/10"
|
||||
}`}
|
||||
style={{ height: `${(slot.h / maxH) * 100}%` }}
|
||||
style={{ height: isDemo ? `${(slot.h / maxH) * 100}%` : "4px" }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@@ -88,7 +109,7 @@ export function StudyTimeOptimizationCard() {
|
||||
</div>
|
||||
{/* X-axis labels */}
|
||||
<div className="flex gap-1 pt-1">
|
||||
{SLOTS.map((slot) => (
|
||||
{slots.map((slot) => (
|
||||
<div key={slot.label} className="flex-1 text-center">
|
||||
<span className="text-[10px] text-muted-foreground font-bold whitespace-nowrap">{slot.label}</span>
|
||||
</div>
|
||||
@@ -100,9 +121,9 @@ export function StudyTimeOptimizationCard() {
|
||||
{/* Efficiency breakdown */}
|
||||
<div className="grid grid-cols-3 gap-2 text-center">
|
||||
{[
|
||||
{ label: "Morning", time: "6–9 AM", eff: 60, color: "text-muted-foreground" },
|
||||
{ label: "Peak", time: "2–5 PM", eff: 95, color: "text-yellow-400" },
|
||||
{ label: "Evening", time: "7–9 PM", eff: 73, color: "text-[#00F0FF]" },
|
||||
{ label: "Morning", time: "6–9 AM", eff: isDemo ? 60 : 0, color: "text-muted-foreground" },
|
||||
{ label: "Peak", time: "2–5 PM", eff: isDemo ? 95 : 0, color: isDemo ? "text-yellow-400" : "text-muted-foreground" },
|
||||
{ label: "Evening", time: "7–9 PM", eff: isDemo ? 73 : 0, color: isDemo ? "text-[#00F0FF]" : "text-muted-foreground" },
|
||||
].map(slot => (
|
||||
<div key={slot.label} className="p-2.5 rounded-xl bg-white/5 border border-border/30">
|
||||
<div className={`text-base font-black ${slot.color}`}>{slot.eff}%</div>
|
||||
@@ -114,10 +135,13 @@ export function StudyTimeOptimizationCard() {
|
||||
|
||||
<div className="px-3.5 py-3 bg-yellow-500/10 border border-yellow-500/20 rounded-xl text-xs md:text-sm text-yellow-500 font-bold flex items-center gap-2">
|
||||
<Flame size={18} className="fill-yellow-500 shrink-0" />
|
||||
You perform best 2–5 PM. Schedule your hardest mock tests and DPPs in this window.
|
||||
{isDemo
|
||||
? "You perform best 2–5 PM. Schedule your hardest mock tests and DPPs in this window."
|
||||
: "Circadian AI calibrating: Complete practice sessions at different times of day to reveal your peak study window."}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,24 +3,33 @@
|
||||
import { TrendingUp, TrendingDown, Brain, Zap, Target, Clock } from "lucide-react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { useState } from "react"
|
||||
import { useActiveUser, isDemoUser } from "@/hooks/useActiveUser"
|
||||
|
||||
const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
||||
const QUESTIONS = [42, 58, 35, 71, 65, 48, 60]
|
||||
const ACCURACY = [82, 78, 91, 88, 85, 79, 93]
|
||||
const HOURS = [2.5, 3.5, 2, 4.5, 4, 3, 3.8]
|
||||
const DEMO_QUESTIONS = [42, 58, 35, 71, 65, 48, 60]
|
||||
const DEMO_ACCURACY = [82, 78, 91, 88, 85, 79, 93]
|
||||
const DEMO_HOURS = [2.5, 3.5, 2, 4.5, 4, 3, 3.8]
|
||||
|
||||
const ZERO_DATA = [0, 0, 0, 0, 0, 0, 0]
|
||||
|
||||
export function WeeklyProgressReport() {
|
||||
const { userId } = useActiveUser()
|
||||
const isDemo = isDemoUser(userId)
|
||||
const [metric, setMetric] = useState<"questions" | "accuracy" | "hours">("accuracy")
|
||||
const [hovered, setHovered] = useState<number | null>(null)
|
||||
|
||||
const QUESTIONS = isDemo ? DEMO_QUESTIONS : ZERO_DATA
|
||||
const ACCURACY = isDemo ? DEMO_ACCURACY : ZERO_DATA
|
||||
const HOURS = isDemo ? DEMO_HOURS : ZERO_DATA
|
||||
|
||||
const DATA = metric === "questions" ? QUESTIONS : metric === "accuracy" ? ACCURACY : HOURS
|
||||
const maxVal = Math.max(...DATA)
|
||||
const maxVal = Math.max(1, ...DATA)
|
||||
const avgVal = Math.round((DATA.reduce((a, b) => a + b, 0) / DATA.length) * 10) / 10
|
||||
const trend = DATA[DATA.length - 1] - DATA[0]
|
||||
const trend = isDemo ? DATA[DATA.length - 1] - DATA[0] : 0
|
||||
const trendUp = trend >= 0
|
||||
|
||||
const UNIT = metric === "accuracy" ? "%" : metric === "hours" ? "h" : ""
|
||||
const avgAccuracy = Math.round(ACCURACY.reduce((a, b) => a + b, 0) / ACCURACY.length)
|
||||
const avgAccuracy = isDemo ? Math.round(ACCURACY.reduce((a, b) => a + b, 0) / ACCURACY.length) : 0
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
@@ -38,7 +47,7 @@ export function WeeklyProgressReport() {
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 text-xs font-bold px-2.5 py-1 rounded-full bg-yellow-500/10 text-yellow-500 border border-yellow-500/20">
|
||||
<Zap size={12} />
|
||||
7/7 days active
|
||||
{isDemo ? "7/7 days active" : "Day 1 Active"}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -89,13 +98,15 @@ export function WeeklyProgressReport() {
|
||||
)}
|
||||
<div
|
||||
className={`w-full rounded-t-md transition-all duration-700 ${
|
||||
i === 6
|
||||
!isDemo
|
||||
? "bg-white/5 border border-white/10"
|
||||
: i === 6
|
||||
? "bg-gradient-to-t from-[#00F0FF]/60 to-[#00F0FF]"
|
||||
: hovered === i
|
||||
? "bg-gradient-to-t from-[#00F0FF]/40 to-[#00F0FF]/80"
|
||||
: "bg-gradient-to-t from-white/10 to-white/25"
|
||||
}`}
|
||||
style={{ height: `${(DATA[i] / maxVal) * 100}%` }}
|
||||
style={{ height: isDemo ? `${(DATA[i] / maxVal) * 100}%` : "4px" }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
@@ -116,8 +127,8 @@ export function WeeklyProgressReport() {
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{[
|
||||
{ label: "Avg Accuracy", value: `${avgAccuracy}%`, icon: <Target size={12} className="text-green-400" /> },
|
||||
{ label: "Questions / Day", value: `${Math.round(QUESTIONS.reduce((a, b) => a + b, 0) / 7)}`, icon: <Zap size={12} className="text-[#00F0FF]" /> },
|
||||
{ label: "Study Hrs / Day", value: `${(HOURS.reduce((a, b) => a + b, 0) / 7).toFixed(1)}h`, icon: <Clock size={12} className="text-amber-400" /> },
|
||||
{ label: "Questions / Day", value: isDemo ? `${Math.round(QUESTIONS.reduce((a, b) => a + b, 0) / 7)}` : "0", icon: <Zap size={12} className="text-[#00F0FF]" /> },
|
||||
{ label: "Study Hrs / Day", value: isDemo ? `${(HOURS.reduce((a, b) => a + b, 0) / 7).toFixed(1)}h` : "0.0h", icon: <Clock size={12} className="text-amber-400" /> },
|
||||
].map(stat => (
|
||||
<div key={stat.label} className="p-2.5 rounded-xl bg-white/5 border border-border/30 text-center">
|
||||
<div className="flex items-center justify-center gap-1 mb-0.5">{stat.icon}</div>
|
||||
@@ -131,8 +142,16 @@ export function WeeklyProgressReport() {
|
||||
<div className="px-3 py-2.5 bg-[#00F0FF]/10 border border-[#00F0FF]/20 rounded-xl text-xs text-[#00F0FF] font-medium flex items-start gap-2.5">
|
||||
<Brain size={15} className="shrink-0 mt-0.5" />
|
||||
<div>
|
||||
{isDemo ? (
|
||||
<>
|
||||
<span className="font-black text-white">Sign errors in Thermodynamics</span> — detected in your last 3 attempts (W = −PΔV misapplied). We've queued 3 targeted practice questions for tomorrow.{" "}
|
||||
<span className="text-[#00F0FF]/80"><strong className="text-white">Tip:</strong> You peak at 7–9 PM. Schedule the hardest topics then.</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="font-black text-white">AI Progress Engine Calibrating</span> — Complete your first CBT mock test or DPP to begin mapping subject retention, speed trends, and error velocity.
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -140,3 +159,4 @@ export function WeeklyProgressReport() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ export const DEMO_USERS = {
|
||||
export const DEMO_IDS = new Set(Object.values(DEMO_USERS).map(u => u.id));
|
||||
|
||||
/** Returns true if the userId matches a known demo account */
|
||||
export function isDemoUser(userId: string): boolean {
|
||||
return DEMO_IDS.has(userId);
|
||||
export function isDemoUser(userId?: string | null): boolean {
|
||||
return !!userId && DEMO_IDS.has(userId);
|
||||
}
|
||||
|
||||
/** Compute trial info from user profile and global settings */
|
||||
|
||||
Reference in New Issue
Block a user