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 diff --git a/lib/pangea/analytics_practice/analytics_practice_page.dart b/lib/pangea/analytics_practice/analytics_practice_page.dart index 42d3e452a..618c3efb4 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 @@ -587,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, ); @@ -602,11 +615,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) { 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: 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), - ], - ), - ); - } -} diff --git a/lib/pangea/analytics_practice/vocab_audio_activity_generator.dart b/lib/pangea/analytics_practice/vocab_audio_activity_generator.dart index f13839116..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'; @@ -16,7 +17,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) => @@ -50,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();