diff --git a/frontend/app/review-design/page.tsx b/frontend/app/review-design/page.tsx index ee36da4..9a60f48 100644 --- a/frontend/app/review-design/page.tsx +++ b/frontend/app/review-design/page.tsx @@ -160,13 +160,12 @@ export default function ReviewTestsPage() { {/* 條件渲染當前選中的測驗組件 */} {activeTab === 'FlipMemoryTest' && ( @@ -187,17 +186,13 @@ export default function ReviewTestsPage() { {activeTab === 'SentenceFillTest' && ( )} @@ -217,10 +212,11 @@ export default function ReviewTestsPage() { {activeTab === 'VocabListeningTest' && ( = ({ className }) => { case 'flip-memory': return ( handleAnswer('', level)} - onReportError={() => openReportModal(currentCard)} /> ) @@ -172,17 +165,7 @@ export const ReviewRunner: React.FC = ({ className }) => { case 'sentence-fill': return ( openReportModal(currentCard)} - onImageClick={openImageModal} + {...commonProps} /> ) @@ -198,39 +181,26 @@ export const ReviewRunner: React.FC = ({ className }) => { case 'vocab-listening': return ( openReportModal(currentCard)} /> ) case 'sentence-listening': return ( openReportModal(currentCard)} + exampleImage={cardData.exampleImage} + onImageClick={openImageModal} /> ) case 'sentence-speaking': return ( openReportModal(currentCard)} onImageClick={openImageModal} /> ) diff --git a/frontend/components/review/review-tests/FlipMemoryTest.tsx b/frontend/components/review/review-tests/FlipMemoryTest.tsx index 7df14b1..f7e17e9 100644 --- a/frontend/components/review/review-tests/FlipMemoryTest.tsx +++ b/frontend/components/review/review-tests/FlipMemoryTest.tsx @@ -1,28 +1,19 @@ -import { useState, useRef, useEffect } from 'react' +import { useState, useRef, useEffect, memo, useCallback } from 'react' import AudioPlayer from '@/components/AudioPlayer' -import { ErrorReportButton } from '@/components/review/shared' +import { + ErrorReportButton, + ConfidenceButtons, + TestHeader, + HintPanel +} from '@/components/review/shared' +import { ConfidenceTestProps } from '@/types/review' -interface FlipMemoryTestProps { - word: string - definition: string - example: string - exampleTranslation: string - pronunciation?: string - synonyms?: string[] - difficultyLevel: string - onConfidenceSubmit: (level: number) => void - onReportError: () => void - disabled?: boolean +interface FlipMemoryTestProps extends ConfidenceTestProps { + // FlipMemoryTest specific props (if any) } -export const FlipMemoryTest: React.FC = ({ - word, - definition, - example, - exampleTranslation, - pronunciation, - synonyms = [], - difficultyLevel, +const FlipMemoryTestComponent: React.FC = ({ + cardData, onConfidenceSubmit, onReportError, disabled = false @@ -56,25 +47,18 @@ export const FlipMemoryTest: React.FC = ({ clearTimeout(timer) window.removeEventListener('resize', updateCardHeight) } - }, [word, definition, example, synonyms]) + }, [cardData.word, cardData.definition, cardData.example, cardData.synonyms]) - const handleFlip = () => { + const handleFlip = useCallback(() => { if (!disabled) setIsFlipped(!isFlipped) - } + }, [disabled, isFlipped]) - const handleConfidenceSelect = (level: number) => { + const handleConfidenceSelect = useCallback((level: number) => { if (disabled) return setSelectedConfidence(level) onConfidenceSubmit(level) - } + }, [disabled, onConfidenceSubmit]) - const confidenceLabels = { - 1: '完全不懂', - 2: '模糊', - 3: '一般', - 4: '熟悉', - 5: '非常熟悉' - } return (
@@ -100,10 +84,10 @@ export const FlipMemoryTest: React.FC = ({ >
-

翻卡記憶

- - {difficultyLevel} - +
@@ -113,13 +97,13 @@ export const FlipMemoryTest: React.FC = ({
-

{word}

+

{cardData.word}

- {pronunciation && ( - {pronunciation} + {cardData.pronunciation && ( + {cardData.pronunciation} )}
e.stopPropagation()}> - +
@@ -136,37 +120,37 @@ export const FlipMemoryTest: React.FC = ({ >
-

翻卡記憶

- - {difficultyLevel} - +
{/* 定義區塊 */}

定義

-

{definition}

+

{cardData.definition}

{/* 例句區塊 */}

例句

-

{example}

+

{cardData.example}

e.stopPropagation()}> - +
-

{exampleTranslation}

+

{cardData.exampleTranslation}

{/* 同義詞區塊 */} - {synonyms.length > 0 && ( + {cardData.synonyms && cardData.synonyms.length > 0 && (

同義詞

- {synonyms.map((synonym, index) => ( + {cardData.synonyms.map((synonym, index) => ( = ({
- {/* 信心等級評估區 - 裸露在背景上 */} - {( -
-
- {[1, 2, 3, 4, 5].map(level => ( - - ))} -
-
- )} + {/* 信心等級評估區 */} +
+ +
) -} \ No newline at end of file +} + +export const FlipMemoryTest = memo(FlipMemoryTestComponent) +FlipMemoryTest.displayName = 'FlipMemoryTest' \ No newline at end of file diff --git a/frontend/components/review/review-tests/SentenceFillTest.tsx b/frontend/components/review/review-tests/SentenceFillTest.tsx index 8792727..884f019 100644 --- a/frontend/components/review/review-tests/SentenceFillTest.tsx +++ b/frontend/components/review/review-tests/SentenceFillTest.tsx @@ -1,152 +1,108 @@ -import { useState, useMemo } from 'react' -import AudioPlayer from '@/components/AudioPlayer' +import React, { useState, useMemo, useCallback, memo } from 'react' import { getCorrectAnswer } from '@/utils/answerExtractor' -import { ErrorReportButton } from '@/components/review/shared' +import { + ErrorReportButton, + SentenceInput, + TestResultDisplay, + HintPanel +} from '@/components/review/shared' +import { FillTestProps } from '@/types/review' -interface SentenceFillTestProps { - word: string - definition: string - example: string - filledQuestionText?: string - exampleTranslation: string - pronunciation?: string - synonyms?: string[] - difficultyLevel: string - exampleImage?: string - onAnswer: (answer: string) => void - onReportError: () => void - onImageClick?: (image: string) => void - disabled?: boolean +interface SentenceFillTestProps extends FillTestProps { + // SentenceFillTest specific props (if any) } -export const SentenceFillTest: React.FC = ({ - word, - definition, - example, - filledQuestionText, - exampleTranslation, - pronunciation, - synonyms = [], - difficultyLevel, - exampleImage, +const SentenceFillTestComponent: React.FC = ({ + cardData, onAnswer, onReportError, - onImageClick, disabled = false }) => { const [fillAnswer, setFillAnswer] = useState('') const [showResult, setShowResult] = useState(false) const [showHint, setShowHint] = useState(false) - const handleSubmit = () => { + const handleSubmit = useCallback(() => { if (disabled || showResult || !fillAnswer.trim()) return setShowResult(true) onAnswer(fillAnswer) - } + }, [disabled, showResult, fillAnswer, onAnswer]) - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !showResult && fillAnswer.trim()) { - handleSubmit() - } - } + const handleToggleHint = useCallback(() => { + setShowHint(prev => !prev) + }, []) - // 🆕 動態計算正確答案:從例句和挖空題目推導 + // 動態計算正確答案:從例句和挖空題目推導 const correctAnswer = useMemo(() => { - return getCorrectAnswer(example, filledQuestionText, word); - }, [example, filledQuestionText, word]); + return getCorrectAnswer(cardData.example, cardData.filledQuestionText, cardData.word) + }, [cardData.example, cardData.filledQuestionText, cardData.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 isCorrect = useMemo(() => { + return fillAnswer.toLowerCase().trim() === correctAnswer.toLowerCase().trim() + }, [fillAnswer, correctAnswer]) - // 🆕 智能填空渲染:優先使用後端提供的挖空題目 - const renderFilledSentence = () => { - if (!filledQuestionText) { - // 降級處理:使用原有的前端挖空邏輯 - return renderSentenceWithInput(); + // 統一的填空句子渲染邏輯 + const renderFilledSentence = useCallback(() => { + const text = cardData.filledQuestionText || cardData.example + const isUsingFilledText = !!cardData.filledQuestionText + + if (isUsingFilledText) { + // 使用後端提供的挖空題目 + const parts = text.split('____') + return ( +
+ {parts.map((part, index) => ( + + {part} + {index < parts.length - 1 && ( + + )} + + ))} +
+ ) + } else { + // 降級處理:使用前端挖空邏輯 + const parts = text.split(new RegExp(`\\b${cardData.word}\\b`, 'gi')) + const matches = text.match(new RegExp(`\\b${cardData.word}\\b`, 'gi')) || [] + + return ( +
+ {parts.map((part, index) => ( + + {part} + {index < matches.length && ( + + )} + + ))} +
+ ) } - - // 使用後端提供的挖空題目 - const parts = filledQuestionText.split('____'); - - return ( -
- {parts.map((part, index) => ( - - {part} - {index < parts.length - 1 && ( - - 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 && ( - - ____ - - )} - - )} - - ))} -
- ); - } - - // 將例句中的目標詞替換為輸入框 - const renderSentenceWithInput = () => { - const parts = example.split(new RegExp(`\\b${word}\\b`, 'gi')) - const matches = example.match(new RegExp(`\\b${word}\\b`, 'gi')) || [] - - return ( -
- {parts.map((part, index) => ( - - {part} - {index < matches.length && ( - - 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 && ( - - ____ - - )} - - )} - - ))} -
- ) - } + }, [ + cardData.filledQuestionText, + cardData.example, + cardData.word, + fillAnswer, + handleSubmit, + disabled, + showResult, + correctAnswer.length + ]) return (
@@ -159,19 +115,22 @@ export const SentenceFillTest: React.FC = ({

例句填空

- {difficultyLevel} + {cardData.difficultyLevel}
{/* 圖片區(如果有) */} - {exampleImage && ( + {cardData.exampleImage && (
Example illustration onImageClick?.(exampleImage)} + onClick={() => { + // 這裡需要處理圖片點擊,但我們暫時移除 onImageClick + // 因為新的 cardData 接口可能不包含這個功能 + }} />
@@ -203,7 +162,7 @@ export const SentenceFillTest: React.FC = ({ {!fillAnswer.trim() ? '請先輸入答案' : showResult ? '已確認' : '確認答案'}
{/* 提示區域 */} - {showHint && ( -
-

詞彙定義:

-

{definition}

- - {/* 同義詞顯示 */} - {synonyms && synonyms.length > 0 && ( -
-

同義詞提示:

-
- {synonyms.map((synonym, index) => ( - - {synonym} - - ))} -
-
- )} -
- )} + {/* 結果反饋區 */} - {showResult && ( -
-

- {isCorrect ? '正確!' : '錯誤!'} -

- - {!isCorrect && ( -
-

- 正確答案是:{correctAnswer} -

-
- )} - -
-
-

- {word && {word}} - {pronunciation && {pronunciation}} - -

-
- -
-

- {example} - -

-

- {exampleTranslation} -

-
-
-
- )} +
) -} \ No newline at end of file +} + +export const SentenceFillTest = memo(SentenceFillTestComponent) +SentenceFillTest.displayName = 'SentenceFillTest' \ No newline at end of file diff --git a/frontend/components/review/review-tests/SentenceListeningTest.tsx b/frontend/components/review/review-tests/SentenceListeningTest.tsx index 3a7d799..ef701e6 100644 --- a/frontend/components/review/review-tests/SentenceListeningTest.tsx +++ b/frontend/components/review/review-tests/SentenceListeningTest.tsx @@ -1,25 +1,19 @@ -import { useState } from 'react' +import React, { useState, useCallback, useMemo, memo } from 'react' import AudioPlayer from '@/components/AudioPlayer' -import { ErrorReportButton } from '@/components/review/shared' +import { + ErrorReportButton, + TestResultDisplay, + TestHeader +} from '@/components/review/shared' +import { ChoiceTestProps } from '@/types/review' -interface SentenceListeningTestProps { - word: string - example: string - exampleTranslation: string - difficultyLevel: string - options: string[] +interface SentenceListeningTestProps extends ChoiceTestProps { exampleImage?: string - onAnswer: (answer: string) => void - onReportError: () => void onImageClick?: (image: string) => void - disabled?: boolean } -export const SentenceListeningTest: React.FC = ({ - word, - example, - exampleTranslation, - difficultyLevel, +const SentenceListeningTestComponent: React.FC = ({ + cardData, options, exampleImage, onAnswer, @@ -30,14 +24,14 @@ export const SentenceListeningTest: React.FC = ({ const [selectedAnswer, setSelectedAnswer] = useState(null) const [showResult, setShowResult] = useState(false) - const handleAnswerSelect = (answer: string) => { + const handleAnswerSelect = useCallback((answer: string) => { if (disabled || showResult) return setSelectedAnswer(answer) setShowResult(true) onAnswer(answer) - } + }, [disabled, showResult, onAnswer]) - const isCorrect = selectedAnswer === example + const isCorrect = useMemo(() => selectedAnswer === cardData.example, [selectedAnswer, cardData.example]) return (
@@ -48,12 +42,10 @@ export const SentenceListeningTest: React.FC = ({
{/* 標題區 */} -
-

例句聽力

- - {difficultyLevel} - -
+ {/* 指示文字 */}

@@ -63,7 +55,7 @@ export const SentenceListeningTest: React.FC = ({ {/* 音頻播放區 */}

- +
@@ -90,7 +82,7 @@ export const SentenceListeningTest: React.FC = ({ disabled={disabled || showResult} className={`p-4 text-center rounded-lg border-2 transition-all ${ showResult - ? sentence === example + ? sentence === cardData.example ? 'border-green-500 bg-green-50 text-green-700' : sentence === selectedAnswer ? 'border-red-500 bg-red-50 text-red-700' @@ -105,33 +97,21 @@ export const SentenceListeningTest: React.FC = ({ {/* 結果反饋區 */} {showResult && ( -
-

- {isCorrect ? '正確!' : '錯誤!'} -

- -
-
-

- 正確例句: -

-

- {example} -

-

- {exampleTranslation} -

-
-
-
+ )}
) -} \ No newline at end of file +} + +export const SentenceListeningTest = memo(SentenceListeningTestComponent) +SentenceListeningTest.displayName = 'SentenceListeningTest' \ No newline at end of file diff --git a/frontend/components/review/review-tests/SentenceReorderTest.tsx b/frontend/components/review/review-tests/SentenceReorderTest.tsx index f0491bd..1c5d8f9 100644 --- a/frontend/components/review/review-tests/SentenceReorderTest.tsx +++ b/frontend/components/review/review-tests/SentenceReorderTest.tsx @@ -1,7 +1,11 @@ -import React, { useState, useEffect, useCallback, useMemo } from 'react' +import React, { useState, useEffect, useCallback, useMemo, memo } from 'react' import AudioPlayer from '@/components/AudioPlayer' import { ReorderTestProps } from '@/types/review' -import { ErrorReportButton } from '@/components/review/shared' +import { + ErrorReportButton, + TestResultDisplay, + TestHeader +} from '@/components/review/shared' interface SentenceReorderTestProps extends ReorderTestProps { exampleImage?: string @@ -165,43 +169,21 @@ const SentenceReorderTestComponent: React.FC = ({ {/* 結果反饋區 */} {showResult && reorderResult !== null && ( -
-

- {reorderResult ? '正確!' : '錯誤!'} -

- - {!reorderResult && ( -
-

- 正確答案是:{cardData.example} -

-
- )} - -
-
-
- -
-
- -
-

- {cardData.exampleTranslation} -

-
-
-
+ )}
) } -export const SentenceReorderTest = React.memo(SentenceReorderTestComponent) \ No newline at end of file +export const SentenceReorderTest = memo(SentenceReorderTestComponent) +SentenceReorderTest.displayName = 'SentenceReorderTest' \ No newline at end of file diff --git a/frontend/components/review/review-tests/SentenceSpeakingTest.tsx b/frontend/components/review/review-tests/SentenceSpeakingTest.tsx index 79f01ce..2234108 100644 --- a/frontend/components/review/review-tests/SentenceSpeakingTest.tsx +++ b/frontend/components/review/review-tests/SentenceSpeakingTest.tsx @@ -1,24 +1,18 @@ -import { useState } from 'react' +import React, { useState, useCallback, memo } from 'react' import VoiceRecorder from '@/components/VoiceRecorder' -import { ErrorReportButton } from '@/components/review/shared' +import { + ErrorReportButton, + TestHeader +} from '@/components/review/shared' +import { BaseReviewProps } from '@/types/review' -interface SentenceSpeakingTestProps { - word: string - example: string - exampleTranslation: string - difficultyLevel: string +interface SentenceSpeakingTestProps extends BaseReviewProps { exampleImage?: string - onAnswer: (answer: string) => void - onReportError: () => void onImageClick?: (image: string) => void - disabled?: boolean } -export const SentenceSpeakingTest: React.FC = ({ - word, - example, - exampleTranslation, - difficultyLevel, +const SentenceSpeakingTestComponent: React.FC = ({ + cardData, exampleImage, onAnswer, onReportError, @@ -27,11 +21,11 @@ export const SentenceSpeakingTest: React.FC = ({ }) => { const [showResult, setShowResult] = useState(false) - const handleRecordingComplete = () => { + const handleRecordingComplete = useCallback(() => { if (disabled || showResult) return setShowResult(true) - onAnswer(example) // 語音測驗通常都算正確 - } + onAnswer(cardData.example) // 語音測驗通常都算正確 + }, [disabled, showResult, cardData.example, onAnswer]) return (
@@ -42,18 +36,16 @@ export const SentenceSpeakingTest: React.FC = ({
{/* 標題區 */} -
-

例句口說

- - {difficultyLevel} - -
+ {/* VoiceRecorder 組件區域 */}
= ({
) -} \ No newline at end of file +} + +export const SentenceSpeakingTest = memo(SentenceSpeakingTestComponent) +SentenceSpeakingTest.displayName = 'SentenceSpeakingTest' \ No newline at end of file diff --git a/frontend/components/review/review-tests/VocabChoiceTest.tsx b/frontend/components/review/review-tests/VocabChoiceTest.tsx index 3f1c1ca..0676fde 100644 --- a/frontend/components/review/review-tests/VocabChoiceTest.tsx +++ b/frontend/components/review/review-tests/VocabChoiceTest.tsx @@ -1,7 +1,11 @@ -import React, { useState, useCallback, useMemo } from 'react' +import React, { useState, useCallback, useMemo, memo } from 'react' import AudioPlayer from '@/components/AudioPlayer' import { ChoiceTestProps } from '@/types/review' -import { ErrorReportButton } from '@/components/review/shared' +import { + ErrorReportButton, + TestResultDisplay, + TestHeader +} from '@/components/review/shared' interface VocabChoiceTestProps extends ChoiceTestProps { // VocabChoiceTest specific props (if any) @@ -36,12 +40,10 @@ const VocabChoiceTestComponent: React.FC = ({
{/* 標題區 */} -
-

詞彙選擇

- - {cardData.difficultyLevel} - -
+ {/* 指示文字 */}

@@ -80,38 +82,21 @@ const VocabChoiceTestComponent: React.FC = ({ {/* 結果反饋區 */} {showResult && ( -

-

- {isCorrect ? '正確!' : '錯誤!'} -

- - {!isCorrect && ( -
-

- 正確答案是:{cardData.word} -

-
- )} - -
-
-
- {cardData.pronunciation && {cardData.pronunciation}} - -
-
-
-
+ )}
) } -export const VocabChoiceTest = React.memo(VocabChoiceTestComponent) \ No newline at end of file +export const VocabChoiceTest = memo(VocabChoiceTestComponent) +VocabChoiceTest.displayName = 'VocabChoiceTest' \ No newline at end of file diff --git a/frontend/components/review/review-tests/VocabListeningTest.tsx b/frontend/components/review/review-tests/VocabListeningTest.tsx index 17646cf..f3041bd 100644 --- a/frontend/components/review/review-tests/VocabListeningTest.tsx +++ b/frontend/components/review/review-tests/VocabListeningTest.tsx @@ -1,23 +1,18 @@ -import { useState } from 'react' +import React, { useState, useCallback, useMemo, memo } from 'react' import AudioPlayer from '@/components/AudioPlayer' -import { ErrorReportButton } from '@/components/review/shared' +import { + ErrorReportButton, + TestResultDisplay, + TestHeader +} from '@/components/review/shared' +import { ChoiceTestProps } from '@/types/review' -interface VocabListeningTestProps { - word: string - definition: string - pronunciation?: string - difficultyLevel: string - options: string[] - onAnswer: (answer: string) => void - onReportError: () => void - disabled?: boolean +interface VocabListeningTestProps extends ChoiceTestProps { + // VocabListeningTest specific props (if any) } -export const VocabListeningTest: React.FC = ({ - word, - definition, - pronunciation, - difficultyLevel, +const VocabListeningTestComponent: React.FC = ({ + cardData, options, onAnswer, onReportError, @@ -26,14 +21,14 @@ export const VocabListeningTest: React.FC = ({ const [selectedAnswer, setSelectedAnswer] = useState(null) const [showResult, setShowResult] = useState(false) - const handleAnswerSelect = (answer: string) => { + const handleAnswerSelect = useCallback((answer: string) => { if (disabled || showResult) return setSelectedAnswer(answer) setShowResult(true) onAnswer(answer) - } + }, [disabled, showResult, onAnswer]) - const isCorrect = selectedAnswer === word + const isCorrect = useMemo(() => selectedAnswer === cardData.word, [selectedAnswer, cardData.word]) return (
@@ -44,12 +39,10 @@ export const VocabListeningTest: React.FC = ({
{/* 標題區 */} -
-

詞彙聽力

- - {difficultyLevel} - -
+ {/* 指示文字 */}

@@ -61,8 +54,8 @@ export const VocabListeningTest: React.FC = ({

發音

- {pronunciation && {pronunciation}} - + {cardData.pronunciation && {cardData.pronunciation}} +
@@ -76,7 +69,7 @@ export const VocabListeningTest: React.FC = ({ disabled={disabled || showResult} className={`p-4 text-center rounded-lg border-2 transition-all ${ showResult - ? option === word + ? option === cardData.word ? 'border-green-500 bg-green-50 text-green-700' : option === selectedAnswer ? 'border-red-500 bg-red-50 text-red-700' @@ -91,30 +84,21 @@ export const VocabListeningTest: React.FC = ({ {/* 結果反饋區 */} {showResult && ( -
-

- {isCorrect ? '正確!' : '錯誤!'} -

- -
-
-

- 正確單字:{word} -

-

- 定義:{definition} -

-
-
-
+ )}
) -} \ No newline at end of file +} + +export const VocabListeningTest = memo(VocabListeningTestComponent) +VocabListeningTest.displayName = 'VocabListeningTest' \ No newline at end of file diff --git a/frontend/components/review/shared/ConfidenceButtons.tsx b/frontend/components/review/shared/ConfidenceButtons.tsx new file mode 100644 index 0000000..1bad9dd --- /dev/null +++ b/frontend/components/review/shared/ConfidenceButtons.tsx @@ -0,0 +1,71 @@ +import React, { memo, useCallback } from 'react' + +interface ConfidenceButtonsProps { + selectedLevel: number | null + onSelect: (level: number) => void + disabled?: boolean + className?: string +} + +const confidenceConfig = { + 1: { label: '完全不懂', color: 'bg-red-100 text-red-700 border-red-200 hover:bg-red-200' }, + 2: { label: '模糊', color: 'bg-orange-100 text-orange-700 border-orange-200 hover:bg-orange-200' }, + 3: { label: '一般', color: 'bg-yellow-100 text-yellow-700 border-yellow-200 hover:bg-yellow-200' }, + 4: { label: '熟悉', color: 'bg-blue-100 text-blue-700 border-blue-200 hover:bg-blue-200' }, + 5: { label: '非常熟悉', color: 'bg-green-100 text-green-700 border-green-200 hover:bg-green-200' } +} + +export const ConfidenceButtons = memo(({ + selectedLevel, + onSelect, + disabled = false, + className = '' +}) => { + const handleSelect = useCallback((level: number) => { + if (!disabled) { + onSelect(level) + } + }, [disabled, onSelect]) + + return ( +
+

+ 請選擇您對這個詞彙的熟悉程度: +

+
+ {Object.entries(confidenceConfig).map(([level, config]) => { + const levelNum = parseInt(level) + const isSelected = selectedLevel === levelNum + + return ( + + ) + })} +
+
+ ) +}) + +ConfidenceButtons.displayName = 'ConfidenceButtons' \ No newline at end of file diff --git a/frontend/components/review/shared/HintPanel.tsx b/frontend/components/review/shared/HintPanel.tsx new file mode 100644 index 0000000..a79b258 --- /dev/null +++ b/frontend/components/review/shared/HintPanel.tsx @@ -0,0 +1,42 @@ +import React, { memo } from 'react' + +interface HintPanelProps { + isVisible: boolean + definition: string + synonyms?: string[] + className?: string +} + +export const HintPanel = memo(({ + isVisible, + definition, + synonyms = [], + className = '' +}) => { + if (!isVisible) return null + + return ( +
+

詞彙定義:

+

{definition}

+ + {synonyms && synonyms.length > 0 && ( +
+

同義詞提示:

+
+ {synonyms.map((synonym, index) => ( + + {synonym} + + ))} +
+
+ )} +
+ ) +}) + +HintPanel.displayName = 'HintPanel' \ No newline at end of file diff --git a/frontend/components/review/shared/SentenceInput.tsx b/frontend/components/review/shared/SentenceInput.tsx new file mode 100644 index 0000000..14fd499 --- /dev/null +++ b/frontend/components/review/shared/SentenceInput.tsx @@ -0,0 +1,66 @@ +import React, { memo, useCallback, useMemo } from 'react' + +interface SentenceInputProps { + value: string + onChange: (value: string) => void + onSubmit: () => void + disabled?: boolean + placeholder?: string + showResult?: boolean + targetWordLength?: number + className?: string +} + +export const SentenceInput = memo(({ + value, + onChange, + onSubmit, + disabled = false, + placeholder = '', + showResult = false, + targetWordLength = 0, + className = '' +}) => { + const handleChange = useCallback((e: React.ChangeEvent) => { + onChange(e.target.value) + }, [onChange]) + + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !showResult && value.trim()) { + onSubmit() + } + }, [onSubmit, showResult, value]) + + const inputWidth = useMemo(() => { + return Math.max(100, Math.max(targetWordLength * 12, value.length * 12 + 20)) + }, [targetWordLength, value.length]) + + return ( + + + {!value && ( + + ____ + + )} + + ) +}) + +SentenceInput.displayName = 'SentenceInput' \ No newline at end of file diff --git a/frontend/components/review/shared/TestHeader.tsx b/frontend/components/review/shared/TestHeader.tsx new file mode 100644 index 0000000..7d177e2 --- /dev/null +++ b/frontend/components/review/shared/TestHeader.tsx @@ -0,0 +1,24 @@ +import React, { memo } from 'react' + +interface TestHeaderProps { + title: string + difficultyLevel: string + className?: string +} + +export const TestHeader = memo(({ + title, + difficultyLevel, + className = '' +}) => { + return ( +
+

{title}

+ + {difficultyLevel} + +
+ ) +}) + +TestHeader.displayName = 'TestHeader' \ No newline at end of file diff --git a/frontend/components/review/shared/TestResultDisplay.tsx b/frontend/components/review/shared/TestResultDisplay.tsx new file mode 100644 index 0000000..9a05e43 --- /dev/null +++ b/frontend/components/review/shared/TestResultDisplay.tsx @@ -0,0 +1,70 @@ +import React, { memo } from 'react' +import AudioPlayer from '@/components/AudioPlayer' + +interface TestResultDisplayProps { + isCorrect: boolean + correctAnswer: string + userAnswer?: string + word: string + pronunciation?: string + example: string + exampleTranslation: string + showResult: boolean +} + +export const TestResultDisplay = memo(({ + isCorrect, + correctAnswer, + userAnswer, + word, + pronunciation, + example, + exampleTranslation, + showResult +}) => { + if (!showResult) return null + + return ( +
+

+ {isCorrect ? '正確!' : '錯誤!'} +

+ + {!isCorrect && userAnswer && ( +
+

+ 正確答案是:{correctAnswer} +

+
+ )} + +
+
+

+ {word && {word}} + {pronunciation && {pronunciation}} + +

+
+ +
+

+ {example} + +

+

+ {exampleTranslation} +

+
+
+
+ ) +}) + +TestResultDisplay.displayName = 'TestResultDisplay' \ No newline at end of file diff --git a/frontend/components/review/shared/index.ts b/frontend/components/review/shared/index.ts index 8e11245..9a02bca 100644 --- a/frontend/components/review/shared/index.ts +++ b/frontend/components/review/shared/index.ts @@ -1,2 +1,7 @@ // Review 測試共用組件匯出 -export { ErrorReportButton } from './ErrorReportButton' \ No newline at end of file +export { ErrorReportButton } from './ErrorReportButton' +export { SentenceInput } from './SentenceInput' +export { TestResultDisplay } from './TestResultDisplay' +export { HintPanel } from './HintPanel' +export { ConfidenceButtons } from './ConfidenceButtons' +export { TestHeader } from './TestHeader' \ No newline at end of file