diff --git a/frontend/app/generate/page.tsx b/frontend/app/generate/page.tsx
index 002e37a..c08cc4b 100644
--- a/frontend/app/generate/page.tsx
+++ b/frontend/app/generate/page.tsx
@@ -20,56 +20,265 @@ function GenerateContent() {
const [isPremium] = useState(true)
- // 處理句子分析 - 使用真實API
+ // 處理句子分析 - 使用假資料測試
const handleAnalyzeSentence = async () => {
- console.log('🚀 handleAnalyzeSentence 被調用 (真實API模式)')
- console.log('📝 輸入文本:', textInput)
-
- if (!textInput.trim()) {
- console.log('❌ 文本為空,退出')
- return
- }
+ console.log('🚀 handleAnalyzeSentence 被調用 (假資料模式)')
setIsAnalyzing(true)
try {
- // 取得用戶設定的程度
- const userLevel = localStorage.getItem('userEnglishLevel') || 'A2'
- console.log('🎯 使用用戶程度:', userLevel)
+ // 模擬API延遲
+ await new Promise(resolve => setTimeout(resolve, 1000))
- const response = await fetch('http://localhost:5000/api/ai/analyze-sentence', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
+ // 使用有語法錯誤的測試句子
+ const testSentence = "She just join the team, so let's cut her some slack until she get used to the workflow."
+
+ // 假資料:完整詞彙分析結果 (包含句子中的所有詞彙)
+ const mockAnalysis = {
+ "she": {
+ word: "she",
+ translation: "她",
+ definition: "female person pronoun",
+ partOfSpeech: "pronoun",
+ pronunciation: "/ʃiː/",
+ difficultyLevel: "A1",
+ isPhrase: false,
+ synonyms: ["her"],
+ example: "She is a teacher.",
+ exampleTranslation: "她是一名老師。"
},
- body: JSON.stringify({
- inputText: textInput,
- userLevel: userLevel, // 傳遞用戶程度
- analysisMode: 'full'
- })
- })
-
- if (response.ok) {
- const result = await response.json()
- console.log('✅ API分析完成:', result)
-
- if (result.success) {
- // 嘗試不同的屬性名稱格式
- const wordAnalysisData = result.data.WordAnalysis || result.data.wordAnalysis || {};
- console.log('🔍 設置sentenceAnalysis:', wordAnalysisData);
- setSentenceAnalysis(wordAnalysisData)
- setSentenceMeaning(result.data.SentenceMeaning?.Translation || '')
- setGrammarCorrection(result.data.GrammarCorrection || null)
- setFinalText(result.data.FinalAnalysisText || textInput)
- setShowAnalysisView(true)
- setUsageCount(prev => prev + 1)
- } else {
- throw new Error(result.error || '分析失敗')
+ "just": {
+ word: "just",
+ translation: "剛剛;僅僅",
+ definition: "recently; only",
+ partOfSpeech: "adverb",
+ pronunciation: "/dʒʌst/",
+ difficultyLevel: "A2",
+ isPhrase: false,
+ synonyms: ["recently", "only", "merely"],
+ example: "I just arrived.",
+ exampleTranslation: "我剛到。"
+ },
+ "join": {
+ word: "join",
+ translation: "加入",
+ definition: "to become a member of",
+ partOfSpeech: "verb",
+ pronunciation: "/dʒɔɪn/",
+ difficultyLevel: "B1",
+ isPhrase: false,
+ synonyms: ["enter", "become part of"],
+ example: "I want to join the team.",
+ exampleTranslation: "我想加入團隊。"
+ },
+ "the": {
+ word: "the",
+ translation: "定冠詞",
+ definition: "definite article",
+ partOfSpeech: "article",
+ pronunciation: "/ðə/",
+ difficultyLevel: "A1",
+ isPhrase: false,
+ synonyms: [],
+ example: "The cat is sleeping.",
+ exampleTranslation: "貓在睡覺。"
+ },
+ "team": {
+ word: "team",
+ translation: "團隊",
+ definition: "a group of people working together",
+ partOfSpeech: "noun",
+ pronunciation: "/tiːm/",
+ difficultyLevel: "A2",
+ isPhrase: false,
+ synonyms: ["group", "crew"],
+ example: "Our team works well together.",
+ exampleTranslation: "我們的團隊合作得很好。"
+ },
+ "so": {
+ word: "so",
+ translation: "所以;如此",
+ definition: "therefore; to such a degree",
+ partOfSpeech: "adverb",
+ pronunciation: "/soʊ/",
+ difficultyLevel: "A1",
+ isPhrase: false,
+ synonyms: ["therefore", "thus"],
+ example: "It was raining, so I stayed home.",
+ exampleTranslation: "下雨了,所以我待在家裡。"
+ },
+ "let's": {
+ word: "let's",
+ translation: "讓我們",
+ definition: "let us (contraction)",
+ partOfSpeech: "contraction",
+ pronunciation: "/lets/",
+ difficultyLevel: "A1",
+ isPhrase: false,
+ synonyms: ["let us"],
+ example: "Let's go to the park.",
+ 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: "對他寬容一點,他是新來的。"
+ },
+ "her": {
+ word: "her",
+ translation: "她的;她",
+ definition: "belonging to or associated with a female",
+ partOfSpeech: "pronoun",
+ pronunciation: "/hər/",
+ difficultyLevel: "A1",
+ isPhrase: false,
+ synonyms: ["hers"],
+ example: "This is her book.",
+ exampleTranslation: "這是她的書。"
+ },
+ "some": {
+ word: "some",
+ translation: "一些",
+ definition: "an unspecified amount or number of",
+ partOfSpeech: "determiner",
+ pronunciation: "/sʌm/",
+ difficultyLevel: "A1",
+ isPhrase: false,
+ synonyms: ["several", "a few"],
+ example: "I need some help.",
+ exampleTranslation: "我需要一些幫助。"
+ },
+ "slack": {
+ word: "slack",
+ translation: "寬鬆;懈怠",
+ definition: "looseness; lack of tension",
+ partOfSpeech: "noun",
+ pronunciation: "/slæk/",
+ difficultyLevel: "B1",
+ isPhrase: false,
+ synonyms: ["looseness", "leeway"],
+ example: "There's too much slack in this rope.",
+ exampleTranslation: "這條繩子太鬆了。"
+ },
+ "until": {
+ word: "until",
+ translation: "直到",
+ definition: "up to a particular time",
+ partOfSpeech: "preposition",
+ pronunciation: "/ʌnˈtɪl/",
+ difficultyLevel: "A2",
+ isPhrase: false,
+ synonyms: ["till", "up to"],
+ example: "Wait until tomorrow.",
+ exampleTranslation: "等到明天。"
+ },
+ "get": {
+ word: "get",
+ translation: "變得;獲得",
+ definition: "to become or obtain",
+ partOfSpeech: "verb",
+ pronunciation: "/ɡet/",
+ difficultyLevel: "A1",
+ isPhrase: false,
+ synonyms: ["become", "obtain"],
+ example: "I get tired easily.",
+ exampleTranslation: "我很容易累。"
+ },
+ "used": {
+ word: "used",
+ translation: "習慣的",
+ definition: "familiar with something (used to)",
+ partOfSpeech: "adjective",
+ pronunciation: "/juːzd/",
+ difficultyLevel: "A2",
+ isPhrase: false,
+ synonyms: ["accustomed", "familiar"],
+ example: "I'm not used to this weather.",
+ exampleTranslation: "我不習慣這種天氣。"
+ },
+ "to": {
+ word: "to",
+ translation: "到;向",
+ definition: "preposition expressing direction",
+ partOfSpeech: "preposition",
+ pronunciation: "/tu/",
+ difficultyLevel: "A1",
+ isPhrase: false,
+ synonyms: [],
+ example: "I'm going to school.",
+ exampleTranslation: "我要去學校。"
+ },
+ "workflow": {
+ word: "workflow",
+ translation: "工作流程",
+ definition: "the sequence of processes through which work passes",
+ partOfSpeech: "noun",
+ pronunciation: "/ˈwɜːrkfloʊ/",
+ difficultyLevel: "B2",
+ isPhrase: false,
+ synonyms: ["process", "procedure", "system"],
+ example: "We need to improve our workflow.",
+ exampleTranslation: "我們需要改善工作流程。"
+ },
+ "joined": {
+ word: "joined",
+ translation: "加入",
+ definition: "became a member of (past tense of join)",
+ partOfSpeech: "verb",
+ pronunciation: "/dʒɔɪnd/",
+ difficultyLevel: "B1",
+ isPhrase: false,
+ synonyms: ["entered", "became part of"],
+ example: "He joined the company last year.",
+ exampleTranslation: "他去年加入了這家公司。"
+ },
+ "gets": {
+ word: "gets",
+ translation: "變得;獲得",
+ definition: "becomes or obtains (third person singular)",
+ partOfSpeech: "verb",
+ pronunciation: "/ɡets/",
+ difficultyLevel: "A1",
+ isPhrase: false,
+ synonyms: ["becomes", "obtains"],
+ example: "It gets cold at night.",
+ exampleTranslation: "晚上會變冷。"
}
- } else {
- throw new Error(`API錯誤: ${response.status}`)
}
+
+ // 設定結果 - 包含語法錯誤情境
+ setFinalText("She just joined the team, so let's cut her some slack until she gets used to the workflow.") // 修正後的句子
+ setSentenceAnalysis(mockAnalysis)
+ setSentenceMeaning("她剛加入團隊,所以讓我們對她寬容一點,直到她習慣工作流程。")
+ setGrammarCorrection({
+ hasErrors: true,
+ originalText: testSentence, // 有錯誤的原始句子
+ correctedText: "She just joined the team, so let's cut her some slack until she gets used to the workflow.",
+ corrections: [
+ {
+ error: "join",
+ correction: "joined",
+ type: "時態錯誤",
+ explanation: "第三人稱單數過去式應使用 'joined'"
+ },
+ {
+ error: "get",
+ correction: "gets",
+ type: "時態錯誤",
+ explanation: "第三人稱單數現在式應使用 'gets'"
+ }
+ ]
+ })
+ setShowAnalysisView(true)
+
+ console.log('✅ 假資料設定完成')
} catch (error) {
console.error('Error in real API analysis:', error)
alert(`分析句子時發生錯誤: ${error instanceof Error ? error.message : '未知錯誤'}`)
@@ -347,47 +556,149 @@ function GenerateContent() {
{/* 主句子展示 - 最重要的內容 */}
+ {/* 詞彙統計卡片區 */}
+ {sentenceAnalysis && (() => {
+ // 計算各類詞彙數量
+ const userLevel = localStorage.getItem('userEnglishLevel') || 'A2'
+ const getLevelIndex = (level: string): number => {
+ const levels = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2']
+ return levels.indexOf(level)
+ }
+
+ let simpleCount = 0
+ let moderateCount = 0
+ let difficultCount = 0
+ let phraseCount = 0
+
+ Object.entries(sentenceAnalysis).forEach(([word, wordData]: [string, any]) => {
+ const isPhrase = wordData?.isPhrase || wordData?.IsPhrase
+ const difficultyLevel = wordData?.difficultyLevel || 'A1'
+
+ if (isPhrase) {
+ phraseCount++
+ } else {
+ const userIndex = getLevelIndex(userLevel)
+ const wordIndex = getLevelIndex(difficultyLevel)
+
+ if (userIndex > wordIndex) {
+ simpleCount++
+ } else if (userIndex === wordIndex) {
+ moderateCount++
+ } else {
+ difficultCount++
+ }
+ }
+ })
+
+ return (
+
+ {/* 簡單詞彙卡片 */}
+
+
+ {/* 適中詞彙卡片 */}
+
+
{moderateCount}
+
適中詞彙
+
+
+ {/* 艱難詞彙卡片 */}
+
+
{difficultCount}
+
艱難詞彙
+
+
+ {/* 片語與俚語卡片 */}
+
+
+ )
+ })()}
+
{/* 句子主體展示 */}
-
-
+
+
{
console.log('Clicked word:', word, analysis)
}}
- onWordCostConfirm={async () => {
- return true
- }}
onSaveWord={handleSaveWord}
/>
- {/* 翻譯 - 次要但重要 */}
-
- {sentenceMeaning}
+ {/* 翻譯 - 參考翻卡背面設計 */}
+
+
中文翻譯
+
{sentenceMeaning}
+
+ {/* 片語和慣用語展示區 */}
+ {(() => {
+ if (!sentenceAnalysis) return null
+
+ // 提取片語
+ const phrases: Array<{
+ phrase: string
+ meaning: string
+ difficultyLevel: string
+ }> = []
+
+ Object.entries(sentenceAnalysis).forEach(([word, wordData]: [string, any]) => {
+ const isPhrase = wordData?.isPhrase || wordData?.IsPhrase
+ if (isPhrase) {
+ phrases.push({
+ phrase: wordData?.word || word,
+ meaning: wordData?.translation || '',
+ difficultyLevel: wordData?.difficultyLevel || 'A1'
+ })
+ }
+ })
+
+ if (phrases.length === 0) return null
+
+ // 獲取CEFR等級顏色
+ const getCEFRColor = (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'
+ }
+ }
+
+ return (
+
+
片語俚語
+
+ {phrases.map((phrase, index) => (
+ {
+ console.log('Clicked phrase:', phrase)
+ }}
+ title={`${phrase.phrase}: ${phrase.meaning}`}
+ >
+ "{phrase.phrase}"
+
+ ))}
+
+
+ )
+ })()}
- {/* 學習提示 - 精簡版 */}
-
{/* 下方操作區 - 簡化 */}
@@ -396,7 +707,6 @@ function GenerateContent() {
onClick={() => setShowAnalysisView(false)}
className="px-8 py-3 bg-primary text-white rounded-lg font-medium hover:bg-primary-hover transition-colors flex items-center gap-2"
>
-
🔄
分析新句子
diff --git a/frontend/components/ClickableTextV2.tsx b/frontend/components/ClickableTextV2.tsx
index fc0cd46..abe1e4a 100644
--- a/frontend/components/ClickableTextV2.tsx
+++ b/frontend/components/ClickableTextV2.tsx
@@ -3,33 +3,6 @@
import { useState, useEffect } from 'react'
import { createPortal } from 'react-dom'
-/**
- * ClickableTextV2 組件架構說明
- *
- * 🎯 **Portal 設計解決方案**:
- * - 使用 React Portal 將彈窗渲染到 document.body
- * - 完全脫離父級 CSS 繼承,避免樣式污染
- * - 確保彈窗樣式與 vocab-designs 頁面的詞卡風格完全一致
- *
- * 🏗️ **組件架構**:
- * ```
- * ClickableTextV2
- * ├─ 文字容器 (text-lg) - 可點擊文字
- * └─ VocabPopup (Portal) - 彈窗渲染到 body,不受父級影響
- * ```
- *
- * ✅ **解決的問題**:
- * - CSS 繼承問題:Portal 完全脫離父級樣式
- * - 字體大小問題:彈窗使用標準字體大小
- * - 對齊問題:彈窗內容正確左對齊
- *
- * 🔧 **技術要點**:
- * - createPortal() 渲染到 document.body
- * - mounted state 確保只在客戶端渲染
- * - 保持原有 API 和功能不變
- */
-
-// 更新的詞彙分析介面
interface WordAnalysis {
word: string
translation: string
@@ -39,73 +12,51 @@ interface WordAnalysis {
synonyms: string[]
antonyms?: string[]
isPhrase: boolean
- isHighValue: boolean // 高學習價值標記
- learningPriority: 'high' | 'medium' | 'low' // 學習優先級
+ isHighValue: boolean
+ learningPriority: 'high' | 'medium' | 'low'
phraseInfo?: {
phrase: string
meaning: string
warning: string
- colorCode: string // 片語顏色代碼
+ colorCode: string
}
difficultyLevel: string
- costIncurred?: number // 點擊此詞彙的成本
+ 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 // 收費確認
- onSaveWord?: (word: string, analysis: WordAnalysis) => Promise // 保存詞彙回調
- remainingUsage?: number // 剩餘使用次數
+ onSaveWord?: (word: string, analysis: WordAnalysis) => Promise
+ remainingUsage?: number
+ showPhrasesInline?: boolean
}
-// Popup 尺寸常數
const POPUP_CONFIG = {
- WIDTH: 320, // w-96 = 384px, 但實際使用320px
- HEIGHT: 400, // 估計彈窗高度
- PADDING: 16, // 最小邊距
- MOBILE_BREAKPOINT: 640 // sm斷點
+ WIDTH: 320,
+ HEIGHT: 400,
+ PADDING: 16,
+ MOBILE_BREAKPOINT: 640
} as const
export function ClickableTextV2({
text,
analysis,
onWordClick,
- onWordCostConfirm,
onSaveWord,
- remainingUsage = 5
+ remainingUsage = 5,
+ showPhrasesInline = true
}: 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 [mounted, setMounted] = useState(false)
- // 確保只在客戶端渲染 Portal
useEffect(() => {
setMounted(true)
}, [])
- // Debug: 檢查接收到的analysis prop
- useEffect(() => {
- if (analysis) {
- console.log('🔍 ClickableTextV2接收到analysis:', analysis);
- console.log('🔍 analysis的keys:', Object.keys(analysis));
- }
- }, [analysis])
-
- // 獲取CEFR等級顏色 - 與詞卡風格完全一致
const getCEFRColor = (level: string) => {
switch (level) {
case 'A1': return 'bg-green-100 text-green-700 border-green-200'
@@ -118,193 +69,95 @@ export function ClickableTextV2({
}
}
- // 輔助函數:兼容大小寫屬性名稱和處理AI資料格式
const getWordProperty = (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) // 首字母小寫
+ propName,
+ propName.toLowerCase(),
+ propName.charAt(0).toUpperCase() + propName.slice(1),
+ propName.charAt(0).toLowerCase() + propName.slice(1)
];
- let result = undefined;
for (const variation of variations) {
if (wordData[variation] !== undefined) {
- result = wordData[variation];
- break;
+ return wordData[variation];
}
}
- // 特殊處理同義詞 - 如果AI沒有提供,使用預設同義詞
if (propName === 'synonyms') {
- const synonyms = result || getSynonymsForWord(wordData?.word || '');
- return Array.isArray(synonyms) ? synonyms : [];
+ return [];
}
- // 特殊處理例句 - 優先使用AI或後端提供的例句
- if (propName === 'example') {
- return result; // 不提供預設例句,只使用AI/後端資料
- }
-
- // 特殊處理例句翻譯
- if (propName === 'exampleTranslation') {
- return result; // 不提供預設翻譯,只使用AI/後端資料
- }
-
- return result;
+ return undefined;
}
- // 統一的詞彙查找函數 - 處理大小寫不匹配問題
const findWordAnalysis = (word: string) => {
const cleanWord = word.toLowerCase().replace(/[.,!?;:]/g, '')
-
- // 嘗試多種格式匹配API回傳的keys
- return analysis?.[cleanWord] || // 小寫
- analysis?.[word] || // 原始
- analysis?.[word.toLowerCase()] || // 確保小寫
- analysis?.[word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()] || // 首字母大寫
- analysis?.[word.toUpperCase()] || // 全大寫
- null
+ return analysis?.[cleanWord] || analysis?.[word] || analysis?.[word.toLowerCase()] || null
}
- // 補充同義詞的本地函數
- const getSynonymsForWord = (word: string): string[] => {
- const synonymsMap: Record = {
- // 你的例句詞彙
- 'company': ['business', 'corporation', 'firm'],
- 'offered': ['provided', 'gave', 'presented'],
- 'bonus': ['reward', 'incentive', 'extra pay'],
- 'employees': ['workers', 'staff', 'personnel'],
- 'wanted': ['desired', 'wished for', 'sought'],
- 'benefits': ['advantages', 'perks', 'rewards'],
+ const getLevelIndex = (level: string): number => {
+ const levels = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2']
+ return levels.indexOf(level)
+ }
- // 常見詞彙
- 'the': [],
- 'a': [],
- 'an': [],
- 'and': [],
- 'but': [],
- 'or': [],
- 'even': [],
- 'more': [],
+ 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"
- // 其他詞彙可以繼續添加
- };
+ if (wordAnalysis) {
+ const isPhrase = getWordProperty(wordAnalysis, 'isPhrase')
+ const difficultyLevel = getWordProperty(wordAnalysis, 'difficultyLevel') || 'A1'
+ const userLevel = typeof window !== 'undefined' ? localStorage.getItem('userEnglishLevel') || 'A2' : 'A2'
- return synonymsMap[word.toLowerCase()] || [];
- };
+ // 如果是片語,跳過標記
+ 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 = text.split(/(\s+|[.,!?;:])/g)
const handleWordClick = 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 viewportWidth = window.innerWidth
-
- // 智能水平定位,適應不同屏幕尺寸
- let x = rect.left + rect.width / 2
- const actualPopupWidth = Math.min(POPUP_CONFIG.WIDTH, viewportWidth - 32) // 實際popup寬度
- const halfPopupWidth = actualPopupWidth / 2
-
- // 手機端特殊處理
- if (viewportWidth <= POPUP_CONFIG.MOBILE_BREAKPOINT) { // sm斷點
- // 小屏幕時居中顯示,避免邊緣問題
- x = viewportWidth / 2
- } else {
- // 大屏幕時智能調整位置
- if (x + halfPopupWidth + POPUP_CONFIG.PADDING > viewportWidth) {
- x = viewportWidth - halfPopupWidth - POPUP_CONFIG.PADDING
- }
- if (x - halfPopupWidth < POPUP_CONFIG.PADDING) {
- x = halfPopupWidth + POPUP_CONFIG.PADDING
- }
- }
-
- // 計算垂直位置
- const spaceAbove = rect.top
- const showBelow = spaceAbove < POPUP_CONFIG.HEIGHT
-
const position = {
- x: x,
- y: showBelow ? rect.bottom + 10 : rect.top - 10,
- showBelow: showBelow
+ x: rect.left + rect.width / 2,
+ y: rect.bottom + 10,
+ showBelow: true
}
- 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:詞彙不在analysis中,直接顯示空彈窗或提示
- // 因為analyze-sentence應該已經包含所有詞彙,這種情況很少發生
- setPopupPosition(position)
- setSelectedWord(cleanWord)
- onWordClick?.(cleanWord, {
- word: cleanWord,
- translation: '查詢中...',
- definition: '正在載入定義...',
- partOfSpeech: 'unknown',
- pronunciation: `/${cleanWord}/`,
- synonyms: [],
- isPhrase: false,
- isHighValue: false,
- learningPriority: 'low',
- difficultyLevel: 'A1'
- })
- }
- }
-
- const handleCostConfirm = async () => {
- if (!showCostConfirm) return
-
- const confirmed = await onWordCostConfirm?.(showCostConfirm.word, showCostConfirm.cost)
-
- if (confirmed) {
- // 由於analyze-sentence已提供完整資料,不再需要額外API調用
- // 使用智能查找尋找詞彙資料
- const wordAnalysis = findWordAnalysis(showCostConfirm.word)
-
- if (wordAnalysis) {
- setPopupPosition({...showCostConfirm.position, showBelow: false})
- setSelectedWord(showCostConfirm.word)
- onWordClick?.(showCostConfirm.word, wordAnalysis)
- } else {
- // 極少數情況:詞彙真的不在analysis中
- setPopupPosition({...showCostConfirm.position, showBelow: false})
- setSelectedWord(showCostConfirm.word)
- onWordClick?.(showCostConfirm.word, {
- word: showCostConfirm.word,
- translation: '此詞彙未在分析中',
- definition: '請重新分析句子以獲取完整資訊',
- partOfSpeech: 'unknown',
- pronunciation: `/${showCostConfirm.word}/`,
- synonyms: [],
- isPhrase: false,
- isHighValue: false,
- learningPriority: 'low',
- difficultyLevel: 'A1'
- })
- }
- }
-
- setShowCostConfirm(null)
+ setPopupPosition(position)
+ setSelectedWord(cleanWord)
+ onWordClick?.(cleanWord, wordAnalysis)
}
const closePopup = () => {
@@ -317,7 +170,7 @@ export function ClickableTextV2({
setIsSavingWord(true)
try {
await onSaveWord(selectedWord, analysis[selectedWord])
- setSelectedWord(null) // 保存成功後關閉popup
+ setSelectedWord(null)
} catch (error) {
console.error('Save word error:', error)
alert(`保存詞彙失敗: ${error instanceof Error ? error.message : '未知錯誤'}`)
@@ -326,63 +179,27 @@ export function ClickableTextV2({
}
}
-
- 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 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`
- }
- }
-
- // 詞彙彈窗組件 - 使用 Portal 渲染
const VocabPopup = () => {
if (!selectedWord || !analysis?.[selectedWord] || !mounted) return null
return createPortal(
<>
- {/* 背景遮罩 */}
- {/* 彈窗內容 - 完全脫離父級樣式 */}
- {/* 標題區 - 漸層背景 */}
- {/* 關閉按鈕 - 獨立一行 */}
- {/* 詞彙標題 */}
{getWordProperty(analysis[selectedWord], 'word')}
- {/* 詞性、發音、播放按鈕、CEFR */}
{getWordProperty(analysis[selectedWord], 'partOfSpeech')}
{getWordProperty(analysis[selectedWord], 'pronunciation')}
-
- {/* CEFR標籤 - 在播放按鈕那一行的最右邊 */}
{getWordProperty(analysis[selectedWord], 'difficultyLevel')}
- {/* 內容區 - 彩色區塊設計 */}
- {/* 翻譯區塊 - 綠色 */}
中文翻譯
{getWordProperty(analysis[selectedWord], 'translation')}
- {/* 定義區塊 - 灰色 */}
英文定義
{getWordProperty(analysis[selectedWord], 'definition')}
- {/* 同義詞區塊 - 紫色 */}
- {(() => {
- const synonyms = getWordProperty(analysis[selectedWord], 'synonyms');
- return synonyms && Array.isArray(synonyms) && synonyms.length > 0;
- })() && (
-
-
同義詞
-
- {getWordProperty(analysis[selectedWord], 'synonyms')?.slice(0, 4).map((synonym: string, idx: number) => (
-
- {synonym}
-
- ))}
-
-
- )}
-
- {/* 例句區塊 - 藍色 */}
{(() => {
const example = getWordProperty(analysis[selectedWord], 'example');
return example && example !== 'null' && example !== 'undefined';
@@ -468,7 +256,6 @@ export function ClickableTextV2({
)}
- {/* 保存按鈕 - 底部平均延展 */}
{onSaveWord && (
@@ -491,96 +275,29 @@ export function ClickableTextV2({
return (
- {/* 文字內容 */}
-
+
{words.map((word, index) => {
- // 如果是空格或標點,直接顯示
if (word.trim() === '' || /^[.,!?;:\s]+$/.test(word)) {
return {word}
}
const className = getWordClass(word)
- const wordAnalysis = findWordAnalysis(word)
- const isHighValue = getWordProperty(wordAnalysis, 'isHighValue')
+ const icon = getWordIcon(word)
return (
handleWordClick(word, e)}
>
{word}
- {isHighValue && (
- ⭐
- )}
+ {icon}
)
})}
- {/* 使用 Portal 渲染的詞彙彈窗 */}
-
- {/* 收費確認對話框 - 保留原有功能 */}
- {showCostConfirm && (
- <>
-
setShowCostConfirm(null)} />
-
-
-
-
- {showCostConfirm.word}
-
-
-
-
-
-
-
💰
-
-
- 低價值詞彙(需消耗額度)
-
-
- 此查詢將消耗 {showCostConfirm.cost} 次 使用額度
-
-
- 剩餘額度:{remainingUsage} 次
-
-
-
-
-
-
-
-
-
-
-
- >
- )}
)
}
\ No newline at end of file