5415 if invalid lemma definition breaks practice (#5466)

* skip error causing lemmas in practice

* update progress on skipping

and play audio/update value after loading question, so a skipped questions isn't displayed

* remove unnecessary line and comment
This commit is contained in:
avashilling 2026-01-28 09:11:03 -05:00 committed by GitHub
parent 0583409b09
commit fc277350f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 126 additions and 50 deletions

View file

@ -1,4 +1,5 @@
class AnalyticsPracticeConstants {
static const int timeForBonus = 60;
static const int practiceGroupSize = 10;
static const int errorBufferSize = 5;
}

View file

@ -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<AnalyticsPractice>
if (activityState.value
is AsyncIdle<MultipleChoicePracticeActivityModel>) {
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<AnalyticsPractice>
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<void> _fillActivityQueue(
@ -378,7 +392,6 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
completer: completer,
),
);
try {
final res = await _fetchActivity(request);
if (!mounted) return;
@ -386,7 +399,7 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
} catch (e) {
if (!mounted) return;
completer.completeError(e);
break;
await recordSkippedUse(request);
}
}
}
@ -443,6 +456,27 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
_choiceEmojis[requestKey]!.addAll(emojis);
}
Future<void> 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<void> onSelectChoice(
String choiceContent,
) async {
@ -484,7 +518,13 @@ class AnalyticsPracticeState extends State<AnalyticsPractice>
_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<List<InlineSpan>?> getExampleMessage(

View file

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

View file

@ -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<String> 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;
}