feat: 优化口语防重复问答、课程推进流转、键盘输入流畅度与竖屏锁定
- AI口语约束:在对话系统提示词与单轮指导中增加防重复问答约束,避免重复寒暄及索取已提供信息,根据历史自然向前推进 - 课程流转:修复多段课程学完后今日任务卡片流转下一课,及退出重进定位逻辑 - 竖屏锁定:在 Flutter、Android 及 iOS 平台配置仅支持竖屏显示 - 对话交互:优化对话界面文本输入重绘与词汇查询,解决键盘输入卡顿 - 测试用例:补充并更新单轮约束、状态流转与返回键退出测试,212项测试全绿
This commit is contained in:
@@ -18,6 +18,67 @@ final Map<String, LessonDialogue> segmentDialogues = <String, LessonDialogue>{};
|
||||
final Map<String, String> segmentGrammarNotes = <String, String>{};
|
||||
final List<DialogueScene> practiceScenes = <DialogueScene>[];
|
||||
|
||||
/// All distinct English words introduced in each CEFR level, populated from
|
||||
/// the course JSON content packs.
|
||||
final Map<String, Set<String>> levelVocabularyWords = <String, Set<String>>{};
|
||||
|
||||
const List<String> cefrLevels = ['A0', 'A1', 'A2', 'B1'];
|
||||
|
||||
/// Registers words for [level] into the vocabulary index.
|
||||
void registerLevelWords(String level, Iterable<String> words) {
|
||||
final set = levelVocabularyWords.putIfAbsent(
|
||||
level.toUpperCase(),
|
||||
() => <String>{},
|
||||
);
|
||||
for (final w in words) {
|
||||
final clean = w.toLowerCase().replaceAll("'", '').trim();
|
||||
if (clean.isNotEmpty) set.add(clean);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fixed demo names and places accepted across all levels as standard practice slots.
|
||||
const Set<String> standardDemoEntities = {
|
||||
'alex',
|
||||
'mia',
|
||||
'shen',
|
||||
'bo',
|
||||
'ava',
|
||||
'tom',
|
||||
'sam',
|
||||
'beijing',
|
||||
'shanghai',
|
||||
'hong',
|
||||
'kong',
|
||||
'china',
|
||||
'london',
|
||||
};
|
||||
|
||||
/// Returns all English words taught in the course up to [level] (inclusive).
|
||||
Set<String> taughtCourseWordsUpToLevel(String level) {
|
||||
final targetIdx = cefrLevels.indexOf(level.toUpperCase());
|
||||
final maxIdx = targetIdx < 0 ? 0 : targetIdx;
|
||||
final result = Set<String>.from(standardDemoEntities);
|
||||
for (var i = 0; i <= maxIdx && i < cefrLevels.length; i++) {
|
||||
final words = levelVocabularyWords[cefrLevels[i]];
|
||||
if (words != null) result.addAll(words);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Returns all English words taught in the course up to [targetItemId]'s level.
|
||||
Set<String> taughtCourseWordsUpToItem(String targetItemId) {
|
||||
return taughtCourseWordsUpToLevel(itemLevel(targetItemId));
|
||||
}
|
||||
|
||||
/// All distinct English words across all loaded course packs.
|
||||
Set<String> allLoadedCourseWords() {
|
||||
final result = <String>{};
|
||||
for (final words in levelVocabularyWords.values) {
|
||||
result.addAll(words);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Empties every registry, for `CourseRepository.resetForTest`.
|
||||
void clearCourseCatalog() {
|
||||
courseLessons.clear();
|
||||
@@ -28,6 +89,7 @@ void clearCourseCatalog() {
|
||||
segmentGrammarNotes.clear();
|
||||
practiceScenes.clear();
|
||||
assessmentPacks.clear();
|
||||
levelVocabularyWords.clear();
|
||||
}
|
||||
|
||||
/// Every lesson the app can teach, in learning order: A0 first, then the
|
||||
|
||||
@@ -120,10 +120,57 @@ class CourseRepository {
|
||||
_register(pack);
|
||||
}
|
||||
|
||||
void _indexPackVocabulary(CoursePack pack) {
|
||||
final words = <String>{};
|
||||
void addText(String? text) {
|
||||
if (text == null || text.isEmpty) return;
|
||||
final matches = RegExp(r"[A-Za-z]+(?:'[A-Za-z]+)?").allMatches(text);
|
||||
for (final m in matches) {
|
||||
final word = m.group(0)!.replaceAll("'", '').toLowerCase();
|
||||
if (word.isNotEmpty) words.add(word);
|
||||
}
|
||||
}
|
||||
|
||||
for (final item in pack.coreItems) {
|
||||
addText(item.en);
|
||||
addText(item.exampleEn);
|
||||
for (final group in item.match) {
|
||||
for (final term in group) {
|
||||
addText(term);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final rw in pack.receptiveWords) {
|
||||
addText(rw.en);
|
||||
}
|
||||
for (final seg in pack.segments) {
|
||||
addText(seg.listeningText);
|
||||
addText(seg.speakingText);
|
||||
addText(seg.readingText);
|
||||
addText(seg.writingExample);
|
||||
if (seg.vocabulary != null) {
|
||||
for (final v in seg.vocabulary!) {
|
||||
addText(v.word);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (final scene in pack.scenes) {
|
||||
for (final turn in scene.turns) {
|
||||
addText(turn.ai);
|
||||
addText(turn.model);
|
||||
}
|
||||
}
|
||||
for (final mat in pack.materials) {
|
||||
addText(mat.text);
|
||||
}
|
||||
registerLevelWords(pack.level, words);
|
||||
}
|
||||
|
||||
/// Adapts one pack into the shared runtime registries. Explicit rules and
|
||||
/// texts in the pack win; anything left out is derived from the core items
|
||||
/// the part teaches.
|
||||
void _register(CoursePack pack) {
|
||||
_indexPackVocabulary(pack);
|
||||
final byId = {for (final item in pack.coreItems) item.id: item};
|
||||
final scenesById = {for (final scene in pack.scenes) scene.id: scene};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user