docs: add comprehensive documentation and debug tools for progressive instruction system

PROGRESSIVE_INSTRUCTION_PLAN.md:
- Complete implementation plan with 4 phases and success criteria
- Technical architecture documentation
- Data structure specifications and implementation approach

Storybook Debug Stories:
- AbacusReact.debug-arrows.stories.tsx: Simple arrow positioning debugging
- AbacusReact.direction-arrows.stories.tsx: Comprehensive direction indicator showcase
- Stories for testing single beads, multiple arrows, and raw SVG validation

Step Advancement Test:
- step-advancement.test.tsx: Isolated unit tests for step progression logic
- Test component implementing full step advancement workflow
- Validates expected step generation and auto-advancement behavior

These tools enabled debugging and validation of the complete progressive instruction system.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-09-22 14:56:34 -05:00
parent 8518d90e85
commit ba1a92230f
4 changed files with 1262 additions and 0 deletions

View File

@ -0,0 +1,244 @@
# Progressive Multi-Step Instruction System Plan
## Overview
Implement a comprehensive system that coordinates multi-step instructions, bead highlighting, and directional movement indicators to create a step-by-step progressive tutorial experience.
## Current System Analysis
### 1. Bead Highlighting System
- **Location**: `packages/abacus-react/src/AbacusReact.tsx`
- **Interface**: `BeadHighlight` (union of PlaceValueBead | ColumnIndexBead)
- **Current functionality**: Static highlighting of beads
- **Limitation**: Shows all highlighted beads at once, no progressive revelation
### 2. Multi-Step Instructions
- **Location**: `apps/web/src/utils/abacusInstructionGenerator.ts`
- **Data**: `multiStepInstructions: string[]`
- **Current functionality**: Generates step-by-step text instructions
- **Example (99+1)**:
1. "Click earth bead 1 in the hundreds column to add it"
2. "Remove 9 from ones column (subtracting second part of decomposition)"
3. "Remove 90 from tens column (subtracting first part of decomposition)"
### 3. Progressive Display (GuidedAdditionTutorial)
- **Location**: `apps/web/src/components/GuidedAdditionTutorial.tsx`
- **State**: `multiStepProgress: number` (tracks current step)
- **Functionality**: Shows current step in bold, dims future steps
- **Limitation**: No coordination with bead highlighting
### 4. Bead Generation Logic
- **Location**: `apps/web/src/utils/abacusInstructionGenerator.ts:generateBeadHighlights()`
- **Current**: Generates all beads for entire operation
- **Limitation**: No step-by-step bead breakdown
## Proposed Solution Architecture
### Phase 1: Enhanced Data Structures
#### 1.1 Extended BeadHighlight Interface
```typescript
export interface StepBeadHighlight extends BeadHighlight {
stepIndex: number // Which instruction step this bead belongs to
direction: 'up' | 'down' | 'activate' | 'deactivate' // Movement direction
order?: number // Order within the step (for multiple beads per step)
}
```
#### 1.2 Enhanced GeneratedInstruction Interface
```typescript
export interface GeneratedInstruction {
// ... existing fields
multiStepInstructions?: string[]
stepBeadHighlights?: StepBeadHighlight[] // NEW: beads grouped by step
totalSteps?: number // NEW: total number of steps
}
```
#### 1.3 Progressive Instruction State
```typescript
interface ProgressiveInstructionState {
currentStep: number
totalSteps: number
currentStepBeads: BeadHighlight[] // Beads for current step only
completedStepBeads: BeadHighlight[] // All beads from previous steps
currentStepInstruction: string
}
```
### Phase 2: Enhanced Instruction Generation
#### 2.1 Modify generateEnhancedStepInstructions()
- Generate beads per step, not just text instructions
- Map each bead operation to its corresponding instruction step
- Determine movement direction for each bead
#### 2.2 New Function: generateStepBeadMapping()
```typescript
function generateStepBeadMapping(
startValue: number,
targetValue: number,
additions: BeadHighlight[],
removals: BeadHighlight[],
decomposition: any,
multiStepInstructions: string[]
): StepBeadHighlight[]
```
### Phase 3: AbacusReact Component Enhancements
#### 3.1 Add Directional Indicators
- **New Props**:
```typescript
interface AbacusReactProps {
// ... existing props
stepBeadHighlights?: StepBeadHighlight[]
showDirectionIndicators?: boolean
}
```
- **Visual Implementation**:
- Arrow overlays on beads (↑ for up/activate, ↓ for down/deactivate)
- Different highlight colors for different directions
- Animation hints for movement direction
#### 3.2 Enhanced Bead Styling
```typescript
interface BeadDirectionStyle extends BeadStyle {
directionIndicator?: {
show: boolean
direction: 'up' | 'down' | 'activate' | 'deactivate'
color?: string
size?: number
}
}
```
### Phase 4: TutorialPlayer Progressive Display
#### 4.1 Add Progressive State Management
```typescript
interface TutorialPlayerState {
// ... existing state
progressiveInstruction: ProgressiveInstructionState | null
}
```
#### 4.2 Progressive Instruction Logic
- Track current step within multi-step instructions
- Update highlighted beads based on current step
- Show only current step instruction in bold
- Dim future steps, mark completed steps
#### 4.3 Step Advancement Logic
- User completes current step → advance to next step
- Update bead highlights to show next step's beads
- Maintain previously completed beads in "completed" state
### Phase 5: Testing Strategy
#### 5.1 Unit Tests for Step-by-Step Generation
```typescript
describe('Progressive Instruction Generation', () => {
test('99+1 generates correct step-bead mapping', () => {
const instruction = generateAbacusInstructions(99, 100)
expect(instruction.stepBeadHighlights).toHaveLength(3)
// Step 0: Add 1 to hundreds
expect(instruction.stepBeadHighlights[0]).toEqual({
stepIndex: 0,
placeValue: 2,
beadType: 'earth',
position: 0,
direction: 'activate'
})
// Step 1: Remove 9 from ones (heaven + 4 earth)
const step1Beads = instruction.stepBeadHighlights.filter(b => b.stepIndex === 1)
expect(step1Beads).toHaveLength(5) // 1 heaven + 4 earth
// Step 2: Remove 90 from tens (heaven + 4 earth)
const step2Beads = instruction.stepBeadHighlights.filter(b => b.stepIndex === 2)
expect(step2Beads).toHaveLength(5) // 1 heaven + 4 earth
})
test('3+98 generates correct step-bead mapping', () => {
// Test simpler complement case
})
})
```
#### 5.2 Integration Tests for Progressive Display
```typescript
describe('Progressive Tutorial Player', () => {
test('shows only current step beads', () => {
// Render tutorial player with 99+1 case
// Verify only step 0 beads are highlighted initially
// Advance step, verify step 1 beads appear
})
test('direction indicators display correctly', () => {
// Verify arrows/indicators show correct directions
})
})
```
#### 5.3 Visual Regression Tests
- Storybook stories for each progression state
- Visual comparisons for direction indicators
- Progressive highlighting behavior
## Implementation Plan
### Iteration 1: Data Structure Foundation
1. **Design and implement enhanced interfaces**
2. **Modify instruction generator to produce step-bead mapping**
3. **Unit tests for step-bead generation**
4. **Validate with 99+1 and 3+98 test cases**
### Iteration 2: AbacusReact Direction Indicators
1. **Add direction indicator props to AbacusReact**
2. **Implement visual direction indicators (arrows/styling)**
3. **Create Storybook stories for direction indicators**
4. **Test various direction combinations**
### Iteration 3: Progressive TutorialPlayer
1. **Add progressive state management to TutorialPlayer**
2. **Implement step advancement logic**
3. **Coordinate instruction text with bead highlights**
4. **Test tutorial progression flow**
### Iteration 4: Integration and Polish
1. **Integrate all components in tutorial editor preview**
2. **End-to-end testing of complete progressive experience**
3. **Performance optimization**
4. **Accessibility improvements**
## Success Criteria
1. **Bead-Instruction Correspondence**: Each instruction step highlights only its relevant beads
2. **Direction Clarity**: Users can see which direction each bead needs to move
3. **Progressive Revelation**: Only current step is active, future steps are dimmed
4. **Pedagogical Alignment**: Visual progression matches mathematical decomposition
5. **99+1 Test Case**:
- Step 1: Show only hundreds bead with "up" indicator
- Step 2: Show only ones column beads with "down" indicators
- Step 3: Show only tens column beads with "down" indicators
## Technical Considerations
### Performance
- Minimize re-renders during step progression
- Efficient bead highlight calculations
- Smooth animations for step transitions
### Accessibility
- Screen reader announcements for step changes
- Keyboard navigation through steps
- High contrast direction indicators
### Backward Compatibility
- Maintain existing single-step tutorial functionality
- Graceful fallback for tutorials without multi-step data
- Preserve existing AbacusReact API surface
This plan provides a comprehensive roadmap for implementing the progressive multi-step instruction system with proper coordination between pedagogy, visual display, and user interaction.

