diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 16fb400ac..b1bcf4e02 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5053,5 +5053,6 @@ "constructUseCorGCDesc": "Correct grammar category practice", "constructUseIncGCDesc": "Incorrect grammar category practice", "constructUseCorGEDesc": "Correct grammar error practice", - "constructUseIncGEDesc": "Incorrect grammar error practice" + "constructUseIncGEDesc": "Incorrect grammar error practice", + "fillInBlank": "Fill in the blank with the correct choice" } diff --git a/lib/pangea/analytics_data/analytics_data_service.dart b/lib/pangea/analytics_data/analytics_data_service.dart index 1c10b1208..0f250f121 100644 --- a/lib/pangea/analytics_data/analytics_data_service.dart +++ b/lib/pangea/analytics_data/analytics_data_service.dart @@ -14,6 +14,7 @@ import 'package:fluffychat/pangea/analytics_data/level_up_analytics_service.dart import 'package:fluffychat/pangea/analytics_misc/client_analytics_extension.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_event.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/analytics_settings/analytics_settings_extension.dart'; @@ -233,12 +234,14 @@ class AnalyticsDataService { int? count, String? roomId, DateTime? since, + ConstructUseTypeEnum? type, }) async { await _ensureInitialized(); final uses = await _analyticsClientGetter.database.getUses( count: count, roomId: roomId, since: since, + type: type, ); final blocked = blockedConstructs; diff --git a/lib/pangea/analytics_data/analytics_database.dart b/lib/pangea/analytics_data/analytics_database.dart index cb215ae94..5fe681042 100644 --- a/lib/pangea/analytics_data/analytics_database.dart +++ b/lib/pangea/analytics_data/analytics_database.dart @@ -10,6 +10,7 @@ import 'package:synchronized/synchronized.dart'; import 'package:fluffychat/pangea/analytics_data/derived_analytics_data_model.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_event.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; @@ -197,6 +198,7 @@ class AnalyticsDatabase with DatabaseFileStorage { int? count, String? roomId, DateTime? since, + ConstructUseTypeEnum? type, }) async { final stopwatch = Stopwatch()..start(); final results = []; @@ -208,6 +210,9 @@ class AnalyticsDatabase with DatabaseFileStorage { if (roomId != null && use.metadata.roomId != roomId) { return true; // skip but continue } + if (type != null && use.useType != type) { + return true; // skip but continue + } results.add(use); return count == null || results.length < count; diff --git a/lib/pangea/analytics_practice/analytics_practice_page.dart b/lib/pangea/analytics_practice/analytics_practice_page.dart index 22bca0912..67b7e2d26 100644 --- a/lib/pangea/analytics_practice/analytics_practice_page.dart +++ b/lib/pangea/analytics_practice/analytics_practice_page.dart @@ -26,18 +26,28 @@ import 'package:fluffychat/pangea/toolbar/message_practice/practice_record_contr import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; -class PracticeChoice { +class VocabPracticeChoice { final String choiceId; final String choiceText; final String? choiceEmoji; - const PracticeChoice({ + const VocabPracticeChoice({ required this.choiceId, required this.choiceText, this.choiceEmoji, }); } +class _PracticeQueueEntry { + final MessageActivityRequest request; + final Completer completer; + + _PracticeQueueEntry({ + required this.request, + required this.completer, + }); +} + class SessionLoader extends AsyncLoader { final ConstructTypeEnum type; SessionLoader({required this.type}); @@ -67,12 +77,10 @@ class AnalyticsPracticeState extends State final ValueNotifier> activityState = ValueNotifier(const AsyncState.idle()); - final Queue< - MapEntry>> _queue = Queue(); + final Queue<_PracticeQueueEntry> _queue = Queue(); - final ValueNotifier activityTarget = - ValueNotifier(null); + final ValueNotifier activityTarget = + ValueNotifier(null); final ValueNotifier progressNotifier = ValueNotifier(0.0); @@ -116,13 +124,13 @@ class AnalyticsPracticeState extends State AnalyticsDataService get _analyticsService => Matrix.of(context).analyticsDataService; - List filteredChoices( + List filteredChoices( MultipleChoicePracticeActivityModel activity, ) { final content = activity.multipleChoiceContent; final choices = content.choices.toList(); final answer = content.answers.first; - final filtered = []; + final filtered = []; final seenTexts = {}; for (final id in choices) { @@ -137,7 +145,7 @@ class AnalyticsPracticeState extends State (choice) => choice.choiceText == text, ); if (index != -1) { - filtered[index] = PracticeChoice( + filtered[index] = VocabPracticeChoice( choiceId: id, choiceText: text, choiceEmoji: getChoiceEmoji(activity.storageKey, id), @@ -148,7 +156,7 @@ class AnalyticsPracticeState extends State seenTexts.add(text); filtered.add( - PracticeChoice( + VocabPracticeChoice( choiceId: id, choiceText: text, choiceEmoji: getChoiceEmoji(activity.storageKey, id), @@ -202,7 +210,7 @@ class AnalyticsPracticeState extends State if (activityTarget.value == null) return; if (widget.type != ConstructTypeEnum.vocab) return; TtsController.tryToSpeak( - activityTarget.value!.tokens.first.vocabConstructID.lemma, + activityTarget.value!.target.tokens.first.vocabConstructID.lemma, langCode: MatrixState.pangeaController.userController.userL2!.langCode, ); } @@ -275,10 +283,10 @@ class AnalyticsPracticeState extends State activityState.value = const AsyncState.loading(); final nextActivityCompleter = _queue.removeFirst(); - activityTarget.value = nextActivityCompleter.key; + activityTarget.value = nextActivityCompleter.request; _playAudio(); - final activity = await nextActivityCompleter.value.future; + final activity = await nextActivityCompleter.completer.future; activityState.value = AsyncState.loaded(activity); } } catch (e) { @@ -298,7 +306,7 @@ class AnalyticsPracticeState extends State activityState.value = const AsyncState.loading(); final req = requests.first; - activityTarget.value = req.target; + activityTarget.value = req; _playAudio(); final res = await _fetchActivity(req); @@ -314,10 +322,17 @@ class AnalyticsPracticeState extends State _fillActivityQueue(requests.skip(1).toList()); } - Future _fillActivityQueue(List requests) async { + Future _fillActivityQueue( + List requests, + ) async { for (final request in requests) { final completer = Completer(); - _queue.add(MapEntry(request.target, completer)); + _queue.add( + _PracticeQueueEntry( + request: request, + completer: completer, + ), + ); try { final res = await _fetchActivity(request); diff --git a/lib/pangea/analytics_practice/analytics_practice_session_model.dart b/lib/pangea/analytics_practice/analytics_practice_session_model.dart index b438cce39..e0ee08718 100644 --- a/lib/pangea/analytics_practice/analytics_practice_session_model.dart +++ b/lib/pangea/analytics_practice/analytics_practice_session_model.dart @@ -6,9 +6,32 @@ import 'package:fluffychat/pangea/analytics_practice/analytics_practice_constant import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart'; import 'package:fluffychat/pangea/practice_activities/practice_target.dart'; +class AnalyticsActivityTarget { + final PracticeTarget target; + final GrammarErrorRequestInfo? grammarErrorInfo; + + AnalyticsActivityTarget({ + required this.target, + this.grammarErrorInfo, + }); + + Map toJson() => { + 'target': target.toJson(), + 'grammarErrorInfo': grammarErrorInfo?.toJson(), + }; + + factory AnalyticsActivityTarget.fromJson(Map json) => + AnalyticsActivityTarget( + target: PracticeTarget.fromJson(json['target']), + grammarErrorInfo: json['grammarErrorInfo'] != null + ? GrammarErrorRequestInfo.fromJson(json['grammarErrorInfo']) + : null, + ); +} + class AnalyticsPracticeSessionModel { final DateTime startedAt; - final List practiceTargets; + final List practiceTargets; final String userL1; final String userL2; @@ -38,7 +61,8 @@ class AnalyticsPracticeSessionModel { userL1: userL1, userL2: userL2, activityQualityFeedback: null, - target: target, + target: target.target, + grammarErrorInfo: target.grammarErrorInfo, ); }).toList(); } @@ -59,8 +83,8 @@ class AnalyticsPracticeSessionModel { return AnalyticsPracticeSessionModel( startedAt: DateTime.parse(json['startedAt'] as String), practiceTargets: (json['practiceTargets'] as List) - .map((e) => PracticeTarget.fromJson(e)) - .whereType() + .map((e) => AnalyticsActivityTarget.fromJson(e)) + .whereType() .toList(), userL1: json['userL1'] as String, userL2: json['userL2'] as String, diff --git a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart index dcf8a1f35..6124234e7 100644 --- a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart +++ b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart @@ -1,14 +1,17 @@ import 'dart:math'; 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_practice/analytics_practice_constants.dart'; import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; +import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; import 'package:fluffychat/pangea/lemmas/lemma.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/message_activity_request.dart'; import 'package:fluffychat/pangea/practice_activities/practice_target.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -24,28 +27,23 @@ class AnalyticsPracticeSessionRepo { (_) => activityTypes[r.nextInt(activityTypes.length)], ); - final List targets = []; + final List targets = []; if (type == ConstructTypeEnum.vocab) { final constructs = await _fetchVocab(); final targetCount = min(constructs.length, types.length); targets.addAll([ for (var i = 0; i < targetCount; i++) - PracticeTarget( - tokens: [constructs[i].asToken], - activityType: types[i], + AnalyticsActivityTarget( + target: PracticeTarget( + tokens: [constructs[i].asToken], + activityType: types[i], + ), ), ]); } else { - final morphs = await _fetchMorphs(); - targets.addAll([ - for (final entry in morphs.entries) - PracticeTarget( - tokens: [entry.key], - activityType: types[targets.length], - morphFeature: entry.value, - ), - ]); + final errorTargets = await _fetchErrors(); + targets.addAll(errorTargets); } final session = AnalyticsPracticeSessionModel( @@ -144,4 +142,99 @@ class AnalyticsPracticeSessionRepo { return targets; } + + static Future> _fetchErrors() async { + final uses = await MatrixState + .pangeaController.matrixState.analyticsDataService + .getUses(count: 100, type: ConstructUseTypeEnum.ga); + + final client = MatrixState.pangeaController.matrixState.client; + final Map idsToEvents = {}; + + for (final use in uses) { + final eventID = use.metadata.eventId; + if (eventID == null || idsToEvents.containsKey(eventID)) continue; + + final roomID = use.metadata.roomId; + if (roomID == null) { + idsToEvents[eventID] = null; + continue; + } + + final room = client.getRoomById(roomID); + final event = await room?.getEventById(eventID); + if (event == null || event.redacted) { + idsToEvents[eventID] = null; + continue; + } + + final timeline = await room!.getTimeline(); + idsToEvents[eventID] = PangeaMessageEvent( + event: event, + timeline: timeline, + ownMessage: event.senderId == client.userID, + ); + } + + final l2Code = + MatrixState.pangeaController.userController.userL2!.langCodeShort; + + final events = idsToEvents.values.whereType().toList(); + final eventsWithContent = events.where((e) { + final originalSent = e.originalSent; + final choreo = originalSent?.choreo; + final tokens = originalSent?.tokens; + return originalSent?.langCode.split("-").first == l2Code && + choreo != null && + tokens != null && + tokens.isNotEmpty && + choreo.choreoSteps.any( + (step) => + step.acceptedOrIgnoredMatch?.isGrammarMatch == true && + step.acceptedOrIgnoredMatch?.match.bestChoice != null, + ); + }); + + final targets = []; + for (final event in eventsWithContent) { + final originalSent = event.originalSent!; + final choreo = originalSent.choreo!; + final tokens = originalSent.tokens!; + + for (int i = 0; i < choreo.choreoSteps.length; i++) { + final step = choreo.choreoSteps[i]; + final igcMatch = step.acceptedOrIgnoredMatch; + if (igcMatch?.isGrammarMatch != true || + igcMatch?.match.bestChoice == null) { + continue; + } + + final choices = igcMatch!.match.choices!.map((c) => c.value).toList(); + final choiceTokens = tokens.where( + (token) => + token.lemma.saveVocab && + choices.any( + (choice) => choice.contains(token.text.content), + ), + ); + + targets.add( + AnalyticsActivityTarget( + target: PracticeTarget( + tokens: choiceTokens.toList(), + activityType: ActivityTypeEnum.grammarError, + morphFeature: null, + ), + grammarErrorInfo: GrammarErrorRequestInfo( + choreo: choreo, + stepIndex: i, + eventID: event.eventId, + ), + ), + ); + } + } + + return targets; + } } diff --git a/lib/pangea/analytics_practice/analytics_practice_view.dart b/lib/pangea/analytics_practice/analytics_practice_view.dart index a41f0ec3e..9b46524a9 100644 --- a/lib/pangea/analytics_practice/analytics_practice_view.dart +++ b/lib/pangea/analytics_practice/analytics_practice_view.dart @@ -143,8 +143,8 @@ class _AnalyticsActivityView extends StatelessWidget { if (controller.widget.type == ConstructTypeEnum.vocab) PhoneticTranscriptionWidget( - text: - target.tokens.first.vocabConstructID.lemma, + text: target + .target.tokens.first.vocabConstructID.lemma, textLanguage: MatrixState .pangeaController.userController.userL2!, style: const TextStyle(fontSize: 14.0), @@ -157,13 +157,8 @@ class _AnalyticsActivityView extends StatelessWidget { Expanded( flex: 2, child: Center( - child: ValueListenableBuilder( - valueListenable: controller.activityTarget, - builder: (context, target, __) => target != null - ? _ExampleMessageWidget( - controller.getExampleMessage(target), - ) - : const SizedBox(), + child: _AnalyticsPracticeCenterContent( + controller: controller, ), ), ), @@ -179,6 +174,36 @@ class _AnalyticsActivityView extends StatelessWidget { } } +class _AnalyticsPracticeCenterContent extends StatelessWidget { + final AnalyticsPracticeState controller; + + const _AnalyticsPracticeCenterContent({ + required this.controller, + }); + + @override + Widget build(BuildContext context) { + return ValueListenableBuilder( + valueListenable: controller.activityTarget, + builder: (context, target, __) => switch (target?.target.activityType) { + null => const SizedBox(), + ActivityTypeEnum.grammarError => ValueListenableBuilder( + valueListenable: controller.activityState, + builder: (context, state, __) => switch (state) { + AsyncLoaded(value: final activity) => _ErrorBlankWidget( + activity: activity as GrammarErrorPracticeActivityModel, + ), + _ => const SizedBox(), + }, + ), + _ => _ExampleMessageWidget( + controller.getExampleMessage(target!.target), + ), + }, + ); + } +} + class _ExampleMessageWidget extends StatelessWidget { final Future?> future; @@ -220,6 +245,62 @@ class _ExampleMessageWidget extends StatelessWidget { } } +class _ErrorBlankWidget extends StatelessWidget { + final GrammarErrorPracticeActivityModel activity; + + const _ErrorBlankWidget({ + required this.activity, + }); + + @override + Widget build(BuildContext context) { + final text = activity.text; + final errorOffset = activity.errorOffset; + final errorLength = activity.errorLength; + + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), + decoration: BoxDecoration( + color: Color.alphaBlend( + Colors.white.withAlpha(180), + ThemeData.dark().colorScheme.primary, + ), + borderRadius: BorderRadius.circular(16), + ), + child: RichText( + text: TextSpan( + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryFixed, + fontSize: AppConfig.fontSizeFactor * AppConfig.messageFontSize, + ), + children: [ + if (errorOffset > 0) + TextSpan(text: text.characters.take(errorOffset).toString()), + WidgetSpan( + child: Container( + height: 4.0, + width: (errorLength * 8).toDouble(), + padding: const EdgeInsets.only(bottom: 2.0), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + ), + ), + ), + if (errorOffset + errorLength < text.length) + TextSpan( + text: + text.characters.skip(errorOffset + errorLength).toString(), + ), + ], + ), + ), + ); + } +} + class _ActivityChoicesWidget extends StatelessWidget { final AnalyticsPracticeState controller; @@ -366,6 +447,20 @@ class _ChoiceCard extends StatelessWidget { isCorrect: isCorrect, ); + case ActivityTypeEnum.grammarError: + final activity = this.activity as GrammarErrorPracticeActivityModel; + return GameChoiceCard( + key: ValueKey( + '${activity.errorLength}_${activity.errorOffset}_${activity.eventID}_${activityType.name}_grammar_error_$choiceId', + ), + shouldFlip: false, + targetId: targetId, + onPressed: onPressed, + isCorrect: isCorrect, + height: cardHeight, + child: Text(choiceText), + ); + default: return GameChoiceCard( key: ValueKey( diff --git a/lib/pangea/analytics_practice/grammar_error_practice_generator.dart b/lib/pangea/analytics_practice/grammar_error_practice_generator.dart index 66ec9c5db..30880ab50 100644 --- a/lib/pangea/analytics_practice/grammar_error_practice_generator.dart +++ b/lib/pangea/analytics_practice/grammar_error_practice_generator.dart @@ -1,41 +1,50 @@ +import 'package:flutter/material.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/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'); + assert( + req.grammarErrorInfo != null, + 'Grammar error info must be provided for grammar error practice', + ); - final errorSpan = igcMatch.errorSpan; - final correctChoice = igcMatch.bestChoice!.value; + final choreo = req.grammarErrorInfo!.choreo; + final stepIndex = req.grammarErrorInfo!.stepIndex; + final eventID = req.grammarErrorInfo!.eventID; + + final igcMatch = + choreo.choreoSteps[stepIndex].acceptedOrIgnoredMatch?.match; + assert(igcMatch?.choices != null, 'IGC match must have choices'); + assert(igcMatch?.bestChoice != null, 'IGC match must have a best choice'); + + 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', - ); + final stepText = choreo.stepText(stepIndex: stepIndex - 1); + final errorSpan = stepText.characters + .skip(igcMatch.offset) + .take(igcMatch.length) + .toString(); choices.add(errorSpan); choices.shuffle(); return MessageActivityResponse( activity: GrammarErrorPracticeActivityModel( - tokens: choiceTokens.toList(), + tokens: req.target.tokens, langCode: req.userL2, multipleChoiceContent: MultipleChoiceActivity( choices: choices.toSet(), answers: {correctChoice}, ), + text: stepText, + errorOffset: igcMatch.offset, + errorLength: igcMatch.length, + eventID: eventID, ), ); } diff --git a/lib/pangea/practice_activities/message_activity_request.dart b/lib/pangea/practice_activities/message_activity_request.dart index fef5c5243..47eebe6db 100644 --- a/lib/pangea/practice_activities/message_activity_request.dart +++ b/lib/pangea/practice_activities/message_activity_request.dart @@ -1,5 +1,11 @@ +import 'package:flutter/material.dart'; + import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/choreographer/choreo_record_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'; @@ -35,23 +41,67 @@ class ActivityQualityFeedback { } } +class GrammarErrorRequestInfo { + final ChoreoRecordModel choreo; + final int stepIndex; + final String eventID; + + const GrammarErrorRequestInfo({ + required this.choreo, + required this.stepIndex, + required this.eventID, + }); + + Map toJson() { + return { + 'choreo': choreo.toJson(), + 'step_index': stepIndex, + 'event_id': eventID, + }; + } + + factory GrammarErrorRequestInfo.fromJson(Map json) { + return GrammarErrorRequestInfo( + choreo: ChoreoRecordModel.fromJson(json['choreo']), + stepIndex: json['step_index'] as int, + eventID: json['event_id'] as String, + ); + } +} + class MessageActivityRequest { final String userL1; final String userL2; final PracticeTarget target; final ActivityQualityFeedback? activityQualityFeedback; + final GrammarErrorRequestInfo? grammarErrorInfo; MessageActivityRequest({ required this.userL1, required this.userL2, required this.activityQualityFeedback, required this.target, + this.grammarErrorInfo, }) { if (target.tokens.isEmpty) { throw Exception('Target tokens must not be empty'); } } + String promptText(BuildContext context) { + switch (target.activityType) { + case ActivityTypeEnum.grammarCategory: + return L10n.of(context).whatIsTheMorphTag( + target.morphFeature!.getDisplayCopy(context), + target.tokens.first.text.content, + ); + case ActivityTypeEnum.grammarError: + return L10n.of(context).fillInBlank; + default: + return target.tokens.first.vocabConstructID.lemma; + } + } + Map toJson() { return { 'user_l1': userL1, @@ -60,6 +110,7 @@ class MessageActivityRequest { 'target_tokens': target.tokens.map((e) => e.toJson()).toList(), 'target_type': target.activityType.name, 'target_morph_feature': target.morphFeature, + 'grammar_error_info': grammarErrorInfo?.toJson(), }; } @@ -72,7 +123,8 @@ class MessageActivityRequest { other.userL2 == userL2 && other.target == target && other.activityQualityFeedback?.feedbackText == - activityQualityFeedback?.feedbackText; + activityQualityFeedback?.feedbackText && + other.grammarErrorInfo == grammarErrorInfo; } @override @@ -80,7 +132,8 @@ class MessageActivityRequest { return activityQualityFeedback.hashCode ^ target.hashCode ^ userL1.hashCode ^ - userL2.hashCode; + userL2.hashCode ^ + grammarErrorInfo.hashCode; } } diff --git a/lib/pangea/practice_activities/practice_activity_model.dart b/lib/pangea/practice_activities/practice_activity_model.dart index 36d284f0b..602165b7c 100644 --- a/lib/pangea/practice_activities/practice_activity_model.dart +++ b/lib/pangea/practice_activities/practice_activity_model.dart @@ -354,10 +354,19 @@ class LemmaPracticeActivityModel extends MultipleChoicePracticeActivityModel { class GrammarErrorPracticeActivityModel extends MultipleChoicePracticeActivityModel { + final String text; + final int errorOffset; + final int errorLength; + final String eventID; + GrammarErrorPracticeActivityModel({ required super.tokens, required super.langCode, required super.multipleChoiceContent, + required this.text, + required this.errorOffset, + required this.errorLength, + required this.eventID, }); } diff --git a/lib/pangea/practice_activities/practice_generation_repo.dart b/lib/pangea/practice_activities/practice_generation_repo.dart index 3ff418594..35a961940 100644 --- a/lib/pangea/practice_activities/practice_generation_repo.dart +++ b/lib/pangea/practice_activities/practice_generation_repo.dart @@ -130,6 +130,10 @@ class PracticeRepo { case ActivityTypeEnum.grammarCategory: return MorphCategoryActivityGenerator.get(req); case ActivityTypeEnum.grammarError: + assert( + req.grammarErrorInfo != null, + 'Grammar error info must be provided for grammar error activities', + ); return GrammarErrorPracticeGenerator.get(req); case ActivityTypeEnum.morphId: return MorphActivityGenerator.get(req); diff --git a/lib/pangea/practice_activities/practice_target.dart b/lib/pangea/practice_activities/practice_target.dart index d6a0a1540..87c1ce7e7 100644 --- a/lib/pangea/practice_activities/practice_target.dart +++ b/lib/pangea/practice_activities/practice_target.dart @@ -1,9 +1,7 @@ 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/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/morphs/morph_features_enum.dart'; @@ -85,18 +83,6 @@ class PracticeTarget { (morphFeature?.name ?? ""); } - String promptText(BuildContext context) { - switch (activityType) { - case ActivityTypeEnum.grammarCategory: - return L10n.of(context).whatIsTheMorphTag( - morphFeature!.getDisplayCopy(context), - tokens.first.text.content, - ); - default: - return tokens.first.vocabConstructID.lemma; - } - } - ConstructIdentifier targetTokenConstructID(PangeaToken token) { final defaultID = token.vocabConstructID; final ConstructIdentifier? cId = morphFeature == null