From 4bbb81e20ca5de5beeb18ba43d228516f18af5f1 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 4 Nov 2025 14:39:16 -0500 Subject: [PATCH] only rebuild choreo-widgets when related data updates --- lib/pages/chat/chat_view.dart | 4 +- lib/pages/chat/input_bar.dart | 2 +- .../activity_role_tooltip.dart | 19 +- lib/pangea/chat/widgets/chat_input_bar.dart | 3 +- .../chat/widgets/chat_view_background.dart | 14 +- .../chat/widgets/pangea_chat_input_row.dart | 4 +- .../controllers/choreographer.dart | 283 +++++++++--------- .../choreographer_state_extension.dart | 24 +- .../controllers/igc_controller.dart | 124 ++------ .../controllers/it_controller.dart | 44 +-- .../choreographer/repo/it_request_model.dart | 12 - .../choreographer/widgets/igc/span_card.dart | 10 +- lib/pangea/choreographer/widgets/it_bar.dart | 16 +- .../choreographer/widgets/send_button.dart | 31 +- .../widgets/start_igc_button.dart | 3 +- .../extensions/pangea_room_extension.dart | 1 + .../extensions/room_events_extension.dart | 36 +++ 17 files changed, 288 insertions(+), 342 deletions(-) diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 443de4427..b0abd2d12 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -353,7 +353,9 @@ class ChatView extends StatelessWidget { child: Stack( children: [ ChatEventList(controller: controller), - ChatViewBackground(controller.choreographer), + ChatViewBackground( + controller.choreographer.itController.open, + ), ], ), // Pangea# diff --git a/lib/pages/chat/input_bar.dart b/lib/pages/chat/input_bar.dart index ee2f84a25..67e29b291 100644 --- a/lib/pages/chat/input_bar.dart +++ b/lib/pages/chat/input_bar.dart @@ -446,7 +446,7 @@ class InputBar extends StatelessWidget { return; } - final match = choreographer.getMatchByOffset( + final match = choreographer.igcController.getMatchByOffset( selection.baseOffset, ); if (match == null) return; diff --git a/lib/pangea/activity_sessions/activity_session_chat/activity_role_tooltip.dart b/lib/pangea/activity_sessions/activity_session_chat/activity_role_tooltip.dart index 9583d02fc..210949f96 100644 --- a/lib/pangea/activity_sessions/activity_session_chat/activity_role_tooltip.dart +++ b/lib/pangea/activity_sessions/activity_session_chat/activity_role_tooltip.dart @@ -3,27 +3,24 @@ import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import 'package:fluffychat/pangea/activity_sessions/activity_room_extension.dart'; -import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/instructions/instructions_inline_tooltip.dart'; class ActivityRoleTooltip extends StatelessWidget { - final Choreographer choreographer; + final Room room; + final ValueNotifier hide; const ActivityRoleTooltip({ - required this.choreographer, + required this.room, + required this.hide, super.key, }); - Room get room => choreographer.chatController.room; - @override Widget build(BuildContext context) { - return ListenableBuilder( - listenable: choreographer, - builder: (context, _) { - if (!room.showActivityChatUI || - room.ownRole?.goal == null || - choreographer.itController.open.value) { + return ValueListenableBuilder( + valueListenable: hide, + builder: (context, hide, _) { + if (!room.showActivityChatUI || room.ownRole?.goal == null || hide) { return const SizedBox(); } diff --git a/lib/pangea/chat/widgets/chat_input_bar.dart b/lib/pangea/chat/widgets/chat_input_bar.dart index dc3ec5cff..767f7361a 100644 --- a/lib/pangea/chat/widgets/chat_input_bar.dart +++ b/lib/pangea/chat/widgets/chat_input_bar.dart @@ -23,7 +23,8 @@ class ChatInputBar extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ ActivityRoleTooltip( - choreographer: controller.choreographer, + room: controller.room, + hide: controller.choreographer.itController.open, ), ITBar(choreographer: controller.choreographer), if (!controller.obscureText) ReplyDisplay(controller), diff --git a/lib/pangea/chat/widgets/chat_view_background.dart b/lib/pangea/chat/widgets/chat_view_background.dart index 7802605e7..f74c79278 100644 --- a/lib/pangea/chat/widgets/chat_view_background.dart +++ b/lib/pangea/chat/widgets/chat_view_background.dart @@ -2,18 +2,16 @@ import 'dart:ui'; import 'package:flutter/material.dart'; -import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; - class ChatViewBackground extends StatelessWidget { - final Choreographer choreographer; - const ChatViewBackground(this.choreographer, {super.key}); + final ValueNotifier visible; + const ChatViewBackground(this.visible, {super.key}); @override Widget build(BuildContext context) { - return ListenableBuilder( - listenable: choreographer, - builder: (context, _) { - return choreographer.itController.open.value + return ValueListenableBuilder( + valueListenable: visible, + builder: (context, value, _) { + return value ? Positioned( left: 0, right: 0, diff --git a/lib/pangea/chat/widgets/pangea_chat_input_row.dart b/lib/pangea/chat/widgets/pangea_chat_input_row.dart index 4aec816ae..e829f1381 100644 --- a/lib/pangea/chat/widgets/pangea_chat_input_row.dart +++ b/lib/pangea/chat/widgets/pangea_chat_input_row.dart @@ -239,7 +239,9 @@ class PangeaChatInputRow extends StatelessWidget { foregroundColor: theme.onBubbleColor, child: const Icon(Icons.mic_none_outlined), ) - : ChoreographerSendButton(controller: controller), + : ChoreographerSendButton( + choreographer: controller.choreographer, + ), ), ], ), diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 86220ce3c..16e3502b9 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -15,13 +15,13 @@ import 'package:fluffychat/pangea/choreographer/enums/choreo_mode.dart'; import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart'; import 'package:fluffychat/pangea/choreographer/enums/pangea_match_status.dart'; import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart'; -import 'package:fluffychat/pangea/choreographer/models/completed_it_step.dart'; import 'package:fluffychat/pangea/choreographer/models/pangea_match_state.dart'; import 'package:fluffychat/pangea/common/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/events/models/representation_content_model.dart'; import 'package:fluffychat/pangea/events/models/tokens_event_content_model.dart'; import 'package:fluffychat/pangea/events/repo/token_api_models.dart'; +import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/learning_settings/constants/language_constants.dart'; import 'package:fluffychat/pangea/subscription/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/toolbar/controllers/tts_controller.dart'; @@ -39,14 +39,14 @@ class Choreographer extends ChangeNotifier { late PangeaTextController textController; late ITController itController; - late IgcController igc; + late IgcController igcController; late ErrorService errorService; ChoreoRecord? _choreoRecord; final ValueNotifier _isFetching = ValueNotifier(false); - int _timesClicked = 0; + int _timesClicked = 0; Timer? _debounceTimer; String? _lastChecked; ChoreoMode _choreoMode = ChoreoMode.igc; @@ -65,14 +65,19 @@ class Choreographer extends ChangeNotifier { void _initialize() { textController = PangeaTextController(choreographer: this); - - itController = ITController(this); - igc = IgcController(this); + textController.addListener(_onChange); errorService = ErrorService(); errorService.addListener(notifyListeners); - textController.addListener(_onChange); + itController = ITController( + (e) => errorService.setErrorAndLock(ChoreoError(raw: e)), + ); + itController.open.addListener(_onCloseIT); + + igcController = IgcController( + (e) => errorService.setErrorAndLock(ChoreoError(raw: e)), + ); _languageStream = pangeaController.userController.languageStream.stream.listen((update) { @@ -95,7 +100,7 @@ class Choreographer extends ChangeNotifier { _choreoRecord = null; itController.clear(); itController.clearSourceText(); - igc.clear(); + igcController.clear(); _resetDebounceTimer(); } @@ -124,7 +129,7 @@ class Choreographer extends ChangeNotifier { // if user is doing IT, call closeIT here to // ensure source text is replaced when needed if (itController.open.value && _timesClicked > 1) { - closeIT(); + itController.closeIT(); } } } @@ -161,7 +166,71 @@ class Choreographer extends ChangeNotifier { } Future requestLanguageAssistance() => - _getLanguageAssistance(manual: true); + _startWritingAssistance(manual: true); + + /// Handles any changes to the text input + void _onChange() { + if (_lastChecked != null && _lastChecked == textController.text) { + return; + } + + _lastChecked = textController.text; + if (textController.editType == EditType.it) { + return; + } + + if (textController.editType == EditType.igc || + textController.editType == EditType.itDismissed) { + textController.editType = EditType.keyboard; + return; + } + + // Close any open IGC overlays + MatrixState.pAnyState.closeOverlay(); + if (errorService.isError) return; + + igcController.clear(); + if (textController.editType == EditType.keyboard) { + itController.clearSourceText(); + } + + _resetDebounceTimer(); + _debounceTimer ??= Timer( + const Duration(milliseconds: ChoreoConstants.msBeforeIGCStart), + () => _startWritingAssistance(), + ); + + //Note: we don't set the keyboard type on each keyboard stroke so this is how we default to + //a change being from the keyboard unless explicitly set to one of the other + //types when that action happens (e.g. an it/igc choice is selected) + textController.editType = EditType.keyboard; + } + + Future _startWritingAssistance({ + bool manual = false, + }) async { + if (errorService.isError || isRunningIT) return; + final SubscriptionStatus canSendStatus = + pangeaController.subscriptionController.subscriptionStatus; + + if (canSendStatus != SubscriptionStatus.subscribed || + l2Lang == null || + l1Lang == null || + (!igcEnabled && !itEnabled) || + (!isAutoIGCEnabled && !manual && _choreoMode != ChoreoMode.it)) { + return; + } + + _resetDebounceTimer(); + _initChoreoRecord(); + _startLoading(); + await igcController.getIGCTextData( + textController.text, + chatController.room.getPreviousMessages(), + ); + _acceptNormalizationMatches(); + _stopLoading(); + } Future send([int recurrence = 0]) async { // if isFetching, already called to getLanguageHelp and hasn't completed yet @@ -188,7 +257,7 @@ class Choreographer extends ChangeNotifier { return; } - if (igc.canShowFirstMatch) { + if (igcController.canShowFirstMatch) { throw OpenMatchesException(); } else if (isRunningIT) { // If the user is in the middle of IT, don't send the message. @@ -213,10 +282,10 @@ class Choreographer extends ChangeNotifier { return; } - if (!igc.hasIGCTextData && !itController.dismissed) { - await _getLanguageAssistance(); + if (!igcController.hasIGCTextData && !itController.dismissed) { + await _startWritingAssistance(); // it's possible for this not to be true, i.e. if IGC has an error - if (igc.hasIGCTextData) { + if (igcController.hasIGCTextData) { await send(recurrence + 1); } } else { @@ -224,72 +293,6 @@ class Choreographer extends ChangeNotifier { } } - /// Handles any changes to the text input - void _onChange() { - if (_lastChecked != null && _lastChecked == textController.text) { - return; - } - - _lastChecked = textController.text; - - if (textController.editType == EditType.it) { - return; - } - - if (textController.editType == EditType.igc || - textController.editType == EditType.itDismissed) { - textController.editType = EditType.keyboard; - return; - } - - // Close any open IGC/IT overlays - MatrixState.pAnyState.closeOverlay(); - if (errorService.isError) return; - - igc.clear(); - if (textController.editType == EditType.keyboard) { - itController.clearSourceText(); - } - - _resetDebounceTimer(); - _debounceTimer ??= Timer( - const Duration(milliseconds: ChoreoConstants.msBeforeIGCStart), - () => _getLanguageAssistance(), - ); - - //Note: we don't set the keyboard type on each keyboard stroke so this is how we default to - //a change being from the keyboard unless explicitly set to one of the other - //types when that action happens (e.g. an it/igc choice is selected) - textController.editType = EditType.keyboard; - } - - /// 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 _getLanguageAssistance({ - bool manual = false, - }) async { - if (errorService.isError) return; - final SubscriptionStatus canSendStatus = - pangeaController.subscriptionController.subscriptionStatus; - - if (canSendStatus != SubscriptionStatus.subscribed || - l2Lang == null || - l1Lang == null || - (!igcEnabled && !itEnabled) || - (!isAutoIGCEnabled && !manual && _choreoMode != ChoreoMode.it)) { - return; - } - - _resetDebounceTimer(); - _initChoreoRecord(); - - _startLoading(); - await igc.getIGCTextData(); - _stopLoading(); - } - Future _sendWithIGC() async { if (chatController.sendController.text.trim().isEmpty) { return; @@ -369,12 +372,14 @@ class Choreographer extends ChangeNotifier { if (!itMatch.updatedMatch.isITStart) { throw Exception("Attempted to open IT with a non-IT start match"); } - chatController.inputFocus.unfocus(); + chatController.inputFocus.unfocus(); setChoreoMode(ChoreoMode.it); - itController.openIT(textController.text); + + final sourceText = currentText; textController.setSystemText("", EditType.it); - igc.clear(); + itController.openIT(sourceText); + igcController.clear(); _initChoreoRecord(); itMatch.setStatus(PangeaMatchStatus.accepted); @@ -382,18 +387,26 @@ class Choreographer extends ChangeNotifier { "", match: itMatch.updatedMatch, ); + notifyListeners(); } - void closeIT() { - itController.closeIT(); + void _onCloseIT() { + if (itController.open.value) return; + if (currentText.isEmpty && itController.sourceText.value != null) { + textController.setSystemText( + itController.sourceText.value!, + EditType.itDismissed, + ); + } + + setChoreoMode(ChoreoMode.igc); errorService.resetError(); notifyListeners(); } - Continuance onSelectContinuance(int index) { - final continuance = itController.onSelectContinuance(index); - notifyListeners(); - return continuance; + void onSubmitEdits(String text) { + textController.setSystemText("", EditType.it); + itController.onSubmitEdits(text); } void onAcceptContinuance(int index) { @@ -402,9 +415,6 @@ class Choreographer extends ChangeNotifier { textController.text + step.continuances[step.chosen].text, EditType.it, ); - textController.selection = TextSelection.collapsed( - offset: textController.text.length, - ); _initChoreoRecord(); _choreoRecord!.addRecord(textController.text, step: step); @@ -412,45 +422,22 @@ class Choreographer extends ChangeNotifier { notifyListeners(); } - void setEditingSourceText(bool value) { - itController.setEditing(value); - notifyListeners(); - } - - void submitSourceTextEdits(String text) { - textController.setSystemText("", EditType.it); - itController.onSubmitEdits(); - notifyListeners(); - } - - PangeaMatchState? getMatchByOffset(int offset) => - igc.getMatchByOffset(offset); - void clearMatches(Object error) { MatrixState.pAnyState.closeAllOverlays(); - igc.clearMatches(); + igcController.clearMatches(); errorService.setError(ChoreoError(raw: error)); } - Future fetchSpanDetails({ - required PangeaMatchState match, - bool force = false, - }) => - igc.fetchSpanDetails( - match: match, - force: force, - ); - void onAcceptReplacement({ required PangeaMatchState match, }) { - final updatedMatch = igc.acceptReplacement( + final updatedMatch = igcController.acceptReplacement( match, PangeaMatchStatus.accepted, ); textController.setSystemText( - igc.currentText!, + igcController.currentText!, EditType.igc, ); @@ -461,19 +448,18 @@ class Choreographer extends ChangeNotifier { match: updatedMatch, ); } - MatrixState.pAnyState.closeOverlay(); notifyListeners(); } void onUndoReplacement(PangeaMatchState match) { - igc.undoReplacement(match); + igcController.undoReplacement(match); _choreoRecord?.choreoSteps.removeWhere( (step) => step.acceptedOrIgnoredMatch == match.updatedMatch, ); textController.setSystemText( - igc.currentText!, + igcController.currentText!, EditType.igc, ); MatrixState.pAnyState.closeOverlay(); @@ -481,7 +467,7 @@ class Choreographer extends ChangeNotifier { } void onIgnoreMatch({required PangeaMatchState match}) { - final updatedMatch = igc.ignoreReplacement(match); + final updatedMatch = igcController.ignoreReplacement(match); if (!updatedMatch.match.isNormalizationError()) { _initChoreoRecord(); _choreoRecord!.addRecord( @@ -493,31 +479,42 @@ class Choreographer extends ChangeNotifier { notifyListeners(); } - void acceptNormalizationMatches() { - final normalizationsMatches = igc.openNormalizationMatches; + void _acceptNormalizationMatches() { + final normalizationsMatches = igcController.openNormalizationMatches; if (normalizationsMatches?.isEmpty ?? true) return; _initChoreoRecord(); - for (final match in normalizationsMatches!) { - match.selectChoice( - match.updatedMatch.match.choices!.indexWhere( - (c) => c.isBestCorrection, - ), - ); + try { + for (final match in normalizationsMatches!) { + match.selectChoice( + match.updatedMatch.match.choices!.indexWhere( + (c) => c.isBestCorrection, + ), + ); + final updatedMatch = igcController.acceptReplacement( + match, + PangeaMatchStatus.automatic, + ); - final updatedMatch = igc.acceptReplacement( - match, - PangeaMatchStatus.automatic, - ); - - textController.setSystemText( - igc.currentText!, - EditType.igc, - ); - - _choreoRecord!.addRecord( - currentText, - match: updatedMatch, + textController.setSystemText( + igcController.currentText!, + EditType.igc, + ); + _choreoRecord!.addRecord( + currentText, + match: updatedMatch, + ); + } + } catch (e, s) { + ErrorHandler.logError( + e: e, + s: s, + data: { + "currentText": currentText, + "l1LangCode": l1LangCode, + "l2LangCode": l2LangCode, + "choreoRecord": _choreoRecord?.toJson(), + }, ); } notifyListeners(); diff --git a/lib/pangea/choreographer/controllers/extensions/choreographer_state_extension.dart b/lib/pangea/choreographer/controllers/extensions/choreographer_state_extension.dart index 40451a1d9..fb6443fa1 100644 --- a/lib/pangea/choreographer/controllers/extensions/choreographer_state_extension.dart +++ b/lib/pangea/choreographer/controllers/extensions/choreographer_state_extension.dart @@ -10,13 +10,13 @@ extension ChoregrapherUserSettingsExtension on Choreographer { itController.currentITStep.value?.isFinal != true; } - String? get currentIGCText => igc.currentText; - PangeaMatchState? get openMatch => igc.openMatch; - PangeaMatchState? get firstOpenMatch => igc.firstOpenMatch; - List? get openIGCMatches => igc.openMatches; - List? get closedIGCMatches => igc.closedMatches; - bool get canShowFirstIGCMatch => igc.canShowFirstMatch; - bool get hasIGCTextData => igc.hasIGCTextData; + String? get currentIGCText => igcController.currentText; + PangeaMatchState? get openMatch => igcController.openMatch; + PangeaMatchState? get firstOpenMatch => igcController.firstOpenMatch; + List? get openIGCMatches => igcController.openMatches; + List? get closedIGCMatches => igcController.closedMatches; + bool get canShowFirstIGCMatch => igcController.canShowFirstMatch; + bool get hasIGCTextData => igcController.hasIGCTextData; AssistanceState get assistanceState { final isSubscribed = pangeaController.subscriptionController.isSubscribed; @@ -29,12 +29,12 @@ extension ChoregrapherUserSettingsExtension on Choreographer { return AssistanceState.error; } - if (igc.hasOpenMatches || isRunningIT) { + if (igcController.hasOpenMatches || isRunningIT) { return AssistanceState.fetched; } if (isFetching.value) return AssistanceState.fetching; - if (!igc.hasIGCTextData) return AssistanceState.notFetched; + if (!igcController.hasIGCTextData) return AssistanceState.notFetched; return AssistanceState.complete; } @@ -57,13 +57,13 @@ extension ChoregrapherUserSettingsExtension on Choreographer { if (isFetching.value) return false; // they're supposed to run IGC but haven't yet, don't let them send - if (!igc.hasIGCTextData) { + if (!igcController.hasIGCTextData) { return itController.dismissed; } // if they have relevant matches, don't let them send - final hasITMatches = igc.hasOpenITMatches; - final hasIGCMatches = igc.hasOpenIGCMatches; + final hasITMatches = igcController.hasOpenITMatches; + final hasIGCMatches = igcController.hasOpenIGCMatches; if ((itEnabled && hasITMatches) || (igcEnabled && hasIGCMatches)) { return false; } diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index f506fd735..d325890aa 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -1,15 +1,7 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; - import 'package:async/async.dart'; -import 'package:matrix/matrix.dart' hide Result; -import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; -import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart'; -import 'package:fluffychat/pangea/choreographer/controllers/extensions/choregrapher_user_settings_extension.dart'; -import 'package:fluffychat/pangea/choreographer/enums/choreo_mode.dart'; import 'package:fluffychat/pangea/choreographer/enums/pangea_match_status.dart'; import 'package:fluffychat/pangea/choreographer/models/igc_text_data_model.dart'; import 'package:fluffychat/pangea/choreographer/models/pangea_match_model.dart'; @@ -18,16 +10,16 @@ import 'package:fluffychat/pangea/choreographer/repo/igc_repo.dart'; import 'package:fluffychat/pangea/choreographer/repo/igc_request_model.dart'; import 'package:fluffychat/pangea/choreographer/repo/span_data_repo.dart'; import 'package:fluffychat/pangea/choreographer/repo/span_data_request.dart'; -import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import '../../common/utils/error_handler.dart'; class IgcController { - final Choreographer _choreographer; + final Function(Object) onError; + + bool _isFetching = false; IGCTextData? _igcTextData; - IgcController(this._choreographer); + IgcController(this.onError); String? get currentText => _igcTextData?.currentText; bool get hasOpenMatches => _igcTextData?.hasOpenMatches == true; @@ -42,12 +34,10 @@ class IgcController { _igcTextData?.openNormalizationMatches; bool get canShowFirstMatch => _igcTextData?.firstOpenMatch != null; - bool get hasIGCTextData { - if (_igcTextData == null) return false; - return _igcTextData!.currentText == _choreographer.currentText; - } + bool get hasIGCTextData => _igcTextData != null; void clear() { + _isFetching = false; _igcTextData = null; MatrixState.pAnyState.closeAllOverlays(); } @@ -64,7 +54,8 @@ class IgcController { if (_igcTextData == null) { throw "acceptReplacement called with null igcTextData"; } - return _igcTextData!.acceptReplacement(match, status); + final updateMatch = _igcTextData!.acceptReplacement(match, status); + return updateMatch; } PangeaMatch ignoreReplacement(PangeaMatchState match) { @@ -82,24 +73,25 @@ class IgcController { _igcTextData!.undoReplacement(match); } - Future getIGCTextData() async { - if (_choreographer.currentText.isEmpty) return clear(); - debugPrint('getIGCTextData called with ${_choreographer.currentText}'); - + Future getIGCTextData( + String text, + List prevMessages, + ) async { + if (text.isEmpty) return clear(); + if (_isFetching) return; + _isFetching = true; final IGCRequestModel reqBody = IGCRequestModel( - fullText: _choreographer.currentText, - userId: _choreographer.pangeaController.userController.userId!, - userL1: _choreographer.l1LangCode!, - userL2: _choreographer.l2LangCode!, - enableIGC: _choreographer.igcEnabled && - _choreographer.choreoMode != ChoreoMode.it, - enableIT: _choreographer.itEnabled && - _choreographer.choreoMode != ChoreoMode.it, - prevMessages: _prevMessages(), + fullText: text, + userId: MatrixState.pangeaController.userController.userId!, + userL1: MatrixState.pangeaController.languageController.activeL1Code()!, + userL2: MatrixState.pangeaController.languageController.activeL2Code()!, + enableIGC: true, + enableIT: true, + prevMessages: prevMessages, ); final res = await IgcRepo.get( - _choreographer.pangeaController.userController.accessToken, + MatrixState.pangeaController.userController.accessToken, reqBody, ).timeout( (const Duration(seconds: 10)), @@ -111,37 +103,18 @@ class IgcController { ); if (res.isError) { - _choreographer.errorService.setErrorAndLock( - ChoreoError(raw: res.asError), - ); + onError(res.asError!); clear(); return; } - // this will happen when the user changes the input while igc is fetching results - if (res.result!.originalInput.trim() != _choreographer.currentText.trim()) { - return; - } - + if (!_isFetching) return; final response = res.result!; _igcTextData = IGCTextData( originalInput: response.originalInput, matches: response.matches, ); - - try { - _choreographer.acceptNormalizationMatches(); - } catch (e, s) { - ErrorHandler.logError( - e: e, - s: s, - level: SentryLevel.warning, - data: { - "igcResponse": response.toJson(), - }, - ); - } - + _isFetching = false; if (_igcTextData != null) { for (final match in _igcTextData!.openMatches) { fetchSpanDetails(match: match).catchError((e) {}); @@ -159,12 +132,12 @@ class IgcController { } final response = await SpanDataRepo.get( - _choreographer.pangeaController.userController.accessToken, + MatrixState.pangeaController.userController.accessToken, request: SpanDetailsRequest( - userL1: _choreographer.l1LangCode!, - userL2: _choreographer.l2LangCode!, - enableIGC: _choreographer.igcEnabled, - enableIT: _choreographer.itEnabled, + userL1: MatrixState.pangeaController.languageController.activeL1Code()!, + userL2: MatrixState.pangeaController.languageController.activeL2Code()!, + enableIGC: true, + enableIT: true, span: span, ), ).timeout( @@ -182,39 +155,4 @@ class IgcController { _igcTextData?.setSpanData(match, response.result!.span); } - - List _prevMessages({int numMessages = 5}) { - final List events = _choreographer.chatController.visibleEvents - .where( - (e) => - e.type == EventTypes.Message && - (e.messageType == MessageTypes.Text || - e.messageType == MessageTypes.Audio), - ) - .toList(); - - final List messages = []; - for (final Event event in events) { - final String? content = event.messageType == MessageTypes.Text - ? event.content.toString() - : PangeaMessageEvent( - event: event, - timeline: _choreographer.chatController.timeline!, - ownMessage: event.senderId == - _choreographer.pangeaController.matrixState.client.userID, - ).getSpeechToTextLocal()?.transcript.text.trim(); // trim whitespace - if (content == null) continue; - messages.add( - PreviousMessage( - content: content, - sender: event.senderId, - timestamp: event.originServerTs, - ), - ); - if (messages.length >= numMessages) { - return messages; - } - } - return messages; - } } diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart index 8b9187cb7..cf5435811 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -5,9 +5,6 @@ import 'package:flutter/foundation.dart'; import 'package:async/async.dart'; -import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart'; -import 'package:fluffychat/pangea/choreographer/enums/choreo_mode.dart'; -import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart'; import 'package:fluffychat/pangea/choreographer/models/gold_route_tracker.dart'; import 'package:fluffychat/pangea/choreographer/models/it_step.dart'; import 'package:fluffychat/pangea/choreographer/repo/it_repo.dart'; @@ -16,21 +13,20 @@ import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import '../models/completed_it_step.dart'; import '../repo/it_request_model.dart'; -import 'choreographer.dart'; class ITController { - final Choreographer _choreographer; - final ValueNotifier _sourceText = ValueNotifier(null); + final Function(Object) onError; - final ValueNotifier _currentITStep = ValueNotifier(null); final Queue> _queue = Queue(); GoldRouteTracker? _goldRouteTracker; + final ValueNotifier _sourceText = ValueNotifier(null); + final ValueNotifier _currentITStep = ValueNotifier(null); final ValueNotifier _open = ValueNotifier(false); final ValueNotifier _editing = ValueNotifier(false); bool _dismissed = false; - ITController(this._choreographer); + ITController(this.onError); ValueNotifier get open => _open; ValueNotifier get editing => _editing; @@ -47,8 +43,6 @@ class ITController { MatrixState.pangeaController.languageController.activeL1Code()!, targetLangCode: MatrixState.pangeaController.languageController.activeL2Code()!, - userId: _choreographer.chatController.room.client.userID!, - roomId: _choreographer.chatController.room.id, goldTranslation: _goldRouteTracker?.fullTranslation, goldContinuances: _goldRouteTracker?.continuances, ); @@ -72,8 +66,6 @@ class ITController { _queue.clear(); _currentITStep.value = null; _goldRouteTracker = null; - - _choreographer.setChoreoMode(ChoreoMode.igc); } void clearSourceText() { @@ -81,38 +73,30 @@ class ITController { } void dispose() { + _open.dispose(); _currentITStep.dispose(); _editing.dispose(); _sourceText.dispose(); } - void openIT(String sourceText) { - _sourceText.value = sourceText; + void openIT(String text) { + _sourceText.value = text; _open.value = true; continueIT(); } - void closeIT() { - // if the user hasn't gone through any IT steps, reset the text - if (_choreographer.currentText.isEmpty && _sourceText.value != null) { - _choreographer.textController.setSystemText( - _sourceText.value!, - EditType.itDismissed, - ); - } - - clear(dismissed: true); - } + void closeIT() => clear(dismissed: true); void setEditing(bool value) { _editing.value = value; } - void onSubmitEdits() { + void onSubmitEdits(String text) { _editing.value = false; _queue.clear(); _currentITStep.value = null; _goldRouteTracker = null; + _sourceText.value = text; continueIT(); } @@ -163,13 +147,13 @@ class ITController { if (_currentITStep.value == null) { await _initTranslationData(); } else if (_queue.isEmpty) { - _choreographer.closeIT(); + closeIT(); } else { final nextStepCompleter = _queue.removeFirst(); _currentITStep.value = await nextStepCompleter.future; } } catch (e) { - _choreographer.errorService.setErrorAndLock(ChoreoError(raw: e)); + onError(e); } finally { _continuing = false; } @@ -179,9 +163,7 @@ class ITController { final res = await _safeRequest(""); if (_sourceText.value == null || !_open.value) return; if (res.isError || res.result?.goldContinuances == null) { - _choreographer.errorService.setErrorAndLock( - ChoreoError(raw: res.asError), - ); + onError(res.asError!); return; } diff --git a/lib/pangea/choreographer/repo/it_request_model.dart b/lib/pangea/choreographer/repo/it_request_model.dart index 54b053b10..6b51011a6 100644 --- a/lib/pangea/choreographer/repo/it_request_model.dart +++ b/lib/pangea/choreographer/repo/it_request_model.dart @@ -8,8 +8,6 @@ class ITRequestModel { final String customInput; final String sourceLangCode; final String targetLangCode; - final String userId; - final String roomId; final String? goldTranslation; final List? goldContinuances; @@ -19,8 +17,6 @@ class ITRequestModel { required this.customInput, required this.sourceLangCode, required this.targetLangCode, - required this.userId, - required this.roomId, required this.goldTranslation, required this.goldContinuances, }); @@ -30,8 +26,6 @@ class ITRequestModel { customInput: json['custom_input'], sourceLangCode: json[ModelKey.srcLang], targetLangCode: json[ModelKey.tgtLang], - userId: json['user_id'], - roomId: json['room_id'], goldTranslation: json['gold_translation'], goldContinuances: json['gold_continuances'] != null ? (json['gold_continuances']) @@ -45,8 +39,6 @@ class ITRequestModel { 'custom_input': customInput, ModelKey.srcLang: sourceLangCode, ModelKey.tgtLang: targetLangCode, - 'user_id': userId, - 'room_id': roomId, 'gold_translation': goldTranslation, 'gold_continuances': goldContinuances != null ? List.from(goldContinuances!.map((e) => e.toJson())) @@ -62,8 +54,6 @@ class ITRequestModel { other.customInput == customInput && other.sourceLangCode == sourceLangCode && other.targetLangCode == targetLangCode && - other.userId == userId && - other.roomId == roomId && other.goldTranslation == goldTranslation && listEquals(other.goldContinuances, goldContinuances); } @@ -74,8 +64,6 @@ class ITRequestModel { customInput.hashCode ^ sourceLangCode.hashCode ^ targetLangCode.hashCode ^ - userId.hashCode ^ - roomId.hashCode ^ goldTranslation.hashCode ^ Object.hashAll(goldContinuances ?? []); } diff --git a/lib/pangea/choreographer/widgets/igc/span_card.dart b/lib/pangea/choreographer/widgets/igc/span_card.dart index ffe88a4d5..c6ce6878f 100644 --- a/lib/pangea/choreographer/widgets/igc/span_card.dart +++ b/lib/pangea/choreographer/widgets/igc/span_card.dart @@ -72,7 +72,7 @@ class SpanCardState extends State { try { setState(() => _loadingChoices = true); - await widget.choreographer.fetchSpanDetails( + await widget.choreographer.igcController.fetchSpanDetails( match: widget.match, ); } catch (e) { @@ -83,7 +83,9 @@ class SpanCardState extends State { 'No choices available for span ${widget.match.updatedMatch.match.message}', ); } - setState(() => _loadingChoices = false); + if (mounted) { + setState(() => _loadingChoices = false); + } } } @@ -95,7 +97,7 @@ class SpanCardState extends State { try { _feedbackModel.setState(FeedbackLoading()); - await widget.choreographer.fetchSpanDetails( + await widget.choreographer.igcController.fetchSpanDetails( match: widget.match, force: true, ); @@ -157,7 +159,7 @@ class SpanCardState extends State { void _showFirstMatch() { if (widget.choreographer.canShowFirstIGCMatch) { OverlayUtil.showIGCMatch( - widget.choreographer.igc.firstOpenMatch!, + widget.choreographer.igcController.firstOpenMatch!, widget.choreographer, context, ); diff --git a/lib/pangea/choreographer/widgets/it_bar.dart b/lib/pangea/choreographer/widgets/it_bar.dart index 0a5e86223..6037b1afb 100644 --- a/lib/pangea/choreographer/widgets/it_bar.dart +++ b/lib/pangea/choreographer/widgets/it_bar.dart @@ -133,7 +133,8 @@ class ITBarState extends State with SingleTickerProviderStateMixin { MatrixState.pAnyState.closeOverlay("it_feedback_card"); Continuance continuance; try { - continuance = widget.choreographer.onSelectContinuance(index); + continuance = + widget.choreographer.itController.onSelectContinuance(index); } catch (e, s) { ErrorHandler.logError( e: e, @@ -143,7 +144,7 @@ class ITBarState extends State with SingleTickerProviderStateMixin { "index": index, }, ); - widget.choreographer.closeIT(); + widget.choreographer.itController.closeIT(); return; } @@ -174,7 +175,7 @@ class ITBarState extends State with SingleTickerProviderStateMixin { "index": index, }, ); - widget.choreographer.closeIT(); + widget.choreographer.itController.closeIT(); } }); } @@ -217,12 +218,14 @@ class ITBarState extends State with SingleTickerProviderStateMixin { spacing: 12.0, children: [ _ITBarHeader( - onClose: widget.choreographer.closeIT, + onClose: widget.choreographer.itController.closeIT, setEditing: widget.choreographer.itController.setEditing, editing: widget.choreographer.itController.editing, sourceTextController: _sourceTextController, sourceText: _sourceText, - onSubmitEdits: widget.choreographer.submitSourceTextEdits, + onSubmitEdits: (_) => widget.choreographer.onSubmitEdits( + _sourceTextController.text, + ), ), Container( padding: const EdgeInsets.symmetric(horizontal: 12.0), @@ -241,7 +244,8 @@ class ITBarState extends State with SingleTickerProviderStateMixin { ), ), IconButton( - onPressed: widget.choreographer.closeIT, + onPressed: + widget.choreographer.itController.closeIT, icon: const Icon( Icons.close, size: 20, diff --git a/lib/pangea/choreographer/widgets/send_button.dart b/lib/pangea/choreographer/widgets/send_button.dart index 9507ecc00..1abeda11a 100644 --- a/lib/pangea/choreographer/widgets/send_button.dart +++ b/lib/pangea/choreographer/widgets/send_button.dart @@ -7,33 +7,31 @@ import 'package:fluffychat/pangea/choreographer/controllers/extensions/choreogra import 'package:fluffychat/pangea/choreographer/enums/assistance_state_enum.dart'; import 'package:fluffychat/pangea/choreographer/widgets/igc/paywall_card.dart'; import 'package:fluffychat/pangea/common/utils/overlay.dart'; -import '../../../pages/chat/chat.dart'; class ChoreographerSendButton extends StatelessWidget { + final Choreographer choreographer; const ChoreographerSendButton({ super.key, - required this.controller, + required this.choreographer, }); - final ChatController controller; Future _onPressed(BuildContext context) async { - controller.choreographer.onClickSend(); + choreographer.onClickSend(); try { - await controller.choreographer.send(); + await choreographer.send(); } on ShowPaywallException { PaywallCard.show( context, - controller.choreographer.inputTransformTargetKey, + choreographer.inputTransformTargetKey, ); } on OpenMatchesException { - if (controller.choreographer.firstOpenMatch != null) { - if (controller.choreographer.firstOpenMatch!.updatedMatch.isITStart) { - controller.choreographer - .openIT(controller.choreographer.firstOpenMatch!); + if (choreographer.firstOpenMatch != null) { + if (choreographer.firstOpenMatch!.updatedMatch.isITStart) { + choreographer.openIT(choreographer.firstOpenMatch!); } else { OverlayUtil.showIGCMatch( - controller.choreographer.firstOpenMatch!, - controller.choreographer, + choreographer.firstOpenMatch!, + choreographer, context, ); } @@ -45,8 +43,8 @@ class ChoreographerSendButton extends StatelessWidget { Widget build(BuildContext context) { return ListenableBuilder( listenable: Listenable.merge([ - controller.choreographer.textController, - controller.choreographer.isFetching, + choreographer.textController, + choreographer.isFetching, ]), builder: (context, _) { return Container( @@ -54,9 +52,8 @@ class ChoreographerSendButton extends StatelessWidget { alignment: Alignment.center, child: IconButton( icon: const Icon(Icons.send_outlined), - color: controller.choreographer.assistanceState - .sendButtonColor(context), - onPressed: controller.choreographer.isFetching.value + color: choreographer.assistanceState.sendButtonColor(context), + onPressed: choreographer.isFetching.value ? null : () => _onPressed(context), tooltip: L10n.of(context).send, diff --git a/lib/pangea/choreographer/widgets/start_igc_button.dart b/lib/pangea/choreographer/widgets/start_igc_button.dart index 3162e9557..31d70901e 100644 --- a/lib/pangea/choreographer/widgets/start_igc_button.dart +++ b/lib/pangea/choreographer/widgets/start_igc_button.dart @@ -63,7 +63,8 @@ class StartIGCButtonState extends State void _showFirstMatch() { if (widget.controller.choreographer.canShowFirstIGCMatch) { - final match = widget.controller.choreographer.igc.firstOpenMatch; + final match = + widget.controller.choreographer.igcController.firstOpenMatch; if (match == null) return; if (match.updatedMatch.isITStart) { widget.controller.choreographer.openIT(match); diff --git a/lib/pangea/extensions/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension.dart index cacdc7d52..d7242a259 100644 --- a/lib/pangea/extensions/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension.dart @@ -22,6 +22,7 @@ import 'package:fluffychat/pangea/bot/utils/bot_room_extension.dart'; import 'package:fluffychat/pangea/chat/constants/default_power_level.dart'; import 'package:fluffychat/pangea/chat_settings/constants/bot_mode.dart'; import 'package:fluffychat/pangea/chat_settings/constants/pangea_room_types.dart'; +import 'package:fluffychat/pangea/choreographer/repo/igc_request_model.dart'; import 'package:fluffychat/pangea/common/constants/model_keys.dart'; import 'package:fluffychat/pangea/common/utils/error_handler.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; diff --git a/lib/pangea/extensions/room_events_extension.dart b/lib/pangea/extensions/room_events_extension.dart index 4b8195601..935511a72 100644 --- a/lib/pangea/extensions/room_events_extension.dart +++ b/lib/pangea/extensions/room_events_extension.dart @@ -385,4 +385,40 @@ extension EventsRoomExtension on Room { return allPangeaMessages; } + + List getPreviousMessages({int numMessages = 5}) { + if (timeline == null) return []; + final events = timeline!.events + .where( + (e) => + e.type == EventTypes.Message && + (e.messageType == MessageTypes.Text || + e.messageType == MessageTypes.Audio), + ) + .toList(); + + final List messages = []; + for (final Event event in events) { + final String? content = event.messageType == MessageTypes.Text + ? event.content.toString() + : PangeaMessageEvent( + event: event, + timeline: timeline!, + ownMessage: event.senderId == client.userID, + ).getSpeechToTextLocal()?.transcript.text.trim(); + + if (content == null) continue; + messages.add( + PreviousMessage( + content: content, + sender: event.senderId, + timestamp: event.originServerTs, + ), + ); + if (messages.length >= numMessages) { + return messages; + } + } + return messages; + } }