This commit is contained in:
sjk
2025-11-17 13:39:05 +08:00
commit d4cfe2b9de
479 changed files with 109324 additions and 0 deletions

View File

@@ -0,0 +1,150 @@
import 'package:flutter/material.dart';
/// AI导师类型枚举
enum TutorType {
business,
daily,
travel,
academic,
}
/// AI导师扩展方法
extension TutorTypeExtension on TutorType {
String get displayName {
switch (this) {
case TutorType.business:
return '商务导师';
case TutorType.daily:
return '日常导师';
case TutorType.travel:
return '旅行导师';
case TutorType.academic:
return '学术导师';
}
}
String get description {
switch (this) {
case TutorType.business:
return '专业商务场景';
case TutorType.daily:
return '生活场景对话';
case TutorType.travel:
return '旅游场景专训';
case TutorType.academic:
return '学术讨论演讲';
}
}
IconData get icon {
switch (this) {
case TutorType.business:
return Icons.business_center;
case TutorType.daily:
return Icons.chat;
case TutorType.travel:
return Icons.flight;
case TutorType.academic:
return Icons.school;
}
}
Color get color {
switch (this) {
case TutorType.business:
return Colors.blue;
case TutorType.daily:
return Colors.green;
case TutorType.travel:
return Colors.orange;
case TutorType.academic:
return Colors.purple;
}
}
}
/// AI导师模型
class AITutor {
final String id;
final TutorType type;
final String name;
final String avatar;
final String introduction;
final List<String> specialties;
final List<String> sampleQuestions;
final String personality;
final DateTime createdAt;
final DateTime updatedAt;
const AITutor({
required this.id,
required this.type,
required this.name,
required this.avatar,
required this.introduction,
required this.specialties,
required this.sampleQuestions,
required this.personality,
required this.createdAt,
required this.updatedAt,
});
factory AITutor.fromJson(Map<String, dynamic> json) {
return AITutor(
id: json['id'] as String,
type: TutorType.values.firstWhere(
(e) => e.name == json['type'],
orElse: () => TutorType.daily,
),
name: json['name'] as String,
avatar: json['avatar'] as String,
introduction: json['introduction'] as String,
specialties: List<String>.from(json['specialties'] as List),
sampleQuestions: List<String>.from(json['sampleQuestions'] as List),
personality: json['personality'] as String,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'type': type.name,
'name': name,
'avatar': avatar,
'introduction': introduction,
'specialties': specialties,
'sampleQuestions': sampleQuestions,
'personality': personality,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
};
}
AITutor copyWith({
String? id,
TutorType? type,
String? name,
String? avatar,
String? introduction,
List<String>? specialties,
List<String>? sampleQuestions,
String? personality,
DateTime? createdAt,
DateTime? updatedAt,
}) {
return AITutor(
id: id ?? this.id,
type: type ?? this.type,
name: name ?? this.name,
avatar: avatar ?? this.avatar,
introduction: introduction ?? this.introduction,
specialties: specialties ?? this.specialties,
sampleQuestions: sampleQuestions ?? this.sampleQuestions,
personality: personality ?? this.personality,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
);
}
}

View File

