feat: 重做阅读找答案题与 AI 情境对话,并归拢本地识别、查词等既有改动

阅读"在对话里找到答案":
- 16 道题全部重写,干扰项真实出现在对话里,靠说话人归属或否定句才能作答
- 选项按题目内容确定性打乱,答案不再固定排第一;听力环节同样处理
- 去掉超纲干扰项、重复题干,收紧自由作答匹配(过去单个字母也能判对)

AI 情境对话:
- 提示词区分"AI 这一句要做什么"与"学习者随后要完成什么",并下发已教词句清单
- JSON 只强制 reply,translation/feedback 可选;不再索要用不上的 slots/evidence
- AI 不可用时页面明确提示当前回复来自内置示范脚本
- 删掉按 stage 下标猜中文翻译的兜底,避免译文与英文对不上
- 整课对话改用逐轮必需表达校验,替换"关键词沾边就算过";修正自由场景正则误伤
- 总结的"完成任务"按实际通过的轮次生成;模型点评只在结束页呈现一次
- 自由场景支持草稿续练(独立存储槽);修正回答轮数文案与永不解锁的场景标注

同时提交此前工作区中累积的改动:SenseVoice 本地识别、查词/句型解析卡、
复习与测评页调整等,并补充对话校验、选项分布和句子解析的测试。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-16 16:38:23 +09:00
co-authored by Claude Opus 5
parent ef2fce207a
commit 7e29c5449f
24 changed files with 2896 additions and 665 deletions
+306 -58
View File
@@ -1,3 +1,4 @@
import 'a0_core.dart';
import 'models.dart';
/// Stable A0 core identities taught by each seed lesson. Wording can vary in
@@ -533,6 +534,7 @@ class LessonActivity {
required this.reading,
required this.readingQuestion,
required this.readingAnswer,
this.readingOptions = const [],
required this.writingPrompt,
required this.writingExample,
required this.independentPrompt,
@@ -545,12 +547,39 @@ class LessonActivity {
final String reading;
final String readingQuestion;
final String readingAnswer;
final List<String> readingOptions;
final String writingPrompt;
final String writingExample;
final String independentPrompt;
final String independentHelp;
}
int _seedHash(String seed) {
var hash = 0x811c9dc5;
for (final code in seed.codeUnits) {
hash = ((hash ^ code) * 0x01000193) & 0x7fffffff;
}
return hash;
}
/// 选项顺序固定会让学习者养成“永远选第一个”的习惯。[options] 的第一项是标准
/// 答案,这里按题目内容确定性地把它挪到某个位置,并打乱其余干扰项:同一道题
/// 每次进入顺序都一样,但答案在三个位置上分布均匀。
List<String> shuffledOptions(List<String> options, String seed) {
if (options.length < 2) return options;
final distractors = [...options.skip(1)];
var hash = _seedHash(seed);
for (var i = distractors.length - 1; i > 0; i--) {
hash = (hash * 1103515245 + 12345) & 0x7fffffff;
final j = hash % (i + 1);
final swap = distractors[i];
distractors[i] = distractors[j];
distractors[j] = swap;
}
return distractors
..insert(_seedHash('$seed#slot') % options.length, options.first);
}
const a0Activities = <String, LessonActivity>{
'a0-01': LessonActivity(
listening: 'Hello. I’m Mia. Nice to meet you.',
@@ -558,9 +587,14 @@ const a0Activities = <String, LessonActivity>{
answers: ['自我介绍', '买东西', '问时间'],
speaking: 'Hello. I’m Shen. Nice to meet you.',
reading:
'Mia: Hello. I’m Mia.\nShen: Hi. I’m Shen.\nMia: Nice to meet you.\nShen: Nice to meet you, too.',
readingQuestion: '谁叫 Mia?',
readingAnswer: '第一位说话的人',
'Mia: Hello. I’m Mia. What’s your name?\nShen: Hi. I’m Shen.\nMia: Nice to meet you.\nShen: Nice to meet you, too.',
readingQuestion: 'Shen 说的最后一句是什么?',
readingAnswer: 'Nice to meet you, too.',
readingOptions: [
'Nice to meet you, too.',
'What’s your name?',
'Hi. I’m Shen.',
],
writingPrompt: '写一句问候和姓名介绍。',
writingExample: 'Hello. I’m Shen.',
independentPrompt: '不看句框,介绍你的名字并回应“Nice to meet you”。',
@@ -572,9 +606,10 @@ const a0Activities = <String, LessonActivity>{
answers: ['你的名字怎么拼?', '你的名字是什么?', '你来自哪里?'],
speaking: 'S H E N',
reading:
'Mia: What’s your name?\nShen: Shen.\nMia: How do you spell that?\nShen: S-H-E-N.',
readingQuestion: 'Shen 的名字怎么拼?',
'Mia: I’m Mia. M-I-A.\nShen: Hi, Mia. I’m Shen. S-H-E-N.\nMia: How do you spell Sam?\nShen: S-A-M.',
readingQuestion: 'Shen 自己的名字怎么拼写?',
readingAnswer: 'S-H-E-N',
readingOptions: ['S-H-E-N', 'M-I-A', 'S-A-M'],
writingPrompt: '把名字和拼写写完整。',
writingExample: 'My name is Shen.\nS-H-E-N.',
independentPrompt: '不看句框,用英文介绍名字并拼读它。',
@@ -586,9 +621,10 @@ const a0Activities = <String, LessonActivity>{
answers: ['你的状态', '你的号码', '你的地点'],
speaking: 'How are you? I’m good, thanks.',
reading:
'Mia: Hi, Shen. How are you?\nShen: I’m good, thanks. How are you?\nMia: I’m okay.',
readingQuestion: 'Mia 状态如何?',
readingAnswer: 'okay',
'Mia: Hi, Shen. How are you?\nShen: I’m good, thanks. How are you?\nMia: I’m tired today.',
readingQuestion: 'Mia 说自己今天怎么样?',
readingAnswer: 'I’m tired today.',
readingOptions: ['I’m tired today.', 'I’m good, thanks.', 'I’m okay.'],
writingPrompt: '写一句今天的状态。',
writingExample: 'I’m okay today.',
independentPrompt: '不看句框,问候并说出你的状态。',
@@ -600,9 +636,10 @@ const a0Activities = <String, LessonActivity>{
answers: ['138', '183', '318'],
speaking: 'My number is one-three-eight.',
reading:
'Mia: What’s your phone number?\nShen: One-three-eight.\nMia: Thank you.',
readingQuestion: 'Shen 的号码是?',
'Mia: What’s your phone number?\nShen: My number is one-three-eight.\nMia: One-eight-three?\nShen: No. One-three-eight.',
readingQuestion: 'Shen 的号码到底是哪一个?',
readingAnswer: '138',
readingOptions: ['138', '183', '318'],
writingPrompt: '把虚拟号码 139 写成英文。',
writingExample: 'one-three-nine',
independentPrompt: '不看句框,说出一个三位虚拟号码。',
@@ -613,9 +650,10 @@ const a0Activities = <String, LessonActivity>{
listeningQuestion: '这是什么?',
answers: ['钥匙', '书', '水'],
speaking: 'What’s this? It’s a pen.',
reading: 'Mia: What’s this?\nShen: It’s a book.',
readingQuestion: '这是什么?',
readingAnswer: 'a book',
reading: 'Mia: What’s this? Is it a pen?\nShen: No. It’s a book.',
readingQuestion: 'Shen 说那件东西是什么?',
readingAnswer: 'A book',
readingOptions: ['A book', 'A pen', 'A phone'],
writingPrompt: '写一句“这是一支笔”。',
writingExample: 'It’s a pen.',
independentPrompt: '不看句框,说出一个身边物品。',
@@ -626,9 +664,11 @@ const a0Activities = <String, LessonActivity>{
listeningQuestion: '对方在问什么?',
answers: ['来自哪里', '叫什么', '喜欢什么'],
speaking: 'I’m from Hong Kong.',
reading: 'Mia: Where are you from?\nShen: I’m from Hong Kong.',
reading:
'Mia: I’m from London. Where are you from?\nShen: I’m from Hong Kong.',
readingQuestion: 'Shen 来自哪里?',
readingAnswer: 'Hong Kong',
readingOptions: ['Hong Kong', 'London', 'Beijing'],
writingPrompt: '写一句你来自哪里。',
writingExample: 'I’m from Hong Kong.',
independentPrompt: '不看句框,说来自哪里并反问对方。',
@@ -639,9 +679,11 @@ const a0Activities = <String, LessonActivity>{
listeningQuestion: '这个人是谁?',
answers: ['妈妈', '朋友', '老师'],
speaking: 'This is my family.',
reading: 'Mia: Who is this?\nShen: This is my mother.',
readingQuestion: 'Shen 在介绍谁?',
readingAnswer: 'my mother',
reading:
'Mia: Who is this? Is this your father?\nShen: No. This is my mother.',
readingQuestion: 'Shen 介绍的是哪一位家人?',
readingAnswer: 'My mother',
readingOptions: ['My mother', 'My father', 'My sister'],
writingPrompt: '介绍一位家人或朋友。',
writingExample: 'This is my mother.',
independentPrompt: '不看句框,介绍一位家人或朋友。',
@@ -652,9 +694,11 @@ const a0Activities = <String, LessonActivity>{
listeningQuestion: '是什么时间?',
answers: ['星期一三点', '星期三一点', '星期一一点'],
speaking: 'It’s three o’clock.',
reading: 'Mia: What day is it?\nShen: It’s Monday.',
readingQuestion: '今天星期几?',
reading:
'Mia: What day is it today? Is it Tuesday?\nShen: No. It’s Monday.',
readingQuestion: '对话中今天到底是星期几?',
readingAnswer: 'Monday',
readingOptions: ['Monday', 'Tuesday', 'Sunday'],
writingPrompt: '写一句今天和整点。',
writingExample: 'It’s Monday. It’s three o’clock.',
independentPrompt: '不看句框,说今天星期几或现在几点。',
@@ -665,9 +709,11 @@ const a0Activities = <String, LessonActivity>{
listeningQuestion: '对方喜欢茶吗?',
answers: ['喜欢', '不喜欢', '不知道'],
speaking: 'I like tea. Do you like tea?',
reading: 'Mia: I like tea. Do you like tea?\nShen: Yes, I do.',
readingQuestion: 'Shen 的回答是什么?',
readingAnswer: 'Yes, I do.',
reading:
'Mia: I like tea. Do you like tea?\nShen: No, I don’t. I like coffee.',
readingQuestion: 'Shen 喜欢喝什么?',
readingAnswer: 'Coffee',
readingOptions: ['Coffee', 'Tea', 'Music'],
writingPrompt: '写一句你的喜好。',
writingExample: 'I like tea.',
independentPrompt: '不看句框,说一个喜好并反问。',
@@ -678,9 +724,11 @@ const a0Activities = <String, LessonActivity>{
listeningQuestion: '对方希望什么?',
answers: ['再说一遍', '说慢一点', '写下来'],
speaking: 'Please speak slowly.',
reading: 'Mia: Please say that again.\nShen: Please speak slowly.',
readingQuestion: 'Shen 希望什么?',
readingAnswer: 'speak slowly',
reading:
'Alex: Hi! I’m Alex. What’s your name?\nShen: Hi! I’m Shen. Nice to meet you.\nAlex: Nice to meet you, too. Where are you from?\nShen: I’m from Hong Kong. Where are you from?\nAlex: I’m from London. Do you like coffee?\nShen: No, I don’t. I like tea.',
readingQuestion: 'Shen 来自哪里、喜欢喝什么?',
readingAnswer: 'Hong Kong, tea',
readingOptions: ['Hong Kong, tea', 'London, coffee', 'Hong Kong, coffee'],
writingPrompt: '写姓名、地点和喜好三句。',
writingExample: 'I’m Shen.\nI’m from Hong Kong.\nI like tea.',
independentPrompt: '不看帮助,完成姓名、地点、喜好和反问。',
@@ -699,8 +747,9 @@ const a0SegmentActivities = <String, LessonActivity>{
answers: ['123', '132', '213'],
speaking: 'Zero, one, two, three.',
reading: 'Mia: One, two, three.\nShen: Four, five.',
readingQuestion: 'Shen 说了哪两个数字?',
readingAnswer: 'four, five',
readingQuestion: 'Shen 接着说了哪两个数字?',
readingAnswer: 'Four, five',
readingOptions: ['Four, five', 'One, two', 'Three, four'],
writingPrompt: '把 0、1、2、3 写成英文。',
writingExample: 'zero, one, two, three',
independentPrompt: '不看帮助,说出三个 0 到 5 的数字。',
@@ -711,9 +760,10 @@ const a0SegmentActivities = <String, LessonActivity>{
listeningQuestion: '听到哪一组数字?',
answers: ['678', '687', '768'],
speaking: 'My phone is here.',
reading: 'Mia: What is this?\nShen: It is a phone.',
readingQuestion: '这是什么?',
readingAnswer: 'a phone',
reading: 'Mia: Six, seven, eight.\nShen: Nine, ten.',
readingQuestion: 'Shen 最后说了哪两个数字?',
readingAnswer: 'Nine, ten',
readingOptions: ['Nine, ten', 'Six, seven', 'Seven, eight'],
writingPrompt: '把 6、7、8 写成英文。',
writingExample: 'six, seven, eight',
independentPrompt: '不看帮助,说出三个 6 到 10 的数字。',
@@ -724,9 +774,11 @@ const a0SegmentActivities = <String, LessonActivity>{
listeningQuestion: '号码是多少?',
answers: ['138', '183', '318'],
speaking: 'My number is one-three-eight.',
reading: 'Mia: What’s your phone number?\nShen: One-three-eight.',
readingQuestion: 'Shen 的号码是?',
readingAnswer: '138',
reading:
'Mia: What’s your phone number?\nShen: My number is five-zero-two.\nMia: Five-two-zero?\nShen: No. Five-zero-two.',
readingQuestion: 'Shen 最后确认的号码是哪一个?',
readingAnswer: '502',
readingOptions: ['502', '520', '205'],
writingPrompt: '把虚拟号码 139 写成英文。',
writingExample: 'one-three-nine',
independentPrompt: '不看句框,说出一个三位虚拟号码。',
@@ -737,9 +789,11 @@ const a0SegmentActivities = <String, LessonActivity>{
listeningQuestion: '今天星期几?',
answers: ['星期一', '星期二', '星期三'],
speaking: 'It is Monday.',
reading: 'Mia: What day is it?\nShen: It is Tuesday.',
readingQuestion: '今天星期几?',
reading:
'Mia: What day is it today? Is it Monday?\nShen: No. It is Tuesday.',
readingQuestion: 'Mia 猜错了,今天其实是星期几?',
readingAnswer: 'Tuesday',
readingOptions: ['Tuesday', 'Monday', 'Wednesday'],
writingPrompt: '写一句今天是星期一、二或三。',
writingExample: 'It is Monday.',
independentPrompt: '不看帮助,说出星期一、二或三。',
@@ -750,9 +804,10 @@ const a0SegmentActivities = <String, LessonActivity>{
listeningQuestion: '今天星期几?',
answers: ['星期五', '星期四', '星期日'],
speaking: 'It is Friday.',
reading: 'Mia: What day is it?\nShen: It is Sunday.',
readingQuestion: '今天星期几?',
reading: 'Mia: Is it Saturday today?\nShen: No. It is Sunday.',
readingQuestion: 'Shen 说今天是哪一天?',
readingAnswer: 'Sunday',
readingOptions: ['Sunday', 'Saturday', 'Friday'],
writingPrompt: '写一句今天是星期四到日。',
writingExample: 'It is Friday.',
independentPrompt: '不看帮助,说出星期四到日。',
@@ -763,9 +818,11 @@ const a0SegmentActivities = <String, LessonActivity>{
listeningQuestion: '是什么时间?',
answers: ['三点', '一点', '星期三'],
speaking: 'It is three o’clock.',
reading: 'Mia: What time is it?\nShen: It is three o’clock.',
readingQuestion: '几点?',
readingAnswer: 'three o’clock',
reading:
'Mia: What time is it? Is it two o’clock?\nShen: No. It is three o’clock.',
readingQuestion: 'Shen 说现在是几点?',
readingAnswer: 'Three o’clock',
readingOptions: ['Three o’clock', 'Two o’clock', 'One o’clock'],
writingPrompt: '写一句整点时间。',
writingExample: 'It is three o’clock.',
independentPrompt: '不看帮助,说一个整点时间。',
@@ -1072,13 +1129,116 @@ class LessonDialogue {
required this.prompts,
required this.hints,
this.translations = const [],
this.requiredTerms = const [],
this.taskLabels = const [],
});
final String goal;
final List<String> prompts;
final List<String> hints;
final List<String> translations;
/// One accepted term group per learner turn. The learner only needs to hit
/// one term in the current group, so natural wording still passes. A term
/// may be a plain word or phrase, an `a + b` conjunction (both parts are
/// required), or a structural token starting with `#`.
final List<List<String>> requiredTerms;
/// Chinese label of what each learner turn has to do. It drives the
/// "还没完成本轮任务" message and the summary of completed tasks, so it must
/// describe the task without giving away the model answer.
final List<String> taskLabels;
}
/// Matches whole words only: the old `text.contains('it')` accepted almost any
/// sentence, including ones that never performed the task.
bool _containsTerm(String text, String term) {
if (term.contains(' + ')) {
return term.split(' + ').every((part) => _containsTerm(text, part.trim()));
}
if (term.startsWith('#')) return _matchesStructure(text, term);
final escaped = RegExp.escape(term.toLowerCase());
return RegExp('(?<![a-z])$escaped(?![a-z])').hasMatch(text);
}
bool _matchesStructure(String text, String token) => switch (token) {
// Three or more letters said one by one, e.g. "S-H-E-N" or "s h e n".
'#spelling' => RegExp(r'(?:^|[^a-z])[a-z](?:[ -][a-z]){2,}').hasMatch(text),
// Any spoken or written digit.
'#digit' => RegExp(
r'(?<![a-z])(zero|one|two|three|four|five|six|seven|eight|nine|ten)(?![a-z])|[0-9]',
).hasMatch(text),
// A real word after the frame, so "I'm" alone is not a name.
'#word' => RegExp(r'[a-z]{2,}').hasMatch(text),
// Any question addressed to the partner.
'#question' =>
text.contains('?') ||
RegExp(
r"(?<![a-z])(what|where|who|when|how|why|do you|are you|can you)(?![a-z])",
).hasMatch(text),
_ => false,
};
String _normalizeDialogueInput(String response) =>
response.toLowerCase().replaceAll('\u2019', "'").replaceAll('\u2018', "'");
/// Whole-lesson and free-scene dialogues validate the same way segment
/// dialogues do: the turn has to contain the language the turn is teaching.
bool matchesDialogueStage(LessonDialogue script, int stage, String response) {
final text = _normalizeDialogueInput(response);
if (stage < 0 || stage >= script.requiredTerms.length) {
// No declared requirement: accept anything that is actually a word.
return RegExp(r'[a-z]{2,}').hasMatch(text);
}
final terms = script.requiredTerms[stage];
if (terms.isEmpty) return RegExp(r'[a-z]{2,}').hasMatch(text);
return terms.any((term) => _containsTerm(text, term));
}
String dialogueTaskLabel(LessonDialogue script, int stage) =>
stage >= 0 && stage < script.taskLabels.length
? script.taskLabels[stage]
: '完成本轮任务';
/// The free "初次见面" scene. It lives next to the lesson dialogues so its
/// tasks are validated and summarised by the same rules.
const a0MeetDialogue = LessonDialogue(
goal: '姓名、地点、状态或喜好,并反问',
prompts: [
'Hi! My name is Mia. What\u2019s your name?',
'Nice to meet you. Where are you from?',
'Great! How are you today? Or what do you like?',
'I\u2019m good, thanks. Now ask me one question!',
],
hints: [
'My name is Alex.',
'I\u2019m from Hong Kong.',
'I\u2019m good, thanks. / I like coffee.',
'What\u2019s your name? / Where are you from?',
],
translations: [
'嗨!我叫 Mia。你叫什么名字?',
'很高兴认识你。你来自哪里?',
'很好!你今天怎么样?或者你喜欢什么?',
'我很好,谢谢。现在请问我一个问题!',
],
requiredTerms: [
["i'm + #word", 'i am + #word', 'my name is', "my name's", 'name is'],
["i'm from", 'i am from', 'from + #word'],
[
'good',
'okay',
'ok',
'fine',
'great',
'tired',
'i like',
'i love',
],
['#question'],
],
taskLabels: ['介绍姓名', '说明来自哪里', '表达状态或喜好', '反问对方'],
);
const a0Dialogues = <String, LessonDialogue>{
'a0-01': LessonDialogue(
goal: '问候、介绍姓名并回应见面问候',
@@ -1100,6 +1260,13 @@ const a0Dialogues = <String, LessonDialogue>{
'太棒了!再打一次招呼吧。',
'现在请问我的名字!',
],
requiredTerms: [
["i'm + #word", 'i am + #word', 'my name is', "my name's", 'name is'],
['nice to meet you'],
['hello', 'hi', 'hey'],
["what's your name", 'what is your name', 'your name', '#question'],
],
taskLabels: ['说出你的名字', '回应 Nice to meet you', '再打一次招呼', '反问对方的名字'],
),
'a0-02': LessonDialogue(
goal: '介绍姓名并完整拼读名字',
@@ -1110,12 +1277,14 @@ const a0Dialogues = <String, LessonDialogue>{
'Now ask me my name!',
],
hints: ['My name is Alex.', 'A-L-E-X.', 'A-L-E-X.', 'What’s your name?'],
translations: [
'嗨!你叫什么名字?',
'那个怎么拼写?',
'谢谢。请再拼写一次。',
'现在请问我的名字!',
translations: ['嗨!你叫什么名字?', '那个怎么拼写?', '谢谢。请再拼写一次。', '现在请问我的名字!'],
requiredTerms: [
["i'm + #word", 'i am + #word', 'my name is', "my name's", 'name is'],
['#spelling'],
['#spelling'],
["what's your name", 'what is your name', 'your name', '#question'],
],
taskLabels: ['说出你的名字', '拼读你的名字', '再拼读一次', '反问对方的名字'],
),
'a0-03': LessonDialogue(
goal: '询问状态、回答并反问',
@@ -1132,6 +1301,13 @@ const a0Dialogues = <String, LessonDialogue>{
'我还好。请再说一次你的状态。',
'现在请说再见!',
],
requiredTerms: [
['good', 'okay', 'ok', 'fine', 'great', 'tired'],
['how are you', '#question'],
['good', 'okay', 'ok', 'fine', 'great', 'tired'],
['bye', 'goodbye', 'see you'],
],
taskLabels: ['说出你今天的状态', '反问对方的状态', '再说一次你的状态', '说再见'],
),
'a0-04': LessonDialogue(
goal: '报告一个虚拟三位号码并确认',
@@ -1153,6 +1329,13 @@ const a0Dialogues = <String, LessonDialogue>{
'请再说一遍这三个数字。',
'现在请问我的电话号码!',
],
requiredTerms: [
['my number is + #digit', "my number's + #digit", 'number is + #digit'],
['yes', 'no', "that's right", 'right', 'correct'],
['#digit'],
["what's your phone number", 'what is your phone number', 'your phone number', 'your number'],
],
taskLabels: ['报出一个三位号码', '确认或纠正听到的号码', '再说一次这三个数字', '反问对方的号码'],
),
'a0-05': LessonDialogue(
goal: '询问并说出一个物品',
@@ -1169,6 +1352,13 @@ const a0Dialogues = <String, LessonDialogue>{
'请问我:What’s this(这是什么)?',
'太棒了!再说一个物品吧。',
],
requiredTerms: [
["it's a", 'it is a', "it's an", 'it is an'],
["it's a", 'it is a', "it's an", 'it is an'],
["what's this", 'what is this'],
["it's a", 'it is a', "it's an", 'it is an'],
],
taskLabels: ['说出这个物品', '完整说出 It’s a … 句型', '反问 What’s this', '再说一个物品'],
),
'a0-06': LessonDialogue(
goal: '说来自哪里并反问',
@@ -1190,6 +1380,13 @@ const a0Dialogues = <String, LessonDialogue>{
'请再说一次你来自哪里。',
'请说再见!',
],
requiredTerms: [
["i'm from", 'i am from', 'from + #word'],
['where are you from', "where're you from", 'where are you'],
["i'm from", 'i am from', 'from + #word'],
['bye', 'goodbye', 'see you'],
],
taskLabels: ['说出你来自哪里', '反问对方来自哪里', '再说一次你来自哪里', '说再见'],
),
'a0-07': LessonDialogue(
goal: '介绍一位家人或朋友',
@@ -1211,6 +1408,13 @@ const a0Dialogues = <String, LessonDialogue>{
'请问我:Who is this(这是谁)?',
'请再介绍一个人。',
],
requiredTerms: [
['this is my', 'this is'],
['this is my', 'this is'],
['who is this', "who's this"],
['this is my', 'this is'],
],
taskLabels: ['介绍一位家人或朋友', '完整说出 This is my … 句型', '反问 Who is this', '再介绍一个人'],
),
'a0-08': LessonDialogue(
goal: '说明星期或整点',
@@ -1226,12 +1430,14 @@ const a0Dialogues = <String, LessonDialogue>{
'It’s three o’clock.',
'What day is it?',
],
translations: [
'今天星期几?',
'现在几点了?',
'请说一个完整的时间句子。',
'现在请问我今天是星期几!',
translations: ['今天星期几?', '现在几点了?', '请说一个完整的时间句子。', '现在请问我今天是星期几!'],
requiredTerms: [
['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
["o'clock", 'oclock', "#digit + o'clock"],
["it's + o'clock", "it is + o'clock", "it's + #digit", 'it is + #digit'],
['what day is it', 'what day', '#question'],
],
taskLabels: ['说出今天星期几', '说出现在几点', '说一个完整的时间句子', '反问今天星期几'],
),
'a0-09': LessonDialogue(
goal: '表达喜好、回答和反问',
@@ -1242,12 +1448,14 @@ const a0Dialogues = <String, LessonDialogue>{
'Say your like one more time.',
],
hints: ['I like tea.', 'Yes, I do.', 'Do you like tea?', 'I like tea.'],
translations: [
'你喜欢什么?',
'你喜欢茶吗?',
'现在请问我喜欢什么。',
'请再说一次你的喜好。',
translations: ['你喜欢什么?', '你喜欢茶吗?', '现在请问我喜欢什么。', '请再说一次你的喜好。'],
requiredTerms: [
['i like', 'i love'],
['yes', 'no', 'i do', "i don't", 'i do not'],
['do you like', '#question'],
['i like', 'i love'],
],
taskLabels: ['说出你喜欢什么', '回答是否喜欢', '反问对方的喜好', '再说一次你的喜好'],
),
'a0-10': LessonDialogue(
goal: '请求重复或放慢语速,并完成基础沟通',
@@ -1269,6 +1477,13 @@ const a0Dialogues = <String, LessonDialogue>{
'现在请问我的名字或我来自哪里。',
'请说一件你喜欢的事物。',
],
requiredTerms: [
['say that again', 'again', 'pardon'],
['slowly', 'slow'],
["what's your name", 'what is your name', 'where are you from', 'your name', '#question'],
['i like', 'i love'],
],
taskLabels: ['请求对方重复', '请求对方放慢语速', '反问名字或来自哪里', '说出一件你喜欢的事物'],
),
};
@@ -1277,37 +1492,52 @@ const a0SegmentDialogues = <String, LessonDialogue>{
goal: '听辨并说出 0 到 5',
prompts: ['Say zero, one, two.', 'Now say three, four, five.'],
hints: ['zero, one, two', 'three, four, five'],
translations: ['请说 zero, one, two(0,1,2)。', '现在请说 three, four, five(3,4,5)。'],
translations: [
'请说 zero, one, two(0,1,2)。',
'现在请说 three, four, five(3,4,5)。',
],
taskLabels: ['说出 zero, one, two', '说出 three, four, five'],
),
'a0-04-b': LessonDialogue(
goal: '听辨 6 到 10 并认识 phone',
prompts: ['Say six, seven, eight.', 'What is this? Say: It is a phone.'],
hints: ['six, seven, eight', 'It is a phone.'],
translations: ['请说 six, seven, eight(6,7,8)。', '这是什么?请说:It is a phone(这是一部手机)。'],
translations: [
'请说 six, seven, eight(6,7,8)。',
'这是什么?请说:It is a phone(这是一部手机)。',
],
taskLabels: ['说出 six, seven, eight', '说出 It is a phone.'],
),
'a0-04-c': LessonDialogue(
goal: '询问并报告三位号码',
prompts: ['What is your phone number?', 'Say a three-digit number again.'],
hints: ['My number is one-three-eight.', 'one-three-eight'],
translations: ['你的电话号码是多少?', '请再说一次三位数字。'],
taskLabels: ['报出你的三位号码', '再说一次三位数字'],
),
'a0-08-a': LessonDialogue(
goal: '询问并说出星期一到三',
prompts: ['What day is it?', 'Say Monday, Tuesday, or Wednesday.'],
hints: ['What day is it?', 'It is Monday.'],
translations: ['今天星期几?', '请说 Monday, Tuesday, 或 Wednesday(周一、周二或周三)。'],
taskLabels: ['问今天星期几', '说出周一到周三中的一天'],
),
'a0-08-b': LessonDialogue(
goal: '说出星期四到日',
prompts: ['What day is it?', 'Say Thursday, Friday, Saturday, or Sunday.'],
hints: ['What day is it?', 'It is Friday.'],
translations: ['今天星期几?', '请说 Thursday, Friday, Saturday, 或 Sunday(周四、周五、周六或周日)。'],
translations: [
'今天星期几?',
'请说 Thursday, Friday, Saturday, 或 Sunday(周四、周五、周六或周日)。',
],
taskLabels: ['问今天星期几', '说出周四到周日中的一天'],
),
'a0-08-c': LessonDialogue(
goal: '询问并说出整点',
prompts: ['What time is it?', 'Say one full time sentence.'],
hints: ['What time is it?', 'It is three o’clock.'],
translations: ['现在几点了?', '请说一个完整的时间句子。'],
taskLabels: ['问现在几点', '说一个完整的时间句子'],
),
};
@@ -1315,3 +1545,21 @@ LessonDialogue dialogueByLessonId(String id) => a0Dialogues[id]!;
LessonDialogue dialogueBySegmentId(String segmentId, String lessonId) =>
a0SegmentDialogues[segmentId] ?? dialogueByLessonId(lessonId);
/// The English a learner has actually met up to and including [lessonId].
/// The AI partner is told to stay inside this list so a beginner never gets an
/// answer built from words the course has not taught yet.
List<String> taughtLanguageUpTo(String lessonId) {
final result = <String>[];
for (final entry in a0TargetItemIdsByLesson.entries) {
for (final id in entry.value) {
final word = a0CoreItems[id];
if (word != null) result.add(word);
}
if (entry.key == lessonId) break;
}
return result;
}
/// Everything A0 teaches, for the free scene that mixes all topics.
List<String> get allTaughtLanguage => a0CoreItems.values.toList();