'use client' import { useState, useEffect, useMemo, useCallback } from 'react' import { createPortal } from 'react-dom' interface WordAnalysis { word: string translation: string definition: string partOfSpeech: string pronunciation: string synonyms: string[] antonyms?: string[] isPhrase: boolean isHighValue?: boolean learningPriority?: 'high' | 'medium' | 'low' phraseInfo?: { phrase: string meaning: string warning: string colorCode: string } difficultyLevel: string costIncurred?: number example?: string exampleTranslation?: string } interface ClickableTextProps { text: string analysis?: Record onWordClick?: (word: string, analysis: WordAnalysis) => void onSaveWord?: (word: string, analysis: WordAnalysis) => Promise remainingUsage?: number showPhrasesInline?: boolean } const POPUP_CONFIG = { WIDTH: 320, HEIGHT: 400, PADDING: 16, MOBILE_BREAKPOINT: 640 } as const export function ClickableTextV2({ text, analysis, onWordClick, onSaveWord, remainingUsage = 5, showPhrasesInline = true }: ClickableTextProps) { const [selectedWord, setSelectedWord] = useState(null) const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0, showBelow: false }) const [isSavingWord, setIsSavingWord] = useState(false) const [mounted, setMounted] = useState(false) useEffect(() => { setMounted(true) }, []) const getCEFRColor = useCallback((level: string) => { switch (level) { case 'A1': return 'bg-green-100 text-green-700 border-green-200' case 'A2': return 'bg-blue-100 text-blue-700 border-blue-200' case 'B1': return 'bg-yellow-100 text-yellow-700 border-yellow-200' case 'B2': return 'bg-orange-100 text-orange-700 border-orange-200' case 'C1': return 'bg-red-100 text-red-700 border-red-200' case 'C2': return 'bg-purple-100 text-purple-700 border-purple-200' default: return 'bg-gray-100 text-gray-700 border-gray-200' } }, []) const getWordProperty = useCallback((wordData: any, propName: string) => { if (!wordData) return undefined; const variations = [ propName, propName.toLowerCase(), propName.charAt(0).toUpperCase() + propName.slice(1), propName.charAt(0).toLowerCase() + propName.slice(1) ]; for (const variation of variations) { if (wordData[variation] !== undefined) { return wordData[variation]; } } if (propName === 'synonyms') { return []; } return undefined; }, []) const findWordAnalysis = useCallback((word: string) => { const cleanWord = word.toLowerCase().replace(/[.,!?;:]/g, '') return analysis?.[cleanWord] || analysis?.[word] || analysis?.[word.toLowerCase()] || null }, [analysis]) const getLevelIndex = useCallback((level: string): number => { const levels = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2'] return levels.indexOf(level) }, []) const getWordClass = (word: string) => { const wordAnalysis = findWordAnalysis(word) const baseClass = "cursor-pointer transition-all duration-200 rounded relative mx-0.5 px-1 py-0.5" if (wordAnalysis) { const isPhrase = getWordProperty(wordAnalysis, 'isPhrase') const difficultyLevel = getWordProperty(wordAnalysis, 'difficultyLevel') || 'A1' const userLevel = typeof window !== 'undefined' ? localStorage.getItem('userEnglishLevel') || 'A2' : 'A2' // 如果是片語,跳過標記 if (isPhrase) { return "" } // 直接進行CEFR等級比較 const userIndex = getLevelIndex(userLevel) const wordIndex = getLevelIndex(difficultyLevel) if (userIndex > wordIndex) { // 簡單詞彙:學習者程度 > 詞彙程度 return `${baseClass} bg-gray-50 border border-dashed border-gray-300 hover:bg-gray-100 hover:border-gray-400 text-gray-600 opacity-80` } else if (userIndex === wordIndex) { // 適中詞彙:學習者程度 = 詞彙程度 return `${baseClass} bg-green-50 border border-green-200 hover:bg-green-100 hover:shadow-lg transform hover:-translate-y-0.5 text-green-700 font-medium` } else { // 艱難詞彙:學習者程度 < 詞彙程度 return `${baseClass} bg-orange-50 border border-orange-200 hover:bg-orange-100 hover:shadow-lg transform hover:-translate-y-0.5 text-orange-700 font-medium` } } else { return "" } } const getWordIcon = (word: string) => { // 移除所有圖標,保持簡潔設計 return null } const words = useMemo(() => text.split(/(\s+|[.,!?;:])/g), [text]) const handleWordClick = useCallback(async (word: string, event: React.MouseEvent) => { const cleanWord = word.toLowerCase().replace(/[.,!?;:]/g, '') const wordAnalysis = findWordAnalysis(word) if (!wordAnalysis) return const rect = event.currentTarget.getBoundingClientRect() const position = { x: rect.left + rect.width / 2, y: rect.bottom + 10, showBelow: true } setPopupPosition(position) setSelectedWord(cleanWord) onWordClick?.(cleanWord, wordAnalysis) }, [findWordAnalysis, onWordClick]) const closePopup = useCallback(() => { setSelectedWord(null) }, []) const handleSaveWord = useCallback(async () => { if (!selectedWord || !analysis?.[selectedWord] || !onSaveWord) return setIsSavingWord(true) try { await onSaveWord(selectedWord, analysis[selectedWord]) setSelectedWord(null) } catch (error) { console.error('Save word error:', error) alert(`保存詞彙失敗: ${error instanceof Error ? error.message : '未知錯誤'}`) } finally { setIsSavingWord(false) } }, [selectedWord, analysis, onSaveWord]) const VocabPopup = () => { if (!selectedWord || !analysis?.[selectedWord] || !mounted) return null return createPortal( <>

{getWordProperty(analysis[selectedWord], 'word')}

{getWordProperty(analysis[selectedWord], 'partOfSpeech')} {getWordProperty(analysis[selectedWord], 'pronunciation')}
{getWordProperty(analysis[selectedWord], 'difficultyLevel')}

中文翻譯

{getWordProperty(analysis[selectedWord], 'translation')}

英文定義

{getWordProperty(analysis[selectedWord], 'definition')}

{(() => { const example = getWordProperty(analysis[selectedWord], 'example'); return example && example !== 'null' && example !== 'undefined'; })() && (

例句

"{getWordProperty(analysis[selectedWord], 'example')}"

{getWordProperty(analysis[selectedWord], 'exampleTranslation')}

)}
{onSaveWord && (
)}
, document.body ) } return (
{words.map((word, index) => { if (word.trim() === '' || /^[.,!?;:\s]+$/.test(word)) { return {word} } const className = getWordClass(word) const icon = getWordIcon(word) return ( handleWordClick(word, e)} > {word} {icon} ) })}
) }