fractions add/sub fix
This commit is contained in:
@@ -1,12 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useCallback } from "react";
|
||||
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, type AnswerResult } from "@/lib/math/validation";
|
||||
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 {
|
||||
@@ -24,7 +34,18 @@ const activeDifficultyStyle: Record<UnitColor, string> = {
|
||||
"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<string[] | null>(null);
|
||||
const [difficulty, setDifficulty] = useState<Difficulty>(1);
|
||||
const [problem, setProblem] = useState<MathProblem>(() => generator(1));
|
||||
const [userAnswer, setUserAnswer] = useState<Record<string, string>>({});
|
||||
@@ -33,15 +54,39 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
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(generator(diff));
|
||||
setProblem(generateUniqueProblem(diff));
|
||||
setUserAnswer({});
|
||||
setResult(null);
|
||||
setShowHint(false);
|
||||
setHintIndex(0);
|
||||
setShowSolution(false);
|
||||
}, [generator]);
|
||||
}, [generateUniqueProblem]);
|
||||
|
||||
function checkAnswer() {
|
||||
const answer = problem.answer;
|
||||
@@ -49,9 +94,9 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
|
||||
switch (answer.kind) {
|
||||
case "fraction": {
|
||||
const num = parseInt(userAnswer.numerator || "0");
|
||||
const den = parseInt(userAnswer.denominator || "0");
|
||||
if (isNaN(num) || isNaN(den)) {
|
||||
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);
|
||||
@@ -59,8 +104,8 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
break;
|
||||
}
|
||||
case "decimal": {
|
||||
const val = parseFloat(userAnswer.value || "");
|
||||
if (isNaN(val)) {
|
||||
const val = parseStrictSignedDecimal(userAnswer.value || "");
|
||||
if (val === null) {
|
||||
res = { correct: false, message: "Enter a valid number" };
|
||||
} else {
|
||||
res = checkDecimalAnswer(val, answer.value);
|
||||
@@ -68,8 +113,8 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
break;
|
||||
}
|
||||
case "integer": {
|
||||
const val = parseInt(userAnswer.value || "");
|
||||
if (isNaN(val)) {
|
||||
const val = parseStrictSignedInt(userAnswer.value || "");
|
||||
if (val === null) {
|
||||
res = { correct: false, message: "Enter a valid number" };
|
||||
} else {
|
||||
res = checkIntegerAnswer(val, answer.value);
|
||||
@@ -86,9 +131,9 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
break;
|
||||
}
|
||||
case "standardForm": {
|
||||
const coeff = parseFloat(userAnswer.coefficient || "");
|
||||
const exp = parseInt(userAnswer.exponent || "");
|
||||
if (isNaN(coeff) || isNaN(exp)) {
|
||||
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 &&
|
||||
@@ -111,10 +156,13 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
}
|
||||
|
||||
function nextHint() {
|
||||
if (!showHint) {
|
||||
setShowHint(true);
|
||||
return;
|
||||
}
|
||||
if (hintIndex < problem.hints.length - 1) {
|
||||
setHintIndex((i) => i + 1);
|
||||
}
|
||||
setShowHint(true);
|
||||
}
|
||||
|
||||
const promptHtml = katex.renderToString(problem.prompt, { throwOnError: false, displayMode: true });
|
||||
@@ -162,7 +210,7 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
denominator={userAnswer.denominator || ""}
|
||||
onNumeratorChange={(v) => setUserAnswer((a) => ({ ...a, numerator: v }))}
|
||||
onDenominatorChange={(v) => setUserAnswer((a) => ({ ...a, denominator: v }))}
|
||||
disabled={result?.correct === true}
|
||||
disabled={isFullyCorrect}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -170,7 +218,7 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
<DecimalInput
|
||||
value={userAnswer.value || ""}
|
||||
onChange={(v) => setUserAnswer((a) => ({ ...a, value: v }))}
|
||||
disabled={result?.correct === true}
|
||||
disabled={isFullyCorrect}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -194,7 +242,7 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
ratio: current.join(":"),
|
||||
}));
|
||||
}}
|
||||
disabled={result?.correct === true}
|
||||
disabled={isFullyCorrect}
|
||||
/>
|
||||
);
|
||||
})()}
|
||||
@@ -206,7 +254,7 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
inputMode="decimal"
|
||||
value={userAnswer.coefficient || ""}
|
||||
onChange={(e) => setUserAnswer((a) => ({ ...a, coefficient: e.target.value }))}
|
||||
disabled={result?.correct === true}
|
||||
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"
|
||||
@@ -217,7 +265,7 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
inputMode="numeric"
|
||||
value={userAnswer.exponent || ""}
|
||||
onChange={(e) => setUserAnswer((a) => ({ ...a, exponent: e.target.value }))}
|
||||
disabled={result?.correct === true}
|
||||
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"
|
||||
@@ -228,7 +276,7 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
|
||||
{/* Actions */}
|
||||
<div className="mb-4 flex flex-wrap justify-center gap-3">
|
||||
{!result?.correct && (
|
||||
{!isFullyCorrect && (
|
||||
<>
|
||||
<Button variant={unitColor} size="sm" onClick={checkAnswer}>
|
||||
Check Answer
|
||||
@@ -241,7 +289,7 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{result?.correct && (
|
||||
{isFullyCorrect && (
|
||||
<Button variant={unitColor} size="sm" onClick={() => generateNew(difficulty)}>
|
||||
Next Problem
|
||||
</Button>
|
||||
@@ -297,7 +345,10 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
{problem.steps.map((step, i) => (
|
||||
<div key={i} className="flex gap-2 text-sm">
|
||||
<span className="shrink-0 font-bold text-muted">{i + 1}.</span>
|
||||
<span>{step.explanation}</span>
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<div>{step.explanation}</div>
|
||||
{step.math && <MathDisplay math={step.math} className="text-lg" />}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
Reference in New Issue
Block a user