整个课程和数据UI重新设计版本
This commit is contained in:
@@ -1,30 +1,31 @@
|
||||
import 'a0_core.dart';
|
||||
import 'courses/courses.dart';
|
||||
import 'models.dart';
|
||||
import 'speech_compare.dart';
|
||||
|
||||
class ReviewCheckResult {
|
||||
const ReviewCheckResult({required this.complete, required this.message});
|
||||
const ReviewCheckResult({
|
||||
required this.complete,
|
||||
required this.message,
|
||||
this.dictation,
|
||||
});
|
||||
|
||||
final bool complete;
|
||||
final String message;
|
||||
|
||||
/// Word-by-word comparison with the dictated sentence; null otherwise.
|
||||
final SpokenComparison? dictation;
|
||||
}
|
||||
|
||||
/// Checks only the minimum information a learner needs to produce in an A0
|
||||
/// review. It intentionally accepts common contractions and variable slot
|
||||
/// values instead of comparing an answer with one fixed sentence.
|
||||
/// Checks only the minimum information a learner needs to produce in a
|
||||
/// review. The rule and the hint come from the core item's course JSON, which
|
||||
/// accepts contractions and variable slot values instead of one fixed
|
||||
/// sentence.
|
||||
class ReviewFeedback {
|
||||
const ReviewFeedback._();
|
||||
|
||||
static String _normalize(String input) => input
|
||||
.toLowerCase()
|
||||
.replaceAll('’', "'")
|
||||
.replaceAll('‘', "'")
|
||||
.replaceAll(RegExp(r'\s+'), ' ')
|
||||
.trim();
|
||||
|
||||
static ReviewCheckResult check(ReviewItem item, String input) {
|
||||
final dictation = item.skill == dictationSkill
|
||||
? a0DictationSentences[item.id]
|
||||
? coreDictationSentences[item.id]
|
||||
: null;
|
||||
if (dictation != null) {
|
||||
final comparison = compareSpoken(dictation.sentence, input);
|
||||
@@ -35,94 +36,22 @@ class ReviewFeedback {
|
||||
message: comparison.matches
|
||||
? '听写正确。'
|
||||
: '有 $wrong 处和听到的句子不一致。再听一次,检查拼写和漏掉的词。',
|
||||
dictation: comparison,
|
||||
);
|
||||
}
|
||||
final complete = a0CoreItems.containsKey(item.id)
|
||||
// A word review checks the word it actually shows.
|
||||
? coreItemUsedIn(item.id, input, word: item.target)
|
||||
: _tokens(_normalize(input)).length >= 2;
|
||||
final complete = isCoreItem(item.id)
|
||||
? coreItemUsedIn(item.id, input)
|
||||
: RegExp(r"[a-z]+(?:'[a-z]+)?")
|
||||
.allMatches(normalizeAnswer(input))
|
||||
.map((match) => match.group(0))
|
||||
.toSet()
|
||||
.length >=
|
||||
2;
|
||||
return ReviewCheckResult(
|
||||
complete: complete,
|
||||
message: complete ? '表达已完成,可以进入下一次复习安排。' : _hint(item.id),
|
||||
message: complete
|
||||
? '表达已完成,可以进入下一次复习安排。'
|
||||
: coreReviewHints[item.id] ?? '再补充本次目标中的关键英文词或句型。',
|
||||
);
|
||||
}
|
||||
|
||||
static Set<String> _tokens(String text) => RegExp(
|
||||
r"[a-z]+(?:'[a-z]+)?",
|
||||
).allMatches(text).map((match) => match.group(0)!).toSet();
|
||||
|
||||
static String _hint(String id) => switch (id) {
|
||||
'A0-P01' => '用 I’m / I am 或 My name is 介绍一个名字。',
|
||||
'A0-P02' => '试着问:What’s your name?',
|
||||
'A0-P04' => '问对方怎样拼写:How do you spell …?',
|
||||
'A0-P08' => '说出至少三个英文数字,也可用 My number is … 开头。',
|
||||
'A0-P10' => '用 It’s / It is 加上一个物品。',
|
||||
'A0-P12' => '用 I’m from … 说出一个地点。',
|
||||
'A0-P17' => '用 It’s … o’clock 说整点时间。',
|
||||
'A0-P18' => '用 I like … 说一项喜好。',
|
||||
_ => '再补充本次目标中的关键英文词或句型。',
|
||||
};
|
||||
}
|
||||
|
||||
/// Whether [input] actually contains A0 core item [id]. Lessons, reviews and
|
||||
/// dialogues use the same rule, so evidence only goes to the items a learner
|
||||
/// really produced, never to every target a task happens to be attached to.
|
||||
bool coreItemUsedIn(String id, String input, {String? word}) {
|
||||
final text = ReviewFeedback._normalize(input);
|
||||
final tokens = ReviewFeedback._tokens(text);
|
||||
bool has(String token) => tokens.contains(ReviewFeedback._normalize(token));
|
||||
bool phrase(String value) => text.contains(ReviewFeedback._normalize(value));
|
||||
bool hasAny(Iterable<String> values) => values.any(has);
|
||||
final introduction = phrase("i'm") || phrase('i am') || phrase('my name is');
|
||||
final itIs = phrase("it's") || phrase('it is');
|
||||
final numberWords = RegExp(
|
||||
r'\b(zero|one|two|three|four|five|six|seven|eight|nine|ten)\b',
|
||||
).allMatches(text).length;
|
||||
const weekdays = [
|
||||
'monday',
|
||||
'tuesday',
|
||||
'wednesday',
|
||||
'thursday',
|
||||
'friday',
|
||||
'saturday',
|
||||
'sunday',
|
||||
];
|
||||
|
||||
return switch (id) {
|
||||
// "I'm from …" and "I'm good" are other items, not a name.
|
||||
'A0-P01' =>
|
||||
phrase('my name is') ||
|
||||
RegExp(
|
||||
r"\bi(?:'m| am) (?!from\b|good\b|okay\b|ok\b|fine\b|great\b|tired\b)[a-z]",
|
||||
).hasMatch(text),
|
||||
'A0-P02' => phrase("what's your name") || phrase('what is your name'),
|
||||
'A0-P03' => phrase('nice to meet you'),
|
||||
'A0-P04' => phrase('how do you spell'),
|
||||
'A0-P05' => phrase('how are you'),
|
||||
'A0-P06' => introduction && hasAny(['good', 'okay', 'tired']),
|
||||
'A0-P07' =>
|
||||
phrase("what's your phone number") || phrase('what is your phone number'),
|
||||
'A0-P08' => numberWords >= 3,
|
||||
'A0-P09' => phrase("what's this") || phrase('what is this'),
|
||||
'A0-P10' => itIs && hasAny(['book', 'pen', 'bag', 'key', 'phone']),
|
||||
'A0-P11' => phrase('where are you from'),
|
||||
'A0-P12' => introduction && has('from') && tokens.length >= 3,
|
||||
'A0-P13' => phrase('this is my') && tokens.length >= 4,
|
||||
'A0-P14' => phrase('what day is it'),
|
||||
'A0-P15' => itIs && hasAny(weekdays),
|
||||
'A0-P16' => phrase('what time is it'),
|
||||
'A0-P17' =>
|
||||
itIs &&
|
||||
(has('clock') ||
|
||||
has("o'clock") ||
|
||||
has('oclock') ||
|
||||
phrase("o'clock")),
|
||||
'A0-P18' => (phrase('i like') || phrase('i love')) && tokens.length >= 3,
|
||||
'A0-P19' => phrase('do you like'),
|
||||
'A0-P20' =>
|
||||
phrase('please say that again') || phrase('please speak slowly'),
|
||||
_ when a0CoreItems.containsKey(id) && id.startsWith('A0-W') =>
|
||||
has(word ?? a0CoreItems[id]!) || phrase(word ?? a0CoreItems[id]!),
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user