some questions, name changes, and a couple switches from grammar to morph uses

pull/1476/head
William Jordan-Cooley 1 year ago
parent 8d86c06456
commit c297dea437

@ -23,7 +23,7 @@ void main() async {
// #Pangea
try {
await dotenv.load(fileName: ".env");
await dotenv.load(fileName: ".env.local_choreo");
} catch (e) {
Logs().e('Failed to load .env file', e);
}

@ -15,8 +15,8 @@ import 'package:fluffychat/pages/chat/event_info_dialog.dart';
import 'package:fluffychat/pages/chat/recording_dialog.dart';
import 'package:fluffychat/pages/chat_details/chat_details.dart';
import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart';
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
import 'package:fluffychat/pangea/enum/message_mode_enum.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
@ -679,18 +679,15 @@ class ChatController extends State<ChatPageWithRoom>
);
if (msgEventId != null) {
pangeaController.myAnalytics.setState(
pangeaController.putAnalytics.setState(
AnalyticsStream(
eventId: msgEventId,
roomId: room.id,
constructs: [
...(choreo!.grammarConstructUses(metadata: metadata)),
...(originalSent!.vocabUses(
choreo: choreo,
tokens: tokensSent!.tokens,
metadata: metadata,
)),
],
constructs: originalSent!.vocabUses(
choreo: choreo,
tokens: tokensSent!.tokens,
metadata: metadata,
),
origin: AnalyticsUpdateOrigin.sendMessage,
),
);

@ -9,7 +9,7 @@ import 'package:fluffychat/pages/chat/pinned_events.dart';
import 'package:fluffychat/pages/chat/reply_display.dart';
import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart';
import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart';
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart';
import 'package:fluffychat/pangea/widgets/animations/gain_points.dart';
import 'package:fluffychat/pangea/widgets/chat/chat_floating_action_button.dart';

@ -1016,8 +1016,9 @@ class ChatListController extends State<ChatList>
}
// #Pangea
MatrixState.pangeaController.myAnalytics.initialize();
MatrixState.pangeaController.analytics.initialize();
//@ggurdin why is are these two initialized separately? why not in the _initPangeaControllers?
MatrixState.pangeaController.putAnalytics.initialize();
MatrixState.pangeaController.getAnalytics.initialize();
await _initPangeaControllers(client);
// Pangea#
if (!mounted) return;

@ -3,7 +3,7 @@ import 'dart:developer';
import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart';
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/enum/edit_type.dart';
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
@ -318,7 +318,7 @@ class ITController {
.toList();
// Save those choices' tokens to local construct analytics as ignored tokens
choreographer.pangeaController.myAnalytics.addDraftUses(
choreographer.pangeaController.putAnalytics.addDraftUses(
ignoredTokens ?? [],
choreographer.roomId,
ConstructUseTypeEnum.ignIt,

@ -7,7 +7,7 @@ import 'package:fluffychat/pangea/choreographer/widgets/it_bar_buttons.dart';
import 'package:fluffychat/pangea/choreographer/widgets/it_feedback_card.dart';
import 'package:fluffychat/pangea/choreographer/widgets/translation_finished_flow.dart';
import 'package:fluffychat/pangea/constants/choreo_constants.dart';
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/enum/instructions_enum.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
@ -369,7 +369,7 @@ class ITChoices extends StatelessWidget {
);
}
if (!continuance.wasClicked) {
controller.choreographer.pangeaController.myAnalytics.addDraftUses(
controller.choreographer.pangeaController.putAnalytics.addDraftUses(
continuance.tokens,
controller.choreographer.roomId,
continuance.level > 1

@ -4,8 +4,8 @@ import 'dart:math';
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/my_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/controllers/put_analytics_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';
@ -70,10 +70,10 @@ class GetAnalyticsController {
void initialize() {
_analyticsUpdateSubscription ??= _pangeaController
.myAnalytics.analyticsUpdateStream.stream
.putAnalytics.analyticsUpdateStream.stream
.listen(onAnalyticsUpdate);
_pangeaController.myAnalytics.lastUpdatedCompleter.future.then((_) {
_pangeaController.putAnalytics.lastUpdatedCompleter.future.then((_) {
getConstructs().then((_) => updateAnalyticsStream());
});
}
@ -127,9 +127,10 @@ class GetAnalyticsController {
uses: constructs,
type: ConstructTypeEnum.vocab,
);
final errors = ConstructListModel(
uses: constructs,
type: ConstructTypeEnum.grammar,
type: ConstructTypeEnum.morph,
);
return words.points + errors.points;
}
@ -168,7 +169,7 @@ class GetAnalyticsController {
return formattedCache;
} catch (err) {
// if something goes wrong while trying to format the local data, clear it
_pangeaController.myAnalytics
_pangeaController.putAnalytics
.clearMessagesSinceUpdate(clearDrafts: true);
return {};
}
@ -205,7 +206,7 @@ class GetAnalyticsController {
await client.roomsLoading;
// don't try to get constructs until last updated time has been loaded
await _pangeaController.myAnalytics.lastUpdatedCompleter.future;
await _pangeaController.putAnalytics.lastUpdatedCompleter.future;
// if forcing a refreshing, clear the cache
if (forceUpdate) _cache.clear();
@ -273,6 +274,9 @@ class GetAnalyticsController {
/// 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
/// @ggurdin - is this still relevant now that we're not doing grammar constructs?
/// maybe it should actually be filtering all grammar uses, though this is maybe more efficiently done
/// in the fromJson of the model reading the event content, then maybe we can get rid of that enum entry entirely
Future<List<OneConstructUse>> filterConstructs({
required List<OneConstructUse> unfilteredConstructs,
}) async {
@ -295,7 +299,7 @@ class GetAnalyticsController {
);
if (index > -1) {
final DateTime? lastUpdated = _pangeaController.myAnalytics.lastUpdated;
final DateTime? lastUpdated = _pangeaController.putAnalytics.lastUpdated;
if (_cache[index].needsUpdate(lastUpdated)) {
_cache.removeAt(index);
return null;

@ -1,535 +0,0 @@
import 'dart:async';
import 'package:fluffychat/pangea/constants/match_rule_ids.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/enum/time_span.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_event.dart';
import 'package:fluffychat/pangea/pages/analytics/base_analytics.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';
import '../constants/class_default_values.dart';
import '../extensions/client_extension/client_extension.dart';
import '../extensions/pangea_room_extension/pangea_room_extension.dart';
import 'base_controller.dart';
import 'pangea_controller.dart';
// controls the fetching of analytics data
class AnalyticsController extends BaseController {
late PangeaController _pangeaController;
final List<ConstructCacheEntry> _cachedConstructs = [];
AnalyticsController(PangeaController pangeaController) : super() {
_pangeaController = pangeaController;
}
String get langCode =>
_pangeaController.languageController.userL2?.langCode ??
_pangeaController.pLanguageStore.targetOptions.first.langCode;
// String get _analyticsTimeSpanKey => "ANALYTICS_TIME_SPAN_KEY";
// TimeSpan get currentAnalyticsTimeSpan {
// try {
// final String? str = _pangeaController.pStoreService.read(
// _analyticsTimeSpanKey,
// );
// return str != null
// ? TimeSpan.values.firstWhere((e) {
// final spanString = e.toString();
// return spanString == str;
// })
// : ClassDefaultValues.defaultTimeSpan;
// } catch (err) {
// debugger(when: kDebugMode);
// return ClassDefaultValues.defaultTimeSpan;
// }
// }
// Future<void> setCurrentAnalyticsTimeSpan(TimeSpan timeSpan) async {
// await _pangeaController.pStoreService.save(
// _analyticsTimeSpanKey,
// timeSpan.toString(),
// );
// setState();
// }
// String get _analyticsSpaceLangKey => "ANALYTICS_SPACE_LANG_KEY";
// LanguageModel get currentAnalyticsLang {
// try {
// final String? str = _pangeaController.pStoreService.read(
// _analyticsSpaceLangKey,
// );
// return str != null
// ? PangeaLanguage.byLangCode(str)
// : _pangeaController.languageController.userL2 ??
// _pangeaController.pLanguageStore.targetOptions.first;
// } catch (err) {
// debugger(when: kDebugMode);
// return _pangeaController.pLanguageStore.targetOptions.first;
// }
// }
// Future<void> setCurrentAnalyticsLang(LanguageModel lang) async {
// await _pangeaController.pStoreService.save(
// _analyticsSpaceLangKey,
// lang.langCode,
// );
// setState();
// }
/// Get the last time the user updated their analytics.
/// Tries to get the last time the user updated analytics for their current L2.
/// If there isn't yet an analytics room reacted for their L2, checks if the
/// user has any other analytics rooms and returns the most recent update time.
Future<DateTime?> myAnalyticsLastUpdated() async {
final List<Room> analyticsRooms =
_pangeaController.matrixState.client.allMyAnalyticsRooms;
final Map<String, DateTime> langCodeLastUpdates = {};
for (final Room analyticsRoom in analyticsRooms) {
final String? roomLang = analyticsRoom.madeForLang;
if (roomLang == null) continue;
final DateTime? lastUpdated = await analyticsRoom.analyticsLastUpdated(
_pangeaController.matrixState.client.userID!,
);
if (lastUpdated != null) {
langCodeLastUpdates[roomLang] = lastUpdated;
}
}
if (langCodeLastUpdates.isEmpty) return null;
final String? l2Code =
_pangeaController.languageController.userL2?.langCode;
if (l2Code != null && langCodeLastUpdates.containsKey(l2Code)) {
return langCodeLastUpdates[l2Code];
}
return langCodeLastUpdates.values.reduce(
(check, mostRecent) => check.isAfter(mostRecent) ? check : mostRecent,
);
}
/// check if any students have recently updated their analytics
/// if any have, then the cache needs to be updated
Future<DateTime?> spaceAnalyticsLastUpdated(
Room space,
) async {
await space.requestParticipants();
final List<Future<DateTime?>> lastUpdatedFutures = [];
for (final student in space.students) {
final Room? analyticsRoom = _pangeaController.matrixState.client
.analyticsRoomLocal(langCode, student.id);
if (analyticsRoom == null) continue;
lastUpdatedFutures.add(
analyticsRoom.analyticsLastUpdated(student.id),
);
}
final List<DateTime?> lastUpdatedWithNulls =
await Future.wait(lastUpdatedFutures);
final List<DateTime> lastUpdates =
lastUpdatedWithNulls.where((e) => e != null).cast<DateTime>().toList();
if (lastUpdates.isNotEmpty) {
return lastUpdates.reduce(
(check, mostRecent) => check.isAfter(mostRecent) ? check : mostRecent,
);
}
return null;
}
Future<List<ConstructAnalyticsEvent>> allMyConstructs(
TimeSpan timeSpan,
) async {
final Room? analyticsRoom =
_pangeaController.matrixState.client.analyticsRoomLocal(langCode);
if (analyticsRoom == null) return [];
final List<ConstructAnalyticsEvent>? roomEvents =
(await analyticsRoom.getAnalyticsEvents(
since: timeSpan.cutOffDate,
userId: _pangeaController.matrixState.client.userID!,
))
?.cast<ConstructAnalyticsEvent>();
final List<ConstructAnalyticsEvent> allConstructs = roomEvents ?? [];
return allConstructs
.where((construct) => construct.content.uses.isNotEmpty)
.toList();
}
Future<List<ConstructAnalyticsEvent>> allSpaceMemberConstructs(
Room space,
TimeSpan timeSpan,
) async {
await space.requestParticipants();
final List<ConstructAnalyticsEvent> constructEvents = [];
for (final student in space.students) {
final Room? analyticsRoom = _pangeaController.matrixState.client
.analyticsRoomLocal(langCode, student.id);
if (analyticsRoom != null) {
final List<ConstructAnalyticsEvent>? roomEvents =
(await analyticsRoom.getAnalyticsEvents(
since: timeSpan.cutOffDate,
userId: student.id,
))
?.cast<ConstructAnalyticsEvent>();
constructEvents.addAll(roomEvents ?? []);
}
}
final List<String> spaceChildrenIds = space.allSpaceChildRoomIds;
final List<ConstructAnalyticsEvent> allConstructs = [];
for (final constructEvent in constructEvents) {
constructEvent.content.uses.removeWhere(
(use) => !spaceChildrenIds.contains(use.chatId),
);
if (constructEvent.content.uses.isNotEmpty) {
allConstructs.add(constructEvent);
}
}
return allConstructs;
}
List<ConstructAnalyticsEvent> filterStudentConstructs(
List<ConstructAnalyticsEvent> unfilteredConstructs,
String? studentId,
) {
final List<ConstructAnalyticsEvent> filtered =
List<ConstructAnalyticsEvent>.from(unfilteredConstructs);
filtered.removeWhere((element) => element.event.senderId != studentId);
return filtered;
}
List<ConstructAnalyticsEvent> filterRoomConstructs(
List<ConstructAnalyticsEvent> unfilteredConstructs,
String? roomID,
) {
final List<ConstructAnalyticsEvent> filtered = [...unfilteredConstructs];
for (final construct in filtered) {
construct.content.uses.removeWhere((u) => u.chatId != roomID);
}
return filtered;
}
Future<List<ConstructAnalyticsEvent>> filterPrivateChatConstructs(
List<ConstructAnalyticsEvent> unfilteredConstructs,
Room space,
) async {
final List<String> privateChatIds = space.allSpaceChildRoomIds;
final resp = await space.client.getSpaceHierarchy(space.id);
final List<String> chatIds = resp.rooms.map((room) => room.roomId).toList();
for (final id in chatIds) {
privateChatIds.removeWhere((e) => e == id);
}
final List<ConstructAnalyticsEvent> filtered =
List<ConstructAnalyticsEvent>.from(unfilteredConstructs);
for (final construct in filtered) {
construct.content.uses.removeWhere(
(use) => !privateChatIds.contains(use.chatId),
);
}
return filtered;
}
Future<List<ConstructAnalyticsEvent>> filterSpaceConstructs(
List<ConstructAnalyticsEvent> unfilteredConstructs,
Room space,
) async {
final resp = await space.client.getSpaceHierarchy(space.id);
final List<String> chatIds = resp.rooms.map((room) => room.roomId).toList();
final List<ConstructAnalyticsEvent> filtered =
List<ConstructAnalyticsEvent>.from(unfilteredConstructs);
for (final construct in filtered) {
construct.content.uses.removeWhere(
(use) => !chatIds.contains(use.chatId),
);
}
return filtered;
}
List<ConstructAnalyticsEvent>? getConstructsLocal({
required TimeSpan timeSpan,
required AnalyticsSelected defaultSelected,
AnalyticsSelected? selected,
DateTime? lastUpdated,
ConstructTypeEnum? constructType,
}) {
final index = _cachedConstructs.indexWhere(
(e) =>
e.timeSpan == timeSpan &&
e.type == constructType &&
e.defaultSelected.id == defaultSelected.id &&
e.defaultSelected.type == defaultSelected.type &&
e.selected?.id == selected?.id &&
e.selected?.type == selected?.type &&
e.langCode == langCode,
);
if (index > -1) {
if (_cachedConstructs[index].needsUpdate(lastUpdated)) {
_cachedConstructs.removeAt(index);
return null;
}
return _cachedConstructs[index].events;
}
return null;
}
void cacheConstructs({
required List<ConstructAnalyticsEvent> events,
required AnalyticsSelected defaultSelected,
required TimeSpan timeSpan,
AnalyticsSelected? selected,
ConstructTypeEnum? constructType,
}) {
final entry = ConstructCacheEntry(
timeSpan: timeSpan,
type: constructType,
events: List.from(events),
defaultSelected: defaultSelected,
selected: selected,
langCode: langCode,
);
_cachedConstructs.add(entry);
}
Future<List<ConstructAnalyticsEvent>> getMyConstructs({
required AnalyticsSelected defaultSelected,
required TimeSpan timeSpan,
ConstructTypeEnum? constructType,
AnalyticsSelected? selected,
}) async {
final List<ConstructAnalyticsEvent> unfilteredConstructs =
await allMyConstructs(timeSpan);
final Room? space = selected?.type == AnalyticsEntryType.space
? _pangeaController.matrixState.client.getRoomById(selected!.id)
: null;
return filterConstructs(
unfilteredConstructs: unfilteredConstructs,
space: space,
defaultSelected: defaultSelected,
selected: selected,
timeSpan: timeSpan,
);
}
Future<List<ConstructAnalyticsEvent>> getSpaceConstructs({
required Room space,
required AnalyticsSelected defaultSelected,
required TimeSpan timeSpan,
AnalyticsSelected? selected,
ConstructTypeEnum? constructType,
}) async {
final List<ConstructAnalyticsEvent> unfilteredConstructs =
await allSpaceMemberConstructs(
space,
timeSpan,
);
return filterConstructs(
unfilteredConstructs: unfilteredConstructs,
space: space,
defaultSelected: defaultSelected,
selected: selected,
timeSpan: timeSpan,
);
}
Future<List<ConstructAnalyticsEvent>> filterConstructs({
required List<ConstructAnalyticsEvent> unfilteredConstructs,
required AnalyticsSelected defaultSelected,
required TimeSpan timeSpan,
Room? space,
AnalyticsSelected? selected,
}) async {
if ([AnalyticsEntryType.privateChats, AnalyticsEntryType.space]
.contains(selected?.type)) {
assert(space != null);
}
for (int i = 0; i < unfilteredConstructs.length; i++) {
final construct = unfilteredConstructs[i];
construct.content.uses.removeWhere(
(use) => use.timeStamp.isBefore(timeSpan.cutOffDate),
);
}
unfilteredConstructs.removeWhere((e) => e.content.uses.isEmpty);
switch (selected?.type) {
case null:
return unfilteredConstructs;
case AnalyticsEntryType.student:
if (defaultSelected.type != AnalyticsEntryType.space) {
throw Exception(
"student filtering not available for default filter ${defaultSelected.type}",
);
}
return filterStudentConstructs(unfilteredConstructs, selected!.id);
case AnalyticsEntryType.room:
return filterRoomConstructs(unfilteredConstructs, selected?.id);
case AnalyticsEntryType.privateChats:
return defaultSelected.type == AnalyticsEntryType.student
? throw "private chat filtering not available for my analytics"
: await filterPrivateChatConstructs(unfilteredConstructs, space!);
case AnalyticsEntryType.space:
return await filterSpaceConstructs(unfilteredConstructs, space!);
default:
throw Exception("invalid filter type - ${selected?.type}");
}
}
Future<List<ConstructAnalyticsEvent>?> getConstructs({
required AnalyticsSelected defaultSelected,
required TimeSpan timeSpan,
AnalyticsSelected? selected,
bool removeIT = true,
bool forceUpdate = false,
ConstructTypeEnum? constructType,
}) async {
debugPrint("getting constructs");
await _pangeaController.matrixState.client.roomsLoading;
Room? space;
if (defaultSelected.type == AnalyticsEntryType.space) {
space = _pangeaController.matrixState.client.getRoomById(
defaultSelected.id,
);
if (space == null) {
ErrorHandler.logError(
m: "space not found in setConstructs",
data: {
"defaultSelected": defaultSelected,
"selected": selected,
},
);
return [];
}
}
DateTime? lastUpdated;
if (defaultSelected.type != AnalyticsEntryType.space) {
// if default selected view is my analytics, check for the last
// time the logged in user updated their analytics events
// this gets passed to getAnalyticsLocal to determine if the cached
// entry is out-of-date
lastUpdated = await myAnalyticsLastUpdated();
} else {
// else, get the last time a student in the space updated their analytics
lastUpdated = await spaceAnalyticsLastUpdated(
space!,
);
}
final List<ConstructAnalyticsEvent>? local = getConstructsLocal(
timeSpan: timeSpan,
constructType: constructType,
defaultSelected: defaultSelected,
selected: selected,
lastUpdated: lastUpdated,
);
if (local != null && !forceUpdate) {
debugPrint("returning local constructs");
return local;
}
debugPrint("fetching new constructs");
final filteredConstructs = space == null
? await getMyConstructs(
constructType: constructType,
defaultSelected: defaultSelected,
selected: selected,
timeSpan: timeSpan,
)
: await getSpaceConstructs(
constructType: constructType,
space: space,
defaultSelected: defaultSelected,
selected: selected,
timeSpan: timeSpan,
);
if (removeIT) {
for (final construct in filteredConstructs) {
construct.content.uses.removeWhere(
(element) =>
element.lemma == "Try interactive translation" ||
element.lemma == "itStart" ||
element.lemma == MatchRuleIds.interactiveTranslation,
);
}
}
if (local == null) {
cacheConstructs(
constructType: constructType,
events: filteredConstructs,
defaultSelected: defaultSelected,
selected: selected,
timeSpan: timeSpan,
);
}
return filteredConstructs;
}
}
abstract class CacheEntry {
final String langCode;
final TimeSpan timeSpan;
final AnalyticsSelected defaultSelected;
AnalyticsSelected? selected;
late final DateTime _createdAt;
CacheEntry({
required this.timeSpan,
required this.defaultSelected,
required this.langCode,
this.selected,
}) {
_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);
}
}
class ConstructCacheEntry extends CacheEntry {
final ConstructTypeEnum? type;
final List<ConstructAnalyticsEvent> events;
ConstructCacheEntry({
required this.events,
required super.timeSpan,
required super.langCode,
required super.defaultSelected,
this.type,
super.selected,
});
}

@ -12,10 +12,10 @@ 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';
import 'package:fluffychat/pangea/controllers/message_data_controller.dart';
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/permissions_controller.dart';
import 'package:fluffychat/pangea/controllers/practice_activity_generation_controller.dart';
import 'package:fluffychat/pangea/controllers/practice_activity_record_controller.dart';
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/speech_to_text_controller.dart';
import 'package:fluffychat/pangea/controllers/subscription_controller.dart';
import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
@ -44,11 +44,12 @@ class PangeaController {
late LanguageController languageController;
late ClassController classController;
late PermissionsController permissionsController;
// late AnalyticsController analytics;
late GetAnalyticsController analytics;
late MyAnalyticsController myAnalytics;
late GetAnalyticsController getAnalytics;
late PutAnalyticsController putAnalytics;
late WordController wordNet;
late MessageDataController messageData;
// TODO: make these static so we can remove from here
late ContextualDefinitionController definitions;
late ITFeedbackController itFeedback;
late InstructionsController instructions;
@ -93,9 +94,8 @@ class PangeaController {
languageController = LanguageController(this);
classController = ClassController(this);
permissionsController = PermissionsController(this);
// analytics = AnalyticsController(this);
analytics = GetAnalyticsController(this);
myAnalytics = MyAnalyticsController(this);
getAnalytics = GetAnalyticsController(this);
putAnalytics = PutAnalyticsController(this);
messageData = MessageDataController(this);
wordNet = WordController(this);
definitions = ContextualDefinitionController(this);
@ -146,13 +146,13 @@ class PangeaController {
case LoginState.loggedOut:
case LoginState.softLoggedOut:
// Reset cached analytics data
MatrixState.pangeaController.myAnalytics.dispose();
MatrixState.pangeaController.analytics.dispose();
MatrixState.pangeaController.putAnalytics.dispose();
MatrixState.pangeaController.getAnalytics.dispose();
break;
case LoginState.loggedIn:
// Initialize analytics data
MatrixState.pangeaController.myAnalytics.initialize();
MatrixState.pangeaController.analytics.initialize();
MatrixState.pangeaController.putAnalytics.initialize();
MatrixState.pangeaController.getAnalytics.initialize();
break;
}
if (state != LoginState.loggedIn) {
@ -169,28 +169,6 @@ class PangeaController {
GoogleAnalytics.analyticsUserUpdate(matrixState.client.userID);
}
// void startChatWithBotIfNotPresent() {
// Future.delayed(const Duration(milliseconds: 5000), () async {
// try {
// if (pStoreService.read("started_bot_chat", addClientIdToKey: false) ??
// false) {
// return;
// }
// await pStoreService.save("started_bot_chat", true,
// addClientIdToKey: false);
// final rooms = matrixState.client.rooms;
// await matrixState.client.startDirectChat(
// BotName.byEnvironment,
// enableEncryption: false,
// );
// } catch (err, stack) {
// debugger(when: kDebugMode);
// ErrorHandler.logError(e: err, s: stack);
// }
// });
// }
void startChatWithBotIfNotPresent() {
Future.delayed(const Duration(milliseconds: 10000), () async {
// check if user is logged in

@ -19,7 +19,7 @@ enum AnalyticsUpdateType { server, local }
/// 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<AnalyticsStream> {
class PutAnalyticsController extends BaseController<AnalyticsStream> {
late PangeaController _pangeaController;
CachedStreamController<AnalyticsUpdate> analyticsUpdateStream =
CachedStreamController<AnalyticsUpdate>();
@ -47,13 +47,13 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
/// the time since the last update that will trigger an automatic update
final Duration _timeSinceUpdate = const Duration(days: 1);
MyAnalyticsController(PangeaController pangeaController) {
PutAnalyticsController(PangeaController pangeaController) {
_pangeaController = pangeaController;
}
void initialize() {
// Listen to a stream that provides the eventIDs
// of new messages sent by the logged in user
// Listen for calls to setState on the analytics stream
// and update the analytics room if necessary
_analyticsStream ??=
stateStream.listen((data) => _onNewAnalyticsData(data));
@ -79,7 +79,7 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
try {
// if lastUpdated hasn't been set yet, set it
lastUpdated ??=
await _pangeaController.analytics.myAnalyticsLastUpdated();
await _pangeaController.getAnalytics.myAnalyticsLastUpdated();
} catch (err, s) {
ErrorHandler.logError(
s: s,
@ -100,8 +100,9 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
}
}
/// Given the data from a newly sent message, format and cache
/// the message's construct data locally and reset the update timer
/// Given new construct uses, format and cache
/// the data locally and reset the update timer
/// Decide whether to update the analytics room
void _onNewAnalyticsData(AnalyticsStream data) {
final List<OneConstructUse> constructs = _getDraftUses(data.roomId);
@ -110,25 +111,24 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
final String eventID = data.eventId;
final String roomID = data.roomId;
_pangeaController.analytics
.filterConstructs(unfilteredConstructs: constructs)
.then((filtered) {
for (final use in filtered) {
if (kDebugMode) {
for (final use in constructs) {
debugPrint(
"_onNewAnalyticsData filtered use: ${use.constructType.string} ${use.useType.string} ${use.lemma} ${use.useType.pointValue}",
);
}
if (filtered.isEmpty) return;
}
final level = _pangeaController.analytics.level;
if (constructs.isEmpty) return;
_addLocalMessage(eventID, filtered).then(
(_) {
_clearDraftUses(roomID);
_decideWhetherToUpdateAnalyticsRoom(level, data.origin);
},
);
});
final level = _pangeaController.getAnalytics.level;
_addLocalMessage(eventID, constructs).then(
(_) {
_clearDraftUses(roomID);
_decideWhetherToUpdateAnalyticsRoom(level, data.origin);
},
);
}
void addDraftUses(
@ -142,8 +142,12 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
timeStamp: DateTime.now(),
);
final uses = tokens
.where((token) => token.lemma.saveVocab)
// we only save those with saveVocab == true
final tokensToSave =
tokens.where((token) => token.lemma.saveVocab).toList();
// get all our vocab constructs
final uses = tokensToSave
.map(
(token) => OneConstructUse(
useType: useType,
@ -155,7 +159,8 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
)
.toList();
for (final token in tokens) {
// get all our grammar constructs
for (final token in tokensToSave) {
for (final entry in token.morph.entries) {
uses.add(
OneConstructUse(
@ -177,19 +182,19 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
}
}
final level = _pangeaController.analytics.level;
final level = _pangeaController.getAnalytics.level;
_addLocalMessage('draft$roomID', uses).then(
(_) => _decideWhetherToUpdateAnalyticsRoom(level, origin),
);
}
List<OneConstructUse> _getDraftUses(String roomID) {
final currentCache = _pangeaController.analytics.messagesSinceUpdate;
final currentCache = _pangeaController.getAnalytics.messagesSinceUpdate;
return currentCache['draft$roomID'] ?? [];
}
void _clearDraftUses(String roomID) {
final currentCache = _pangeaController.analytics.messagesSinceUpdate;
final currentCache = _pangeaController.getAnalytics.messagesSinceUpdate;
currentCache.remove('draft$roomID');
_setMessagesSinceUpdate(currentCache);
}
@ -201,7 +206,7 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
List<OneConstructUse> constructs,
) async {
try {
final currentCache = _pangeaController.analytics.messagesSinceUpdate;
final currentCache = _pangeaController.getAnalytics.messagesSinceUpdate;
constructs.addAll(currentCache[cacheKey] ?? []);
currentCache[cacheKey] = constructs;
@ -231,14 +236,14 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
sendLocalAnalyticsToAnalyticsRoom();
});
if (_pangeaController.analytics.messagesSinceUpdate.length >
if (_pangeaController.getAnalytics.messagesSinceUpdate.length >
_maxMessagesCached) {
debugPrint("reached max messages, updating");
sendLocalAnalyticsToAnalyticsRoom();
return;
}
final int newLevel = _pangeaController.analytics.level;
final int newLevel = _pangeaController.getAnalytics.level;
newLevel > prevLevel
? sendLocalAnalyticsToAnalyticsRoom()
: analyticsUpdateStream.add(
@ -253,7 +258,7 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
return;
}
final localCache = _pangeaController.analytics.messagesSinceUpdate;
final localCache = _pangeaController.getAnalytics.messagesSinceUpdate;
final draftKeys = localCache.keys.where((key) => key.startsWith('draft'));
if (draftKeys.isEmpty) {
_pangeaController.pStoreService.delete(PLocalKey.messagesSinceUpdate);
@ -328,7 +333,7 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
/// The analytics room is determined based on the user's current target language.
Future<void> _updateAnalytics() async {
// if there's no cached construct data, there's nothing to send
final cachedConstructs = _pangeaController.analytics.messagesSinceUpdate;
final cachedConstructs = _pangeaController.getAnalytics.messagesSinceUpdate;
final bool onlyDraft = cachedConstructs.length == 1 &&
cachedConstructs.keys.single.startsWith('draft');
if (cachedConstructs.isEmpty || onlyDraft) return;
@ -341,7 +346,7 @@ class MyAnalyticsController extends BaseController<AnalyticsStream> {
// and send cached analytics data to the room
await analyticsRoom?.sendConstructsEvent(
_pangeaController.analytics.locallyCachedSentConstructs,
_pangeaController.getAnalytics.locallyCachedSentConstructs,
);
}
}

@ -1,16 +1,19 @@
import 'dart:developer';
import 'package:fluffychat/pangea/constants/analytics_constants.dart';
import 'package:flutter/foundation.dart';
enum ConstructTypeEnum {
grammar,
/// for vocabulary words
vocab,
/// for morphs, actually called "Grammar" in the UI... :P
morph,
}
extension ConstructExtension on ConstructTypeEnum {
String get string {
switch (this) {
case ConstructTypeEnum.grammar:
return 'grammar';
case ConstructTypeEnum.vocab:
return 'vocab';
case ConstructTypeEnum.morph:
@ -20,8 +23,6 @@ extension ConstructExtension on ConstructTypeEnum {
int get maxXPPerLemma {
switch (this) {
case ConstructTypeEnum.grammar:
return 0;
case ConstructTypeEnum.vocab:
return AnalyticsConstants.vocabUseMaxXP;
case ConstructTypeEnum.morph:
@ -33,9 +34,6 @@ extension ConstructExtension on ConstructTypeEnum {
class ConstructTypeUtil {
static ConstructTypeEnum fromString(String? string) {
switch (string) {
case 'g':
case 'grammar':
return ConstructTypeEnum.grammar;
case 'v':
case 'vocab':
return ConstructTypeEnum.vocab;
@ -43,6 +41,7 @@ class ConstructTypeUtil {
case 'morph':
return ConstructTypeEnum.morph;
default:
debugger(when: kDebugMode);
return ConstructTypeEnum.vocab;
}
}

@ -37,13 +37,13 @@ import '../../models/choreo_record.dart';
import '../../models/representation_content_model.dart';
import '../client_extension/client_extension.dart';
part "children_and_parents_extension.dart";
part "events_extension.dart";
part "room_analytics_extension.dart";
part "room_children_and_parents_extension.dart";
part "room_events_extension.dart";
part "room_information_extension.dart";
part "room_settings_extension.dart";
part "space_settings_extension.dart";
part "user_permissions_extension.dart";
part "room_space_settings_extension.dart";
part "room_user_permissions_extension.dart";
extension PangeaRoom on Room {
// analytics

@ -282,70 +282,6 @@ extension EventsRoomExtension on Room {
);
}
// ConstructEvent? _vocabEventLocal(String lemma) {
// if (!isAnalyticsRoom) throw Exception("not an analytics room");
// final Event? matrixEvent = getState(PangeaEventTypes.vocab, lemma);
// return matrixEvent != null ? ConstructEvent(event: matrixEvent) : null;
// }
// Future<ConstructEvent> _vocabEvent(
// String lemma,
// ConstructType type, [
// bool makeIfNull = false,
// ]) async {
// try {
// if (!isAnalyticsRoom) throw Exception("not an analytics room");
// ConstructEvent? localEvent = _vocabEventLocal(lemma);
// if (localEvent != null) return localEvent;
// await postLoad();
// localEvent = _vocabEventLocal(lemma);
// if (localEvent == null && isRoomOwner && makeIfNull) {
// final Event matrixEvent = await _createVocabEvent(lemma, type);
// localEvent = ConstructEvent(event: matrixEvent);
// }
// return localEvent!;
// } catch (err) {
// debugger(when: kDebugMode);
// rethrow;
// }
// }
// Future<Event> _createVocabEvent(String lemma, ConstructType type) async {
// try {
// if (!isRoomOwner) {
// throw Exception(
// "Tried to create vocab event in room where user is not owner",
// );
// }
// final String eventId = await client.setRoomStateWithKey(
// id,
// PangeaEventTypes.vocab,
// lemma,
// ConstructUses(lemma: lemma, type: type).toJson(),
// );
// final Event? event = await getEventById(eventId);
// if (event == null) {
// debugger(when: kDebugMode);
// throw Exception(
// "null event after creation with eventId $eventId in _createVocabEvent",
// );
// }
// return event;
// } catch (err, stack) {
// debugger(when: kDebugMode);
// ErrorHandler.logError(e: err, s: stack, data: powerLevels);
// rethrow;
// }
// }
/// Get a list of events in the room that are of type [PangeaEventTypes.construct]
/// and have the sender as [userID]. If [count] is provided, the function will
/// return at most [count] events.

@ -7,7 +7,6 @@ import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart';
import 'package:fluffychat/pangea/enum/audio_encoding_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/choreo_record.dart';
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
import 'package:fluffychat/pangea/models/representation_content_model.dart';
@ -639,34 +638,4 @@ class PangeaMessageEvent {
/// Returns a list of [PracticeActivityEvent] for the user's active l2.
List<PracticeActivityEvent> get practiceActivities =>
l2Code == null ? [] : practiceActivitiesByLangCode(l2Code!);
/// all construct uses for the message, including vocab and grammar
List<OneConstructUse> get allConstructUses => [
..._grammarConstructUses,
..._vocabUses,
];
/// get construct uses of type vocab for the message
List<OneConstructUse> get _vocabUses {
if (originalSent?.tokens != null) {
return originalSent!.content.vocabUses(
event: event,
choreo: originalSent!.choreo,
tokens: originalSent!.tokens!,
);
}
return [];
}
/// get construct uses of type grammar for the message
List<OneConstructUse> get _grammarConstructUses =>
originalSent?.choreo?.grammarConstructUses(event: event) ?? [];
}
class URLFinder {
static Iterable<Match> getMatches(String text) {
final RegExp exp =
RegExp(r'(?:(?:https?|ftp):\/\/)?[\w/\-?=%.]+\.[\w/\-?=%.]+');
return exp.allMatches(text);
}
}

@ -18,53 +18,29 @@ class ConstructAnalyticsModel {
factory ConstructAnalyticsModel.fromJson(Map<String, dynamic> json) {
final List<OneConstructUse> uses = [];
if (json[_usesKey] is List) {
// This is the new format
uses.addAll(
(json[_usesKey] as List)
.map((use) => OneConstructUse.fromJson(use))
.cast<OneConstructUse>()
.toList(),
);
} else {
// This is the old format. No data on production should be
// structured this way, but it's useful for testing.
try {
final useValues = (json[_usesKey] as Map<String, dynamic>).values;
for (final useValue in useValues) {
final lemma = useValue['lemma'];
final lemmaUses = useValue[_usesKey];
for (final useData in lemmaUses) {
final use = OneConstructUse(
useType: ConstructUseTypeEnum.ga,
lemma: lemma,
form: useData["form"],
constructType: ConstructTypeEnum.grammar,
metadata: ConstructUseMetaData(
eventId: useData["msgId"],
roomId: useData["chatId"],
timeStamp: DateTime.parse(useData["timeStamp"]),
),
);
uses.add(use);
}
for (final useJson in json[_usesKey]) {
// grammar construct uses are deprecated so but some are saved
// here we're filtering from data
if (["grammar", "g"].contains(useJson['constructType'])) {
continue;
} else {
uses.add(OneConstructUse.fromJson(useJson));
}
} catch (err, s) {
debugPrint("Error parsing ConstructAnalyticsModel");
ErrorHandler.logError(
e: err,
s: s,
m: "Error parsing ConstructAnalyticsModel",
);
debugger(when: kDebugMode);
}
} else {
debugger(when: kDebugMode);
ErrorHandler.logError(m: "Analytics room with non-list uses");
}
return ConstructAnalyticsModel(
uses: uses,
);
}
toJson() {
Map<String, dynamic> toJson() {
return {
_usesKey: uses.map((use) => use.toJson()).toList(),
};

@ -1,10 +1,6 @@
import 'dart:convert';
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/pangea_match_model.dart';
import 'package:matrix/matrix.dart';
import 'it_step.dart';
@ -115,44 +111,6 @@ class ChoreoRecord {
String get finalMessage =>
choreoSteps.isNotEmpty ? choreoSteps.last.text : "";
/// 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,
}) {
final List<OneConstructUse> uses = [];
if (event?.roomId == null && metadata?.roomId == null) {
return uses;
}
metadata ??= ConstructUseMetaData(
roomId: event!.roomId!,
eventId: event.eventId,
timeStamp: event.originServerTs,
);
for (final step in choreoSteps) {
if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted) {
final String name = step.acceptedOrIgnoredMatch!.match.rule?.id ??
step.acceptedOrIgnoredMatch!.match.shortMessage ??
step.acceptedOrIgnoredMatch!.match.type.typeName.name;
uses.add(
OneConstructUse(
useType: ConstructUseTypeEnum.ga,
lemma: name,
form: name,
constructType: ConstructTypeEnum.grammar,
id: "${metadata.eventId}_${step.acceptedOrIgnoredMatch!.match.offset}_${step.acceptedOrIgnoredMatch!.match.length}",
metadata: metadata,
),
);
}
}
return uses;
}
}
/// A new ChoreoRecordStep is saved in the following cases:

@ -120,7 +120,7 @@ class PangeaRepresentation {
tokens.where((token) => token.lemma.saveVocab).toList();
for (final token in tokensToSave) {
uses.addAll(
getUsesForToken(
_getUsesForToken(
token,
metadata,
choreo: choreo,
@ -138,7 +138,7 @@ class PangeaRepresentation {
/// If the [token] is in the [choreo.acceptedOrIgnoredMatch], it is considered to be a [ConstructUseTypeEnum.ga].
/// If the [token] is in the [choreo.acceptedOrIgnoredMatch.choices], it is considered to be a [ConstructUseTypeEnum.corIt].
/// If the [token] is not included in any choreoStep, it is considered to be a [ConstructUseTypeEnum.wa].
List<OneConstructUse> getUsesForToken(
List<OneConstructUse> _getUsesForToken(
PangeaToken token,
ConstructUseMetaData metadata, {
ChoreoRecord? choreo,

@ -1,572 +0,0 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/enum/construct_type_enum.dart';
import 'package:fluffychat/pangea/enum/time_span.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart';
import 'package:fluffychat/pangea/models/analytics/construct_list_model.dart';
import 'package:fluffychat/pangea/models/analytics/constructs_model.dart';
import 'package:fluffychat/pangea/models/pangea_match_model.dart';
import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart';
import 'package:fluffychat/pangea/utils/error_handler.dart';
import 'package:fluffychat/utils/date_time_extension.dart';
import 'package:fluffychat/utils/string_color.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/l10n.dart';
import 'package:matrix/matrix.dart';
class ConstructList extends StatefulWidget {
final ConstructTypeEnum constructType;
final AnalyticsSelected defaultSelected;
final AnalyticsSelected? selected;
final TimeSpan timeSpan;
final PangeaController pangeaController;
final StreamController refreshStream;
const ConstructList({
super.key,
required this.constructType,
required this.defaultSelected,
required this.pangeaController,
required this.refreshStream,
required this.timeSpan,
this.selected,
});
@override
State<StatefulWidget> createState() => ConstructListState();
}
class ConstructListState extends State<ConstructList> {
String? langCode;
String? error;
@override
Widget build(BuildContext context) {
return error != null
? Center(
child: Text(error!),
)
: Column(
children: [
ConstructListView(
pangeaController: widget.pangeaController,
defaultSelected: widget.defaultSelected,
selected: widget.selected,
refreshStream: widget.refreshStream,
timeSpan: widget.timeSpan,
),
],
);
}
}
// list view of construct events
// parameters
// 1) a list of construct events and
// 2) a boolean indicating whether the list has been initialized
// if not initialized, show loading indicator
// for each tile,
// title = construct.content.lemma
// subtitle = total uses, equal to construct.content.uses.length
// list has a fixed height of 400 and is scrollable
class ConstructListView extends StatefulWidget {
final PangeaController pangeaController;
final AnalyticsSelected defaultSelected;
final TimeSpan timeSpan;
final AnalyticsSelected? selected;
final StreamController refreshStream;
const ConstructListView({
super.key,
required this.pangeaController,
required this.defaultSelected,
required this.timeSpan,
required this.refreshStream,
this.selected,
});
@override
State<StatefulWidget> createState() => ConstructListViewState();
}
class ConstructListViewState extends State<ConstructListView> {
final ConstructTypeEnum constructType = ConstructTypeEnum.grammar;
final Map<String, Timeline> _timelinesCache = {};
final Map<String, PangeaMessageEvent> _msgEventCache = {};
final List<PangeaMessageEvent> _msgEvents = [];
bool fetchingConstructs = true;
bool fetchingUses = false;
StreamSubscription? refreshSubscription;
String? currentLemma;
@override
void initState() {
super.initState();
widget.pangeaController.analytics
.getConstructs(
constructType: constructType,
forceUpdate: true,
)
.whenComplete(() => setState(() => fetchingConstructs = false))
.then(
(value) => setState(
() => constructs = ConstructListModel(
type: constructType,
uses: value,
),
),
);
refreshSubscription = widget.refreshStream.stream.listen((forceUpdate) {
// postframe callback to let widget rebuild with the new selected parameter
// before sending selected to getConstructs function
WidgetsBinding.instance.addPostFrameCallback((_) {
widget.pangeaController.analytics
.getConstructs(
constructType: constructType,
forceUpdate: true,
)
.then(
(value) => setState(() {
ConstructListModel(
type: constructType,
uses: value,
);
}),
);
});
});
}
@override
void dispose() {
refreshSubscription?.cancel();
super.dispose();
}
void setCurrentLemma(String? lemma) {
currentLemma = lemma;
setState(() {});
}
Future<PangeaMessageEvent?> getMessageEvent(
OneConstructUse use,
) async {
final Client client = Matrix.of(context).client;
PangeaMessageEvent msgEvent;
if (_msgEventCache.containsKey(use.msgId)) {
return _msgEventCache[use.msgId]!;
}
final Room? msgRoom = use.getRoom(client);
if (msgRoom == null) {
return null;
}
Timeline? timeline;
if (_timelinesCache.containsKey(use.chatId)) {
timeline = _timelinesCache[use.chatId];
} else {
timeline = msgRoom.timeline ?? await msgRoom.getTimeline();
_timelinesCache[use.chatId] = timeline;
}
final Event? event = await use.getEvent(client);
if (event == null || timeline == null) {
return null;
}
msgEvent = PangeaMessageEvent(
event: event,
timeline: timeline,
ownMessage: event.senderId == client.userID,
);
_msgEventCache[use.msgId] = msgEvent;
return msgEvent;
}
Future<void> fetchUses() async {
if (fetchingUses) return;
if (currentLemma == null) {
setState(() => _msgEvents.clear());
return;
}
setState(() => fetchingUses = true);
try {
final List<OneConstructUse> uses = constructs?.constructList
.firstWhereOrNull(
(element) => element.lemma == currentLemma,
)
?.uses ??
[];
_msgEvents.clear();
for (final OneConstructUse use in uses) {
final PangeaMessageEvent? msgEvent = await getMessageEvent(use);
final RepresentationEvent? repEvent =
msgEvent?.originalSent ?? msgEvent?.originalWritten;
if (repEvent?.choreo == null) {
continue;
}
_msgEvents.add(msgEvent!);
}
setState(() => fetchingUses = false);
} catch (err, s) {
setState(() => fetchingUses = false);
debugPrint("Error fetching uses: $err");
ErrorHandler.logError(
e: err,
s: s,
m: "Failed to fetch uses for current construct $currentLemma",
);
}
}
ConstructListModel? constructs;
// given the current lemma and list of message events, return a list of
// MessageEventMatch objects, which contain one PangeaMessageEvent to one PangeaMatch
// this is because some message events may have has more than one PangeaMatch of a
// given lemma type.
List<MessageEventMatch> getMessageEventMatches() {
if (currentLemma == null) return [];
final List<MessageEventMatch> allMsgErrorSteps = [];
for (final msgEvent in _msgEvents) {
if (allMsgErrorSteps.any(
(element) => element.msgEvent.eventId == msgEvent.eventId,
)) {
continue;
}
// get all the pangea matches in that message which have that lemma
final List<PangeaMatch>? msgErrorSteps = msgEvent.errorSteps(
currentLemma!,
);
if (msgErrorSteps == null) continue;
allMsgErrorSteps.addAll(
msgErrorSteps.map(
(errorStep) => MessageEventMatch(
msgEvent: msgEvent,
lemmaMatch: errorStep,
),
),
);
}
return allMsgErrorSteps;
}
Future<void> showConstructMessagesDialog() async {
await showDialog<ConstructMessagesDialog>(
context: context,
builder: (c) => ConstructMessagesDialog(controller: this),
);
}
@override
Widget build(BuildContext context) {
if (fetchingConstructs || fetchingUses) {
return const Expanded(
child: Center(child: CircularProgressIndicator()),
);
}
if (constructs?.constructList.isEmpty ?? true) {
return Expanded(
child: Center(child: Text(L10n.of(context)!.noDataFound)),
);
}
return Expanded(
child: ListView.builder(
itemCount: constructs!.constructList.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(
constructs!.constructList[index].lemma,
),
subtitle: Text(
'${L10n.of(context)!.total} ${constructs!.constructList[index].uses.length}',
),
onTap: () async {
final String lemma = constructs!.constructList[index].lemma;
setCurrentLemma(lemma);
fetchUses().then((_) => showConstructMessagesDialog());
},
);
},
),
);
}
}
class ConstructMessagesDialog extends StatelessWidget {
final ConstructListViewState controller;
const ConstructMessagesDialog({
super.key,
required this.controller,
});
@override
Widget build(BuildContext context) {
if (controller.currentLemma == null || controller.constructs == null) {
return const AlertDialog(content: CircularProgressIndicator.adaptive());
}
final msgEventMatches = controller.getMessageEventMatches();
final currentConstruct =
controller.constructs!.constructList.firstWhereOrNull(
(construct) => construct.lemma == controller.currentLemma,
);
final noData = currentConstruct == null ||
currentConstruct.uses.length > controller._msgEvents.length;
return AlertDialog(
title: Center(child: Text(controller.currentLemma!)),
content: SizedBox(
height: noData ? 90 : 250,
width: noData ? 200 : 400,
child: Column(
children: [
if (noData)
Center(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(L10n.of(context)!.roomDataMissing),
),
),
Expanded(
child: ListView(
children: [
...msgEventMatches.mapIndexed(
(index, event) => Column(
children: [
ConstructMessage(
msgEvent: event.msgEvent,
lemma: controller.currentLemma!,
errorMessage: event.lemmaMatch,
),
if (index < msgEventMatches.length - 1)
const Divider(height: 1),
],
),
),
],
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context, rootNavigator: false).pop(),
child: Text(
L10n.of(context)!.close.toUpperCase(),
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
],
);
}
}
class ConstructMessage extends StatelessWidget {
final PangeaMessageEvent msgEvent;
final PangeaMatch errorMessage;
final String lemma;
const ConstructMessage({
super.key,
required this.msgEvent,
required this.errorMessage,
required this.lemma,
});
@override
Widget build(BuildContext context) {
final String? chosen = errorMessage.match.choices
?.firstWhereOrNull(
(element) => element.selected == true,
)
?.value;
if (chosen == null) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.all(20),
child: Row(
children: [
ConstructMessageMetadata(msgEvent: msgEvent),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FutureBuilder<User?>(
future: msgEvent.event.fetchSenderUser(),
builder: (context, snapshot) {
final displayname = snapshot.data?.calcDisplayname() ??
msgEvent.event.senderFromMemoryOrFallback
.calcDisplayname();
return Text(
displayname,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: (Theme.of(context).brightness ==
Brightness.light
? displayname.color
: displayname.lightColorText),
),
);
},
),
ConstructMessageBubble(
errorText: errorMessage.match.fullText,
replacementText: chosen,
start: errorMessage.match.offset,
end:
errorMessage.match.offset + errorMessage.match.length,
),
],
),
],
),
),
],
),
);
}
}
class ConstructMessageBubble extends StatelessWidget {
final String errorText;
final String replacementText;
final int start;
final int end;
const ConstructMessageBubble({
super.key,
required this.errorText,
required this.replacementText,
required this.start,
required this.end,
});
@override
Widget build(BuildContext context) {
final defaultStyle = TextStyle(
color: Theme.of(context).colorScheme.onSurface,
fontSize: AppConfig.messageFontSize * AppConfig.fontSizeFactor,
height: 1.3,
);
return IntrinsicWidth(
child: Material(
color: Theme.of(context).colorScheme.primaryContainer,
clipBehavior: Clip.antiAlias,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(4),
topRight: Radius.circular(AppConfig.borderRadius),
bottomLeft: Radius.circular(AppConfig.borderRadius),
bottomRight: Radius.circular(AppConfig.borderRadius),
),
),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(
AppConfig.borderRadius,
),
),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
child: RichText(
text: TextSpan(
children: [
TextSpan(
text: errorText.substring(0, start),
style: defaultStyle,
),
TextSpan(
text: errorText.substring(start, end),
style: defaultStyle.merge(
TextStyle(
backgroundColor: Colors.red.withOpacity(0.25),
decoration: TextDecoration.lineThrough,
decorationThickness: 2.5,
),
),
),
const TextSpan(text: " "),
TextSpan(
text: replacementText,
style: defaultStyle.merge(
TextStyle(
backgroundColor: Colors.green.withOpacity(0.25),
),
),
),
TextSpan(
text: errorText.substring(end),
style: defaultStyle,
),
],
),
),
),
),
);
}
}
class ConstructMessageMetadata extends StatelessWidget {
final PangeaMessageEvent msgEvent;
const ConstructMessageMetadata({
super.key,
required this.msgEvent,
});
@override
Widget build(BuildContext context) {
final String roomName = msgEvent.event.room.getLocalizedDisplayname();
return Padding(
padding: const EdgeInsets.fromLTRB(10, 0, 30, 0),
child: Column(
children: [
Text(
msgEvent.event.originServerTs.localizedTime(context),
style: TextStyle(fontSize: 13 * AppConfig.fontSizeFactor),
),
Text(roomName),
],
),
);
}
}
class MessageEventMatch {
final PangeaMessageEvent msgEvent;
final PangeaMatch lemmaMatch;
MessageEventMatch({
required this.msgEvent,
required this.lemmaMatch,
});
}

@ -1,101 +0,0 @@
// import 'dart:math';
// import 'package:fluffychat/pangea/models/analytics/chart_analytics_model.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_gen/gen_l10n/l10n.dart';
// import '../../enum/use_type.dart';
// class ListSummaryAnalytics extends StatelessWidget {
// final ChartAnalyticsModel? chartAnalytics;
// const ListSummaryAnalytics({super.key, this.chartAnalytics});
// TimeSeriesTotals? get totals => chartAnalytics?.totals;
// String spacer(int baseLength, int number) =>
// " " * max(baseLength - number.toString().length, 0);
// WidgetSpan spacerIconText(
// String toolTip,
// String space,
// IconData icon,
// int value,
// Color? color, [
// percentage = true,
// ]) =>
// WidgetSpan(
// child: Tooltip(
// message: toolTip,
// child: RichText(
// text: TextSpan(
// children: [
// TextSpan(
// text: space,
// ),
// WidgetSpan(child: Icon(icon, size: 14, color: color)),
// TextSpan(
// text: " $value${percentage ? "%" : ""}",
// style: TextStyle(color: color),
// ),
// ],
// ),
// ),
// ),
// );
// @override
// Widget build(BuildContext context) {
// if (totals == null) {
// return const LinearProgressIndicator();
// }
// final l10n = L10n.of(context);
// return RichText(
// text: TextSpan(
// children: [
// spacerIconText(
// L10n.of(context) != null
// ? L10n.of(context)!.totalMessages
// : "Total messages sent",
// "",
// Icons.chat_bubble,
// totals!.all,
// Theme.of(context).textTheme.bodyLarge!.color,
// false,
// ),
// if (totals!.all != 0) ...[
// spacerIconText(
// l10n != null ? l10n.taTooltip : "With translation assistance",
// spacer(8, totals!.all),
// UseType.ta.iconData,
// totals!.taPercent,
// UseType.ta.color(context),
// ),
// spacerIconText(
// l10n != null ? l10n.gaTooltip : "With grammar assistance",
// spacer(4, totals!.taPercent),
// UseType.ga.iconData,
// totals!.gaPercent,
// UseType.ga.color(context),
// ),
// spacerIconText(
// l10n != null ? l10n.waTooltip : "Without assistance",
// spacer(4, totals!.gaPercent),
// UseType.wa.iconData,
// totals!.waPercent,
// UseType.wa.color(context),
// ),
// spacerIconText(
// l10n != null ? l10n.unTooltip : "Other",
// spacer(4, totals!.waPercent),
// UseType.un.iconData,
// totals!.unPercent,
// UseType.un.color(context),
// ),
// ],
// ],
// ),
// );
// }
// }

@ -20,7 +20,7 @@ void pLogoutAction(BuildContext context, {bool? isDestructiveAction}) async {
final matrix = Matrix.of(context);
// before wiping out locally cached construct data, save it to the server
await MatrixState.pangeaController.myAnalytics
await MatrixState.pangeaController.putAnalytics
.sendLocalAnalyticsToAnalyticsRoom(onLogout: true);
await showFutureLoadingDialog(

@ -1,7 +1,7 @@
import 'dart:async';
import 'package:fluffychat/pangea/controllers/get_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
import 'package:fluffychat/pangea/utils/bot_style.dart';
import 'package:fluffychat/widgets/matrix.dart';
import 'package:flutter/material.dart';
@ -29,8 +29,8 @@ class PointsGainedAnimationState extends State<PointsGainedAnimation>
late Animation<double> _fadeAnimation;
StreamSubscription? _pointsSubscription;
int? get _prevXP => MatrixState.pangeaController.analytics.prevXP;
int? get _currentXP => MatrixState.pangeaController.analytics.currentXP;
int? get _prevXP => MatrixState.pangeaController.getAnalytics.prevXP;
int? get _currentXP => MatrixState.pangeaController.getAnalytics.currentXP;
int? _addedPoints;
@override
@ -62,7 +62,7 @@ class PointsGainedAnimationState extends State<PointsGainedAnimation>
);
_pointsSubscription = MatrixState
.pangeaController.analytics.analyticsStream.stream
.pangeaController.getAnalytics.analyticsStream.stream
.listen(_showPointsGained);
}

@ -54,21 +54,21 @@ class LearningProgressIndicatorsState
// int get totalXP => _pangeaController.analytics.currentXP;
// int get level => _pangeaController.analytics.level;
List<OneConstructUse> currentConstructs = [];
int get currentXP => _pangeaController.analytics.calcXP(currentConstructs);
int get localXP => _pangeaController.analytics.calcXP(
_pangeaController.analytics.locallyCachedConstructs,
int get currentXP => _pangeaController.getAnalytics.calcXP(currentConstructs);
int get localXP => _pangeaController.getAnalytics.calcXP(
_pangeaController.getAnalytics.locallyCachedConstructs,
);
int get serverXP => currentXP - localXP;
int get level => _pangeaController.analytics.level;
int get level => _pangeaController.getAnalytics.level;
@override
void initState() {
super.initState();
updateAnalyticsData(
_pangeaController.analytics.analyticsStream.value?.constructs ?? [],
_pangeaController.getAnalytics.analyticsStream.value?.constructs ?? [],
);
_analyticsUpdateSubscription = _pangeaController
.analytics.analyticsStream.stream
.getAnalytics.analyticsStream.stream
.listen((update) => updateAnalyticsData(update.constructs));
}
@ -146,12 +146,12 @@ class LearningProgressIndicatorsState
? const Color.fromARGB(255, 0, 190, 83)
: Theme.of(context).colorScheme.primary,
currentPoints: currentXP,
widthMultiplier: _pangeaController.analytics.levelProgress,
widthMultiplier: _pangeaController.getAnalytics.levelProgress,
),
LevelBarDetails(
fillColor: Theme.of(context).colorScheme.primary,
currentPoints: serverXP,
widthMultiplier: _pangeaController.analytics.serverLevelProgress,
widthMultiplier: _pangeaController.getAnalytics.serverLevelProgress,
),
],
);

@ -1,7 +1,7 @@
import 'dart:developer';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart';
import 'package:fluffychat/pangea/enum/span_data_type.dart';
import 'package:fluffychat/pangea/models/pangea_token_model.dart';
@ -125,7 +125,7 @@ class SpanCardState extends State<SpanCard> {
selectedChoiceIndex = index;
if (selectedChoice != null) {
if (!selectedChoice!.selected) {
MatrixState.pangeaController.myAnalytics.addDraftUses(
MatrixState.pangeaController.putAnalytics.addDraftUses(
selectedChoice!.tokens,
widget.roomId,
selectedChoice!.isBestCorrection
@ -158,7 +158,7 @@ class SpanCardState extends State<SpanCard> {
/// Adds the ignored tokens to locally cached analytics
void addIgnoredTokenUses() {
MatrixState.pangeaController.myAnalytics.addDraftUses(
MatrixState.pangeaController.putAnalytics.addDraftUses(
ignoredTokens ?? [],
widget.roomId,
ConstructUseTypeEnum.ignIGC,

@ -2,7 +2,7 @@ import 'dart:developer';
import 'package:collection/collection.dart';
import 'package:fluffychat/pangea/choreographer/widgets/choice_array.dart';
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
@ -73,7 +73,7 @@ class MultipleChoiceActivityState extends State<MultipleChoiceActivity> {
return;
}
MatrixState.pangeaController.myAnalytics.setState(
MatrixState.pangeaController.putAnalytics.setState(
AnalyticsStream(
// note - this maybe should be the activity event id
eventId:

@ -1,9 +1,9 @@
import 'dart:async';
import 'dart:developer';
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/pangea_controller.dart';
import 'package:fluffychat/pangea/controllers/practice_activity_generation_controller.dart';
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
import 'package:fluffychat/pangea/enum/activity_type_enum.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart';
import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart';

@ -26,7 +26,7 @@ class TargetTokensController {
_targetTokens = await _initialize(pangeaMessageEvent);
final allConstructs = MatrixState
.pangeaController.analytics.analyticsStream.value?.constructs;
.pangeaController.getAnalytics.analyticsStream.value?.constructs;
await updateTokensWithConstructs(
allConstructs ?? [],
pangeaMessageEvent,

@ -1,7 +1,7 @@
import 'dart:developer';
import 'package:fluffychat/config/app_config.dart';
import 'package:fluffychat/pangea/controllers/my_analytics_controller.dart';
import 'package:fluffychat/pangea/controllers/put_analytics_controller.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/multiple_choice_activity_model.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart';
import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart';
@ -69,7 +69,7 @@ class WordFocusListeningActivityState
return;
}
MatrixState.pangeaController.myAnalytics.setState(
MatrixState.pangeaController.putAnalytics.setState(
AnalyticsStream(
// note - this maybe should be the activity event id
eventId:

@ -93,7 +93,12 @@ Future<void> pLanguageDialog(
context: context,
future: () async {
try {
pangeaController.myAnalytics
//@ggurdin while this is obviously working, it feels pretty hidden
//and could lead to errors if someone where to change the user L2 via some
// other means. with analytics being dependent on languages, it probably
// would make sense for analytics to listen to the language stateStream
// and update in this case
pangeaController.putAnalytics
.sendLocalAnalyticsToAnalyticsRoom()
.then((_) {
pangeaController.userController.updateProfile(
@ -109,11 +114,11 @@ Future<void> pLanguageDialog(
}).then((_) {
// if the profile update is successful, reset cached analytics
// data, since analytics data corresponds to the user's L2
pangeaController.myAnalytics.dispose();
pangeaController.analytics.dispose();
pangeaController.putAnalytics.dispose();
pangeaController.getAnalytics.dispose();
pangeaController.myAnalytics.initialize();
pangeaController.analytics.initialize();
pangeaController.putAnalytics.initialize();
pangeaController.getAnalytics.initialize();
Navigator.pop(context);
});

Loading…
Cancel
Save