form 1 topics added

This commit is contained in:
2026-07-05 11:12:45 -04:00
parent a614532810
commit a23fa53772
58 changed files with 7089 additions and 475 deletions

View File

@@ -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>