View File

@ -0,0 +1,289 @@
import type { Meta, StoryObj } from '@storybook/react';
import { AbacusReact, StepBeadHighlight } from './AbacusReact';
import React from 'react';
const meta: Meta<typeof AbacusReact> = {
title: 'Debug/Arrow Positioning',
component: AbacusReact,
parameters: {
layout: 'centered',
docs: {
description: {
component: `
# Arrow Positioning Debug
Simple stories to debug arrow centering issues.
`,
},
},
},
tags: ['autodocs'],
};
export default meta;
type Story = StoryObj<typeof AbacusReact>;
// Single earth bead with up arrow - minimal test case
export const SingleEarthBeadUp: Story = {
args: {
value: 0,
columns: 1,
scaleFactor: 4, // Large scale to see details
interactive: false,
animated: false,
colorScheme: 'place-value',
stepBeadHighlights: [{
placeValue: 0,
beadType: 'earth',
position: 0,
stepIndex: 0,
direction: 'activate',
order: 0
}],
currentStep: 0,
showDirectionIndicators: true,
},
parameters: {
docs: {
description: {
story: `
**Single Earth Bead with Up Arrow**
- Value: 0 (no beads active)
- Shows green up arrow on first earth bead
- Large scale factor for debugging
- Should be centered on the bead
`
}
}
}
};
// Single heaven bead with down arrow
export const SingleHeavenBeadDown: Story = {
args: {
value: 0,
columns: 1,
scaleFactor: 4,
interactive: false,
animated: false,
colorScheme: 'place-value',
stepBeadHighlights: [{
placeValue: 0,
beadType: 'heaven',
stepIndex: 0,
direction: 'activate',
order: 0
}],
currentStep: 0,
showDirectionIndicators: true,
},
parameters: {
docs: {
description: {
story: `
**Single Heaven Bead with Down Arrow**
- Value: 0 (no beads active)
- Shows red down arrow on heaven bead
- Large scale factor for debugging
- Should be centered on the bead
`
}
}
}
};
// Active earth bead with down arrow (deactivate)
export const ActiveEarthBeadDown: Story = {
args: {
value: 1,
columns: 1,
scaleFactor: 4,
interactive: false,
animated: false,
colorScheme: 'place-value',
stepBeadHighlights: [{
placeValue: 0,
beadType: 'earth',
position: 0,
stepIndex: 0,
direction: 'deactivate',
order: 0
}],
currentStep: 0,
showDirectionIndicators: true,
},
parameters: {
docs: {
description: {
story: `
**Active Earth Bead with Down Arrow**
- Value: 1 (first earth bead active)
- Shows red down arrow for deactivation
- Large scale factor for debugging
- Should be centered on the active bead
`
}
}
}
};
// Active heaven bead with up arrow (deactivate)
export const ActiveHeavenBeadUp: Story = {
args: {
value: 5,
columns: 1,
scaleFactor: 4,
interactive: false,
animated: false,
colorScheme: 'place-value',
stepBeadHighlights: [{
placeValue: 0,
beadType: 'heaven',
stepIndex: 0,
direction: 'deactivate',
order: 0
}],
currentStep: 0,
showDirectionIndicators: true,
},
parameters: {
docs: {
description: {
story: `
**Active Heaven Bead with Up Arrow**
- Value: 5 (heaven bead active)
- Shows green up arrow for deactivation
- Large scale factor for debugging
- Should be centered on the active bead
`
}
}
}
};
// Multiple arrows for comparison
export const MultipleArrows: Story = {
args: {
value: 0,
columns: 1,
scaleFactor: 3,
interactive: false,
animated: false,
colorScheme: 'place-value',
stepBeadHighlights: [
{
placeValue: 0,
beadType: 'heaven',
stepIndex: 0,
direction: 'activate',
order: 0
},
{
placeValue: 0,
beadType: 'earth',
position: 0,
stepIndex: 0,
direction: 'activate',
order: 1
},
{
placeValue: 0,
beadType: 'earth',
position: 1,
stepIndex: 0,
direction: 'activate',
order: 2
}
],
currentStep: 0,
showDirectionIndicators: true,
},
parameters: {
docs: {
description: {
story: `
**Multiple Arrows for Comparison**
- Shows arrows on heaven bead and first two earth beads
- All should point in correct directions
- All should be centered on their respective beads
- Helps identify if positioning is consistent across bead types
`
}
}
}
};
// Raw SVG test - just arrows without beads
export const RawArrowTest: Story = {
render: () => {
return (
<div style={{ textAlign: 'center', padding: '20px' }}>
<h3>Raw SVG Arrow Test</h3>
<svg width="200" height="200" style={{ border: '1px solid #ccc' }}>
{/* Grid lines for reference */}
<line x1="0" y1="100" x2="200" y2="100" stroke="#eee" strokeWidth="1" />
<line x1="100" y1="0" x2="100" y2="200" stroke="#eee" strokeWidth="1" />
{/* Center point */}
<circle cx="100" cy="100" r="2" fill="black" />
{/* Up arrow at center */}
<g transform="translate(100, 100)">
<polygon
points="-10,5 10,5 0,-10"
fill="rgba(0, 150, 0, 0.8)"
stroke="rgba(0, 100, 0, 1)"
strokeWidth="1.5"
/>
</g>
{/* Text label */}
<text x="100" y="180" textAnchor="middle" fontSize="12">
Up Arrow (should be centered at intersection)
</text>
</svg>
<svg width="200" height="200" style={{ border: '1px solid #ccc', marginLeft: '20px' }}>
{/* Grid lines for reference */}
<line x1="0" y1="100" x2="200" y2="100" stroke="#eee" strokeWidth="1" />
<line x1="100" y1="0" x2="100" y2="200" stroke="#eee" strokeWidth="1" />
{/* Center point */}
<circle cx="100" cy="100" r="2" fill="black" />
{/* Down arrow at center */}
<g transform="translate(100, 100)">
<polygon
points="-10,-10 10,-10 0,10"
fill="rgba(200, 0, 0, 0.8)"
stroke="rgba(150, 0, 0, 1)"
strokeWidth="1.5"
/>
</g>
{/* Text label */}
<text x="100" y="180" textAnchor="middle" fontSize="12">
Down Arrow (should be centered at intersection)
</text>
</svg>
</div>
);
},
parameters: {
docs: {
description: {
story: `
**Raw SVG Arrow Test**
Pure SVG arrows to verify our arrow shapes and positioning work correctly.
The arrows should be perfectly centered at the grid intersection.
`
}
}
}
};

