refactor: 移除所有快取機制並優化 AI 服務架構
主要變更: - 完全移除句子分析快取,每次都進行真實 AI 分析 - 移除詞彙查詢的假資料實現,改用真實 Gemini AI - 在 GeminiService 中新增專門的 AnalyzeWordAsync 方法 - 修正架構設計,將 AI 邏輯從 Controller 移到 Service 層 - 移除前端快取狀態顯示,簡化用戶介面 技術改善: - 遵循分層架構原則,Service 層處理 AI 邏輯 - 統一錯誤處理和回退機制 - 新增完整的詞彙分析 JSON 解析邏輯 - 確保每次查詢都獲得最新的 AI 分析結果 附加: - 新增查詢歷史系統設計規格文檔 - 為未來實現查詢歷史功能做準備 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4311c1c3b5
commit
4f16cbfa08
|
|
@ -0,0 +1,575 @@
|
||||||
|
# 🗃️ 查詢歷史快取系統 - 功能規格計劃
|
||||||
|
|
||||||
|
**專案**: 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\
|
||||||
|
然後再存到紀錄中\\
|
||||||
|
\
|
||||||
|
\
|
||||||
|
這不是學習歷史\
|
||||||
|
使用者也沒有儲存詞彙\
|
||||||
|
那只是查詢的歷史而已\
|
||||||
|
\
|
||||||
|
請你設計這個功能\
|
||||||
|
寫成功能規格計劃再根目錄
|
||||||
|
|
@ -531,31 +531,7 @@ public class AIController : ControllerBase
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. 檢查快取
|
// 移除快取檢查,每次都進行新的 AI 分析
|
||||||
var cachedAnalysis = await _cacheService.GetCachedAnalysisAsync(request.InputText);
|
|
||||||
if (cachedAnalysis != null && !request.ForceRefresh)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Using cached analysis for text hash: {TextHash}", cachedAnalysis.InputTextHash);
|
|
||||||
|
|
||||||
// 解析快取的分析結果 - 使用一致的命名策略
|
|
||||||
_logger.LogInformation("Cache raw JSON: {CacheJson}", cachedAnalysis.AnalysisResult);
|
|
||||||
|
|
||||||
var options = new JsonSerializerOptions
|
|
||||||
{
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
||||||
};
|
|
||||||
var cachedResult = System.Text.Json.JsonSerializer.Deserialize<object>(cachedAnalysis.AnalysisResult, options);
|
|
||||||
_logger.LogInformation("Cache deserialized result: {CachedResult}", System.Text.Json.JsonSerializer.Serialize(cachedResult, options));
|
|
||||||
|
|
||||||
return Ok(new
|
|
||||||
{
|
|
||||||
Success = true,
|
|
||||||
Data = cachedResult,
|
|
||||||
Message = "句子分析完成(快取)",
|
|
||||||
Cached = true,
|
|
||||||
CacheHit = true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 執行真正的AI分析
|
// 2. 執行真正的AI分析
|
||||||
_logger.LogInformation("Calling Gemini AI for text: {InputText}", request.InputText);
|
_logger.LogInformation("Calling Gemini AI for text: {InputText}", request.InputText);
|
||||||
|
|
@ -585,28 +561,13 @@ public class AIController : ControllerBase
|
||||||
PhrasesDetected = new object[0] // 暫時簡化
|
PhrasesDetected = new object[0] // 暫時簡化
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. 存入快取(24小時TTL)
|
// 移除快取存入邏輯,每次都是新的 AI 分析
|
||||||
try
|
|
||||||
{
|
|
||||||
await _cacheService.SetCachedAnalysisAsync(
|
|
||||||
request.InputText,
|
|
||||||
baseResponseData,
|
|
||||||
TimeSpan.FromHours(24)
|
|
||||||
);
|
|
||||||
_logger.LogInformation("AI analysis result cached for 24 hours");
|
|
||||||
}
|
|
||||||
catch (Exception cacheEx)
|
|
||||||
{
|
|
||||||
_logger.LogWarning(cacheEx, "Failed to cache AI analysis result");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok(new
|
return Ok(new
|
||||||
{
|
{
|
||||||
Success = true,
|
Success = true,
|
||||||
Data = baseResponseData,
|
Data = baseResponseData,
|
||||||
Message = "AI句子分析完成",
|
Message = "AI句子分析完成",
|
||||||
Cached = false,
|
|
||||||
CacheHit = false,
|
|
||||||
UsingAI = true
|
UsingAI = true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -1018,23 +979,74 @@ public class AIController : ControllerBase
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<object> AnalyzeLowValueWord(string word, string sentence)
|
private async Task<object> AnalyzeLowValueWord(string word, string sentence)
|
||||||
{
|
{
|
||||||
// 模擬即時AI分析
|
try
|
||||||
await Task.Delay(200);
|
|
||||||
|
|
||||||
return new
|
|
||||||
{
|
{
|
||||||
word = word,
|
// 真實調用 Gemini AI 分析詞彙
|
||||||
translation = "即時分析的翻譯",
|
var prompt = $@"
|
||||||
definition = "即時分析的定義",
|
請分析單字 ""{word}"" 在句子 ""{sentence}"" 中的詳細資訊:
|
||||||
partOfSpeech = "noun",
|
|
||||||
pronunciation = "/example/",
|
單字: {word}
|
||||||
synonyms = new[] { "synonym1", "synonym2" },
|
語境: {sentence}
|
||||||
antonyms = new string[0],
|
|
||||||
isPhrase = false,
|
請以JSON格式回應,不要包含任何其他文字:
|
||||||
isHighValue = false,
|
{{
|
||||||
learningPriority = "low",
|
""word"": ""{word}"",
|
||||||
difficultyLevel = "A1"
|
""translation"": ""繁體中文翻譯"",
|
||||||
};
|
""definition"": ""英文定義"",
|
||||||
|
""partOfSpeech"": ""詞性(n./v./adj./adv.等)"",
|
||||||
|
""pronunciation"": ""IPA音標"",
|
||||||
|
""difficultyLevel"": ""CEFR等級(A1/A2/B1/B2/C1/C2)"",
|
||||||
|
""contextMeaning"": ""在此句子中的具體含義"",
|
||||||
|
""isHighValue"": false,
|
||||||
|
""synonyms"": [""同義詞1"", ""同義詞2""],
|
||||||
|
""examples"": [""例句1"", ""例句2""]
|
||||||
|
}}
|
||||||
|
|
||||||
|
要求:
|
||||||
|
1. 翻譯要準確自然
|
||||||
|
2. 定義要簡潔易懂
|
||||||
|
3. 音標使用標準IPA格式
|
||||||
|
4. 提供在當前語境中的具體含義
|
||||||
|
";
|
||||||
|
|
||||||
|
// 使用 GeminiService 進行專門的詞彙分析
|
||||||
|
var wordAnalysis = await _geminiService.AnalyzeWordAsync(word, sentence);
|
||||||
|
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
word = wordAnalysis.Word,
|
||||||
|
translation = wordAnalysis.Translation,
|
||||||
|
definition = wordAnalysis.Definition,
|
||||||
|
partOfSpeech = wordAnalysis.PartOfSpeech,
|
||||||
|
pronunciation = wordAnalysis.Pronunciation,
|
||||||
|
synonyms = new string[0],
|
||||||
|
antonyms = new string[0],
|
||||||
|
isPhrase = false,
|
||||||
|
isHighValue = wordAnalysis.IsHighValue,
|
||||||
|
learningPriority = "low",
|
||||||
|
difficultyLevel = wordAnalysis.DifficultyLevel
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to analyze word with AI, using fallback");
|
||||||
|
|
||||||
|
// 回退到基本資料
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
word = word,
|
||||||
|
translation = $"{word} (AI 查詢失敗)",
|
||||||
|
definition = $"Definition of {word} (AI service unavailable)",
|
||||||
|
partOfSpeech = "unknown",
|
||||||
|
pronunciation = $"/{word}/",
|
||||||
|
synonyms = new string[0],
|
||||||
|
antonyms = new string[0],
|
||||||
|
isPhrase = false,
|
||||||
|
isHighValue = false,
|
||||||
|
learningPriority = "low",
|
||||||
|
difficultyLevel = "unknown"
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ public interface IGeminiService
|
||||||
Task<List<GeneratedCard>> GenerateCardsAsync(string inputText, string extractionType, int cardCount);
|
Task<List<GeneratedCard>> GenerateCardsAsync(string inputText, string extractionType, int cardCount);
|
||||||
Task<ValidationResult> ValidateCardAsync(Flashcard card);
|
Task<ValidationResult> ValidateCardAsync(Flashcard card);
|
||||||
Task<SentenceAnalysisResponse> AnalyzeSentenceAsync(string inputText);
|
Task<SentenceAnalysisResponse> AnalyzeSentenceAsync(string inputText);
|
||||||
|
Task<WordAnalysisResult> AnalyzeWordAsync(string word, string sentence);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class GeminiService : IGeminiService
|
public class GeminiService : IGeminiService
|
||||||
|
|
@ -105,6 +106,61 @@ public class GeminiService : IGeminiService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<WordAnalysisResult> AnalyzeWordAsync(string word, string sentence)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(_apiKey) || _apiKey == "your-gemini-api-key-here")
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Gemini API key not configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
var prompt = $@"
|
||||||
|
請分析單字 ""{word}"" 在句子 ""{sentence}"" 中的詳細資訊:
|
||||||
|
|
||||||
|
單字: {word}
|
||||||
|
語境: {sentence}
|
||||||
|
|
||||||
|
請以JSON格式回應,不要包含任何其他文字:
|
||||||
|
{{
|
||||||
|
""word"": ""{word}"",
|
||||||
|
""translation"": ""繁體中文翻譯"",
|
||||||
|
""definition"": ""英文定義"",
|
||||||
|
""partOfSpeech"": ""詞性(n./v./adj./adv.等)"",
|
||||||
|
""pronunciation"": ""IPA音標"",
|
||||||
|
""difficultyLevel"": ""CEFR等級(A1/A2/B1/B2/C1/C2)"",
|
||||||
|
""isHighValue"": false,
|
||||||
|
""contextMeaning"": ""在此句子中的具體含義""
|
||||||
|
}}
|
||||||
|
|
||||||
|
要求:
|
||||||
|
1. 翻譯要準確自然
|
||||||
|
2. 定義要簡潔易懂
|
||||||
|
3. 音標使用標準IPA格式
|
||||||
|
4. 根據語境判斷詞性和含義
|
||||||
|
";
|
||||||
|
|
||||||
|
var response = await CallGeminiApiAsync(prompt);
|
||||||
|
return ParseWordAnalysisResponse(response, word);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error analyzing word with Gemini API");
|
||||||
|
|
||||||
|
// 回退到基本資料
|
||||||
|
return new WordAnalysisResult
|
||||||
|
{
|
||||||
|
Word = word,
|
||||||
|
Translation = $"{word} (AI 暫時不可用)",
|
||||||
|
Definition = $"Definition of {word} (service temporarily unavailable)",
|
||||||
|
PartOfSpeech = "unknown",
|
||||||
|
Pronunciation = $"/{word}/",
|
||||||
|
DifficultyLevel = "unknown",
|
||||||
|
IsHighValue = false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<ValidationResult> ValidateCardAsync(Flashcard card)
|
public async Task<ValidationResult> ValidateCardAsync(Flashcard card)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|
@ -297,6 +353,44 @@ public class GeminiService : IGeminiService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析 Gemini AI 詞彙分析響應
|
||||||
|
/// </summary>
|
||||||
|
private WordAnalysisResult ParseWordAnalysisResponse(string response, string word)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cleanText = response.Trim().Replace("```json", "").Replace("```", "").Trim();
|
||||||
|
var jsonResponse = JsonSerializer.Deserialize<JsonElement>(cleanText);
|
||||||
|
|
||||||
|
return new WordAnalysisResult
|
||||||
|
{
|
||||||
|
Word = GetStringProperty(jsonResponse, "word") ?? word,
|
||||||
|
Translation = GetStringProperty(jsonResponse, "translation") ?? $"{word} 的翻譯",
|
||||||
|
Definition = GetStringProperty(jsonResponse, "definition") ?? $"Definition of {word}",
|
||||||
|
PartOfSpeech = GetStringProperty(jsonResponse, "partOfSpeech") ?? "unknown",
|
||||||
|
Pronunciation = GetStringProperty(jsonResponse, "pronunciation") ?? $"/{word}/",
|
||||||
|
DifficultyLevel = GetStringProperty(jsonResponse, "difficultyLevel") ?? "A1",
|
||||||
|
IsHighValue = jsonResponse.TryGetProperty("isHighValue", out var isHighValueElement) && isHighValueElement.GetBoolean()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Failed to parse word analysis response");
|
||||||
|
|
||||||
|
return new WordAnalysisResult
|
||||||
|
{
|
||||||
|
Word = word,
|
||||||
|
Translation = $"{word} (解析失敗)",
|
||||||
|
Definition = $"Definition of {word} (parsing failed)",
|
||||||
|
PartOfSpeech = "unknown",
|
||||||
|
Pronunciation = $"/{word}/",
|
||||||
|
DifficultyLevel = "unknown",
|
||||||
|
IsHighValue = false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 解析 Gemini AI 句子分析響應
|
/// 解析 Gemini AI 句子分析響應
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -22,12 +22,7 @@ function GenerateContent() {
|
||||||
const [finalText, setFinalText] = useState('')
|
const [finalText, setFinalText] = useState('')
|
||||||
const [usageCount, setUsageCount] = useState(0)
|
const [usageCount, setUsageCount] = useState(0)
|
||||||
const [isPremium] = useState(true)
|
const [isPremium] = useState(true)
|
||||||
const [cacheStatus, setCacheStatus] = useState<{
|
// 移除快取狀態,每次都是新查詢
|
||||||
isCached: boolean
|
|
||||||
cacheHit: boolean
|
|
||||||
usingAI: boolean
|
|
||||||
message: string
|
|
||||||
} | null>(null)
|
|
||||||
|
|
||||||
// 處理句子分析 - 使用真實AI API
|
// 處理句子分析 - 使用真實AI API
|
||||||
const handleAnalyzeSentence = async () => {
|
const handleAnalyzeSentence = async () => {
|
||||||
|
|
@ -72,13 +67,7 @@ function GenerateContent() {
|
||||||
console.log('📦 完整API響應:', result)
|
console.log('📦 完整API響應:', result)
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
// 設定快取狀態
|
// 移除快取狀態,每次都是新的 AI 分析
|
||||||
setCacheStatus({
|
|
||||||
isCached: result.cached || false,
|
|
||||||
cacheHit: result.cacheHit || false,
|
|
||||||
usingAI: result.usingAI || false,
|
|
||||||
message: result.message || '分析完成'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 使用真實AI的回應資料 - 支援兩種key格式 (小寫/大寫)
|
// 使用真實AI的回應資料 - 支援兩種key格式 (小寫/大寫)
|
||||||
setSentenceAnalysis(result.data.wordAnalysis || result.data.WordAnalysis || {})
|
setSentenceAnalysis(result.data.wordAnalysis || result.data.WordAnalysis || {})
|
||||||
|
|
@ -331,25 +320,10 @@ function GenerateContent() {
|
||||||
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
|
<div className="bg-white rounded-xl shadow-sm p-6 mb-6">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h2 className="text-lg font-semibold">句子分析</h2>
|
<h2 className="text-lg font-semibold">句子分析</h2>
|
||||||
{cacheStatus && (
|
<div className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-blue-100 text-blue-800">
|
||||||
<div className={`inline-flex items-center px-3 py-1 rounded-full text-sm font-medium ${
|
<span className="mr-1">🤖</span>
|
||||||
cacheStatus.isCached
|
<span>AI 分析</span>
|
||||||
? 'bg-green-100 text-green-800'
|
</div>
|
||||||
: 'bg-blue-100 text-blue-800'
|
|
||||||
}`}>
|
|
||||||
{cacheStatus.isCached ? (
|
|
||||||
<>
|
|
||||||
<span className="mr-1">💾</span>
|
|
||||||
<span>快取結果</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<span className="mr-1">🤖</span>
|
|
||||||
<span>AI 分析</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue