import 'models.dart'; class ReviewCheckResult { const ReviewCheckResult({required this.complete, required this.message}); final bool complete; final String message; } /// 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. class ReviewFeedback { const ReviewFeedback._(); static ReviewCheckResult check(ReviewItem item, String input) { final text = input.toLowerCase().replaceAll('’', "'"); final tokens = RegExp( r"[a-z]+(?:'[a-z]+)?", ).allMatches(text).map((match) => match.group(0)!).toSet(); bool has(String token) => tokens.contains(token); bool phrase(String value) => text.contains(value); bool hasAny(Iterable 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; final complete = switch (item.id) { 'A0-P01' => introduction && tokens.length >= 2, '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' => (phrase('my number is') || numberWords >= 3) && numberWords >= 3, 'A0-P09' => phrase("what's this") || phrase('what is this'), 'A0-P10' => itIs && hasAny(['book', 'pen', 'bag', 'key']), '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([ 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday', ]), 'A0-P16' => phrase('what time is it'), 'A0-P17' => itIs && (has('clock') || has("o'clock") || has('oclock') || phrase("o'clock")), 'A0-P18' => introduction && has('like') && tokens.length >= 3, 'A0-P19' => phrase('do you like'), 'A0-P20' => phrase('please say that again') || phrase('please speak slowly'), _ when item.id.startsWith('A0-W') => tokens.contains( item.target.toLowerCase(), ), _ => tokens.length >= 2, }; return ReviewCheckResult( complete: complete, message: complete ? '表达已完成,可以进入下一次复习安排。' : _hint(item.id), ); } 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 … 说一项喜好。', _ => '再补充本次目标中的关键英文词或句型。', }; }