refactor(web): import utility functions from abacus-react
Re-export core utility functions from @soroban/abacus-react instead of duplicating implementations: - beadDiff.ts: Re-exports calculateBeadDiff, calculateBeadDiffFromValues, areStatesEqual, etc. Keeps app-specific calculateMultiStepBeadDiffs and validateBeadDiff. - abacusInstructionGenerator.ts: Re-exports numberToAbacusState, calculateBeadChanges, and related types. Keeps app-specific tutorial generation logic. - TutorialPlayer.tsx: Import calculateBeadDiffFromValues from abacus-react - TutorialEditor.tsx: Import calculateBeadDiffFromValues from abacus-react Eliminates ~200 lines of duplicate utility function implementations. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import Resizable from 'react-resizable-layout'
|
||||
import { calculateBeadDiffFromValues } from '@soroban/abacus-react'
|
||||
import { css } from '../../../styled-system/css'
|
||||
import { hstack, stack, vstack } from '../../../styled-system/patterns'
|
||||
import {
|
||||
@@ -13,7 +14,6 @@ import {
|
||||
type TutorialValidation,
|
||||
} from '../../types/tutorial'
|
||||
import { generateAbacusInstructions } from '../../utils/abacusInstructionGenerator'
|
||||
import { calculateBeadDiffFromValues } from '../../utils/beadDiff'
|
||||
import { generateSingleProblem } from '../../utils/problemGenerator'
|
||||
import {
|
||||
createBasicAllowedConfiguration,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
AbacusReact,
|
||||
type StepBeadHighlight,
|
||||
useAbacusDisplay,
|
||||
calculateBeadDiffFromValues,
|
||||
} from '@soroban/abacus-react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
@@ -18,7 +19,6 @@ import type {
|
||||
TutorialStep,
|
||||
UIState,
|
||||
} from '../../types/tutorial'
|
||||
import { calculateBeadDiffFromValues } from '../../utils/beadDiff'
|
||||
import { generateUnifiedInstructionSequence } from '../../utils/unifiedStepGenerator'
|
||||
import { CoachBar } from './CoachBar/CoachBar'
|
||||
import { DecompositionWithReasons } from './DecompositionWithReasons'
|
||||
|
||||
@@ -1,29 +1,30 @@
|
||||
// Automatic instruction generator for abacus tutorial steps
|
||||
import type { ValidPlaceValues } from '@soroban/abacus-react'
|
||||
// Re-exports core types and functions from abacus-react
|
||||
|
||||
export interface BeadState {
|
||||
heavenActive: boolean
|
||||
earthActive: number // 0-4
|
||||
}
|
||||
export type { ValidPlaceValues } from '@soroban/abacus-react'
|
||||
export {
|
||||
type BeadState,
|
||||
type AbacusState,
|
||||
type PlaceValueBasedBead as BeadHighlight,
|
||||
numberToAbacusState,
|
||||
calculateBeadChanges,
|
||||
} from '@soroban/abacus-react'
|
||||
|
||||
export interface AbacusState {
|
||||
[placeValue: number]: BeadState
|
||||
}
|
||||
import type { ValidPlaceValues, PlaceValueBasedBead } from '@soroban/abacus-react'
|
||||
import { numberToAbacusState, calculateBeadChanges } from '@soroban/abacus-react'
|
||||
|
||||
export interface BeadHighlight {
|
||||
placeValue: ValidPlaceValues
|
||||
beadType: 'heaven' | 'earth'
|
||||
position?: number
|
||||
}
|
||||
// Type alias for internal use
|
||||
type BeadHighlight = PlaceValueBasedBead
|
||||
|
||||
export interface StepBeadHighlight extends BeadHighlight {
|
||||
// App-specific extension for step-based tutorial highlighting
|
||||
export interface StepBeadHighlight extends PlaceValueBasedBead {
|
||||
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)
|
||||
}
|
||||
|
||||
export interface GeneratedInstruction {
|
||||
highlightBeads: BeadHighlight[]
|
||||
highlightBeads: PlaceValueBasedBead[]
|
||||
expectedAction: 'add' | 'remove' | 'multi-step'
|
||||
actionDescription: string
|
||||
multiStepInstructions?: string[]
|
||||
@@ -40,68 +41,7 @@ export interface GeneratedInstruction {
|
||||
}
|
||||
}
|
||||
|
||||
// Convert a number to abacus state representation
|
||||
export function numberToAbacusState(value: number, maxPlaces: number = 5): AbacusState {
|
||||
const state: AbacusState = {}
|
||||
|
||||
for (let place = 0; place < maxPlaces; place++) {
|
||||
const placeValueNum = 10 ** place
|
||||
const digit = Math.floor(value / placeValueNum) % 10
|
||||
|
||||
state[place] = {
|
||||
heavenActive: digit >= 5,
|
||||
earthActive: digit >= 5 ? digit - 5 : digit,
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
// Calculate the difference between two abacus states
|
||||
export function calculateBeadChanges(
|
||||
startState: AbacusState,
|
||||
targetState: AbacusState
|
||||
): {
|
||||
additions: BeadHighlight[]
|
||||
removals: BeadHighlight[]
|
||||
placeValue: number
|
||||
} {
|
||||
const additions: BeadHighlight[] = []
|
||||
const removals: BeadHighlight[] = []
|
||||
let mainPlaceValue = 0
|
||||
|
||||
for (const placeStr in targetState) {
|
||||
const place = parseInt(placeStr, 10) as ValidPlaceValues
|
||||
const start = startState[place] || { heavenActive: false, earthActive: 0 }
|
||||
const target = targetState[place]
|
||||
|
||||
// Check heaven bead changes
|
||||
if (!start.heavenActive && target.heavenActive) {
|
||||
additions.push({ placeValue: place, beadType: 'heaven' })
|
||||
mainPlaceValue = place
|
||||
} else if (start.heavenActive && !target.heavenActive) {
|
||||
removals.push({ placeValue: place, beadType: 'heaven' })
|
||||
mainPlaceValue = place
|
||||
}
|
||||
|
||||
// Check earth bead changes
|
||||
if (target.earthActive > start.earthActive) {
|
||||
// Adding earth beads
|
||||
for (let pos = start.earthActive; pos < target.earthActive; pos++) {
|
||||
additions.push({ placeValue: place, beadType: 'earth', position: pos })
|
||||
mainPlaceValue = place
|
||||
}
|
||||
} else if (target.earthActive < start.earthActive) {
|
||||
// Removing earth beads
|
||||
for (let pos = start.earthActive - 1; pos >= target.earthActive; pos--) {
|
||||
removals.push({ placeValue: place, beadType: 'earth', position: pos })
|
||||
mainPlaceValue = place
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { additions, removals, placeValue: mainPlaceValue }
|
||||
}
|
||||
// Note: numberToAbacusState and calculateBeadChanges are now re-exported from @soroban/abacus-react above
|
||||
|
||||
// Generate proper complement breakdown using simple bead movements
|
||||
function generateProperComplementDescription(
|
||||
|
||||
@@ -1,107 +1,25 @@
|
||||
// Dynamic bead diff algorithm for calculating transitions between abacus states
|
||||
// Provides arrows, highlights, and movement directions for tutorial UI
|
||||
// Re-export core bead diff functionality from abacus-react
|
||||
// App-specific extensions for multi-step tutorials and validation
|
||||
|
||||
import type { ValidPlaceValues } from '@soroban/abacus-react'
|
||||
import {
|
||||
export {
|
||||
type BeadDiffResult,
|
||||
type BeadDiffOutput,
|
||||
calculateBeadDiff,
|
||||
calculateBeadDiffFromValues,
|
||||
areStatesEqual,
|
||||
type AbacusState,
|
||||
type BeadHighlight,
|
||||
calculateBeadChanges,
|
||||
numberToAbacusState,
|
||||
} from './abacusInstructionGenerator'
|
||||
type BeadState,
|
||||
} from '@soroban/abacus-react'
|
||||
|
||||
export interface BeadDiffResult {
|
||||
placeValue: ValidPlaceValues
|
||||
beadType: 'heaven' | 'earth'
|
||||
position?: number
|
||||
direction: 'activate' | 'deactivate'
|
||||
order: number // Order of operations for animations
|
||||
}
|
||||
|
||||
export interface BeadDiffOutput {
|
||||
changes: BeadDiffResult[]
|
||||
highlights: BeadHighlight[]
|
||||
hasChanges: boolean
|
||||
summary: string
|
||||
}
|
||||
|
||||
/**
|
||||
* THE BEAD DIFF ALGORITHM
|
||||
*
|
||||
* Takes current and desired abacus states and returns exactly which beads
|
||||
* need to move with arrows and highlights for the tutorial UI.
|
||||
*
|
||||
* This is the core "diff" function that keeps tutorial highlights in sync.
|
||||
*/
|
||||
export function calculateBeadDiff(fromState: AbacusState, toState: AbacusState): BeadDiffOutput {
|
||||
const { additions, removals } = calculateBeadChanges(fromState, toState)
|
||||
|
||||
const changes: BeadDiffResult[] = []
|
||||
const highlights: BeadHighlight[] = []
|
||||
let order = 0
|
||||
|
||||
// Process removals first (pedagogical order: clear before adding)
|
||||
removals.forEach((removal) => {
|
||||
changes.push({
|
||||
placeValue: removal.placeValue,
|
||||
beadType: removal.beadType,
|
||||
position: removal.position,
|
||||
direction: 'deactivate',
|
||||
order: order++,
|
||||
})
|
||||
|
||||
highlights.push({
|
||||
placeValue: removal.placeValue,
|
||||
beadType: removal.beadType,
|
||||
position: removal.position,
|
||||
})
|
||||
})
|
||||
|
||||
// Process additions second (pedagogical order: add after clearing)
|
||||
additions.forEach((addition) => {
|
||||
changes.push({
|
||||
placeValue: addition.placeValue,
|
||||
beadType: addition.beadType,
|
||||
position: addition.position,
|
||||
direction: 'activate',
|
||||
order: order++,
|
||||
})
|
||||
|
||||
highlights.push({
|
||||
placeValue: addition.placeValue,
|
||||
beadType: addition.beadType,
|
||||
position: addition.position,
|
||||
})
|
||||
})
|
||||
|
||||
// Generate summary
|
||||
const summary = generateDiffSummary(changes)
|
||||
|
||||
return {
|
||||
changes,
|
||||
highlights,
|
||||
hasChanges: changes.length > 0,
|
||||
summary,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate bead diff from numeric values
|
||||
* Convenience function for when you have numbers instead of states
|
||||
*/
|
||||
export function calculateBeadDiffFromValues(
|
||||
fromValue: number,
|
||||
toValue: number,
|
||||
maxPlaces: number = 5
|
||||
): BeadDiffOutput {
|
||||
const fromState = numberToAbacusState(fromValue, maxPlaces)
|
||||
const toState = numberToAbacusState(toValue, maxPlaces)
|
||||
return calculateBeadDiff(fromState, toState)
|
||||
}
|
||||
import type { BeadDiffOutput, BeadDiffResult, AbacusState } from '@soroban/abacus-react'
|
||||
import { calculateBeadDiffFromValues } from '@soroban/abacus-react'
|
||||
|
||||
/**
|
||||
* Calculate step-by-step bead diffs for multi-step operations
|
||||
* This is used for tutorial multi-step instructions where we want to show
|
||||
* the progression through intermediate states
|
||||
*
|
||||
* APP-SPECIFIC FUNCTION - not in core abacus-react
|
||||
*/
|
||||
export function calculateMultiStepBeadDiffs(
|
||||
startValue: number,
|
||||
@@ -133,126 +51,10 @@ export function calculateMultiStepBeadDiffs(
|
||||
return stepDiffs
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a human-readable summary of what the diff does
|
||||
* Respects pedagogical order: removals first, then additions
|
||||
*/
|
||||
function generateDiffSummary(changes: BeadDiffResult[]): string {
|
||||
if (changes.length === 0) {
|
||||
return 'No changes needed'
|
||||
}
|
||||
|
||||
// Sort by order to respect pedagogical sequence
|
||||
const sortedChanges = [...changes].sort((a, b) => a.order - b.order)
|
||||
|
||||
const deactivations = sortedChanges.filter((c) => c.direction === 'deactivate')
|
||||
const activations = sortedChanges.filter((c) => c.direction === 'activate')
|
||||
|
||||
const parts: string[] = []
|
||||
|
||||
// Process deactivations first (pedagogical order)
|
||||
if (deactivations.length > 0) {
|
||||
const deactivationsByPlace = groupByPlace(deactivations)
|
||||
Object.entries(deactivationsByPlace).forEach(([place, beads]) => {
|
||||
const placeName = getPlaceName(parseInt(place, 10))
|
||||
const heavenBeads = beads.filter((b) => b.beadType === 'heaven')
|
||||
const earthBeads = beads.filter((b) => b.beadType === 'earth')
|
||||
|
||||
if (heavenBeads.length > 0) {
|
||||
parts.push(`remove heaven bead in ${placeName}`)
|
||||
}
|
||||
if (earthBeads.length > 0) {
|
||||
const count = earthBeads.length
|
||||
parts.push(`remove ${count} earth bead${count > 1 ? 's' : ''} in ${placeName}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Process activations second (pedagogical order)
|
||||
if (activations.length > 0) {
|
||||
const activationsByPlace = groupByPlace(activations)
|
||||
Object.entries(activationsByPlace).forEach(([place, beads]) => {
|
||||
const placeName = getPlaceName(parseInt(place, 10))
|
||||
const heavenBeads = beads.filter((b) => b.beadType === 'heaven')
|
||||
const earthBeads = beads.filter((b) => b.beadType === 'earth')
|
||||
|
||||
if (heavenBeads.length > 0) {
|
||||
parts.push(`add heaven bead in ${placeName}`)
|
||||
}
|
||||
if (earthBeads.length > 0) {
|
||||
const count = earthBeads.length
|
||||
parts.push(`add ${count} earth bead${count > 1 ? 's' : ''} in ${placeName}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return parts.join(', then ')
|
||||
}
|
||||
|
||||
/**
|
||||
* Group bead changes by place value
|
||||
*/
|
||||
function groupByPlace(changes: BeadDiffResult[]): {
|
||||
[place: string]: BeadDiffResult[]
|
||||
} {
|
||||
return changes.reduce(
|
||||
(groups, change) => {
|
||||
const place = change.placeValue.toString()
|
||||
if (!groups[place]) {
|
||||
groups[place] = []
|
||||
}
|
||||
groups[place].push(change)
|
||||
return groups
|
||||
},
|
||||
{} as { [place: string]: BeadDiffResult[] }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable place name
|
||||
*/
|
||||
function getPlaceName(place: number): string {
|
||||
switch (place) {
|
||||
case 0:
|
||||
return 'ones column'
|
||||
case 1:
|
||||
return 'tens column'
|
||||
case 2:
|
||||
return 'hundreds column'
|
||||
case 3:
|
||||
return 'thousands column'
|
||||
default:
|
||||
return `place ${place} column`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if two abacus states are equal
|
||||
*/
|
||||
export function areStatesEqual(state1: AbacusState, state2: AbacusState): boolean {
|
||||
const places1 = Object.keys(state1)
|
||||
.map((k) => parseInt(k, 10))
|
||||
.sort()
|
||||
const places2 = Object.keys(state2)
|
||||
.map((k) => parseInt(k, 10))
|
||||
.sort()
|
||||
|
||||
if (places1.length !== places2.length) return false
|
||||
|
||||
for (const place of places1) {
|
||||
const bead1 = state1[place]
|
||||
const bead2 = state2[place]
|
||||
|
||||
if (!bead2) return false
|
||||
if (bead1.heavenActive !== bead2.heavenActive) return false
|
||||
if (bead1.earthActive !== bead2.earthActive) return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a bead diff is feasible (no impossible bead states)
|
||||
*
|
||||
* APP-SPECIFIC FUNCTION - not in core abacus-react
|
||||
*/
|
||||
export function validateBeadDiff(diff: BeadDiffOutput): {
|
||||
isValid: boolean
|
||||
@@ -282,3 +84,20 @@ export function validateBeadDiff(diff: BeadDiffOutput): {
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function for validation
|
||||
function groupByPlace(changes: BeadDiffResult[]): {
|
||||
[place: string]: BeadDiffResult[]
|
||||
} {
|
||||
return changes.reduce(
|
||||
(groups, change) => {
|
||||
const place = change.placeValue.toString()
|
||||
if (!groups[place]) {
|
||||
groups[place] = []
|
||||
}
|
||||
groups[place].push(change)
|
||||
return groups
|
||||
},
|
||||
{} as { [place: string]: BeadDiffResult[] }
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user