diff --git a/lib/pangea/analytics_practice/analytics_practice_constants.dart b/lib/pangea/analytics_practice/analytics_practice_constants.dart index 3d049b597..a8e06083c 100644 --- a/lib/pangea/analytics_practice/analytics_practice_constants.dart +++ b/lib/pangea/analytics_practice/analytics_practice_constants.dart @@ -1,4 +1,5 @@ class AnalyticsPracticeConstants { static const int timeForBonus = 60; static const int practiceGroupSize = 10; + static const int errorBufferSize = 5; } diff --git a/lib/pangea/analytics_practice/analytics_practice_page.dart b/lib/pangea/analytics_practice/analytics_practice_page.dart index 9329c5170..44b471f92 100644 --- a/lib/pangea/analytics_practice/analytics_practice_page.dart +++ b/lib/pangea/analytics_practice/analytics_practice_page.dart @@ -10,6 +10,8 @@ import 'package:fluffychat/pangea/analytics_data/analytics_data_service.dart'; import 'package:fluffychat/pangea/analytics_data/analytics_updater_mixin.dart'; import 'package:fluffychat/pangea/analytics_data/derived_analytics_data_model.dart'; import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; +import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/analytics_misc/example_message_util.dart'; import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_model.dart'; import 'package:fluffychat/pangea/analytics_practice/analytics_practice_session_repo.dart'; @@ -317,19 +319,27 @@ class AnalyticsPracticeState extends State if (activityState.value is AsyncIdle) { await _initActivityData(); - } else if (_queue.isEmpty) { - await _completeSession(); } else { - activityState.value = const AsyncState.loading(); - selectedMorphChoice.value = null; - final nextActivityCompleter = _queue.removeFirst(); + // Keep trying to load activities from the queue until one succeeds or queue is empty + while (_queue.isNotEmpty) { + activityState.value = const AsyncState.loading(); + selectedMorphChoice.value = null; + final nextActivityCompleter = _queue.removeFirst(); - activityTarget.value = nextActivityCompleter.request; - _playAudio(); - - final activity = await nextActivityCompleter.completer.future; - activityState.value = AsyncState.loaded(activity); - AnalyticsPractice.bypassExitConfirmation = false; + try { + final activity = await nextActivityCompleter.completer.future; + activityTarget.value = nextActivityCompleter.request; + _playAudio(); + activityState.value = AsyncState.loaded(activity); + AnalyticsPractice.bypassExitConfirmation = false; + return; + } catch (e) { + // Completer failed, skip to next + continue; + } + } + // Queue is empty, complete the session + await _completeSession(); } } catch (e) { AnalyticsPractice.bypassExitConfirmation = true; @@ -345,26 +355,30 @@ class AnalyticsPracticeState extends State throw L10n.of(context).noActivityRequest; } - try { - activityState.value = const AsyncState.loading(); - final req = requests.first; - - activityTarget.value = req; - _playAudio(); - - final res = await _fetchActivity(req); - if (!mounted) return; - - activityState.value = AsyncState.loaded(res); - AnalyticsPractice.bypassExitConfirmation = false; - } catch (e) { - AnalyticsPractice.bypassExitConfirmation = true; - if (!mounted) return; - activityState.value = AsyncState.error(e); - return; + for (var i = 0; i < requests.length; i++) { + try { + activityState.value = const AsyncState.loading(); + final req = requests[i]; + final res = await _fetchActivity(req); + if (!mounted) return; + activityTarget.value = req; + _playAudio(); + activityState.value = AsyncState.loaded(res); + AnalyticsPractice.bypassExitConfirmation = false; + // Fill queue with remaining requests + _fillActivityQueue(requests.skip(i + 1).toList()); + return; + } catch (e) { + await recordSkippedUse(requests[i]); + // Try next request + continue; + } } - - _fillActivityQueue(requests.skip(1).toList()); + AnalyticsPractice.bypassExitConfirmation = true; + if (!mounted) return; + activityState.value = + AsyncState.error(L10n.of(context).oopsSomethingWentWrong); + return; } Future _fillActivityQueue( @@ -378,7 +392,6 @@ class AnalyticsPracticeState extends State completer: completer, ), ); - try { final res = await _fetchActivity(request); if (!mounted) return; @@ -386,7 +399,7 @@ class AnalyticsPracticeState extends State } catch (e) { if (!mounted) return; completer.completeError(e); - break; + await recordSkippedUse(request); } } } @@ -443,6 +456,27 @@ class AnalyticsPracticeState extends State _choiceEmojis[requestKey]!.addAll(emojis); } + Future recordSkippedUse(MessageActivityRequest request) async { + // Record a 0 XP use so that activity isn't chosen again soon + _sessionLoader.value!.incrementSkippedActivities(); + final token = request.target.tokens.first; + + final use = OneConstructUse( + useType: ConstructUseTypeEnum.ignPA, + constructType: widget.type, + metadata: ConstructUseMetaData( + roomId: null, + timeStamp: DateTime.now(), + ), + category: token.pos, + lemma: token.lemma.text, + form: token.lemma.text, + xp: 0, + ); + + await _analyticsService.updateService.addAnalytics(null, [use]); + } + Future onSelectChoice( String choiceContent, ) async { @@ -484,7 +518,13 @@ class AnalyticsPracticeState extends State _sessionLoader.value!.completeActivity(); progressNotifier.value = _sessionLoader.value!.progress; - _isComplete ? await _completeSession() : await _continueSession(); + if (_queue.isEmpty) { + await _completeSession(); + } else if (_isComplete) { + await _completeSession(); + } else { + await _continueSession(); + } } Future?> getExampleMessage( diff --git a/lib/pangea/analytics_practice/analytics_practice_session_model.dart b/lib/pangea/analytics_practice/analytics_practice_session_model.dart index e0ee08718..9ab395a13 100644 --- a/lib/pangea/analytics_practice/analytics_practice_session_model.dart +++ b/lib/pangea/analytics_practice/analytics_practice_session_model.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:fluffychat/pangea/analytics_misc/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart'; import 'package:fluffychat/pangea/analytics_practice/analytics_practice_constants.dart'; @@ -45,15 +43,33 @@ class AnalyticsPracticeSessionModel { AnalyticsPracticeSessionState? state, }) : state = state ?? const AnalyticsPracticeSessionState(); - int get _availableActivities => min( - AnalyticsPracticeConstants.practiceGroupSize, - practiceTargets.length, - ); + // Maximum activities to attempt (including skips) + int get _maxAttempts => (AnalyticsPracticeConstants.practiceGroupSize + + AnalyticsPracticeConstants.errorBufferSize) + .clamp(0, practiceTargets.length) + .toInt(); - bool get isComplete => state.currentIndex >= _availableActivities; + int get _completionGoal => AnalyticsPracticeConstants.practiceGroupSize + .clamp(0, practiceTargets.length); - double get progress => - (state.currentIndex / _availableActivities).clamp(0.0, 1.0); + // Total attempted so far (completed + skipped) + int get _totalAttempted => state.currentIndex + state.skippedActivities; + + bool get isComplete { + final complete = state.finished || + state.currentIndex >= _completionGoal || + _totalAttempted >= _maxAttempts; + return complete; + } + + double get progress { + final possibleCompletions = + (state.currentIndex + _maxAttempts - _totalAttempted) + .clamp(0, _completionGoal); + return possibleCompletions > 0 + ? (state.currentIndex / possibleCompletions).clamp(0.0, 1.0) + : 1.0; + } List get activityRequests { return practiceTargets.map((target) { @@ -75,6 +91,10 @@ class AnalyticsPracticeSessionModel { void completeActivity() => state = state.copyWith(currentIndex: state.currentIndex + 1); + void incrementSkippedActivities() => state = state.copyWith( + skippedActivities: state.skippedActivities + 1, + ); + void submitAnswer(OneConstructUse use) => state = state.copyWith( completedUses: [...state.completedUses, use], ); @@ -110,12 +130,14 @@ class AnalyticsPracticeSessionState { final int currentIndex; final bool finished; final int elapsedSeconds; + final int skippedActivities; const AnalyticsPracticeSessionState({ this.completedUses = const [], this.currentIndex = 0, this.finished = false, this.elapsedSeconds = 0, + this.skippedActivities = 0, }); int get totalXpGained => completedUses.fold(0, (sum, use) => sum + use.xp); @@ -168,12 +190,14 @@ class AnalyticsPracticeSessionState { int? currentIndex, bool? finished, int? elapsedSeconds, + int? skippedActivities, }) { return AnalyticsPracticeSessionState( completedUses: completedUses ?? this.completedUses, currentIndex: currentIndex ?? this.currentIndex, finished: finished ?? this.finished, elapsedSeconds: elapsedSeconds ?? this.elapsedSeconds, + skippedActivities: skippedActivities ?? this.skippedActivities, ); } @@ -183,6 +207,7 @@ class AnalyticsPracticeSessionState { 'currentIndex': currentIndex, 'finished': finished, 'elapsedSeconds': elapsedSeconds, + 'skippedActivities': skippedActivities, }; } @@ -196,6 +221,7 @@ class AnalyticsPracticeSessionState { currentIndex: json['currentIndex'] as int? ?? 0, finished: json['finished'] as bool? ?? false, elapsedSeconds: json['elapsedSeconds'] as int? ?? 0, + skippedActivities: json['skippedActivities'] as int? ?? 0, ); } } diff --git a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart index 6a6ef66cf..c2c507741 100644 --- a/lib/pangea/analytics_practice/analytics_practice_session_repo.dart +++ b/lib/pangea/analytics_practice/analytics_practice_session_repo.dart @@ -34,7 +34,8 @@ class AnalyticsPracticeSessionRepo { final activityTypes = ActivityTypeEnum.analyticsPracticeTypes(type); final types = List.generate( - AnalyticsPracticeConstants.practiceGroupSize, + AnalyticsPracticeConstants.practiceGroupSize + + AnalyticsPracticeConstants.errorBufferSize, (_) => activityTypes[r.nextInt(activityTypes.length)], ); @@ -55,11 +56,13 @@ class AnalyticsPracticeSessionRepo { } else { final errorTargets = await _fetchErrors(); targets.addAll(errorTargets); - - if (targets.length < AnalyticsPracticeConstants.practiceGroupSize) { + if (targets.length < + (AnalyticsPracticeConstants.practiceGroupSize + + AnalyticsPracticeConstants.errorBufferSize)) { final morphs = await _fetchMorphs(); - final remainingCount = - AnalyticsPracticeConstants.practiceGroupSize - targets.length; + final remainingCount = (AnalyticsPracticeConstants.practiceGroupSize + + AnalyticsPracticeConstants.errorBufferSize) - + targets.length; final morphEntries = morphs.entries.take(remainingCount); for (final entry in morphEntries) { @@ -113,7 +116,9 @@ class AnalyticsPracticeSessionRepo { if (seemLemmas.contains(construct.lemma)) continue; seemLemmas.add(construct.lemma); targets.add(construct.id); - if (targets.length >= AnalyticsPracticeConstants.practiceGroupSize) { + if (targets.length >= + (AnalyticsPracticeConstants.practiceGroupSize + + AnalyticsPracticeConstants.errorBufferSize)) { break; } } @@ -140,7 +145,9 @@ class AnalyticsPracticeSessionRepo { final Set seenForms = {}; for (final entry in constructs) { - if (targets.length >= AnalyticsPracticeConstants.practiceGroupSize) { + if (targets.length >= + (AnalyticsPracticeConstants.practiceGroupSize + + AnalyticsPracticeConstants.errorBufferSize)) { break; } @@ -150,7 +157,9 @@ class AnalyticsPracticeSessionRepo { } for (final use in entry.cappedUses) { - if (targets.length >= AnalyticsPracticeConstants.practiceGroupSize) { + if (targets.length >= + (AnalyticsPracticeConstants.practiceGroupSize + + AnalyticsPracticeConstants.errorBufferSize)) { break; }