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