"use client"; import { useState, useCallback } from "react"; import { Card } from "@/components/ui/card"; import { StepControls } from "./step-controls"; import { MathDisplay } from "@/components/math/math-display"; import { simplify, toKatex, gcd } from "@/lib/math/fractions"; import { parseStrictDecimal, parseStrictInt } from "@/lib/math/validation"; type ConvertMode = "decToFrac" | "fracToDec"; interface Step { label: string; math: string; gridFilled?: number; // out of 100 for decimal grid barFilled?: number; barTotal?: number; } function buildDecToFracSteps(decimal: string): Step[] { const steps: Step[] = []; const val = parseFloat(decimal); steps.push({ label: `Convert ${decimal} to a fraction`, math: `${decimal} = \\;?`, gridFilled: Math.round(val * 100), }); // Count decimal places const parts = decimal.split("."); const decPlaces = parts[1]?.length || 0; const denominator = Math.pow(10, decPlaces); const numerator = Math.round(val * denominator); steps.push({ label: `${decPlaces} decimal place${decPlaces !== 1 ? "s" : ""} → denominator is ${denominator}`, math: `${decimal} = ${toKatex(numerator, denominator)}`, gridFilled: Math.round(val * 100), barFilled: numerator, barTotal: denominator, }); // Simplify const [sn, sd] = simplify(numerator, denominator); if (sn !== numerator || sd !== denominator) { const g = gcd(numerator, denominator); steps.push({ label: `Simplify by dividing by GCD = ${g}`, math: `${toKatex(numerator, denominator)} = ${toKatex(sn, sd)}`, gridFilled: Math.round(val * 100), barFilled: sn, barTotal: sd, }); } steps.push({ label: `Result: ${decimal} = ${sn}/${sd}`, math: `${decimal} = ${toKatex(sn, sd)}`, gridFilled: Math.round(val * 100), barFilled: sn, barTotal: sd, }); return steps; } function buildFracToDecSteps(num: number, den: number): Step[] { const steps: Step[] = []; steps.push({ label: `Convert ${num}/${den} to a decimal`, math: `${toKatex(num, den)} = \\;?`, barFilled: num, barTotal: den, }); // Step: Perform the division const result = num / den; const resultStr = Number.isInteger(result) ? result.toString() : result.toFixed(6).replace(/0+$/, ""); steps.push({ label: `Divide numerator by denominator`, math: `${num} \\div ${den} = ${resultStr}`, barFilled: num, barTotal: den, gridFilled: Math.min(100, Math.round(result * 100)), }); // Check for terminating vs repeating let tempDen = den; const [, simpDen] = simplify(num, den); tempDen = simpDen; // Remove factors of 2 and 5 while (tempDen % 2 === 0) tempDen /= 2; while (tempDen % 5 === 0) tempDen /= 5; const isTerminating = tempDen === 1; steps.push({ label: isTerminating ? `This is a terminating decimal` : `This is a recurring decimal`, math: `${toKatex(num, den)} = ${resultStr}${!isTerminating ? "..." : ""}`, barFilled: num, barTotal: den, gridFilled: Math.min(100, Math.round(result * 100)), }); return steps; } function DecimalGrid({ filled }: { filled: number }) { return (
{Array.from({ length: 100 }, (_, i) => (
))}
); } function FractionBarSmall({ filled, total }: { filled: number; total: number }) { const segments = Math.min(total, 20); const fillCount = Math.min(filled, segments); return (
{Array.from({ length: segments }, (_, i) => (
))}
); } export function ConversionExplorer() { const [mode, setMode] = useState("decToFrac"); const [decInput, setDecInput] = useState("0.75"); const [numInput, setNumInput] = useState("3"); const [denInput, setDenInput] = useState("8"); 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; function handleGo() { setError(""); try { let s: Step[]; if (mode === "decToFrac") { const val = parseStrictDecimal(decInput); if (val === null || val < 0 || val >= 10) { setError("Enter a valid decimal between 0 and 10."); return; } s = buildDecToFracSteps(decInput.trim()); } else { const n = parseStrictInt(numInput); const d = parseStrictInt(denInput); if (n === null || d === null || d === 0) { setError("Enter a valid fraction (denominator ≠ 0)."); return; } s = buildFracToDecSteps(n, d); } setSteps(s); setCurrentStep(0); setIsPlaying(false); } catch { setError("Could not compute. 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 reset = useCallback(() => { setSteps(null); setCurrentStep(0); setIsPlaying(false); }, []); const step = steps ? steps[currentStep] : null; return (
{/* Mode tabs */}
{/* Input */}

{mode === "decToFrac" ? "Enter a decimal" : "Enter a fraction"}

{mode === "decToFrac" ? ( setDecInput(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleGo()} placeholder="e.g. 0.75" autoComplete="off" className="max-w-[180px] rounded-lg border-2 border-border bg-surface px-3.5 py-2.5 text-lg font-medium outline-none transition-colors focus:border-unit-3" /> ) : (
setNumInput(e.target.value)} className="w-14 rounded-lg border-2 border-border bg-surface px-2 py-1.5 text-center text-lg font-bold outline-none focus:border-unit-3" aria-label="Numerator" />
setDenInput(e.target.value)} className="w-14 rounded-lg border-2 border-border bg-surface px-2 py-1.5 text-center text-lg font-bold outline-none focus:border-unit-3" aria-label="Denominator" />
)}
{error &&

{error}

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

Enter a value above and click Convert

) : ( <>

{step.label}

{step.gridFilled !== undefined && (
{step.gridFilled}/100
)} {step.barFilled !== undefined && step.barTotal !== undefined && (
{step.barFilled}/{step.barTotal}
)}
)}
{/* Controls */} 0} /> {/* Result */} {!done || !steps ? (

Result will appear here when steps are complete

) : ( <>

Answer

)}
); }