- Add dual-stream calibration: phone sends both raw and cropped preview frames during calibration so users can see what practice will look like - Add "Adjust" button to modify existing manual calibration without resetting to auto-detection first - Hide calibration quad editor overlay when not in calibration mode - Fix rotation buttons to update cropped preview immediately - Add rate limiting (10fps) for cropped preview frames during calibration - Fix multiple bugs preventing dual-stream mode from working: - Don't mark calibration as complete during preview mode - Don't stop detection loop when receiving preview calibration - Sync refs properly in frame mode change effects Also includes accumulated formatting and cleanup changes. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| DecompositionAudit.stories.tsx | ||
| DecompositionDisplay.tsx | ||
| README.md | ||
| ReasonTooltip.tsx | ||
| decomposition.css | ||
| index.ts | ||
| reason-tooltip.css | ||
README.md
Decomposition Display Components
Interactive mathematical decomposition visualization for soroban addition/subtraction operations.
Overview
The decomposition system breaks down soroban arithmetic into step-by-step operations, showing users exactly how to perform calculations using complement-based methods. It supports:
- Interactive term highlighting - Hover over terms to see pedagogical explanations
- Grouped operations - Related terms (e.g., "+10 -3" for adding 7) are grouped visually
- Current step tracking - Integrates with tutorial/practice step progression
- Abacus coordination - Bidirectional highlighting between decomposition and abacus display
Quick Start
import { DecompositionProvider, DecompositionDisplay } from '@/components/decomposition'
// Basic usage - just provide start and target values
function MyComponent() {
return (
<DecompositionProvider startValue={45} targetValue={72}>
<DecompositionDisplay />
</DecompositionProvider>
)
}
Components
DecompositionProvider
Context provider that generates all decomposition data from start/target values.
interface DecompositionContextConfig {
/** Starting value on the abacus */
startValue: number;
/** Target value to reach */
targetValue: number;
/** Current step index for highlighting (optional) */
currentStepIndex?: number;
/** Number of abacus columns for coordinate mapping (default: 5) */
abacusColumns?: number;
/** Callback when segment changes (optional) */
onSegmentChange?: (segment: PedagogicalSegment | null) => void;
/** Callback when term is hovered (optional) */
onTermHover?: (termIndex: number | null, columnIndex: number | null) => void;
}
Props:
| Prop | Type | Required | Description |
|---|---|---|---|
startValue |
number |
Yes | Initial abacus value |
targetValue |
number |
Yes | Target value to calculate |
currentStepIndex |
number |
No | Which step to highlight (default: 0) |
abacusColumns |
number |
No | Number of columns for mapping (default: 5) |
onSegmentChange |
function |
No | Called when user hovers a grouped segment |
onTermHover |
function |
No | Called when user hovers individual term |
DecompositionDisplay
Renders the interactive decomposition string with hoverable terms.
import { DecompositionDisplay } from '@/components/decomposition'
// Must be inside a DecompositionProvider
<DecompositionDisplay />
The display automatically:
- Renders all terms from the decomposition
- Groups related terms (pedagogical segments)
- Shows tooltips with explanations on hover
- Highlights the current step
- Coordinates with external abacus highlighting
ReasonTooltip
Tooltip component showing pedagogical reasoning for each term.
import { ReasonTooltip } from '@/components/decomposition'
// Usually used internally by DecompositionDisplay
<ReasonTooltip
reason={termReason}
variant="blue"
open={isOpen}
onOpenChange={setIsOpen}
>
<span>+10</span>
</ReasonTooltip>
Hooks
useDecomposition()
Access decomposition context data. Must be inside DecompositionProvider.
const {
// Data
fullDecomposition, // "45 +10 -3 +20" (full string)
termPositions, // Position metadata for each term
segments, // Grouped pedagogical segments
steps, // Unified instruction steps
currentStepIndex, // Current highlighted step
// Highlighting state
activeTermIndices, // Set of currently highlighted term indices
activeIndividualTermIndex, // Single hovered term index
// Actions
setActiveTermIndices, // Highlight multiple terms
setActiveIndividualTermIndex, // Highlight single term
getGroupTermIndicesFromTermIndex, // Get all terms in a group
getColumnIndexFromTermIndex, // Map term to abacus column
} = useDecomposition();
useDecompositionOptional()
Same as useDecomposition() but returns null outside provider (doesn't throw).
const decomposition = useDecompositionOptional();
if (decomposition) {
// Use decomposition data
}
Integration Examples
Tutorial Player
import { DecompositionProvider, DecompositionDisplay } from '@/components/decomposition'
function TutorialPlayer({ step, currentMultiStep }) {
return (
<DecompositionProvider
startValue={step.startValue}
targetValue={step.targetValue}
currentStepIndex={currentMultiStep}
abacusColumns={5}
>
<div className="tutorial-content">
<DecompositionDisplay />
<AbacusWithHighlighting />
</div>
</DecompositionProvider>
)
}
With Help Mode
import { DecompositionProvider, DecompositionDisplay } from '@/components/decomposition'
function HelpOverlay({ currentValue, targetValue, showHelp }) {
// Show decomposition when help is requested
if (!showHelp) return null
return (
<DecompositionProvider
startValue={currentValue}
targetValue={targetValue}
abacusColumns={3}
>
<div className="step-by-step-help">
<DecompositionDisplay />
</div>
</DecompositionProvider>
)
}
With Abacus Coordination
function CoordinatedDisplay({ startValue, targetValue }) {
const [highlightedColumn, setHighlightedColumn] = useState<number | null>(null)
return (
<DecompositionProvider
startValue={startValue}
targetValue={targetValue}
onTermHover={(termIndex, columnIndex) => {
setHighlightedColumn(columnIndex)
}}
>
<DecompositionDisplay />
<AbacusReact
value={startValue}
highlightedColumn={highlightedColumn}
/>
</DecompositionProvider>
)
}
Architecture
Data Flow
startValue, targetValue
│
▼
generateUnifiedInstructionSequence()
│
▼
┌────────────────────────────────────┐
│ DecompositionContext │
│ │
│ - fullDecomposition: "45 +10 -3" │
│ - termPositions: [{start, end}] │
│ - segments: [PedagogicalSegment] │
│ - steps: [UnifiedStep] │
│ - highlighting state │
└────────────────────────────────────┘
│
▼
DecompositionDisplay
│
▼
TermSpan / SegmentGroup
│
▼
ReasonTooltip (on hover)
Key Types
/** Position of a term in the decomposition string */
interface TermPosition {
index: number; // Term index in sequence
start: number; // Character start position
end: number; // Character end position
term: string; // The term text (e.g., "+10")
value: number; // Numeric value
columnIndex?: number; // Abacus column this affects
}
/** Group of related terms */
interface PedagogicalSegment {
segmentIndex: number;
termIndices: number[]; // Which terms belong to this segment
ruleName: string; // e.g., "Add 7 using complement"
description: string; // User-friendly explanation
}
/** Pedagogical explanation for a term */
interface TermReason {
name: string; // Rule name
description: string; // Why this operation
emoji: string; // Visual indicator
variant: "green" | "blue" | "purple" | "orange" | "gray";
steps?: BeadStep[]; // Physical bead movements
expansion?: string; // Mathematical expansion
context?: string; // Additional context
}
Styling
The components use CSS files for styling:
decomposition.css- Term and segment stylingreason-tooltip.css- Tooltip appearance
CSS Classes
.decomposition {
} /* Container */
.term {
} /* Individual term */
.term--current {
} /* Current step highlight */
.term--active {
} /* Hovered/selected term */
.term--grouped {
} /* Term in a segment */
.segment-group {
} /* Grouped segment wrapper */
Customization
Override CSS variables or classes:
/* Custom term highlight color */
.term--current {
background: rgba(139, 92, 246, 0.2);
border-color: rgba(139, 92, 246, 0.6);
}
/* Custom tooltip variant */
.reason-tooltip--custom {
border-color: #10b981;
background: linear-gradient(135deg, #ffffff 0%, #f0fdf4 100%);
}
File Structure
src/components/decomposition/
├── README.md # This file
├── index.ts # Public exports
├── DecompositionDisplay.tsx # Main display component
├── ReasonTooltip.tsx # Tooltip with explanations
├── decomposition.css # Term styling
└── reason-tooltip.css # Tooltip styling
src/contexts/
└── DecompositionContext.tsx # Provider and hooks
Testing
# Type check
npm run type-check
# Lint
npm run lint
# Full pre-commit check
npm run pre-commit
Related Documentation
Parent: apps/web/README.md - Web application overview
Tutorial System: src/components/tutorial/ - Tutorial player integration
Practice System: src/components/practice/ - Practice help panel integration
Instruction Generation: src/utils/generateUnifiedInstructionSequence.ts - Core algorithm
Changelog
v1.0.0 (December 2024)
- Initial standalone extraction from TutorialContext
- Decoupled from tutorial-specific UI context
- Added support for practice help panel integration
- Simplified API: only requires startValue and targetValue