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:
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user