From f87849eb5209b86988f3146d07ca9e5d59a6f782 Mon Sep 17 00:00:00 2001 From: shenlei Date: Wed, 16 Sep 2026 17:55:08 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=E8=BE=93=E5=85=A5=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E5=BF=BD=E7=95=A5=E5=A4=9A=E4=B8=AA=E8=BF=9E=E7=BB=AD=E7=A9=BA?= =?UTF-8?q?=E6=A0=BC=E4=B8=8E=E5=A4=A7=E5=B0=8F=E5=86=99=EF=BC=9B=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E4=B8=AD=E8=8B=B1=E6=96=87=E5=BC=95=E5=8F=B7=E4=B8=8E?= =?UTF-8?q?=E7=A9=BA=E7=99=BD=E8=A7=84=E8=8C=83=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- kouyu_english/lib/core/assessment_bank.dart | 25 +++++++++++++++-- kouyu_english/lib/core/generated_content.dart | 2 ++ kouyu_english/lib/core/review_feedback.dart | 18 ++++++++---- kouyu_english/lib/core/seed_courses.dart | 28 ++++++++++++------- kouyu_english/lib/core/writing_feedback.dart | 15 +++++++--- kouyu_english/lib/widgets/lexicon_lookup.dart | 28 ++++++++++++++++--- kouyu_english/test/dialogue_task_test.dart | 19 +++++++++++++ kouyu_english/test/review_feedback_test.dart | 27 ++++++++++++++++++ kouyu_english/test/writing_feedback_test.dart | 27 ++++++++++++++++++ 9 files changed, 162 insertions(+), 27 deletions(-) diff --git a/kouyu_english/lib/core/assessment_bank.dart b/kouyu_english/lib/core/assessment_bank.dart index 07c31d6..0cb35c5 100644 --- a/kouyu_english/lib/core/assessment_bank.dart +++ b/kouyu_english/lib/core/assessment_bank.dart @@ -155,12 +155,31 @@ AssessmentPack? replacementFor(String packId) => switch (packId) { /// Local checks for the frozen A0 exit tasks. They accept variable names and /// places, but require the communicative information named by each task. bool checkOpenAssessmentAnswer(AssessmentTask task, String input) { - final text = input.toLowerCase().replaceAll('’', "'"); + final text = input + .toLowerCase() + .replaceAll('’', "'") + .replaceAll('‘', "'") + .replaceAll(RegExp(r"\s+"), " ") + .trim(); final words = RegExp( r"[a-z]+(?:'[a-z]+)?", ).allMatches(text).map((match) => match.group(0)!).toSet(); - bool has(String word) => words.contains(word); - bool phrase(String value) => text.contains(value); + bool has(String word) => words.contains( + word + .toLowerCase() + .replaceAll('’', "'") + .replaceAll('‘', "'") + .trim(), + ); + bool phrase(String value) { + final normVal = value + .toLowerCase() + .replaceAll('’', "'") + .replaceAll('‘', "'") + .replaceAll(RegExp(r"\s+"), " ") + .trim(); + return text.contains(normVal); + } 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'); diff --git a/kouyu_english/lib/core/generated_content.dart b/kouyu_english/lib/core/generated_content.dart index f05ee5e..683ddbd 100644 --- a/kouyu_english/lib/core/generated_content.dart +++ b/kouyu_english/lib/core/generated_content.dart @@ -122,6 +122,8 @@ bool matchesAdaptiveLessonAnswer(GeneratedLessonTask task, String answer) { String _normalizeAnswer(String value) => value .toLowerCase() + .replaceAll('’', "'") + .replaceAll('‘', "'") .replaceAll(RegExp(r"[^a-z0-9']+"), ' ') .trim() .replaceAll(RegExp(r'\s+'), ' '); diff --git a/kouyu_english/lib/core/review_feedback.dart b/kouyu_english/lib/core/review_feedback.dart index 80fef73..2ce932a 100644 --- a/kouyu_english/lib/core/review_feedback.dart +++ b/kouyu_english/lib/core/review_feedback.dart @@ -13,13 +13,20 @@ class ReviewCheckResult { 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 text = input.toLowerCase().replaceAll('’', "'"); + final text = _normalize(input); 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 has(String token) => tokens.contains(_normalize(token)); + bool phrase(String value) => text.contains(_normalize(value)); bool hasAny(Iterable values) => values.any(has); final introduction = phrase("i'm") || phrase('i am') || phrase('my name is'); @@ -68,9 +75,8 @@ class ReviewFeedback { '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(), - ), + _ when item.id.startsWith('A0-W') => + tokens.contains(_normalize(item.target)) || phrase(item.target), _ => tokens.length >= 2, }; return ReviewCheckResult( diff --git a/kouyu_english/lib/core/seed_courses.dart b/kouyu_english/lib/core/seed_courses.dart index 3e47e67..52e2278 100644 --- a/kouyu_english/lib/core/seed_courses.dart +++ b/kouyu_english/lib/core/seed_courses.dart @@ -1081,8 +1081,17 @@ const a0SegmentGrammarNotes = { String grammarNoteForSegment(String segmentId, String lessonId) => a0SegmentGrammarNotes[segmentId] ?? grammarNoteForLesson(lessonId); +String _normalizeDialogueInput(String response) => response + .toLowerCase() + .replaceAll('\u2019', "'") + .replaceAll('\u2018', "'") + .replaceAll('’', "'") + .replaceAll('‘', "'") + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + bool matchesSegmentDialogue(String segmentId, int stage, String response) { - final text = response.toLowerCase().replaceAll('’', "'"); + final text = _normalizeDialogueInput(response); final segment = a0LessonSegments.values .expand((segments) => segments) .where((item) => item.id == segmentId) @@ -1091,7 +1100,7 @@ bool matchesSegmentDialogue(String segmentId, int stage, String response) { return false; } bool hasAny(Iterable terms) => - terms.any((term) => text.contains(term)); + terms.any((term) => text.contains(_normalizeDialogueInput(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) { @@ -1102,7 +1111,7 @@ bool matchesSegmentDialogue(String segmentId, int stage, String response) { } bool matchesSegmentIndependent(String segmentId, String response) { - final text = response.toLowerCase().replaceAll('’', "'"); + final text = _normalizeDialogueInput(response); final segment = a0LessonSegments.values .expand((segments) => segments) .where((item) => item.id == segmentId) @@ -1110,17 +1119,18 @@ bool matchesSegmentIndependent(String segmentId, String response) { if (segment == null) return true; if (segmentId == 'a0-04-a' || segmentId == 'a0-04-b') { final count = segment.independentRequiredTerms - .where((term) => text.contains(term)) + .where((term) => text.contains(_normalizeDialogueInput(term))) .length; return count >= 3; } if (segmentId == 'a0-04-c') { return segment.independentRequiredTerms - .where((term) => text.contains(term)) + .where((term) => text.contains(_normalizeDialogueInput(term))) .length >= 3; } - return segment.independentRequiredTerms.any((term) => text.contains(term)); + return segment.independentRequiredTerms + .any((term) => text.contains(_normalizeDialogueInput(term))); } class LessonDialogue { @@ -1156,7 +1166,8 @@ bool _containsTerm(String text, String term) { return term.split(' + ').every((part) => _containsTerm(text, part.trim())); } if (term.startsWith('#')) return _matchesStructure(text, term); - final escaped = RegExp.escape(term.toLowerCase()); + final normTerm = _normalizeDialogueInput(term); + final escaped = RegExp.escape(normTerm); return RegExp('(? switch (token) { _ => 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) { diff --git a/kouyu_english/lib/core/writing_feedback.dart b/kouyu_english/lib/core/writing_feedback.dart index 7fc3630..c4c275d 100644 --- a/kouyu_english/lib/core/writing_feedback.dart +++ b/kouyu_english/lib/core/writing_feedback.dart @@ -11,18 +11,25 @@ class WritingCheckResult { class WritingFeedback { const WritingFeedback._(); + static String _normalize(String input) => input + .toLowerCase() + .replaceAll('’', "'") + .replaceAll('‘', "'") + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + static WritingCheckResult check( String lessonId, String input, { String? segmentId, }) { - final text = input.toLowerCase().replaceAll('’', "'"); + final text = _normalize(input); final words = RegExp( r"[a-z]+(?:'[a-z]+)?", ).allMatches(text).map((match) => match.group(0)!).toSet(); - bool has(String word) => words.contains(word); + bool has(String word) => words.contains(_normalize(word)); - bool hasPhrase(String phrase) => text.contains(phrase); + bool hasPhrase(String phrase) => text.contains(_normalize(phrase)); bool hasAny(Iterable choices) => choices.any(has); final hasIntroduction = @@ -40,7 +47,7 @@ class WritingFeedback { '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")), + 'a0-08-c' => hasItIs && (has('oclock') || hasPhrase("o'clock")), _ => switch (lessonId) { 'a0-01' => hasAny(['hello', 'hi']) && hasIntroduction, 'a0-02' => diff --git a/kouyu_english/lib/widgets/lexicon_lookup.dart b/kouyu_english/lib/widgets/lexicon_lookup.dart index 1cd9a82..913cda6 100644 --- a/kouyu_english/lib/widgets/lexicon_lookup.dart +++ b/kouyu_english/lib/widgets/lexicon_lookup.dart @@ -21,10 +21,20 @@ List get courseLexiconEntries { /// Finds a course item by exact query, then by the longest known phrase in it. VocabularyItem? findCourseLexicon(String text) { - final normalized = text.toLowerCase().replaceAll('’', "'").trim(); + final normalized = text + .toLowerCase() + .replaceAll('’', "'") + .replaceAll('‘', "'") + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); if (normalized.isEmpty) return null; return courseLexiconEntries.cast().firstWhere((entry) { - final word = entry!.word.toLowerCase().replaceAll('’', "'"); + final word = entry!.word + .toLowerCase() + .replaceAll('’', "'") + .replaceAll('‘', "'") + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); if (normalized == word) return true; final pattern = RegExp( r'(? extractCourseLexiconPhrases(String text) { - final normalized = text.toLowerCase().replaceAll('’', "'").trim(); + final normalized = text + .toLowerCase() + .replaceAll('’', "'") + .replaceAll('‘', "'") + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); if (normalized.isEmpty) return const []; final matched = []; final seen = {}; for (final item in courseLexiconEntries) { - final word = item.word.toLowerCase().replaceAll('’', "'"); + final word = item.word + .toLowerCase() + .replaceAll('’', "'") + .replaceAll('‘', "'") + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); if (word.isEmpty || word == normalized) continue; final pattern = RegExp( r'(?