perf(homepage): optimize SSR with deferred processing and dynamic imports

- Defer tutorial processing to after hydration (~100-200ms savings)
- Dynamic import TutorialPlayer, InteractiveFlashcards, LevelSliderDisplay with ssr:false
- Add skeleton placeholders to prevent layout shift
- Parallelize headers/cookies access in i18n/request.ts
- Add missing flowchart definitions (order-of-operations, sentence-type)

Target: Reduce homepage SSR from 300-500ms to ~100-150ms

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2026-01-23 11:40:54 -06:00
parent fc15334aec
commit c845281a60
5 changed files with 790 additions and 55 deletions

View File

@ -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 (
<div
className={css({
width: '100%',
maxWidth: '250px',
height: '300px',
bg: 'bg.muted',
rounded: 'lg',
animation: 'pulse 2s ease-in-out infinite',
})}
/>
)
}
function FlashcardsSkeleton() {
return (
<div
className={css({
width: '100%',
height: '200px',
bg: 'bg.muted',
rounded: 'lg',
animation: 'pulse 2s ease-in-out infinite',
})}
/>
)
}
function LevelSliderSkeleton() {
return (
<div
className={css({
width: '100%',
height: '120px',
bg: 'bg.muted',
rounded: 'lg',
animation: 'pulse 2s ease-in-out infinite',
})}
/>
)
}
// Lazy load heavy components - skip SSR entirely for performance
const TutorialPlayer = dynamic(
() => import('@/components/tutorial/TutorialPlayer').then((m) => m.TutorialPlayer),
{ ssr: false, loading: () => <TutorialSkeleton /> }
)
const InteractiveFlashcards = dynamic(
() => import('@/components/InteractiveFlashcards').then((m) => m.InteractiveFlashcards),
{ ssr: false, loading: () => <FlashcardsSkeleton /> }
)
const LevelSliderDisplay = dynamic(
() => import('@/components/LevelSliderDisplay').then((m) => m.LevelSliderDisplay),
{ ssr: false, loading: () => <LevelSliderSkeleton /> }
)
// 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<Tutorial | null>(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 (
<PageWithNav>
@ -336,17 +405,21 @@ export default function HomePage() {
maxW: '250px',
})}
>
<TutorialPlayer
key={selectedTutorial.id}
tutorial={selectedTutorial}
isDebugMode={false}
showDebugPanel={false}
hideNavigation={true}
hideTooltip={true}
silentErrors={true}
abacusColumns={1}
theme="dark"
/>
{selectedTutorial ? (
<TutorialPlayer
key={selectedTutorial.id}
tutorial={selectedTutorial}
isDebugMode={false}
showDebugPanel={false}
hideNavigation={true}
hideTooltip={true}
silentErrors={true}
abacusColumns={1}
theme="dark"
/>
) : (
<TutorialSkeleton />
)}
</div>
{/* What you'll learn on the right */}

View File

@ -5,8 +5,8 @@ import { getMessages } from './messages'
export async function getRequestLocale(): Promise<Locale> {
// 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

View File

@ -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": "<math><mn>{{answer}}</mn></math>"
}
},
"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 }
}
]
}

View File

@ -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": "<b>{{finalType}}</b>"
}
},
"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" }
}
]
}

View File

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