Files
cabrits-math/components/explorers/identity-inverse-explorer.tsx
2026-07-05 11:12:45 -04:00

332 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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";
type Mode = "identity" | "inverse" | "zero";
type Op = "add" | "multiply";
interface Step {
label: string;
math: string;
danger?: boolean;
}
function buildIdentitySteps(n: number, op: Op): Step[] {
if (op === "add") {
return [
{ label: "The identity for addition is 0 — it leaves a number unchanged.", math: `${n} + 0` },
{ label: "Adding 0 changes nothing.", math: `${n} + 0 = ${n}` },
{ label: "So 0 is the identity element for addition.", math: `n + 0 = n` },
];
}
return [
{ label: "The identity for multiplication is 1 — it leaves a number unchanged.", math: `${n} \\times 1` },
{ label: "Multiplying by 1 changes nothing.", math: `${n} \\times 1 = ${n}` },
{ label: "So 1 is the identity element for multiplication.", math: `n \\times 1 = n` },
];
}
function buildInverseSteps(n: number, op: Op): Step[] {
if (op === "add") {
const inv = -n;
const invText = inv < 0 ? `(${inv})` : `${inv}`;
return [
{ label: "The additive inverse is the number that brings you back to the identity, 0.", math: `${n} + \\square = 0` },
{ label: `Flip the sign of ${n} to get its additive inverse.`, math: `\\text{inverse of } ${n} = ${inv}` },
{ label: "Check: adding a number and its inverse gives 0.", math: `${n} + ${invText} = 0` },
];
}
// multiplicative inverse (reciprocal)
const absN = Math.abs(n);
const recip = n < 0 ? `-\\frac{1}{${absN}}` : `\\frac{1}{${absN}}`;
const recipParen = n < 0 ? `\\left(-\\frac{1}{${absN}}\\right)` : `\\frac{1}{${absN}}`;
return [
{ label: "The multiplicative inverse (reciprocal) brings you back to the identity, 1.", math: `${n} \\times \\square = 1` },
{ label: `Write ${n} as a fraction and flip it. The sign stays in front.`, math: `\\text{inverse of } ${n} = ${recip}` },
{ label: "Check: a number times its reciprocal gives 1.", math: `${n} \\times ${recipParen} = 1` },
];
}
function buildZeroSteps(n: number): Step[] {
return [
{ label: "Multiplying any number by zero always gives zero.", math: `${n} \\times 0 = 0` },
{ label: "Zero shared into any number of groups is still zero.", math: `0 \\div ${n === 0 ? 5 : n} = 0` },
{
label: "But you can never divide BY zero — there is no answer. It is undefined.",
math: `${n} \\div 0 = \\text{undefined}`,
danger: true,
},
];
}
// ── Quick practice ───────────────────────────────────────────────────────────
interface PracticeQ {
prompt: string;
answer: number;
}
function randInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function makeQuestion(): PracticeQ {
const roll = Math.floor(Math.random() * 5);
if (roll === 0) {
const n = randInt(-12, 12) || 3;
return { prompt: `What is the additive inverse of ${n}?`, answer: -n };
}
if (roll === 1) {
const n = randInt(1, 20);
return { prompt: `What is ${n} × 0?`, answer: 0 };
}
if (roll === 2) {
return { prompt: `What number is the identity for addition?`, answer: 0 };
}
if (roll === 3) {
return { prompt: `What number is the identity for multiplication?`, answer: 1 };
}
const n = randInt(2, 12);
return { prompt: `The multiplicative inverse of ${n} is 1 over what number?`, answer: n };
}
function QuickPractice() {
const [q, setQ] = useState<PracticeQ>(() => makeQuestion());
const [val, setVal] = useState("");
const [feedback, setFeedback] = useState<"correct" | "incorrect" | null>(null);
const [score, setScore] = useState({ correct: 0, total: 0 });
function check() {
const parsed = parseInt(val, 10);
if (isNaN(parsed)) return;
const correct = parsed === q.answer;
setFeedback(correct ? "correct" : "incorrect");
setScore((s) => ({ correct: s.correct + (correct ? 1 : 0), total: s.total + 1 }));
}
function next() {
setQ(makeQuestion());
setVal("");
setFeedback(null);
}
return (
<Card className="space-y-4">
<div className="flex items-center justify-between">
<p className="text-xs font-bold uppercase tracking-wider text-unit-8">Quick Practice</p>
<span className="rounded-full bg-unit-8-light px-3 py-1 text-xs font-bold text-unit-8-dark">
{score.correct}/{score.total}
</span>
</div>
<p className="text-center text-lg font-bold">{q.prompt}</p>
<div className="flex flex-wrap items-center justify-center gap-3">
<input
type="number"
value={val}
onChange={(e) => {
setVal(e.target.value);
setFeedback(null);
}}
onKeyDown={(e) => {
if (e.key === "Enter") {
if (feedback !== null) next();
else check();
}
}}
className={`w-24 rounded-lg border-2 px-3 py-2 text-center text-xl font-bold outline-none transition-colors ${
feedback === "correct"
? "border-correct bg-correct-light"
: feedback === "incorrect"
? "border-incorrect bg-incorrect-light"
: "border-border focus:border-unit-8"
}`}
placeholder="?"
aria-label="Your answer"
/>
{feedback === null ? (
<button onClick={check} className="rounded-lg bg-unit-8 px-5 py-2.5 text-sm font-bold text-white hover:bg-unit-8-dark">
Check
</button>
) : (
<button onClick={next} className="rounded-lg bg-foreground px-5 py-2.5 text-sm font-bold text-background hover:bg-foreground/80">
Next
</button>
)}
</div>
{feedback === "correct" && <p className="text-center text-sm font-bold text-correct">Correct!</p>}
{feedback === "incorrect" && (
<p className="text-center text-sm font-bold text-incorrect">Not quite the answer is {q.answer}.</p>
)}
</Card>
);
}
// ── Main explorer ────────────────────────────────────────────────────────────
export function IdentityInverseExplorer() {
const [mode, setMode] = useState<Mode>("identity");
const [op, setOp] = useState<Op>("add");
const [input, setInput] = useState("3");
const [error, setError] = useState("");
const [steps, setSteps] = useState<Step[] | null>(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 switchMode(m: Mode) {
setMode(m);
setError("");
reset();
}
function handleGo() {
setError("");
const n = parseInt(input, 10);
if (isNaN(n)) return setError("Enter a valid whole number.");
if (Math.abs(n) > 100) return setError("Keep the number between -100 and 100.");
if (mode === "inverse" && op === "multiply" && n === 0) {
return setError("Zero has no multiplicative inverse — try another number.");
}
if (mode === "identity") setSteps(buildIdentitySteps(n, op));
else if (mode === "inverse") setSteps(buildInverseSteps(n, op));
else setSteps(buildZeroSteps(n));
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 showOp = mode !== "zero";
return (
<div className="space-y-4">
{/* Mode toggle */}
<div className="flex flex-wrap gap-2">
{(["identity", "inverse", "zero"] as Mode[]).map((m) => (
<button
key={m}
onClick={() => switchMode(m)}
className={`rounded-full border-2 px-4 py-2 text-sm font-semibold transition-colors ${
mode === m
? "border-unit-8 bg-unit-8 text-white"
: "border-unit-8/40 text-unit-8 hover:bg-unit-8-light"
}`}
>
{m === "identity" ? "Identity" : m === "inverse" ? "Inverse" : "Zero Facts"}
</button>
))}
</div>
{/* Operation tabs */}
{showOp && (
<div className="flex flex-wrap gap-2">
{(["add", "multiply"] as Op[]).map((o) => (
<button
key={o}
onClick={() => {
setOp(o);
reset();
}}
className={`rounded-full border-2 px-4 py-2 text-sm font-semibold transition-colors ${
op === o
? "border-unit-8-dark bg-unit-8-dark text-white"
: "border-unit-8/30 text-unit-8-dark hover:bg-unit-8-light"
}`}
>
{o === "add" ? "Addition" : "Multiplication"}
</button>
))}
</div>
)}
{/* Input card */}
<Card>
<p className="mb-3 text-xs font-bold uppercase tracking-wider text-unit-8">
{mode === "identity"
? "See how the identity leaves a number unchanged"
: mode === "inverse"
? "Find the inverse of a number"
: "Explore what happens with zero"}
</p>
<div className="flex flex-wrap items-center gap-3">
<input
type="number"
value={input}
onChange={(e) => setInput(e.target.value)}
className="w-24 rounded-lg border-2 border-border bg-surface px-3 py-2 text-center text-lg font-bold outline-none focus:border-unit-8"
aria-label="Number"
/>
<button
onClick={handleGo}
className="rounded-lg bg-unit-8 px-6 py-2.5 text-sm font-bold text-white transition-colors hover:bg-unit-8-dark"
>
Go
</button>
</div>
{error && <p className="mt-2 text-sm text-incorrect">{error}</p>}
</Card>
{/* Display card */}
<Card className="flex min-h-[220px] flex-col items-center justify-center gap-4 p-6">
{!step ? (
<p className="text-muted/50">
Enter a number above and click <strong>Go</strong>
</p>
) : (
<>
<p className="text-center text-sm font-medium text-muted">{step.label}</p>
<MathDisplay
math={step.math}
className={`text-3xl ${step.danger ? "text-incorrect" : ""}`}
/>
</>
)}
</Card>
{/* Controls */}
<Card className="p-4">
<StepControls
currentStep={currentStep + 1}
totalSteps={steps?.length ?? 0}
isPlaying={isPlaying}
onStepForward={stepForward}
onStepBack={stepBack}
onTogglePlay={togglePlay}
onReset={reset}
canStepForward={!!steps && !done}
canStepBack={!!steps && currentStep > 0}
/>
</Card>
{/* Quick practice */}
<ClientOnly fallback={<Card className="min-h-[220px]" />}>
<QuickPractice />
</ClientOnly>
</div>
);
}