dramaling-app/apps/mobile/lib/features/dialogue/services/dialogue_service.dart

372 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 '我明白了。還有什麼我可以為您服務的嗎?';
}
}
}