阅读"在对话里找到答案": - 16 道题全部重写,干扰项真实出现在对话里,靠说话人归属或否定句才能作答 - 选项按题目内容确定性打乱,答案不再固定排第一;听力环节同样处理 - 去掉超纲干扰项、重复题干,收紧自由作答匹配(过去单个字母也能判对) AI 情境对话: - 提示词区分"AI 这一句要做什么"与"学习者随后要完成什么",并下发已教词句清单 - JSON 只强制 reply,translation/feedback 可选;不再索要用不上的 slots/evidence - AI 不可用时页面明确提示当前回复来自内置示范脚本 - 删掉按 stage 下标猜中文翻译的兜底,避免译文与英文对不上 - 整课对话改用逐轮必需表达校验,替换"关键词沾边就算过";修正自由场景正则误伤 - 总结的"完成任务"按实际通过的轮次生成;模型点评只在结束页呈现一次 - 自由场景支持草稿续练(独立存储槽);修正回答轮数文案与永不解锁的场景标注 同时提交此前工作区中累积的改动:SenseVoice 本地识别、查词/句型解析卡、 复习与测评页调整等,并补充对话校验、选项分布和句子解析的测试。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1566 lines
51 KiB
Dart
1566 lines
51 KiB
Dart
import 'a0_core.dart';
|
||
import 'models.dart';
|
||
|
||
/// Stable A0 core identities taught by each seed lesson. Wording can vary in
|
||
/// future exercises without creating a different mastery item.
|
||
const a0TargetItemIdsByLesson = <String, List<String>>{
|
||
'a0-01': ['A0-W36', 'A0-W37', 'A0-W40', 'A0-P01', 'A0-P02', 'A0-P03'],
|
||
'a0-02': ['A0-P04'],
|
||
'a0-03': ['A0-W33', 'A0-W34', 'A0-W35', 'A0-P05', 'A0-P06'],
|
||
'a0-04': [
|
||
'A0-W01',
|
||
'A0-W02',
|
||
'A0-W03',
|
||
'A0-W04',
|
||
'A0-W05',
|
||
'A0-W06',
|
||
'A0-W07',
|
||
'A0-W08',
|
||
'A0-W09',
|
||
'A0-W10',
|
||
'A0-W11',
|
||
'A0-W20',
|
||
'A0-P07',
|
||
'A0-P08',
|
||
],
|
||
'a0-05': ['A0-W19', 'A0-W21', 'A0-W22', 'A0-W23', 'A0-P09', 'A0-P10'],
|
||
'a0-06': ['A0-P11', 'A0-P12'],
|
||
'a0-07': ['A0-W25', 'A0-W26', 'A0-W27', 'A0-W28', 'A0-P13'],
|
||
'a0-08': [
|
||
'A0-W12',
|
||
'A0-W13',
|
||
'A0-W14',
|
||
'A0-W15',
|
||
'A0-W16',
|
||
'A0-W17',
|
||
'A0-W18',
|
||
'A0-P14',
|
||
'A0-P15',
|
||
'A0-P16',
|
||
'A0-P17',
|
||
],
|
||
'a0-09': [
|
||
'A0-W29',
|
||
'A0-W30',
|
||
'A0-W31',
|
||
'A0-W32',
|
||
'A0-W38',
|
||
'A0-W39',
|
||
'A0-P18',
|
||
'A0-P19',
|
||
],
|
||
'a0-10': ['A0-W24', 'A0-P20'],
|
||
};
|
||
|
||
/// A topic may contain multiple independently resumable teaching segments.
|
||
/// Segment IDs are stable even if later wording or exercises change.
|
||
class LessonSegment {
|
||
const LessonSegment({
|
||
required this.id,
|
||
required this.targetItemIds,
|
||
required this.previewItemIds,
|
||
this.dialogueRequiredTerms = const [],
|
||
this.independentRequiredTerms = const [],
|
||
this.estimatedMinutes = 12,
|
||
});
|
||
|
||
final String id;
|
||
final List<String> targetItemIds;
|
||
final List<String> previewItemIds;
|
||
|
||
/// One accepted term group per controlled dialogue turn. A learner only
|
||
/// needs to use one term from the current group; the surrounding wording
|
||
/// may vary naturally.
|
||
final List<List<String>> dialogueRequiredTerms;
|
||
|
||
/// Terms that show the independent attempt contains the taught content.
|
||
final List<String> independentRequiredTerms;
|
||
final int estimatedMinutes;
|
||
}
|
||
|
||
const a0LessonSegments = <String, List<LessonSegment>>{
|
||
'a0-04': [
|
||
LessonSegment(
|
||
id: 'a0-04-a',
|
||
targetItemIds: [
|
||
'A0-W01',
|
||
'A0-W02',
|
||
'A0-W03',
|
||
'A0-W04',
|
||
'A0-W05',
|
||
'A0-W06',
|
||
],
|
||
previewItemIds: [
|
||
'A0-W01',
|
||
'A0-W02',
|
||
'A0-W03',
|
||
'A0-W04',
|
||
'A0-W05',
|
||
'A0-W06',
|
||
],
|
||
dialogueRequiredTerms: [
|
||
['zero', 'one', 'two'],
|
||
['three', 'four', 'five'],
|
||
],
|
||
independentRequiredTerms: ['zero', 'one', 'two', 'three', 'four', 'five'],
|
||
),
|
||
LessonSegment(
|
||
id: 'a0-04-b',
|
||
targetItemIds: [
|
||
'A0-W07',
|
||
'A0-W08',
|
||
'A0-W09',
|
||
'A0-W10',
|
||
'A0-W11',
|
||
'A0-W20',
|
||
],
|
||
previewItemIds: [
|
||
'A0-W07',
|
||
'A0-W08',
|
||
'A0-W09',
|
||
'A0-W10',
|
||
'A0-W11',
|
||
'A0-W20',
|
||
],
|
||
dialogueRequiredTerms: [
|
||
['six', 'seven', 'eight'],
|
||
['it is a phone', "it's a phone"],
|
||
],
|
||
independentRequiredTerms: ['six', 'seven', 'eight', 'nine', 'ten'],
|
||
),
|
||
LessonSegment(
|
||
id: 'a0-04-c',
|
||
targetItemIds: ['A0-P07', 'A0-P08'],
|
||
previewItemIds: ['A0-P07', 'A0-P08'],
|
||
dialogueRequiredTerms: [
|
||
[
|
||
'my number is',
|
||
'one',
|
||
'two',
|
||
'three',
|
||
'four',
|
||
'five',
|
||
'six',
|
||
'seven',
|
||
'eight',
|
||
'nine',
|
||
'zero',
|
||
],
|
||
[
|
||
'one',
|
||
'two',
|
||
'three',
|
||
'four',
|
||
'five',
|
||
'six',
|
||
'seven',
|
||
'eight',
|
||
'nine',
|
||
'zero',
|
||
],
|
||
],
|
||
independentRequiredTerms: [
|
||
'one',
|
||
'two',
|
||
'three',
|
||
'four',
|
||
'five',
|
||
'six',
|
||
'seven',
|
||
'eight',
|
||
'nine',
|
||
'zero',
|
||
],
|
||
),
|
||
],
|
||
'a0-08': [
|
||
LessonSegment(
|
||
id: 'a0-08-a',
|
||
targetItemIds: ['A0-W12', 'A0-W13', 'A0-W14', 'A0-P14', 'A0-P15'],
|
||
previewItemIds: ['A0-W12', 'A0-W13', 'A0-W14', 'A0-P14', 'A0-P15'],
|
||
dialogueRequiredTerms: [
|
||
['what day'],
|
||
['monday', 'tuesday', 'wednesday'],
|
||
],
|
||
independentRequiredTerms: ['monday', 'tuesday', 'wednesday'],
|
||
),
|
||
LessonSegment(
|
||
id: 'a0-08-b',
|
||
targetItemIds: ['A0-W15', 'A0-W16', 'A0-W17', 'A0-W18'],
|
||
previewItemIds: ['A0-W15', 'A0-W16', 'A0-W17', 'A0-W18'],
|
||
dialogueRequiredTerms: [
|
||
['what day'],
|
||
['thursday', 'friday', 'saturday', 'sunday'],
|
||
],
|
||
independentRequiredTerms: ['thursday', 'friday', 'saturday', 'sunday'],
|
||
),
|
||
LessonSegment(
|
||
id: 'a0-08-c',
|
||
targetItemIds: ['A0-P16', 'A0-P17'],
|
||
previewItemIds: ['A0-P16', 'A0-P17'],
|
||
dialogueRequiredTerms: [
|
||
['what time'],
|
||
["o'clock", 'oclock'],
|
||
],
|
||
independentRequiredTerms: ["o'clock", 'oclock'],
|
||
),
|
||
],
|
||
};
|
||
|
||
class SeedLesson {
|
||
const SeedLesson({
|
||
required this.id,
|
||
required this.number,
|
||
required this.title,
|
||
required this.outcome,
|
||
required this.vocabulary,
|
||
this.schemaVersion = '1.0',
|
||
this.revision = 1,
|
||
this.stageVersion = 'A0-1.0',
|
||
this.source = ContentSource.builtInOriginal,
|
||
this.status = ContentStatus.approved,
|
||
this.estimatedMinutes = 12,
|
||
});
|
||
final String id;
|
||
final int number;
|
||
final String title;
|
||
final String outcome;
|
||
final List<VocabularyItem> vocabulary;
|
||
final String schemaVersion;
|
||
final int revision;
|
||
final String stageVersion;
|
||
final ContentSource source;
|
||
final ContentStatus status;
|
||
final int estimatedMinutes;
|
||
|
||
List<String> get targetItemIds =>
|
||
a0TargetItemIdsByLesson[id] ?? const <String>[];
|
||
List<String> get abilityIds => ['A0-C${number.toString().padLeft(2, '0')}'];
|
||
List<String> get prerequisiteIds => number == 1
|
||
? const []
|
||
: ['a0-${(number - 1).toString().padLeft(2, '0')}'];
|
||
List<LessonSegment> get segments =>
|
||
a0LessonSegments[id] ??
|
||
[
|
||
LessonSegment(
|
||
id: '$id-a',
|
||
targetItemIds: targetItemIds,
|
||
previewItemIds: targetItemIds,
|
||
estimatedMinutes: estimatedMinutes,
|
||
),
|
||
];
|
||
}
|
||
|
||
const a0SeedLessons = [
|
||
SeedLesson(
|
||
id: 'a0-01',
|
||
number: 1,
|
||
title: '你好,我是……',
|
||
outcome: '介绍姓名并回应问候',
|
||
vocabulary: [
|
||
VocabularyItem(
|
||
id: 'a0-01-hello',
|
||
word: 'Hello',
|
||
meaning: '你好',
|
||
example: 'Hello. I’m Mia.',
|
||
exampleMeaning: '你好。我是 Mia。',
|
||
ipa: '/həˈləʊ/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'a0-01-name',
|
||
word: 'name',
|
||
meaning: '名字',
|
||
example: 'My name is Shen.',
|
||
exampleMeaning: '我的名字是 Shen。',
|
||
ipa: '/neɪm/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'a0-01-nice',
|
||
word: 'Nice to meet you',
|
||
meaning: '很高兴认识你',
|
||
example: 'Nice to meet you, too.',
|
||
exampleMeaning: '我也很高兴认识你。',
|
||
),
|
||
],
|
||
),
|
||
SeedLesson(
|
||
id: 'a0-02',
|
||
number: 2,
|
||
title: '拼读我的名字',
|
||
outcome: '听懂拼写,并说出名字字母',
|
||
vocabulary: [
|
||
VocabularyItem(
|
||
id: 'a0-02-spell',
|
||
word: 'spell',
|
||
meaning: '拼写',
|
||
example: 'How do you spell that?',
|
||
exampleMeaning: '那个怎么拼?',
|
||
ipa: '/spel/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'a0-02-name',
|
||
word: 'name',
|
||
meaning: '名字',
|
||
example: 'My name is Shen.',
|
||
exampleMeaning: '我的名字是 Shen。',
|
||
ipa: '/neɪm/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'a0-02-question',
|
||
word: 'How do you spell that?',
|
||
meaning: '那个怎么拼?',
|
||
example: 'How do you spell your name?',
|
||
exampleMeaning: '你的名字怎么拼?',
|
||
),
|
||
],
|
||
),
|
||
SeedLesson(
|
||
id: 'a0-03',
|
||
number: 3,
|
||
title: '你好吗?',
|
||
outcome: '问候并表达简单状态',
|
||
vocabulary: [
|
||
VocabularyItem(
|
||
id: 'a0-03-good',
|
||
word: 'good',
|
||
meaning: '好的',
|
||
example: 'I’m good, thanks.',
|
||
exampleMeaning: '我很好,谢谢。',
|
||
ipa: '/ɡʊd/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'a0-03-tired',
|
||
word: 'tired',
|
||
meaning: '累的',
|
||
example: 'I’m tired today.',
|
||
exampleMeaning: '我今天有点累。',
|
||
ipa: '/ˈtaɪəd/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'a0-03-how',
|
||
word: 'How are you?',
|
||
meaning: '你好吗?',
|
||
example: 'How are you today?',
|
||
exampleMeaning: '你今天怎么样?',
|
||
),
|
||
],
|
||
),
|
||
SeedLesson(
|
||
id: 'a0-04',
|
||
number: 4,
|
||
title: '数字与电话号码',
|
||
outcome: '说 0–10 与虚拟号码',
|
||
vocabulary: [
|
||
VocabularyItem(
|
||
id: 'a0-04-three',
|
||
word: 'three',
|
||
meaning: '三',
|
||
example: 'One, three, eight.',
|
||
exampleMeaning: '一、三、八。',
|
||
ipa: '/θriː/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'a0-04-number',
|
||
word: 'number',
|
||
meaning: '号码',
|
||
example: 'My number is 1-3-8.',
|
||
exampleMeaning: '我的号码是 138。',
|
||
ipa: '/ˈnʌmbə(r)/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'a0-04-phone',
|
||
word: 'phone',
|
||
meaning: '电话',
|
||
example: 'What’s your phone number?',
|
||
exampleMeaning: '你的电话号码是多少?',
|
||
),
|
||
],
|
||
),
|
||
SeedLesson(
|
||
id: 'a0-05',
|
||
number: 5,
|
||
title: '这是什么?',
|
||
outcome: '询问并说出常见物品',
|
||
vocabulary: [
|
||
VocabularyItem(
|
||
id: 'a0-05-book',
|
||
word: 'book',
|
||
meaning: '书',
|
||
example: 'It’s a book.',
|
||
exampleMeaning: '这是一本书。',
|
||
ipa: '/bʊk/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'a0-05-pen',
|
||
word: 'pen',
|
||
meaning: '笔',
|
||
example: 'It’s a pen.',
|
||
exampleMeaning: '这是一支笔。',
|
||
ipa: '/pen/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'a0-05-this',
|
||
word: 'What’s this?',
|
||
meaning: '这是什么?',
|
||
example: 'What’s this? It’s a key.',
|
||
exampleMeaning: '这是什么?这是钥匙。',
|
||
),
|
||
],
|
||
),
|
||
SeedLesson(
|
||
id: 'a0-06',
|
||
number: 6,
|
||
title: '我来自哪里?',
|
||
outcome: '说明来自哪里并反问',
|
||
vocabulary: [
|
||
VocabularyItem(
|
||
id: 'a0-06-from',
|
||
word: 'from',
|
||
meaning: '来自',
|
||
example: 'I’m from Hong Kong.',
|
||
exampleMeaning: '我来自香港。',
|
||
ipa: '/frəm/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'a0-06-place',
|
||
word: 'place',
|
||
meaning: '地点',
|
||
example: 'Where are you from?',
|
||
exampleMeaning: '你来自哪里?',
|
||
),
|
||
],
|
||
),
|
||
SeedLesson(
|
||
id: 'a0-07',
|
||
number: 7,
|
||
title: '我的家人',
|
||
outcome: '介绍一位家人',
|
||
vocabulary: [
|
||
VocabularyItem(
|
||
id: 'a0-07-family',
|
||
word: 'family',
|
||
meaning: '家人',
|
||
example: 'This is my family.',
|
||
exampleMeaning: '这是我的家人。',
|
||
ipa: '/ˈfæməli/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'a0-07-mother',
|
||
word: 'mother',
|
||
meaning: '母亲',
|
||
example: 'This is my mother.',
|
||
exampleMeaning: '这是我的妈妈。',
|
||
),
|
||
],
|
||
),
|
||
SeedLesson(
|
||
id: 'a0-08',
|
||
number: 8,
|
||
title: '星期与时间',
|
||
outcome: '说今天和整点',
|
||
vocabulary: [
|
||
VocabularyItem(
|
||
id: 'a0-08-monday',
|
||
word: 'Monday',
|
||
meaning: '星期一',
|
||
example: 'It is Monday.',
|
||
exampleMeaning: '今天是星期一。',
|
||
),
|
||
VocabularyItem(
|
||
id: 'a0-08-oclock',
|
||
word: 'o’clock',
|
||
meaning: '整点',
|
||
example: 'It’s three o’clock.',
|
||
exampleMeaning: '现在三点整。',
|
||
),
|
||
],
|
||
),
|
||
SeedLesson(
|
||
id: 'a0-09',
|
||
number: 9,
|
||
title: '我喜欢……',
|
||
outcome: '表达喜好并回答',
|
||
vocabulary: [
|
||
VocabularyItem(
|
||
id: 'a0-09-like',
|
||
word: 'like',
|
||
meaning: '喜欢',
|
||
example: 'I like tea.',
|
||
exampleMeaning: '我喜欢茶。',
|
||
ipa: '/laɪk/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'a0-09-tea',
|
||
word: 'tea',
|
||
meaning: '茶',
|
||
example: 'Do you like tea?',
|
||
exampleMeaning: '你喜欢茶吗?',
|
||
),
|
||
],
|
||
),
|
||
SeedLesson(
|
||
id: 'a0-10',
|
||
number: 10,
|
||
title: 'A0 综合任务',
|
||
outcome: '完成自我介绍、喜好和反问',
|
||
vocabulary: [
|
||
VocabularyItem(
|
||
id: 'a0-10-again',
|
||
word: 'Please say that again.',
|
||
meaning: '请再说一遍。',
|
||
example: 'Please say that again.',
|
||
exampleMeaning: '请再说一遍。',
|
||
),
|
||
VocabularyItem(
|
||
id: 'a0-10-slowly',
|
||
word: 'Please speak slowly.',
|
||
meaning: '请说慢一点。',
|
||
example: 'Please speak slowly.',
|
||
exampleMeaning: '请说慢一点。',
|
||
),
|
||
],
|
||
),
|
||
];
|
||
|
||
SeedLesson lessonById(String id) =>
|
||
a0SeedLessons.firstWhere((lesson) => lesson.id == id);
|
||
|
||
class LessonActivity {
|
||
const LessonActivity({
|
||
required this.listening,
|
||
required this.listeningQuestion,
|
||
required this.answers,
|
||
required this.speaking,
|
||
required this.reading,
|
||
required this.readingQuestion,
|
||
required this.readingAnswer,
|
||
this.readingOptions = const [],
|
||
required this.writingPrompt,
|
||
required this.writingExample,
|
||
required this.independentPrompt,
|
||
required this.independentHelp,
|
||
});
|
||
final String listening;
|
||
final String listeningQuestion;
|
||
final List<String> answers;
|
||
final String speaking;
|
||
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.',
|
||
listeningQuestion: 'Mia 在做什么?',
|
||
answers: ['自我介绍', '买东西', '问时间'],
|
||
speaking: 'Hello. I’m Shen. Nice to meet you.',
|
||
reading:
|
||
'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”。',
|
||
independentHelp: 'Hi. I’m … / Nice to meet you, too.',
|
||
),
|
||
'a0-02': LessonActivity(
|
||
listening: 'How do you spell your name?',
|
||
listeningQuestion: '这句话是什么意思?',
|
||
answers: ['你的名字怎么拼?', '你的名字是什么?', '你来自哪里?'],
|
||
speaking: 'S H E N',
|
||
reading:
|
||
'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: '不看句框,用英文介绍名字并拼读它。',
|
||
independentHelp: 'My name is … / A-B-C.',
|
||
),
|
||
'a0-03': LessonActivity(
|
||
listening: 'How are you today?',
|
||
listeningQuestion: '对方在问什么?',
|
||
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 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: '不看句框,问候并说出你的状态。',
|
||
independentHelp: 'How are you? / I’m …',
|
||
),
|
||
'a0-04': LessonActivity(
|
||
listening: 'One, three, eight.',
|
||
listeningQuestion: '你听到的是哪个号码?',
|
||
answers: ['138', '183', '318'],
|
||
speaking: 'My number is one-three-eight.',
|
||
reading:
|
||
'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: '不看句框,说出一个三位虚拟号码。',
|
||
independentHelp: 'My number is one-two-three.',
|
||
),
|
||
'a0-05': LessonActivity(
|
||
listening: 'What’s this? It’s a key.',
|
||
listeningQuestion: '这是什么?',
|
||
answers: ['钥匙', '书', '水'],
|
||
speaking: 'What’s this? It’s a pen.',
|
||
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: '不看句框,说出一个身边物品。',
|
||
independentHelp: 'It’s a …',
|
||
),
|
||
'a0-06': LessonActivity(
|
||
listening: 'Where are you from?',
|
||
listeningQuestion: '对方在问什么?',
|
||
answers: ['来自哪里', '叫什么', '喜欢什么'],
|
||
speaking: '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: '不看句框,说来自哪里并反问对方。',
|
||
independentHelp: 'I’m from … / Where are you from?',
|
||
),
|
||
'a0-07': LessonActivity(
|
||
listening: 'Who is this? This is my mother.',
|
||
listeningQuestion: '这个人是谁?',
|
||
answers: ['妈妈', '朋友', '老师'],
|
||
speaking: 'This is my family.',
|
||
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: '不看句框,介绍一位家人或朋友。',
|
||
independentHelp: 'This is my …',
|
||
),
|
||
'a0-08': LessonActivity(
|
||
listening: 'It’s three o’clock on Monday.',
|
||
listeningQuestion: '是什么时间?',
|
||
answers: ['星期一三点', '星期三一点', '星期一一点'],
|
||
speaking: 'It’s three o’clock.',
|
||
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: '不看句框,说今天星期几或现在几点。',
|
||
independentHelp: 'It’s … / It’s … o’clock.',
|
||
),
|
||
'a0-09': LessonActivity(
|
||
listening: 'Do you like tea? Yes, I do.',
|
||
listeningQuestion: '对方喜欢茶吗?',
|
||
answers: ['喜欢', '不喜欢', '不知道'],
|
||
speaking: 'I like tea. Do you like tea?',
|
||
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: '不看句框,说一个喜好并反问。',
|
||
independentHelp: 'I like … / Do you like …?',
|
||
),
|
||
'a0-10': LessonActivity(
|
||
listening: 'Please say that again.',
|
||
listeningQuestion: '对方希望什么?',
|
||
answers: ['再说一遍', '说慢一点', '写下来'],
|
||
speaking: 'Please 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: '不看帮助,完成姓名、地点、喜好和反问。',
|
||
independentHelp: 'I’m … / I’m from … / I like …',
|
||
),
|
||
};
|
||
|
||
LessonActivity activityByLessonId(String id) => a0Activities[id]!;
|
||
|
||
/// Reviewed activities for split topics. They intentionally avoid values from
|
||
/// later segments so a segment can be completed independently.
|
||
const a0SegmentActivities = <String, LessonActivity>{
|
||
'a0-04-a': LessonActivity(
|
||
listening: 'One, two, three.',
|
||
listeningQuestion: '听到哪一组数字?',
|
||
answers: ['123', '132', '213'],
|
||
speaking: 'Zero, one, two, three.',
|
||
reading: 'Mia: One, two, three.\nShen: 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 的数字。',
|
||
independentHelp: 'zero / one / two / three / four / five',
|
||
),
|
||
'a0-04-b': LessonActivity(
|
||
listening: 'Six, seven, eight.',
|
||
listeningQuestion: '听到哪一组数字?',
|
||
answers: ['678', '687', '768'],
|
||
speaking: 'My phone is here.',
|
||
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 的数字。',
|
||
independentHelp: 'six / seven / eight / nine / ten',
|
||
),
|
||
'a0-04-c': LessonActivity(
|
||
listening: 'My number is one-three-eight.',
|
||
listeningQuestion: '号码是多少?',
|
||
answers: ['138', '183', '318'],
|
||
speaking: 'My number is one-three-eight.',
|
||
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: '不看句框,说出一个三位虚拟号码。',
|
||
independentHelp: 'My number is one-two-three.',
|
||
),
|
||
'a0-08-a': LessonActivity(
|
||
listening: 'It is Monday.',
|
||
listeningQuestion: '今天星期几?',
|
||
answers: ['星期一', '星期二', '星期三'],
|
||
speaking: 'It is Monday.',
|
||
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: '不看帮助,说出星期一、二或三。',
|
||
independentHelp: 'It is Monday / Tuesday / Wednesday.',
|
||
),
|
||
'a0-08-b': LessonActivity(
|
||
listening: 'It is Friday.',
|
||
listeningQuestion: '今天星期几?',
|
||
answers: ['星期五', '星期四', '星期日'],
|
||
speaking: 'It is Friday.',
|
||
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: '不看帮助,说出星期四到日。',
|
||
independentHelp: 'It is Thursday / Friday / Saturday / Sunday.',
|
||
),
|
||
'a0-08-c': LessonActivity(
|
||
listening: 'It is three o’clock.',
|
||
listeningQuestion: '是什么时间?',
|
||
answers: ['三点', '一点', '星期三'],
|
||
speaking: 'It is 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: '不看帮助,说一个整点时间。',
|
||
independentHelp: 'It is … o’clock.',
|
||
),
|
||
};
|
||
|
||
LessonActivity activityBySegmentId(String segmentId, String lessonId) =>
|
||
a0SegmentActivities[segmentId] ?? activityByLessonId(lessonId);
|
||
|
||
/// Short, reviewed entries used before each split segment. These are kept
|
||
/// separate from review prompts: a review hint is not a learner-facing
|
||
/// dictionary definition.
|
||
const a0SegmentVocabulary = <String, List<VocabularyItem>>{
|
||
'a0-04-a': [
|
||
VocabularyItem(
|
||
id: 'A0-W01',
|
||
word: 'zero',
|
||
meaning: '零',
|
||
example: 'Zero, one, two.',
|
||
exampleMeaning: '零、一、二。',
|
||
ipa: '/ˈzɪrəʊ/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'A0-W02',
|
||
word: 'one',
|
||
meaning: '一',
|
||
example: 'One, two, three.',
|
||
exampleMeaning: '一、二、三。',
|
||
ipa: '/wʌn/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'A0-W03',
|
||
word: 'two',
|
||
meaning: '二',
|
||
example: 'Two and three.',
|
||
exampleMeaning: '二和三。',
|
||
ipa: '/tuː/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'A0-W04',
|
||
word: 'three',
|
||
meaning: '三',
|
||
example: 'Three, four, five.',
|
||
exampleMeaning: '三、四、五。',
|
||
ipa: '/θriː/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'A0-W05',
|
||
word: 'four',
|
||
meaning: '四',
|
||
example: 'Four and five.',
|
||
exampleMeaning: '四和五。',
|
||
ipa: '/fɔː/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'A0-W06',
|
||
word: 'five',
|
||
meaning: '五',
|
||
example: 'Five, please.',
|
||
exampleMeaning: '五,请。',
|
||
ipa: '/faɪv/',
|
||
),
|
||
],
|
||
'a0-04-b': [
|
||
VocabularyItem(
|
||
id: 'A0-W07',
|
||
word: 'six',
|
||
meaning: '六',
|
||
example: 'Six, seven, eight.',
|
||
exampleMeaning: '六、七、八。',
|
||
ipa: '/sɪks/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'A0-W08',
|
||
word: 'seven',
|
||
meaning: '七',
|
||
example: 'Seven, eight, nine.',
|
||
exampleMeaning: '七、八、九。',
|
||
ipa: '/ˈsevən/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'A0-W09',
|
||
word: 'eight',
|
||
meaning: '八',
|
||
example: 'Eight, nine, ten.',
|
||
exampleMeaning: '八、九、十。',
|
||
ipa: '/eɪt/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'A0-W10',
|
||
word: 'nine',
|
||
meaning: '九',
|
||
example: 'Nine and ten.',
|
||
exampleMeaning: '九和十。',
|
||
ipa: '/naɪn/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'A0-W11',
|
||
word: 'ten',
|
||
meaning: '十',
|
||
example: 'Ten, please.',
|
||
exampleMeaning: '十,请。',
|
||
ipa: '/ten/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'A0-W20',
|
||
word: 'phone',
|
||
meaning: '电话;手机',
|
||
example: 'It is a phone.',
|
||
exampleMeaning: '它是一部手机。',
|
||
ipa: '/fəʊn/',
|
||
),
|
||
],
|
||
'a0-04-c': [
|
||
VocabularyItem(
|
||
id: 'A0-P07',
|
||
word: "What's your phone number?",
|
||
meaning: '你的电话号码是多少?',
|
||
example: "What's your phone number?",
|
||
exampleMeaning: '你的电话号码是多少?',
|
||
),
|
||
VocabularyItem(
|
||
id: 'A0-P08',
|
||
word: 'My number is one-three-eight.',
|
||
meaning: '我的号码是 138。',
|
||
example: 'My number is one-three-eight.',
|
||
exampleMeaning: '我的号码是 138。',
|
||
),
|
||
],
|
||
'a0-08-a': [
|
||
VocabularyItem(
|
||
id: 'A0-W12',
|
||
word: 'Monday',
|
||
meaning: '星期一',
|
||
example: 'It is Monday.',
|
||
exampleMeaning: '今天是星期一。',
|
||
ipa: '/ˈmʌndeɪ/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'A0-W13',
|
||
word: 'Tuesday',
|
||
meaning: '星期二',
|
||
example: 'It is Tuesday.',
|
||
exampleMeaning: '今天是星期二。',
|
||
ipa: '/ˈtjuːzdeɪ/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'A0-W14',
|
||
word: 'Wednesday',
|
||
meaning: '星期三',
|
||
example: 'It is Wednesday.',
|
||
exampleMeaning: '今天是星期三。',
|
||
ipa: '/ˈwenzdeɪ/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'A0-P14',
|
||
word: 'What day is it today?',
|
||
meaning: '今天星期几?',
|
||
example: 'What day is it today?',
|
||
exampleMeaning: '今天星期几?',
|
||
),
|
||
VocabularyItem(
|
||
id: 'A0-P15',
|
||
word: "It's Monday.",
|
||
meaning: '今天是星期一。',
|
||
example: "It's Monday.",
|
||
exampleMeaning: '今天是星期一。',
|
||
),
|
||
],
|
||
'a0-08-b': [
|
||
VocabularyItem(
|
||
id: 'A0-W15',
|
||
word: 'Thursday',
|
||
meaning: '星期四',
|
||
example: 'It is Thursday.',
|
||
exampleMeaning: '今天是星期四。',
|
||
ipa: '/ˈθɜːzdeɪ/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'A0-W16',
|
||
word: 'Friday',
|
||
meaning: '星期五',
|
||
example: 'It is Friday.',
|
||
exampleMeaning: '今天是星期五。',
|
||
ipa: '/ˈfraɪdeɪ/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'A0-W17',
|
||
word: 'Saturday',
|
||
meaning: '星期六',
|
||
example: 'It is Saturday.',
|
||
exampleMeaning: '今天是星期六。',
|
||
ipa: '/ˈsætədeɪ/',
|
||
),
|
||
VocabularyItem(
|
||
id: 'A0-W18',
|
||
word: 'Sunday',
|
||
meaning: '星期日',
|
||
example: 'It is Sunday.',
|
||
exampleMeaning: '今天是星期日。',
|
||
ipa: '/ˈsʌndeɪ/',
|
||
),
|
||
],
|
||
'a0-08-c': [
|
||
VocabularyItem(
|
||
id: 'A0-P16',
|
||
word: 'What time is it?',
|
||
meaning: '现在几点?',
|
||
example: 'What time is it?',
|
||
exampleMeaning: '现在几点?',
|
||
),
|
||
VocabularyItem(
|
||
id: 'A0-P17',
|
||
word: "It's three o'clock.",
|
||
meaning: '现在三点。',
|
||
example: "It's three o'clock.",
|
||
exampleMeaning: '现在三点。',
|
||
),
|
||
],
|
||
};
|
||
|
||
List<VocabularyItem> vocabularyBySegmentId(String segmentId, String lessonId) {
|
||
if (!a0SegmentActivities.containsKey(segmentId)) {
|
||
return lessonById(lessonId).vocabulary;
|
||
}
|
||
return a0SegmentVocabulary[segmentId] ?? const <VocabularyItem>[];
|
||
}
|
||
|
||
/// One practical rule per seed lesson. These are reminders for using a sentence
|
||
/// in context, not abstract grammar lectures.
|
||
const a0GrammarNotes = <String, String>{
|
||
'a0-01': '介绍自己时,把名字放在 I’m 后面:I’m Mia. 不需要先学复杂时态。',
|
||
'a0-02': 'My name is 后面接名字;拼读时每个字母之间稍微停顿。',
|
||
'a0-03': '说状态用 I’m + 状态:I’m good / okay / tired。',
|
||
'a0-04': '报号码时数字一个一个说:one-three-eight,不要当成一百三十八。',
|
||
'a0-05': '问物品用 What’s this?;回答用 It’s a + 物品。',
|
||
'a0-06': '说地点用 I’m from + 地点:I’m from Hong Kong。',
|
||
'a0-07': '介绍人用 This is my + 人:This is my mother。',
|
||
'a0-08': '星期和整点都可用 It’s:It’s Monday. / It’s three o’clock。',
|
||
'a0-09': '说喜好用 I like + 东西;一般问句用 Do you like + 东西?',
|
||
'a0-10': '礼貌请求以 Please 开头:Please say that again.',
|
||
};
|
||
|
||
String grammarNoteForLesson(String id) => a0GrammarNotes[id] ?? '';
|
||
|
||
const a0SegmentGrammarNotes = <String, String>{
|
||
'a0-04-a': '数字单独说就可以:zero, one, two。每个词之间稍微停顿。',
|
||
'a0-04-b': '认物品可以说 It is a phone. 口语里也常说 It’s a phone.',
|
||
'a0-04-c': '报号码时一个数字一个数字说:one-three-eight,不把它读成一百三十八。',
|
||
'a0-08-a': '问星期用 What day is it?;回答用 It is / It’s + 星期。',
|
||
'a0-08-b': '星期四到星期日也沿用 It is / It’s + 星期这个句型。',
|
||
'a0-08-c': '说整点用 It is / It’s + 数字 + o’clock,例如 It’s three o’clock。',
|
||
};
|
||
|
||
String grammarNoteForSegment(String segmentId, String lessonId) =>
|
||
a0SegmentGrammarNotes[segmentId] ?? grammarNoteForLesson(lessonId);
|
||
|
||
bool matchesSegmentDialogue(String segmentId, int stage, String response) {
|
||
final text = response.toLowerCase().replaceAll('’', "'");
|
||
final segment = a0LessonSegments.values
|
||
.expand((segments) => segments)
|
||
.where((item) => item.id == segmentId)
|
||
.firstOrNull;
|
||
if (segment == null || stage >= segment.dialogueRequiredTerms.length) {
|
||
return false;
|
||
}
|
||
bool hasAny(Iterable<String> terms) =>
|
||
terms.any((term) => text.contains(term));
|
||
// A phone-number report needs both the reporting frame and at least one
|
||
// spoken digit. Other segment turns need one reviewed, task-specific term.
|
||
if (segmentId == 'a0-04-c' && stage == 0) {
|
||
return text.contains('my number is') &&
|
||
hasAny(segment.dialogueRequiredTerms[stage].skip(1));
|
||
}
|
||
return hasAny(segment.dialogueRequiredTerms[stage]);
|
||
}
|
||
|
||
bool matchesSegmentIndependent(String segmentId, String response) {
|
||
final text = response.toLowerCase().replaceAll('’', "'");
|
||
final segment = a0LessonSegments.values
|
||
.expand((segments) => segments)
|
||
.where((item) => item.id == segmentId)
|
||
.firstOrNull;
|
||
if (segment == null) return true;
|
||
if (segmentId == 'a0-04-a' || segmentId == 'a0-04-b') {
|
||
final count = segment.independentRequiredTerms
|
||
.where((term) => text.contains(term))
|
||
.length;
|
||
return count >= 3;
|
||
}
|
||
if (segmentId == 'a0-04-c') {
|
||
return segment.independentRequiredTerms
|
||
.where((term) => text.contains(term))
|
||
.length >=
|
||
3;
|
||
}
|
||
return segment.independentRequiredTerms.any((term) => text.contains(term));
|
||
}
|
||
|
||
class LessonDialogue {
|
||
const LessonDialogue({
|
||
required this.goal,
|
||
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: '问候、介绍姓名并回应见面问候',
|
||
prompts: [
|
||
'Hi! I’m Mia. What’s your name?',
|
||
'Nice to meet you. Say: Nice to meet you, too.',
|
||
'Great! Say hello one more time.',
|
||
'Now ask me my name!',
|
||
],
|
||
hints: [
|
||
'I’m Alex.',
|
||
'Nice to meet you, too.',
|
||
'Hello!',
|
||
'What’s your name?',
|
||
],
|
||
translations: [
|
||
'嗨!我是 Mia。你叫什么名字?',
|
||
'很高兴认识你。请说:Nice to meet you, too(我也很高兴认识你)。',
|
||
'太棒了!再打一次招呼吧。',
|
||
'现在请问我的名字!',
|
||
],
|
||
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: '介绍姓名并完整拼读名字',
|
||
prompts: [
|
||
'Hi! What’s your name?',
|
||
'How do you spell that?',
|
||
'Thank you. Spell it one more time.',
|
||
'Now ask me my name!',
|
||
],
|
||
hints: ['My name is Alex.', 'A-L-E-X.', 'A-L-E-X.', 'What’s your name?'],
|
||
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: '询问状态、回答并反问',
|
||
prompts: [
|
||
'Hi! How are you today?',
|
||
'Good! Ask me: How are you?',
|
||
'I’m okay. Say your state one more time.',
|
||
'Now say goodbye!',
|
||
],
|
||
hints: ['I’m good, thanks.', 'How are you?', 'I’m okay.', 'Bye!'],
|
||
translations: [
|
||
'嗨!你今天好吗?',
|
||
'很好!请问我:How are you(你好吗)?',
|
||
'我还好。请再说一次你的状态。',
|
||
'现在请说再见!',
|
||
],
|
||
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: '报告一个虚拟三位号码并确认',
|
||
prompts: [
|
||
'What’s your phone number? Use a fake three-digit number.',
|
||
'I heard one-three-eight. Is that right?',
|
||
'Please say the three numbers again.',
|
||
'Now ask me for my number!',
|
||
],
|
||
hints: [
|
||
'My number is one-three-eight.',
|
||
'Yes.',
|
||
'One-three-eight.',
|
||
'What’s your phone number?',
|
||
],
|
||
translations: [
|
||
'你的电话号码是多少?可以使用一个虚拟的三位数字。',
|
||
'我听到了 1-3-8。对吗?',
|
||
'请再说一遍这三个数字。',
|
||
'现在请问我的电话号码!',
|
||
],
|
||
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: '询问并说出一个物品',
|
||
prompts: [
|
||
'What’s this? It is a pen.',
|
||
'Now say: It’s a pen.',
|
||
'Ask me: What’s this?',
|
||
'Great! Say one more object.',
|
||
],
|
||
hints: ['It’s a pen.', 'It’s a pen.', 'What’s this?', 'It’s a book.'],
|
||
translations: [
|
||
'这是什么?这是一支笔。',
|
||
'现在请说:It’s a pen(这是一支笔)。',
|
||
'请问我: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: '说来自哪里并反问',
|
||
prompts: [
|
||
'Where are you from?',
|
||
'Nice! Ask me: Where are you from?',
|
||
'Say where you are from one more time.',
|
||
'Say goodbye!',
|
||
],
|
||
hints: [
|
||
'I’m from Hong Kong.',
|
||
'Where are you from?',
|
||
'I’m from Hong Kong.',
|
||
'Bye!',
|
||
],
|
||
translations: [
|
||
'你来自哪里?',
|
||
'很好!请问我:Where are you from(你来自哪里)?',
|
||
'请再说一次你来自哪里。',
|
||
'请说再见!',
|
||
],
|
||
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: '介绍一位家人或朋友',
|
||
prompts: [
|
||
'Who is this?',
|
||
'Good. Say: This is my mother.',
|
||
'Ask me: Who is this?',
|
||
'Introduce one person again.',
|
||
],
|
||
hints: [
|
||
'This is my mother.',
|
||
'This is my mother.',
|
||
'Who is this?',
|
||
'This is my friend.',
|
||
],
|
||
translations: [
|
||
'这是谁?',
|
||
'很好。请说:This is my mother(这是我的妈妈)。',
|
||
'请问我: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: '说明星期或整点',
|
||
prompts: [
|
||
'What day is it today?',
|
||
'What time is it?',
|
||
'Say one full time sentence.',
|
||
'Now ask me the day!',
|
||
],
|
||
hints: [
|
||
'It’s Monday.',
|
||
'It’s three o’clock.',
|
||
'It’s three o’clock.',
|
||
'What day is it?',
|
||
],
|
||
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: '表达喜好、回答和反问',
|
||
prompts: [
|
||
'What do you like?',
|
||
'Do you like tea?',
|
||
'Now ask me what I like.',
|
||
'Say your like one more time.',
|
||
],
|
||
hints: ['I like tea.', 'Yes, I do.', 'Do you like tea?', 'I like tea.'],
|
||
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: '请求重复或放慢语速,并完成基础沟通',
|
||
prompts: [
|
||
'Say: Please say that again.',
|
||
'Say: Please speak slowly.',
|
||
'Now ask me your name or where I am from.',
|
||
'Say one thing you like.',
|
||
],
|
||
hints: [
|
||
'Please say that again.',
|
||
'Please speak slowly.',
|
||
'What’s your name?',
|
||
'I like tea.',
|
||
],
|
||
translations: [
|
||
'请说:Please say that again(请再说一遍)。',
|
||
'请说:Please speak slowly(请说慢一点)。',
|
||
'现在请问我的名字或我来自哪里。',
|
||
'请说一件你喜欢的事物。',
|
||
],
|
||
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: ['请求对方重复', '请求对方放慢语速', '反问名字或来自哪里', '说出一件你喜欢的事物'],
|
||
),
|
||
};
|
||
|
||
const a0SegmentDialogues = <String, LessonDialogue>{
|
||
'a0-04-a': 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)。',
|
||
],
|
||
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(这是一部手机)。',
|
||
],
|
||
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(周四、周五、周六或周日)。',
|
||
],
|
||
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: ['问现在几点', '说一个完整的时间句子'],
|
||
),
|
||
};
|
||
|
||
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();
|