当前位置: 首页 > ai >正文

使用 XState 状态机打造英语单词学习界面(demo)

单词学习界面

在现代 Web 应用开发中,管理复杂的用户交互和状态流转是一项极具挑战性的任务。而 XState 作为一个强大的状态机库,为我们提供了一种清晰、可维护的方式来处理这些复杂场景。本文将介绍如何使用 XState 状态机来构建一个英语单词学习界面,通过 studyMachine.ts 状态机文件来驱动 StudyFlow.tsx 组件的交互。

项目概述

我们的项目是一个英语单词学习应用,用户可以学习一系列单词,包括单词的含义、词组和例句。应用通过状态机来管理学习流程,根据用户的输入和操作,在不同的学习阶段之间进行切换。

状态机文件:studyMachine.ts

状态机是整个应用的核心,它定义了学习流程的各个状态和状态之间的转换规则。在 studyMachine.ts 文件中,我们使用 createMachine 函数创建了一个名为 study 的状态机。

状态机结构

状态机包含以下几个主要状态:

  • idle:初始状态,等待用户开始学习。
  • showWordCard:显示单词卡片,用户可以输入单词的含义。
  • chooseMeaning:提供单词含义的选项,用户选择正确答案。
  • showClueMeaning:如果用户回答错误,显示提示信息。
  • choosePhrase:提供词组含义的选项,用户选择正确答案。
  • showCluePhrase:如果用户回答错误,显示提示信息。
  • nextWord:学习下一个单词。
  • sentenceReview:进行例句复习,用户选择正确答案。
  • sentenceReviewNext:判断是否继续复习下一个例句。
  • completed:学习完成状态。

状态转换

状态机通过事件来触发状态转换,例如 START 事件从 idle 状态转换到 showWordCard 状态,ANSWER 事件根据用户的回答在不同的选择状态之间进行转换。

上下文管理

状态机的上下文(context)用于存储学习过程中的相关信息,如当前单词索引、题目索引、聊天记录等。通过 assign 函数,我们可以在状态转换时更新上下文信息。

