Files
English/kouyu_english/lib/core/models.dart
T

338 lines
8.0 KiB
Dart

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,
required this.slots,
required this.evidence,
required this.suggestsComplete,
this.translation,
this.feedback,
});
final String reply;
final String? translation;
final Map<String, String> slots;
final List<String> 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<String> 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<AssessmentSkill, bool> results;
final Set<AssessmentSkill> pendingSkills;
bool get passed =>
AssessmentSkill.values.every((skill) => results[skill] == true);
List<AssessmentSkill> 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<String, bool> 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<DialogueTurn> 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;
}
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<EvidenceKind> evidence;
final bool needsReview;
final int checkpoint;
final DateTime? firstTaughtAt;
MasteryItem copyWith({
MasteryStatus? status,
List<EvidenceKind>? 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,
});
final List<String> completedTasks;
final String personalSentence;
final bool usedHelp;
}