Spaces:
Sleeping
Sleeping
| import React, { useState, useEffect } from 'react'; | |
| import { | |
| Box, | |
| Typography, | |
| Radio, | |
| RadioGroup, | |
| FormControlLabel, | |
| TextField, | |
| Button, | |
| Alert, | |
| Collapse, | |
| Stack, | |
| Paper, | |
| Divider | |
| } from '@mui/material'; | |
| import LightbulbIcon from '@mui/icons-material/Lightbulb'; | |
| import NavigateNextIcon from '@mui/icons-material/NavigateNext'; | |
| import axios from 'axios'; | |
| function Exercise({ exercise, onSubmit, onNextExercise, selectedAnswer: externalSelectedAnswer, onAnswerSelect, result: externalResult }) { | |
| const [localSelectedAnswer, setLocalSelectedAnswer] = useState(''); | |
| const [showHints, setShowHints] = useState(false); | |
| const [currentHintIndex, setCurrentHintIndex] = useState(0); | |
| const [localResult, setLocalResult] = useState(null); | |
| // 使用外部傳入的 selectedAnswer 和 result,如果有的話 | |
| const selectedAnswer = externalSelectedAnswer || localSelectedAnswer; | |
| const result = externalResult || localResult; | |
| // 當外部 selectedAnswer 變化時更新本地狀態 | |
| useEffect(() => { | |
| if (externalSelectedAnswer) { | |
| setLocalSelectedAnswer(externalSelectedAnswer); | |
| } | |
| }, [externalSelectedAnswer]); | |
| // 當外部 result 變化時更新本地狀態 | |
| useEffect(() => { | |
| if (externalResult) { | |
| setLocalResult(externalResult); | |
| } | |
| }, [externalResult]); | |
| const handleOptionClick = (value) => { | |
| setLocalSelectedAnswer(value); | |
| // 找到選中選項的文本 | |
| if (exercise.type === 'multiple_choice' && exercise.options) { | |
| const optionIndex = parseInt(value, 10); | |
| const optionText = exercise.options[optionIndex]; | |
| // 通知父組件選擇變化 | |
| if (onAnswerSelect) { | |
| onAnswerSelect(value, optionText); | |
| } | |
| } else { | |
| // 對於文本輸入,直接傳遞值 | |
| if (onAnswerSelect) { | |
| onAnswerSelect(value, value); | |
| } | |
| } | |
| }; | |
| const handleSubmit = async () => { | |
| try { | |
| if (!selectedAnswer.trim()) { | |
| return; | |
| } | |
| // 找到選中選項的文本 | |
| let answerText = selectedAnswer; | |
| if (exercise.type === 'multiple_choice' && exercise.options) { | |
| const optionIndex = parseInt(selectedAnswer, 10); | |
| answerText = exercise.options[optionIndex]; | |
| } | |
| // 通知父組件 | |
| if (onSubmit) { | |
| await onSubmit(selectedAnswer, answerText); | |
| } else { | |
| // 如果沒有提供 onSubmit,則使用本地邏輯 | |
| const response = await axios.post(`/api/exercises/check/${exercise.id}`, { | |
| answer: selectedAnswer | |
| }); | |
| setLocalResult(response.data); | |
| } | |
| } catch (error) { | |
| console.error('Error submitting answer:', error); | |
| setLocalResult({ correct: false, explanation: "檢查答案時發生錯誤" }); | |
| } | |
| }; | |
| const handleNextExercise = () => { | |
| setLocalSelectedAnswer(''); | |
| setShowHints(false); | |
| setCurrentHintIndex(0); | |
| setLocalResult(null); | |
| if (onNextExercise) { | |
| onNextExercise(); | |
| } | |
| }; | |
| const showNextHint = () => { | |
| if (currentHintIndex < exercise.hints.length - 1) { | |
| setCurrentHintIndex(prev => prev + 1); | |
| } | |
| setShowHints(true); | |
| }; | |
| return ( | |
| <Box sx={{ | |
| mb: 4, | |
| display: 'flex', | |
| gap: 3, | |
| minHeight: 200 | |
| }}> | |
| {/* 左側:題目和作答區域 */} | |
| <Paper sx={{ | |
| flex: 2, | |
| p: 2, | |
| display: 'flex', | |
| flexDirection: 'column', | |
| backgroundColor: '#f8f9fa' | |
| }}> | |
| <Typography variant="h6" sx={{ mb: 2, color: 'text.primary' }}> | |
| 題目 | |
| </Typography> | |
| <Typography variant="body1" sx={{ | |
| mb: 3, | |
| fontWeight: 500, | |
| whiteSpace: 'pre-wrap' | |
| }}> | |
| {exercise.question} | |
| </Typography> | |
| {/* 作答區域整合到題目下方 */} | |
| <Box sx={{ mb: 2 }}> | |
| {exercise.type === 'multiple_choice' ? ( | |
| <RadioGroup | |
| value={selectedAnswer} | |
| onChange={(e) => { | |
| // 只有在還沒有結果或答錯的情況下才允許更改答案 | |
| if (!result?.correct) { | |
| handleOptionClick(e.target.value); | |
| } | |
| }} | |
| > | |
| {exercise.options.map((option, index) => { | |
| const isSelected = selectedAnswer === index.toString(); | |
| const isWrongAnswer = result && !result.correct && isSelected; | |
| return ( | |
| <FormControlLabel | |
| key={index} | |
| value={index.toString()} | |
| control={<Radio />} | |
| label={`${String.fromCharCode(65 + index)}. ${option}`} | |
| sx={{ | |
| mb: 1, | |
| color: isWrongAnswer ? 'error.main' : 'text.primary', | |
| '&.Mui-disabled': { | |
| color: result?.correct ? 'success.main' : 'text.disabled' | |
| } | |
| }} | |
| disabled={result?.correct} | |
| /> | |
| ); | |
| })} | |
| </RadioGroup> | |
| ) : ( | |
| <TextField | |
| fullWidth | |
| value={selectedAnswer} | |
| onChange={(e) => { | |
| // 只有在還沒有結果或答錯的情況下才允許更改答案 | |
| if (!result?.correct) { | |
| handleOptionClick(e.target.value); | |
| } | |
| }} | |
| placeholder="請輸入答案" | |
| variant="outlined" | |
| size="small" | |
| disabled={result?.correct} | |
| error={result && !result.correct} | |
| helperText={result && !result.correct ? "答案不正確" : ""} | |
| /> | |
| )} | |
| </Box> | |
| </Paper> | |
| {/* 右側:操作和反饋區域 */} | |
| <Paper sx={{ | |
| flex: 1, | |
| p: 2, | |
| display: 'flex', | |
| flexDirection: 'column' | |
| }}> | |
| <Typography variant="h6" sx={{ mb: 2, color: 'text.primary' }}> | |
| 操作區 | |
| </Typography> | |
| {/* 按鈕組 */} | |
| <Stack spacing={2} sx={{ mb: 2 }}> | |
| <Button | |
| variant="contained" | |
| onClick={handleSubmit} | |
| disabled={!selectedAnswer.trim() || result?.correct} | |
| fullWidth | |
| > | |
| 提交答案 | |
| </Button> | |
| <Button | |
| variant="outlined" | |
| startIcon={<LightbulbIcon />} | |
| onClick={showNextHint} | |
| disabled={showHints && currentHintIndex >= exercise.hints.length - 1} | |
| fullWidth | |
| > | |
| {showHints ? '下一個提示' : '顯示提示'} | |
| </Button> | |
| {onNextExercise && ( | |
| <Button | |
| variant="contained" | |
| color="success" | |
| endIcon={<NavigateNextIcon />} | |
| onClick={handleNextExercise} | |
| disabled={!result?.correct} | |
| fullWidth | |
| > | |
| 下一題 | |
| </Button> | |
| )} | |
| </Stack> | |
| {/* 提示和結果區域 */} | |
| <Box sx={{ mt: 'auto' }}> | |
| <Collapse in={showHints}> | |
| <Box sx={{ mb: 2 }}> | |
| {exercise.hints.slice(0, currentHintIndex + 1).map((hint, index) => ( | |
| <Alert key={index} severity="info" sx={{ mb: 1 }}> | |
| 提示 {index + 1}: {hint} | |
| </Alert> | |
| ))} | |
| </Box> | |
| </Collapse> | |
| {result && ( | |
| <Alert | |
| severity={result.correct ? "success" : "error"} | |
| sx={{ | |
| '& .MuiAlert-message': { width: '100%' } | |
| }} | |
| > | |
| <Box sx={{ | |
| display: 'flex', | |
| flexDirection: 'column', | |
| gap: 1 | |
| }}> | |
| <Typography> | |
| {result.correct ? '答對了!' : '再試一次!'} | |
| </Typography> | |
| {result.explanation && ( | |
| <Typography | |
| sx={{ | |
| mt: 1, | |
| p: 1, | |
| backgroundColor: 'rgba(0, 0, 0, 0.04)', | |
| borderRadius: 1 | |
| }} | |
| > | |
| 解釋:{result.explanation} | |
| </Typography> | |
| )} | |
| </Box> | |
| </Alert> | |
| )} | |
| </Box> | |
| </Paper> | |
| </Box> | |
| ); | |
| } | |
| export default Exercise; |