feat: add practice page system to guided addition tutorial
Add dynamically generated practice problems interspersed with tutorial steps: - Practice pages with 10-20 problems based on skill level - Problem grid layout with input fields for answers - "Check Work" functionality with delayed feedback - Auto-removal of correct problems, keeping incorrect ones - Progress tracking across combined tutorial and practice steps 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback, useRef } from 'react'
|
||||
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { stack, hstack } from '../../styled-system/patterns'
|
||||
import { AbacusReact } from '@soroban/abacus-react'
|
||||
@@ -31,6 +31,22 @@ interface TutorialStep {
|
||||
multiStepInstructions?: string[]
|
||||
}
|
||||
|
||||
interface PracticeStep {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
skillLevel: 'basic' | 'heaven' | 'five-complements' | 'mixed'
|
||||
problemCount: number
|
||||
maxTerms: number // max numbers to add in a single problem
|
||||
}
|
||||
|
||||
interface Problem {
|
||||
id: string
|
||||
terms: number[]
|
||||
userAnswer?: number
|
||||
isCorrect?: boolean
|
||||
}
|
||||
|
||||
const tutorialSteps: TutorialStep[] = [
|
||||
// Phase 1: Basic Addition (1-4)
|
||||
{
|
||||
@@ -212,32 +228,32 @@ const tutorialSteps: TutorialStep[] = [
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'complement-3',
|
||||
title: 'Five Complement: 1 + 3',
|
||||
id: 'direct-addition-3',
|
||||
title: 'Direct Addition: 1 + 3',
|
||||
problem: '1 + 3',
|
||||
description: 'Add 3 when you have 3 spaces. Use complement: 3 = 5 - 2',
|
||||
description: 'Add 3 to 1. You have space, so add directly.',
|
||||
startValue: 1,
|
||||
targetValue: 4,
|
||||
highlightBeads: [
|
||||
{ columnIndex: 0, beadType: 'heaven' },
|
||||
{ columnIndex: 0, beadType: 'earth', position: 0 },
|
||||
{ columnIndex: 0, beadType: 'earth', position: 1 }
|
||||
{ columnIndex: 0, beadType: 'earth', position: 1 },
|
||||
{ columnIndex: 0, beadType: 'earth', position: 2 },
|
||||
{ columnIndex: 0, beadType: 'earth', position: 3 }
|
||||
],
|
||||
expectedAction: 'multi-step',
|
||||
actionDescription: 'Add heaven bead (5), then remove 2 earth beads',
|
||||
actionDescription: 'Add 3 earth beads one by one',
|
||||
multiStepInstructions: [
|
||||
'Click the heaven bead to add 5',
|
||||
'Click the first earth bead to remove it',
|
||||
'Click the second earth bead to remove it'
|
||||
'Click the second earth bead to add it',
|
||||
'Click the third earth bead to add it',
|
||||
'Click the fourth earth bead to add it'
|
||||
],
|
||||
tooltip: {
|
||||
content: 'Five Complement: 3 = 5 - 2',
|
||||
explanation: 'To add 3: add 5 (heaven bead), then subtract 2 (remove 2 earth beads)'
|
||||
content: 'Direct Addition - Check Your Space',
|
||||
explanation: 'You have 1 earth bead up and need to add 3 more. Since there are 4 earth positions total, you have 3 spaces available - perfect!'
|
||||
},
|
||||
errorMessages: {
|
||||
wrongBead: 'Use complement method: add heaven, remove 2 earth beads',
|
||||
wrongAction: 'Add 5, then subtract 2',
|
||||
hint: 'Complement: 3 = 5 - 2, so add 5 and take away 2'
|
||||
wrongBead: 'Add the earth beads directly - you have space!',
|
||||
wrongAction: 'Move the earth beads UP to add them',
|
||||
hint: 'No complement needed! Just add the remaining 3 earth beads directly.'
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -307,20 +323,20 @@ const tutorialSteps: TutorialStep[] = [
|
||||
id: 'mixed-1',
|
||||
title: 'Practice: 2 + 3',
|
||||
problem: '2 + 3',
|
||||
description: 'Add 3 to 2. You can add directly or use complement (3 = 5 - 2)',
|
||||
description: 'Add 3 to 2. You have space, so add directly.',
|
||||
startValue: 2,
|
||||
targetValue: 5,
|
||||
highlightBeads: [{ columnIndex: 0, beadType: 'earth', position: 2 }],
|
||||
expectedAction: 'add',
|
||||
actionDescription: 'Add the third earth bead directly, or use complement method',
|
||||
actionDescription: 'Add the third earth bead to complete 5',
|
||||
tooltip: {
|
||||
content: 'Choice: Direct or Complement',
|
||||
explanation: 'You have space to add directly, but complement (3 = 5 - 2) is also valid'
|
||||
content: 'Direct Addition',
|
||||
explanation: 'Since you have space (only 2 earth beads are up), simply add the third earth bead'
|
||||
},
|
||||
errorMessages: {
|
||||
wrongBead: 'Either add the third earth bead or use complement method',
|
||||
wrongAction: 'Add the earth bead UP or use heaven bead + remove 2',
|
||||
hint: 'For 3: add earth bead OR use complement 3 = 5 - 2'
|
||||
wrongBead: 'Click the highlighted third earth bead',
|
||||
wrongAction: 'Move the earth bead UP to add it',
|
||||
hint: 'You have space for one more earth bead - no complement needed!'
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -352,9 +368,225 @@ const tutorialSteps: TutorialStep[] = [
|
||||
}
|
||||
]
|
||||
|
||||
const practiceSteps: PracticeStep[] = [
|
||||
{
|
||||
id: 'practice-basic',
|
||||
title: 'Practice: Basic Addition (1-4)',
|
||||
description: 'Practice adding numbers 1-4 using only earth beads',
|
||||
skillLevel: 'basic',
|
||||
problemCount: 12,
|
||||
maxTerms: 3
|
||||
},
|
||||
{
|
||||
id: 'practice-heaven',
|
||||
title: 'Practice: Heaven Bead & Simple Combinations',
|
||||
description: 'Practice using the heaven bead (5) and combining it with earth beads',
|
||||
skillLevel: 'heaven',
|
||||
problemCount: 15,
|
||||
maxTerms: 3
|
||||
},
|
||||
{
|
||||
id: 'practice-complements',
|
||||
title: 'Practice: Five Complements',
|
||||
description: 'Practice using five complements when you run out of space',
|
||||
skillLevel: 'five-complements',
|
||||
problemCount: 20,
|
||||
maxTerms: 4
|
||||
},
|
||||
{
|
||||
id: 'practice-mixed',
|
||||
title: 'Practice: Mixed Problems',
|
||||
description: 'Practice all techniques together with varied problem types',
|
||||
skillLevel: 'mixed',
|
||||
problemCount: 18,
|
||||
maxTerms: 5
|
||||
}
|
||||
]
|
||||
|
||||
// Problem generation functions
|
||||
function generateBasicProblems(count: number, maxTerms: number): Problem[] {
|
||||
const problems: Problem[] = []
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const termCount = Math.floor(Math.random() * (maxTerms - 1)) + 2 // 2-maxTerms terms
|
||||
const terms: number[] = []
|
||||
|
||||
for (let j = 0; j < termCount; j++) {
|
||||
// Only use 1-4 for basic problems
|
||||
terms.push(Math.floor(Math.random() * 4) + 1)
|
||||
}
|
||||
|
||||
// Ensure the sum doesn't exceed 9 (single digit)
|
||||
const sum = terms.reduce((a, b) => a + b, 0)
|
||||
if (sum <= 9) {
|
||||
problems.push({
|
||||
id: `basic-${i}`,
|
||||
terms
|
||||
})
|
||||
} else {
|
||||
i-- // Try again if sum is too large
|
||||
}
|
||||
}
|
||||
|
||||
return problems
|
||||
}
|
||||
|
||||
function generateHeavenProblems(count: number, maxTerms: number): Problem[] {
|
||||
const problems: Problem[] = []
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const termCount = Math.floor(Math.random() * (maxTerms - 1)) + 2
|
||||
const terms: number[] = []
|
||||
|
||||
for (let j = 0; j < termCount; j++) {
|
||||
// Use 1-9, but ensure at least one term needs heaven bead (5 or more)
|
||||
const num = Math.floor(Math.random() * 9) + 1
|
||||
terms.push(num)
|
||||
}
|
||||
|
||||
// Ensure we have at least one 5+ or the sum involves 5+
|
||||
const hasLargeNum = terms.some(t => t >= 5)
|
||||
const sum = terms.reduce((a, b) => a + b, 0)
|
||||
|
||||
if ((hasLargeNum || sum >= 5) && sum <= 9) {
|
||||
problems.push({
|
||||
id: `heaven-${i}`,
|
||||
terms
|
||||
})
|
||||
} else {
|
||||
i-- // Try again
|
||||
}
|
||||
}
|
||||
|
||||
return problems
|
||||
}
|
||||
|
||||
function generateComplementProblems(count: number, maxTerms: number): Problem[] {
|
||||
const problems: Problem[] = []
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const termCount = Math.floor(Math.random() * (maxTerms - 1)) + 2
|
||||
const terms: number[] = []
|
||||
|
||||
// Generate problems that specifically require complements
|
||||
// This means having a situation where direct addition won't work
|
||||
const firstTerm = Math.floor(Math.random() * 4) + 1 // 1-4
|
||||
terms.push(firstTerm)
|
||||
|
||||
// Add a term that forces complement usage
|
||||
const secondTerm = Math.floor(Math.random() * 3) + 3 // 3-5
|
||||
terms.push(secondTerm)
|
||||
|
||||
// Maybe add more terms
|
||||
for (let j = 2; j < termCount; j++) {
|
||||
const num = Math.floor(Math.random() * 4) + 1 // 1-4
|
||||
terms.push(num)
|
||||
}
|
||||
|
||||
const sum = terms.reduce((a, b) => a + b, 0)
|
||||
if (sum <= 9) {
|
||||
problems.push({
|
||||
id: `complement-${i}`,
|
||||
terms
|
||||
})
|
||||
} else {
|
||||
i-- // Try again
|
||||
}
|
||||
}
|
||||
|
||||
return problems
|
||||
}
|
||||
|
||||
function generateMixedProblems(count: number, maxTerms: number): Problem[] {
|
||||
const problems: Problem[] = []
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const termCount = Math.floor(Math.random() * (maxTerms - 1)) + 2
|
||||
const terms: number[] = []
|
||||
|
||||
for (let j = 0; j < termCount; j++) {
|
||||
// Use full range 1-9
|
||||
terms.push(Math.floor(Math.random() * 9) + 1)
|
||||
}
|
||||
|
||||
const sum = terms.reduce((a, b) => a + b, 0)
|
||||
if (sum <= 9) {
|
||||
problems.push({
|
||||
id: `mixed-${i}`,
|
||||
terms
|
||||
})
|
||||
} else {
|
||||
i-- // Try again
|
||||
}
|
||||
}
|
||||
|
||||
return problems
|
||||
}
|
||||
|
||||
function generateProblems(step: PracticeStep): Problem[] {
|
||||
switch (step.skillLevel) {
|
||||
case 'basic':
|
||||
return generateBasicProblems(step.problemCount, step.maxTerms)
|
||||
case 'heaven':
|
||||
return generateHeavenProblems(step.problemCount, step.maxTerms)
|
||||
case 'five-complements':
|
||||
return generateComplementProblems(step.problemCount, step.maxTerms)
|
||||
case 'mixed':
|
||||
return generateMixedProblems(step.problemCount, step.maxTerms)
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
// Combined tutorial flow with practice steps interspersed
|
||||
const combinedSteps = [
|
||||
// Basic addition tutorial steps
|
||||
tutorialSteps[0], // basic-1
|
||||
tutorialSteps[1], // basic-2
|
||||
tutorialSteps[2], // basic-3
|
||||
tutorialSteps[3], // basic-4
|
||||
|
||||
// Practice basic addition
|
||||
practiceSteps[0], // practice-basic
|
||||
|
||||
// Heaven bead tutorial steps
|
||||
tutorialSteps[4], // heaven-intro
|
||||
tutorialSteps[5], // heaven-plus-earth
|
||||
|
||||
// Practice heaven bead
|
||||
practiceSteps[1], // practice-heaven
|
||||
|
||||
// Five complements tutorial steps
|
||||
tutorialSteps[6], // complement-intro
|
||||
tutorialSteps[7], // complement-2
|
||||
tutorialSteps[8], // complement-3
|
||||
tutorialSteps[9], // complement-4
|
||||
tutorialSteps[10], // complement-5
|
||||
|
||||
// Practice five complements
|
||||
practiceSteps[2], // practice-complements
|
||||
|
||||
// Final mixed tutorial steps
|
||||
tutorialSteps[11], // mixed-1
|
||||
tutorialSteps[12], // mixed-2
|
||||
|
||||
// Final mixed practice
|
||||
practiceSteps[3] // practice-mixed
|
||||
]
|
||||
|
||||
type StepType = TutorialStep | PracticeStep
|
||||
|
||||
function isTutorialStep(step: StepType): step is TutorialStep {
|
||||
return 'problem' in step && 'startValue' in step
|
||||
}
|
||||
|
||||
function isPracticeStep(step: StepType): step is PracticeStep {
|
||||
return 'skillLevel' in step && 'problemCount' in step
|
||||
}
|
||||
|
||||
export function GuidedAdditionTutorial() {
|
||||
const [currentStepIndex, setCurrentStepIndex] = useState(0)
|
||||
const [currentValue, setCurrentValue] = useState(tutorialSteps[0].startValue)
|
||||
const [currentValue, setCurrentValue] = useState(0)
|
||||
const [feedback, setFeedback] = useState<string | null>(null)
|
||||
const [isCorrect, setIsCorrect] = useState(false)
|
||||
const [multiStepProgress, setMultiStepProgress] = useState(0)
|
||||
@@ -362,8 +594,12 @@ export function GuidedAdditionTutorial() {
|
||||
const transitionTimeoutRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const lastProcessedValueRef = useRef<number | null>(null)
|
||||
|
||||
const currentStep = tutorialSteps[currentStepIndex]
|
||||
const isLastStep = currentStepIndex === tutorialSteps.length - 1
|
||||
// Practice-specific state
|
||||
const [currentProblems, setCurrentProblems] = useState<Problem[]>([])
|
||||
const [showPracticeResults, setShowPracticeResults] = useState(false)
|
||||
|
||||
const currentStep = combinedSteps[currentStepIndex]
|
||||
const isLastStep = currentStepIndex === combinedSteps.length - 1
|
||||
|
||||
const nextStep = useCallback(() => {
|
||||
if (isLastStep || isTransitioning) return
|
||||
@@ -374,7 +610,18 @@ export function GuidedAdditionTutorial() {
|
||||
setTimeout(() => {
|
||||
const nextStepIndex = currentStepIndex + 1
|
||||
setCurrentStepIndex(nextStepIndex)
|
||||
setCurrentValue(tutorialSteps[nextStepIndex].startValue)
|
||||
|
||||
const nextStep = combinedSteps[nextStepIndex]
|
||||
if (isTutorialStep(nextStep)) {
|
||||
setCurrentValue(nextStep.startValue)
|
||||
} else {
|
||||
// Practice step - generate problems
|
||||
const problems = generateProblems(nextStep)
|
||||
setCurrentProblems(problems)
|
||||
setCurrentValue(0)
|
||||
setShowPracticeResults(false)
|
||||
}
|
||||
|
||||
setFeedback(null)
|
||||
setIsCorrect(false)
|
||||
setMultiStepProgress(0)
|
||||
@@ -384,6 +631,9 @@ export function GuidedAdditionTutorial() {
|
||||
}, [currentStepIndex, isLastStep, isTransitioning])
|
||||
|
||||
const checkStep = useCallback((newValue: number) => {
|
||||
// Only check tutorial steps, not practice steps
|
||||
if (!isTutorialStep(currentStep)) return
|
||||
|
||||
// Prevent processing the same value multiple times
|
||||
if (lastProcessedValueRef.current === newValue) return
|
||||
lastProcessedValueRef.current = newValue
|
||||
@@ -415,7 +665,7 @@ export function GuidedAdditionTutorial() {
|
||||
|
||||
// Auto-advance to next step after a brief delay
|
||||
transitionTimeoutRef.current = setTimeout(() => {
|
||||
if (currentStepIndex < tutorialSteps.length - 1) {
|
||||
if (currentStepIndex < combinedSteps.length - 1) {
|
||||
nextStep()
|
||||
}
|
||||
transitionTimeoutRef.current = null
|
||||
@@ -425,6 +675,80 @@ export function GuidedAdditionTutorial() {
|
||||
}
|
||||
}, [currentStep, multiStepProgress, currentStepIndex, nextStep, isTransitioning])
|
||||
|
||||
// Practice step functions
|
||||
const updateProblemAnswer = useCallback((problemId: string, answer: number) => {
|
||||
setCurrentProblems(prev => prev.map(p =>
|
||||
p.id === problemId ? { ...p, userAnswer: answer } : p
|
||||
))
|
||||
}, [])
|
||||
|
||||
const checkPracticeWork = useCallback(() => {
|
||||
if (!isPracticeStep(currentStep)) return
|
||||
|
||||
const updatedProblems = currentProblems.map(problem => {
|
||||
const correctAnswer = problem.terms.reduce((sum, term) => sum + term, 0)
|
||||
return {
|
||||
...problem,
|
||||
isCorrect: problem.userAnswer === correctAnswer
|
||||
}
|
||||
})
|
||||
|
||||
setCurrentProblems(updatedProblems)
|
||||
setShowPracticeResults(true)
|
||||
|
||||
// Remove correct problems after a delay
|
||||
setTimeout(() => {
|
||||
const incorrectProblems = updatedProblems.filter(p => !p.isCorrect)
|
||||
|
||||
if (incorrectProblems.length === 0) {
|
||||
// All correct - advance to next step
|
||||
setFeedback('Perfect! All problems completed correctly.')
|
||||
setTimeout(() => {
|
||||
nextStep()
|
||||
}, 2000)
|
||||
} else {
|
||||
// Keep only incorrect problems
|
||||
setCurrentProblems(incorrectProblems.map(p => ({ ...p, userAnswer: undefined, isCorrect: undefined })))
|
||||
setShowPracticeResults(false)
|
||||
setFeedback(`${incorrectProblems.length} problem(s) need correction. Try again!`)
|
||||
|
||||
setTimeout(() => {
|
||||
setFeedback(null)
|
||||
}, 3000)
|
||||
}
|
||||
}, 2000)
|
||||
}, [currentStep, currentProblems, nextStep])
|
||||
|
||||
// Initialize practice problems when entering a practice step
|
||||
useEffect(() => {
|
||||
if (isPracticeStep(currentStep)) {
|
||||
const step = currentStep as PracticeStep
|
||||
let problems: Problem[] = []
|
||||
|
||||
switch (step.skillLevel) {
|
||||
case 'basic':
|
||||
problems = generateBasicProblems(step.problemCount, step.maxTerms)
|
||||
break
|
||||
case 'heaven':
|
||||
problems = generateHeavenProblems(step.problemCount, step.maxTerms)
|
||||
break
|
||||
case 'five-complements':
|
||||
problems = generateComplementProblems(step.problemCount, step.maxTerms)
|
||||
break
|
||||
case 'mixed':
|
||||
problems = generateMixedProblems(step.problemCount, step.maxTerms)
|
||||
break
|
||||
}
|
||||
|
||||
setCurrentProblems(problems)
|
||||
setShowPracticeResults(false)
|
||||
} else {
|
||||
// Reset practice state for tutorial steps
|
||||
setCurrentProblems([])
|
||||
setShowPracticeResults(false)
|
||||
}
|
||||
}, [currentStepIndex, currentStep])
|
||||
|
||||
const resetTutorial = useCallback(() => {
|
||||
// Clear any pending transition timeout
|
||||
if (transitionTimeoutRef.current) {
|
||||
@@ -433,11 +757,13 @@ export function GuidedAdditionTutorial() {
|
||||
}
|
||||
|
||||
setCurrentStepIndex(0)
|
||||
setCurrentValue(tutorialSteps[0].startValue)
|
||||
setCurrentValue(combinedSteps[0] && isTutorialStep(combinedSteps[0]) ? combinedSteps[0].startValue : 0)
|
||||
setFeedback(null)
|
||||
setIsCorrect(false)
|
||||
setMultiStepProgress(0)
|
||||
setIsTransitioning(false)
|
||||
setCurrentProblems([])
|
||||
setShowPracticeResults(false)
|
||||
lastProcessedValueRef.current = null // Reset for restart
|
||||
}, [])
|
||||
|
||||
@@ -454,7 +780,7 @@ export function GuidedAdditionTutorial() {
|
||||
bg: 'blue.500',
|
||||
h: 'full',
|
||||
transition: 'width',
|
||||
width: `${((currentStepIndex + 1) / tutorialSteps.length) * 100}%`
|
||||
width: `${((currentStepIndex + 1) / combinedSteps.length) * 100}%`
|
||||
})} />
|
||||
</div>
|
||||
|
||||
@@ -462,111 +788,281 @@ export function GuidedAdditionTutorial() {
|
||||
<div className={css({
|
||||
textAlign: 'center',
|
||||
p: '4',
|
||||
bg: 'blue.50',
|
||||
bg: isPracticeStep(currentStep) ? 'purple.50' : 'blue.50',
|
||||
rounded: 'lg',
|
||||
border: '1px solid',
|
||||
borderColor: 'blue.200'
|
||||
borderColor: isPracticeStep(currentStep) ? 'purple.200' : 'blue.200'
|
||||
})}>
|
||||
<h4 className={css({
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'semibold',
|
||||
color: 'blue.800',
|
||||
color: isPracticeStep(currentStep) ? 'purple.800' : 'blue.800',
|
||||
mb: '2'
|
||||
})}>
|
||||
Step {currentStepIndex + 1} of {tutorialSteps.length}: {currentStep.title}
|
||||
Step {currentStepIndex + 1} of {combinedSteps.length}: {isPracticeStep(currentStep) ? currentStep.title : currentStep.title}
|
||||
</h4>
|
||||
<p className={css({
|
||||
fontSize: 'md',
|
||||
color: 'blue.700',
|
||||
mb: '2'
|
||||
})}>
|
||||
Problem: <strong>{currentStep.problem}</strong>
|
||||
</p>
|
||||
<p className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'blue.600'
|
||||
})}>
|
||||
{currentStep.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Tooltip */}
|
||||
<div className={css({
|
||||
bg: 'yellow.50',
|
||||
border: '1px solid',
|
||||
borderColor: 'yellow.300',
|
||||
rounded: 'lg',
|
||||
p: '4'
|
||||
})}>
|
||||
<h5 className={css({
|
||||
fontWeight: 'semibold',
|
||||
color: 'yellow.800',
|
||||
mb: '2'
|
||||
})}>
|
||||
💡 {currentStep.tooltip.content}
|
||||
</h5>
|
||||
<p className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'yellow.700'
|
||||
})}>
|
||||
{currentStep.tooltip.explanation}
|
||||
</p>
|
||||
{currentStep.multiStepInstructions && (
|
||||
<div className={css({ mt: '3' })}>
|
||||
<p className={css({ fontSize: 'sm', fontWeight: 'medium', color: 'yellow.800', mb: '1' })}>
|
||||
Instructions:
|
||||
{isPracticeStep(currentStep) ? (
|
||||
<p className={css({
|
||||
fontSize: 'md',
|
||||
color: 'purple.700',
|
||||
mb: '2'
|
||||
})}>
|
||||
Complete all problems using the techniques you've learned
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p className={css({
|
||||
fontSize: 'md',
|
||||
color: 'blue.700',
|
||||
mb: '2'
|
||||
})}>
|
||||
Problem: <strong>{currentStep.problem}</strong>
|
||||
</p>
|
||||
<ol className={css({ fontSize: 'sm', color: 'yellow.700', pl: '4' })}>
|
||||
{currentStep.multiStepInstructions.map((instruction, index) => (
|
||||
<li key={index} className={css({
|
||||
mb: '1',
|
||||
opacity: index <= multiStepProgress ? '1' : '0.6',
|
||||
fontWeight: index === multiStepProgress ? 'semibold' : 'normal'
|
||||
})}>
|
||||
{index + 1}. {instruction}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
<p className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'blue.600'
|
||||
})}>
|
||||
{currentStep.description}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Interactive Abacus */}
|
||||
<div className={css({
|
||||
bg: 'white',
|
||||
border: '2px solid',
|
||||
borderColor: 'blue.300',
|
||||
rounded: 'xl',
|
||||
p: '6',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
minHeight: '400px',
|
||||
alignItems: 'center'
|
||||
})}>
|
||||
<AbacusReact
|
||||
value={currentValue}
|
||||
columns={1}
|
||||
beadShape="diamond"
|
||||
colorScheme="place-value"
|
||||
hideInactiveBeads={false}
|
||||
scaleFactor={2.0}
|
||||
interactive={true}
|
||||
showNumbers={true}
|
||||
animated={true}
|
||||
onValueChange={checkStep}
|
||||
customStyles={{
|
||||
beadHighlight: currentStep.highlightBeads?.reduce((acc, bead) => {
|
||||
const key = `${bead.columnIndex}-${bead.beadType}${bead.position !== undefined ? `-${bead.position}` : ''}`
|
||||
acc[key] = {
|
||||
stroke: '#3B82F6',
|
||||
strokeWidth: 3,
|
||||
filter: 'drop-shadow(0 0 8px rgba(59, 130, 246, 0.6))'
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<string, any>)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/* Tutorial tooltip or Practice problems */}
|
||||
{isPracticeStep(currentStep) ? (
|
||||
<div className={css({
|
||||
bg: 'purple.50',
|
||||
border: '1px solid',
|
||||
borderColor: 'purple.300',
|
||||
rounded: 'lg',
|
||||
p: '4'
|
||||
})}>
|
||||
<h5 className={css({
|
||||
fontWeight: 'semibold',
|
||||
color: 'purple.800',
|
||||
mb: '4',
|
||||
textAlign: 'center'
|
||||
})}>
|
||||
Practice Problems
|
||||
</h5>
|
||||
|
||||
{/* Problem grid */}
|
||||
<div className={css({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))',
|
||||
gap: '3',
|
||||
mb: '4'
|
||||
})}>
|
||||
{currentProblems.map((problem, index) => (
|
||||
<div key={problem.id} className={css({
|
||||
p: '3',
|
||||
bg: 'white',
|
||||
border: '1px solid',
|
||||
borderColor: problem.isCorrect === true ? 'green.300' : problem.isCorrect === false ? 'red.300' : 'gray.300',
|
||||
rounded: 'md',
|
||||
textAlign: 'center'
|
||||
})}>
|
||||
<div className={css({
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: 'gray.600',
|
||||
mb: '2'
|
||||
})}>
|
||||
#{index + 1}
|
||||
</div>
|
||||
<div className={css({
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'semibold',
|
||||
mb: '2'
|
||||
})}>
|
||||
{problem.terms.join(' + ')} = ?
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
value={problem.userAnswer || ''}
|
||||
onChange={(e) => updateProblemAnswer(problem.id, parseInt(e.target.value) || 0)}
|
||||
className={css({
|
||||
w: 'full',
|
||||
p: '2',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.300',
|
||||
rounded: 'md',
|
||||
textAlign: 'center',
|
||||
fontSize: 'md'
|
||||
})}
|
||||
placeholder="Answer"
|
||||
/>
|
||||
{showPracticeResults && problem.isCorrect === false && (
|
||||
<div className={css({
|
||||
mt: '2',
|
||||
fontSize: 'sm',
|
||||
color: 'red.600'
|
||||
})}>
|
||||
Incorrect. Try again!
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Check work button */}
|
||||
{!showPracticeResults && currentProblems.length > 0 && (
|
||||
<div className={css({ textAlign: 'center' })}>
|
||||
<button
|
||||
onClick={checkPracticeWork}
|
||||
className={css({
|
||||
px: '6',
|
||||
py: '3',
|
||||
bg: 'purple.500',
|
||||
color: 'white',
|
||||
rounded: 'lg',
|
||||
fontWeight: 'semibold',
|
||||
cursor: 'pointer',
|
||||
_hover: { bg: 'purple.600' }
|
||||
})}
|
||||
>
|
||||
Check Work
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Continue button after all problems correct */}
|
||||
{showPracticeResults && currentProblems.every(p => p.isCorrect) && (
|
||||
<div className={css({ textAlign: 'center' })}>
|
||||
<p className={css({
|
||||
fontSize: 'lg',
|
||||
fontWeight: 'semibold',
|
||||
color: 'green.600',
|
||||
mb: '3'
|
||||
})}>
|
||||
🎉 All problems correct! Great job!
|
||||
</p>
|
||||
<button
|
||||
onClick={nextStep}
|
||||
className={css({
|
||||
px: '6',
|
||||
py: '3',
|
||||
bg: 'green.500',
|
||||
color: 'white',
|
||||
rounded: 'lg',
|
||||
fontWeight: 'semibold',
|
||||
cursor: 'pointer',
|
||||
_hover: { bg: 'green.600' }
|
||||
})}
|
||||
>
|
||||
Continue Tutorial
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Practice Abacus */}
|
||||
<div className={css({
|
||||
bg: 'white',
|
||||
border: '2px solid',
|
||||
borderColor: 'purple.300',
|
||||
rounded: 'xl',
|
||||
p: '4',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
minHeight: '350px',
|
||||
alignItems: 'center',
|
||||
overflow: 'visible'
|
||||
})}>
|
||||
<div style={{ width: 'fit-content', height: 'fit-content' }}>
|
||||
<AbacusReact
|
||||
value={0}
|
||||
columns={3}
|
||||
beadShape="diamond"
|
||||
colorScheme="place-value"
|
||||
hideInactiveBeads={false}
|
||||
scaleFactor={2.0}
|
||||
interactive={true}
|
||||
showNumbers={false}
|
||||
animated={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={css({
|
||||
bg: 'yellow.50',
|
||||
border: '1px solid',
|
||||
borderColor: 'yellow.300',
|
||||
rounded: 'lg',
|
||||
p: '4'
|
||||
})}>
|
||||
<h5 className={css({
|
||||
fontWeight: 'semibold',
|
||||
color: 'yellow.800',
|
||||
mb: '2'
|
||||
})}>
|
||||
💡 {currentStep.tooltip.content}
|
||||
</h5>
|
||||
<p className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'yellow.700'
|
||||
})}>
|
||||
{currentStep.tooltip.explanation}
|
||||
</p>
|
||||
{currentStep.multiStepInstructions && (
|
||||
<div className={css({ mt: '3' })}>
|
||||
<p className={css({ fontSize: 'sm', fontWeight: 'medium', color: 'yellow.800', mb: '1' })}>
|
||||
Instructions:
|
||||
</p>
|
||||
<ol className={css({ fontSize: 'sm', color: 'yellow.700', pl: '4' })}>
|
||||
{currentStep.multiStepInstructions.map((instruction, index) => (
|
||||
<li key={index} className={css({
|
||||
mb: '1',
|
||||
opacity: index <= multiStepProgress ? '1' : '0.6',
|
||||
fontWeight: index === multiStepProgress ? 'semibold' : 'normal'
|
||||
})}>
|
||||
{index + 1}. {instruction}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Interactive Abacus - only show for tutorial steps */}
|
||||
{!isPracticeStep(currentStep) && (
|
||||
<div className={css({
|
||||
bg: 'white',
|
||||
border: '2px solid',
|
||||
borderColor: 'blue.300',
|
||||
rounded: 'xl',
|
||||
p: '6',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
minHeight: '400px',
|
||||
alignItems: 'center'
|
||||
})}>
|
||||
<AbacusReact
|
||||
value={currentValue}
|
||||
columns={1}
|
||||
beadShape="diamond"
|
||||
colorScheme="place-value"
|
||||
hideInactiveBeads={false}
|
||||
scaleFactor={2.0}
|
||||
interactive={true}
|
||||
showNumbers={true}
|
||||
animated={true}
|
||||
onValueChange={checkStep}
|
||||
customStyles={{
|
||||
beadHighlight: currentStep.highlightBeads?.reduce((acc, bead) => {
|
||||
const key = `${bead.columnIndex}-${bead.beadType}${bead.position !== undefined ? `-${bead.position}` : ''}`
|
||||
acc[key] = {
|
||||
stroke: '#3B82F6',
|
||||
strokeWidth: 3,
|
||||
filter: 'drop-shadow(0 0 8px rgba(59, 130, 246, 0.6))'
|
||||
}
|
||||
return acc
|
||||
}, {} as Record<string, any>)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Feedback */}
|
||||
{feedback && (
|
||||
|
||||
Reference in New Issue
Block a user