diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 0e326ab6e..77130f635 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -183,6 +183,7 @@ class Choreographer { _textController.setSystemText("", EditType.itStart); } + /// Handles any changes to the text input _onChangeListener() { if (_noChange) { return; @@ -191,21 +192,26 @@ class Choreographer { if ([ EditType.igc, ].contains(_textController.editType)) { + // this may be unnecessary now that tokens are not used + // to allow click of words in the input field and we're getting this at the end + // TODO - turn it off and tested that this is fine igc.justGetTokensAndAddThemToIGCTextData(); + + // we set editType to keyboard here because that is the default for it + // and we want to make sure that the next change is treated as a keyboard change + // unless the system explicity sets it to something else. this textController.editType = EditType.keyboard; return; } + // not sure if this is necessary now MatrixState.pAnyState.closeOverlay(); if (errorService.isError) { return; } - // if (igc.igcTextData != null) { igc.clear(); - // setState(); - // } _resetDebounceTimer(); @@ -215,7 +221,9 @@ class Choreographer { () => getLanguageHelp(), ); } else { - getLanguageHelp(ChoreoMode.it == choreoMode); + getLanguageHelp( + onlyTokensAndLanguageDetection: ChoreoMode.it == choreoMode, + ); } //Note: we don't set the keyboard type on each keyboard stroke so this is how we default to @@ -224,10 +232,14 @@ class Choreographer { textController.editType = EditType.keyboard; } - Future getLanguageHelp([ - bool tokensOnly = false, + /// Fetches the language help for the current text, including grammar correction, language detection, + /// tokens, and translations. Includes logic to exit the flow if the user is not subscribed, if the tools are not enabled, or + /// or if autoIGC is not enabled and the user has not manually requested it. + /// [onlyTokensAndLanguageDetection] will + Future getLanguageHelp({ + bool onlyTokensAndLanguageDetection = false, bool manual = false, - ]) async { + }) async { try { if (errorService.isError) return; final CanSendStatus canSendStatus = @@ -242,13 +254,15 @@ class Choreographer { startLoading(); if (choreoMode == ChoreoMode.it && itController.isTranslationDone && - !tokensOnly) { + !onlyTokensAndLanguageDetection) { // debugger(when: kDebugMode); } await (choreoMode == ChoreoMode.it && !itController.isTranslationDone ? itController.getTranslationData(_useCustomInput) - : igc.getIGCTextData(tokensOnly: tokensOnly)); + : igc.getIGCTextData( + onlyTokensAndLanguageDetection: onlyTokensAndLanguageDetection, + )); } catch (err, stack) { ErrorHandler.logError(e: err, s: stack); } finally { @@ -494,8 +508,9 @@ class Choreographer { // TODO - this is a bit of a hack, and should be tested more // we should also check that user has not done customInput - if (itController.completedITSteps.isNotEmpty && itController.allCorrect) + if (itController.completedITSteps.isNotEmpty && itController.allCorrect) { return l2LangCode!; + } return null; } @@ -533,9 +548,11 @@ class Choreographer { chatController.room, ); - bool get itAutoPlayEnabled => pangeaController.pStoreService.read( + bool get itAutoPlayEnabled => + pangeaController.pStoreService.read( MatrixProfile.itAutoPlay.title, - ) ?? false; + ) ?? + false; bool get definitionsEnabled => pangeaController.permissionsController.isToolEnabled( diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index 68d81389e..a694c48a5 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -3,7 +3,7 @@ import 'dart:developer'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart'; -import 'package:fluffychat/pangea/controllers/span_data_controller.dart'; +import 'package:fluffychat/pangea/choreographer/controllers/span_data_controller.dart'; import 'package:fluffychat/pangea/models/igc_text_data_model.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; import 'package:fluffychat/pangea/repo/igc_repo.dart'; @@ -29,59 +29,64 @@ class IgcController { spanDataController = SpanDataController(choreographer); } - Future getIGCTextData({required bool tokensOnly}) async { + Future getIGCTextData({ + required bool onlyTokensAndLanguageDetection, + }) async { try { if (choreographer.currentText.isEmpty) return clear(); // the error spans are going to be reloaded, so clear the cache + // @ggurdin: Why is this separate from the clear() call? + // Also, if the spans are equal according the to the equals method, why not reuse the cached span data? + // It seems this would save some calls if the user makes some tiny changes to the text that don't + // change the matches at all. spanDataController.clearCache(); debugPrint('getIGCTextData called with ${choreographer.currentText}'); - debugPrint('getIGCTextData called with tokensOnly = $tokensOnly'); + debugPrint( + 'getIGCTextData called with tokensOnly = $onlyTokensAndLanguageDetection', + ); final IGCRequestBody reqBody = IGCRequestBody( fullText: choreographer.currentText, userL1: choreographer.l1LangCode!, userL2: choreographer.l2LangCode!, - enableIGC: choreographer.igcEnabled && !tokensOnly, - enableIT: choreographer.itEnabled && !tokensOnly, - tokensOnly: tokensOnly, + enableIGC: choreographer.igcEnabled && !onlyTokensAndLanguageDetection, + enableIT: choreographer.itEnabled && !onlyTokensAndLanguageDetection, ); final IGCTextData igcTextDataResponse = await IgcRepo.getIGC( await choreographer.accessToken, igcRequest: reqBody, ); - // temp fix - igcTextDataResponse.originalInput = reqBody.fullText; - //this will happen when the user changes the input while igc is fetching results + // this will happen when the user changes the input while igc is fetching results if (igcTextDataResponse.originalInput != choreographer.currentText) { - // final current = choreographer.currentText; - // final igctext = igcTextDataResponse.originalInput; - // Sentry.addBreadcrumb( - // Breadcrumb(message: "igc return input does not match current text"), - // ); - // debugger(when: kDebugMode); return; } //TO-DO: in api call, specify turning off IT and/or grammar checking - if (!choreographer.igcEnabled) { - igcTextDataResponse.matches = igcTextDataResponse.matches - .where((match) => !match.isGrammarMatch) - .toList(); - } - if (!choreographer.itEnabled) { - igcTextDataResponse.matches = igcTextDataResponse.matches - .where((match) => !match.isOutOfTargetMatch) - .toList(); - } - if (!choreographer.itEnabled && !choreographer.igcEnabled) { - igcTextDataResponse.matches = []; - } + // UPDATE: This is now done in the API call. New TODO is to test this. + // if (!choreographer.igcEnabled) { + // igcTextDataResponse.matches = igcTextDataResponse.matches + // .where((match) => !match.isGrammarMatch) + // .toList(); + // } + // if (!choreographer.itEnabled) { + // igcTextDataResponse.matches = igcTextDataResponse.matches + // .where((match) => !match.isOutOfTargetMatch) + // .toList(); + // } + // if (!choreographer.itEnabled && !choreographer.igcEnabled) { + // igcTextDataResponse.matches = []; + // } igcTextData = igcTextDataResponse; + // TODO - for each new match, + // check if existing igcTextData has one and only one match with the same error text and correction + // if so, keep the original match and discard the new one + // if not, add the new match to the existing igcTextData + // After fetching igc data, pre-call span details for each match optimistically. // This will make the loading of span details faster for the user if (igcTextData?.matches.isNotEmpty ?? false) { @@ -170,11 +175,9 @@ class IgcController { const int firstMatchIndex = 0; final PangeaMatch match = igcTextData!.matches[firstMatchIndex]; - if ( - match.isITStart && + if (match.isITStart && choreographer.itAutoPlayEnabled && - igcTextData != null - ) { + igcTextData != null) { choreographer.onITStart(igcTextData!.matches[firstMatchIndex]); return; } diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart index 8bd60270b..225d2fec6 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -72,6 +72,7 @@ class ITController { /// if IGC isn't positive that text is full L1 then translate to L1 Future _setSourceText() async { + debugger(when: kDebugMode); // try { if (_itStartData == null || _itStartData!.text.isEmpty) { Sentry.addBreadcrumb( @@ -167,7 +168,7 @@ class ITController { if (isTranslationDone) { choreographer.altTranslator.setTranslationFeedback(); - choreographer.getLanguageHelp(true); + choreographer.getLanguageHelp(onlyTokensAndLanguageDetection: true); } else { getNextTranslationData(); } @@ -218,7 +219,6 @@ class ITController { Future onEditSourceTextSubmit(String newSourceText) async { try { - _isOpen = true; _isEditingSourceText = false; _itStartData = ITStartData(newSourceText, choreographer.l1LangCode); @@ -230,7 +230,6 @@ class ITController { _setSourceText(); getTranslationData(false); - } catch (err, stack) { debugger(when: kDebugMode); if (err is! http.Response) { diff --git a/lib/pangea/controllers/span_data_controller.dart b/lib/pangea/choreographer/controllers/span_data_controller.dart similarity index 100% rename from lib/pangea/controllers/span_data_controller.dart rename to lib/pangea/choreographer/controllers/span_data_controller.dart diff --git a/lib/pangea/choreographer/widgets/start_igc_button.dart b/lib/pangea/choreographer/widgets/start_igc_button.dart index 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/config/environment.dart b/lib/pangea/config/environment.dart index 4d4378999..de7039f9d 100644 --- a/lib/pangea/config/environment.dart +++ b/lib/pangea/config/environment.dart @@ -5,7 +5,7 @@ class Environment { DateTime.utc(2023, 1, 25).isBefore(DateTime.now()); static String get fileName { - return ".env"; + return ".local_choreo.env"; } static bool get isStaging => synapsURL.contains("staging"); diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 5a2b9d02a..75a18ad74 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -239,7 +239,7 @@ class MyAnalyticsController { } final List> recentMsgs = (await Future.wait(recentMsgFutures)).toList(); - final List recentActivityReconds = + final List recentActivityRecords = (await Future.wait(recentActivityFutures)) .expand((e) => e) .map((event) => PracticeActivityRecordEvent(event: event)) @@ -284,14 +284,14 @@ class MyAnalyticsController { } // get constructs for messages - final List constructContent = []; + final List recentConstructUses = []; for (final PangeaMessageEvent message in allRecentMessages) { - constructContent.addAll(message.allConstructUses); + recentConstructUses.addAll(message.allConstructUses); } // get constructs for practice activities final List>> constructFutures = []; - for (final PracticeActivityRecordEvent activity in recentActivityReconds) { + for (final PracticeActivityRecordEvent activity in recentActivityRecords) { final Timeline? timeline = timelineMap[activity.event.roomId!]; if (timeline == null) { debugger(when: kDebugMode); @@ -306,13 +306,13 @@ class MyAnalyticsController { final List> constructLists = await Future.wait(constructFutures); - constructContent.addAll(constructLists.expand((e) => e)); + recentConstructUses.addAll(constructLists.expand((e) => e)); //TODO - confirm that this is the correct construct content - debugger(when: kDebugMode); + debugger(when: kDebugMode && recentConstructUses.isNotEmpty); await analyticsRoom.sendConstructsEvent( - constructContent, + recentConstructUses, ); } } diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index b6702d7d2..28253d419 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -656,106 +656,47 @@ class PangeaMessageEvent { } } - List get allConstructUses => - [...grammarConstructUses, ..._vocabUses]; - /// Returns a list of [PracticeActivityEvent] for the user's active l2. - List get practiceActivities { - final String? l2code = - MatrixState.pangeaController.languageController.activeL2Code(); - if (l2code == null) return []; - return practiceActivitiesByLangCode(l2code); - } + List get practiceActivities => + l2Code == null ? [] : practiceActivitiesByLangCode(l2Code!); - // List get activities => - //each match is turned into an activity that other students can access - //they're not told the answer but have to find it themselves - //the message has a blank piece which they fill in themselves + /// all construct uses for the message, including vocab and grammar + List get allConstructUses => + [..._grammarConstructUses, ..._vocabUses]; - /// [tokens] is the final list of tokens that were sent - /// if no ga or ta, - /// make wa use for each and return - /// else - /// for each saveable vocab in the final message - /// if vocab is contained in an accepted replacement, make ga use - /// if vocab is contained in ta choice, - /// if selected as choice, corIt - /// if written as customInput, corIt? (account for score in this) - /// for each it step - /// for each continuance - /// if not within the final message, save ignIT/incIT + /// get construct uses of type vocab for the message List get _vocabUses { + debugger(); final List uses = []; - if (event.roomId == null) return uses; - - List lemmasToVocabUses( - List lemmas, - ConstructUseTypeEnum type, - ) { - final List uses = []; - for (final lemma in lemmas) { - if (lemma.saveVocab) { - uses.add( - OneConstructUse( - useType: type, - chatId: event.roomId!, - timeStamp: event.originServerTs, - lemma: lemma.text, - form: lemma.form, - msgId: event.eventId, - constructType: ConstructTypeEnum.vocab, - ), - ); - } - } + // missing vital info so return. should not happen + if (event.roomId == null) { + debugger(when: kDebugMode); return uses; } - List getVocabUseForToken(PangeaToken token) { - if (originalSent?.choreo == null) { - final bool inUserL2 = originalSent?.langCode == l2Code; - return lemmasToVocabUses( - token.lemmas, - inUserL2 ? ConstructUseTypeEnum.wa : ConstructUseTypeEnum.unk, - ); - } - - // - for (final step in originalSent!.choreo!.choreoSteps) { - /// if 1) accepted match 2) token is in the replacement and 3) replacement - /// is in the overall step text, then token was a ga - if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted && - (step.acceptedOrIgnoredMatch!.match.choices?.any( - (r) => - r.value.contains(token.text.content) && - step.text.contains(r.value), - ) ?? - false)) { - return lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.ga); - } - if (step.itStep != null) { - final bool pickedThroughIT = step.itStep!.chosenContinuance?.text - .contains(token.text.content) ?? - false; - if (pickedThroughIT) { - return lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.corIt); - //PTODO - check if added via custom input in IT flow - } - } - } - return lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.wa); - } - - /// for each token, record whether selected in ga, ta, or wa + // for each token, record whether selected in ga, ta, or wa if (originalSent?.tokens != null) { for (final token in originalSent!.tokens!) { - uses.addAll(getVocabUseForToken(token)); + uses.addAll(_getVocabUseForToken(token)); } } - if (originalSent?.choreo == null) return uses; + // add construct uses related to IT use + uses.addAll(_itStepsToConstructUses); + return uses; + } + + /// Returns a list of [OneConstructUse] from itSteps for which the continuance + /// was selected or ignored. Correct selections are considered in the tokens + /// flow. Once all continuances have lemmas, we can do both correct and incorrect + /// in this flow. It actually doesn't do anything at all right now, because the + /// choregrapher is not returning lemmas for continuances. This is a TODO. + /// So currently only the lemmas can be gotten from the tokens for choices that + /// are actually in the final message. + List get _itStepsToConstructUses { + final List uses = []; for (final itStep in originalSent!.choreo!.itSteps) { for (final continuance in itStep.continuances) { // this seems to always be false for continuances right now @@ -767,23 +708,98 @@ class PangeaMessageEvent { //PTODO - account for end of flow score if (continuance.level != ChoreoConstants.levelThresholdForGreen) { uses.addAll( - lemmasToVocabUses(continuance.lemmas, ConstructUseTypeEnum.incIt), + _lemmasToVocabUses( + continuance.lemmas, + ConstructUseTypeEnum.incIt, + ), ); } } else { if (continuance.level != ChoreoConstants.levelThresholdForGreen) { uses.addAll( - lemmasToVocabUses(continuance.lemmas, ConstructUseTypeEnum.ignIt), + _lemmasToVocabUses( + continuance.lemmas, + ConstructUseTypeEnum.ignIt, + ), ); } } } } - return uses; } - List get grammarConstructUses { + /// Returns a list of [OneConstructUse] objects for the given [token] + /// If there is no [originalSent] or [originalSent.choreo], the [token] is + /// considered to be a [ConstructUseTypeEnum.wa] as long as it matches the target language. + /// Later on, we may want to consider putting it in some category of like 'pending' + /// If the [token] is in the [originalSent.choreo.acceptedOrIgnoredMatch], + /// it is considered to be a [ConstructUseTypeEnum.ga]. + /// If the [token] is in the [originalSent.choreo.acceptedOrIgnoredMatch.choices], + /// it is considered to be a [ConstructUseTypeEnum.corIt]. + /// If the [token] is not included in any choreoStep, it is considered to be a [ConstructUseTypeEnum.wa]. + List _getVocabUseForToken(PangeaToken token) { + debugger(); + if (originalSent?.choreo == null) { + final bool inUserL2 = originalSent?.langCode == l2Code; + return _lemmasToVocabUses( + token.lemmas, + inUserL2 ? ConstructUseTypeEnum.wa : ConstructUseTypeEnum.unk, + ); + } + + for (final step in originalSent!.choreo!.choreoSteps) { + /// if 1) accepted match 2) token is in the replacement and 3) replacement + /// is in the overall step text, then token was a ga + if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted && + (step.acceptedOrIgnoredMatch!.match.choices?.any( + (r) => + r.value.contains(token.text.content) && + step.text.contains(r.value), + ) ?? + false)) { + return _lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.ga); + } + if (step.itStep != null) { + final bool pickedThroughIT = + step.itStep!.chosenContinuance?.text.contains(token.text.content) ?? + false; + if (pickedThroughIT) { + return _lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.corIt); + //PTODO - check if added via custom input in IT flow + } + } + } + return _lemmasToVocabUses(token.lemmas, ConstructUseTypeEnum.wa); + } + + /// Convert a list of [lemmas] into a list of vocab uses + /// with the given [type] + List _lemmasToVocabUses( + List lemmas, + ConstructUseTypeEnum type, + ) { + final List uses = []; + for (final lemma in lemmas) { + if (lemma.saveVocab) { + uses.add( + OneConstructUse( + useType: type, + chatId: event.roomId!, + timeStamp: event.originServerTs, + lemma: lemma.text, + form: lemma.form, + msgId: event.eventId, + constructType: ConstructTypeEnum.vocab, + ), + ); + } + } + return uses; + } + + /// get construct uses of type grammar for the message + List get _grammarConstructUses { final List uses = []; if (originalSent?.choreo == null || event.roomId == null) return uses; diff --git a/lib/pangea/repo/igc_repo.dart b/lib/pangea/repo/igc_repo.dart index 9517515d0..5f281abe6 100644 --- a/lib/pangea/repo/igc_repo.dart +++ b/lib/pangea/repo/igc_repo.dart @@ -89,7 +89,6 @@ class IGCRequestBody { String fullText; String userL1; String userL2; - bool tokensOnly; bool enableIT; bool enableIGC; @@ -99,7 +98,6 @@ class IGCRequestBody { required this.userL2, required this.enableIGC, required this.enableIT, - this.tokensOnly = false, }); Map toJson() => { @@ -108,6 +106,5 @@ class IGCRequestBody { ModelKey.userL2: userL2, "enable_it": enableIT, "enable_igc": enableIGC, - "tokens_only": tokensOnly, }; }