From 89088779e98a79301c1788c9e83c3919f082cbc4 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 7 Nov 2024 12:09:27 -0500 Subject: [PATCH] make ConstructListModel updatable and added models for vocab and morphs to GetAnalyticsController --- .../controllers/get_analytics_controller.dart | 41 +++-- .../controllers/my_analytics_controller.dart | 57 ++++--- .../analytics/construct_list_model.dart | 156 +++++------------- .../models/analytics/constructs_model.dart | 6 + .../practice_activity_model.dart | 2 + .../learning_progress_indicators.dart | 25 +-- .../target_tokens_controller.dart | 7 +- 7 files changed, 126 insertions(+), 168 deletions(-) diff --git a/lib/pangea/controllers/get_analytics_controller.dart b/lib/pangea/controllers/get_analytics_controller.dart index 1f891ccd9..db2fee1d6 100644 --- a/lib/pangea/controllers/get_analytics_controller.dart +++ b/lib/pangea/controllers/get_analytics_controller.dart @@ -26,6 +26,27 @@ class GetAnalyticsController { CachedStreamController analyticsStream = CachedStreamController(); + ConstructListModel vocabModel = ConstructListModel( + type: ConstructTypeEnum.vocab, + uses: [], + ); + ConstructListModel grammarModel = ConstructListModel( + type: ConstructTypeEnum.morph, + uses: [], + ); + + List get allConstructUses { + final List storedUses = getConstructsLocal() ?? []; + final List localUses = locallyCachedConstructs; + + final List allConstructs = [ + ...storedUses, + ...localUses, + ]; + + return allConstructs; + } + /// The previous XP points of the user, before the last update. /// Used for animating analytics updates. int? prevXP; @@ -74,7 +95,11 @@ class GetAnalyticsController { .listen(onAnalyticsUpdate); _pangeaController.myAnalytics.lastUpdatedCompleter.future.then((_) { - getConstructs().then((_) => updateAnalyticsStream()); + getConstructs().then((_) { + vocabModel.updateConstructs(allConstructUses); + grammarModel.updateConstructs(allConstructUses); + updateAnalyticsStream(); + }); }); } @@ -88,6 +113,8 @@ class GetAnalyticsController { } Future onAnalyticsUpdate(AnalyticsUpdate analyticsUpdate) async { + vocabModel.updateConstructs(analyticsUpdate.newConstructs); + grammarModel.updateConstructs(analyticsUpdate.newConstructs); if (analyticsUpdate.isLogout) return; if (analyticsUpdate.type == AnalyticsUpdateType.server) { await getConstructs(forceUpdate: true); @@ -134,18 +161,6 @@ class GetAnalyticsController { return words.points + errors.points; } - List get allConstructUses { - final List storedUses = getConstructsLocal() ?? []; - final List localUses = locallyCachedConstructs; - - final List allConstructs = [ - ...storedUses, - ...localUses, - ]; - - return allConstructs; - } - /// A local cache of eventIds and construct uses for messages sent since the last update. /// It's a map of eventIDs to a list of OneConstructUses. Not just a list of OneConstructUses /// because, with practice activity constructs, we might need to add to the list for a given diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 9c7523991..676a5429c 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -110,25 +110,24 @@ class MyAnalyticsController extends BaseController { final String eventID = data.eventId; final String roomID = data.roomId; - _pangeaController.analytics - .filterConstructs(unfilteredConstructs: constructs) - .then((filtered) { - for (final use in filtered) { - debugPrint( - "_onNewAnalyticsData filtered use: ${use.constructType.string} ${use.useType.string} ${use.lemma} ${use.useType.pointValue}", - ); - } - if (filtered.isEmpty) return; - - final level = _pangeaController.analytics.level; - - _addLocalMessage(eventID, filtered).then( - (_) { - _clearDraftUses(roomID); - _decideWhetherToUpdateAnalyticsRoom(level, data.origin); - }, + for (final use in constructs) { + debugPrint( + "_onNewAnalyticsData filtered use: ${use.constructType.string} ${use.useType.string} ${use.lemma} ${use.useType.pointValue}", ); - }); + } + if (constructs.isEmpty) return; + + final level = _pangeaController.analytics.level; + _addLocalMessage(eventID, constructs).then( + (_) { + _clearDraftUses(roomID); + _decideWhetherToUpdateAnalyticsRoom( + level, + data.origin, + data.constructs, + ); + }, + ); } void addDraftUses( @@ -178,8 +177,12 @@ class MyAnalyticsController extends BaseController { } final level = _pangeaController.analytics.level; + + // the list 'uses' gets altered in the _addLocalMessage method, + // so copy it here to that the list of new uses is accurate + final List newUses = List.from(uses); _addLocalMessage('draft$roomID', uses).then( - (_) => _decideWhetherToUpdateAnalyticsRoom(level, origin), + (_) => _decideWhetherToUpdateAnalyticsRoom(level, origin, newUses), ); } @@ -222,6 +225,7 @@ class MyAnalyticsController extends BaseController { void _decideWhetherToUpdateAnalyticsRoom( int prevLevel, AnalyticsUpdateOrigin? origin, + List newConstructs, ) { // cancel the last timer that was set on message event and // reset it to fire after _minutesBeforeUpdate minutes @@ -242,7 +246,11 @@ class MyAnalyticsController extends BaseController { newLevel > prevLevel ? sendLocalAnalyticsToAnalyticsRoom() : analyticsUpdateStream.add( - AnalyticsUpdate(AnalyticsUpdateType.local, origin: origin), + AnalyticsUpdate( + AnalyticsUpdateType.local, + newConstructs, + origin: origin, + ), ); } @@ -309,6 +317,7 @@ class MyAnalyticsController extends BaseController { analyticsUpdateStream.add( AnalyticsUpdate( AnalyticsUpdateType.server, + [], isLogout: onLogout, ), ); @@ -371,7 +380,13 @@ enum AnalyticsUpdateOrigin { class AnalyticsUpdate { final AnalyticsUpdateType type; final AnalyticsUpdateOrigin? origin; + final List newConstructs; final bool isLogout; - AnalyticsUpdate(this.type, {this.isLogout = false, this.origin}); + AnalyticsUpdate( + this.type, + this.newConstructs, { + this.isLogout = false, + this.origin, + }); } diff --git a/lib/pangea/models/analytics/construct_list_model.dart b/lib/pangea/models/analytics/construct_list_model.dart index 23763824c..4a0d7c83a 100644 --- a/lib/pangea/models/analytics/construct_list_model.dart +++ b/lib/pangea/models/analytics/construct_list_model.dart @@ -1,138 +1,88 @@ import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; /// A wrapper around a list of [OneConstructUse]s, used to simplify /// the process of filtering / sorting / displaying the events. /// Takes a construct type and a list of events class ConstructListModel { final ConstructTypeEnum? type; - final List _uses; - List? _constructList; - List? _typedConstructs; /// A map of lemmas to ConstructUses, each of which contains a lemma /// key = lemmma + constructType.string, value = ConstructUses - Map? _constructMap; + final Map _constructMap = {}; + + /// Storing this to avoid re-running the sort operation each time this needs to + /// be accessed. It contains the same information as _constructMap, but sorted. + List constructList = []; ConstructListModel({ required this.type, required List uses, - }) : _uses = uses; - - List get uses => - _uses.where((use) => use.constructType == type || type == null).toList(); - - /// All unique lemmas used in the construct events - List get lemmas => constructList.map((e) => e.lemma).toSet().toList(); + }) { + updateConstructs(uses); + } - /// All unique lemmas used in the construct events with non-zero points - List get lemmasWithPoints => - constructListWithPoints.map((e) => e.lemma).toSet().toList(); + /// Given a list of new construct uses, update the map of construct + /// IDs to ConstructUses and re-sort the list of ConstructUses + void updateConstructs(List newUses) { + final List filteredUses = newUses + .where((use) => use.constructType == type || type == null) + .toList(); + _updateConstructMap(filteredUses); + _updateConstructList(); + } /// A map of lemmas to ConstructUses, each of which contains a lemma /// key = lemmma + constructType.string, value = ConstructUses - void _buildConstructMap() { - final Map> lemmaToUses = {}; - for (final use in uses) { + void _updateConstructMap(final List newUses) { + for (final use in newUses) { if (use.lemma == null) continue; - lemmaToUses[use.lemma! + use.constructType.string] ??= []; - lemmaToUses[use.lemma! + use.constructType.string]!.add(use); + final currentUses = _constructMap[use.identifier.string] ?? + ConstructUses( + uses: [], + constructType: use.constructType, + lemma: use.lemma!, + ); + currentUses.uses.add(use); + _constructMap[use.identifier.string] = currentUses; } - - _constructMap = lemmaToUses.map( - (key, value) => MapEntry( - key, - ConstructUses( - uses: value, - constructType: value.first.constructType, - lemma: value.first.lemma!, - ), - ), - ); - } - - ConstructUses? getConstructUses(String lemma, ConstructTypeEnum type) { - if (_constructMap == null) _buildConstructMap(); - return _constructMap![lemma + type.string]; } /// A list of ConstructUses, each of which contains a lemma and /// a list of uses, sorted by the number of uses - List get constructList { - // the list of uses doesn't change so we don't have to re-calculate this - if (_constructList != null) return _constructList!; - - if (_constructMap == null) _buildConstructMap(); - - _constructList = _constructMap!.values.toList(); - - _constructList!.sort((a, b) { + void _updateConstructList() { + // TODO check how expensive this is + constructList = _constructMap.values.toList(); + constructList.sort((a, b) { final comp = b.uses.length.compareTo(a.uses.length); if (comp != 0) return comp; return a.lemma.compareTo(b.lemma); }); + } - return _constructList!; + ConstructUses? getConstructUses(ConstructIdentifier identifier) { + return _constructMap[identifier.string]; } List get constructListWithPoints => constructList.where((constructUse) => constructUse.points > 0).toList(); - get maxXPPerLemma { - return type != null - ? type!.maxXPPerLemma - : ConstructTypeEnum.vocab.maxXPPerLemma; - } + /// All unique lemmas used in the construct events with non-zero points + List get lemmasWithPoints => + constructListWithPoints.map((e) => e.lemma).toSet().toList(); - /// A list of ConstructUseTypeUses, each of which - /// contains a lemma, a use type, and a list of uses - List get typedConstructs { - if (_typedConstructs != null) return _typedConstructs!; - final List typedConstructs = []; - for (final construct in constructList) { - final typeToUses = >{}; - for (final use in construct.uses) { - typeToUses[use.useType] ??= []; - typeToUses[use.useType]!.add(use); - } - for (final typeEntry in typeToUses.entries) { - typedConstructs.add( - ConstructUseTypeUses( - lemma: construct.lemma, - constructType: typeEntry.value.first.constructType, - useType: typeEntry.key, - uses: typeEntry.value, - ), - ); - } - } - return typedConstructs; - } + int get maxXPPerLemma => + type?.maxXPPerLemma ?? ConstructTypeEnum.vocab.maxXPPerLemma; /// The total number of points for all uses of this construct type int get points { - // double totalPoints = 0; - return typedConstructs.fold( - 0, - (total, typedConstruct) => - total + - typedConstruct.useType.pointValue * typedConstruct.uses.length, - ); - // Commenting this out for now - // Minimize the amount of points given for repeated uses of the same lemma. - // i.e., if a lemma is used 4 times without assistance, the point value for - // a use without assistance is 3. So the points would be - // 3/1 + 3/2 + 3/3 + 3/4 = 3 + 1.5 + 1 + 0.75 = 5.25 (instead of 12) - // for (final typedConstruct in typedConstructs) { - // final pointValue = typedConstruct.useType.pointValue; - // double calc = 0.0; - // for (int k = 1; k <= typedConstruct.uses.length; k++) { - // calc += pointValue / k; - // } - // totalPoints += calc; - // } - // return totalPoints.round(); + int totalPoints = 0; + for (final constructUse in _constructMap.values.toList()) { + totalPoints += constructUse.points; + } + return totalPoints; } } @@ -166,19 +116,3 @@ class ConstructUses { return _lastUsed = lastUse; } } - -/// One lemma, a use type, and a list of uses -/// for that lemma and use type -class ConstructUseTypeUses { - final ConstructUseTypeEnum useType; - final ConstructTypeEnum constructType; - final String lemma; - final List uses; - - ConstructUseTypeUses({ - required this.useType, - required this.constructType, - required this.lemma, - required this.uses, - }); -} diff --git a/lib/pangea/models/analytics/constructs_model.dart b/lib/pangea/models/analytics/constructs_model.dart index 191683140..11024c203 100644 --- a/lib/pangea/models/analytics/constructs_model.dart +++ b/lib/pangea/models/analytics/constructs_model.dart @@ -1,6 +1,7 @@ import 'dart:developer'; import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; @@ -157,6 +158,11 @@ class OneConstructUse { } int get pointValue => useType.pointValue; + + ConstructIdentifier get identifier => ConstructIdentifier( + lemma: lemma!, + type: constructType, + ); } class ConstructUseMetaData { diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart index 65f02c4f7..db8e4a023 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart @@ -51,6 +51,8 @@ class ConstructIdentifier { int get hashCode { return lemma.hashCode ^ type.hashCode; } + + String get string => "$lemma-${type.string}"; } class CandidateMessage { diff --git a/lib/pangea/widgets/chat_list/analytics_summary/learning_progress_indicators.dart b/lib/pangea/widgets/chat_list/analytics_summary/learning_progress_indicators.dart index 980ee1488..1ebcbaf80 100644 --- a/lib/pangea/widgets/chat_list/analytics_summary/learning_progress_indicators.dart +++ b/lib/pangea/widgets/chat_list/analytics_summary/learning_progress_indicators.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:fluffychat/pages/chat_list/client_chooser_button.dart'; import 'package:fluffychat/pangea/controllers/get_analytics_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart'; import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; @@ -38,13 +37,6 @@ class LearningProgressIndicatorsState /// A stream subscription to listen for updates to /// the analytics data, either locally or from events StreamSubscription? _analyticsUpdateSubscription; - - /// Vocabulary constructs model - ConstructListModel? words; - - /// Morph constructs model - ConstructListModel? morphs; - bool loading = true; // Some buggy stuff is happening with this data not being updated at login, so switching @@ -81,15 +73,6 @@ class LearningProgressIndicatorsState /// Update the analytics data shown in the UI. This comes from a /// combination of stored events and locally cached data. Future updateAnalyticsData(List constructs) async { - words = ConstructListModel( - type: ConstructTypeEnum.vocab, - uses: constructs, - ); - morphs = ConstructListModel( - type: ConstructTypeEnum.morph, - uses: constructs, - ); - currentConstructs = constructs; if (loading) loading = false; if (mounted) setState(() {}); @@ -99,9 +82,9 @@ class LearningProgressIndicatorsState ConstructListModel? getConstructsModel(ProgressIndicatorEnum indicator) { switch (indicator) { case ProgressIndicatorEnum.wordsUsed: - return words; + return _pangeaController.analytics.vocabModel; case ProgressIndicatorEnum.morphsUsed: - return morphs; + return _pangeaController.analytics.grammarModel; default: return null; } @@ -111,9 +94,9 @@ class LearningProgressIndicatorsState int? getProgressPoints(ProgressIndicatorEnum indicator) { switch (indicator) { case ProgressIndicatorEnum.wordsUsed: - return words?.lemmasWithPoints.length; + return _pangeaController.analytics.vocabModel.lemmasWithPoints.length; case ProgressIndicatorEnum.morphsUsed: - return morphs?.lemmasWithPoints.length; + return _pangeaController.analytics.grammarModel.lemmasWithPoints.length; case ProgressIndicatorEnum.level: return level; } diff --git a/lib/pangea/widgets/practice_activity/target_tokens_controller.dart b/lib/pangea/widgets/practice_activity/target_tokens_controller.dart index 69be7f6c2..c1544ef00 100644 --- a/lib/pangea/widgets/practice_activity/target_tokens_controller.dart +++ b/lib/pangea/widgets/practice_activity/target_tokens_controller.dart @@ -4,6 +4,7 @@ import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dar import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/message_activity_request.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; @@ -73,8 +74,10 @@ class TargetTokensController { for (final construct in token.constructs) { final constructUseModel = constructList.getConstructUses( - construct.id.lemma, - construct.id.type, + ConstructIdentifier( + lemma: construct.id.lemma, + type: construct.id.type, + ), ); if (constructUseModel != null) { construct.xp += constructUseModel.points;