tweaking selection criteria
This commit is contained in:
parent
867c55bb36
commit
1a4dc0ba95
6 changed files with 74 additions and 50 deletions
|
|
@ -1694,7 +1694,7 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
chatController: this,
|
||||
event: pangeaMessageEvent.event,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
selectedTokenOnInitialization: selectedToken,
|
||||
initialSelectedToken: selectedToken,
|
||||
nextEvent: nextEvent,
|
||||
prevEvent: prevEvent,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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<ConstructIdentifier> allTokenConstructs = tokens
|
||||
final List<ConstructIdentifier> 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) {
|
||||
|
|
|
|||
|
|
@ -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<ActivityTypeEnum> get eligibleActivityTypes {
|
||||
final List<ActivityTypeEnum> 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<DateTime?>(
|
||||
/// lastUsed by activity type
|
||||
DateTime? _lastUsedByActivityType(ActivityTypeEnum a) =>
|
||||
constructs.where((c) => a.constructFilter(c.id)).fold<DateTime?>(
|
||||
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<ConstructIdentifier> get _constructIDs {
|
||||
|
|
|
|||
|
|
@ -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<MessageSelectionOverlay>
|
|||
|
||||
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<MessageSelectionOverlay>
|
|||
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<MessageSelectionOverlay>
|
|||
}
|
||||
|
||||
Future<void> _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);
|
||||
|
|
|
|||
|
|
@ -128,8 +128,6 @@ class MessageToolbar extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
return PracticeActivityCard(
|
||||
selectedTargetTokenForWordMeaning:
|
||||
overLayController.selectedTargetTokenForWordMeaning,
|
||||
pangeaMessageEvent: pangeaMessageEvent,
|
||||
overlayController: overLayController,
|
||||
ttsController: ttsController,
|
||||
|
|
|
|||
|
|
@ -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<PracticeActivityCard> {
|
|||
List<PracticeActivityEvent> 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<PracticeActivityCard> {
|
|||
/// Get an existing activity if there is one.
|
||||
/// If not, get a new activity from the server.
|
||||
Future<void> initialize() async {
|
||||
startingToken = widget.overlayController.selectedTargetTokenForWordMeaning;
|
||||
_setPracticeActivity(
|
||||
await _fetchActivity(
|
||||
selectedTargetTokenForWordMeaning:
|
||||
widget.selectedTargetTokenForWordMeaning,
|
||||
),
|
||||
await _fetchActivity(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<PracticeActivityModel?> _fetchActivity({
|
||||
ActivityQualityFeedback? activityFeedback,
|
||||
PangeaToken? selectedTargetTokenForWordMeaning,
|
||||
}) async {
|
||||
// try {
|
||||
debugPrint('Fetching activity');
|
||||
|
|
@ -125,13 +123,22 @@ class PracticeActivityCardState extends State<PracticeActivityCard> {
|
|||
// 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<PracticeActivityCard> {
|
|||
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);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue