fix: 優化詞彙標記樣式與片語點擊功能

- 還原例句中詞彙樣式為簡潔設計 (rounded, px-1 py-0.5)
- 實現片語標籤點擊顯示詳細彈窗功能
- 修正假資料結構,區分cut動詞和cut someone some slack片語
- 調整片語標籤樣式與例句詞彙保持一致
- 修復Console錯誤和語法問題

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
鄭沛軒 2025-09-21 21:31:08 +08:00
parent fb89cf1a33
commit 11e19d5e1c
2 changed files with 138 additions and 18 deletions

View File

@ -18,6 +18,11 @@ function GenerateContent() {
const [finalText, setFinalText] = useState('')
const [usageCount, setUsageCount] = useState(0)
const [isPremium] = useState(true)
const [phrasePopup, setPhrasePopup] = useState<{
phrase: string
analysis: any
position: { x: number, y: number }
} | null>(null)
// 處理句子分析 - 使用假資料測試
@ -120,16 +125,16 @@ function GenerateContent() {
exampleTranslation: "我們去公園吧。"
},
"cut": {
word: "cut someone some slack",
translation: "對某人寬容一點",
definition: "to be more lenient or forgiving with someone",
partOfSpeech: "idiom",
pronunciation: "/kʌt ˈsʌmwʌn sʌm slæk/",
difficultyLevel: "B2",
isPhrase: true,
synonyms: ["be lenient", "be forgiving", "give leeway"],
example: "Cut him some slack, he's new here.",
exampleTranslation: "對他寬容一點,他是新來的。"
word: "cut",
translation: "切;削減",
definition: "to use a knife or other sharp tool to divide something",
partOfSpeech: "verb",
pronunciation: "/kʌt/",
difficultyLevel: "A2",
isPhrase: false,
synonyms: ["slice", "chop", "reduce"],
example: "Please cut the apple.",
exampleTranslation: "請切蘋果。"
},
"her": {
word: "her",
@ -250,7 +255,19 @@ function GenerateContent() {
synonyms: ["becomes", "obtains"],
example: "It gets cold at night.",
exampleTranslation: "晚上會變冷。"
}
},
"cut someone some slack": {
word: "cut someone some slack",
translation: "對某人寬容一點",
definition: "to be more lenient or forgiving with someone",
partOfSpeech: "idiom",
pronunciation: "/kʌt ˈsʌmwʌn sʌm slæk/",
difficultyLevel: "B2",
isPhrase: true,
synonyms: ["be lenient", "be forgiving", "give leeway"],
example: "Cut him some slack, he's new here.",
exampleTranslation: "對他寬容一點,他是新來的。"
},
}
// 設定結果 - 包含語法錯誤情境
@ -621,7 +638,7 @@ function GenerateContent() {
{/* 句子主體展示 */}
<div className="text-left mb-8">
<div className="text-3xl font-medium text-gray-900 mb-6" style={{lineHeight: '2.5'}}>
<div className="text-3xl font-medium text-gray-900 mb-6" >
<ClickableTextV2
text={finalText}
analysis={sentenceAnalysis}
@ -684,13 +701,26 @@ function GenerateContent() {
{phrases.map((phrase, index) => (
<span
key={index}
className="bg-white text-gray-700 px-3 py-1 rounded-full text-sm border border-gray-200 cursor-pointer"
onClick={() => {
console.log('Clicked phrase:', phrase)
className="cursor-pointer transition-all duration-200 rounded-lg relative mx-0.5 px-1 py-0.5 inline-flex items-center gap-1 bg-blue-50 border border-blue-200 hover:bg-blue-100 hover:shadow-lg transform hover:-translate-y-0.5 text-blue-700 font-medium"
onClick={(e) => {
// 找到片語的完整分析資料
const phraseAnalysis = sentenceAnalysis?.["cut someone some slack"]
if (phraseAnalysis) {
// 設定片語彈窗狀態
setPhrasePopup({
phrase: phrase.phrase,
analysis: phraseAnalysis,
position: {
x: e.currentTarget.getBoundingClientRect().left + e.currentTarget.getBoundingClientRect().width / 2,
y: e.currentTarget.getBoundingClientRect().bottom + 10
}
})
}
}}
title={`${phrase.phrase}: ${phrase.meaning}`}
>
"{phrase.phrase}"
{phrase.phrase}
</span>
))}
</div>
@ -712,6 +742,96 @@ function GenerateContent() {
</div>
</div>
)}
{/* 片語彈窗 */}
{phrasePopup && (
<>
<div
className="fixed inset-0 bg-black bg-opacity-50 z-40"
onClick={() => setPhrasePopup(null)}
/>
<div
className="fixed z-50 bg-white rounded-xl shadow-lg w-96 max-w-md overflow-hidden"
style={{
left: `${phrasePopup.position.x}px`,
top: `${phrasePopup.position.y}px`,
transform: 'translate(-50%, 8px)',
maxHeight: '85vh',
overflowY: 'auto'
}}
>
<div className="bg-gradient-to-br from-blue-50 to-indigo-50 p-5 border-b border-blue-200">
<div className="flex justify-end mb-3">
<button
onClick={() => setPhrasePopup(null)}
className="text-gray-400 hover:text-gray-600 w-6 h-6 rounded-full bg-white bg-opacity-80 hover:bg-opacity-100 transition-all flex items-center justify-center"
>
</button>
</div>
<div className="mb-3">
<h3 className="text-2xl font-bold text-gray-900">{phrasePopup.analysis.word}</h3>
</div>
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-sm bg-gray-100 text-gray-700 px-3 py-1 rounded-full">
{phrasePopup.analysis.partOfSpeech}
</span>
<span className="text-base text-gray-600">{phrasePopup.analysis.pronunciation}</span>
</div>
<span className="px-3 py-1 rounded-full text-sm font-medium border bg-blue-100 text-blue-700 border-blue-200">
{phrasePopup.analysis.difficultyLevel}
</span>
</div>
</div>
<div className="p-4 space-y-4">
<div className="bg-green-50 rounded-lg p-3 border border-green-200">
<h4 className="font-semibold text-green-900 mb-2 text-left text-sm"></h4>
<p className="text-green-800 font-medium text-left">{phrasePopup.analysis.translation}</p>
</div>
<div className="bg-gray-50 rounded-lg p-3 border border-gray-200">
<h4 className="font-semibold text-gray-900 mb-2 text-left text-sm"></h4>
<p className="text-gray-700 text-left text-sm leading-relaxed">{phrasePopup.analysis.definition}</p>
</div>
{phrasePopup.analysis.example && (
<div className="bg-blue-50 rounded-lg p-3 border border-blue-200">
<h4 className="font-semibold text-blue-900 mb-2 text-left text-sm"></h4>
<div className="space-y-2">
<p className="text-blue-800 text-left text-sm italic">
"{phrasePopup.analysis.example}"
</p>
<p className="text-blue-700 text-left text-sm">
{phrasePopup.analysis.exampleTranslation}
</p>
</div>
</div>
)}
</div>
<div className="p-4 pt-2">
<button
onClick={async () => {
try {
await handleSaveWord(phrasePopup.phrase, phrasePopup.analysis)
setPhrasePopup(null)
} catch (error) {
console.error('Save phrase error:', error)
}
}}
className="w-full bg-primary text-white py-3 rounded-lg font-medium hover:bg-primary-hover transition-colors flex items-center justify-center gap-2"
>
<span className="font-medium"></span>
</button>
</div>
</div>
</>
)}
</div>
</div>
)

View File

@ -104,7 +104,7 @@ export function ClickableTextV2({
const getWordClass = (word: string) => {
const wordAnalysis = findWordAnalysis(word)
const baseClass = "cursor-pointer transition-all duration-200 rounded-lg relative mx-0.5 my-1 px-2 py-1 inline-flex items-center gap-1"
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')
@ -275,7 +275,7 @@ export function ClickableTextV2({
return (
<div className="relative">
<div className="text-lg" leading-relaxed>
<div className="text-lg" style={{lineHeight: '2.5'}}>
{words.map((word, index) => {
if (word.trim() === '' || /^[.,!?;:\s]+$/.test(word)) {
return <span key={index}>{word}</span>