199 lines
8.1 KiB
TypeScript
199 lines
8.1 KiB
TypeScript
"use client"
|
||
|
||
import { useState } from "react"
|
||
import { Card, CardContent } from "@/components/ui/card"
|
||
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'
|
||
import 'katex/dist/katex.min.css'
|
||
|
||
export interface MistakePayload {
|
||
mistake: {
|
||
id: string
|
||
user_id: string
|
||
question_id: string
|
||
error_type: string
|
||
topic: string
|
||
subtopic: string
|
||
review_count: number
|
||
consecutive_correct: number
|
||
is_mastered: boolean
|
||
}
|
||
question: {
|
||
id: string
|
||
subject: string
|
||
question_text: string
|
||
options: Record<string, string>
|
||
correct_answer: { correct: string }
|
||
solution_text: string
|
||
}
|
||
}
|
||
|
||
interface MistakeReviewCardProps {
|
||
data: MistakePayload
|
||
onNext: () => void
|
||
onSubmitReview: (mistakeId: string, selectedAnswer: string) => Promise<{is_correct: boolean, correct_answer: string}>
|
||
}
|
||
|
||
export function MistakeReviewCard({ data, onNext, onSubmitReview }: MistakeReviewCardProps) {
|
||
const [selected, setSelected] = useState<string | null>(null)
|
||
const [result, setResult] = useState<'success' | 'error' | null>(null)
|
||
const [correctAnswer, setCorrectAnswer] = useState<string | null>(null)
|
||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||
|
||
const handleCheck = async () => {
|
||
if (!selected) return;
|
||
setIsSubmitting(true)
|
||
|
||
try {
|
||
const res = await onSubmitReview(data.mistake.id, selected)
|
||
setResult(res.is_correct ? 'success' : 'error')
|
||
setCorrectAnswer(res.correct_answer)
|
||
} catch (err) {
|
||
console.error(err)
|
||
} finally {
|
||
setIsSubmitting(false)
|
||
}
|
||
}
|
||
|
||
const handleNext = () => {
|
||
setSelected(null)
|
||
setResult(null)
|
||
setCorrectAnswer(null)
|
||
onNext()
|
||
}
|
||
|
||
const { mistake, question } = data;
|
||
|
||
return (
|
||
<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>
|
||
</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-xs font-bold text-[#00F0FF]">Attempt 1</span>
|
||
<span className="text-[11px] text-muted-foreground font-semibold">10 Jul</span>
|
||
</div>
|
||
|
||
<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-xs font-bold text-[#00F0FF]">Attempt 2</span>
|
||
<span className="text-[11px] 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-sm font-bold text-[#00F0FF]">Today</span>
|
||
<span className="text-xs 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-xs font-bold text-muted-foreground">Mastery</span>
|
||
<span className="text-[11px] text-muted-foreground font-semibold">26 Jul</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Mistake Card */}
|
||
<Card className="border-border bg-card shadow-[0_4px_24px_rgba(0,0,0,0.3)] min-h-[540px] h-full relative overflow-hidden flex flex-col justify-between group hover:border-[#00F0FF]/30 transition-colors">
|
||
<CardContent className="p-6 flex-1 flex flex-col justify-between">
|
||
<div className="flex items-center gap-2 mb-4">
|
||
<span className="text-xs font-bold px-3 py-1 rounded-md bg-white/5 border border-border text-muted-foreground uppercase tracking-widest">
|
||
{mistake.topic}
|
||
</span>
|
||
<span className="text-xs font-bold text-red-500 bg-red-500/10 px-2.5 py-1 rounded-md">
|
||
{mistake.error_type}
|
||
</span>
|
||
</div>
|
||
|
||
<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-4 rounded-xl border-[1.5px] transition-all text-base md:text-lg font-bold flex gap-3.5 items-center ${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>
|
||
)
|
||
}
|