feat: replace tutorial player arrows with dynamic bead diff algorithm

Replaces the static getCurrentStepBeads() function with the new dynamic
bead diff algorithm. Now calculates arrows and highlights in real-time
based on current abacus state and target values.

Key improvements:
- Dynamic arrow calculation using calculateBeadDiffFromValues()
- Real-time state transition highlighting
- Comprehensive debug logging for bead diff calculations
- Consistent arrow display across all multi-step instructions

Fixes issue where only the first multi-step instruction showed arrows.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-22 21:15:40 -05:00
parent 472bdf8e74
commit e8fe467c6c

View File

@@ -7,6 +7,7 @@ import { stack, hstack, vstack } from '../../../styled-system/patterns'
import { Tutorial, TutorialStep, PracticeStep, TutorialEvent, NavigationState, UIState } from '../../types/tutorial'
import { PracticeProblemPlayer, PracticeResults } from './PracticeProblemPlayer'
import { generateAbacusInstructions } from '../../utils/abacusInstructionGenerator'
import { calculateBeadDiffFromValues } from '../../utils/beadDiff'
// Reducer state and actions
interface TutorialPlayerState {
@@ -28,6 +29,7 @@ type TutorialPlayerAction =
| { type: 'ADD_EVENT'; event: TutorialEvent }
| { type: 'UPDATE_UI_STATE'; updates: Partial<UIState> }
| { type: 'ADVANCE_MULTI_STEP' }
| { type: 'PREVIOUS_MULTI_STEP' }
| { type: 'RESET_MULTI_STEP' }
function tutorialPlayerReducer(state: TutorialPlayerState, action: TutorialPlayerAction): TutorialPlayerState {
@@ -98,6 +100,12 @@ function tutorialPlayerReducer(state: TutorialPlayerState, action: TutorialPlaye
currentMultiStep: state.currentMultiStep + 1
}
case 'PREVIOUS_MULTI_STEP':
return {
...state,
currentMultiStep: Math.max(0, state.currentMultiStep - 1)
}
case 'RESET_MULTI_STEP':
return {
...state,
@@ -222,35 +230,48 @@ export function TutorialPlayer({
// Get arrows for the immediate next action to reach current expected step
const getCurrentStepBeads = useCallback(() => {
// If we've reached the final target, no arrows needed
if (currentValue === currentStep.targetValue) return undefined
// If no expected steps, fall back to original behavior
if (expectedSteps.length === 0) return currentStep.stepBeadHighlights
// Get the current expected step we're working toward
const currentExpectedStep = expectedSteps[currentMultiStep]
if (!currentExpectedStep) return undefined
if (!currentExpectedStep) {
// If we're past the last step, check if we've reached the final target
if (currentValue === currentStep.targetValue) return undefined
return undefined
}
// Generate arrows to get from current value to this expected step's target
// Use the new bead diff algorithm to get arrows for current step
try {
const instruction = generateAbacusInstructions(currentValue, currentExpectedStep.targetValue)
console.log(`🎯 TutorialPlayer: Calculating bead diff for step ${currentMultiStep}`)
console.log(` From: ${currentValue} → To: ${currentExpectedStep.targetValue}`)
// Take only the FIRST step (immediate next action)
const immediateAction = instruction.stepBeadHighlights?.filter(bead => bead.stepIndex === 0)
const beadDiff = calculateBeadDiffFromValues(currentValue, currentExpectedStep.targetValue)
console.log('🎯 Expected step progression:', {
currentValue,
expectedStepIndex: currentMultiStep,
expectedStepTarget: currentExpectedStep.targetValue,
expectedStepDescription: currentExpectedStep.description,
immediateActionBeads: immediateAction?.length || 0,
totalExpectedSteps: expectedSteps.length
})
console.log(` Bead diff summary: "${beadDiff.summary}"`)
console.log(` Has changes: ${beadDiff.hasChanges}`)
console.log(` Changes: ${beadDiff.changes.length}`)
return immediateAction && immediateAction.length > 0 ? immediateAction : undefined
if (!beadDiff.hasChanges) {
console.log(` ✅ No changes needed - current value ${currentValue} matches target ${currentExpectedStep.targetValue}`)
return undefined
}
// Convert bead diff results to StepBeadHighlight format expected by AbacusReact
const stepBeadHighlights: StepBeadHighlight[] = beadDiff.changes.map((change, index) => ({
placeValue: change.placeValue,
beadType: change.beadType,
position: change.position,
direction: change.direction,
stepIndex: 0, // Always use step 0 since we're showing immediate next action
order: change.order
}))
console.log(` Generated ${stepBeadHighlights.length} step bead highlights`)
return stepBeadHighlights
} catch (error) {
console.warn('⚠️ Failed to generate step guidance:', error)
console.error('Error generating step beads with bead diff:', error)
return undefined
}
}, [currentValue, currentStep.targetValue, expectedSteps, currentMultiStep])
@@ -391,7 +412,7 @@ export function TutorialPlayer({
}
}
}
}, [currentValue, currentStep, currentMultiStep, expectedSteps])
}, [currentValue, currentStep, currentMultiStep, expectedSteps, showDebugPanel])
// Update the reference when the step changes (not just value changes)
useEffect(() => {
@@ -637,6 +658,77 @@ export function TutorialPlayer({
>
Steps
</button>
{/* Multi-step navigation controls */}
{currentStep.multiStepInstructions && currentStep.multiStepInstructions.length > 1 && (
<>
<div className={css({
fontSize: 'xs',
color: 'gray.600',
px: 2,
borderLeft: '1px solid',
borderColor: 'gray.300',
ml: 2,
pl: 3
})}>
Multi-Step: {currentMultiStep + 1} / {currentStep.multiStepInstructions.length}
</div>
<button
onClick={() => dispatch({ type: 'RESET_MULTI_STEP' })}
disabled={currentMultiStep === 0}
className={css({
px: 2,
py: 1,
fontSize: 'xs',
border: '1px solid',
borderColor: currentMultiStep === 0 ? 'gray.200' : 'orange.300',
borderRadius: 'md',
bg: currentMultiStep === 0 ? 'gray.100' : 'white',
color: currentMultiStep === 0 ? 'gray.400' : 'orange.700',
cursor: currentMultiStep === 0 ? 'not-allowed' : 'pointer',
_hover: currentMultiStep === 0 ? {} : { bg: 'orange.50' }
})}
>
First
</button>
<button
onClick={() => dispatch({ type: 'PREVIOUS_MULTI_STEP' })}
disabled={currentMultiStep === 0}
className={css({
px: 2,
py: 1,
fontSize: 'xs',
border: '1px solid',
borderColor: currentMultiStep === 0 ? 'gray.200' : 'orange.300',
borderRadius: 'md',
bg: currentMultiStep === 0 ? 'gray.100' : 'white',
color: currentMultiStep === 0 ? 'gray.400' : 'orange.700',
cursor: currentMultiStep === 0 ? 'not-allowed' : 'pointer',
_hover: currentMultiStep === 0 ? {} : { bg: 'orange.50' }
})}
>
Prev
</button>
<button
onClick={() => dispatch({ type: 'ADVANCE_MULTI_STEP' })}
disabled={currentMultiStep >= currentStep.multiStepInstructions.length - 1}
className={css({
px: 2,
py: 1,
fontSize: 'xs',
border: '1px solid',
borderColor: currentMultiStep >= currentStep.multiStepInstructions.length - 1 ? 'gray.200' : 'green.300',
borderRadius: 'md',
bg: currentMultiStep >= currentStep.multiStepInstructions.length - 1 ? 'gray.100' : 'white',
color: currentMultiStep >= currentStep.multiStepInstructions.length - 1 ? 'gray.400' : 'green.700',
cursor: currentMultiStep >= currentStep.multiStepInstructions.length - 1 ? 'not-allowed' : 'pointer',
_hover: currentMultiStep >= currentStep.multiStepInstructions.length - 1 ? {} : { bg: 'green.50' }
})}
>
Next
</button>
</>
)}
<label className={hstack({ gap: 2, fontSize: 'sm' })}>
<input
type="checkbox"