'use client' import React, { createContext, useCallback, useContext, useMemo, useReducer, useRef, useState, } from 'react' import type { Tutorial, TutorialEvent, TutorialStep, UIState } from '../../types/tutorial' import { generateUnifiedInstructionSequence, type UnifiedStepData, } from '../../utils/unifiedStepGenerator' // Exact same interfaces from TutorialPlayer.tsx 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' } // Exact same reducer from TutorialPlayer.tsx 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 } } // Context interfaces interface TutorialContextType { state: TutorialPlayerState dispatch: React.Dispatch tutorial: Tutorial isProgrammaticChange: React.MutableRefObject showHelpForCurrentStep: boolean setShowHelpForCurrentStep: React.Dispatch> beadRefs: React.MutableRefObject> // Computed values currentStep: TutorialStep expectedSteps: ExpectedStep[] fullDecomposition: string unifiedSteps: UnifiedStepData[] // NEW: Add unified steps with provenance customStyles: any // Term-to-column highlighting state activeTermIndices: Set setActiveTermIndices: (indices: Set) => void activeIndividualTermIndex: number | null setActiveIndividualTermIndex: (index: number | null) => void activeGroupTargetColumn: number | null setActiveGroupTargetColumn: (columnIndex: number | null) => void getColumnFromTermIndex: (termIndex: number, useGroupColumn?: boolean) => number | null getTermIndicesFromColumn: (columnIndex: number) => number[] getGroupTermIndicesFromTermIndex: (termIndex: number) => number[] handleAbacusColumnHover: (columnIndex: number, isHovering: boolean) => void // Action functions goToStep: (stepIndex: number) => void goToNextStep: () => void goToPreviousStep: () => void handleValueChange: (newValue: number) => void advanceMultiStep: () => void previousMultiStep: () => void resetMultiStep: () => void handleBeadClick: (beadInfo: any) => void handleBeadRef: (bead: any, element: SVGElement | null) => void toggleDebugPanel: () => void toggleStepList: () => void toggleAutoAdvance: () => void notifyEvent: (event: TutorialEvent) => void getCurrentStepBeads: () => any[] getCurrentStepSummary: () => any renderHighlightedDecomposition: () => any } interface TutorialProviderProps { tutorial: Tutorial initialStepIndex?: number showDebugPanel?: boolean onStepChange?: (stepIndex: number, step: TutorialStep) => void onStepComplete?: (stepIndex: number, step: TutorialStep, success: boolean) => void onTutorialComplete?: (score: number, timeSpent: number) => void onEvent?: (event: TutorialEvent) => void children: React.ReactNode } // Create context const TutorialContext = createContext(null) // Provider component export function TutorialProvider({ tutorial, initialStepIndex = 0, showDebugPanel = false, onStepChange, onStepComplete, onTutorialComplete, onEvent, children, }: TutorialProviderProps) { const isProgrammaticChange = useRef(false) const [showHelpForCurrentStep, setShowHelpForCurrentStep] = useState(false) const beadRefs = useRef>(new Map()) // Term-to-column highlighting state const [activeTermIndices, setActiveTermIndices] = useState>(new Set()) const [activeIndividualTermIndex, setActiveIndividualTermIndex] = useState(null) const [activeGroupTargetColumn, setActiveGroupTargetColumn] = useState(null) const [state, dispatch] = useReducer(tutorialPlayerReducer, { currentStepIndex: initialStepIndex, currentValue: 0, isStepCompleted: false, error: null, events: [], stepStartTime: Date.now(), multiStepStartTime: Date.now(), currentMultiStep: 0, uiState: { isPlaying: true, isPaused: false, isEditing: false, showDebugPanel, showStepList: false, autoAdvance: false, playbackSpeed: 1, }, }) // Initialize the first step on mount React.useEffect(() => { if (tutorial.steps.length > 0) { const step = tutorial.steps[initialStepIndex] // Mark as programmatic change to prevent AbacusReact feedback loop isProgrammaticChange.current = true dispatch({ type: 'INITIALIZE_STEP', stepIndex: initialStepIndex, startValue: step.startValue, stepId: step.id, }) onStepChange?.(initialStepIndex, step) } }, [initialStepIndex, onStepChange, tutorial.steps.length, tutorial.steps[initialStepIndex]]) // Empty dependency array - only run on mount // Current step and computed values const currentStep = tutorial.steps[state.currentStepIndex] const { expectedSteps, fullDecomposition, unifiedSteps } = useMemo(() => { try { const unifiedSequence = generateUnifiedInstructionSequence( currentStep.startValue, currentStep.targetValue ) // Map UnifiedStepData to ExpectedStep format const mappedSteps: ExpectedStep[] = unifiedSequence.steps.map((step, index) => ({ index, stepIndex: step.stepIndex, targetValue: step.expectedValue, startValue: index === 0 ? currentStep.startValue : unifiedSequence.steps[index - 1].expectedValue, description: step.englishInstruction, mathematicalTerm: step.mathematicalTerm, termPosition: step.termPosition, })) return { expectedSteps: mappedSteps, fullDecomposition: unifiedSequence.fullDecomposition, unifiedSteps: unifiedSequence.steps, // NEW: Include raw steps with provenance } } catch (error) { console.warn('Failed to generate unified sequence:', error) return { expectedSteps: [], fullDecomposition: '', unifiedSteps: [] } } }, [currentStep.startValue, currentStep.targetValue]) // Term-to-column mapping function const getColumnFromTermIndex = useCallback( (termIndex: number, useGroupColumn = false) => { const step = unifiedSteps[termIndex] if (!step?.provenance) return null // For group highlighting: use rhsPlace (target column) // For individual highlighting: use termPlace (individual term column) const placeValue = useGroupColumn ? step.provenance.rhsPlace : (step.provenance.termPlace ?? step.provenance.rhsPlace) // Convert place value (0=ones, 1=tens, 2=hundreds) to columnIndex (4=ones, 3=tens, 2=hundreds) return 4 - placeValue }, [unifiedSteps] ) // Column-to-terms mapping function (for bidirectional interaction) const getTermIndicesFromColumn = useCallback( (columnIndex: number) => { const termIndices: number[] = [] unifiedSteps.forEach((step, index) => { if (step.provenance) { // Use termPlace if available, otherwise fallback to rhsPlace const placeValue = step.provenance.termPlace ?? step.provenance.rhsPlace const stepColumnIndex = 4 - placeValue if (stepColumnIndex === columnIndex) { termIndices.push(index) } } }) return termIndices }, [unifiedSteps] ) // Group-to-terms mapping function (for complement groups) const getGroupTermIndicesFromTermIndex = useCallback( (termIndex: number) => { console.log('🔍 getGroupTermIndicesFromTermIndex called with termIndex:', termIndex) const step = unifiedSteps[termIndex] console.log(' - Step data:', { mathematicalTerm: step?.mathematicalTerm, hasProvenance: !!step?.provenance, groupId: step?.provenance?.groupId, rhsPlace: step?.provenance?.rhsPlace, rhsValue: step?.provenance?.rhsValue, }) if (!step?.provenance?.groupId) { console.log(' - No groupId found, returning empty array') return [] } const groupId = step.provenance.groupId console.log(' - Found groupId:', groupId) const groupTermIndices: number[] = [] unifiedSteps.forEach((groupStep, index) => { if (groupStep.provenance?.groupId === groupId) { groupTermIndices.push(index) console.log( ` - Found group member: term ${index} "${groupStep.mathematicalTerm}" (rhsPlace: ${groupStep.provenance.rhsPlace})` ) } }) console.log(' - Final group term indices:', groupTermIndices) return groupTermIndices }, [unifiedSteps] ) // Abacus column hover handler for bidirectional interaction const handleAbacusColumnHover = useCallback( (columnIndex: number, isHovering: boolean) => { if (isHovering) { // Find all terms that correspond to this column const relatedTerms = getTermIndicesFromColumn(columnIndex) setActiveTermIndices(new Set(relatedTerms)) } else { setActiveTermIndices(new Set()) } }, [getTermIndicesFromColumn] ) // Navigation state const navigationState = useMemo( () => ({ canGoNext: state.currentStepIndex < tutorial.steps.length - 1, canGoPrevious: state.currentStepIndex > 0, }), [state.currentStepIndex, tutorial.steps.length] ) // Action functions const notifyEvent = useCallback( (event: TutorialEvent) => { onEvent?.(event) }, [onEvent] ) const goToStep = useCallback( (stepIndex: number) => { if (stepIndex >= 0 && stepIndex < tutorial.steps.length) { const step = tutorial.steps[stepIndex] // Mark as programmatic change to prevent AbacusReact feedback loop isProgrammaticChange.current = true dispatch({ type: 'INITIALIZE_STEP', stepIndex, startValue: step.startValue, stepId: step.id, }) onStepChange?.(stepIndex, step) } }, [tutorial.steps, onStepChange] ) // Clear isProgrammaticChange flag after external value changes have settled React.useEffect(() => { // Use a small timeout to ensure the AbacusReact has processed the value change const timeoutId = setTimeout(() => { if (isProgrammaticChange.current) { isProgrammaticChange.current = false } }, 100) return () => clearTimeout(timeoutId) }, []) const goToNextStep = useCallback(() => { if (navigationState.canGoNext) { goToStep(state.currentStepIndex + 1) } else if (state.isStepCompleted && state.currentStepIndex === tutorial.steps.length - 1) { // On the last step and step is completed - trigger tutorial completion const startTime = state.events.length > 0 ? new Date(state.events[0].timestamp).getTime() : Date.now() - 60000 // fallback to 1 minute ago const timeSpent = Math.round((Date.now() - startTime) / 1000) // Calculate score based on completed steps (100% for completing all steps) const score = 100 onTutorialComplete?.(score, timeSpent) } }, [ navigationState.canGoNext, goToStep, state.currentStepIndex, state.isStepCompleted, state.events, tutorial.steps.length, onTutorialComplete, ]) const goToPreviousStep = useCallback(() => { if (navigationState.canGoPrevious) { goToStep(state.currentStepIndex - 1) } }, [navigationState.canGoPrevious, goToStep, state.currentStepIndex]) const handleValueChange = useCallback( (newValue: number) => { if (isProgrammaticChange.current) { isProgrammaticChange.current = false return } const oldValue = state.currentValue dispatch({ type: 'USER_VALUE_CHANGE', oldValue, newValue, stepId: currentStep.id, }) // Check if step is completed if (newValue === currentStep.targetValue) { dispatch({ type: 'COMPLETE_STEP', stepId: currentStep.id, }) onStepComplete?.(state.currentStepIndex, currentStep, true) } }, [state.currentValue, currentStep, onStepComplete, state.currentStepIndex] ) const handleBeadClick = useCallback( (beadInfo: any) => { dispatch({ type: 'ADD_EVENT', event: { type: 'BEAD_CLICKED', stepId: currentStep.id, timestamp: new Date(), beadInfo, }, }) }, [currentStep.id] ) 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) } }, []) const toggleDebugPanel = useCallback(() => { dispatch({ type: 'UPDATE_UI_STATE', updates: { showDebugPanel: !state.uiState.showDebugPanel }, }) }, [state.uiState.showDebugPanel]) const toggleStepList = useCallback(() => { dispatch({ type: 'UPDATE_UI_STATE', updates: { showStepList: !state.uiState.showStepList }, }) }, [state.uiState.showStepList]) const toggleAutoAdvance = useCallback(() => { dispatch({ type: 'UPDATE_UI_STATE', updates: { autoAdvance: !state.uiState.autoAdvance }, }) }, [state.uiState.autoAdvance]) const advanceMultiStep = useCallback(() => { dispatch({ type: 'ADVANCE_MULTI_STEP' }) }, []) const previousMultiStep = useCallback(() => { dispatch({ type: 'PREVIOUS_MULTI_STEP' }) }, []) const resetMultiStep = useCallback(() => { dispatch({ type: 'RESET_MULTI_STEP' }) }, []) const getCurrentStepBeads = useCallback(() => { if (expectedSteps.length === 0) return currentStep.stepBeadHighlights || [] if (state.currentMultiStep < expectedSteps.length) { // Since we mapped from UnifiedStepData, we need to get the original step data // to access beadMovements. For now, fall back to existing stepBeadHighlights return currentStep.stepBeadHighlights || [] } return [] }, [expectedSteps, currentStep.stepBeadHighlights, state.currentMultiStep]) const getCurrentStepSummary = useCallback(() => { if (expectedSteps.length === 0) return null if (state.currentMultiStep < expectedSteps.length) { const currentExpectedStep = expectedSteps[state.currentMultiStep] return { description: currentExpectedStep.description, mathematicalTerm: currentExpectedStep.mathematicalTerm, termPosition: currentExpectedStep.termPosition, } } return null }, [expectedSteps, state.currentMultiStep]) const renderHighlightedDecomposition = useCallback(() => { if (!fullDecomposition || expectedSteps.length === 0) return null const currentExpectedStep = expectedSteps[state.currentMultiStep] if (!currentExpectedStep?.termPosition) return fullDecomposition const { startIndex, endIndex } = currentExpectedStep.termPosition const before = fullDecomposition.slice(0, startIndex) const highlighted = fullDecomposition.slice(startIndex, endIndex + 1) const after = fullDecomposition.slice(endIndex + 1) return { before, highlighted, after } }, [fullDecomposition, expectedSteps, state.currentMultiStep]) const customStyles = useMemo(() => { if (!currentStep.highlightBeads || !Array.isArray(currentStep.highlightBeads)) { return undefined } return { beads: currentStep.highlightBeads.reduce((acc, highlight) => { const columnIndex = 4 - highlight.placeValue if (!acc[columnIndex]) { acc[columnIndex] = {} } if (highlight.beadType === 'heaven') { acc[columnIndex].heaven = { backgroundColor: '#ffeb3b', border: '2px solid #ff9800', } } else if (highlight.beadType === 'earth') { if (!acc[columnIndex].earth) { acc[columnIndex].earth = {} } const position = highlight.position || 0 acc[columnIndex].earth[position] = { backgroundColor: '#ffeb3b', border: '2px solid #ff9800', } } return acc }, {} as any), } }, [currentStep.highlightBeads]) const value: TutorialContextType = { state, dispatch, tutorial, isProgrammaticChange, showHelpForCurrentStep, setShowHelpForCurrentStep, beadRefs, // Computed values currentStep, expectedSteps, fullDecomposition, unifiedSteps, customStyles, // Term-to-column highlighting state activeTermIndices, setActiveTermIndices, activeIndividualTermIndex, setActiveIndividualTermIndex, activeGroupTargetColumn, setActiveGroupTargetColumn, getColumnFromTermIndex, getTermIndicesFromColumn, getGroupTermIndicesFromTermIndex, handleAbacusColumnHover, // Action functions goToStep, goToNextStep, goToPreviousStep, handleValueChange, advanceMultiStep, previousMultiStep, resetMultiStep, handleBeadClick, handleBeadRef, toggleDebugPanel, toggleStepList, toggleAutoAdvance, notifyEvent, getCurrentStepBeads, getCurrentStepSummary, renderHighlightedDecomposition, } return {children} } // Hook to use the context export function useTutorialContext() { const context = useContext(TutorialContext) if (!context) { throw new Error('useTutorialContext must be used within a TutorialProvider') } return context } // Custom hooks for convenient access to specific context parts export function useTutorialState() { const { state } = useTutorialContext() return state } export function useTutorialData() { const { tutorial, currentStep, expectedSteps, fullDecomposition } = useTutorialContext() return { tutorial, currentStep, expectedSteps, fullDecomposition } } export function useTutorialActions() { const { goToStep, goToNextStep, goToPreviousStep, handleValueChange, handleBeadClick, handleBeadRef, toggleDebugPanel, toggleStepList, toggleAutoAdvance, notifyEvent, } = useTutorialContext() return { goToStep, goToNextStep, goToPreviousStep, handleValueChange, handleBeadClick, handleBeadRef, toggleDebugPanel, toggleStepList, toggleAutoAdvance, notifyEvent, } } export function useTutorialHelpers() { const { getCurrentStepBeads, getCurrentStepSummary, renderHighlightedDecomposition, customStyles, } = useTutorialContext() return { getCurrentStepBeads, getCurrentStepSummary, renderHighlightedDecomposition, customStyles, } } export function useTutorialRefs() { const { isProgrammaticChange, beadRefs, showHelpForCurrentStep, setShowHelpForCurrentStep } = useTutorialContext() return { isProgrammaticChange, beadRefs, showHelpForCurrentStep, setShowHelpForCurrentStep, } } // Export types for use in other components export type { TutorialPlayerState, TutorialPlayerAction, ExpectedStep }