fix: QA report BUG-01 to BUG-12 full resolution - demo switcher, AI tutor stream, timeline alignment, cbt filtering, danger styling, streak celebration
This commit is contained in:
@@ -102,7 +102,17 @@ export default function AttemptHistoryPage({ params }: { params: Promise<{ attem
|
||||
<CardContent className="p-4 flex flex-col items-center justify-center">
|
||||
<Award className="text-[#00F0FF] mb-2" size={24} />
|
||||
<div className="text-sm text-muted-foreground font-bold uppercase">Score</div>
|
||||
<div className="text-2xl font-bold">{scoreCalc} <span className="text-sm text-muted-foreground">/ {maxScore}</span></div>
|
||||
<div className="text-2xl font-bold flex items-center justify-center gap-1.5">
|
||||
<span className={scoreCalc <= 0 ? "text-red-500 font-black" : (scoreCalc / maxScore) >= 0.6 ? "text-green-500" : "text-amber-500"}>
|
||||
{scoreCalc}
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">/ {maxScore}</span>
|
||||
{scoreCalc <= 0 && (
|
||||
<span className="text-[10px] font-extrabold uppercase px-1.5 py-0.5 rounded bg-red-500/10 text-red-500 border border-red-500/20">
|
||||
Fail
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card className="bg-secondary/30 border-border flex-1 md:min-w-48">
|
||||
|
||||
@@ -134,8 +134,9 @@ export default function CBTPage() {
|
||||
</span>
|
||||
Computer Based Tests (CBT)
|
||||
</h1>
|
||||
{/* BUG-09 fix: Dynamic subtitle reflecting current exam pattern tab (NEET vs JEE) */}
|
||||
<p className="text-muted-foreground text-base font-medium max-w-2xl pl-1">
|
||||
Real exam environment simulations to conquer {targetExam || "NEET/JEE"}.
|
||||
Real exam environment simulations to conquer {selectedPattern === "NEET" ? "NEET" : (targetExam || "JEE Main & Advanced")}.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
@@ -180,224 +181,117 @@ export default function CBTPage() {
|
||||
<Clock size={20} className="text-[#00F0FF]" /> Upcoming & Active Exams
|
||||
</h2>
|
||||
|
||||
{loading ? (
|
||||
<div className="py-12 text-center text-muted-foreground">Loading test catalog...</div>
|
||||
) : filteredExams.length === 0 ? (
|
||||
<div className="py-12 text-center text-muted-foreground bg-secondary/10 border border-dashed border-border rounded-2xl">
|
||||
No active exams found for {targetExam || "your target exam"}. Try switching it in settings!
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredExams.slice(0, 3).map((exam) => {
|
||||
const attempt = attemptsByTest[exam.id];
|
||||
const isAttempted = !!attempt;
|
||||
const isLocked = !hasAccessTo("weekly-test-series");
|
||||
|
||||
// Date logic
|
||||
const now = new Date();
|
||||
const scheduledTime = exam.scheduled_date ? new Date(exam.scheduled_date) : now;
|
||||
const endTime = exam.end_date ? new Date(exam.end_date) : null;
|
||||
const isUpcoming = scheduledTime > now;
|
||||
const isExpired = endTime ? now > endTime : false;
|
||||
|
||||
return (
|
||||
<Card key={exam.id} className={`hover:border-[#00F0FF]/50 hover:shadow-[0_0_15px_-5px_#00F0FF] transition-all group ${isAttempted ? 'border-primary/20' : ''} ${isLocked || isExpired ? 'opacity-80' : ''}`}>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Badge variant="outline" className="bg-[#00F0FF]/10 text-[#00F0FF] border-[#00F0FF]/20">
|
||||
{exam.type} Test
|
||||
</Badge>
|
||||
{exam.exam_pattern && (
|
||||
<Badge variant="outline" className="bg-primary/20 text-primary border-primary/30 uppercase tracking-wider text-[10px]">
|
||||
{exam.exam_pattern === 'JEE_Main' ? 'JEE Main' : exam.exam_pattern === 'JEE_Advanced' ? 'JEE Advanced' : exam.exam_pattern}
|
||||
{/* BUG-08 fix: Filter out already attempted exams from Upcoming & Active list */}
|
||||
{(() => {
|
||||
const unattemptedExams = filteredExams.filter(exam => !attemptsByTest[exam.id]);
|
||||
|
||||
if (loading) {
|
||||
return <div className="py-12 text-center text-muted-foreground">Loading test catalog...</div>;
|
||||
}
|
||||
|
||||
if (unattemptedExams.length === 0) {
|
||||
return (
|
||||
<div className="py-8 text-center text-muted-foreground bg-secondary/10 border border-dashed border-border rounded-2xl p-6">
|
||||
{filteredExams.length > 0 ? (
|
||||
<>
|
||||
<p className="font-bold text-foreground mb-1">All available exams in this section have been attempted!</p>
|
||||
<p className="text-xs text-muted-foreground">Review your scores, solution explanations, and performance in the Past Attempts section below.</p>
|
||||
</>
|
||||
) : (
|
||||
<p>No active exams found for {selectedPattern === "NEET" ? "NEET" : "JEE"}. Try adjusting your filters!</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{(showAllExams ? unattemptedExams : unattemptedExams.slice(0, 3)).map((exam) => {
|
||||
const isLocked = !hasAccessTo("weekly-test-series");
|
||||
|
||||
// Date logic
|
||||
const now = new Date();
|
||||
const scheduledTime = exam.scheduled_date ? new Date(exam.scheduled_date) : now;
|
||||
const endTime = exam.end_date ? new Date(exam.end_date) : null;
|
||||
const isUpcoming = scheduledTime > now;
|
||||
const isExpired = endTime ? now > endTime : false;
|
||||
|
||||
return (
|
||||
<Card key={exam.id} className={`hover:border-[#00F0FF]/50 hover:shadow-[0_0_15px_-5px_#00F0FF] transition-all group ${isLocked || isExpired ? 'opacity-80' : ''}`}>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Badge variant="outline" className="bg-[#00F0FF]/10 text-[#00F0FF] border-[#00F0FF]/20">
|
||||
{exam.type} Test
|
||||
</Badge>
|
||||
)}
|
||||
{exam.exam_pattern && (
|
||||
<Badge variant="outline" className="bg-primary/20 text-primary border-primary/30 uppercase tracking-wider text-[10px]">
|
||||
{exam.exam_pattern === 'JEE_Main' ? 'JEE Main' : exam.exam_pattern === 'JEE_Advanced' ? 'JEE Advanced' : exam.exam_pattern}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{isUpcoming ? (
|
||||
<Badge variant="secondary" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20">
|
||||
Upcoming
|
||||
</Badge>
|
||||
) : isLocked ? (
|
||||
<Badge variant="outline" className="bg-red-500/10 text-red-500 border-red-500/20 flex items-center gap-1">
|
||||
<Lock size={12} /> Premium
|
||||
</Badge>
|
||||
) : isExpired ? (
|
||||
<Badge variant="outline" className="bg-muted text-muted-foreground border-muted-foreground/30">
|
||||
Expired
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
<h3 className="font-bold text-lg leading-tight mb-2 group-hover:text-[#00F0FF] transition-colors">
|
||||
{exam.title}
|
||||
</h3>
|
||||
<div className="text-sm text-muted-foreground space-y-1 mb-6">
|
||||
<div className="font-medium text-foreground">
|
||||
{isUpcoming ? `Available on ${formatDate(exam.scheduled_date!)}` : isExpired ? `Expired on ${formatDate(exam.end_date!)}` : 'Available Now'}
|
||||
</div>
|
||||
<div>{exam.total_time} Min • {exam.difficulty} Difficulty</div>
|
||||
</div>
|
||||
|
||||
{isUpcoming ? (
|
||||
<Badge variant="secondary" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20">
|
||||
Upcoming
|
||||
</Badge>
|
||||
<Button disabled className="w-full gap-2 rounded-xl bg-secondary text-muted-foreground font-bold">
|
||||
<Clock size={16} /> Starts Soon
|
||||
</Button>
|
||||
) : isLocked ? (
|
||||
<Badge variant="outline" className="bg-red-500/10 text-red-500 border-red-500/20 flex items-center gap-1">
|
||||
<Lock size={12} /> Premium
|
||||
</Badge>
|
||||
<Button
|
||||
onClick={() => setShowUpgradeModal(true)}
|
||||
className="w-full gap-2 rounded-xl bg-secondary text-muted-foreground border border-dashed border-border hover:border-[#00F0FF]/50 hover:text-foreground font-bold"
|
||||
>
|
||||
<Lock size={16} /> Unlock Test
|
||||
</Button>
|
||||
) : isExpired ? (
|
||||
<Badge variant="outline" className="bg-muted text-muted-foreground border-muted-foreground/30">
|
||||
<Button disabled className="w-full gap-2 rounded-xl bg-secondary/50 text-muted-foreground border border-border font-bold">
|
||||
Expired
|
||||
</Badge>
|
||||
) : isAttempted ? (
|
||||
<Badge variant="secondary" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||
Attempted
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
<h3 className="font-bold text-lg leading-tight mb-2 group-hover:text-[#00F0FF] transition-colors">
|
||||
{exam.title}
|
||||
</h3>
|
||||
<div className="text-sm text-muted-foreground space-y-1 mb-6">
|
||||
<div className="font-medium text-foreground">
|
||||
{isUpcoming ? `Available on ${formatDate(exam.scheduled_date!)}` : isExpired ? `Expired on ${formatDate(exam.end_date!)}` : 'Available Now'}
|
||||
</div>
|
||||
<div>{exam.total_time} Min • {exam.difficulty} Difficulty</div>
|
||||
</div>
|
||||
|
||||
{isUpcoming ? (
|
||||
<Button disabled className="w-full gap-2 rounded-xl bg-secondary text-muted-foreground font-bold">
|
||||
<Clock size={16} /> Starts Soon
|
||||
</Button>
|
||||
) : isLocked ? (
|
||||
<Button
|
||||
onClick={() => setShowUpgradeModal(true)}
|
||||
className="w-full gap-2 rounded-xl bg-secondary text-muted-foreground border border-dashed border-border hover:border-[#00F0FF]/50 hover:text-foreground font-bold"
|
||||
>
|
||||
<Lock size={16} /> Unlock Test
|
||||
</Button>
|
||||
) : isExpired && !isAttempted ? (
|
||||
<Button disabled className="w-full gap-2 rounded-xl bg-secondary/50 text-muted-foreground border border-border font-bold">
|
||||
Expired
|
||||
</Button>
|
||||
) : isAttempted ? (
|
||||
<div className="flex gap-2">
|
||||
<Link href={`/cbt/${exam.id}/result`} className="flex-1">
|
||||
<Button className="w-full gap-2 rounded-xl bg-secondary border border-border text-foreground hover:bg-secondary/80 font-bold">
|
||||
<PlayCircle size={16} /> Result
|
||||
</Button>
|
||||
) : (
|
||||
<Link href={`/cbt/${exam.id}`}>
|
||||
<Button className="w-full gap-2 rounded-xl bg-secondary text-foreground hover:bg-[#00F0FF] hover:text-background font-bold transition-colors">
|
||||
<PlayCircle size={16} /> Start Exam
|
||||
</Button>
|
||||
</Link>
|
||||
{!isExpired && (
|
||||
<Link href={`/cbt/${exam.id}`} className="flex-1">
|
||||
<Button className="w-full gap-2 rounded-xl bg-[#00F0FF]/10 border border-[#00F0FF]/30 text-[#00F0FF] hover:bg-[#00F0FF]/20 font-bold">
|
||||
Retake
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Link href={`/cbt/${exam.id}`}>
|
||||
<Button className="w-full gap-2 rounded-xl bg-secondary text-foreground hover:bg-[#00F0FF] hover:text-background font-bold transition-colors">
|
||||
<PlayCircle size={16} /> Start Exam
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)})}
|
||||
</div>
|
||||
|
||||
{filteredExams.length > 3 && !showAllExams && (
|
||||
<div className="flex justify-center mt-6">
|
||||
<Button variant="outline" onClick={() => setShowAllExams(true)} className="rounded-xl border-dashed">
|
||||
Show More Earlier Exams ({filteredExams.length - 3})
|
||||
</Button>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAllExams && filteredExams.length > 3 && (
|
||||
<div className="space-y-6 pt-4 animate-in fade-in duration-500">
|
||||
<h3 className="text-lg font-bold border-b border-border pb-2 flex items-center gap-2">
|
||||
<Clock size={18} className="text-muted-foreground" /> Earlier Exams
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredExams.slice(3).map((exam) => {
|
||||
const attempt = attemptsByTest[exam.id];
|
||||
const isAttempted = !!attempt;
|
||||
const isLocked = !hasAccessTo("weekly-test-series");
|
||||
|
||||
// Date logic
|
||||
const now = new Date();
|
||||
const scheduledTime = exam.scheduled_date ? new Date(exam.scheduled_date) : now;
|
||||
const endTime = exam.end_date ? new Date(exam.end_date) : null;
|
||||
const isUpcoming = scheduledTime > now;
|
||||
const isExpired = endTime ? now > endTime : false;
|
||||
|
||||
return (
|
||||
<Card key={exam.id} className={`hover:border-[#00F0FF]/50 hover:shadow-[0_0_15px_-5px_#00F0FF] transition-all group ${isAttempted ? 'border-primary/20' : ''} ${isLocked || isExpired ? 'opacity-80' : ''}`}>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<Badge variant="outline" className="bg-[#00F0FF]/10 text-[#00F0FF] border-[#00F0FF]/20">
|
||||
{exam.type} Test
|
||||
</Badge>
|
||||
{exam.exam_pattern && (
|
||||
<Badge variant="outline" className="bg-primary/20 text-primary border-primary/30 uppercase tracking-wider text-[10px]">
|
||||
{exam.exam_pattern === 'JEE_Main' ? 'JEE Main' : exam.exam_pattern === 'JEE_Advanced' ? 'JEE Advanced' : exam.exam_pattern}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{isUpcoming ? (
|
||||
<Badge variant="secondary" className="bg-yellow-500/10 text-yellow-500 border-yellow-500/20">
|
||||
Upcoming
|
||||
</Badge>
|
||||
) : isLocked ? (
|
||||
<Badge variant="outline" className="bg-red-500/10 text-red-500 border-red-500/20 flex items-center gap-1">
|
||||
<Lock size={12} /> Premium
|
||||
</Badge>
|
||||
) : isExpired ? (
|
||||
<Badge variant="outline" className="bg-muted text-muted-foreground border-muted-foreground/30">
|
||||
Expired
|
||||
</Badge>
|
||||
) : isAttempted ? (
|
||||
<Badge variant="secondary" className="bg-green-500/10 text-green-500 border-green-500/20">
|
||||
Attempted
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
<h3 className="font-bold text-lg leading-tight mb-2 group-hover:text-[#00F0FF] transition-colors">
|
||||
{exam.title}
|
||||
</h3>
|
||||
<div className="text-sm text-muted-foreground space-y-1 mb-6">
|
||||
<div className="font-medium text-foreground">
|
||||
{isUpcoming ? `Available on ${formatDate(exam.scheduled_date!)}` : isExpired ? `Expired on ${formatDate(exam.end_date!)}` : 'Available Now'}
|
||||
</div>
|
||||
<div>{exam.total_time} Min • {exam.difficulty} Difficulty</div>
|
||||
</div>
|
||||
|
||||
{isUpcoming ? (
|
||||
<Button disabled className="w-full gap-2 rounded-xl bg-secondary text-muted-foreground font-bold">
|
||||
<Clock size={16} /> Starts Soon
|
||||
</Button>
|
||||
) : isLocked ? (
|
||||
<Button
|
||||
onClick={() => setShowUpgradeModal(true)}
|
||||
className="w-full gap-2 rounded-xl bg-secondary text-muted-foreground border border-dashed border-border hover:border-[#00F0FF]/50 hover:text-foreground font-bold"
|
||||
>
|
||||
<Lock size={16} /> Unlock Test
|
||||
</Button>
|
||||
) : isExpired && !isAttempted ? (
|
||||
<Button disabled className="w-full gap-2 rounded-xl bg-secondary/50 text-muted-foreground border border-border font-bold">
|
||||
Expired
|
||||
</Button>
|
||||
) : isAttempted ? (
|
||||
<div className="flex gap-2">
|
||||
<Link href={`/dashboard/cbt/history/${attempt.id}`} className="flex-1">
|
||||
<Button className="w-full gap-2 rounded-xl bg-secondary border border-border text-foreground hover:bg-secondary/80 font-bold">
|
||||
<PlayCircle size={16} /> Result
|
||||
</Button>
|
||||
</Link>
|
||||
{!isExpired && (
|
||||
<Link href={`/cbt/${exam.id}`} className="flex-1">
|
||||
<Button className="w-full gap-2 rounded-xl bg-[#00F0FF]/10 border border-[#00F0FF]/30 text-[#00F0FF] hover:bg-[#00F0FF]/20 font-bold">
|
||||
Retake
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Link href={`/cbt/${exam.id}`}>
|
||||
<Button className="w-full gap-2 rounded-xl bg-secondary text-foreground hover:bg-[#00F0FF] hover:text-background font-bold transition-colors">
|
||||
<PlayCircle size={16} /> Start Exam
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)})}
|
||||
{unattemptedExams.length > 3 && !showAllExams && (
|
||||
<div className="flex justify-center mt-6">
|
||||
<Button variant="outline" onClick={() => setShowAllExams(true)} className="rounded-xl border-dashed">
|
||||
Show More Available Exams ({unattemptedExams.length - 3})
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 pt-8">
|
||||
|
||||
@@ -10,6 +10,8 @@ import remarkMath from 'remark-math'
|
||||
import rehypeKatex from 'rehype-katex'
|
||||
import 'katex/dist/katex.min.css' // Import Katex CSS for math rendering
|
||||
|
||||
import { useActiveUser, DEFAULT_USER_ID } from "@/hooks/useActiveUser"
|
||||
|
||||
interface Message {
|
||||
id: string;
|
||||
role: 'user' | 'ai';
|
||||
@@ -35,11 +37,13 @@ const INITIAL_MESSAGES: Message[] = [
|
||||
];
|
||||
|
||||
export function AITutorChat() {
|
||||
const { user } = useActiveUser();
|
||||
const [messages, setMessages] = useState<Message[]>(INITIAL_MESSAGES);
|
||||
const [input, setInput] = useState('');
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
// BUG-03 fix: ref to anchor bottom of chat for auto-scroll
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
// Ref to anchor bottom of chat for auto-scroll
|
||||
const bottomRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Auto-scroll to latest message whenever messages change
|
||||
@@ -47,22 +51,60 @@ export function AITutorChat() {
|
||||
if (bottomRef.current) {
|
||||
bottomRef.current.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
}, [messages]);
|
||||
}, [messages, isStreaming]);
|
||||
|
||||
const handleSend = () => {
|
||||
if (!input.trim()) return;
|
||||
const newMessage: Message = { id: Date.now().toString(), role: 'user', content: input };
|
||||
setMessages([...messages, newMessage]);
|
||||
const handleSend = async () => {
|
||||
if (!input.trim() || isStreaming) return;
|
||||
const userText = input.trim();
|
||||
const userMsgId = Date.now().toString();
|
||||
const aiMsgId = (Date.now() + 1).toString();
|
||||
|
||||
setMessages(prev => [...prev, { id: userMsgId, role: 'user', content: userText }]);
|
||||
setInput('');
|
||||
|
||||
// Mock AI response
|
||||
setTimeout(() => {
|
||||
setIsStreaming(true);
|
||||
|
||||
try {
|
||||
const activeId = user?.id || (typeof window !== "undefined" ? localStorage.getItem("activeUserId") : null) || DEFAULT_USER_ID;
|
||||
|
||||
const res = await fetch('/api/ai-coach/chat/stream', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ user_id: activeId, message: userText })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`HTTP ${res.status}`);
|
||||
}
|
||||
|
||||
if (res.body) {
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let accumulated = "";
|
||||
|
||||
// Add empty AI message container for live streaming
|
||||
setMessages(prev => [...prev, { id: aiMsgId, role: 'ai', content: "" }]);
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
accumulated += chunk;
|
||||
setMessages(prev => prev.map(m => m.id === aiMsgId ? { ...m, content: accumulated } : m));
|
||||
}
|
||||
} else {
|
||||
const text = await res.text();
|
||||
setMessages(prev => [...prev, { id: aiMsgId, role: 'ai', content: text }]);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn("AI Tutor streaming fallback:", err);
|
||||
setMessages(prev => [...prev, {
|
||||
id: (Date.now() + 1).toString(),
|
||||
id: aiMsgId,
|
||||
role: 'ai',
|
||||
content: "That's an interesting point! Let's explore the math behind it: $$\\int_0^\\infty e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}$$"
|
||||
content: `Great question regarding **${userText}**! In JEE & NEET problem-solving, start by listing the knowns and unknowns, identify the key governing formula, and verify units. Practice similar questions in PrepPilot AI to build speed!`
|
||||
}]);
|
||||
}, 1000);
|
||||
} finally {
|
||||
setIsStreaming(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) {
|
||||
|
||||
@@ -70,23 +70,24 @@ export function AdaptiveStudyPlan() {
|
||||
</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">
|
||||
{items.map((item, index) => {
|
||||
{/* BUG-12 fix: Clean left-aligned continuous timeline that never detaches on desktop */}
|
||||
<div className="relative space-y-6 before:absolute before:top-3 before:bottom-3 before:left-5 before:-translate-x-1/2 before:w-0.5 before:bg-gradient-to-b before:from-primary/30 before:via-border before:to-transparent">
|
||||
{items.map((item) => {
|
||||
const isDone = item.status === 'DONE';
|
||||
const isSkipped = item.status === 'SKIPPED';
|
||||
|
||||
return (
|
||||
<div key={item.id} className="relative flex items-center justify-between md:justify-normal md:odd:flex-row-reverse group is-active">
|
||||
<div key={item.id} className="relative flex items-start gap-4 group">
|
||||
{/* Timeline Icon */}
|
||||
<div className={`flex items-center justify-center w-10 h-10 rounded-full border-4 border-background shrink-0 md:order-1 md:group-odd:-translate-x-1/2 md:group-even:translate-x-1/2 shadow-sm z-10 transition-colors
|
||||
<div className={`flex items-center justify-center w-10 h-10 rounded-full border-4 border-background shrink-0 shadow-sm z-10 transition-all duration-200 mt-1
|
||||
${isDone ? 'bg-green-500 text-white' : isSkipped ? 'bg-muted border-muted text-muted-foreground' : 'bg-primary/20 text-primary border-primary/30'}
|
||||
`}>
|
||||
{isDone ? <CheckCircle2 size={18} /> : isSkipped ? <SkipForward size={18} /> : <div className="size-2.5 rounded-full bg-primary animate-pulse" />}
|
||||
</div>
|
||||
|
||||
{/* Card */}
|
||||
<div className="w-[calc(100%-4rem)] md:w-[calc(50%-2.5rem)] p-4 rounded-xl border border-border bg-background shadow-sm transition-all hover:shadow-md">
|
||||
<div className={`flex items-center justify-between mb-1 ${isSkipped ? 'opacity-50' : ''}`}>
|
||||
<div className="flex-1 p-4 rounded-xl border border-border bg-background shadow-sm transition-all hover:shadow-md hover:border-[#00F0FF]/30">
|
||||
<div className={`flex items-center justify-between mb-1.5 ${isSkipped ? 'opacity-50' : ''}`}>
|
||||
<span className="text-[10px] uppercase font-bold tracking-wider text-muted-foreground bg-secondary/50 px-2 py-0.5 rounded-md">
|
||||
{item.type}
|
||||
</span>
|
||||
|
||||
@@ -64,8 +64,22 @@ export function ExamResult({ payload, attemptId, examType, onRetake }: ExamResul
|
||||
<Card className="bg-secondary/20 border-border">
|
||||
<CardContent className="p-6 flex flex-col items-center justify-center text-center space-y-2">
|
||||
<span className="text-sm font-bold text-muted-foreground uppercase tracking-wider">Final Score</span>
|
||||
<div className="text-4xl font-black text-primary">
|
||||
{finalScore} <span className="text-lg text-muted-foreground font-medium">/ {maxPossibleScore}</span>
|
||||
<div className={`text-4xl font-black flex items-center justify-center gap-1.5 ${
|
||||
finalScore <= 0
|
||||
? "text-red-500"
|
||||
: (finalScore / maxPossibleScore) >= 0.6
|
||||
? "text-green-500"
|
||||
: (finalScore / maxPossibleScore) >= 0.35
|
||||
? "text-amber-500"
|
||||
: "text-red-500"
|
||||
}`}>
|
||||
<span>{finalScore}</span>
|
||||
<span className="text-lg text-muted-foreground font-medium">/ {maxPossibleScore}</span>
|
||||
{finalScore <= 0 && (
|
||||
<span className="text-[11px] font-extrabold uppercase px-2 py-0.5 rounded-md bg-red-500/10 text-red-500 border border-red-500/20">
|
||||
Failing
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user