diff --git a/lib/pangea/analytics_practice/analytics_practice_page.dart b/lib/pangea/analytics_practice/analytics_practice_page.dart index d85725f48..e9d83baca 100644 --- a/lib/pangea/analytics_practice/analytics_practice_page.dart +++ b/lib/pangea/analytics_practice/analytics_practice_page.dart @@ -17,12 +17,11 @@ import 'package:fluffychat/pangea/analytics_practice/analytics_practice_view.dar import 'package:fluffychat/pangea/common/utils/async_state.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/lemmas/lemma_info_repo.dart'; -import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; 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/pangea/practice_activities/practice_generation_repo.dart'; import 'package:fluffychat/pangea/practice_activities/practice_target.dart'; +import 'package:fluffychat/pangea/toolbar/message_practice/practice_record_controller.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -74,8 +73,8 @@ class AnalyticsPracticeState extends State final ValueNotifier progressNotifier = ValueNotifier(0.0); - final Map> _choiceTexts = {}; - final Map> _choiceEmojis = {}; + final Map> _choiceTexts = {}; + final Map> _choiceEmojis = {}; StreamSubscription? _languageStreamSubscription; @@ -120,16 +119,16 @@ class AnalyticsPracticeState extends State Matrix.of(context).analyticsDataService; List filteredChoices( - PracticeTarget target, - MultipleChoiceActivity activity, + MultipleChoicePracticeActivityModel activity, ) { - final choices = activity.choices.toList(); - final answer = activity.answers.first; + final content = activity.multipleChoiceContent; + final choices = content.choices.toList(); + final answer = content.answers.first; final filtered = []; final seenTexts = {}; for (final id in choices) { - final text = getChoiceText(target, id); + final text = getChoiceText(activity.storageKey, id); if (seenTexts.contains(text)) { if (id != answer) { @@ -143,7 +142,7 @@ class AnalyticsPracticeState extends State filtered[index] = PracticeChoice( choiceId: id, choiceText: text, - choiceEmoji: getChoiceEmoji(target, id), + choiceEmoji: getChoiceEmoji(activity.storageKey, id), ); } continue; @@ -154,7 +153,7 @@ class AnalyticsPracticeState extends State PracticeChoice( choiceId: id, choiceText: text, - choiceEmoji: getChoiceEmoji(target, id), + choiceEmoji: getChoiceEmoji(activity.storageKey, id), ), ); } @@ -162,21 +161,21 @@ class AnalyticsPracticeState extends State return filtered; } - String getChoiceText(PracticeTarget target, String choiceId) { + String getChoiceText(String key, String choiceId) { if (widget.type == ConstructTypeEnum.morph) { return choiceId; } - if (_choiceTexts.containsKey(target) && - _choiceTexts[target]!.containsKey(choiceId)) { - return _choiceTexts[target]![choiceId]!; + if (_choiceTexts.containsKey(key) && + _choiceTexts[key]!.containsKey(choiceId)) { + return _choiceTexts[key]![choiceId]!; } final cId = ConstructIdentifier.fromString(choiceId); return cId?.lemma ?? choiceId; } - String? getChoiceEmoji(PracticeTarget target, String choiceId) { + String? getChoiceEmoji(String key, String choiceId) { if (widget.type == ConstructTypeEnum.morph) return null; - return _choiceEmojis[target]?[choiceId]; + return _choiceEmojis[key]?[choiceId]; } String choiceTargetId(String choiceId) => @@ -297,7 +296,7 @@ class AnalyticsPracticeState extends State final res = await _fetchActivity(req); if (!mounted) return; - activityTarget.value = req.practiceTarget; + activityTarget.value = req.target; activityState.value = AsyncState.loaded(res); } catch (e) { if (!mounted) return; @@ -311,12 +310,7 @@ class AnalyticsPracticeState extends State Future _fillActivityQueue(List requests) async { for (final request in requests) { final completer = Completer(); - _queue.add( - MapEntry( - request.practiceTarget, - completer, - ), - ); + _queue.add(MapEntry(request.target, completer)); try { final res = await _fetchActivity(request); @@ -346,16 +340,16 @@ class AnalyticsPracticeState extends State final activityModel = result.result as MultipleChoicePracticeActivityModel; // Prefetch lemma info for meaning activities before marking ready - if (activityModel.activityType == ActivityTypeEnum.lemmaMeaning) { + if (activityModel is VocabMeaningPracticeActivityModel) { final choices = activityModel.multipleChoiceContent.choices.toList(); - await _fetchLemmaInfo(activityModel.practiceTarget, choices); + await _fetchLemmaInfo(activityModel.storageKey, choices); } return activityModel; } Future _fetchLemmaInfo( - PracticeTarget target, + String requestKey, List choiceIds, ) async { final texts = {}; @@ -375,22 +369,25 @@ class AnalyticsPracticeState extends State emojis[id] = res.result!.emoji.firstOrNull; } - _choiceTexts.putIfAbsent(target, () => {}); - _choiceEmojis.putIfAbsent(target, () => {}); + _choiceTexts.putIfAbsent(requestKey, () => {}); + _choiceEmojis.putIfAbsent(requestKey, () => {}); - _choiceTexts[target]!.addAll(texts); - _choiceEmojis[target]!.addAll(emojis); + _choiceTexts[requestKey]!.addAll(texts); + _choiceEmojis[requestKey]!.addAll(emojis); } Future onSelectChoice( - ConstructIdentifier choiceConstruct, String choiceContent, ) async { if (_currentActivity == null) return; final activity = _currentActivity!; // Update activity record - activity.onMultipleChoiceSelect(choiceConstruct, choiceContent); + PracticeRecordController.onSelectChoice( + choiceContent, + activity.tokens.first, + activity, + ); final use = activity.constructUse(choiceContent); _sessionLoader.value!.submitAnswer(use); diff --git a/lib/pangea/analytics_practice/analytics_practice_session_model.dart b/lib/pangea/analytics_practice/analytics_practice_session_model.dart index 9c27769e9..2b35e45e9 100644 --- a/lib/pangea/analytics_practice/analytics_practice_session_model.dart +++ b/lib/pangea/analytics_practice/analytics_practice_session_model.dart @@ -38,9 +38,7 @@ class AnalyticsPracticeSessionModel { userL1: userL1, userL2: userL2, activityQualityFeedback: null, - targetTokens: target.tokens, - targetType: target.activityType, - targetMorphFeature: target.morphFeature, + target: target, ); }).toList(); } diff --git a/lib/pangea/analytics_practice/analytics_practice_view.dart b/lib/pangea/analytics_practice/analytics_practice_view.dart index 3e9379d26..8dfc22999 100644 --- a/lib/pangea/analytics_practice/analytics_practice_view.dart +++ b/lib/pangea/analytics_practice/analytics_practice_view.dart @@ -236,10 +236,7 @@ class _ActivityChoicesWidget extends StatelessWidget { AsyncLoaded(:final value) => LayoutBuilder( builder: (context, constraints) { - final choices = controller.filteredChoices( - value.practiceTarget, - value.multipleChoiceContent, - ); + final choices = controller.filteredChoices(value); final constrainedHeight = constraints.maxHeight.clamp(0.0, 400.0); final cardHeight = (constrainedHeight / (choices.length + 1)) @@ -258,7 +255,6 @@ class _ActivityChoicesWidget extends StatelessWidget { controller.choiceTargetId(choice.choiceId), choiceId: choice.choiceId, onPressed: () => controller.onSelectChoice( - value.targetTokens.first.vocabConstructID, choice.choiceId, ), cardHeight: cardHeight, @@ -307,7 +303,7 @@ class _ChoiceCard extends StatelessWidget { Widget build(BuildContext context) { final isCorrect = activity.multipleChoiceContent.isCorrect(choiceId); final activityType = activity.activityType; - final constructId = activity.targetTokens.first.vocabConstructID; + final constructId = activity.tokens.first.vocabConstructID; switch (activity.activityType) { case ActivityTypeEnum.lemmaMeaning: diff --git a/lib/pangea/analytics_practice/morph_category_activity_generator.dart b/lib/pangea/analytics_practice/morph_category_activity_generator.dart index adae7a3ea..545e466ba 100644 --- a/lib/pangea/analytics_practice/morph_category_activity_generator.dart +++ b/lib/pangea/analytics_practice/morph_category_activity_generator.dart @@ -11,14 +11,14 @@ class MorphCategoryActivityGenerator { static Future get( MessageActivityRequest req, ) async { - if (req.targetMorphFeature == null) { + if (req.target.morphFeature == null) { throw ArgumentError( "MorphCategoryActivityGenerator requires a targetMorphFeature", ); } - final feature = req.targetMorphFeature!; - final morphTag = req.targetTokens.first.getMorphTag(feature); + final feature = req.target.morphFeature!; + final morphTag = req.target.tokens.first.getMorphTag(feature); if (morphTag == null) { throw ArgumentError( "Token does not have the specified morph feature", @@ -51,7 +51,7 @@ class MorphCategoryActivityGenerator { return MessageActivityResponse( activity: MorphCategoryPracticeActivityModel( - targetTokens: [req.targetTokens.first], + tokens: req.target.tokens, langCode: req.userL2, morphFeature: feature, multipleChoiceContent: MultipleChoiceActivity( diff --git a/lib/pangea/analytics_practice/vocab_audio_activity_generator.dart b/lib/pangea/analytics_practice/vocab_audio_activity_generator.dart index bcbbbb721..7b2954f51 100644 --- a/lib/pangea/analytics_practice/vocab_audio_activity_generator.dart +++ b/lib/pangea/analytics_practice/vocab_audio_activity_generator.dart @@ -7,7 +7,7 @@ class VocabAudioActivityGenerator { static Future get( MessageActivityRequest req, ) async { - final token = req.targetTokens.first; + final token = req.target.tokens.first; final choices = await LemmaActivityGenerator.lemmaActivityDistractors(token); @@ -16,7 +16,7 @@ class VocabAudioActivityGenerator { return MessageActivityResponse( activity: VocabAudioPracticeActivityModel( - targetTokens: [token], + tokens: req.target.tokens, langCode: req.userL2, multipleChoiceContent: MultipleChoiceActivity( choices: choicesList.toSet(), diff --git a/lib/pangea/analytics_practice/vocab_meaning_activity_generator.dart b/lib/pangea/analytics_practice/vocab_meaning_activity_generator.dart index cc258a76b..7acc77b0d 100644 --- a/lib/pangea/analytics_practice/vocab_meaning_activity_generator.dart +++ b/lib/pangea/analytics_practice/vocab_meaning_activity_generator.dart @@ -7,7 +7,7 @@ class VocabMeaningActivityGenerator { static Future get( MessageActivityRequest req, ) async { - final token = req.targetTokens.first; + final token = req.target.tokens.first; final choices = await LemmaActivityGenerator.lemmaActivityDistractors(token); @@ -19,7 +19,7 @@ class VocabMeaningActivityGenerator { return MessageActivityResponse( activity: VocabMeaningPracticeActivityModel( - targetTokens: [token], + tokens: req.target.tokens, langCode: req.userL2, multipleChoiceContent: MultipleChoiceActivity( choices: constructIdChoices, diff --git a/lib/pangea/practice_activities/activity_type_enum.dart b/lib/pangea/practice_activities/activity_type_enum.dart index 42696c226..02a9e9c91 100644 --- a/lib/pangea/practice_activities/activity_type_enum.dart +++ b/lib/pangea/practice_activities/activity_type_enum.dart @@ -228,12 +228,12 @@ enum ActivityTypeEnum { ActivityTypeEnum.morphId, ]; - static List get vocabPracticeTypes => [ + static List get _vocabPracticeTypes => [ ActivityTypeEnum.lemmaMeaning, // ActivityTypeEnum.lemmaAudio, ]; - static List get grammarPracticeTypes => [ + static List get _grammarPracticeTypes => [ ActivityTypeEnum.grammarCategory, ]; @@ -242,26 +242,9 @@ enum ActivityTypeEnum { ) { switch (constructType) { case ConstructTypeEnum.vocab: - return vocabPracticeTypes; + return _vocabPracticeTypes; case ConstructTypeEnum.morph: - return grammarPracticeTypes; - } - } - - ConstructTypeEnum get constructType { - switch (this) { - case ActivityTypeEnum.wordMeaning: - case ActivityTypeEnum.wordFocusListening: - case ActivityTypeEnum.hiddenWordListening: - case ActivityTypeEnum.lemmaId: - case ActivityTypeEnum.emoji: - case ActivityTypeEnum.messageMeaning: - case ActivityTypeEnum.lemmaMeaning: - case ActivityTypeEnum.lemmaAudio: - return ConstructTypeEnum.vocab; - case ActivityTypeEnum.morphId: - case ActivityTypeEnum.grammarCategory: - return ConstructTypeEnum.morph; + return _grammarPracticeTypes; } } } diff --git a/lib/pangea/practice_activities/emoji_activity_generator.dart b/lib/pangea/practice_activities/emoji_activity_generator.dart index 817162a8b..5b8881a5a 100644 --- a/lib/pangea/practice_activities/emoji_activity_generator.dart +++ b/lib/pangea/practice_activities/emoji_activity_generator.dart @@ -12,7 +12,7 @@ class EmojiActivityGenerator { MessageActivityRequest req, { required Map messageInfo, }) async { - if (req.targetTokens.length <= 1) { + if (req.target.tokens.length <= 1) { throw Exception("Emoji activity requires at least 2 tokens"); } @@ -27,7 +27,7 @@ class EmojiActivityGenerator { final List missingEmojis = []; final List usedEmojis = []; - for (final token in req.targetTokens) { + for (final token in req.target.tokens) { final userSavedEmoji = token.vocabConstructID.userSetEmoji; if (userSavedEmoji != null && !usedEmojis.contains(userSavedEmoji)) { matchInfo[token.vocabForm] = [userSavedEmoji]; @@ -65,7 +65,7 @@ class EmojiActivityGenerator { return MessageActivityResponse( activity: EmojiPracticeActivityModel( - targetTokens: req.targetTokens, + tokens: req.target.tokens, langCode: req.userL2, matchContent: PracticeMatchActivity( matchInfo: matchInfo, diff --git a/lib/pangea/practice_activities/lemma_activity_generator.dart b/lib/pangea/practice_activities/lemma_activity_generator.dart index 198c3161a..83ff095de 100644 --- a/lib/pangea/practice_activities/lemma_activity_generator.dart +++ b/lib/pangea/practice_activities/lemma_activity_generator.dart @@ -14,15 +14,15 @@ class LemmaActivityGenerator { static Future get( MessageActivityRequest req, ) async { - debugger(when: kDebugMode && req.targetTokens.length != 1); + debugger(when: kDebugMode && req.target.tokens.length != 1); - final token = req.targetTokens.first; + final token = req.target.tokens.first; final choices = await lemmaActivityDistractors(token); // TODO - modify MultipleChoiceActivity flow to allow no correct answer return MessageActivityResponse( activity: LemmaPracticeActivityModel( - targetTokens: [token], + tokens: req.target.tokens, langCode: req.userL2, multipleChoiceContent: MultipleChoiceActivity( choices: choices.map((c) => c.lemma).toSet(), diff --git a/lib/pangea/practice_activities/lemma_meaning_activity_generator.dart b/lib/pangea/practice_activities/lemma_meaning_activity_generator.dart index 981668c20..3e32611b0 100644 --- a/lib/pangea/practice_activities/lemma_meaning_activity_generator.dart +++ b/lib/pangea/practice_activities/lemma_meaning_activity_generator.dart @@ -15,7 +15,7 @@ class LemmaMeaningActivityGenerator { required Map messageInfo, }) async { final List>> lemmaInfoFutures = req - .targetTokens + .target.tokens .map((token) => token.vocabConstructID.getLemmaInfo(messageInfo)) .toList(); @@ -27,13 +27,13 @@ class LemmaMeaningActivityGenerator { } final Map> matchInfo = Map.fromIterables( - req.targetTokens.map((token) => token.vocabForm), + req.target.tokens.map((token) => token.vocabForm), lemmaInfos.map((lemmaInfo) => [lemmaInfo.asValue!.value.meaning]), ); return MessageActivityResponse( activity: LemmaMeaningPracticeActivityModel( - targetTokens: req.targetTokens, + tokens: req.target.tokens, langCode: req.userL2, matchContent: PracticeMatchActivity( matchInfo: matchInfo, diff --git a/lib/pangea/practice_activities/message_activity_request.dart b/lib/pangea/practice_activities/message_activity_request.dart index 824cad45a..fef5c5243 100644 --- a/lib/pangea/practice_activities/message_activity_request.dart +++ b/lib/pangea/practice_activities/message_activity_request.dart @@ -1,9 +1,5 @@ -import 'package:collection/collection.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; -import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; -import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart'; import 'package:fluffychat/pangea/practice_activities/practice_target.dart'; @@ -42,70 +38,49 @@ class ActivityQualityFeedback { class MessageActivityRequest { final String userL1; final String userL2; - - final List targetTokens; - final ActivityTypeEnum targetType; - final MorphFeaturesEnum? targetMorphFeature; - + final PracticeTarget target; final ActivityQualityFeedback? activityQualityFeedback; MessageActivityRequest({ required this.userL1, required this.userL2, required this.activityQualityFeedback, - required this.targetTokens, - required this.targetType, - required this.targetMorphFeature, + required this.target, }) { - if (targetTokens.isEmpty) { + if (target.tokens.isEmpty) { throw Exception('Target tokens must not be empty'); } } - String get activityText { - switch (targetType) { - case ActivityTypeEnum.grammarCategory: - return "${targetTokens.first.vocabConstructID.lemma}: ${targetMorphFeature!.name}"; - default: - return targetTokens.first.vocabConstructID.lemma; - } - } - Map toJson() { return { 'user_l1': userL1, 'user_l2': userL2, 'activity_quality_feedback': activityQualityFeedback?.toJson(), - 'target_tokens': targetTokens.map((e) => e.toJson()).toList(), - 'target_type': targetType.name, - 'target_morph_feature': targetMorphFeature, + 'target_tokens': target.tokens.map((e) => e.toJson()).toList(), + 'target_type': target.activityType.name, + 'target_morph_feature': target.morphFeature, }; } - PracticeTarget get practiceTarget => PracticeTarget( - activityType: targetType, - tokens: targetTokens, - morphFeature: targetMorphFeature, - ); - @override bool operator ==(Object other) { if (identical(this, other)) return true; return other is MessageActivityRequest && - other.targetType == targetType && + other.userL1 == userL1 && + other.userL2 == userL2 && + other.target == target && other.activityQualityFeedback?.feedbackText == - activityQualityFeedback?.feedbackText && - const ListEquality().equals(other.targetTokens, targetTokens) && - other.targetMorphFeature == targetMorphFeature; + activityQualityFeedback?.feedbackText; } @override int get hashCode { - return targetType.hashCode ^ - activityQualityFeedback.hashCode ^ - targetTokens.hashCode ^ - targetMorphFeature.hashCode; + return activityQualityFeedback.hashCode ^ + target.hashCode ^ + userL1.hashCode ^ + userL2.hashCode; } } diff --git a/lib/pangea/practice_activities/morph_activity_generator.dart b/lib/pangea/practice_activities/morph_activity_generator.dart index 434929490..0c2e2bf5b 100644 --- a/lib/pangea/practice_activities/morph_activity_generator.dart +++ b/lib/pangea/practice_activities/morph_activity_generator.dart @@ -17,13 +17,13 @@ class MorphActivityGenerator { static MessageActivityResponse get( MessageActivityRequest req, ) { - debugger(when: kDebugMode && req.targetTokens.length != 1); + debugger(when: kDebugMode && req.target.tokens.length != 1); - debugger(when: kDebugMode && req.targetMorphFeature == null); + debugger(when: kDebugMode && req.target.morphFeature == null); - final PangeaToken token = req.targetTokens.first; + final PangeaToken token = req.target.tokens.first; - final MorphFeaturesEnum morphFeature = req.targetMorphFeature!; + final MorphFeaturesEnum morphFeature = req.target.morphFeature!; final String? morphTag = token.getMorphTag(morphFeature); if (morphTag == null) { @@ -38,7 +38,7 @@ class MorphActivityGenerator { return MessageActivityResponse( activity: MorphMatchPracticeActivityModel( - targetTokens: req.targetTokens, + tokens: req.target.tokens, langCode: req.userL2, morphFeature: morphFeature, multipleChoiceContent: MultipleChoiceActivity( diff --git a/lib/pangea/practice_activities/practice_activity_model.dart b/lib/pangea/practice_activities/practice_activity_model.dart index eac79e60a..e9d37f2ba 100644 --- a/lib/pangea/practice_activities/practice_activity_model.dart +++ b/lib/pangea/practice_activities/practice_activity_model.dart @@ -3,31 +3,54 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; -import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; import 'package:fluffychat/pangea/practice_activities/multiple_choice_activity_model.dart'; -import 'package:fluffychat/pangea/practice_activities/practice_choice.dart'; import 'package:fluffychat/pangea/practice_activities/practice_match.dart'; import 'package:fluffychat/pangea/practice_activities/practice_target.dart'; sealed class PracticeActivityModel { - final List targetTokens; - final ActivityTypeEnum activityType; + final List tokens; final String langCode; const PracticeActivityModel({ - required this.targetTokens, + required this.tokens, required this.langCode, - required this.activityType, }); + String get storageKey => + '${activityType.name}-${tokens.map((e) => e.text.content).join("-")}'; + PracticeTarget get practiceTarget => PracticeTarget( - tokens: targetTokens, activityType: activityType, + tokens: tokens, + morphFeature: this is MorphPracticeActivityModel + ? (this as MorphPracticeActivityModel).morphFeature + : null, ); + ActivityTypeEnum get activityType { + switch (this) { + case MorphCategoryPracticeActivityModel(): + return ActivityTypeEnum.grammarCategory; + case VocabAudioPracticeActivityModel(): + return ActivityTypeEnum.lemmaAudio; + case VocabMeaningPracticeActivityModel(): + return ActivityTypeEnum.lemmaMeaning; + case EmojiPracticeActivityModel(): + return ActivityTypeEnum.emoji; + case LemmaPracticeActivityModel(): + return ActivityTypeEnum.lemmaId; + case LemmaMeaningPracticeActivityModel(): + return ActivityTypeEnum.wordMeaning; + case MorphMatchPracticeActivityModel(): + return ActivityTypeEnum.morphId; + case WordListeningPracticeActivityModel(): + return ActivityTypeEnum.wordFocusListening; + } + } + factory PracticeActivityModel.fromJson(Map json) { if (json['lang_code'] is! String) { Sentry.addBreadcrumb( @@ -83,7 +106,7 @@ sealed class PracticeActivityModel { ); return MorphCategoryPracticeActivityModel( langCode: langCode, - targetTokens: tokens, + tokens: tokens, morphFeature: morph!, multipleChoiceContent: multipleChoiceContent!, ); @@ -94,7 +117,7 @@ sealed class PracticeActivityModel { ); return VocabAudioPracticeActivityModel( langCode: langCode, - targetTokens: tokens, + tokens: tokens, multipleChoiceContent: multipleChoiceContent!, ); case ActivityTypeEnum.lemmaMeaning: @@ -104,7 +127,7 @@ sealed class PracticeActivityModel { ); return VocabMeaningPracticeActivityModel( langCode: langCode, - targetTokens: tokens, + tokens: tokens, multipleChoiceContent: multipleChoiceContent!, ); case ActivityTypeEnum.emoji: @@ -114,7 +137,7 @@ sealed class PracticeActivityModel { ); return EmojiPracticeActivityModel( langCode: langCode, - targetTokens: tokens, + tokens: tokens, matchContent: matchContent!, ); case ActivityTypeEnum.lemmaId: @@ -124,7 +147,7 @@ sealed class PracticeActivityModel { ); return LemmaPracticeActivityModel( langCode: langCode, - targetTokens: tokens, + tokens: tokens, multipleChoiceContent: multipleChoiceContent!, ); case ActivityTypeEnum.wordMeaning: @@ -134,7 +157,7 @@ sealed class PracticeActivityModel { ); return LemmaMeaningPracticeActivityModel( langCode: langCode, - targetTokens: tokens, + tokens: tokens, matchContent: matchContent!, ); case ActivityTypeEnum.morphId: @@ -148,7 +171,7 @@ sealed class PracticeActivityModel { ); return MorphMatchPracticeActivityModel( langCode: langCode, - targetTokens: tokens, + tokens: tokens, morphFeature: morph!, multipleChoiceContent: multipleChoiceContent!, ); @@ -159,7 +182,7 @@ sealed class PracticeActivityModel { ); return WordListeningPracticeActivityModel( langCode: langCode, - targetTokens: tokens, + tokens: tokens, matchContent: matchContent!, ); default: @@ -171,7 +194,7 @@ sealed class PracticeActivityModel { return { 'lang_code': langCode, 'activity_type': activityType.name, - 'target_tokens': targetTokens.map((e) => e.toJson()).toList(), + 'target_tokens': tokens.map((e) => e.toJson()).toList(), }; } } @@ -180,40 +203,18 @@ sealed class MultipleChoicePracticeActivityModel extends PracticeActivityModel { final MultipleChoiceActivity multipleChoiceContent; MultipleChoicePracticeActivityModel({ - required super.targetTokens, + required super.tokens, required super.langCode, - required super.activityType, required this.multipleChoiceContent, }); - bool onMultipleChoiceSelect( - ConstructIdentifier choiceConstruct, - String choice, - ) { - if (practiceTarget.isComplete || - practiceTarget.record.alreadyHasMatchResponse( - choiceConstruct, - choice, - )) { - // the user has already selected this choice - // so we don't want to record it again - return false; - } - - final bool isCorrect = multipleChoiceContent.isCorrect(choice); - practiceTarget.record.addResponse( - cId: choiceConstruct, - target: practiceTarget, - text: choice, - score: isCorrect ? 1 : 0, - ); - return isCorrect; - } + bool isCorrect(String choice) => multipleChoiceContent.isCorrect(choice); OneConstructUse constructUse(String choiceContent) { final correct = multipleChoiceContent.isCorrect(choiceContent); final useType = correct ? activityType.correctUse : activityType.incorrectUse; + final token = tokens.first; return OneConstructUse( useType: useType, @@ -222,9 +223,9 @@ sealed class MultipleChoicePracticeActivityModel extends PracticeActivityModel { roomId: null, timeStamp: DateTime.now(), ), - category: targetTokens.first.pos, - lemma: targetTokens.first.lemma.text, - form: targetTokens.first.lemma.text, + category: token.pos, + lemma: token.lemma.text, + form: token.lemma.text, xp: useType.pointValue, ); } @@ -241,37 +242,16 @@ sealed class MatchPracticeActivityModel extends PracticeActivityModel { final PracticeMatchActivity matchContent; MatchPracticeActivityModel({ - required super.targetTokens, + required super.tokens, required super.langCode, - required super.activityType, required this.matchContent, }); - bool onMatch( + bool isCorrect( PangeaToken token, - PracticeChoice choice, - ) { - // the user has already selected this choice - // so we don't want to record it again - if (practiceTarget.isComplete || - practiceTarget.record.alreadyHasMatchResponse( - token.vocabConstructID, - choice.choiceContent, - )) { - return false; - } - - final answers = matchContent.matchInfo[token.vocabForm]; - final isCorrect = answers!.contains(choice.choiceContent); - practiceTarget.record.addResponse( - cId: token.vocabConstructID, - target: practiceTarget, - text: choice.choiceContent, - score: isCorrect ? 1 : 0, - ); - - return isCorrect; - } + String choice, + ) => + matchContent.matchInfo[token.vocabForm]!.contains(choice); @override Map toJson() { @@ -286,19 +266,15 @@ sealed class MorphPracticeActivityModel final MorphFeaturesEnum morphFeature; MorphPracticeActivityModel({ - required super.targetTokens, + required super.tokens, required super.langCode, - required super.activityType, required super.multipleChoiceContent, required this.morphFeature, }); @override - PracticeTarget get practiceTarget => PracticeTarget( - tokens: targetTokens, - activityType: activityType, - morphFeature: morphFeature, - ); + String get storageKey => + '${activityType.name}-${tokens.map((e) => e.text.content).join("-")}-${morphFeature.name}'; @override Map toJson() { @@ -310,20 +286,19 @@ sealed class MorphPracticeActivityModel class MorphCategoryPracticeActivityModel extends MorphPracticeActivityModel { MorphCategoryPracticeActivityModel({ - required super.targetTokens, + required super.tokens, required super.langCode, required super.morphFeature, required super.multipleChoiceContent, - }) : super( - activityType: ActivityTypeEnum.grammarCategory, - ); + }); @override OneConstructUse constructUse(String choiceContent) { final correct = multipleChoiceContent.isCorrect(choiceContent); + final token = tokens.first; final useType = correct ? activityType.correctUse : activityType.incorrectUse; - final tag = targetTokens.first.getMorphTag(morphFeature)!; + final tag = token.getMorphTag(morphFeature)!; return OneConstructUse( useType: useType, @@ -334,7 +309,7 @@ class MorphCategoryPracticeActivityModel extends MorphPracticeActivityModel { ), category: morphFeature.name, lemma: tag, - form: targetTokens.first.lemma.form, + form: token.lemma.form, xp: useType.pointValue, ); } @@ -342,73 +317,59 @@ class MorphCategoryPracticeActivityModel extends MorphPracticeActivityModel { class MorphMatchPracticeActivityModel extends MorphPracticeActivityModel { MorphMatchPracticeActivityModel({ - required super.targetTokens, + required super.tokens, required super.langCode, required super.morphFeature, required super.multipleChoiceContent, - }) : super( - activityType: ActivityTypeEnum.morphId, - ); + }); } class VocabAudioPracticeActivityModel extends MultipleChoicePracticeActivityModel { VocabAudioPracticeActivityModel({ - required super.targetTokens, + required super.tokens, required super.langCode, required super.multipleChoiceContent, - }) : super( - activityType: ActivityTypeEnum.lemmaAudio, - ); + }); } class VocabMeaningPracticeActivityModel extends MultipleChoicePracticeActivityModel { VocabMeaningPracticeActivityModel({ - required super.targetTokens, + required super.tokens, required super.langCode, required super.multipleChoiceContent, - }) : super( - activityType: ActivityTypeEnum.lemmaMeaning, - ); + }); } class LemmaPracticeActivityModel extends MultipleChoicePracticeActivityModel { LemmaPracticeActivityModel({ - required super.targetTokens, + required super.tokens, required super.langCode, required super.multipleChoiceContent, - }) : super( - activityType: ActivityTypeEnum.lemmaId, - ); + }); } class EmojiPracticeActivityModel extends MatchPracticeActivityModel { EmojiPracticeActivityModel({ - required super.targetTokens, + required super.tokens, required super.langCode, required super.matchContent, - }) : super( - activityType: ActivityTypeEnum.emoji, - ); + }); } class LemmaMeaningPracticeActivityModel extends MatchPracticeActivityModel { LemmaMeaningPracticeActivityModel({ - required super.targetTokens, + required super.tokens, required super.langCode, required super.matchContent, - }) : super( - activityType: ActivityTypeEnum.wordMeaning, - ); + }); } class WordListeningPracticeActivityModel extends MatchPracticeActivityModel { WordListeningPracticeActivityModel({ - required super.targetTokens, + required super.tokens, required super.langCode, required super.matchContent, - }) : super( - activityType: ActivityTypeEnum.wordFocusListening, - ); + }); } diff --git a/lib/pangea/practice_activities/practice_generation_repo.dart b/lib/pangea/practice_activities/practice_generation_repo.dart index 84883cd50..68d3cb821 100644 --- a/lib/pangea/practice_activities/practice_generation_repo.dart +++ b/lib/pangea/practice_activities/practice_generation_repo.dart @@ -117,7 +117,7 @@ class PracticeRepo { required Map messageInfo, }) async { // some activities we'll get from the server and others we'll generate locally - switch (req.targetType) { + switch (req.target.activityType) { case ActivityTypeEnum.emoji: return EmojiActivityGenerator.get(req, messageInfo: messageInfo); case ActivityTypeEnum.lemmaId: diff --git a/lib/pangea/practice_activities/practice_target.dart b/lib/pangea/practice_activities/practice_target.dart index 465535591..5e5da4adc 100644 --- a/lib/pangea/practice_activities/practice_target.dart +++ b/lib/pangea/practice_activities/practice_target.dart @@ -1,19 +1,12 @@ -import 'dart:developer'; - import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/common/utils/error_handler.dart'; -import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; -import 'package:fluffychat/pangea/practice_activities/practice_choice.dart'; -import 'package:fluffychat/pangea/practice_activities/practice_record.dart'; -import 'package:fluffychat/pangea/practice_activities/practice_record_repo.dart'; /// Picks which tokens to do activities on and what types of activities to do /// Caches result so that we don't have to recompute it @@ -91,82 +84,6 @@ class PracticeTarget { (morphFeature?.name ?? ""); } - PracticeRecord get record => PracticeRecordRepo.get(this); - - bool get isComplete { - if (activityType == ActivityTypeEnum.morphId) { - return record.completeResponses > 0; - } - - return tokens.every( - (t) => record.responses - .any((res) => res.cId == t.vocabConstructID && res.isCorrect), - ); - } - - bool isCompleteByToken(PangeaToken token, [MorphFeaturesEnum? morph]) { - final ConstructIdentifier? cId = - morph == null ? token.vocabConstructID : token.morphIdByFeature(morph); - if (cId == null) { - debugger(when: kDebugMode); - ErrorHandler.logError( - m: "isCompleteByToken: cId is null for token ${token.text.content}", - data: { - "t": token.toJson(), - "morph": morph?.name, - }, - ); - return false; - } - - if (activityType == ActivityTypeEnum.morphId) { - return record.responses.any( - (res) => res.cId == token.morphIdByFeature(morph!) && res.isCorrect, - ); - } - - return record.responses.any( - (res) => res.cId == token.vocabConstructID && res.isCorrect, - ); - } - - bool? wasCorrectChoice(String choice) { - for (final response in record.responses) { - if (response.text == choice) { - return response.isCorrect; - } - } - return null; - } - - /// if any of the choices were correct, return true - /// if all of the choices were incorrect, return false - /// if null, it means the user has not yet responded with that choice - bool? wasCorrectMatch(PracticeChoice choice) { - for (final response in record.responses) { - if (response.text == choice.choiceContent && response.isCorrect) { - return true; - } - } - for (final response in record.responses) { - if (response.text == choice.choiceContent) { - return false; - } - } - return null; - } - - bool get hasAnyResponses => record.responses.isNotEmpty; - - bool get hasAnyCorrectChoices { - for (final response in record.responses) { - if (response.isCorrect) { - return true; - } - } - return false; - } - String promptText(BuildContext context) { switch (activityType) { case ActivityTypeEnum.grammarCategory: diff --git a/lib/pangea/practice_activities/word_focus_listening_generator.dart b/lib/pangea/practice_activities/word_focus_listening_generator.dart index 80878a0e6..0511b6fdb 100644 --- a/lib/pangea/practice_activities/word_focus_listening_generator.dart +++ b/lib/pangea/practice_activities/word_focus_listening_generator.dart @@ -7,7 +7,7 @@ class WordFocusListeningGenerator { static MessageActivityResponse get( MessageActivityRequest req, ) { - if (req.targetTokens.length <= 1) { + if (req.target.tokens.length <= 1) { throw Exception( "Word focus listening activity requires at least 2 tokens", ); @@ -15,11 +15,11 @@ class WordFocusListeningGenerator { return MessageActivityResponse( activity: WordListeningPracticeActivityModel( - targetTokens: req.targetTokens, + tokens: req.target.tokens, langCode: req.userL2, matchContent: PracticeMatchActivity( matchInfo: Map.fromEntries( - req.targetTokens.map( + req.target.tokens.map( (token) => MapEntry( ConstructForm( cId: token.vocabConstructID, diff --git a/lib/pangea/toolbar/layout/message_selection_positioner.dart b/lib/pangea/toolbar/layout/message_selection_positioner.dart index a905a1fa1..477a3b5f8 100644 --- a/lib/pangea/toolbar/layout/message_selection_positioner.dart +++ b/lib/pangea/toolbar/layout/message_selection_positioner.dart @@ -482,7 +482,7 @@ class MessageSelectionPositionerState extends State final type = practice.practiceMode.associatedActivityType; final complete = type != null && - practice.isPracticeActivityDone(type); + practice.isPracticeSessionDone(type); if (instruction != null && !complete) { return InstructionsInlineTooltip( diff --git a/lib/pangea/toolbar/message_practice/message_morph_choice.dart b/lib/pangea/toolbar/message_practice/message_morph_choice.dart index 12ae15dc6..458932773 100644 --- a/lib/pangea/toolbar/message_practice/message_morph_choice.dart +++ b/lib/pangea/toolbar/message_practice/message_morph_choice.dart @@ -51,7 +51,7 @@ class MessageMorphInputBarContentState extends State { String? selectedTag; - PangeaToken get token => widget.activity.targetTokens.first; + PangeaToken get token => widget.activity.tokens.first; MorphFeaturesEnum get morph => widget.activity.morphFeature; @override @@ -116,8 +116,7 @@ class MessageMorphInputBarContentState runSpacing: spacing, children: widget.activity.multipleChoiceContent.choices.mapIndexed( (index, choice) { - final wasCorrect = - widget.activity.practiceTarget.wasCorrectChoice(choice); + final wasCorrect = widget.controller.wasCorrectChoice(choice); return ChoiceAnimationWidget( isSelected: selectedTag == choice, @@ -135,8 +134,7 @@ class MessageMorphInputBarContentState PracticeChoice( choiceContent: choice, form: ConstructForm( - cId: widget.activity.targetTokens.first - .morphIdByFeature( + cId: widget.activity.tokens.first.morphIdByFeature( widget.activity.morphFeature, )!, form: token.text.content, diff --git a/lib/pangea/toolbar/message_practice/practice_controller.dart b/lib/pangea/toolbar/message_practice/practice_controller.dart index 99d56cd1f..1f572ada2 100644 --- a/lib/pangea/toolbar/message_practice/practice_controller.dart +++ b/lib/pangea/toolbar/message_practice/practice_controller.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:async/async.dart'; import 'package:collection/collection.dart'; @@ -18,6 +18,7 @@ import 'package:fluffychat/pangea/practice_activities/practice_selection_repo.da import 'package:fluffychat/pangea/practice_activities/practice_target.dart'; import 'package:fluffychat/pangea/toolbar/message_practice/message_practice_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/message_practice/morph_selection.dart'; +import 'package:fluffychat/pangea/toolbar/message_practice/practice_record_controller.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -37,14 +38,43 @@ class PracticeController with ChangeNotifier { PracticeSelection? practiceSelection; - bool get isTotallyDone => - isPracticeActivityDone(ActivityTypeEnum.emoji) && - isPracticeActivityDone(ActivityTypeEnum.wordMeaning) && - isPracticeActivityDone(ActivityTypeEnum.wordFocusListening) && - isPracticeActivityDone(ActivityTypeEnum.morphId); + bool? wasCorrectMatch(PracticeChoice choice) { + if (_activity == null) return false; + final record = PracticeRecordController.recordByActivity(_activity!); + for (final response in record.responses) { + if (response.text == choice.choiceContent && response.isCorrect) { + return true; + } + } + for (final response in record.responses) { + if (response.text == choice.choiceContent) { + return false; + } + } + return null; + } - bool isPracticeActivityDone(ActivityTypeEnum activityType) => - practiceSelection?.activities(activityType).every((a) => a.isComplete) == + bool? wasCorrectChoice(String choice) { + if (_activity == null) return false; + final record = PracticeRecordController.recordByActivity(_activity!); + for (final response in record.responses) { + if (response.text == choice) { + return response.isCorrect; + } + } + return null; + } + + bool get isTotallyDone => + isPracticeSessionDone(ActivityTypeEnum.emoji) && + isPracticeSessionDone(ActivityTypeEnum.wordMeaning) && + isPracticeSessionDone(ActivityTypeEnum.wordFocusListening) && + isPracticeSessionDone(ActivityTypeEnum.morphId); + + bool isPracticeSessionDone(ActivityTypeEnum activityType) => + practiceSelection + ?.activities(activityType) + .every((a) => PracticeRecordController.isCompleteByTarget(a)) == true; bool isPracticeButtonEmpty(PangeaToken token) { @@ -67,21 +97,38 @@ class PracticeController with ChangeNotifier { ? (_activity as MorphPracticeActivityModel).morphFeature : null; - return target == null || target.isCompleteByToken(token, morph) == true; + return target == null || + PracticeRecordController.isCompleteByToken( + target, + token, + morph, + ); } bool get showChoiceShimmer { if (_activity == null) return false; + final record = PracticeRecordController.recordByActivity(_activity!); - if (_activity!.activityType == ActivityTypeEnum.morphId) { - return selectedMorph != null && - !_activity!.practiceTarget.hasAnyResponses; + if (_activity is MorphMatchPracticeActivityModel) { + return selectedMorph != null && record.responses.isEmpty; } - return selectedChoice == null && - !_activity!.practiceTarget.hasAnyCorrectChoices; + return selectedChoice == null && !record.responses.any((r) => r.isCorrect); } + // PracticeTarget? get _target => _activity != null + // ? PracticeTarget( + // activityType: _activity!.activityType, + // tokens: _activity!.tokens, + // morphFeature: _activity is MorphPracticeActivityModel + // ? (_activity as MorphPracticeActivityModel).morphFeature + // : null, + // ) + // : null; + + // PracticeRecord? get _record => + // _target != null ? PracticeRecordRepo.get(_target!) : null; + Future _fetchPracticeSelection() async { if (pangeaMessageEvent.messageDisplayRepresentation?.tokens == null) return; practiceSelection = await PracticeSelectionRepo.get( @@ -98,9 +145,7 @@ class PracticeController with ChangeNotifier { userL1: MatrixState.pangeaController.userController.userL1!.langCode, userL2: MatrixState.pangeaController.userController.userL2!.langCode, activityQualityFeedback: null, - targetTokens: target.tokens, - targetType: target.activityType, - targetMorphFeature: target.morphFeature, + target: target, ); final result = await PracticeRepo.getPracticeActivity( @@ -148,13 +193,12 @@ class PracticeController with ChangeNotifier { void onMatch(PangeaToken token, PracticeChoice choice) { if (_activity == null) return; - final isCorrect = switch (_activity!) { - MultipleChoicePracticeActivityModel() => - (_activity as MultipleChoicePracticeActivityModel) - .onMultipleChoiceSelect(choice.form.cId, choice.choiceContent), - MatchPracticeActivityModel() => - (_activity as MatchPracticeActivityModel).onMatch(token, choice), - }; + final record = PracticeRecordController.recordByActivity(_activity!); + final isCorrect = PracticeRecordController.onSelectChoice( + choice.choiceContent, + token, + _activity!, + ); final targetId = "message-token-${token.text.uniqueKey}-${pangeaMessageEvent.eventId}"; @@ -163,9 +207,9 @@ class PracticeController with ChangeNotifier { .pangeaController.matrixState.analyticsDataService.updateService; // we don't take off points for incorrect emoji matches - if (_activity!.activityType != ActivityTypeEnum.emoji || isCorrect) { - final constructUseType = _activity!.practiceTarget.record.responses.last - .useType(_activity!.activityType); + if (_activity is! EmojiPracticeActivityModel || isCorrect) { + final constructUseType = + record.responses.last.useType(_activity!.activityType); final constructs = [ OneConstructUse( @@ -191,14 +235,14 @@ class PracticeController with ChangeNotifier { } if (isCorrect) { - if (_activity!.activityType == ActivityTypeEnum.emoji) { + if (_activity is EmojiPracticeActivityModel) { updateService.setLemmaInfo( choice.form.cId, emoji: choice.choiceContent, ); } - if (_activity!.activityType == ActivityTypeEnum.wordMeaning) { + if (_activity is LemmaMeaningPracticeActivityModel) { updateService.setLemmaInfo( choice.form.cId, meaning: choice.choiceContent, diff --git a/lib/pangea/toolbar/message_practice/practice_match_card.dart b/lib/pangea/toolbar/message_practice/practice_match_card.dart index 1b7ae8d1a..69b8f4a96 100644 --- a/lib/pangea/toolbar/message_practice/practice_match_card.dart +++ b/lib/pangea/toolbar/message_practice/practice_match_card.dart @@ -1,13 +1,9 @@ -import 'dart:developer'; - -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pangea/common/widgets/choice_animation.dart'; -import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart'; import 'package:fluffychat/pangea/practice_activities/practice_choice.dart'; import 'package:fluffychat/pangea/toolbar/message_practice/message_audio_card.dart'; @@ -25,16 +21,16 @@ class MatchActivityCard extends StatelessWidget { required this.controller, }); - ActivityTypeEnum get activityType => currentActivity.activityType; + // ActivityTypeEnum get activityType => currentActivity.activityType; Widget choiceDisplayContent( BuildContext context, String choice, double? fontSize, ) { - switch (activityType) { - case ActivityTypeEnum.emoji: - case ActivityTypeEnum.wordMeaning: + switch (currentActivity) { + case EmojiPracticeActivityModel(): + case LemmaMeaningPracticeActivityModel(): return Padding( padding: const EdgeInsets.all(8), child: Text( @@ -43,7 +39,7 @@ class MatchActivityCard extends StatelessWidget { textAlign: TextAlign.center, ), ); - case ActivityTypeEnum.wordFocusListening: + case WordListeningPracticeActivityModel(): return Padding( padding: const EdgeInsets.all(8), child: Icon( @@ -51,9 +47,6 @@ class MatchActivityCard extends StatelessWidget { size: fontSize, ), ); - default: - debugger(when: kDebugMode); - return const SizedBox(); } } @@ -83,13 +76,12 @@ class MatchActivityCard extends StatelessWidget { runSpacing: 4.0, children: currentActivity.matchContent.choices.map( (PracticeChoice cf) { - final bool? wasCorrect = - currentActivity.practiceTarget.wasCorrectMatch(cf); + final bool? wasCorrect = controller.wasCorrectMatch(cf); return ChoiceAnimationWidget( isSelected: controller.selectedChoice == cf, isCorrect: wasCorrect, child: PracticeMatchItem( - token: currentActivity.practiceTarget.tokens.firstWhereOrNull( + token: currentActivity.tokens.firstWhereOrNull( (t) => t.vocabConstructID == cf.form.cId, ), isSelected: controller.selectedChoice == cf, @@ -98,7 +90,7 @@ class MatchActivityCard extends StatelessWidget { content: choiceDisplayContent(context, cf.choiceContent, fontSize), audioContent: - activityType == ActivityTypeEnum.wordFocusListening + currentActivity is WordListeningPracticeActivityModel ? cf.choiceContent : null, controller: controller, diff --git a/lib/pangea/toolbar/message_practice/practice_record_controller.dart b/lib/pangea/toolbar/message_practice/practice_record_controller.dart new file mode 100644 index 000000000..78716b89c --- /dev/null +++ b/lib/pangea/toolbar/message_practice/practice_record_controller.dart @@ -0,0 +1,108 @@ +import 'dart:developer'; + +import 'package:flutter/foundation.dart'; + +import 'package:fluffychat/pangea/common/utils/error_handler.dart'; +import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; +import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; +import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; +import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart'; +import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart'; +import 'package:fluffychat/pangea/practice_activities/practice_record.dart'; +import 'package:fluffychat/pangea/practice_activities/practice_record_repo.dart'; +import 'package:fluffychat/pangea/practice_activities/practice_target.dart'; + +class PracticeRecordController { + static PracticeRecord recordByActivity(PracticeActivityModel activity) => + PracticeRecordRepo.get(activity.practiceTarget); + + static bool isCompleteByTarget(PracticeTarget target) { + final record = PracticeRecordRepo.get(target); + if (target.activityType == ActivityTypeEnum.morphId) { + return record.completeResponses > 0; + } + + return target.tokens.every( + (t) => record.responses + .any((res) => res.cId == t.vocabConstructID && res.isCorrect), + ); + } + + static bool isCompleteByActivity(PracticeActivityModel activity) { + final activityRecord = recordByActivity(activity); + if (activity.activityType == ActivityTypeEnum.morphId) { + return activityRecord.completeResponses > 0; + } + + return activity.tokens.every( + (t) => activityRecord.responses + .any((res) => res.cId == t.vocabConstructID && res.isCorrect), + ); + } + + static bool isCompleteByToken( + PracticeTarget target, + PangeaToken token, [ + MorphFeaturesEnum? morph, + ]) { + final ConstructIdentifier? cId = + morph == null ? token.vocabConstructID : token.morphIdByFeature(morph); + + if (cId == null) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "isCompleteByToken: cId is null for token ${token.text.content}", + data: { + "t": token.toJson(), + "morph": morph?.name, + }, + ); + return false; + } + + final record = PracticeRecordRepo.get(target); + if (target.activityType == ActivityTypeEnum.morphId) { + return record.responses.any( + (res) => res.cId == token.morphIdByFeature(morph!) && res.isCorrect, + ); + } + + return record.responses.any( + (res) => res.cId == token.vocabConstructID && res.isCorrect, + ); + } + + static bool hasAnyCorrectChoices(PracticeTarget target) { + final record = PracticeRecordRepo.get(target); + return record.responses.any((response) => response.isCorrect); + } + + static bool onSelectChoice( + String choice, + PangeaToken token, + PracticeActivityModel activity, + ) { + final record = recordByActivity(activity); + if (isCompleteByActivity(activity) || + record.alreadyHasMatchResponse( + token.vocabConstructID, + choice, + )) { + return false; + } + + final isCorrect = switch (activity) { + MatchPracticeActivityModel() => activity.isCorrect(token, choice), + MultipleChoicePracticeActivityModel() => activity.isCorrect(choice), + }; + + record.addResponse( + cId: token.vocabConstructID, + target: activity.practiceTarget, + text: choice, + score: isCorrect ? 1 : 0, + ); + + return isCorrect; + } +} diff --git a/lib/pangea/toolbar/message_practice/reading_assistance_input_bar.dart b/lib/pangea/toolbar/message_practice/reading_assistance_input_bar.dart index 6cbcb249c..5eaf37f51 100644 --- a/lib/pangea/toolbar/message_practice/reading_assistance_input_bar.dart +++ b/lib/pangea/toolbar/message_practice/reading_assistance_input_bar.dart @@ -54,7 +54,7 @@ class ReadingAssistanceInputBarState extends State { children: [ ...MessagePracticeMode.practiceModes.map( (m) { - final complete = widget.controller.isPracticeActivityDone( + final complete = widget.controller.isPracticeSessionDone( m.associatedActivityType!, ); return ToolbarButton( @@ -125,7 +125,7 @@ class _ReadingAssistanceBarContent extends StatelessWidget { } final activityType = mode.associatedActivityType; final activityCompleted = - activityType != null && controller.isPracticeActivityDone(activityType); + activityType != null && controller.isPracticeSessionDone(activityType); switch (mode) { case MessagePracticeMode.noneSelected: diff --git a/lib/pangea/toolbar/message_practice/token_practice_button.dart b/lib/pangea/toolbar/message_practice/token_practice_button.dart index 5ec21afa3..46c1ceff1 100644 --- a/lib/pangea/toolbar/message_practice/token_practice_button.dart +++ b/lib/pangea/toolbar/message_practice/token_practice_button.dart @@ -14,11 +14,13 @@ import 'package:fluffychat/pangea/morphs/get_grammar_copy.dart'; import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; import 'package:fluffychat/pangea/morphs/morph_icon.dart'; import 'package:fluffychat/pangea/practice_activities/practice_choice.dart'; +import 'package:fluffychat/pangea/practice_activities/practice_record_repo.dart'; import 'package:fluffychat/pangea/practice_activities/practice_target.dart'; import 'package:fluffychat/pangea/toolbar/message_practice/dotted_border_painter.dart'; import 'package:fluffychat/pangea/toolbar/message_practice/message_practice_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/message_practice/morph_selection.dart'; import 'package:fluffychat/pangea/toolbar/message_practice/practice_controller.dart'; +import 'package:fluffychat/pangea/toolbar/message_practice/practice_record_controller.dart'; import 'package:fluffychat/widgets/hover_builder.dart'; const double tokenButtonHeight = 40.0; @@ -48,11 +50,8 @@ class TokenPracticeButton extends StatelessWidget { PracticeTarget? get _activity => controller.practiceTargetForToken(token); bool get isActivityCompleteOrNullForToken { - return _activity?.isCompleteByToken( - token, - _activity!.morphFeature, - ) == - true; + if (_activity == null) return true; + return PracticeRecordController.isCompleteByToken(_activity!, token); } bool get _isEmpty => controller.isPracticeButtonEmpty(token); @@ -94,7 +93,8 @@ class TokenPracticeButton extends StatelessWidget { ), ), shimmer: controller.selectedMorph == null && - _activity?.hasAnyCorrectChoices == false, + _activity != null && + !PracticeRecordController.hasAnyCorrectChoices(_activity!), ); } else { child = _StandardMatchButton( @@ -257,8 +257,9 @@ class _NoActivityContentButton extends StatelessWidget { @override Widget build(BuildContext context) { - if (practiceMode == MessagePracticeMode.wordEmoji) { - final displayEmoji = target?.record.responses + if (practiceMode == MessagePracticeMode.wordEmoji && target != null) { + final record = PracticeRecordRepo.get(target!); + final displayEmoji = record.responses .firstWhereOrNull( (res) => res.cId == token.vocabConstructID && res.isCorrect, )