113 lines
4.5 KiB
Dart
113 lines
4.5 KiB
Dart
class WritingCheckResult {
|
||
const WritingCheckResult({required this.complete, required this.message});
|
||
|
||
final bool complete;
|
||
final String message;
|
||
}
|
||
|
||
/// A deliberately small offline checker for teaching tasks. It checks only
|
||
/// the essential A0 information, accepts variable names/places, and never
|
||
/// claims to grade pronunciation or nuanced grammar.
|
||
class WritingFeedback {
|
||
const WritingFeedback._();
|
||
|
||
static WritingCheckResult check(
|
||
String lessonId,
|
||
String input, {
|
||
String? segmentId,
|
||
}) {
|
||
final text = input.toLowerCase().replaceAll('’', "'");
|
||
final words = RegExp(
|
||
r"[a-z]+(?:'[a-z]+)?",
|
||
).allMatches(text).map((match) => match.group(0)!).toSet();
|
||
bool has(String word) => words.contains(word);
|
||
|
||
bool hasPhrase(String phrase) => text.contains(phrase);
|
||
|
||
bool hasAny(Iterable<String> choices) => choices.any(has);
|
||
final hasIntroduction =
|
||
hasPhrase("i'm") || hasPhrase('i am') || hasPhrase('my name is');
|
||
final hasItIs = hasPhrase("it's") || hasPhrase('it is');
|
||
|
||
final complete = switch (segmentId) {
|
||
'a0-04-a' => hasAll(words, ['zero', 'one', 'two', 'three']),
|
||
'a0-04-b' => hasAll(words, ['six', 'seven', 'eight']),
|
||
'a0-04-c' =>
|
||
RegExp(
|
||
r'\b(one|two|three|four|five|six|seven|eight|nine|zero)\b',
|
||
).allMatches(text).length >=
|
||
3,
|
||
'a0-08-a' => hasItIs && hasAny(['monday', 'tuesday', 'wednesday']),
|
||
'a0-08-b' =>
|
||
hasItIs && hasAny(['thursday', 'friday', 'saturday', 'sunday']),
|
||
'a0-08-c' => hasItIs && (has('oclock') || text.contains("o'clock")),
|
||
_ => switch (lessonId) {
|
||
'a0-01' => hasAny(['hello', 'hi']) && hasIntroduction,
|
||
'a0-02' =>
|
||
(hasPhrase('my name') || hasIntroduction) &&
|
||
RegExp(r'[a-z](?:[ -]?[a-z]){2,}').hasMatch(text),
|
||
'a0-03' => hasIntroduction && hasAny(['good', 'okay', 'tired']),
|
||
'a0-04' =>
|
||
RegExp(
|
||
r'\b(zero|one|two|three|four|five|six|seven|eight|nine|ten)\b',
|
||
).allMatches(text).length >=
|
||
3,
|
||
'a0-05' => hasItIs && hasAny(['book', 'pen', 'bag', 'key']),
|
||
'a0-06' => hasIntroduction && has('from') && words.length >= 3,
|
||
'a0-07' =>
|
||
hasPhrase('this is my') &&
|
||
hasAny(['mother', 'father', 'sister', 'brother', 'friend']),
|
||
'a0-08' =>
|
||
hasItIs &&
|
||
(hasAny([
|
||
'monday',
|
||
'tuesday',
|
||
'wednesday',
|
||
'thursday',
|
||
'friday',
|
||
'saturday',
|
||
'sunday',
|
||
]) ||
|
||
has('clock')),
|
||
'a0-09' => hasIntroduction && has('like') && words.length >= 3,
|
||
'a0-10' => hasIntroduction && has('from') && has('like'),
|
||
_ => words.length >= 2,
|
||
},
|
||
};
|
||
|
||
if (complete) {
|
||
return const WritingCheckResult(
|
||
complete: true,
|
||
message: '这句已经表达完整。继续在对话里用一次吧。',
|
||
);
|
||
}
|
||
return WritingCheckResult(
|
||
complete: false,
|
||
message: _hintFor(segmentId ?? lessonId),
|
||
);
|
||
}
|
||
|
||
static bool hasAll(Set<String> words, Iterable<String> required) =>
|
||
required.every(words.contains);
|
||
|
||
static String _hintFor(String lessonId) => switch (lessonId) {
|
||
'a0-04-a' => '请写出 zero、one、two、three 四个数字。',
|
||
'a0-04-b' => '请写出 six、seven、eight 三个数字。',
|
||
'a0-04-c' => '用英文一个一个写出三个数字,例如 one-three-nine。',
|
||
'a0-08-a' => '用 It is / It’s 加 Monday、Tuesday 或 Wednesday。',
|
||
'a0-08-b' => '用 It is / It’s 加 Thursday、Friday、Saturday 或 Sunday。',
|
||
'a0-08-c' => '用 It is / It’s 加数字和 o’clock,例如 It’s three o’clock。',
|
||
'a0-01' => '试着同时写一句问候和姓名,例如:Hello. I’m …',
|
||
'a0-02' => '写出姓名,并把至少三个字母用空格或连字符拼出来。',
|
||
'a0-03' => '用 I’m / I am 加上你的状态,例如 good 或 tired。',
|
||
'a0-04' => '用英文写出三个数字,例如 one-three-nine。',
|
||
'a0-05' => '用 It’s / It is 加一个物品,例如 a pen。',
|
||
'a0-06' => '用 I’m from … 写成一个完整句子。',
|
||
'a0-07' => '用 This is my … 介绍一位家人或朋友。',
|
||
'a0-08' => '用 It’s … 写一个星期或整点时间。',
|
||
'a0-09' => '用 I like … 写出一种喜好。',
|
||
'a0-10' => '分别写姓名、来自哪里和一项喜好三部分。',
|
||
_ => '再补充一个完整英文句子。',
|
||
};
|
||
}
|