'use client' import { useState, useEffect, use } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import { Navigation } from '@/components/shared/Navigation' import { ProtectedRoute } from '@/components/shared/ProtectedRoute' import { useToast } from '@/components/shared/Toast' import { flashcardsService, type Flashcard } from '@/lib/services/flashcards' import { imageGenerationService } from '@/lib/services/imageGeneration' interface FlashcardDetailPageProps { params: Promise<{ id: string }> } export default function FlashcardDetailPage({ params }: FlashcardDetailPageProps) { const { id } = use(params) return ( ) } function FlashcardDetailContent({ cardId }: { cardId: string }) { const router = useRouter() const searchParams = useSearchParams() const toast = useToast() const [flashcard, setFlashcard] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [isEditing, setIsEditing] = useState(false) const [editedCard, setEditedCard] = useState(null) // 圖片生成狀態 const [isGeneratingImage, setIsGeneratingImage] = useState(false) const [generationProgress, setGenerationProgress] = useState('') const [isPlayingWord, setIsPlayingWord] = useState(false) const [isPlayingExample, setIsPlayingExample] = useState(false) // TTS播放控制 - 詞彙發音 const toggleWordTTS = (text: string, lang: string = 'en-US') => { if (!('speechSynthesis' in window)) { toast.error('您的瀏覽器不支援語音播放'); return; } // 如果正在播放詞彙,則停止 if (isPlayingWord) { speechSynthesis.cancel(); setIsPlayingWord(false); return; } // 停止所有播放並開始新播放 speechSynthesis.cancel(); setIsPlayingWord(true); setIsPlayingExample(false); const utterance = new SpeechSynthesisUtterance(text); utterance.lang = lang; utterance.rate = 0.8; // 詞彙播放稍慢 utterance.pitch = 1.0; utterance.volume = 1.0; utterance.onend = () => setIsPlayingWord(false); utterance.onerror = () => { setIsPlayingWord(false); toast.error('語音播放失敗'); }; speechSynthesis.speak(utterance); } // TTS播放控制 - 例句發音 const toggleExampleTTS = (text: string, lang: string = 'en-US') => { if (!('speechSynthesis' in window)) { toast.error('您的瀏覽器不支援語音播放'); return; } // 如果正在播放例句,則停止 if (isPlayingExample) { speechSynthesis.cancel(); setIsPlayingExample(false); return; } // 停止所有播放並開始新播放 speechSynthesis.cancel(); setIsPlayingExample(true); setIsPlayingWord(false); const utterance = new SpeechSynthesisUtterance(text); utterance.lang = lang; utterance.rate = 0.9; // 例句播放正常語速 utterance.pitch = 1.0; utterance.volume = 1.0; utterance.onend = () => setIsPlayingExample(false); utterance.onerror = () => { setIsPlayingExample(false); toast.error('語音播放失敗'); }; speechSynthesis.speak(utterance); } // 假資料 - 用於展示效果 const mockCards: {[key: string]: any} = { 'mock1': { id: 'mock1', word: 'hello', translation: '你好', partOfSpeech: 'interjection', pronunciation: '/həˈloʊ/', definition: 'A greeting word used when meeting someone or beginning a phone conversation', example: 'Hello, how are you today?', exampleTranslation: '你好,你今天怎麼樣?', masteryLevel: 95, timesReviewed: 15, isFavorite: true, nextReviewDate: '2025-09-21', cardSet: { name: '基礎詞彙', color: 'bg-blue-500' }, cefr: 'A1', createdAt: '2025-09-17', synonyms: ['hi', 'greetings', 'good day'], // 添加圖片欄位 exampleImages: [], hasExampleImage: false, primaryImageUrl: null }, 'mock2': { id: 'mock2', word: 'elaborate', translation: '詳細說明', partOfSpeech: 'verb', pronunciation: '/ɪˈlæbərət/', definition: 'To explain something in more detail; to develop or present a theory, policy, or system in further detail', example: 'Could you elaborate on your proposal?', exampleTranslation: '你能詳細說明一下你的提案嗎?', masteryLevel: 45, timesReviewed: 5, isFavorite: false, nextReviewDate: '2025-09-19', cardSet: { name: '高級詞彙', color: 'bg-purple-500' }, cefr: 'B2', createdAt: '2025-09-14', synonyms: ['explain', 'detail', 'expand', 'clarify'], // 添加圖片欄位 exampleImages: [], hasExampleImage: false, primaryImageUrl: null } } // 載入詞卡資料 useEffect(() => { const loadFlashcard = async () => { try { setLoading(true) // 首先檢查是否為假資料 if (mockCards[cardId]) { setFlashcard(mockCards[cardId]) setEditedCard(mockCards[cardId]) setLoading(false) return } // 載入真實詞卡 - 使用直接 API 調用 const result = await flashcardsService.getFlashcard(cardId) if (result.success && result.data) { setFlashcard(result.data) setEditedCard(result.data) } else { throw new Error(result.error || '詞卡不存在') } } catch (err) { setError('載入詞卡時發生錯誤') } finally { setLoading(false) } } loadFlashcard() }, [cardId]) // 檢查 URL 參數,自動開啟編輯模式 useEffect(() => { if (searchParams.get('edit') === 'true' && flashcard) { setIsEditing(true) // 清理 URL 參數,保持 URL 乾淨 router.replace(`/flashcards/${cardId}`) } }, [flashcard, searchParams, cardId, router]) // 獲取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' } } // 獲取例句圖片 - 使用 API 資料 const getExampleImage = (card: Flashcard): string | null => { return card.primaryImageUrl || null } // 詞性簡寫轉換 const getPartOfSpeechDisplay = (partOfSpeech: string): string => { const shortMap: {[key: string]: string} = { 'noun': 'n.', 'verb': 'v.', 'adjective': 'adj.', 'adverb': 'adv.', 'pronoun': 'pron.', 'conjunction': 'conj.', 'preposition': 'prep.', 'interjection': 'int.', 'idiom': 'idiom' } // 處理複合詞性 (如 "preposition/adverb") if (partOfSpeech?.includes('/')) { return partOfSpeech.split('/').map(p => shortMap[p.trim()] || p.trim()).join('/') } return shortMap[partOfSpeech] || partOfSpeech || '' } // 處理收藏切換 const handleToggleFavorite = async () => { if (!flashcard) return try { // 假資料處理 if (flashcard.id.startsWith('mock')) { const updated = { ...flashcard, isFavorite: !flashcard.isFavorite } setFlashcard(updated) setEditedCard(updated) toast.success(`${flashcard.isFavorite ? '已取消收藏' : '已加入收藏'}「${flashcard.word}」`) return } // 真實API調用 const result = await flashcardsService.toggleFavorite(flashcard.id) if (result.success) { setFlashcard(prev => prev ? { ...prev, isFavorite: !prev.isFavorite } : null) toast.success(`${flashcard.isFavorite ? '已取消收藏' : '已加入收藏'}「${flashcard.word}」`) } } catch (error) { toast.error('操作失敗,請重試') } } // 處理編輯保存 const handleSaveEdit = async () => { if (!flashcard || !editedCard) return try { // 假資料處理 if (flashcard.id.startsWith('mock')) { setFlashcard(editedCard) setIsEditing(false) toast.success('詞卡更新成功!') return } // 真實API調用 const result = await flashcardsService.updateFlashcard(flashcard.id, { word: editedCard.word, translation: editedCard.translation, definition: editedCard.definition, pronunciation: editedCard.pronunciation, partOfSpeech: editedCard.partOfSpeech, example: editedCard.example, exampleTranslation: editedCard.exampleTranslation, cefr: editedCard.cefr }) if (result.success) { setFlashcard(editedCard) setIsEditing(false) toast.success('詞卡更新成功!') } else { toast.error(result.error || '更新失敗') } } catch (error) { toast.error('更新失敗,請重試') } } // 處理刪除 const handleDelete = async () => { if (!flashcard) return if (!confirm(`確定要刪除詞卡「${flashcard.word}」嗎?`)) { return } try { // 假資料處理 if (flashcard.id.startsWith('mock')) { toast.success('詞卡已刪除(模擬)') router.push('/flashcards') return } // 真實API調用 const result = await flashcardsService.deleteFlashcard(flashcard.id) if (result.success) { toast.success('詞卡已刪除') router.push('/flashcards') } else { toast.error(result.error || '刪除失敗') } } catch (error) { toast.error('刪除失敗,請重試') } } // 處理圖片生成 const handleGenerateImage = async () => { if (!flashcard || isGeneratingImage) return try { setIsGeneratingImage(true) setGenerationProgress('啟動生成中...') toast.info(`開始為「${flashcard.word}」生成例句圖片...`) const generateResult = await imageGenerationService.generateImage(flashcard.id) if (!generateResult.success || !generateResult.data) { throw new Error(generateResult.error || '啟動生成失敗') } const requestId = generateResult.data.requestId setGenerationProgress('Gemini 生成描述中...') const finalStatus = await imageGenerationService.pollUntilComplete( requestId, (status: any) => { const stage = status.stages.gemini.status === 'completed' ? 'Replicate 生成圖片中...' : 'Gemini 生成描述中...' setGenerationProgress(stage) }, 5 ) if (finalStatus.overallStatus === 'completed') { setGenerationProgress('生成完成,載入中...') // 重新載入詞卡資料 const result = await flashcardsService.getFlashcard(cardId) if (result.success && result.data) { setFlashcard(result.data) setEditedCard(result.data) } toast.success(`「${flashcard.word}」的例句圖片生成完成!`) } else { throw new Error('圖片生成未完成') } } catch (error: any) { toast.error(`圖片生成失敗: ${error.message || '未知錯誤'}`) } finally { setIsGeneratingImage(false) setGenerationProgress('') } } if (loading) { return ( 載入中... ) } if (error || !flashcard) { return ( {error || '詞卡不存在'} router.push('/flashcards')} className="px-4 py-2 bg-primary text-white rounded-lg hover:bg-primary-hover transition-colors" > 返回詞卡列表 ) } return ( {/* 導航欄 */} router.push('/flashcards')} className="text-gray-600 hover:text-gray-900 flex items-center gap-2" > 返回詞卡列表 {/* 主要詞卡內容 - 學習功能風格 */} {/* CEFR標籤 - 右上角 */} {flashcard.cefr || 'A1'} {/* 標題區 */} {flashcard.word} {getPartOfSpeechDisplay(flashcard.partOfSpeech)} {flashcard.pronunciation} toggleWordTTS(flashcard.word, 'en-US')} disabled={isPlayingExample} title={isPlayingWord ? "點擊停止播放" : "點擊聽詞彙發音"} aria-label={isPlayingWord ? `停止播放詞彙:${flashcard.word}` : `播放詞彙發音:${flashcard.word}`} className={`group relative w-12 h-12 rounded-full shadow-lg transform transition-all duration-200 ${isPlayingWord ? 'bg-gradient-to-br from-green-500 to-green-600 shadow-green-200 scale-105' : 'bg-gradient-to-br from-blue-500 to-blue-600 hover:shadow-xl hover:scale-105 shadow-blue-200' } ${isPlayingExample ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'} `} > {/* 播放中波紋效果 */} {isPlayingWord && ( )} {/* 按鈕圖標 */} {isPlayingWord ? ( ) : ( )} {/* 懸停提示光環 */} {!isPlayingWord && !isPlayingExample && ( )} {/* 學習統計 */} {flashcard.masteryLevel}% 掌握程度 {flashcard.timesReviewed} 複習次數 {Math.ceil((new Date(flashcard.nextReviewDate).getTime() - new Date().getTime()) / (1000 * 60 * 60 * 24))} 天後複習 {/* 內容區 - 學習卡片風格 */} {/* 翻譯區塊 */} 中文翻譯 {isEditing ? ( setEditedCard((prev: any) => ({ ...prev, translation: e.target.value }))} className="w-full p-3 border border-green-300 rounded-lg focus:ring-2 focus:ring-green-500 bg-white" placeholder="輸入中文翻譯" /> ) : ( {flashcard.translation} )} {/* 定義區塊 */} 英文定義 {isEditing ? ( setEditedCard((prev: any) => ({ ...prev, definition: e.target.value }))} className="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 bg-white h-20 resize-none" placeholder="輸入英文定義" /> ) : ( {flashcard.definition} )} {/* 例句區塊 */} 例句 {/* 例句圖片 */} {getExampleImage(flashcard) ? ( ) : ( 尚無例句圖片 {isGeneratingImage ? generationProgress : '生成圖片'} )} {/* 圖片上的生成按鈕 */} {getExampleImage(flashcard) && !isGeneratingImage && ( 重新生成 )} {/* 生成進度覆蓋 */} {isGeneratingImage && getExampleImage(flashcard) && ( {generationProgress} )} {isEditing ? ( <> setEditedCard((prev: any) => ({ ...prev, example: e.target.value }))} className="w-full p-3 border border-blue-300 rounded-lg focus:ring-2 focus:ring-blue-500 bg-white h-16 resize-none" placeholder="輸入英文例句" /> setEditedCard((prev: any) => ({ ...prev, exampleTranslation: e.target.value }))} className="w-full p-3 border border-blue-300 rounded-lg focus:ring-2 focus:ring-blue-500 bg-white h-16 resize-none" placeholder="輸入例句翻譯" /> > ) : ( <> "{flashcard.example}" toggleExampleTTS(flashcard.example, 'en-US')} disabled={isPlayingWord} title={isPlayingExample ? "點擊停止播放" : "點擊聽例句發音"} aria-label={isPlayingExample ? `停止播放例句:${flashcard.example}` : `播放例句發音:${flashcard.example}`} className={`group relative w-12 h-12 rounded-full shadow-lg transform transition-all duration-200 ${isPlayingExample ? 'bg-gradient-to-br from-green-500 to-green-600 shadow-green-200 scale-105' : 'bg-gradient-to-br from-blue-500 to-blue-600 hover:shadow-xl hover:scale-105 shadow-blue-200' } ${isPlayingWord ? 'cursor-not-allowed opacity-50' : 'cursor-pointer'} `} > {/* 播放中波紋效果 */} {isPlayingExample && ( )} {/* 按鈕圖標 */} {isPlayingExample ? ( ) : ( )} {/* 懸停提示光環 */} {!isPlayingWord && !isPlayingExample && ( )} "{flashcard.exampleTranslation}" > )} {/* 同義詞區塊 */} {(flashcard as any).synonyms && (flashcard as any).synonyms.length > 0 && ( 同義詞 {(flashcard as any).synonyms.map((synonym: string, index: number) => ( {synonym} ))} )} {/* 詞卡資訊 */} 詞卡資訊 詞性: {getPartOfSpeechDisplay(flashcard.partOfSpeech)} 創建時間: {new Date(flashcard.createdAt).toLocaleDateString()} 下次複習: {new Date(flashcard.nextReviewDate).toLocaleDateString()} 複習次數: {flashcard.timesReviewed} 次 {/* 編輯模式的操作按鈕 */} {isEditing && ( 保存修改 { setIsEditing(false) setEditedCard(flashcard) }} className="flex-1 bg-gray-500 text-white py-3 rounded-lg font-medium hover:bg-gray-600 transition-colors" > 取消編輯 )} {/* 底部操作區 - 平均延展按鈕 */} {flashcard.isFavorite ? '已收藏' : '收藏'} setIsEditing(!isEditing)} className={`flex-1 py-3 rounded-lg font-medium transition-colors ${ isEditing ? 'bg-gray-100 text-gray-700 border border-gray-300' : 'bg-blue-100 text-blue-700 border border-blue-300 hover:bg-blue-200' }`} > {isEditing ? '取消編輯' : '編輯詞卡'} 刪除詞卡 {/* Toast 通知系統 */} ) }
{flashcard.translation}
{flashcard.definition}
尚無例句圖片
{generationProgress}
"{flashcard.example}"
"{flashcard.exampleTranslation}"