-
Notifications
You must be signed in to change notification settings - Fork 5
Defer audio example resolution out of practice selection (#7702) #7708
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,58 +1,63 @@ | ||
| import 'package:fluffychat/routes/analytics/construct_analytics/practice/analytics_practice_session_model.dart'; | ||
| import 'package:fluffychat/routes/analytics/construct_analytics/practice/example_message_util.dart'; | ||
| import 'package:fluffychat/routes/analytics/construct_analytics/practice/vocab_meaning_practice_exercise_generator.dart'; | ||
| import 'package:fluffychat/routes/chat/events/models/pangea_token_model.dart'; | ||
| import 'package:fluffychat/routes/chat/toolbar/practice_exercises/lemma_practice_exercise_generator.dart'; | ||
| import 'package:fluffychat/routes/chat/toolbar/practice_exercises/message_practice_exercise_request.dart'; | ||
| import 'package:fluffychat/routes/chat/toolbar/practice_exercises/multiple_choice_practice_exercise_model.dart'; | ||
| import 'package:fluffychat/routes/chat/toolbar/practice_exercises/practice_exercise_model.dart'; | ||
| import 'package:fluffychat/routes/chat/toolbar/practice_exercises/practice_exercise_type_enum.dart'; | ||
| import 'package:fluffychat/routes/chat/toolbar/practice_exercises/practice_target.dart'; | ||
| import 'package:fluffychat/widgets/matrix.dart'; | ||
|
|
||
| class VocabAudioPracticeExerciseGenerator { | ||
| static Future<MessagePracticeExerciseResponse> get( | ||
| MessagePracticeExerciseRequest req, | ||
| ) async { | ||
| final token = req.target.tokens.first; | ||
| final audioExample = req.audioExampleMessage; | ||
|
|
||
| // Find the matching token in the audio example message to get the correct form | ||
| PangeaToken targetToken = token; | ||
| if (audioExample != null) { | ||
| final matchingToken = audioExample.tokens.firstWhere( | ||
| (t) => t.lemma.text.toLowerCase() == token.lemma.text.toLowerCase(), | ||
| orElse: () => token, | ||
| ); | ||
| targetToken = matchingToken; | ||
| // The audio example message is resolved HERE (in the eager background | ||
| // queue), not at selection — resolving it during selection blocked first | ||
| // paint on N serial event fetches (#7702). If it can't be resolved, fall | ||
| // back to a meaning exercise for the same lemma so the slot isn't lost. | ||
| final audioExample = | ||
| req.audioExampleMessage ?? await _resolveAudioExample(req); | ||
| if (audioExample == null) { | ||
| return VocabMeaningPracticeExerciseGenerator.get(_asMeaningRequest(req)); | ||
|
Comment on lines
+23
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Make resolver errors trigger the meaning fallback. The fallback handles only Also applies to: 102-115 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| // Find the matching token in the audio example message to get the correct form | ||
| final matchingToken = audioExample.tokens.firstWhere( | ||
| (t) => t.lemma.text.toLowerCase() == token.lemma.text.toLowerCase(), | ||
| orElse: () => token, | ||
| ); | ||
| final PangeaToken targetToken = matchingToken; | ||
|
|
||
| final Set<String> answers = {}; | ||
| final Set<String> wordsInMessage = {}; | ||
| final Set<String> lemmasInMessage = {}; | ||
| final List<PangeaToken> answerTokens = [targetToken]; | ||
|
|
||
| if (audioExample != null) { | ||
| // Collect all words/lemmas in message and select additional answer words | ||
| final List<PangeaToken> potentialAnswers = []; | ||
| // Collect all words/lemmas in message and select additional answer words | ||
| final List<PangeaToken> potentialAnswers = []; | ||
|
|
||
| for (final t in audioExample.tokens) { | ||
| wordsInMessage.add(t.text.content.toLowerCase()); | ||
| lemmasInMessage.add(t.lemma.text.toLowerCase()); | ||
| for (final t in audioExample.tokens) { | ||
| wordsInMessage.add(t.text.content.toLowerCase()); | ||
| lemmasInMessage.add(t.lemma.text.toLowerCase()); | ||
|
|
||
| if (t != targetToken && | ||
| t.lemma.saveVocab && | ||
| t.text.content.trim().isNotEmpty) { | ||
| potentialAnswers.add(t); | ||
| } | ||
| if (t != targetToken && | ||
| t.lemma.saveVocab && | ||
| t.text.content.trim().isNotEmpty) { | ||
| potentialAnswers.add(t); | ||
| } | ||
| } | ||
|
|
||
| // Shuffle and select up to 3 additional answer words | ||
| potentialAnswers.shuffle(); | ||
| final otherAnswerTokens = potentialAnswers.take(3).toList(); | ||
| // Shuffle and select up to 3 additional answer words | ||
| potentialAnswers.shuffle(); | ||
| final otherAnswerTokens = potentialAnswers.take(3).toList(); | ||
|
|
||
| answerTokens.addAll(otherAnswerTokens); | ||
| answers.addAll(answerTokens.map((t) => t.text.content.toLowerCase())); | ||
| } else { | ||
| answers.add(targetToken.text.content.toLowerCase()); | ||
| wordsInMessage.add(targetToken.text.content.toLowerCase()); | ||
| lemmasInMessage.add(targetToken.lemma.text.toLowerCase()); | ||
| } | ||
| answerTokens.addAll(otherAnswerTokens); | ||
| answers.addAll(answerTokens.map((t) => t.text.content.toLowerCase())); | ||
|
|
||
| // Generate distractors, filtering out anything in the message (by form or lemma) | ||
| final choices = | ||
|
|
@@ -83,12 +88,43 @@ class VocabAudioPracticeExerciseGenerator { | |
| choices: allChoices.toSet(), | ||
| answers: answers, | ||
| ), | ||
| roomId: audioExample?.roomId, | ||
| eventId: audioExample?.eventId, | ||
| exampleMessage: | ||
| audioExample?.exampleMessage ?? | ||
| const ExampleMessageInfo(exampleMessage: []), | ||
| roomId: audioExample.roomId, | ||
| eventId: audioExample.eventId, | ||
| exampleMessage: audioExample.exampleMessage, | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| /// Resolve the audio example message for [req]'s lemma at generation time | ||
| /// (off the selection critical path, #7702). Looks the construct up from | ||
| /// local analytics, then finds an audio-capable example message. Null when | ||
| /// no usable example exists — the caller falls back to a meaning exercise. | ||
| static Future<AudioExampleMessage?> _resolveAudioExample( | ||
| MessagePracticeExerciseRequest req, | ||
| ) async { | ||
| final id = req.target.tokens.first.vocabConstructID; | ||
| final l2 = req.userL2.split('-').first; | ||
| final constructs = await MatrixState | ||
| .pangeaController | ||
| .matrixState | ||
| .analyticsDataService | ||
| .getConstructUses([id], l2); | ||
| final construct = constructs[id]; | ||
| if (construct == null) return null; | ||
| return ExampleMessageUtil.getAudioExampleMessage(construct, noBold: true); | ||
| } | ||
|
|
||
| /// A meaning-exercise request for the same lemma, used when an audio example | ||
| /// can't be resolved. Meaning has no example-message dependency. | ||
| static MessagePracticeExerciseRequest _asMeaningRequest( | ||
| MessagePracticeExerciseRequest req, | ||
| ) => MessagePracticeExerciseRequest( | ||
| userL1: req.userL1, | ||
| userL2: req.userL2, | ||
| exerciseQualityFeedback: null, | ||
| target: PracticeTarget( | ||
| tokens: req.target.tokens, | ||
| exerciseType: PracticeExerciseTypeEnum.lemmaMeaning, | ||
| ), | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Finish updating the Activity Generation section to the new contract.
These lines document the new routes and
MessagePracticeExerciseRequest/PracticeExerciseModel, but Lines 197-210 still reference the oldlib/pangea/paths,MessageActivityRequest, andPracticeActivityModel. Update that section to avoid conflicting flow documentation.🤖 Prompt for AI Agents