'use client' import { useState } from 'react' // 更新的詞彙分析介面 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 // 點擊此詞彙的成本 } interface ClickableTextProps { text: string analysis?: Record highValueWords?: string[] // 高價值詞彙列表 phrasesDetected?: Array<{ phrase: string words: string[] colorCode: string }> onWordClick?: (word: string, analysis: WordAnalysis) => void onWordCostConfirm?: (word: string, cost: number) => Promise // 收費確認 onNewWordAnalysis?: (word: string, analysis: WordAnalysis) => void // 新詞彙分析資料回調 onSaveWord?: (word: string, analysis: WordAnalysis) => Promise // 保存詞彙回調 remainingUsage?: number // 剩餘使用次數 } export function ClickableTextV2({ text, analysis, highValueWords = [], phrasesDetected = [], onWordClick, onWordCostConfirm, onNewWordAnalysis, onSaveWord, remainingUsage = 5 }: ClickableTextProps) { const [selectedWord, setSelectedWord] = useState(null) const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0, showBelow: false }) const [showCostConfirm, setShowCostConfirm] = useState<{ word: string cost: number position: { x: number, y: number } } | null>(null) const [isSavingWord, setIsSavingWord] = useState(false) // 輔助函數:兼容大小寫屬性名稱 const getWordProperty = (wordData: any, propName: string) => { const lowerProp = propName.toLowerCase() const upperProp = propName.charAt(0).toUpperCase() + propName.slice(1) return wordData?.[lowerProp] || wordData?.[upperProp] } // 將文字分割成單字,保留空格 const words = text.split(/(\s+|[.,!?;:])/g) const handleWordClick = async (word: string, event: React.MouseEvent) => { const cleanWord = word.toLowerCase().replace(/[.,!?;:]/g, '') const wordAnalysis = analysis?.[cleanWord] const rect = event.currentTarget.getBoundingClientRect() const viewportWidth = window.innerWidth const viewportHeight = window.innerHeight const popupWidth = 320 // popup寬度 w-80 = 320px const popupHeight = 400 // 估計popup高度 // 智能水平定位,適應不同屏幕尺寸 let x = rect.left + rect.width / 2 const actualPopupWidth = Math.min(popupWidth, viewportWidth - 32) // 實際popup寬度 const halfPopupWidth = actualPopupWidth / 2 const padding = 16 // 最小邊距 // 手機端特殊處理 if (viewportWidth <= 640) { // sm斷點 // 小屏幕時居中顯示,避免邊緣問題 x = viewportWidth / 2 } else { // 大屏幕時智能調整位置 if (x + halfPopupWidth + padding > viewportWidth) { x = viewportWidth - halfPopupWidth - padding } if (x - halfPopupWidth < padding) { x = halfPopupWidth + padding } } // 計算垂直位置 const spaceAbove = rect.top const spaceBelow = viewportHeight - rect.bottom const showBelow = spaceAbove < popupHeight const position = { x: x, y: showBelow ? rect.bottom + 10 : rect.top - 10, showBelow: showBelow } if (wordAnalysis) { // 場景A:有預存資料的詞彙 const isHighValue = getWordProperty(wordAnalysis, 'isHighValue') if (isHighValue) { // 高價值詞彙 → 直接免費顯示 setPopupPosition(position) setSelectedWord(cleanWord) onWordClick?.(cleanWord, wordAnalysis) } else { // 低價值詞彙 → 直接顯示(移除付費限制) setPopupPosition(position) setSelectedWord(cleanWord) onWordClick?.(cleanWord, wordAnalysis) } } else { // 場景B:無預存資料的詞彙 → 即時調用 AI 查詢 await queryWordWithAI(cleanWord, position) } } const handleCostConfirm = async () => { if (!showCostConfirm) return const confirmed = await onWordCostConfirm?.(showCostConfirm.word, showCostConfirm.cost) if (confirmed) { // 調用真實的單字查詢API try { const response = await fetch('http://localhost:5000/api/ai/query-word', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ word: showCostConfirm.word, sentence: text, analysisId: null // 可以傳入分析ID }) }) if (response.ok) { const result = await response.json() if (result.success) { // 更新分析資料 const newAnalysis = { ...analysis, [showCostConfirm.word]: result.data.analysis } setPopupPosition({...showCostConfirm.position, showBelow: false}) setSelectedWord(showCostConfirm.word) onWordClick?.(showCostConfirm.word, result.data.analysis) } } } catch (error) { console.error('Query word API error:', error) // 回退到現有資料 const wordAnalysis = analysis?.[showCostConfirm.word] if (wordAnalysis) { setPopupPosition({...showCostConfirm.position, showBelow: false}) setSelectedWord(showCostConfirm.word) onWordClick?.(showCostConfirm.word, wordAnalysis) } } } setShowCostConfirm(null) } const closePopup = () => { setSelectedWord(null) } const handleSaveWord = async () => { if (!selectedWord || !analysis?.[selectedWord] || !onSaveWord) return setIsSavingWord(true) try { await onSaveWord(selectedWord, analysis[selectedWord]) setSelectedWord(null) // 保存成功後關閉popup } catch (error) { console.error('Save word error:', error) alert(`保存詞彙失敗: ${error instanceof Error ? error.message : '未知錯誤'}`) } finally { setIsSavingWord(false) } } const queryWordWithAI = async (word: string, position: { x: number, y: number, showBelow: boolean }) => { try { console.log(`🤖 查詢單字: ${word}`) const response = await fetch('http://localhost:5000/api/ai/query-word', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ word: word, sentence: text, analysisId: null }) }) if (response.ok) { const result = await response.json() console.log('AI 查詢結果:', result) if (result.success && result.data?.analysis) { // 將新的分析資料通知父組件 onNewWordAnalysis?.(word, result.data.analysis) // 顯示分析結果 setPopupPosition(position) setSelectedWord(word) onWordClick?.(word, result.data.analysis) } else { alert(`❌ 查詢 "${word}" 失敗,請稍後再試`) } } else { throw new Error(`API 錯誤: ${response.status}`) } } catch (error) { console.error('AI 查詢錯誤:', error) alert(`❌ 查詢 "${word}" 時發生錯誤,請稍後再試`) } } const getWordClass = (word: string) => { const cleanWord = word.toLowerCase().replace(/[.,!?;:]/g, '') const wordAnalysis = analysis?.[cleanWord] const baseClass = "cursor-pointer transition-all duration-200 rounded relative mx-0.5 px-1 py-0.5" if (wordAnalysis) { // 有預存資料的詞彙 const isHighValue = getWordProperty(wordAnalysis, 'isHighValue') const isPhrase = getWordProperty(wordAnalysis, 'isPhrase') // 高價值片語(黃色系) if (isHighValue && isPhrase) { return `${baseClass} bg-yellow-100 border-2 border-yellow-400 hover:bg-yellow-200 hover:shadow-sm transform hover:-translate-y-0.5` } // 高價值單字(綠色系) if (isHighValue && !isPhrase) { return `${baseClass} bg-green-100 border-2 border-green-400 hover:bg-green-200 hover:shadow-sm transform hover:-translate-y-0.5` } // 普通單字(藍色系) return `${baseClass} bg-blue-100 border-2 border-blue-300 hover:bg-blue-200 hover:shadow-sm` } else { // 無預存資料的詞彙(灰色虛線,表示需要即時查詢) return `${baseClass} border-2 border-dashed border-gray-300 hover:border-gray-400 bg-gray-50 hover:bg-gray-100` } } return (
{/* 點擊區域遮罩 */} {selectedWord && (
)} {/* 文字內容 */}
{words.map((word, index) => { // 如果是空格或標點,直接顯示 if (word.trim() === '' || /^[.,!?;:\s]+$/.test(word)) { return {word} } const className = getWordClass(word) const cleanWord = word.toLowerCase().replace(/[.,!?;:]/g, '') const wordAnalysis = analysis?.[cleanWord] const isHighValue = wordAnalysis?.isHighValue return ( handleWordClick(word, e)} > {word} {isHighValue && ( )} ) })}
{/* 詞卡風格詞彙彈窗 */} {selectedWord && analysis?.[selectedWord] && (
{/* 標題區 - 漸層背景 */}
{/* 關閉按鈕 - 獨立一行 */}
{/* 詞彙標題 */}

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

{/* 詞性、發音、播放按鈕、CEFR */}
{getWordProperty(analysis[selectedWord], 'partOfSpeech')} {getWordProperty(analysis[selectedWord], 'pronunciation')}
{/* CEFR標籤 - 在播放按鈕那一行的最右邊 */} { const difficulty = getWordProperty(analysis[selectedWord], 'difficultyLevel') return difficulty === 'A1' || difficulty === 'A2' ? 'bg-green-100 text-green-700 border-green-200' : difficulty === 'B1' || difficulty === 'B2' ? 'bg-yellow-100 text-yellow-700 border-yellow-200' : difficulty === 'C1' ? 'bg-red-100 text-red-700 border-red-200' : difficulty === 'C2' ? 'bg-purple-100 text-purple-700 border-purple-200' : 'bg-gray-100 text-gray-700 border-gray-200' })() }`}> {getWordProperty(analysis[selectedWord], 'difficultyLevel')}
{/* 內容區 - 彩色區塊設計 */}
{/* 重點學習標記 */} {getWordProperty(analysis[selectedWord], 'isHighValue') && (
🎯
重點學習詞彙
⭐⭐⭐⭐⭐
)} {/* 翻譯區塊 - 綠色 */}

中文翻譯

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

{/* 定義區塊 - 灰色 */}

英文定義

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

{/* 同義詞區塊 - 紫色 */} {getWordProperty(analysis[selectedWord], 'synonyms')?.length > 0 && (

同義詞

{getWordProperty(analysis[selectedWord], 'synonyms')?.slice(0, 4).map((synonym: string, idx: number) => ( {synonym} ))}
)}
{/* 保存按鈕 - 詞卡風格 */} {onSaveWord && (
)}
)} {/* 收費確認對話框 */} {showCostConfirm && ( <>
setShowCostConfirm(null)} />

{showCostConfirm.word}

💰
低價值詞彙(需消耗額度)
此查詢將消耗 {showCostConfirm.cost} 次 使用額度
剩餘額度:{remainingUsage}
)}
) }