form 1 topics added
This commit is contained in:
@@ -11,6 +11,8 @@ import {
|
||||
parseStrictDecimal,
|
||||
parseStrictSignedDecimal,
|
||||
parseStrictSignedInt,
|
||||
wrong,
|
||||
SOLVED,
|
||||
type AnswerResult,
|
||||
} from "@/lib/math/validation";
|
||||
import { FractionInput, DecimalInput, RatioInput } from "./fraction-input";
|
||||
@@ -22,7 +24,7 @@ import katex from "katex";
|
||||
interface PracticeSectionProps {
|
||||
title: string;
|
||||
generator: (difficulty: Difficulty) => MathProblem;
|
||||
unitColor?: "unit-1" | "unit-2" | "unit-3" | "unit-4";
|
||||
unitColor?: "unit-1" | "unit-2" | "unit-3" | "unit-4" | "unit-5" | "unit-6" | "unit-8";
|
||||
}
|
||||
|
||||
type UnitColor = NonNullable<PracticeSectionProps["unitColor"]>;
|
||||
@@ -32,6 +34,9 @@ const activeDifficultyStyle: Record<UnitColor, string> = {
|
||||
"unit-2": "bg-unit-2 text-white",
|
||||
"unit-3": "bg-unit-3 text-white",
|
||||
"unit-4": "bg-unit-4 text-white",
|
||||
"unit-5": "bg-unit-5 text-white",
|
||||
"unit-6": "bg-unit-6 text-white",
|
||||
"unit-8": "bg-unit-8 text-white",
|
||||
};
|
||||
|
||||
const RECENT_PROBLEM_LIMIT = 8;
|
||||
@@ -54,7 +59,11 @@ 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 isFullyCorrect = result?.status === "solved";
|
||||
// Per-problem scoring guards: a problem is counted at most once, when solved,
|
||||
// and only counts as "correct" if solved with no earlier wrong attempt or peek.
|
||||
const missedCurrentRef = useRef(false);
|
||||
const scoredCurrentRef = useRef(false);
|
||||
|
||||
const generateUniqueProblem = useCallback((diff: Difficulty): MathProblem => {
|
||||
const recentProblemKeys = recentProblemKeysRef.current ?? [getProblemKey(problem)];
|
||||
@@ -86,6 +95,8 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
setShowHint(false);
|
||||
setHintIndex(0);
|
||||
setShowSolution(false);
|
||||
missedCurrentRef.current = false;
|
||||
scoredCurrentRef.current = false;
|
||||
}, [generateUniqueProblem]);
|
||||
|
||||
function checkAnswer() {
|
||||
@@ -97,7 +108,7 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
const num = parseStrictSignedInt(userAnswer.numerator || "");
|
||||
const den = parseStrictSignedInt(userAnswer.denominator || "");
|
||||
if (num === null || den === null) {
|
||||
res = { correct: false, message: "Enter valid numbers" };
|
||||
res = wrong("Enter valid numbers");
|
||||
} else {
|
||||
res = checkFractionAnswer(num, den, answer.numerator, answer.denominator);
|
||||
}
|
||||
@@ -106,7 +117,7 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
case "decimal": {
|
||||
const val = parseStrictSignedDecimal(userAnswer.value || "");
|
||||
if (val === null) {
|
||||
res = { correct: false, message: "Enter a valid number" };
|
||||
res = wrong("Enter a valid number");
|
||||
} else {
|
||||
res = checkDecimalAnswer(val, answer.value);
|
||||
}
|
||||
@@ -115,7 +126,7 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
case "integer": {
|
||||
const val = parseStrictSignedInt(userAnswer.value || "");
|
||||
if (val === null) {
|
||||
res = { correct: false, message: "Enter a valid number" };
|
||||
res = wrong("Enter a valid number");
|
||||
} else {
|
||||
res = checkIntegerAnswer(val, answer.value);
|
||||
}
|
||||
@@ -124,7 +135,7 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
case "ratio": {
|
||||
const parts = (userAnswer.ratio || "").split(":").map((p) => parseInt(p.trim()));
|
||||
if (parts.some(isNaN)) {
|
||||
res = { correct: false, message: "Enter valid ratio parts" };
|
||||
res = wrong("Enter valid ratio parts");
|
||||
} else {
|
||||
res = checkRatioAnswer(parts, answer.parts);
|
||||
}
|
||||
@@ -134,27 +145,44 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
const coeff = parseStrictDecimal(userAnswer.coefficient || "");
|
||||
const exp = parseStrictSignedInt(userAnswer.exponent || "");
|
||||
if (coeff === null || exp === null) {
|
||||
res = { correct: false, message: "Enter valid numbers" };
|
||||
res = wrong("Enter valid numbers");
|
||||
} else if (
|
||||
Math.abs(coeff - answer.coefficient) < 0.01 &&
|
||||
exp === answer.exponent
|
||||
) {
|
||||
res = { correct: true, simplified: true };
|
||||
res = SOLVED;
|
||||
} else {
|
||||
res = { correct: false, message: "That's not quite right. Try again!" };
|
||||
res = wrong("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 }));
|
||||
|
||||
// Score each problem exactly once, when it is solved. A wrong attempt (or an
|
||||
// "unsimplified" nudge that never gets finished) never scores on its own; it
|
||||
// just marks the current problem as missed so the eventual solve counts as
|
||||
// incorrect. This makes the score "problems solved / first-time right" and
|
||||
// makes double-counting impossible.
|
||||
if (res.status === "wrong") {
|
||||
missedCurrentRef.current = true;
|
||||
} else if (res.status === "solved" && !scoredCurrentRef.current) {
|
||||
scoredCurrentRef.current = true;
|
||||
const gotFirstTime = !missedCurrentRef.current;
|
||||
setScore((s) => ({
|
||||
correct: s.correct + (gotFirstTime ? 1 : 0),
|
||||
total: s.total + 1,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// Enter key: check the answer, or move to the next problem once it's solved.
|
||||
function submit() {
|
||||
if (isFullyCorrect) generateNew(difficulty);
|
||||
else checkAnswer();
|
||||
}
|
||||
|
||||
function nextHint() {
|
||||
if (!showHint) {
|
||||
setShowHint(true);
|
||||
@@ -210,6 +238,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 }))}
|
||||
onSubmit={submit}
|
||||
disabled={isFullyCorrect}
|
||||
/>
|
||||
)}
|
||||
@@ -218,6 +247,7 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
<DecimalInput
|
||||
value={userAnswer.value || ""}
|
||||
onChange={(v) => setUserAnswer((a) => ({ ...a, value: v }))}
|
||||
onSubmit={submit}
|
||||
disabled={isFullyCorrect}
|
||||
/>
|
||||
)}
|
||||
@@ -284,7 +314,14 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
<Button variant="secondary" size="sm" onClick={nextHint}>
|
||||
Hint
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => setShowSolution(true)}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
missedCurrentRef.current = true;
|
||||
setShowSolution(true);
|
||||
}}
|
||||
>
|
||||
Show Solution
|
||||
</Button>
|
||||
</>
|
||||
@@ -304,18 +341,18 @@ export function PracticeSection({ title, generator, unitColor = "unit-1" }: Prac
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className={`rounded-xl px-4 py-3 text-center text-sm font-medium ${
|
||||
result.correct
|
||||
? result.simplified
|
||||
? "bg-correct-light text-correct"
|
||||
: "bg-hint-light text-hint"
|
||||
: "bg-incorrect-light text-incorrect"
|
||||
result.status === "solved"
|
||||
? "bg-correct-light text-correct"
|
||||
: result.status === "unsimplified"
|
||||
? "bg-hint-light text-hint"
|
||||
: "bg-incorrect-light text-incorrect"
|
||||
}`}
|
||||
>
|
||||
{result.correct
|
||||
? result.simplified
|
||||
? "Correct!"
|
||||
: "Correct, but can you simplify further?"
|
||||
: result.message}
|
||||
{result.status === "solved"
|
||||
? "Correct!"
|
||||
: result.status === "unsimplified"
|
||||
? "Correct, but can you simplify further?"
|
||||
: result.message}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
Reference in New Issue
Block a user