"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 */} }>
); }