From ffbc62ba555a1faa378a00268da5072a527c33a4 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Fri, 28 Jun 2024 11:34:55 -0400 Subject: [PATCH 1/7] consolidating language constants and using for unknown detection instance --- lib/pages/chat/chat_input_row.dart | 2 +- .../controllers/choreographer.dart | 120 ++++++++++-------- .../controllers/it_controller.dart | 3 - .../controllers/message_options.dart | 2 +- lib/pangea/constants/keys.dart | 4 - lib/pangea/constants/language_constants.dart | 24 ++++ lib/pangea/constants/language_keys.dart | 6 - lib/pangea/constants/language_level_type.dart | 3 - lib/pangea/constants/language_list_keys.dart | 4 - .../controllers/language_controller.dart | 2 +- .../language_detection_controller.dart | 18 ++- .../controllers/language_list_controller.dart | 3 +- .../controllers/my_analytics_controller.dart | 2 +- lib/pangea/controllers/user_controller.dart | 2 +- .../controllers/word_net_controller.dart | 2 +- .../pangea_room_extension.dart | 2 +- .../pangea_message_event.dart | 2 +- .../pangea_representation_event.dart | 2 +- lib/pangea/models/language_model.dart | 2 +- lib/pangea/models/space_model.dart | 2 +- lib/pangea/models/user_model.dart | 2 +- .../student_analytics/student_analytics.dart | 2 +- .../utils/get_chat_list_item_subtitle.dart | 2 +- lib/pangea/widgets/igc/word_data_card.dart | 2 +- .../space/language_level_dropdown.dart | 2 +- .../user_settings/p_language_dialog.dart | 2 +- lib/utils/background_push.dart | 2 +- 27 files changed, 116 insertions(+), 105 deletions(-) delete mode 100644 lib/pangea/constants/keys.dart create mode 100644 lib/pangea/constants/language_constants.dart delete mode 100644 lib/pangea/constants/language_keys.dart delete mode 100644 lib/pangea/constants/language_level_type.dart delete mode 100644 lib/pangea/constants/language_list_keys.dart diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 6bf19a850e..0577d6094a 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -3,7 +3,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index a0c6a5f6bc..9a7f384281 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -5,13 +5,12 @@ import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/choreographer/controllers/alternative_translator.dart'; import 'package:fluffychat/pangea/choreographer/controllers/igc_controller.dart'; import 'package:fluffychat/pangea/choreographer/controllers/message_options.dart'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/enum/assistance_state_enum.dart'; import 'package:fluffychat/pangea/enum/edit_type.dart'; import 'package:fluffychat/pangea/models/it_step.dart'; -import 'package:fluffychat/pangea/models/language_detection_model.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/models/space_model.dart'; import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; @@ -94,63 +93,67 @@ class Choreographer { } Future _sendWithIGC(BuildContext context) async { - if (igc.canSendMessage) { - final PangeaRepresentation? originalWritten = - choreoRecord.includedIT && itController.sourceText != null - ? PangeaRepresentation( - langCode: l1LangCode ?? LanguageKeys.unknownLanguage, - text: itController.sourceText!, - originalWritten: true, - originalSent: false, - ) - : null; - - final PangeaRepresentation originalSent = PangeaRepresentation( - langCode: langCodeOfCurrentText ?? LanguageKeys.unknownLanguage, - text: currentText, - originalSent: true, - originalWritten: originalWritten == null, - ); - final ChoreoRecord? applicableChoreo = - isITandIGCEnabled && igc.igcTextData != null ? choreoRecord : null; - - // if the message has not been processed to determine its language - // then run it through the language detection endpoint. If the detection - // confidence is high enough, use that language code as the message's language - // to save that pangea representation - if (applicableChoreo == null) { - final resp = await pangeaController.languageDetection.detectLanguage( + if (!igc.canSendMessage) { + igc.showFirstMatch(context); + return; + } + + final PangeaRepresentation? originalWritten = + choreoRecord.includedIT && itController.sourceText != null + ? PangeaRepresentation( + langCode: l1LangCode ?? LanguageKeys.unknownLanguage, + text: itController.sourceText!, + originalWritten: true, + originalSent: false, + ) + : null; + //TODO - confirm that IT is indeed making sure the message is in the user's L1 + + // if the message has not been processed to determine its language + // then run it through the language detection endpoint. If the detection + // confidence is high enough, use that language code as the message's language + // to save that pangea representation + // TODO - move this to somewhere such that the message can be cleared from the input field + // before the language detection is complete. Otherwise, user is going to be waiting + // in cases of slow internet or slow language detection + final String originalSentLangCode = langCodeOfCurrentText ?? + (await pangeaController.languageDetection.detectLanguage( currentText, pangeaController.languageController.userL2?.langCode, pangeaController.languageController.userL1?.langCode, - ); - final LanguageDetection? bestDetection = resp.bestDetection(); - if (bestDetection != null) { - originalSent.langCode = bestDetection.langCode; - } - } + )) + .bestDetection() + .langCode; + + final PangeaRepresentation originalSent = PangeaRepresentation( + langCode: originalSentLangCode, + text: currentText, + originalSent: true, + originalWritten: originalWritten == null, + ); - final UseType useType = useTypeCalculator(applicableChoreo); - debugPrint("use type in choreographer $useType"); - - chatController.send( - // PTODO - turn this back on in conjunction with saving tokens - // we need to save those tokens as well, in order for exchanges to work - // properly. in an exchange, the other user will want - // originalWritten: originalWritten, - originalSent: originalSent, - tokensSent: igc.igcTextData?.tokens != null - ? PangeaMessageTokens(tokens: igc.igcTextData!.tokens) - : null, - //TODO - save originalwritten tokens - choreo: applicableChoreo, - useType: useType, - ); + // TODO - why does both it and igc need to be enabled for choreo to be applicable? + final ChoreoRecord? applicableChoreo = + isITandIGCEnabled && igc.igcTextData != null ? choreoRecord : null; + + final UseType useType = useTypeCalculator(applicableChoreo); + debugPrint("use type in choreographer $useType"); + + chatController.send( + // PTODO - turn this back on in conjunction with saving tokens + // we need to save those tokens as well, in order for exchanges to work + // properly. in an exchange, the other user will want + // originalWritten: originalWritten, + originalSent: originalSent, + tokensSent: igc.igcTextData?.tokens != null + ? PangeaMessageTokens(tokens: igc.igcTextData!.tokens) + : null, + //TODO - save originalwritten tokens + choreo: applicableChoreo, + useType: useType, + ); - clear(); - } else { - igc.showFirstMatch(context); - } + clear(); } _resetDebounceTimer() { @@ -481,10 +484,17 @@ class Choreographer { bool get editTypeIsKeyboard => EditType.keyboard == _textController.editType; + /// If there is applicable igcTextData, return the detected langCode + /// Otherwise, if the IT controller is open, return the user's L2 langCode + /// This second piece assumes that IT is being used to translate into the user's L2 + /// and could be spotty. It's a bit of a hack, and should be tested more. String? get langCodeOfCurrentText { if (igc.detectedLangCode != null) return igc.detectedLangCode!; - if (itController.isOpen) return l2LangCode!; + // TODO - this is a bit of a hack, and should be tested more + // we should also check that user has not done customInput + if (itController.completedITSteps.isNotEmpty && itController.allCorrect) + return l2LangCode!; return null; } diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart index 632db7e9ba..a79f2d6fad 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -340,9 +340,6 @@ class ITController { bool get isLoading => choreographer.isFetching; - bool get correctChoicesSelected => - completedITSteps.every((ITStep step) => step.isCorrect); - String latestChoiceFeedback(BuildContext context) => completedITSteps.isNotEmpty ? completedITSteps.last.choiceFeedback(context) diff --git a/lib/pangea/choreographer/controllers/message_options.dart b/lib/pangea/choreographer/controllers/message_options.dart index 0e5e684617..96df5d9213 100644 --- a/lib/pangea/choreographer/controllers/message_options.dart +++ b/lib/pangea/choreographer/controllers/message_options.dart @@ -1,7 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; diff --git a/lib/pangea/constants/keys.dart b/lib/pangea/constants/keys.dart deleted file mode 100644 index 092b1b0a9e..0000000000 --- a/lib/pangea/constants/keys.dart +++ /dev/null @@ -1,4 +0,0 @@ -class PrefKey { - static const lastFetched = 'LAST_FETCHED'; - static const flags = 'flags'; -} diff --git a/lib/pangea/constants/language_constants.dart b/lib/pangea/constants/language_constants.dart new file mode 100644 index 0000000000..73137a3008 --- /dev/null +++ b/lib/pangea/constants/language_constants.dart @@ -0,0 +1,24 @@ +import 'package:fluffychat/pangea/models/language_detection_model.dart'; + +class LanguageKeys { + static const unknownLanguage = "unk"; + static const mixedLanguage = "mixed"; + static const defaultLanguage = "en"; + static const multiLanguage = "multi"; +} + +class LanguageLevelType { + static List get allInts => [0, 1, 2, 3, 4, 5, 6]; +} + +class PrefKey { + static const lastFetched = 'p_lang_lastfetched'; + static const flags = 'p_lang_flag'; +} + +final LanguageDetection unknownLanguageDetection = LanguageDetection( + langCode: LanguageKeys.unknownLanguage, + confidence: 0.5, +); + +const double languageDetectionConfidenceThreshold = 0.95; diff --git a/lib/pangea/constants/language_keys.dart b/lib/pangea/constants/language_keys.dart deleted file mode 100644 index cfe0c96c05..0000000000 --- a/lib/pangea/constants/language_keys.dart +++ /dev/null @@ -1,6 +0,0 @@ -class LanguageKeys { - static const unknownLanguage = "unk"; - static const mixedLanguage = "mixed"; - static const defaultLanguage = "en"; - static const multiLanguage = "multi"; -} diff --git a/lib/pangea/constants/language_level_type.dart b/lib/pangea/constants/language_level_type.dart deleted file mode 100644 index 49136ca774..0000000000 --- a/lib/pangea/constants/language_level_type.dart +++ /dev/null @@ -1,3 +0,0 @@ -class LanguageLevelType { - static List get allInts => [0, 1, 2, 3, 4, 5, 6]; -} diff --git a/lib/pangea/constants/language_list_keys.dart b/lib/pangea/constants/language_list_keys.dart deleted file mode 100644 index 7fff924a9f..0000000000 --- a/lib/pangea/constants/language_list_keys.dart +++ /dev/null @@ -1,4 +0,0 @@ -class PrefKey { - static const lastFetched = 'p_lang_lastfetched'; - static const flags = 'p_lang_flag'; -} diff --git a/lib/pangea/controllers/language_controller.dart b/lib/pangea/controllers/language_controller.dart index c906e6b4e4..b1bc023806 100644 --- a/lib/pangea/controllers/language_controller.dart +++ b/lib/pangea/controllers/language_controller.dart @@ -1,6 +1,6 @@ import 'dart:developer'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; diff --git a/lib/pangea/controllers/language_detection_controller.dart b/lib/pangea/controllers/language_detection_controller.dart index 4ddfabc88a..08f38ea2c9 100644 --- a/lib/pangea/controllers/language_detection_controller.dart +++ b/lib/pangea/controllers/language_detection_controller.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/models/language_detection_model.dart'; import 'package:fluffychat/pangea/network/urls.dart'; @@ -75,19 +76,16 @@ class LanguageDetectionResponse { }; } - LanguageDetection? get _bestDetection { + LanguageDetection get _bestDetection { detections.sort((a, b) => b.confidence.compareTo(a.confidence)); - return detections.isNotEmpty ? detections.first : null; + return detections.firstOrNull ?? unknownLanguageDetection; } - final double _confidenceThreshold = 0.95; - - LanguageDetection? bestDetection({double? threshold}) { - threshold ??= _confidenceThreshold; - return (_bestDetection?.confidence ?? 0) >= _confidenceThreshold - ? _bestDetection! - : null; - } + LanguageDetection bestDetection({double? threshold}) => + _bestDetection.confidence >= + (threshold ?? languageDetectionConfidenceThreshold) + ? _bestDetection + : unknownLanguageDetection; } class _LanguageDetectionCacheItem { diff --git a/lib/pangea/controllers/language_list_controller.dart b/lib/pangea/controllers/language_list_controller.dart index 59c3cce88a..31e4513fa1 100644 --- a/lib/pangea/controllers/language_list_controller.dart +++ b/lib/pangea/controllers/language_list_controller.dart @@ -1,13 +1,12 @@ import 'dart:async'; import 'dart:developer'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/repo/language_repo.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; -import '../constants/language_list_keys.dart'; import '../utils/shared_prefs.dart'; class PangeaLanguage { diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 614fcf9db4..99fdb14c78 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:developer'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/base_controller.dart'; diff --git a/lib/pangea/controllers/user_controller.dart b/lib/pangea/controllers/user_controller.dart index c7a35b3e86..390f17b9df 100644 --- a/lib/pangea/controllers/user_controller.dart +++ b/lib/pangea/controllers/user_controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:collection/collection.dart'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/controllers/base_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; diff --git a/lib/pangea/controllers/word_net_controller.dart b/lib/pangea/controllers/word_net_controller.dart index ce8324ebef..a67941465a 100644 --- a/lib/pangea/controllers/word_net_controller.dart +++ b/lib/pangea/controllers/word_net_controller.dart @@ -1,7 +1,7 @@ import 'package:collection/collection.dart'; import 'package:http/http.dart' as http; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/repo/word_repo.dart'; import '../models/word_data_model.dart'; import 'base_controller.dart'; diff --git a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart index b8dae7ab3a..298a519adf 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -4,7 +4,7 @@ import 'dart:developer'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 951b77dfc0..4ead9982cb 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -22,7 +22,7 @@ import 'package:matrix/matrix.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import '../../widgets/matrix.dart'; -import '../constants/language_keys.dart'; +import '../constants/language_constants.dart'; import '../constants/pangea_event_types.dart'; import '../enum/use_type.dart'; import '../utils/error_handler.dart'; diff --git a/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart b/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart index e774dc4f25..e6e45b7564 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart @@ -12,7 +12,7 @@ import 'package:matrix/src/utils/markdown.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import '../../widgets/matrix.dart'; -import '../constants/language_keys.dart'; +import '../constants/language_constants.dart'; import '../constants/pangea_event_types.dart'; import '../models/choreo_record.dart'; import '../models/representation_content_model.dart'; diff --git a/lib/pangea/models/language_model.dart b/lib/pangea/models/language_model.dart index ae960b2126..be3cc71bae 100644 --- a/lib/pangea/models/language_model.dart +++ b/lib/pangea/models/language_model.dart @@ -1,6 +1,6 @@ import 'dart:developer'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; diff --git a/lib/pangea/models/space_model.dart b/lib/pangea/models/space_model.dart index f17507fa8b..946da6dc75 100644 --- a/lib/pangea/models/space_model.dart +++ b/lib/pangea/models/space_model.dart @@ -7,7 +7,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import '../constants/class_default_values.dart'; -import '../constants/language_keys.dart'; +import '../constants/language_constants.dart'; import '../constants/pangea_event_types.dart'; import 'language_model.dart'; diff --git a/lib/pangea/models/user_model.dart b/lib/pangea/models/user_model.dart index f8e929a13b..10b7ae747f 100644 --- a/lib/pangea/models/user_model.dart +++ b/lib/pangea/models/user_model.dart @@ -9,7 +9,7 @@ import 'package:fluffychat/pangea/utils/instructions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import '../constants/language_keys.dart'; +import '../constants/language_constants.dart'; import 'language_model.dart'; PUserModel pUserModelFromJson(String str) => diff --git a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart index d6bc9d7667..a345243ff7 100644 --- a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart +++ b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:developer'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart'; import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; diff --git a/lib/pangea/utils/get_chat_list_item_subtitle.dart b/lib/pangea/utils/get_chat_list_item_subtitle.dart index 5adc0040cd..bd8d064f51 100644 --- a/lib/pangea/utils/get_chat_list_item_subtitle.dart +++ b/lib/pangea/utils/get_chat_list_item_subtitle.dart @@ -1,5 +1,5 @@ import 'package:collection/collection.dart'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; diff --git a/lib/pangea/widgets/igc/word_data_card.dart b/lib/pangea/widgets/igc/word_data_card.dart index 5a785c795a..1d789424d9 100644 --- a/lib/pangea/widgets/igc/word_data_card.dart +++ b/lib/pangea/widgets/igc/word_data_card.dart @@ -1,6 +1,6 @@ import 'dart:developer'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/controllers/contextual_definition_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; diff --git a/lib/pangea/widgets/space/language_level_dropdown.dart b/lib/pangea/widgets/space/language_level_dropdown.dart index 3b2678e293..aeb1cfd365 100644 --- a/lib/pangea/widgets/space/language_level_dropdown.dart +++ b/lib/pangea/widgets/space/language_level_dropdown.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/pangea/constants/language_level_type.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/utils/language_level_copy.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; diff --git a/lib/pangea/widgets/user_settings/p_language_dialog.dart b/lib/pangea/widgets/user_settings/p_language_dialog.dart index 51082a7e66..8b7ae33b55 100644 --- a/lib/pangea/widgets/user_settings/p_language_dialog.dart +++ b/lib/pangea/widgets/user_settings/p_language_dialog.dart @@ -1,6 +1,6 @@ import 'dart:developer'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index 00ebd3cb5e..093b78f818 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -24,7 +24,7 @@ import 'dart:io'; import 'package:fcm_shared_isolate/fcm_shared_isolate.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/utils/push_helper.dart'; import 'package:fluffychat/widgets/fluffy_chat_app.dart'; From 8ceb7851e5923ec7455f2930507d53065e071e05 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Fri, 28 Jun 2024 11:36:10 -0400 Subject: [PATCH 2/7] refactoring of my analytics controller and related flows --- assets/l10n/intl_en.arb | 2 +- assets/l10n/intl_es.arb | 2 +- lib/pages/chat_list/chat_list.dart | 2 +- .../controllers/my_analytics_controller.dart | 298 +++++++++--------- .../client_extension/client_extension.dart | 4 +- .../client_extension/space_extension.dart | 12 + .../events_extension.dart | 34 +- .../pangea_room_extension.dart | 1 + .../room_analytics_extension.dart | 34 +- .../models/analytics/analytics_event.dart | 30 -- .../models/analytics/constructs_event.dart | 15 - .../analytics/summary_analytics_event.dart | 14 - .../practice_activity_record_model.dart | 22 +- .../student_analytics/student_analytics.dart | 35 +- 14 files changed, 238 insertions(+), 267 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index f0aba02195..986cb9cb02 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3109,7 +3109,7 @@ "prettyGood": "Pretty good! Here's what I would have said.", "letMeThink": "Hmm, let's see how you did!", "clickMessageTitle": "Need help?", - "clickMessageBody": "Click messages to access definitions, translations, and audio!", + "clickMessageBody": "Click a message for language help! Click and hold to react 😀.", "understandingMessagesTitle": "Definitions and translations!", "understandingMessagesBody": "Click underlined words for definitions. Translate with message options (upper right).", "allDone": "All done!", diff --git a/assets/l10n/intl_es.arb b/assets/l10n/intl_es.arb index dc328294a3..e771c86a3a 100644 --- a/assets/l10n/intl_es.arb +++ b/assets/l10n/intl_es.arb @@ -4512,7 +4512,7 @@ "definitions": "definiciones", "subscribedToUnlockTools": "Suscríbase para desbloquear herramientas lingüísticas, como", "clickMessageTitle": "¿Necesitas ayuda?", - "clickMessageBody": "Haga clic en los mensajes para acceder a las definiciones, traducciones y audio.", + "clickMessageBody": "¡Lame un mensaje para obtener ayuda con el idioma! Haz clic y mantén presionado para reaccionar 😀", "more": "Más", "translationTooltip": "Traducir", "audioTooltip": "Reproducir audio", diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 569eba8808..89f31fd68d 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -917,7 +917,7 @@ class ChatListController extends State if (mounted) { GoogleAnalytics.analyticsUserUpdate(client.userID); await pangeaController.subscriptionController.initialize(); - await pangeaController.myAnalytics.addEventsListener(); + await pangeaController.myAnalytics.initialize(); pangeaController.afterSyncAndFirstLoginInitialization(context); await pangeaController.inviteBotToExistingSpaces(); await pangeaController.setPangeaPushRules(); diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 99fdb14c78..82b7af3c74 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -1,16 +1,17 @@ import 'dart:async'; import 'dart:developer'; +<<<<<<< Updated upstream import 'package:fluffychat/pangea/constants/language_constants.dart'; +======= +>>>>>>> Stashed changes import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; -import 'package:fluffychat/pangea/controllers/base_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; -import 'package:fluffychat/pangea/models/analytics/constructs_event.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; -import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart'; import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; @@ -18,11 +19,18 @@ import 'package:matrix/matrix.dart'; import '../extensions/client_extension/client_extension.dart'; import '../extensions/pangea_room_extension/pangea_room_extension.dart'; -// controls the sending of analytics events -class MyAnalyticsController extends BaseController { +/// 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 { late PangeaController _pangeaController; Timer? _updateTimer; + + /// the max number of messages that will be cached before + /// an automatic update is triggered final int _maxMessagesCached = 10; + + /// the number of minutes before an automatic update is triggered final int _minutesBeforeUpdate = 5; /// the time since the last update that will trigger an automatic update @@ -33,41 +41,50 @@ class MyAnalyticsController extends BaseController { } /// adds the listener that handles when to run automatic updates - /// to analytics - either after a certain number of messages sent/ + /// to analytics - either after a certain number of messages sent /// received or after a certain amount of time [_timeSinceUpdate] without an update - Future addEventsListener() async { - final Client client = _pangeaController.matrixState.client; + Future initialize() async { + final lastUpdated = await _refreshAnalyticsIfOutdated(); + + // listen for new messages and updateAnalytics timer + // we are doing this in an attempt to update analytics when activitiy is low + // both in messages sent by this client and other clients that you're connected with + // doesn't account for messages sent by other clients that you're not connected with + _client.onSync.stream + .where((SyncUpdate update) => update.rooms?.join != null) + .listen((update) { + updateAnalyticsTimer(update, lastUpdated); + }); + } - // if analytics haven't been updated in the last day, update them + /// If analytics haven't been updated in the last day, update them + Future _refreshAnalyticsIfOutdated() async { DateTime? lastUpdated = await _pangeaController.analytics .myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics); final DateTime yesterday = DateTime.now().subtract(_timeSinceUpdate); + if (lastUpdated?.isBefore(yesterday) ?? true) { debugPrint("analytics out-of-date, updating"); await updateAnalytics(); lastUpdated = await _pangeaController.analytics .myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics); } - - client.onSync.stream - .where((SyncUpdate update) => update.rooms?.join != null) - .listen((update) { - updateAnalyticsTimer(update, lastUpdated); - }); + return lastUpdated; } - /// given an update from sync stream, check if the update contains + Client get _client => _pangeaController.matrixState.client; + + /// Given an update from sync stream, check if the update contains /// messages for which analytics will be saved. If so, reset the timer /// and add the event ID to the cache of un-added event IDs void updateAnalyticsTimer(SyncUpdate update, DateTime? lastUpdated) { for (final entry in update.rooms!.join!.entries) { - final Room room = - _pangeaController.matrixState.client.getRoomById(entry.key)!; + final Room room = _client.getRoomById(entry.key)!; // get the new events in this sync that are messages final List? events = entry.value.timeline?.events ?.map((event) => Event.fromMatrixEvent(event, room)) - .where((event) => eventHasAnalytics(event, lastUpdated)) + .where((event) => hasUserAnalyticsToCache(event, lastUpdated)) .toList(); // add their event IDs to the cache of un-added event IDs @@ -87,8 +104,9 @@ class MyAnalyticsController extends BaseController { } // checks if event from sync update is a message that should have analytics - bool eventHasAnalytics(Event event, DateTime? lastUpdated) { - return (lastUpdated == null || event.originServerTs.isAfter(lastUpdated)) && + bool hasUserAnalyticsToCache(Event event, DateTime? lastUpdated) { + return event.senderId == _client.userID && + (lastUpdated == null || event.originServerTs.isAfter(lastUpdated)) && event.type == EventTypes.Message && event.messageType == MessageTypes.Text && !(event.eventId.contains("web") && @@ -176,21 +194,18 @@ class MyAnalyticsController extends BaseController { } } + String? get userL2 => _pangeaController.languageController.activeL2Code(); + // top level analytics sending function. Send analytics // for each type of analytics event // to each of the applicable analytics rooms Future _updateAnalytics() async { - // if the user's l2 is not sent, don't send analytics - final String? userL2 = _pangeaController.languageController.activeL2Code(); - if (userL2 == null) { + // if missing important info, don't send analytics + if (userL2 == null || _client.userID == null) { + debugger(when: kDebugMode); return; } - // fetch a list of all the chats that the user is studying - // and a list of all the spaces in which the user is studying - await setStudentChats(); - await setStudentSpaces(); - // get the last updated time for each analytics room // and the least recent update, which will be used to determine // how far to go back in the chat history to get messages @@ -217,151 +232,128 @@ class MyAnalyticsController extends BaseController { lastUpdates.isNotEmpty ? lastUpdates.first : null; } - // for each chat the user is studying in, get all the messages - // since the least recent update analytics update, and sort them - // by their langCodes - final Map> langCodeToMsgs = - await getLangCodesToMsgs( - userL2, + final List chats = await _client.chatsImAStudentIn; + + final List recentMsgs = + await _getMessagesWithUnsavedAnalytics( l2AnalyticsLastUpdated, + chats, + ); + + final List recentActivities = + await getRecentActivities(userL2!, l2AnalyticsLastUpdated, chats); + + // FOR DISCUSSION: + // we want to make sure we save something for every message send + // however, we're currently saving analytics for messages not in the userL2 + // based on bad language detection results. maybe it would be better to + // save the analytics for these messages in the userL2 analytics room, but + // with useType of unknown + + final Room analyticsRoom = await _client.getMyAnalyticsRoom(userL2!); + + // if there is no analytics room for this langCode, then user hadn't sent + // message in this language at the time of the last analytics update + // so fallback to the least recent update time + final DateTime? lastUpdated = + lastUpdatedMap[analyticsRoom.id] ?? l2AnalyticsLastUpdated; + + // final String msgLangCode = (msg.originalSent?.langCode != null && + // msg.originalSent?.langCode != LanguageKeys.unknownLanguage) + // ? msg.originalSent!.langCode + // : userL2; + + // finally, send the analytics events to the analytics room + await _sendAnalyticsEvents( + analyticsRoom, + recentMsgs, + lastUpdated, + recentActivities, ); + } - final List langCodes = langCodeToMsgs.keys.toList(); - for (final String langCode in langCodes) { - // for each of the langs that the user has sent message in, get - // the corresponding analytics room (or create it) - final Room analyticsRoom = await _pangeaController.matrixState.client - .getMyAnalyticsRoom(langCode); - - // if there is no analytics room for this langCode, then user hadn't sent - // message in this language at the time of the last analytics update - // so fallback to the least recent update time - final DateTime? lastUpdated = - lastUpdatedMap[analyticsRoom.id] ?? l2AnalyticsLastUpdated; - - // get the corresponding list of recent messages for this langCode - final List recentMsgs = - langCodeToMsgs[langCode] ?? []; - - // finally, send the analytics events to the analytics room - await sendAnalyticsEvents( - analyticsRoom, - recentMsgs, - lastUpdated, + Future> getRecentActivities( + String userL2, + DateTime? lastUpdated, + List chats, + ) async { + final List>> recentActivityFutures = []; + for (final Room chat in chats) { + recentActivityFutures.add( + chat.getEventsBySender( + type: PangeaEventTypes.activityRecord, + sender: _client.userID!, + since: lastUpdated, + ), ); } + final List> recentActivityLists = + await Future.wait(recentActivityFutures); + + return recentActivityLists + .expand((e) => e) + .map((e) => ActivityRecordResponse.fromJson(e.content)) + .toList(); } - Future>> getLangCodesToMsgs( - String userL2, + /// Returns the new messages that have not yet been saved to analytics. + /// The keys in the map correspond to different categories or groups of messages, + /// while the values are lists of [PangeaMessageEvent] objects belonging to each category. + Future> _getMessagesWithUnsavedAnalytics( DateTime? since, + List chats, ) async { - // get a map of langCodes to messages for each chat the user is studying in - final Map> langCodeToMsgs = {}; - for (final Room chat in _studentChats) { - List? recentMsgs; - try { - recentMsgs = await chat.myMessageEventsInChat( + // get the recent messages for each chat + final List>> futures = []; + for (final Room chat in chats) { + futures.add( + chat.myMessageEventsInChat( since: since, - ); - } catch (err) { - debugPrint("failed to fetch messages for chat ${chat.id}"); - continue; - } - - // sort those messages by their langCode - // langCode is hopefully based on the original sent rep, but if that - // is null or unk, it will be based on the user's current l2 - for (final msg in recentMsgs) { - final String msgLangCode = (msg.originalSent?.langCode != null && - msg.originalSent?.langCode != LanguageKeys.unknownLanguage) - ? msg.originalSent!.langCode - : userL2; - langCodeToMsgs[msgLangCode] ??= []; - langCodeToMsgs[msgLangCode]!.add(msg); - } + ), + ); } - return langCodeToMsgs; + final List> recentMsgLists = + await Future.wait(futures); + + // flatten the list of lists of messages + return recentMsgLists.expand((e) => e).toList(); } - Future sendAnalyticsEvents( + Future _sendAnalyticsEvents( Room analyticsRoom, List recentMsgs, DateTime? lastUpdated, + List recentActivities, ) async { - // remove messages that were sent before the last update - if (recentMsgs.isEmpty) return; - if (lastUpdated != null) { - recentMsgs.removeWhere( - (msg) => msg.event.originServerTs.isBefore(lastUpdated), - ); - } + final List constructContent = []; + + if (recentMsgs.isNotEmpty) { + // remove messages that were sent before the last update + + // format the analytics data + final List summaryContent = + SummaryAnalyticsModel.formatSummaryContent(recentMsgs); + // if there's new content to be sent, or if lastUpdated hasn't been + // set yet for this room, send the analytics events + if (summaryContent.isNotEmpty || lastUpdated == null) { + await analyticsRoom.sendSummaryAnalyticsEvent( + summaryContent, + ); + } - // format the analytics data - final List summaryContent = - SummaryAnalyticsModel.formatSummaryContent(recentMsgs); - final List constructContent = - ConstructAnalyticsModel.formatConstructsContent(recentMsgs); - - // if there's new content to be sent, or if lastUpdated hasn't been - // set yet for this room, send the analytics events - if (summaryContent.isNotEmpty || lastUpdated == null) { - await SummaryAnalyticsEvent.sendSummaryAnalyticsEvent( - analyticsRoom, - summaryContent, - ); + constructContent + .addAll(ConstructAnalyticsModel.formatConstructsContent(recentMsgs)); } - if (constructContent.isNotEmpty) { - await ConstructAnalyticsEvent.sendConstructsEvent( - analyticsRoom, - constructContent, - ); + if (recentActivities.isNotEmpty) { + // TODO - Concert recentActivities into list of constructUse objects. + // First, We need to get related practiceActivityEvent from timeline in order to get its related constructs. Alternatively we + // could search for completed practice activities and see which have been completed by the user. + // It's not clear which is the best approach at the moment and we should consider both. } - } - List _studentChats = []; - - Future setStudentChats() async { - final List teacherRoomIds = - await _pangeaController.matrixState.client.teacherRoomIds; - _studentChats = _pangeaController.matrixState.client.rooms - .where( - (r) => - !r.isSpace && - !r.isAnalyticsRoom && - !teacherRoomIds.contains(r.id), - ) - .toList(); - setState(data: _studentChats); - } - - List get studentChats { - try { - if (_studentChats.isNotEmpty) return _studentChats; - setStudentChats(); - return _studentChats; - } catch (err) { - debugger(when: kDebugMode); - return []; - } - } - - List _studentSpaces = []; - - Future setStudentSpaces() async { - _studentSpaces = - await _pangeaController.matrixState.client.spacesImStudyingIn; - } - - List get studentSpaces { - try { - if (_studentSpaces.isNotEmpty) return _studentSpaces; - setStudentSpaces(); - return _studentSpaces; - } catch (err) { - debugger(when: kDebugMode); - return []; - } + await analyticsRoom.sendConstructsEvent( + constructContent, + ); } } diff --git a/lib/pangea/extensions/client_extension/client_extension.dart b/lib/pangea/extensions/client_extension/client_extension.dart index 779f8ee0a2..3dda99237a 100644 --- a/lib/pangea/extensions/client_extension/client_extension.dart +++ b/lib/pangea/extensions/client_extension/client_extension.dart @@ -51,7 +51,9 @@ extension PangeaClient on Client { Future> get spacesImTeaching async => await _spacesImTeaching; - Future> get spacesImStudyingIn async => await _spacesImStudyingIn; + Future> get chatsImAStudentIn async => await _chatsImAStudentIn; + + Future> get spaceImAStudentIn async => await _spacesImStudyingIn; List get spacesImIn => _spacesImIn; diff --git a/lib/pangea/extensions/client_extension/space_extension.dart b/lib/pangea/extensions/client_extension/space_extension.dart index 3bef46e45b..0adc704698 100644 --- a/lib/pangea/extensions/client_extension/space_extension.dart +++ b/lib/pangea/extensions/client_extension/space_extension.dart @@ -19,6 +19,18 @@ extension SpaceClientExtension on Client { return spaces; } + Future> get _chatsImAStudentIn async { + final List nowteacherRoomIds = await teacherRoomIds; + return rooms + .where( + (r) => + !r.isSpace && + !r.isAnalyticsRoom && + !nowteacherRoomIds.contains(r.id), + ) + .toList(); + } + Future> get _spacesImStudyingIn async { final List joinedSpaces = rooms .where( diff --git a/lib/pangea/extensions/pangea_room_extension/events_extension.dart b/lib/pangea/extensions/pangea_room_extension/events_extension.dart index 2d67db5b29..6cdde1ce21 100644 --- a/lib/pangea/extensions/pangea_room_extension/events_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/events_extension.dart @@ -429,21 +429,27 @@ extension EventsRoomExtension on Room { Future> myMessageEventsInChat({ DateTime? since, }) async { - final List msgEvents = await getEventsBySender( - type: EventTypes.Message, - sender: client.userID!, - since: since, - ); - final Timeline timeline = await getTimeline(); - return msgEvents - .where((event) => (event.content['msgtype'] == MessageTypes.Text)) - .map((event) { - return PangeaMessageEvent( - event: event, - timeline: timeline, - ownMessage: true, + try { + final List msgEvents = await getEventsBySender( + type: EventTypes.Message, + sender: client.userID!, + since: since, ); - }).toList(); + final Timeline timeline = await getTimeline(); + return msgEvents + .where((event) => (event.content['msgtype'] == MessageTypes.Text)) + .map((event) { + return PangeaMessageEvent( + event: event, + timeline: timeline, + ownMessage: true, + ); + }).toList(); + } catch (err, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: s); + return []; + } } // fetch event of a certain type by a certain sender diff --git a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart index 298a519adf..78ecb9cc0a 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -11,6 +11,7 @@ import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/analytics/analytics_event.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_event.dart'; +import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart'; import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart'; import 'package:fluffychat/pangea/models/bot_options_model.dart'; diff --git a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart index e3d00f8a85..04e1686128 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -99,7 +99,7 @@ extension AnalyticsRoomExtension on Room { return; } - for (final Room space in (await client.spacesImStudyingIn)) { + for (final Room space in (await client.spaceImAStudentIn)) { if (space.spaceChildren.any((sc) => sc.roomId == id)) continue; await space.addAnalyticsRoomToSpace(this); } @@ -175,7 +175,7 @@ extension AnalyticsRoomExtension on Room { return; } - for (final Room space in (await client.spacesImStudyingIn)) { + for (final Room space in (await client.spaceImAStudentIn)) { await space.inviteSpaceTeachersToAnalyticsRoom(this); } } @@ -249,4 +249,34 @@ extension AnalyticsRoomExtension on Room { return creationContent?.tryGet(ModelKey.langCode) == langCode || creationContent?.tryGet(ModelKey.oldLangCode) == langCode; } + + Future sendSummaryAnalyticsEvent( + List records, + ) async { + if (records.isEmpty) return null; + + final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel( + messages: records, + ); + final String? eventId = await sendEvent( + analyticsModel.toJson(), + type: PangeaEventTypes.summaryAnalytics, + ); + return eventId; + } + + Future sendConstructsEvent( + List uses, + ) async { + if (uses.isEmpty) return null; + final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel( + uses: uses, + ); + + final String? eventId = await sendEvent( + constructsModel.toJson(), + type: PangeaEventTypes.construct, + ); + return eventId; + } } diff --git a/lib/pangea/models/analytics/analytics_event.dart b/lib/pangea/models/analytics/analytics_event.dart index 2453e62efa..7010d3591b 100644 --- a/lib/pangea/models/analytics/analytics_event.dart +++ b/lib/pangea/models/analytics/analytics_event.dart @@ -1,8 +1,6 @@ import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/models/analytics/analytics_model.dart'; -import 'package:fluffychat/pangea/models/analytics/constructs_event.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; -import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart'; import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart'; import 'package:matrix/matrix.dart'; @@ -28,32 +26,4 @@ abstract class AnalyticsEvent { } return contentCache!; } - - static List analyticsEventTypes = [ - PangeaEventTypes.summaryAnalytics, - PangeaEventTypes.construct, - ]; - - static Future sendEvent( - Room analyticsRoom, - String type, - List analyticsContent, - ) async { - String? eventId; - switch (type) { - case PangeaEventTypes.summaryAnalytics: - eventId = await SummaryAnalyticsEvent.sendSummaryAnalyticsEvent( - analyticsRoom, - analyticsContent.cast(), - ); - break; - case PangeaEventTypes.construct: - eventId = await ConstructAnalyticsEvent.sendConstructsEvent( - analyticsRoom, - analyticsContent.cast(), - ); - break; - } - return eventId; - } } diff --git a/lib/pangea/models/analytics/constructs_event.dart b/lib/pangea/models/analytics/constructs_event.dart index c2930faba7..481051b1c3 100644 --- a/lib/pangea/models/analytics/constructs_event.dart +++ b/lib/pangea/models/analytics/constructs_event.dart @@ -18,19 +18,4 @@ class ConstructAnalyticsEvent extends AnalyticsEvent { contentCache ??= ConstructAnalyticsModel.fromJson(event.content); return contentCache as ConstructAnalyticsModel; } - - static Future sendConstructsEvent( - Room analyticsRoom, - List uses, - ) async { - final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel( - uses: uses, - ); - - final String? eventId = await analyticsRoom.sendEvent( - constructsModel.toJson(), - type: PangeaEventTypes.construct, - ); - return eventId; - } } diff --git a/lib/pangea/models/analytics/summary_analytics_event.dart b/lib/pangea/models/analytics/summary_analytics_event.dart index e7034eaa41..a764d5597a 100644 --- a/lib/pangea/models/analytics/summary_analytics_event.dart +++ b/lib/pangea/models/analytics/summary_analytics_event.dart @@ -18,18 +18,4 @@ class SummaryAnalyticsEvent extends AnalyticsEvent { contentCache ??= SummaryAnalyticsModel.fromJson(event.content); return contentCache as SummaryAnalyticsModel; } - - static Future sendSummaryAnalyticsEvent( - Room analyticsRoom, - List records, - ) async { - final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel( - messages: records, - ); - final String? eventId = await analyticsRoom.sendEvent( - analyticsModel.toJson(), - type: PangeaEventTypes.summaryAnalytics, - ); - return eventId; - } } diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart index 3fe3e859d6..e5c3a1c18a 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart @@ -7,14 +7,14 @@ import 'dart:typed_data'; class PracticeActivityRecordModel { final String? question; - late List responses; + late List responses; PracticeActivityRecordModel({ required this.question, - List? responses, + List? responses, }) { if (responses == null) { - this.responses = List.empty(growable: true); + this.responses = List.empty(growable: true); } else { this.responses = responses; } @@ -26,7 +26,9 @@ class PracticeActivityRecordModel { return PracticeActivityRecordModel( question: json['question'] as String, responses: (json['responses'] as List) - .map((e) => ActivityResponse.fromJson(e as Map)) + .map( + (e) => ActivityRecordResponse.fromJson(e as Map), + ) .toList(), ); } @@ -55,7 +57,7 @@ class PracticeActivityRecordModel { }) { try { responses.add( - ActivityResponse( + ActivityRecordResponse( text: text, audioBytes: audioBytes, imageBytes: imageBytes, @@ -84,7 +86,7 @@ class PracticeActivityRecordModel { int get hashCode => question.hashCode ^ responses.hashCode; } -class ActivityResponse { +class ActivityRecordResponse { // the user's response // has nullable string, nullable audio bytes, nullable image bytes, and timestamp final String? text; @@ -92,15 +94,15 @@ class ActivityResponse { final Uint8List? imageBytes; final DateTime timestamp; - ActivityResponse({ + ActivityRecordResponse({ this.text, this.audioBytes, this.imageBytes, required this.timestamp, }); - factory ActivityResponse.fromJson(Map json) { - return ActivityResponse( + factory ActivityRecordResponse.fromJson(Map json) { + return ActivityRecordResponse( text: json['text'] as String?, audioBytes: json['audio'] as Uint8List?, imageBytes: json['image'] as Uint8List?, @@ -121,7 +123,7 @@ class ActivityResponse { bool operator ==(Object other) { if (identical(this, other)) return true; - return other is ActivityResponse && + return other is ActivityRecordResponse && other.text == text && other.audioBytes == audioBytes && other.imageBytes == imageBytes && diff --git a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart index a345243ff7..5c694b6cad 100644 --- a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart +++ b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:developer'; import 'package:fluffychat/pangea/constants/language_constants.dart'; @@ -29,49 +28,35 @@ class StudentAnalyticsPage extends StatefulWidget { class StudentAnalyticsController extends State { final PangeaController _pangeaController = MatrixState.pangeaController; AnalyticsSelected? selected; - StreamSubscription? stateSub; @override void initState() { super.initState(); - - final listFutures = [ - _pangeaController.myAnalytics.setStudentChats(), - _pangeaController.myAnalytics.setStudentSpaces(), - ]; - Future.wait(listFutures).then((_) => setState(() {})); - - stateSub = _pangeaController.myAnalytics.stateStream.listen((_) { - setState(() {}); - }); } @override void dispose() { - stateSub?.cancel(); super.dispose(); } + List _chats = []; List get chats { - if (_pangeaController.myAnalytics.studentChats.isEmpty) { - _pangeaController.myAnalytics.setStudentChats().then((_) { - if (_pangeaController.myAnalytics.studentChats.isNotEmpty) { - setState(() {}); - } + if (_chats.isEmpty) { + _pangeaController.matrixState.client.chatsImAStudentIn.then((result) { + setState(() => _chats = result); }); } - return _pangeaController.myAnalytics.studentChats; + return _chats; } + List _spaces = []; List get spaces { - if (_pangeaController.myAnalytics.studentSpaces.isEmpty) { - _pangeaController.myAnalytics.setStudentSpaces().then((_) { - if (_pangeaController.myAnalytics.studentSpaces.isNotEmpty) { - setState(() {}); - } + if (_spaces.isEmpty) { + _pangeaController.matrixState.client.spaceImAStudentIn.then((result) { + setState(() => _spaces = result); }); } - return _pangeaController.myAnalytics.studentSpaces; + return _spaces; } String? get userId { From 5c8666b3e27b3391353f7781493868eeaae85501 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Fri, 28 Jun 2024 15:30:57 -0400 Subject: [PATCH 3/7] rough draft complete --- .../message_analytics_controller.dart | 12 +- .../controllers/my_analytics_controller.dart | 210 +++++++----------- ...actice_activity_generation_controller.dart | 2 +- lib/pangea/enum/construct_type_enum.dart | 16 +- lib/pangea/enum/construct_use_type_enum.dart | 93 ++++++++ .../events_extension.dart | 26 --- .../pangea_message_event.dart | 151 ++++++++++++- .../practice_acitivity_record_event.dart | 24 -- .../practice_activity_event.dart | 4 +- .../practice_activity_record_event.dart | 89 ++++++++ .../models/analytics/analytics_model.dart | 6 +- .../models/analytics/constructs_model.dart | 127 +---------- lib/pangea/models/choreo_record.dart | 134 ----------- lib/pangea/models/headwords.dart | 24 +- .../practice_activity_model.dart | 4 +- .../practice_activity_record_model.dart | 21 +- .../pages/analytics/base_analytics_view.dart | 2 +- .../pages/analytics/construct_list.dart | 4 +- .../practice_activity_content.dart | 11 +- 19 files changed, 485 insertions(+), 475 deletions(-) create mode 100644 lib/pangea/enum/construct_use_type_enum.dart delete mode 100644 lib/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart create mode 100644 lib/pangea/matrix_event_wrappers/practice_activity_record_event.dart diff --git a/lib/pangea/controllers/message_analytics_controller.dart b/lib/pangea/controllers/message_analytics_controller.dart index 1475c0e6e4..3739b25962 100644 --- a/lib/pangea/controllers/message_analytics_controller.dart +++ b/lib/pangea/controllers/message_analytics_controller.dart @@ -641,7 +641,7 @@ class AnalyticsController extends BaseController { List? getConstructsLocal({ required TimeSpan timeSpan, - required ConstructType constructType, + required ConstructTypeEnum constructType, required AnalyticsSelected defaultSelected, AnalyticsSelected? selected, DateTime? lastUpdated, @@ -669,7 +669,7 @@ class AnalyticsController extends BaseController { } void cacheConstructs({ - required ConstructType constructType, + required ConstructTypeEnum constructType, required List events, required AnalyticsSelected defaultSelected, AnalyticsSelected? selected, @@ -687,7 +687,7 @@ class AnalyticsController extends BaseController { Future> getMyConstructs({ required AnalyticsSelected defaultSelected, - required ConstructType constructType, + required ConstructTypeEnum constructType, AnalyticsSelected? selected, }) async { final List unfilteredConstructs = @@ -706,7 +706,7 @@ class AnalyticsController extends BaseController { } Future> getSpaceConstructs({ - required ConstructType constructType, + required ConstructTypeEnum constructType, required Room space, required AnalyticsSelected defaultSelected, AnalyticsSelected? selected, @@ -768,7 +768,7 @@ class AnalyticsController extends BaseController { } Future?> getConstructs({ - required ConstructType constructType, + required ConstructTypeEnum constructType, required AnalyticsSelected defaultSelected, AnalyticsSelected? selected, bool removeIT = true, @@ -898,7 +898,7 @@ abstract class CacheEntry { } class ConstructCacheEntry extends CacheEntry { - final ConstructType type; + final ConstructTypeEnum type; final List events; ConstructCacheEntry({ diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 82b7af3c74..ef6baad1ea 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -1,17 +1,13 @@ import 'dart:async'; import 'dart:developer'; -<<<<<<< Updated upstream -import 'package:fluffychat/pangea/constants/language_constants.dart'; -======= ->>>>>>> Stashed changes import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; 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/analytics/summary_analytics_model.dart'; -import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; @@ -196,9 +192,8 @@ class MyAnalyticsController { String? get userL2 => _pangeaController.languageController.activeL2Code(); - // top level analytics sending function. Send analytics - // for each type of analytics event - // to each of the applicable analytics rooms + /// top level analytics sending function. Gather recent messages and activity records, + /// convert them into the correct formats, and send them to the analytics room Future _updateAnalytics() async { // if missing important info, don't send analytics if (userL2 == null || _client.userID == null) { @@ -206,151 +201,108 @@ class MyAnalyticsController { return; } - // get the last updated time for each analytics room - // and the least recent update, which will be used to determine - // how far to go back in the chat history to get messages - final Map lastUpdatedMap = await _pangeaController - .matrixState.client - .allAnalyticsRoomsLastUpdated(); - final List lastUpdates = lastUpdatedMap.values - .where((lastUpdate) => lastUpdate != null) - .cast() - .toList(); - - /// Get the last time that analytics to for current target language - /// were updated. This my present a problem is the user has analytics - /// rooms for multiple languages, and a non-target language was updated - /// less recently than the target language. In this case, some data may - /// be missing, but a case like that seems relatively rare, and could - /// result in unnecessaily going too far back in the chat history - DateTime? l2AnalyticsLastUpdated = lastUpdatedMap[userL2]; - if (l2AnalyticsLastUpdated == null) { - /// if the target language has never been updated, use the least - /// recent update time - lastUpdates.sort((a, b) => a.compareTo(b)); - l2AnalyticsLastUpdated = - lastUpdates.isNotEmpty ? lastUpdates.first : null; - } - - final List chats = await _client.chatsImAStudentIn; - - final List recentMsgs = - await _getMessagesWithUnsavedAnalytics( - l2AnalyticsLastUpdated, - chats, - ); - - final List recentActivities = - await getRecentActivities(userL2!, l2AnalyticsLastUpdated, chats); - - // FOR DISCUSSION: - // we want to make sure we save something for every message send - // however, we're currently saving analytics for messages not in the userL2 - // based on bad language detection results. maybe it would be better to - // save the analytics for these messages in the userL2 analytics room, but - // with useType of unknown - + // analytics room for the user and current target language final Room analyticsRoom = await _client.getMyAnalyticsRoom(userL2!); - // if there is no analytics room for this langCode, then user hadn't sent - // message in this language at the time of the last analytics update - // so fallback to the least recent update time - final DateTime? lastUpdated = - lastUpdatedMap[analyticsRoom.id] ?? l2AnalyticsLastUpdated; - - // final String msgLangCode = (msg.originalSent?.langCode != null && - // msg.originalSent?.langCode != LanguageKeys.unknownLanguage) - // ? msg.originalSent!.langCode - // : userL2; - - // finally, send the analytics events to the analytics room - await _sendAnalyticsEvents( - analyticsRoom, - recentMsgs, - lastUpdated, - recentActivities, + // get the last time analytics were updated for this room + final DateTime? l2AnalyticsLastUpdated = + await analyticsRoom.analyticsLastUpdated( + PangeaEventTypes.summaryAnalytics, + _client.userID!, ); - } - Future> getRecentActivities( - String userL2, - DateTime? lastUpdated, - List chats, - ) async { + // all chats in which user is a student + final List chats = await _client.chatsImAStudentIn; + + // get the recent message events and activity records for each chat + final List>> recentMsgFutures = []; final List>> recentActivityFutures = []; for (final Room chat in chats) { + chat.getEventsBySender( + type: EventTypes.Message, + sender: _client.userID!, + since: l2AnalyticsLastUpdated, + ); recentActivityFutures.add( chat.getEventsBySender( type: PangeaEventTypes.activityRecord, sender: _client.userID!, - since: lastUpdated, + since: l2AnalyticsLastUpdated, ), ); } - final List> recentActivityLists = - await Future.wait(recentActivityFutures); + final List> recentMsgs = + (await Future.wait(recentMsgFutures)).toList(); + final List recentActivityReconds = + (await Future.wait(recentActivityFutures)) + .expand((e) => e) + .map((event) => PracticeActivityRecordEvent(event: event)) + .toList(); + + // get the timelines for each chat + final List> timelineFutures = []; + for (final chat in chats) { + timelineFutures.add(chat.getTimeline()); + } + final List timelines = await Future.wait(timelineFutures); + final Map timelineMap = + Map.fromIterables(chats.map((e) => e.id), timelines); + + //convert into PangeaMessageEvents + final List> recentPangeaMessageEvents = []; + for (final (index, eventList) in recentMsgs.indexed) { + recentPangeaMessageEvents.add( + eventList + .map( + (event) => PangeaMessageEvent( + event: event, + timeline: timelines[index], + ownMessage: true, + ), + ) + .toList(), + ); + } - return recentActivityLists - .expand((e) => e) - .map((e) => ActivityRecordResponse.fromJson(e.content)) - .toList(); - } + final List allRecentMessages = + recentPangeaMessageEvents.expand((e) => e).toList(); - /// Returns the new messages that have not yet been saved to analytics. - /// The keys in the map correspond to different categories or groups of messages, - /// while the values are lists of [PangeaMessageEvent] objects belonging to each category. - Future> _getMessagesWithUnsavedAnalytics( - DateTime? since, - List chats, - ) async { - // get the recent messages for each chat - final List>> futures = []; - for (final Room chat in chats) { - futures.add( - chat.myMessageEventsInChat( - since: since, - ), + final List summaryContent = + SummaryAnalyticsModel.formatSummaryContent(allRecentMessages); + // if there's new content to be sent, or if lastUpdated hasn't been + // set yet for this room, send the analytics events + if (summaryContent.isNotEmpty || l2AnalyticsLastUpdated == null) { + await analyticsRoom.sendSummaryAnalyticsEvent( + summaryContent, ); } - final List> recentMsgLists = - await Future.wait(futures); - // flatten the list of lists of messages - return recentMsgLists.expand((e) => e).toList(); - } - - Future _sendAnalyticsEvents( - Room analyticsRoom, - List recentMsgs, - DateTime? lastUpdated, - List recentActivities, - ) async { + // get constructs for messages final List constructContent = []; + for (final PangeaMessageEvent message in allRecentMessages) { + constructContent.addAll(message.allConstructUses); + } - if (recentMsgs.isNotEmpty) { - // remove messages that were sent before the last update - - // format the analytics data - final List summaryContent = - SummaryAnalyticsModel.formatSummaryContent(recentMsgs); - // if there's new content to be sent, or if lastUpdated hasn't been - // set yet for this room, send the analytics events - if (summaryContent.isNotEmpty || lastUpdated == null) { - await analyticsRoom.sendSummaryAnalyticsEvent( - summaryContent, + // get constructs for practice activities + final List>> constructFutures = []; + for (final PracticeActivityRecordEvent activity in recentActivityReconds) { + final Timeline? timeline = timelineMap[activity.event.roomId!]; + if (timeline == null) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "PracticeActivityRecordEvent has null timeline", + data: activity.event.toJson(), ); + continue; } - - constructContent - .addAll(ConstructAnalyticsModel.formatConstructsContent(recentMsgs)); + constructFutures.add(activity.uses(timeline)); } + final List> constructLists = + await Future.wait(constructFutures); - if (recentActivities.isNotEmpty) { - // TODO - Concert recentActivities into list of constructUse objects. - // First, We need to get related practiceActivityEvent from timeline in order to get its related constructs. Alternatively we - // could search for completed practice activities and see which have been completed by the user. - // It's not clear which is the best approach at the moment and we should consider both. - } + constructContent.addAll(constructLists.expand((e) => e)); + + debugger(when: kDebugMode); await analyticsRoom.sendConstructsEvent( constructContent, diff --git a/lib/pangea/controllers/practice_activity_generation_controller.dart b/lib/pangea/controllers/practice_activity_generation_controller.dart index 29047d0c4b..8ecdc8740c 100644 --- a/lib/pangea/controllers/practice_activity_generation_controller.dart +++ b/lib/pangea/controllers/practice_activity_generation_controller.dart @@ -88,7 +88,7 @@ class PracticeGenerationController { PracticeActivityModel dummyModel(PangeaMessageEvent event) => PracticeActivityModel( tgtConstructs: [ - ConstructIdentifier(lemma: "be", type: ConstructType.vocab), + ConstructIdentifier(lemma: "be", type: ConstructTypeEnum.vocab), ], activityType: ActivityTypeEnum.multipleChoice, langCode: event.messageDisplayLangCode, diff --git a/lib/pangea/enum/construct_type_enum.dart b/lib/pangea/enum/construct_type_enum.dart index 2a7d5583d2..7db7f9cd52 100644 --- a/lib/pangea/enum/construct_type_enum.dart +++ b/lib/pangea/enum/construct_type_enum.dart @@ -1,30 +1,30 @@ -enum ConstructType { +enum ConstructTypeEnum { grammar, vocab, } -extension ConstructExtension on ConstructType { +extension ConstructExtension on ConstructTypeEnum { String get string { switch (this) { - case ConstructType.grammar: + case ConstructTypeEnum.grammar: return 'grammar'; - case ConstructType.vocab: + case ConstructTypeEnum.vocab: return 'vocab'; } } } class ConstructTypeUtil { - static ConstructType fromString(String? string) { + static ConstructTypeEnum fromString(String? string) { switch (string) { case 'g': case 'grammar': - return ConstructType.grammar; + return ConstructTypeEnum.grammar; case 'v': case 'vocab': - return ConstructType.vocab; + return ConstructTypeEnum.vocab; default: - return ConstructType.vocab; + return ConstructTypeEnum.vocab; } } } diff --git a/lib/pangea/enum/construct_use_type_enum.dart b/lib/pangea/enum/construct_use_type_enum.dart new file mode 100644 index 0000000000..0e3c52bbbe --- /dev/null +++ b/lib/pangea/enum/construct_use_type_enum.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; + +enum ConstructUseTypeEnum { + /// produced in chat by user, igc was run, and we've judged it to be a correct use + wa, + + /// produced in chat by user, igc was run, and we've judged it to be a incorrect use + /// Note: if the IGC match is ignored, this is not counted as an incorrect use + ga, + + /// produced in chat by user and igc was not run + unk, + + /// selected correctly in IT flow + corIt, + + /// encountered as IT distractor and correctly ignored it + ignIt, + + /// encountered as it distractor and selected it + incIt, + + /// encountered in igc match and ignored match + ignIGC, + + /// selected correctly in IGC flow + corIGC, + + /// encountered as distractor in IGC flow and selected it + incIGC, + + /// selected correctly in practice activity flow + corPA, + + /// was target construct in practice activity but user did not select correctly + incPA, +} + +extension ConstructUseTypeExtension on ConstructUseTypeEnum { + String get string { + switch (this) { + case ConstructUseTypeEnum.ga: + return 'ga'; + case ConstructUseTypeEnum.wa: + return 'wa'; + case ConstructUseTypeEnum.corIt: + return 'corIt'; + case ConstructUseTypeEnum.incIt: + return 'incIt'; + case ConstructUseTypeEnum.ignIt: + return 'ignIt'; + case ConstructUseTypeEnum.ignIGC: + return 'ignIGC'; + case ConstructUseTypeEnum.corIGC: + return 'corIGC'; + case ConstructUseTypeEnum.incIGC: + return 'incIGC'; + case ConstructUseTypeEnum.unk: + return 'unk'; + case ConstructUseTypeEnum.corPA: + return 'corPA'; + case ConstructUseTypeEnum.incPA: + return 'incPA'; + } + } + + IconData get icon { + switch (this) { + case ConstructUseTypeEnum.ga: + return Icons.check; + case ConstructUseTypeEnum.wa: + return Icons.thumb_up_sharp; + case ConstructUseTypeEnum.corIt: + return Icons.check; + case ConstructUseTypeEnum.incIt: + return Icons.close; + case ConstructUseTypeEnum.ignIt: + return Icons.close; + case ConstructUseTypeEnum.ignIGC: + return Icons.close; + case ConstructUseTypeEnum.corIGC: + return Icons.check; + case ConstructUseTypeEnum.incIGC: + return Icons.close; + case ConstructUseTypeEnum.corPA: + return Icons.check; + case ConstructUseTypeEnum.incPA: + return Icons.close; + case ConstructUseTypeEnum.unk: + return Icons.help; + } + } +} diff --git a/lib/pangea/extensions/pangea_room_extension/events_extension.dart b/lib/pangea/extensions/pangea_room_extension/events_extension.dart index 6cdde1ce21..0f40da5c7f 100644 --- a/lib/pangea/extensions/pangea_room_extension/events_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/events_extension.dart @@ -426,32 +426,6 @@ extension EventsRoomExtension on Room { // } // } - Future> myMessageEventsInChat({ - DateTime? since, - }) async { - try { - final List msgEvents = await getEventsBySender( - type: EventTypes.Message, - sender: client.userID!, - since: since, - ); - final Timeline timeline = await getTimeline(); - return msgEvents - .where((event) => (event.content['msgtype'] == MessageTypes.Text)) - .map((event) { - return PangeaMessageEvent( - event: event, - timeline: timeline, - ownMessage: true, - ); - }).toList(); - } catch (err, s) { - debugger(when: kDebugMode); - ErrorHandler.logError(e: err, s: s); - return []; - } - } - // fetch event of a certain type by a certain sender // since a certain time or up to a certain amount Future> getEventsBySender({ diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 4ead9982cb..080b07617e 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -2,14 +2,20 @@ import 'dart:convert'; import 'dart:developer'; import 'package:collection/collection.dart'; +import 'package:fluffychat/pangea/constants/choreo_constants.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/enum/audio_encoding_enum.dart'; +import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; +import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.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/lemma.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; +import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/models/space_model.dart'; import 'package:fluffychat/pangea/models/speech_to_text_models.dart'; @@ -658,14 +664,145 @@ class PangeaMessageEvent { } } - // List get activities => - //each match is turned into an activity that other students can access - //they're not told the answer but have to find it themselves - //the message has a blank piece which they fill in themselves + List get allConstructUses => + [...grammarConstructUses, ..._vocabUses]; + + /// [tokens] is the final list of tokens that were sent + /// if no ga or ta, + /// make wa use for each and return + /// else + /// for each saveable vocab in the final message + /// if vocab is contained in an accepted replacement, make ga use + /// if vocab is contained in ta choice, + /// if selected as choice, corIt + /// if written as customInput, corIt? (account for score in this) + /// for each it step + /// for each continuance + /// if not within the final message, save ignIT/incIT + List get _vocabUses { + final List uses = []; + + if (event.roomId == null) return uses; + + List lemmasToVocabUses( + List lemmas, + ConstructUseTypeEnum type, + ) { + final List uses = []; + for (final lemma in lemmas) { + if (lemma.saveVocab) { + uses.add( + OneConstructUse( + useType: type, + chatId: event.roomId!, + timeStamp: event.originServerTs, + lemma: lemma.text, + form: lemma.form, + msgId: event.eventId, + constructType: ConstructTypeEnum.vocab, + ), + ); + } + } + return uses; + } + + List getVocabUseForToken(PangeaToken token) { + if (originalSent?.choreo == null) { + final bool inUserL2 = originalSent?.langCode == l2Code; + return lemmasToVocabUses( + token.lemmas, + inUserL2 ? ConstructUseTypeEnum.wa : ConstructUseTypeEnum.unk, + ); + } + + for (final step in originalSent!.choreo!.choreoSteps) { + /// if 1) accepted match 2) token is in the replacement and 3) replacement + /// is in the overall step text, then token was a ga + if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted && + (step.acceptedOrIgnoredMatch!.match.choices?.any( + (r) => + r.value.contains(token.text.content) && + step.text.contains(r.value), + ) ?? + false)) { + return lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.ga); + } + if (step.itStep != null) { + final bool pickedThroughIT = step.itStep!.chosenContinuance?.text + .contains(token.text.content) ?? + false; + if (pickedThroughIT) { + return lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.corIt); + //PTODO - check if added via custom input in IT flow + } + } + } + return lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.wa); + } + + /// for each token, record whether selected in ga, ta, or wa + if (originalSent?.tokens != null) { + for (final token in originalSent!.tokens!) { + uses.addAll(getVocabUseForToken(token)); + } + } + + if (originalSent?.choreo == null) return uses; + + for (final itStep in originalSent!.choreo!.itSteps) { + for (final continuance in itStep.continuances) { + // this seems to always be false for continuances right now + + if (originalSent!.choreo!.finalMessage.contains(continuance.text)) { + continue; + } + if (continuance.wasClicked) { + //PTODO - account for end of flow score + if (continuance.level != ChoreoConstants.levelThresholdForGreen) { + uses.addAll( + lemmasToVocabUses(continuance.lemmas, ConstructUseTypeEnum.incIt), + ); + } + } else { + if (continuance.level != ChoreoConstants.levelThresholdForGreen) { + uses.addAll( + lemmasToVocabUses(continuance.lemmas, ConstructUseTypeEnum.ignIt), + ); + } + } + } + } - // replication of logic from message_content.dart - // bool get isHtml => - // AppConfig.renderHtml && !_event.redacted && _event.isRichMessage; + return uses; + } + + List get grammarConstructUses { + final List uses = []; + + if (originalSent?.choreo == null || event.roomId == null) return uses; + + for (final step in originalSent!.choreo!.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, + chatId: event.roomId!, + timeStamp: event.originServerTs, + lemma: name, + form: name, + msgId: event.eventId, + constructType: ConstructTypeEnum.grammar, + id: "${event.eventId}_${step.acceptedOrIgnoredMatch!.match.offset}_${step.acceptedOrIgnoredMatch!.match.length}", + ), + ); + } + } + return uses; + } } class URLFinder { diff --git a/lib/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart b/lib/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart deleted file mode 100644 index d4b9cde239..0000000000 --- a/lib/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; -import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; -import 'package:matrix/matrix.dart'; - -import '../constants/pangea_event_types.dart'; - -class PracticeActivityRecordEvent { - Event event; - - PracticeActivityRecordModel? _content; - - PracticeActivityRecordEvent({required this.event}) { - if (event.type != PangeaEventTypes.activityRecord) { - throw Exception( - "${event.type} should not be used to make a PracticeActivityRecordEvent", - ); - } - } - - PracticeActivityRecordModel? get record { - _content ??= event.getPangeaContent(); - return _content!; - } -} diff --git a/lib/pangea/matrix_event_wrappers/practice_activity_event.dart b/lib/pangea/matrix_event_wrappers/practice_activity_event.dart index c5f35be911..9d7b17ccc4 100644 --- a/lib/pangea/matrix_event_wrappers/practice_activity_event.dart +++ b/lib/pangea/matrix_event_wrappers/practice_activity_event.dart @@ -1,7 +1,7 @@ import 'dart:developer'; import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; -import 'package:fluffychat/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; @@ -61,6 +61,8 @@ class PracticeActivityEvent { ) .toList(); + String get parentMessageId => event.relationshipEventId!; + /// Checks if there are any user records in the list for this activity, /// and, if so, then the activity is complete bool get isComplete => userRecords.isNotEmpty; diff --git a/lib/pangea/matrix_event_wrappers/practice_activity_record_event.dart b/lib/pangea/matrix_event_wrappers/practice_activity_record_event.dart new file mode 100644 index 0000000000..77b4948fdd --- /dev/null +++ b/lib/pangea/matrix_event_wrappers/practice_activity_record_event.dart @@ -0,0 +1,89 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/extensions/pangea_event_extension.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/practice_activities.dart/practice_activity_model.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:matrix/matrix.dart'; + +import '../constants/pangea_event_types.dart'; + +class PracticeActivityRecordEvent { + Event event; + + PracticeActivityRecordModel? _content; + + PracticeActivityRecordEvent({required this.event}) { + if (event.type != PangeaEventTypes.activityRecord) { + throw Exception( + "${event.type} should not be used to make a PracticeActivityRecordEvent", + ); + } + } + + PracticeActivityRecordModel get record { + _content ??= event.getPangeaContent(); + return _content!; + } + + Future> uses(Timeline timeline) async { + try { + final String? parent = event.relationshipEventId; + if (parent == null) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "PracticeActivityRecordEvent has null event.relationshipEventId", + data: event.toJson(), + ); + return []; + } + + final Event? practiceEvent = + await timeline.getEventById(event.relationshipEventId!); + + if (practiceEvent == null) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "PracticeActivityRecordEvent has null practiceActivityEvent with id $parent", + data: event.toJson(), + ); + return []; + } + + final PracticeActivityEvent practiceActivity = PracticeActivityEvent( + event: practiceEvent, + timeline: timeline, + ); + + final List uses = []; + + final List constructIds = + practiceActivity.practiceActivity.tgtConstructs; + + for (final construct in constructIds) { + uses.add( + OneConstructUse( + lemma: construct.lemma, + constructType: construct.type, + useType: record.useType, + //TODO - find form of construct within the message + //this is related to the feature of highlighting the target construct in the message + form: construct.lemma, + chatId: event.roomId ?? practiceEvent.roomId ?? timeline.room.id, + msgId: practiceActivity.parentMessageId, + timeStamp: event.originServerTs, + ), + ); + } + + return uses; + } catch (e, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, s: s, data: event.toJson()); + rethrow; + } + } +} diff --git a/lib/pangea/models/analytics/analytics_model.dart b/lib/pangea/models/analytics/analytics_model.dart index bdb3bc6d5c..d8732ad977 100644 --- a/lib/pangea/models/analytics/analytics_model.dart +++ b/lib/pangea/models/analytics/analytics_model.dart @@ -12,7 +12,11 @@ abstract class AnalyticsModel { case PangeaEventTypes.summaryAnalytics: return SummaryAnalyticsModel.formatSummaryContent(recentMsgs); case PangeaEventTypes.construct: - return ConstructAnalyticsModel.formatConstructsContent(recentMsgs); + final List uses = []; + for (final msg in recentMsgs) { + uses.addAll(msg.allConstructUses); + } + return uses; } return []; } diff --git a/lib/pangea/models/analytics/constructs_model.dart b/lib/pangea/models/analytics/constructs_model.dart index 18c6d3d5ae..54e81789fc 100644 --- a/lib/pangea/models/analytics/constructs_model.dart +++ b/lib/pangea/models/analytics/constructs_model.dart @@ -1,11 +1,9 @@ import 'dart:developer'; -import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/models/analytics/analytics_model.dart'; -import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import '../../enum/construct_type_enum.dart'; @@ -24,7 +22,7 @@ class ConstructAnalyticsModel extends AnalyticsModel { if (json[_usesKey] is List) { // This is the new format uses.addAll( - json[_usesKey] + (json[_usesKey] as List) .map((use) => OneConstructUse.fromJson(use)) .cast() .toList(), @@ -39,13 +37,13 @@ class ConstructAnalyticsModel extends AnalyticsModel { final lemmaUses = useValue[_usesKey]; for (final useData in lemmaUses) { final use = OneConstructUse( - useType: ConstructUseType.ga, + useType: ConstructUseTypeEnum.ga, chatId: useData["chatId"], timeStamp: DateTime.parse(useData["timeStamp"]), lemma: lemma, form: useData["form"], msgId: useData["msgId"], - constructType: ConstructType.grammar, + constructType: ConstructTypeEnum.grammar, ); uses.add(use); } @@ -70,122 +68,13 @@ class ConstructAnalyticsModel extends AnalyticsModel { _usesKey: uses.map((use) => use.toJson()).toList(), }; } - - static List formatConstructsContent( - List recentMsgs, - ) { - final List filtered = List.from(recentMsgs); - final List uses = []; - - for (final msg in filtered) { - if (msg.originalSent?.choreo == null) continue; - uses.addAll( - msg.originalSent!.choreo!.toGrammarConstructUse( - msg.eventId, - msg.room.id, - msg.originServerTs, - ), - ); - - final List? tokens = msg.originalSent?.tokens; - if (tokens == null) continue; - uses.addAll( - msg.originalSent!.choreo!.toVocabUse( - tokens, - msg.room.id, - msg.eventId, - msg.originServerTs, - ), - ); - } - - return uses; - } -} - -enum ConstructUseType { - /// produced in chat by user, igc was run, and we've judged it to be a correct use - wa, - - /// produced in chat by user, igc was run, and we've judged it to be a incorrect use - /// Note: if the IGC match is ignored, this is not counted as an incorrect use - ga, - - /// produced in chat by user and igc was not run - unk, - - /// selected correctly in IT flow - corIt, - - /// encountered as IT distractor and correctly ignored it - ignIt, - - /// encountered as it distractor and selected it - incIt, - - /// encountered in igc match and ignored match - ignIGC, - - /// selected correctly in IGC flow - corIGC, - - /// encountered as distractor in IGC flow and selected it - incIGC, -} - -extension on ConstructUseType { - String get string { - switch (this) { - case ConstructUseType.ga: - return 'ga'; - case ConstructUseType.wa: - return 'wa'; - case ConstructUseType.corIt: - return 'corIt'; - case ConstructUseType.incIt: - return 'incIt'; - case ConstructUseType.ignIt: - return 'ignIt'; - case ConstructUseType.ignIGC: - return 'ignIGC'; - case ConstructUseType.corIGC: - return 'corIGC'; - case ConstructUseType.incIGC: - return 'incIGC'; - case ConstructUseType.unk: - return 'unk'; - } - } - - IconData get icon { - switch (this) { - case ConstructUseType.ga: - return Icons.check; - case ConstructUseType.wa: - return Icons.thumb_up_sharp; - case ConstructUseType.corIt: - return Icons.check; - case ConstructUseType.incIt: - return Icons.close; - case ConstructUseType.ignIt: - return Icons.close; - case ConstructUseType.ignIGC: - return Icons.close; - case ConstructUseType.corIGC: - return Icons.check; - case ConstructUseType.incIGC: - return Icons.close; - case ConstructUseType.unk: - return Icons.help; - } - } } class OneConstructUse { String? lemma; - ConstructType? constructType; + ConstructTypeEnum? constructType; String? form; - ConstructUseType useType; + ConstructUseTypeEnum useType; String chatId; String? msgId; DateTime timeStamp; @@ -204,7 +93,7 @@ class OneConstructUse { factory OneConstructUse.fromJson(Map json) { return OneConstructUse( - useType: ConstructUseType.values + useType: ConstructUseTypeEnum.values .firstWhere((e) => e.string == json['useType']), chatId: json['chatId'], timeStamp: DateTime.parse(json['timeStamp']), @@ -248,7 +137,7 @@ class OneConstructUse { class ConstructUses { final List uses; - final ConstructType constructType; + final ConstructTypeEnum constructType; final String lemma; ConstructUses({ diff --git a/lib/pangea/models/choreo_record.dart b/lib/pangea/models/choreo_record.dart index 413f007168..3586fcee10 100644 --- a/lib/pangea/models/choreo_record.dart +++ b/lib/pangea/models/choreo_record.dart @@ -1,13 +1,8 @@ import 'dart:convert'; -import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; -import 'package:fluffychat/pangea/models/pangea_token_model.dart'; -import '../constants/choreo_constants.dart'; -import '../enum/construct_type_enum.dart'; import 'it_step.dart'; -import 'lemma.dart'; /// this class lives within a [PangeaIGCEvent] /// it always has a [RepresentationEvent] parent @@ -111,135 +106,6 @@ class ChoreoRecord { openMatches: [], ); - /// [tokens] is the final list of tokens that were sent - /// if no ga or ta, - /// make wa use for each and return - /// else - /// for each saveable vocab in the final message - /// if vocab is contained in an accepted replacement, make ga use - /// if vocab is contained in ta choice, - /// if selected as choice, corIt - /// if written as customInput, corIt? (account for score in this) - /// for each it step - /// for each continuance - /// if not within the final message, save ignIT/incIT - List toVocabUse( - List tokens, - String chatId, - String msgId, - DateTime timestamp, - ) { - final List uses = []; - final DateTime now = DateTime.now(); - List lemmasToVocabUses( - List lemmas, - ConstructUseType type, - ) { - final List uses = []; - for (final lemma in lemmas) { - if (lemma.saveVocab) { - uses.add( - OneConstructUse( - useType: type, - chatId: chatId, - timeStamp: timestamp, - lemma: lemma.text, - form: lemma.form, - msgId: msgId, - constructType: ConstructType.vocab, - ), - ); - } - } - return uses; - } - - List getVocabUseForToken(PangeaToken token) { - for (final step in choreoSteps) { - /// if 1) accepted match 2) token is in the replacement and 3) replacement - /// is in the overall step text, then token was a ga - if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted && - (step.acceptedOrIgnoredMatch!.match.choices?.any( - (r) => - r.value.contains(token.text.content) && - step.text.contains(r.value), - ) ?? - false)) { - return lemmasToVocabUses(token.lemmas, ConstructUseType.ga); - } - if (step.itStep != null) { - final bool pickedThroughIT = step.itStep!.chosenContinuance?.text - .contains(token.text.content) ?? - false; - if (pickedThroughIT) { - return lemmasToVocabUses(token.lemmas, ConstructUseType.corIt); - //PTODO - check if added via custom input in IT flow - } - } - } - return lemmasToVocabUses(token.lemmas, ConstructUseType.wa); - } - - /// for each token, record whether selected in ga, ta, or wa - for (final token in tokens) { - uses.addAll(getVocabUseForToken(token)); - } - - for (final itStep in itSteps) { - for (final continuance in itStep.continuances) { - // this seems to always be false for continuances right now - - if (finalMessage.contains(continuance.text)) { - continue; - } - if (continuance.wasClicked) { - //PTODO - account for end of flow score - if (continuance.level != ChoreoConstants.levelThresholdForGreen) { - uses.addAll( - lemmasToVocabUses(continuance.lemmas, ConstructUseType.incIt), - ); - } - } else { - if (continuance.level != ChoreoConstants.levelThresholdForGreen) { - uses.addAll( - lemmasToVocabUses(continuance.lemmas, ConstructUseType.ignIt), - ); - } - } - } - } - - return uses; - } - - List toGrammarConstructUse( - String msgId, - String chatId, - DateTime timestamp, - ) { - final List uses = []; - 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: ConstructUseType.ga, - chatId: chatId, - timeStamp: timestamp, - lemma: name, - form: name, - msgId: msgId, - constructType: ConstructType.grammar, - id: "${msgId}_${step.acceptedOrIgnoredMatch!.match.offset}_${step.acceptedOrIgnoredMatch!.match.length}", - ), - ); - } - } - return uses; - } - List get itSteps => choreoSteps.where((e) => e.itStep != null).map((e) => e.itStep!).toList(); diff --git a/lib/pangea/models/headwords.dart b/lib/pangea/models/headwords.dart index 497381fa11..a55eeb1882 100644 --- a/lib/pangea/models/headwords.dart +++ b/lib/pangea/models/headwords.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:developer'; +import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -154,32 +155,37 @@ class VocabTotals { void addVocabUseBasedOnUseType(List uses) { for (final use in uses) { switch (use.useType) { - case ConstructUseType.ga: + case ConstructUseTypeEnum.ga: ga++; break; - case ConstructUseType.wa: + case ConstructUseTypeEnum.wa: wa++; break; - case ConstructUseType.corIt: + case ConstructUseTypeEnum.corIt: corIt++; break; - case ConstructUseType.incIt: + case ConstructUseTypeEnum.incIt: incIt++; break; - case ConstructUseType.ignIt: + case ConstructUseTypeEnum.ignIt: ignIt++; break; //TODO - these shouldn't be counted as such - case ConstructUseType.ignIGC: + case ConstructUseTypeEnum.ignIGC: ignIt++; break; - case ConstructUseType.corIGC: + case ConstructUseTypeEnum.corIGC: corIt++; break; - case ConstructUseType.incIGC: + case ConstructUseTypeEnum.incIGC: incIt++; break; - case ConstructUseType.unk: + //TODO if we bring back Headwords then we need to add these + case ConstructUseTypeEnum.corPA: + break; + case ConstructUseTypeEnum.incPA: + break; + case ConstructUseTypeEnum.unk: break; } } diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart index ae8455c7f9..bd597b6a2c 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart @@ -8,7 +8,7 @@ import 'package:flutter/foundation.dart'; class ConstructIdentifier { final String lemma; - final ConstructType type; + final ConstructTypeEnum type; ConstructIdentifier({required this.lemma, required this.type}); @@ -16,7 +16,7 @@ class ConstructIdentifier { try { return ConstructIdentifier( lemma: json['lemma'] as String, - type: ConstructType.values.firstWhere( + type: ConstructTypeEnum.values.firstWhere( (e) => e.string == json['type'], ), ); diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart index e5c3a1c18a..0c4ea52bf6 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart @@ -5,6 +5,8 @@ import 'dart:developer'; import 'dart:typed_data'; +import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; + class PracticeActivityRecordModel { final String? question; late List responses; @@ -42,18 +44,25 @@ class PracticeActivityRecordModel { /// get the latest response index according to the response timeStamp /// sort the responses by timestamp and get the index of the last response - String? get latestResponse { + ActivityRecordResponse? get latestResponse { if (responses.isEmpty) { return null; } responses.sort((a, b) => a.timestamp.compareTo(b.timestamp)); - return responses[responses.length - 1].text; + return responses[responses.length - 1]; } + ConstructUseTypeEnum get useType => latestResponse?.score != null + ? (latestResponse!.score > 0 + ? ConstructUseTypeEnum.corPA + : ConstructUseTypeEnum.incPA) + : ConstructUseTypeEnum.unk; + void addResponse({ String? text, Uint8List? audioBytes, Uint8List? imageBytes, + required double score, }) { try { responses.add( @@ -62,6 +71,7 @@ class PracticeActivityRecordModel { audioBytes: audioBytes, imageBytes: imageBytes, timestamp: DateTime.now(), + score: score, ), ); } catch (e) { @@ -93,11 +103,13 @@ class ActivityRecordResponse { final Uint8List? audioBytes; final Uint8List? imageBytes; final DateTime timestamp; + final double score; ActivityRecordResponse({ this.text, this.audioBytes, this.imageBytes, + required this.score, required this.timestamp, }); @@ -107,6 +119,10 @@ class ActivityRecordResponse { audioBytes: json['audio'] as Uint8List?, imageBytes: json['image'] as Uint8List?, timestamp: DateTime.parse(json['timestamp'] as String), + // this has a default of 1 to make this backwards compatible + // score was added later and is not present in all records + // currently saved to Matrix + score: json['score'] ?? 1.0, ); } @@ -116,6 +132,7 @@ class ActivityRecordResponse { 'audio': audioBytes, 'image': imageBytes, 'timestamp': timestamp.toIso8601String(), + 'score': score, }; } diff --git a/lib/pangea/pages/analytics/base_analytics_view.dart b/lib/pangea/pages/analytics/base_analytics_view.dart index 3d70c9b4cb..cca0c7f4ee 100644 --- a/lib/pangea/pages/analytics/base_analytics_view.dart +++ b/lib/pangea/pages/analytics/base_analytics_view.dart @@ -35,7 +35,7 @@ class BaseAnalyticsView extends StatelessWidget { ); case BarChartViewSelection.grammar: return ConstructList( - constructType: ConstructType.grammar, + constructType: ConstructTypeEnum.grammar, defaultSelected: controller.widget.defaultSelected, selected: controller.selected, controller: controller, diff --git a/lib/pangea/pages/analytics/construct_list.dart b/lib/pangea/pages/analytics/construct_list.dart index 8651b7a74e..d46936c864 100644 --- a/lib/pangea/pages/analytics/construct_list.dart +++ b/lib/pangea/pages/analytics/construct_list.dart @@ -19,7 +19,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; class ConstructList extends StatefulWidget { - final ConstructType constructType; + final ConstructTypeEnum constructType; final AnalyticsSelected defaultSelected; final AnalyticsSelected? selected; final BaseAnalyticsController controller; @@ -94,7 +94,7 @@ class ConstructListView extends StatefulWidget { } class ConstructListViewState extends State { - final ConstructType constructType = ConstructType.grammar; + final ConstructTypeEnum constructType = ConstructTypeEnum.grammar; final Map _timelinesCache = {}; final Map _msgEventCache = {}; final List _msgEvents = []; diff --git a/lib/pangea/widgets/practice_activity/practice_activity_content.dart b/lib/pangea/widgets/practice_activity/practice_activity_content.dart index 8080c27eea..af43081f3d 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_content.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_content.dart @@ -2,7 +2,7 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.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_acitivity_record_event.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; @@ -11,6 +11,7 @@ import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_ca import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:get_storage/get_storage.dart'; class PracticeActivityContent extends StatefulWidget { final PracticeActivityEvent practiceEvent; @@ -65,9 +66,9 @@ class MessagePracticeActivityContentState recordModel = recordEvent!.record; //Note that only MultipleChoice activities will have this so we probably should move this logic to the MultipleChoiceActivity widget - selectedChoiceIndex = recordModel?.latestResponse != null + selectedChoiceIndex = recordModel?.latestResponse?.text != null ? widget.practiceEvent.practiceActivity.multipleChoice - ?.choiceIndex(recordModel!.latestResponse!) + ?.choiceIndex(recordModel!.latestResponse!.text!) : null; recordSubmittedPreviousSession = true; @@ -80,6 +81,10 @@ class MessagePracticeActivityContentState setState(() { selectedChoiceIndex = index; recordModel!.addResponse( + score: widget.practiceEvent.practiceActivity.multipleChoice! + .isCorrect(index) + ? 1 + : 0, text: widget .practiceEvent.practiceActivity.multipleChoice!.choices[index], ); From 919cfc4bd39b8f8b61c2a003f2cf0f6a20eaf38b Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Sun, 30 Jun 2024 10:36:09 -0400 Subject: [PATCH 4/7] improving documentation --- .../controllers/choreographer.dart | 41 ++-- .../controllers/igc_controller.dart | 67 +++--- .../controllers/it_controller.dart | 5 +- .../controllers/span_data_controller.dart | 0 .../widgets/start_igc_button.dart | 4 +- lib/pangea/config/environment.dart | 2 +- .../controllers/my_analytics_controller.dart | 14 +- .../pangea_message_event.dart | 194 ++++++++++-------- lib/pangea/repo/igc_repo.dart | 3 - 9 files changed, 181 insertions(+), 149 deletions(-) rename lib/pangea/{ => choreographer}/controllers/span_data_controller.dart (100%) diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 0e326ab6ed..77130f635e 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -183,6 +183,7 @@ class Choreographer { _textController.setSystemText("", EditType.itStart); } + /// Handles any changes to the text input _onChangeListener() { if (_noChange) { return; @@ -191,21 +192,26 @@ class Choreographer { if ([ EditType.igc, ].contains(_textController.editType)) { + // this may be unnecessary now that tokens are not used + // to allow click of words in the input field and we're getting this at the end + // TODO - turn it off and tested that this is fine igc.justGetTokensAndAddThemToIGCTextData(); + + // we set editType to keyboard here because that is the default for it + // and we want to make sure that the next change is treated as a keyboard change + // unless the system explicity sets it to something else. this textController.editType = EditType.keyboard; return; } + // not sure if this is necessary now MatrixState.pAnyState.closeOverlay(); if (errorService.isError) { return; } - // if (igc.igcTextData != null) { igc.clear(); - // setState(); - // } _resetDebounceTimer(); @@ -215,7 +221,9 @@ class Choreographer { () => getLanguageHelp(), ); } else { - getLanguageHelp(ChoreoMode.it == choreoMode); + getLanguageHelp( + onlyTokensAndLanguageDetection: ChoreoMode.it == choreoMode, + ); } //Note: we don't set the keyboard type on each keyboard stroke so this is how we default to @@ -224,10 +232,14 @@ class Choreographer { textController.editType = EditType.keyboard; } - Future getLanguageHelp([ - bool tokensOnly = false, + /// Fetches the language help for the current text, including grammar correction, language detection, + /// tokens, and translations. Includes logic to exit the flow if the user is not subscribed, if the tools are not enabled, or + /// or if autoIGC is not enabled and the user has not manually requested it. + /// [onlyTokensAndLanguageDetection] will + Future getLanguageHelp({ + bool onlyTokensAndLanguageDetection = false, bool manual = false, - ]) async { + }) async { try { if (errorService.isError) return; final CanSendStatus canSendStatus = @@ -242,13 +254,15 @@ class Choreographer { startLoading(); if (choreoMode == ChoreoMode.it && itController.isTranslationDone && - !tokensOnly) { + !onlyTokensAndLanguageDetection) { // debugger(when: kDebugMode); } await (choreoMode == ChoreoMode.it && !itController.isTranslationDone ? itController.getTranslationData(_useCustomInput) - : igc.getIGCTextData(tokensOnly: tokensOnly)); + : igc.getIGCTextData( + onlyTokensAndLanguageDetection: onlyTokensAndLanguageDetection, + )); } catch (err, stack) { ErrorHandler.logError(e: err, s: stack); } finally { @@ -494,8 +508,9 @@ class Choreographer { // TODO - this is a bit of a hack, and should be tested more // we should also check that user has not done customInput - if (itController.completedITSteps.isNotEmpty && itController.allCorrect) + if (itController.completedITSteps.isNotEmpty && itController.allCorrect) { return l2LangCode!; + } return null; } @@ -533,9 +548,11 @@ class Choreographer { chatController.room, ); - bool get itAutoPlayEnabled => pangeaController.pStoreService.read( + bool get itAutoPlayEnabled => + pangeaController.pStoreService.read( MatrixProfile.itAutoPlay.title, - ) ?? false; + ) ?? + false; bool get definitionsEnabled => pangeaController.permissionsController.isToolEnabled( diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index 68d81389e3..a694c48a5f 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -3,7 +3,7 @@ import 'dart:developer'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart'; -import 'package:fluffychat/pangea/controllers/span_data_controller.dart'; +import 'package:fluffychat/pangea/choreographer/controllers/span_data_controller.dart'; import 'package:fluffychat/pangea/models/igc_text_data_model.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; import 'package:fluffychat/pangea/repo/igc_repo.dart'; @@ -29,59 +29,64 @@ class IgcController { spanDataController = SpanDataController(choreographer); } - Future getIGCTextData({required bool tokensOnly}) async { + Future getIGCTextData({ + required bool onlyTokensAndLanguageDetection, + }) async { try { if (choreographer.currentText.isEmpty) return clear(); // the error spans are going to be reloaded, so clear the cache + // @ggurdin: Why is this separate from the clear() call? + // Also, if the spans are equal according the to the equals method, why not reuse the cached span data? + // It seems this would save some calls if the user makes some tiny changes to the text that don't + // change the matches at all. spanDataController.clearCache(); debugPrint('getIGCTextData called with ${choreographer.currentText}'); - debugPrint('getIGCTextData called with tokensOnly = $tokensOnly'); + debugPrint( + 'getIGCTextData called with tokensOnly = $onlyTokensAndLanguageDetection', + ); final IGCRequestBody reqBody = IGCRequestBody( fullText: choreographer.currentText, userL1: choreographer.l1LangCode!, userL2: choreographer.l2LangCode!, - enableIGC: choreographer.igcEnabled && !tokensOnly, - enableIT: choreographer.itEnabled && !tokensOnly, - tokensOnly: tokensOnly, + enableIGC: choreographer.igcEnabled && !onlyTokensAndLanguageDetection, + enableIT: choreographer.itEnabled && !onlyTokensAndLanguageDetection, ); final IGCTextData igcTextDataResponse = await IgcRepo.getIGC( await choreographer.accessToken, igcRequest: reqBody, ); - // temp fix - igcTextDataResponse.originalInput = reqBody.fullText; - //this will happen when the user changes the input while igc is fetching results + // this will happen when the user changes the input while igc is fetching results if (igcTextDataResponse.originalInput != choreographer.currentText) { - // final current = choreographer.currentText; - // final igctext = igcTextDataResponse.originalInput; - // Sentry.addBreadcrumb( - // Breadcrumb(message: "igc return input does not match current text"), - // ); - // debugger(when: kDebugMode); return; } //TO-DO: in api call, specify turning off IT and/or grammar checking - if (!choreographer.igcEnabled) { - igcTextDataResponse.matches = igcTextDataResponse.matches - .where((match) => !match.isGrammarMatch) - .toList(); - } - if (!choreographer.itEnabled) { - igcTextDataResponse.matches = igcTextDataResponse.matches - .where((match) => !match.isOutOfTargetMatch) - .toList(); - } - if (!choreographer.itEnabled && !choreographer.igcEnabled) { - igcTextDataResponse.matches = []; - } + // UPDATE: This is now done in the API call. New TODO is to test this. + // if (!choreographer.igcEnabled) { + // igcTextDataResponse.matches = igcTextDataResponse.matches + // .where((match) => !match.isGrammarMatch) + // .toList(); + // } + // if (!choreographer.itEnabled) { + // igcTextDataResponse.matches = igcTextDataResponse.matches + // .where((match) => !match.isOutOfTargetMatch) + // .toList(); + // } + // if (!choreographer.itEnabled && !choreographer.igcEnabled) { + // igcTextDataResponse.matches = []; + // } igcTextData = igcTextDataResponse; + // TODO - for each new match, + // check if existing igcTextData has one and only one match with the same error text and correction + // if so, keep the original match and discard the new one + // if not, add the new match to the existing igcTextData + // After fetching igc data, pre-call span details for each match optimistically. // This will make the loading of span details faster for the user if (igcTextData?.matches.isNotEmpty ?? false) { @@ -170,11 +175,9 @@ class IgcController { const int firstMatchIndex = 0; final PangeaMatch match = igcTextData!.matches[firstMatchIndex]; - if ( - match.isITStart && + if (match.isITStart && choreographer.itAutoPlayEnabled && - igcTextData != null - ) { + igcTextData != null) { choreographer.onITStart(igcTextData!.matches[firstMatchIndex]); return; } diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart index 8bd60270b5..225d2fec64 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -72,6 +72,7 @@ class ITController { /// if IGC isn't positive that text is full L1 then translate to L1 Future _setSourceText() async { + debugger(when: kDebugMode); // try { if (_itStartData == null || _itStartData!.text.isEmpty) { Sentry.addBreadcrumb( @@ -167,7 +168,7 @@ class ITController { if (isTranslationDone) { choreographer.altTranslator.setTranslationFeedback(); - choreographer.getLanguageHelp(true); + choreographer.getLanguageHelp(onlyTokensAndLanguageDetection: true); } else { getNextTranslationData(); } @@ -218,7 +219,6 @@ class ITController { Future onEditSourceTextSubmit(String newSourceText) async { try { - _isOpen = true; _isEditingSourceText = false; _itStartData = ITStartData(newSourceText, choreographer.l1LangCode); @@ -230,7 +230,6 @@ class ITController { _setSourceText(); getTranslationData(false); - } catch (err, stack) { debugger(when: kDebugMode); if (err is! http.Response) { diff --git a/lib/pangea/controllers/span_data_controller.dart b/lib/pangea/choreographer/controllers/span_data_controller.dart similarity index 100% rename from lib/pangea/controllers/span_data_controller.dart rename to lib/pangea/choreographer/controllers/span_data_controller.dart diff --git a/lib/pangea/choreographer/widgets/start_igc_button.dart b/lib/pangea/choreographer/widgets/start_igc_button.dart index 877158dd2d..e8625da955 100644 --- a/lib/pangea/choreographer/widgets/start_igc_button.dart +++ b/lib/pangea/choreographer/widgets/start_igc_button.dart @@ -91,8 +91,8 @@ class StartIGCButtonState extends State if (assistanceState != AssistanceState.fetching) { widget.controller.choreographer .getLanguageHelp( - false, - true, + onlyTokensAndLanguageDetection: false, + manual: true, ) .then((_) { if (widget.controller.choreographer.igc.igcTextData != null && diff --git a/lib/pangea/config/environment.dart b/lib/pangea/config/environment.dart index 4d43789996..de7039f9d3 100644 --- a/lib/pangea/config/environment.dart +++ b/lib/pangea/config/environment.dart @@ -5,7 +5,7 @@ class Environment { DateTime.utc(2023, 1, 25).isBefore(DateTime.now()); static String get fileName { - return ".env"; + return ".local_choreo.env"; } static bool get isStaging => synapsURL.contains("staging"); diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 5a2b9d02a8..75a18ad748 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -239,7 +239,7 @@ class MyAnalyticsController { } final List> recentMsgs = (await Future.wait(recentMsgFutures)).toList(); - final List recentActivityReconds = + final List recentActivityRecords = (await Future.wait(recentActivityFutures)) .expand((e) => e) .map((event) => PracticeActivityRecordEvent(event: event)) @@ -284,14 +284,14 @@ class MyAnalyticsController { } // get constructs for messages - final List constructContent = []; + final List recentConstructUses = []; for (final PangeaMessageEvent message in allRecentMessages) { - constructContent.addAll(message.allConstructUses); + recentConstructUses.addAll(message.allConstructUses); } // get constructs for practice activities final List>> constructFutures = []; - for (final PracticeActivityRecordEvent activity in recentActivityReconds) { + for (final PracticeActivityRecordEvent activity in recentActivityRecords) { final Timeline? timeline = timelineMap[activity.event.roomId!]; if (timeline == null) { debugger(when: kDebugMode); @@ -306,13 +306,13 @@ class MyAnalyticsController { final List> constructLists = await Future.wait(constructFutures); - constructContent.addAll(constructLists.expand((e) => e)); + recentConstructUses.addAll(constructLists.expand((e) => e)); //TODO - confirm that this is the correct construct content - debugger(when: kDebugMode); + debugger(when: kDebugMode && recentConstructUses.isNotEmpty); await analyticsRoom.sendConstructsEvent( - constructContent, + recentConstructUses, ); } } diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index b6702d7d29..28253d4198 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -656,106 +656,47 @@ class PangeaMessageEvent { } } - List get allConstructUses => - [...grammarConstructUses, ..._vocabUses]; - /// Returns a list of [PracticeActivityEvent] for the user's active l2. - List get practiceActivities { - final String? l2code = - MatrixState.pangeaController.languageController.activeL2Code(); - if (l2code == null) return []; - return practiceActivitiesByLangCode(l2code); - } + List get practiceActivities => + l2Code == null ? [] : practiceActivitiesByLangCode(l2Code!); - // List get activities => - //each match is turned into an activity that other students can access - //they're not told the answer but have to find it themselves - //the message has a blank piece which they fill in themselves - - /// [tokens] is the final list of tokens that were sent - /// if no ga or ta, - /// make wa use for each and return - /// else - /// for each saveable vocab in the final message - /// if vocab is contained in an accepted replacement, make ga use - /// if vocab is contained in ta choice, - /// if selected as choice, corIt - /// if written as customInput, corIt? (account for score in this) - /// for each it step - /// for each continuance - /// if not within the final message, save ignIT/incIT + /// all construct uses for the message, including vocab and grammar + List get allConstructUses => + [..._grammarConstructUses, ..._vocabUses]; + + /// get construct uses of type vocab for the message List get _vocabUses { + debugger(); final List uses = []; - if (event.roomId == null) return uses; - - List lemmasToVocabUses( - List lemmas, - ConstructUseTypeEnum type, - ) { - final List uses = []; - for (final lemma in lemmas) { - if (lemma.saveVocab) { - uses.add( - OneConstructUse( - useType: type, - chatId: event.roomId!, - timeStamp: event.originServerTs, - lemma: lemma.text, - form: lemma.form, - msgId: event.eventId, - constructType: ConstructTypeEnum.vocab, - ), - ); - } - } + // missing vital info so return. should not happen + if (event.roomId == null) { + debugger(when: kDebugMode); return uses; } - List getVocabUseForToken(PangeaToken token) { - if (originalSent?.choreo == null) { - final bool inUserL2 = originalSent?.langCode == l2Code; - return lemmasToVocabUses( - token.lemmas, - inUserL2 ? ConstructUseTypeEnum.wa : ConstructUseTypeEnum.unk, - ); - } - - // - for (final step in originalSent!.choreo!.choreoSteps) { - /// if 1) accepted match 2) token is in the replacement and 3) replacement - /// is in the overall step text, then token was a ga - if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted && - (step.acceptedOrIgnoredMatch!.match.choices?.any( - (r) => - r.value.contains(token.text.content) && - step.text.contains(r.value), - ) ?? - false)) { - return lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.ga); - } - if (step.itStep != null) { - final bool pickedThroughIT = step.itStep!.chosenContinuance?.text - .contains(token.text.content) ?? - false; - if (pickedThroughIT) { - return lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.corIt); - //PTODO - check if added via custom input in IT flow - } - } - } - return lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.wa); - } - - /// for each token, record whether selected in ga, ta, or wa + // for each token, record whether selected in ga, ta, or wa if (originalSent?.tokens != null) { for (final token in originalSent!.tokens!) { - uses.addAll(getVocabUseForToken(token)); + uses.addAll(_getVocabUseForToken(token)); } } - if (originalSent?.choreo == null) return uses; + // add construct uses related to IT use + uses.addAll(_itStepsToConstructUses); + return uses; + } + + /// Returns a list of [OneConstructUse] from itSteps for which the continuance + /// was selected or ignored. Correct selections are considered in the tokens + /// flow. Once all continuances have lemmas, we can do both correct and incorrect + /// in this flow. It actually doesn't do anything at all right now, because the + /// choregrapher is not returning lemmas for continuances. This is a TODO. + /// So currently only the lemmas can be gotten from the tokens for choices that + /// are actually in the final message. + List get _itStepsToConstructUses { + final List uses = []; for (final itStep in originalSent!.choreo!.itSteps) { for (final continuance in itStep.continuances) { // this seems to always be false for continuances right now @@ -767,23 +708,98 @@ class PangeaMessageEvent { //PTODO - account for end of flow score if (continuance.level != ChoreoConstants.levelThresholdForGreen) { uses.addAll( - lemmasToVocabUses(continuance.lemmas, ConstructUseTypeEnum.incIt), + _lemmasToVocabUses( + continuance.lemmas, + ConstructUseTypeEnum.incIt, + ), ); } } else { if (continuance.level != ChoreoConstants.levelThresholdForGreen) { uses.addAll( - lemmasToVocabUses(continuance.lemmas, ConstructUseTypeEnum.ignIt), + _lemmasToVocabUses( + continuance.lemmas, + ConstructUseTypeEnum.ignIt, + ), ); } } } } + return uses; + } + + /// Returns a list of [OneConstructUse] objects for the given [token] + /// If there is no [originalSent] or [originalSent.choreo], the [token] is + /// considered to be a [ConstructUseTypeEnum.wa] as long as it matches the target language. + /// Later on, we may want to consider putting it in some category of like 'pending' + /// If the [token] is in the [originalSent.choreo.acceptedOrIgnoredMatch], + /// it is considered to be a [ConstructUseTypeEnum.ga]. + /// If the [token] is in the [originalSent.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 _getVocabUseForToken(PangeaToken token) { + debugger(); + if (originalSent?.choreo == null) { + final bool inUserL2 = originalSent?.langCode == l2Code; + return _lemmasToVocabUses( + token.lemmas, + inUserL2 ? ConstructUseTypeEnum.wa : ConstructUseTypeEnum.unk, + ); + } + + for (final step in originalSent!.choreo!.choreoSteps) { + /// if 1) accepted match 2) token is in the replacement and 3) replacement + /// is in the overall step text, then token was a ga + if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted && + (step.acceptedOrIgnoredMatch!.match.choices?.any( + (r) => + r.value.contains(token.text.content) && + step.text.contains(r.value), + ) ?? + false)) { + return _lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.ga); + } + if (step.itStep != null) { + final bool pickedThroughIT = + step.itStep!.chosenContinuance?.text.contains(token.text.content) ?? + false; + if (pickedThroughIT) { + return _lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.corIt); + //PTODO - check if added via custom input in IT flow + } + } + } + return _lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.wa); + } + /// Convert a list of [lemmas] into a list of vocab uses + /// with the given [type] + List _lemmasToVocabUses( + List lemmas, + ConstructUseTypeEnum type, + ) { + final List uses = []; + for (final lemma in lemmas) { + if (lemma.saveVocab) { + uses.add( + OneConstructUse( + useType: type, + chatId: event.roomId!, + timeStamp: event.originServerTs, + lemma: lemma.text, + form: lemma.form, + msgId: event.eventId, + constructType: ConstructTypeEnum.vocab, + ), + ); + } + } return uses; } - List get grammarConstructUses { + /// get construct uses of type grammar for the message + List get _grammarConstructUses { final List uses = []; if (originalSent?.choreo == null || event.roomId == null) return uses; diff --git a/lib/pangea/repo/igc_repo.dart b/lib/pangea/repo/igc_repo.dart index 9517515d01..5f281abe61 100644 --- a/lib/pangea/repo/igc_repo.dart +++ b/lib/pangea/repo/igc_repo.dart @@ -89,7 +89,6 @@ class IGCRequestBody { String fullText; String userL1; String userL2; - bool tokensOnly; bool enableIT; bool enableIGC; @@ -99,7 +98,6 @@ class IGCRequestBody { required this.userL2, required this.enableIGC, required this.enableIT, - this.tokensOnly = false, }); Map toJson() => { @@ -108,6 +106,5 @@ class IGCRequestBody { ModelKey.userL2: userL2, "enable_it": enableIT, "enable_igc": enableIGC, - "tokens_only": tokensOnly, }; } From 25263317068e058e0f84d58f7cf1d252974f62c0 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Sun, 30 Jun 2024 12:08:30 -0400 Subject: [PATCH 5/7] using getIGCTextData to fetch tokens and languages if not present --- .../controllers/choreographer.dart | 57 +++----- .../controllers/igc_controller.dart | 137 +++++++----------- .../controllers/it_controller.dart | 13 +- .../controllers/my_analytics_controller.dart | 5 +- .../pangea_message_event.dart | 2 - lib/pangea/models/igc_text_data_model.dart | 26 ++-- lib/pangea/repo/igc_repo.dart | 6 +- 7 files changed, 105 insertions(+), 141 deletions(-) diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 77130f635e..42661b81e0 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -117,34 +117,39 @@ class Choreographer { // TODO - move this to somewhere such that the message can be cleared from the input field // before the language detection is complete. Otherwise, user is going to be waiting // in cases of slow internet or slow language detection - final String originalSentLangCode = langCodeOfCurrentText ?? - (await pangeaController.languageDetection.detectLanguage( - currentText, - pangeaController.languageController.userL2?.langCode, - pangeaController.languageController.userL1?.langCode, - )) - .bestDetection() - .langCode; - - final PangeaRepresentation originalSent = PangeaRepresentation( - langCode: originalSentLangCode, - text: currentText, - originalSent: true, - originalWritten: originalWritten == null, - ); + final String? originalSentLangCode = igc.igcTextData?.detectedLanguage; // TODO - why does both it and igc need to be enabled for choreo to be applicable? final ChoreoRecord? applicableChoreo = isITandIGCEnabled && igc.igcTextData != null ? choreoRecord : null; final UseType useType = useTypeCalculator(applicableChoreo); - debugPrint("use type in choreographer $useType"); + + // if tokens or language detection are not available, get them + // note that we probably need to move this to after we clear the input field + // or the user could experience some lag here. note that this call is being + // made after we've determined if we have an applicable choreo in order to + // say whether correction was run on the message. we may eventually want + // to edit the useType after + if (igc.igcTextData?.tokens == null || + igc.igcTextData?.detectedLanguage == null) { + await igc.getIGCTextData(onlyTokensAndLanguageDetection: true); + } + + final PangeaRepresentation originalSent = PangeaRepresentation( + langCode: originalSentLangCode ?? LanguageKeys.unknownLanguage, + text: currentText, + originalSent: true, + originalWritten: originalWritten == null, + ); + debugger(when: kDebugMode); chatController.send( // PTODO - turn this back on in conjunction with saving tokens // we need to save those tokens as well, in order for exchanges to work // properly. in an exchange, the other user will want // originalWritten: originalWritten, + originalSent: originalSent, tokensSent: igc.igcTextData?.tokens != null ? PangeaMessageTokens(tokens: igc.igcTextData!.tokens) @@ -170,7 +175,7 @@ class Choreographer { } choreoMode = ChoreoMode.it; itController.initializeIT( - ITStartData(_textController.text, igc.detectedLangCode), + ITStartData(_textController.text, igc.igcTextData?.detectedLanguage), ); itMatch.status = PangeaMatchStatus.accepted; @@ -195,7 +200,7 @@ class Choreographer { // this may be unnecessary now that tokens are not used // to allow click of words in the input field and we're getting this at the end // TODO - turn it off and tested that this is fine - igc.justGetTokensAndAddThemToIGCTextData(); + // igc.justGetTokensAndAddThemToIGCTextData(); // we set editType to keyboard here because that is the default for it // and we want to make sure that the next change is treated as a keyboard change @@ -499,22 +504,6 @@ class Choreographer { bool get editTypeIsKeyboard => EditType.keyboard == _textController.editType; - /// If there is applicable igcTextData, return the detected langCode - /// Otherwise, if the IT controller is open, return the user's L2 langCode - /// This second piece assumes that IT is being used to translate into the user's L2 - /// and could be spotty. It's a bit of a hack, and should be tested more. - String? get langCodeOfCurrentText { - if (igc.detectedLangCode != null) return igc.detectedLangCode!; - - // TODO - this is a bit of a hack, and should be tested more - // we should also check that user has not done customInput - if (itController.completedITSteps.isNotEmpty && itController.allCorrect) { - return l2LangCode!; - } - - return null; - } - setState() { if (!stateListener.isClosed) { stateListener.add(0); diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index a694c48a5f..fed4a31672 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -10,11 +10,8 @@ import 'package:fluffychat/pangea/repo/igc_repo.dart'; import 'package:fluffychat/pangea/widgets/igc/span_card.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -import '../../models/language_detection_model.dart'; import '../../models/span_card_model.dart'; -import '../../repo/tokens_repo.dart'; import '../../utils/error_handler.dart'; import '../../utils/overlay.dart'; @@ -64,22 +61,6 @@ class IgcController { return; } - //TO-DO: in api call, specify turning off IT and/or grammar checking - // UPDATE: This is now done in the API call. New TODO is to test this. - // if (!choreographer.igcEnabled) { - // igcTextDataResponse.matches = igcTextDataResponse.matches - // .where((match) => !match.isGrammarMatch) - // .toList(); - // } - // if (!choreographer.itEnabled) { - // igcTextDataResponse.matches = igcTextDataResponse.matches - // .where((match) => !match.isOutOfTargetMatch) - // .toList(); - // } - // if (!choreographer.itEnabled && !choreographer.igcEnabled) { - // igcTextDataResponse.matches = []; - // } - igcTextData = igcTextDataResponse; // TODO - for each new match, @@ -106,61 +87,61 @@ class IgcController { } } - Future justGetTokensAndAddThemToIGCTextData() async { - try { - if (igcTextData == null) { - debugger(when: kDebugMode); - choreographer.getLanguageHelp(); - return; - } - igcTextData!.loading = true; - choreographer.startLoading(); - if (igcTextData!.originalInput != choreographer.textController.text) { - debugger(when: kDebugMode); - ErrorHandler.logError( - m: "igcTextData fullText does not match current text", - s: StackTrace.current, - data: igcTextData!.toJson(), - ); - } - - if (choreographer.l1LangCode == null || - choreographer.l2LangCode == null) { - debugger(when: kDebugMode); - ErrorHandler.logError( - m: "l1LangCode and/or l2LangCode is null", - s: StackTrace.current, - data: { - "l1LangCode": choreographer.l1LangCode, - "l2LangCode": choreographer.l2LangCode, - }, - ); - return; - } - - final TokensResponseModel res = await TokensRepo.tokenize( - await choreographer.pangeaController.userController.accessToken, - TokensRequestModel( - fullText: igcTextData!.originalInput, - userL1: choreographer.l1LangCode!, - userL2: choreographer.l2LangCode!, - ), - ); - igcTextData?.tokens = res.tokens; - } catch (err, stack) { - debugger(when: kDebugMode); - choreographer.errorService.setError( - ChoreoError(type: ChoreoErrorType.unknown, raw: err), - ); - Sentry.addBreadcrumb( - Breadcrumb.fromJson({"igctextDdata": igcTextData?.toJson()}), - ); - ErrorHandler.logError(e: err, s: stack); - } finally { - igcTextData?.loading = false; - choreographer.stopLoading(); - } - } + // Future justGetTokensAndAddThemToIGCTextData() async { + // try { + // if (igcTextData == null) { + // debugger(when: kDebugMode); + // choreographer.getLanguageHelp(); + // return; + // } + // igcTextData!.loading = true; + // choreographer.startLoading(); + // if (igcTextData!.originalInput != choreographer.textController.text) { + // debugger(when: kDebugMode); + // ErrorHandler.logError( + // m: "igcTextData fullText does not match current text", + // s: StackTrace.current, + // data: igcTextData!.toJson(), + // ); + // } + + // if (choreographer.l1LangCode == null || + // choreographer.l2LangCode == null) { + // debugger(when: kDebugMode); + // ErrorHandler.logError( + // m: "l1LangCode and/or l2LangCode is null", + // s: StackTrace.current, + // data: { + // "l1LangCode": choreographer.l1LangCode, + // "l2LangCode": choreographer.l2LangCode, + // }, + // ); + // return; + // } + + // final TokensResponseModel res = await TokensRepo.tokenize( + // await choreographer.pangeaController.userController.accessToken, + // TokensRequestModel( + // fullText: igcTextData!.originalInput, + // userL1: choreographer.l1LangCode!, + // userL2: choreographer.l2LangCode!, + // ), + // ); + // igcTextData?.tokens = res.tokens; + // } catch (err, stack) { + // debugger(when: kDebugMode); + // choreographer.errorService.setError( + // ChoreoError(type: ChoreoErrorType.unknown, raw: err), + // ); + // Sentry.addBreadcrumb( + // Breadcrumb.fromJson({"igctextDdata": igcTextData?.toJson()}), + // ); + // ErrorHandler.logError(e: err, s: stack); + // } finally { + // igcTextData?.loading = false; + // choreographer.stopLoading(); + // } + // } void showFirstMatch(BuildContext context) { if (igcTextData == null || igcTextData!.matches.isEmpty) { @@ -218,14 +199,6 @@ class IgcController { return true; } - String? get detectedLangCode { - if (!hasRelevantIGCTextData) return null; - - final LanguageDetection first = igcTextData!.detections.first; - - return first.langCode; - } - clear() { igcTextData = null; spanDataController.clearCache(); diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart index 225d2fec64..9e70287cd7 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -72,8 +72,6 @@ class ITController { /// if IGC isn't positive that text is full L1 then translate to L1 Future _setSourceText() async { - debugger(when: kDebugMode); - // try { if (_itStartData == null || _itStartData!.text.isEmpty) { Sentry.addBreadcrumb( Breadcrumb( @@ -98,21 +96,12 @@ class ITController { request: FullTextTranslationRequestModel( text: _itStartData!.text, tgtLang: choreographer.l1LangCode!, - srcLang: choreographer.l2LangCode, + srcLang: _itStartData!.langCode, userL1: choreographer.l1LangCode!, userL2: choreographer.l2LangCode!, ), ); sourceText = res.bestTranslation; - // } catch (err, stack) { - // debugger(when: kDebugMode); - // if (_itStartData?.text.isNotEmpty ?? false) { - // ErrorHandler.logError(e: err, s: stack); - // sourceText = _itStartData!.text; - // } else { - // rethrow; - // } - // } } // used 1) at very beginning (with custom input = null) diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 75a18ad748..14b3555a16 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -309,7 +309,10 @@ class MyAnalyticsController { recentConstructUses.addAll(constructLists.expand((e) => e)); //TODO - confirm that this is the correct construct content - debugger(when: kDebugMode && recentConstructUses.isNotEmpty); + debugger( + when: kDebugMode && + (recentPangeaMessageEvents.isNotEmpty || + recentActivityRecords.isNotEmpty)); await analyticsRoom.sendConstructsEvent( recentConstructUses, diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 28253d4198..2f529e5280 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -666,7 +666,6 @@ class PangeaMessageEvent { /// get construct uses of type vocab for the message List get _vocabUses { - debugger(); final List uses = []; // missing vital info so return. should not happen @@ -739,7 +738,6 @@ class PangeaMessageEvent { /// 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 _getVocabUseForToken(PangeaToken token) { - debugger(); if (originalSent?.choreo == null) { final bool inUserL2 = originalSent?.langCode == l2Code; return _lemmasToVocabUses( diff --git a/lib/pangea/models/igc_text_data_model.dart b/lib/pangea/models/igc_text_data_model.dart index 6a3eec96e0..fb93885149 100644 --- a/lib/pangea/models/igc_text_data_model.dart +++ b/lib/pangea/models/igc_text_data_model.dart @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:fluffychat/pangea/controllers/language_detection_controller.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/models/span_card_model.dart'; @@ -13,12 +14,11 @@ import 'package:matrix/matrix.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import '../constants/model_keys.dart'; -import 'language_detection_model.dart'; // import 'package:language_tool/language_tool.dart'; class IGCTextData { - List detections; + LanguageDetectionResponse detections; String originalInput; String? fullTextCorrection; List tokens; @@ -42,6 +42,17 @@ class IGCTextData { }); factory IGCTextData.fromJson(Map json) { + // changing this to allow for use of the LanguageDetectionResponse methods + // TODO - change API after we're sure all clients are updated. not urgent. + final LanguageDetectionResponse detections = + json[_detectionsKey] is Iterable + ? LanguageDetectionResponse.fromJson({ + "detections": json[_detectionsKey], + "full_text": json["original_input"], + }) + : LanguageDetectionResponse.fromJson( + json[_detectionsKey] as Map); + return IGCTextData( tokens: (json[_tokensKey] as Iterable) .map( @@ -59,12 +70,7 @@ class IGCTextData { .toList() .cast() : [], - detections: (json[_detectionsKey] as Iterable) - .map( - (e) => LanguageDetection.fromJson(e as Map), - ) - .toList() - .cast(), + detections: detections, originalInput: json["original_input"], fullTextCorrection: json["full_text_correction"], userL1: json[ModelKey.userL1], @@ -79,7 +85,7 @@ class IGCTextData { static const String _detectionsKey = "detections"; Map toJson() => { - _detectionsKey: detections.map((e) => e.toJson()).toList(), + _detectionsKey: detections.toJson(), "original_input": originalInput, "full_text_correction": fullTextCorrection, _tokensKey: tokens.map((e) => e.toJson()).toList(), @@ -90,6 +96,8 @@ class IGCTextData { "enable_igc": enableIGC, }; + String get detectedLanguage => detections.bestDetection().langCode; + // reconstruct fullText based on accepted match //update offsets in existing matches to reflect the change //if existing matches overlap with the accepted one, remove them?? diff --git a/lib/pangea/repo/igc_repo.dart b/lib/pangea/repo/igc_repo.dart index 5f281abe61..d3dbbcb6bc 100644 --- a/lib/pangea/repo/igc_repo.dart +++ b/lib/pangea/repo/igc_repo.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:fluffychat/pangea/controllers/language_detection_controller.dart'; import 'package:fluffychat/pangea/models/language_detection_model.dart'; import 'package:fluffychat/pangea/models/lemma.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; @@ -39,7 +40,10 @@ class IgcRepo { await Future.delayed(const Duration(seconds: 2)); final IGCTextData igcTextData = IGCTextData( - detections: [LanguageDetection(langCode: "en", confidence: 0.99)], + detections: LanguageDetectionResponse( + detections: [LanguageDetection(langCode: "en", confidence: 0.99)], + fullText: "This be a sample text", + ), tokens: [ PangeaToken( text: PangeaTokenText(content: "This", offset: 0, length: 4), From c6d3f36805eaf8140915b77f8012abcaedd67b08 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Sun, 30 Jun 2024 18:31:53 -0400 Subject: [PATCH 6/7] saving of tokens, changing of top language calculation, documentation --- lib/pages/chat/chat.dart | 4 -- lib/pages/chat/events/message.dart | 2 +- .../controllers/choreographer.dart | 48 ++++++--------- lib/pangea/constants/model_keys.dart | 1 - .../language_detection_controller.dart | 13 ++-- .../controllers/my_analytics_controller.dart | 13 ++-- lib/pangea/enum/use_type.dart | 16 ----- .../events_extension.dart | 4 +- .../pangea_room_extension.dart | 3 - .../pangea_message_event.dart | 61 +++++++++++-------- .../analytics/summary_analytics_model.dart | 2 +- lib/pangea/models/igc_text_data_model.dart | 15 ++++- lib/pangea/models/lemma.dart | 7 +++ lib/pangea/utils/firebase_analytics.dart | 4 +- lib/pangea/widgets/chat/overlay_message.dart | 2 +- lib/pangea/widgets/new_group/vocab_list.dart | 9 ++- 16 files changed, 96 insertions(+), 108 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 13f09ffb6c..5629d809ef 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -16,7 +16,6 @@ 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/pangea_controller.dart'; -import 'package:fluffychat/pangea/enum/use_type.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; @@ -586,7 +585,6 @@ class ChatController extends State PangeaMessageTokens? tokensSent, PangeaMessageTokens? tokensWritten, ChoreoRecord? choreo, - UseType? useType, }) async { // Pangea# if (sendController.text.trim().isEmpty) return; @@ -630,7 +628,6 @@ class ChatController extends State tokensSent: tokensSent, tokensWritten: tokensWritten, choreo: choreo, - useType: useType, ) .then( (String? msgEventId) async { @@ -644,7 +641,6 @@ class ChatController extends State GoogleAnalytics.sendMessage( room.id, room.classCode, - useType ?? UseType.un, ); if (msgEventId == null) { diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index c5756438c0..3b2c1b2fb5 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -470,7 +470,7 @@ class Message extends StatelessWidget { ?.showUseType ?? false) ...[ pangeaMessageEvent! - .useType + .msgUseType .iconView( context, textColor diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 42661b81e0..3e7668323b 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -24,7 +24,6 @@ import 'package:flutter/material.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import '../../../widgets/matrix.dart'; -import '../../enum/use_type.dart'; import '../../models/choreo_record.dart'; import '../../models/language_model.dart'; import '../../models/pangea_match_model.dart'; @@ -108,27 +107,16 @@ class Choreographer { originalSent: false, ) : null; - //TODO - confirm that IT is indeed making sure the message is in the user's L1 - - // if the message has not been processed to determine its language - // then run it through the language detection endpoint. If the detection - // confidence is high enough, use that language code as the message's language - // to save that pangea representation - // TODO - move this to somewhere such that the message can be cleared from the input field - // before the language detection is complete. Otherwise, user is going to be waiting - // in cases of slow internet or slow language detection - final String? originalSentLangCode = igc.igcTextData?.detectedLanguage; // TODO - why does both it and igc need to be enabled for choreo to be applicable? - final ChoreoRecord? applicableChoreo = - isITandIGCEnabled && igc.igcTextData != null ? choreoRecord : null; - - final UseType useType = useTypeCalculator(applicableChoreo); - - // if tokens or language detection are not available, get them - // note that we probably need to move this to after we clear the input field - // or the user could experience some lag here. note that this call is being - // made after we've determined if we have an applicable choreo in order to + // final ChoreoRecord? applicableChoreo = + // isITandIGCEnabled && igc.igcTextData != null ? choreoRecord : null; + + // if tokens or language detection are not available, we should get them + // notes + // 1) we probably need to move this to after we clear the input field + // or the user could experience some lag here. + // 2) that this call is being made after we've determined if we have an applicable choreo in order to // say whether correction was run on the message. we may eventually want // to edit the useType after if (igc.igcTextData?.tokens == null || @@ -137,26 +125,24 @@ class Choreographer { } final PangeaRepresentation originalSent = PangeaRepresentation( - langCode: originalSentLangCode ?? LanguageKeys.unknownLanguage, + langCode: + igc.igcTextData?.detectedLanguage ?? LanguageKeys.unknownLanguage, text: currentText, originalSent: true, originalWritten: originalWritten == null, ); - debugger(when: kDebugMode); + + final PangeaMessageTokens? tokensSent = igc.igcTextData?.tokens != null + ? PangeaMessageTokens(tokens: igc.igcTextData!.tokens) + : null; chatController.send( - // PTODO - turn this back on in conjunction with saving tokens - // we need to save those tokens as well, in order for exchanges to work - // properly. in an exchange, the other user will want // originalWritten: originalWritten, - originalSent: originalSent, - tokensSent: igc.igcTextData?.tokens != null - ? PangeaMessageTokens(tokens: igc.igcTextData!.tokens) - : null, + tokensSent: tokensSent, //TODO - save originalwritten tokens - choreo: applicableChoreo, - useType: useType, + // choreo: applicableChoreo, + choreo: choreoRecord, ); clear(); diff --git a/lib/pangea/constants/model_keys.dart b/lib/pangea/constants/model_keys.dart index ca59d89b74..372e726060 100644 --- a/lib/pangea/constants/model_keys.dart +++ b/lib/pangea/constants/model_keys.dart @@ -66,7 +66,6 @@ class ModelKey { static const String tokensSent = "tokens_sent"; static const String tokensWritten = "tokens_written"; static const String choreoRecord = "choreo_record"; - static const String useType = "use_type"; static const String baseDefinition = "base_definition"; static const String targetDefinition = "target_definition"; diff --git a/lib/pangea/controllers/language_detection_controller.dart b/lib/pangea/controllers/language_detection_controller.dart index 08f38ea2c9..a3e07b0a35 100644 --- a/lib/pangea/controllers/language_detection_controller.dart +++ b/lib/pangea/controllers/language_detection_controller.dart @@ -76,15 +76,20 @@ class LanguageDetectionResponse { }; } - LanguageDetection get _bestDetection { + /// Return the highest confidence detection. + /// If there are no detections, the unknown language detection is returned. + LanguageDetection get highestConfidenceDetection { detections.sort((a, b) => b.confidence.compareTo(a.confidence)); return detections.firstOrNull ?? unknownLanguageDetection; } - LanguageDetection bestDetection({double? threshold}) => - _bestDetection.confidence >= + /// Returns the highest validated detection based on the confidence threshold. + /// If the highest confidence detection is below the threshold, the unknown language + /// detection is returned. + LanguageDetection highestValidatedDetection({double? threshold}) => + highestConfidenceDetection.confidence >= (threshold ?? languageDetectionConfidenceThreshold) - ? _bestDetection + ? highestConfidenceDetection : unknownLanguageDetection; } diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 14b3555a16..1ae6f2a5ba 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -216,8 +216,6 @@ class MyAnalyticsController { .where((room) => !room.isSpace && !room.isAnalyticsRoom) .toList(); - final DateTime now = DateTime.now(); - // get the recent message events and activity records for each chat final List>> recentMsgFutures = []; final List>> recentActivityFutures = []; @@ -309,10 +307,13 @@ class MyAnalyticsController { recentConstructUses.addAll(constructLists.expand((e) => e)); //TODO - confirm that this is the correct construct content - debugger( - when: kDebugMode && - (recentPangeaMessageEvents.isNotEmpty || - recentActivityRecords.isNotEmpty)); + // debugger( + // when: kDebugMode, + // ); + // ; debugger( + // when: kDebugMode && + // (allRecentMessages.isNotEmpty || recentActivityRecords.isNotEmpty), + // ); await analyticsRoom.sendConstructsEvent( recentConstructUses, diff --git a/lib/pangea/enum/use_type.dart b/lib/pangea/enum/use_type.dart index 771b262209..56a4fa3b0f 100644 --- a/lib/pangea/enum/use_type.dart +++ b/lib/pangea/enum/use_type.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; -import '../models/choreo_record.dart'; import '../utils/bot_style.dart'; enum UseType { wa, ta, ga, un } @@ -93,17 +91,3 @@ extension UseTypeMethods on UseType { } } } - -UseType useTypeCalculator( - ChoreoRecord? choreoRecord, -) { - if (choreoRecord == null) { - return UseType.un; - } else if (choreoRecord.includedIT) { - return UseType.ta; - } else if (choreoRecord.hasAcceptedMatches) { - return UseType.ga; - } else { - return UseType.wa; - } -} diff --git a/lib/pangea/extensions/pangea_room_extension/events_extension.dart b/lib/pangea/extensions/pangea_room_extension/events_extension.dart index 0f40da5c7f..ce9d3451cb 100644 --- a/lib/pangea/extensions/pangea_room_extension/events_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/events_extension.dart @@ -229,7 +229,6 @@ extension EventsRoomExtension on Room { PangeaMessageTokens? tokensSent, PangeaMessageTokens? tokensWritten, ChoreoRecord? choreo, - UseType? useType, }) { // if (parseCommands) { // return client.parseAndRunCommand(this, message, @@ -247,7 +246,6 @@ extension EventsRoomExtension on Room { ModelKey.originalWritten: originalWritten?.toJson(), ModelKey.tokensSent: tokensSent?.toJson(), ModelKey.tokensWritten: tokensWritten?.toJson(), - ModelKey.useType: useType?.string, }; if (parseMarkdown) { final html = markdown( @@ -347,7 +345,7 @@ extension EventsRoomExtension on Room { RecentMessageRecord( eventId: event.eventId, chatId: id, - useType: pMsgEvent.useType, + useType: pMsgEvent.msgUseType, time: event.originServerTs, ), ); diff --git a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart index 78ecb9cc0a..21f5abac57 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -34,7 +34,6 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import '../../../config/app_config.dart'; import '../../constants/pangea_event_types.dart'; -import '../../enum/use_type.dart'; import '../../models/choreo_record.dart'; import '../../models/representation_content_model.dart'; import '../client_extension/client_extension.dart'; @@ -181,7 +180,6 @@ extension PangeaRoom on Room { PangeaMessageTokens? tokensSent, PangeaMessageTokens? tokensWritten, ChoreoRecord? choreo, - UseType? useType, }) => _pangeaSendTextEvent( message, @@ -198,7 +196,6 @@ extension PangeaRoom on Room { tokensSent: tokensSent, tokensWritten: tokensWritten, choreo: choreo, - useType: useType, ); Future updateStateEvent(Event stateEvent) => diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 2f529e5280..e0820d665d 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -37,7 +37,6 @@ class PangeaMessageEvent { late Event _event; final Timeline timeline; final bool ownMessage; - bool _isValidPangeaMessageEvent = true; PangeaMessageEvent({ required Event event, @@ -45,7 +44,7 @@ class PangeaMessageEvent { required this.ownMessage, }) { if (event.type != EventTypes.Message) { - _isValidPangeaMessageEvent = false; + debugger(when: kDebugMode); ErrorHandler.logError( m: "${event.type} should not be used to make a PangeaMessageEvent", ); @@ -548,7 +547,18 @@ class PangeaMessageEvent { originalWritten: false, ); - UseType get useType => useTypeCalculator(originalSent?.choreo); + UseType get msgUseType { + final ChoreoRecord? choreoRecord = originalSent?.choreo; + if (choreoRecord == null) { + return UseType.un; + } else if (choreoRecord.includedIT) { + return UseType.ta; + } else if (choreoRecord.hasAcceptedMatches) { + return UseType.ga; + } else { + return UseType.wa; + } + } bool get showUseType => !ownMessage && @@ -662,30 +672,7 @@ class PangeaMessageEvent { /// all construct uses for the message, including vocab and grammar List get allConstructUses => - [..._grammarConstructUses, ..._vocabUses]; - - /// get construct uses of type vocab for the message - List get _vocabUses { - final List uses = []; - - // missing vital info so return. should not happen - if (event.roomId == null) { - debugger(when: kDebugMode); - return uses; - } - - // for each token, record whether selected in ga, ta, or wa - if (originalSent?.tokens != null) { - for (final token in originalSent!.tokens!) { - uses.addAll(_getVocabUseForToken(token)); - } - } - - // add construct uses related to IT use - uses.addAll(_itStepsToConstructUses); - - return uses; - } + [..._grammarConstructUses, ..._vocabUses, ..._itStepsToConstructUses]; /// Returns a list of [OneConstructUse] from itSteps for which the continuance /// was selected or ignored. Correct selections are considered in the tokens @@ -696,6 +683,8 @@ class PangeaMessageEvent { /// are actually in the final message. List get _itStepsToConstructUses { final List uses = []; + if (originalSent?.choreo == null) return uses; + for (final itStep in originalSent!.choreo!.itSteps) { for (final continuance in itStep.continuances) { // this seems to always be false for continuances right now @@ -728,6 +717,24 @@ class PangeaMessageEvent { return uses; } + /// get construct uses of type vocab for the message + List get _vocabUses { + final List uses = []; + + // missing vital info so return + if (event.roomId == null || originalSent?.tokens == null) { + debugger(when: kDebugMode); + return uses; + } + + // for each token, record whether selected in ga, ta, or wa + for (final token in originalSent!.tokens!) { + uses.addAll(_getVocabUseForToken(token)); + } + + return uses; + } + /// Returns a list of [OneConstructUse] objects for the given [token] /// If there is no [originalSent] or [originalSent.choreo], the [token] is /// considered to be a [ConstructUseTypeEnum.wa] as long as it matches the target language. diff --git a/lib/pangea/models/analytics/summary_analytics_model.dart b/lib/pangea/models/analytics/summary_analytics_model.dart index b09d0a8700..0b8e4b27ce 100644 --- a/lib/pangea/models/analytics/summary_analytics_model.dart +++ b/lib/pangea/models/analytics/summary_analytics_model.dart @@ -50,7 +50,7 @@ class SummaryAnalyticsModel extends AnalyticsModel { (msg) => RecentMessageRecord( eventId: msg.eventId, chatId: msg.room.id, - useType: msg.useType, + useType: msg.msgUseType, time: msg.originServerTs, ), ) diff --git a/lib/pangea/models/igc_text_data_model.dart b/lib/pangea/models/igc_text_data_model.dart index fb93885149..442bf4a602 100644 --- a/lib/pangea/models/igc_text_data_model.dart +++ b/lib/pangea/models/igc_text_data_model.dart @@ -51,7 +51,8 @@ class IGCTextData { "full_text": json["original_input"], }) : LanguageDetectionResponse.fromJson( - json[_detectionsKey] as Map); + json[_detectionsKey] as Map, + ); return IGCTextData( tokens: (json[_tokensKey] as Iterable) @@ -96,7 +97,17 @@ class IGCTextData { "enable_igc": enableIGC, }; - String get detectedLanguage => detections.bestDetection().langCode; + /// if we haven't run IGC or IT or there are no matches, we use the highest validated detection + /// from [LanguageDetectionResponse.highestValidatedDetection] + /// if we have run igc/it and there are no matches, we can relax the threshold + /// and use the highest confidence detection + String get detectedLanguage { + if (!(enableIGC && enableIT) || matches.isNotEmpty) { + return detections.highestValidatedDetection().langCode; + } else { + return detections.highestConfidenceDetection.langCode; + } + } // reconstruct fullText based on accepted match //update offsets in existing matches to reflect the change diff --git a/lib/pangea/models/lemma.dart b/lib/pangea/models/lemma.dart index 2ad0b29509..56f23d8769 100644 --- a/lib/pangea/models/lemma.dart +++ b/lib/pangea/models/lemma.dart @@ -1,6 +1,13 @@ +/// Represents a lemma object class Lemma { + /// [text] ex "ir" - text of the lemma of the word final String text; + + /// [form] ex "vamos" - conjugated form of the lemma and as it appeared in some original text final String form; + + /// [saveVocab] true - whether to save the lemma to the user's vocabulary + /// vocab that are not saved: emails, urls, numbers, punctuation, etc. final bool saveVocab; Lemma({required this.text, required this.saveVocab, required this.form}); diff --git a/lib/pangea/utils/firebase_analytics.dart b/lib/pangea/utils/firebase_analytics.dart index 59485d35b9..323a7a1723 100644 --- a/lib/pangea/utils/firebase_analytics.dart +++ b/lib/pangea/utils/firebase_analytics.dart @@ -4,7 +4,6 @@ import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; import 'package:flutter/widgets.dart'; import '../../config/firebase_options.dart'; -import '../enum/use_type.dart'; // PageRoute import @@ -90,13 +89,12 @@ class GoogleAnalytics { logEvent('join_group', parameters: {'group_id': classCode}); } - static sendMessage(String chatRoomId, String classCode, UseType useType) { + static sendMessage(String chatRoomId, String classCode) { logEvent( 'sent_message', parameters: { "chat_id": chatRoomId, 'group_id': classCode, - "message_type": useType.toString(), }, ); } diff --git a/lib/pangea/widgets/chat/overlay_message.dart b/lib/pangea/widgets/chat/overlay_message.dart index d7c99f07bd..5f3d46c7e1 100644 --- a/lib/pangea/widgets/chat/overlay_message.dart +++ b/lib/pangea/widgets/chat/overlay_message.dart @@ -166,7 +166,7 @@ class OverlayMessage extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ if (pangeaMessageEvent.showUseType) ...[ - pangeaMessageEvent.useType.iconView( + pangeaMessageEvent.msgUseType.iconView( context, textColor.withAlpha(164), ), diff --git a/lib/pangea/widgets/new_group/vocab_list.dart b/lib/pangea/widgets/new_group/vocab_list.dart index 240e99a859..67089145ec 100644 --- a/lib/pangea/widgets/new_group/vocab_list.dart +++ b/lib/pangea/widgets/new_group/vocab_list.dart @@ -1,9 +1,8 @@ +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import '../../models/chat_topic_model.dart'; import '../../models/lemma.dart'; import '../../repo/topic_data_repo.dart'; @@ -76,7 +75,7 @@ class ChatVocabularyList extends StatelessWidget { for (final word in topic.vocab) Chip( labelStyle: Theme.of(context).textTheme.bodyMedium, - label: Text(word.form), + label: Text(word.text), onDeleted: () { onChanged(topic.vocab..remove(word)); }, @@ -464,7 +463,7 @@ class PromptsFieldState extends State { // button to call API ElevatedButton.icon( - icon: BotFace( + icon: const BotFace( width: 50.0, expression: BotExpression.idle, ), From e6fa2b1b4b6c361fdf08c370eb4ad469ec689a35 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 1 Jul 2024 11:57:17 -0400 Subject: [PATCH 7/7] changed messages since update back to 10, re-enabled getting tokens after accepting matching in IGC, and allowed saving of initial empty analytics event --- .../controllers/choreographer.dart | 2 +- .../controllers/igc_controller.dart | 118 +++++++++--------- .../controllers/my_analytics_controller.dart | 10 +- .../room_analytics_extension.dart | 3 - .../practice_activity_content.dart | 66 +--------- 5 files changed, 66 insertions(+), 133 deletions(-) diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 3e7668323b..66e11808b0 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -186,7 +186,7 @@ class Choreographer { // this may be unnecessary now that tokens are not used // to allow click of words in the input field and we're getting this at the end // TODO - turn it off and tested that this is fine - // igc.justGetTokensAndAddThemToIGCTextData(); + igc.justGetTokensAndAddThemToIGCTextData(); // we set editType to keyboard here because that is the default for it // and we want to make sure that the next change is treated as a keyboard change diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index fed4a31672..533b36c83e 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -7,9 +7,11 @@ import 'package:fluffychat/pangea/choreographer/controllers/span_data_controller import 'package:fluffychat/pangea/models/igc_text_data_model.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; import 'package:fluffychat/pangea/repo/igc_repo.dart'; +import 'package:fluffychat/pangea/repo/tokens_repo.dart'; import 'package:fluffychat/pangea/widgets/igc/span_card.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import '../../models/span_card_model.dart'; import '../../utils/error_handler.dart'; @@ -32,12 +34,6 @@ class IgcController { try { if (choreographer.currentText.isEmpty) return clear(); - // the error spans are going to be reloaded, so clear the cache - // @ggurdin: Why is this separate from the clear() call? - // Also, if the spans are equal according the to the equals method, why not reuse the cached span data? - // It seems this would save some calls if the user makes some tiny changes to the text that don't - // change the matches at all. - spanDataController.clearCache(); debugPrint('getIGCTextData called with ${choreographer.currentText}'); debugPrint( 'getIGCTextData called with tokensOnly = $onlyTokensAndLanguageDetection', @@ -87,61 +83,61 @@ class IgcController { } } - // Future justGetTokensAndAddThemToIGCTextData() async { - // try { - // if (igcTextData == null) { - // debugger(when: kDebugMode); - // choreographer.getLanguageHelp(); - // return; - // } - // igcTextData!.loading = true; - // choreographer.startLoading(); - // if (igcTextData!.originalInput != choreographer.textController.text) { - // debugger(when: kDebugMode); - // ErrorHandler.logError( - // m: "igcTextData fullText does not match current text", - // s: StackTrace.current, - // data: igcTextData!.toJson(), - // ); - // } - - // if (choreographer.l1LangCode == null || - // choreographer.l2LangCode == null) { - // debugger(when: kDebugMode); - // ErrorHandler.logError( - // m: "l1LangCode and/or l2LangCode is null", - // s: StackTrace.current, - // data: { - // "l1LangCode": choreographer.l1LangCode, - // "l2LangCode": choreographer.l2LangCode, - // }, - // ); - // return; - // } - - // final TokensResponseModel res = await TokensRepo.tokenize( - // await choreographer.pangeaController.userController.accessToken, - // TokensRequestModel( - // fullText: igcTextData!.originalInput, - // userL1: choreographer.l1LangCode!, - // userL2: choreographer.l2LangCode!, - // ), - // ); - // igcTextData?.tokens = res.tokens; - // } catch (err, stack) { - // debugger(when: kDebugMode); - // choreographer.errorService.setError( - // ChoreoError(type: ChoreoErrorType.unknown, raw: err), - // ); - // Sentry.addBreadcrumb( - // Breadcrumb.fromJson({"igctextDdata": igcTextData?.toJson()}), - // ); - // ErrorHandler.logError(e: err, s: stack); - // } finally { - // igcTextData?.loading = false; - // choreographer.stopLoading(); - // } - // } + Future justGetTokensAndAddThemToIGCTextData() async { + try { + if (igcTextData == null) { + debugger(when: kDebugMode); + choreographer.getLanguageHelp(); + return; + } + igcTextData!.loading = true; + choreographer.startLoading(); + if (igcTextData!.originalInput != choreographer.textController.text) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "igcTextData fullText does not match current text", + s: StackTrace.current, + data: igcTextData!.toJson(), + ); + } + + if (choreographer.l1LangCode == null || + choreographer.l2LangCode == null) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "l1LangCode and/or l2LangCode is null", + s: StackTrace.current, + data: { + "l1LangCode": choreographer.l1LangCode, + "l2LangCode": choreographer.l2LangCode, + }, + ); + return; + } + + final TokensResponseModel res = await TokensRepo.tokenize( + await choreographer.pangeaController.userController.accessToken, + TokensRequestModel( + fullText: igcTextData!.originalInput, + userL1: choreographer.l1LangCode!, + userL2: choreographer.l2LangCode!, + ), + ); + igcTextData?.tokens = res.tokens; + } catch (err, stack) { + debugger(when: kDebugMode); + choreographer.errorService.setError( + ChoreoError(type: ChoreoErrorType.unknown, raw: err), + ); + Sentry.addBreadcrumb( + Breadcrumb.fromJson({"igctextDdata": igcTextData?.toJson()}), + ); + ErrorHandler.logError(e: err, s: stack); + } finally { + igcTextData?.loading = false; + choreographer.stopLoading(); + } + } void showFirstMatch(BuildContext context) { if (igcTextData == null || igcTextData!.matches.isEmpty) { diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 1ae6f2a5ba..535af6b8b4 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -24,7 +24,7 @@ class MyAnalyticsController { /// the max number of messages that will be cached before /// an automatic update is triggered - final int _maxMessagesCached = 1; + final int _maxMessagesCached = 10; /// the number of minutes before an automatic update is triggered final int _minutesBeforeUpdate = 5; @@ -315,8 +315,10 @@ class MyAnalyticsController { // (allRecentMessages.isNotEmpty || recentActivityRecords.isNotEmpty), // ); - await analyticsRoom.sendConstructsEvent( - recentConstructUses, - ); + if (recentConstructUses.isNotEmpty) { + await analyticsRoom.sendConstructsEvent( + recentConstructUses, + ); + } } } diff --git a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart index 34370306ec..a27526a2b7 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -253,8 +253,6 @@ extension AnalyticsRoomExtension on Room { Future sendSummaryAnalyticsEvent( List records, ) async { - if (records.isEmpty) return null; - final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel( messages: records, ); @@ -268,7 +266,6 @@ extension AnalyticsRoomExtension on Room { Future sendConstructsEvent( List uses, ) async { - if (uses.isEmpty) return null; final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel( uses: uses, ); diff --git a/lib/pangea/widgets/practice_activity/practice_activity_content.dart b/lib/pangea/widgets/practice_activity/practice_activity_content.dart index 9bdc95eea0..6de31829c9 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_content.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_content.dart @@ -1,7 +1,5 @@ import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; -import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart'; -import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; import 'package:flutter/material.dart'; @@ -18,70 +16,10 @@ class PracticeActivity extends StatefulWidget { }); @override - MessagePracticeActivityContentState createState() => - MessagePracticeActivityContentState(); + PracticeActivityContentState createState() => PracticeActivityContentState(); } -class MessagePracticeActivityContentState extends State { - int? selectedChoiceIndex; - PracticeActivityRecordModel? recordModel; - bool recordSubmittedThisSession = false; - bool recordSubmittedPreviousSession = false; - - PracticeActivityEvent get practiceEvent => widget.practiceEvent; - - @override - void initState() { - super.initState(); - initalizeActivity(); - } - - @override - void didUpdateWidget(covariant PracticeActivity oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.practiceEvent.event.eventId != - widget.practiceEvent.event.eventId) { - initalizeActivity(); - } - } - - void initalizeActivity() { - final PracticeActivityRecordEvent? recordEvent = - widget.practiceEvent.userRecord; - if (recordEvent?.record == null) { - recordModel = PracticeActivityRecordModel( - question: - widget.practiceEvent.practiceActivity.multipleChoice!.question, - ); - } else { - recordModel = recordEvent!.record; - - //Note that only MultipleChoice activities will have this so we probably should move this logic to the MultipleChoiceActivity widget - selectedChoiceIndex = recordModel?.latestResponse?.text != null - ? widget.practiceEvent.practiceActivity.multipleChoice - ?.choiceIndex(recordModel!.latestResponse!.text!) - : null; - - recordSubmittedPreviousSession = true; - recordSubmittedThisSession = true; - } - setState(() {}); - } - - void updateChoice(int index) { - setState(() { - selectedChoiceIndex = index; - recordModel!.addResponse( - score: widget.practiceEvent.practiceActivity.multipleChoice! - .isCorrect(index) - ? 1 - : 0, - text: widget - .practiceEvent.practiceActivity.multipleChoice!.choices[index], - ); - }); - } - +class PracticeActivityContentState extends State { Widget get activityWidget { switch (widget.practiceEvent.practiceActivity.activityType) { case ActivityTypeEnum.multipleChoice: