324 lines
8.7 KiB
Markdown
324 lines
8.7 KiB
Markdown
# 例句圖生成前後端完整整合計劃
|
||
|
||
## 📋 **項目概覽**
|
||
|
||
**目標**: 將已實現的例句圖生成後端 API 完整整合到前端詞卡管理介面
|
||
**預估時間**: 6-9 小時
|
||
**複雜度**: 中等 (需要前後端協調)
|
||
|
||
---
|
||
|
||
## 🎯 **當前狀況評估**
|
||
|
||
### ✅ **已完成功能**
|
||
- **後端 API**: 完整的兩階段圖片生成系統 (Gemini + Replicate)
|
||
- **圖片壓縮**: 自動壓縮 1024x1024 → 512x512
|
||
- **資料庫設計**: 完整的圖片關聯表格和追蹤系統
|
||
- **API 測試**: 至少 1 次成功生成驗證
|
||
- **Git 安全**: wwwroot 已被忽略,API Keys 安全存儲
|
||
|
||
### ❌ **缺失功能**
|
||
- **後端資料整合**: FlashcardsController 未返回圖片資訊
|
||
- **前端 API 整合**: 所有圖片生成功能都未實現
|
||
- **前端狀態管理**: 沒有生成進度追蹤
|
||
- **用戶體驗**: 仍使用硬編碼圖片映射
|
||
|
||
---
|
||
|
||
## 🚀 **Phase 1: 後端資料整合 (1-2 小時)**
|
||
|
||
### 🎯 **目標**: 讓 flashcards API 返回圖片資訊
|
||
|
||
#### **1.1 修改 FlashcardsController (30分鐘)**
|
||
```csharp
|
||
// 當前查詢
|
||
var flashcards = await _context.Flashcards
|
||
.Where(f => f.UserId == userId)
|
||
.ToListAsync();
|
||
|
||
// 改為包含圖片關聯
|
||
var flashcards = await _context.Flashcards
|
||
.Include(f => f.FlashcardExampleImages)
|
||
.ThenInclude(fei => fei.ExampleImage)
|
||
.Where(f => f.UserId == userId)
|
||
.ToListAsync();
|
||
```
|
||
|
||
#### **1.2 擴展 FlashcardDto (30分鐘)**
|
||
```csharp
|
||
public class FlashcardDto
|
||
{
|
||
// 現有欄位...
|
||
|
||
// 新增圖片相關欄位
|
||
public List<ExampleImageDto> ExampleImages { get; set; } = new();
|
||
public bool HasExampleImage => ExampleImages.Any();
|
||
public string? PrimaryImageUrl => ExampleImages.FirstOrDefault(img => img.IsPrimary)?.ImageUrl;
|
||
}
|
||
|
||
public class ExampleImageDto
|
||
{
|
||
public string Id { get; set; }
|
||
public string ImageUrl { get; set; }
|
||
public bool IsPrimary { get; set; }
|
||
public decimal? QualityScore { get; set; }
|
||
}
|
||
```
|
||
|
||
#### **1.3 添加圖片 URL 生成邏輯 (30分鐘)**
|
||
```csharp
|
||
private async Task<List<ExampleImageDto>> MapExampleImages(List<FlashcardExampleImage> flashcardImages)
|
||
{
|
||
var result = new List<ExampleImageDto>();
|
||
|
||
foreach (var item in flashcardImages)
|
||
{
|
||
var imageUrl = await _imageStorageService.GetImageUrlAsync(item.ExampleImage.RelativePath);
|
||
|
||
result.Add(new ExampleImageDto
|
||
{
|
||
Id = item.ExampleImage.Id.ToString(),
|
||
ImageUrl = imageUrl,
|
||
IsPrimary = item.IsPrimary,
|
||
QualityScore = item.ExampleImage.QualityScore
|
||
});
|
||
}
|
||
|
||
return result;
|
||
}
|
||
```
|
||
|
||
#### **1.4 測試後端更新 (30分鐘)**
|
||
- 驗證 API 回應包含圖片資訊
|
||
- 確認圖片 URL 正確生成
|
||
- 測試有圖片和無圖片的詞卡
|
||
|
||
---
|
||
|
||
## 🎨 **Phase 2: 前端 API 服務整合 (2-3 小時)**
|
||
|
||
### 🎯 **目標**: 創建完整的前端圖片生成服務
|
||
|
||
#### **2.1 創建圖片生成 API 服務 (1小時)**
|
||
**檔案**: `/frontend/lib/services/imageGeneration.ts`
|
||
```typescript
|
||
export interface ImageGenerationRequest {
|
||
style: 'cartoon' | 'realistic' | 'minimal';
|
||
priority: 'normal' | 'high' | 'low';
|
||
replicateModel: string;
|
||
options: {
|
||
useGeminiCache: boolean;
|
||
useImageCache: boolean;
|
||
maxRetries: number;
|
||
learnerLevel: string;
|
||
scenario: string;
|
||
};
|
||
}
|
||
|
||
export interface GenerationStatus {
|
||
requestId: string;
|
||
overallStatus: string;
|
||
stages: {
|
||
gemini: StageStatus;
|
||
replicate: StageStatus;
|
||
};
|
||
result?: {
|
||
imageUrl: string;
|
||
imageId: string;
|
||
};
|
||
}
|
||
|
||
export class ImageGenerationService {
|
||
async generateImage(flashcardId: string, request: ImageGenerationRequest): Promise<{requestId: string}> {
|
||
// 調用 POST /api/imagegeneration/flashcards/{flashcardId}/generate
|
||
}
|
||
|
||
async getGenerationStatus(requestId: string): Promise<GenerationStatus> {
|
||
// 調用 GET /api/imagegeneration/requests/{requestId}/status
|
||
}
|
||
|
||
async pollUntilComplete(requestId: string, onProgress?: (status: GenerationStatus) => void): Promise<GenerationStatus> {
|
||
// 輪詢直到完成
|
||
}
|
||
}
|
||
```
|
||
|
||
#### **2.2 創建 React Hook (1小時)**
|
||
**檔案**: `/frontend/hooks/useImageGeneration.ts`
|
||
```typescript
|
||
export const useImageGeneration = () => {
|
||
const [generationStates, setGenerationStates] = useState<Record<string, GenerationState>>({});
|
||
|
||
const generateImage = async (flashcardId: string) => {
|
||
// 啟動生成流程
|
||
// 更新狀態為 generating
|
||
// 開始輪詢進度
|
||
};
|
||
|
||
const getGenerationState = (flashcardId: string) => {
|
||
return generationStates[flashcardId] || { status: 'idle' };
|
||
};
|
||
|
||
return { generateImage, getGenerationState };
|
||
};
|
||
```
|
||
|
||
#### **2.3 更新 flashcards 服務 (30分鐘)**
|
||
**檔案**: `/frontend/lib/services/flashcards.ts`
|
||
```typescript
|
||
export interface Flashcard {
|
||
// 現有欄位...
|
||
|
||
// 新增圖片欄位
|
||
exampleImages: ExampleImage[];
|
||
hasExampleImage: boolean;
|
||
primaryImageUrl?: string;
|
||
}
|
||
|
||
export interface ExampleImage {
|
||
id: string;
|
||
imageUrl: string;
|
||
isPrimary: boolean;
|
||
qualityScore?: number;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🎮 **Phase 3: 前端 UI 整合 (2-3 小時)**
|
||
|
||
### 🎯 **目標**: 完整的用戶介面功能
|
||
|
||
#### **3.1 修改圖片顯示邏輯 (1小時)**
|
||
**檔案**: `/frontend/app/flashcards/page.tsx`
|
||
|
||
```typescript
|
||
// 移除硬編碼映射
|
||
const getExampleImage = (card: Flashcard): string | null => {
|
||
return card.primaryImageUrl || null;
|
||
};
|
||
|
||
const hasExampleImage = (card: Flashcard): boolean => {
|
||
return card.hasExampleImage;
|
||
};
|
||
```
|
||
|
||
#### **3.2 實現圖片生成功能 (1小時)**
|
||
```typescript
|
||
const { generateImage, getGenerationState } = useImageGeneration();
|
||
|
||
const handleGenerateExampleImage = async (card: Flashcard) => {
|
||
try {
|
||
setGeneratingCards(prev => new Set([...prev, card.id]));
|
||
|
||
await generateImage(card.id);
|
||
|
||
// 生成完成後刷新詞卡列表
|
||
await searchActions.refresh();
|
||
|
||
toast.success(`「${card.word}」的例句圖片生成完成!`);
|
||
} catch (error) {
|
||
toast.error(`圖片生成失敗: ${error.message}`);
|
||
} finally {
|
||
setGeneratingCards(prev => {
|
||
const newSet = new Set(prev);
|
||
newSet.delete(card.id);
|
||
return newSet;
|
||
});
|
||
}
|
||
};
|
||
```
|
||
|
||
#### **3.3 添加生成進度 UI (30分鐘)**
|
||
```typescript
|
||
const GenerationProgress = ({ flashcardId }: { flashcardId: string }) => {
|
||
const generationState = getGenerationState(flashcardId);
|
||
|
||
if (generationState.status === 'generating') {
|
||
return (
|
||
<div className="flex items-center gap-2 text-blue-600">
|
||
<Spinner className="w-4 h-4" />
|
||
<span className="text-xs">
|
||
{generationState.currentStage === 'description_generation' ? '生成描述中...' : '生成圖片中...'}
|
||
</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return null;
|
||
};
|
||
```
|
||
|
||
#### **3.4 錯誤處理和重試 (30分鐘)**
|
||
```typescript
|
||
const RetryButton = ({ flashcardId, onRetry }: RetryButtonProps) => {
|
||
return (
|
||
<button
|
||
onClick={() => onRetry(flashcardId)}
|
||
className="text-xs text-red-600 hover:text-red-800"
|
||
>
|
||
重試生成
|
||
</button>
|
||
);
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## 🧪 **Phase 4: 測試與部署 (1 小時)**
|
||
|
||
### **4.1 功能測試 (30分鐘)**
|
||
- 完整的圖片生成流程測試
|
||
- 多詞卡並發生成測試
|
||
- 錯誤情境測試 (網路中斷、API 失敗等)
|
||
|
||
### **4.2 用戶體驗優化 (20分鐘)**
|
||
- 載入動畫調整
|
||
- 成功/失敗訊息優化
|
||
- 響應式顯示調整
|
||
|
||
### **4.3 文檔更新 (10分鐘)**
|
||
- 更新使用說明
|
||
- 記錄整合完成狀態
|
||
|
||
---
|
||
|
||
## 📊 **成功指標**
|
||
|
||
### **功能指標**
|
||
- ✅ 點擊"新增例句圖"按鈕能啟動實際生成
|
||
- ✅ 能看到即時的生成進度 (描述生成 → 圖片生成)
|
||
- ✅ 生成完成後圖片立即顯示在詞卡中
|
||
- ✅ 錯誤處理優雅,用戶體驗流暢
|
||
|
||
### **技術指標**
|
||
- ✅ 前端完全不依賴硬編碼圖片映射
|
||
- ✅ 所有圖片資訊從後端 API 動態載入
|
||
- ✅ 支援多張圖片的詞卡
|
||
- ✅ 完整的狀態管理和錯誤處理
|
||
|
||
### **用戶體驗指標**
|
||
- ✅ 生成進度清楚可見 (預計 2-3 分鐘)
|
||
- ✅ 可以並發生成多個詞卡的圖片
|
||
- ✅ 響應式設計在各裝置正常顯示
|
||
|
||
---
|
||
|
||
## 🎛️ **實施建議**
|
||
|
||
### **建議順序**
|
||
1. **先完成後端整合** - 確保資料正確返回
|
||
2. **再進行前端整合** - 逐步替換硬編碼邏輯
|
||
3. **最後優化體驗** - 完善 UI 和錯誤處理
|
||
|
||
### **風險控制**
|
||
- **漸進式替換**: 保留硬編碼映射作為 fallback
|
||
- **功能開關**: 可以暫時關閉圖片生成功能
|
||
- **測試優先**: 每個階段都要充分測試
|
||
|
||
---
|
||
|
||
**文檔版本**: v1.0
|
||
**建立日期**: 2025-09-24
|
||
**預估完成**: 2025-09-25
|
||
**負責團隊**: 全端開發團隊 |