diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index c246cfccb2..93d4fc0c75 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3111,7 +3111,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 8631f49afe..ac6b30c762 100644 --- a/assets/l10n/intl_es.arb +++ b/assets/l10n/intl_es.arb @@ -4529,7 +4529,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/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/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 852065acba..2ba6a09573 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -2,7 +2,7 @@ import 'package:animations/animations.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.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/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/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/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 45659a1d0c..66e11808b0 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'; @@ -25,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'; @@ -95,63 +93,59 @@ 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( - currentText, - pangeaController.languageController.userL2?.langCode, - pangeaController.languageController.userL1?.langCode, - ); - final LanguageDetection? bestDetection = resp.bestDetection(); - if (bestDetection != null) { - originalSent.langCode = bestDetection.langCode; - } - } - - 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 { + 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 - why does both it and igc need to be enabled for choreo to be applicable? + // 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 || + igc.igcTextData?.detectedLanguage == null) { + await igc.getIGCTextData(onlyTokensAndLanguageDetection: true); } + + final PangeaRepresentation originalSent = PangeaRepresentation( + langCode: + igc.igcTextData?.detectedLanguage ?? LanguageKeys.unknownLanguage, + text: currentText, + originalSent: true, + originalWritten: originalWritten == null, + ); + + final PangeaMessageTokens? tokensSent = igc.igcTextData?.tokens != null + ? PangeaMessageTokens(tokens: igc.igcTextData!.tokens) + : null; + + chatController.send( + // originalWritten: originalWritten, + originalSent: originalSent, + tokensSent: tokensSent, + //TODO - save originalwritten tokens + // choreo: applicableChoreo, + choreo: choreoRecord, + ); + + clear(); } _resetDebounceTimer() { @@ -167,7 +161,7 @@ class Choreographer { } choreoMode = ChoreoMode.it; itController.initializeIT( - ITStartData(_textController.text, igc.detectedLangCode), + ITStartData(_textController.text, igc.igcTextData?.detectedLanguage), ); itMatch.status = PangeaMatchStatus.accepted; @@ -180,6 +174,7 @@ class Choreographer { _textController.setSystemText("", EditType.itStart); } + /// Handles any changes to the text input _onChangeListener() { if (_noChange) { return; @@ -188,21 +183,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(); @@ -212,7 +212,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 @@ -221,10 +223,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 = @@ -239,13 +245,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 { @@ -482,14 +490,6 @@ class Choreographer { bool get editTypeIsKeyboard => EditType.keyboard == _textController.editType; - String? get langCodeOfCurrentText { - if (igc.detectedLangCode != null) return igc.detectedLangCode!; - - if (itController.isOpen) return l2LangCode!; - - return null; - } - setState() { if (!stateListener.isClosed) { stateListener.add(0); @@ -523,9 +523,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..533b36c83e 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -3,18 +3,17 @@ 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'; +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/language_detection_model.dart'; import '../../models/span_card_model.dart'; -import '../../repo/tokens_repo.dart'; import '../../utils/error_handler.dart'; import '../../utils/overlay.dart'; @@ -29,59 +28,42 @@ 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 - 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 = []; - } - 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 +152,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; } @@ -215,14 +195,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 d30732d329..9e70287cd7 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -72,7 +72,6 @@ class ITController { /// if IGC isn't positive that text is full L1 then translate to L1 Future _setSourceText() async { - // try { if (_itStartData == null || _itStartData!.text.isEmpty) { Sentry.addBreadcrumb( Breadcrumb( @@ -97,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) @@ -167,7 +157,7 @@ class ITController { if (isTranslationDone) { choreographer.altTranslator.setTranslationFeedback(); - choreographer.getLanguageHelp(true); + choreographer.getLanguageHelp(onlyTokensAndLanguageDetection: true); } else { getNextTranslationData(); } @@ -218,7 +208,6 @@ class ITController { Future onEditSourceTextSubmit(String newSourceText) async { try { - _isOpen = true; _isEditingSourceText = false; _itStartData = ITStartData(newSourceText, choreographer.l1LangCode); @@ -230,7 +219,6 @@ class ITController { _setSourceText(); getTranslationData(false); - } catch (err, stack) { debugger(when: kDebugMode); if (err is! http.Response) { @@ -332,9 +320,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/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/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/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_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..a3e07b0a35 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,21 @@ 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.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; - } + /// 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) + ? highestConfidenceDetection + : 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/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 614fcf9db4..535af6b8b4 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -1,15 +1,12 @@ import 'dart:async'; import 'dart:developer'; -import 'package:fluffychat/pangea/constants/language_keys.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'; 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/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_event.dart'; import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; @@ -18,11 +15,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 +37,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 +100,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,192 +190,135 @@ class MyAnalyticsController extends BaseController { } } - // top level analytics sending function. Send analytics - // for each type of analytics event - // to each of the applicable analytics rooms + String? get userL2 => _pangeaController.languageController.activeL2Code(); + + /// 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 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 - final Map lastUpdatedMap = await _pangeaController - .matrixState.client - .allAnalyticsRoomsLastUpdated(); - final List lastUpdates = lastUpdatedMap.values - .where((lastUpdate) => lastUpdate != null) - .cast() - .toList(); + // analytics room for the user and current target language + final Room analyticsRoom = await _client.getMyAnalyticsRoom(userL2!); - /// 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; - } - - // 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, - l2AnalyticsLastUpdated, + // get the last time analytics were updated for this room + final DateTime? l2AnalyticsLastUpdated = + await analyticsRoom.analyticsLastUpdated( + PangeaEventTypes.summaryAnalytics, + _client.userID!, ); - 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, + // all chats in which user is a student + final List chats = _client.rooms + .where((room) => !room.isSpace && !room.isAnalyticsRoom) + .toList(); + + // get the recent message events and activity records for each chat + final List>> recentMsgFutures = []; + final List>> recentActivityFutures = []; + for (final Room chat in chats) { + recentMsgFutures.add( + chat.getEventsBySender( + type: EventTypes.Message, + sender: _client.userID!, + since: l2AnalyticsLastUpdated, + ), + ); + recentActivityFutures.add( + chat.getEventsBySender( + type: PangeaEventTypes.activityRecord, + sender: _client.userID!, + since: l2AnalyticsLastUpdated, + ), ); } - } - - Future>> getLangCodesToMsgs( - String userL2, - DateTime? since, - ) 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( - 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); - } + final List> recentMsgs = + (await Future.wait(recentMsgFutures)).toList(); + final List recentActivityRecords = + (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()); } - return langCodeToMsgs; - } - - Future sendAnalyticsEvents( - Room analyticsRoom, - List recentMsgs, - DateTime? lastUpdated, - ) 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 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(), ); } - // format the analytics data - final List summaryContent = - SummaryAnalyticsModel.formatSummaryContent(recentMsgs); - final List constructContent = - ConstructAnalyticsModel.formatConstructsContent(recentMsgs); + final List allRecentMessages = + recentPangeaMessageEvents.expand((e) => e).toList(); + 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 || lastUpdated == null) { - await SummaryAnalyticsEvent.sendSummaryAnalyticsEvent( - analyticsRoom, + if (summaryContent.isNotEmpty || l2AnalyticsLastUpdated == null) { + await analyticsRoom.sendSummaryAnalyticsEvent( summaryContent, ); } - if (constructContent.isNotEmpty) { - await ConstructAnalyticsEvent.sendConstructsEvent( - analyticsRoom, - constructContent, - ); + // get constructs for messages + final List recentConstructUses = []; + for (final PangeaMessageEvent message in allRecentMessages) { + recentConstructUses.addAll(message.allConstructUses); } - } - 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 []; + // get constructs for practice activities + final List>> constructFutures = []; + for (final PracticeActivityRecordEvent activity in recentActivityRecords) { + 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; + } + constructFutures.add(activity.uses(timeline)); } - } - - 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 []; + final List> constructLists = + await Future.wait(constructFutures); + + recentConstructUses.addAll(constructLists.expand((e) => e)); + + //TODO - confirm that this is the correct construct content + // debugger( + // when: kDebugMode, + // ); + // ; debugger( + // when: kDebugMode && + // (allRecentMessages.isNotEmpty || recentActivityRecords.isNotEmpty), + // ); + + if (recentConstructUses.isNotEmpty) { + await analyticsRoom.sendConstructsEvent( + recentConstructUses, + ); } } } diff --git a/lib/pangea/controllers/practice_activity_generation_controller.dart b/lib/pangea/controllers/practice_activity_generation_controller.dart index 8ea5b5c820..9b7f6b66e4 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/controllers/user_controller.dart b/lib/pangea/controllers/user_controller.dart index 33d74244a9..873397969e 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/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/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/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..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, ), ); @@ -426,26 +424,6 @@ 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, - ); - }).toList(); - } - // 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/extensions/pangea_room_extension/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart index b8dae7ab3a..21f5abac57 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -4,13 +4,14 @@ 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'; 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'; @@ -33,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'; @@ -180,7 +180,6 @@ extension PangeaRoom on Room { PangeaMessageTokens? tokensSent, PangeaMessageTokens? tokensWritten, ChoreoRecord? choreo, - UseType? useType, }) => _pangeaSendTextEvent( message, @@ -197,7 +196,6 @@ extension PangeaRoom on Room { tokensSent: tokensSent, tokensWritten: tokensWritten, choreo: choreo, - useType: useType, ); Future updateStateEvent(Event stateEvent) => 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..a27526a2b7 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); } } @@ -194,7 +194,7 @@ extension AnalyticsRoomExtension on Room { final List events = await getEventsBySender( type: type, sender: userId, - count: 1, + count: 10, ); if (events.isEmpty) return null; final Event event = events.first; @@ -249,4 +249,31 @@ extension AnalyticsRoomExtension on Room { return creationContent?.tryGet(ModelKey.langCode) == langCode || creationContent?.tryGet(ModelKey.oldLangCode) == langCode; } + + Future sendSummaryAnalyticsEvent( + List records, + ) async { + final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel( + messages: records, + ); + final String? eventId = await sendEvent( + analyticsModel.toJson(), + type: PangeaEventTypes.summaryAnalytics, + ); + return eventId; + } + + Future sendConstructsEvent( + List uses, + ) async { + final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel( + uses: uses, + ); + + final String? eventId = await sendEvent( + constructsModel.toJson(), + type: PangeaEventTypes.construct, + ); + return eventId; + } } diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 6621874f73..e0820d665d 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'; @@ -22,7 +28,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'; @@ -31,7 +37,6 @@ class PangeaMessageEvent { late Event _event; final Timeline timeline; final bool ownMessage; - bool _isValidPangeaMessageEvent = true; PangeaMessageEvent({ required Event event, @@ -39,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", ); @@ -542,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 && @@ -651,21 +667,169 @@ class PangeaMessageEvent { } /// 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!); + + /// all construct uses for the message, including vocab and grammar + List get allConstructUses => + [..._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 + /// 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 = []; + 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, + ), + ); + } + } + } + } + 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. + /// 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) { + 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); } - // 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 + /// 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; + } - // replication of logic from message_content.dart - // bool get isHtml => - // AppConfig.renderHtml && !_event.redacted && _event.isRichMessage; + /// get construct uses of type grammar for the message + 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/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/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 10dd814ec3..3d1185d05f 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'; @@ -71,7 +71,9 @@ class PracticeActivityEvent { return records.firstOrNull; } - /// Checks if there is a user record for this activity, + 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 => userRecord != null; } 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_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/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_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/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/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/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/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/igc_text_data_model.dart b/lib/pangea/models/igc_text_data_model.dart index 6a3eec96e0..442bf4a602 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,18 @@ 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 +71,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 +86,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 +97,18 @@ class IGCTextData { "enable_igc": enableIGC, }; + /// 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 //if existing matches overlap with the accepted one, remove them?? 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/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/models/practice_activities.dart/practice_activity_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart index f5f4348c69..fa3e25acf8 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 3fe3e859d6..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,16 +5,18 @@ 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; + 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 +28,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(), ); } @@ -40,26 +44,34 @@ 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( - ActivityResponse( + ActivityRecordResponse( text: text, audioBytes: audioBytes, imageBytes: imageBytes, timestamp: DateTime.now(), + score: score, ), ); } catch (e) { @@ -84,27 +96,33 @@ 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; final Uint8List? audioBytes; final Uint8List? imageBytes; final DateTime timestamp; + final double score; - ActivityResponse({ + ActivityRecordResponse({ this.text, this.audioBytes, this.imageBytes, + required this.score, 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?, 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, ); } @@ -114,6 +132,7 @@ class ActivityResponse { 'audio': audioBytes, 'image': imageBytes, 'timestamp': timestamp.toIso8601String(), + 'score': score, }; } @@ -121,7 +140,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/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 43cd9afb53..dd5ec0fa52 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/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/pages/analytics/student_analytics/student_analytics.dart b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart index d6bc9d7667..5c694b6cad 100644 --- a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart +++ b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart @@ -1,7 +1,6 @@ -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'; @@ -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 { diff --git a/lib/pangea/repo/igc_repo.dart b/lib/pangea/repo/igc_repo.dart index 9517515d01..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), @@ -89,7 +93,6 @@ class IGCRequestBody { String fullText; String userL1; String userL2; - bool tokensOnly; bool enableIT; bool enableIGC; @@ -99,7 +102,6 @@ class IGCRequestBody { required this.userL2, required this.enableIGC, required this.enableIT, - this.tokensOnly = false, }); Map toJson() => { @@ -108,6 +110,5 @@ class IGCRequestBody { ModelKey.userL2: userL2, "enable_it": enableIT, "enable_igc": enableIGC, - "tokens_only": tokensOnly, }; } 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/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/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/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/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, ), diff --git a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart index 28fc00bd18..54ff5586c2 100644 --- a/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart +++ b/lib/pangea/widgets/practice_activity/multiple_choice_activity.dart @@ -28,7 +28,7 @@ class MultipleChoiceActivityState extends State { widget.controller.currentRecordModel; bool get isSubmitted => - widget.currentActivity?.userRecord?.record?.latestResponse != null; + widget.currentActivity?.userRecord?.record.latestResponse != null; @override void initState() { @@ -64,15 +64,19 @@ class MultipleChoiceActivityState extends State { .setCurrentModel(widget.currentActivity!.userRecord!.record); selectedChoiceIndex = widget .currentActivity?.practiceActivity.multipleChoice! - .choiceIndex(currentRecordModel!.latestResponse!); + .choiceIndex(currentRecordModel!.latestResponse!.text!); } setState(() {}); } void updateChoice(int index) { currentRecordModel?.addResponse( - text: widget.controller.currentActivity?.practiceActivity.multipleChoice! + text: widget.controller.currentActivity!.practiceActivity.multipleChoice! .choices[index], + score: widget.controller.currentActivity!.practiceActivity.multipleChoice! + .isCorrect(index) + ? 1 + : 0, ); setState(() => selectedChoiceIndex = index); } 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'; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 09c588b295..47b695fb92 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -29,6 +29,7 @@ import pasteboard import path_provider_foundation import purchases_flutter import record_darwin +import rive_common import sentry_flutter import share_plus import shared_preferences_foundation @@ -65,6 +66,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PurchasesFlutterPlugin.register(with: registry.registrar(forPlugin: "PurchasesFlutterPlugin")) RecordPlugin.register(with: registry.registrar(forPlugin: "RecordPlugin")) + RivePlugin.register(with: registry.registrar(forPlugin: "RivePlugin")) SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 4798ca3894..8fbbffa186 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); RecordWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("RecordWindowsPluginCApi")); + RivePluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("RivePlugin")); SentryFlutterPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("SentryFlutterPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index bf885c6b1b..315ce5112c 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -12,6 +12,7 @@ list(APPEND FLUTTER_PLUGIN_LIST pasteboard permission_handler_windows record_windows + rive_common sentry_flutter share_plus sqlcipher_flutter_libs