'use client' import { type AbacusOverlay, AbacusReact, type StepBeadHighlight, useAbacusDisplay, calculateBeadDiffFromValues, } from '@soroban/abacus-react' import { useTranslations } from 'next-intl' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { css } from '../../../styled-system/css' import { hstack, stack, vstack } from '../../../styled-system/patterns' import type { NavigationState, Tutorial, TutorialEvent, TutorialStep, UIState, } from '../../types/tutorial' import { findTopmostBeadWithArrows, hasActiveBeadsToLeft } from '../../utils/beadTooltipUtils' import { generateUnifiedInstructionSequence } from '../../utils/unifiedStepGenerator' import { CoachBar } from './CoachBar/CoachBar' import { DecompositionDisplay, DecompositionProvider } from '../decomposition' import { BeadTooltipContent } from '../shared/BeadTooltipContent' import { TutorialProvider, useTutorialContext } from './TutorialContext' import { TutorialUIProvider } from './TutorialUIContext' import type { SkillTutorialControlAction } from '@/lib/classroom/socket-events' import './CoachBar/coachbar.css' // Reducer state and actions interface TutorialPlayerState { currentStepIndex: number currentValue: number isStepCompleted: boolean error: string | null events: TutorialEvent[] stepStartTime: number multiStepStartTime: number // Track when current multi-step started uiState: UIState currentMultiStep: number // Current step within multi-step instructions (0-based) } interface ExpectedStep { index: number stepIndex: number targetValue: number startValue: number description: string mathematicalTerm?: string // Pedagogical term like "10", "(5 - 1)", "-6" termPosition?: { startIndex: number; endIndex: number } // Position in full decomposition } type TutorialPlayerAction = | { type: 'INITIALIZE_STEP' stepIndex: number startValue: number stepId: string } | { type: 'USER_VALUE_CHANGE' oldValue: number newValue: number stepId: string } | { type: 'COMPLETE_STEP'; stepId: string } | { type: 'SET_ERROR'; error: string | null } | { type: 'ADD_EVENT'; event: TutorialEvent } | { type: 'UPDATE_UI_STATE'; updates: Partial } | { type: 'ADVANCE_MULTI_STEP' } | { type: 'PREVIOUS_MULTI_STEP' } | { type: 'RESET_MULTI_STEP' } function _tutorialPlayerReducer( state: TutorialPlayerState, action: TutorialPlayerAction ): TutorialPlayerState { switch (action.type) { case 'INITIALIZE_STEP': return { ...state, currentStepIndex: action.stepIndex, currentValue: action.startValue, isStepCompleted: false, error: null, stepStartTime: Date.now(), multiStepStartTime: Date.now(), // Start timing for first multi-step currentMultiStep: 0, // Reset to first multi-step events: [ ...state.events, { type: 'STEP_STARTED', stepId: action.stepId, timestamp: new Date(), }, ], } case 'USER_VALUE_CHANGE': return { ...state, currentValue: action.newValue, events: [ ...state.events, { type: 'VALUE_CHANGED', stepId: action.stepId, oldValue: action.oldValue, newValue: action.newValue, timestamp: new Date(), }, ], } case 'COMPLETE_STEP': return { ...state, isStepCompleted: true, error: null, events: [ ...state.events, { type: 'STEP_COMPLETED', stepId: action.stepId, success: true, timestamp: new Date(), }, ], } case 'SET_ERROR': return { ...state, error: action.error, } case 'ADD_EVENT': return { ...state, events: [...state.events, action.event], } case 'UPDATE_UI_STATE': return { ...state, uiState: { ...state.uiState, ...action.updates }, } case 'ADVANCE_MULTI_STEP': return { ...state, currentMultiStep: state.currentMultiStep + 1, multiStepStartTime: Date.now(), // Reset timer for new multi-step } case 'PREVIOUS_MULTI_STEP': return { ...state, currentMultiStep: Math.max(0, state.currentMultiStep - 1), } case 'RESET_MULTI_STEP': return { ...state, currentMultiStep: 0, } default: return state } } /** * Observed state for teacher observation mode */ export interface TutorialObservedState { currentStepIndex: number currentMultiStep: number currentValue: number isStepCompleted: boolean } interface TutorialPlayerProps { tutorial: Tutorial initialStepIndex?: number isDebugMode?: boolean showDebugPanel?: boolean hideNavigation?: boolean hideTooltip?: boolean silentErrors?: boolean abacusColumns?: number theme?: 'light' | 'dark' onStepChange?: (stepIndex: number, step: TutorialStep) => void onStepComplete?: (stepIndex: number, step: TutorialStep, success: boolean) => void onTutorialComplete?: (score: number, timeSpent: number) => void onEvent?: (event: TutorialEvent) => void /** Callback when multi-step index changes (for broadcasting to observers) */ onMultiStepChange?: (multiStep: number) => void className?: string /** Control action from teacher (optional, for remote control) */ controlAction?: SkillTutorialControlAction | null /** Callback when control action has been processed */ onControlActionProcessed?: () => void /** * Observed state from WebSocket (for teacher observation mode). * When set, the component becomes read-only and displays this state. */ observedState?: TutorialObservedState /** * Callback for sending control actions (used in observation mode). * When provided, button clicks send control actions instead of local state changes. */ onControl?: (action: SkillTutorialControlAction) => void } function TutorialPlayerContent({ tutorial, initialStepIndex = 0, isDebugMode = false, showDebugPanel = false, hideNavigation = false, hideTooltip = false, silentErrors = false, abacusColumns = 5, theme = 'light', onStepChange, onStepComplete, onTutorialComplete, onEvent, onMultiStepChange, className, controlAction, onControlActionProcessed, observedState, onControl, }: TutorialPlayerProps) { const t = useTranslations('tutorial.player') const [_startTime] = useState(Date.now()) const isProgrammaticChange = useRef(false) const [showHelpForCurrentStep, setShowHelpForCurrentStep] = useState(false) // Whether we're in observation mode (read-only, state comes from WebSocket) const isObservationMode = !!observedState // Use tutorial context instead of local state const { state, dispatch, currentStep: contextCurrentStep, goToStep: contextGoToStep, goToNextStep: contextGoToNextStep, goToPreviousStep: contextGoToPreviousStep, handleValueChange: contextHandleValueChange, advanceMultiStep, previousMultiStep, resetMultiStep, activeTermIndices, activeIndividualTermIndex, getColumnFromTermIndex, getGroupTermIndicesFromTermIndex, handleAbacusColumnHover, } = useTutorialContext() // In observation mode, override state values with observed values const currentStepIndex = observedState?.currentStepIndex ?? state.currentStepIndex const currentValue = observedState?.currentValue ?? state.currentValue const isStepCompleted = observedState?.isStepCompleted ?? state.isStepCompleted const currentMultiStep = observedState?.currentMultiStep ?? state.currentMultiStep // Get the current step based on the (possibly observed) step index const currentStep = isObservationMode ? tutorial.steps[observedState.currentStepIndex] : contextCurrentStep // Non-observed state values (only used in interactive mode) const { error, events, stepStartTime, multiStepStartTime, uiState } = state // Use universal abacus display configuration const { config: abacusConfig } = useAbacusDisplay() const [isSuccessPopupDismissed, setIsSuccessPopupDismissed] = useState(false) // Keep refs needed for step advancement and bead tracking const lastValueForStepAdvancement = useRef(currentValue) const userHasInteracted = useRef(false) const lastMovedBead = useRef(null) // Handle control actions from teacher useEffect(() => { if (!controlAction) return console.log('[TutorialPlayer] Received control action:', controlAction) switch (controlAction.type) { case 'next-step': contextGoToNextStep() break case 'previous-step': contextGoToPreviousStep() break case 'go-to-step': if ('stepIndex' in controlAction) { contextGoToStep(controlAction.stepIndex) } break case 'set-abacus-value': if ('value' in controlAction) { // Trigger a programmatic value change isProgrammaticChange.current = true contextHandleValueChange(controlAction.value) } break case 'advance-multi-step': advanceMultiStep() break case 'previous-multi-step': previousMultiStep() break } // Mark action as processed onControlActionProcessed?.() }, [ controlAction, contextGoToNextStep, contextGoToPreviousStep, contextGoToStep, contextHandleValueChange, advanceMultiStep, previousMultiStep, currentValue, onControlActionProcessed, ]) // Reset success popup when moving to new step useEffect(() => { setIsSuccessPopupDismissed(false) }, []) // Auto-dismiss success toast after 3 seconds useEffect(() => { if (isStepCompleted && !isSuccessPopupDismissed) { const timer = setTimeout(() => { setIsSuccessPopupDismissed(true) }, 3000) return () => clearTimeout(timer) } }, [isStepCompleted, isSuccessPopupDismissed]) // Current step comes from context const beadRefs = useRef>(new Map()) // Navigation state const navigationState: NavigationState = { currentStepIndex, canGoNext: currentStepIndex < tutorial.steps.length - 1, canGoPrevious: currentStepIndex > 0, totalSteps: tutorial.steps.length, completionPercentage: (currentStepIndex / tutorial.steps.length) * 100, } // Define the static expected steps using our unified step generator const { expectedSteps, fullDecomposition, isMeaningfulDecomposition } = useMemo(() => { try { const unifiedSequence = generateUnifiedInstructionSequence( currentStep.startValue, currentStep.targetValue ) // Convert unified sequence to expected steps format const steps = unifiedSequence.steps.map((step, index) => ({ index: index, stepIndex: index, targetValue: step.expectedValue, startValue: index === 0 ? currentStep.startValue : unifiedSequence.steps[index - 1].expectedValue, description: step.englishInstruction, mathematicalTerm: step.mathematicalTerm, // Add the pedagogical term termPosition: step.termPosition, // Add the precise position information })) return { expectedSteps: steps, fullDecomposition: unifiedSequence.fullDecomposition, isMeaningfulDecomposition: unifiedSequence.isMeaningfulDecomposition, } } catch (_error) { return { expectedSteps: [], fullDecomposition: '', isMeaningfulDecomposition: false, } } }, [currentStep.startValue, currentStep.targetValue]) // Get arrows for the immediate next action to reach current expected step const getCurrentStepBeads = useCallback(() => { // 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) { // If we're past the last step, check if we've reached the final target if (currentValue === currentStep.targetValue) { return undefined } return undefined } // Use the new bead diff algorithm to get arrows for current step try { const beadDiff = calculateBeadDiffFromValues(currentValue, currentExpectedStep.targetValue) if (!beadDiff.hasChanges) { return undefined } // Convert bead diff results to StepBeadHighlight format expected by AbacusReact // Filter to only include beads from columns that exist const minValidPlaceValue = Math.max(0, 5 - abacusColumns) const stepBeadHighlights: StepBeadHighlight[] = beadDiff.changes .filter((change) => change.placeValue < abacusColumns) .map((change, _index) => ({ placeValue: change.placeValue, beadType: change.beadType, position: change.position, direction: change.direction, stepIndex: currentMultiStep, // Use current multi-step index to match AbacusReact filtering order: change.order, })) return stepBeadHighlights.length > 0 ? stepBeadHighlights : undefined } catch (error) { console.error('Error generating step beads with bead diff:', error) return undefined } }, [ currentValue, currentStep.targetValue, expectedSteps, currentMultiStep, currentStep.stepBeadHighlights, abacusColumns, ]) // Get the current step's bead diff summary for real-time user feedback const getCurrentStepSummary = useCallback(() => { if (expectedSteps.length === 0) return null const currentExpectedStep = expectedSteps[currentMultiStep] if (!currentExpectedStep) return null try { const beadDiff = calculateBeadDiffFromValues(currentValue, currentExpectedStep.targetValue) return beadDiff.hasChanges ? beadDiff.summary : null } catch (_error) { return null } }, [currentValue, expectedSteps, currentMultiStep]) // Get current step beads (dynamic arrows for static expected steps) const currentStepBeads = getCurrentStepBeads() // Get current step summary for real-time user feedback const currentStepSummary = getCurrentStepSummary() // Filter highlightBeads to only include valid columns const filteredHighlightBeads = useMemo(() => { if (!currentStep.highlightBeads) return undefined return currentStep.highlightBeads.filter((highlight) => { return highlight.placeValue < abacusColumns }) }, [currentStep.highlightBeads, abacusColumns]) // Helper function to highlight the current mathematical term in the full decomposition const renderHighlightedDecomposition = useCallback(() => { if (!fullDecomposition || expectedSteps.length === 0) return null const currentStep = expectedSteps[currentMultiStep] if (!currentStep?.mathematicalTerm) return null const mathTerm = currentStep.mathematicalTerm // Try to use precise position first if (currentStep.termPosition) { const { startIndex, endIndex } = currentStep.termPosition const highlighted = fullDecomposition.substring(startIndex, endIndex) // Validate that the highlighted text makes sense if (highlighted.includes(mathTerm.replace('-', '')) || highlighted === mathTerm) { return { before: fullDecomposition.substring(0, startIndex), highlighted, after: fullDecomposition.substring(endIndex), } } } // Fallback: search for the mathematical term in the decomposition const searchTerm = mathTerm.startsWith('-') ? mathTerm.substring(1) : mathTerm const searchIndex = fullDecomposition.indexOf(searchTerm) if (searchIndex !== -1) { const startIndex = mathTerm.startsWith('-') ? // For negative terms, try to include the preceding dash Math.max(0, searchIndex - 1) : searchIndex const endIndex = mathTerm.startsWith('-') ? searchIndex + searchTerm.length : searchIndex + mathTerm.length return { before: fullDecomposition.substring(0, startIndex), highlighted: fullDecomposition.substring(startIndex, endIndex), after: fullDecomposition.substring(endIndex), } } // Final fallback: highlight the first occurrence of just the number part const numberMatch = mathTerm.match(/\d+/) if (numberMatch) { const number = numberMatch[0] const numberIndex = fullDecomposition.indexOf(number) if (numberIndex !== -1) { return { before: fullDecomposition.substring(0, numberIndex), highlighted: fullDecomposition.substring(numberIndex, numberIndex + number.length), after: fullDecomposition.substring(numberIndex + number.length), } } } return null }, [fullDecomposition, expectedSteps, currentMultiStep]) // Create overlay for tooltip positioned precisely at topmost bead using smart collision detection const tooltipOverlay = useMemo(() => { // Show tooltip if step is completed AND still at target value OR if we have step instructions const showCelebration = isStepCompleted && currentValue === currentStep.targetValue const showInstructions = !showCelebration && currentStepSummary && currentStepBeads?.length if (!showCelebration && !showInstructions) { return null } let topmostBead: StepBeadHighlight | null = null if (showCelebration) { // For celebration, use the last moved bead or fallback if (lastMovedBead.current) { topmostBead = lastMovedBead.current } else { // Use the ones place (rightmost column) heaven bead as fallback topmostBead = { placeValue: 0, // Ones place beadType: 'heaven' as const, position: 0, direction: 'none' as const, stepIndex: currentMultiStep, order: 0, } } } else if (showInstructions) { // For instructions, use the topmost bead with arrows topmostBead = findTopmostBeadWithArrows(currentStepBeads) } if (!topmostBead) { return null } // Validate that the bead is from a column that exists if (topmostBead.placeValue >= abacusColumns) { // Bead is from an invalid column, skip tooltip return null } // Smart positioning logic: avoid covering active beads using shared utility const targetColumnIndex = abacusColumns - 1 - topmostBead.placeValue const activeToLeft = hasActiveBeadsToLeft( currentValue, currentStepBeads, abacusColumns, targetColumnIndex ) // Determine tooltip position and target const shouldPositionAbove = activeToLeft const tooltipSide = shouldPositionAbove ? 'top' : 'left' const tooltipTarget = shouldPositionAbove ? { // Target the heaven bead position for the column type: 'bead' as const, columnIndex: targetColumnIndex, beadType: 'heaven' as const, beadPosition: 0, // Heaven beads are always at position 0 } : { // Target the actual bead type: 'bead' as const, columnIndex: targetColumnIndex, beadType: topmostBead.beadType, beadPosition: topmostBead.position, } // Create an overlay that positions tooltip to avoid covering active beads const overlay: AbacusOverlay = { id: 'bead-tooltip', type: 'tooltip', target: tooltipTarget, content: ( ), offset: { x: 0, y: 0 }, visible: true, } return overlay }, [ currentStepSummary, currentStepBeads, isStepCompleted, currentMultiStep, renderHighlightedDecomposition, currentValue, currentStep, isMeaningfulDecomposition, abacusColumns, theme, ]) // Timer for smart help detection useEffect(() => { setShowHelpForCurrentStep(false) // Reset help when step changes const timer = setTimeout(() => { setShowHelpForCurrentStep(true) }, 8000) // 8 seconds return () => clearTimeout(timer) }, []) // Reset when step changes or timer resets // Event logging - now just notifies parent, state is managed by reducer const notifyEvent = useCallback( (event: TutorialEvent) => { onEvent?.(event) }, [onEvent] ) // Navigation functions - declare these first since they're used in useEffects // Use context goToStep function instead of local one const goToStep = contextGoToStep // Use context goToNextStep function, or send control in observation mode const goToNextStep = useCallback(() => { if (isObservationMode && onControl) { onControl({ type: 'next-step' }) } else { contextGoToNextStep() } }, [isObservationMode, onControl, contextGoToNextStep]) // Use context goToPreviousStep function, or send control in observation mode const goToPreviousStep = useCallback(() => { if (isObservationMode && onControl) { onControl({ type: 'previous-step' }) } else { contextGoToPreviousStep() } }, [isObservationMode, onControl, contextGoToPreviousStep]) // Initialize step on mount only useEffect(() => { if (currentStep && currentStepIndex === initialStepIndex) { // Mark this as a programmatic change to prevent feedback loop isProgrammaticChange.current = true // Dispatch initialization action dispatch({ type: 'INITIALIZE_STEP', stepIndex: currentStepIndex, startValue: currentStep.startValue, stepId: currentStep.id, }) // Notify parent of step change onStepChange?.(currentStepIndex, currentStep) } }, [ currentStep, currentStepIndex, // Dispatch initialization action dispatch, initialStepIndex, // Notify parent of step change onStepChange, ]) // Only run on mount // Check if step is completed - only complete when we've gone through all multi-steps AND reached target useEffect(() => { if (currentStep && currentValue === currentStep.targetValue && !isStepCompleted) { // For multi-step problems, only complete when we've finished all expected steps const isMultiStepProblem = expectedSteps.length > 0 const hasFinishedAllMultiSteps = currentMultiStep >= expectedSteps.length - 1 // Complete the step if: // 1. It's not a multi-step problem, OR // 2. It's a multi-step problem and we've finished all steps if (!isMultiStepProblem || hasFinishedAllMultiSteps) { dispatch({ type: 'COMPLETE_STEP', stepId: currentStep.id }) onStepComplete?.(currentStepIndex, currentStep, true) // Auto-advance if enabled if (uiState.autoAdvance && navigationState.canGoNext) { setTimeout(() => goToNextStep(), 1500) } } } }, [ currentValue, currentStep, isStepCompleted, expectedSteps, currentMultiStep, uiState.autoAdvance, navigationState.canGoNext, onStepComplete, currentStepIndex, goToNextStep, dispatch, ]) // These refs are already defined above // Check if user completed the current expected step and advance to next expected step useEffect(() => { const valueChanged = currentValue !== lastValueForStepAdvancement.current // Get current expected step const currentExpectedStep = expectedSteps[currentMultiStep] console.log('🔍 Expected step advancement check:', { currentValue, lastValue: lastValueForStepAdvancement.current, valueChanged, userHasInteracted: userHasInteracted.current, expectedStepIndex: currentMultiStep, expectedStepTarget: currentExpectedStep?.targetValue, expectedStepReached: currentExpectedStep ? currentValue === currentExpectedStep.targetValue : false, totalExpectedSteps: expectedSteps.length, finalTargetReached: currentValue === currentStep?.targetValue, }) // Only advance if user interacted and we have expected steps if ( valueChanged && userHasInteracted.current && expectedSteps.length > 0 && currentExpectedStep ) { // Check if user reached the current expected step's target if (currentValue === currentExpectedStep.targetValue) { const hasMoreExpectedSteps = currentMultiStep < expectedSteps.length - 1 if (hasMoreExpectedSteps) { // Auto-advance to next expected step after a delay const timeoutId = setTimeout(() => { advanceMultiStep() lastValueForStepAdvancement.current = currentValue }, 1000) return () => clearTimeout(timeoutId) } } } }, [currentValue, currentStep, currentMultiStep, expectedSteps, advanceMultiStep]) // Update the reference when the step changes (not just value changes) useEffect(() => { lastValueForStepAdvancement.current = currentValue // Reset user interaction flag when step changes userHasInteracted.current = false // Reset last moved bead when step changes lastMovedBead.current = null }, [currentValue]) // Notify parent of events when they're added to state useEffect(() => { if (events.length > 0) { const lastEvent = events[events.length - 1] notifyEvent(lastEvent) } }, [events, notifyEvent]) // Track previous multi-step to detect changes const prevMultiStepRef = useRef(state.currentMultiStep) // Notify parent when multi-step changes (for broadcasting to observers) useEffect(() => { // Only notify in interactive mode (not observation mode) if (isObservationMode) return // Only notify if multi-step actually changed if (state.currentMultiStep !== prevMultiStepRef.current) { prevMultiStepRef.current = state.currentMultiStep onMultiStepChange?.(state.currentMultiStep) } }, [state.currentMultiStep, isObservationMode, onMultiStepChange]) // Wrap context handleValueChange to track user interaction const handleValueChange = useCallback( (newValue: number) => { // Mark that user has interacted userHasInteracted.current = true // Try to determine which bead was moved by looking at current step beads if (currentStepBeads?.length) { // Find the first bead with direction arrows as the likely moved bead const likelyMovedBead = findTopmostBeadWithArrows(currentStepBeads) if (likelyMovedBead) { lastMovedBead.current = likelyMovedBead } } // Call the context's handleValueChange contextHandleValueChange(newValue) }, [contextHandleValueChange, currentStepBeads] ) // Cleanup handled by context // Value tracking handled by context const handleBeadClick = useCallback( (beadInfo: any) => { dispatch({ type: 'ADD_EVENT', event: { type: 'BEAD_CLICKED', stepId: currentStep.id, beadInfo, timestamp: new Date(), }, }) // Check if this is the correct action if (currentStep.highlightBeads && Array.isArray(currentStep.highlightBeads)) { const isCorrectBead = currentStep.highlightBeads.some((highlight) => { // Get place value from highlight const highlightPlaceValue = highlight.placeValue // Get place value from bead click event const beadPlaceValue = beadInfo.bead ? beadInfo.bead.placeValue : 4 - beadInfo.columnIndex return ( highlightPlaceValue === beadPlaceValue && highlight.beadType === beadInfo.beadType && (highlight.position === undefined || highlight.position === beadInfo.position) ) }) if (!isCorrectBead && !silentErrors) { const errorMessage = t('error.highlight') dispatch({ type: 'SET_ERROR', error: errorMessage, }) dispatch({ type: 'ADD_EVENT', event: { type: 'ERROR_OCCURRED', stepId: currentStep.id, error: errorMessage, timestamp: new Date(), }, }) } else { dispatch({ type: 'SET_ERROR', error: null }) } } }, [currentStep, dispatch, silentErrors, t] ) const handleBeadRef = useCallback((bead: any, element: SVGElement | null) => { const key = `${bead.placeValue}-${bead.type}-${bead.position}` if (element) { beadRefs.current.set(key, element) } else { beadRefs.current.delete(key) } }, []) // UI state updaters const toggleDebugPanel = useCallback(() => { dispatch({ type: 'UPDATE_UI_STATE', updates: { showDebugPanel: !uiState.showDebugPanel }, }) }, [uiState.showDebugPanel, dispatch]) const toggleStepList = useCallback(() => { dispatch({ type: 'UPDATE_UI_STATE', updates: { showStepList: !uiState.showStepList }, }) }, [uiState.showStepList, dispatch]) const toggleAutoAdvance = useCallback(() => { dispatch({ type: 'UPDATE_UI_STATE', updates: { autoAdvance: !uiState.autoAdvance }, }) }, [uiState.autoAdvance, dispatch]) // Two-level dynamic column highlights: group terms + individual term const dynamicColumnHighlights = useMemo(() => { const highlights: Record = {} // Level 1: Group highlights (blue glow for all terms in activeTermIndices) activeTermIndices.forEach((termIndex) => { const columnIndex = getColumnFromTermIndex(termIndex, true) // Use group column (rhsPlace) if (columnIndex !== null) { highlights[columnIndex] = { // Group background glow effect (blue) backgroundGlow: { fill: 'rgba(59, 130, 246, 0.2)', blur: 4, spread: 16, }, // Group numeral highlighting numerals: { color: '#1e40af', backgroundColor: 'rgba(219, 234, 254, 0.8)', fontWeight: 'bold', borderRadius: 4, borderWidth: 1, borderColor: '#3b82f6', }, } } }) // Level 2: Individual term highlight (orange glow, overrides group styling) if (activeIndividualTermIndex !== null) { const individualColumnIndex = getColumnFromTermIndex(activeIndividualTermIndex, false) // Use individual column (termPlace) if (individualColumnIndex !== null) { highlights[individualColumnIndex] = { // Individual background glow effect (orange) - overrides group glow backgroundGlow: { fill: 'rgba(249, 115, 22, 0.3)', blur: 6, spread: 20, }, // Individual numeral highlighting (orange) numerals: { color: '#c2410c', backgroundColor: 'rgba(254, 215, 170, 0.9)', fontWeight: 'bold', borderRadius: 6, borderWidth: 2, borderColor: '#ea580c', }, } } } return highlights }, [activeTermIndices, activeIndividualTermIndex, getColumnFromTermIndex]) // Memoize custom styles calculation to avoid expensive recalculation on every render const customStyles = useMemo(() => { // Separate bead-level and column-level styles const beadLevelHighlights: Record = {} const columnLevelHighlights: Record = {} // Process static highlights from step configuration (bead-specific) if (currentStep.highlightBeads && Array.isArray(currentStep.highlightBeads)) { currentStep.highlightBeads.forEach((highlight) => { // Convert placeValue to columnIndex for AbacusReact compatibility const columnIndex = abacusColumns - 1 - highlight.placeValue // Skip highlights for columns that don't exist in the rendered abacus if (columnIndex < 0 || columnIndex >= abacusColumns) { return } // Initialize column if it doesn't exist if (!beadLevelHighlights[columnIndex]) { beadLevelHighlights[columnIndex] = {} } // Add the bead style to the appropriate type if (highlight.beadType === 'earth' && highlight.position !== undefined) { if (!beadLevelHighlights[columnIndex].earth) { beadLevelHighlights[columnIndex].earth = {} } beadLevelHighlights[columnIndex].earth[highlight.position] = { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 3, } } else { beadLevelHighlights[columnIndex][highlight.beadType] = { fill: '#fbbf24', stroke: '#f59e0b', strokeWidth: 3, } } }) } // Process dynamic column highlights (column-level: backgroundGlow, numerals) Object.keys(dynamicColumnHighlights).forEach((columnIndexStr) => { const columnIndex = parseInt(columnIndexStr, 10) // Skip highlights for columns that don't exist in the rendered abacus if (columnIndex < 0 || columnIndex >= abacusColumns) { return } // Dynamic highlights are column-level (backgroundGlow, numerals) columnLevelHighlights[columnIndex] = dynamicColumnHighlights[columnIndex] }) // Build the custom styles object const styles: any = {} // Add bead-level highlights to styles.beads if (Object.keys(beadLevelHighlights).length > 0) { styles.beads = beadLevelHighlights } // Add column-level highlights to styles.columns if (Object.keys(columnLevelHighlights).length > 0) { styles.columns = columnLevelHighlights } // Add frame styling for dark mode if (theme === 'dark') { // Column dividers (global for all columns) styles.columnPosts = { fill: 'rgba(255, 255, 255, 0.3)', // High contrast fill for visibility stroke: 'rgba(255, 255, 255, 0.2)', strokeWidth: 2, } // Reckoning bar (horizontal middle bar) styles.reckoningBar = { fill: 'rgba(255, 255, 255, 0.4)', // High contrast fill for visibility stroke: 'rgba(255, 255, 255, 0.25)', strokeWidth: 3, } } // Debug logging for custom styles if (Object.keys(styles).length > 0) { console.log( '📋 TUTORIAL CUSTOM STYLES:', JSON.stringify( { beadLevelHighlights, columnLevelHighlights, finalStyles: styles, currentStepHighlightBeads: currentStep.highlightBeads, abacusColumns, }, null, 2 ) ) } return Object.keys(styles).length > 0 ? styles : undefined }, [currentStep.highlightBeads, dynamicColumnHighlights, abacusColumns, theme]) if (!currentStep) { return
{t('noSteps')}
} return (
{/* Header */} {!hideNavigation && (

{tutorial.title}

{t('header.step', { current: currentStepIndex + 1, total: tutorial.steps.length, title: currentStep.title, })}

{isDebugMode && ( <> {/* Multi-step navigation controls */} {currentStep.multiStepInstructions && currentStep.multiStepInstructions.length > 1 && ( <>
{t('controls.multiStep.label', { current: currentMultiStep + 1, total: currentStep.multiStepInstructions.length, })}
)} )}
{/* Progress bar */}
)}
{/* Step list sidebar */} {uiState.showStepList && (

{t('sidebar.title')}

{tutorial.steps && Array.isArray(tutorial.steps) ? ( tutorial.steps.map((step, index) => ( )) ) : (
{t('sidebar.empty')}
)}
)} {/* Main content */}
{/* Step content */}
{/* Step instructions */}

{currentStep.problem}

{currentStep.description}

{/* Hide action description for multi-step problems since it duplicates pedagogical decomposition */} {!currentStep.multiStepInstructions && (

{currentStep.actionDescription}

)}
{/* Multi-step instructions panel */} {!hideTooltip && currentStep.multiStepInstructions && currentStep.multiStepInstructions.length > 0 && (

{t('guidance.title')}

{/* Pedagogical decomposition with interactive reasoning */} {fullDecomposition && isMeaningfulDecomposition && (
)}
{(() => { // Only show the current step instruction const currentInstruction = currentStep.multiStepInstructions[currentMultiStep] const _mathTerm = expectedSteps[currentMultiStep]?.mathematicalTerm if (!currentInstruction) return null // Hide "Next Action" when at the expected starting state for this step const isAtExpectedStartingState = (() => { if (currentMultiStep === 0) { // First step: check if current value matches tutorial step start value return currentValue === currentStep.startValue } else { // Subsequent steps: check if current value matches previous step's target const previousStepTarget = expectedSteps[currentMultiStep - 1]?.targetValue return currentValue === previousStepTarget } })() const hasMeaningfulSummary = currentStepSummary && !currentStepSummary.includes('No changes needed') // Only show help if: // 1. Not at expected starting state (user needs to do something) // 2. Has meaningful summary to show // 3. Timer has expired (user appears stuck for 8+ seconds) const _needsAction = !isAtExpectedStartingState && hasMeaningfulSummary && showHelpForCurrentStep return (
{currentInstruction}
) })()}
)} {/* Error message */} {error && (
{error}
)} {/* Success message removed from inline layout - now positioned as overlay */} {/* Abacus */}
{/* Debug info */} {isDebugMode && (
Step Debug Info:
Current Multi-Step: {currentMultiStep}
Total Steps: {currentStep.totalSteps || 'undefined'}
Step Bead Highlights:{' '} {currentStepBeads ? currentStepBeads.length : 'undefined'}
Dynamic Recalc: {currentValue} → {currentStep.targetValue}
Show Direction Indicators: true
Multi-Step Instructions:{' '} {currentStep.multiStepInstructions?.length || 'undefined'}
{currentStepBeads && (
Current Step Beads ({currentMultiStep}):
{currentStepBeads .filter((bead) => bead.stepIndex === currentMultiStep) .map((bead, i) => (
- Place {bead.placeValue} {bead.beadType}{' '} {bead.position !== undefined ? `pos ${bead.position}` : ''} →{' '} {bead.direction}
))}
)}
)}
{/* Tooltip */} {!hideTooltip && currentStep.tooltip && (

{currentStep.tooltip.content}

{currentStep.tooltip.explanation}

)}
{/* Navigation controls */} {!hideNavigation && (
{t('navigation.stepCounter', { current: currentStepIndex + 1, total: navigationState.totalSteps, })}
)}
{/* Debug panel */} {uiState.showDebugPanel && (

{t('debugPanel.title')}

{/* Current state */}

{t('debugPanel.currentState')}

{t('debugPanel.step', { current: currentStepIndex + 1, total: navigationState.totalSteps, })}
{t('debugPanel.value', { value: currentValue })}
{t('debugPanel.target', { value: currentStep.targetValue })}
{t('debugPanel.completed', { status: t(`debugPanel.completedStatus.${isStepCompleted ? 'yes' : 'no'}`), })}
{t('debugPanel.time', { seconds: Math.round((Date.now() - stepStartTime) / 1000), })}
{/* Event log */}

{t('debugPanel.eventLog')}

{events .slice(-20) .reverse() .map((event, index) => (
{event.type}
{event.timestamp.toLocaleTimeString()}
{event.type === 'VALUE_CHANGED' && (
{event.oldValue} → {event.newValue}
)} {event.type === 'ERROR_OCCURRED' && (
{event.error}
)}
))}
)}
{/* Add CSS animations */}
) } // Export wrapper component with provider export function TutorialPlayer(props: TutorialPlayerProps) { return ( ) }