diff --git a/.github/workflows/main_deploy.yaml b/.github/workflows/main_deploy.yaml index 6bda0c1db..46f909e1f 100644 --- a/.github/workflows/main_deploy.yaml +++ b/.github/workflows/main_deploy.yaml @@ -79,5 +79,7 @@ jobs: with: name: web path: build/web + - name: Update packages + run: flutter pub get - name: Update sentry run: flutter packages pub run sentry_dart_plugin diff --git a/.gitignore b/.gitignore index 3c47c0cc2..1071f4410 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ prime *.env !/public/.env +*.env.local_choreo +*.env.prod # libolm package /assets/js/package diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index c246cfccb..7f44188a6 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!", @@ -4065,6 +4065,8 @@ "practice": "Practice", "noLanguagesSet": "No languages set", "noActivitiesFound": "No practice activities found for this message", + "hintTitle": "Hint:", + "speechToTextBody": "See how well you did by looking at your Accuracy and Words Per Minute scores", "previous": "Previous", "languageButtonLabel": "Language: {currentLanguage}", "@languageButtonLabel": { @@ -4082,5 +4084,6 @@ "@interactiveTranslatorAutoPlayDesc": { "type": "text", "placeholders": {} - } + }, + "changeAnalyticsView": "Change Analytics View" } \ No newline at end of file diff --git a/assets/l10n/intl_es.arb b/assets/l10n/intl_es.arb index 8631f49af..ac6b30c76 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/config/routes.dart b/lib/config/routes.dart index eba1ae511..51c631ff3 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -170,31 +170,11 @@ abstract class AppRoutes { pageBuilder: (context, state) => defaultPageBuilder( context, state, - const StudentAnalyticsPage(), + const StudentAnalyticsPage( + selectedView: BarChartViewSelection.messages, + ), ), redirect: loggedOutRedirect, - routes: [ - GoRoute( - path: 'messages', - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - const StudentAnalyticsPage( - selectedView: BarChartViewSelection.messages, - ), - ), - ), - GoRoute( - path: 'errors', - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - const StudentAnalyticsPage( - selectedView: BarChartViewSelection.grammar, - ), - ), - ), - ], ), GoRoute( path: 'analytics', @@ -207,34 +187,13 @@ abstract class AppRoutes { routes: [ GoRoute( path: ':spaceid', - redirect: loggedOutRedirect, pageBuilder: (context, state) => defaultPageBuilder( context, state, - const SpaceAnalyticsPage(), + const SpaceAnalyticsPage( + selectedView: BarChartViewSelection.messages, + ), ), - routes: [ - GoRoute( - path: 'messages', - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - const SpaceAnalyticsPage( - selectedView: BarChartViewSelection.messages, - ), - ), - ), - GoRoute( - path: 'errors', - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - const SpaceAnalyticsPage( - selectedView: BarChartViewSelection.grammar, - ), - ), - ), - ], ), ], ), diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 13f09ffb6..5629d809e 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_event_list.dart b/lib/pages/chat/chat_event_list.dart index 048b54443..24508bb62 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -4,8 +4,8 @@ import 'package:fluffychat/pages/chat/events/message.dart'; import 'package:fluffychat/pages/chat/seen_by_row.dart'; import 'package:fluffychat/pages/chat/typing_indicators.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; +import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/utils/instructions.dart'; import 'package:fluffychat/pangea/widgets/chat/locked_chat_message.dart'; import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; @@ -46,7 +46,7 @@ class ChatEventList extends StatelessWidget { // card, attach it on top of the first shown message WidgetsBinding.instance.addPostFrameCallback((_) { if (events.isEmpty) return; - controller.pangeaController.instructions.show( + controller.pangeaController.instructions.showInstructionsPopup( context, InstructionsEnum.clickMessage, events[0].eventId, diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 852065acb..2ba6a0957 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/chat_view.dart b/lib/pages/chat/chat_view.dart index 5b4f96606..3c819f412 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -89,19 +89,17 @@ class ChatView extends StatelessWidget { } }, itemBuilder: (context) => [ - // #Pangea - // PopupMenuItem( - // value: _EventContextAction.info, - // child: Row( - // mainAxisSize: MainAxisSize.min, - // children: [ - // const Icon(Icons.info_outlined), - // const SizedBox(width: 12), - // Text(L10n.of(context)!.messageInfo), - // ], - // ), - // ), - // Pangea# + PopupMenuItem( + value: _EventContextAction.info, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.info_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.messageInfo), + ], + ), + ), if (controller.selectedEvents.single.status.isSent) PopupMenuItem( value: _EventContextAction.report, @@ -442,7 +440,8 @@ class ChatView extends StatelessWidget { children: [ const ConnectionStatusHeader(), ITBar( - choreographer: controller.choreographer, + choreographer: + controller.choreographer, ), ReactionsPicker(controller), ReplyDisplay(controller), diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index c5756438c..3b2c1b2fb 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 569eba880..89f31fd68 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 45659a1d0..66e11808b 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 68d81389e..533b36c83 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 d30732d32..3187d4a6a 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -3,7 +3,6 @@ import 'dart:developer'; import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart'; import 'package:fluffychat/pangea/constants/choreo_constants.dart'; -import 'package:fluffychat/pangea/repo/full_text_translation_repo.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -72,7 +71,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( @@ -86,32 +84,23 @@ class ITController { throw Exception("null _itStartData or empty text in _setSourceText"); } debugPrint("_setSourceText with detectedLang ${_itStartData!.langCode}"); - if (_itStartData!.langCode == choreographer.l1LangCode) { - sourceText = _itStartData!.text; - return; - } - - final FullTextTranslationResponseModel res = - await FullTextTranslationRepo.translate( - accessToken: await choreographer.accessToken, - request: FullTextTranslationRequestModel( - text: _itStartData!.text, - tgtLang: choreographer.l1LangCode!, - srcLang: choreographer.l2LangCode, - 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; - // } + // if (_itStartData!.langCode == choreographer.l1LangCode) { + sourceText = _itStartData!.text; + return; // } + + // final FullTextTranslationResponseModel res = + // await FullTextTranslationRepo.translate( + // accessToken: await choreographer.accessToken, + // request: FullTextTranslationRequestModel( + // text: _itStartData!.text, + // tgtLang: choreographer.l1LangCode!, + // srcLang: _itStartData!.langCode, + // userL1: choreographer.l1LangCode!, + // userL2: choreographer.l2LangCode!, + // ), + // ); + // sourceText = res.bestTranslation; } // used 1) at very beginning (with custom input = null) @@ -167,7 +156,7 @@ class ITController { if (isTranslationDone) { choreographer.altTranslator.setTranslationFeedback(); - choreographer.getLanguageHelp(true); + choreographer.getLanguageHelp(onlyTokensAndLanguageDetection: true); } else { getNextTranslationData(); } @@ -218,7 +207,6 @@ class ITController { Future onEditSourceTextSubmit(String newSourceText) async { try { - _isOpen = true; _isEditingSourceText = false; _itStartData = ITStartData(newSourceText, choreographer.l1LangCode); @@ -230,7 +218,6 @@ class ITController { _setSourceText(); getTranslationData(false); - } catch (err, stack) { debugger(when: kDebugMode); if (err is! http.Response) { @@ -332,9 +319,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 0e5e68461..96df5d921 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/it_bar_buttons.dart b/lib/pangea/choreographer/widgets/it_bar_buttons.dart index 786c17c07..9fcaa927e 100644 --- a/lib/pangea/choreographer/widgets/it_bar_buttons.dart +++ b/lib/pangea/choreographer/widgets/it_bar_buttons.dart @@ -1,8 +1,8 @@ +import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/enum/instructions_enum.dart'; +import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; -import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/utils/instructions.dart'; -import 'package:fluffychat/widgets/matrix.dart'; import '../../widgets/common/bot_face_svg.dart'; import '../controllers/choreographer.dart'; import '../controllers/it_controller.dart'; @@ -37,7 +37,7 @@ class ITBotButton extends StatelessWidget { @override Widget build(BuildContext context) { - choreographer.pangeaController.instructions.show( + choreographer.pangeaController.instructions.showInstructionsPopup( context, InstructionsEnum.itInstructions, choreographer.itBotTransformTargetKey, @@ -46,7 +46,8 @@ class ITBotButton extends StatelessWidget { return IconButton( icon: const BotFace(width: 40.0, expression: BotExpression.idle), - onPressed: () => choreographer.pangeaController.instructions.show( + onPressed: () => + choreographer.pangeaController.instructions.showInstructionsPopup( context, InstructionsEnum.itInstructions, choreographer.itBotTransformTargetKey, diff --git a/lib/pangea/choreographer/widgets/start_igc_button.dart b/lib/pangea/choreographer/widgets/start_igc_button.dart index 877158dd2..e8625da95 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/constants/keys.dart b/lib/pangea/constants/keys.dart deleted file mode 100644 index 092b1b0a9..000000000 --- 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 000000000..73137a300 --- /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 cfe0c96c0..000000000 --- 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 49136ca77..000000000 --- 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 7fff924a9..000000000 --- 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 ca59d89b7..372e72606 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 c906e6b4e..b1bc02380 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 4ddfabc88..a3e07b0a3 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 59c3cce88..31e4513fa 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 1475c0e6e..3739b2596 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 614fcf9db..535af6b8b 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(); - // if analytics haven't been updated in the last day, update them - 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 + // 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); }); } - /// given an update from sync stream, check if the update contains + /// 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); + } + return lastUpdated; + } + + 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(); + // analytics room for the user and current target language + final Room analyticsRoom = await _client.getMyAnalyticsRoom(userL2!); - // get the last updated time for each analytics room - // and the least recent update, which will be used to determine - // how far to go back in the chat history to get messages - final Map lastUpdatedMap = await _pangeaController - .matrixState.client - .allAnalyticsRoomsLastUpdated(); - final List lastUpdates = lastUpdatedMap.values - .where((lastUpdate) => lastUpdate != null) - .cast() - .toList(); - - /// Get the last time that analytics to for current target language - /// were updated. This my present a problem is the user has analytics - /// rooms for multiple languages, and a non-target language was updated - /// less recently than the target language. In this case, some data may - /// be missing, but a case like that seems relatively rare, and could - /// result in unnecessaily going too far back in the chat history - DateTime? l2AnalyticsLastUpdated = lastUpdatedMap[userL2]; - if (l2AnalyticsLastUpdated == null) { - /// if the target language has never been updated, use the least - /// recent update time - lastUpdates.sort((a, b) => a.compareTo(b)); - l2AnalyticsLastUpdated = - lastUpdates.isNotEmpty ? lastUpdates.first : null; - } - - // 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); + // all chats in which user is a student + final List chats = _client.rooms + .where((room) => !room.isSpace && !room.isAnalyticsRoom) + .toList(); - // 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, + // 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, + ), ); } - } + final List> recentMsgs = + (await Future.wait(recentMsgFutures)).toList(); + final List recentActivityRecords = + (await Future.wait(recentActivityFutures)) + .expand((e) => e) + .map((event) => PracticeActivityRecordEvent(event: event)) + .toList(); - 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); - } + // get the timelines for each chat + final List> timelineFutures = []; + for (final chat in chats) { + timelineFutures.add(chat.getTimeline()); } - return langCodeToMsgs; - } + final List timelines = await Future.wait(timelineFutures); + final Map timelineMap = + Map.fromIterables(chats.map((e) => e.id), timelines); - 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), + //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 allRecentMessages = + recentPangeaMessageEvents.expand((e) => e).toList(); + final List summaryContent = - SummaryAnalyticsModel.formatSummaryContent(recentMsgs); - final List constructContent = - ConstructAnalyticsModel.formatConstructsContent(recentMsgs); - + 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); + } + + // 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)); + } + 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, ); } } - - List _studentChats = []; - - Future setStudentChats() async { - final List teacherRoomIds = - await _pangeaController.matrixState.client.teacherRoomIds; - _studentChats = _pangeaController.matrixState.client.rooms - .where( - (r) => - !r.isSpace && - !r.isAnalyticsRoom && - !teacherRoomIds.contains(r.id), - ) - .toList(); - setState(data: _studentChats); - } - - List get studentChats { - try { - if (_studentChats.isNotEmpty) return _studentChats; - setStudentChats(); - return _studentChats; - } catch (err) { - debugger(when: kDebugMode); - return []; - } - } - - List _studentSpaces = []; - - Future setStudentSpaces() async { - _studentSpaces = - await _pangeaController.matrixState.client.spacesImStudyingIn; - } - - List get studentSpaces { - try { - if (_studentSpaces.isNotEmpty) return _studentSpaces; - setStudentSpaces(); - return _studentSpaces; - } catch (err) { - debugger(when: kDebugMode); - return []; - } - } } diff --git a/lib/pangea/controllers/practice_activity_generation_controller.dart b/lib/pangea/controllers/practice_activity_generation_controller.dart index 8ea5b5c82..9b7f6b66e 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 33d74244a..873397969 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 ce8324ebe..a67941465 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/bar_chart_view_enum.dart b/lib/pangea/enum/bar_chart_view_enum.dart index 3fe812634..aba0652af 100644 --- a/lib/pangea/enum/bar_chart_view_enum.dart +++ b/lib/pangea/enum/bar_chart_view_enum.dart @@ -29,15 +29,4 @@ extension BarChartViewSelectionExtension on BarChartViewSelection { return Icons.spellcheck_outlined; } } - - String get route { - switch (this) { - case BarChartViewSelection.messages: - return 'messages'; - // case BarChartViewSelection.vocab: - // return 'vocab'; - case BarChartViewSelection.grammar: - return 'errors'; - } - } } diff --git a/lib/pangea/enum/construct_type_enum.dart b/lib/pangea/enum/construct_type_enum.dart index 2a7d5583d..7db7f9cd5 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 000000000..0e3c52bbb --- /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/instructions_enum.dart b/lib/pangea/enum/instructions_enum.dart new file mode 100644 index 000000000..c684d5b14 --- /dev/null +++ b/lib/pangea/enum/instructions_enum.dart @@ -0,0 +1,45 @@ +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +enum InstructionsEnum { + itInstructions, + clickMessage, + blurMeansTranslate, + tooltipInstructions, + speechToText, +} + +extension Copy on InstructionsEnum { + String title(BuildContext context) { + switch (this) { + case InstructionsEnum.itInstructions: + return L10n.of(context)!.itInstructionsTitle; + case InstructionsEnum.clickMessage: + return L10n.of(context)!.clickMessageTitle; + case InstructionsEnum.blurMeansTranslate: + return L10n.of(context)!.blurMeansTranslateTitle; + case InstructionsEnum.tooltipInstructions: + return L10n.of(context)!.tooltipInstructionsTitle; + case InstructionsEnum.speechToText: + return L10n.of(context)!.hintTitle; + } + } + + String body(BuildContext context) { + switch (this) { + case InstructionsEnum.itInstructions: + return L10n.of(context)!.itInstructionsBody; + case InstructionsEnum.clickMessage: + return L10n.of(context)!.clickMessageBody; + case InstructionsEnum.blurMeansTranslate: + return L10n.of(context)!.blurMeansTranslateBody; + case InstructionsEnum.speechToText: + return L10n.of(context)!.speechToTextBody; + case InstructionsEnum.tooltipInstructions: + return PlatformInfos.isMobile + ? L10n.of(context)!.tooltipInstructionsMobileBody + : L10n.of(context)!.tooltipInstructionsBrowserBody; + } + } +} diff --git a/lib/pangea/enum/use_type.dart b/lib/pangea/enum/use_type.dart index 771b26220..56a4fa3b0 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 779f8ee0a..3dda99237 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 3bef46e45..0adc70469 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 2d67db5b2..ce9d3451c 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 b8dae7ab3..21f5abac5 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 e3d00f8a8..a27526a2b 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 6621874f7..334c0fa78 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; } - // 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 + /// get construct uses of type vocab for the message + List get _vocabUses { + final List uses = []; - // replication of logic from message_content.dart - // bool get isHtml => - // AppConfig.renderHtml && !_event.redacted && _event.isRichMessage; + // 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); + } + + /// 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; + } + + /// 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 e774dc4f2..e6e45b756 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 d4b9cde23..000000000 --- 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 10dd814ec..3d1185d05 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 000000000..77b4948fd --- /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 2453e62ef..7010d3591 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 bdb3bc6d5..d8732ad97 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 c2930faba..481051b1c 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 18c6d3d5a..54e81789f 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 e7034eaa4..a764d5597 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 b09d0a870..0b8e4b27c 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 413f00716..3586fcee1 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 497381fa1..a55eeb188 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 6a3eec96e..442bf4a60 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 ae960b212..be3cc71ba 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 2ad0b2950..81c8f911d 100644 --- a/lib/pangea/models/lemma.dart +++ b/lib/pangea/models/lemma.dart @@ -1,24 +1,51 @@ +/// 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}); + /// [pos] ex "v" - part of speech of the lemma + /// https://universaldependencies.org/u/pos/ + final String pos; + + /// [morph] ex {} - morphological features of the lemma + /// https://universaldependencies.org/u/feat/ + final Map morph; + + Lemma({ + required this.text, + required this.saveVocab, + required this.form, + this.pos = '', + this.morph = const {}, + }); factory Lemma.fromJson(Map json) { return Lemma( text: json['text'], saveVocab: json['save_vocab'] ?? json['saveVocab'] ?? false, form: json["form"] ?? json['text'], + pos: json['pos'] ?? '', + morph: json['morph'] ?? {}, ); } toJson() { - return {'text': text, 'save_vocab': saveVocab, 'form': form}; + return { + 'text': text, + 'save_vocab': saveVocab, + 'form': form, + 'pos': pos, + 'morph': morph, + }; } - static Lemma get empty => Lemma(text: '', saveVocab: true, form: ''); - static Lemma create(String form) => Lemma(text: '', saveVocab: true, form: form); } diff --git a/lib/pangea/models/pangea_token_model.dart b/lib/pangea/models/pangea_token_model.dart index a671256ab..9dddd149b 100644 --- a/lib/pangea/models/pangea_token_model.dart +++ b/lib/pangea/models/pangea_token_model.dart @@ -9,12 +9,10 @@ import 'lemma.dart'; class PangeaToken { PangeaTokenText text; - bool hasInfo; List lemmas; PangeaToken({ required this.text, - required this.hasInfo, required this.lemmas, }); @@ -37,7 +35,6 @@ class PangeaToken { PangeaTokenText.fromJson(json[_textKey] as Map); return PangeaToken( text: text, - hasInfo: json[_hasInfoKey] ?? text.length > 2, lemmas: getLemmas(text.content, json[_lemmaKey]), ); } catch (err, s) { @@ -56,12 +53,10 @@ class PangeaToken { } static const String _textKey = "text"; - static const String _hasInfoKey = "has_info"; static const String _lemmaKey = ModelKey.lemma; Map toJson() => { _textKey: text.toJson(), - _hasInfoKey: hasInfo, _lemmaKey: lemmas.map((e) => e.toJson()).toList(), }; 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 f5f4348c6..fa3e25acf 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 3fe3e859d..0c4ea52bf 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 f17507fa8..946da6dc7 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 43cd9afb5..3e3a508cd 100644 --- a/lib/pangea/models/user_model.dart +++ b/lib/pangea/models/user_model.dart @@ -4,12 +4,12 @@ import 'package:country_picker/country_picker.dart'; import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/models/space_model.dart'; -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/analytics_language_button.dart b/lib/pangea/pages/analytics/analytics_language_button.dart index 2c3923fb4..08b3220b0 100644 --- a/lib/pangea/pages/analytics/analytics_language_button.dart +++ b/lib/pangea/pages/analytics/analytics_language_button.dart @@ -34,9 +34,7 @@ class AnalyticsLanguageButton extends StatelessWidget { }).toList(), child: TextButton.icon( label: Text( - L10n.of(context)!.languageButtonLabel( - value.getDisplayName(context) ?? value.langCode, - ), + value.getDisplayName(context) ?? value.langCode, style: TextStyle( color: Theme.of(context).colorScheme.onSurface, ), diff --git a/lib/pangea/pages/analytics/analytics_list_tile.dart b/lib/pangea/pages/analytics/analytics_list_tile.dart index 8b1e1ba49..a49ef4cb4 100644 --- a/lib/pangea/pages/analytics/analytics_list_tile.dart +++ b/lib/pangea/pages/analytics/analytics_list_tile.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -141,9 +140,7 @@ class AnalyticsListTileState extends State { return; } if ((room?.isSpace ?? false) && widget.allowNavigateOnSelect) { - final String selectedView = - widget.controller!.widget.selectedView!.route; - context.go('/rooms/analytics/${room!.id}/$selectedView'); + context.go('/rooms/analytics/${room!.id}'); return; } widget.onTap(widget.selected); diff --git a/lib/pangea/pages/analytics/analytics_view_button.dart b/lib/pangea/pages/analytics/analytics_view_button.dart new file mode 100644 index 000000000..c98bf47fc --- /dev/null +++ b/lib/pangea/pages/analytics/analytics_view_button.dart @@ -0,0 +1,49 @@ +import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class AnalyticsViewButton extends StatelessWidget { + final BarChartViewSelection value; + final void Function(BarChartViewSelection) onChange; + const AnalyticsViewButton({ + super.key, + required this.value, + required this.onChange, + }); + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + tooltip: L10n.of(context)!.changeAnalyticsView, + initialValue: value, + onSelected: (BarChartViewSelection? view) { + if (view == null) { + debugPrint("when is view null?"); + return; + } + onChange(view); + }, + itemBuilder: (BuildContext context) => BarChartViewSelection.values + .map>( + (BarChartViewSelection view) { + return PopupMenuItem( + value: view, + child: Text(view.string(context)), + ); + }).toList(), + child: TextButton.icon( + label: Text( + value.string(context), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + icon: Icon( + value.icon, + color: Theme.of(context).colorScheme.onSurface, + ), + onPressed: null, + ), + ); + } +} diff --git a/lib/pangea/pages/analytics/base_analytics.dart b/lib/pangea/pages/analytics/base_analytics.dart index 146a5ac70..cd41d2312 100644 --- a/lib/pangea/pages/analytics/base_analytics.dart +++ b/lib/pangea/pages/analytics/base_analytics.dart @@ -20,7 +20,7 @@ import '../../models/analytics/chart_analytics_model.dart'; class BaseAnalyticsPage extends StatefulWidget { final String pageTitle; final List tabs; - final BarChartViewSelection? selectedView; + final BarChartViewSelection selectedView; final AnalyticsSelected defaultSelected; final AnalyticsSelected? alwaysSelected; @@ -33,7 +33,7 @@ class BaseAnalyticsPage extends StatefulWidget { required this.tabs, required this.alwaysSelected, required this.defaultSelected, - this.selectedView, + required this.selectedView, this.myAnalyticsController, targetLanguages, }) : targetLanguages = (targetLanguages?.isNotEmpty ?? false) @@ -50,6 +50,7 @@ class BaseAnalyticsController extends State { String? currentLemma; ChartAnalyticsModel? chartData; StreamController refreshStream = StreamController.broadcast(); + BarChartViewSelection currentView = BarChartViewSelection.messages; bool isSelected(String chatOrStudentId) => chatOrStudentId == selected?.id; @@ -63,6 +64,7 @@ class BaseAnalyticsController extends State { @override void initState() { super.initState(); + currentView = widget.selectedView; if (widget.defaultSelected.type == AnalyticsEntryType.student) { runFirstRefresh(); } @@ -168,6 +170,12 @@ class BaseAnalyticsController extends State { refreshStream.add(false); } + Future toggleView(BarChartViewSelection view) async { + currentView = view; + await setChartData(); + refreshStream.add(false); + } + void setCurrentLemma(String? lemma) { currentLemma = lemma; setState(() {}); diff --git a/lib/pangea/pages/analytics/base_analytics_view.dart b/lib/pangea/pages/analytics/base_analytics_view.dart index 3d70c9b4c..13e49aa38 100644 --- a/lib/pangea/pages/analytics/base_analytics_view.dart +++ b/lib/pangea/pages/analytics/base_analytics_view.dart @@ -5,6 +5,7 @@ import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/enum/time_span.dart'; import 'package:fluffychat/pangea/pages/analytics/analytics_language_button.dart'; import 'package:fluffychat/pangea/pages/analytics/analytics_list_tile.dart'; +import 'package:fluffychat/pangea/pages/analytics/analytics_view_button.dart'; import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart'; import 'package:fluffychat/pangea/pages/analytics/construct_list.dart'; import 'package:fluffychat/pangea/pages/analytics/messages_bar_chart.dart'; @@ -24,18 +25,14 @@ class BaseAnalyticsView extends StatelessWidget { final BaseAnalyticsController controller; Widget chartView(BuildContext context) { - if (controller.widget.selectedView == null) { - return const SizedBox(); - } - - switch (controller.widget.selectedView!) { + switch (controller.currentView) { case BarChartViewSelection.messages: return MessagesBarChart( chartAnalytics: controller.chartData, ); case BarChartViewSelection.grammar: return ConstructList( - constructType: ConstructType.grammar, + constructType: ConstructTypeEnum.grammar, defaultSelected: controller.widget.defaultSelected, selected: controller.selected, controller: controller, @@ -75,27 +72,13 @@ class BaseAnalyticsView extends StatelessWidget { if (controller.activeSpace != null) TextSpan( text: controller.activeSpace!.getLocalizedDisplayname(), - style: const TextStyle(decoration: TextDecoration.underline), - recognizer: TapGestureRecognizer() - ..onTap = () { - if (controller.widget.selectedView == null) return; - String route = - "/rooms/${controller.widget.defaultSelected.type.route}"; - if (controller.widget.defaultSelected.type == - AnalyticsEntryType.space) { - route += "/${controller.widget.defaultSelected.id}"; - } - context.go(route); - }, - ), - if (controller.widget.selectedView != null) - const TextSpan( - text: " > ", - ), - if (controller.widget.selectedView != null) - TextSpan( - text: controller.widget.selectedView!.string(context), ), + const TextSpan( + text: " > ", + ), + TextSpan( + text: controller.currentView.string(context), + ), ], ), overflow: TextOverflow.ellipsis, @@ -104,220 +87,156 @@ class BaseAnalyticsView extends StatelessWidget { ), body: MaxWidthBody( withScrolling: false, - child: controller.widget.selectedView != null - ? Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - // if (controller.widget.defaultSelected.type == - // AnalyticsEntryType.student) - // IconButton( - // icon: const Icon(Icons.refresh), - // onPressed: controller.onRefresh, - // tooltip: L10n.of(context)!.refresh, - // ), - TimeSpanMenuButton( - value: controller.currentTimeSpan, - onChange: (TimeSpan value) => - controller.toggleTimeSpan(context, value), - ), - AnalyticsLanguageButton( - value: controller - .pangeaController.analytics.currentAnalyticsLang, - onChange: (lang) => controller.toggleSpaceLang(lang), - languages: controller.widget.targetLanguages, - ), - ], - ), - Expanded( - flex: 1, - child: chartView(context), - ), - Expanded( - flex: 1, - child: DefaultTabController( - length: 2, - child: Column( - children: [ - TabBar( - tabs: [ - ...controller.widget.tabs.map( - (tab) => Tab( - icon: Icon( - tab.icon, - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TimeSpanMenuButton( + value: controller.currentTimeSpan, + onChange: (TimeSpan value) => + controller.toggleTimeSpan(context, value), + ), + AnalyticsViewButton( + value: controller.currentView, + onChange: controller.toggleView, + ), + AnalyticsLanguageButton( + value: controller + .pangeaController.analytics.currentAnalyticsLang, + onChange: (lang) => controller.toggleSpaceLang(lang), + languages: controller.widget.targetLanguages, + ), + ], + ), + const SizedBox( + height: 10, + ), + Expanded( + flex: 1, + child: chartView(context), + ), + Expanded( + flex: 1, + child: DefaultTabController( + length: 2, + child: Column( + children: [ + TabBar( + tabs: [ + ...controller.widget.tabs.map( + (tab) => Tab( + icon: Icon( + tab.icon, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + ), + ], + ), + Expanded( + child: SingleChildScrollView( + child: SizedBox( + height: max( + controller.widget.tabs[0].items.length + 1, + controller.widget.tabs[1].items.length, + ) * + 72, + child: TabBarView( + physics: const NeverScrollableScrollPhysics(), + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ...controller.widget.tabs[0].items.map( + (item) => AnalyticsListTile( + refreshStream: controller.refreshStream, + avatar: item.avatar, + defaultSelected: + controller.widget.defaultSelected, + selected: AnalyticsSelected( + item.id, + controller.widget.tabs[0].type, + item.displayName, + ), + isSelected: + controller.isSelected(item.id), + onTap: (_) => controller.toggleSelection( + AnalyticsSelected( + item.id, + controller.widget.tabs[0].type, + item.displayName, + ), + ), + allowNavigateOnSelect: controller + .widget.tabs[0].allowNavigateOnSelect, + pangeaController: + controller.pangeaController, + controller: controller, + ), ), - ), + if (controller.widget.defaultSelected.type == + AnalyticsEntryType.space) + AnalyticsListTile( + refreshStream: controller.refreshStream, + defaultSelected: + controller.widget.defaultSelected, + avatar: null, + selected: AnalyticsSelected( + controller.widget.defaultSelected.id, + AnalyticsEntryType.privateChats, + L10n.of(context)!.allPrivateChats, + ), + allowNavigateOnSelect: false, + isSelected: controller.isSelected( + controller.widget.defaultSelected.id, + ), + onTap: controller.toggleSelection, + pangeaController: + controller.pangeaController, + controller: controller, + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: controller.widget.tabs[1].items + .map( + (item) => AnalyticsListTile( + refreshStream: controller.refreshStream, + avatar: item.avatar, + defaultSelected: + controller.widget.defaultSelected, + selected: AnalyticsSelected( + item.id, + controller.widget.tabs[1].type, + item.displayName, + ), + isSelected: + controller.isSelected(item.id), + onTap: controller.toggleSelection, + allowNavigateOnSelect: controller.widget + .tabs[1].allowNavigateOnSelect, + pangeaController: + controller.pangeaController, + controller: controller, + ), + ) + .toList(), ), ], ), - Expanded( - child: SingleChildScrollView( - child: SizedBox( - height: max( - controller.widget.tabs[0].items.length + - 1, - controller.widget.tabs[1].items.length, - ) * - 72, - child: TabBarView( - physics: const NeverScrollableScrollPhysics(), - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - ...controller.widget.tabs[0].items.map( - (item) => AnalyticsListTile( - refreshStream: - controller.refreshStream, - avatar: item.avatar, - defaultSelected: controller - .widget.defaultSelected, - selected: AnalyticsSelected( - item.id, - controller.widget.tabs[0].type, - item.displayName, - ), - isSelected: - controller.isSelected(item.id), - onTap: (_) => - controller.toggleSelection( - AnalyticsSelected( - item.id, - controller.widget.tabs[0].type, - item.displayName, - ), - ), - allowNavigateOnSelect: controller - .widget - .tabs[0] - .allowNavigateOnSelect, - pangeaController: - controller.pangeaController, - controller: controller, - ), - ), - if (controller - .widget.defaultSelected.type == - AnalyticsEntryType.space) - AnalyticsListTile( - refreshStream: - controller.refreshStream, - defaultSelected: controller - .widget.defaultSelected, - avatar: null, - selected: AnalyticsSelected( - controller - .widget.defaultSelected.id, - AnalyticsEntryType.privateChats, - L10n.of(context)!.allPrivateChats, - ), - allowNavigateOnSelect: false, - isSelected: controller.isSelected( - controller - .widget.defaultSelected.id, - ), - onTap: controller.toggleSelection, - pangeaController: - controller.pangeaController, - controller: controller, - ), - ], - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: controller.widget.tabs[1].items - .map( - (item) => AnalyticsListTile( - refreshStream: - controller.refreshStream, - avatar: item.avatar, - defaultSelected: controller - .widget.defaultSelected, - selected: AnalyticsSelected( - item.id, - controller.widget.tabs[1].type, - item.displayName, - ), - isSelected: controller - .isSelected(item.id), - onTap: controller.toggleSelection, - allowNavigateOnSelect: controller - .widget - .tabs[1] - .allowNavigateOnSelect, - pangeaController: - controller.pangeaController, - controller: controller, - ), - ) - .toList(), - ), - ], - ), - ), - ), - ), - ], + ), ), ), - ), - ], - ) - : Column( - children: [ - const Divider(height: 1), - ListTile( - title: Text(L10n.of(context)!.grammarAnalytics), - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: - Theme.of(context).textTheme.bodyLarge!.color, - child: Icon(BarChartViewSelection.grammar.icon), - ), - trailing: const Icon(Icons.chevron_right), - onTap: () { - String route = - "/rooms/${controller.widget.defaultSelected.type.route}"; - if (controller.widget.defaultSelected.type == - AnalyticsEntryType.space) { - route += "/${controller.widget.defaultSelected.id}"; - } - route += "/${BarChartViewSelection.grammar.route}"; - context.go(route); - }, - ), - const Divider(height: 1), - ListTile( - title: Text(L10n.of(context)!.messageAnalytics), - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: - Theme.of(context).textTheme.bodyLarge!.color, - child: Icon(BarChartViewSelection.messages.icon), - ), - trailing: const Icon(Icons.chevron_right), - onTap: () { - String route = - "/rooms/${controller.widget.defaultSelected.type.route}"; - if (controller.widget.defaultSelected.type == - AnalyticsEntryType.space) { - route += "/${controller.widget.defaultSelected.id}"; - } - route += "/${BarChartViewSelection.messages.route}"; - context.go(route); - }, - ), - const Divider(height: 1), - ], + ], + ), ), + ), + ], + ), ), ); } diff --git a/lib/pangea/pages/analytics/construct_list.dart b/lib/pangea/pages/analytics/construct_list.dart index 8651b7a74..d46936c86 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/space_analytics/space_analytics.dart b/lib/pangea/pages/analytics/space_analytics/space_analytics.dart index b32780761..2db1acb4c 100644 --- a/lib/pangea/pages/analytics/space_analytics/space_analytics.dart +++ b/lib/pangea/pages/analytics/space_analytics/space_analytics.dart @@ -18,8 +18,8 @@ import '../../../utils/sync_status_util_v2.dart'; import 'space_analytics_view.dart'; class SpaceAnalyticsPage extends StatefulWidget { - final BarChartViewSelection? selectedView; - const SpaceAnalyticsPage({super.key, this.selectedView}); + final BarChartViewSelection selectedView; + const SpaceAnalyticsPage({super.key, required this.selectedView}); @override State createState() => SpaceAnalyticsV2Controller(); diff --git a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart index d6bc9d766..e06c6d0ba 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'; @@ -19,8 +18,8 @@ import '../base_analytics.dart'; import 'student_analytics_view.dart'; class StudentAnalyticsPage extends StatefulWidget { - final BarChartViewSelection? selectedView; - const StudentAnalyticsPage({super.key, this.selectedView}); + final BarChartViewSelection selectedView; + const StudentAnalyticsPage({super.key, required this.selectedView}); @override State createState() => StudentAnalyticsController(); @@ -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 9517515d0..d6271470a 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,31 +40,29 @@ 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), - hasInfo: true, lemmas: [Lemma(form: "This", text: "this", saveVocab: true)], ), PangeaToken( text: PangeaTokenText(content: "be", offset: 5, length: 2), - hasInfo: true, lemmas: [Lemma(form: "be", text: "be", saveVocab: true)], ), PangeaToken( text: PangeaTokenText(content: "a", offset: 8, length: 1), - hasInfo: false, lemmas: [], ), PangeaToken( text: PangeaTokenText(content: "sample", offset: 10, length: 6), - hasInfo: false, lemmas: [], ), PangeaToken( text: PangeaTokenText(content: "text", offset: 17, length: 4), - hasInfo: false, lemmas: [], ), ], @@ -89,7 +88,6 @@ class IGCRequestBody { String fullText; String userL1; String userL2; - bool tokensOnly; bool enableIT; bool enableIGC; @@ -99,7 +97,6 @@ class IGCRequestBody { required this.userL2, required this.enableIGC, required this.enableIT, - this.tokensOnly = false, }); Map toJson() => { @@ -108,6 +105,5 @@ class IGCRequestBody { ModelKey.userL2: userL2, "enable_it": enableIT, "enable_igc": enableIGC, - "tokens_only": tokensOnly, }; } diff --git a/lib/pangea/utils/error_handler.dart b/lib/pangea/utils/error_handler.dart index d66c29186..2144e9f87 100644 --- a/lib/pangea/utils/error_handler.dart +++ b/lib/pangea/utils/error_handler.dart @@ -8,6 +8,11 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:http/http.dart' as http; import 'package:sentry_flutter/sentry_flutter.dart'; +class PangeaWarningError implements Exception { + final String message; + PangeaWarningError(message) : message = "Pangea Warning Error: $message"; +} + class ErrorHandler { ErrorHandler(); diff --git a/lib/pangea/utils/firebase_analytics.dart b/lib/pangea/utils/firebase_analytics.dart index 59485d35b..323a7a172 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 5adc0040c..bd8d064f5 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/utils/inline_tooltip.dart b/lib/pangea/utils/inline_tooltip.dart new file mode 100644 index 000000000..253288a84 --- /dev/null +++ b/lib/pangea/utils/inline_tooltip.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +class InlineTooltip extends StatelessWidget { + final String body; + final VoidCallback onClose; + + const InlineTooltip({ + super.key, + required this.body, + required this.onClose, + }); + + @override + Widget build(BuildContext context) { + return Badge( + offset: const Offset(0, -7), + backgroundColor: Colors.transparent, + label: CircleAvatar( + radius: 10, + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon( + Icons.close_outlined, + size: 15, + ), + onPressed: onClose, + ), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Theme.of(context).colorScheme.primary.withAlpha(20), + ), + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.justify, + text: TextSpan( + children: [ + const WidgetSpan( + child: Icon( + Icons.lightbulb, + size: 16, + ), + ), + const WidgetSpan( + child: SizedBox(width: 5), + ), + TextSpan( + text: body, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/pangea/utils/instructions.dart b/lib/pangea/utils/instructions.dart index ff0fea8f6..382268082 100644 --- a/lib/pangea/utils/instructions.dart +++ b/lib/pangea/utils/instructions.dart @@ -1,4 +1,5 @@ -import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/pangea/enum/instructions_enum.dart'; +import 'package:fluffychat/pangea/utils/inline_tooltip.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -14,17 +15,29 @@ import 'overlay.dart'; class InstructionsController { late PangeaController _pangeaController; + // We have these three methods to make sure that the instructions are not shown too much + + /// Instruction popup was closed by the user final Map _instructionsClosed = {}; + + /// Instruction popup has already been shown this session final Map _instructionsShown = {}; + /// Returns true if the user requested this popup not be shown again + bool? toggledOff(InstructionsEnum key) => + _pangeaController.pStoreService.read(key.toString()); + InstructionsController(PangeaController pangeaController) { _pangeaController = pangeaController; } + /// Returns true if the instructions were closed + /// or turned off by the user via the toggle switch bool wereInstructionsTurnedOff(InstructionsEnum key) => - _pangeaController.pStoreService.read(key.toString()) ?? - _instructionsClosed[key] ?? - false; + toggledOff(key) ?? _instructionsClosed[key] ?? false; + + void turnOffInstruction(InstructionsEnum key) => + _instructionsClosed[key] = true; Future updateEnableInstructions( InstructionsEnum key, @@ -35,7 +48,9 @@ class InstructionsController { value, ); - Future show( + /// Instruction Card gives users tips on + /// how to use Pangea Chat's features + Future showInstructionsPopup( BuildContext context, InstructionsEnum key, String transformTargetKey, [ @@ -98,45 +113,35 @@ class InstructionsController { ), ); } -} -enum InstructionsEnum { - itInstructions, - clickMessage, - blurMeansTranslate, - tooltipInstructions, -} - -extension Copy on InstructionsEnum { - String title(BuildContext context) { - switch (this) { - case InstructionsEnum.itInstructions: - return L10n.of(context)!.itInstructionsTitle; - case InstructionsEnum.clickMessage: - return L10n.of(context)!.clickMessageTitle; - case InstructionsEnum.blurMeansTranslate: - return L10n.of(context)!.blurMeansTranslateTitle; - case InstructionsEnum.tooltipInstructions: - return L10n.of(context)!.tooltipInstructionsTitle; + /// Returns a widget that will be added to existing widget + /// which displays hint text defined in the enum extension + Widget getInstructionInlineTooltip( + BuildContext context, + InstructionsEnum key, + VoidCallback onClose, + ) { + if (wereInstructionsTurnedOff(key)) { + return const SizedBox(); } - } - String body(BuildContext context) { - switch (this) { - case InstructionsEnum.itInstructions: - return L10n.of(context)!.itInstructionsBody; - case InstructionsEnum.clickMessage: - return L10n.of(context)!.clickMessageBody; - case InstructionsEnum.blurMeansTranslate: - return L10n.of(context)!.blurMeansTranslateBody; - case InstructionsEnum.tooltipInstructions: - return PlatformInfos.isMobile - ? L10n.of(context)!.tooltipInstructionsMobileBody - : L10n.of(context)!.tooltipInstructionsBrowserBody; + if (L10n.of(context) == null) { + ErrorHandler.logError( + m: "null context in ITBotButton.showCard", + s: StackTrace.current, + ); + return const SizedBox(); } + + return InlineTooltip( + body: InstructionsEnum.speechToText.body(context), + onClose: onClose, + ); } } +/// User can toggle on to prevent Instruction Card +/// from appearing in future sessions class InstructionsToggle extends StatefulWidget { const InstructionsToggle({ super.key, diff --git a/lib/pangea/widgets/chat/message_speech_to_text_card.dart b/lib/pangea/widgets/chat/message_speech_to_text_card.dart index 9099b9b95..a26a36658 100644 --- a/lib/pangea/widgets/chat/message_speech_to_text_card.dart +++ b/lib/pangea/widgets/chat/message_speech_to_text_card.dart @@ -1,9 +1,9 @@ import 'dart:developer'; +import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/speech_to_text_models.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:fluffychat/pangea/utils/instructions.dart'; import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart'; import 'package:fluffychat/pangea/widgets/common/icon_number_widget.dart'; import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart'; @@ -65,6 +65,13 @@ class MessageSpeechToTextCardState extends State { } } + void closeHint() { + MatrixState.pangeaController.instructions.turnOffInstruction( + InstructionsEnum.speechToText, + ); + setState(() {}); + } + TextSpan _buildTranscriptText(BuildContext context) { final Transcript transcript = speechToTextResponse!.transcript; final List spans = []; @@ -172,7 +179,8 @@ class MessageSpeechToTextCardState extends State { number: "${selectedToken?.confidence ?? speechToTextResponse!.transcript.confidence}%", toolTip: L10n.of(context)!.accuracy, - onPressed: () => MatrixState.pangeaController.instructions.show( + onPressed: () => MatrixState.pangeaController.instructions + .showInstructionsPopup( context, InstructionsEnum.tooltipInstructions, widget.messageEvent.eventId, @@ -184,7 +192,8 @@ class MessageSpeechToTextCardState extends State { number: wordsPerMinuteString != null ? "$wordsPerMinuteString" : "??", toolTip: L10n.of(context)!.wordsPerMinute, - onPressed: () => MatrixState.pangeaController.instructions.show( + onPressed: () => MatrixState.pangeaController.instructions + .showInstructionsPopup( context, InstructionsEnum.tooltipInstructions, widget.messageEvent.eventId, @@ -193,6 +202,11 @@ class MessageSpeechToTextCardState extends State { ), ], ), + MatrixState.pangeaController.instructions.getInstructionInlineTooltip( + context, + InstructionsEnum.speechToText, + closeHint, + ), ], ); } diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 57b5845a4..8d2d66b7d 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -137,6 +137,8 @@ class ToolbarDisplayController { ? Alignment.bottomLeft : Alignment.topLeft, backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(100), + closePrevOverlay: + MatrixState.pangeaController.subscriptionController.isSubscribed, ); if (MatrixState.pAnyState.entries.isNotEmpty) { diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart index 094f481ec..31b6addb2 100644 --- a/lib/pangea/widgets/chat/message_translation_card.dart +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -29,41 +29,26 @@ class MessageTranslationCardState extends State { PangeaRepresentation? repEvent; String? selectionTranslation; String? oldSelectedText; - String? l1Code; - String? l2Code; bool _fetchingRepresentation = false; - String? translationLangCode() { - if (widget.immersionMode) return l1Code; - final String? originalSentCode = - widget.messageEvent.originalSent?.content.langCode; - return (l1Code == originalSentCode || originalSentCode == null) - ? l2Code - : l1Code; - } - Future fetchRepresentation(BuildContext context) async { - final String? langCode = translationLangCode(); - if (langCode == null) return; + if (l1Code == null) return; repEvent = widget.messageEvent .representationByLanguage( - langCode, + l1Code!, ) ?.content; if (repEvent == null && mounted) { repEvent = await widget.messageEvent.representationByLanguageGlobal( - langCode: langCode, + langCode: l1Code!, ); } } Future translateSelection() async { - final String? targetLang = translationLangCode(); - if (widget.selection.selectedText == null || - targetLang == null || l1Code == null || l2Code == null) { selectionTranslation = null; @@ -78,7 +63,7 @@ class MessageTranslationCardState extends State { accessToken: accessToken, request: FullTextTranslationRequestModel( text: widget.selection.messageText, - tgtLang: translationLangCode()!, + tgtLang: l1Code!, userL1: l1Code!, userL2: l2Code!, srcLang: widget.messageEvent.messageDisplayLangCode, @@ -106,11 +91,14 @@ class MessageTranslationCardState extends State { } } + String? get l1Code => + MatrixState.pangeaController.languageController.activeL1Code(); + String? get l2Code => + MatrixState.pangeaController.languageController.activeL2Code(); + @override void initState() { super.initState(); - l1Code = MatrixState.pangeaController.languageController.activeL1Code(); - l2Code = MatrixState.pangeaController.languageController.activeL2Code(); if (mounted) { setState(() {}); } diff --git a/lib/pangea/widgets/chat/overlay_message.dart b/lib/pangea/widgets/chat/overlay_message.dart index d7c99f07b..5f3d46c7e 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/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index bbd6868bf..abf583b3b 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -3,10 +3,10 @@ import 'dart:ui'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:fluffychat/pangea/utils/instructions.dart'; import 'package:fluffychat/pangea/widgets/chat/message_context_menu.dart'; import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -57,20 +57,28 @@ class PangeaRichTextState extends State { } void _setTextSpan(String newTextSpan) { - widget.toolbarController?.toolbar?.textSelection.setMessageText( - newTextSpan, - ); - if (mounted) { + try { + if (!mounted) return; // Early exit if the widget is no longer in the tree + + widget.toolbarController?.toolbar?.textSelection.setMessageText( + newTextSpan, + ); setState(() { textSpan = newTextSpan; }); + } catch (error, stackTrace) { + ErrorHandler.logError( + e: PangeaWarningError(error), + s: stackTrace, + m: "Error setting text span in PangeaRichText", + ); } } void setTextSpan() { - if (_fetchingRepresentation == true) { + if (_fetchingRepresentation) { _setTextSpan( - textSpan = widget.pangeaMessageEvent.event + widget.pangeaMessageEvent.event .getDisplayEvent(widget.pangeaMessageEvent.timeline) .body, ); @@ -91,13 +99,17 @@ class PangeaRichTextState extends State { setState(() => _fetchingRepresentation = true); widget.pangeaMessageEvent .representationByLanguageGlobal( - langCode: widget.pangeaMessageEvent.messageDisplayLangCode, - ) - .onError( - (error, stackTrace) => - ErrorHandler.logError(e: error, s: stackTrace), - ) - .then((event) { + langCode: widget.pangeaMessageEvent.messageDisplayLangCode, + ) + .onError((error, stackTrace) { + ErrorHandler.logError( + e: PangeaWarningError(error), + s: stackTrace, + m: "Error fetching representation", + ); + return null; + }).then((event) { + if (!mounted) return; repEvent = event; _setTextSpan(repEvent?.text ?? widget.pangeaMessageEvent.body); }).whenComplete(() { @@ -115,7 +127,7 @@ class PangeaRichTextState extends State { @override Widget build(BuildContext context) { if (blur > 0) { - pangeaController.instructions.show( + pangeaController.instructions.showInstructionsPopup( context, InstructionsEnum.blurMeansTranslate, widget.pangeaMessageEvent.eventId, diff --git a/lib/pangea/widgets/igc/word_data_card.dart b/lib/pangea/widgets/igc/word_data_card.dart index 5a785c795..1d789424d 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 240e99a85..67089145e 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: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 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.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 28fc00bd1..54ff5586c 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 3b2678e29..aeb1cfd36 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 51082a7e6..8b7ae33b5 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 00ebd3cb5..093b78f81 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 09c588b29..47b695fb9 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/pangea_packages/fcm_shared_isolate/pubspec.lock b/pangea_packages/fcm_shared_isolate/pubspec.lock index 55c0cb16e..7f3457dbe 100644 --- a/pangea_packages/fcm_shared_isolate/pubspec.lock +++ b/pangea_packages/fcm_shared_isolate/pubspec.lock @@ -149,33 +149,41 @@ packages: description: name: matcher sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted version: "0.12.16+1" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted version: "0.8.0" + version: "0.8.0" meta: dependency: transitive description: name: meta sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted version: "1.12.0" + version: "1.12.0" path: dependency: transitive description: name: path sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted version: "1.9.0" + version: "1.9.0" pedantic: dependency: "direct dev" description: @@ -242,9 +250,11 @@ packages: description: name: test_api sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted version: "0.7.0" + version: "0.7.0" vector_math: dependency: transitive description: @@ -272,3 +282,5 @@ packages: sdks: dart: ">=3.3.0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 4798ca389..8fbbffa18 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 bf885c6b1..315ce5112 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