dramaling-vocab-learning/frontend/components/review/core/ReviewRunner.tsx

308 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useState, useCallback } from 'react'
import { useReviewSessionStore } from '@/store/review/useReviewSessionStore'
import { useTestQueueStore } from '@/store/review/useTestQueueStore'
import { useTestResultStore } from '@/store/review/useTestResultStore'
import { useReviewUIStore } from '@/store/review/useReviewUIStore'
import { SmartNavigationController } from './NavigationController'
import { ProgressBar } from '../ui/ProgressBar'
import {
FlipMemoryTest,
VocabChoiceTest,
SentenceFillTest,
SentenceReorderTest,
VocabListeningTest,
SentenceListeningTest,
SentenceSpeakingTest
} from '../review-tests'
interface TestRunnerProps {
className?: string
}
export const ReviewRunner: React.FC<TestRunnerProps> = ({ className }) => {
const { currentCard, error } = useReviewSessionStore()
const { currentMode, testItems, currentTestIndex, markTestCompleted, goToNextTest, skipCurrentTest } = useTestQueueStore()
const { updateScore, recordTestResult, score } = useTestResultStore()
// 答題狀態管理
const [hasAnswered, setHasAnswered] = useState(false)
const [isProcessingAnswer, setIsProcessingAnswer] = useState(false)
const {
openReportModal,
openImageModal
} = useReviewUIStore()
// 重置答題狀態(切換測驗時)
useEffect(() => {
setHasAnswered(false)
setIsProcessingAnswer(false)
}, [currentTestIndex, currentMode])
// 處理答題(只記錄答案,不進行導航)
const handleAnswer = useCallback(async (answer: string, confidenceLevel?: number) => {
if (!currentCard || hasAnswered || isProcessingAnswer) return
setIsProcessingAnswer(true)
try {
// 檢查答案正確性
const isCorrect = checkAnswer(answer, currentCard, currentMode)
// 更新分數
updateScore(isCorrect)
// 記錄到後端
const success = await recordTestResult({
flashcardId: currentCard.id,
testType: currentMode,
isCorrect,
userAnswer: answer,
confidenceLevel,
responseTimeMs: 2000
})
if (success) {
// 標記測驗為完成
markTestCompleted(currentTestIndex)
// 設定已答題狀態(啟用導航按鈕)
setHasAnswered(true)
// 如果答錯,將題目移到隊列最後(優先級 20
if (!isCorrect && currentMode !== 'flip-memory') {
// TODO: 實現答錯重排邏輯
console.log('答錯,將重新排入隊列')
}
}
} catch (error) {
console.error('答題處理失敗:', error)
} finally {
setIsProcessingAnswer(false)
}
}, [currentCard, hasAnswered, isProcessingAnswer, currentMode, updateScore, recordTestResult, markTestCompleted, currentTestIndex])
// 檢查答案正確性
const checkAnswer = (answer: string, card: any, mode: string): boolean => {
switch (mode) {
case 'flip-memory':
return true // 翻卡記憶沒有對錯,只有信心等級
case 'vocab-choice':
case 'vocab-listening':
return answer === card.word
case 'sentence-fill':
return answer.toLowerCase().trim() === card.word.toLowerCase()
case 'sentence-reorder':
case 'sentence-listening':
return answer.toLowerCase().trim() === card.example.toLowerCase().trim()
case 'sentence-speaking':
return true // 口說測驗通常算正確
default:
return false
}
}
// 生成測驗選項
const generateOptions = (card: any, mode: string): string[] => {
// 這裡應該根據測驗類型生成對應的選項
// 暫時返回簡單的佔位符
switch (mode) {
case 'vocab-choice':
case 'vocab-listening':
return [card.word, '其他選項1', '其他選項2', '其他選項3'].sort(() => Math.random() - 0.5)
case 'sentence-listening':
return [
card.example,
'其他例句選項1',
'其他例句選項2',
'其他例句選項3'
].sort(() => Math.random() - 0.5)
default:
return []
}
}
// 處理跳過
const handleSkip = useCallback(() => {
if (hasAnswered) return // 已答題後不能跳過
skipCurrentTest()
// 重置狀態準備下一題
setHasAnswered(false)
setIsProcessingAnswer(false)
}, [hasAnswered, skipCurrentTest])
// 處理繼續
const handleContinue = useCallback(() => {
if (!hasAnswered) return // 未答題不能繼續
goToNextTest()
// 重置狀態準備下一題
setHasAnswered(false)
setIsProcessingAnswer(false)
}, [hasAnswered, goToNextTest])
if (error) {
return (
<div className="text-center py-8">
<div className="bg-red-50 border border-red-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-red-700 mb-2"></h3>
<p className="text-red-600">{error}</p>
</div>
</div>
)
}
if (!currentCard) {
return (
<div className="text-center py-8">
<div className="text-gray-500">...</div>
</div>
)
}
// 共同的 props
const cardData = {
id: currentCard.id,
word: currentCard.word,
definition: currentCard.definition,
example: currentCard.example,
translation: currentCard.translation || '',
exampleTranslation: currentCard.translation || '',
pronunciation: currentCard.pronunciation,
cefr: currentCard.cefr || 'A1',
exampleImage: currentCard.exampleImage,
synonyms: currentCard.synonyms || []
}
const commonProps = {
cardData,
onAnswer: handleAnswer,
onReportError: () => openReportModal(currentCard)
}
// 測驗內容渲染函數
const renderTestContent = () => {
// 渲染對應的測驗組件(不包含導航)
switch (currentMode) {
case 'flip-memory':
return (
<FlipMemoryTest
{...commonProps}
onConfidenceSubmit={(level) => handleAnswer('', level)}
disabled={isProcessingAnswer}
/>
)
case 'vocab-choice':
return (
<VocabChoiceTest
{...commonProps}
options={generateOptions(currentCard, currentMode)}
disabled={isProcessingAnswer}
/>
)
case 'sentence-fill':
return (
<SentenceFillTest
{...commonProps}
disabled={isProcessingAnswer}
/>
)
case 'sentence-reorder':
return (
<SentenceReorderTest
{...commonProps}
exampleImage={cardData.exampleImage}
onImageClick={openImageModal}
disabled={isProcessingAnswer}
/>
)
case 'vocab-listening':
return (
<VocabListeningTest
{...commonProps}
options={generateOptions(currentCard, currentMode)}
disabled={isProcessingAnswer}
/>
)
case 'sentence-listening':
return (
<SentenceListeningTest
{...commonProps}
options={generateOptions(currentCard, currentMode)}
exampleImage={cardData.exampleImage}
onImageClick={openImageModal}
disabled={isProcessingAnswer}
/>
)
case 'sentence-speaking':
return (
<SentenceSpeakingTest
{...commonProps}
exampleImage={cardData.exampleImage}
onImageClick={openImageModal}
disabled={isProcessingAnswer}
/>
)
default:
return (
<div className="text-center py-8">
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-6">
<h3 className="text-lg font-semibold text-yellow-700 mb-2"></h3>
<p className="text-yellow-600"> "{currentMode}" </p>
</div>
</div>
)
}
}
return (
<div className={`review-runner ${className}`}>
{/* 進度條 (僅在測試模式顯示) */}
{testItems.length > 0 && (
<div className="mb-6">
<ProgressBar
current={currentTestIndex}
total={testItems.length}
correct={score.correct}
incorrect={score.total - score.correct}
skipped={testItems.filter(item => item.isSkipped).length}
/>
</div>
)}
{/* 測驗內容 */}
<div className="mb-6">
{renderTestContent()}
</div>
{/* 智能導航控制器 */}
<div className="border-t pt-6">
<SmartNavigationController
hasAnswered={hasAnswered}
disabled={isProcessingAnswer}
onSkipCallback={handleSkip}
onContinueCallback={handleContinue}
className="mt-4"
/>
</div>
</div>
)
}