"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 { shuffle } from "@/lib/utils";
type Mode = "number-line" | "order" | "compare";
type Direction = "asc" | "desc";
type Tone = "start" | "highlight" | "muted";
const MIN_VAL = -20;
const MAX_VAL = 20;
const RANGE = MAX_VAL - MIN_VAL;
interface Marker {
value: number;
label?: string;
tone?: Tone;
}
interface OrderChips {
pool: number[];
ordered: number[];
total: number;
}
interface Step {
label: string;
math: string;
markers: Marker[];
order?: OrderChips;
}
// Real-world contexts for the "Number Line" mode
const CONTEXTS = {
number: {
emoji: "🔢",
label: "Number",
describe: (n: number) =>
n === 0
? "0 is neither positive nor negative — it separates the two sides."
: `${n} is ${Math.abs(n)} step${Math.abs(n) === 1 ? "" : "s"} to the ${n < 0 ? "LEFT" : "RIGHT"} of zero.`,
},
thermometer: {
emoji: "🌡️",
label: "Temperature",
describe: (n: number) =>
n === 0
? "0 °C is freezing point — the boundary between warm and cold."
: `${n} °C means ${Math.abs(n)} degree${Math.abs(n) === 1 ? "" : "s"} ${n < 0 ? "BELOW" : "ABOVE"} freezing.`,
},
money: {
emoji: "💰",
label: "Money",
describe: (n: number) =>
n === 0
? "$0 means you have nothing and owe nothing."
: n < 0
? `−$${Math.abs(n)} means you OWE $${Math.abs(n)} (an overdraft).`
: `+$${n} means you HAVE $${n} in the bank.`,
},
sea: {
emoji: "🌊",
label: "Sea level",
describe: (n: number) =>
n === 0
? "0 m is exactly at sea level."
: `${n} m means ${Math.abs(n)} metre${Math.abs(n) === 1 ? "" : "s"} ${n < 0 ? "BELOW" : "ABOVE"} sea level.`,
},
} as const;
type ContextKey = keyof typeof CONTEXTS;
function buildNumberLineSteps(n: number, ctx: ContextKey): Step[] {
const context = CONTEXTS[ctx];
const typeWord = n === 0 ? "zero" : n < 0 ? "a NEGATIVE integer" : "a POSITIVE integer";
return [
{
label: "Zero sits in the middle. Negative numbers are to its LEFT, positive numbers to its RIGHT.",
math: `\\ldots -3,\\ -2,\\ -1,\\ 0,\\ 1,\\ 2,\\ 3 \\ldots`,
markers: [{ value: 0, label: "0", tone: "start" }],
},
{
label: `Find ${n} on the number line and mark it.`,
math: `\\text{Mark } ${n}`,
markers: [
{ value: 0, tone: "muted" },
{ value: n, label: `${n}`, tone: "highlight" },
],
},
{
label: `${context.emoji} ${context.describe(n)}`,
math: `${n}`,
markers: [{ value: n, label: `${n}`, tone: "start" }],
},
{
label: `So ${n} is ${typeWord}.`,
math: n === 0 ? `0` : `${n} \\text{ is } ${n < 0 ? "<" : ">"} 0`,
markers: [{ value: n, label: `${n}`, tone: "start" }],
},
];
}
function buildOrderSteps(values: number[], direction: Direction): Step[] {
const sorted = [...values].sort((a, b) => (direction === "asc" ? a - b : b - a));
const sizeWord = direction === "asc" ? "smallest" : "largest";
const posWord = direction === "asc" ? "furthest LEFT" : "furthest RIGHT";
const total = values.length;
const steps: Step[] = [
{
label: "Here are the numbers we need to put in order.",
math: `\\{\\, ${values.join(",\\ ")} \\,\\}`,
markers: values.map((v) => ({ value: v, tone: "muted" as Tone })),
order: { pool: [...values], ordered: [], total },
},
{
label: "Place every number on the number line. Smaller numbers sit further left.",
math: `\\text{left = smaller} \\qquad \\text{right = larger}`,
markers: values.map((v) => ({ value: v, tone: "highlight" as Tone })),
order: { pool: [...values], ordered: [], total },
},
];
const pool = [...values];
const ordered: number[] = [];
for (let i = 0; i < sorted.length; i++) {
const pick = sorted[i];
pool.splice(pool.indexOf(pick), 1);
ordered.push(pick);
steps.push({
label: `The ${sizeWord} number still to place is the one ${posWord}: ${pick}.`,
math: `${ordered.join(direction === "asc" ? " < " : " > ")}${pool.length ? "\\ \\ldots" : ""}`,
markers: [
...ordered.map((v) => ({ value: v, tone: "start" as Tone })),
...pool.map((v) => ({ value: v, tone: "muted" as Tone })),
],
order: { pool: [...pool], ordered: [...ordered], total },
});
}
steps.push({
label:
direction === "asc"
? "Done! In ascending order: least → greatest."
: "Done! In descending order: greatest → least.",
math: `${sorted.join(direction === "asc" ? " < " : " > ")}`,
markers: sorted.map((v) => ({ value: v, tone: "start" as Tone })),
order: { pool: [], ordered: [...sorted], total },
});
return steps;
}
function buildCompareSteps(a: number, b: number): Step[] {
const symbol = a < b ? "<" : a > b ? ">" : "=";
const steps: Step[] = [
{
label: `Put both numbers on the number line.`,
math: `${a} \\;\\square\\; ${b}`,
markers: [
{ value: a, label: `${a}`, tone: "highlight" },
{ value: b, label: `${b}`, tone: "highlight" },
],
},
];
if (symbol === "=") {
steps.push({
label: "Both numbers land on the same spot, so they are equal.",
math: `${a} = ${b}`,
markers: [{ value: a, label: `${a}`, tone: "start" }],
});
return steps;
}
const larger = a > b ? a : b;
const smaller = a > b ? b : a;
steps.push(
{
label: `The number further RIGHT is always larger. ${larger} is to the right of ${smaller}.`,
math: `${larger} \\text{ is larger}`,
markers: [
{ value: smaller, label: `${smaller}`, tone: "muted" },
{ value: larger, label: `${larger}`, tone: "start" },
],
},
{
label: `The open mouth of the sign always points to the LARGER number.`,
math: `${a} ${symbol} ${b}`,
markers: [
{ value: a, label: `${a}`, tone: a === larger ? "start" : "muted" },
{ value: b, label: `${b}`, tone: b === larger ? "start" : "muted" },
],
},
);
return steps;
}
function NumberLine({ markers }: { markers: Marker[] }) {
return (
{/* Markers above the line */}
{markers.map((m, i) => {
const x = ((m.value - MIN_VAL) / RANGE) * 100;
const tone = m.tone ?? "highlight";
const bg =
tone === "start" ? "bg-unit-5" : tone === "muted" ? "bg-muted/70" : "bg-correct";
return (
{m.label !== undefined && (
{m.label}
)}
);
})}
{/* Number line */}
{Array.from({ length: RANGE + 1 }, (_, i) => {
const val = MIN_VAL + i;
const x = (i / RANGE) * 100;
const isZero = val === 0;
const marked = markers.some((m) => m.value === val);
return (
{(val % 5 === 0 || marked) && (
{val}
)}
);
})}
{/* Edge labels */}
← negative
positive →
);
}
function OrderChipsView({ order, direction }: { order: OrderChips; direction: Direction }) {
const emptySlots = order.total - order.ordered.length;
return (
Still to place
{order.pool.length === 0 ? (
— all placed —
) : (
order.pool.map((v, i) => (
{v}
))
)}
Ordered ({direction === "asc" ? "small → large" : "large → small"})
{order.ordered.map((v, i) => (
{i > 0 && {direction === "asc" ? "<" : ">"}}
{v}
))}
{Array.from({ length: emptySlots }, (_, i) => (
{(order.ordered.length > 0 || i > 0) && (
{direction === "asc" ? "<" : ">"}
)}
))}
);
}
// ── Quick practice ───────────────────────────────────────────────────────────
const SEED_PAIRS: [number, number][] = [
[-5, 5],
[4, -16],
[-6, -11],
[0, -3],
[8, -1],
];
function randInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function makePair(): [number, number] {
if (Math.random() < 0.4) return SEED_PAIRS[Math.floor(Math.random() * SEED_PAIRS.length)];
const a = randInt(-15, 15);
let b = randInt(-15, 15);
if (a === b) b = a + 1;
return [a, b];
}
function makeOrderValues(): number[] {
const set = new Set();
while (set.size < 4) set.add(randInt(-12, 12));
return shuffle([...set]);
}
function ComparePractice({ onScore }: { onScore: (correct: boolean) => void }) {
const [pair, setPair] = useState<[number, number]>(() => makePair());
const [feedback, setFeedback] = useState<"correct" | "incorrect" | null>(null);
const [a, b] = pair;
const answer = a < b ? "<" : ">";
function pick(sign: "<" | ">") {
if (feedback === "correct") return;
const correct = sign === answer;
setFeedback(correct ? "correct" : "incorrect");
if (correct) onScore(true);
else onScore(false);
}
function next() {
setPair(makePair());
setFeedback(null);
}
return (
Which sign makes it true?
{a}
?
{b}
{(["<", ">"] as const).map((sign) => (
))}
{feedback === "correct" && (
Correct! {a} {answer} {b}
)}
{feedback === "incorrect" && (
Not quite — remember, further right means larger. Try again.
)}
);
}
function OrderPractice({ onScore }: { onScore: (correct: boolean) => void }) {
const [values, setValues] = useState(() => makeOrderValues());
const [placed, setPlaced] = useState([]);
const [wrong, setWrong] = useState(null);
const [mistakes, setMistakes] = useState(0);
const [done, setDone] = useState(false);
const sorted = [...values].sort((a, b) => a - b);
const nextExpected = sorted[placed.length];
function clickChip(v: number) {
if (done || placed.includes(v)) return;
if (v === nextExpected) {
const nextPlaced = [...placed, v];
setPlaced(nextPlaced);
setWrong(null);
if (nextPlaced.length === sorted.length) {
setDone(true);
onScore(mistakes === 0);
}
} else {
setWrong(v);
setMistakes((m) => m + 1);
setTimeout(() => setWrong((w) => (w === v ? null : w)), 500);
}
}
function next() {
setValues(makeOrderValues());
setPlaced([]);
setWrong(null);
setMistakes(0);
setDone(false);
}
return (
Click the numbers in ascending order (smallest first).
{values.map((v) => {
const isPlaced = placed.includes(v);
const isWrong = wrong === v;
return (
);
})}
{placed.map((v, i) => (
{i > 0 && {"<"}}
{v}
))}
{placed.length < sorted.length && …}
{done && (
{mistakes === 0 ? "Perfect ordering!" : `Ordered — with ${mistakes} slip${mistakes === 1 ? "" : "s"}.`}
)}
);
}
function QuickPractice() {
const [kind, setKind] = useState<"compare" | "order">("compare");
const [score, setScore] = useState({ correct: 0, total: 0 });
const record = useCallback((correct: boolean) => {
setScore((s) => ({ correct: s.correct + (correct ? 1 : 0), total: s.total + 1 }));
}, []);
return (
{(["compare", "order"] as const).map((k) => (
))}
{score.correct}/{score.total}
{kind === "compare" ? (
) : (
)}
);
}
// ── Main explorer ────────────────────────────────────────────────────────────
export function IntegerOrderCompareExplorer() {
const [mode, setMode] = useState("number-line");
// number-line mode
const [nlInput, setNlInput] = useState("-7");
const [context, setContext] = useState("thermometer");
// order mode
const [orderInput, setOrderInput] = useState("-8, -10, -4, -3, -6, 5");
const [direction, setDirection] = useState("asc");
// compare mode
const [cmpA, setCmpA] = useState("-5");
const [cmpB, setCmpB] = useState("5");
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 switchMode(m: Mode) {
setMode(m);
setError("");
reset();
}
function handleGo() {
setError("");
try {
if (mode === "number-line") {
const n = parseInt(nlInput, 10);
if (isNaN(n)) return setError("Enter a valid integer.");
if (n < MIN_VAL || n > MAX_VAL) return setError(`Keep the number between ${MIN_VAL} and ${MAX_VAL}.`);
setSteps(buildNumberLineSteps(n, context));
} else if (mode === "order") {
const parts = orderInput
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
const nums = parts.map((s) => parseInt(s, 10));
if (nums.some((n) => isNaN(n))) return setError("Use whole numbers separated by commas, e.g. -8, -10, 5.");
if (nums.length < 3 || nums.length > 6) return setError("Enter between 3 and 6 numbers.");
if (nums.some((n) => n < MIN_VAL || n > MAX_VAL)) return setError(`Keep every number between ${MIN_VAL} and ${MAX_VAL}.`);
setSteps(buildOrderSteps(nums, direction));
} else {
const a = parseInt(cmpA, 10);
const b = parseInt(cmpB, 10);
if (isNaN(a) || isNaN(b)) return setError("Enter two valid integers.");
if ([a, b].some((n) => n < MIN_VAL || n > MAX_VAL)) return setError(`Keep both numbers between ${MIN_VAL} and ${MAX_VAL}.`);
setSteps(buildCompareSteps(a, b));
}
setCurrentStep(0);
setIsPlaying(false);
} catch {
setError("Could not build the steps. Check your input.");
}
}
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 (
{/* Mode toggle */}
{(["number-line", "order", "compare"] as Mode[]).map((m) => (
))}
{/* Input card */}
{mode === "number-line" && (
<>
Place an integer on the number line
{(Object.keys(CONTEXTS) as ContextKey[]).map((key) => (
))}
setNlInput(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-5"
aria-label="Integer"
/>
>
)}
{mode === "order" && (
<>
Enter 3–6 integers to order
{(["asc", "desc"] as Direction[]).map((d) => (
))}
setOrderInput(e.target.value)}
className="min-w-[16rem] flex-1 rounded-lg border-2 border-border bg-surface px-3 py-2 text-center text-lg font-bold outline-none focus:border-unit-5"
aria-label="Comma-separated integers"
/>
>
)}
{mode === "compare" && (
<>
Compare two integers with < or >
setCmpA(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-5"
aria-label="First integer"
/>
?
setCmpB(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-5"
aria-label="Second integer"
/>
>
)}
{error && {error}
}
{/* Display card */}
{!step ? (
Set up the numbers above and click Go
) : (
<>
{step.label}
{step.order && }
>
)}
{/* Controls */}
0}
/>
{/* Quick practice */}
}>
);
}