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:
Thomas Hallock
2025-11-03 17:14:28 -06:00
parent ff1d60a233
commit 7228bbc2eb
4 changed files with 52 additions and 293 deletions

View File

@@ -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,

View File

@@ -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'

View File

@@ -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(

View File

@@ -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[] }
)
}