"use client"; import { useState, useCallback } from "react"; import { Card } from "@/components/ui/card"; import { ClientOnly } from "@/components/ui/client-only"; import { StepControls } from "./step-controls"; import { MathDisplay } from "@/components/math/math-display"; import { FractionInput } from "@/components/practice/fraction-input"; import { Fraction, lcm } from "@/lib/math/fractions"; import { checkFractionAnswer } from "@/lib/math/validation"; type Op = "+" | "-" | "\\times" | "\\div"; interface Step { label: string; math: string; } const OP_LABEL: Record = { "+": "+", "-": "−", "\\times": "×", "\\div": "÷" }; /** Wrap a negative operand in parentheses so "a + (-b)" reads clearly. */ function operand(f: Fraction): string { return f.n < 0 ? `\\left(${f.toSignedKatex()}\\right)` : f.toSignedKatex(); } function signWord(f: Fraction): string { return f.n < 0 ? "negative" : "positive"; } function buildAddSubtractSteps(a: Fraction, b: Fraction, op: "+" | "-"): Step[] { const opSym = op; const steps: Step[] = [ { label: "Start with the expression.", math: `${a.toSignedKatex()} ${opSym} ${operand(b)}`, }, ]; const lcd = lcm(a.d, b.d); let an = a.n; let bn = b.n; if (a.d !== b.d) { an = a.n * (lcd / a.d); bn = b.n * (lcd / b.d); steps.push({ label: `The denominators are different. Find the common denominator: the LCM of ${a.d} and ${b.d} is ${lcd}.`, math: `\\text{LCD} = ${lcd}`, }); steps.push({ label: "Rewrite both fractions over the common denominator (the sign travels with the numerator).", math: `${a.toSignedKatex()} = ${new Fraction(an, lcd).toSignedKatex()} \\quad ${b.toSignedKatex()} = ${new Fraction(bn, lcd).toSignedKatex()}`, }); } const rawNum = op === "+" ? an + bn : an - bn; const bnShown = bn < 0 ? `\\left(${bn}\\right)` : `${bn}`; steps.push({ label: op === "+" ? "Add the numerators using the integer rules, keeping the common denominator." : "Subtract the numerators using the integer rules (subtracting a negative adds), keeping the common denominator.", math: `\\frac{${an} ${opSym} ${bnShown}}{${lcd}} = \\frac{${rawNum}}{${lcd}}`, }); const raw = new Fraction(rawNum, lcd); const result = raw.simplified(); if (result.n !== raw.n || result.d !== raw.d) { steps.push({ label: "Simplify to lowest terms.", math: `${raw.toSignedKatex()} = ${result.toSignedKatex()}`, }); } steps.push({ label: `The answer is ${result.n < 0 ? "negative" : "positive"}.`, math: `${a.toSignedKatex()} ${opSym} ${operand(b)} = ${result.toSignedKatex()}`, }); return steps; } function buildMultiplyDivideSteps(a: Fraction, b: Fraction, op: "\\times" | "\\div"): Step[] { const steps: Step[] = [ { label: "Start with the expression.", math: `${a.toSignedKatex()} ${op} ${operand(b)}`, }, ]; let working = b; if (op === "\\div") { working = new Fraction(b.d, b.n); // reciprocal keeps the sign steps.push({ label: "Dividing by a fraction is the same as multiplying by its reciprocal — keep, change, flip.", math: `${a.toSignedKatex()} \\times ${operand(working)}`, }); } const negatives = (a.n < 0 ? 1 : 0) + (working.n < 0 ? 1 : 0); const resultNegative = negatives % 2 === 1; steps.push({ label: `Decide the sign: ${signWord(a)} and ${signWord(working)}. ${ negatives === 1 ? "Different signs give a NEGATIVE answer." : "The signs match, so the answer is POSITIVE." }`, math: `(${a.n < 0 ? "-" : "+"}) \\times (${working.n < 0 ? "-" : "+"}) = (${resultNegative ? "-" : "+"})`, }); const absNum = Math.abs(a.n) * Math.abs(working.n); const den = a.d * working.d; steps.push({ label: "Multiply the numerators, multiply the denominators, then apply the sign.", math: `\\frac{${Math.abs(a.n)} \\times ${Math.abs(working.n)}}{${a.d} \\times ${working.d}} = ${new Fraction(resultNegative ? -absNum : absNum, den).toSignedKatex()}`, }); const raw = new Fraction(resultNegative ? -absNum : absNum, den); const result = raw.simplified(); if (result.n !== raw.n || result.d !== raw.d) { steps.push({ label: "Simplify to lowest terms.", math: `${raw.toSignedKatex()} = ${result.toSignedKatex()}`, }); } steps.push({ label: "Final answer.", math: `${a.toSignedKatex()} ${op} ${operand(b)} = ${result.toSignedKatex()}`, }); return steps; } // ── Quick practice ─────────────────────────────────────────────────────────── function randInt(min: number, max: number) { return Math.floor(Math.random() * (max - min + 1)) + min; } /** A proper-fraction numerator with a random sign: magnitude 1..den-1. */ function properSigned(den: number): number { const mag = randInt(1, Math.max(1, den - 1)); return Math.random() < 0.5 ? -mag : mag; } interface PracticeProblem { a: Fraction; b: Fraction; op: Op; answer: Fraction; } function makeProblem(): PracticeProblem { const op = (["+", "-", "\\times", "\\div"] as Op[])[randInt(0, 3)]; const d1 = randInt(2, 6); const d2 = randInt(2, 6); let n1 = properSigned(d1); let n2 = properSigned(d2); // This is the negative-fractions topic — make sure at least one operand is negative. if (n1 > 0 && n2 > 0) { if (Math.random() < 0.5) n1 = -n1; else n2 = -n2; } const a = new Fraction(n1, d1); const b = new Fraction(n2, d2); let answer: Fraction; if (op === "+") answer = a.plus(b).simplified(); else if (op === "-") answer = a.minus(b).simplified(); else if (op === "\\times") answer = a.times(b).simplified(); else answer = a.dividedBy(b).simplified(); return { a, b, op, answer }; } function QuickPractice() { const [problem, setProblem] = useState(() => makeProblem()); const [num, setNum] = useState(""); const [den, setDen] = useState(""); const [status, setStatus] = useState<"solved" | "unsimplified" | "wrong" | null>(null); const [score, setScore] = useState({ correct: 0, total: 0 }); const [missed, setMissed] = useState(false); const [scored, setScored] = useState(false); function check() { const n = parseInt(num, 10); const d = parseInt(den, 10); if (isNaN(n) || isNaN(d)) return; const res = checkFractionAnswer(n, d, problem.answer.n, problem.answer.d); setStatus(res.status); if (res.status === "wrong") { setMissed(true); } else if (res.status === "solved" && !scored) { setScored(true); setScore((s) => ({ correct: s.correct + (missed ? 0 : 1), total: s.total + 1 })); } } function next() { setProblem(makeProblem()); setNum(""); setDen(""); setStatus(null); setMissed(false); setScored(false); } return (

Quick Practice

{score.correct}/{score.total}

Work it out and give your answer in its simplest form.

{status === "solved" ? ( ) : ( )}
{status === "solved" &&

Correct!

} {status === "unsimplified" && (

Right value — now write it in its simplest form.

)} {status === "wrong" && (

Not quite. Watch the signs, then try again.

)}
); } // ── Main explorer ──────────────────────────────────────────────────────────── export function NegativeFractionsExplorer() { const [op, setOp] = useState("+"); const [n1, setN1] = useState("-1"); const [d1, setD1] = useState("2"); const [n2, setN2] = useState("3"); const [d2, setD2] = useState("4"); const [error, setError] = useState(""); const [steps, setSteps] = useState(null); const [currentStep, setCurrentStep] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const done = steps ? currentStep >= steps.length - 1 : false; const reset = useCallback(() => { setSteps(null); setCurrentStep(0); setIsPlaying(false); }, []); function handleGo() { setError(""); const pn1 = parseInt(n1, 10); const pd1 = parseInt(d1, 10); const pn2 = parseInt(n2, 10); const pd2 = parseInt(d2, 10); if ([pn1, pd1, pn2, pd2].some((v) => isNaN(v))) return setError("Fill in every box with a whole number."); if (pd1 === 0 || pd2 === 0) return setError("A denominator cannot be zero."); if ([pn1, pd1, pn2, pd2].some((v) => Math.abs(v) > 20)) return setError("Keep the numbers between -20 and 20."); if (op === "\\div" && pn2 === 0) return setError("You cannot divide by zero."); const a = new Fraction(pn1, pd1); const b = new Fraction(pn2, pd2); setSteps( op === "+" || op === "-" ? buildAddSubtractSteps(a, b, op) : buildMultiplyDivideSteps(a, b, op), ); setCurrentStep(0); setIsPlaying(false); } const stepForward = useCallback(() => { if (!steps || currentStep >= steps.length - 1) return; setCurrentStep((s) => s + 1); if (currentStep + 1 >= steps.length - 1) setIsPlaying(false); }, [steps, currentStep]); const stepBack = useCallback(() => { if (currentStep <= 0) return; setCurrentStep((s) => s - 1); }, [currentStep]); const togglePlay = useCallback(() => setIsPlaying((p) => !p), []); const step = steps ? steps[currentStep] : null; const fieldClass = "w-16 rounded-lg border-2 border-border bg-surface px-2 py-1.5 text-center text-lg font-bold outline-none focus:border-unit-5"; return (
{/* Operation selector */}
{(["+", "-", "\\times", "\\div"] as Op[]).map((o) => ( ))}
{/* Input card */}

Enter two fractions (numerators can be negative)

setN1(e.target.value)} className={fieldClass} aria-label="First numerator" />
setD1(e.target.value)} className={fieldClass} aria-label="First denominator" />
{OP_LABEL[op]}
setN2(e.target.value)} className={fieldClass} aria-label="Second numerator" />
setD2(e.target.value)} className={fieldClass} aria-label="Second denominator" />
{error &&

{error}

} {/* Display card */} {!step ? (

Enter two fractions above and click Go

) : ( <>

{step.label}

)}
{/* Controls */} 0} /> {/* Quick practice */} }>
); }