From 044ae521d26eb0141464ee6795a92bbe218b681f Mon Sep 17 00:00:00 2001 From: Ava Shilling <165050625+avashilling@users.noreply.github.com> Date: Wed, 11 Feb 2026 15:06:11 -0500 Subject: [PATCH 1/6] chore: dont allow reclicking choices --- .../analytics_practice_page.dart | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/pangea/analytics_practice/analytics_practice_page.dart b/lib/pangea/analytics_practice/analytics_practice_page.dart index 42d3e452a..989c4c922 100644 --- a/lib/pangea/analytics_practice/analytics_practice_page.dart +++ b/lib/pangea/analytics_practice/analytics_practice_page.dart @@ -97,7 +97,7 @@ class AnalyticsPracticeState extends State final ValueNotifier hintPressedNotifier = ValueNotifier(false); - final Set _selectedCorrectAnswers = {}; + final Set _clickedChoices = {}; // Track if we're showing the completion message for audio activities final ValueNotifier showingAudioCompletion = ValueNotifier(false); @@ -347,7 +347,7 @@ class AnalyticsPracticeState extends State activityState.value = const AsyncState.loading(); selectedMorphChoice.value = null; hintPressedNotifier.value = false; - _selectedCorrectAnswers.clear(); + _clickedChoices.clear(); final nextActivityCompleter = _queue.removeFirst(); try { @@ -565,6 +565,14 @@ class AnalyticsPracticeState extends State if (_currentActivity == null) return; final activity = _currentActivity!; + // Mark this choice as clicked so it can't be clicked again + if (_clickedChoices.contains(choiceContent)) { + return; + } else { + setState(() { + _clickedChoices.add(choiceContent); + }); + } // Track the selection for display if (activity is MorphPracticeActivityModel) { selectedMorphChoice.value = SelectedMorphChoice( @@ -577,9 +585,6 @@ class AnalyticsPracticeState extends State final isAudioActivity = activity.activityType == ActivityTypeEnum.lemmaAudio; - if (isAudioActivity && isCorrect) { - _selectedCorrectAnswers.add(choiceContent); - } if (isCorrect && !isAudioActivity) { // Non-audio activities disable choices after first correct answer @@ -602,11 +607,11 @@ class AnalyticsPracticeState extends State if (!isCorrect) return; - // For audio activities, check if all answers have been selected + // For audio activities, check if all correct answers have been clicked if (isAudioActivity) { final allAnswers = activity.multipleChoiceContent.answers; final allSelected = allAnswers.every( - (answer) => _selectedCorrectAnswers.contains(answer), + (answer) => _clickedChoices.contains(answer), ); if (!allSelected) { From ce83b928cbbc333334592a4e89747b45d3e86ba5 Mon Sep 17 00:00:00 2001 From: Ava Shilling <165050625+avashilling@users.noreply.github.com> Date: Wed, 11 Feb 2026 15:06:58 -0500 Subject: [PATCH 2/6] chore: remove unused audio choice card --- .../choice_cards/audio_choice_card.dart | 52 ------------------- 1 file changed, 52 deletions(-) delete mode 100644 lib/pangea/analytics_practice/choice_cards/audio_choice_card.dart diff --git a/lib/pangea/analytics_practice/choice_cards/audio_choice_card.dart b/lib/pangea/analytics_practice/choice_cards/audio_choice_card.dart deleted file mode 100644 index a7a26864e..000000000 --- a/lib/pangea/analytics_practice/choice_cards/audio_choice_card.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:fluffychat/l10n/l10n.dart'; -import 'package:fluffychat/pangea/analytics_practice/choice_cards/game_choice_card.dart'; -import 'package:fluffychat/pangea/common/widgets/word_audio_button.dart'; -import 'package:fluffychat/widgets/matrix.dart'; - -/// Displays an audio button with a select label in a row layout -/// TODO: needs a better design and button handling -class AudioChoiceCard extends StatelessWidget { - final String text; - final String targetId; - final VoidCallback onPressed; - final bool isCorrect; - final double height; - final bool isEnabled; - - const AudioChoiceCard({ - required this.text, - required this.targetId, - required this.onPressed, - required this.isCorrect, - this.height = 72.0, - this.isEnabled = true, - super.key, - }); - - @override - Widget build(BuildContext context) { - return GameChoiceCard( - shouldFlip: false, - targetId: targetId, - onPressed: onPressed, - isCorrect: isCorrect, - height: height, - isEnabled: isEnabled, - child: Row( - children: [ - Expanded( - child: WordAudioButton( - text: text, - uniqueID: "vocab_practice_choice_$text", - langCode: - MatrixState.pangeaController.userController.userL2!.langCode, - ), - ), - Text(L10n.of(context).select), - ], - ), - ); - } -} From c1b18ab29cfb74d4cb6d83718bb3d101a7d1606b Mon Sep 17 00:00:00 2001 From: Ava Shilling <165050625+avashilling@users.noreply.github.com> Date: Wed, 11 Feb 2026 15:13:47 -0500 Subject: [PATCH 3/6] chore: dispose audio player between questions by adding a valuekey, so that the audio stops playing during next question --- lib/pangea/analytics_practice/analytics_practice_view.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pangea/analytics_practice/analytics_practice_view.dart b/lib/pangea/analytics_practice/analytics_practice_view.dart index 9046f840a..56b3eeca4 100644 --- a/lib/pangea/analytics_practice/analytics_practice_view.dart +++ b/lib/pangea/analytics_practice/analytics_practice_view.dart @@ -247,6 +247,7 @@ class _AnalyticsPracticeCenterContent extends StatelessWidget { child: Center( child: AudioPlayerWidget( null, + key: ValueKey('audio_${activity.eventId}'), color: Theme.of(context).colorScheme.primary, linkColor: Theme.of(context).colorScheme.secondary, fontSize: From 1deb3c9ad7b3027c9e51751a358bf83634f19acc Mon Sep 17 00:00:00 2001 From: Ava Shilling <165050625+avashilling@users.noreply.github.com> Date: Wed, 11 Feb 2026 15:31:09 -0500 Subject: [PATCH 4/6] chore: give autoplay in audioplayer a function --- lib/pages/chat/events/audio_player.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/pages/chat/events/audio_player.dart b/lib/pages/chat/events/audio_player.dart index f8e32b05b..ec53acdea 100644 --- a/lib/pages/chat/events/audio_player.dart +++ b/lib/pages/chat/events/audio_player.dart @@ -456,6 +456,10 @@ class AudioPlayerState extends State { final duration = Duration(milliseconds: durationInt); _durationString = duration.minuteSecondString; } + + if (widget.autoplay) { + WidgetsBinding.instance.addPostFrameCallback((_) => _onButtonTap()); + } } @override From 526681255e11a65d0e00750b38ea58246a077cab Mon Sep 17 00:00:00 2001 From: Ava Shilling <165050625+avashilling@users.noreply.github.com> Date: Wed, 11 Feb 2026 15:50:50 -0500 Subject: [PATCH 5/6] chore: shuffle tokens before choosing answers --- .../analytics_practice/vocab_audio_activity_generator.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pangea/analytics_practice/vocab_audio_activity_generator.dart b/lib/pangea/analytics_practice/vocab_audio_activity_generator.dart index f13839116..8b794944a 100644 --- a/lib/pangea/analytics_practice/vocab_audio_activity_generator.dart +++ b/lib/pangea/analytics_practice/vocab_audio_activity_generator.dart @@ -16,7 +16,8 @@ class VocabAudioActivityGenerator { wordsInMessage.add(t.text.content.toLowerCase()); } - // Extract up to 3 additional words as answers + // Extract up to 3 additional words as answers, from shuffled message + audioExample.tokens.shuffle(); final otherWords = audioExample.tokens .where( (t) => From a05796e6de787b066051d510d6728a29d73b073c Mon Sep 17 00:00:00 2001 From: Ava Shilling <165050625+avashilling@users.noreply.github.com> Date: Wed, 11 Feb 2026 16:46:38 -0500 Subject: [PATCH 6/6] fix: give XP to clicked choice in audio practice Or, upon wrong click give XP to chosen activity token --- .../analytics_practice_page.dart | 10 +++++++- .../vocab_audio_activity_generator.dart | 15 +++++++++++- .../practice_activity_model.dart | 24 +++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/lib/pangea/analytics_practice/analytics_practice_page.dart b/lib/pangea/analytics_practice/analytics_practice_page.dart index 989c4c922..618c3efb4 100644 --- a/lib/pangea/analytics_practice/analytics_practice_page.dart +++ b/lib/pangea/analytics_practice/analytics_practice_page.dart @@ -592,9 +592,17 @@ class AnalyticsPracticeState extends State } // Update activity record + // For audio activities, find the token that matches the clicked word + final tokenForChoice = isAudioActivity + ? activity.tokens.firstWhere( + (t) => t.text.content.toLowerCase() == choiceContent.toLowerCase(), + orElse: () => activity.tokens.first, + ) + : activity.tokens.first; + PracticeRecordController.onSelectChoice( choiceContent, - activity.tokens.first, + tokenForChoice, activity, ); diff --git a/lib/pangea/analytics_practice/vocab_audio_activity_generator.dart b/lib/pangea/analytics_practice/vocab_audio_activity_generator.dart index 8b794944a..3f2f7e489 100644 --- a/lib/pangea/analytics_practice/vocab_audio_activity_generator.dart +++ b/lib/pangea/analytics_practice/vocab_audio_activity_generator.dart @@ -1,4 +1,5 @@ import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart'; +import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/practice_activities/lemma_activity_generator.dart'; import 'package:fluffychat/pangea/practice_activities/message_activity_request.dart'; import 'package:fluffychat/pangea/practice_activities/multiple_choice_activity_model.dart'; @@ -51,9 +52,21 @@ class VocabAudioActivityGenerator { final allChoices = [...choicesList, ...answers]; allChoices.shuffle(); + final allTokens = audioExample?.tokens ?? req.target.tokens; + final answerTokens = []; + + answerTokens.add(token); + if (audioExample != null) { + for (final t in allTokens) { + if (t != token && answers.contains(t.text.content.toLowerCase())) { + answerTokens.add(t); + } + } + } + return MessageActivityResponse( activity: VocabAudioPracticeActivityModel( - tokens: req.target.tokens, + tokens: answerTokens, langCode: req.userL2, multipleChoiceContent: MultipleChoiceActivity( choices: allChoices.toSet(), diff --git a/lib/pangea/practice_activities/practice_activity_model.dart b/lib/pangea/practice_activities/practice_activity_model.dart index 29dc56791..76fe845e6 100644 --- a/lib/pangea/practice_activities/practice_activity_model.dart +++ b/lib/pangea/practice_activities/practice_activity_model.dart @@ -358,6 +358,30 @@ class VocabAudioPracticeActivityModel required this.exampleMessage, }); + @override + OneConstructUse constructUse(String choiceContent) { + final correct = multipleChoiceContent.isCorrect(choiceContent); + final useType = correct + ? activityType.correctUse + : activityType.incorrectUse; + + // For audio activities, find the token that matches the clicked word + final matchingToken = tokens.firstWhere( + (t) => t.text.content.toLowerCase() == choiceContent.toLowerCase(), + orElse: () => tokens.first, + ); + + return OneConstructUse( + useType: useType, + constructType: ConstructTypeEnum.vocab, + metadata: ConstructUseMetaData(roomId: null, timeStamp: DateTime.now()), + category: matchingToken.pos, + lemma: matchingToken.lemma.text, + form: matchingToken.lemma.text, + xp: useType.pointValue, + ); + } + @override Map toJson() { final json = super.toJson();