From 78ca8832cd9a7821dacbfa8fa728f9e8eb6f3d21 Mon Sep 17 00:00:00 2001 From: Ava Shilling <165050625+avashilling@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:35:26 -0500 Subject: [PATCH 1/7] grammar practice UI updates - add morph icon to card - track last selected answer and display hint/description at the bottom after each one (like chat practice) --- .../analytics_practice_page.dart | 25 ++++++++ .../analytics_practice_view.dart | 63 ++++++++++++------- .../choice_cards/grammar_choice_card.dart | 33 +++++++++- 3 files changed, 98 insertions(+), 23 deletions(-) diff --git a/lib/pangea/analytics_practice/analytics_practice_page.dart b/lib/pangea/analytics_practice/analytics_practice_page.dart index 6fb2da694..f6f22e224 100644 --- a/lib/pangea/analytics_practice/analytics_practice_page.dart +++ b/lib/pangea/analytics_practice/analytics_practice_page.dart @@ -17,6 +17,7 @@ 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/morphs/morph_features_enum.dart'; import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart'; import 'package:fluffychat/pangea/practice_activities/practice_activity_model.dart'; import 'package:fluffychat/pangea/practice_activities/practice_generation_repo.dart'; @@ -26,6 +27,16 @@ 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 SelectedMorphChoice { + final MorphFeaturesEnum feature; + final String tag; + + const SelectedMorphChoice({ + required this.feature, + required this.tag, + }); +} + class PracticeChoice { final String choiceId; final String choiceText; @@ -74,6 +85,9 @@ class AnalyticsPracticeState extends State final ValueNotifier progressNotifier = ValueNotifier(0.0); + final ValueNotifier selectedMorphChoice = + ValueNotifier(null); + final Map> _choiceTexts = {}; final Map> _choiceEmojis = {}; @@ -101,6 +115,7 @@ class AnalyticsPracticeState extends State activityState.dispose(); activityTarget.dispose(); progressNotifier.dispose(); + selectedMorphChoice.dispose(); super.dispose(); } @@ -185,6 +200,7 @@ class AnalyticsPracticeState extends State void _resetActivityState() { activityState.value = const AsyncState.loading(); activityTarget.value = null; + selectedMorphChoice.value = null; } void _resetSessionState() { @@ -285,6 +301,7 @@ class AnalyticsPracticeState extends State await _completeSession(); } else { activityState.value = const AsyncState.loading(); + selectedMorphChoice.value = null; final nextActivityCompleter = _queue.removeFirst(); activityTarget.value = nextActivityCompleter.key; @@ -401,6 +418,14 @@ class AnalyticsPracticeState extends State if (_currentActivity == null) return; final activity = _currentActivity!; + // Track the selection for display + if (activity is MorphPracticeActivityModel) { + selectedMorphChoice.value = SelectedMorphChoice( + feature: activity.morphFeature, + tag: choiceContent, + ); + } + // Update activity record PracticeRecordController.onSelectChoice( choiceContent, diff --git a/lib/pangea/analytics_practice/analytics_practice_view.dart b/lib/pangea/analytics_practice/analytics_practice_view.dart index a41f0ec3e..562eedefa 100644 --- a/lib/pangea/analytics_practice/analytics_practice_view.dart +++ b/lib/pangea/analytics_practice/analytics_practice_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/analytics_details_popup/morph_meaning_widget.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; import 'package:fluffychat/pangea/analytics_practice/analytics_practice_page.dart'; import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart'; @@ -263,28 +264,46 @@ class _ActivityChoicesWidget extends StatelessWidget { final cardHeight = (constrainedHeight / (choices.length + 1)) .clamp(50.0, 80.0); - return Container( - constraints: const BoxConstraints(maxHeight: 400.0), - child: Column( - spacing: 4.0, - mainAxisSize: MainAxisSize.min, - children: choices - .map( - (choice) => _ChoiceCard( - activity: value, - targetId: - controller.choiceTargetId(choice.choiceId), - choiceId: choice.choiceId, - onPressed: () => controller.onSelectChoice( - choice.choiceId, - ), - cardHeight: cardHeight, - choiceText: choice.choiceText, - choiceEmoji: choice.choiceEmoji, - ), - ) - .toList(), - ), + return Column( + children: [ + Container( + constraints: const BoxConstraints(maxHeight: 400.0), + child: Column( + spacing: 4.0, + mainAxisSize: MainAxisSize.min, + children: choices + .map( + (choice) => _ChoiceCard( + activity: value, + targetId: + controller.choiceTargetId(choice.choiceId), + choiceId: choice.choiceId, + onPressed: () => controller.onSelectChoice( + choice.choiceId, + ), + cardHeight: cardHeight, + choiceText: choice.choiceText, + choiceEmoji: choice.choiceEmoji, + ), + ) + .toList(), + ), + ), + if (value.activityType == ActivityTypeEnum.grammarCategory) + ValueListenableBuilder( + valueListenable: controller.selectedMorphChoice, + builder: (context, selectedChoice, __) { + if (selectedChoice == null) { + return const SizedBox.shrink(); + } + + return MorphMeaningWidget( + feature: selectedChoice.feature, + tag: selectedChoice.tag, + ); + }, + ), + ], ); }, ), diff --git a/lib/pangea/analytics_practice/choice_cards/grammar_choice_card.dart b/lib/pangea/analytics_practice/choice_cards/grammar_choice_card.dart index c1299b611..ffc4464db 100644 --- a/lib/pangea/analytics_practice/choice_cards/grammar_choice_card.dart +++ b/lib/pangea/analytics_practice/choice_cards/grammar_choice_card.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:fluffychat/pangea/analytics_practice/choice_cards/game_choice_card.dart'; 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'; /// Choice card for meaning activity with emoji, and alt text on flip class GrammarChoiceCard extends StatelessWidget { @@ -29,6 +30,10 @@ class GrammarChoiceCard extends StatelessWidget { @override Widget build(BuildContext context) { + final baseTextSize = + (Theme.of(context).textTheme.titleMedium?.fontSize ?? 16) * + (height / 72.0).clamp(1.0, 1.4); + final emojiSize = baseTextSize * 1.2; final copy = getGrammarCopy( category: feature.name, lemma: tag, @@ -42,7 +47,33 @@ class GrammarChoiceCard extends StatelessWidget { onPressed: onPressed, isCorrect: isCorrect, height: height, - child: Text(copy), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: height * .7, + height: height, + child: Center( + child: MorphIcon( + morphFeature: feature, + morphTag: tag, + size: Size(emojiSize, emojiSize), + ), + ), + ), + Expanded( + child: Text( + copy, + overflow: TextOverflow.ellipsis, + maxLines: 2, + textAlign: TextAlign.left, + style: TextStyle( + fontSize: baseTextSize, + ), + ), + ), + ], + ), ); } } From 34ae1f30aa812a5c08ef7b6edf18496512588fe9 Mon Sep 17 00:00:00 2001 From: Ava Shilling <165050625+avashilling@users.noreply.github.com> Date: Tue, 20 Jan 2026 16:28:08 -0500 Subject: [PATCH 2/7] Fix grammar error null error and only reload current question upon encountering error --- .../analytics_practice_page.dart | 19 +++++++++++++++ .../analytics_practice_session_repo.dart | 23 +++++++++++-------- .../analytics_practice_view.dart | 2 +- 3 files changed, 34 insertions(+), 10 deletions(-) diff --git a/lib/pangea/analytics_practice/analytics_practice_page.dart b/lib/pangea/analytics_practice/analytics_practice_page.dart index f5fd80ea8..d2300633b 100644 --- a/lib/pangea/analytics_practice/analytics_practice_page.dart +++ b/lib/pangea/analytics_practice/analytics_practice_page.dart @@ -269,6 +269,25 @@ class AnalyticsPracticeState extends State await _startSession(); } + Future reloadCurrentActivity() async { + if (activityTarget.value == null) return; + + try { + activityState.value = const AsyncState.loading(); + selectedMorphChoice.value = null; + + final req = activityTarget.value!; + final res = await _fetchActivity(req); + + if (!mounted) return; + activityState.value = AsyncState.loaded(res); + _playAudio(); + } catch (e) { + if (!mounted) return; + activityState.value = AsyncState.error(e); + } + } + Future _completeSession() async { _sessionLoader.value!.finishSession(); setState(() {}); diff --git a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart index 378dc4486..e7d908391 100644 --- a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart +++ b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart @@ -56,7 +56,7 @@ class AnalyticsPracticeSessionRepo { AnalyticsActivityTarget( target: PracticeTarget( tokens: [entry.key], - activityType: types[targets.length], + activityType: ActivityTypeEnum.grammarCategory, morphFeature: entry.value, ), ), @@ -231,18 +231,23 @@ class AnalyticsPracticeSessionRepo { } 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), - ), - ); + final choiceTokens = tokens + .where( + (token) => + token.lemma.saveVocab && + choices.any( + (choice) => choice.contains(token.text.content), + ), + ) + .toList(); + + // Skip if no valid tokens found for this grammar error + if (choiceTokens.isEmpty) continue; targets.add( AnalyticsActivityTarget( target: PracticeTarget( - tokens: choiceTokens.toList(), + tokens: choiceTokens, activityType: ActivityTypeEnum.grammarError, morphFeature: null, ), diff --git a/lib/pangea/analytics_practice/analytics_practice_view.dart b/lib/pangea/analytics_practice/analytics_practice_view.dart index 5d5be5966..ba8dddb82 100644 --- a/lib/pangea/analytics_practice/analytics_practice_view.dart +++ b/lib/pangea/analytics_practice/analytics_practice_view.dart @@ -329,7 +329,7 @@ class _ActivityChoicesWidget extends StatelessWidget { ErrorIndicator(message: error.toString()), const SizedBox(height: 16), TextButton.icon( - onPressed: controller.reloadSession, + onPressed: controller.reloadCurrentActivity, icon: const Icon(Icons.refresh), label: Text(L10n.of(context).tryAgain), ), From 0068ef5965951cfaf1a34ac6047ca0a6bb5e24c4 Mon Sep 17 00:00:00 2001 From: Ava Shilling <165050625+avashilling@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:56:48 -0500 Subject: [PATCH 3/7] add translations for error questions and some spacing tweaks to improve layout and overflow issues --- .../morph_meaning_widget.dart | 10 +- .../analytics_practice_page.dart | 1 + .../analytics_practice_session_repo.dart | 1 + .../analytics_practice_view.dart | 269 +++++++++++++++--- .../message_activity_request.dart | 9 +- 5 files changed, 241 insertions(+), 49 deletions(-) diff --git a/lib/pangea/analytics_details_popup/morph_meaning_widget.dart b/lib/pangea/analytics_details_popup/morph_meaning_widget.dart index fd34f5c9d..a1716846e 100644 --- a/lib/pangea/analytics_details_popup/morph_meaning_widget.dart +++ b/lib/pangea/analytics_details_popup/morph_meaning_widget.dart @@ -1,7 +1,5 @@ import 'dart:math'; -import 'package:flutter/material.dart'; - import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart'; import 'package:fluffychat/pangea/languages/language_constants.dart'; @@ -11,17 +9,20 @@ import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_repo.dart'; import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_request.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; class MorphMeaningWidget extends StatefulWidget { final MorphFeaturesEnum feature; final String tag; final TextStyle? style; + final bool blankErrorFeedback; const MorphMeaningWidget({ super.key, required this.feature, required this.tag, this.style, + this.blankErrorFeedback = false, }); @override @@ -91,12 +92,13 @@ class MorphMeaningWidgetState extends State { ); if (result.isError) { - return L10n.of(context).meaningNotFound; + return widget.blankErrorFeedback ? '' : L10n.of(context).meaningNotFound; } final morph = result.result!.getFeatureByCode(widget.feature.name); final data = morph?.getTagByCode(widget.tag); - return data?.l1Description ?? L10n.of(context).meaningNotFound; + return data?.l1Description ?? + (widget.blankErrorFeedback ? '' : L10n.of(context).meaningNotFound); } void _toggleEditMode(bool value) => setState(() => _editMode = value); diff --git a/lib/pangea/analytics_practice/analytics_practice_page.dart b/lib/pangea/analytics_practice/analytics_practice_page.dart index 1b8b2c0e2..f3c5a8e49 100644 --- a/lib/pangea/analytics_practice/analytics_practice_page.dart +++ b/lib/pangea/analytics_practice/analytics_practice_page.dart @@ -505,4 +505,5 @@ class AnalyticsPracticeState extends State @override Widget build(BuildContext context) => AnalyticsPracticeView(this); + final request = activityTarget.value; } diff --git a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart index 15cefb1a9..cf48314f9 100644 --- a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart +++ b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart @@ -267,6 +267,7 @@ class AnalyticsPracticeSessionRepo { choreo: choreo, stepIndex: i, eventID: event.eventId, + event: event, ), ), ); diff --git a/lib/pangea/analytics_practice/analytics_practice_view.dart b/lib/pangea/analytics_practice/analytics_practice_view.dart index 44275cf4d..86acb81d1 100644 --- a/lib/pangea/analytics_practice/analytics_practice_view.dart +++ b/lib/pangea/analytics_practice/analytics_practice_view.dart @@ -1,4 +1,5 @@ import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_details_popup/morph_meaning_widget.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; @@ -74,8 +75,7 @@ class AnalyticsPracticeView extends StatelessWidget { ), body: Padding( padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 24.0, + horizontal: 8.0, ), child: MaxWidthBody( withScrolling: false, @@ -123,25 +123,36 @@ class _AnalyticsActivityView extends StatelessWidget { ), Expanded( child: Column( - spacing: 16.0, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Expanded( - flex: 1, - child: ValueListenableBuilder( - valueListenable: controller.activityTarget, - builder: (context, target, __) => target != null - ? Column( + ValueListenableBuilder( + valueListenable: controller.activityTarget, + builder: (context, target, __) => target != null + ? Padding( + padding: const EdgeInsets.only( + left: 16.0, + right: 16.0, + top: 16.0, + ), + child: Column( spacing: 12.0, children: [ Text( target.promptText(context), textAlign: TextAlign.center, - style: Theme.of(context) - .textTheme - .titleLarge - ?.copyWith( - fontWeight: FontWeight.bold, - ), + style: FluffyThemes.isColumnMode(context) + ? Theme.of(context) + .textTheme + .titleLarge + ?.copyWith( + fontWeight: FontWeight.bold, + ) + : Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.bold, + ), ), if (controller.widget.type == ConstructTypeEnum.vocab) @@ -153,22 +164,56 @@ class _AnalyticsActivityView extends StatelessWidget { style: const TextStyle(fontSize: 14.0), ), ], - ) - : const SizedBox(), - ), + ), + ) + : const SizedBox.shrink(), ), - Expanded( - flex: 2, - child: Center( + Flexible( + fit: FlexFit.loose, + child: SingleChildScrollView( child: _AnalyticsPracticeCenterContent( controller: controller, ), ), ), Expanded( - flex: 6, child: _ActivityChoicesWidget(controller), ), + //reserve space for grammar category morph meaning to avoid shifting, but only in those questions + AnimatedBuilder( + animation: Listenable.merge([ + controller.activityState, + controller.selectedMorphChoice, + ]), + builder: (context, _) { + final activityState = controller.activityState.value; + final selectedChoice = controller.selectedMorphChoice.value; + + final isGrammarCategory = activityState + is AsyncLoaded && + activityState.value.activityType == + ActivityTypeEnum.grammarCategory; + + if (!isGrammarCategory) { + return const SizedBox.shrink(); + } + + return ConstrainedBox( + constraints: const BoxConstraints( + minHeight: 80, + ), + child: selectedChoice == null + ? const SizedBox.shrink() + : SingleChildScrollView( + child: MorphMeaningWidget( + feature: selectedChoice.feature, + tag: selectedChoice.tag, + blankErrorFeedback: true, + ), + ), + ); + }, + ), ], ), ), @@ -193,8 +238,23 @@ class _AnalyticsPracticeCenterContent extends StatelessWidget { ActivityTypeEnum.grammarError => ValueListenableBuilder( valueListenable: controller.activityState, builder: (context, state, __) => switch (state) { - AsyncLoaded(value: final activity) => _ErrorBlankWidget( - activity: activity as GrammarErrorPracticeActivityModel, + AsyncLoaded( + value: final GrammarErrorPracticeActivityModel activity + ) => + Column( + mainAxisSize: MainAxisSize.min, + children: [ + _ErrorBlankWidget( + activity: activity, + ), + const SizedBox(height: 12), + _GrammarErrorTranslationButton( + key: ValueKey( + '${activity.eventID}_${activity.errorOffset}_${activity.errorLength}', + ), + controller: controller, + ), + ], ), _ => const SizedBox(), }, @@ -349,11 +409,10 @@ class _ActivityChoicesWidget extends StatelessWidget { return Column( children: [ - Container( - constraints: const BoxConstraints(maxHeight: 400.0), + Expanded( child: Column( spacing: 4.0, - mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, children: choices .map( (choice) => _ChoiceCard( @@ -372,20 +431,6 @@ class _ActivityChoicesWidget extends StatelessWidget { .toList(), ), ), - if (value.activityType == ActivityTypeEnum.grammarCategory) - ValueListenableBuilder( - valueListenable: controller.selectedMorphChoice, - builder: (context, selectedChoice, __) { - if (selectedChoice == null) { - return const SizedBox.shrink(); - } - - return MorphMeaningWidget( - feature: selectedChoice.feature, - tag: selectedChoice.tag, - ); - }, - ), ], ); }, @@ -470,6 +515,7 @@ class _ChoiceCard extends StatelessWidget { tag: choiceText, onPressed: onPressed, isCorrect: isCorrect, + height: cardHeight, enabled: enabled, ); @@ -504,3 +550,144 @@ class _ChoiceCard extends StatelessWidget { } } } + +class _GrammarErrorTranslationButton extends StatefulWidget { + final AnalyticsPracticeState controller; + + const _GrammarErrorTranslationButton({ + super.key, + required this.controller, + }); + + @override + State<_GrammarErrorTranslationButton> createState() => + _GrammarErrorTranslationButtonState(); +} + +class _GrammarErrorTranslationButtonState + extends State<_GrammarErrorTranslationButton> { + Future? _translationFuture; + bool _showTranslation = false; + + void _toggleTranslation() { + if (_showTranslation) { + setState(() { + _showTranslation = false; + _translationFuture = null; + }); + } else { + setState(() { + _showTranslation = true; + _translationFuture = widget.controller.requestTranslation(); + }); + } + } + + @override + Widget build(BuildContext context) { + return Center( + child: GestureDetector( + onTap: _toggleTranslation, + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 8.0, + children: [ + if (_showTranslation) + Flexible( + child: FutureBuilder( + future: _translationFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + 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: const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator.adaptive( + strokeWidth: 2, + ), + ), + ); + } + + if (snapshot.hasError) { + 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: Text( + L10n.of(context).oopsSomethingWentWrong, + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryFixed, + fontSize: AppConfig.fontSizeFactor * + AppConfig.messageFontSize, + ), + ), + ); + } + + if (snapshot.hasData) { + 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: Text( + snapshot.data!, + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryFixed, + fontSize: AppConfig.fontSizeFactor * + AppConfig.messageFontSize, + ), + textAlign: TextAlign.center, + ), + ); + } + + return const SizedBox(); + }, + ), + ), + if (!_showTranslation) + ElevatedButton( + onPressed: _toggleTranslation, + style: ElevatedButton.styleFrom( + shape: const CircleBorder(), + padding: const EdgeInsets.all(8), + ), + child: const Icon( + Icons.lightbulb_outline, + size: 20, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pangea/practice_activities/message_activity_request.dart b/lib/pangea/practice_activities/message_activity_request.dart index 47eebe6db..23454688c 100644 --- a/lib/pangea/practice_activities/message_activity_request.dart +++ b/lib/pangea/practice_activities/message_activity_request.dart @@ -1,13 +1,12 @@ -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/events/event_wrappers/pangea_message_event.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'; +import 'package:flutter/material.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; // includes feedback text and the bad activity model class ActivityQualityFeedback { @@ -45,11 +44,13 @@ class GrammarErrorRequestInfo { final ChoreoRecordModel choreo; final int stepIndex; final String eventID; + final PangeaMessageEvent? event; const GrammarErrorRequestInfo({ required this.choreo, required this.stepIndex, required this.eventID, + this.event, }); Map toJson() { From c881f61c4ffce5f9f4cf4c275b83fe7cba31db0e Mon Sep 17 00:00:00 2001 From: Ava Shilling <165050625+avashilling@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:06:57 -0500 Subject: [PATCH 4/7] forgot to push file and formatting --- .../morph_meaning_widget.dart | 3 ++- .../analytics_practice/analytics_practice_page.dart | 13 ++++++++++++- .../analytics_practice/analytics_practice_view.dart | 3 ++- .../choice_cards/grammar_choice_card.dart | 3 ++- .../message_activity_request.dart | 6 ++++-- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/lib/pangea/analytics_details_popup/morph_meaning_widget.dart b/lib/pangea/analytics_details_popup/morph_meaning_widget.dart index a1716846e..d9c619b32 100644 --- a/lib/pangea/analytics_details_popup/morph_meaning_widget.dart +++ b/lib/pangea/analytics_details_popup/morph_meaning_widget.dart @@ -1,5 +1,7 @@ import 'dart:math'; +import 'package:flutter/material.dart'; + import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_misc/text_loading_shimmer.dart'; import 'package:fluffychat/pangea/languages/language_constants.dart'; @@ -9,7 +11,6 @@ import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_repo.dart'; import 'package:fluffychat/pangea/morphs/morph_meaning/morph_info_request.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; class MorphMeaningWidget extends StatefulWidget { final MorphFeaturesEnum feature; diff --git a/lib/pangea/analytics_practice/analytics_practice_page.dart b/lib/pangea/analytics_practice/analytics_practice_page.dart index f3c5a8e49..aa4d4c034 100644 --- a/lib/pangea/analytics_practice/analytics_practice_page.dart +++ b/lib/pangea/analytics_practice/analytics_practice_page.dart @@ -1,7 +1,10 @@ import 'dart:async'; import 'dart:collection'; +import 'package:flutter/material.dart'; + import 'package:collection/collection.dart'; + import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/pangea/analytics_data/analytics_data_service.dart'; import 'package:fluffychat/pangea/analytics_data/analytics_updater_mixin.dart'; @@ -23,7 +26,6 @@ import 'package:fluffychat/pangea/text_to_speech/tts_controller.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'; -import 'package:flutter/material.dart'; class SelectedMorphChoice { final MorphFeaturesEnum feature; @@ -505,5 +507,14 @@ class AnalyticsPracticeState extends State @override Widget build(BuildContext context) => AnalyticsPracticeView(this); + + Future requestTranslation() async { final request = activityTarget.value; + if (request?.grammarErrorInfo == null) { + throw L10n.of(context).oopsSomethingWentWrong; + } + + final event = request!.grammarErrorInfo!.event!; + return await event.requestRespresentationByL1(); + } } diff --git a/lib/pangea/analytics_practice/analytics_practice_view.dart b/lib/pangea/analytics_practice/analytics_practice_view.dart index 86acb81d1..922bdad0d 100644 --- a/lib/pangea/analytics_practice/analytics_practice_view.dart +++ b/lib/pangea/analytics_practice/analytics_practice_view.dart @@ -1,3 +1,5 @@ +import 'package:flutter/material.dart'; + import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/l10n/l10n.dart'; @@ -22,7 +24,6 @@ import 'package:fluffychat/pangea/practice_activities/practice_activity_model.da import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/widgets/layouts/max_width_body.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:flutter/material.dart'; class AnalyticsPracticeView extends StatelessWidget { final AnalyticsPracticeState controller; diff --git a/lib/pangea/analytics_practice/choice_cards/grammar_choice_card.dart b/lib/pangea/analytics_practice/choice_cards/grammar_choice_card.dart index 4bc1314d9..266ed1b8e 100644 --- a/lib/pangea/analytics_practice/choice_cards/grammar_choice_card.dart +++ b/lib/pangea/analytics_practice/choice_cards/grammar_choice_card.dart @@ -1,8 +1,9 @@ +import 'package:flutter/material.dart'; + import 'package:fluffychat/pangea/analytics_practice/choice_cards/game_choice_card.dart'; 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:flutter/material.dart'; /// Choice card for meaning activity with emoji, and alt text on flip class GrammarChoiceCard extends StatelessWidget { diff --git a/lib/pangea/practice_activities/message_activity_request.dart b/lib/pangea/practice_activities/message_activity_request.dart index 23454688c..d7788cb80 100644 --- a/lib/pangea/practice_activities/message_activity_request.dart +++ b/lib/pangea/practice_activities/message_activity_request.dart @@ -1,3 +1,7 @@ +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/events/event_wrappers/pangea_message_event.dart'; @@ -5,8 +9,6 @@ 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'; -import 'package:flutter/material.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; // includes feedback text and the bad activity model class ActivityQualityFeedback { From dd8d35962fd6f21bf1c719ca4b32b324aeb239a4 Mon Sep 17 00:00:00 2001 From: Ava Shilling <165050625+avashilling@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:42:54 -0500 Subject: [PATCH 5/7] re-enable choice notifier --- .../analytics_practice_page.dart | 1 + .../analytics_practice_view.dart | 49 ++++++++++--------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/lib/pangea/analytics_practice/analytics_practice_page.dart b/lib/pangea/analytics_practice/analytics_practice_page.dart index aa4d4c034..4e6f57d51 100644 --- a/lib/pangea/analytics_practice/analytics_practice_page.dart +++ b/lib/pangea/analytics_practice/analytics_practice_page.dart @@ -121,6 +121,7 @@ class AnalyticsPracticeState extends State activityState.dispose(); activityTarget.dispose(); progressNotifier.dispose(); + enableChoicesNotifier.dispose(); selectedMorphChoice.dispose(); super.dispose(); } diff --git a/lib/pangea/analytics_practice/analytics_practice_view.dart b/lib/pangea/analytics_practice/analytics_practice_view.dart index 922bdad0d..fa743eb85 100644 --- a/lib/pangea/analytics_practice/analytics_practice_view.dart +++ b/lib/pangea/analytics_practice/analytics_practice_view.dart @@ -409,30 +409,35 @@ class _ActivityChoicesWidget extends StatelessWidget { .clamp(50.0, 80.0); return Column( - children: [ - Expanded( - child: Column( - spacing: 4.0, - mainAxisAlignment: MainAxisAlignment.center, - children: choices - .map( - (choice) => _ChoiceCard( - activity: value, - targetId: - controller.choiceTargetId(choice.choiceId), - choiceId: choice.choiceId, - onPressed: () => controller.onSelectChoice( - choice.choiceId, + return ValueListenableBuilder( + valueListenable: controller.enableChoicesNotifier, + builder: (context, enabled, __) => Column( + children: [ + Expanded( + child: Column( + spacing: 4.0, + mainAxisAlignment: MainAxisAlignment.center, + children: choices + .map( + (choice) => _ChoiceCard( + activity: value, + targetId: controller + .choiceTargetId(choice.choiceId), + choiceId: choice.choiceId, + onPressed: () => controller.onSelectChoice( + choice.choiceId, + ), + cardHeight: cardHeight, + choiceText: choice.choiceText, + choiceEmoji: choice.choiceEmoji, + enabled: enabled, ), - cardHeight: cardHeight, - choiceText: choice.choiceText, - choiceEmoji: choice.choiceEmoji, - ), - ) - .toList(), + ) + .toList(), + ), ), - ), - ], + ], + ), ); }, ), From 5938b15820c18e9ffa20bd1f18494519dfb7863f Mon Sep 17 00:00:00 2001 From: Ava Shilling <165050625+avashilling@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:43:48 -0500 Subject: [PATCH 6/7] fix syntax --- lib/pangea/analytics_practice/analytics_practice_view.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pangea/analytics_practice/analytics_practice_view.dart b/lib/pangea/analytics_practice/analytics_practice_view.dart index fa743eb85..c7722e275 100644 --- a/lib/pangea/analytics_practice/analytics_practice_view.dart +++ b/lib/pangea/analytics_practice/analytics_practice_view.dart @@ -408,7 +408,6 @@ class _ActivityChoicesWidget extends StatelessWidget { final cardHeight = (constrainedHeight / (choices.length + 1)) .clamp(50.0, 80.0); - return Column( return ValueListenableBuilder( valueListenable: controller.enableChoicesNotifier, builder: (context, enabled, __) => Column( From 558d8fdc503bd471e6c66602058e773aea9e092c Mon Sep 17 00:00:00 2001 From: ggurdin Date: Fri, 23 Jan 2026 10:30:26 -0500 Subject: [PATCH 7/7] fetch translation on activity target generation --- .../analytics_practice_page.dart | 10 -- .../analytics_practice_session_repo.dart | 19 +++- .../analytics_practice_view.dart | 107 ++++-------------- .../grammar_error_practice_generator.dart | 1 + .../message_activity_request.dart | 7 +- .../practice_activity_model.dart | 28 +++++ 6 files changed, 75 insertions(+), 97 deletions(-) diff --git a/lib/pangea/analytics_practice/analytics_practice_page.dart b/lib/pangea/analytics_practice/analytics_practice_page.dart index 4e6f57d51..7650a3a60 100644 --- a/lib/pangea/analytics_practice/analytics_practice_page.dart +++ b/lib/pangea/analytics_practice/analytics_practice_page.dart @@ -508,14 +508,4 @@ class AnalyticsPracticeState extends State @override Widget build(BuildContext context) => AnalyticsPracticeView(this); - - Future requestTranslation() async { - final request = activityTarget.value; - if (request?.grammarErrorInfo == null) { - throw L10n.of(context).oopsSomethingWentWrong; - } - - final event = request!.grammarErrorInfo!.event!; - return await event.requestRespresentationByL1(); - } } diff --git a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart index cf48314f9..3884dfc19 100644 --- a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart +++ b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart @@ -5,6 +5,7 @@ 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/common/network/requests.dart'; +import 'package:fluffychat/pangea/common/utils/error_handler.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'; @@ -256,6 +257,22 @@ class AnalyticsPracticeSessionRepo { // Skip if no valid tokens found for this grammar error if (choiceTokens.isEmpty) continue; + String? translation; + try { + translation = await event.requestRespresentationByL1(); + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: { + 'context': 'AnalyticsPracticeSessionRepo._fetchErrors', + 'message': 'Failed to fetch translation for analytics practice', + 'event_id': event.eventId, + }, + ); + } + + if (translation == null) continue; targets.add( AnalyticsActivityTarget( target: PracticeTarget( @@ -267,7 +284,7 @@ class AnalyticsPracticeSessionRepo { choreo: choreo, stepIndex: i, eventID: event.eventId, - event: event, + translation: translation, ), ), ); diff --git a/lib/pangea/analytics_practice/analytics_practice_view.dart b/lib/pangea/analytics_practice/analytics_practice_view.dart index c7722e275..21cef073d 100644 --- a/lib/pangea/analytics_practice/analytics_practice_view.dart +++ b/lib/pangea/analytics_practice/analytics_practice_view.dart @@ -253,7 +253,7 @@ class _AnalyticsPracticeCenterContent extends StatelessWidget { key: ValueKey( '${activity.eventID}_${activity.errorOffset}_${activity.errorLength}', ), - controller: controller, + translation: activity.translation, ), ], ), @@ -557,11 +557,11 @@ class _ChoiceCard extends StatelessWidget { } class _GrammarErrorTranslationButton extends StatefulWidget { - final AnalyticsPracticeState controller; + final String translation; const _GrammarErrorTranslationButton({ super.key, - required this.controller, + required this.translation, }); @override @@ -571,19 +571,16 @@ class _GrammarErrorTranslationButton extends StatefulWidget { class _GrammarErrorTranslationButtonState extends State<_GrammarErrorTranslationButton> { - Future? _translationFuture; bool _showTranslation = false; void _toggleTranslation() { if (_showTranslation) { setState(() { _showTranslation = false; - _translationFuture = null; }); } else { setState(() { _showTranslation = true; - _translationFuture = widget.controller.requestTranslation(); }); } } @@ -599,83 +596,27 @@ class _GrammarErrorTranslationButtonState children: [ if (_showTranslation) Flexible( - child: FutureBuilder( - future: _translationFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.waiting) { - 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: const SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator.adaptive( - strokeWidth: 2, - ), - ), - ); - } - - if (snapshot.hasError) { - 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: Text( - L10n.of(context).oopsSomethingWentWrong, - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryFixed, - fontSize: AppConfig.fontSizeFactor * - AppConfig.messageFontSize, - ), - ), - ); - } - - if (snapshot.hasData) { - 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: Text( - snapshot.data!, - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimaryFixed, - fontSize: AppConfig.fontSizeFactor * - AppConfig.messageFontSize, - ), - textAlign: TextAlign.center, - ), - ); - } - - return const SizedBox(); - }, + child: 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: Text( + widget.translation, + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimaryFixed, + fontSize: + AppConfig.fontSizeFactor * AppConfig.messageFontSize, + ), + textAlign: TextAlign.center, + ), ), ), if (!_showTranslation) diff --git a/lib/pangea/analytics_practice/grammar_error_practice_generator.dart b/lib/pangea/analytics_practice/grammar_error_practice_generator.dart index 30880ab50..854d1b66a 100644 --- a/lib/pangea/analytics_practice/grammar_error_practice_generator.dart +++ b/lib/pangea/analytics_practice/grammar_error_practice_generator.dart @@ -45,6 +45,7 @@ class GrammarErrorPracticeGenerator { errorOffset: igcMatch.offset, errorLength: igcMatch.length, eventID: eventID, + translation: req.grammarErrorInfo!.translation, ), ); } diff --git a/lib/pangea/practice_activities/message_activity_request.dart b/lib/pangea/practice_activities/message_activity_request.dart index d7788cb80..7452dd026 100644 --- a/lib/pangea/practice_activities/message_activity_request.dart +++ b/lib/pangea/practice_activities/message_activity_request.dart @@ -4,7 +4,6 @@ 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/events/event_wrappers/pangea_message_event.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'; @@ -46,13 +45,13 @@ class GrammarErrorRequestInfo { final ChoreoRecordModel choreo; final int stepIndex; final String eventID; - final PangeaMessageEvent? event; + final String translation; const GrammarErrorRequestInfo({ required this.choreo, required this.stepIndex, required this.eventID, - this.event, + required this.translation, }); Map toJson() { @@ -60,6 +59,7 @@ class GrammarErrorRequestInfo { 'choreo': choreo.toJson(), 'step_index': stepIndex, 'event_id': eventID, + 'translation': translation, }; } @@ -68,6 +68,7 @@ class GrammarErrorRequestInfo { choreo: ChoreoRecordModel.fromJson(json['choreo']), stepIndex: json['step_index'] as int, eventID: json['event_id'] as String, + translation: json['translation'] as String, ); } } diff --git a/lib/pangea/practice_activities/practice_activity_model.dart b/lib/pangea/practice_activities/practice_activity_model.dart index 602165b7c..c0531dcba 100644 --- a/lib/pangea/practice_activities/practice_activity_model.dart +++ b/lib/pangea/practice_activities/practice_activity_model.dart @@ -187,6 +187,21 @@ sealed class PracticeActivityModel { tokens: tokens, matchContent: matchContent!, ); + case ActivityTypeEnum.grammarError: + assert( + multipleChoiceContent != null, + "multipleChoiceContent is null in PracticeActivityModel.fromJson for grammarError", + ); + return GrammarErrorPracticeActivityModel( + langCode: langCode, + tokens: tokens, + multipleChoiceContent: multipleChoiceContent!, + text: json['text'] as String, + errorOffset: json['error_offset'] as int, + errorLength: json['error_length'] as int, + eventID: json['event_id'] as String, + translation: json['translation'] as String, + ); default: throw ("Unsupported activity type in PracticeActivityModel.fromJson: $type"); } @@ -358,6 +373,7 @@ class GrammarErrorPracticeActivityModel final int errorOffset; final int errorLength; final String eventID; + final String translation; GrammarErrorPracticeActivityModel({ required super.tokens, @@ -367,7 +383,19 @@ class GrammarErrorPracticeActivityModel required this.errorOffset, required this.errorLength, required this.eventID, + required this.translation, }); + + @override + Map toJson() { + final json = super.toJson(); + json['text'] = text; + json['error_offset'] = errorOffset; + json['error_length'] = errorLength; + json['event_id'] = eventID; + json['translation'] = translation; + return json; + } } class EmojiPracticeActivityModel extends MatchPracticeActivityModel {