"use client"; import { useState, useCallback, useRef } from "react"; import { motion, AnimatePresence } from "framer-motion"; import type { MathProblem, Difficulty } from "@/lib/problems/types"; import { checkFractionAnswer, checkDecimalAnswer, checkRatioAnswer, checkIntegerAnswer, parseStrictDecimal, parseStrictSignedDecimal, parseStrictSignedInt, 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 { MathDisplay } from "@/components/math/math-display"; 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", }; const RECENT_PROBLEM_LIMIT = 8; const MAX_GENERATION_RETRIES = 12; function getProblemKey(problem: MathProblem): string { return JSON.stringify({ prompt: problem.prompt, answer: problem.answer, }); } export function PracticeSection({ title, generator, unitColor = "unit-1" }: PracticeSectionProps) { const recentProblemKeysRef = useRef(null); 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 isFullyCorrect = result?.correct === true && result.simplified === true; const generateUniqueProblem = useCallback((diff: Difficulty): MathProblem => { const recentProblemKeys = recentProblemKeysRef.current ?? [getProblemKey(problem)]; let nextProblem = generator(diff); let nextKey = getProblemKey(nextProblem); let retries = 0; while ( recentProblemKeys.includes(nextKey) && retries < MAX_GENERATION_RETRIES ) { nextProblem = generator(diff); nextKey = getProblemKey(nextProblem); retries += 1; } recentProblemKeysRef.current = [ nextKey, ...recentProblemKeys.filter((key) => key !== nextKey), ].slice(0, RECENT_PROBLEM_LIMIT); return nextProblem; }, [generator, problem]); const generateNew = useCallback((diff: Difficulty) => { setProblem(generateUniqueProblem(diff)); setUserAnswer({}); setResult(null); setShowHint(false); setHintIndex(0); setShowSolution(false); }, [generateUniqueProblem]); function checkAnswer() { const answer = problem.answer; let res: AnswerResult; switch (answer.kind) { case "fraction": { const num = parseStrictSignedInt(userAnswer.numerator || ""); const den = parseStrictSignedInt(userAnswer.denominator || ""); if (num === null || den === null) { res = { correct: false, message: "Enter valid numbers" }; } else { res = checkFractionAnswer(num, den, answer.numerator, answer.denominator); } break; } case "decimal": { const val = parseStrictSignedDecimal(userAnswer.value || ""); if (val === null) { res = { correct: false, message: "Enter a valid number" }; } else { res = checkDecimalAnswer(val, answer.value); } break; } case "integer": { const val = parseStrictSignedInt(userAnswer.value || ""); if (val === null) { 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 = parseStrictDecimal(userAnswer.coefficient || ""); const exp = parseStrictSignedInt(userAnswer.exponent || ""); if (coeff === null || exp === null) { 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 (!showHint) { setShowHint(true); return; } if (hintIndex < problem.hints.length - 1) { setHintIndex((i) => i + 1); } } 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={isFullyCorrect} /> )} {(problem.answer.kind === "decimal" || problem.answer.kind === "integer") && ( setUserAnswer((a) => ({ ...a, value: v }))} disabled={isFullyCorrect} /> )} {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={isFullyCorrect} /> ); })()} {problem.answer.kind === "standardForm" && (
setUserAnswer((a) => ({ ...a, coefficient: e.target.value }))} disabled={isFullyCorrect} 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={isFullyCorrect} 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 */}
{!isFullyCorrect && ( <> )} {isFullyCorrect && ( )}
{/* 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}
{step.math && }
))}
)}
); }