enum LearningGoal { dailyLife, travel, workStarter } enum PlacementLevel { beginner, someBasics, simpleConversation } enum AppTab { home, learn, dialogue, review, progress } enum LessonStep { preview, listening, speaking, reading, writing, dialogue, independent, complete, } enum MasteryStatus { newItem, recognize, recall, use, master, needsReview } enum EvidenceKind { exposure, assisted, independentSuccess, languageError, pending, } class AttemptEvidence { const AttemptEvidence({ required this.id, required this.itemId, required this.taskId, required this.skill, required this.inputMode, required this.outcome, required this.createdAt, this.rawAnswer, this.recordingPath, this.assisted = false, this.variantIndex = 0, this.originalTranscript, this.transcriptConfirmed = false, this.transcriptEdited = false, }); final String id; final String itemId; final String taskId; final String skill; final String inputMode; final EvidenceKind outcome; final DateTime createdAt; final String? rawAnswer; /// Local-only file reference. It is absent unless the learner opted to keep /// recordings; no path or audio is sent to an AI provider. final String? recordingPath; final bool assisted; final int variantIndex; /// The device STT result before the learner edits it. This local-only field /// distinguishes confirmed spoken input from a subsequent typed rewrite. final String? originalTranscript; final bool transcriptConfirmed; final bool transcriptEdited; } enum AiProviderType { mock, gemini, openAi, compatible } enum AssessmentSkill { listening, speaking, reading, writing } enum ContentStatus { draft, validated, approved, rejected } enum ContentSource { builtInOriginal, aiGenerated, importedReference } /// A validated, bounded suggestion from an AI conversation provider. /// The client still owns task completion and mastery decisions. class DialogueAiResponse { const DialogueAiResponse({ required this.reply, this.slots = const {}, this.evidence = const [], this.suggestsComplete = false, this.translation, this.feedback, }); final String reply; final String? translation; final Map slots; final List evidence; final bool suggestsComplete; final String? feedback; } /// A bounded suggestion only. It never changes completion, mastery, or a /// stage result; those decisions remain on-device and evidence-based. class WritingAiFeedback { const WritingAiFeedback({ required this.verdict, required this.feedback, required this.suggestion, required this.missing, }); /// `accepted`, `rewrite`, or `uncertain`. final String verdict; final String feedback; final String? suggestion; final List missing; } class AssessmentRecord { const AssessmentRecord({ required this.packId, required this.completedAt, required this.results, this.pendingSkills = const {}, }); final String packId; final DateTime completedAt; final Map results; final Set pendingSkills; bool get passed => AssessmentSkill.values.every((skill) => results[skill] == true); List get failedSkills => AssessmentSkill.values .where( (skill) => results[skill] != true && !pendingSkills.contains(skill), ) .toList(); } class AssessmentDraft { const AssessmentDraft({ required this.packId, required this.taskIndex, required this.results, }); final String packId; final int taskIndex; final Map results; } class DialogueDraft { const DialogueDraft({ required this.lessonId, required this.stage, required this.turns, required this.usedHelp, }); final String lessonId; final int stage; final List turns; final bool usedHelp; } class LessonSummary { const LessonSummary({ required this.number, required this.title, required this.outcome, this.isComplete = false, this.isCurrent = false, }); final int number; final String title; final String outcome; final bool isComplete; final bool isCurrent; } class VocabularyItem { const VocabularyItem({ required this.id, required this.word, required this.meaning, required this.example, required this.exampleMeaning, this.ipa, }); final String id; final String word; final String meaning; final String example; final String exampleMeaning; final String? ipa; } /// A non-core explanation returned by a configured text provider for a word /// or phrase that is not present in the reviewed course lexicon. These /// entries deliberately never participate in review scheduling or mastery. class TemporaryLexiconEntry { const TemporaryLexiconEntry({ required this.query, required this.definition, required this.provider, required this.model, required this.createdAt, }); final String query; final String definition; final String provider; final String model; final DateTime createdAt; } /// Represents an extracted key phrase or vocabulary item within a sentence. class PhraseBreakdownItem { const PhraseBreakdownItem({ required this.phrase, required this.meaning, this.ipa, this.usageNote, }); final String phrase; final String meaning; final String? ipa; final String? usageNote; Map toJson() => { 'phrase': phrase, 'meaning': meaning, if (ipa != null) 'ipa': ipa, if (usageNote != null) 'usageNote': usageNote, }; factory PhraseBreakdownItem.fromJson(Map json) => PhraseBreakdownItem( phrase: json['phrase'] as String? ?? '', meaning: json['meaning'] as String? ?? '', ipa: json['ipa'] as String?, usageNote: json['usageNote'] as String?, ); } /// Structured multi-dimensional analysis for a sentence or phrase, including /// translation, sentence pattern, grammar note, pronunciation tips, and extracted phrases. class SentenceAnalysisResult { const SentenceAnalysisResult({ required this.originalText, required this.translation, this.sentencePattern, this.grammarNote, this.pronunciationTips, this.phrases = const [], this.provider = 'unknown', this.model = '', required this.createdAt, }); final String originalText; final String translation; final String? sentencePattern; final String? grammarNote; final String? pronunciationTips; final List phrases; final String provider; final String model; final DateTime createdAt; Map toJson() => { 'originalText': originalText, 'translation': translation, if (sentencePattern != null) 'sentencePattern': sentencePattern, if (grammarNote != null) 'grammarNote': grammarNote, if (pronunciationTips != null) 'pronunciationTips': pronunciationTips, 'phrases': phrases.map((p) => p.toJson()).toList(), 'provider': provider, 'model': model, 'createdAt': createdAt.toIso8601String(), }; factory SentenceAnalysisResult.fromJson(Map json) => SentenceAnalysisResult( originalText: json['originalText'] as String? ?? '', translation: json['translation'] as String? ?? '', sentencePattern: json['sentencePattern'] as String?, grammarNote: json['grammarNote'] as String?, pronunciationTips: json['pronunciationTips'] as String?, phrases: (json['phrases'] as List? ?? const []) .whereType>() .map(PhraseBreakdownItem.fromJson) .toList(), provider: json['provider'] as String? ?? 'unknown', model: json['model'] as String? ?? '', createdAt: json['createdAt'] != null ? DateTime.tryParse(json['createdAt'] as String) ?? DateTime.now() : DateTime.now(), ); } class ReviewItem { const ReviewItem({ required this.id, required this.target, required this.prompt, required this.hint, required this.dueAt, required this.skill, this.attempts = 0, this.successfulReviews = 0, this.variantIndex = 0, this.lastProgressedAt, this.isAiGenerated = false, }); final String id; final String target; final String prompt; final String hint; final DateTime dueAt; final String skill; final int attempts; final int successfulReviews; final int variantIndex; final DateTime? lastProgressedAt; final bool isAiGenerated; ReviewItem copyWith({ DateTime? dueAt, int? attempts, int? successfulReviews, int? variantIndex, String? prompt, String? hint, String? skill, DateTime? lastProgressedAt, bool? isAiGenerated, }) => ReviewItem( id: id, target: target, prompt: prompt ?? this.prompt, hint: hint ?? this.hint, dueAt: dueAt ?? this.dueAt, skill: skill ?? this.skill, attempts: attempts ?? this.attempts, successfulReviews: successfulReviews ?? this.successfulReviews, variantIndex: variantIndex ?? this.variantIndex, lastProgressedAt: lastProgressedAt ?? this.lastProgressedAt, isAiGenerated: isAiGenerated ?? this.isAiGenerated, ); } class MasteryItem { const MasteryItem({ required this.id, required this.label, required this.status, required this.evidence, this.needsReview = false, this.checkpoint = 0, this.firstTaughtAt, }); final String id; final String label; final MasteryStatus status; final List evidence; final bool needsReview; final int checkpoint; final DateTime? firstTaughtAt; MasteryItem copyWith({ MasteryStatus? status, List? evidence, bool? needsReview, int? checkpoint, DateTime? firstTaughtAt, }) => MasteryItem( id: id, label: label, status: status ?? this.status, evidence: evidence ?? this.evidence, needsReview: needsReview ?? this.needsReview, checkpoint: checkpoint ?? this.checkpoint, firstTaughtAt: firstTaughtAt ?? this.firstTaughtAt, ); } class DialogueTurn { const DialogueTurn({ required this.text, required this.isLearner, this.translation, }); final String text; final bool isLearner; final String? translation; DialogueTurn copyWith({ String? text, bool? isLearner, String? translation, }) { return DialogueTurn( text: text ?? this.text, isLearner: isLearner ?? this.isLearner, translation: translation ?? this.translation, ); } } /// Ephemeral presentation data for a completed controlled dialogue. The /// related recap review is separately persisted in AppState. class DialogueSummaryData { const DialogueSummaryData({ required this.completedTasks, required this.personalSentence, required this.usedHelp, this.improvement, }); final List completedTasks; final String personalSentence; final bool usedHelp; /// One improvement point collected during the dialogue and shown only at the /// end, per "对话中不逐句打断" in the conversation spec. final String? improvement; }