Merge branch 'main' into 5421-grammar-practice-todos
This commit is contained in:
commit
44673a4149
8 changed files with 170 additions and 109 deletions
|
|
@ -124,7 +124,10 @@ class AnalyticsDataService {
|
|||
|
||||
_invalidateCaches();
|
||||
final analyticsUserId = await _analyticsClientGetter.database.getUserID();
|
||||
if (analyticsUserId != client.userID) {
|
||||
final lastUpdated =
|
||||
await _analyticsClientGetter.database.getLastUpdated();
|
||||
|
||||
if (analyticsUserId != client.userID || lastUpdated == null) {
|
||||
await _clearDatabase();
|
||||
await _analyticsClientGetter.database.updateUserID(client.userID!);
|
||||
}
|
||||
|
|
@ -253,6 +256,8 @@ class AnalyticsDataService {
|
|||
final Map<ConstructIdentifier, DateTime?> cappedLastUseCache = {};
|
||||
for (final use in uses) {
|
||||
if (blocked.contains(use.identifier)) continue;
|
||||
if (use.category == 'other') continue;
|
||||
|
||||
if (!cappedLastUseCache.containsKey(use.identifier)) {
|
||||
final constructs = await getConstructUse(use.identifier);
|
||||
cappedLastUseCache[use.identifier] = constructs.cappedLastUse;
|
||||
|
|
@ -324,7 +329,8 @@ class AnalyticsDataService {
|
|||
final existing = cleaned[canonical];
|
||||
if (existing != null) {
|
||||
existing.merge(entry);
|
||||
} else if (!blocked.contains(canonical)) {
|
||||
} else if (!blocked.contains(canonical) &&
|
||||
canonical.category != 'other') {
|
||||
cleaned[canonical] = entry;
|
||||
}
|
||||
}
|
||||
|
|
@ -345,7 +351,10 @@ class AnalyticsDataService {
|
|||
final blocked = blockedConstructs;
|
||||
final uses = newConstructs
|
||||
.where(
|
||||
(c) => c.constructType == type && !blocked.contains(c.identifier),
|
||||
(c) =>
|
||||
c.constructType == type &&
|
||||
!blocked.contains(c.identifier) &&
|
||||
c.identifier.category != 'other',
|
||||
)
|
||||
.toList();
|
||||
|
||||
|
|
|
|||
|
|
@ -178,6 +178,12 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
|
||||
Future<String?> getUserID() => _lastEventTimestampBox.get('user_id');
|
||||
|
||||
Future<DateTime?> getLastUpdated() async {
|
||||
final entry = await _lastEventTimestampBox.get('last_updated');
|
||||
if (entry == null) return null;
|
||||
return DateTime.tryParse(entry);
|
||||
}
|
||||
|
||||
Future<DateTime?> getLastEventTimestamp() async {
|
||||
final timestampString =
|
||||
await _lastEventTimestampBox.get('last_event_timestamp');
|
||||
|
|
@ -482,6 +488,15 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
});
|
||||
}
|
||||
|
||||
Future<void> updateLastUpdated(DateTime timestamp) {
|
||||
return _transaction(() async {
|
||||
await _lastEventTimestampBox.put(
|
||||
'last_updated',
|
||||
timestamp.toIso8601String(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> updateXPOffset(int offset) {
|
||||
return _transaction(() async {
|
||||
final stats = await getDerivedStats();
|
||||
|
|
@ -577,6 +592,8 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
);
|
||||
});
|
||||
|
||||
await updateLastUpdated(DateTime.now());
|
||||
|
||||
stopwatch.stop();
|
||||
Logs().i(
|
||||
"Server analytics update took ${stopwatch.elapsedMilliseconds} ms",
|
||||
|
|
@ -631,6 +648,8 @@ class AnalyticsDatabase with DatabaseFileStorage {
|
|||
}
|
||||
});
|
||||
|
||||
await updateLastUpdated(DateTime.now());
|
||||
|
||||
stopwatch.stop();
|
||||
Logs().i("Local analytics update took ${stopwatch.elapsedMilliseconds} ms");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import 'package:collection/collection.dart';
|
||||
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/construct_use_model.dart';
|
||||
import 'package:fluffychat/pangea/analytics_misc/constructs_model.dart';
|
||||
|
|
@ -7,7 +5,6 @@ import 'package:fluffychat/pangea/constructs/construct_identifier.dart';
|
|||
|
||||
class ConstructMergeTable {
|
||||
Map<String, Set<ConstructIdentifier>> lemmaTypeGroups = {};
|
||||
Map<ConstructIdentifier, ConstructIdentifier> otherToSpecific = {};
|
||||
final Map<ConstructIdentifier, ConstructIdentifier> caseInsensitive = {};
|
||||
|
||||
void addConstructs(
|
||||
|
|
@ -27,6 +24,8 @@ class ConstructMergeTable {
|
|||
for (final use in uses) {
|
||||
final id = use.identifier;
|
||||
if (exclude.contains(id)) continue;
|
||||
if (id.category == 'other') continue;
|
||||
|
||||
final composite = id.compositeKey;
|
||||
(lemmaTypeGroups[composite] ??= {}).add(id);
|
||||
}
|
||||
|
|
@ -34,6 +33,8 @@ class ConstructMergeTable {
|
|||
for (final use in uses) {
|
||||
final id = use.identifier;
|
||||
if (exclude.contains(id)) continue;
|
||||
if (id.category == 'other') continue;
|
||||
|
||||
final group = lemmaTypeGroups[id.compositeKey];
|
||||
if (group == null) continue;
|
||||
final matches = group.where((m) => m != id && m.string == id.string);
|
||||
|
|
@ -42,20 +43,6 @@ class ConstructMergeTable {
|
|||
caseInsensitive[id] = id;
|
||||
}
|
||||
}
|
||||
|
||||
for (final use in uses) {
|
||||
if (exclude.contains(use.identifier)) continue;
|
||||
final id = use.identifier;
|
||||
final composite = id.compositeKey;
|
||||
if (id.category == 'other' && !otherToSpecific.containsKey(id)) {
|
||||
final specific = lemmaTypeGroups[composite]!.firstWhereOrNull(
|
||||
(k) => k.category != 'other',
|
||||
);
|
||||
if (specific != null) {
|
||||
otherToSpecific[id] = caseInsensitive[specific] ?? specific;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void removeConstruct(ConstructIdentifier id) {
|
||||
|
|
@ -68,17 +55,6 @@ class ConstructMergeTable {
|
|||
lemmaTypeGroups.remove(composite);
|
||||
}
|
||||
|
||||
if (id.category != 'other') {
|
||||
final otherId = ConstructIdentifier(
|
||||
lemma: id.lemma,
|
||||
type: id.type,
|
||||
category: 'other',
|
||||
);
|
||||
otherToSpecific.remove(otherId);
|
||||
} else {
|
||||
otherToSpecific.remove(id);
|
||||
}
|
||||
|
||||
final caseEntry = caseInsensitive[id];
|
||||
if (caseEntry != null && caseEntry != id) {
|
||||
caseInsensitive.remove(caseEntry);
|
||||
|
|
@ -87,8 +63,7 @@ class ConstructMergeTable {
|
|||
}
|
||||
|
||||
ConstructIdentifier resolve(ConstructIdentifier key) {
|
||||
final specific = otherToSpecific[key] ?? key;
|
||||
return caseInsensitive[specific] ?? specific;
|
||||
return caseInsensitive[key] ?? key;
|
||||
}
|
||||
|
||||
List<ConstructIdentifier> groupedIds(
|
||||
|
|
@ -96,10 +71,12 @@ class ConstructMergeTable {
|
|||
Set<ConstructIdentifier> exclude,
|
||||
) {
|
||||
final keys = <ConstructIdentifier>[];
|
||||
if (!exclude.contains(id)) {
|
||||
keys.add(id);
|
||||
if (exclude.contains(id) || id.category == 'other') {
|
||||
return keys;
|
||||
}
|
||||
|
||||
keys.add(id);
|
||||
|
||||
// if this key maps to a different case variant, include that as well
|
||||
final differentCase = caseInsensitive[id];
|
||||
if (differentCase != null && differentCase != id) {
|
||||
|
|
@ -108,28 +85,6 @@ class ConstructMergeTable {
|
|||
}
|
||||
}
|
||||
|
||||
// if this is an broad ('other') key, find the specific key it maps to
|
||||
// and include it if available
|
||||
if (id.category == 'other') {
|
||||
final specificKey = otherToSpecific[id];
|
||||
if (specificKey != null) {
|
||||
keys.add(specificKey);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
// if this is a specific key, and there existing an 'other' construct
|
||||
// in the same group, and that 'other' construct maps to this specific key,
|
||||
// include the 'other' construct as well
|
||||
final otherEntry = lemmaTypeGroups[id.compositeKey]
|
||||
?.firstWhereOrNull((k) => k.category == 'other');
|
||||
if (otherEntry == null) {
|
||||
return keys;
|
||||
}
|
||||
|
||||
if (otherToSpecific[otherEntry] == id) {
|
||||
keys.add(otherEntry);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
|
|
@ -152,7 +107,6 @@ class ConstructMergeTable {
|
|||
|
||||
void clear() {
|
||||
lemmaTypeGroups.clear();
|
||||
otherToSpecific.clear();
|
||||
caseInsensitive.clear();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
class AnalyticsPracticeConstants {
|
||||
static const int timeForBonus = 60;
|
||||
static const int practiceGroupSize = 10;
|
||||
static const int errorBufferSize = 5;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -31,7 +31,10 @@ class GrammarErrorPracticeGenerator {
|
|||
.take(igcMatch.length)
|
||||
.toString();
|
||||
|
||||
choices.add(errorSpan);
|
||||
if (!req.grammarErrorInfo!.translation.contains(errorSpan)) {
|
||||
choices.add(errorSpan);
|
||||
}
|
||||
|
||||
choices.shuffle();
|
||||
return MessageActivityResponse(
|
||||
activity: GrammarErrorPracticeActivityModel(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue