Merge pull request #5666 from pangeachat/5658-audio-practice-issues

5658 audio practice issues
This commit is contained in:
ggurdin 2026-02-12 09:17:31 -05:00 committed by GitHub
commit f52f9c3744
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 66 additions and 62 deletions

View file

@ -456,6 +456,10 @@ class AudioPlayerState extends State<AudioPlayerWidget> {
final duration = Duration(milliseconds: durationInt);
_durationString = duration.minuteSecondString;
}
if (widget.autoplay) {
WidgetsBinding.instance.addPostFrameCallback((_) => _onButtonTap());
}
}
@override

View file

@ -97,7 +97,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
final ValueNotifier<bool> hintPressedNotifier = ValueNotifier<bool>(false);
final Set<String> _selectedCorrectAnswers = {};
final Set<String> _clickedChoices = {};
// Track if we're showing the completion message for audio activities
final ValueNotifier<bool> showingAudioCompletion = ValueNotifier<bool>(false);
@ -347,7 +347,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
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<AnalyticsPractice>
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<AnalyticsPractice>
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<AnalyticsPractice>
}
// 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<AnalyticsPractice>
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) {

View file

@ -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:

View file

@ -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),
],
),
);
}
}

View file

@ -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 = <PangeaToken>[];
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(),

View file

@ -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<String, dynamic> toJson() {
final json = super.toJson();