fix: add missing dependency (#5707)

This commit is contained in:
ggurdin 2026-02-16 11:56:39 -05:00 committed by GitHub
parent 20b1619126
commit c2472bd2a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 176 additions and 73 deletions

View file

@ -153,9 +153,7 @@ extension BotClientExtension on Client {
onError: (e, s) => ErrorHandler.logError(
e: e,
s: s,
data: {
'userSettings': userSettings.toJson(),
},
data: {'userSettings': userSettings.toJson()},
),
);
}

View file

@ -1,20 +1,27 @@
import 'package:get_storage/get_storage.dart';
import 'package:fluffychat/pangea/events/models/pangea_token_model.dart';
import 'package:fluffychat/pangea/morphs/morph_features_enum.dart';
import 'package:fluffychat/pangea/practice_activities/activity_type_enum.dart';
import 'package:fluffychat/pangea/practice_activities/practice_selection.dart';
import 'package:fluffychat/pangea/practice_activities/practice_target.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:get_storage/get_storage.dart';
class _PracticeSelectionCacheEntry {
final PracticeSelection selection;
final DateTime timestamp;
_PracticeSelectionCacheEntry({required this.selection, required this.timestamp});
_PracticeSelectionCacheEntry({
required this.selection,
required this.timestamp,
});
bool get isExpired => DateTime.now().difference(timestamp).inDays > 1;
Map<String, dynamic> toJson() => {'selection': selection.toJson(), 'timestamp': timestamp.toIso8601String()};
Map<String, dynamic> toJson() => {
'selection': selection.toJson(),
'timestamp': timestamp.toIso8601String(),
};
factory _PracticeSelectionCacheEntry.fromJson(Map<String, dynamic> json) {
return _PracticeSelectionCacheEntry(
@ -27,7 +34,11 @@ class _PracticeSelectionCacheEntry {
class PracticeSelectionRepo {
static final GetStorage _storage = GetStorage('practice_selection_cache');
static Future<PracticeSelection?> get(String eventId, String messageLanguage, List<PangeaToken> tokens) async {
static Future<PracticeSelection?> get(
String eventId,
String messageLanguage,
List<PangeaToken> tokens,
) async {
final userL2 = MatrixState.pangeaController.userController.userL2;
if (userL2?.langCodeShort != messageLanguage.split("-").first) {
return null;
@ -42,8 +53,12 @@ class PracticeSelectionRepo {
return newEntry;
}
static Future<PracticeSelection> _fetch({required List<PangeaToken> tokens, required String langCode}) async {
if (langCode.split("-")[0] != MatrixState.pangeaController.userController.userL2?.langCodeShort) {
static Future<PracticeSelection> _fetch({
required List<PangeaToken> tokens,
required String langCode,
}) async {
if (langCode.split("-")[0] !=
MatrixState.pangeaController.userController.userL2?.langCodeShort) {
return PracticeSelection({});
}
@ -51,7 +66,10 @@ class PracticeSelectionRepo {
if (eligibleTokens.isEmpty) {
return PracticeSelection({});
}
final queue = await _fillActivityQueue(eligibleTokens, langCode.split('-')[0]);
final queue = await _fillActivityQueue(
eligibleTokens,
langCode.split('-')[0],
);
final selection = PracticeSelection(queue);
return selection;
}
@ -60,7 +78,9 @@ class PracticeSelectionRepo {
try {
final keys = List.from(_storage.getKeys());
for (final String key in keys) {
final cacheEntry = _PracticeSelectionCacheEntry.fromJson(_storage.read(key));
final cacheEntry = _PracticeSelectionCacheEntry.fromJson(
_storage.read(key),
);
if (cacheEntry.isExpired) {
_storage.remove(key);
}
@ -74,7 +94,9 @@ class PracticeSelectionRepo {
if (entry == null) return null;
try {
return _PracticeSelectionCacheEntry.fromJson(_storage.read(eventId)).selection;
return _PracticeSelectionCacheEntry.fromJson(
_storage.read(eventId),
).selection;
} catch (e) {
_storage.remove(eventId);
return null;
@ -82,7 +104,10 @@ class PracticeSelectionRepo {
}
static void _setCached(String eventId, PracticeSelection entry) {
final cachedEntry = _PracticeSelectionCacheEntry(selection: entry, timestamp: DateTime.now());
final cachedEntry = _PracticeSelectionCacheEntry(
selection: entry,
timestamp: DateTime.now(),
);
_storage.write(eventId, cachedEntry.toJson());
}
@ -97,9 +122,19 @@ class PracticeSelectionRepo {
return queue;
}
static int _sortTokens(PangeaToken a, PangeaToken b, int aScore, int bScore) => bScore.compareTo(aScore);
static int _sortTokens(
PangeaToken a,
PangeaToken b,
int aScore,
int bScore,
) => bScore.compareTo(aScore);
static int _sortMorphTargets(PracticeTarget a, PracticeTarget b, int aScore, int bScore) => bScore.compareTo(aScore);
static int _sortMorphTargets(
PracticeTarget a,
PracticeTarget b,
int aScore,
int bScore,
) => bScore.compareTo(aScore);
static List<PracticeTarget> _tokenToMorphTargets(PangeaToken t) {
return t.morphsBasicallyEligibleForPracticeByPriority
@ -136,7 +171,11 @@ class PracticeSelectionRepo {
return [];
}
final scores = await _fetchPriorityScores(practiceTokens, activityType, language);
final scores = await _fetchPriorityScores(
practiceTokens,
activityType,
language,
);
practiceTokens.sort((a, b) => _sortTokens(a, b, scores[a]!, scores[b]!));
practiceTokens = practiceTokens.take(8).toList();
@ -150,11 +189,25 @@ class PracticeSelectionRepo {
];
}
static Future<List<PracticeTarget>> _buildMorphActivity(List<PangeaToken> tokens, String language) async {
static Future<List<PracticeTarget>> _buildMorphActivity(
List<PangeaToken> tokens,
String language,
) async {
final List<PangeaToken> practiceTokens = List<PangeaToken>.from(tokens);
final candidates = practiceTokens.expand(_tokenToMorphTargets).toList();
final scores = await _fetchPriorityScores(practiceTokens, ActivityTypeEnum.morphId, language);
candidates.sort((a, b) => _sortMorphTargets(a, b, scores[a.tokens.first]!, scores[b.tokens.first]!));
final scores = await _fetchPriorityScores(
practiceTokens,
ActivityTypeEnum.morphId,
language,
);
candidates.sort(
(a, b) => _sortMorphTargets(
a,
b,
scores[a.tokens.first]!,
scores[b.tokens.first]!,
),
);
final seenTexts = <String>{};
final seenLemmas = <String>{};
@ -179,18 +232,24 @@ class PracticeSelectionRepo {
final ids = tokens.map((t) => t.vocabConstructID).toList();
final idMap = {for (final token in tokens) token: token.vocabConstructID};
final constructs = await MatrixState.pangeaController.matrixState.analyticsDataService.getConstructUses(
ids,
language,
);
final constructs = await MatrixState
.pangeaController
.matrixState
.analyticsDataService
.getConstructUses(ids, language);
for (final token in tokens) {
final construct = constructs[idMap[token]];
final lastUsed = construct?.lastUseByTypes(activityType.associatedUseTypes);
final lastUsed = construct?.lastUseByTypes(
activityType.associatedUseTypes,
);
final daysSinceLastUsed = lastUsed == null ? 20 : DateTime.now().difference(lastUsed).inDays;
final daysSinceLastUsed = lastUsed == null
? 20
: DateTime.now().difference(lastUsed).inDays;
scores[token] = daysSinceLastUsed * (token.vocabConstructID.isContentWord ? 10 : 7);
scores[token] =
daysSinceLastUsed * (token.vocabConstructID.isContentWord ? 10 : 7);
}
return scores;
}

View file

@ -1486,7 +1486,7 @@ packages:
source: git
version: "4.1.0"
meta:
dependency: transitive
dependency: "direct main"
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"

View file

@ -126,6 +126,7 @@ dependencies:
rive: 0.11.11
flutter_tts: ^4.2.0
animated_flip_counter: ^0.3.4
meta: ^1.17.0
# Pangea#
dev_dependencies:

View file

@ -1,9 +1,10 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:fluffychat/pangea/chat_settings/models/bot_options_model.dart';
import 'package:fluffychat/pangea/chat_settings/utils/bot_client_extension.dart';
import 'package:fluffychat/pangea/learning_settings/gender_enum.dart';
import 'package:fluffychat/pangea/learning_settings/language_level_type_enum.dart';
import 'package:fluffychat/pangea/user/user_model.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
// ---------------------------------------------------------------------------
@ -27,7 +28,11 @@ void main() {
userGenders: const {userId: GenderEnum.woman},
);
final result = buildUpdatedBotOptions(currentOptions: currentOptions, userSettings: baseSettings, userId: userId);
final result = buildUpdatedBotOptions(
currentOptions: currentOptions,
userSettings: baseSettings,
userId: userId,
);
expect(result, isNull);
});
@ -40,7 +45,11 @@ void main() {
userGenders: const {userId: GenderEnum.woman},
);
final result = buildUpdatedBotOptions(currentOptions: currentOptions, userSettings: baseSettings, userId: userId);
final result = buildUpdatedBotOptions(
currentOptions: currentOptions,
userSettings: baseSettings,
userId: userId,
);
expect(result, isNotNull);
expect(result!.targetLanguage, 'es');
@ -57,7 +66,11 @@ void main() {
userGenders: const {userId: GenderEnum.woman},
);
final result = buildUpdatedBotOptions(currentOptions: currentOptions, userSettings: baseSettings, userId: userId);
final result = buildUpdatedBotOptions(
currentOptions: currentOptions,
userSettings: baseSettings,
userId: userId,
);
expect(result, isNotNull);
expect(result!.languageLevel, LanguageLevelTypeEnum.b1);
@ -71,7 +84,11 @@ void main() {
userGenders: const {userId: GenderEnum.woman},
);
final result = buildUpdatedBotOptions(currentOptions: currentOptions, userSettings: baseSettings, userId: userId);
final result = buildUpdatedBotOptions(
currentOptions: currentOptions,
userSettings: baseSettings,
userId: userId,
);
expect(result, isNotNull);
expect(result!.targetVoice, 'voice_1');
@ -85,14 +102,22 @@ void main() {
userGenders: const {userId: GenderEnum.man},
);
final result = buildUpdatedBotOptions(currentOptions: currentOptions, userSettings: baseSettings, userId: userId);
final result = buildUpdatedBotOptions(
currentOptions: currentOptions,
userSettings: baseSettings,
userId: userId,
);
expect(result, isNotNull);
expect(result!.userGenders[userId], GenderEnum.woman);
});
test('defaults to empty BotOptionsModel when currentOptions is null', () {
final result = buildUpdatedBotOptions(currentOptions: null, userSettings: baseSettings, userId: userId);
final result = buildUpdatedBotOptions(
currentOptions: null,
userSettings: baseSettings,
userId: userId,
);
expect(result, isNotNull);
expect(result!.targetLanguage, 'es');
@ -106,10 +131,17 @@ void main() {
targetLanguage: 'fr', // different triggers update
languageLevel: LanguageLevelTypeEnum.b1,
targetVoice: 'voice_1',
userGenders: const {'@other:server': GenderEnum.man, userId: GenderEnum.woman},
userGenders: const {
'@other:server': GenderEnum.man,
userId: GenderEnum.woman,
},
);
final result = buildUpdatedBotOptions(currentOptions: currentOptions, userSettings: baseSettings, userId: userId);
final result = buildUpdatedBotOptions(
currentOptions: currentOptions,
userSettings: baseSettings,
userId: userId,
);
expect(result, isNotNull);
expect(result!.userGenders['@other:server'], GenderEnum.man);
@ -119,7 +151,11 @@ void main() {
test('handles null userId gracefully', () {
const currentOptions = BotOptionsModel(targetLanguage: 'fr');
final result = buildUpdatedBotOptions(currentOptions: currentOptions, userSettings: baseSettings, userId: null);
final result = buildUpdatedBotOptions(
currentOptions: currentOptions,
userSettings: baseSettings,
userId: null,
);
expect(result, isNotNull);
expect(result!.targetLanguage, 'es');
@ -193,52 +229,58 @@ void main() {
);
});
test('remaining updates do NOT execute when priority update fails', () async {
final callLog = <String>[];
test(
'remaining updates do NOT execute when priority update fails',
() async {
final callLog = <String>[];
try {
await applyBotOptionUpdatesInOrder(
priorityUpdate: () async {
throw Exception('DM failed');
},
remainingUpdates: [
() async {
callLog.add('room_a');
},
],
);
} catch (_) {}
expect(callLog, isEmpty);
},
);
test(
'isolates errors in remaining updates and continues to next room',
() async {
final callLog = <String>[];
final errors = <Object>[];
try {
await applyBotOptionUpdatesInOrder(
priorityUpdate: () async {
throw Exception('DM failed');
callLog.add('dm');
},
remainingUpdates: [
() async {
callLog.add('room_a');
},
() async {
throw Exception('room_b failed');
},
() async {
callLog.add('room_c');
},
],
onError: (e, _) => errors.add(e),
);
} catch (_) {}
expect(callLog, isEmpty);
});
test('isolates errors in remaining updates and continues to next room', () async {
final callLog = <String>[];
final errors = <Object>[];
await applyBotOptionUpdatesInOrder(
priorityUpdate: () async {
callLog.add('dm');
},
remainingUpdates: [
() async {
callLog.add('room_a');
},
() async {
throw Exception('room_b failed');
},
() async {
callLog.add('room_c');
},
],
onError: (e, _) => errors.add(e),
);
// room_b's error didn't prevent room_c from running
expect(callLog, ['dm', 'room_a', 'room_c']);
expect(errors, hasLength(1));
expect(errors.first, isA<Exception>());
});
// room_b's error didn't prevent room_c from running
expect(callLog, ['dm', 'room_a', 'room_c']);
expect(errors, hasLength(1));
expect(errors.first, isA<Exception>());
},
);
test('works correctly when priority update is null', () async {
final callLog = <String>[];
@ -273,7 +315,10 @@ void main() {
test('handles all null / empty gracefully', () async {
// Should complete without error
await applyBotOptionUpdatesInOrder(priorityUpdate: null, remainingUpdates: []);
await applyBotOptionUpdatesInOrder(
priorityUpdate: null,
remainingUpdates: [],
);
});
test('multiple remaining errors are all reported', () async {