32 KiB
32 KiB
智能複習系統 - 前端功能規格書 (FFS)
目標讀者: 前端開發工程師、UI/UX設計師 版本: 1.0 日期: 2025-09-25
🎯 功能概述
智能複習系統前端負責呈現動態熟悉度、復習進度追蹤、學習統計等功能,提供直觀的學習體驗。
核心特色
- 實時熟悉度顯示: 詞彙熟悉度隨時間動態變化
- 智能復習提醒: 基於算法的個人化復習建議
- 進度可視化: 清晰的學習進度和統計圖表
- 響應式設計: 支援各種設備和屏幕尺寸
🏗️ 組件架構 (基於現有實現)
實際頁面結構
frontend/
├── app/
│ ├── learn/
│ │ └── page.tsx # 🎯 主復習頁面 (已完成7種題型UI)
│ ├── flashcards/
│ │ ├── page.tsx # 詞卡列表頁面 (已完成)
│ │ └── [id]/page.tsx # 詞卡詳細頁面 (已完成)
│ ├── dashboard/page.tsx # 儀表板 (已完成)
│ └── settings/page.tsx # 設定頁面 (已完成)
├── components/
│ ├── Navigation.tsx # 導航組件 (已完成)
│ ├── AudioPlayer.tsx # 音頻播放組件 (已完成)
│ ├── VoiceRecorder.tsx # 音頻錄製組件 (已完成)
│ ├── LearningComplete.tsx # 學習完成組件 (已完成)
│ └── Toast.tsx # 通知組件 (已完成)
├── lib/services/
│ ├── flashcards.ts # 詞卡API服務 (已完成)
│ ├── auth.ts # 認證服務 (已完成)
│ └── imageGeneration.ts # 圖片生成服務 (已完成)
└── contexts/
└── AuthContext.tsx # 認證上下文 (已完成)
需要新增的智能複習組件
frontend/components/review/ # 🆕 智能複習專用組件
├── ReviewTypeIndicator.tsx # 🆕 題型顯示指示器
├── MasteryIndicator.tsx # 🆕 熟悉度指示器
├── ReviewSchedule.tsx # 🆕 復習排程組件
└── utils/
├── masteryCalculator.ts # 🆕 熟悉度計算工具
├── reviewTypeSelector.ts # 🆕 自動題型選擇邏輯
└── spacedRepetition.ts # 🆕 間隔重複算法整合
frontend/lib/services/
└── review.ts # 🆕 智能複習API服務
frontend/contexts/
└── SpacedRepetitionContext.tsx # 🆕 復習狀態管理
📱 核心組件設計 (基於現有實現)
1. LearnPage 主複習頁面 (已完成)
實際功能實現
- ✅ 7種複習模式完整UI實現
- ✅ 手動模式切換 (待改為自動選擇)
- ✅ 進度追蹤和分數統計
- ✅ 完整的答題反饋系統
- ✅ 音頻播放和錄音整合
- ✅ 響應式設計和動畫效果
現有狀態管理
// 基於實際 learn/page.tsx 的狀態結構
interface LearnPageState {
currentCardIndex: number;
isFlipped: boolean;
mode: 'flip-memory' | 'vocab-choice' | 'vocab-listening' |
'sentence-listening' | 'sentence-fill' | 'sentence-reorder' |
'sentence-speaking';
score: { correct: number; total: number };
selectedAnswer: string | null;
showResult: boolean;
fillAnswer: string;
showHint: boolean;
// 例句重組專用狀態
shuffledWords: string[];
arrangedWords: string[];
reorderResult: boolean | null;
// UI狀態
cardHeight: number;
modalImage: string | null;
showReportModal: boolean;
showComplete: boolean;
}
需要新增的智能化狀態
interface EnhancedLearnPageState extends LearnPageState {
// 智能複習相關
reviewMode: ReviewType; // 系統自動選擇的題型
userLevel: number; // 學習者程度
currentCard: FlashcardExtended; // 包含複習相關資料的詞卡
isAutoSelecting: boolean; // 系統正在選擇題型
masteryLevel: number; // 當前熟悉度
nextReviewDate: string; // 下次復習日期
}
2. 現有7種複習題型實現 (已完成UI)
實際程式碼結構基於 learn/page.tsx
// 現有的7種複習模式 (已完成UI,需整合智能邏輯)
type LearnMode =
| 'flip-memory' // ✅ 翻卡記憶 (對應 FlipCardQuestion)
| 'vocab-choice' // ✅ 詞彙選擇 (對應 MultipleChoiceQuestion)
| 'vocab-listening' // ✅ 詞彙聽力 (對應 VocabularyListeningQuestion)
| 'sentence-listening' // ⚠️ 例句聽力 (UI框架完成,邏輯開發中)
| 'sentence-fill' // ✅ 例句填空 (對應 FillBlankQuestion)
| 'sentence-reorder' // ✅ 例句重組 (對應 SentenceReconstructionQuestion)
| 'sentence-speaking' // ✅ 例句口說 (對應 SentenceSpeakingQuestion)
// 現有的詞卡資料結構
interface CurrentCardData {
id: number;
word: string;
partOfSpeech: string;
pronunciation: string;
translation: string;
definition: string;
example: string;
exampleTranslation: string;
exampleImage: string;
synonyms: string[];
difficulty: string; // CEFR等級 (A1, B1, B2, C1)
}
2. MasteryIndicator 組件
功能需求
- 視覺化顯示熟悉度百分比
- 區分基礎熟悉度和當前熟悉度
- 衰減狀態提示
設計規格
interface MasteryIndicatorProps {
level: number; // 0-100
isDecaying?: boolean; // 是否正在衰減
showPercentage?: boolean; // 是否顯示百分比數字
size?: 'small' | 'medium' | 'large';
}
export const MasteryIndicator: React.FC<MasteryIndicatorProps> = ({
level,
isDecaying = false,
showPercentage = true,
size = 'medium'
}) => {
const getColor = (level: number, isDecaying: boolean) => {
if (isDecaying) return '#ff9500'; // 橙色表示衰減中
if (level >= 80) return '#34c759'; // 綠色表示熟悉
if (level >= 50) return '#007aff'; // 藍色表示中等
return '#ff3b30'; // 紅色表示需要加強
};
return (
<div className={`mastery-indicator ${size}`}>
<div className="progress-circle">
<svg viewBox="0 0 36 36">
<circle
cx="18" cy="18" r="15.915"
fill="transparent"
stroke="#e5e5e7"
strokeWidth="2"
/>
<circle
cx="18" cy="18" r="15.915"
fill="transparent"
stroke={getColor(level, isDecaying)}
strokeWidth="2"
strokeDasharray={`${level} 100`}
transform="rotate(-90 18 18)"
/>
</svg>
{showPercentage && (
<div className="percentage">
{level}%
{isDecaying && <span className="decay-icon">↓</span>}
</div>
)}
</div>
<div className="mastery-label">
{level >= 80 ? '熟悉' :
level >= 50 ? '中等' : '需加強'}
</div>
</div>
);
};
3. ReviewSchedule 組件
功能需求
- 顯示今日復習列表
- 按優先級排序(逾期 > 到期 > 提前復習)
- 復習進度追蹤
實現概要
export const ReviewSchedule: React.FC = () => {
const [dueCards, setDueCards] = useState<Flashcard[]>([]);
const [completedCount, setCompletedCount] = useState(0);
useEffect(() => {
loadDueCards();
}, []);
const loadDueCards = async () => {
const response = await reviewApi.getDueFlashcards();
setDueCards(response.data);
};
const handleReviewComplete = (cardId: number) => {
setDueCards(prev => prev.filter(card => card.id !== cardId));
setCompletedCount(prev => prev + 1);
};
// 按優先級排序
const sortedCards = useMemo(() => {
return [...dueCards].sort((a, b) => {
if (a.isOverdue !== b.isOverdue) {
return a.isOverdue ? -1 : 1; // 逾期優先
}
if (a.isOverdue && b.isOverdue) {
return b.overdueDays - a.overdueDays; // 逾期天數多的優先
}
return 0;
});
}, [dueCards]);
return (
<div className="review-schedule">
<div className="progress-header">
<h2>今日復習</h2>
<div className="progress">
{completedCount} / {dueCards.length + completedCount}
</div>
</div>
<div className="card-list">
{sortedCards.map(card => (
<FlashcardItem
key={card.id}
flashcard={card}
onReviewClick={() => startReview(card.id)}
/>
))}
</div>
</div>
);
};
4. ReviewPage 組件
功能需求
- 復習界面(翻卡、選擇題等)
- 信心程度評分 (1-5)
- 復習結果反饋
- 下一張卡片自動載入
🎓 複習方式設計
複習題型規劃
1. 翻卡題 (Flipcard)
- 操作方式: 顯示詞彙,學習者自己憑感覺評估記憶情況
- 學習效益: 對詞彙形成全面的初步印象
- 適用情境:
- A1學習者的基礎學習
- 困難詞彙(學習者程度 < 詞彙程度)的重新熟悉
2. 選擇題 (Multiple Choice)
- 操作方式: 給定義,選擇正確的詞彙
- 學習效益: 加深詞彙定義與詞彙之間的連結
- 適用情境:
- A1學習者的概念建立
- 困難詞彙的定義強化
3. 詞彙聽力題 (Vocabulary Listening)
- 操作方式: 播放詞彙發音,選擇正確詞彙
- 學習效益: 加強詞彙的發音記憶
- 限制說明: 人類短期記憶能力強,當次學習時聽力複習由短期記憶驅動,可能壓縮發音與詞彙本身的連結效果
- 適用情境: A1學習者的發音熟悉
4. 例句聽力題 (Sentence Listening)
- 操作方式: 播放例句,選擇正確例句
- 學習效益: 強化例句的發音記憶
- 限制說明: 受短期記憶影響,對學習新例句幫助有限
- 適用情境: 長期複習中的聽力維持
5. 填空題 (Fill in the Blank)
- 操作方式: 提供挖空例句,學習者填入正確詞彙
- 學習效益:
- 練習拼字能力
- 加深詞彙與使用情境的連結
- 適用情境:
- 簡單詞彙(學習者程度 > 詞彙程度)
- 適中詞彙(學習者程度 = 詞彙程度)
6. 例句重組題 (Sentence Reconstruction)
- 操作方式: 打亂例句單字順序,重新組織成完整句子
- 學習效益: 快速練習組織句子的能力
- 適用情境:
- 簡單詞彙的語法練習
- 適中詞彙的句型熟悉
7. 例句口說題 (Sentence Speaking)
- 操作方式: 給出例句,學習者朗讀例句
- 學習效益:
- 練習看圖揣摩情境
- 練習完整句子表達
- 加深例句與情境的連結
- 模仿母語者表達方式
- 適用情境: 適中詞彙的口語表達練習
學習程度適配策略
A1初學者策略
const A1_REVIEW_TYPES = ['flipcard', 'vocabulary_listening', 'multiple_choice'];
// 統一使用基礎題型,重點建立信心和基本概念
function getA1ReviewType(flashcard: Flashcard): ReviewType {
// 隨機選擇基礎題型,或根據上次表現調整
const weights = {
flipcard: 0.4, // 40% - 主要熟悉方式
multiple_choice: 0.4, // 40% - 概念強化
vocabulary_listening: 0.2 // 20% - 發音熟悉
};
return weightedRandomSelect(A1_REVIEW_TYPES, weights);
}
程度適配算法
interface DifficultyMapping {
userLevel: number; // 學習者程度 (1-100)
wordLevel: number; // 詞彙難度 (1-100)
reviewTypes: ReviewType[];
}
function getReviewTypesByDifficulty(userLevel: number, wordLevel: number): ReviewType[] {
const difficulty = wordLevel - userLevel;
if (userLevel <= 20) {
// A1學習者 - 統一基礎題型
return ['flipcard', 'multiple_choice', 'vocabulary_listening'];
} else if (difficulty < -10) {
// 簡單詞彙 (學習者程度 > 詞彙程度)
return ['sentence_reconstruction', 'fill_blank'];
} else if (difficulty >= -10 && difficulty <= 10) {
// 適中詞彙 (學習者程度 ≈ 詞彙程度)
return ['fill_blank', 'sentence_reconstruction', 'sentence_speaking'];
} else {
// 困難詞彙 (學習者程度 < 詞彙程度)
return ['flipcard', 'multiple_choice'];
}
}
複習題型顯示組件設計
ReviewTypeIndicator 組件
interface ReviewTypeIndicatorProps {
currentMode: ReviewType;
userLevel: number;
wordLevel: number;
}
export const ReviewTypeIndicator: React.FC<ReviewTypeIndicatorProps> = ({
currentMode,
userLevel,
wordLevel
}) => {
const modeLabels = {
flipcard: '翻卡題',
multiple_choice: '選擇題',
vocabulary_listening: '詞彙聽力',
sentence_listening: '例句聽力',
fill_blank: '填空題',
sentence_reconstruction: '例句重組',
sentence_speaking: '例句口說'
};
const getDifficultyLabel = (userLevel: number, wordLevel: number) => {
const difficulty = wordLevel - userLevel;
if (userLevel <= 20) return 'A1學習者';
if (difficulty < -10) return '簡單詞彙';
if (difficulty >= -10 && difficulty <= 10) return '適中詞彙';
return '困難詞彙';
};
return (
<div className="review-type-indicator">
<div className="current-type">
<span className="type-label">{modeLabels[currentMode]}</span>
<span className="auto-selected">系統智能選擇</span>
</div>
<div className="difficulty-info">
<span className="difficulty-label">
{getDifficultyLabel(userLevel, wordLevel)}適配
</span>
</div>
</div>
);
};
更新的狀態管理
interface ReviewState {
currentCard: Flashcard | null;
showAnswer: boolean;
reviewMode: ReviewType; // 系統自動選擇的題型
confidenceLevel: number | null;
userAnswer: string | null;
isCorrect: boolean | null;
isSubmitting: boolean;
currentQuestionData: QuestionData | null;
startTime: number; // 題目開始時間
}
// 移除 availableReviewModes,因為用戶不需要選擇
interface QuestionData {
questionType: ReviewType;
options?: string[]; // 選擇題選項
correctAnswer: string; // 正確答案
userLevel: number; // 學習者程度
wordLevel: number; // 詞彙難度
audioUrl?: string; // 聽力題音頻
sentence?: string; // 例句
blankedSentence?: string; // 填空題的挖空句子
scrambledWords?: string[]; // 重組題的打亂單字
}
export const ReviewPage: React.FC = () => {
const [state, setState] = useState<ReviewState>({
currentCard: null,
showAnswer: false,
reviewMode: 'flipcard',
confidenceLevel: null,
userAnswer: null,
isCorrect: null,
isSubmitting: false,
currentQuestionData: null,
startTime: Date.now()
});
useEffect(() => {
loadNextCard();
}, []);
const loadNextCard = async () => {
try {
const response = await reviewApi.getNextReviewCard();
const card = response.data;
// 系統自動選擇最適合的複習模式
const selectedMode = await selectOptimalReviewMode(card);
// 生成對應的題目數據
const questionData = await generateQuestionData(card, selectedMode);
setState(prev => ({
...prev,
currentCard: card,
reviewMode: selectedMode,
currentQuestionData: questionData,
showAnswer: false,
userAnswer: null,
isCorrect: null,
startTime: Date.now()
}));
} catch (error) {
console.error('載入卡片失敗:', error);
}
};
// 新增:系統自動選擇題型的函數
const selectOptimalReviewMode = async (card: Flashcard): Promise<ReviewType> => {
const response = await reviewApi.getOptimalReviewMode(card.id, card.userLevel, card.wordLevel);
return response.data.selectedMode;
};
const handleAnswerSubmit = async (userAnswer: string | boolean) => {
if (!state.currentCard || !state.currentQuestionData) return;
setState(prev => ({ ...prev, isSubmitting: true }));
try {
// 檢查答案正確性
const isCorrect = checkAnswer(userAnswer, state.currentQuestionData);
setState(prev => ({
...prev,
userAnswer: typeof userAnswer === 'string' ? userAnswer : null,
isCorrect,
showAnswer: true
}));
// 提交復習結果
const result = await reviewApi.submitReview(state.currentCard.id, {
isCorrect,
confidenceLevel: state.confidenceLevel,
questionType: state.reviewMode,
userAnswer: typeof userAnswer === 'string' ? userAnswer : null,
timeTaken: Date.now() - state.startTime
});
// 顯示結果反饋
showFeedback(result.data);
} catch (error) {
console.error('復習提交失敗:', error);
} finally {
setState(prev => ({ ...prev, isSubmitting: false }));
}
};
const renderQuestionComponent = () => {
if (!state.currentCard || !state.currentQuestionData) return null;
const commonProps = {
flashcard: state.currentCard,
questionData: state.currentQuestionData,
onAnswerSubmit: handleAnswerSubmit,
isSubmitting: state.isSubmitting,
showResult: state.showAnswer,
isCorrect: state.isCorrect,
userAnswer: state.userAnswer
};
switch (state.reviewMode) {
case 'flipcard':
return <FlipCardQuestion {...commonProps} />;
case 'multiple_choice':
return <MultipleChoiceQuestion {...commonProps} />;
case 'vocabulary_listening':
return <VocabularyListeningQuestion {...commonProps} />;
case 'sentence_listening':
return <SentenceListeningQuestion {...commonProps} />;
case 'fill_blank':
return <FillBlankQuestion {...commonProps} />;
case 'sentence_reconstruction':
return <SentenceReconstructionQuestion {...commonProps} />;
case 'sentence_speaking':
return <SentenceSpeakingQuestion {...commonProps} />;
default:
return <FlipCardQuestion {...commonProps} />;
}
};
return (
<div className="review-page">
{state.currentCard && (
<>
<ReviewTypeIndicator
currentMode={state.reviewMode}
userLevel={state.currentCard.userLevel}
wordLevel={state.currentCard.wordLevel}
/>
{renderQuestionComponent()}
{state.showAnswer && (
<div className="next-card-section">
<button
className="next-btn"
onClick={loadNextCard}
disabled={state.isSubmitting}
>
下一張卡片
</button>
</div>
)}
</>
)}
</div>
);
};
### **現有7種題型UI實現分析**
#### **1. 翻卡記憶 (flip-memory)** ✅ 已完成
```typescript
// 基於現有實現 learn/page.tsx lines 412-535
實現特色:
- ✅ 3D翻卡動畫效果 (CSS transform)
- ✅ 動態卡片高度計算 (useLayoutEffect)
- ✅ 前面顯示:詞彙 + 發音 + 音頻播放
- ✅ 背面顯示:定義 + 例句 + 同義詞
- ✅ 自適應響應式設計
- ✅ 錯誤回報功能整合
現有邏輯:點擊翻面,用戶自評熟悉程度
需要整合:信心等級評分 (1-5) + 間隔算法
2. 詞彙選擇 (vocab-choice) ✅ 已完成
// 基於現有實現 learn/page.tsx lines 536-647
實現特色:
- ✅ 4選項多選題界面
- ✅ 選項自動生成邏輯 (避免重複)
- ✅ 即時結果反饋 (正確/錯誤高亮)
- ✅ 答案解析顯示
- ✅ 音頻播放整合
現有邏輯:顯示定義,選擇正確詞彙
需要整合:後端選項生成 + 難度適配
3. 例句填空 (sentence-fill) ✅ 已完成
// 基於現有實現 learn/page.tsx lines 649-817
實現特色:
- ✅ 動態輸入框 (自適應寬度)
- ✅ 例句圖片顯示 + 模態框放大
- ✅ 虛線邊框設計美觀
- ✅ 大小寫不敏感驗證
- ✅ 提示功能 (顯示/隱藏定義)
- ✅ Enter鍵快速提交
現有邏輯:挖空例句,用戶填入詞彙
需要整合:後端挖空邏輯 + 拼字評分
4. 詞彙聽力 (vocab-listening) ✅ 已完成
// 基於現有實現 learn/page.tsx lines 818-927
實現特色:
- ✅ AudioPlayer組件整合
- ✅ 2x2網格選項佈局
- ✅ 聽力專用UI提示
- ✅ 發音展示 + 重複播放
- ✅ 選項結果高亮反饋
現有邏輯:播放詞彙發音,選擇正確詞彙
需要整合:音頻檔案管理 + 選項後端生成
5. 例句口說 (sentence-speaking) ✅ 已完成
// 基於現有實現 learn/page.tsx lines 928-996
實現特色:
- ✅ VoiceRecorder組件完整整合
- ✅ 目標例句 + 中文翻譯顯示
- ✅ 例句圖片情境提示
- ✅ 錄音完成自動處理
- ✅ 簡化評分機制
現有邏輯:看圖說例句,錄音提交
需要整合:語音識別評分 + 發音準確度
6. 例句重組 (sentence-reorder) ✅ 已完成
// 基於現有實現 learn/page.tsx lines 1064-1228
實現特色:
- ✅ 拖放式單字重組界面
- ✅ 雙區域設計 (重組區 + 可用單字區)
- ✅ 動態單字按鈕 (點擊移動)
- ✅ 即時答案檢查邏輯
- ✅ 重置功能 + 例句圖片顯示
- ✅ 字符串比較驗證 (大小寫不敏感)
現有邏輯:打亂單字,重組成正確例句
需要整合:語法難度評估 + 句型分析
7. 例句聽力 (sentence-listening) ⚠️ 框架完成
// 基於現有實現 learn/page.tsx lines 997-1063
實現狀況:
- ✅ UI框架和佈局完成
- ✅ AudioPlayer整合
- ⚠️ 選項生成邏輯待完成
- ⚠️ 例句音頻檔案管理待完成
現有邏輯:播放例句,選擇正確選項 (開發中)
需要完成:例句選項生成 + 音頻檔案系統
3. 現有音頻組件整合分析
AudioPlayer 組件 ✅ 已完成整合
// 現有實現已完美整合到各題型中
使用場景:
- 翻卡記憶:詞彙發音播放
- 詞彙聽力:音頻播放按鈕
- 例句聽力:例句音頻播放
- 填空題:提示音頻播放
現有功能:
- ✅ 文字轉語音 (TTS)
- ✅ 播放控制按鈕
- ✅ 載入狀態處理
- ✅ 錯誤處理機制
VoiceRecorder 組件 ✅ 已完成整合
// 完整整合到例句口說題型中
使用特色:
- ✅ 麥克風權限處理
- ✅ 錄音品質設定
- ✅ 即時錄音反饋
- ✅ 錄音回放功能
- ✅ 目標例句顯示
- ✅ 情境圖片輔助
現有Props介面:
interface VoiceRecorderProps {
targetText: string; // 目標例句
targetTranslation: string; // 中文翻譯
exampleImage: string; // 情境圖片
instructionText: string; // 指導文字
onRecordingComplete: () => void;
}
🔌 API 整合 (基於現有架構)
現有服務層 (已完成)
flashcardsService (已存在於 lib/services/flashcards.ts)
// 現有API服務,需要擴展智能複習功能
class FlashcardsService {
// ✅ 已完成的基礎功能
async getFlashcards(search?, favoritesOnly?, cefrLevel?, partOfSpeech?, ...): Promise<ApiResponse<FlashcardsResponse>>
async getFlashcard(id: string): Promise<ApiResponse<Flashcard>>
async createFlashcard(data: CreateFlashcardRequest): Promise<ApiResponse<Flashcard>>
async updateFlashcard(id: string, data: Partial<CreateFlashcardRequest>): Promise<ApiResponse<Flashcard>>
async deleteFlashcard(id: string): Promise<ApiResponse<void>>
async toggleFavorite(id: string): Promise<ApiResponse<void>>
// 🆕 需要新增的智能複習方法
async getDueFlashcards(limit = 50): Promise<ApiResponse<Flashcard[]>>
async getNextReviewCard(): Promise<ApiResponse<FlashcardExtended>>
async submitReview(id: string, reviewData: ReviewSubmission): Promise<ApiResponse<ReviewResult>>
async getOptimalReviewMode(cardId: string, userLevel: number, wordLevel: number): Promise<ApiResponse<{selectedMode: ReviewType}>>
async generateQuestionOptions(cardId: string, questionType: ReviewType): Promise<ApiResponse<QuestionData>>
}
需要新增的智能複習服務
reviewService.ts (新增)
interface ReviewService {
// 間隔重複算法整合
calculateNextReviewDate(cardId: string, isCorrect: boolean, confidenceLevel?: number): Promise<string>
calculateCurrentMastery(baseMastery: number, lastReviewDate: string): number
// 四情境自動適配
getReviewTypesByDifficulty(userLevel: number, wordLevel: number): ReviewType[]
selectOptimalReviewMode(card: FlashcardExtended, reviewHistory?: ReviewRecord[]): Promise<ReviewType>
// 學習統計和進度
getReviewStatistics(userId: string): Promise<ReviewStats>
getTodayProgress(): Promise<TodayProgress>
// A1學習者保護
isA1Learner(userLevel: number): boolean
getA1ProtectedModes(): ReviewType[]
}
智能複習邏輯重構方案
現有狀態 vs 智能化目標
// 現況:手動模式切換
const [mode, setMode] = useState<LearnMode>('flip-memory')
// 目標:系統自動選擇
const [reviewMode, setReviewMode] = useState<ReviewType>()
const [isAutoSelecting, setIsAutoSelecting] = useState(true)
// 重構策略:
// 1. 保留所有現有UI邏輯
// 2. 移除模式切換按鈕組
// 3. 新增 ReviewTypeIndicator 顯示當前題型
// 4. 整合後端自動選擇API
智能適配邏輯整合
// 需要新增到現有 learn/page.tsx 的函數
const loadNextCardWithAutoMode = async () => {
try {
// 1. 取得下一張到期詞卡
const cardResponse = await flashcardsService.getNextReviewCard()
if (!cardResponse.success) throw new Error(cardResponse.error)
const card = cardResponse.data
// 2. 系統自動選擇最適合的題型
const modeResponse = await flashcardsService.getOptimalReviewMode(
card.id,
card.userLevel,
card.wordLevel
)
if (!modeResponse.success) throw new Error(modeResponse.error)
const selectedMode = modeResponse.data.selectedMode
// 3. 更新狀態 (無用戶選擇)
setCurrentCard(card)
setMode(selectedMode) // 系統決定的模式
setIsAutoSelecting(false)
// 4. 重置題型專用狀態
resetQuestionStates()
} catch (error) {
console.error('載入複習卡片失敗:', error)
}
}
// 映射系統選擇的題型到現有模式
const mapReviewTypeToMode = (reviewType: ReviewType): LearnMode => {
const mapping = {
'flipcard': 'flip-memory',
'multiple_choice': 'vocab-choice',
'vocabulary_listening': 'vocab-listening',
'sentence_listening': 'sentence-listening',
'fill_blank': 'sentence-fill',
'sentence_reconstruction': 'sentence-reorder',
'sentence_speaking': 'sentence-speaking'
}
return mapping[reviewType] || 'flip-memory'
}
---
## 🎨 **UI/UX 設計規範**
### **色彩設計**
```css
:root {
/* 熟悉度顏色 */
--mastery-high: #34c759; /* 綠色 80-100% */
--mastery-medium: #007aff; /* 藍色 50-79% */
--mastery-low: #ff3b30; /* 紅色 0-49% */
--mastery-decaying: #ff9500; /* 橙色 衰減中 */
/* 復習狀態顏色 */
--status-due: #007aff; /* 到期 */
--status-overdue: #ff3b30; /* 逾期 */
--status-future: #8e8e93; /* 未到期 */
/* 背景色 */
--bg-primary: #ffffff;
--bg-secondary: #f2f2f7;
--bg-tertiary: #e5e5ea;
}
響應式設計
/* 手機端 */
@media (max-width: 768px) {
.flashcard-item {
padding: 12px;
margin: 8px 0;
}
.mastery-indicator.medium {
width: 40px;
height: 40px;
}
}
/* 平板端 */
@media (min-width: 769px) and (max-width: 1024px) {
.card-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
}
/* 桌面端 */
@media (min-width: 1025px) {
.card-list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
}
動畫效果
.mastery-indicator .progress-circle circle {
transition: stroke-dasharray 0.6s ease-in-out;
}
.flashcard-item {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.flashcard-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.decay-icon {
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
📊 狀態管理
使用 React Context
interface SpacedRepetitionContextValue {
flashcards: Flashcard[];
dueCount: number;
completedToday: number;
refreshFlashcards: () => Promise<void>;
updateFlashcard: (id: number, updates: Partial<Flashcard>) => void;
}
const SpacedRepetitionContext = createContext<SpacedRepetitionContextValue | null>(null);
export const SpacedRepetitionProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [flashcards, setFlashcards] = useState<Flashcard[]>([]);
const [dueCount, setDueCount] = useState(0);
const [completedToday, setCompletedToday] = useState(0);
const refreshFlashcards = async () => {
const response = await reviewApi.getAllFlashcards();
setFlashcards(response.data);
// 計算到期數量
const today = new Date().toISOString().split('T')[0];
const due = response.data.filter(card => card.nextReviewDate <= today).length;
setDueCount(due);
};
return (
<SpacedRepetitionContext.Provider value={{
flashcards,
dueCount,
completedToday,
refreshFlashcards,
updateFlashcard
}}>
{children}
</SpacedRepetitionContext.Provider>
);
};
🧪 測試策略
單元測試
// masteryCalculator.test.js
describe('calculateCurrentMastery', () => {
test('should return base mastery for same day', () => {
const today = new Date().toISOString().split('T')[0];
const result = calculateCurrentMastery(80, today);
expect(result).toBe(80);
});
test('should apply decay for overdue cards', () => {
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
.toISOString().split('T')[0];
const result = calculateCurrentMastery(80, sevenDaysAgo);
expect(result).toBeLessThan(80);
expect(result).toBeGreaterThan(50);
});
});
整合測試
- API 呼叫測試
- 組件互動測試
- 狀態更新測試
E2E 測試
- 復習流程測試
- 熟悉度更新測試
- 響應式設計測試
📋 智能化重構檢查清單 (基於現有實現)
核心邏輯重構 ⭐ 主要工作
- 移除手動模式切換 - 刪除7個模式按鈕 (learn/page.tsx lines 337-410)
- 新增 ReviewTypeIndicator - 純顯示當前系統選擇的題型
- 整合自動選擇API - 替換mock data為真實到期詞卡
- 四情境適配邏輯 - A1/簡單/適中/困難自動判斷
- 間隔重複算法 - 整合實時熟悉度計算和下次復習時間
API服務擴展
- flashcardsService 擴展 - 新增6個智能複習方法
- reviewService 新增 - 專門的複習邏輯服務
- masteryCalculator 新增 - 前端熟悉度實時計算
- 後端API對接 - 確保前後端數據格式一致
UI保持和微調
- 7種題型UI完成 - 現有實現已非常完善
- 音頻功能完成 - AudioPlayer + VoiceRecorder 整合良好
- 響應式設計完成 - 現有設計已適配各種螢幕
- 新增熟悉度指示器 - 實時顯示當前詞彙熟悉程度
- 例句聽力補完 - 完成選項邏輯 (目前標記為開發中)
狀態管理升級
- 擴展現有狀態 - 新增智能複習相關狀態
- SpacedRepetitionContext - 全域復習狀態管理
- A1保護邏輯 - 自動限制複雜題型
- 復習進度追蹤 - 整合間隔重複算法
測試和優化
- 基礎功能測試 - 現有7種題型已運作良好
- 智能邏輯測試 - 自動選擇和適配算法
- A1保護測試 - 確保初學者體驗
- 性能測試 - API整合後的響應速度
🚀 重構時程調整 (大幅縮短)
原預估: 3-4週全新開發 實際需求: 1-2週智能化重構
Week 1: 核心邏輯重構
- 移除手動選擇 + 整合自動選擇API
- 新增智能適配邏輯
- 完成例句聽力補完
Week 2: 測試和優化
- API整合測試
- A1保護邏輯驗證
- 性能優化和錯誤處理
重構優勢:
- ✅ UI已完成95%,無需重建
- ✅ 音頻功能完整,無需重寫
- ✅ 7種題型邏輯成熟,只需API整合