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

373 lines
14 KiB
TypeScript
Raw Permalink 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 = "expand" | "index";
type IndexOp = "multiply" | "divide";
interface Step {
label: string;
math: string;
}
function repeat(base: number, count: number): string {
return Array.from({ length: count }, () => `${base}`).join(" \\times ");
}
function buildExpandSteps(base: number, exp: number): Step[] {
if (exp === 0) {
return [
{ label: "The power (index) is 0.", math: `${base}^{0}` },
{ label: "Any number raised to the power 0 is equal to 1.", math: `${base}^{0} = 1` },
];
}
if (exp === 1) {
return [
{ label: "The power (index) is 1.", math: `${base}^{1}` },
{ label: "Any number raised to the power 1 is itself.", math: `${base}^{1} = ${base}` },
];
}
const steps: Step[] = [
{
label: `The base is ${base} and the power is ${exp}: multiply the base by itself ${exp} times.`,
math: `${base}^{${exp}}`,
},
];
let running = base;
for (let k = 2; k <= exp; k++) {
running *= base;
steps.push({
label: `Multiply on another ${base}.`,
math: `${repeat(base, k)} = ${running}`,
});
}
steps.push({ label: "So the power is worked out.", math: `${base}^{${exp}} = ${running}` });
return steps;
}
function buildIndexSteps(base: number, m: number, n: number, op: IndexOp): Step[] {
if (op === "multiply") {
const total = m + n;
const value = Math.pow(base, total);
const steps: Step[] = [
{ label: "Multiplying powers of the SAME base.", math: `${base}^{${m}} \\times ${base}^{${n}}` },
{ label: "Write each power out in full.", math: `(${repeat(base, m)}) \\times (${repeat(base, n)})` },
{ label: `Count the factors: ${m} + ${n} = ${total}. When multiplying, ADD the powers.`, math: `${base}^{${m}} \\times ${base}^{${n}} = ${base}^{${m}+${n}} = ${base}^{${total}}` },
];
if (value <= 1e7) steps.push({ label: "Work out the value.", math: `${base}^{${total}} = ${value}` });
return steps;
}
// divide, m >= n
const diff = m - n;
const value = Math.pow(base, diff);
const steps: Step[] = [
{ label: "Dividing powers of the SAME base.", math: `\\frac{${base}^{${m}}}{${base}^{${n}}}` },
{ label: "Write each power out in full.", math: `\\frac{${repeat(base, m)}}{${repeat(base, n)}}` },
{ label: `Cancel the ${n} matching factor${n === 1 ? "" : "s"} from top and bottom.`, math: `${base}^{${m}} \\div ${base}^{${n}} = ${base}^{${m}-${n}} = ${base}^{${diff}}` },
];
if (value <= 1e7) steps.push({ label: "Work out the value.", math: `${base}^{${diff}} = ${value}` });
return steps;
}
// ── Quick practice ───────────────────────────────────────────────────────────
interface PracticeQ {
prompt: string;
answer: number;
}
function randInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
const SUPERSCRIPT: Record<string, string> = {
"0": "⁰", "1": "¹", "2": "²", "3": "³", "4": "⁴",
"5": "⁵", "6": "⁶", "7": "⁷", "8": "⁸", "9": "⁹",
};
function sup(n: number): string {
return String(n).split("").map((c) => SUPERSCRIPT[c] ?? c).join("");
}
function makeQuestion(): PracticeQ {
const roll = Math.floor(Math.random() * 5);
if (roll === 0) {
const base = randInt(2, 7);
const exp = randInt(2, 3);
return { prompt: `What is ${base}${sup(exp)}?`, answer: Math.pow(base, exp) };
}
if (roll === 1) {
const base = randInt(2, 9);
return { prompt: `What is ${base}${sup(0)}?`, answer: 1 };
}
if (roll === 2) {
const base = randInt(2, 4);
const a = randInt(2, 3);
const b = 1;
return { prompt: `What is ${base}${sup(a)} ${base}${sup(b)}?`, answer: Math.pow(base, a) - Math.pow(base, b) };
}
if (roll === 3) {
const base = randInt(2, 5);
const m = randInt(3, 6);
const n = randInt(1, m - 1);
return { prompt: `Simplify ${base}${sup(m)} ÷ ${base}${sup(n)}. The answer is ${base} to what power?`, answer: m - n };
}
const base = randInt(2, 6);
const m = randInt(1, 4);
const n = randInt(1, 4);
return { prompt: `Simplify ${base}${sup(m)} × ${base}${sup(n)}. The answer is ${base} to what power?`, answer: m + 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 ExponentsExplorer() {
const [mode, setMode] = useState<Mode>("expand");
const [indexOp, setIndexOp] = useState<IndexOp>("multiply");
const [base, setBase] = useState("2");
const [power, setPower] = useState("4");
const [m, setM] = useState("4");
const [n, setN] = 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(md: Mode) {
setMode(md);
setError("");
reset();
}
function handleGo() {
setError("");
const b = parseInt(base, 10);
if (isNaN(b) || b < 2 || b > 12) return setError("Use a base between 2 and 12.");
if (mode === "expand") {
const e = parseInt(power, 10);
if (isNaN(e) || e < 0 || e > 6) return setError("Use a power between 0 and 6.");
if (Math.pow(b, e) > 1_000_000) return setError("That gets too big. Try a smaller base or power.");
setSteps(buildExpandSteps(b, e));
} else {
const mm = parseInt(m, 10);
const nn = parseInt(n, 10);
if (isNaN(mm) || isNaN(nn) || mm < 1 || nn < 1 || mm > 8 || nn > 8) return setError("Use powers between 1 and 8.");
if (indexOp === "divide" && mm < nn) return setError("For division, the top power must be at least the bottom power.");
setSteps(buildIndexSteps(b, mm, nn, indexOp));
}
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;
return (
<div className="space-y-4">
{/* Mode toggle */}
<div className="flex flex-wrap gap-2">
{(["expand", "index"] as Mode[]).map((md) => (
<button
key={md}
onClick={() => switchMode(md)}
className={`rounded-full border-2 px-4 py-2 text-sm font-semibold transition-colors ${
mode === md
? "border-unit-8 bg-unit-8 text-white"
: "border-unit-8/40 text-unit-8 hover:bg-unit-8-light"
}`}
>
{md === "expand" ? "Expand a Power" : "Index Laws"}
</button>
))}
</div>
{/* Index op tabs */}
{mode === "index" && (
<div className="flex flex-wrap gap-2">
{(["multiply", "divide"] as IndexOp[]).map((o) => (
<button
key={o}
onClick={() => {
setIndexOp(o);
reset();
}}
className={`rounded-full border-2 px-4 py-2 text-sm font-semibold transition-colors ${
indexOp === 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 === "multiply" ? "Multiply (add powers)" : "Divide (subtract powers)"}
</button>
))}
</div>
)}
{/* Input card */}
<Card>
{mode === "expand" ? (
<>
<p className="mb-3 text-xs font-bold uppercase tracking-wider text-unit-8">Enter a base and a power</p>
<div className="flex flex-wrap items-center gap-3">
<input type="number" value={base} onChange={(e) => setBase(e.target.value)} className="w-20 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="Base" />
<span className="text-sm font-bold text-muted">to the power</span>
<input type="number" value={power} onChange={(e) => setPower(e.target.value)} className="w-20 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="Power" />
<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>
</>
) : (
<>
<p className="mb-3 text-xs font-bold uppercase tracking-wider text-unit-8">
Same base, two powers {indexOp === "multiply" ? "to multiply" : "to divide"}
</p>
<div className="flex flex-wrap items-center gap-3">
<span className="text-sm font-bold text-muted">base</span>
<input type="number" value={base} onChange={(e) => setBase(e.target.value)} className="w-20 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="Base" />
<span className="text-sm font-bold text-muted">powers</span>
<input type="number" value={m} onChange={(e) => setM(e.target.value)} className="w-20 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="First power" />
<span className="text-lg font-bold text-muted">{indexOp === "multiply" ? "×" : "÷"}</span>
<input type="number" value={n} onChange={(e) => setN(e.target.value)} className="w-20 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="Second power" />
<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 values 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-2xl" />
</>
)}
</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>
);
}