diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index 2d346159..67d7f7e6 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -1,20 +1,78 @@
'use client'
import Link from 'next/link'
-import { useEffect, useState, useRef } from 'react'
+import dynamic from 'next/dynamic'
+import { useEffect, useState, useRef, useMemo } from 'react'
import { useTranslations, useMessages } from 'next-intl'
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
import { useHomeHero } from '@/contexts/HomeHeroContext'
import { PageWithNav } from '@/components/PageWithNav'
-import { TutorialPlayer } from '@/components/tutorial/TutorialPlayer'
-import { getTutorialForEditor } from '@/utils/tutorialConverter'
+import { getTutorialForEditor, type Tutorial } from '@/utils/tutorialConverter'
import { getAvailableGames } from '@/lib/arcade/game-registry'
-import { InteractiveFlashcards } from '@/components/InteractiveFlashcards'
-import { LevelSliderDisplay } from '@/components/LevelSliderDisplay'
import { HomeBlogSection } from '@/components/HomeBlogSection'
import { css } from '../../styled-system/css'
import { container, grid, hstack, stack } from '../../styled-system/patterns'
+// Skeleton placeholders for lazy-loaded components
+function TutorialSkeleton() {
+ return (
+
+ )
+}
+
+function FlashcardsSkeleton() {
+ return (
+
+ )
+}
+
+function LevelSliderSkeleton() {
+ return (
+
+ )
+}
+
+// Lazy load heavy components - skip SSR entirely for performance
+const TutorialPlayer = dynamic(
+ () => import('@/components/tutorial/TutorialPlayer').then((m) => m.TutorialPlayer),
+ { ssr: false, loading: () => }
+)
+
+const InteractiveFlashcards = dynamic(
+ () => import('@/components/InteractiveFlashcards').then((m) => m.InteractiveFlashcards),
+ { ssr: false, loading: () => }
+)
+
+const LevelSliderDisplay = dynamic(
+ () => import('@/components/LevelSliderDisplay').then((m) => m.LevelSliderDisplay),
+ { ssr: false, loading: () => }
+)
+
// Hero section placeholder - the actual abacus is rendered by MyAbacus component
function HeroSection() {
const { subtitle, setIsHeroVisible, isSubtitleLoaded } = useHomeHero()
@@ -213,45 +271,56 @@ export default function HomePage() {
const t = useTranslations('home')
const messages = useMessages() as any
const [selectedSkillIndex, setSelectedSkillIndex] = useState(1) // Default to "Friends techniques"
- const fullTutorial = getTutorialForEditor(messages.tutorial || {})
- // Create different tutorials for each skill level
- const skillTutorials = [
- // Skill 0: Read and set numbers (0-9999)
- {
- ...fullTutorial,
- id: 'read-numbers-demo',
- title: t('skills.readNumbers.tutorialTitle'),
- description: t('skills.readNumbers.tutorialDesc'),
- steps: fullTutorial.steps.filter((step) => step.id.startsWith('basic-')),
- },
- // Skill 1: Friends techniques (5 = 2+3)
- {
- ...fullTutorial,
- id: 'friends-of-5-demo',
- title: t('skills.friends.tutorialTitle'),
- description: t('skills.friends.tutorialDesc'),
- steps: fullTutorial.steps.filter((step) => step.id === 'complement-2'),
- },
- // Skill 2: Multiply & divide (12×34)
- {
- ...fullTutorial,
- id: 'multiply-demo',
- title: t('skills.multiply.tutorialTitle'),
- description: t('skills.multiply.tutorialDesc'),
- steps: fullTutorial.steps.filter((step) => step.id.includes('complement')).slice(0, 3),
- },
- // Skill 3: Mental calculation (Speed math)
- {
- ...fullTutorial,
- id: 'mental-calc-demo',
- title: t('skills.mental.tutorialTitle'),
- description: t('skills.mental.tutorialDesc'),
- steps: fullTutorial.steps.slice(-3),
- },
- ]
+ // Defer tutorial processing to after hydration for better SSR performance
+ const [fullTutorial, setFullTutorial] = useState(null)
- const selectedTutorial = skillTutorials[selectedSkillIndex]
+ useEffect(() => {
+ // Process tutorial data after hydration to avoid blocking SSR
+ setFullTutorial(getTutorialForEditor(messages.tutorial || {}))
+ }, [messages.tutorial])
+
+ // Create different tutorials for each skill level (memoized to avoid recalc on every render)
+ const skillTutorials = useMemo(() => {
+ if (!fullTutorial) return null
+
+ return [
+ // Skill 0: Read and set numbers (0-9999)
+ {
+ ...fullTutorial,
+ id: 'read-numbers-demo',
+ title: t('skills.readNumbers.tutorialTitle'),
+ description: t('skills.readNumbers.tutorialDesc'),
+ steps: fullTutorial.steps.filter((step) => step.id.startsWith('basic-')),
+ },
+ // Skill 1: Friends techniques (5 = 2+3)
+ {
+ ...fullTutorial,
+ id: 'friends-of-5-demo',
+ title: t('skills.friends.tutorialTitle'),
+ description: t('skills.friends.tutorialDesc'),
+ steps: fullTutorial.steps.filter((step) => step.id === 'complement-2'),
+ },
+ // Skill 2: Multiply & divide (12×34)
+ {
+ ...fullTutorial,
+ id: 'multiply-demo',
+ title: t('skills.multiply.tutorialTitle'),
+ description: t('skills.multiply.tutorialDesc'),
+ steps: fullTutorial.steps.filter((step) => step.id.includes('complement')).slice(0, 3),
+ },
+ // Skill 3: Mental calculation (Speed math)
+ {
+ ...fullTutorial,
+ id: 'mental-calc-demo',
+ title: t('skills.mental.tutorialTitle'),
+ description: t('skills.mental.tutorialDesc'),
+ steps: fullTutorial.steps.slice(-3),
+ },
+ ]
+ }, [fullTutorial, t])
+
+ const selectedTutorial = skillTutorials?.[selectedSkillIndex] ?? null
return (
@@ -336,17 +405,21 @@ export default function HomePage() {
maxW: '250px',
})}
>
-
+ {selectedTutorial ? (
+
+ ) : (
+
+ )}
{/* What you'll learn on the right */}
diff --git a/apps/web/src/i18n/request.ts b/apps/web/src/i18n/request.ts
index 0e5eadfd..a8497f9e 100644
--- a/apps/web/src/i18n/request.ts
+++ b/apps/web/src/i18n/request.ts
@@ -5,8 +5,8 @@ import { getMessages } from './messages'
export async function getRequestLocale(): Promise {
// Get locale from header (set by middleware) or cookie
- const headersList = await headers()
- const cookieStore = await cookies()
+ // Parallelize async operations to reduce SSR latency
+ const [headersList, cookieStore] = await Promise.all([headers(), cookies()])
let locale = headersList.get('x-locale') as Locale | null
diff --git a/apps/web/src/lib/flowcharts/definitions/order-of-operations.flow.json b/apps/web/src/lib/flowcharts/definitions/order-of-operations.flow.json
new file mode 100644
index 00000000..4a0d93ee
--- /dev/null
+++ b/apps/web/src/lib/flowcharts/definitions/order-of-operations.flow.json
@@ -0,0 +1,456 @@
+{
+ "id": "order-of-operations",
+ "title": "Order of Operations (PEMDAS)",
+ "mermaidFile": "embedded",
+ "problemInput": {
+ "schema": "pemdas-expression",
+ "fields": [
+ {
+ "name": "expr",
+ "label": "Expression",
+ "type": "string"
+ },
+ {
+ "name": "numSteps",
+ "label": "Number of operations",
+ "type": "integer",
+ "min": 1,
+ "max": 3
+ },
+ {
+ "name": "s1Priority",
+ "label": "Step 1: Priority level",
+ "type": "choice",
+ "options": ["P", "E", "MD", "AS"]
+ },
+ {
+ "name": "s1Left",
+ "label": "Step 1: Left operand",
+ "type": "number"
+ },
+ {
+ "name": "s1Op",
+ "label": "Step 1: Operator",
+ "type": "choice",
+ "options": ["+", "-", "×", "÷", "^"]
+ },
+ {
+ "name": "s1Right",
+ "label": "Step 1: Right operand",
+ "type": "number"
+ },
+ {
+ "name": "s1Result",
+ "label": "Step 1: Result",
+ "type": "number"
+ },
+ {
+ "name": "s1Expr",
+ "label": "Expression after step 1",
+ "type": "string"
+ },
+ {
+ "name": "s2Priority",
+ "label": "Step 2: Priority level",
+ "type": "choice",
+ "options": ["P", "E", "MD", "AS", "none"]
+ },
+ {
+ "name": "s2Left",
+ "label": "Step 2: Left operand",
+ "type": "number"
+ },
+ {
+ "name": "s2Op",
+ "label": "Step 2: Operator",
+ "type": "choice",
+ "options": ["+", "-", "×", "÷", "^", ""]
+ },
+ {
+ "name": "s2Right",
+ "label": "Step 2: Right operand",
+ "type": "number"
+ },
+ {
+ "name": "s2Result",
+ "label": "Step 2: Result",
+ "type": "number"
+ },
+ {
+ "name": "s2Expr",
+ "label": "Expression after step 2",
+ "type": "string"
+ },
+ {
+ "name": "s3Priority",
+ "label": "Step 3: Priority level",
+ "type": "choice",
+ "options": ["P", "E", "MD", "AS", "none"]
+ },
+ {
+ "name": "s3Left",
+ "label": "Step 3: Left operand",
+ "type": "number"
+ },
+ {
+ "name": "s3Op",
+ "label": "Step 3: Operator",
+ "type": "choice",
+ "options": ["+", "-", "×", "÷", "^", ""]
+ },
+ {
+ "name": "s3Right",
+ "label": "Step 3: Right operand",
+ "type": "number"
+ },
+ {
+ "name": "s3Result",
+ "label": "Step 3: Result",
+ "type": "number"
+ },
+ {
+ "name": "answer",
+ "label": "Final answer",
+ "type": "number"
+ }
+ ],
+ "examples": [
+ {
+ "name": "Mult before Add",
+ "description": "3 + 4 × 2 - multiplication has higher precedence",
+ "values": {
+ "expr": "3 + 4 × 2",
+ "numSteps": 2,
+ "s1Priority": "MD", "s1Left": 4, "s1Op": "×", "s1Right": 2, "s1Result": 8, "s1Expr": "3 + 8",
+ "s2Priority": "AS", "s2Left": 3, "s2Op": "+", "s2Right": 8, "s2Result": 11, "s2Expr": "11",
+ "s3Priority": "none", "s3Left": 0, "s3Op": "", "s3Right": 0, "s3Result": 0,
+ "answer": 11
+ },
+ "expectedAnswer": "11"
+ },
+ {
+ "name": "Parentheses first",
+ "description": "(3 + 4) × 2 - parentheses override precedence",
+ "values": {
+ "expr": "(3 + 4) × 2",
+ "numSteps": 2,
+ "s1Priority": "P", "s1Left": 3, "s1Op": "+", "s1Right": 4, "s1Result": 7, "s1Expr": "7 × 2",
+ "s2Priority": "MD", "s2Left": 7, "s2Op": "×", "s2Right": 2, "s2Result": 14, "s2Expr": "14",
+ "s3Priority": "none", "s3Left": 0, "s3Op": "", "s3Right": 0, "s3Result": 0,
+ "answer": 14
+ },
+ "expectedAnswer": "14"
+ },
+ {
+ "name": "Exponent first",
+ "description": "2 + 3² - exponents before addition",
+ "values": {
+ "expr": "2 + 3²",
+ "numSteps": 2,
+ "s1Priority": "E", "s1Left": 3, "s1Op": "^", "s1Right": 2, "s1Result": 9, "s1Expr": "2 + 9",
+ "s2Priority": "AS", "s2Left": 2, "s2Op": "+", "s2Right": 9, "s2Result": 11, "s2Expr": "11",
+ "s3Priority": "none", "s3Left": 0, "s3Op": "", "s3Right": 0, "s3Result": 0,
+ "answer": 11
+ },
+ "expectedAnswer": "11"
+ },
+ {
+ "name": "Division before subtraction",
+ "description": "10 - 8 ÷ 2 - division before subtraction",
+ "values": {
+ "expr": "10 - 8 ÷ 2",
+ "numSteps": 2,
+ "s1Priority": "MD", "s1Left": 8, "s1Op": "÷", "s1Right": 2, "s1Result": 4, "s1Expr": "10 - 4",
+ "s2Priority": "AS", "s2Left": 10, "s2Op": "-", "s2Right": 4, "s2Result": 6, "s2Expr": "6",
+ "s3Priority": "none", "s3Left": 0, "s3Op": "", "s3Right": 0, "s3Result": 0,
+ "answer": 6
+ },
+ "expectedAnswer": "6"
+ },
+ {
+ "name": "Left to right (same precedence)",
+ "description": "12 - 4 + 2 - same precedence, go left to right",
+ "values": {
+ "expr": "12 - 4 + 2",
+ "numSteps": 2,
+ "s1Priority": "AS", "s1Left": 12, "s1Op": "-", "s1Right": 4, "s1Result": 8, "s1Expr": "8 + 2",
+ "s2Priority": "AS", "s2Left": 8, "s2Op": "+", "s2Right": 2, "s2Result": 10, "s2Expr": "10",
+ "s3Priority": "none", "s3Left": 0, "s3Op": "", "s3Right": 0, "s3Result": 0,
+ "answer": 10
+ },
+ "expectedAnswer": "10"
+ },
+ {
+ "name": "Three operations",
+ "description": "2 + 3 × 4 - 5 - mult first, then left to right",
+ "values": {
+ "expr": "2 + 3 × 4 - 5",
+ "numSteps": 3,
+ "s1Priority": "MD", "s1Left": 3, "s1Op": "×", "s1Right": 4, "s1Result": 12, "s1Expr": "2 + 12 - 5",
+ "s2Priority": "AS", "s2Left": 2, "s2Op": "+", "s2Right": 12, "s2Result": 14, "s2Expr": "14 - 5",
+ "s3Priority": "AS", "s3Left": 14, "s3Op": "-", "s3Right": 5, "s3Result": 9,
+ "answer": 9
+ },
+ "expectedAnswer": "9"
+ },
+ {
+ "name": "Exponent with multiply",
+ "description": "2² × 3 - exponent first, then multiply",
+ "values": {
+ "expr": "2² × 3",
+ "numSteps": 2,
+ "s1Priority": "E", "s1Left": 2, "s1Op": "^", "s1Right": 2, "s1Result": 4, "s1Expr": "4 × 3",
+ "s2Priority": "MD", "s2Left": 4, "s2Op": "×", "s2Right": 3, "s2Result": 12, "s2Expr": "12",
+ "s3Priority": "none", "s3Left": 0, "s3Op": "", "s3Right": 0, "s3Result": 0,
+ "answer": 12
+ },
+ "expectedAnswer": "12"
+ },
+ {
+ "name": "Nested priorities",
+ "description": "(2 + 3)² - parentheses, then exponent",
+ "values": {
+ "expr": "(2 + 3)²",
+ "numSteps": 2,
+ "s1Priority": "P", "s1Left": 2, "s1Op": "+", "s1Right": 3, "s1Result": 5, "s1Expr": "5²",
+ "s2Priority": "E", "s2Left": 5, "s2Op": "^", "s2Right": 2, "s2Result": 25, "s2Expr": "25",
+ "s3Priority": "none", "s3Left": 0, "s3Op": "", "s3Right": 0, "s3Result": 0,
+ "answer": 25
+ },
+ "expectedAnswer": "25"
+ }
+ ]
+ },
+ "generation": {
+ "useExamplesOnly": true,
+ "preferred": {
+ "expr": ["3 + 4 × 2", "(3 + 4) × 2", "2 + 3²", "10 - 8 ÷ 2", "12 - 4 + 2"],
+ "numSteps": [2],
+ "s1Priority": ["P", "E", "MD", "AS"],
+ "s1Left": [2, 3, 4, 5, 6, 7, 8, 10, 12],
+ "s1Op": ["+", "-", "×", "÷", "^"],
+ "s1Right": [2, 3, 4, 5],
+ "s1Result": [4, 6, 7, 8, 9, 10, 11, 12, 14, 25],
+ "s1Expr": ["3 + 8", "7 × 2", "2 + 9", "10 - 4", "8 + 2"],
+ "s2Priority": ["P", "E", "MD", "AS", "none"],
+ "s2Left": [2, 3, 7, 8, 10, 14],
+ "s2Op": ["+", "-", "×", "÷", "^", ""],
+ "s2Right": [2, 3, 4, 5, 8, 9],
+ "s2Result": [6, 10, 11, 12, 14, 25],
+ "s2Expr": ["6", "10", "11", "12", "14", "25"],
+ "s3Priority": ["P", "E", "MD", "AS", "none"],
+ "s3Left": [0, 14],
+ "s3Op": ["+", "-", "×", "÷", "^", ""],
+ "s3Right": [0, 5],
+ "s3Result": [0, 9],
+ "answer": [6, 9, 10, 11, 12, 14, 25]
+ }
+ },
+ "entryNode": "START",
+ "nodes": {
+ "START": {
+ "type": "instruction",
+ "advance": "tap",
+ "next": "EXPLAIN_PEMDAS",
+ "transform": [
+ { "key": "currentExpr", "expr": "expr" },
+ { "key": "currentStep", "expr": "1" }
+ ]
+ },
+ "EXPLAIN_PEMDAS": {
+ "type": "instruction",
+ "advance": "tap",
+ "next": "IDENTIFY_STEP1"
+ },
+ "IDENTIFY_STEP1": {
+ "type": "decision",
+ "correctAnswer": "s1Priority",
+ "options": [
+ { "value": "P", "label": "Parentheses (P)", "next": "DO_STEP1", "pathLabel": "P" },
+ { "value": "E", "label": "Exponents (E)", "next": "DO_STEP1", "pathLabel": "E" },
+ { "value": "MD", "label": "Multiply/Divide (MD)", "next": "DO_STEP1", "pathLabel": "MD" },
+ { "value": "AS", "label": "Add/Subtract (AS)", "next": "DO_STEP1", "pathLabel": "AS" }
+ ]
+ },
+ "DO_STEP1": {
+ "type": "instruction",
+ "advance": "tap",
+ "next": "CHECK_STEP1",
+ "transform": [
+ { "key": "opDisplay", "expr": "s1Left + ' ' + s1Op + ' ' + s1Right" }
+ ]
+ },
+ "CHECK_STEP1": {
+ "type": "checkpoint",
+ "prompt": "Calculate the result",
+ "inputType": "number",
+ "expected": "s1Result",
+ "next": "AFTER_STEP1",
+ "transform": [
+ { "key": "currentExpr", "expr": "s1Expr" },
+ { "key": "currentStep", "expr": "2" }
+ ]
+ },
+ "AFTER_STEP1": {
+ "type": "instruction",
+ "advance": "tap",
+ "next": "CHECK_MORE_STEPS1",
+ "transform": [
+ { "key": "showExpr", "expr": "s1Expr" }
+ ]
+ },
+ "CHECK_MORE_STEPS1": {
+ "type": "embellishment",
+ "next": "IDENTIFY_STEP2",
+ "skipIf": "numSteps < 2",
+ "skipTo": "DONE"
+ },
+ "IDENTIFY_STEP2": {
+ "type": "decision",
+ "correctAnswer": "s2Priority",
+ "options": [
+ { "value": "P", "label": "Parentheses (P)", "next": "DO_STEP2", "pathLabel": "P" },
+ { "value": "E", "label": "Exponents (E)", "next": "DO_STEP2", "pathLabel": "E" },
+ { "value": "MD", "label": "Multiply/Divide (MD)", "next": "DO_STEP2", "pathLabel": "MD" },
+ { "value": "AS", "label": "Add/Subtract (AS)", "next": "DO_STEP2", "pathLabel": "AS" }
+ ]
+ },
+ "DO_STEP2": {
+ "type": "instruction",
+ "advance": "tap",
+ "next": "CHECK_STEP2",
+ "transform": [
+ { "key": "opDisplay", "expr": "s2Left + ' ' + s2Op + ' ' + s2Right" }
+ ]
+ },
+ "CHECK_STEP2": {
+ "type": "checkpoint",
+ "prompt": "Calculate the result",
+ "inputType": "number",
+ "expected": "s2Result",
+ "next": "AFTER_STEP2",
+ "transform": [
+ { "key": "currentExpr", "expr": "s2Expr" },
+ { "key": "currentStep", "expr": "3" }
+ ]
+ },
+ "AFTER_STEP2": {
+ "type": "instruction",
+ "advance": "tap",
+ "next": "CHECK_MORE_STEPS2",
+ "transform": [
+ { "key": "showExpr", "expr": "s2Expr" }
+ ]
+ },
+ "CHECK_MORE_STEPS2": {
+ "type": "embellishment",
+ "next": "IDENTIFY_STEP3",
+ "skipIf": "numSteps < 3",
+ "skipTo": "DONE"
+ },
+ "IDENTIFY_STEP3": {
+ "type": "decision",
+ "correctAnswer": "s3Priority",
+ "options": [
+ { "value": "P", "label": "Parentheses (P)", "next": "DO_STEP3", "pathLabel": "P" },
+ { "value": "E", "label": "Exponents (E)", "next": "DO_STEP3", "pathLabel": "E" },
+ { "value": "MD", "label": "Multiply/Divide (MD)", "next": "DO_STEP3", "pathLabel": "MD" },
+ { "value": "AS", "label": "Add/Subtract (AS)", "next": "DO_STEP3", "pathLabel": "AS" }
+ ]
+ },
+ "DO_STEP3": {
+ "type": "instruction",
+ "advance": "tap",
+ "next": "CHECK_STEP3",
+ "transform": [
+ { "key": "opDisplay", "expr": "s3Left + ' ' + s3Op + ' ' + s3Right" }
+ ]
+ },
+ "CHECK_STEP3": {
+ "type": "checkpoint",
+ "prompt": "Calculate the result",
+ "inputType": "number",
+ "expected": "s3Result",
+ "next": "DONE",
+ "transform": [
+ { "key": "currentExpr", "expr": "s3Result" },
+ { "key": "currentStep", "expr": "'done'" }
+ ]
+ },
+ "DONE": {
+ "type": "terminal",
+ "celebration": true
+ }
+ },
+ "answer": {
+ "values": {
+ "result": "answer"
+ },
+ "display": {
+ "text": "{{answer}}",
+ "web": ""
+ }
+ },
+ "tests": [
+ {
+ "name": "3 + 4 × 2 = 11",
+ "values": {
+ "expr": "3 + 4 × 2",
+ "numSteps": 2,
+ "s1Priority": "MD", "s1Left": 4, "s1Op": "×", "s1Right": 2, "s1Result": 8, "s1Expr": "3 + 8",
+ "s2Priority": "AS", "s2Left": 3, "s2Op": "+", "s2Right": 8, "s2Result": 11, "s2Expr": "11",
+ "s3Priority": "none", "s3Left": 0, "s3Op": "", "s3Right": 0, "s3Result": 0,
+ "answer": 11
+ },
+ "expected": { "result": 11 }
+ },
+ {
+ "name": "(3 + 4) × 2 = 14",
+ "values": {
+ "expr": "(3 + 4) × 2",
+ "numSteps": 2,
+ "s1Priority": "P", "s1Left": 3, "s1Op": "+", "s1Right": 4, "s1Result": 7, "s1Expr": "7 × 2",
+ "s2Priority": "MD", "s2Left": 7, "s2Op": "×", "s2Right": 2, "s2Result": 14, "s2Expr": "14",
+ "s3Priority": "none", "s3Left": 0, "s3Op": "", "s3Right": 0, "s3Result": 0,
+ "answer": 14
+ },
+ "expected": { "result": 14 }
+ },
+ {
+ "name": "2 + 3² = 11",
+ "values": {
+ "expr": "2 + 3²",
+ "numSteps": 2,
+ "s1Priority": "E", "s1Left": 3, "s1Op": "^", "s1Right": 2, "s1Result": 9, "s1Expr": "2 + 9",
+ "s2Priority": "AS", "s2Left": 2, "s2Op": "+", "s2Right": 9, "s2Result": 11, "s2Expr": "11",
+ "s3Priority": "none", "s3Left": 0, "s3Op": "", "s3Right": 0, "s3Result": 0,
+ "answer": 11
+ },
+ "expected": { "result": 11 }
+ },
+ {
+ "name": "12 - 4 + 2 = 10 (left to right)",
+ "values": {
+ "expr": "12 - 4 + 2",
+ "numSteps": 2,
+ "s1Priority": "AS", "s1Left": 12, "s1Op": "-", "s1Right": 4, "s1Result": 8, "s1Expr": "8 + 2",
+ "s2Priority": "AS", "s2Left": 8, "s2Op": "+", "s2Right": 2, "s2Result": 10, "s2Expr": "10",
+ "s3Priority": "none", "s3Left": 0, "s3Op": "", "s3Right": 0, "s3Result": 0,
+ "answer": 10
+ },
+ "expected": { "result": 10 }
+ },
+ {
+ "name": "2 + 3 × 4 - 5 = 9",
+ "values": {
+ "expr": "2 + 3 × 4 - 5",
+ "numSteps": 3,
+ "s1Priority": "MD", "s1Left": 3, "s1Op": "×", "s1Right": 4, "s1Result": 12, "s1Expr": "2 + 12 - 5",
+ "s2Priority": "AS", "s2Left": 2, "s2Op": "+", "s2Right": 12, "s2Result": 14, "s2Expr": "14 - 5",
+ "s3Priority": "AS", "s3Left": 14, "s3Op": "-", "s3Right": 5, "s3Result": 9,
+ "answer": 9
+ },
+ "expected": { "result": 9 }
+ }
+ ]
+}
diff --git a/apps/web/src/lib/flowcharts/definitions/sentence-type.flow.json b/apps/web/src/lib/flowcharts/definitions/sentence-type.flow.json
new file mode 100644
index 00000000..a31e1034
--- /dev/null
+++ b/apps/web/src/lib/flowcharts/definitions/sentence-type.flow.json
@@ -0,0 +1,205 @@
+{
+ "id": "sentence-type",
+ "title": "Identify Sentence Types",
+ "mermaidFile": "embedded",
+ "problemInput": {
+ "schema": "sentence-classification",
+ "fields": [
+ {
+ "name": "sentence",
+ "label": "Sentence",
+ "type": "choice",
+ "options": [
+ "The cat sat on the mat.",
+ "Where is my book?",
+ "Close the door!",
+ "What a beautiful day!",
+ "Please pass the salt.",
+ "Are you coming to the party?"
+ ]
+ },
+ {
+ "name": "punctuation",
+ "label": "Ending punctuation",
+ "type": "choice",
+ "options": [".", "?", "!"]
+ },
+ {
+ "name": "intent",
+ "label": "Intent (for ! or . sentences)",
+ "type": "choice",
+ "options": ["statement", "command", "feeling", "request"]
+ }
+ ],
+ "examples": [
+ {
+ "name": "Statement (period)",
+ "description": "Simple declarative sentence",
+ "values": { "sentence": "The cat sat on the mat.", "punctuation": ".", "intent": "statement" },
+ "expectedAnswer": "declarative"
+ },
+ {
+ "name": "Question",
+ "description": "Interrogative sentence",
+ "values": { "sentence": "Where is my book?", "punctuation": "?", "intent": "statement" },
+ "expectedAnswer": "interrogative"
+ },
+ {
+ "name": "Command",
+ "description": "Imperative sentence with exclamation",
+ "values": { "sentence": "Close the door!", "punctuation": "!", "intent": "command" },
+ "expectedAnswer": "imperative"
+ },
+ {
+ "name": "Strong feeling",
+ "description": "Exclamatory sentence",
+ "values": { "sentence": "What a beautiful day!", "punctuation": "!", "intent": "feeling" },
+ "expectedAnswer": "exclamatory"
+ },
+ {
+ "name": "Polite request",
+ "description": "Imperative with period",
+ "values": { "sentence": "Please pass the salt.", "punctuation": ".", "intent": "request" },
+ "expectedAnswer": "imperative"
+ },
+ {
+ "name": "Yes/No question",
+ "description": "Another interrogative",
+ "values": { "sentence": "Are you coming to the party?", "punctuation": "?", "intent": "statement" },
+ "expectedAnswer": "interrogative"
+ }
+ ]
+ },
+ "entryNode": "START",
+ "nodes": {
+ "START": {
+ "type": "instruction",
+ "advance": "tap",
+ "next": "CHECK_END",
+ "transform": [
+ { "key": "sentenceText", "expr": "sentence" }
+ ]
+ },
+ "CHECK_END": {
+ "type": "decision",
+ "correctAnswer": "punctuation == '?' ? 'question' : (punctuation == '!' ? 'exclaim' : 'period')",
+ "options": [
+ { "value": "question", "label": "? Question mark", "next": "IS_QUESTION", "pathLabel": "?" },
+ { "value": "exclaim", "label": "! Exclamation mark", "next": "CHECK_COMMAND", "pathLabel": "!" },
+ { "value": "period", "label": ". Period", "next": "CHECK_STATEMENT", "pathLabel": "." }
+ ]
+ },
+ "IS_QUESTION": {
+ "type": "instruction",
+ "advance": "tap",
+ "next": "RESULT_INTERROGATIVE",
+ "transform": [
+ { "key": "sentenceType", "expr": "'interrogative'" },
+ { "key": "explanation", "expr": "'Questions ask for information and end with ?'" }
+ ]
+ },
+ "RESULT_INTERROGATIVE": {
+ "type": "embellishment",
+ "next": "DONE",
+ "transform": [
+ { "key": "finalType", "expr": "'interrogative'" }
+ ]
+ },
+ "CHECK_COMMAND": {
+ "type": "decision",
+ "correctAnswer": "intent == 'command' ? 'command' : 'feeling'",
+ "options": [
+ { "value": "command", "label": "Giving an order or request", "next": "RESULT_IMPERATIVE_EXCLAIM", "pathLabel": "command" },
+ { "value": "feeling", "label": "Expressing strong feeling", "next": "RESULT_EXCLAMATORY", "pathLabel": "feeling" }
+ ]
+ },
+ "RESULT_IMPERATIVE_EXCLAIM": {
+ "type": "instruction",
+ "advance": "tap",
+ "next": "DONE",
+ "transform": [
+ { "key": "sentenceType", "expr": "'imperative'" },
+ { "key": "explanation", "expr": "'Commands tell someone to do something'" },
+ { "key": "finalType", "expr": "'imperative'" }
+ ]
+ },
+ "RESULT_EXCLAMATORY": {
+ "type": "instruction",
+ "advance": "tap",
+ "next": "DONE",
+ "transform": [
+ { "key": "sentenceType", "expr": "'exclamatory'" },
+ { "key": "explanation", "expr": "'Exclamations express strong emotions'" },
+ { "key": "finalType", "expr": "'exclamatory'" }
+ ]
+ },
+ "CHECK_STATEMENT": {
+ "type": "decision",
+ "correctAnswer": "intent == 'statement' ? 'statement' : 'request'",
+ "options": [
+ { "value": "statement", "label": "Making a statement", "next": "RESULT_DECLARATIVE", "pathLabel": "statement" },
+ { "value": "request", "label": "Polite request or command", "next": "RESULT_IMPERATIVE_POLITE", "pathLabel": "request" }
+ ]
+ },
+ "RESULT_DECLARATIVE": {
+ "type": "instruction",
+ "advance": "tap",
+ "next": "DONE",
+ "transform": [
+ { "key": "sentenceType", "expr": "'declarative'" },
+ { "key": "explanation", "expr": "'Declarative sentences state facts or opinions'" },
+ { "key": "finalType", "expr": "'declarative'" }
+ ]
+ },
+ "RESULT_IMPERATIVE_POLITE": {
+ "type": "instruction",
+ "advance": "tap",
+ "next": "DONE",
+ "transform": [
+ { "key": "sentenceType", "expr": "'imperative'" },
+ { "key": "explanation", "expr": "'Polite commands can end with a period'" },
+ { "key": "finalType", "expr": "'imperative'" }
+ ]
+ },
+ "DONE": {
+ "type": "terminal",
+ "celebration": true
+ }
+ },
+ "answer": {
+ "values": {
+ "type": "finalType"
+ },
+ "display": {
+ "text": "{{finalType}}",
+ "web": "{{finalType}}"
+ }
+ },
+ "tests": [
+ {
+ "name": "Declarative sentence",
+ "values": { "sentence": "The cat sat on the mat.", "punctuation": ".", "intent": "statement" },
+ "expected": { "type": "declarative" }
+ },
+ {
+ "name": "Interrogative sentence",
+ "values": { "sentence": "Where is my book?", "punctuation": "?", "intent": "statement" },
+ "expected": { "type": "interrogative" }
+ },
+ {
+ "name": "Imperative with exclamation",
+ "values": { "sentence": "Close the door!", "punctuation": "!", "intent": "command" },
+ "expected": { "type": "imperative" }
+ },
+ {
+ "name": "Exclamatory sentence",
+ "values": { "sentence": "What a beautiful day!", "punctuation": "!", "intent": "feeling" },
+ "expected": { "type": "exclamatory" }
+ },
+ {
+ "name": "Polite request",
+ "values": { "sentence": "Please pass the salt.", "punctuation": ".", "intent": "request" },
+ "expected": { "type": "imperative" }
+ }
+ ]
+}
diff --git a/apps/web/src/utils/tutorialConverter.ts b/apps/web/src/utils/tutorialConverter.ts
index 84bc3fa9..60b4da1f 100644
--- a/apps/web/src/utils/tutorialConverter.ts
+++ b/apps/web/src/utils/tutorialConverter.ts
@@ -1,6 +1,7 @@
// Utility to extract and convert the existing GuidedAdditionTutorial data
import type { Tutorial } from '../types/tutorial'
+export type { Tutorial }
import { generateAbacusInstructions } from './abacusInstructionGenerator'
// Import the existing tutorial step interface to match the current structure