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:
parent
8518d90e85
commit
ba1a92230f
|
|
@ -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.
|
||||||
|
|
@ -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.
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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
|
||||||
|
`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
Loading…
Reference in New Issue