@@ -0,0 +1,155 @@
enum MessageType {
user,
ai,
system;
}
enum ConversationStatus {
active,
paused,
completed,
cancelled;
String get displayName {
switch (this) {
case ConversationStatus.active:
return '进行中';
case ConversationStatus.paused:
return '已暂停';
case ConversationStatus.completed:
return '已完成';
case ConversationStatus.cancelled:
return '已取消';
}
}
}
class ConversationMessage {
final String id;
final String content;
final MessageType type;
final DateTime timestamp;
final String? audioUrl;
final double? confidence; // 语音识别置信度
final Map<String, dynamic>? metadata;
const ConversationMessage({
required this.id,
required this.content,
required this.type,
required this.timestamp,
this.audioUrl,
this.confidence,
this.metadata,
});
factory ConversationMessage.fromJson(Map<String, dynamic> json) {
return ConversationMessage(
id: json['id'] as String,
content: json['content'] as String,
type: MessageType.values.firstWhere(
(e) => e.name == json['type'],
orElse: () => MessageType.user,
),
timestamp: DateTime.parse(json['timestamp'] as String),
audioUrl: json['audioUrl'] as String?,
confidence: json['confidence'] as double?,
metadata: json['metadata'] as Map<String, dynamic>?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'content': content,
'type': type.name,
'timestamp': timestamp.toIso8601String(),
'audioUrl': audioUrl,
'confidence': confidence,
'metadata': metadata,
};
}
}
class Conversation {
final String id;
final String taskId;
final String userId;
final List<ConversationMessage> messages;
final ConversationStatus status;
final DateTime startTime;
final DateTime? endTime;
final int totalDuration; // 总时长(秒)
final Map<String, dynamic>? settings;
const Conversation({
required this.id,
required this.taskId,
required this.userId,
required this.messages,
required this.status,
required this.startTime,
this.endTime,
required this.totalDuration,
this.settings,
});
factory Conversation.fromJson(Map<String, dynamic> json) {
return Conversation(
id: json['id'] as String,
taskId: json['taskId'] as String,
userId: json['userId'] as String,
messages: (json['messages'] as List<dynamic>)
.map((e) => ConversationMessage.fromJson(e as Map<String, dynamic>))
.toList(),
status: ConversationStatus.values.firstWhere(
(e) => e.name == json['status'],
orElse: () => ConversationStatus.active,
),
startTime: DateTime.parse(json['startTime'] as String),
endTime: json['endTime'] != null
? DateTime.parse(json['endTime'] as String)
: null,
totalDuration: json['totalDuration'] as int,
settings: json['settings'] as Map<String, dynamic>?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'taskId': taskId,
'userId': userId,
'messages': messages.map((e) => e.toJson()).toList(),
'status': status.name,
'startTime': startTime.toIso8601String(),
'endTime': endTime?.toIso8601String(),
'totalDuration': totalDuration,
'settings': settings,
};
}
Conversation copyWith({
String? id,
String? taskId,
String? userId,
List<ConversationMessage>? messages,
ConversationStatus? status,
DateTime? startTime,
DateTime? endTime,
int? totalDuration,
Map<String, dynamic>? settings,
}) {
return Conversation(
id: id ?? this.id,
taskId: taskId ?? this.taskId,
userId: userId ?? this.userId,
messages: messages ?? this.messages,
status: status ?? this.status,
startTime: startTime ?? this.startTime,
endTime: endTime ?? this.endTime,
totalDuration: totalDuration ?? this.totalDuration,
settings: settings ?? this.settings,
);
}
}

View File

