feat: setup admin JSON generation and pdf viewer for free plan DPPs
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { GraduationCap, MapPin, RefreshCw } from "lucide-react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { RefreshCw } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
|
||||
export function CollegePredictorCard() {
|
||||
const [data, setData] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [activeTab, setActiveTab] = useState("Target")
|
||||
const { userId, loading: userLoading } = useActiveUser();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -54,34 +56,65 @@ export function CollegePredictorCard() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="border-border shadow-sm h-full flex flex-col bg-gradient-to-br from-background to-secondary/30 relative overflow-hidden">
|
||||
<div className="absolute -right-12 -top-12 opacity-5 pointer-events-none">
|
||||
<GraduationCap size={150} />
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex justify-between items-center pb-3">
|
||||
<h2 className="text-base font-bold text-primary flex items-center gap-2">
|
||||
🎓 College Predictor
|
||||
</h2>
|
||||
<Link href="/dashboard/ai-coach/college-predictor" className="text-xs text-[#00F0FF] font-semibold hover:underline transition-colors">Expand View ↗</Link>
|
||||
</div>
|
||||
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-xl flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<MapPin className="text-primary" size={24} />
|
||||
Admissions Radar
|
||||
|
||||
<Card className="border-border bg-card shadow-[0_4px_24px_rgba(0,0,0,0.3)] flex-1 overflow-hidden group hover:border-[#00F0FF]/30 transition-colors">
|
||||
<CardContent className="p-4 flex flex-col h-full">
|
||||
<div className="flex gap-1.5 mb-3.5">
|
||||
{["Reach", "Target", "Safety"].map(tab => (
|
||||
<div
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`flex-1 p-2 text-center text-[11px] font-semibold rounded-md cursor-pointer transition-colors border ${
|
||||
activeTab === tab
|
||||
? "bg-[#00F0FF]/10 border-[#00F0FF]/20 text-[#00F0FF]"
|
||||
: "bg-white/5 border-transparent text-muted-foreground hover:bg-white/10"
|
||||
}`}
|
||||
>
|
||||
{tab}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<span className={`px-3 py-1 rounded-full text-[10px] font-black uppercase tracking-widest border ${getCategoryColor(category)}`}>
|
||||
{category}
|
||||
</span>
|
||||
</CardTitle>
|
||||
<CardDescription>AI-projected admission trajectory.</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex-1 flex flex-col justify-center">
|
||||
<div className="p-5 bg-background rounded-xl border border-border shadow-sm mt-4">
|
||||
<p className="font-semibold text-lg leading-snug">
|
||||
"{prediction}"
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-4 font-medium flex items-center gap-1 opacity-70">
|
||||
<RefreshCw size={10} /> Prediction updates dynamically after every test.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex flex-col gap-2 flex-1">
|
||||
<div className="flex items-center gap-3 p-2.5 rounded-md bg-white/5 border border-border hover:bg-white/10 transition-colors">
|
||||
<div className="w-7 h-7 rounded-full flex items-center justify-center text-[11px] font-bold shrink-0 bg-[#00F0FF]/10 text-[#00F0FF]">
|
||||
#1
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-[13px] font-bold text-primary truncate">NIT Trichy</div>
|
||||
<div className="text-[11px] text-muted-foreground truncate">Computer Science</div>
|
||||
</div>
|
||||
<div className="text-[12px] font-bold px-2 py-0.5 rounded-md shrink-0 text-green-500 bg-green-500/10">
|
||||
85%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 p-2.5 rounded-md bg-white/5 border border-border hover:bg-white/10 transition-colors">
|
||||
<div className="w-7 h-7 rounded-full flex items-center justify-center text-[11px] font-bold shrink-0 bg-[#00F0FF]/10 text-[#00F0FF]">
|
||||
#2
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-[13px] font-bold text-primary truncate">NIT Surathkal</div>
|
||||
<div className="text-[11px] text-muted-foreground truncate">Information Tech</div>
|
||||
</div>
|
||||
<div className="text-[12px] font-bold px-2 py-0.5 rounded-md shrink-0 text-yellow-500 bg-yellow-500/10">
|
||||
72%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-2.5 bg-yellow-500/10 border border-yellow-500/20 rounded-md text-xs text-yellow-500 font-medium text-center">
|
||||
Just 15 more marks in Maths can upgrade you to IIT Roorkee!
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
87
src/components/AI/DailyChallengeBanner.tsx
Normal file
87
src/components/AI/DailyChallengeBanner.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import { Flame, ArrowRight, Timer, BookOpen } from "lucide-react"
|
||||
|
||||
const FOCUS_TOPICS = [
|
||||
{ subject: "Physics", topic: "Rotational Dynamics", color: "from-blue-600/30 to-[#00F0FF]/20", accent: "#00F0FF", icon: "⚛️" },
|
||||
{ subject: "Chemistry", topic: "Organic Reactions — Mechanism", color: "from-amber-600/30 to-yellow-500/20", accent: "#F59E0B", icon: "🧪" },
|
||||
{ subject: "Biology", topic: "Human Nervous System", color: "from-green-600/30 to-emerald-400/20", accent: "#10B981", icon: "🧬" },
|
||||
{ subject: "Maths", topic: "Integral Calculus", color: "from-purple-600/30 to-violet-400/20", accent: "#A78BFA", icon: "∫" },
|
||||
]
|
||||
|
||||
export function DailyChallengeBanner() {
|
||||
const [todayFocus] = useState(() => FOCUS_TOPICS[new Date().getDay() % FOCUS_TOPICS.length])
|
||||
const [timeLeft, setTimeLeft] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
const tick = () => {
|
||||
const now = new Date()
|
||||
const end = new Date()
|
||||
end.setHours(23, 59, 59, 0)
|
||||
const diff = Math.max(0, end.getTime() - now.getTime())
|
||||
const h = Math.floor(diff / 3600000)
|
||||
const m = Math.floor((diff % 3600000) / 60000)
|
||||
const s = Math.floor((diff % 60000) / 1000)
|
||||
setTimeLeft(`${h}h ${m}m ${s}s`)
|
||||
}
|
||||
tick()
|
||||
const id = setInterval(tick, 1000)
|
||||
return () => clearInterval(id)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative overflow-hidden rounded-2xl border border-white/10 bg-gradient-to-r ${todayFocus.color} p-5 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 shadow-[0_4px_30px_rgba(0,0,0,0.3)]`}
|
||||
>
|
||||
<div
|
||||
className="absolute -right-10 -top-10 w-48 h-48 rounded-full blur-3xl opacity-20 pointer-events-none"
|
||||
style={{ background: todayFocus.accent }}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-4 relative z-10">
|
||||
<div
|
||||
className="w-12 h-12 rounded-xl flex items-center justify-center text-2xl font-bold border border-white/10 shrink-0"
|
||||
style={{ background: `${todayFocus.accent}20` }}
|
||||
>
|
||||
{todayFocus.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="text-[10px] font-black uppercase tracking-widest text-white/50">
|
||||
AI-Recommended Focus · Today
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-bold px-2 py-0.5 rounded-full bg-white/10 text-white/70">
|
||||
<Flame size={9} />
|
||||
DAILY CHALLENGE
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-lg font-extrabold text-white tracking-tight leading-tight">
|
||||
{todayFocus.subject}:{" "}
|
||||
<span style={{ color: todayFocus.accent }}>{todayFocus.topic}</span>
|
||||
</h3>
|
||||
<p className="text-xs text-white/50 mt-0.5 flex items-center gap-1.5">
|
||||
<BookOpen size={11} />
|
||||
Mastering this will boost your rank by an estimated{" "}
|
||||
<span className="font-bold text-white/70">+120 positions</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 relative z-10 shrink-0">
|
||||
<div className="text-right hidden sm:block">
|
||||
<div className="text-[10px] text-white/40 font-semibold uppercase flex items-center gap-1 justify-end">
|
||||
<Timer size={10} /> Expires In
|
||||
</div>
|
||||
<div className="text-sm font-black text-white/80 font-mono tracking-wider">{timeLeft}</div>
|
||||
</div>
|
||||
<button
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-xl font-bold text-sm text-background transition-all hover:scale-105 active:scale-95"
|
||||
style={{ background: todayFocus.accent }}
|
||||
>
|
||||
Start Session <ArrowRight size={15} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
48
src/components/AI/ExamStrategyCard.tsx
Normal file
48
src/components/AI/ExamStrategyCard.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Printer } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
export function ExamStrategyCard() {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex justify-between items-center pb-3">
|
||||
<h2 className="text-base font-bold text-primary flex items-center gap-2">
|
||||
🧠 Exam Day Strategy
|
||||
</h2>
|
||||
<Link href="/dashboard/ai-coach/strategy" className="text-xs text-[#00F0FF] font-semibold hover:underline transition-colors">Expand View ↗</Link>
|
||||
</div>
|
||||
|
||||
<Card className="border-border bg-card shadow-[0_4px_24px_rgba(0,0,0,0.3)] flex-1 overflow-hidden group hover:border-[#00F0FF]/30 transition-colors">
|
||||
<CardContent className="p-4 flex flex-col h-full justify-between">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="p-3 rounded-md bg-white/5 border border-border text-center">
|
||||
<div className="text-xl mb-1">🧪</div>
|
||||
<div className="text-[11px] font-bold text-primary">Chemistry First</div>
|
||||
<div className="text-[10px] text-muted-foreground mt-0.5">Finish in 40 mins</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-md bg-white/5 border border-border text-center">
|
||||
<div className="text-xl mb-1">🧲</div>
|
||||
<div className="text-[11px] font-bold text-primary">Physics Next</div>
|
||||
<div className="text-[10px] text-muted-foreground mt-0.5">Target 55 mins</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-md bg-white/5 border border-border text-center">
|
||||
<div className="text-xl mb-1">📐</div>
|
||||
<div className="text-[11px] font-bold text-primary">Maths Last</div>
|
||||
<div className="text-[10px] text-muted-foreground mt-0.5">Reserve 75 mins</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-md bg-white/5 border border-border text-center">
|
||||
<div className="text-xl mb-1">⏱️</div>
|
||||
<div className="text-[11px] font-bold text-primary">Buffer Time</div>
|
||||
<div className="text-[10px] text-muted-foreground mt-0.5">10 mins for review</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex items-center justify-center gap-1.5 p-2.5 rounded-md bg-[#00F0FF]/10 border border-dashed border-[#00F0FF]/20 text-xs text-[#00F0FF] font-bold cursor-pointer hover:bg-[#00F0FF]/20 transition-colors">
|
||||
<Printer size={16} className="stroke-[#00F0FF]" />
|
||||
Print Exam Day Blueprint
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
33
src/components/AI/FeatureIndex.tsx
Normal file
33
src/components/AI/FeatureIndex.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import Link from "next/link"
|
||||
|
||||
const features = [
|
||||
{ name: "Mistake Tracking", href: "/dashboard/ai-coach/mistakes" },
|
||||
{ name: "Spaced Repetition", href: "/dashboard/ai-coach/mistakes" },
|
||||
{ name: "Study Plan", href: "/dashboard/ai-coach/study-plan" },
|
||||
{ name: "AI Tutor", href: "/dashboard/ai-coach/tutor" },
|
||||
{ name: "Readiness Score", href: "/dashboard/ai-coach/readiness" },
|
||||
{ name: "Exam Strategy", href: "/dashboard/ai-coach/strategy" },
|
||||
{ name: "College Predictor", href: "/dashboard/ai-coach/college-predictor" },
|
||||
{ name: "Time Optimization", href: "/dashboard/ai-coach/strategy" },
|
||||
]
|
||||
|
||||
export function FeatureIndex() {
|
||||
return (
|
||||
<div className="py-2 mb-4">
|
||||
<div className="flex flex-wrap gap-2.5">
|
||||
{features.map((f) => (
|
||||
<Link
|
||||
key={f.name}
|
||||
href={f.href}
|
||||
className="inline-flex items-center gap-2 px-3.5 py-1.5 rounded-full text-xs font-extrabold bg-[#00F0FF]/10 border border-[#00F0FF]/30 text-[#00F0FF] hover:bg-[#00F0FF] hover:text-black transition-all shadow-[0_0_12px_rgba(0,240,255,0.1)] group"
|
||||
>
|
||||
<span className="size-2 rounded-full bg-[#00F0FF] group-hover:bg-black transition-colors" />
|
||||
<span>{f.name}</span>
|
||||
<span className="text-[10px] opacity-75">→</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
|
||||
import { useState } from "react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { CheckCircle2, XCircle, BrainCircuit } from "lucide-react"
|
||||
import { Flame } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkMath from 'remark-math'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
@@ -69,101 +68,132 @@ export function MistakeReviewCard({ data, onNext, onSubmitReview }: MistakeRevie
|
||||
const { mistake, question } = data;
|
||||
|
||||
return (
|
||||
<Card className="border-border shadow-sm overflow-hidden flex flex-col">
|
||||
<div className="p-4 border-b border-border bg-secondary/20 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge variant="outline" className="text-muted-foreground border-primary/20 bg-primary/5 text-primary">
|
||||
Attempt {mistake.review_count + 1}
|
||||
</Badge>
|
||||
<span className="text-xs font-bold text-muted-foreground uppercase tracking-wider">{mistake.topic} • {mistake.subtopic}</span>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex justify-between items-center pb-1">
|
||||
<h2 className="text-base font-bold text-primary flex items-center gap-2">
|
||||
♻️ Spaced Repetition Queue
|
||||
</h2>
|
||||
<Link href="/dashboard/ai-coach/mistakes" className="text-xs text-[#00F0FF] font-semibold hover:underline transition-colors">Expand View ↗</Link>
|
||||
</div>
|
||||
|
||||
{/* Spaced Repetition Timeline */}
|
||||
<div className="flex justify-between items-center relative py-4 px-2">
|
||||
<div className="absolute top-1/2 left-0 right-0 h-0.5 bg-secondary -translate-y-1/2 z-0" />
|
||||
<div className="absolute top-1/2 left-0 w-1/2 h-0.5 bg-[#00F0FF] -translate-y-1/2 z-0" />
|
||||
|
||||
<div className="relative z-10 flex flex-col items-center gap-1.5 opacity-60">
|
||||
<div className="w-3.5 h-3.5 rounded-full bg-[#00F0FF] border-2 border-background" />
|
||||
<span className="text-[10px] font-bold text-[#00F0FF]">Attempt 1</span>
|
||||
<span className="text-[9px] text-muted-foreground font-semibold">10 Jul</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs font-medium text-muted-foreground">
|
||||
<BrainCircuit size={14} className={mistake.consecutive_correct > 0 ? "text-amber-500" : ""} />
|
||||
Consecutive Correct: <span className="font-bold text-foreground">{mistake.consecutive_correct}/3</span> before mastery
|
||||
|
||||
<div className="relative z-10 flex flex-col items-center gap-1.5 opacity-60">
|
||||
<div className="w-3.5 h-3.5 rounded-full bg-[#00F0FF] border-2 border-background" />
|
||||
<span className="text-[10px] font-bold text-[#00F0FF]">Attempt 2</span>
|
||||
<span className="text-[9px] text-muted-foreground font-semibold">14 Jul</span>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col items-center gap-1.5 scale-110">
|
||||
<div className="w-4 h-4 rounded-full border-[3px] border-[#00F0FF] bg-background shadow-[0_0_10px_rgba(0,240,255,0.5)]" />
|
||||
<span className="text-[11px] font-bold text-[#00F0FF]">Today</span>
|
||||
<span className="text-[9px] text-muted-foreground font-semibold">19 Jul</span>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 flex flex-col items-center gap-1.5 opacity-50">
|
||||
<div className="w-3.5 h-3.5 rounded-full bg-secondary border-2 border-background" />
|
||||
<span className="text-[10px] font-bold text-muted-foreground">Mastery</span>
|
||||
<span className="text-[9px] text-muted-foreground font-semibold">26 Jul</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent className="p-6 flex-1 flex flex-col">
|
||||
<div className="prose prose-neutral dark:prose-invert max-w-none text-base md:text-lg mb-8 font-medium">
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
|
||||
{question.question_text}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 mt-auto">
|
||||
{Object.entries(question.options).map(([letter, text]) => {
|
||||
const isSelected = selected === letter
|
||||
|
||||
let btnClass = "border-border bg-background hover:border-primary/50 hover:bg-secondary/20"
|
||||
let iconClass = "border-border text-muted-foreground group-hover:border-primary/50 group-hover:text-primary"
|
||||
|
||||
if (result) {
|
||||
// Post-check styling
|
||||
if (letter === correctAnswer) {
|
||||
btnClass = "border-green-500 bg-green-500/10 shadow-[0_0_15px_-5px_#22c55e]"
|
||||
iconClass = "border-green-500 bg-green-500 text-white"
|
||||
} else if (isSelected && letter !== correctAnswer) {
|
||||
btnClass = "border-red-500 bg-red-500/10"
|
||||
iconClass = "border-red-500 bg-red-500 text-white"
|
||||
}
|
||||
} else if (isSelected) {
|
||||
btnClass = "border-primary bg-primary/5 shadow-[0_0_15px_-5px_var(--color-primary)]"
|
||||
iconClass = "border-primary bg-primary text-primary-foreground"
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={letter}
|
||||
onClick={() => !result && setSelected(letter)}
|
||||
disabled={!!result}
|
||||
className={`w-full text-left p-4 rounded-xl border transition-all duration-200 flex items-center gap-4 group ${btnClass}`}
|
||||
>
|
||||
<div className={`flex-shrink-0 size-8 rounded-lg border flex items-center justify-center font-medium transition-colors ${iconClass}`}>
|
||||
{letter}
|
||||
</div>
|
||||
<span className="text-base font-medium flex-1">
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>{text}</ReactMarkdown>
|
||||
</span>
|
||||
{result && letter === correctAnswer && <CheckCircle2 className="text-green-500 ml-2" size={20} />}
|
||||
{result && isSelected && letter !== correctAnswer && <XCircle className="text-red-500 ml-2" size={20} />}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex justify-end">
|
||||
{!result ? (
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={handleCheck}
|
||||
disabled={!selected || isSubmitting}
|
||||
className="rounded-xl px-8 font-bold"
|
||||
>
|
||||
Check Answer
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="lg"
|
||||
onClick={handleNext}
|
||||
className="rounded-xl px-8 font-bold bg-secondary text-foreground hover:bg-secondary/80 border border-border"
|
||||
>
|
||||
Continue to Next
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Solution Block */}
|
||||
{result && (
|
||||
<div className="mt-6 p-4 rounded-xl border border-primary/20 bg-primary/5 animate-in slide-in-from-top-2 fade-in">
|
||||
<h4 className="text-sm font-bold text-primary mb-2 flex items-center gap-2"><BrainCircuit size={16}/> AI Coach Explanation</h4>
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none text-muted-foreground">
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
|
||||
{question.solution_text}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
{/* Mistake Card */}
|
||||
<Card className="border-border bg-card shadow-[0_4px_24px_rgba(0,0,0,0.3)] relative overflow-hidden flex flex-col group hover:border-[#00F0FF]/30 transition-colors">
|
||||
<CardContent className="p-5 flex-1 flex flex-col">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<span className="text-[10px] font-bold px-2.5 py-1 rounded-md bg-white/5 border border-border text-muted-foreground uppercase tracking-widest">
|
||||
{mistake.topic}
|
||||
</span>
|
||||
<span className="text-[11px] font-bold text-red-500 bg-red-500/10 px-2 py-0.5 rounded-md">
|
||||
{mistake.error_type}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none text-base md:text-lg mb-6 font-medium text-foreground leading-relaxed">
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
|
||||
{question.question_text}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3 mt-auto">
|
||||
{Object.entries(question.options).map(([letter, text]) => {
|
||||
const isSelected = selected === letter
|
||||
|
||||
let btnClass = "border-border bg-white/5 hover:bg-white/10 hover:border-border text-muted-foreground"
|
||||
|
||||
if (result) {
|
||||
if (letter === correctAnswer) {
|
||||
btnClass = "border-green-500 bg-green-500/10 text-green-500 shadow-[0_0_15px_rgba(34,197,94,0.15)]"
|
||||
} else if (isSelected && letter !== correctAnswer) {
|
||||
btnClass = "border-red-500 bg-red-500/10 text-red-500"
|
||||
}
|
||||
} else if (isSelected) {
|
||||
btnClass = "border-[#00F0FF] bg-[#00F0FF]/10 text-[#00F0FF] shadow-[0_0_15px_rgba(0,240,255,0.15)]"
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={letter}
|
||||
onClick={() => !result && setSelected(letter)}
|
||||
disabled={!!result || isSubmitting}
|
||||
className={`text-left p-3.5 rounded-lg border-[1.5px] transition-all font-medium flex gap-3 ${btnClass}`}
|
||||
>
|
||||
<span className="font-bold opacity-70">{letter})</span>
|
||||
<span className="flex-1">
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>{text}</ReactMarkdown>
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
{!result ? (
|
||||
<div className="mt-5 flex justify-end">
|
||||
<button
|
||||
onClick={handleCheck}
|
||||
disabled={!selected || isSubmitting}
|
||||
className="px-6 py-2.5 rounded-md font-bold text-sm bg-gradient-to-r from-[#00F0FF] to-teal-400 text-background hover:opacity-90 transition-opacity disabled:opacity-50"
|
||||
>
|
||||
{isSubmitting ? "Checking..." : "Submit Answer"}
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Solution Block */}
|
||||
<div className="mt-5 p-4 rounded-xl border border-[#00F0FF]/20 bg-[#00F0FF]/5">
|
||||
<h4 className="text-[13px] font-bold text-[#00F0FF] mb-2 flex items-center gap-2">
|
||||
<Flame size={16} className="fill-[#00F0FF]/20" />
|
||||
AI Coach Explanation
|
||||
</h4>
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none text-[13px] text-muted-foreground leading-relaxed [&>p]:m-0">
|
||||
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
|
||||
{question.solution_text}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 flex justify-end">
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className="px-6 py-2.5 rounded-md font-bold text-sm bg-gradient-to-r from-[#00F0FF] to-teal-400 text-background hover:opacity-90 transition-opacity"
|
||||
>
|
||||
Got It, Next Question →
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { Activity, RefreshCw } from "lucide-react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { TrendingUp, Flame, RefreshCw } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
|
||||
@@ -59,65 +60,108 @@ export function ReadinessGauge() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="border-border shadow-sm h-full flex flex-col">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-xl flex items-center gap-2">
|
||||
<Activity className="text-primary" size={24} />
|
||||
Exam Readiness
|
||||
</CardTitle>
|
||||
<CardDescription>Real-time AI composite score.</CardDescription>
|
||||
</CardHeader>
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex justify-between items-center pb-3">
|
||||
<h2 className="text-base font-bold text-primary flex items-center gap-2">
|
||||
🎯 Exam Readiness
|
||||
</h2>
|
||||
<Link href="/dashboard/ai-coach/readiness" className="text-xs text-[#00F0FF] font-semibold hover:underline transition-colors">Expand View ↗</Link>
|
||||
</div>
|
||||
|
||||
<CardContent className="flex flex-col items-center flex-1 justify-center p-6 space-y-6">
|
||||
<Card className="border-border bg-card shadow-[0_4px_24px_rgba(0,0,0,0.3)] relative overflow-hidden flex-1 flex flex-col justify-between p-4 group hover:border-[#00F0FF]/30 transition-colors">
|
||||
{/* Background glow decoration */}
|
||||
<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" />
|
||||
|
||||
{/* SVG Circular Gauge */}
|
||||
<div className="relative w-32 h-32 flex items-center justify-center">
|
||||
<svg className="w-full h-full transform -rotate-90" viewBox="0 0 100 100">
|
||||
{/* Background Ring */}
|
||||
<circle cx="50" cy="50" r="45" stroke="currentColor" strokeWidth="8" fill="transparent" className="text-secondary" />
|
||||
{/* Progress Ring */}
|
||||
<circle
|
||||
cx="50" cy="50" r="45"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
fill="transparent"
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
strokeLinecap="round"
|
||||
className={`transition-all duration-1000 ease-out ${getColor(score)}`}
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute flex flex-col items-center justify-center">
|
||||
<span className="text-3xl font-black">{score}</span>
|
||||
<span className="text-[10px] text-muted-foreground uppercase tracking-wider font-bold">/ 100</span>
|
||||
<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">
|
||||
<TrendingUp size={14} className="stroke-[2.5px]" />
|
||||
+5 this week
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col md:flex-row items-center gap-6 mb-4 relative z-10">
|
||||
{/* Circular Score */}
|
||||
<div className="relative w-[110px] h-[110px] shrink-0">
|
||||
<svg className="w-full h-full transform -rotate-90" viewBox="0 0 100 100">
|
||||
<defs>
|
||||
<linearGradient id="scoreGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
||||
<stop offset="0%" stopColor="#00F0FF" />
|
||||
<stop offset="100%" stopColor="#3B82F6" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="50" cy="50" r="45" stroke="currentColor" strokeWidth="6" fill="transparent" className="text-[#00F0FF]/10" />
|
||||
<circle
|
||||
cx="50" cy="50" r="45"
|
||||
stroke="url(#scoreGradient)"
|
||||
strokeWidth="6"
|
||||
fill="transparent"
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={strokeDashoffset}
|
||||
strokeLinecap="round"
|
||||
className="transition-all duration-1500 ease-out"
|
||||
/>
|
||||
</svg>
|
||||
<div className="absolute inset-0 flex flex-col items-center justify-center text-center mt-1">
|
||||
<span className="text-[32px] font-extrabold leading-none">{score}</span>
|
||||
<span className="text-[9px] text-muted-foreground font-semibold mt-0.5">SCORE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick metric grid beside ring */}
|
||||
<div className="flex-1 grid grid-cols-2 gap-2 w-full">
|
||||
<div className="bg-white/5 rounded-md p-2">
|
||||
<div className="text-sm font-bold flex items-baseline gap-1">88% <span className="text-[10px] text-green-500">↑</span></div>
|
||||
<div className="text-[10px] font-medium text-muted-foreground">Accuracy</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md p-2">
|
||||
<div className="text-sm font-bold flex items-baseline gap-1">45s <span className="text-[10px] text-red-500">↓</span></div>
|
||||
<div className="text-[10px] font-medium text-muted-foreground">Speed/Q</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md p-2">
|
||||
<div className="text-sm font-bold flex items-baseline gap-1">High <span className="text-[10px] text-green-500">↑</span></div>
|
||||
<div className="text-[10px] font-medium text-muted-foreground">Consistency</div>
|
||||
</div>
|
||||
<div className="bg-white/5 rounded-md p-2">
|
||||
<div className="text-sm font-bold flex items-baseline gap-1">64% <span className="text-[10px] text-green-500">↑</span></div>
|
||||
<div className="text-[10px] font-medium text-muted-foreground">Syllabus</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Breakdown Bars */}
|
||||
<div className="w-full space-y-3">
|
||||
{/* Subject Bars */}
|
||||
<div className="space-y-1.5 mb-3 relative z-10">
|
||||
{[
|
||||
{ label: "Accuracy", value: breakdown.accuracy, max: 40 },
|
||||
{ label: "Speed", value: breakdown.speed, max: 20 },
|
||||
{ label: "Coverage", value: breakdown.coverage, max: 20 },
|
||||
{ label: "Consistency", value: breakdown.consistency, max: 10 },
|
||||
{ label: "Recovery", value: breakdown.recovery, max: 10 },
|
||||
].map((item, idx) => (
|
||||
<div key={idx} className="space-y-1">
|
||||
<div className="flex justify-between text-xs font-semibold">
|
||||
<span className="text-muted-foreground">{item.label}</span>
|
||||
<span>{item.value} <span className="opacity-50">/ {item.max}</span></span>
|
||||
</div>
|
||||
<div className="h-2 w-full bg-secondary rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-primary transition-all duration-1000 ease-out"
|
||||
style={{ width: `${(item.value / item.max) * 100}%` }}
|
||||
{ name: "Physics", val: 68, color: "#00F0FF" },
|
||||
{ name: "Chemistry", val: 74, color: "#F59E0B" },
|
||||
{ name: "Biology", val: 82, color: "#10B981" },
|
||||
].map(sub => (
|
||||
<div key={sub.name} className="flex items-center gap-2">
|
||||
<span className="text-[9px] font-semibold text-muted-foreground w-14 shrink-0">{sub.name}</span>
|
||||
<div className="flex-1 h-1.5 rounded-full bg-white/10 overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-1000"
|
||||
style={{ width: `${sub.val}%`, background: sub.color }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[9px] font-bold w-7 text-right" style={{ color: sub.color }}>{sub.val}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* National Avg */}
|
||||
<div className="flex items-center justify-between px-2 py-1.5 bg-white/5 rounded-md mb-3 relative z-10">
|
||||
<span className="text-[10px] text-muted-foreground font-semibold">National Avg</span>
|
||||
<span className="text-[10px] font-bold text-muted-foreground">58 / 100</span>
|
||||
<span className="text-[10px] font-bold text-green-500">You: +{score - 58} above avg</span>
|
||||
</div>
|
||||
|
||||
<div className="mt-auto px-3 py-2.5 bg-[#00F0FF]/10 border border-[#00F0FF]/20 rounded-md text-xs text-[#00F0FF] font-semibold flex items-center gap-2 relative z-10">
|
||||
<Flame size={16} className="fill-[#00F0FF]/20 shrink-0" />
|
||||
Your speed has dropped slightly. Focus on time-bound practice today.
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { CalendarClock, CheckCircle2, Circle, Clock, Loader2, PlayCircle, RefreshCw, XCircle } from "lucide-react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Check, RefreshCw } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
|
||||
import { useActiveUser } from "@/hooks/useActiveUser"
|
||||
|
||||
@@ -30,9 +30,11 @@ export function StudyPlanCard() {
|
||||
}
|
||||
// Mock fallback if celery hasn't seeded yet
|
||||
setTasks([
|
||||
{ type: "Revision", description: "Revise Thermodynamics", duration_minutes: 15, status: "Pending" },
|
||||
{ type: "Mistake_Review", description: "Daily Mistake Review Session", duration_minutes: 10, status: "Pending" },
|
||||
{ type: "DPS", description: "Attempt Thermodynamics Practice Sheet", duration_minutes: 20, status: "Pending" }
|
||||
{ type: "DPS", description: "DPS — Electrostatics + Kinematics", duration_minutes: 20, status: "Done" },
|
||||
{ type: "Mistake_Review", description: "Mistake Review — Thermodynamics", duration_minutes: 10, status: "Done" },
|
||||
{ type: "Micro_test", description: "Micro-test — GOC (General Organic Chemistry)", duration_minutes: 5, status: "Pending" },
|
||||
{ type: "Test", description: "Weekly Test — Full Syllabus", duration_minutes: 180, status: "Pending" },
|
||||
{ type: "Revision", description: "Revise — Circles + Straight Lines", duration_minutes: 15, status: "Pending" }
|
||||
])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -49,91 +51,112 @@ export function StudyPlanCard() {
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="border-border shadow-sm h-full flex flex-col">
|
||||
<CardHeader className="pb-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-xl flex items-center gap-2">
|
||||
<CalendarClock className="text-primary" size={24} />
|
||||
Today's Plan
|
||||
</CardTitle>
|
||||
<Badge variant="outline" className="bg-primary/5 text-primary border-primary/20">
|
||||
{tasks.reduce((acc, curr) => acc + curr.duration_minutes, 0)} min total
|
||||
</Badge>
|
||||
</div>
|
||||
<CardDescription>AI-generated curriculum targeting your weak spots.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex-1 flex flex-col">
|
||||
{loading ? (
|
||||
<div className="flex-1 flex items-center justify-center p-8">
|
||||
<RefreshCw className="animate-spin text-muted-foreground mr-3" /> Loading schedule...
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{tasks.map((task, idx) => {
|
||||
const isDone = task.status === "Done"
|
||||
const isSkipped = task.status === "Skipped"
|
||||
const isPending = task.status === "Pending"
|
||||
const completedTasks = tasks.filter(t => t.status === "Done").length
|
||||
const totalTasks = tasks.length
|
||||
const progressPercent = totalTasks === 0 ? 0 : Math.round((completedTasks / totalTasks) * 100)
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
className={`flex items-center p-4 rounded-xl border transition-all duration-300 ${
|
||||
isDone ? "bg-green-500/5 border-green-500/20" :
|
||||
isSkipped ? "bg-secondary/50 border-transparent opacity-60" :
|
||||
"bg-background border-border hover:border-primary/50"
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col flex-1 mr-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Badge variant="secondary" className="text-[10px] uppercase tracking-wider">{task.type}</Badge>
|
||||
<span className="text-xs font-semibold text-muted-foreground flex items-center gap-1">
|
||||
<Clock size={12}/> {task.duration_minutes} min
|
||||
</span>
|
||||
</div>
|
||||
<p className={`font-semibold text-sm transition-all duration-300 ${
|
||||
isDone || isSkipped ? "line-through text-muted-foreground" : "text-foreground"
|
||||
}`}>
|
||||
{task.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex items-center gap-2">
|
||||
{isPending && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => toggleTask(idx, "Skipped")}
|
||||
className="p-2 rounded-full hover:bg-secondary text-muted-foreground hover:text-foreground transition-colors"
|
||||
title="Skip Task"
|
||||
>
|
||||
<XCircle size={20} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => toggleTask(idx, "Done")}
|
||||
className="p-2 rounded-full hover:bg-green-500/10 text-muted-foreground hover:text-green-500 transition-colors"
|
||||
title="Complete Task"
|
||||
>
|
||||
<Circle size={20} />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{(isDone || isSkipped) && (
|
||||
<button
|
||||
onClick={() => toggleTask(idx, "Pending")}
|
||||
className={`flex items-center gap-1 text-xs font-bold px-3 py-1.5 rounded-full ${isDone ? "bg-green-500 text-white" : "bg-secondary text-muted-foreground"}`}
|
||||
>
|
||||
{isDone ? <CheckCircle2 size={14}/> : <PlayCircle size={14}/>}
|
||||
{isDone ? "Completed" : "Resume"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
const getTagStyle = (type: string) => {
|
||||
switch(type) {
|
||||
case "Revision": return "bg-yellow-500/10 text-yellow-500"
|
||||
case "Mistake_Review": return "bg-red-500/10 text-red-500"
|
||||
case "DPS": return "bg-[#00F0FF]/10 text-[#00F0FF]"
|
||||
case "Micro_test": return "bg-green-500/10 text-green-500"
|
||||
case "Test": return "bg-purple-500/10 text-purple-500"
|
||||
default: return "bg-secondary text-muted-foreground"
|
||||
}
|
||||
}
|
||||
|
||||
const getDisplayType = (type: string) => {
|
||||
switch(type) {
|
||||
case "Revision": return "Revision"
|
||||
case "Mistake_Review": return "Review"
|
||||
case "DPS": return "Practice"
|
||||
case "Micro_test": return "Micro-test"
|
||||
case "Test": return "Test"
|
||||
default: return "Task"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex justify-between items-center pb-3">
|
||||
<h2 className="text-base font-bold text-primary flex items-center gap-2">
|
||||
📅 Today's Study Plan
|
||||
</h2>
|
||||
<Link href="/dashboard/ai-coach/study-plan" className="text-xs text-[#00F0FF] font-semibold hover:underline transition-colors">Expand View ↗</Link>
|
||||
</div>
|
||||
|
||||
<Card className="border-border bg-card shadow-[0_4px_24px_rgba(0,0,0,0.3)] flex-1 overflow-hidden group hover:border-[#00F0FF]/30 transition-colors">
|
||||
<div className="px-4 py-3.5 flex justify-between items-center border-b border-border">
|
||||
<div className="text-[13px] font-bold text-muted-foreground">
|
||||
{new Date().toLocaleDateString('en-US', { weekday: 'long', day: 'numeric', month: 'short' })}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="text-[12px] text-[#00F0FF] font-bold">{progressPercent}% Completed</div>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="h-[3px] bg-[#00F0FF]/10 w-full">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-[#00F0FF] to-teal-400 rounded-r-[2px] transition-all duration-500"
|
||||
style={{ width: `${progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CardContent className="p-0 flex-1 flex flex-col">
|
||||
{loading ? (
|
||||
<div className="flex-1 flex items-center justify-center p-8 min-h-[200px]">
|
||||
<RefreshCw className="animate-spin text-muted-foreground mr-3" /> Loading schedule...
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-1">
|
||||
{tasks.map((task, idx) => {
|
||||
const isDone = task.status === "Done"
|
||||
const isCurrent = idx === completedTasks && !isDone // First pending task is current
|
||||
|
||||
return (
|
||||
<div
|
||||
key={idx}
|
||||
onClick={() => toggleTask(idx, isDone ? "Pending" : "Done")}
|
||||
className={`flex items-center gap-3 px-4 py-2.5 transition-colors cursor-pointer border-l-[3px] ${
|
||||
isDone ? "opacity-60 border-green-500 hover:bg-white/5" :
|
||||
isCurrent ? "border-[#00F0FF] bg-[#00F0FF]/5 hover:bg-[#00F0FF]/10" :
|
||||
"border-transparent hover:bg-white/5"
|
||||
}`}
|
||||
>
|
||||
{/* Custom Checkbox */}
|
||||
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 transition-all ${
|
||||
isDone ? "bg-green-500 border-green-500" :
|
||||
isCurrent ? "border-[#00F0FF] shadow-[0_0_0_0_rgba(0,240,255,0.3)] animate-[pulse-border_2s_infinite]" :
|
||||
"border-muted-foreground"
|
||||
}`}>
|
||||
{isDone && <Check size={12} className="stroke-white stroke-[3px]" />}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-[13px] font-bold text-primary truncate">{task.description}</div>
|
||||
<div className="text-[11px] text-muted-foreground mt-0.5">{task.type.replace('_', ' ')} • Active Session</div>
|
||||
</div>
|
||||
|
||||
<div className={`text-[10px] font-bold px-2 py-0.5 rounded-md shrink-0 ${getTagStyle(task.type)}`}>
|
||||
{getDisplayType(task.type)}
|
||||
</div>
|
||||
|
||||
<div className="text-[12px] font-bold text-muted-foreground bg-white/5 px-2 py-0.5 rounded-md shrink-0">
|
||||
{task.duration_minutes}m
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
<style dangerouslySetInnerHTML={{__html: `
|
||||
@keyframes pulse-border {
|
||||
0%, 100% { box-shadow: 0 0 0 0 rgba(0, 240, 255, 0.3); }
|
||||
50% { box-shadow: 0 0 0 6px rgba(0, 240, 255, 0); }
|
||||
}
|
||||
`}} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
74
src/components/AI/StudyTimeOptimizationCard.tsx
Normal file
74
src/components/AI/StudyTimeOptimizationCard.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
"use client"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Flame, Star } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import Link from "next/link"
|
||||
|
||||
const SLOTS = [
|
||||
{ label: "6 AM", h: 30, peak: false },
|
||||
{ label: "9 AM", h: 50, peak: false },
|
||||
{ label: "12 PM", h: 65, peak: false },
|
||||
{ label: "3 PM", h: 100, peak: true },
|
||||
{ label: "6 PM", h: 80, peak: false },
|
||||
{ label: "9 PM", h: 60, peak: false },
|
||||
{ label: "12 AM", h: 38, peak: false },
|
||||
]
|
||||
|
||||
export function StudyTimeOptimizationCard() {
|
||||
const [hovered, setHovered] = useState<number | null>(null)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center pb-3">
|
||||
<h2 className="text-base font-bold text-primary flex items-center gap-2">
|
||||
📈 Study Time Optimization
|
||||
</h2>
|
||||
<Link href="/dashboard/ai-coach/strategy" className="text-xs text-[#00F0FF] font-semibold hover:underline transition-colors">Expand View ↗</Link>
|
||||
</div>
|
||||
<Card className="border-border bg-card shadow-[0_4px_24px_rgba(0,0,0,0.3)] group hover:border-[#00F0FF]/30 transition-colors">
|
||||
<CardContent className="p-4">
|
||||
{/* Peak window badge */}
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Star size={13} className="fill-yellow-500 text-yellow-500" />
|
||||
<span className="text-xs font-bold text-yellow-500">Peak Productivity:</span>
|
||||
<span className="text-xs font-semibold text-foreground bg-yellow-500/10 border border-yellow-500/20 px-2 py-0.5 rounded-full">
|
||||
2 PM – 5 PM
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Bar chart */}
|
||||
<div className="flex items-end gap-1.5 h-20 px-1 mb-6">
|
||||
{SLOTS.map((slot, i) => (
|
||||
<div
|
||||
key={slot.label}
|
||||
className="flex-1 flex flex-col items-center gap-0.5 cursor-pointer"
|
||||
onMouseEnter={() => setHovered(i)}
|
||||
onMouseLeave={() => setHovered(null)}
|
||||
>
|
||||
<div
|
||||
className={`w-full rounded-t-md transition-all duration-300 ${
|
||||
slot.peak
|
||||
? "bg-gradient-to-t from-yellow-600/60 to-yellow-400 shadow-[0_0_10px_rgba(234,179,8,0.4)]"
|
||||
: hovered === i
|
||||
? "bg-gradient-to-t from-[#00F0FF]/50 to-[#00F0FF]"
|
||||
: "bg-gradient-to-t from-[#00F0FF]/20 to-[#00F0FF]/60"
|
||||
}`}
|
||||
style={{ height: `${slot.h}%` }}
|
||||
/>
|
||||
<span className="text-[8px] text-muted-foreground font-medium whitespace-nowrap mt-1">
|
||||
{slot.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-1 px-3 py-2.5 bg-yellow-500/10 border border-yellow-500/20 rounded-md text-xs text-yellow-500 font-medium flex items-center gap-2">
|
||||
<Flame size={16} className="fill-yellow-500 shrink-0" />
|
||||
You perform best 2 PM – 5 PM. Schedule your hardest mock tests in this window.
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useRef, useEffect } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Send, Bot, User, Loader2 } from "lucide-react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Send, Loader2 } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import ReactMarkdown from "react-markdown"
|
||||
import remarkMath from "remark-math"
|
||||
import rehypeKatex from "rehype-katex"
|
||||
@@ -93,53 +92,81 @@ export function TutorChat() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="border-border shadow-sm flex flex-col h-[600px]">
|
||||
<CardHeader className="border-b bg-secondary/10 pb-4">
|
||||
<CardTitle className="text-xl flex items-center gap-2">
|
||||
<Bot className="text-primary" size={24} />
|
||||
AI Tutor Chat
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{messages.map((msg, idx) => (
|
||||
<div key={idx} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
|
||||
<div className={`max-w-[80%] rounded-2xl p-4 flex gap-3 ${
|
||||
msg.role === "user" ? "bg-primary text-primary-foreground rounded-tr-sm" : "bg-secondary rounded-tl-sm"
|
||||
}`}>
|
||||
{msg.role === "assistant" && <Bot size={20} className="mt-1 shrink-0" />}
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none overflow-hidden">
|
||||
{msg.role === "assistant" ? (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkMath]}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
>
|
||||
{msg.content || "..."}
|
||||
</ReactMarkdown>
|
||||
) : (
|
||||
msg.content
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col h-full min-h-[500px]">
|
||||
<div className="flex justify-between items-center pb-3">
|
||||
<h2 className="text-base font-bold text-primary flex items-center gap-2">
|
||||
🤖 Ask AI Tutor
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<Card className="border-border bg-card shadow-[0_4px_24px_rgba(0,0,0,0.3)] flex-1 overflow-hidden flex flex-col group hover:border-[#00F0FF]/30 transition-colors">
|
||||
<div className="px-4 py-3 bg-[#00F0FF]/5 border-b border-border flex items-center gap-2.5">
|
||||
<div className="w-8 h-8 rounded-full bg-gradient-to-br from-[#00F0FF] to-blue-500 flex items-center justify-center text-sm font-bold text-background shrink-0">
|
||||
AI
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[13px] font-bold text-primary">PragatiVeer Tutor</div>
|
||||
<div className="text-[11px] text-green-500 flex items-center gap-1 font-medium">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-green-500"></span> Online
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</CardContent>
|
||||
<div className="ml-auto text-[11px] text-muted-foreground">
|
||||
<span className="text-yellow-500 font-bold">14</span> msgs left
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{messages.map((msg, idx) => (
|
||||
<div key={idx} className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}>
|
||||
<div className="max-w-[85%] flex flex-col gap-1">
|
||||
<div className={`p-3 text-[13px] leading-relaxed ${
|
||||
msg.role === "user"
|
||||
? "bg-[#00F0FF] text-background rounded-[12px] rounded-br-[4px] font-medium"
|
||||
: "bg-[#00F0FF]/10 border border-[#00F0FF]/20 text-primary rounded-[12px] rounded-bl-[4px]"
|
||||
}`}>
|
||||
<div className="prose prose-sm dark:prose-invert max-w-none [&>p]:m-0">
|
||||
{msg.role === "assistant" ? (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkMath]}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
>
|
||||
{msg.content || "..."}
|
||||
</ReactMarkdown>
|
||||
) : (
|
||||
msg.content
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`text-[10px] text-muted-foreground ${msg.role === "user" ? "text-right" : "text-left"}`}>
|
||||
Now
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div ref={messagesEndRef} />
|
||||
</CardContent>
|
||||
|
||||
<div className="p-4 border-t bg-background">
|
||||
<form onSubmit={handleSend} className="flex gap-2">
|
||||
<Input
|
||||
<div className="p-2.5 border-t border-border bg-black/20 flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="Ask me to explain a concept..."
|
||||
className="flex-1 bg-secondary/50 border-transparent focus-visible:ring-primary"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleSend();
|
||||
}}
|
||||
placeholder="Message AI Tutor..."
|
||||
className="flex-1 bg-white/5 border border-border rounded-full px-4 py-2 text-[13px] text-primary focus:outline-none focus:border-[#00F0FF] focus:bg-[#00F0FF]/5 transition-colors"
|
||||
disabled={streaming}
|
||||
/>
|
||||
<Button type="submit" size="icon" disabled={!input.trim() || streaming}>
|
||||
{streaming ? <Loader2 className="animate-spin" size={18} /> : <Send size={18} />}
|
||||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
</Card>
|
||||
<button
|
||||
onClick={handleSend}
|
||||
disabled={!input.trim() || streaming}
|
||||
className="w-9 h-9 rounded-full bg-[#00F0FF] flex items-center justify-center shrink-0 hover:bg-[#00F0FF]/90 transition-transform hover:scale-105 disabled:opacity-50 disabled:hover:scale-100"
|
||||
>
|
||||
{streaming ? <Loader2 className="animate-spin text-background" size={16} /> : <Send size={16} className="text-background" />}
|
||||
</button>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
77
src/components/AI/WeeklyProgressReport.tsx
Normal file
77
src/components/AI/WeeklyProgressReport.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
"use client"
|
||||
|
||||
import { TrendingUp, TrendingDown, Brain, Zap } from "lucide-react"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import Link from "next/link"
|
||||
|
||||
const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
||||
const BAR_HEIGHTS = [55, 70, 45, 90, 80, 60, 75] // % heights
|
||||
const ACCURACY = [82, 78, 91, 88, 85, 79, 93]
|
||||
|
||||
export function WeeklyProgressReport() {
|
||||
const avgAccuracy = Math.round(ACCURACY.reduce((a, b) => a + b, 0) / ACCURACY.length)
|
||||
const trend = ACCURACY[ACCURACY.length - 1] - ACCURACY[0]
|
||||
const maxBar = Math.max(...BAR_HEIGHTS)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex justify-between items-center pb-3">
|
||||
<h2 className="text-base font-bold text-primary flex items-center gap-2">
|
||||
📊 This Week at a Glance
|
||||
</h2>
|
||||
<Link href="/dashboard/ai-coach/study-plan" className="text-xs text-[#00F0FF] font-semibold hover:underline transition-colors">
|
||||
Expand View ↗
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<Card className="border-border bg-card shadow-[0_4px_24px_rgba(0,0,0,0.3)] flex-1 flex flex-col group hover:border-[#00F0FF]/30 transition-colors">
|
||||
<CardContent className="p-4 flex flex-col h-full gap-4">
|
||||
{/* Summary chips */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<div className="flex items-center gap-1.5 text-xs font-bold px-2.5 py-1 rounded-full bg-green-500/10 text-green-500 border border-green-500/20">
|
||||
<TrendingUp size={12} />
|
||||
{avgAccuracy}% avg accuracy
|
||||
</div>
|
||||
<div className={`flex items-center gap-1.5 text-xs font-bold px-2.5 py-1 rounded-full border ${trend >= 0 ? "bg-[#00F0FF]/10 text-[#00F0FF] border-[#00F0FF]/20" : "bg-red-500/10 text-red-500 border-red-500/20"}`}>
|
||||
{trend >= 0 ? <TrendingUp size={12} /> : <TrendingDown size={12} />}
|
||||
{trend >= 0 ? "+" : ""}{trend}% vs Mon
|
||||
</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
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bar Chart */}
|
||||
<div className="flex items-end gap-2 h-24 flex-1">
|
||||
{DAYS.map((day, i) => (
|
||||
<div key={day} className="flex flex-col items-center gap-1 flex-1">
|
||||
<div className="relative w-full flex items-end justify-center" style={{ height: "80px" }}>
|
||||
<div
|
||||
className={`w-full rounded-t-md transition-all duration-700 ${
|
||||
i === 6
|
||||
? "bg-gradient-to-t from-[#00F0FF]/60 to-[#00F0FF]"
|
||||
: "bg-gradient-to-t from-white/10 to-white/20"
|
||||
}`}
|
||||
style={{ height: `${(BAR_HEIGHTS[i] / maxBar) * 80}px` }}
|
||||
title={`${ACCURACY[i]}% accuracy`}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-[9px] text-muted-foreground font-semibold">{day}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* AI insight */}
|
||||
<div className="px-3 py-2.5 bg-[#00F0FF]/10 border border-[#00F0FF]/20 rounded-md text-xs text-[#00F0FF] font-medium flex items-start gap-3">
|
||||
<Brain size={16} className="shrink-0 mt-0.5" />
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<span><strong className="text-white">You keep making sign errors in Thermodynamics.</strong> In your last 3 attempts, you've used the wrong sign for work done (W = -PΔV). Review the First Law of Thermodynamics — we've queued 3 practice questions for tomorrow.</span>
|
||||
<span className="text-[#00F0FF]/80">💡 <strong className="text-white">Tip:</strong> You perform best between 7-9 PM. Schedule difficult topics then.</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user