feat: add practice page system to guided addition tutorial

Add dynamically generated practice problems interspersed with tutorial steps:
- Practice pages with 10-20 problems based on skill level
- Problem grid layout with input fields for answers
- "Check Work" functionality with delayed feedback
- Auto-removal of correct problems, keeping incorrect ones
- Progress tracking across combined tutorial and practice steps

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-20 09:08:30 -05:00
parent 0a4022343e
commit 9adc3db966

View File

@@ -1,6 +1,6 @@
'use client'
import { useState, useCallback, useRef } from 'react'
import { useState, useCallback, useRef, useEffect } from 'react'
import { css } from '../../styled-system/css'
import { stack, hstack } from '../../styled-system/patterns'
import { AbacusReact } from '@soroban/abacus-react'
@@ -31,6 +31,22 @@ interface TutorialStep {
multiStepInstructions?: string[]
}
interface PracticeStep {
id: string
title: string
description: string
skillLevel: 'basic' | 'heaven' | 'five-complements' | 'mixed'
problemCount: number
maxTerms: number // max numbers to add in a single problem
}
interface Problem {
id: string
terms: number[]
userAnswer?: number
isCorrect?: boolean
}
const tutorialSteps: TutorialStep[] = [
// Phase 1: Basic Addition (1-4)
{
@@ -212,32 +228,32 @@ const tutorialSteps: TutorialStep[] = [
}
},
{
id: 'complement-3',
title: 'Five Complement: 1 + 3',
id: 'direct-addition-3',
title: 'Direct Addition: 1 + 3',
problem: '1 + 3',
description: 'Add 3 when you have 3 spaces. Use complement: 3 = 5 - 2',
description: 'Add 3 to 1. You have space, so add directly.',
startValue: 1,
targetValue: 4,
highlightBeads: [
{ columnIndex: 0, beadType: 'heaven' },
{ columnIndex: 0, beadType: 'earth', position: 0 },
{ columnIndex: 0, beadType: 'earth', position: 1 }
{ columnIndex: 0, beadType: 'earth', position: 1 },
{ columnIndex: 0, beadType: 'earth', position: 2 },
{ columnIndex: 0, beadType: 'earth', position: 3 }
],
expectedAction: 'multi-step',
actionDescription: 'Add heaven bead (5), then remove 2 earth beads',
actionDescription: 'Add 3 earth beads one by one',
multiStepInstructions: [
'Click the heaven bead to add 5',
'Click the first earth bead to remove it',
'Click the second earth bead to remove it'
'Click the second earth bead to add it',
'Click the third earth bead to add it',
'Click the fourth earth bead to add it'
],
tooltip: {
content: 'Five Complement: 3 = 5 - 2',
explanation: 'To add 3: add 5 (heaven bead), then subtract 2 (remove 2 earth beads)'
content: 'Direct Addition - Check Your Space',
explanation: 'You have 1 earth bead up and need to add 3 more. Since there are 4 earth positions total, you have 3 spaces available - perfect!'
},
errorMessages: {
wrongBead: 'Use complement method: add heaven, remove 2 earth beads',
wrongAction: 'Add 5, then subtract 2',
hint: 'Complement: 3 = 5 - 2, so add 5 and take away 2'
wrongBead: 'Add the earth beads directly - you have space!',
wrongAction: 'Move the earth beads UP to add them',
hint: 'No complement needed! Just add the remaining 3 earth beads directly.'
}
},
{
@@ -307,20 +323,20 @@ const tutorialSteps: TutorialStep[] = [
id: 'mixed-1',
title: 'Practice: 2 + 3',
problem: '2 + 3',
description: 'Add 3 to 2. You can add directly or use complement (3 = 5 - 2)',
description: 'Add 3 to 2. You have space, so add directly.',
startValue: 2,
targetValue: 5,
highlightBeads: [{ columnIndex: 0, beadType: 'earth', position: 2 }],
expectedAction: 'add',
actionDescription: 'Add the third earth bead directly, or use complement method',
actionDescription: 'Add the third earth bead to complete 5',
tooltip: {
content: 'Choice: Direct or Complement',
explanation: 'You have space to add directly, but complement (3 = 5 - 2) is also valid'
content: 'Direct Addition',
explanation: 'Since you have space (only 2 earth beads are up), simply add the third earth bead'
},
errorMessages: {
wrongBead: 'Either add the third earth bead or use complement method',
wrongAction: 'Add the earth bead UP or use heaven bead + remove 2',
hint: 'For 3: add earth bead OR use complement 3 = 5 - 2'
wrongBead: 'Click the highlighted third earth bead',
wrongAction: 'Move the earth bead UP to add it',
hint: 'You have space for one more earth bead - no complement needed!'
}
},
{
@@ -352,9 +368,225 @@ const tutorialSteps: TutorialStep[] = [
}
]
const practiceSteps: PracticeStep[] = [
{
id: 'practice-basic',
title: 'Practice: Basic Addition (1-4)',
description: 'Practice adding numbers 1-4 using only earth beads',
skillLevel: 'basic',
problemCount: 12,
maxTerms: 3
},
{
id: 'practice-heaven',
title: 'Practice: Heaven Bead & Simple Combinations',
description: 'Practice using the heaven bead (5) and combining it with earth beads',
skillLevel: 'heaven',
problemCount: 15,
maxTerms: 3
},
{
id: 'practice-complements',
title: 'Practice: Five Complements',
description: 'Practice using five complements when you run out of space',
skillLevel: 'five-complements',
problemCount: 20,
maxTerms: 4
},
{
id: 'practice-mixed',
title: 'Practice: Mixed Problems',
description: 'Practice all techniques together with varied problem types',
skillLevel: 'mixed',
problemCount: 18,
maxTerms: 5
}
]
// Problem generation functions
function generateBasicProblems(count: number, maxTerms: number): Problem[] {
const problems: Problem[] = []
for (let i = 0; i < count; i++) {
const termCount = Math.floor(Math.random() * (maxTerms - 1)) + 2 // 2-maxTerms terms
const terms: number[] = []
for (let j = 0; j < termCount; j++) {
// Only use 1-4 for basic problems
terms.push(Math.floor(Math.random() * 4) + 1)
}
// Ensure the sum doesn't exceed 9 (single digit)
const sum = terms.reduce((a, b) => a + b, 0)
if (sum <= 9) {
problems.push({
id: `basic-${i}`,
terms
})
} else {
i-- // Try again if sum is too large
}
}
return problems
}
function generateHeavenProblems(count: number, maxTerms: number): Problem[] {
const problems: Problem[] = []
for (let i = 0; i < count; i++) {
const termCount = Math.floor(Math.random() * (maxTerms - 1)) + 2
const terms: number[] = []
for (let j = 0; j < termCount; j++) {
// Use 1-9, but ensure at least one term needs heaven bead (5 or more)
const num = Math.floor(Math.random() * 9) + 1
terms.push(num)
}
// Ensure we have at least one 5+ or the sum involves 5+
const hasLargeNum = terms.some(t => t >= 5)
const sum = terms.reduce((a, b) => a + b, 0)
if ((hasLargeNum || sum >= 5) && sum <= 9) {
problems.push({
id: `heaven-${i}`,
terms
})
} else {
i-- // Try again
}
}
return problems
}
function generateComplementProblems(count: number, maxTerms: number): Problem[] {
const problems: Problem[] = []
for (let i = 0; i < count; i++) {
const termCount = Math.floor(Math.random() * (maxTerms - 1)) + 2
const terms: number[] = []
// Generate problems that specifically require complements
// This means having a situation where direct addition won't work
const firstTerm = Math.floor(Math.random() * 4) + 1 // 1-4
terms.push(firstTerm)
// Add a term that forces complement usage
const secondTerm = Math.floor(Math.random() * 3) + 3 // 3-5
terms.push(secondTerm)
// Maybe add more terms
for (let j = 2; j < termCount; j++) {
const num = Math.floor(Math.random() * 4) + 1 // 1-4
terms.push(num)
}
const sum = terms.reduce((a, b) => a + b, 0)
if (sum <= 9) {
problems.push({
id: `complement-${i}`,
terms
})
} else {
i-- // Try again
}
}
return problems
}
function generateMixedProblems(count: number, maxTerms: number): Problem[] {
const problems: Problem[] = []
for (let i = 0; i < count; i++) {
const termCount = Math.floor(Math.random() * (maxTerms - 1)) + 2
const terms: number[] = []
for (let j = 0; j < termCount; j++) {
// Use full range 1-9
terms.push(Math.floor(Math.random() * 9) + 1)
}
const sum = terms.reduce((a, b) => a + b, 0)
if (sum <= 9) {
problems.push({
id: `mixed-${i}`,
terms
})
} else {
i-- // Try again
}
}
return problems
}
function generateProblems(step: PracticeStep): Problem[] {
switch (step.skillLevel) {
case 'basic':
return generateBasicProblems(step.problemCount, step.maxTerms)
case 'heaven':
return generateHeavenProblems(step.problemCount, step.maxTerms)
case 'five-complements':
return generateComplementProblems(step.problemCount, step.maxTerms)
case 'mixed':
return generateMixedProblems(step.problemCount, step.maxTerms)
default:
return []
}
}
// Combined tutorial flow with practice steps interspersed
const combinedSteps = [
// Basic addition tutorial steps
tutorialSteps[0], // basic-1
tutorialSteps[1], // basic-2
tutorialSteps[2], // basic-3
tutorialSteps[3], // basic-4
// Practice basic addition
practiceSteps[0], // practice-basic
// Heaven bead tutorial steps
tutorialSteps[4], // heaven-intro
tutorialSteps[5], // heaven-plus-earth
// Practice heaven bead
practiceSteps[1], // practice-heaven
// Five complements tutorial steps
tutorialSteps[6], // complement-intro
tutorialSteps[7], // complement-2
tutorialSteps[8], // complement-3
tutorialSteps[9], // complement-4
tutorialSteps[10], // complement-5
// Practice five complements
practiceSteps[2], // practice-complements
// Final mixed tutorial steps
tutorialSteps[11], // mixed-1
tutorialSteps[12], // mixed-2
// Final mixed practice
practiceSteps[3] // practice-mixed
]
type StepType = TutorialStep | PracticeStep
function isTutorialStep(step: StepType): step is TutorialStep {
return 'problem' in step && 'startValue' in step
}
function isPracticeStep(step: StepType): step is PracticeStep {
return 'skillLevel' in step && 'problemCount' in step
}
export function GuidedAdditionTutorial() {
const [currentStepIndex, setCurrentStepIndex] = useState(0)
const [currentValue, setCurrentValue] = useState(tutorialSteps[0].startValue)
const [currentValue, setCurrentValue] = useState(0)
const [feedback, setFeedback] = useState<string | null>(null)
const [isCorrect, setIsCorrect] = useState(false)
const [multiStepProgress, setMultiStepProgress] = useState(0)
@@ -362,8 +594,12 @@ export function GuidedAdditionTutorial() {
const transitionTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const lastProcessedValueRef = useRef<number | null>(null)
const currentStep = tutorialSteps[currentStepIndex]
const isLastStep = currentStepIndex === tutorialSteps.length - 1
// Practice-specific state
const [currentProblems, setCurrentProblems] = useState<Problem[]>([])
const [showPracticeResults, setShowPracticeResults] = useState(false)
const currentStep = combinedSteps[currentStepIndex]
const isLastStep = currentStepIndex === combinedSteps.length - 1
const nextStep = useCallback(() => {
if (isLastStep || isTransitioning) return
@@ -374,7 +610,18 @@ export function GuidedAdditionTutorial() {
setTimeout(() => {
const nextStepIndex = currentStepIndex + 1
setCurrentStepIndex(nextStepIndex)
setCurrentValue(tutorialSteps[nextStepIndex].startValue)
const nextStep = combinedSteps[nextStepIndex]
if (isTutorialStep(nextStep)) {
setCurrentValue(nextStep.startValue)
} else {
// Practice step - generate problems
const problems = generateProblems(nextStep)
setCurrentProblems(problems)
setCurrentValue(0)
setShowPracticeResults(false)
}
setFeedback(null)
setIsCorrect(false)
setMultiStepProgress(0)
@@ -384,6 +631,9 @@ export function GuidedAdditionTutorial() {
}, [currentStepIndex, isLastStep, isTransitioning])
const checkStep = useCallback((newValue: number) => {
// Only check tutorial steps, not practice steps
if (!isTutorialStep(currentStep)) return
// Prevent processing the same value multiple times
if (lastProcessedValueRef.current === newValue) return
lastProcessedValueRef.current = newValue
@@ -415,7 +665,7 @@ export function GuidedAdditionTutorial() {
// Auto-advance to next step after a brief delay
transitionTimeoutRef.current = setTimeout(() => {
if (currentStepIndex < tutorialSteps.length - 1) {
if (currentStepIndex < combinedSteps.length - 1) {
nextStep()
}
transitionTimeoutRef.current = null
@@ -425,6 +675,80 @@ export function GuidedAdditionTutorial() {
}
}, [currentStep, multiStepProgress, currentStepIndex, nextStep, isTransitioning])
// Practice step functions
const updateProblemAnswer = useCallback((problemId: string, answer: number) => {
setCurrentProblems(prev => prev.map(p =>
p.id === problemId ? { ...p, userAnswer: answer } : p
))
}, [])
const checkPracticeWork = useCallback(() => {
if (!isPracticeStep(currentStep)) return
const updatedProblems = currentProblems.map(problem => {
const correctAnswer = problem.terms.reduce((sum, term) => sum + term, 0)
return {
...problem,
isCorrect: problem.userAnswer === correctAnswer
}
})
setCurrentProblems(updatedProblems)
setShowPracticeResults(true)
// Remove correct problems after a delay
setTimeout(() => {
const incorrectProblems = updatedProblems.filter(p => !p.isCorrect)
if (incorrectProblems.length === 0) {
// All correct - advance to next step
setFeedback('Perfect! All problems completed correctly.')
setTimeout(() => {
nextStep()
}, 2000)
} else {
// Keep only incorrect problems
setCurrentProblems(incorrectProblems.map(p => ({ ...p, userAnswer: undefined, isCorrect: undefined })))
setShowPracticeResults(false)
setFeedback(`${incorrectProblems.length} problem(s) need correction. Try again!`)
setTimeout(() => {
setFeedback(null)
}, 3000)
}
}, 2000)
}, [currentStep, currentProblems, nextStep])
// Initialize practice problems when entering a practice step
useEffect(() => {
if (isPracticeStep(currentStep)) {
const step = currentStep as PracticeStep
let problems: Problem[] = []
switch (step.skillLevel) {
case 'basic':
problems = generateBasicProblems(step.problemCount, step.maxTerms)
break
case 'heaven':
problems = generateHeavenProblems(step.problemCount, step.maxTerms)
break
case 'five-complements':
problems = generateComplementProblems(step.problemCount, step.maxTerms)
break
case 'mixed':
problems = generateMixedProblems(step.problemCount, step.maxTerms)
break
}
setCurrentProblems(problems)
setShowPracticeResults(false)
} else {
// Reset practice state for tutorial steps
setCurrentProblems([])
setShowPracticeResults(false)
}
}, [currentStepIndex, currentStep])
const resetTutorial = useCallback(() => {
// Clear any pending transition timeout
if (transitionTimeoutRef.current) {
@@ -433,11 +757,13 @@ export function GuidedAdditionTutorial() {
}
setCurrentStepIndex(0)
setCurrentValue(tutorialSteps[0].startValue)
setCurrentValue(combinedSteps[0] && isTutorialStep(combinedSteps[0]) ? combinedSteps[0].startValue : 0)
setFeedback(null)
setIsCorrect(false)
setMultiStepProgress(0)
setIsTransitioning(false)
setCurrentProblems([])
setShowPracticeResults(false)
lastProcessedValueRef.current = null // Reset for restart
}, [])
@@ -454,7 +780,7 @@ export function GuidedAdditionTutorial() {
bg: 'blue.500',
h: 'full',
transition: 'width',
width: `${((currentStepIndex + 1) / tutorialSteps.length) * 100}%`
width: `${((currentStepIndex + 1) / combinedSteps.length) * 100}%`
})} />
</div>
@@ -462,111 +788,281 @@ export function GuidedAdditionTutorial() {
<div className={css({
textAlign: 'center',
p: '4',
bg: 'blue.50',
bg: isPracticeStep(currentStep) ? 'purple.50' : 'blue.50',
rounded: 'lg',
border: '1px solid',
borderColor: 'blue.200'
borderColor: isPracticeStep(currentStep) ? 'purple.200' : 'blue.200'
})}>
<h4 className={css({
fontSize: 'lg',
fontWeight: 'semibold',
color: 'blue.800',
color: isPracticeStep(currentStep) ? 'purple.800' : 'blue.800',
mb: '2'
})}>
Step {currentStepIndex + 1} of {tutorialSteps.length}: {currentStep.title}
Step {currentStepIndex + 1} of {combinedSteps.length}: {isPracticeStep(currentStep) ? currentStep.title : currentStep.title}
</h4>
<p className={css({
fontSize: 'md',
color: 'blue.700',
mb: '2'
})}>
Problem: <strong>{currentStep.problem}</strong>
</p>
<p className={css({
fontSize: 'sm',
color: 'blue.600'
})}>
{currentStep.description}
</p>
</div>
{/* Tooltip */}
<div className={css({
bg: 'yellow.50',
border: '1px solid',
borderColor: 'yellow.300',
rounded: 'lg',
p: '4'
})}>
<h5 className={css({
fontWeight: 'semibold',
color: 'yellow.800',
mb: '2'
})}>
💡 {currentStep.tooltip.content}
</h5>
<p className={css({
fontSize: 'sm',
color: 'yellow.700'
})}>
{currentStep.tooltip.explanation}
</p>
{currentStep.multiStepInstructions && (
<div className={css({ mt: '3' })}>
<p className={css({ fontSize: 'sm', fontWeight: 'medium', color: 'yellow.800', mb: '1' })}>
Instructions:
{isPracticeStep(currentStep) ? (
<p className={css({
fontSize: 'md',
color: 'purple.700',
mb: '2'
})}>
Complete all problems using the techniques you've learned
</p>
) : (
<>
<p className={css({
fontSize: 'md',
color: 'blue.700',
mb: '2'
})}>
Problem: <strong>{currentStep.problem}</strong>
</p>
<ol className={css({ fontSize: 'sm', color: 'yellow.700', pl: '4' })}>
{currentStep.multiStepInstructions.map((instruction, index) => (
<li key={index} className={css({
mb: '1',
opacity: index <= multiStepProgress ? '1' : '0.6',
fontWeight: index === multiStepProgress ? 'semibold' : 'normal'
})}>
{index + 1}. {instruction}
</li>
))}
</ol>
</div>
<p className={css({
fontSize: 'sm',
color: 'blue.600'
})}>
{currentStep.description}
</p>
</>
)}
</div>
{/* Interactive Abacus */}
<div className={css({
bg: 'white',
border: '2px solid',
borderColor: 'blue.300',
rounded: 'xl',
p: '6',
display: 'flex',
justifyContent: 'center',
minHeight: '400px',
alignItems: 'center'
})}>
<AbacusReact
value={currentValue}
columns={1}
beadShape="diamond"
colorScheme="place-value"
hideInactiveBeads={false}
scaleFactor={2.0}
interactive={true}
showNumbers={true}
animated={true}
onValueChange={checkStep}
customStyles={{
beadHighlight: currentStep.highlightBeads?.reduce((acc, bead) => {
const key = `${bead.columnIndex}-${bead.beadType}${bead.position !== undefined ? `-${bead.position}` : ''}`
acc[key] = {
stroke: '#3B82F6',
strokeWidth: 3,
filter: 'drop-shadow(0 0 8px rgba(59, 130, 246, 0.6))'
}
return acc
}, {} as Record<string, any>)
}}
/>
</div>
{/* Tutorial tooltip or Practice problems */}
{isPracticeStep(currentStep) ? (
<div className={css({
bg: 'purple.50',
border: '1px solid',
borderColor: 'purple.300',
rounded: 'lg',
p: '4'
})}>
<h5 className={css({
fontWeight: 'semibold',
color: 'purple.800',
mb: '4',
textAlign: 'center'
})}>
Practice Problems
</h5>
{/* Problem grid */}
<div className={css({
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '3',
mb: '4'
})}>
{currentProblems.map((problem, index) => (
<div key={problem.id} className={css({
p: '3',
bg: 'white',
border: '1px solid',
borderColor: problem.isCorrect === true ? 'green.300' : problem.isCorrect === false ? 'red.300' : 'gray.300',
rounded: 'md',
textAlign: 'center'
})}>
<div className={css({
fontSize: 'sm',
fontWeight: 'medium',
color: 'gray.600',
mb: '2'
})}>
#{index + 1}
</div>
<div className={css({
fontSize: 'lg',
fontWeight: 'semibold',
mb: '2'
})}>
{problem.terms.join(' + ')} = ?
</div>
<input
type="number"
value={problem.userAnswer || ''}
onChange={(e) => updateProblemAnswer(problem.id, parseInt(e.target.value) || 0)}
className={css({
w: 'full',
p: '2',
border: '1px solid',
borderColor: 'gray.300',
rounded: 'md',
textAlign: 'center',
fontSize: 'md'
})}
placeholder="Answer"
/>
{showPracticeResults && problem.isCorrect === false && (
<div className={css({
mt: '2',
fontSize: 'sm',
color: 'red.600'
})}>
Incorrect. Try again!
</div>
)}
</div>
))}
</div>
{/* Check work button */}
{!showPracticeResults && currentProblems.length > 0 && (
<div className={css({ textAlign: 'center' })}>
<button
onClick={checkPracticeWork}
className={css({
px: '6',
py: '3',
bg: 'purple.500',
color: 'white',
rounded: 'lg',
fontWeight: 'semibold',
cursor: 'pointer',
_hover: { bg: 'purple.600' }
})}
>
Check Work
</button>
</div>
)}
{/* Continue button after all problems correct */}
{showPracticeResults && currentProblems.every(p => p.isCorrect) && (
<div className={css({ textAlign: 'center' })}>
<p className={css({
fontSize: 'lg',
fontWeight: 'semibold',
color: 'green.600',
mb: '3'
})}>
🎉 All problems correct! Great job!
</p>
<button
onClick={nextStep}
className={css({
px: '6',
py: '3',
bg: 'green.500',
color: 'white',
rounded: 'lg',
fontWeight: 'semibold',
cursor: 'pointer',
_hover: { bg: 'green.600' }
})}
>
Continue Tutorial
</button>
</div>
)}
{/* Practice Abacus */}
<div className={css({
bg: 'white',
border: '2px solid',
borderColor: 'purple.300',
rounded: 'xl',
p: '4',
display: 'flex',
justifyContent: 'center',
minHeight: '350px',
alignItems: 'center',
overflow: 'visible'
})}>
<div style={{ width: 'fit-content', height: 'fit-content' }}>
<AbacusReact
value={0}
columns={3}
beadShape="diamond"
colorScheme="place-value"
hideInactiveBeads={false}
scaleFactor={2.0}
interactive={true}
showNumbers={false}
animated={true}
/>
</div>
</div>
</div>
) : (
<div className={css({
bg: 'yellow.50',
border: '1px solid',
borderColor: 'yellow.300',
rounded: 'lg',
p: '4'
})}>
<h5 className={css({
fontWeight: 'semibold',
color: 'yellow.800',
mb: '2'
})}>
💡 {currentStep.tooltip.content}
</h5>
<p className={css({
fontSize: 'sm',
color: 'yellow.700'
})}>
{currentStep.tooltip.explanation}
</p>
{currentStep.multiStepInstructions && (
<div className={css({ mt: '3' })}>
<p className={css({ fontSize: 'sm', fontWeight: 'medium', color: 'yellow.800', mb: '1' })}>
Instructions:
</p>
<ol className={css({ fontSize: 'sm', color: 'yellow.700', pl: '4' })}>
{currentStep.multiStepInstructions.map((instruction, index) => (
<li key={index} className={css({
mb: '1',
opacity: index <= multiStepProgress ? '1' : '0.6',
fontWeight: index === multiStepProgress ? 'semibold' : 'normal'
})}>
{index + 1}. {instruction}
</li>
))}
</ol>
</div>
)}
</div>
)}
{/* Interactive Abacus - only show for tutorial steps */}
{!isPracticeStep(currentStep) && (
<div className={css({
bg: 'white',
border: '2px solid',
borderColor: 'blue.300',
rounded: 'xl',
p: '6',
display: 'flex',
justifyContent: 'center',
minHeight: '400px',
alignItems: 'center'
})}>
<AbacusReact
value={currentValue}
columns={1}
beadShape="diamond"
colorScheme="place-value"
hideInactiveBeads={false}
scaleFactor={2.0}
interactive={true}
showNumbers={true}
animated={true}
onValueChange={checkStep}
customStyles={{
beadHighlight: currentStep.highlightBeads?.reduce((acc, bead) => {
const key = `${bead.columnIndex}-${bead.beadType}${bead.position !== undefined ? `-${bead.position}` : ''}`
acc[key] = {
stroke: '#3B82F6',
strokeWidth: 3,
filter: 'drop-shadow(0 0 8px rgba(59, 130, 246, 0.6))'
}
return acc
}, {} as Record<string, any>)
}}
/>
</div>
)}
{/* Feedback */}
{feedback && (