dramaling-vocab-learning/frontend/components/review/review-tests/SentenceFillTest.tsx

196 lines
6.4 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 React, { useState, useMemo, useCallback, memo } from 'react'
import { getCorrectAnswer } from '@/utils/answerExtractor'
import {
ErrorReportButton,
SentenceInput,
TestResultDisplay,
HintPanel
} from '@/components/review/shared'
import { FillTestProps } from '@/types/review'
interface SentenceFillTestProps extends FillTestProps {
// SentenceFillTest specific props (if any)
}
const SentenceFillTestComponent: React.FC<SentenceFillTestProps> = ({
cardData,
onAnswer,
onReportError,
disabled = false
}) => {
const [fillAnswer, setFillAnswer] = useState('')
const [showResult, setShowResult] = useState(false)
const [showHint, setShowHint] = useState(false)
const handleSubmit = useCallback(() => {
if (disabled || showResult || !fillAnswer.trim()) return
setShowResult(true)
onAnswer(fillAnswer)
}, [disabled, showResult, fillAnswer, onAnswer])
const handleToggleHint = useCallback(() => {
setShowHint(prev => !prev)
}, [])
// 動態計算正確答案:從例句和挖空題目推導
const correctAnswer = useMemo(() => {
return getCorrectAnswer(cardData.example, cardData.filledQuestionText, cardData.word)
}, [cardData.example, cardData.filledQuestionText, cardData.word])
const isCorrect = useMemo(() => {
return fillAnswer.toLowerCase().trim() === correctAnswer.toLowerCase().trim()
}, [fillAnswer, correctAnswer])
// 統一的填空句子渲染邏輯
const renderFilledSentence = useCallback(() => {
const text = cardData.filledQuestionText || cardData.example
const isUsingFilledText = !!cardData.filledQuestionText
if (isUsingFilledText) {
// 使用後端提供的挖空題目
const parts = text.split('____')
return (
<div className="text-lg text-gray-700 leading-relaxed">
{parts.map((part, index) => (
<span key={index}>
{part}
{index < parts.length - 1 && (
<SentenceInput
value={fillAnswer}
onChange={setFillAnswer}
onSubmit={handleSubmit}
disabled={disabled}
showResult={showResult}
targetWordLength={correctAnswer.length}
/>
)}
</span>
))}
</div>
)
} else {
// 降級處理:使用前端挖空邏輯
const parts = text.split(new RegExp(`\\b${cardData.word}\\b`, 'gi'))
const matches = text.match(new RegExp(`\\b${cardData.word}\\b`, 'gi')) || []
return (
<div className="text-lg text-gray-700 leading-relaxed">
{parts.map((part, index) => (
<span key={index}>
{part}
{index < matches.length && (
<SentenceInput
value={fillAnswer}
onChange={setFillAnswer}
onSubmit={handleSubmit}
disabled={disabled}
showResult={showResult}
targetWordLength={correctAnswer.length}
/>
)}
</span>
))}
</div>
)
}
}, [
cardData.filledQuestionText,
cardData.example,
cardData.word,
fillAnswer,
handleSubmit,
disabled,
showResult,
correctAnswer.length
])
return (
<div className="relative">
<div className="flex justify-end mb-2">
<ErrorReportButton onClick={onReportError} />
</div>
<div className="bg-white rounded-xl shadow-lg p-8">
{/* 標題區 */}
<div className="flex justify-between items-start mb-6">
<h2 className="text-2xl font-bold text-gray-900"></h2>
<span className="bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-full">
{cardData.difficultyLevel}
</span>
</div>
{/* 圖片區(如果有) */}
{cardData.exampleImage && (
<div className="mb-6">
<div className="bg-gray-50 rounded-lg p-4">
<img
src={cardData.exampleImage}
alt="Example illustration"
className="w-full max-w-md mx-auto rounded-lg cursor-pointer"
onClick={() => {
// 這裡需要處理圖片點擊,但我們暫時移除 onImageClick
// 因為新的 cardData 接口可能不包含這個功能
}}
/>
</div>
</div>
)}
{/* 指示文字 */}
<p className="text-lg text-gray-700 mb-6 text-left">
</p>
{/* 填空句子區域 */}
<div className="mb-6">
<div className="bg-gray-50 rounded-lg p-6">
{renderFilledSentence()}
</div>
</div>
{/* 操作按鈕 */}
<div className="flex gap-3 mb-4">
<button
onClick={handleSubmit}
disabled={!fillAnswer.trim() || showResult}
className={`px-6 py-2 rounded-lg transition-colors ${
!fillAnswer.trim() || showResult
? 'bg-gray-300 text-gray-500 cursor-not-allowed'
: 'bg-blue-600 text-white hover:bg-blue-700'
}`}
>
{!fillAnswer.trim() ? '請先輸入答案' : showResult ? '已確認' : '確認答案'}
</button>
<button
onClick={handleToggleHint}
className="px-6 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors"
>
{showHint ? '隱藏提示' : '顯示提示'}
</button>
</div>
{/* 提示區域 */}
<HintPanel
isVisible={showHint}
definition={cardData.definition}
synonyms={cardData.synonyms}
/>
{/* 結果反饋區 */}
<TestResultDisplay
isCorrect={isCorrect}
correctAnswer={correctAnswer}
userAnswer={fillAnswer}
word={cardData.word}
pronunciation={cardData.pronunciation}
example={cardData.example}
exampleTranslation={cardData.exampleTranslation}
showResult={showResult}
/>
</div>
</div>
)
}
export const SentenceFillTest = memo(SentenceFillTestComponent)
SentenceFillTest.displayName = 'SentenceFillTest'