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:
parent
fc15334aec
commit
c845281a60
|
|
@ -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 */}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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" }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue