fix: 修正学习流程中的证据、复习与进度问题

- 独立尝试按课程要求校验内容,不再任意输入即算独立成功
- 复习节点改为学后第 1/3/7/14 天,之后每 30 天抽查;复习轮换情境
- 掌握状态按作答记录推导:需认识、可回忆、跨情境使用及最近两次无帮助
- 课程证据按实际用到的目标记录;跟读与课程对话只算辅助练习
- 复习成功不再降低已有状态
- 学完课程后进入下一节未完成课程;全部学完后引导巩固与阶段评估
- 新增 3 题基础定位,按结果设置起始课程
- 新增按课程开放的对话场景,首页推荐最少练习的场景
- 切换课程时清空上一课的步骤进度
- AI 返回 JSON 格式并按 JSON 回放历史;拼写校验忽略大小写与连字符;移除学习目标选择

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-17 18:52:01 +09:00
co-authored by Claude Opus 5
parent efdff769e4
commit 9ab08c25ef
19 changed files with 1645 additions and 302 deletions
+99 -17
View File
@@ -28,7 +28,10 @@ void main() {
);
}
expect(a0MeetDialogue.taskLabels.length, a0MeetDialogue.prompts.length);
expect(a0MeetDialogue.requiredTerms.length, a0MeetDialogue.prompts.length);
expect(
a0MeetDialogue.requiredTerms.length,
a0MeetDialogue.prompts.length,
);
});
test('示范答案能通过对应回合的校验', () {
@@ -46,37 +49,101 @@ void main() {
test('跑题回答不再因为关键词沾边而通过', () {
// 旧实现用 text.contains('it'),下面这些句子全部会被判为完成任务。
expect(matchesDialogueStage(a0Dialogues['a0-05']!, 0, 'I did it.'), isFalse);
expect(matchesDialogueStage(a0Dialogues['a0-08']!, 0, 'It is good.'), isFalse);
expect(matchesDialogueStage(a0Dialogues['a0-09']!, 2, 'I like tea.'), isFalse);
expect(
matchesDialogueStage(a0Dialogues['a0-05']!, 0, 'I did it.'),
isFalse,
);
expect(
matchesDialogueStage(a0Dialogues['a0-08']!, 0, 'It is good.'),
isFalse,
);
expect(
matchesDialogueStage(a0Dialogues['a0-09']!, 2, 'I like tea.'),
isFalse,
);
expect(matchesDialogueStage(a0MeetDialogue, 0, 'Hello.'), isFalse);
expect(matchesDialogueStage(a0MeetDialogue, 1, 'I am good.'), isFalse);
});
test('自由场景接受同样正确的其他说法', () {
// 旧正则只认 good/okay/tired,也不认 my name's。
expect(matchesDialogueStage(a0MeetDialogue, 0, "My name's Shen."), isTrue);
expect(
matchesDialogueStage(a0MeetDialogue, 0, "My name's Shen."),
isTrue,
);
expect(matchesDialogueStage(a0MeetDialogue, 0, 'I am Shen.'), isTrue);
expect(matchesDialogueStage(a0MeetDialogue, 2, "I'm fine, thanks."), isTrue);
expect(
matchesDialogueStage(a0MeetDialogue, 2, "I'm fine, thanks."),
isTrue,
);
expect(matchesDialogueStage(a0MeetDialogue, 2, 'I am great!'), isTrue);
expect(matchesDialogueStage(a0MeetDialogue, 3, 'Where are you from?'), isTrue);
expect(matchesDialogueStage(a0MeetDialogue, 3, 'Do you like tea?'), isTrue);
expect(
matchesDialogueStage(a0MeetDialogue, 3, 'Where are you from?'),
isTrue,
);
expect(
matchesDialogueStage(a0MeetDialogue, 3, 'Do you like tea?'),
isTrue,
);
});
test('只写半句不算完成任务', () {
expect(matchesDialogueStage(a0MeetDialogue, 0, "I'm"), isFalse);
expect(matchesDialogueStage(a0Dialogues['a0-04']!, 0, 'My number is'), isFalse);
expect(
matchesDialogueStage(a0Dialogues['a0-04']!, 0, 'My number is one-three-eight.'),
matchesDialogueStage(a0Dialogues['a0-04']!, 0, 'My number is'),
isFalse,
);
expect(
matchesDialogueStage(
a0Dialogues['a0-04']!,
0,
'My number is one-three-eight.',
),
isTrue,
);
});
test('拼读和数字这类结构化要求可以识别', () {
expect(matchesDialogueStage(a0Dialogues['a0-02']!, 1, 'S-H-E-N'), isTrue);
expect(matchesDialogueStage(a0Dialogues['a0-02']!, 1, 'Shen'), isFalse);
expect(matchesDialogueStage(a0Dialogues['a0-08']!, 2, "It's three o'clock."), isTrue);
expect(matchesDialogueStage(a0Dialogues['a0-08']!, 2, "It's Monday."), isFalse);
});
test('拼读忽略大小写和连字符', () {
final spelling = a0Dialogues['a0-02']!;
for (final answer in [
'A-A-A',
'aaa',
'AAA',
'a-a-a',
'A - A - A',
'a a a',
'Shen',
'S-H-E-N.',
'She-n',
'L-I',
'Li',
'My name is Alex. A-L-E-X.',
]) {
expect(
matchesDialogueStage(spelling, 1, answer),
isTrue,
reason: answer,
);
}
for (final answer in ['a', 'My name is Alex.', '123', '---']) {
expect(
matchesDialogueStage(spelling, 1, answer),
isFalse,
reason: answer,
);
}
expect(
matchesDialogueStage(a0Dialogues['a0-08']!, 2, "It's three o'clock."),
isTrue,
);
expect(
matchesDialogueStage(a0Dialogues['a0-08']!, 2, "It's Monday."),
isFalse,
);
});
test('任务标签可用于提示和总结', () {
@@ -86,7 +153,11 @@ void main() {
test('输入校验忽略多余空格与大小写', () {
expect(
matchesDialogueStage(a0MeetDialogue, 0, " MY NAME IS SHEN "),
matchesDialogueStage(
a0MeetDialogue,
0,
" MY NAME IS SHEN ",
),
isTrue,
);
expect(
@@ -94,11 +165,19 @@ void main() {
isTrue,
);
expect(
matchesDialogueStage(a0Dialogues["a0-04"]!, 0, "MY NUMBER IS ONE-THREE-EIGHT"),
matchesDialogueStage(
a0Dialogues["a0-04"]!,
0,
"MY NUMBER IS ONE-THREE-EIGHT",
),
isTrue,
);
expect(
matchesDialogueStage(a0Dialogues["a0-08"]!, 2, "IT'S THREE O'CLOCK"),
matchesDialogueStage(
a0Dialogues["a0-08"]!,
2,
"IT'S THREE O'CLOCK",
),
isTrue,
);
});
@@ -110,7 +189,10 @@ void main() {
expect(later.length, greaterThan(first.length));
expect(first.every(later.contains), isTrue);
expect(later.length, lessThanOrEqualTo(allTaughtLanguage.length));
expect(first.any((word) => word.toLowerCase().contains('coffee')), isFalse);
expect(
first.any((word) => word.toLowerCase().contains('coffee')),
isFalse,
);
});
});
}