From 1a4dc0ba956c183a969fb8f0ff94f33e302881d8 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Fri, 15 Nov 2024 15:17:48 -0500 Subject: [PATCH] tweaking selection criteria --- lib/pages/chat/chat.dart | 2 +- .../message_analytics_controller.dart | 16 +++++++- lib/pangea/models/pangea_token_model.dart | 40 +++++++++---------- .../chat/message_selection_overlay.dart | 25 ++++++++---- lib/pangea/widgets/chat/message_toolbar.dart | 2 - .../practice_activity_card.dart | 39 ++++++++++-------- 6 files changed, 74 insertions(+), 50 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 9cc5cab5a..b8b253649 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -1694,7 +1694,7 @@ class ChatController extends State chatController: this, event: pangeaMessageEvent.event, pangeaMessageEvent: pangeaMessageEvent, - selectedTokenOnInitialization: selectedToken, + initialSelectedToken: selectedToken, nextEvent: nextEvent, prevEvent: prevEvent, ); diff --git a/lib/pangea/controllers/message_analytics_controller.dart b/lib/pangea/controllers/message_analytics_controller.dart index 5d4a07360..33459e6c5 100644 --- a/lib/pangea/controllers/message_analytics_controller.dart +++ b/lib/pangea/controllers/message_analytics_controller.dart @@ -30,14 +30,14 @@ class TargetTokensAndActivityType { // This is kind of complicated // if it's causing problems, // maybe we just verify that the target span of the activity is the same as the target span of the target? - final List allTokenConstructs = tokens + final List relevantConstructs = tokens .map((t) => t.constructs) .expand((e) => e) .map((c) => c.id) .where(activityType.constructFilter) .toList(); - return listEquals(activity.tgtConstructs, allTokenConstructs); + return listEquals(activity.tgtConstructs, relevantConstructs); } @override @@ -74,6 +74,8 @@ class MessageAnalyticsEntry { TargetTokensAndActivityType? get nextActivity => _activityQueue.isNotEmpty ? _activityQueue.first : null; + /// If there are more than 4 tokens that can be heard, we don't want to do word focus listening + /// Otherwise, we don't have enough distractors bool get canDoWordFocusListening => _tokens.where((t) => t.canBeHeard).length > 4; @@ -125,6 +127,16 @@ class MessageAnalyticsEntry { return queue.take(3).toList(); } + /// Removes the last activity from the queue + /// This should only used when there is a startingToken in practice flow + /// and we want to go down to 2 activities + the activity with the startingToken + void goDownTo2Activities() { + if (_activityQueue.isNotEmpty && _activityQueue.length > 2) { + _activityQueue.removeLast(); + } + } + + /// Returns a hidden word activity if there is a sequence of tokens that have hiddenWordListening in their eligibleActivityTypes TargetTokensAndActivityType? getHiddenWordActivity(int numOtherActivities) { // don't do hidden word listening on own messages if (!_includeHiddenWordActivities) { diff --git a/lib/pangea/models/pangea_token_model.dart b/lib/pangea/models/pangea_token_model.dart index 01061832c..cf9acb210 100644 --- a/lib/pangea/models/pangea_token_model.dart +++ b/lib/pangea/models/pangea_token_model.dart @@ -137,7 +137,8 @@ class PangeaToken { /// alias for the end of the token ie offset + length int get end => text.offset + text.length; - bool get isContentWord => ["NOUN", "VERB", "ADJ", "ADV"].contains(pos); + bool get isContentWord => + ["NOUN", "VERB", "ADJ", "ADV", "AUX", "PRON"].contains(pos); bool get canBeHeard => [ "ADJ", @@ -224,36 +225,31 @@ class PangeaToken { } } - bool isActivityProbablyLevelAppropriate(ActivityTypeEnum a) { + bool _isActivityProbablyLevelAppropriate(ActivityTypeEnum a) { switch (a) { case ActivityTypeEnum.wordMeaning: - return vocabConstruct.points < 15; + return vocabConstruct.points < 15 || daysSinceLastUseByType(a) > 2; case ActivityTypeEnum.wordFocusListening: - return !_didActivitySuccessfully(a); + return !_didActivitySuccessfully(a) || daysSinceLastUseByType(a) > 2; case ActivityTypeEnum.hiddenWordListening: - return true; + return daysSinceLastUseByType(a) > 2; } } - bool shouldDoActivity(ActivityTypeEnum a) { - final bool notEmpty = text.content.trim().isNotEmpty; - final bool isEligible = _isActivityBasicallyEligible(a); - final bool isProbablyLevelAppropriate = - isActivityProbablyLevelAppropriate(a); - - return notEmpty && isEligible && isProbablyLevelAppropriate; - } + bool shouldDoActivity(ActivityTypeEnum a) => + lemma.saveVocab && + _isActivityBasicallyEligible(a) && + _isActivityProbablyLevelAppropriate(a); List get eligibleActivityTypes { final List eligibleActivityTypes = []; - if (!lemma.saveVocab || daysSinceLastUse < 1) { + if (!lemma.saveVocab) { return eligibleActivityTypes; } for (final type in ActivityTypeEnum.values) { - if (_isActivityBasicallyEligible(type) && - !_didActivitySuccessfully(type)) { + if (shouldDoActivity(type)) { eligibleActivityTypes.add(type); } } @@ -283,8 +279,9 @@ class PangeaToken { ); } - /// - DateTime? get lastUsed => constructs.fold( + /// lastUsed by activity type + DateTime? _lastUsedByActivityType(ActivityTypeEnum a) => + constructs.where((c) => a.constructFilter(c.id)).fold( null, (previousValue, element) { if (previousValue == null) return element.lastUsed; @@ -295,10 +292,11 @@ class PangeaToken { }, ); - /// daysSinceLastUse - int get daysSinceLastUse { + /// daysSinceLastUse by activity type + int daysSinceLastUseByType(ActivityTypeEnum a) { + final lastUsed = _lastUsedByActivityType(a); if (lastUsed == null) return 1000; - return DateTime.now().difference(lastUsed!).inDays; + return DateTime.now().difference(lastUsed).inDays; } List get _constructIDs { diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index 3a44f04b8..6254db276 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -39,11 +39,11 @@ class MessageSelectionOverlay extends StatefulWidget { required this.chatController, required Event event, required PangeaMessageEvent pangeaMessageEvent, - required PangeaToken? selectedTokenOnInitialization, + required PangeaToken? initialSelectedToken, required Event? nextEvent, required Event? prevEvent, super.key, - }) : _initialSelectedToken = selectedTokenOnInitialization, + }) : _initialSelectedToken = initialSelectedToken, _pangeaMessageEvent = pangeaMessageEvent, _nextEvent = nextEvent, _prevEvent = prevEvent, @@ -78,19 +78,25 @@ class MessageOverlayController extends State bool get showToolbarButtons => !widget._pangeaMessageEvent.isAudioMessage; + /// Decides whether an _initialSelectedToken should be used + /// for a first practice activity on the word meaning PangeaToken? get selectedTargetTokenForWordMeaning { + // if there is no initial selected token, then we don't need to do anything if (widget._initialSelectedToken == null || messageAnalyticsEntry == null) { return null; } - final isInActivity = messageAnalyticsEntry!.isTokenInHiddenWordActivity( + // should not already be involved in a hidden word activity + final isInHiddenWordActivity = + messageAnalyticsEntry!.isTokenInHiddenWordActivity( widget._initialSelectedToken!, ); + // whether the activity should generally be involved in an activity final shouldDoActivity = widget._initialSelectedToken! .shouldDoActivity(ActivityTypeEnum.wordMeaning); - return isInActivity && shouldDoActivity + return !isInHiddenWordActivity && shouldDoActivity ? widget._initialSelectedToken : null; } @@ -106,6 +112,13 @@ class MessageOverlayController extends State const Duration(milliseconds: AppConfig.overlayAnimationDuration), ); + debugPrint( + "selected token: ${widget._initialSelectedToken?.toJson()} total_xp:${widget._initialSelectedToken?.xp} vocab_construct_xp: ${widget._initialSelectedToken?.vocabConstruct.points} daysSincelastUseInWordMeaning ${widget._initialSelectedToken?.daysSinceLastUseByType(ActivityTypeEnum.wordMeaning)}", + ); + debugPrint( + "${widget._initialSelectedToken?.vocabConstruct.uses.map((u) => "${u.useType} ${u.timeStamp}").join(", ")}", + ); + _getTokens(); activitiesLeftToComplete = activitiesLeftToComplete - @@ -216,10 +229,6 @@ class MessageOverlayController extends State } Future _setInitialToolbarModeAndSelectedSpan() async { - debugPrint( - "setting initial toolbar mode and selected span with tokens $tokens", - ); - if (widget._pangeaMessageEvent.isAudioMessage) { toolbarMode = MessageMode.speechToText; return setState(() => toolbarMode = MessageMode.practiceActivity); diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index f8fb574bf..63261ddde 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -128,8 +128,6 @@ class MessageToolbar extends StatelessWidget { ); } return PracticeActivityCard( - selectedTargetTokenForWordMeaning: - overLayController.selectedTargetTokenForWordMeaning, pangeaMessageEvent: pangeaMessageEvent, overlayController: overLayController, ttsController: ttsController, diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 504a3c9f5..e84f57566 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -33,14 +33,12 @@ class PracticeActivityCard extends StatefulWidget { final PangeaMessageEvent pangeaMessageEvent; final MessageOverlayController overlayController; final TtsController ttsController; - final PangeaToken? selectedTargetTokenForWordMeaning; const PracticeActivityCard({ super.key, required this.pangeaMessageEvent, required this.overlayController, required this.ttsController, - required this.selectedTargetTokenForWordMeaning, }); @override @@ -57,6 +55,9 @@ class PracticeActivityCardState extends State { List get practiceActivities => widget.pangeaMessageEvent.practiceActivities; + // if the user has selected a token, we're going to give them an activity on that token first + late PangeaToken? startingToken; + // Used to show an animation when the user completes an activity // while simultaneously fetching a new activity and not showing the loading spinner // until the appropriate time has passed to 'savor the joy' @@ -96,17 +97,14 @@ class PracticeActivityCardState extends State { /// Get an existing activity if there is one. /// If not, get a new activity from the server. Future initialize() async { + startingToken = widget.overlayController.selectedTargetTokenForWordMeaning; _setPracticeActivity( - await _fetchActivity( - selectedTargetTokenForWordMeaning: - widget.selectedTargetTokenForWordMeaning, - ), + await _fetchActivity(), ); } Future _fetchActivity({ ActivityQualityFeedback? activityFeedback, - PangeaToken? selectedTargetTokenForWordMeaning, }) async { // try { debugPrint('Fetching activity'); @@ -125,13 +123,22 @@ class PracticeActivityCardState extends State { // if the user selected a token which is not already in a hidden word activity, // we're going to give them an activity on that token first // otherwise, we're going to give them an activity on the next token in the queue - final TargetTokensAndActivityType? nextActivitySpecs = - selectedTargetTokenForWordMeaning != null - ? TargetTokensAndActivityType( - tokens: [selectedTargetTokenForWordMeaning], - activityType: ActivityTypeEnum.wordMeaning, - ) - : widget.overlayController.messageAnalyticsEntry?.nextActivity; + TargetTokensAndActivityType? nextActivitySpecs; + if (startingToken != null) { + // if the user selected a token, we're going to give them an activity on that token first + nextActivitySpecs = TargetTokensAndActivityType( + tokens: [startingToken!], + activityType: ActivityTypeEnum.wordMeaning, + ); + // clear the starting token so that the next activity is not based on it + startingToken = null; + // we want to go down to 2 activities + the activity with the startingToken + // so we remove the last activity from the queue if there's more than 2 + widget.overlayController.messageAnalyticsEntry?.goDownTo2Activities(); + } else { + nextActivitySpecs = + widget.overlayController.messageAnalyticsEntry?.nextActivity; + } // the client is going to be choosing the next activity now // if nothing is set then it must be done with practice @@ -141,11 +148,11 @@ class PracticeActivityCardState extends State { return null; } + // check if we already have an activity matching the specs final existingActivity = practiceActivities.firstWhereOrNull( (activity) => - nextActivitySpecs.matchesActivity(activity.practiceActivity), + nextActivitySpecs!.matchesActivity(activity.practiceActivity), ); - if (existingActivity != null) { debugPrint('found existing activity'); _updateFetchingActivity(false);