1111 lines
31 KiB
Markdown
1111 lines
31 KiB
Markdown
# AI生成功能前後端串接規格
|
||
|
||
## 📋 **文件資訊**
|
||
|
||
- **文件名稱**: AI生成功能前後端串接規格
|
||
- **版本**: v1.0
|
||
- **建立日期**: 2025-01-25
|
||
- **最後更新**: 2025-01-25
|
||
- **負責團隊**: DramaLing全端開發團隊
|
||
|
||
---
|
||
|
||
## 🎯 **串接架構概述**
|
||
|
||
### **系統架構圖**
|
||
|
||
```
|
||
┌─────────────────┐ HTTP/JSON ┌──────────────────┐ Gemini API ┌─────────────────┐
|
||
│ │ Request │ │ Request │ │
|
||
│ Frontend │ ──────────────► │ Backend API │ ──────────────► │ Google Gemini │
|
||
│ (Next.js) │ │ (.NET Core) │ │ AI Service │
|
||
│ Port 3000 │ ◄────────────── │ Port 5008 │ ◄────────────── │ │
|
||
│ │ Response │ │ Response │ │
|
||
└─────────────────┘ └──────────────────┘ └─────────────────┘
|
||
│ │
|
||
│ │
|
||
▼ ▼
|
||
┌─────────────────┐ ┌──────────────────┐
|
||
│ Local Storage │ │ SQLite Database │
|
||
│ - auth_token │ │ - Cache │
|
||
│ - user_level │ │ - Usage Stats │
|
||
└─────────────────┘ └──────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 🔄 **API串接流程**
|
||
|
||
### **完整用戶操作流程**
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant U as 用戶
|
||
participant F as 前端(3000)
|
||
participant B as 後端(5008)
|
||
participant G as Gemini API
|
||
participant C as Cache
|
||
participant D as Database
|
||
|
||
U->>F: 1. 輸入英文句子
|
||
U->>F: 2. 點擊「分析句子」
|
||
|
||
F->>F: 3. 驗證輸入(≤300字符)
|
||
F->>F: 4. 讀取userLevel
|
||
|
||
F->>B: 5. POST /api/ai/analyze-sentence
|
||
Note over F,B: Content-Type: application/json<br/>Body: {inputText, userLevel, options}
|
||
|
||
B->>B: 6. 輸入驗證
|
||
B->>C: 7. 檢查快取
|
||
|
||
alt 快取命中
|
||
C->>B: 8a. 返回快取結果
|
||
B->>F: 9a. 返回分析結果
|
||
else 快取未命中
|
||
B->>G: 8b. 調用Gemini API
|
||
G->>B: 9b. AI分析結果
|
||
B->>B: 10. 解析和處理
|
||
B->>C: 11. 儲存快取
|
||
B->>D: 12. 記錄使用統計
|
||
B->>F: 13. 返回分析結果
|
||
end
|
||
|
||
F->>F: 14. 處理API回應
|
||
F->>F: 15. 渲染詞彙標記
|
||
F->>U: 16. 顯示分析結果
|
||
|
||
U->>F: 17. 點擊詞彙
|
||
F->>F: 18. 顯示詞彙彈窗
|
||
|
||
U->>F: 19. 保存詞卡
|
||
F->>B: 20. POST /api/flashcards
|
||
B->>D: 21. 儲存詞卡
|
||
B->>F: 22. 返回成功狀態
|
||
F->>U: 23. 顯示成功提示
|
||
```
|
||
|
||
---
|
||
|
||
## 📡 **API端點詳細串接**
|
||
|
||
### **1. 句子分析API**
|
||
|
||
#### **前端請求實現**
|
||
```typescript
|
||
// 位置: frontend/app/generate/page.tsx:65-87
|
||
const handleAnalyzeSentence = async () => {
|
||
const userLevel = localStorage.getItem('userEnglishLevel') || 'A2'
|
||
|
||
const response = await fetch('http://localhost:5008/api/ai/analyze-sentence', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json'
|
||
},
|
||
body: JSON.stringify({
|
||
inputText: textInput,
|
||
userLevel: userLevel,
|
||
analysisMode: 'full',
|
||
options: {
|
||
includeGrammarCheck: true,
|
||
includeVocabularyAnalysis: true,
|
||
includeTranslation: true,
|
||
includeIdiomDetection: true,
|
||
includeExamples: true
|
||
}
|
||
})
|
||
})
|
||
|
||
const result = await response.json()
|
||
// 處理API回應...
|
||
}
|
||
```
|
||
|
||
#### **後端處理實現**
|
||
```csharp
|
||
// 位置: backend/Controllers/AIController.cs:32-105
|
||
[HttpPost("analyze-sentence")]
|
||
public async Task<ActionResult<SentenceAnalysisResponse>> AnalyzeSentence(
|
||
[FromBody] SentenceAnalysisRequest request)
|
||
{
|
||
// 1. 輸入驗證
|
||
if (!ModelState.IsValid) return BadRequest(...)
|
||
|
||
// 2. 檢查快取
|
||
var cachedResult = await _cacheService.GetCachedAnalysisAsync(request.InputText);
|
||
if (cachedResult != null) return Ok(...)
|
||
|
||
// 3. 調用Gemini AI
|
||
var analysisData = await _geminiService.AnalyzeSentenceAsync(
|
||
request.InputText, request.UserLevel, options);
|
||
|
||
// 4. 快取結果
|
||
await _cacheService.SetCachedAnalysisAsync(request.InputText, analysisData, TimeSpan.FromHours(24));
|
||
|
||
// 5. 返回結果
|
||
return Ok(new SentenceAnalysisResponse { Success = true, Data = analysisData });
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 💾 **數據格式串接**
|
||
|
||
### **前端到後端請求格式**
|
||
|
||
#### **SentenceAnalysisRequest**
|
||
```json
|
||
{
|
||
"inputText": "She just join the team, so let's cut her some slack until she get used to the workflow.",
|
||
"userLevel": "A2",
|
||
"analysisMode": "full",
|
||
"options": {
|
||
"includeGrammarCheck": true,
|
||
"includeVocabularyAnalysis": true,
|
||
"includeTranslation": true,
|
||
"includeIdiomDetection": true,
|
||
"includeExamples": true
|
||
}
|
||
}
|
||
```
|
||
|
||
### **後端到前端回應格式**
|
||
|
||
#### **SentenceAnalysisResponse**
|
||
```json
|
||
{
|
||
"success": true,
|
||
"processingTime": 2.34,
|
||
"data": {
|
||
"analysisId": "uuid-string",
|
||
"originalText": "She just join the team, so let's cut her some slack until she get used to the workflow.",
|
||
"grammarCorrection": {
|
||
"hasErrors": true,
|
||
"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'"
|
||
}
|
||
]
|
||
},
|
||
"sentenceMeaning": "她剛加入團隊,所以讓我們對她寬容一點,直到她習慣工作流程。",
|
||
"vocabularyAnalysis": {
|
||
"she": {
|
||
"word": "she",
|
||
"translation": "她",
|
||
"definition": "female person pronoun",
|
||
"partOfSpeech": "pronoun",
|
||
"pronunciation": "/ʃiː/",
|
||
"difficultyLevel": "A1",
|
||
"frequency": "very_high",
|
||
"synonyms": ["her"],
|
||
"example": "She is a teacher.",
|
||
"exampleTranslation": "她是一名老師。",
|
||
"tags": ["basic", "pronoun"]
|
||
},
|
||
"cut someone some slack": {
|
||
"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",
|
||
"frequency": "medium",
|
||
"synonyms": ["be lenient", "be forgiving", "give leeway"],
|
||
"example": "Cut him some slack, he's new here.",
|
||
"exampleTranslation": "對他寬容一點,他是新來的。",
|
||
"tags": ["idiom", "workplace", "tolerance"]
|
||
}
|
||
},
|
||
"statistics": {
|
||
"totalWords": 16,
|
||
"uniqueWords": 15,
|
||
"simpleWords": 8,
|
||
"moderateWords": 4,
|
||
"difficultWords": 3,
|
||
"idioms": 1,
|
||
"averageDifficulty": "A2"
|
||
},
|
||
"metadata": {
|
||
"analysisModel": "gemini-pro",
|
||
"analysisVersion": "1.0",
|
||
"processingDate": "2025-01-25T10:30:00Z",
|
||
"userLevel": "A2"
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🔧 **前端處理邏輯**
|
||
|
||
### **API回應處理**
|
||
|
||
#### **數據解析和狀態更新**
|
||
```typescript
|
||
// 位置: frontend/app/generate/page.tsx:101-124
|
||
const apiData = result.data
|
||
|
||
// 設定分析結果
|
||
setSentenceAnalysis(apiData.vocabularyAnalysis || {})
|
||
setSentenceMeaning(apiData.sentenceMeaning || '')
|
||
|
||
// 處理語法修正
|
||
if (apiData.grammarCorrection) {
|
||
setGrammarCorrection({
|
||
hasErrors: apiData.grammarCorrection.hasErrors,
|
||
originalText: textInput,
|
||
correctedText: apiData.grammarCorrection.correctedText || textInput,
|
||
corrections: apiData.grammarCorrection.corrections || []
|
||
})
|
||
|
||
// 使用修正後的文本作為最終文本
|
||
setFinalText(apiData.grammarCorrection.correctedText || textInput)
|
||
} else {
|
||
setFinalText(textInput)
|
||
}
|
||
|
||
setShowAnalysisView(true)
|
||
```
|
||
|
||
### **詞彙標記渲染**
|
||
|
||
#### **CEFR等級比較邏輯**
|
||
```typescript
|
||
// 位置: frontend/components/ClickableTextV2.tsx:107-129
|
||
const getWordClass = useCallback((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) return ""
|
||
|
||
// 慣用語不在句子中顯示標記,統一在慣用語區域展示
|
||
|
||
const difficultyLevel = getWordProperty(wordAnalysis, 'difficultyLevel') || 'A1'
|
||
const userLevel = typeof window !== 'undefined' ? localStorage.getItem('userEnglishLevel') || 'A2' : 'A2'
|
||
|
||
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`
|
||
}
|
||
}, [findWordAnalysis, getWordProperty, getLevelIndex])
|
||
```
|
||
|
||
### **統計卡片計算**
|
||
|
||
#### **詞彙分類統計**
|
||
```typescript
|
||
// 位置: frontend/app/generate/page.tsx:353-383
|
||
const vocabularyStats = useMemo(() => {
|
||
if (!sentenceAnalysis) return null
|
||
|
||
const userLevel = localStorage.getItem('userEnglishLevel') || 'A2'
|
||
let simpleCount = 0, moderateCount = 0, difficultCount = 0, idiomCount = 0
|
||
|
||
Object.entries(sentenceAnalysis).forEach(([, wordData]: [string, any]) => {
|
||
// 慣用語由獨立的 idioms 陣列處理,不在 vocabularyAnalysis 中
|
||
const difficultyLevel = wordData?.difficultyLevel || 'A1'
|
||
|
||
// 所有 vocabularyAnalysis 中的詞彙都是一般詞彙,無慣用語
|
||
const userIndex = getLevelIndex(userLevel)
|
||
const wordIndex = getLevelIndex(difficultyLevel)
|
||
|
||
if (userIndex > wordIndex) {
|
||
simpleCount++
|
||
} else if (userIndex === wordIndex) {
|
||
moderateCount++
|
||
} else {
|
||
difficultCount++
|
||
}
|
||
}
|
||
})
|
||
|
||
// idiomCount 由獨立的 idioms 陣列長度計算
|
||
return { simpleCount, moderateCount, difficultCount, idiomCount: sentenceAnalysis.idioms?.length || 0 }
|
||
}, [sentenceAnalysis])
|
||
```
|
||
|
||
---
|
||
|
||
## 🏗️ **後端處理架構**
|
||
|
||
### **Gemini API整合**
|
||
|
||
#### **AI分析服務實現**
|
||
```csharp
|
||
// 位置: backend/Services/GeminiService.cs:33-56
|
||
public async Task<SentenceAnalysisData> AnalyzeSentenceAsync(string inputText, string userLevel, AnalysisOptions options)
|
||
{
|
||
var startTime = DateTime.UtcNow;
|
||
|
||
try
|
||
{
|
||
_logger.LogInformation("Starting sentence analysis for text: {Text}, UserLevel: {UserLevel}",
|
||
inputText.Substring(0, Math.Min(50, inputText.Length)), userLevel);
|
||
|
||
var prompt = BuildAnalysisPrompt(inputText, userLevel, options);
|
||
var response = await CallGeminiAPI(prompt);
|
||
var analysisData = ParseGeminiResponse(response, inputText, userLevel);
|
||
|
||
var processingTime = (DateTime.UtcNow - startTime).TotalSeconds;
|
||
analysisData.Metadata.ProcessingDate = DateTime.UtcNow;
|
||
|
||
_logger.LogInformation("Sentence analysis completed in {ProcessingTime}s", processingTime);
|
||
|
||
return analysisData;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "Error analyzing sentence: {Text}", inputText);
|
||
throw;
|
||
}
|
||
}
|
||
```
|
||
|
||
#### **智能Prompt構建**
|
||
```csharp
|
||
// 位置: backend/Services/GeminiService.cs:58-105
|
||
private string BuildAnalysisPrompt(string inputText, string userLevel, AnalysisOptions options)
|
||
{
|
||
var userIndex = Array.IndexOf(_cefrLevels, userLevel);
|
||
var targetLevels = GetTargetLevels(userIndex);
|
||
|
||
return $@"
|
||
請分析以下英文句子並以JSON格式回應:
|
||
句子: ""{inputText}""
|
||
學習者程度: {userLevel}
|
||
|
||
請提供完整的分析,包含:
|
||
|
||
1. 語法檢查:檢查是否有語法錯誤,如有則提供修正建議
|
||
2. 詞彙分析:分析句子中每個有意義的詞彙
|
||
3. 中文翻譯:提供自然流暢的繁體中文翻譯
|
||
4. 慣用語識別:識別句子中的慣用語和片語
|
||
|
||
詞彙分析要求:
|
||
- 為每個詞彙標註CEFR等級 (A1-C2)
|
||
- 慣用語單獨放在 idioms 陣列中,不在 vocabularyAnalysis 中
|
||
- 提供IPA發音標記
|
||
- 包含同義詞
|
||
- 提供適當的例句和翻譯
|
||
|
||
重要:回應必須是有效的JSON格式,不要包含任何其他文字。";
|
||
}
|
||
```
|
||
|
||
### **緩存機制整合**
|
||
|
||
#### **分析結果緩存**
|
||
```csharp
|
||
// 位置: backend/Services/AnalysisCacheService.cs:33-60
|
||
public async Task<SentenceAnalysisCache?> GetCachedAnalysisAsync(string inputText)
|
||
{
|
||
try
|
||
{
|
||
var textHash = GenerateTextHash(inputText);
|
||
var cached = await _context.SentenceAnalysisCache
|
||
.FirstOrDefaultAsync(c => c.InputTextHash == textHash && c.ExpiresAt > DateTime.UtcNow);
|
||
|
||
if (cached != null)
|
||
{
|
||
// 更新訪問統計
|
||
cached.AccessCount++;
|
||
cached.LastAccessedAt = DateTime.UtcNow;
|
||
await _context.SaveChangesAsync();
|
||
|
||
_logger.LogInformation("Cache hit for text hash: {TextHash}", textHash);
|
||
return cached;
|
||
}
|
||
|
||
_logger.LogInformation("Cache miss for text hash: {TextHash}", textHash);
|
||
return null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError(ex, "Error getting cached analysis for text: {InputText}", inputText);
|
||
return null;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🎨 **前端UI串接**
|
||
|
||
### **詞彙標記渲染**
|
||
|
||
#### **ClickableTextV2組件**
|
||
```typescript
|
||
// 位置: frontend/components/ClickableTextV2.tsx:281-299
|
||
return (
|
||
<div className="relative">
|
||
<div className="text-lg" style={{lineHeight: '2.5'}}>
|
||
{words.map((word, index) => {
|
||
if (word.trim() === '' || /^[.,!?;:\s]+$/.test(word)) {
|
||
return <span key={index}>{word}</span>
|
||
}
|
||
|
||
const className = getWordClass(word)
|
||
const icon = getWordIcon(word)
|
||
|
||
return (
|
||
<span
|
||
key={index}
|
||
className={className}
|
||
onClick={(e) => handleWordClick(word, e)}
|
||
>
|
||
{word}
|
||
{icon}
|
||
</span>
|
||
)
|
||
})}
|
||
</div>
|
||
<VocabPopup />
|
||
</div>
|
||
)
|
||
```
|
||
|
||
### **統計卡片展示**
|
||
|
||
#### **四張統計卡片實現**
|
||
```tsx
|
||
// 位置: frontend/app/generate/page.tsx:581-605
|
||
{vocabularyStats && (
|
||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4 mb-6">
|
||
{/* 簡單詞彙卡片 */}
|
||
<div className="bg-gray-50 border border-dashed border-gray-300 rounded-lg p-3 sm:p-4 text-center">
|
||
<div className="text-xl sm:text-2xl font-bold text-gray-600 mb-1">{vocabularyStats.simpleCount}</div>
|
||
<div className="text-gray-600 text-xs sm:text-sm font-medium">太簡單啦</div>
|
||
</div>
|
||
|
||
{/* 適中詞彙卡片 */}
|
||
<div className="bg-green-50 border border-green-200 rounded-lg p-3 sm:p-4 text-center">
|
||
<div className="text-xl sm:text-2xl font-bold text-green-700 mb-1">{vocabularyStats.moderateCount}</div>
|
||
<div className="text-green-700 text-xs sm:text-sm font-medium">重點學習</div>
|
||
</div>
|
||
|
||
{/* 艱難詞彙卡片 */}
|
||
<div className="bg-orange-50 border border-orange-200 rounded-lg p-3 sm:p-4 text-center">
|
||
<div className="text-xl sm:text-2xl font-bold text-orange-700 mb-1">{vocabularyStats.difficultCount}</div>
|
||
<div className="text-orange-700 text-xs sm:text-sm font-medium">有點挑戰</div>
|
||
</div>
|
||
|
||
{/* 慣用語卡片 */}
|
||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-3 sm:p-4 text-center">
|
||
<div className="text-xl sm:text-2xl font-bold text-blue-700 mb-1">{vocabularyStats.idiomCount}</div>
|
||
<div className="text-blue-700 text-xs sm:text-sm font-medium">慣用語</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
```
|
||
|
||
---
|
||
|
||
## 🔒 **認證與安全串接**
|
||
|
||
### **JWT Token處理**
|
||
|
||
#### **前端Token管理**
|
||
```typescript
|
||
// 位置: frontend/lib/services/auth.ts:77-81
|
||
if (response.success && response.data?.token) {
|
||
// Store token in localStorage
|
||
localStorage.setItem('auth_token', response.data.token);
|
||
localStorage.setItem('user_data', JSON.stringify(response.data.user));
|
||
}
|
||
|
||
// 位置: frontend/lib/services/flashcards.ts:72-82
|
||
private async makeRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||
const token = this.getAuthToken();
|
||
const url = `${API_BASE_URL}/api${endpoint}`;
|
||
|
||
const response = await fetch(url, {
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...(token && { 'Authorization': `Bearer ${token}` }),
|
||
...options.headers,
|
||
},
|
||
...options,
|
||
});
|
||
}
|
||
```
|
||
|
||
#### **後端認證驗證**
|
||
```csharp
|
||
// 位置: backend/Controllers/AIController.cs:123-135
|
||
private string GetCurrentUserId()
|
||
{
|
||
var userIdClaim = User.FindFirst(ClaimTypes.NameIdentifier)
|
||
?? User.FindFirst("sub")
|
||
?? User.FindFirst("user_id");
|
||
|
||
if (userIdClaim?.Value == null)
|
||
{
|
||
throw new UnauthorizedAccessException("用戶ID未找到");
|
||
}
|
||
|
||
return userIdClaim.Value;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🔄 **錯誤處理串接**
|
||
|
||
### **前端錯誤處理**
|
||
|
||
#### **API錯誤捕獲**
|
||
```typescript
|
||
// 位置: frontend/app/generate/page.tsx:89-99
|
||
if (!response.ok) {
|
||
const errorData = await response.json()
|
||
throw new Error(errorData.error?.message || `API請求失敗: ${response.status}`)
|
||
}
|
||
|
||
const result = await response.json()
|
||
|
||
if (!result.success || !result.data) {
|
||
throw new Error('API回應格式錯誤')
|
||
}
|
||
```
|
||
|
||
#### **優雅錯誤回退**
|
||
```typescript
|
||
// 位置: frontend/app/generate/page.tsx:125-137
|
||
} catch (error) {
|
||
console.error('Error in sentence analysis:', error)
|
||
setGrammarCorrection({
|
||
hasErrors: true,
|
||
originalText: textInput,
|
||
correctedText: textInput,
|
||
corrections: []
|
||
})
|
||
setSentenceMeaning('分析過程中發生錯誤,請稍後再試。')
|
||
setFinalText(textInput)
|
||
setShowAnalysisView(true)
|
||
}
|
||
```
|
||
|
||
### **後端錯誤處理**
|
||
|
||
#### **結構化錯誤回應**
|
||
```csharp
|
||
// 位置: backend/Controllers/AIController.cs:137-155
|
||
private ApiErrorResponse CreateErrorResponse(string code, string message, object? details, string requestId)
|
||
{
|
||
var suggestions = GetSuggestionsForError(code);
|
||
|
||
return new ApiErrorResponse
|
||
{
|
||
Success = false,
|
||
Error = new ApiError
|
||
{
|
||
Code = code,
|
||
Message = message,
|
||
Details = details,
|
||
Suggestions = suggestions
|
||
},
|
||
RequestId = requestId,
|
||
Timestamp = DateTime.UtcNow
|
||
};
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🚀 **性能優化串接**
|
||
|
||
### **前端性能優化**
|
||
|
||
#### **React記憶化**
|
||
```typescript
|
||
// 統計計算優化
|
||
const vocabularyStats = useMemo(() => {
|
||
// 計算邏輯...
|
||
}, [sentenceAnalysis])
|
||
|
||
// 詞彙分類函數記憶化
|
||
const getWordClass = useCallback((word: string) => {
|
||
// 分類邏輯...
|
||
}, [findWordAnalysis, getWordProperty, getLevelIndex])
|
||
|
||
// 彈窗定位計算優化
|
||
const calculatePopupPosition = useCallback((rect: DOMRect) => {
|
||
// 位置計算邏輯...
|
||
}, [])
|
||
```
|
||
|
||
### **後端性能優化**
|
||
|
||
#### **快取策略**
|
||
```csharp
|
||
// 快取鍵生成: 文本內容 + 用戶等級
|
||
var cacheKey = $"{request.InputText}_{request.UserLevel}";
|
||
|
||
// 快取過期時間: 24小時
|
||
await _cacheService.SetCachedAnalysisAsync(cacheKey, analysisData, TimeSpan.FromHours(24));
|
||
|
||
// 智能快取清理
|
||
[BackgroundService]
|
||
public class CacheCleanupService : BackgroundService
|
||
{
|
||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||
{
|
||
while (!stoppingToken.IsCancellationRequested)
|
||
{
|
||
await _cacheService.CleanExpiredCacheAsync();
|
||
await Task.Delay(TimeSpan.FromHours(1), stoppingToken);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🔧 **配置管理串接**
|
||
|
||
### **環境配置**
|
||
|
||
#### **前端環境設定**
|
||
```typescript
|
||
// API基礎URL配置
|
||
const API_BASE_URL = 'http://localhost:5008'; // 開發環境
|
||
// const API_BASE_URL = 'https://api.dramaling.com'; // 生產環境
|
||
|
||
// 字符限制配置
|
||
const MAX_MANUAL_INPUT_LENGTH = 300;
|
||
|
||
// CEFR等級配置
|
||
const CEFR_LEVELS = ['A1', 'A2', 'B1', 'B2', 'C1', 'C2'] as const;
|
||
```
|
||
|
||
#### **後端配置管理**
|
||
```csharp
|
||
// User Secrets配置
|
||
dotnet user-secrets set "AI:GeminiApiKey" "your-real-gemini-api-key"
|
||
|
||
// appsettings.json配置
|
||
{
|
||
"Gemini": {
|
||
"ApiKey": "fallback-key" // 僅作為後備
|
||
},
|
||
"ConnectionStrings": {
|
||
"DefaultConnection": "Data Source=dramaling_test.db"
|
||
}
|
||
}
|
||
|
||
// 環境變數配置
|
||
Environment.GetEnvironmentVariable("GEMINI_API_KEY") // 優先級最高
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 **數據流向分析**
|
||
|
||
### **請求數據流**
|
||
|
||
```
|
||
1. 用戶輸入 → 前端驗證 → API請求構建
|
||
2. HTTP POST → 後端接收 → 模型驗證
|
||
3. 快取檢查 → AI服務調用 → 結果處理
|
||
4. 數據序列化 → HTTP回應 → 前端接收
|
||
5. JSON解析 → 狀態更新 → UI重新渲染
|
||
```
|
||
|
||
### **回應數據流**
|
||
|
||
```
|
||
1. Gemini API → JSON回應 → 後端解析
|
||
2. 數據轉換 → DTO映射 → 統計計算
|
||
3. 快取儲存 → 結構化回應 → 前端接收
|
||
4. 狀態分派 → 組件更新 → 詞彙標記
|
||
5. 彈窗渲染 → 統計卡片 → 用戶互動
|
||
```
|
||
|
||
---
|
||
|
||
## 🧪 **測試串接規格**
|
||
|
||
### **前端測試**
|
||
|
||
#### **單元測試**
|
||
```typescript
|
||
// 測試API調用
|
||
describe('handleAnalyzeSentence', () => {
|
||
it('should call API with correct parameters', async () => {
|
||
// Mock fetch
|
||
global.fetch = jest.fn(() =>
|
||
Promise.resolve({
|
||
ok: true,
|
||
json: () => Promise.resolve(mockApiResponse),
|
||
})
|
||
);
|
||
|
||
await handleAnalyzeSentence();
|
||
|
||
expect(fetch).toHaveBeenCalledWith(
|
||
'http://localhost:5008/api/ai/analyze-sentence',
|
||
{
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
inputText: 'test sentence',
|
||
userLevel: 'A2',
|
||
analysisMode: 'full'
|
||
})
|
||
}
|
||
);
|
||
});
|
||
});
|
||
```
|
||
|
||
#### **整合測試**
|
||
```typescript
|
||
// 測試完整流程
|
||
describe('AI Analysis Integration', () => {
|
||
it('should complete full analysis workflow', async () => {
|
||
// 1. 輸入文本
|
||
fireEvent.change(screen.getByRole('textbox'), {
|
||
target: { value: 'She just join the team' }
|
||
});
|
||
|
||
// 2. 點擊分析按鈕
|
||
fireEvent.click(screen.getByText('🔍 分析句子'));
|
||
|
||
// 3. 等待API回應
|
||
await waitFor(() => {
|
||
expect(screen.getByText('語法修正建議')).toBeInTheDocument();
|
||
});
|
||
|
||
// 4. 驗證詞彙標記
|
||
expect(screen.getByText('she')).toHaveClass('bg-gray-50');
|
||
expect(screen.getByText('join')).toHaveClass('bg-orange-50');
|
||
});
|
||
});
|
||
```
|
||
|
||
### **後端測試**
|
||
|
||
#### **API端點測試**
|
||
```csharp
|
||
[Test]
|
||
public async Task AnalyzeSentence_WithValidInput_ReturnsSuccessResponse()
|
||
{
|
||
// Arrange
|
||
var request = new SentenceAnalysisRequest
|
||
{
|
||
InputText = "She just join the team",
|
||
UserLevel = "A2",
|
||
AnalysisMode = "full"
|
||
};
|
||
|
||
// Act
|
||
var response = await _controller.AnalyzeSentence(request);
|
||
|
||
// Assert
|
||
var okResult = Assert.IsType<OkObjectResult>(response.Result);
|
||
var analysisResponse = Assert.IsType<SentenceAnalysisResponse>(okResult.Value);
|
||
|
||
Assert.True(analysisResponse.Success);
|
||
Assert.NotNull(analysisResponse.Data);
|
||
Assert.NotEmpty(analysisResponse.Data.VocabularyAnalysis);
|
||
}
|
||
```
|
||
|
||
#### **Gemini服務測試**
|
||
```csharp
|
||
[Test]
|
||
public async Task GeminiService_WithValidApiKey_CallsRealAPI()
|
||
{
|
||
// Arrange
|
||
var service = new GeminiService(_httpClient, _configuration, _logger);
|
||
|
||
// Act
|
||
var result = await service.AnalyzeSentenceAsync("test sentence", "A2", new AnalysisOptions());
|
||
|
||
// Assert
|
||
Assert.NotNull(result);
|
||
Assert.NotEmpty(result.VocabularyAnalysis);
|
||
Assert.Equal("gemini-pro", result.Metadata.AnalysisModel);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🔧 **開發環境串接**
|
||
|
||
### **啟動順序**
|
||
|
||
#### **開發環境啟動腳本**
|
||
```bash
|
||
# 1. 啟動後端 (Port 5008)
|
||
cd backend/DramaLing.Api
|
||
dotnet run
|
||
|
||
# 2. 啟動前端 (Port 3000)
|
||
cd frontend
|
||
npm run dev
|
||
|
||
# 3. 設置Gemini API Key
|
||
dotnet user-secrets set "AI:GeminiApiKey" "your-real-api-key"
|
||
```
|
||
|
||
#### **健康檢查**
|
||
```bash
|
||
# 檢查後端健康狀態
|
||
curl http://localhost:5008/health
|
||
curl http://localhost:5008/api/ai/health
|
||
|
||
# 檢查前端運行狀態
|
||
curl http://localhost:3000
|
||
```
|
||
|
||
### **調試工具**
|
||
|
||
#### **前端調試**
|
||
```typescript
|
||
// 在瀏覽器Console中
|
||
console.log('API回應:', result.data);
|
||
console.log('詞彙分析:', sentenceAnalysis);
|
||
console.log('統計資料:', vocabularyStats);
|
||
|
||
// Network面板查看
|
||
// 檢查API請求和回應內容
|
||
// 驗證請求頭和載荷格式
|
||
```
|
||
|
||
#### **後端調試**
|
||
```csharp
|
||
// 日誌輸出
|
||
_logger.LogInformation("Processing sentence analysis request {RequestId} for user {UserId}", requestId, userId);
|
||
_logger.LogInformation("Gemini API response: {Response}", response);
|
||
_logger.LogInformation("Sentence analysis completed in {ProcessingTime}s", processingTime);
|
||
|
||
// SQL查詢調試
|
||
// 檢查Entity Framework查詢日誌
|
||
// 監控數據庫性能指標
|
||
```
|
||
|
||
---
|
||
|
||
## 📈 **監控與分析**
|
||
|
||
### **性能監控**
|
||
|
||
#### **前端性能指標**
|
||
```typescript
|
||
// 性能測量
|
||
const startTime = performance.now();
|
||
await handleAnalyzeSentence();
|
||
const endTime = performance.now();
|
||
console.log(`分析耗時: ${endTime - startTime}ms`);
|
||
|
||
// 記憶體使用監控
|
||
const observer = new PerformanceObserver((list) => {
|
||
list.getEntries().forEach((entry) => {
|
||
console.log('Performance:', entry.name, entry.duration);
|
||
});
|
||
});
|
||
observer.observe({ entryTypes: ['measure'] });
|
||
```
|
||
|
||
#### **後端性能指標**
|
||
```csharp
|
||
// 請求處理時間
|
||
var stopwatch = Stopwatch.StartNew();
|
||
// ... 處理邏輯
|
||
stopwatch.Stop();
|
||
_logger.LogInformation("Request processed in {ElapsedMs}ms", stopwatch.ElapsedMilliseconds);
|
||
|
||
// 資料庫查詢性能
|
||
services.AddDbContext<DramaLingDbContext>(options =>
|
||
{
|
||
options.UseSqlite(connectionString)
|
||
.EnableSensitiveDataLogging()
|
||
.LogTo(Console.WriteLine, LogLevel.Information);
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 🔮 **未來擴展串接**
|
||
|
||
### **批次分析支援**
|
||
|
||
#### **前端批次請求**
|
||
```typescript
|
||
interface BatchAnalysisRequest {
|
||
sentences: string[];
|
||
userLevel: string;
|
||
analysisMode: string;
|
||
}
|
||
|
||
const handleBatchAnalysis = async (sentences: string[]) => {
|
||
const response = await fetch('/api/ai/batch-analyze', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
sentences,
|
||
userLevel: getUserLevel(),
|
||
analysisMode: 'full'
|
||
})
|
||
});
|
||
};
|
||
```
|
||
|
||
#### **後端批次處理**
|
||
```csharp
|
||
[HttpPost("batch-analyze")]
|
||
public async Task<ActionResult<BatchAnalysisResponse>> BatchAnalyze(
|
||
[FromBody] BatchAnalysisRequest request)
|
||
{
|
||
var results = new List<SentenceAnalysisData>();
|
||
|
||
foreach (var sentence in request.Sentences)
|
||
{
|
||
var analysis = await _geminiService.AnalyzeSentenceAsync(
|
||
sentence, request.UserLevel, request.Options);
|
||
results.Add(analysis);
|
||
}
|
||
|
||
return Ok(new BatchAnalysisResponse { Results = results });
|
||
}
|
||
```
|
||
|
||
### **即時分析支援**
|
||
|
||
#### **WebSocket連接**
|
||
```typescript
|
||
// 前端WebSocket客戶端
|
||
const ws = new WebSocket('ws://localhost:5008/ws/analysis');
|
||
|
||
ws.onmessage = (event) => {
|
||
const partialResult = JSON.parse(event.data);
|
||
updateAnalysisProgress(partialResult);
|
||
};
|
||
|
||
// 即時分析請求
|
||
ws.send(JSON.stringify({
|
||
type: 'analyze',
|
||
inputText: textInput,
|
||
userLevel: userLevel
|
||
}));
|
||
```
|
||
|
||
#### **SignalR整合**
|
||
```csharp
|
||
// 後端SignalR Hub
|
||
public class AnalysisHub : Hub
|
||
{
|
||
public async Task StartAnalysis(string inputText, string userLevel)
|
||
{
|
||
await Clients.Caller.SendAsync("AnalysisStarted");
|
||
|
||
// 分段回傳結果
|
||
await Clients.Caller.SendAsync("GrammarCheckComplete", grammarResult);
|
||
await Clients.Caller.SendAsync("VocabularyAnalysisComplete", vocabResult);
|
||
await Clients.Caller.SendAsync("AnalysisComplete", finalResult);
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📋 **串接檢查清單**
|
||
|
||
### **開發階段檢查**
|
||
|
||
#### **前端檢查項目**
|
||
- [ ] API端點URL正確配置
|
||
- [ ] 請求格式符合後端期望
|
||
- [ ] 錯誤處理涵蓋所有情況
|
||
- [ ] 認證Token正確傳遞
|
||
- [ ] 響應數據正確解析
|
||
- [ ] UI狀態正確更新
|
||
- [ ] 性能優化已實施
|
||
|
||
#### **後端檢查項目**
|
||
- [ ] API端點路由正確設置
|
||
- [ ] 輸入驗證完整實現
|
||
- [ ] Gemini API正確調用
|
||
- [ ] 數據模型匹配前端需求
|
||
- [ ] 錯誤處理完善
|
||
- [ ] 緩存機制有效
|
||
- [ ] 日誌記錄詳細
|
||
|
||
### **生產環境檢查**
|
||
|
||
#### **安全性檢查**
|
||
- [ ] API Key安全存儲
|
||
- [ ] JWT認證正確實施
|
||
- [ ] CORS策略適當配置
|
||
- [ ] 輸入驗證防止注入
|
||
- [ ] 錯誤訊息不洩露敏感資訊
|
||
|
||
#### **效能檢查**
|
||
- [ ] API回應時間 < 5秒
|
||
- [ ] 快取命中率 > 70%
|
||
- [ ] 記憶體使用穩定
|
||
- [ ] 資料庫查詢優化
|
||
- [ ] 並發處理正常
|
||
|
||
---
|
||
|
||
## 📞 **故障排除指南**
|
||
|
||
### **常見串接問題**
|
||
|
||
#### **CORS錯誤**
|
||
```
|
||
錯誤: Access to fetch blocked by CORS policy
|
||
解決: 檢查後端CORS設定是否包含前端域名
|
||
```
|
||
|
||
#### **認證失敗**
|
||
```
|
||
錯誤: 401 Unauthorized
|
||
解決: 檢查JWT Token是否有效、格式是否正確
|
||
```
|
||
|
||
#### **數據格式不匹配**
|
||
```
|
||
錯誤: Cannot read property 'vocabularyAnalysis' of undefined
|
||
解決: 檢查後端回應格式是否符合前端期望
|
||
```
|
||
|
||
#### **API超時**
|
||
```
|
||
錯誤: Network timeout
|
||
解決: 檢查Gemini API是否可達、增加超時設定
|
||
```
|
||
|
||
### **調試步驟**
|
||
|
||
1. **檢查網路連通性**: `curl http://localhost:5008/health`
|
||
2. **驗證API端點**: `curl -X POST http://localhost:5008/api/ai/analyze-sentence`
|
||
3. **檢查前端日誌**: 瀏覽器開發者工具Console
|
||
4. **檢查後端日誌**: dotnet應用程式輸出
|
||
5. **驗證數據庫**: 檢查SQLite文件和表結構
|
||
|
||
---
|
||
|
||
**文件版本**: v1.0
|
||
**對應實現**: main分支 commit 03c1756
|
||
**最後驗證**: 2025-01-25
|
||
**下次檢查**: 功能更新時 |