import { createMachine, assign } from 'xstate';
import { WORDS } from '../words';export type ChatItem = {type: 'bot' | 'user' | 'feedback' | 'clue';content: any;correct?: boolean;color?: string;
};interface Word {tip: string;
}interface Question {question: string;options: string[];
}interface StudyContext {wordIndex: number;        // 当前单词索引 0-4questionInWord: number;   // 当前单词内题目索引 0-3phase: 'learn' | 'review';completed: boolean;currentWord?: Word;currentQuestion?: Question;chat: ChatItem[];input: string;inputDisabled: boolean;meaningWrongCount: number; // 第二题错误次数phraseWrong: boolean;      // 第三题是否答错过sentenceIndex?: number;    // 当前sentence复习索引
}export const studyMachine = createMachine<StudyContext>({id: 'study',initial: 'idle',context: {wordIndex: 0,questionInWord: 0,phase: 'learn',completed: false,chat: [],input: '',inputDisabled: false,meaningWrongCount: 0,phraseWrong: false,sentenceIndex: 0,},states: {idle: {entry: assign({chat: (_) => [],input: (_) => '',inputDisabled: (_) => false,meaningWrongCount: (_) => 0,phraseWrong: (_) => false}),on: { START: 'showWordCard' }},showWordCard: {entry: assign({chat: (ctx) => [...ctx.chat,{ type: 'bot', content: {type: 'wordCard',word: WORDS[ctx.wordIndex].word,phonetic: WORDS[ctx.wordIndex].phonetic,cet4Count: WORDS[ctx.wordIndex].cet4Count,cet6Count: WORDS[ctx.wordIndex].cet6Count,audio: WORDS[ctx.wordIndex].audio} }],input: (_) => '',inputDisabled: (_) => false,meaningWrongCount: (_) => 0,phraseWrong: (_) => false}),on: {INPUT: {actions: assign({ input: (ctx, e) => e.input })},ANSWER: {actions: assign({chat: (ctx, e) => [...ctx.chat,{ type: 'user', content: ctx.input || '我不知道这个单词意思' }],input: (_) => '',inputDisabled: (_) => true}),target: 'chooseMeaning'},IDK: {actions: assign({chat: (ctx) => [...ctx.chat,{ type: 'user', content: '我不知道这个单词意思' },{ type: 'clue', content: WORDS[ctx.wordIndex].clues[0] },{ type: 'clue', content: WORDS[ctx.wordIndex].clues[1] }],input: (_) => '',inputDisabled: (_) => true}),target: 'chooseMeaning'}}},chooseMeaning: {entry: assign({chat: (ctx) => [...ctx.chat,{type: 'bot',content: {type: 'wordOptions',word: WORDS[ctx.wordIndex].word,phonetic: WORDS[ctx.wordIndex].phonetic,audio: WORDS[ctx.wordIndex].audio,options: WORDS[ctx.wordIndex].options}}]}),on: {ANSWER: [{cond: (ctx, e) => e.answer === WORDS[ctx.wordIndex].meaning,actions: assign({chat: (ctx, e) => [...ctx.chat,{ type: 'user', content: e.answer }]}),target: 'choosePhrase'},{actions: assign({chat: (ctx, e) => [...ctx.chat,{ type: 'user', content: e.answer }],meaningWrongCount: (ctx) => ctx.meaningWrongCount + 1}),target: 'showClueMeaning'}]}},showClueMeaning: {entry: assign({chat: (ctx) => [...ctx.chat,{ type: 'clue', content: WORDS[ctx.wordIndex].clues[Math.min(ctx.meaningWrongCount, 2)] }]}),always: 'chooseMeaning'},choosePhrase: {entry: assign({chat: (ctx) => [...ctx.chat,{type: 'bot',content: {type: 'phraseOptions',phrase: WORDS[ctx.wordIndex].phrase,audio: WORDS[ctx.wordIndex].audio,options: WORDS[ctx.wordIndex].phraseOptions}}]}),on: {ANSWER: [{cond: (ctx, e) => e.answer === WORDS[ctx.wordIndex].phraseMeaning,actions: assign({chat: (ctx, e) => [...ctx.chat,{ type: 'user', content: e.answer }]}),target: 'nextWord'},{actions: assign({chat: (ctx, e) => [...ctx.chat,{ type: 'user', content: e.answer }],phraseWrong: (_) => true}),target: 'showCluePhrase'}]}},showCluePhrase: {entry: assign({chat: (ctx) => [...ctx.chat,{ type: 'clue', content: WORDS[ctx.wordIndex].clues[3] }]}),always: 'choosePhrase'},nextWord: {entry: assign((ctx) => {let wordIndex = ctx.wordIndex + 1;let completed = false;if (wordIndex >= WORDS.length) {completed = true;wordIndex = WORDS.length - 1;}return {wordIndex,completed,chat: [],input: '',inputDisabled: false,meaningWrongCount: 0,phraseWrong: false,sentenceIndex: ctx.sentenceIndex};}),always: [{ target: 'showWordCard', cond: (ctx) => !ctx.completed },{ target: 'sentenceReview' }]},sentenceReview: {entry: assign({chat: (ctx) => [...ctx.chat,{type: 'bot',content: {type: 'sentenceOptions',sentence: WORDS[ctx.sentenceIndex || 0].sentence,options: WORDS[ctx.sentenceIndex || 0].sentenceOptions,audio: WORDS[ctx.sentenceIndex || 0].audio,phonetic: WORDS[ctx.sentenceIndex || 0].phonetic,cet4Count: WORDS[ctx.sentenceIndex || 0].cet4Count,cet6Count: WORDS[ctx.sentenceIndex || 0].cet6Count}}]}),on: {ANSWER: [{cond: (ctx, e) => e.answer === WORDS[ctx.sentenceIndex || 0].sentenceMeaning,actions: assign({chat: (ctx, e) => [...ctx.chat,{ type: 'user', content: e.answer },{ type: 'feedback', content: '回答正确!', correct: true }],sentenceIndex: (ctx) => (ctx.sentenceIndex || 0) + 1}),target: 'sentenceReviewNext'},{actions: assign({chat: (ctx, e) => [...ctx.chat,{ type: 'user', content: e.answer },{ type: 'feedback', content: '回答错误,请再试一次。', correct: false }]})}]}},sentenceReviewNext: {always: [{ target: 'sentenceReview', cond: (ctx) => (ctx.sentenceIndex || 0) < WORDS.length },{ target: 'completed' }]},completed: {type: 'final',entry: assign({chat: (ctx) => [...ctx.chat,{ type: 'bot', content: { type: 'end', text: '恭喜你,学习完成!' } }]})}}
});

学习界面组件:StudyFlow.tsx

StudyFlow.tsx 组件是应用的用户界面,它使用 useInterpret 和 useSelector 钩子来与状态机进行交互。

组件结构

组件包含以下几个主要部分:

  • 顶部进度条:显示学习进度。
  • 聊天气泡区:显示聊天记录,包括单词卡片、用户回答、提示信息和反馈信息。
  • 底部输入框:用户输入答案或选择不知道。

事件处理

组件通过事件处理函数来触发状态机的事件,例如 handleStart 函数触发 START 事件,handleAnswer 函数触发 ANSWER 事件。

import React, { useRef, useEffect } from 'react';
import { useInterpret, useSelector } from '@xstate/react';
import { studyMachine, ChatItem } from '../machines/studyMachine';
import {Box, Card, Typography, Button, Avatar, LinearProgress, Stack, IconButton, TextField, InputAdornment, Fade, Grid
} from '@mui/material';
import SchoolIcon from '@mui/icons-material/School';
import VolumeUpIcon from '@mui/icons-material/VolumeUp';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import PersonIcon from '@mui/icons-material/Person';// 定义气泡样式
const bubbleStyles = {bot: { bgcolor: '#eaf4ff', color: '#222', borderRadius: 3, border: '1px solid #b6d6f6', alignSelf: 'flex-start', maxWidth: 500 },user: { bgcolor: '#1976d2', color: '#fff', borderRadius: 3, border: '1px solid #1976d2', alignSelf: 'flex-end', maxWidth: 400 },feedbackRight: { bgcolor: '#e6ffed', color: '#1a7f37', borderRadius: 3, border: '1px solid #7ee787', alignSelf: 'flex-start', maxWidth: 500 },feedbackWrong: { bgcolor: '#ffeaea', color: '#d32f2f', borderRadius: 3, border: '1px solid #f7bdbd', alignSelf: 'flex-start', maxWidth: 500 },clue: { bgcolor: '#fffbe6', color: '#b08800', borderRadius: 3, border: '1px solid #ffe58f', alignSelf: 'flex-start', maxWidth: 500 },
};// 其他组件定义...const StudyFlow: React.FC = () => {const service = useInterpret(studyMachine);const state = useSelector(service, (s) => s);const chat = state.context.chat;const isCompleted = state.done || state.context.completed;const progress = isCompleted ? 100 : ((state.context.wordIndex + 1) / 5) * 100;const chatEndRef = useRef<HTMLDivElement>(null);useEffect(() => {if (chatEndRef.current) {chatEndRef.current.scrollIntoView({ behavior: 'smooth' });}}, [chat]);// 事件派发const handleStart = () => service.send('START');const handleAnswer = (answer: string) => service.send({ type: 'ANSWER', answer });const handleNext = () => service.send('NEXT');const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => service.send({ type: 'INPUT', input: e.target.value });const handleIDK = () => service.send('IDK');// 判断当前是否可选(选项按钮可用)const canSelect = state.matches('chooseMeaning') || state.matches('choosePhrase') || state.matches('chooseSentence') || state.matches('sentenceReview');// 首页卡片if (state.matches('idle') && chat.length === 0) {return (<Box sx={{ minHeight: '100vh', bgcolor: '#f6f9fe', display: 'flex', alignItems: 'center', justifyContent: 'center' }}><Card sx={{ minWidth: 350, p: 4, borderRadius: 3, boxShadow: 6, textAlign: 'center' }}><Avatar sx={{ bgcolor: '#e3f0ff', width: 64, height: 64, mx: 'auto', mb: 2 }}><SchoolIcon sx={{ color: '#1976d2', fontSize: 40 }} /></Avatar><Typography variant="h5" fontWeight={700} mb={1}>英语词汇学习</Typography><Box sx={{ bgcolor: '#f5f7fa', color: '#1976d2', borderRadius: 2, px: 2, py: 0.5, display: 'inline-block', mb: 3, fontSize: 16 }}>今日新词 5</Box><Buttonvariant="contained"size="large"endIcon={<ArrowForwardIcon />}sx={{ width: '100%', fontSize: 18, py: 1.5 }}onClick={handleStart}>Start</Button></Card></Box>);}return (<Box sx={{ minHeight: '100vh', bgcolor: '#f6f9fe', width: '100vw', maxWidth: '100vw', px: 0 }}>{/* 顶部进度条和标题固定 */}<Box sx={{ position: 'fixed', top: 0, left: 0, right: 0, zIndex: 100, bgcolor: '#f6f9fe', maxWidth: '100vw', width: '100vw' }}><Box sx={{ maxWidth: 1700, mx: 'auto', pt: 3, pb: 1 }}><Stack direction="row" alignItems="center" spacing={1} mb={1}><SchoolIcon color="primary" /><Typography variant="h6" fontWeight={700}>学习进度</Typography><Box flex={1} /><Typography variant="body2" color="text.secondary">{state.context.wordIndex + 1}/5</Typography></Stack><LinearProgress variant="determinate" value={progress} sx={{ height: 8, borderRadius: 4 }} /></Box></Box>{/* 聊天气泡区 */}<Box sx={{ width: '100vw', maxWidth: 1700, pt: 12, px: 0, minHeight: '70vh', pb: 10, boxSizing: 'border-box', display: 'flex', flexDirection: 'column', ml: 5 }}><Box sx={{ width: '100%', maxWidth: '100%', mr: 0 }}>{chat.map((item: ChatItem, idx: number) => {if (item.type === 'bot') return (<Box key={idx} sx={{ display: 'flex', justifyContent: 'flex-start' }}><BotBubble>{renderBotContent(item.content, handleAnswer, handleNext, canSelect)}</BotBubble></Box>);if (item.type === 'user') return (<Box key={idx} sx={{ display: 'flex', justifyContent: 'flex-end'}}><UserBubble>{item.content}</UserBubble></Box>);if (item.type === 'clue') return (<Box key={idx} sx={{ display: 'flex', justifyContent: 'flex-start' }}><ClueBubble>{item.content}</ClueBubble></Box>);if (item.type === 'feedback') return (<Box key={idx} sx={{ display: 'flex', justifyContent: 'flex-start' }}><FeedbackBubble correct={!!item.correct}>{item.content}</FeedbackBubble></Box>);return null;})}<div ref={chatEndRef} /></Box></Box>{/* 底部输入框,始终显示 */}<Box sx={{ position: 'fixed', left: 0, right: 0, bottom: 0, bgcolor: '#fff', borderTop: '1px solid #eee', px: 2, py: 1.5, display: 'flex', alignItems: 'center', zIndex: 10, width: '100vw', maxWidth: '100vw' }}><TextFieldplaceholder="请输入中文含义..."variant="outlined"size="small"sx={{ flex: 1, bgcolor: '#f6f9fe', borderRadius: 2, mr: 2 }}value={state.context.input}onChange={handleInput}onKeyDown={e => {if (e.key === 'Enter' && state.context.input.trim() && !state.context.inputDisabled) {handleAnswer(state.context.input.trim());}}}InputProps={{endAdornment: (<InputAdornment position="end"><IconButton edge="end" color="primary" disabled={!state.context.input.trim() || state.context.inputDisabled} onClick={() => state.context.input.trim() && !state.context.inputDisabled && handleAnswer(state.context.input.trim())}><ArrowForwardIcon /></IconButton></InputAdornment>)}}disabled={state.context.inputDisabled}/>{!state.context.inputDisabled && (<Buttonvariant="outlined"sx={{ ml: 1, px: 3, fontWeight: 700, borderRadius: 2 }}onClick={handleIDK}>我不知道</Button>)}</Box></Box>);
};export default StudyFlow;

