187 lines
6.4 KiB
TypeScript
187 lines
6.4 KiB
TypeScript
'use client'
|
||
|
||
import { useState } from 'react'
|
||
|
||
// 模擬分析後的詞彙資料
|
||
interface WordAnalysis {
|
||
word: string
|
||
translation: string
|
||
definition: string
|
||
partOfSpeech: string
|
||
pronunciation: string
|
||
synonyms: string[]
|
||
isPhrase: boolean
|
||
phraseInfo?: {
|
||
phrase: string
|
||
meaning: string
|
||
warning: string
|
||
}
|
||
}
|
||
|
||
interface ClickableTextProps {
|
||
text: string
|
||
analysis?: Record<string, WordAnalysis>
|
||
onWordClick?: (word: string, analysis: WordAnalysis) => void
|
||
}
|
||
|
||
export function ClickableText({ text, analysis, onWordClick }: ClickableTextProps) {
|
||
const [selectedWord, setSelectedWord] = useState<string | null>(null)
|
||
const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0 })
|
||
|
||
// 將文字分割成單字
|
||
const words = text.split(/(\s+|[.,!?;:])/g).filter(word => word.trim())
|
||
|
||
const handleWordClick = (word: string, event: React.MouseEvent) => {
|
||
const cleanWord = word.toLowerCase().replace(/[.,!?;:]/g, '')
|
||
const wordAnalysis = analysis?.[cleanWord]
|
||
|
||
if (wordAnalysis) {
|
||
const rect = event.currentTarget.getBoundingClientRect()
|
||
setPopupPosition({
|
||
x: rect.left + rect.width / 2,
|
||
y: rect.top - 10
|
||
})
|
||
setSelectedWord(cleanWord)
|
||
onWordClick?.(cleanWord, wordAnalysis)
|
||
}
|
||
}
|
||
|
||
const closePopup = () => {
|
||
setSelectedWord(null)
|
||
}
|
||
|
||
const getWordClass = (word: string) => {
|
||
const cleanWord = word.toLowerCase().replace(/[.,!?;:]/g, '')
|
||
const wordAnalysis = analysis?.[cleanWord]
|
||
|
||
if (!wordAnalysis) return "cursor-default"
|
||
|
||
const baseClass = "cursor-pointer transition-all duration-200 hover:bg-blue-100 rounded px-1"
|
||
|
||
if (wordAnalysis.isPhrase) {
|
||
return `${baseClass} bg-yellow-100 border-b-2 border-yellow-400 hover:bg-yellow-200`
|
||
}
|
||
|
||
return `${baseClass} hover:bg-blue-200 border-b border-blue-300`
|
||
}
|
||
|
||
return (
|
||
<div className="relative">
|
||
{/* 點擊區域遮罩 */}
|
||
{selectedWord && (
|
||
<div
|
||
className="fixed inset-0 z-10"
|
||
onClick={closePopup}
|
||
/>
|
||
)}
|
||
|
||
{/* 文字內容 */}
|
||
<div className="text-lg leading-relaxed">
|
||
{words.map((word, index) => {
|
||
if (word.trim() === '') return <span key={index}>{word}</span>
|
||
|
||
return (
|
||
<span
|
||
key={index}
|
||
className={getWordClass(word)}
|
||
onClick={(e) => handleWordClick(word, e)}
|
||
>
|
||
{word}
|
||
</span>
|
||
)
|
||
})}
|
||
</div>
|
||
|
||
{/* 彈出視窗 */}
|
||
{selectedWord && analysis?.[selectedWord] && (
|
||
<div
|
||
className="fixed z-20 bg-white border border-gray-300 rounded-lg shadow-lg p-4 w-80 max-w-sm"
|
||
style={{
|
||
left: `${popupPosition.x}px`,
|
||
top: `${popupPosition.y}px`,
|
||
transform: 'translate(-50%, -100%)',
|
||
}}
|
||
>
|
||
<div className="space-y-3">
|
||
{/* 標題 */}
|
||
<div className="flex items-center justify-between">
|
||
<h3 className="text-lg font-bold text-gray-900">
|
||
{analysis[selectedWord].word}
|
||
</h3>
|
||
<button
|
||
onClick={closePopup}
|
||
className="text-gray-400 hover:text-gray-600"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
|
||
{/* 片語警告 */}
|
||
{analysis[selectedWord].isPhrase && analysis[selectedWord].phraseInfo && (
|
||
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
||
<div className="flex items-start gap-2">
|
||
<div className="text-yellow-600 text-lg">⚠️</div>
|
||
<div>
|
||
<div className="text-sm font-medium text-yellow-800">
|
||
注意:這個單字屬於片語
|
||
</div>
|
||
<div className="text-sm text-yellow-700 mt-1">
|
||
<strong>片語:</strong>{analysis[selectedWord].phraseInfo.phrase}
|
||
</div>
|
||
<div className="text-sm text-yellow-700">
|
||
<strong>意思:</strong>{analysis[selectedWord].phraseInfo.meaning}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 詞性和發音 */}
|
||
<div className="flex items-center gap-4">
|
||
<span className="text-sm bg-gray-100 px-2 py-1 rounded">
|
||
{analysis[selectedWord].partOfSpeech}
|
||
</span>
|
||
<span className="text-sm text-gray-600">
|
||
{analysis[selectedWord].pronunciation}
|
||
</span>
|
||
<button className="text-blue-600 hover:text-blue-800">
|
||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.536 8.464a5 5 0 010 7.072m2.828-9.9a9 9 0 010 12.728M5.586 15H4a1 1 0 01-1-1v-4a1 1 0 011-1h1.586l4.707-4.707C10.923 3.663 12 4.109 12 5v14c0 .891-1.077 1.337-1.707.707L5.586 15z" />
|
||
</svg>
|
||
</button>
|
||
</div>
|
||
|
||
{/* 翻譯 */}
|
||
<div>
|
||
<div className="text-sm font-medium text-gray-700">翻譯</div>
|
||
<div className="text-base text-gray-900">{analysis[selectedWord].translation}</div>
|
||
</div>
|
||
|
||
{/* 定義 */}
|
||
<div>
|
||
<div className="text-sm font-medium text-gray-700">定義</div>
|
||
<div className="text-sm text-gray-600">{analysis[selectedWord].definition}</div>
|
||
</div>
|
||
|
||
{/* 同義詞 */}
|
||
{analysis[selectedWord].synonyms.length > 0 && (
|
||
<div>
|
||
<div className="text-sm font-medium text-gray-700">同義詞</div>
|
||
<div className="flex flex-wrap gap-1 mt-1">
|
||
{analysis[selectedWord].synonyms.map((synonym, idx) => (
|
||
<span
|
||
key={idx}
|
||
className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full"
|
||
>
|
||
{synonym}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
} |