372 lines
12 KiB
Dart
372 lines
12 KiB
Dart
import 'dart:async';
|
||
|
||
import '../models/dialogue_models.dart';
|
||
|
||
/// 對話服務
|
||
///
|
||
/// 提供完整的對話管理功能,包括:
|
||
/// - 場景和角色加載
|
||
/// - AI回應生成
|
||
/// - 語言分析和評分
|
||
/// - 任務進度跟踪
|
||
class DialogueService {
|
||
/// 加載場景信息
|
||
Future<DialogueScene> loadScene(String scenarioId, String levelId) async {
|
||
// 模擬API調用延遲
|
||
await Future.delayed(const Duration(milliseconds: 500));
|
||
|
||
// 返回模擬數據
|
||
return DialogueScene(
|
||
id: scenarioId,
|
||
name: '餐廳用餐',
|
||
description: '在餐廳與服務員進行日常對話',
|
||
backgroundImageUrl: 'assets/images/restaurant_bg.jpg',
|
||
characterId: 'waiter_001',
|
||
difficultyLevel: 'beginner',
|
||
tags: ['restaurant', 'ordering', 'daily'],
|
||
);
|
||
}
|
||
|
||
/// 加載角色信息
|
||
Future<DialogueCharacter> loadCharacter(String characterId) async {
|
||
await Future.delayed(const Duration(milliseconds: 300));
|
||
|
||
return DialogueCharacter(
|
||
id: characterId,
|
||
name: '小王',
|
||
description: '友善的餐廳服務員',
|
||
avatarUrl: 'assets/images/waiter_avatar.jpg',
|
||
personality: '友善、耐心、專業',
|
||
role: '服務員',
|
||
background: '在餐廳工作了3年,非常熟悉菜單和服務流程',
|
||
specialities: ['點餐服務', '菜品介紹', '客戶服務'],
|
||
);
|
||
}
|
||
|
||
/// 加載任務信息
|
||
Future<DialogueTask> loadTask(String levelId) async {
|
||
await Future.delayed(const Duration(milliseconds: 200));
|
||
|
||
return DialogueTask(
|
||
id: 'task_$levelId',
|
||
title: '完成點餐',
|
||
description: '與服務員完成一次完整的點餐對話,包括詢問菜品、下訂單、確認價格',
|
||
type: DialogueTaskType.conversation,
|
||
requirements: {
|
||
'minTurns': 5,
|
||
'mustUseWords': ['menu', 'order', 'price'],
|
||
'completionCriteria': ['greeting', 'ordering', 'confirmation'],
|
||
},
|
||
);
|
||
}
|
||
|
||
/// 加載必需詞彙
|
||
Future<List<String>> loadRequiredVocabulary(String levelId) async {
|
||
await Future.delayed(const Duration(milliseconds: 150));
|
||
|
||
return [
|
||
'menu',
|
||
'order',
|
||
'price',
|
||
'recommendation',
|
||
'delicious',
|
||
'bill',
|
||
];
|
||
}
|
||
|
||
/// 獲取開場對話
|
||
Future<DialogueMessage> getOpeningDialogue(String scenarioId, String levelId) async {
|
||
await Future.delayed(const Duration(milliseconds: 400));
|
||
|
||
return DialogueMessage(
|
||
id: 'opening_${DateTime.now().millisecondsSinceEpoch}',
|
||
content: '歡迎光臨!請問您需要什麼嗎?我可以為您介紹今天的特色菜。',
|
||
isUser: false,
|
||
timestamp: DateTime.now(),
|
||
type: DialogueMessageType.text,
|
||
);
|
||
}
|
||
|
||
/// 分析用戶回覆
|
||
Future<DialogueAnalysis> analyzeReply({
|
||
required String scenarioId,
|
||
required String levelId,
|
||
required String replyText,
|
||
required List<String> requiredVocabulary,
|
||
DialogueTask? currentTask,
|
||
}) async {
|
||
await Future.delayed(const Duration(milliseconds: 800));
|
||
|
||
// 模擬AI分析
|
||
final usedWords = _findUsedVocabulary(replyText, requiredVocabulary);
|
||
final grammarIssues = _analyzeGrammar(replyText);
|
||
|
||
// 計算得分
|
||
final grammarScore = _calculateGrammarScore(grammarIssues);
|
||
final semanticsScore = _calculateSemanticsScore(replyText, currentTask);
|
||
final fluencyScore = _calculateFluencyScore(replyText);
|
||
|
||
// 計算任務進度
|
||
double? taskProgress;
|
||
if (currentTask != null) {
|
||
taskProgress = _calculateTaskProgress(replyText, currentTask, usedWords);
|
||
}
|
||
|
||
return DialogueAnalysis(
|
||
id: 'analysis_${DateTime.now().millisecondsSinceEpoch}',
|
||
userReply: replyText,
|
||
timestamp: DateTime.now(),
|
||
grammarScore: grammarScore,
|
||
semanticsScore: semanticsScore,
|
||
fluencyScore: fluencyScore,
|
||
grammarIssues: grammarIssues,
|
||
usedVocabulary: usedWords,
|
||
missedVocabulary: requiredVocabulary.where((word) => !usedWords.contains(word)).toList(),
|
||
suggestions: _generateSuggestions(replyText, grammarIssues),
|
||
taskProgress: taskProgress,
|
||
isDialogueComplete: taskProgress != null && taskProgress >= 1.0,
|
||
);
|
||
}
|
||
|
||
/// 獲取AI回應
|
||
Future<DialogueMessage> getAIResponse({
|
||
required String scenarioId,
|
||
required String levelId,
|
||
required String userReply,
|
||
required DialogueAnalysis analysis,
|
||
}) async {
|
||
await Future.delayed(const Duration(milliseconds: 600));
|
||
|
||
// 根據用戶回覆生成AI回應
|
||
String response = _generateAIResponse(userReply, analysis);
|
||
|
||
return DialogueMessage(
|
||
id: 'ai_response_${DateTime.now().millisecondsSinceEpoch}',
|
||
content: response,
|
||
isUser: false,
|
||
timestamp: DateTime.now(),
|
||
type: DialogueMessageType.text,
|
||
metadata: {
|
||
'responseType': 'contextual',
|
||
'grammarScore': analysis.grammarScore,
|
||
'semanticsScore': analysis.semanticsScore,
|
||
},
|
||
);
|
||
}
|
||
|
||
/// 獲取回覆輔助建議
|
||
Future<List<String>> getReplyAssistance({
|
||
required String scenarioId,
|
||
required String levelId,
|
||
required String currentDialogue,
|
||
DialogueTask? currentTask,
|
||
}) async {
|
||
await Future.delayed(const Duration(milliseconds: 400));
|
||
|
||
// 根據當前對話內容生成建議回覆
|
||
return [
|
||
'可以給我看一下菜單嗎?',
|
||
'請推薦一些招牌菜。',
|
||
'這個菜的價格是多少?',
|
||
'我想要點這個。',
|
||
'謝謝,我考慮一下。',
|
||
];
|
||
}
|
||
|
||
/// 查找使用的詞彙
|
||
List<String> _findUsedVocabulary(String text, List<String> requiredVocabulary) {
|
||
final usedWords = <String>[];
|
||
final lowerText = text.toLowerCase();
|
||
|
||
for (final word in requiredVocabulary) {
|
||
if (lowerText.contains(word.toLowerCase())) {
|
||
usedWords.add(word);
|
||
}
|
||
}
|
||
|
||
return usedWords;
|
||
}
|
||
|
||
/// 分析語法
|
||
List<GrammarIssue> _analyzeGrammar(String text) {
|
||
final issues = <GrammarIssue>[];
|
||
|
||
// 簡單的語法檢查模擬
|
||
if (text.length < 5) {
|
||
issues.add(GrammarIssue(
|
||
type: 'length',
|
||
description: '回覆太短,請提供更完整的句子',
|
||
originalText: text,
|
||
suggestedText: '$text(建議擴展內容)',
|
||
position: 0,
|
||
length: text.length,
|
||
severity: GrammarIssueSeverity.minor,
|
||
));
|
||
}
|
||
|
||
if (!text.endsWith('.') && !text.endsWith('?') && !text.endsWith('!') && !text.endsWith('?')) {
|
||
issues.add(GrammarIssue(
|
||
type: 'punctuation',
|
||
description: '建議在句尾加上標點符號',
|
||
originalText: text,
|
||
suggestedText: '$text。',
|
||
position: text.length,
|
||
length: 0,
|
||
severity: GrammarIssueSeverity.minor,
|
||
));
|
||
}
|
||
|
||
return issues;
|
||
}
|
||
|
||
/// 計算語法得分
|
||
double _calculateGrammarScore(List<GrammarIssue> issues) {
|
||
if (issues.isEmpty) return 95.0;
|
||
|
||
double penalty = 0.0;
|
||
for (final issue in issues) {
|
||
switch (issue.severity) {
|
||
case GrammarIssueSeverity.critical:
|
||
penalty += 20.0;
|
||
break;
|
||
case GrammarIssueSeverity.major:
|
||
penalty += 15.0;
|
||
break;
|
||
case GrammarIssueSeverity.moderate:
|
||
penalty += 10.0;
|
||
break;
|
||
case GrammarIssueSeverity.minor:
|
||
penalty += 5.0;
|
||
break;
|
||
}
|
||
}
|
||
|
||
return (100.0 - penalty).clamp(0.0, 100.0);
|
||
}
|
||
|
||
/// 計算語意得分
|
||
double _calculateSemanticsScore(String text, DialogueTask? task) {
|
||
// 基礎語意得分
|
||
double score = 75.0;
|
||
|
||
// 根據文字長度和內容豐富度調整
|
||
if (text.length > 20) score += 10.0;
|
||
if (text.length > 50) score += 5.0;
|
||
|
||
// 根據任務相關性調整
|
||
if (task != null) {
|
||
final requirements = task.requirements['mustUseWords'] as List<dynamic>?;
|
||
if (requirements != null) {
|
||
final requiredWords = requirements.cast<String>();
|
||
final usedCount = requiredWords.where((word) => text.toLowerCase().contains(word.toLowerCase())).length;
|
||
score += (usedCount / requiredWords.length) * 20.0;
|
||
}
|
||
}
|
||
|
||
return score.clamp(0.0, 100.0);
|
||
}
|
||
|
||
/// 計算流暢度得分
|
||
double _calculateFluencyScore(String text) {
|
||
// 基礎流暢度得分
|
||
double score = 80.0;
|
||
|
||
// 根據句子結構調整
|
||
if (text.contains(',') || text.contains(',')) score += 5.0;
|
||
if (text.split(' ').length > 5 || text.length > 15) score += 10.0;
|
||
|
||
// 檢查是否有重複詞語
|
||
final words = text.split(RegExp(r'\s+'));
|
||
final uniqueWords = words.toSet();
|
||
if (words.length != uniqueWords.length) score -= 5.0;
|
||
|
||
return score.clamp(0.0, 100.0);
|
||
}
|
||
|
||
/// 計算任務進度
|
||
double _calculateTaskProgress(String text, DialogueTask task, List<String> usedWords) {
|
||
double progress = 0.0;
|
||
final requirements = task.requirements;
|
||
|
||
// 檢查必需詞彙
|
||
final mustUseWords = requirements['mustUseWords'] as List<dynamic>?;
|
||
if (mustUseWords != null) {
|
||
final requiredWords = mustUseWords.cast<String>();
|
||
final usedRequiredWords = requiredWords.where((word) => usedWords.contains(word)).length;
|
||
progress += (usedRequiredWords / requiredWords.length) * 0.5;
|
||
}
|
||
|
||
// 檢查完成標準
|
||
final completionCriteria = requirements['completionCriteria'] as List<dynamic>?;
|
||
if (completionCriteria != null) {
|
||
final criteria = completionCriteria.cast<String>();
|
||
int metCriteria = 0;
|
||
|
||
for (final criterion in criteria) {
|
||
if (_checkCriterion(text, criterion)) {
|
||
metCriteria++;
|
||
}
|
||
}
|
||
|
||
progress += (metCriteria / criteria.length) * 0.5;
|
||
}
|
||
|
||
return progress.clamp(0.0, 1.0);
|
||
}
|
||
|
||
/// 檢查完成標準
|
||
bool _checkCriterion(String text, String criterion) {
|
||
final lowerText = text.toLowerCase();
|
||
|
||
switch (criterion) {
|
||
case 'greeting':
|
||
return lowerText.contains('hello') || lowerText.contains('hi') ||
|
||
lowerText.contains('你好') || lowerText.contains('哈囉');
|
||
case 'ordering':
|
||
return lowerText.contains('order') || lowerText.contains('want') ||
|
||
lowerText.contains('點') || lowerText.contains('要');
|
||
case 'confirmation':
|
||
return lowerText.contains('confirm') || lowerText.contains('yes') ||
|
||
lowerText.contains('ok') || lowerText.contains('確認') ||
|
||
lowerText.contains('好的');
|
||
default:
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// 生成建議
|
||
List<String> _generateSuggestions(String text, List<GrammarIssue> issues) {
|
||
final suggestions = <String>[];
|
||
|
||
for (final issue in issues) {
|
||
suggestions.add('${issue.description}: "${issue.suggestedText}"');
|
||
}
|
||
|
||
if (text.length < 10) {
|
||
suggestions.add('試著提供更詳細的回應');
|
||
}
|
||
|
||
return suggestions;
|
||
}
|
||
|
||
/// 生成AI回應
|
||
String _generateAIResponse(String userReply, DialogueAnalysis analysis) {
|
||
final lowerReply = userReply.toLowerCase();
|
||
|
||
// 根據用戶回覆內容生成相應回應
|
||
if (lowerReply.contains('menu') || lowerReply.contains('菜單')) {
|
||
return '好的,這是我們的菜單。我們今天的特色菜是紅燒肉和宮保雞丁,都很受歡迎呢!';
|
||
} else if (lowerReply.contains('recommend') || lowerReply.contains('推薦')) {
|
||
return '我推薦我們的招牌菜紅燒肉,還有今天新鮮的清蒸魚。您比較喜歡什麼口味的呢?';
|
||
} else if (lowerReply.contains('price') || lowerReply.contains('多少錢') || lowerReply.contains('價格')) {
|
||
return '紅燒肉是28元,清蒸魚是35元。這些都是我們的人氣菜品,分量也很足。';
|
||
} else if (lowerReply.contains('order') || lowerReply.contains('點') || lowerReply.contains('要')) {
|
||
return '好的,已經為您記下了。還需要什麼其他的嗎?飲料或者湯品?';
|
||
} else if (lowerReply.contains('thank') || lowerReply.contains('謝謝')) {
|
||
return '不客氣!如果還有什麼需要,請隨時告訴我。';
|
||
} else {
|
||
// 預設回應
|
||
return '我明白了。還有什麼我可以為您服務的嗎?';
|
||
}
|
||
}
|
||
} |