"use client"; import { useState, useCallback } from "react"; import { motion, AnimatePresence } from "framer-motion"; import type { MathProblem, Difficulty } from "@/lib/problems/types"; import { checkFractionAnswer, checkDecimalAnswer, checkRatioAnswer, checkIntegerAnswer, type AnswerResult } from "@/lib/math/validation"; import { FractionInput, DecimalInput, RatioInput } from "./fraction-input"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import katex from "katex"; interface PracticeSectionProps { title: string; generator: (difficulty: Difficulty) => MathProblem; unitColor?: "unit-1" | "unit-2" | "unit-3" | "unit-4"; } type UnitColor = NonNullable; const activeDifficultyStyle: Record = { "unit-1": "bg-unit-1 text-white", "unit-2": "bg-unit-2 text-white", "unit-3": "bg-unit-3 text-white", "unit-4": "bg-unit-4 text-white", }; export function PracticeSection({ title, generator, unitColor = "unit-1" }: PracticeSectionProps) { const [difficulty, setDifficulty] = useState(1); const [problem, setProblem] = useState(() => generator(1)); const [userAnswer, setUserAnswer] = useState>({}); const [result, setResult] = useState(null); const [showHint, setShowHint] = useState(false); const [hintIndex, setHintIndex] = useState(0); const [showSolution, setShowSolution] = useState(false); const [score, setScore] = useState({ correct: 0, total: 0 }); const generateNew = useCallback((diff: Difficulty) => { setProblem(generator(diff)); setUserAnswer({}); setResult(null); setShowHint(false); setHintIndex(0); setShowSolution(false); }, [generator]); function checkAnswer() { const answer = problem.answer; let res: AnswerResult; switch (answer.kind) { case "fraction": { const num = parseInt(userAnswer.numerator || "0"); const den = parseInt(userAnswer.denominator || "0"); if (isNaN(num) || isNaN(den)) { res = { correct: false, message: "Enter valid numbers" }; } else { res = checkFractionAnswer(num, den, answer.numerator, answer.denominator); } break; } case "decimal": { const val = parseFloat(userAnswer.value || ""); if (isNaN(val)) { res = { correct: false, message: "Enter a valid number" }; } else { res = checkDecimalAnswer(val, answer.value); } break; } case "integer": { const val = parseInt(userAnswer.value || ""); if (isNaN(val)) { res = { correct: false, message: "Enter a valid number" }; } else { res = checkIntegerAnswer(val, answer.value); } break; } case "ratio": { const parts = (userAnswer.ratio || "").split(":").map((p) => parseInt(p.trim())); if (parts.some(isNaN)) { res = { correct: false, message: "Enter valid ratio parts" }; } else { res = checkRatioAnswer(parts, answer.parts); } break; } case "standardForm": { const coeff = parseFloat(userAnswer.coefficient || ""); const exp = parseInt(userAnswer.exponent || ""); if (isNaN(coeff) || isNaN(exp)) { res = { correct: false, message: "Enter valid numbers" }; } else if ( Math.abs(coeff - answer.coefficient) < 0.01 && exp === answer.exponent ) { res = { correct: true, simplified: true }; } else { res = { correct: false, message: "That's not quite right. Try again!" }; } break; } } setResult(res); if (res.correct) { setScore((s) => ({ correct: s.correct + 1, total: s.total + 1 })); } else { setScore((s) => ({ ...s, total: s.total + 1 })); } } function nextHint() { if (hintIndex < problem.hints.length - 1) { setHintIndex((i) => i + 1); } setShowHint(true); } const promptHtml = katex.renderToString(problem.prompt, { throwOnError: false, displayMode: true }); return (

{title}

{score.correct}/{score.total}
{([1, 2, 3] as Difficulty[]).map((d) => ( ))}
{/* Problem */}
{/* Input */}
{problem.answer.kind === "fraction" && ( setUserAnswer((a) => ({ ...a, numerator: v }))} onDenominatorChange={(v) => setUserAnswer((a) => ({ ...a, denominator: v }))} disabled={result?.correct === true} /> )} {(problem.answer.kind === "decimal" || problem.answer.kind === "integer") && ( setUserAnswer((a) => ({ ...a, value: v }))} disabled={result?.correct === true} /> )} {problem.answer.kind === "ratio" && (() => { const ratioParts = (problem.answer as { kind: "ratio"; parts: number[] }).parts; return ( "") } onChange={(i, v) => { const current = userAnswer.ratioParts ? JSON.parse(userAnswer.ratioParts) : ratioParts.map(() => ""); current[i] = v; setUserAnswer((a) => ({ ...a, ratioParts: JSON.stringify(current), ratio: current.join(":"), })); }} disabled={result?.correct === true} /> ); })()} {problem.answer.kind === "standardForm" && (
setUserAnswer((a) => ({ ...a, coefficient: e.target.value }))} disabled={result?.correct === true} className="w-16 rounded-lg border border-border bg-surface px-2 py-2 text-center font-bold focus:border-unit-2 focus:outline-none" placeholder="?" aria-label="Coefficient" /> x 10 setUserAnswer((a) => ({ ...a, exponent: e.target.value }))} disabled={result?.correct === true} className="w-12 -translate-y-2 rounded-lg border border-border bg-surface px-2 py-1 text-center text-sm font-bold focus:border-unit-2 focus:outline-none" placeholder="?" aria-label="Exponent" />
)}
{/* Actions */}
{!result?.correct && ( <> )} {result?.correct && ( )}
{/* Feedback */} {result && ( {result.correct ? result.simplified ? "Correct!" : "Correct, but can you simplify further?" : result.message} )} {/* Hint */} {showHint && ( {problem.hints[hintIndex]} )} {/* Solution */} {showSolution && (

Solution

{problem.steps.map((step, i) => (
{i + 1}. {step.explanation}
))}
)}
); }