From 33b05f6f24dccd736fd7c8c27e50e92e47e488b4 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 19 Jan 2026 15:34:20 -0500 Subject: [PATCH] setup for grammar error practice --- lib/l10n/intl_en.arb | 4 +- .../construct_use_type_enum.dart | 18 ++++++++ .../grammar_error_practice_generator.dart | 42 +++++++++++++++++++ .../choreographer/igc/span_data_model.dart | 5 ++- .../activity_type_enum.dart | 19 ++++++++- .../practice_activity_model.dart | 11 +++++ .../practice_generation_repo.dart | 3 ++ 7 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 lib/pangea/analytics_practice/grammar_error_practice_generator.dart diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index be647da95..16fb400ac 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5051,5 +5051,7 @@ "practiceGrammar": "Practice grammar", "notEnoughToPractice": "Send more messages to unlock practice", "constructUseCorGCDesc": "Correct grammar category practice", - "constructUseIncGCDesc": "Incorrect grammar category practice" + "constructUseIncGCDesc": "Incorrect grammar category practice", + "constructUseCorGEDesc": "Correct grammar error practice", + "constructUseIncGEDesc": "Incorrect grammar error practice" } diff --git a/lib/pangea/analytics_misc/construct_use_type_enum.dart b/lib/pangea/analytics_misc/construct_use_type_enum.dart index 028759fe4..ec5cae6d8 100644 --- a/lib/pangea/analytics_misc/construct_use_type_enum.dart +++ b/lib/pangea/analytics_misc/construct_use_type_enum.dart @@ -86,6 +86,10 @@ enum ConstructUseTypeEnum { // grammar category activity corGC, incGC, + + // grammar error activity + corGE, + incGE, } extension ConstructUseTypeExtension on ConstructUseTypeEnum { @@ -171,6 +175,10 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { return L10n.of(context).constructUseCorGCDesc; case ConstructUseTypeEnum.incGC: return L10n.of(context).constructUseIncGCDesc; + case ConstructUseTypeEnum.corGE: + return L10n.of(context).constructUseCorGEDesc; + case ConstructUseTypeEnum.incGE: + return L10n.of(context).constructUseIncGEDesc; } } @@ -213,6 +221,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.ignM: case ConstructUseTypeEnum.corGC: case ConstructUseTypeEnum.incGC: + case ConstructUseTypeEnum.corGE: + case ConstructUseTypeEnum.incGE: return ActivityTypeEnum.morphId.icon; case ConstructUseTypeEnum.em: return ActivityTypeEnum.emoji.icon; @@ -246,6 +256,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.corLM: case ConstructUseTypeEnum.corLA: case ConstructUseTypeEnum.corGC: + case ConstructUseTypeEnum.corGE: return 5; case ConstructUseTypeEnum.pvm: @@ -287,6 +298,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.incLM: case ConstructUseTypeEnum.incLA: case ConstructUseTypeEnum.incGC: + case ConstructUseTypeEnum.incGE: return -1; case ConstructUseTypeEnum.incPA: @@ -340,6 +352,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.bonus: case ConstructUseTypeEnum.corGC: case ConstructUseTypeEnum.incGC: + case ConstructUseTypeEnum.corGE: + case ConstructUseTypeEnum.incGE: return false; } } @@ -385,6 +399,8 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.incLM: case ConstructUseTypeEnum.corGC: case ConstructUseTypeEnum.incGC: + case ConstructUseTypeEnum.corGE: + case ConstructUseTypeEnum.incGE: return LearningSkillsEnum.reading; case ConstructUseTypeEnum.pvm: return LearningSkillsEnum.speaking; @@ -415,6 +431,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.corLM: case ConstructUseTypeEnum.corLA: case ConstructUseTypeEnum.corGC: + case ConstructUseTypeEnum.corGE: return SpaceAnalyticsSummaryEnum.numChoicesCorrect; case ConstructUseTypeEnum.incIt: @@ -428,6 +445,7 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.incLM: case ConstructUseTypeEnum.incLA: case ConstructUseTypeEnum.incGC: + case ConstructUseTypeEnum.incGE: return SpaceAnalyticsSummaryEnum.numChoicesIncorrect; case ConstructUseTypeEnum.ignIt: diff --git a/lib/pangea/analytics_practice/grammar_error_practice_generator.dart b/lib/pangea/analytics_practice/grammar_error_practice_generator.dart new file mode 100644 index 000000000..66ec9c5db --- /dev/null +++ b/lib/pangea/analytics_practice/grammar_error_practice_generator.dart @@ -0,0 +1,42 @@ +import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart'; +import 'package:fluffychat/pangea/practice_activities/multiple_choice_activity_model.dart'; +import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class GrammarErrorPracticeGenerator { + static Future get( + MessageActivityRequest req, + ) async { + final igcMatch = target.igcMatch; + assert(igcMatch.bestChoice != null, 'IGC match must have a best choice'); + assert(igcMatch.choices != null, 'IGC match must have choices'); + + final errorSpan = igcMatch.errorSpan; + final correctChoice = igcMatch.bestChoice!.value; + final choices = igcMatch.choices!.map((c) => c.value).toList(); + + final choiceTokens = target.tokens.where( + (token) => choices.any( + (choice) => choice.contains(token.text.content), + ), + ); + + assert( + choiceTokens.isNotEmpty, + 'At least one token should match the error choices', + ); + + choices.add(errorSpan); + choices.shuffle(); + return MessageActivityResponse( + activity: GrammarErrorPracticeActivityModel( + tokens: choiceTokens.toList(), + langCode: req.userL2, + multipleChoiceContent: MultipleChoiceActivity( + choices: choices.toSet(), + answers: {correctChoice}, + ), + ), + ); + } +} diff --git a/lib/pangea/choreographer/igc/span_data_model.dart b/lib/pangea/choreographer/igc/span_data_model.dart index 220e245f2..ac65a1347 100644 --- a/lib/pangea/choreographer/igc/span_data_model.dart +++ b/lib/pangea/choreographer/igc/span_data_model.dart @@ -132,6 +132,9 @@ class SpanData { return choices![index]; } + String get errorSpan => + fullText.characters.skip(offset).take(length).toString(); + bool isNormalizationError() { final correctChoice = choices ?.firstWhereOrNull( @@ -139,8 +142,6 @@ class SpanData { ) ?.value; - final errorSpan = fullText.characters.skip(offset).take(length).toString(); - final l2Code = MatrixState.pangeaController.userController.userL2?.langCodeShort; diff --git a/lib/pangea/practice_activities/activity_type_enum.dart b/lib/pangea/practice_activities/activity_type_enum.dart index 02a9e9c91..b8e493d9d 100644 --- a/lib/pangea/practice_activities/activity_type_enum.dart +++ b/lib/pangea/practice_activities/activity_type_enum.dart @@ -15,7 +15,8 @@ enum ActivityTypeEnum { messageMeaning, lemmaMeaning, lemmaAudio, - grammarCategory; + grammarCategory, + grammarError; bool get includeTTSOnClick { switch (this) { @@ -30,6 +31,7 @@ enum ActivityTypeEnum { case ActivityTypeEnum.lemmaAudio: case ActivityTypeEnum.lemmaMeaning: case ActivityTypeEnum.grammarCategory: + case ActivityTypeEnum.grammarError: return true; } } @@ -68,6 +70,9 @@ enum ActivityTypeEnum { case 'grammar_category': case 'grammarCategory': return ActivityTypeEnum.grammarCategory; + case 'grammar_error': + case 'grammarError': + return ActivityTypeEnum.grammarError; default: throw Exception('Unknown activity type: $split'); } @@ -128,6 +133,11 @@ enum ActivityTypeEnum { ConstructUseTypeEnum.corGC, ConstructUseTypeEnum.incGC, ]; + case ActivityTypeEnum.grammarError: + return [ + ConstructUseTypeEnum.corGE, + ConstructUseTypeEnum.incGE, + ]; } } @@ -153,6 +163,8 @@ enum ActivityTypeEnum { return ConstructUseTypeEnum.corLM; case ActivityTypeEnum.grammarCategory: return ConstructUseTypeEnum.corGC; + case ActivityTypeEnum.grammarError: + return ConstructUseTypeEnum.corGE; } } @@ -178,6 +190,8 @@ enum ActivityTypeEnum { return ConstructUseTypeEnum.incLM; case ActivityTypeEnum.grammarCategory: return ConstructUseTypeEnum.incGC; + case ActivityTypeEnum.grammarError: + return ConstructUseTypeEnum.incGE; } } @@ -198,6 +212,7 @@ enum ActivityTypeEnum { return Icons.format_shapes; case ActivityTypeEnum.messageMeaning: case ActivityTypeEnum.grammarCategory: + case ActivityTypeEnum.grammarError: return Icons.star; // TODO: Add to L10n } } @@ -217,6 +232,7 @@ enum ActivityTypeEnum { case ActivityTypeEnum.lemmaMeaning: case ActivityTypeEnum.lemmaAudio: case ActivityTypeEnum.grammarCategory: + case ActivityTypeEnum.grammarError: return 1; } } @@ -235,6 +251,7 @@ enum ActivityTypeEnum { static List get _grammarPracticeTypes => [ ActivityTypeEnum.grammarCategory, + ActivityTypeEnum.grammarError, ]; static List analyticsPracticeTypes( diff --git a/lib/pangea/practice_activities/practice_activity_model.dart b/lib/pangea/practice_activities/practice_activity_model.dart index e9d37f2ba..36d284f0b 100644 --- a/lib/pangea/practice_activities/practice_activity_model.dart +++ b/lib/pangea/practice_activities/practice_activity_model.dart @@ -48,6 +48,8 @@ sealed class PracticeActivityModel { return ActivityTypeEnum.morphId; case WordListeningPracticeActivityModel(): return ActivityTypeEnum.wordFocusListening; + case GrammarErrorPracticeActivityModel(): + return ActivityTypeEnum.grammarError; } } @@ -350,6 +352,15 @@ class LemmaPracticeActivityModel extends MultipleChoicePracticeActivityModel { }); } +class GrammarErrorPracticeActivityModel + extends MultipleChoicePracticeActivityModel { + GrammarErrorPracticeActivityModel({ + required super.tokens, + required super.langCode, + required super.multipleChoiceContent, + }); +} + class EmojiPracticeActivityModel extends MatchPracticeActivityModel { EmojiPracticeActivityModel({ required super.tokens, diff --git a/lib/pangea/practice_activities/practice_generation_repo.dart b/lib/pangea/practice_activities/practice_generation_repo.dart index 68d3cb821..3ff418594 100644 --- a/lib/pangea/practice_activities/practice_generation_repo.dart +++ b/lib/pangea/practice_activities/practice_generation_repo.dart @@ -9,6 +9,7 @@ import 'package:async/async.dart'; import 'package:get_storage/get_storage.dart'; import 'package:http/http.dart'; +import 'package:fluffychat/pangea/analytics_practice/grammar_error_practice_generator.dart'; import 'package:fluffychat/pangea/analytics_practice/morph_category_activity_generator.dart'; import 'package:fluffychat/pangea/analytics_practice/vocab_audio_activity_generator.dart'; import 'package:fluffychat/pangea/analytics_practice/vocab_meaning_activity_generator.dart'; @@ -128,6 +129,8 @@ class PracticeRepo { return VocabAudioActivityGenerator.get(req); case ActivityTypeEnum.grammarCategory: return MorphCategoryActivityGenerator.get(req); + case ActivityTypeEnum.grammarError: + return GrammarErrorPracticeGenerator.get(req); case ActivityTypeEnum.morphId: return MorphActivityGenerator.get(req); case ActivityTypeEnum.wordMeaning: