store new construct uses locally. use a combination of those and stored analytics events to update mini analytics UI.
This commit is contained in:
parent
a1a1d92f54
commit
4ede7c9bdd
10 changed files with 507 additions and 164 deletions
|
|
@ -636,7 +636,15 @@ class ChatController extends State<ChatPageWithRoom>
|
|||
// analytics based on when / how many messages the logged in user send. This
|
||||
// stream sends the data for newly sent messages.
|
||||
if (msgEventId != null) {
|
||||
pangeaController.myAnalytics.setState(data: {'eventID': msgEventId});
|
||||
pangeaController.myAnalytics.setState(
|
||||
data: {
|
||||
'eventID': msgEventId,
|
||||
'roomID': room.id,
|
||||
'originalSent': originalSent,
|
||||
'tokensSent': tokensSent,
|
||||
'choreo': choreo,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (previousEdit != null) {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,9 @@ class StartIGCButtonState extends State<StartIGCButton>
|
|||
_controller?.stop();
|
||||
_controller?.reverse();
|
||||
}
|
||||
setState(() => prevState = assistanceState);
|
||||
if (mounted) {
|
||||
setState(() => prevState = assistanceState);
|
||||
}
|
||||
}
|
||||
|
||||
bool get itEnabled => widget.controller.choreographer.itEnabled;
|
||||
|
|
|
|||
212
lib/pangea/controllers/get_analytics_controller.dart
Normal file
212
lib/pangea/controllers/get_analytics_controller.dart
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
||||
import 'package:fluffychat/pangea/constants/local.key.dart';
|
||||
import 'package:fluffychat/pangea/constants/match_rule_ids.dart';
|
||||
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
|
||||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart';
|
||||
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
/// A minimized version of AnalyticsController that get the logged in user's analytics
|
||||
class GetAnalyticsController {
|
||||
late PangeaController _pangeaController;
|
||||
final List<AnalyticsCacheEntry> _cache = [];
|
||||
|
||||
GetAnalyticsController(PangeaController pangeaController) {
|
||||
_pangeaController = pangeaController;
|
||||
}
|
||||
|
||||
String? get l2Code => _pangeaController.languageController.userL2?.langCode;
|
||||
|
||||
// A local cache of eventIds and construct uses for messages sent since the last update
|
||||
Map<String, List<OneConstructUse>> get messagesSinceUpdate {
|
||||
try {
|
||||
final dynamic locallySaved = _pangeaController.pStoreService.read(
|
||||
PLocalKey.messagesSinceUpdate,
|
||||
);
|
||||
if (locallySaved == null) {
|
||||
_pangeaController.myAnalytics.setMessagesSinceUpdate({});
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
// try to get the local cache of messages and format them as OneConstructUses
|
||||
final Map<String, List<dynamic>> cache =
|
||||
Map<String, List<dynamic>>.from(locallySaved);
|
||||
final Map<String, List<OneConstructUse>> formattedCache = {};
|
||||
for (final entry in cache.entries) {
|
||||
formattedCache[entry.key] =
|
||||
entry.value.map((e) => OneConstructUse.fromJson(e)).toList();
|
||||
}
|
||||
return formattedCache;
|
||||
} catch (err) {
|
||||
// if something goes wrong while trying to format the local data, clear it
|
||||
_pangeaController.myAnalytics.setMessagesSinceUpdate({});
|
||||
return {};
|
||||
}
|
||||
} catch (exception, stackTrace) {
|
||||
ErrorHandler.logError(
|
||||
e: PangeaWarningError(
|
||||
"Failed to get messages since update: $exception",
|
||||
),
|
||||
s: stackTrace,
|
||||
m: 'Failed to retrieve messages since update',
|
||||
);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a list of all the construct analytics events
|
||||
/// for the logged in user in their current L2
|
||||
Future<List<ConstructAnalyticsEvent>?> getConstructs({
|
||||
bool forceUpdate = false,
|
||||
ConstructTypeEnum? constructType,
|
||||
}) async {
|
||||
debugPrint("getting constructs");
|
||||
await _pangeaController.matrixState.client.roomsLoading;
|
||||
|
||||
final DateTime? lastUpdated = await myAnalyticsLastUpdated();
|
||||
final List<ConstructAnalyticsEvent>? local = getConstructsLocal(
|
||||
constructType: constructType,
|
||||
lastUpdated: lastUpdated,
|
||||
);
|
||||
if (local != null && !forceUpdate) {
|
||||
debugPrint("returning local constructs");
|
||||
return local;
|
||||
}
|
||||
debugPrint("fetching new constructs");
|
||||
|
||||
final unfilteredConstructs = await allMyConstructs();
|
||||
final filteredConstructs = await filterConstructs(
|
||||
unfilteredConstructs: unfilteredConstructs,
|
||||
);
|
||||
|
||||
if (local == null) {
|
||||
cacheConstructs(
|
||||
constructType: constructType,
|
||||
events: filteredConstructs,
|
||||
);
|
||||
}
|
||||
|
||||
return filteredConstructs;
|
||||
}
|
||||
|
||||
/// Get the last time the user updated their analytics for their current l2
|
||||
Future<DateTime?> myAnalyticsLastUpdated() async {
|
||||
if (l2Code == null) return null;
|
||||
final Room? analyticsRoom =
|
||||
_pangeaController.matrixState.client.analyticsRoomLocal(l2Code!);
|
||||
if (analyticsRoom == null) return null;
|
||||
final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated(
|
||||
_pangeaController.matrixState.client.userID!,
|
||||
);
|
||||
return lastUpdated;
|
||||
}
|
||||
|
||||
/// Get the cached construct analytics events for the current user, if it exists
|
||||
List<ConstructAnalyticsEvent>? getConstructsLocal({
|
||||
DateTime? lastUpdated,
|
||||
ConstructTypeEnum? constructType,
|
||||
}) {
|
||||
final index = _cache.indexWhere(
|
||||
(e) => e.type == constructType && e.langCode == l2Code,
|
||||
);
|
||||
|
||||
if (index > -1) {
|
||||
if (_cache[index].needsUpdate(lastUpdated)) {
|
||||
_cache.removeAt(index);
|
||||
return null;
|
||||
}
|
||||
return _cache[index].events;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get all the construct analytics events for the logged in user
|
||||
Future<List<ConstructAnalyticsEvent>> allMyConstructs() async {
|
||||
if (l2Code == null) return [];
|
||||
final Room? analyticsRoom =
|
||||
_pangeaController.matrixState.client.analyticsRoomLocal(l2Code!);
|
||||
if (analyticsRoom == null) return [];
|
||||
|
||||
return await analyticsRoom.getAnalyticsEvents(
|
||||
userId: _pangeaController.matrixState.client.userID!,
|
||||
) ??
|
||||
[];
|
||||
}
|
||||
|
||||
/// Filter out constructs that are not relevant to the user, specifically those from
|
||||
/// rooms in which the user is a teacher and those that are interative translation span constructs
|
||||
Future<List<ConstructAnalyticsEvent>> filterConstructs({
|
||||
required List<ConstructAnalyticsEvent> unfilteredConstructs,
|
||||
}) async {
|
||||
final List<String> adminSpaceRooms =
|
||||
await _pangeaController.matrixState.client.teacherRoomIds;
|
||||
for (final construct in unfilteredConstructs) {
|
||||
construct.content.uses.removeWhere(
|
||||
(use) {
|
||||
if (adminSpaceRooms.contains(use.chatId)) {
|
||||
return true;
|
||||
}
|
||||
return use.lemma == "Try interactive translation" ||
|
||||
use.lemma == "itStart" ||
|
||||
use.lemma == MatchRuleIds.interactiveTranslation;
|
||||
},
|
||||
);
|
||||
}
|
||||
unfilteredConstructs.removeWhere((e) => e.content.uses.isEmpty);
|
||||
return unfilteredConstructs;
|
||||
}
|
||||
|
||||
/// Cache the construct analytics events for the current user
|
||||
void cacheConstructs({
|
||||
required List<ConstructAnalyticsEvent> events,
|
||||
ConstructTypeEnum? constructType,
|
||||
}) {
|
||||
if (l2Code == null) return;
|
||||
final entry = AnalyticsCacheEntry(
|
||||
type: constructType,
|
||||
events: List.from(events),
|
||||
langCode: l2Code!,
|
||||
);
|
||||
_cache.add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
class AnalyticsCacheEntry {
|
||||
final String langCode;
|
||||
final ConstructTypeEnum? type;
|
||||
final List<ConstructAnalyticsEvent> events;
|
||||
late final DateTime _createdAt;
|
||||
|
||||
AnalyticsCacheEntry({
|
||||
required this.langCode,
|
||||
required this.type,
|
||||
required this.events,
|
||||
}) {
|
||||
_createdAt = DateTime.now();
|
||||
}
|
||||
|
||||
bool get isExpired =>
|
||||
DateTime.now().difference(_createdAt).inMinutes >
|
||||
ClassDefaultValues.minutesDelayToMakeNewChartAnalytics;
|
||||
|
||||
bool needsUpdate(DateTime? lastEventUpdated) {
|
||||
// cache entry is invalid if it's older than the last event update
|
||||
// if lastEventUpdated is null, that would indicate that no events
|
||||
// of this type have been sent to the room. In this case, there
|
||||
// shouldn't be any cached data.
|
||||
if (lastEventUpdated == null) {
|
||||
Sentry.addBreadcrumb(
|
||||
Breadcrumb(message: "lastEventUpdated is null in needsUpdate"),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return _createdAt.isBefore(lastEventUpdated);
|
||||
}
|
||||
}
|
||||
|
|
@ -10,16 +10,19 @@ import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_e
|
|||
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
|
||||
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/models/choreo_record.dart';
|
||||
import 'package:fluffychat/pangea/models/representation_content_model.dart';
|
||||
import 'package:fluffychat/pangea/models/tokens_event_content_model.dart';
|
||||
import 'package:fluffychat/pangea/utils/error_handler.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
|
||||
/// handles the processing of analytics for
|
||||
/// 1) messages sent by the user and
|
||||
/// 2) constructs used by the user, both in sending messages and doing practice activities
|
||||
class MyAnalyticsController extends BaseController {
|
||||
late PangeaController _pangeaController;
|
||||
final StreamController analyticsUpdateStream = StreamController.broadcast();
|
||||
Timer? _updateTimer;
|
||||
|
||||
/// the max number of messages that will be cached before
|
||||
|
|
@ -38,10 +41,8 @@ class MyAnalyticsController extends BaseController {
|
|||
|
||||
// Listen to a stream that provides the eventIDs
|
||||
// of new messages sent by the logged in user
|
||||
stateStream
|
||||
.where((data) => data is Map && data.containsKey("eventID"))
|
||||
.listen((data) {
|
||||
updateAnalyticsTimer(data['eventID']);
|
||||
stateStream.where((data) => data is Map).listen((data) {
|
||||
onMessageSent(data as Map<String, dynamic>);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -67,11 +68,9 @@ class MyAnalyticsController extends BaseController {
|
|||
|
||||
Client get _client => _pangeaController.matrixState.client;
|
||||
|
||||
/// Given an newly sent message, reset the timer
|
||||
/// and add the event ID to the cache of un-added event IDs
|
||||
void updateAnalyticsTimer(String newEventId) {
|
||||
addMessageSinceUpdate(newEventId);
|
||||
|
||||
/// Given the data from a newly sent message, format and cache
|
||||
/// the message's construct data locally and reset the update timer
|
||||
void onMessageSent(Map<String, dynamic> data) {
|
||||
// cancel the last timer that was set on message event and
|
||||
// reset it to fire after _minutesBeforeUpdate minutes
|
||||
_updateTimer?.cancel();
|
||||
|
|
@ -79,99 +78,90 @@ class MyAnalyticsController extends BaseController {
|
|||
debugPrint("timer fired, updating analytics");
|
||||
updateAnalytics();
|
||||
});
|
||||
}
|
||||
|
||||
// adds an event ID to the cache of un-added event IDs
|
||||
// if the event IDs isn't already added
|
||||
void addMessageSinceUpdate(String eventId) {
|
||||
try {
|
||||
final List<String> currentCache = messagesSinceUpdate;
|
||||
if (!currentCache.contains(eventId)) {
|
||||
currentCache.add(eventId);
|
||||
_pangeaController.pStoreService.save(
|
||||
PLocalKey.messagesSinceUpdate,
|
||||
currentCache,
|
||||
);
|
||||
}
|
||||
// extract the relevant data about this message
|
||||
final String? eventID = data['eventID'];
|
||||
final String? roomID = data['roomID'];
|
||||
final PangeaRepresentation? originalSent = data['originalSent'];
|
||||
final PangeaMessageTokens? tokensSent = data['tokensSent'];
|
||||
final ChoreoRecord? choreo = data['choreo'];
|
||||
|
||||
// if the cached has reached if max-length, update analytics
|
||||
if (messagesSinceUpdate.length > _maxMessagesCached) {
|
||||
debugPrint("reached max messages, updating");
|
||||
updateAnalytics();
|
||||
}
|
||||
} catch (exception, stackTrace) {
|
||||
ErrorHandler.logError(
|
||||
e: PangeaWarningError("Failed to add message since update: $exception"),
|
||||
s: stackTrace,
|
||||
m: 'Failed to add message since update for eventId: $eventId',
|
||||
);
|
||||
Sentry.captureException(
|
||||
exception,
|
||||
stackTrace: stackTrace,
|
||||
withScope: (scope) {
|
||||
scope.setExtra(
|
||||
'extra_info',
|
||||
'Failed during addMessageSinceUpdate with eventId: $eventId',
|
||||
);
|
||||
scope.setTag('where', 'addMessageSinceUpdate');
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
if (roomID == null || eventID == null) return;
|
||||
|
||||
// called before updating analytics
|
||||
void clearMessagesSinceUpdate() {
|
||||
_pangeaController.pStoreService.save(
|
||||
PLocalKey.messagesSinceUpdate,
|
||||
[],
|
||||
// convert that data into construct uses and add it to the cache
|
||||
final metadata = ConstructUseMetaData(
|
||||
roomId: roomID,
|
||||
eventId: eventID,
|
||||
timeStamp: DateTime.now(),
|
||||
);
|
||||
|
||||
final grammarConstructs = choreo?.grammarConstructUses(metadata: metadata);
|
||||
final itConstructs = choreo?.itStepsToConstructUses(metadata: metadata);
|
||||
final vocabUses = tokensSent != null
|
||||
? originalSent?.vocabUses(
|
||||
choreo: choreo,
|
||||
tokens: tokensSent.tokens,
|
||||
metadata: metadata,
|
||||
)
|
||||
: null;
|
||||
final List<OneConstructUse> constructs = [
|
||||
...(grammarConstructs ?? []),
|
||||
...(itConstructs ?? []),
|
||||
...(vocabUses ?? []),
|
||||
];
|
||||
addMessageSinceUpdate(
|
||||
eventID,
|
||||
constructs,
|
||||
);
|
||||
}
|
||||
|
||||
// a local cache of eventIds for messages sent since the last update
|
||||
// it's possible for this cache to be invalid or deleted
|
||||
// It's a proxy measure for messages sent since last update
|
||||
List<String> get messagesSinceUpdate {
|
||||
/// Add a list of construct uses for a new message to the local
|
||||
/// cache of recently sent messages
|
||||
void addMessageSinceUpdate(
|
||||
String eventID,
|
||||
List<OneConstructUse> constructs,
|
||||
) {
|
||||
try {
|
||||
Logs().d('Reading messages since update from local storage');
|
||||
final dynamic locallySaved = _pangeaController.pStoreService.read(
|
||||
PLocalKey.messagesSinceUpdate,
|
||||
);
|
||||
if (locallySaved == null) {
|
||||
Logs().d('No locally saved messages found, initializing empty list.');
|
||||
_pangeaController.pStoreService.save(
|
||||
PLocalKey.messagesSinceUpdate,
|
||||
[],
|
||||
);
|
||||
return [];
|
||||
final currentCache = _pangeaController.analytics.messagesSinceUpdate;
|
||||
if (!currentCache.containsKey(eventID)) {
|
||||
currentCache[eventID] = constructs;
|
||||
setMessagesSinceUpdate(currentCache);
|
||||
}
|
||||
return locallySaved.cast<String>();
|
||||
} catch (exception, stackTrace) {
|
||||
|
||||
// if the cached has reached if max-length, update analytics
|
||||
if (_pangeaController.analytics.messagesSinceUpdate.length >
|
||||
_maxMessagesCached) {
|
||||
debugPrint("reached max messages, updating");
|
||||
updateAnalytics();
|
||||
}
|
||||
} catch (e, s) {
|
||||
ErrorHandler.logError(
|
||||
e: PangeaWarningError(
|
||||
"Failed to get messages since update: $exception",
|
||||
),
|
||||
s: stackTrace,
|
||||
m: 'Failed to retrieve messages since update',
|
||||
e: PangeaWarningError("Failed to add message since update: $e"),
|
||||
s: s,
|
||||
m: 'Failed to add message since update for eventId: $eventID',
|
||||
);
|
||||
Sentry.captureException(
|
||||
exception,
|
||||
stackTrace: stackTrace,
|
||||
withScope: (scope) {
|
||||
scope.setExtra(
|
||||
'extra_info',
|
||||
'Error during messagesSinceUpdate getter',
|
||||
);
|
||||
scope.setTag('where', 'messagesSinceUpdate');
|
||||
},
|
||||
);
|
||||
_pangeaController.pStoreService.save(
|
||||
PLocalKey.messagesSinceUpdate,
|
||||
[],
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Clears the local cache of recently sent constructs. Called before updating analytics
|
||||
void clearMessagesSinceUpdate() {
|
||||
setMessagesSinceUpdate({});
|
||||
}
|
||||
|
||||
/// Save the local cache of recently sent constructs to the local storage
|
||||
void setMessagesSinceUpdate(Map<String, List<OneConstructUse>> cache) {
|
||||
final formattedCache = {};
|
||||
for (final entry in cache.entries) {
|
||||
final constructJsons = entry.value.map((e) => e.toJson()).toList();
|
||||
formattedCache[entry.key] = constructJsons;
|
||||
}
|
||||
_pangeaController.pStoreService.save(
|
||||
PLocalKey.messagesSinceUpdate,
|
||||
formattedCache,
|
||||
);
|
||||
analyticsUpdateStream.add(null);
|
||||
}
|
||||
|
||||
Completer<void>? _updateCompleter;
|
||||
Future<void> updateAnalytics() async {
|
||||
if (!(_updateCompleter?.isCompleted ?? true)) {
|
||||
|
|
@ -182,6 +172,7 @@ class MyAnalyticsController extends BaseController {
|
|||
try {
|
||||
await _updateAnalytics();
|
||||
clearMessagesSinceUpdate();
|
||||
analyticsUpdateStream.add(null);
|
||||
} catch (err, s) {
|
||||
ErrorHandler.logError(
|
||||
e: err,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:fluffychat/pangea/constants/class_default_values.dart';
|
|||
import 'package:fluffychat/pangea/constants/pangea_event_types.dart';
|
||||
import 'package:fluffychat/pangea/controllers/class_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/contextual_definition_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/get_analytics_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/language_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/language_detection_controller.dart';
|
||||
import 'package:fluffychat/pangea/controllers/language_list_controller.dart';
|
||||
|
|
@ -35,7 +36,6 @@ import '../../config/app_config.dart';
|
|||
import '../utils/firebase_analytics.dart';
|
||||
import '../utils/p_store.dart';
|
||||
import 'it_feedback_controller.dart';
|
||||
import 'message_analytics_controller.dart';
|
||||
|
||||
class PangeaController {
|
||||
///pangeaControllers
|
||||
|
|
@ -43,7 +43,8 @@ class PangeaController {
|
|||
late LanguageController languageController;
|
||||
late ClassController classController;
|
||||
late PermissionsController permissionsController;
|
||||
late AnalyticsController analytics;
|
||||
// late AnalyticsController analytics;
|
||||
late GetAnalyticsController analytics;
|
||||
late MyAnalyticsController myAnalytics;
|
||||
late WordController wordNet;
|
||||
late MessageDataController messageData;
|
||||
|
|
@ -91,7 +92,8 @@ class PangeaController {
|
|||
languageController = LanguageController(this);
|
||||
classController = ClassController(this);
|
||||
permissionsController = PermissionsController(this);
|
||||
analytics = AnalyticsController(this);
|
||||
// analytics = AnalyticsController(this);
|
||||
analytics = GetAnalyticsController(this);
|
||||
myAnalytics = MyAnalyticsController(this);
|
||||
messageData = MessageDataController(this);
|
||||
wordNet = WordController(this);
|
||||
|
|
|
|||
50
lib/pangea/models/analytics/construct_list_model.dart
Normal file
50
lib/pangea/models/analytics/construct_list_model.dart
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_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 {
|
||||
ConstructTypeEnum type;
|
||||
List<OneConstructUse> uses;
|
||||
|
||||
ConstructListModel({
|
||||
required this.type,
|
||||
required this.uses,
|
||||
});
|
||||
|
||||
/// All unique lemmas used in the construct events
|
||||
List<String> get lemmas => constructs.map((e) => e.lemma).toSet().toList();
|
||||
|
||||
/// A list of ConstructUses, each of which contains a lemma and
|
||||
/// a list of uses, sorted by the number of uses
|
||||
List<ConstructUses> get constructs {
|
||||
final List<OneConstructUse> filtered =
|
||||
uses.where((use) => use.constructType == type).toList();
|
||||
|
||||
final Map<String, List<OneConstructUse>> lemmaToUses = {};
|
||||
for (final use in filtered) {
|
||||
if (use.lemma == null) continue;
|
||||
lemmaToUses[use.lemma!] ??= [];
|
||||
lemmaToUses[use.lemma!]!.add(use);
|
||||
}
|
||||
|
||||
final constructUses = lemmaToUses.entries
|
||||
.map(
|
||||
(entry) => ConstructUses(
|
||||
lemma: entry.key,
|
||||
uses: entry.value,
|
||||
constructType: type,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
constructUses.sort((a, b) {
|
||||
final comp = b.uses.length.compareTo(a.uses.length);
|
||||
if (comp != 0) return comp;
|
||||
return a.lemma.compareTo(b.lemma);
|
||||
});
|
||||
|
||||
return constructUses;
|
||||
}
|
||||
}
|
||||
|
|
@ -118,7 +118,10 @@ class ChoreoRecord {
|
|||
String get finalMessage =>
|
||||
choreoSteps.isNotEmpty ? choreoSteps.last.text : "";
|
||||
|
||||
/// get construct uses of type grammar for the message
|
||||
/// Get construct uses of type grammar for the message from this ChoreoRecord.
|
||||
/// Takes either an event (typically when the Representation itself is
|
||||
/// available) or construct use metadata (when the event is not available,
|
||||
/// i.e. immediately after message send) to create the construct uses.
|
||||
List<OneConstructUse> grammarConstructUses({
|
||||
Event? event,
|
||||
ConstructUseMetaData? metadata,
|
||||
|
|
|
|||
|
|
@ -110,11 +110,7 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
widget.pangeaController.analytics
|
||||
.getConstructs(
|
||||
constructType: constructType,
|
||||
removeIT: true,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: widget.selected,
|
||||
forceUpdate: true,
|
||||
timeSpan: widget.timeSpan,
|
||||
)
|
||||
.whenComplete(() => setState(() => fetchingConstructs = false))
|
||||
.then((value) => setState(() => _constructs = value));
|
||||
|
|
@ -126,11 +122,7 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
widget.pangeaController.analytics
|
||||
.getConstructs(
|
||||
constructType: constructType,
|
||||
removeIT: true,
|
||||
defaultSelected: widget.defaultSelected,
|
||||
selected: widget.selected,
|
||||
forceUpdate: true,
|
||||
timeSpan: widget.timeSpan,
|
||||
)
|
||||
.then(
|
||||
(value) => setState(() {
|
||||
|
|
@ -163,11 +155,11 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
) async {
|
||||
final Client client = Matrix.of(context).client;
|
||||
PangeaMessageEvent msgEvent;
|
||||
if (_msgEventCache.containsKey(use.msgId!)) {
|
||||
return _msgEventCache[use.msgId!]!;
|
||||
if (_msgEventCache.containsKey(use.msgId)) {
|
||||
return _msgEventCache[use.msgId]!;
|
||||
}
|
||||
final Room? msgRoom = use.getRoom(client);
|
||||
if (msgRoom == null || use.msgId == null) {
|
||||
if (msgRoom == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -189,7 +181,7 @@ class ConstructListViewState extends State<ConstructListView> {
|
|||
timeline: timeline,
|
||||
ownMessage: event.senderId == client.userID,
|
||||
);
|
||||
_msgEventCache[use.msgId!] = msgEvent;
|
||||
_msgEventCache[use.msgId] = msgEvent;
|
||||
return msgEvent;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:fluffychat/config/app_config.dart';
|
||||
import 'package:fluffychat/config/themes.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/enum/time_span.dart';
|
||||
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
|
||||
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
|
||||
import 'package:fluffychat/pangea/widgets/chat_list/analytics_summary/progress_indicator.dart';
|
||||
import 'package:fluffychat/utils/string_color.dart';
|
||||
import 'package:fluffychat/widgets/avatar.dart';
|
||||
|
|
@ -30,64 +32,78 @@ class LearningProgressIndicators extends StatefulWidget {
|
|||
class LearningProgressIndicatorsState
|
||||
extends State<LearningProgressIndicators> {
|
||||
final PangeaController _pangeaController = MatrixState.pangeaController;
|
||||
int? wordsUsed;
|
||||
int? errorTypes;
|
||||
|
||||
/// A stream subscription to listen for updates to
|
||||
/// the analytics data, either locally or from events
|
||||
StreamSubscription? _onAnalyticsUpdate;
|
||||
|
||||
/// Vocabulary constructs model
|
||||
ConstructListModel? words;
|
||||
|
||||
/// Grammar constructs model
|
||||
ConstructListModel? errors;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
setData();
|
||||
updateAnalyticsData();
|
||||
// listen for changes to analytics data and update the UI
|
||||
_onAnalyticsUpdate = _pangeaController
|
||||
.myAnalytics.analyticsUpdateStream.stream
|
||||
.listen((_) => updateAnalyticsData());
|
||||
}
|
||||
|
||||
AnalyticsSelected get defaultSelected => AnalyticsSelected(
|
||||
_pangeaController.matrixState.client.userID!,
|
||||
AnalyticsEntryType.student,
|
||||
"",
|
||||
@override
|
||||
void dispose() {
|
||||
_onAnalyticsUpdate?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// Update the analytics data shown in the UI. This comes from a
|
||||
/// combination of stored events and locally cached data.
|
||||
Future<void> updateAnalyticsData() async {
|
||||
final constructEvents = await _pangeaController.analytics.getConstructs();
|
||||
final List<OneConstructUse> localUses = [];
|
||||
for (final uses in _pangeaController.analytics.messagesSinceUpdate.values) {
|
||||
localUses.addAll(uses);
|
||||
}
|
||||
|
||||
if (constructEvents == null || constructEvents.isEmpty) {
|
||||
words = ConstructListModel(
|
||||
type: ConstructTypeEnum.vocab,
|
||||
uses: localUses,
|
||||
);
|
||||
errors = ConstructListModel(
|
||||
type: ConstructTypeEnum.grammar,
|
||||
uses: localUses,
|
||||
);
|
||||
|
||||
Future<void> setData() async {
|
||||
await getNumLemmasUsed();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> getNumLemmasUsed() async {
|
||||
final constructs = await _pangeaController.analytics.getConstructs(
|
||||
defaultSelected: defaultSelected,
|
||||
timeSpan: TimeSpan.forever,
|
||||
);
|
||||
if (constructs == null) {
|
||||
errorTypes = 0;
|
||||
wordsUsed = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
final List<String> errorLemmas = [];
|
||||
final List<String> vocabLemmas = [];
|
||||
for (final event in constructs) {
|
||||
for (final use in event.content.uses) {
|
||||
if (use.lemma == null) continue;
|
||||
switch (use.constructType) {
|
||||
case ConstructTypeEnum.grammar:
|
||||
errorLemmas.add(use.lemma!);
|
||||
break;
|
||||
case ConstructTypeEnum.vocab:
|
||||
vocabLemmas.add(use.lemma!);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
errorTypes = errorLemmas.toSet().length;
|
||||
wordsUsed = vocabLemmas.toSet().length;
|
||||
final List<OneConstructUse> storedConstruct =
|
||||
constructEvents.expand((e) => e.content.uses).toList();
|
||||
final List<OneConstructUse> allConstructs = [
|
||||
...storedConstruct,
|
||||
...localUses,
|
||||
];
|
||||
|
||||
words = ConstructListModel(
|
||||
type: ConstructTypeEnum.vocab,
|
||||
uses: allConstructs,
|
||||
);
|
||||
errors = ConstructListModel(
|
||||
type: ConstructTypeEnum.grammar,
|
||||
uses: allConstructs,
|
||||
);
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
int? getProgressPoints(ProgressIndicatorEnum indicator) {
|
||||
switch (indicator) {
|
||||
case ProgressIndicatorEnum.wordsUsed:
|
||||
return wordsUsed;
|
||||
return words?.lemmas.length;
|
||||
case ProgressIndicatorEnum.errorTypes:
|
||||
return errorTypes;
|
||||
return errors?.lemmas.length;
|
||||
case ProgressIndicatorEnum.level:
|
||||
return level;
|
||||
}
|
||||
|
|
@ -95,8 +111,8 @@ class LearningProgressIndicatorsState
|
|||
|
||||
int get xpPoints {
|
||||
final points = [
|
||||
wordsUsed ?? 0,
|
||||
errorTypes ?? 0,
|
||||
words?.lemmas.length ?? 0,
|
||||
errors?.lemmas.length ?? 0,
|
||||
];
|
||||
return points.reduce((a, b) => a + b);
|
||||
}
|
||||
|
|
@ -161,14 +177,36 @@ class LearningProgressIndicatorsState
|
|||
children: [
|
||||
SizedBox(
|
||||
width: FluffyThemes.columnWidth - (36 * 2) - 25,
|
||||
child: LinearProgressIndicator(
|
||||
value: (xpPoints % 100) / 100,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
backgroundColor:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
minHeight: 15,
|
||||
borderRadius:
|
||||
BorderRadius.circular(AppConfig.borderRadius),
|
||||
child: Expanded(
|
||||
child: Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
Container(
|
||||
height: 15,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
color:
|
||||
Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
AnimatedContainer(
|
||||
duration: FluffyThemes.animationDuration,
|
||||
curve: FluffyThemes.animationCurve,
|
||||
height: 15,
|
||||
width:
|
||||
(FluffyThemes.columnWidth - (36 * 2) - 25) *
|
||||
((xpPoints % 100) / 100),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(
|
||||
AppConfig.borderRadius,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:fluffychat/config/themes.dart';
|
||||
import 'package:fluffychat/pangea/enum/progress_indicators_enum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
|
@ -33,8 +34,8 @@ class ProgressIndicatorBadge extends StatelessWidget {
|
|||
),
|
||||
const SizedBox(width: 5),
|
||||
points != null
|
||||
? Text(
|
||||
points.toString(),
|
||||
? AnimatedCount(
|
||||
count: points!,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -49,3 +50,47 @@ class ProgressIndicatorBadge extends StatelessWidget {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedCount extends ImplicitlyAnimatedWidget {
|
||||
const AnimatedCount({
|
||||
super.key,
|
||||
required this.count,
|
||||
this.style,
|
||||
super.duration = const Duration(seconds: 1),
|
||||
super.curve = FluffyThemes.animationCurve,
|
||||
});
|
||||
|
||||
final int count;
|
||||
final TextStyle? style;
|
||||
|
||||
@override
|
||||
ImplicitlyAnimatedWidgetState<ImplicitlyAnimatedWidget> createState() {
|
||||
return _AnimatedCountState();
|
||||
}
|
||||
}
|
||||
|
||||
class _AnimatedCountState extends AnimatedWidgetBaseState<AnimatedCount> {
|
||||
IntTween _intCount = IntTween(begin: 0, end: 1);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_intCount = IntTween(begin: 0, end: widget.count.toInt());
|
||||
controller.forward();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final String text = _intCount.evaluate(animation).toString();
|
||||
return Text(text, style: widget.style);
|
||||
}
|
||||
|
||||
@override
|
||||
void forEachTween(TweenVisitor<dynamic> visitor) {
|
||||
_intCount = visitor(
|
||||
_intCount,
|
||||
widget.count,
|
||||
(dynamic value) => IntTween(begin: value),
|
||||
) as IntTween;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue