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": "{{answer}}" + } + }, + "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