dramaling-vocab-learning/QUERY_HISTORY_CACHE_SYSTEM_...

575 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 🗃️ 查詢歷史快取系統 - 功能規格計劃
**專案**: DramaLing 英語學習平台
**功能**: 查詢歷史記錄與智能快取系統
**文檔版本**: v1.0
**建立日期**: 2025-01-18
**核心概念**: 將技術快取包裝為用戶查詢歷史,提升體驗透明度
---
## 🎯 **核心設計理念**
### **從「快取機制」到「查詢歷史」**
| 技術實現 | 用戶概念 | 實際意義 |
|----------|----------|----------|
| Cache Hit | 查詢過的句子 | "您之前查詢過這個句子" |
| Cache Miss | 新句子查詢 | "正在為您分析新句子..." |
| Word Cache | 查詢過的詞彙 | "您之前查詢過這個詞彙" |
| API Call | 即時查詢 | "正在為您查詢詞彙資訊..." |
### **使用者場景**
```
場景1: 句子查詢
用戶輸入: "Hello world"
第1次: "正在分析..." (3-5秒) → 存入查詢歷史
第2次: "您之前查詢過,立即顯示" (<200ms)
場景2: 詞彙查詢
句子: "The apple"
點擊 "The": "正在查詢..." → 存入詞彙查詢歷史
新句子: "The orange"
點擊 "The": "您之前查詢過,立即顯示" → 從歷史載入
```
---
## 📋 **技術規格設計**
## 🎯 **A. 句子查詢歷史系統**
### **A1. 當前實現改造**
**現有**: `SentenceAnalysisCache` (技術導向命名)
**改為**: 保持技術實現,改變用戶訊息
#### **API 回應訊息改造**
**檔案**: `/backend/DramaLing.Api/Controllers/AIController.cs:547`
```csharp
// 當前 (技術導向)
return Ok(new {
Success = true,
Data = cachedResult,
Message = "句子分析完成(快取)", // ❌ 技術術語
Cached = true,
CacheHit = true
});
// 改為 (用戶導向)
return Ok(new {
Success = true,
Data = cachedResult,
Message = "您之前查詢過這個句子,立即為您顯示結果", // ✅ 用戶友善
FromHistory = true, // ✅ 更直觀的欄位名
QueryDate = cachedAnalysis.CreatedAt,
TimesQueried = cachedAnalysis.AccessCount
});
```
### **A2. 前端顯示改造**
**檔案**: `/frontend/app/generate/page.tsx`
```typescript
// 查詢歷史狀態顯示
{queryStatus && (
<div className={`inline-flex items-center px-4 py-2 rounded-lg text-sm font-medium ${
queryStatus.fromHistory
? 'bg-purple-100 text-purple-800'
: 'bg-blue-100 text-blue-800'
}`}>
{queryStatus.fromHistory ? (
<>
<span className="mr-2">🗃️</span>
<span>查詢歷史 ({queryStatus.timesQueried})</span>
<span className="ml-2 text-xs text-purple-600">
首次查詢: {formatDate(queryStatus.queryDate)}
</span>
</>
) : (
<>
<span className="mr-2">🔍</span>
<span>新句子分析中...</span>
</>
)}
</div>
)}
```
---
## 🎯 **B. 詞彙查詢歷史系統**
### **B1. 新增詞彙查詢快取表**
```sql
-- 用戶詞彙查詢歷史表
CREATE TABLE UserVocabularyQueryHistory (
Id UNIQUEIDENTIFIER PRIMARY KEY,
UserId UNIQUEIDENTIFIER NOT NULL, -- 用戶ID (未來用戶系統)
Word NVARCHAR(100) NOT NULL, -- 查詢的詞彙
WordLowercase NVARCHAR(100) NOT NULL, -- 小寫版本 (查詢鍵)
-- 查詢結果快取
AnalysisResult NVARCHAR(MAX) NOT NULL, -- JSON 格式的分析結果
Translation NVARCHAR(200) NOT NULL, -- 快速存取的翻譯
Definition NVARCHAR(500) NOT NULL, -- 快速存取的定義
-- 查詢上下文
FirstQueriedInSentence NVARCHAR(1000), -- 首次查詢時的句子語境
LastQueriedInSentence NVARCHAR(1000), -- 最後查詢時的句子語境
-- 查詢歷史統計
FirstQueriedAt DATETIME2 NOT NULL, -- 首次查詢時間
LastQueriedAt DATETIME2 NOT NULL, -- 最後查詢時間
QueryCount INT DEFAULT 1, -- 查詢次數
-- 系統欄位
CreatedAt DATETIME2 NOT NULL,
UpdatedAt DATETIME2 NOT NULL,
-- 索引優化
INDEX IX_UserVocabularyQueryHistory_UserId_Word (UserId, WordLowercase),
INDEX IX_UserVocabularyQueryHistory_LastQueriedAt (LastQueriedAt),
-- 暫時不設定外鍵,因為用戶系統還未完全實現
-- FOREIGN KEY (UserId) REFERENCES Users(Id)
);
```
### **B2. 詞彙查詢服務重構**
**檔案**: `/backend/DramaLing.Api/Services/VocabularyQueryService.cs`
```csharp
public interface IVocabularyQueryService
{
Task<VocabularyQueryResponse> QueryWordAsync(string word, string sentence, Guid? userId = null);
Task<List<UserVocabularyQueryHistory>> GetUserQueryHistoryAsync(Guid userId, int limit = 50);
}
public class VocabularyQueryService : IVocabularyQueryService
{
private readonly DramaLingDbContext _context;
private readonly IGeminiService _geminiService;
private readonly ILogger<VocabularyQueryService> _logger;
public async Task<VocabularyQueryResponse> QueryWordAsync(string word, string sentence, Guid? userId = null)
{
var wordLower = word.ToLower();
var mockUserId = userId ?? Guid.Parse("00000000-0000-0000-0000-000000000001"); // 模擬用戶
// 1. 檢查用戶的詞彙查詢歷史
var queryHistory = await _context.UserVocabularyQueryHistory
.FirstOrDefaultAsync(h => h.UserId == mockUserId && h.WordLowercase == wordLower);
if (queryHistory != null)
{
// 更新查詢統計
queryHistory.LastQueriedAt = DateTime.UtcNow;
queryHistory.LastQueriedInSentence = sentence;
queryHistory.QueryCount++;
await _context.SaveChangesAsync();
// 返回歷史查詢結果
var historicalAnalysis = JsonSerializer.Deserialize<object>(queryHistory.AnalysisResult);
return new VocabularyQueryResponse
{
Success = true,
Data = new
{
Word = word,
Analysis = historicalAnalysis,
QueryHistory = new
{
IsFromHistory = true,
FirstQueriedAt = queryHistory.FirstQueriedAt,
QueryCount = queryHistory.QueryCount,
DaysSinceFirstQuery = (DateTime.UtcNow - queryHistory.FirstQueriedAt).Days,
FirstContext = queryHistory.FirstQueriedInSentence,
CurrentContext = sentence
}
},
Message = $"您之前查詢過 \"{word}\",這是第{queryHistory.QueryCount}次查詢"
};
}
// 2. 新詞彙查詢 - 調用 AI
var aiAnalysis = await AnalyzeWordWithAI(word, sentence);
// 3. 存入查詢歷史
var newHistory = new UserVocabularyQueryHistory
{
Id = Guid.NewGuid(),
UserId = mockUserId,
Word = word,
WordLowercase = wordLower,
AnalysisResult = JsonSerializer.Serialize(aiAnalysis),
Translation = aiAnalysis.Translation,
Definition = aiAnalysis.Definition,
FirstQueriedInSentence = sentence,
LastQueriedInSentence = sentence,
FirstQueriedAt = DateTime.UtcNow,
LastQueriedAt = DateTime.UtcNow,
QueryCount = 1,
CreatedAt = DateTime.UtcNow,
UpdatedAt = DateTime.UtcNow
};
_context.UserVocabularyQueryHistory.Add(newHistory);
await _context.SaveChangesAsync();
return new VocabularyQueryResponse
{
Success = true,
Data = new
{
Word = word,
Analysis = aiAnalysis,
QueryHistory = new
{
IsFromHistory = false,
IsNewQuery = true,
FirstQueriedAt = DateTime.UtcNow,
QueryCount = 1,
Context = sentence
}
},
Message = $"首次查詢 \"{word}\",已加入您的查詢歷史"
};
}
private async Task<object> AnalyzeWordWithAI(string word, string sentence)
{
try
{
// 🚀 這裡應該是真實的 AI 調用,不是模擬
var prompt = $@"
請分析單字 ""{word}"" 在句子 ""{sentence}"" 中的詳細資訊:
單字: {word}
語境: {sentence}
請以JSON格式回應
{{
""word"": ""{word}"",
""translation"": ""繁體中文翻譯"",
""definition"": ""英文定義"",
""partOfSpeech"": ""詞性"",
""pronunciation"": ""IPA音標"",
""difficultyLevel"": ""CEFR等級"",
""contextMeaning"": ""在此句子中的具體含義"",
""isHighValue"": false,
""examples"": [""例句1"", ""例句2""]
}}
";
var response = await _geminiService.CallGeminiApiAsync(prompt);
return ParseVocabularyAnalysisResponse(response);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "AI vocabulary analysis failed, using fallback data");
// 回退到基本資料
return new
{
word = word,
translation = $"{word} 的翻譯",
definition = $"Definition of {word}",
partOfSpeech = "unknown",
pronunciation = $"/{word}/",
difficultyLevel = "unknown",
contextMeaning = $"在句子 \"{sentence}\" 中的含義",
isHighValue = false,
examples = new string[0]
};
}
}
}
```
---
## 🎯 **C. API 端點重構**
### **C1. 更新現有端點**
**檔案**: `/backend/DramaLing.Api/Controllers/AIController.cs`
#### **句子分析端點保持不變**
```http
POST /api/ai/analyze-sentence
```
**只修改回應訊息,讓用戶理解是查詢歷史**
#### **詞彙查詢端點整合歷史服務**
```csharp
[HttpPost("query-word")]
[AllowAnonymous]
public async Task<ActionResult> QueryWord([FromBody] QueryWordRequest request)
{
try
{
// 使用新的查詢歷史服務
var result = await _vocabularyQueryService.QueryWordAsync(
request.Word,
request.Sentence,
userId: null // 暫時使用模擬用戶
);
return Ok(result);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in vocabulary query");
return StatusCode(500, new
{
Success = false,
Error = "詞彙查詢失敗",
Details = ex.Message
});
}
}
```
---
## 🎯 **D. 前端查詢歷史整合**
### **D1. ClickableTextV2 組件改造**
**檔案**: `/frontend/components/ClickableTextV2.tsx`
```typescript
// 修改詞彙查詢成功的處理
if (result.success && result.data?.analysis) {
// 顯示查詢歷史資訊
const queryHistory = result.data.queryHistory;
if (queryHistory.isFromHistory) {
console.log(`📚 從查詢歷史載入: ${word} (第${queryHistory.queryCount}次查詢)`);
} else {
console.log(`🔍 新詞彙查詢: ${word} (已加入查詢歷史)`);
}
// 將新的分析資料通知父組件
onNewWordAnalysis?.(word, {
...result.data.analysis,
queryHistory: queryHistory // 附帶查詢歷史資訊
});
// 顯示分析結果
setPopupPosition(position);
setSelectedWord(word);
onWordClick?.(word, result.data.analysis);
}
```
### **D2. 詞彙彈窗增加歷史資訊**
```typescript
// 在詞彙彈窗中顯示查詢歷史
function VocabularyPopup({ word, analysis, queryHistory }: Props) {
return (
<div className="vocabulary-popup bg-white border rounded-lg shadow-lg p-4 w-80">
{/* 詞彙基本資訊 */}
<div className="word-basic-info mb-3">
<h3 className="text-lg font-bold">{word}</h3>
<p className="text-gray-600">{analysis.pronunciation}</p>
<p className="text-blue-600 font-medium">{analysis.translation}</p>
<p className="text-gray-700 text-sm mt-1">{analysis.definition}</p>
</div>
{/* 查詢歷史資訊 */}
{queryHistory && (
<div className="query-history bg-gray-50 p-3 rounded-lg">
<h4 className="font-semibold text-xs text-gray-700 mb-2 flex items-center">
<span className="mr-1">🗃️</span>
查詢歷史
</h4>
{queryHistory.isFromHistory ? (
<div className="text-xs text-gray-600 space-y-1">
<div className="flex justify-between">
<span>查詢次數:</span>
<span className="font-medium">{queryHistory.queryCount} </span>
</div>
<div className="flex justify-between">
<span>首次查詢:</span>
<span className="font-medium">{formatDate(queryHistory.firstQueriedAt)}</span>
</div>
{queryHistory.firstContext !== queryHistory.currentContext && (
<div className="mt-2 p-2 bg-blue-50 rounded text-xs">
<p className="text-blue-700">
<strong>首次語境:</strong> {queryHistory.firstContext}
</p>
<p className="text-blue-700 mt-1">
<strong>當前語境:</strong> {queryHistory.currentContext}
</p>
</div>
)}
</div>
) : (
<div className="text-xs text-green-600">
首次查詢,已加入您的查詢歷史
</div>
)}
</div>
)}
</div>
);
}
```
---
## 🎯 **E. 用戶介面語言優化**
### **E1. 訊息文案改造**
| 情況 | 技術訊息 | 用戶友善訊息 |
|------|----------|--------------|
| 快取命中 | "句子分析完成(快取)" | "您之前查詢過這個句子,立即為您顯示結果" |
| 新查詢 | "AI句子分析完成" | "新句子分析完成,已加入您的查詢歷史" |
| 詞彙快取 | "高價值詞彙查詢完成(免費)" | "您之前查詢過這個詞彙 (第N次查詢)" |
| 詞彙新查詢 | "低價值詞彙查詢完成" | "首次查詢此詞彙,已加入查詢歷史" |
### **E2. 載入狀態文案**
```typescript
// 分析中的狀態提示
const getLoadingMessage = (type: 'sentence' | 'vocabulary', isNew: boolean) => {
if (type === 'sentence') {
return isNew
? "🔍 正在分析新句子,約需 3-5 秒..."
: "📚 從查詢歷史載入...";
} else {
return isNew
? "🤖 正在查詢詞彙資訊..."
: "🗃️ 從查詢歷史載入...";
}
};
```
---
## 🛠️ **實施計劃**
### **📋 Phase 1: 後端查詢歷史服務 (1-2天)**
#### **1.1 建立詞彙查詢歷史表**
```bash
# 建立 Entity Framework 遷移
dotnet ef migrations add AddUserVocabularyQueryHistory
dotnet ef database update
```
#### **1.2 建立查詢歷史服務**
- 新增 `VocabularyQueryService.cs`
- 實現真實的 AI 詞彙查詢 (替換模擬)
- 整合查詢歷史記錄功能
#### **1.3 修改現有 API 回應訊息**
- 將技術術語改為用戶友善語言
- 新增查詢歷史相關欄位
- 保持 API 結構相容性
### **📋 Phase 2: 前端查詢歷史整合 (2-3天)**
#### **2.1 更新 ClickableTextV2 組件**
- 整合查詢歷史資訊顯示
- 優化詞彙彈窗包含歷史資訊
- 改善視覺提示系統
#### **2.2 修改 generate 頁面**
- 更新查詢狀態顯示
- 改善載入狀態文案
- 新增查詢歷史統計
#### **2.3 訊息文案全面優化**
- 替換所有技術術語
- 採用用戶友善的描述
- 增加情境化的提示
### **📋 Phase 3: 查詢歷史頁面 (3-4天)**
#### **3.1 建立查詢歷史頁面**
```typescript
// 新頁面: /frontend/app/query-history/page.tsx
- 顯示所有查詢過的句子
- 顯示所有查詢過的詞彙
- 提供搜尋和篩選功能
- 支援重新查詢功能
```
#### **3.2 導航整合**
- 在主導航中新增「查詢歷史」
- 在 generate 頁面新增快速連結
- 在詞彙彈窗中新增「查看完整歷史」
---
## 📊 **與現有快取系統的關係**
### **保持底層技術優勢**
-**效能優化**: 繼續享受快取帶來的速度提升
-**成本控制**: 避免重複的 AI API 調用
-**系統穩定性**: 保持現有的錯誤處理機制
### **改善用戶認知**
- 🔄 **概念轉換**: 從「快取」到「查詢歷史」
- 📊 **透明化**: 讓用戶了解系統行為
- 🎯 **價值感知**: 用戶看到查詢的累積價值
### **技術實現不變,體驗大幅提升**
```
底層: 仍然是高效的快取機制
表層: 包裝為有意義的查詢歷史體驗
結果: 技術效益 + 用戶體驗雙贏
```
---
## 🎯 **預期效果**
### **用戶體驗轉變**
- **舊**: "為什麼這個查詢這麼快?"
- **新**: "我之前查詢過這個詞彙這是第3次遇到"
### **系統感知轉變**
- **舊**: 神秘的黑盒子系統
- **新**: 透明的查詢歷史助手
### **價值感知轉變**
- **舊**: 一次性工具
- **新**: 個人化查詢資料庫
## 📋 **成功指標**
### **定量指標**
- **歷史查看率**: >60% 用戶注意到查詢歷史資訊
- **重複查詢滿意度**: >80% 用戶對快速載入感到滿意
- **功能理解度**: >90% 用戶理解為什麼有些查詢很快
### **定性指標**
- **透明感**: 用戶明白系統行為邏輯
- **積累感**: 用戶感受到查詢的累積價值
- **信任感**: 用戶信任系統會記住他們的查詢
---
**© 2025 DramaLing Development Team**
**設計理念**: 技術服務於用戶體驗,快取包裝為查詢歷史
**核心價值**: 讓用戶感受到每次查詢的累積意義
> 我覺得快取機制不太貼切,\
具體應該改成歷史紀錄的概念\
使用者查完某個原始例句後\
就會存成紀錄\
如果在查詢非高價值的詞彙因為還沒有紀錄所以就會再去問ad\
然後再存到紀錄中\\
\
\
這不是學習歷史\
使用者也沒有儲存詞彙\
那只是查詢的歷史而已\
\
請你設計這個功能\
寫成功能規格計劃再根目錄