View File

@ -0,0 +1,435 @@
import type { Meta, StoryObj } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import { AbacusReact, StepBeadHighlight } from './AbacusReact';
import React, { useState } from 'react';
const meta: Meta<typeof AbacusReact> = {
title: 'Soroban/AbacusReact/Direction Arrows',
component: AbacusReact,
parameters: {
layout: 'centered',
docs: {
description: {
component: `
# Direction Arrows Feature
Progressive instruction system with direction indicators for tutorial guidance.
## Features
- 🎯 **Step-based highlighting** - Show beads for current instruction step only
- **Direction arrows** - Visual indicators showing which direction to move beads
- 🔄 **Progressive revelation** - Steps advance as user follows instructions
- 🎨 **Correct movement logic** - Heaven beads activate down, earth beads activate up
## Arrow Logic
- **Earth beads**: 'activate' (push up), 'deactivate' (release down)
- **Heaven beads**: 'activate' (pull down), 'deactivate' (release up)
`,
},
},
},
tags: ['autodocs'],
argTypes: {
currentStep: {
control: { type: 'number', min: 0, max: 5 },
description: 'Current step index to highlight',
},
showDirectionIndicators: {
control: { type: 'boolean' },
description: 'Show direction arrows on beads',
},
},
};
export default meta;
type Story = StoryObj<typeof AbacusReact>;
// Helper function to create step bead highlights for different scenarios
const createStepBeads = (scenario: 'earth-activate' | 'heaven-activate' | 'multi-step' | 'heaven-earth-combo'): StepBeadHighlight[] => {
switch (scenario) {
case 'earth-activate':
return [
{
placeValue: 0,
beadType: 'earth',
position: 0,
stepIndex: 0,
direction: 'activate',
order: 0
}
];
case 'heaven-activate':
return [
{
placeValue: 0,
beadType: 'heaven',
stepIndex: 0,
direction: 'activate',
order: 0
}
];
case 'multi-step':
return [
// Step 0: Add heaven bead (5)
{
placeValue: 0,
beadType: 'heaven',
stepIndex: 0,
direction: 'activate',
order: 0
},
// Step 1: Remove 2 earth beads (subtract 2)
{
placeValue: 0,
beadType: 'earth',
position: 0,
stepIndex: 1,
direction: 'deactivate',
order: 0
},
{
placeValue: 0,
beadType: 'earth',
position: 1,
stepIndex: 1,
direction: 'deactivate',
order: 1
}
];
case 'heaven-earth-combo':
return [
// Step 0: Multiple beads in same step
{
placeValue: 1,
beadType: 'earth',
position: 0,
stepIndex: 0,
direction: 'activate',
order: 0
},
{
placeValue: 0,
beadType: 'heaven',
stepIndex: 0,
direction: 'deactivate',
order: 1
},
{
placeValue: 0,
beadType: 'earth',
position: 0,
stepIndex: 0,
direction: 'deactivate',
order: 2
}
];
default:
return [];
}
};
export const EarthBeadActivate: Story = {
args: {
value: 0,
columns: 1,
scaleFactor: 3,
interactive: true,
animated: true,
colorScheme: 'place-value',
colorPalette: 'default',
stepBeadHighlights: createStepBeads('earth-activate'),
currentStep: 0,
showDirectionIndicators: true,
onValueChange: action('value-changed'),
},
parameters: {
docs: {
description: {
story: `
**Earth Bead Activation (0 + 1)**
- Shows green up arrow on first earth bead
- Earth beads activate by moving UP
- Click the highlighted bead to see the value change to 1
`
}
}
}
};
export const HeavenBeadActivate: Story = {
args: {
value: 0,
columns: 1,
scaleFactor: 3,
interactive: true,
animated: true,
colorScheme: 'place-value',
stepBeadHighlights: createStepBeads('heaven-activate'),
currentStep: 0,
showDirectionIndicators: true,
onValueChange: action('value-changed'),
},
parameters: {
docs: {
description: {
story: `
**Heaven Bead Activation (0 + 5)**
- Shows red down arrow on heaven bead
- Heaven beads activate by moving DOWN
- Click the highlighted bead to see the value change to 5
`
}
}
}
};
export const MultiStepSequence: Story = {
args: {
value: 2,
columns: 1,
scaleFactor: 3,
interactive: true,
animated: true,
colorScheme: 'place-value',
stepBeadHighlights: createStepBeads('multi-step'),
currentStep: 0,
showDirectionIndicators: true,
onValueChange: action('value-changed'),
},
parameters: {
docs: {
description: {
story: `
**Multi-Step Complement (2 + 3 = 5)**
Step progression using five complement: 3 = 5 - 2
**Step 0**: Red down arrow on heaven bead (add 5)
**Step 1**: Red down arrows on earth beads (remove 2)
Use the currentStep control to see different steps highlighted.
`
}
}
}
};
export const ComplexOperation: Story = {
args: {
value: 7,
columns: 2,
scaleFactor: 2.5,
interactive: true,
animated: true,
colorScheme: 'place-value',
stepBeadHighlights: createStepBeads('heaven-earth-combo'),
currentStep: 0,
showDirectionIndicators: true,
onValueChange: action('value-changed'),
},
parameters: {
docs: {
description: {
story: `
**Complex Multi-Column Operation**
Shows multiple beads across different columns and types:
- Green up arrow on tens earth bead (activate)
- Red up arrow on ones heaven bead (deactivate)
- Red down arrow on ones earth bead (deactivate)
All beads are part of step 0, showing simultaneous actions.
`
}
}
}
};
// Interactive story to demonstrate step progression
export const InteractiveStepProgression: Story = {
render: (args) => {
const [currentStep, setCurrentStep] = useState(0);
const [value, setValue] = useState(2);
const stepBeads = createStepBeads('multi-step');
const maxSteps = Math.max(...stepBeads.map(bead => bead.stepIndex)) + 1;
const handleValueChange = (newValue: number) => {
setValue(newValue);
action('value-changed')(newValue);
// Auto-advance step when value changes (simple demo logic)
if (currentStep < maxSteps - 1) {
setTimeout(() => setCurrentStep(prev => prev + 1), 500);
}
};
const resetDemo = () => {
setValue(2);
setCurrentStep(0);
};
return (
<div style={{ textAlign: 'center' }}>
<AbacusReact
{...args}
value={value}
currentStep={currentStep}
stepBeadHighlights={stepBeads}
onValueChange={handleValueChange}
/>
<div style={{ marginTop: '20px', fontSize: '14px', color: '#666' }}>
<p><strong>Step {currentStep + 1} of {maxSteps}</strong></p>
<p>Value: {value}</p>
<button
onClick={resetDemo}
style={{
padding: '8px 16px',
marginTop: '10px',
backgroundColor: '#4A90E2',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer'
}}
>
Reset Demo
</button>
</div>
</div>
);
},
args: {
columns: 1,
scaleFactor: 3,
interactive: true,
animated: true,
colorScheme: 'place-value',
showDirectionIndicators: true,
},
parameters: {
docs: {
description: {
story: `
**Interactive Step Progression Demo**
Demonstrates automatic step advancement:
1. Start with value 2
2. Click the heaven bead (red down arrow) to add 5 value becomes 7
3. Step automatically advances to show earth bead deactivation arrows
4. Click earth beads to remove 2 final value becomes 5
This simulates the tutorial experience where steps progress as users follow instructions.
`
}
}
}
};
// Static showcase of all arrow types
export const AllArrowTypes: Story = {
render: () => {
const earthActivate: StepBeadHighlight[] = [
{ placeValue: 0, beadType: 'earth', position: 0, stepIndex: 0, direction: 'activate' }
];
const earthDeactivate: StepBeadHighlight[] = [
{ placeValue: 0, beadType: 'earth', position: 0, stepIndex: 0, direction: 'deactivate' }
];
const heavenActivate: StepBeadHighlight[] = [
{ placeValue: 0, beadType: 'heaven', stepIndex: 0, direction: 'activate' }
];
const heavenDeactivate: StepBeadHighlight[] = [
{ placeValue: 0, beadType: 'heaven', stepIndex: 0, direction: 'deactivate' }
];
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '20px', textAlign: 'center' }}>
<div>
<h4>Earth Activate ()</h4>
<AbacusReact
value={0}
columns={1}
scaleFactor={2}
stepBeadHighlights={earthActivate}
currentStep={0}
showDirectionIndicators={true}
/>
<p style={{ fontSize: '12px', color: '#666', marginTop: '8px' }}>
Green up arrow - push earth bead up to activate
</p>
</div>
<div>
<h4>Earth Deactivate ()</h4>
<AbacusReact
value={1}
columns={1}
scaleFactor={2}
stepBeadHighlights={earthDeactivate}
currentStep={0}
showDirectionIndicators={true}
/>
<p style={{ fontSize: '12px', color: '#666', marginTop: '8px' }}>
Red down arrow - release earth bead down to deactivate
</p>
</div>
<div>
<h4>Heaven Activate ()</h4>
<AbacusReact
value={0}
columns={1}
scaleFactor={2}
stepBeadHighlights={heavenActivate}
currentStep={0}
showDirectionIndicators={true}
/>
<p style={{ fontSize: '12px', color: '#666', marginTop: '8px' }}>
Red down arrow - pull heaven bead down to activate
</p>
</div>
<div>
<h4>Heaven Deactivate ()</h4>
<AbacusReact
value={5}
columns={1}
scaleFactor={2}
stepBeadHighlights={heavenDeactivate}
currentStep={0}
showDirectionIndicators={true}
/>
<p style={{ fontSize: '12px', color: '#666', marginTop: '8px' }}>
Green up arrow - release heaven bead up to deactivate
</p>
</div>
</div>
);
},
parameters: {
docs: {
description: {
story: `
**Complete Arrow Reference**
Shows all four arrow types with correct colors and directions:
- **Green arrows**: Positive/activating actions
- **Red arrows**: Negative/deactivating actions
- **Direction**: Based on physical bead movement direction
`
}
}
}
};

