From 69b85c542c42eb5cec823b2a2be1c02fe4f4fb6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=84=AD=E6=B2=9B=E8=BB=92?= Date: Thu, 9 Oct 2025 22:50:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=A6=E7=8F=BE=E7=94=A8=E6=88=B6?= =?UTF-8?q?=E9=9A=94=E9=9B=A2=E5=92=8C=E9=87=8D=E8=A4=87=E4=BF=9D=E5=AD=98?= =?UTF-8?q?=E5=91=8A=E8=AD=A6=E6=A9=9F=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重大改進: - 修復用戶隔離快取機制,使用用戶 ID 區分快取 key - 添加重複保存告警,防止同一次分析中重複保存相同詞彙 - 實現 JWT token 解析獲取用戶 ID,確保資料隔離 - 添加 24 小時快取過期機制 用戶體驗優化: - 已保存詞彙顯示綠色勾號視覺標記 - 重複保存時提供確認對話框和清楚說明 - 新分析時自動重置已保存詞彙追蹤 - 完整的快取驗證和安全檢查 隱私保護: - 不同用戶的分析結果完全隔離 - 換帳號後無法看到其他用戶的快取內容 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- frontend/app/generate/page.tsx | 125 +++++++++++++++++++++++++++------ 1 file changed, 105 insertions(+), 20 deletions(-) diff --git a/frontend/app/generate/page.tsx b/frontend/app/generate/page.tsx index 361cf39..4afec90 100644 --- a/frontend/app/generate/page.tsx +++ b/frontend/app/generate/page.tsx @@ -48,32 +48,82 @@ function GenerateContent() { // UX 改善:追蹤分析狀態,避免輸入和結果不匹配 const [lastAnalyzedText, setLastAnalyzedText] = useState('') - // localStorage 快取函數 - const saveAnalysisToCache = useCallback((cacheData: any) => { - try { - localStorage.setItem('generate_analysis_cache', JSON.stringify(cacheData)) - } catch (error) { - console.warn('無法保存分析快取:', error) - } - }, []) + // 追蹤本次會話已保存的詞彙(防重複保存) + const [savedWordsInSession, setSavedWordsInSession] = useState>(new Set()) + + // 獲取當前用戶 ID 用於快取隔離 + const getCurrentUserId = useCallback(() => { + const token = typeof window !== 'undefined' ? localStorage.getItem('auth_token') : null + if (!token) return null - const loadAnalysisFromCache = useCallback(() => { try { - const cached = localStorage.getItem('generate_analysis_cache') - return cached ? JSON.parse(cached) : null - } catch (error) { - console.warn('無法載入分析快取:', error) + // 解析 JWT token 獲取用戶 ID + const payload = JSON.parse(atob(token.split('.')[1])) + return payload.nameid || payload.sub || null + } catch { return null } }, []) + // localStorage 快取函數(用戶隔離) + const saveAnalysisToCache = useCallback((cacheData: any) => { + try { + const userId = getCurrentUserId() + if (!userId) return + + const cacheKey = `generate_analysis_cache_${userId}` + localStorage.setItem(cacheKey, JSON.stringify({ + ...cacheData, + userId, + timestamp: Date.now() + })) + } catch (error) { + console.warn('無法保存分析快取:', error) + } + }, [getCurrentUserId]) + + const loadAnalysisFromCache = useCallback(() => { + try { + const userId = getCurrentUserId() + if (!userId) return null + + const cacheKey = `generate_analysis_cache_${userId}` + const cached = localStorage.getItem(cacheKey) + if (!cached) return null + + const cacheData = JSON.parse(cached) + + // 驗證快取是否屬於當前用戶 + if (cacheData.userId !== userId) { + localStorage.removeItem(cacheKey) + return null + } + + // 檢查快取是否過期(24小時) + const isExpired = Date.now() - cacheData.timestamp > 24 * 60 * 60 * 1000 + if (isExpired) { + localStorage.removeItem(cacheKey) + return null + } + + return cacheData + } catch (error) { + console.warn('無法載入分析快取:', error) + return null + } + }, [getCurrentUserId]) + const clearAnalysisCache = useCallback(() => { try { - localStorage.removeItem('generate_analysis_cache') + const userId = getCurrentUserId() + if (!userId) return + + const cacheKey = `generate_analysis_cache_${userId}` + localStorage.removeItem(cacheKey) } catch (error) { console.warn('無法清除分析快取:', error) } - }, []) + }, [getCurrentUserId]) // 組件載入時恢復快取的分析結果 useEffect(() => { @@ -84,6 +134,12 @@ function GenerateContent() { setSentenceAnalysis(cached.sentenceAnalysis || null) setSentenceMeaning(cached.sentenceMeaning || '') setGrammarCorrection(cached.grammarCorrection || null) + + // 恢復已保存詞彙的狀態 + if (cached.savedWordsInSession) { + setSavedWordsInSession(new Set(cached.savedWordsInSession)) + } + console.log('✅ 已恢復快取的分析結果') } }, [loadAnalysisFromCache]) @@ -94,6 +150,9 @@ function GenerateContent() { // 清除舊的分析快取 clearAnalysisCache() + // 重置本次會話的已保存詞彙追蹤 + setSavedWordsInSession(new Set()) + setIsAnalyzing(true) try { @@ -154,7 +213,7 @@ function GenerateContent() { }) } - // 保存分析結果到快取 + // 保存分析結果到快取(包含已保存詞彙狀態) const cacheData = { textInput, sentenceAnalysis: analysisData, @@ -165,7 +224,8 @@ function GenerateContent() { correctedText: apiData.grammarCorrection.correctedText || textInput, corrections: apiData.grammarCorrection.corrections || [], confidenceScore: apiData.grammarCorrection.confidenceScore || 1.0 - } : null + } : null, + savedWordsInSession: Array.from(savedWordsInSession) } saveAnalysisToCache(cacheData) @@ -222,9 +282,21 @@ function GenerateContent() { } }, [sentenceAnalysis, userLevel]) - // 保存詞彙 + // 保存詞彙(帶重複檢查) const handleSaveWord = useCallback(async (word: string, analysis: any) => { try { + // 檢查是否已在本次會話中保存過 + if (savedWordsInSession.has(word.toLowerCase())) { + const confirmSave = window.confirm( + `詞彙「${word}」已在本次分析中保存過,確定要重複保存嗎?\n\n重複保存會創建完全相同的詞卡。` + ) + + if (!confirmSave) { + toast.info(`已取消保存「${word}」`) + return { success: false, cancelled: true } + } + } + const cardData = { word: word, translation: analysis.translation || '', @@ -239,6 +311,9 @@ function GenerateContent() { const response = await flashcardsService.createFlashcard(cardData) if (response.success) { + // 標記為已保存 + setSavedWordsInSession(prev => new Set([...prev, word.toLowerCase()])) + toast.success(`已成功將「${word}」保存到詞卡庫!`) return { success: true } } else { @@ -249,7 +324,7 @@ function GenerateContent() { toast.error(`保存詞卡失敗: ${errorMessage}`) return { success: false, error: errorMessage } } - }, [toast]) + }, [toast, savedWordsInSession]) return (
@@ -400,15 +475,25 @@ function GenerateContent() { return {token} } + const isSaved = savedWordsInSession.has(cleanToken.toLowerCase()) + return ( setSelectedWord(cleanToken)} role="button" tabIndex={0} > {token} + {isSaved && ( + + + + )} )