'use client' import { useState, useEffect, useMemo, useCallback } from 'react' import { createPortal } from 'react-dom' import { Play } from 'lucide-react' import { cefrToNumeric, compareCEFRLevels, getLevelIndex } from '@/lib/utils/cefrUtils' import { flashcardsService } from '@/lib/services/flashcards' interface WordAnalysis { word: string translation: string definition: string partOfSpeech: string pronunciation: string synonyms: string[] antonyms?: string[] isIdiom: boolean isHighValue?: boolean learningPriority?: 'high' | 'medium' | 'low' idiomInfo?: { idiom: string meaning: string warning: string colorCode: string } difficultyLevel: string difficultyLevelNumeric?: number // 新增數字難度等級支援 frequency?: string // 新增頻率屬性:'high' | 'medium' | 'low' costIncurred?: number example?: string exampleTranslation?: string } interface ClickableTextProps { text: string analysis?: Record onWordClick?: (word: string, analysis: WordAnalysis) => void onSaveWord?: (word: string, analysis: WordAnalysis) => Promise<{ success: boolean; error?: string }> remainingUsage?: number showIdiomsInline?: boolean } const POPUP_CONFIG = { WIDTH: 320, HEIGHT: 400, PADDING: 16, MOBILE_BREAKPOINT: 640 } as const export function ClickableTextV2({ text, analysis, onWordClick, onSaveWord, remainingUsage = 5, showIdiomsInline = 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, '') const capitalizedWord = word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() return analysis?.[word] || analysis?.[capitalizedWord] || analysis?.[cleanWord] || analysis?.[word.toLowerCase()] || analysis?.[word.toUpperCase()] || null }, [analysis]) const getWordClass = useCallback((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) return "" const isIdiom = getWordProperty(wordAnalysis, 'isIdiom') if (isIdiom) return "" const difficultyLevel = getWordProperty(wordAnalysis, 'difficultyLevel') || 'A1' const userLevel = typeof window !== 'undefined' ? localStorage.getItem('userEnglishLevel') || 'A2' : 'A2' 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` } }, [findWordAnalysis, getWordProperty]) const getWordIcon = (word: string) => { // 移除所有圖標,保持簡潔設計 return null } const shouldShowStar = useCallback((word: string) => { try { const wordAnalysis = findWordAnalysis(word) if (!wordAnalysis) return false const frequency = getWordProperty(wordAnalysis, 'frequency') const wordCefr = getWordProperty(wordAnalysis, 'cefrLevel') const userLevel = typeof window !== 'undefined' ? localStorage.getItem('userEnglishLevel') || 'A2' : 'A2' // 只有當詞彙為常用且不是簡單詞彙時才顯示星星 // 簡單詞彙定義:學習者CEFR > 詞彙CEFR const isHighFrequency = frequency === 'high' const isNotSimpleWord = !compareCEFRLevels(userLevel, wordCefr, '>') return isHighFrequency && isNotSimpleWord } catch (error) { console.warn('Error checking word frequency for star display:', error) return false } }, [findWordAnalysis, getWordProperty]) const words = useMemo(() => text.split(/(\s+|[.,!?;:])/g), [text]) const calculatePopupPosition = useCallback((rect: DOMRect) => { const popupWidth = 320 // w-80 = 320px const popupHeight = 400 // estimated popup height const margin = 16 const viewportWidth = window.innerWidth const viewportHeight = window.innerHeight let x = rect.left + rect.width / 2 let y = rect.bottom + 10 let showBelow = true // Check if popup would go off right edge if (x + popupWidth / 2 > viewportWidth - margin) { x = viewportWidth - popupWidth / 2 - margin } // Check if popup would go off left edge if (x - popupWidth / 2 < margin) { x = popupWidth / 2 + margin } // Check if popup would go off bottom edge if (y + popupHeight > viewportHeight - margin) { y = rect.top - 10 showBelow = false } // Check if popup would go off top edge (when showing above) if (!showBelow && y - popupHeight < margin) { y = rect.bottom + 10 showBelow = true } return { x, y, showBelow } }, []) const handleWordClick = useCallback(async (word: string, event: React.MouseEvent) => { const wordAnalysis = findWordAnalysis(word) if (!wordAnalysis) return // 找到實際在analysis中的key const cleanWord = word.toLowerCase().replace(/[.,!?;:]/g, '') const capitalizedWord = word.charAt(0).toUpperCase() + word.slice(1).toLowerCase() let actualKey = '' if (analysis?.[word]) actualKey = word else if (analysis?.[capitalizedWord]) actualKey = capitalizedWord else if (analysis?.[cleanWord]) actualKey = cleanWord else if (analysis?.[word.toLowerCase()]) actualKey = word.toLowerCase() else if (analysis?.[word.toUpperCase()]) actualKey = word.toUpperCase() const rect = event.currentTarget.getBoundingClientRect() const position = calculatePopupPosition(rect) setPopupPosition(position) setSelectedWord(actualKey) // 使用實際的key onWordClick?.(actualKey, wordAnalysis) }, [findWordAnalysis, onWordClick, calculatePopupPosition, analysis]) const closePopup = useCallback(() => { setSelectedWord(null) }, []) const handleSaveWord = useCallback(async () => { if (!selectedWord || !analysis?.[selectedWord] || !onSaveWord) return setIsSavingWord(true) try { const result = await onSaveWord(selectedWord, analysis[selectedWord]) if (result?.success) { setSelectedWord(null) } else { console.error('Save word error:', result?.error || '保存失敗') } } catch (error) { console.error('Save word error:', error) } 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')}

)} {(() => { const synonyms = getWordProperty(analysis[selectedWord], 'synonyms'); return synonyms && Array.isArray(synonyms) && synonyms.length > 0; })() && (

同義詞

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