View File

@ -0,0 +1,294 @@
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { render, fireEvent, screen } from '@testing-library/react';
import { vi } from 'vitest';
// Mock the instruction generator
const generateAbacusInstructions = (startValue: number, targetValue: number) => {
// Mock implementation for 3+14=17 case
if (startValue === 3 && targetValue === 8) {
return {
stepBeadHighlights: [{
placeValue: 0,
beadType: 'heaven' as const,
stepIndex: 0,
direction: 'activate' as const,
order: 0
}]
};
}
if (startValue === 8 && targetValue === 18) {
return {
stepBeadHighlights: [{
placeValue: 1,
beadType: 'earth' as const,
position: 0,
stepIndex: 0,
direction: 'activate' as const,
order: 0
}]
};
}
if (startValue === 18 && targetValue === 17) {
return {
stepBeadHighlights: [{
placeValue: 0,
beadType: 'earth' as const,
position: 0,
stepIndex: 0,
direction: 'deactivate' as const,
order: 0
}]
};
}
return { stepBeadHighlights: [] };
};
// Test component that implements the step advancement logic
const StepAdvancementTest: React.FC = () => {
const [currentValue, setCurrentValue] = useState(3);
const [currentMultiStep, setCurrentMultiStep] = useState(0);
const lastValueForStepAdvancement = useRef<number>(currentValue);
const userHasInteracted = useRef<boolean>(false);
// Mock current step data (3 + 14 = 17)
const currentStep = {
startValue: 3,
targetValue: 17,
stepBeadHighlights: [
{
placeValue: 0,
beadType: 'heaven' as const,
stepIndex: 0,
direction: 'activate' as const,
order: 0
},
{
placeValue: 1,
beadType: 'earth' as const,
position: 0,
stepIndex: 1,
direction: 'activate' as const,
order: 0
},
{
placeValue: 0,
beadType: 'earth' as const,
position: 0,
stepIndex: 2,
direction: 'deactivate' as const,
order: 0
}
],
totalSteps: 3
};
// Define the static expected steps
const expectedSteps = useMemo(() => {
if (!currentStep.stepBeadHighlights || !currentStep.totalSteps || currentStep.totalSteps <= 1) {
return [];
}
const stepIndices = [...new Set(currentStep.stepBeadHighlights.map(bead => bead.stepIndex))].sort();
const steps = [];
let value = currentStep.startValue;
if (currentStep.startValue === 3 && currentStep.targetValue === 17) {
const milestones = [8, 18, 17];
for (let i = 0; i < stepIndices.length && i < milestones.length; i++) {
steps.push({
index: i,
stepIndex: stepIndices[i],
targetValue: milestones[i],
startValue: value,
description: `Step ${i + 1}`
});
value = milestones[i];
}
}
console.log('📋 Generated expected steps:', steps);
return steps;
}, []);
// Get arrows for immediate next action
const getCurrentStepBeads = useCallback(() => {
if (currentValue === currentStep.targetValue) return undefined;
if (expectedSteps.length === 0) return currentStep.stepBeadHighlights;
const currentExpectedStep = expectedSteps[currentMultiStep];
if (!currentExpectedStep) return undefined;
try {
const instruction = generateAbacusInstructions(currentValue, currentExpectedStep.targetValue);
const immediateAction = instruction.stepBeadHighlights?.filter(bead => bead.stepIndex === 0);
console.log('🎯 Expected step progression:', {
currentValue,
expectedStepIndex: currentMultiStep,
expectedStepTarget: currentExpectedStep.targetValue,
expectedStepDescription: currentExpectedStep.description,
immediateActionBeads: immediateAction?.length || 0,
totalExpectedSteps: expectedSteps.length
});
return immediateAction && immediateAction.length > 0 ? immediateAction : undefined;
} catch (error) {
console.warn('⚠️ Failed to generate step guidance:', error);
return undefined;
}
}, [currentValue, expectedSteps, currentMultiStep]);
// Step advancement logic
useEffect(() => {
const valueChanged = currentValue !== lastValueForStepAdvancement.current;
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
});
if (valueChanged && userHasInteracted.current && expectedSteps.length > 0 && currentExpectedStep) {
if (currentValue === currentExpectedStep.targetValue) {
const hasMoreExpectedSteps = currentMultiStep < expectedSteps.length - 1;
console.log('🎯 Expected step completed:', {
completedStep: currentMultiStep,
targetReached: currentExpectedStep.targetValue,
hasMoreSteps: hasMoreExpectedSteps,
willAdvance: hasMoreExpectedSteps
});
if (hasMoreExpectedSteps) {
const timeoutId = setTimeout(() => {
console.log('⚡ Advancing to next expected step:', currentMultiStep, '→', currentMultiStep + 1);
setCurrentMultiStep(prev => prev + 1);
lastValueForStepAdvancement.current = currentValue;
}, 100); // Shorter delay for testing
return () => clearTimeout(timeoutId);
}
}
}
}, [currentValue, currentMultiStep, expectedSteps]);
// Update reference when step changes
useEffect(() => {
lastValueForStepAdvancement.current = currentValue;
userHasInteracted.current = false;
}, [currentMultiStep]);
const handleValueChange = (newValue: number) => {
userHasInteracted.current = true;
setCurrentValue(newValue);
};
const currentStepBeads = getCurrentStepBeads();
return (
<div data-testid="step-test">
<div data-testid="current-value">{currentValue}</div>
<div data-testid="expected-step-index">{currentMultiStep}</div>
<div data-testid="expected-steps-length">{expectedSteps.length}</div>
<div data-testid="current-expected-target">
{expectedSteps[currentMultiStep]?.targetValue || 'N/A'}
</div>
<div data-testid="has-step-beads">{currentStepBeads ? 'yes' : 'no'}</div>
<button
data-testid="set-value-8"
onClick={() => handleValueChange(8)}
>
Set Value to 8
</button>
<button
data-testid="set-value-18"
onClick={() => handleValueChange(18)}
>
Set Value to 18
</button>
<button
data-testid="set-value-17"
onClick={() => handleValueChange(17)}
>
Set Value to 17
</button>
<div data-testid="expected-steps">
{JSON.stringify(expectedSteps)}
</div>
</div>
);
};
// Test cases
describe('Step Advancement Logic', () => {
beforeEach(() => {
vi.clearAllMocks();
console.log = vi.fn();
});
test('should generate expected steps for 3+14=17', () => {
render(<StepAdvancementTest />);
expect(screen.getByTestId('expected-steps-length')).toHaveTextContent('3');
expect(screen.getByTestId('current-expected-target')).toHaveTextContent('8');
expect(screen.getByTestId('expected-step-index')).toHaveTextContent('0');
});
test('should advance from step 0 to step 1 when reaching value 8', async () => {
render(<StepAdvancementTest />);
// Initial state
expect(screen.getByTestId('current-value')).toHaveTextContent('3');
expect(screen.getByTestId('expected-step-index')).toHaveTextContent('0');
expect(screen.getByTestId('current-expected-target')).toHaveTextContent('8');
// Click to set value to 8
fireEvent.click(screen.getByTestId('set-value-8'));
// Should still be step 0 immediately
expect(screen.getByTestId('current-value')).toHaveTextContent('8');
expect(screen.getByTestId('expected-step-index')).toHaveTextContent('0');
// Wait for timeout to advance step
await new Promise(resolve => setTimeout(resolve, 150));
// Should now be step 1
expect(screen.getByTestId('expected-step-index')).toHaveTextContent('1');
expect(screen.getByTestId('current-expected-target')).toHaveTextContent('18');
});
test('should advance through all steps', async () => {
render(<StepAdvancementTest />);
// Step 0 → 1 (3 → 8)
fireEvent.click(screen.getByTestId('set-value-8'));
await new Promise(resolve => setTimeout(resolve, 150));
expect(screen.getByTestId('expected-step-index')).toHaveTextContent('1');
// Step 1 → 2 (8 → 18)
fireEvent.click(screen.getByTestId('set-value-18'));
await new Promise(resolve => setTimeout(resolve, 150));
expect(screen.getByTestId('expected-step-index')).toHaveTextContent('2');
// Step 2 → complete (18 → 17)
fireEvent.click(screen.getByTestId('set-value-17'));
await new Promise(resolve => setTimeout(resolve, 150));
// Should stay at step 2 since it's the last step
expect(screen.getByTestId('expected-step-index')).toHaveTextContent('2');
});
});
export default StepAdvancementTest;