feat: complete pragativeer v2 frontend and backend, docker setup, and cbt engine
This commit is contained in:
120
src/components/AI/ReadinessGauge.tsx
Normal file
120
src/components/AI/ReadinessGauge.tsx
Normal file
@@ -0,0 +1,120 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
|
||||
import { Activity, RefreshCw } from "lucide-react"
|
||||
|
||||
const USER_ID = "b0e17378-864d-439e-8806-708dc48a891b"
|
||||
|
||||
export function ReadinessGauge() {
|
||||
const [data, setData] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchReadiness() {
|
||||
try {
|
||||
const res = await fetch(`http://localhost:8000/api/ai-coach/readiness/${USER_ID}`)
|
||||
if (!res.ok) throw new Error("Failed to fetch")
|
||||
const json = await res.json()
|
||||
setData(json)
|
||||
} catch (err) {
|
||||
console.warn("Using mock fallback data for Readiness Gauge.")
|
||||
setData({
|
||||
score: 72,
|
||||
breakdown: {
|
||||
accuracy: 30,
|
||||
speed: 15,
|
||||
consistency: 8,
|
||||
recovery: 5,
|
||||
coverage: 14
|
||||
}
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
fetchReadiness()
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className="border-border shadow-sm h-full flex items-center justify-center min-h-[300px]">
|
||||
<RefreshCw className="animate-spin text-muted-foreground" />
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const score = data?.score || 0
|
||||
const breakdown = data?.breakdown || {}
|
||||
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 (
|
||||
<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>
|
||||
|
||||
<CardContent className="flex flex-col items-center flex-1 justify-center p-6 space-y-6">
|
||||
|
||||
{/* 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>
|
||||
</div>
|
||||
|
||||
{/* Breakdown Bars */}
|
||||
<div className="w-full space-y-3">
|
||||
{[
|
||||
{ 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}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user