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

269 lines
9.3 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 { useState, useMemo } from 'react'
import AudioPlayer from '@/components/AudioPlayer'
import { getCorrectAnswer } from '@/utils/answerExtractor'
interface SentenceFillTestProps {
word: string
definition: string
example: string
filledQuestionText?: string
exampleTranslation: string
pronunciation?: string
difficultyLevel: string
exampleImage?: string
onAnswer: (answer: string) => void
onReportError: () => void
onImageClick?: (image: string) => void
disabled?: boolean
}
export const SentenceFillTest: React.FC<SentenceFillTestProps> = ({
word,
definition,
example,
filledQuestionText,
exampleTranslation,
pronunciation,
difficultyLevel,
exampleImage,
onAnswer,
onReportError,
onImageClick,
disabled = false
}) => {
const [fillAnswer, setFillAnswer] = useState('')
const [showResult, setShowResult] = useState(false)
const [showHint, setShowHint] = useState(false)
const handleSubmit = () => {
if (disabled || showResult || !fillAnswer.trim()) return
setShowResult(true)
onAnswer(fillAnswer)
}
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !showResult && fillAnswer.trim()) {
handleSubmit()
}
}
// 🆕 動態計算正確答案:從例句和挖空題目推導
const correctAnswer = useMemo(() => {
return getCorrectAnswer(example, filledQuestionText, word);
}, [example, filledQuestionText, word]);
const isCorrect = fillAnswer.toLowerCase().trim() === correctAnswer.toLowerCase().trim()
const targetWordLength = correctAnswer.length
const inputWidth = Math.max(100, Math.max(targetWordLength * 12, fillAnswer.length * 12 + 20))
// 🆕 智能填空渲染:優先使用後端提供的挖空題目
const renderFilledSentence = () => {
if (!filledQuestionText) {
// 降級處理:使用原有的前端挖空邏輯
return renderSentenceWithInput();
}
// 使用後端提供的挖空題目
const parts = filledQuestionText.split('____');
return (
<div className="text-lg text-gray-700 leading-relaxed">
{parts.map((part, index) => (
<span key={index}>
{part}
{index < parts.length - 1 && (
<span className="relative inline-block mx-1">
<input
type="text"
value={fillAnswer}
onChange={(e) => setFillAnswer(e.target.value)}
onKeyDown={handleKeyDown}
placeholder=""
disabled={disabled || showResult}
className={`inline-block px-2 py-1 text-center bg-transparent focus:outline-none disabled:bg-gray-100 ${
fillAnswer
? 'border-b-2 border-blue-500'
: 'border-b-2 border-dashed border-gray-400 focus:border-blue-400 focus:border-solid'
}`}
style={{ width: `${inputWidth}px` }}
/>
{!fillAnswer && (
<span
className="absolute inset-0 flex items-center justify-center text-gray-400 pointer-events-none"
style={{ paddingBottom: '8px' }}
>
____
</span>
)}
</span>
)}
</span>
))}
</div>
);
}
// 將例句中的目標詞替換為輸入框
const renderSentenceWithInput = () => {
const parts = example.split(new RegExp(`\\b${word}\\b`, 'gi'))
const matches = example.match(new RegExp(`\\b${word}\\b`, 'gi')) || []
return (
<div className="text-lg text-gray-700 leading-relaxed">
{parts.map((part, index) => (
<span key={index}>
{part}
{index < matches.length && (
<span className="relative inline-block mx-1">
<input
type="text"
value={fillAnswer}
onChange={(e) => setFillAnswer(e.target.value)}
onKeyDown={handleKeyDown}
placeholder=""
disabled={disabled || showResult}
className={`inline-block px-2 py-1 text-center bg-transparent focus:outline-none disabled:bg-gray-100 ${
fillAnswer
? 'border-b-2 border-blue-500'
: 'border-b-2 border-dashed border-gray-400 focus:border-blue-400 focus:border-solid'
}`}
style={{ width: `${inputWidth}px` }}
/>
{!fillAnswer && (
<span
className="absolute inset-0 flex items-center justify-center text-gray-400 pointer-events-none"
style={{ paddingBottom: '8px' }}
>
____
</span>
)}
</span>
)}
</span>
))}
</div>
)
}
return (
<div className="relative">
{/* 錯誤回報按鈕 */}
<div className="flex justify-end mb-2">
<button
onClick={onReportError}
className="px-3 py-2 rounded-md transition-colors text-gray-600 hover:text-gray-900"
>
🚩
</button>
</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">
{difficultyLevel}
</span>
</div>
{/* 圖片區(如果有) */}
{exampleImage && (
<div className="mb-6">
<div className="bg-gray-50 rounded-lg p-4">
<img
src={exampleImage}
alt="Example illustration"
className="w-full max-w-md mx-auto rounded-lg cursor-pointer"
onClick={() => onImageClick?.(exampleImage)}
/>
</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={() => setShowHint(!showHint)}
className="px-6 py-2 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition-colors"
>
{showHint ? '隱藏提示' : '顯示提示'}
</button>
</div>
{/* 提示區域 */}
{showHint && (
<div className="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
<h4 className="font-semibold text-yellow-800 mb-2"></h4>
<p className="text-yellow-800">{definition}</p>
</div>
)}
{/* 結果反饋區 */}
{showResult && (
<div className={`mt-6 p-6 rounded-lg w-full ${
isCorrect
? 'bg-green-50 border border-green-200'
: 'bg-red-50 border border-red-200'
}`}>
<p className={`font-semibold text-left text-xl mb-4 ${
isCorrect ? 'text-green-700' : 'text-red-700'
}`}>
{isCorrect ? '正確!' : '錯誤!'}
</p>
{!isCorrect && (
<div className="mb-4">
<p className="text-gray-700 text-left">
<strong className="text-lg">{correctAnswer}</strong>
</p>
</div>
)}
<div className="space-y-3">
<div className="text-left">
<p className="text-gray-600">
{word && <span className="font-semibold text-left text-xl">{word}</span>}
{pronunciation && <span className="mx-2">{pronunciation}</span>}
<AudioPlayer text={correctAnswer} />
</p>
</div>
<div className="text-left">
<p className="text-gray-600">
{example}
<AudioPlayer text={example}/>
</p>
<p className="text-gray-500 text-sm">
{exampleTranslation}
</p>
</div>
</div>
</div>
)}
</div>
</div>
)
}