fix(tutorial): expose activeGroupTargetColumn state to context

Fixed production runtime error "TypeError: d.value is not a function"
in tutorial component caused by missing context values.

Root cause:
- TutorialContext interface declared activeGroupTargetColumn and
  setActiveGroupTargetColumn properties
- State variables were defined with underscore prefixes (_activeGroupTargetColumn)
  indicating they were unused
- These values were not included in the context value object
- Components attempting to call setActiveGroupTargetColumn() received
  undefined and crashed with "is not a function"

Fix:
- Removed underscore prefixes from state variable declarations
- Added activeGroupTargetColumn and setActiveGroupTargetColumn to
  context value object
- Components now receive proper state and setter functions

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-11-08 20:31:30 -06:00
parent e6a8cb4227
commit 69f759a178

View File

@@ -1,4 +1,4 @@
"use client";
'use client'
import React, {
createContext,
@@ -8,69 +8,64 @@ import React, {
useReducer,
useRef,
useState,
} from "react";
import type {
Tutorial,
TutorialEvent,
TutorialStep,
UIState,
} from "../../types/tutorial";
} from 'react'
import type { Tutorial, TutorialEvent, TutorialStep, UIState } from '../../types/tutorial'
import {
generateUnifiedInstructionSequence,
type UnifiedStepData,
} from "../../utils/unifiedStepGenerator";
} 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)
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
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: 'INITIALIZE_STEP'
stepIndex: number
startValue: number
stepId: string
}
| {
type: "USER_VALUE_CHANGE";
oldValue: number;
newValue: 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<UIState> }
| { type: "ADVANCE_MULTI_STEP" }
| { type: "PREVIOUS_MULTI_STEP" }
| { type: "RESET_MULTI_STEP" };
| { type: 'COMPLETE_STEP'; stepId: string }
| { type: 'SET_ERROR'; error: string | null }
| { type: 'ADD_EVENT'; event: TutorialEvent }
| { type: 'UPDATE_UI_STATE'; updates: Partial<UIState> }
| { type: 'ADVANCE_MULTI_STEP' }
| { type: 'PREVIOUS_MULTI_STEP' }
| { type: 'RESET_MULTI_STEP' }
// Exact same reducer from TutorialPlayer.tsx
function tutorialPlayerReducer(
state: TutorialPlayerState,
action: TutorialPlayerAction,
action: TutorialPlayerAction
): TutorialPlayerState {
switch (action.type) {
case "INITIALIZE_STEP":
case 'INITIALIZE_STEP':
return {
...state,
currentStepIndex: action.stepIndex,
@@ -83,30 +78,30 @@ function tutorialPlayerReducer(
events: [
...state.events,
{
type: "STEP_STARTED",
type: 'STEP_STARTED',
stepId: action.stepId,
timestamp: new Date(),
},
],
};
}
case "USER_VALUE_CHANGE":
case 'USER_VALUE_CHANGE':
return {
...state,
currentValue: action.newValue,
events: [
...state.events,
{
type: "VALUE_CHANGED",
type: 'VALUE_CHANGED',
stepId: action.stepId,
oldValue: action.oldValue,
newValue: action.newValue,
timestamp: new Date(),
},
],
};
}
case "COMPLETE_STEP":
case 'COMPLETE_STEP':
return {
...state,
isStepCompleted: true,
@@ -114,124 +109,117 @@ function tutorialPlayerReducer(
events: [
...state.events,
{
type: "STEP_COMPLETED",
type: 'STEP_COMPLETED',
stepId: action.stepId,
success: true,
timestamp: new Date(),
},
],
};
}
case "SET_ERROR":
case 'SET_ERROR':
return {
...state,
error: action.error,
};
}
case "ADD_EVENT":
case 'ADD_EVENT':
return {
...state,
events: [...state.events, action.event],
};
}
case "UPDATE_UI_STATE":
case 'UPDATE_UI_STATE':
return {
...state,
uiState: { ...state.uiState, ...action.updates },
};
}
case "ADVANCE_MULTI_STEP":
case 'ADVANCE_MULTI_STEP':
return {
...state,
currentMultiStep: state.currentMultiStep + 1,
multiStepStartTime: Date.now(), // Reset timer for new multi-step
};
}
case "PREVIOUS_MULTI_STEP":
case 'PREVIOUS_MULTI_STEP':
return {
...state,
currentMultiStep: Math.max(0, state.currentMultiStep - 1),
};
}
case "RESET_MULTI_STEP":
case 'RESET_MULTI_STEP':
return {
...state,
currentMultiStep: 0,
};
}
default:
return state;
return state
}
}
// Context interfaces
interface TutorialContextType {
state: TutorialPlayerState;
dispatch: React.Dispatch<TutorialPlayerAction>;
tutorial: Tutorial;
isProgrammaticChange: React.MutableRefObject<boolean>;
showHelpForCurrentStep: boolean;
setShowHelpForCurrentStep: React.Dispatch<React.SetStateAction<boolean>>;
beadRefs: React.MutableRefObject<Map<string, SVGElement>>;
state: TutorialPlayerState
dispatch: React.Dispatch<TutorialPlayerAction>
tutorial: Tutorial
isProgrammaticChange: React.MutableRefObject<boolean>
showHelpForCurrentStep: boolean
setShowHelpForCurrentStep: React.Dispatch<React.SetStateAction<boolean>>
beadRefs: React.MutableRefObject<Map<string, SVGElement>>
// Computed values
currentStep: TutorialStep;
expectedSteps: ExpectedStep[];
fullDecomposition: string;
unifiedSteps: UnifiedStepData[]; // NEW: Add unified steps with provenance
customStyles: any;
currentStep: TutorialStep
expectedSteps: ExpectedStep[]
fullDecomposition: string
unifiedSteps: UnifiedStepData[] // NEW: Add unified steps with provenance
customStyles: any
// Term-to-column highlighting state
activeTermIndices: Set<number>;
setActiveTermIndices: (indices: Set<number>) => 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;
activeTermIndices: Set<number>
setActiveTermIndices: (indices: Set<number>) => 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;
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;
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<TutorialContextType | null>(null);
const TutorialContext = createContext<TutorialContextType | null>(null)
// Provider component
export function TutorialProvider({
@@ -244,20 +232,14 @@ export function TutorialProvider({
onEvent,
children,
}: TutorialProviderProps) {
const isProgrammaticChange = useRef(false);
const [showHelpForCurrentStep, setShowHelpForCurrentStep] = useState(false);
const beadRefs = useRef<Map<string, SVGElement>>(new Map());
const isProgrammaticChange = useRef(false)
const [showHelpForCurrentStep, setShowHelpForCurrentStep] = useState(false)
const beadRefs = useRef<Map<string, SVGElement>>(new Map())
// Term-to-column highlighting state
const [activeTermIndices, setActiveTermIndices] = useState<Set<number>>(
new Set(),
);
const [activeIndividualTermIndex, setActiveIndividualTermIndex] = useState<
number | null
>(null);
const [_activeGroupTargetColumn, _setActiveGroupTargetColumn] = useState<
number | null
>(null);
const [activeTermIndices, setActiveTermIndices] = useState<Set<number>>(new Set())
const [activeIndividualTermIndex, setActiveIndividualTermIndex] = useState<number | null>(null)
const [activeGroupTargetColumn, setActiveGroupTargetColumn] = useState<number | null>(null)
const [state, dispatch] = useReducer(tutorialPlayerReducer, {
currentStepIndex: initialStepIndex,
@@ -277,163 +259,150 @@ export function TutorialProvider({
autoAdvance: false,
playbackSpeed: 1,
},
});
})
// Initialize the first step on mount
React.useEffect(() => {
if (tutorial.steps.length > 0) {
const step = tutorial.steps[initialStepIndex];
const step = tutorial.steps[initialStepIndex]
// Mark as programmatic change to prevent AbacusReact feedback loop
isProgrammaticChange.current = true;
isProgrammaticChange.current = true
dispatch({
type: "INITIALIZE_STEP",
type: 'INITIALIZE_STEP',
stepIndex: initialStepIndex,
startValue: step.startValue,
stepId: step.id,
});
onStepChange?.(initialStepIndex, step);
})
onStepChange?.(initialStepIndex, step)
}
}, [
initialStepIndex,
onStepChange,
tutorial.steps.length,
tutorial.steps[initialStepIndex],
]); // Empty dependency array - only run on mount
}, [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 currentStep = tutorial.steps[state.currentStepIndex]
const { expectedSteps, fullDecomposition, unifiedSteps } = useMemo(() => {
try {
const unifiedSequence = generateUnifiedInstructionSequence(
currentStep.startValue,
currentStep.targetValue,
);
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,
}),
);
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: [] };
console.warn('Failed to generate unified sequence:', error)
return { expectedSteps: [], fullDecomposition: '', unifiedSteps: [] }
}
}, [currentStep.startValue, currentStep.targetValue]);
}, [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;
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);
: (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;
return 4 - placeValue
},
[unifiedSteps],
);
[unifiedSteps]
)
// Column-to-terms mapping function (for bidirectional interaction)
const getTermIndicesFromColumn = useCallback(
(columnIndex: number) => {
const termIndices: 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;
const placeValue = step.provenance.termPlace ?? step.provenance.rhsPlace
const stepColumnIndex = 4 - placeValue
if (stepColumnIndex === columnIndex) {
termIndices.push(index);
termIndices.push(index)
}
}
});
})
return termIndices;
return termIndices
},
[unifiedSteps],
);
[unifiedSteps]
)
// Group-to-terms mapping function (for complement groups)
const getGroupTermIndicesFromTermIndex = useCallback(
(termIndex: number) => {
console.log(
"🔍 getGroupTermIndicesFromTermIndex called with termIndex:",
termIndex,
);
console.log('🔍 getGroupTermIndicesFromTermIndex called with termIndex:', termIndex)
const step = unifiedSteps[termIndex];
console.log(" - Step data:", {
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 [];
console.log(' - No groupId found, returning empty array')
return []
}
const groupId = step.provenance.groupId;
console.log(" - Found groupId:", groupId);
const groupId = step.provenance.groupId
console.log(' - Found groupId:', groupId)
const groupTermIndices: number[] = [];
const groupTermIndices: number[] = []
unifiedSteps.forEach((groupStep, index) => {
if (groupStep.provenance?.groupId === groupId) {
groupTermIndices.push(index);
groupTermIndices.push(index)
console.log(
` - Found group member: term ${index} "${groupStep.mathematicalTerm}" (rhsPlace: ${groupStep.provenance.rhsPlace})`,
);
` - Found group member: term ${index} "${groupStep.mathematicalTerm}" (rhsPlace: ${groupStep.provenance.rhsPlace})`
)
}
});
})
console.log(" - Final group term indices:", groupTermIndices);
return groupTermIndices;
console.log(' - Final group term indices:', groupTermIndices)
return groupTermIndices
},
[unifiedSteps],
);
[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));
const relatedTerms = getTermIndicesFromColumn(columnIndex)
setActiveTermIndices(new Set(relatedTerms))
} else {
setActiveTermIndices(new Set());
setActiveTermIndices(new Set())
}
},
[getTermIndicesFromColumn],
);
[getTermIndicesFromColumn]
)
// Navigation state
const navigationState = useMemo(
@@ -441,222 +410,219 @@ export function TutorialProvider({
canGoNext: state.currentStepIndex < tutorial.steps.length - 1,
canGoPrevious: state.currentStepIndex > 0,
}),
[state.currentStepIndex, tutorial.steps.length],
);
[state.currentStepIndex, tutorial.steps.length]
)
// Action functions
const notifyEvent = useCallback(
(event: TutorialEvent) => {
onEvent?.(event);
onEvent?.(event)
},
[onEvent],
);
[onEvent]
)
const goToStep = useCallback(
(stepIndex: number) => {
if (stepIndex >= 0 && stepIndex < tutorial.steps.length) {
const step = tutorial.steps[stepIndex];
const step = tutorial.steps[stepIndex]
// Mark as programmatic change to prevent AbacusReact feedback loop
isProgrammaticChange.current = true;
isProgrammaticChange.current = true
dispatch({
type: "INITIALIZE_STEP",
type: 'INITIALIZE_STEP',
stepIndex,
startValue: step.startValue,
stepId: step.id,
});
onStepChange?.(stepIndex, step);
})
onStepChange?.(stepIndex, step)
}
},
[tutorial.steps, onStepChange],
);
[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;
isProgrammaticChange.current = false
}
}, 100);
}, 100)
return () => clearTimeout(timeoutId);
}, []);
return () => clearTimeout(timeoutId)
}, [])
const goToNextStep = useCallback(() => {
if (navigationState.canGoNext) {
goToStep(state.currentStepIndex + 1);
goToStep(state.currentStepIndex + 1)
}
}, [navigationState.canGoNext, goToStep, state.currentStepIndex]);
}, [navigationState.canGoNext, goToStep, state.currentStepIndex])
const goToPreviousStep = useCallback(() => {
if (navigationState.canGoPrevious) {
goToStep(state.currentStepIndex - 1);
goToStep(state.currentStepIndex - 1)
}
}, [navigationState.canGoPrevious, goToStep, state.currentStepIndex]);
}, [navigationState.canGoPrevious, goToStep, state.currentStepIndex])
const handleValueChange = useCallback(
(newValue: number) => {
if (isProgrammaticChange.current) {
isProgrammaticChange.current = false;
return;
isProgrammaticChange.current = false
return
}
const oldValue = state.currentValue;
const oldValue = state.currentValue
dispatch({
type: "USER_VALUE_CHANGE",
type: 'USER_VALUE_CHANGE',
oldValue,
newValue,
stepId: currentStep.id,
});
})
// Check if step is completed
if (newValue === currentStep.targetValue) {
dispatch({
type: "COMPLETE_STEP",
type: 'COMPLETE_STEP',
stepId: currentStep.id,
});
onStepComplete?.(state.currentStepIndex, currentStep, true);
})
onStepComplete?.(state.currentStepIndex, currentStep, true)
}
},
[state.currentValue, currentStep, onStepComplete, state.currentStepIndex],
);
[state.currentValue, currentStep, onStepComplete, state.currentStepIndex]
)
const handleBeadClick = useCallback(
(beadInfo: any) => {
dispatch({
type: "ADD_EVENT",
type: 'ADD_EVENT',
event: {
type: "BEAD_CLICKED",
type: 'BEAD_CLICKED',
stepId: currentStep.id,
timestamp: new Date(),
beadInfo,
},
});
})
},
[currentStep.id],
);
[currentStep.id]
)
const handleBeadRef = useCallback((bead: any, element: SVGElement | null) => {
const key = `${bead.placeValue}-${bead.type}-${bead.position}`;
const key = `${bead.placeValue}-${bead.type}-${bead.position}`
if (element) {
beadRefs.current.set(key, element);
beadRefs.current.set(key, element)
} else {
beadRefs.current.delete(key);
beadRefs.current.delete(key)
}
}, []);
}, [])
const toggleDebugPanel = useCallback(() => {
dispatch({
type: "UPDATE_UI_STATE",
type: 'UPDATE_UI_STATE',
updates: { showDebugPanel: !state.uiState.showDebugPanel },
});
}, [state.uiState.showDebugPanel]);
})
}, [state.uiState.showDebugPanel])
const toggleStepList = useCallback(() => {
dispatch({
type: "UPDATE_UI_STATE",
type: 'UPDATE_UI_STATE',
updates: { showStepList: !state.uiState.showStepList },
});
}, [state.uiState.showStepList]);
})
}, [state.uiState.showStepList])
const toggleAutoAdvance = useCallback(() => {
dispatch({
type: "UPDATE_UI_STATE",
type: 'UPDATE_UI_STATE',
updates: { autoAdvance: !state.uiState.autoAdvance },
});
}, [state.uiState.autoAdvance]);
})
}, [state.uiState.autoAdvance])
const advanceMultiStep = useCallback(() => {
dispatch({ type: "ADVANCE_MULTI_STEP" });
}, []);
dispatch({ type: 'ADVANCE_MULTI_STEP' })
}, [])
const previousMultiStep = useCallback(() => {
dispatch({ type: "PREVIOUS_MULTI_STEP" });
}, []);
dispatch({ type: 'PREVIOUS_MULTI_STEP' })
}, [])
const resetMultiStep = useCallback(() => {
dispatch({ type: "RESET_MULTI_STEP" });
}, []);
dispatch({ type: 'RESET_MULTI_STEP' })
}, [])
const getCurrentStepBeads = useCallback(() => {
if (expectedSteps.length === 0) return currentStep.stepBeadHighlights || [];
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 currentStep.stepBeadHighlights || []
}
return [];
}, [expectedSteps, currentStep.stepBeadHighlights, state.currentMultiStep]);
return []
}, [expectedSteps, currentStep.stepBeadHighlights, state.currentMultiStep])
const getCurrentStepSummary = useCallback(() => {
if (expectedSteps.length === 0) return null;
if (expectedSteps.length === 0) return null
if (state.currentMultiStep < expectedSteps.length) {
const currentExpectedStep = expectedSteps[state.currentMultiStep];
const currentExpectedStep = expectedSteps[state.currentMultiStep]
return {
description: currentExpectedStep.description,
mathematicalTerm: currentExpectedStep.mathematicalTerm,
termPosition: currentExpectedStep.termPosition,
};
}
}
return null;
}, [expectedSteps, state.currentMultiStep]);
return null
}, [expectedSteps, state.currentMultiStep])
const renderHighlightedDecomposition = useCallback(() => {
if (!fullDecomposition || expectedSteps.length === 0) return null;
if (!fullDecomposition || expectedSteps.length === 0) return null
const currentExpectedStep = expectedSteps[state.currentMultiStep];
if (!currentExpectedStep?.termPosition) return fullDecomposition;
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);
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]);
return { before, highlighted, after }
}, [fullDecomposition, expectedSteps, state.currentMultiStep])
const customStyles = useMemo(() => {
if (
!currentStep.highlightBeads ||
!Array.isArray(currentStep.highlightBeads)
) {
return undefined;
if (!currentStep.highlightBeads || !Array.isArray(currentStep.highlightBeads)) {
return undefined
}
return {
beads: currentStep.highlightBeads.reduce((acc, highlight) => {
const columnIndex = 4 - highlight.placeValue;
const columnIndex = 4 - highlight.placeValue
if (!acc[columnIndex]) {
acc[columnIndex] = {};
acc[columnIndex] = {}
}
if (highlight.beadType === "heaven") {
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 = {};
backgroundColor: '#ffeb3b',
border: '2px solid #ff9800',
}
const position = highlight.position || 0;
} 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",
};
backgroundColor: '#ffeb3b',
border: '2px solid #ff9800',
}
}
return acc;
return acc
}, {} as any),
};
}, [currentStep.highlightBeads]);
}
}, [currentStep.highlightBeads])
const value: TutorialContextType = {
state,
@@ -679,6 +645,8 @@ export function TutorialProvider({
setActiveTermIndices,
activeIndividualTermIndex,
setActiveIndividualTermIndex,
activeGroupTargetColumn,
setActiveGroupTargetColumn,
getColumnFromTermIndex,
getTermIndicesFromColumn,
getGroupTermIndicesFromTermIndex,
@@ -701,36 +669,29 @@ export function TutorialProvider({
getCurrentStepBeads,
getCurrentStepSummary,
renderHighlightedDecomposition,
};
}
return (
<TutorialContext.Provider value={value}>
{children}
</TutorialContext.Provider>
);
return <TutorialContext.Provider value={value}>{children}</TutorialContext.Provider>
}
// Hook to use the context
export function useTutorialContext() {
const context = useContext(TutorialContext);
const context = useContext(TutorialContext)
if (!context) {
throw new Error(
"useTutorialContext must be used within a TutorialProvider",
);
throw new Error('useTutorialContext must be used within a TutorialProvider')
}
return context;
return context
}
// Custom hooks for convenient access to specific context parts
export function useTutorialState() {
const { state } = useTutorialContext();
return state;
const { state } = useTutorialContext()
return state
}
export function useTutorialData() {
const { tutorial, currentStep, expectedSteps, fullDecomposition } =
useTutorialContext();
return { tutorial, currentStep, expectedSteps, fullDecomposition };
const { tutorial, currentStep, expectedSteps, fullDecomposition } = useTutorialContext()
return { tutorial, currentStep, expectedSteps, fullDecomposition }
}
export function useTutorialActions() {
@@ -745,7 +706,7 @@ export function useTutorialActions() {
toggleStepList,
toggleAutoAdvance,
notifyEvent,
} = useTutorialContext();
} = useTutorialContext()
return {
goToStep,
@@ -758,7 +719,7 @@ export function useTutorialActions() {
toggleStepList,
toggleAutoAdvance,
notifyEvent,
};
}
}
export function useTutorialHelpers() {
@@ -767,30 +728,26 @@ export function useTutorialHelpers() {
getCurrentStepSummary,
renderHighlightedDecomposition,
customStyles,
} = useTutorialContext();
} = useTutorialContext()
return {
getCurrentStepBeads,
getCurrentStepSummary,
renderHighlightedDecomposition,
customStyles,
};
}
}
export function useTutorialRefs() {
const {
isProgrammaticChange,
beadRefs,
showHelpForCurrentStep,
setShowHelpForCurrentStep,
} = useTutorialContext();
const { isProgrammaticChange, beadRefs, showHelpForCurrentStep, setShowHelpForCurrentStep } =
useTutorialContext()
return {
isProgrammaticChange,
beadRefs,
showHelpForCurrentStep,
setShowHelpForCurrentStep,
};
}
}
// Export types for use in other components
export type { TutorialPlayerState, TutorialPlayerAction, ExpectedStep };
export type { TutorialPlayerState, TutorialPlayerAction, ExpectedStep }