使用状态机工具进行调试

XState 提供了一个强大的状态机工具 Stately Studiohttps://stately.ai/docs/machines,我们可以将 studyMachine.ts 文件导入到该工具中,可视化地查看状态机的结构和状态转换过程。通过该工具,我们可以模拟用户操作,验证状态机的正确性,并且可以深入了解每个状态和事件的详细信息。

状态机转换过程:

学习界面: 

总结

通过使用 XState 状态机,我们成功地构建了一个功能丰富、交互流畅的英语单词学习界面。状态机的使用使得学习流程的管理变得清晰、可维护,同时也方便了我们进行调试和扩展。如果你也在处理复杂的用户交互和状态管理问题,不妨尝试使用 XState 来提升你的开发效率和代码质量。

http://www.xdnf.cn/news/12452.html

相关文章:

  • 深入Kubernetes源码阅读指南核心概念- /pkg/api
  • 使用qsort函数对字符串中的星期名称进行排序
  • 30.【新型数据架构】-区块链数据架构
  • Java并发编程实战 Day 13:Fork/Join框架与并行计算
  • 如何解决 远程 合并冲突
  • Docker容器运行一段时间后GPU无法使用报错Failed to initialize NVML: Unknown Error
  • AFNetworking `setSecurityPolicy:` 方法源码解析及最佳实践
  • 以太网原理图设计和PCB设计deepseek
  • 三十三、面向对象底层逻辑-SpringMVC九大组件之HandlerExceptionResolver接口设计
  • 张量的理解
  • Python如何去除图片干扰
  • pp-ocrv5的关键改进PPHGNetV2_B4
  • java 异步
  • 2025-适用于Windows11Version 24H2的05累积更新,适合基于x64的系统(KB5058411) 安装错误-0x800f0831
  • 第四章 信息系统管理-4.1 管理方法
  • 正式上线!在 Sui 主网上使用 Nautilus 构建防篡改预言机
  • MCP是什么
  • STM32实战:数字音频播放器开发指南
  • DFT测试之TAP/SIB/TDR
  • 29.【新型数据架构】-边缘计算数据架构
  • Linux top 命令 的使用总结
  • Leetcode 1645. Hopper 公司查询 II
  • python字符串方法
  • NY118NY120美光固态闪存NY124NY129
  • 掌握子网划分:优化IP分配与管理
  • Java建造者模式(Builder Pattern)详解与实践
  • 【PhysUnits】16.1 完善Var 结构体及其运算(variable.rs)
  • Lrc歌词分析
  • 〈软件安装管家软件目录〉▷Windows系统版
  • JAVA理论-JAVA基础知识