@@ -0,0 +1,193 @@
/// 对话场景数据模型
class ConversationScenario {
final String id;
final String title;
final String subtitle;
final String description;
final String duration;
final String level;
final ScenarioType type;
final List<String> objectives;
final List<String> keyPhrases;
final List<ScenarioStep> steps;
final DateTime createdAt;
ConversationScenario({
required this.id,
required this.title,
required this.subtitle,
required this.description,
required this.duration,
required this.level,
required this.type,
required this.objectives,
required this.keyPhrases,
required this.steps,
required this.createdAt,
});
factory ConversationScenario.fromJson(Map<String, dynamic> json) {
return ConversationScenario(
id: json['id'],
title: json['title'],
subtitle: json['subtitle'],
description: json['description'],
duration: json['duration'],
level: json['level'],
type: ScenarioType.values.firstWhere(
(e) => e.toString().split('.').last == json['type'],
),
objectives: List<String>.from(json['objectives']),
keyPhrases: List<String>.from(json['keyPhrases']),
steps: (json['steps'] as List)
.map((step) => ScenarioStep.fromJson(step))
.toList(),
createdAt: DateTime.parse(json['createdAt']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'subtitle': subtitle,
'description': description,
'duration': duration,
'level': level,
'type': type.toString().split('.').last,
'objectives': objectives,
'keyPhrases': keyPhrases,
'steps': steps.map((step) => step.toJson()).toList(),
'createdAt': createdAt.toIso8601String(),
};
}
ConversationScenario copyWith({
String? id,
String? title,
String? subtitle,
String? description,
String? duration,
String? level,
ScenarioType? type,
List<String>? objectives,
List<String>? keyPhrases,
List<ScenarioStep>? steps,
DateTime? createdAt,
}) {
return ConversationScenario(
id: id ?? this.id,
title: title ?? this.title,
subtitle: subtitle ?? this.subtitle,
description: description ?? this.description,
duration: duration ?? this.duration,
level: level ?? this.level,
type: type ?? this.type,
objectives: objectives ?? this.objectives,
keyPhrases: keyPhrases ?? this.keyPhrases,
steps: steps ?? this.steps,
createdAt: createdAt ?? this.createdAt,
);
}
}
/// 场景类型枚举
enum ScenarioType {
restaurant,
interview,
business,
travel,
shopping,
medical,
education,
social,
}
extension ScenarioTypeExtension on ScenarioType {
String get displayName {
switch (this) {
case ScenarioType.restaurant:
return '餐厅用餐';
case ScenarioType.interview:
return '工作面试';
case ScenarioType.business:
return '商务会议';
case ScenarioType.travel:
return '旅行出行';
case ScenarioType.shopping:
return '购物消费';
case ScenarioType.medical:
return '医疗健康';
case ScenarioType.education:
return '教育学习';
case ScenarioType.social:
return '社交聚会';
}
}
String get icon {
switch (this) {
case ScenarioType.restaurant:
return '🍽️';
case ScenarioType.interview:
return '💼';
case ScenarioType.business:
return '🏢';
case ScenarioType.travel:
return '✈️';
case ScenarioType.shopping:
return '🛍️';
case ScenarioType.medical:
return '🏥';
case ScenarioType.education:
return '📚';
case ScenarioType.social:
return '🎉';
}
}
}
/// 场景步骤
class ScenarioStep {
final int stepNumber;
final String title;
final String description;
final String role; // 'user' or 'npc'
final String content;
final List<String> options;
final String? correctOption;
ScenarioStep({
required this.stepNumber,
required this.title,
required this.description,
required this.role,
required this.content,
required this.options,
this.correctOption,
});
factory ScenarioStep.fromJson(Map<String, dynamic> json) {
return ScenarioStep(
stepNumber: json['stepNumber'],
title: json['title'],
description: json['description'],
role: json['role'],
content: json['content'],
options: List<String>.from(json['options']),
correctOption: json['correctOption'],
);
}
Map<String, dynamic> toJson() {
return {
'stepNumber': stepNumber,
'title': title,
'description': description,
'role': role,
'content': content,
'options': options,
'correctOption': correctOption,
};
}
}

View File

@@ -0,0 +1,176 @@
enum PronunciationCriteria {
accuracy,
fluency,
completeness,
prosody;
String get displayName {
switch (this) {
case PronunciationCriteria.accuracy:
return '准确性';
case PronunciationCriteria.fluency:
return '流利度';
case PronunciationCriteria.completeness:
return '完整性';
case PronunciationCriteria.prosody:
return '韵律';
}
}
String get description {
switch (this) {
case PronunciationCriteria.accuracy:
return '发音的准确程度';
case PronunciationCriteria.fluency:
return '语音的流畅程度';
case PronunciationCriteria.completeness:
return '内容的完整程度';
case PronunciationCriteria.prosody:
return '语调和节奏的自然程度';
}
}
}
class WordPronunciation {
final String word;
final double accuracyScore; // 0-100
final String? errorType;
final List<String> phonemes;
final List<double> phonemeScores;
final int startTime; // 毫秒
final int endTime; // 毫秒
const WordPronunciation({
required this.word,
required this.accuracyScore,
this.errorType,
required this.phonemes,
required this.phonemeScores,
required this.startTime,
required this.endTime,
});
factory WordPronunciation.fromJson(Map<String, dynamic> json) {
return WordPronunciation(
word: json['word'] as String,
accuracyScore: (json['accuracyScore'] as num).toDouble(),
errorType: json['errorType'] as String?,
phonemes: List<String>.from(json['phonemes'] ?? []),
phonemeScores: List<double>.from(
(json['phonemeScores'] as List<dynamic>? ?? [])
.map((e) => (e as num).toDouble()),
),
startTime: json['startTime'] as int,
endTime: json['endTime'] as int,
);
}
Map<String, dynamic> toJson() {
return {
'word': word,
'accuracyScore': accuracyScore,
'errorType': errorType,
'phonemes': phonemes,
'phonemeScores': phonemeScores,
'startTime': startTime,
'endTime': endTime,
};
}
}
class PronunciationAssessment {
final String id;
final String conversationId;
final String messageId;
final String originalText;
final String recognizedText;
final Map<PronunciationCriteria, double> scores; // 0-100
final double overallScore; // 0-100
final List<WordPronunciation> wordDetails;
final List<String> suggestions;
final DateTime assessedAt;
final Map<String, dynamic>? metadata;
const PronunciationAssessment({
required this.id,
required this.conversationId,
required this.messageId,
required this.originalText,
required this.recognizedText,
required this.scores,
required this.overallScore,
required this.wordDetails,
required this.suggestions,
required this.assessedAt,
this.metadata,
});
factory PronunciationAssessment.fromJson(Map<String, dynamic> json) {
final scoresMap = <PronunciationCriteria, double>{};
final scoresJson = json['scores'] as Map<String, dynamic>? ?? {};
for (final criteria in PronunciationCriteria.values) {
scoresMap[criteria] = (scoresJson[criteria.name] as num?)?.toDouble() ?? 0.0;
}
return PronunciationAssessment(
id: json['id'] as String,
conversationId: json['conversationId'] as String,
messageId: json['messageId'] as String,
originalText: json['originalText'] as String,
recognizedText: json['recognizedText'] as String,
scores: scoresMap,
overallScore: (json['overallScore'] as num).toDouble(),
wordDetails: (json['wordDetails'] as List<dynamic>? ?? [])
.map((e) => WordPronunciation.fromJson(e as Map<String, dynamic>))
.toList(),
suggestions: List<String>.from(json['suggestions'] ?? []),
assessedAt: DateTime.parse(json['assessedAt'] as String),
metadata: json['metadata'] as Map<String, dynamic>?,
);
}
Map<String, dynamic> toJson() {
final scoresJson = <String, double>{};
for (final entry in scores.entries) {
scoresJson[entry.key.name] = entry.value;
}
return {
'id': id,
'conversationId': conversationId,
'messageId': messageId,
'originalText': originalText,
'recognizedText': recognizedText,
'scores': scoresJson,
'overallScore': overallScore,
'wordDetails': wordDetails.map((e) => e.toJson()).toList(),
'suggestions': suggestions,
'assessedAt': assessedAt.toIso8601String(),
'metadata': metadata,
};
}
String get accuracyLevel {
if (overallScore >= 90) return '优秀';
if (overallScore >= 80) return '良好';
if (overallScore >= 70) return '中等';
if (overallScore >= 60) return '及格';
return '需要改进';
}
List<String> get mainIssues {
final issues = <String>[];
if (scores[PronunciationCriteria.accuracy]! < 70) {
issues.add('发音准确性需要提高');
}
if (scores[PronunciationCriteria.fluency]! < 70) {
issues.add('语音流利度有待改善');
}
if (scores[PronunciationCriteria.prosody]! < 70) {
issues.add('语调和节奏需要调整');
}
return issues;
}
}

View File

@@ -0,0 +1,201 @@
/// 发音练习项目数据模型
class PronunciationItem {
final String id;
final String text;
final String phonetic;
final String audioUrl;
final PronunciationType type;
final DifficultyLevel difficulty;
final String category;
final List<String> tips;
final DateTime createdAt;
PronunciationItem({
required this.id,
required this.text,
required this.phonetic,
required this.audioUrl,
required this.type,
required this.difficulty,
required this.category,
required this.tips,
required this.createdAt,
});
factory PronunciationItem.fromJson(Map<String, dynamic> json) {
return PronunciationItem(
id: json['id'],
text: json['text'],
phonetic: json['phonetic'],
audioUrl: json['audioUrl'],
type: PronunciationType.values.firstWhere(
(e) => e.toString().split('.').last == json['type'],
),
difficulty: DifficultyLevel.values.firstWhere(
(e) => e.toString().split('.').last == json['difficulty'],
),
category: json['category'],
tips: List<String>.from(json['tips']),
createdAt: DateTime.parse(json['createdAt']),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'text': text,
'phonetic': phonetic,
'audioUrl': audioUrl,
'type': type.toString().split('.').last,
'difficulty': difficulty.toString().split('.').last,
'category': category,
'tips': tips,
'createdAt': createdAt.toIso8601String(),
};
}
PronunciationItem copyWith({
String? id,
String? text,
String? phonetic,
String? audioUrl,
PronunciationType? type,
DifficultyLevel? difficulty,
String? category,
List<String>? tips,
DateTime? createdAt,
}) {
return PronunciationItem(
id: id ?? this.id,
text: text ?? this.text,
phonetic: phonetic ?? this.phonetic,
audioUrl: audioUrl ?? this.audioUrl,
type: type ?? this.type,
difficulty: difficulty ?? this.difficulty,
category: category ?? this.category,
tips: tips ?? this.tips,
createdAt: createdAt ?? this.createdAt,
);
}
}
/// 发音练习类型
enum PronunciationType {
word,
sentence,
phrase,
phoneme,
}
extension PronunciationTypeExtension on PronunciationType {
String get displayName {
switch (this) {
case PronunciationType.word:
return '单词发音';
case PronunciationType.sentence:
return '句子朗读';
case PronunciationType.phrase:
return '短语练习';
case PronunciationType.phoneme:
return '音素练习';
}
}
String get description {
switch (this) {
case PronunciationType.word:
return '练习单个单词的准确发音';
case PronunciationType.sentence:
return '练习完整句子的语调和节奏';
case PronunciationType.phrase:
return '练习常用短语的连读';
case PronunciationType.phoneme:
return '练习基础音素的发音';
}
}
String get icon {
switch (this) {
case PronunciationType.word:
return '🔤';
case PronunciationType.sentence:
return '📝';
case PronunciationType.phrase:
return '💬';
case PronunciationType.phoneme:
return '🔊';
}
}
}
/// 难度级别
enum DifficultyLevel {
beginner,
intermediate,
advanced,
}
extension DifficultyLevelExtension on DifficultyLevel {
String get displayName {
switch (this) {
case DifficultyLevel.beginner:
return '初级';
case DifficultyLevel.intermediate:
return '中级';
case DifficultyLevel.advanced:
return '高级';
}
}
String get code {
switch (this) {
case DifficultyLevel.beginner:
return 'A1-A2';
case DifficultyLevel.intermediate:
return 'B1-B2';
case DifficultyLevel.advanced:
return 'C1-C2';
}
}
}
/// 发音练习记录
class PronunciationRecord {
final String id;
final String itemId;
final double score;
final String feedback;
final DateTime practiceDate;
final int attempts;
PronunciationRecord({
required this.id,
required this.itemId,
required this.score,
required this.feedback,
required this.practiceDate,
required this.attempts,
});
factory PronunciationRecord.fromJson(Map<String, dynamic> json) {
return PronunciationRecord(
id: json['id'],
itemId: json['itemId'],
score: json['score'].toDouble(),
feedback: json['feedback'],
practiceDate: DateTime.parse(json['practiceDate']),
attempts: json['attempts'],
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'itemId': itemId,
'score': score,
'feedback': feedback,
'practiceDate': practiceDate.toIso8601String(),
'attempts': attempts,
};
}
}

View File

@@ -0,0 +1,163 @@
enum SpeakingScenario {
dailyConversation,
businessMeeting,
jobInterview,
shopping,
restaurant,
travel,
academic,
socializing,
phoneCall,
presentation;
String get displayName {
switch (this) {
case SpeakingScenario.dailyConversation:
return '日常对话';
case SpeakingScenario.businessMeeting:
return '商务会议';
case SpeakingScenario.jobInterview:
return '求职面试';
case SpeakingScenario.shopping:
return '购物';
case SpeakingScenario.restaurant:
return '餐厅';
case SpeakingScenario.travel:
return '旅行';
case SpeakingScenario.academic:
return '学术讨论';
case SpeakingScenario.socializing:
return '社交聚会';
case SpeakingScenario.phoneCall:
return '电话通话';
case SpeakingScenario.presentation:
return '演讲展示';
}
}
String get description {
switch (this) {
case SpeakingScenario.dailyConversation:
return '练习日常生活中的基本对话';
case SpeakingScenario.businessMeeting:
return '提升商务环境下的沟通能力';
case SpeakingScenario.jobInterview:
return '准备求职面试的常见问题';
case SpeakingScenario.shopping:
return '学习购物时的实用表达';
case SpeakingScenario.restaurant:
return '掌握餐厅点餐的对话技巧';
case SpeakingScenario.travel:
return '旅行中的必备口语交流';
case SpeakingScenario.academic:
return '学术环境下的专业讨论';
case SpeakingScenario.socializing:
return '社交场合的自然交流';
case SpeakingScenario.phoneCall:
return '电话沟通的特殊技巧';
case SpeakingScenario.presentation:
return '公开演讲和展示技能';
}
}
}
enum SpeakingDifficulty {
beginner,
elementary,
intermediate,
upperIntermediate,
advanced;
String get displayName {
switch (this) {
case SpeakingDifficulty.beginner:
return '初学者';
case SpeakingDifficulty.elementary:
return '基础';
case SpeakingDifficulty.intermediate:
return '中级';
case SpeakingDifficulty.upperIntermediate:
return '中高级';
case SpeakingDifficulty.advanced:
return '高级';
}
}
}
class SpeakingTask {
final String id;
final String title;
final String description;
final SpeakingScenario scenario;
final SpeakingDifficulty difficulty;
final List<String> objectives;
final List<String> keyPhrases;
final String? backgroundInfo;
final int estimatedDuration; // 预估时长(分钟)
final bool isRecommended; // 是否推荐
final bool isFavorite; // 是否收藏
final int completionCount; // 完成次数
final DateTime createdAt;
final DateTime updatedAt;
const SpeakingTask({
required this.id,
required this.title,
required this.description,
required this.scenario,
required this.difficulty,
required this.objectives,
required this.keyPhrases,
this.backgroundInfo,
required this.estimatedDuration,
this.isRecommended = false,
this.isFavorite = false,
this.completionCount = 0,
required this.createdAt,
required this.updatedAt,
});
factory SpeakingTask.fromJson(Map<String, dynamic> json) {
return SpeakingTask(
id: json['id'] as String,
title: json['title'] as String,
description: json['description'] as String,
scenario: SpeakingScenario.values.firstWhere(
(e) => e.name == json['scenario'],
orElse: () => SpeakingScenario.dailyConversation,
),
difficulty: SpeakingDifficulty.values.firstWhere(
(e) => e.name == json['difficulty'],
orElse: () => SpeakingDifficulty.intermediate,
),
objectives: List<String>.from(json['objectives'] ?? []),
keyPhrases: List<String>.from(json['keyPhrases'] ?? []),
backgroundInfo: json['backgroundInfo'] as String?,
estimatedDuration: json['estimatedDuration'] as int,
isRecommended: json['isRecommended'] as bool? ?? false,
isFavorite: json['isFavorite'] as bool? ?? false,
completionCount: json['completionCount'] as int? ?? 0,
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'description': description,
'scenario': scenario.name,
'difficulty': difficulty.name,
'objectives': objectives,
'keyPhrases': keyPhrases,
'backgroundInfo': backgroundInfo,
'estimatedDuration': estimatedDuration,
'isRecommended': isRecommended,
'isFavorite': isFavorite,
'completionCount': completionCount,
'createdAt': createdAt.toIso8601String(),
'updatedAt': updatedAt.toIso8601String(),
};
}
}

View File

@@ -0,0 +1,244 @@
import 'speaking_scenario.dart';
import 'pronunciation_assessment.dart';
class SpeakingStats {
final int totalSessions;
final int totalMinutes;
final double averageScore;
final Map<SpeakingScenario, int> scenarioStats;
final Map<SpeakingDifficulty, int> difficultyStats;
final List<SpeakingProgressData> progressData;
final SpeakingSkillAnalysis skillAnalysis;
final DateTime lastUpdated;
const SpeakingStats({
required this.totalSessions,
required this.totalMinutes,
required this.averageScore,
required this.scenarioStats,
required this.difficultyStats,
required this.progressData,
required this.skillAnalysis,
required this.lastUpdated,
});
factory SpeakingStats.fromJson(Map<String, dynamic> json) {
final scenarioStatsJson = json['scenarioStats'] as Map<String, dynamic>? ?? {};
final scenarioStats = <SpeakingScenario, int>{};
for (final scenario in SpeakingScenario.values) {
final v = scenarioStatsJson[scenario.name];
scenarioStats[scenario] = (v is num) ? v.toInt() : 0;
}
final difficultyStatsLegacy = json['difficultyStats'] as Map<String, dynamic>?;
final levelStatsBackend = json['stats_by_level'] as Map<String, dynamic>?;
final difficultyStats = <SpeakingDifficulty, int>{};
if (difficultyStatsLegacy != null) {
for (final difficulty in SpeakingDifficulty.values) {
final v = difficultyStatsLegacy[difficulty.name];
difficultyStats[difficulty] = (v is num) ? v.toInt() : 0;
}
} else {
for (final difficulty in SpeakingDifficulty.values) {
final key = {
SpeakingDifficulty.beginner: 'beginner',
SpeakingDifficulty.elementary: 'elementary',
SpeakingDifficulty.intermediate: 'intermediate',
SpeakingDifficulty.upperIntermediate: 'upper_intermediate',
SpeakingDifficulty.advanced: 'advanced',
}[difficulty]!;
final entry = levelStatsBackend?[key];
int count = 0;
if (entry is Map) {
final c = entry['count'];
if (c is num) count = c.toInt();
}
difficultyStats[difficulty] = count;
}
}
final avgScores = json['average_scores'] as Map<String, dynamic>? ?? {};
final averageScore = (json['averageScore'] as num?)?.toDouble() ?? (avgScores['overall'] as num?)?.toDouble() ?? 0.0;
final skillAnalysisJson = json['skillAnalysis'] as Map<String, dynamic>?;
final skillAnalysis = skillAnalysisJson != null
? SpeakingSkillAnalysis.fromJson(skillAnalysisJson)
: SpeakingSkillAnalysis(
criteriaScores: {
PronunciationCriteria.accuracy: (avgScores['accuracy'] as num?)?.toDouble() ?? 0.0,
PronunciationCriteria.fluency: (avgScores['fluency'] as num?)?.toDouble() ?? 0.0,
PronunciationCriteria.completeness: (avgScores['completeness'] as num?)?.toDouble() ?? 0.0,
PronunciationCriteria.prosody: (avgScores['prosody'] as num?)?.toDouble() ?? 0.0,
},
commonErrors: {},
strengths: const [],
weaknesses: const [],
recommendations: const [],
improvementRate: 0.0,
lastAnalyzed: DateTime.now(),
);
return SpeakingStats(
totalSessions: (json['totalSessions'] as int?) ?? (json['total_records'] as num?)?.toInt() ?? 0,
totalMinutes: (json['totalMinutes'] as int?) ?? (json['total_duration'] as num?)?.toInt() ?? 0,
averageScore: averageScore,
scenarioStats: scenarioStats,
difficultyStats: difficultyStats,
progressData: (json['progressData'] as List<dynamic>? ?? [])
.map((e) => SpeakingProgressData.fromJson(e as Map<String, dynamic>))
.toList(),
skillAnalysis: skillAnalysis,
lastUpdated: (json['lastUpdated'] is String)
? DateTime.parse(json['lastUpdated'] as String)
: DateTime.now(),
);
}
Map<String, dynamic> toJson() {
final scenarioStatsJson = <String, int>{};
for (final entry in scenarioStats.entries) {
scenarioStatsJson[entry.key.name] = entry.value;
}
final difficultyStatsJson = <String, int>{};
for (final entry in difficultyStats.entries) {
difficultyStatsJson[entry.key.name] = entry.value;
}
return {
'totalSessions': totalSessions,
'totalMinutes': totalMinutes,
'averageScore': averageScore,
'scenarioStats': scenarioStatsJson,
'difficultyStats': difficultyStatsJson,
'progressData': progressData.map((e) => e.toJson()).toList(),
'skillAnalysis': skillAnalysis.toJson(),
'lastUpdated': lastUpdated.toIso8601String(),
};
}
}
class SpeakingProgressData {
final DateTime date;
final double averageScore;
final int sessionCount;
final int totalMinutes;
final Map<PronunciationCriteria, double> criteriaScores;
const SpeakingProgressData({
required this.date,
required this.averageScore,
required this.sessionCount,
required this.totalMinutes,
required this.criteriaScores,
});
factory SpeakingProgressData.fromJson(Map<String, dynamic> json) {
final criteriaScoresJson = json['criteriaScores'] as Map<String, dynamic>? ?? {};
final criteriaScores = <PronunciationCriteria, double>{};
for (final criteria in PronunciationCriteria.values) {
criteriaScores[criteria] = (criteriaScoresJson[criteria.name] as num?)?.toDouble() ?? 0.0;
}
return SpeakingProgressData(
date: DateTime.parse(json['date'] as String),
averageScore: (json['averageScore'] as num).toDouble(),
sessionCount: (json['sessionCount'] as num).toInt(),
totalMinutes: (json['totalMinutes'] as num).toInt(),
criteriaScores: criteriaScores,
);
}
Map<String, dynamic> toJson() {
final criteriaScoresJson = <String, double>{};
for (final entry in criteriaScores.entries) {
criteriaScoresJson[entry.key.name] = entry.value;
}
return {
'date': date.toIso8601String(),
'averageScore': averageScore,
'sessionCount': sessionCount,
'totalMinutes': totalMinutes,
'criteriaScores': criteriaScoresJson,
};
}
}
class SpeakingSkillAnalysis {
final Map<PronunciationCriteria, double> criteriaScores;
final Map<String, int> commonErrors;
final List<String> strengths;
final List<String> weaknesses;
final List<String> recommendations;
final double improvementRate; // 改进速度
final DateTime lastAnalyzed;
const SpeakingSkillAnalysis({
required this.criteriaScores,
required this.commonErrors,
required this.strengths,
required this.weaknesses,
required this.recommendations,
required this.improvementRate,
required this.lastAnalyzed,
});
factory SpeakingSkillAnalysis.fromJson(Map<String, dynamic> json) {
final criteriaScoresJson = json['criteriaScores'] as Map<String, dynamic>? ?? {};
final criteriaScores = <PronunciationCriteria, double>{};
for (final criteria in PronunciationCriteria.values) {
criteriaScores[criteria] = (criteriaScoresJson[criteria.name] as num?)?.toDouble() ?? 0.0;
}
return SpeakingSkillAnalysis(
criteriaScores: criteriaScores,
commonErrors: Map<String, int>.from(json['commonErrors'] ?? {}),
strengths: List<String>.from(json['strengths'] ?? []),
weaknesses: List<String>.from(json['weaknesses'] ?? []),
recommendations: List<String>.from(json['recommendations'] ?? []),
improvementRate: (json['improvementRate'] as num?)?.toDouble() ?? 0.0,
lastAnalyzed: (json['lastAnalyzed'] is String)
? DateTime.parse(json['lastAnalyzed'] as String)
: DateTime.now(),
);
}
Map<String, dynamic> toJson() {
final criteriaScoresJson = <String, double>{};
for (final entry in criteriaScores.entries) {
criteriaScoresJson[entry.key.name] = entry.value;
}
return {
'criteriaScores': criteriaScoresJson,
'commonErrors': commonErrors,
'strengths': strengths,
'weaknesses': weaknesses,
'recommendations': recommendations,
'improvementRate': improvementRate,
'lastAnalyzed': lastAnalyzed.toIso8601String(),
};
}
String get overallLevel {
final averageScore = criteriaScores.values.reduce((a, b) => a + b) / criteriaScores.length;
if (averageScore >= 90) return '优秀';
if (averageScore >= 80) return '良好';
if (averageScore >= 70) return '中等';
if (averageScore >= 60) return '及格';
return '需要改进';
}
PronunciationCriteria get strongestSkill {
return criteriaScores.entries
.reduce((a, b) => a.value > b.value ? a : b)
.key;
}
PronunciationCriteria get weakestSkill {
return criteriaScores.entries
.reduce((a, b) => a.value < b.value ? a : b)
.key;
}
}