From a077cc4f6a2fc48a7712ab6331e7d368afee7ebc Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 20 May 2024 10:09:47 -0400 Subject: [PATCH] button to run IGC manually --- assets/l10n/intl_en.arb | 7 +- lib/pages/chat/chat_view.dart | 10 +- .../controllers/choreographer.dart | 46 +++++- .../choreographer/widgets/send_button.dart | 9 +- .../widgets/start_igc_button.dart | 150 ++++++++++++++++++ lib/pangea/models/class_model.dart | 25 ++- lib/pangea/models/user_model.dart | 3 + 7 files changed, 234 insertions(+), 16 deletions(-) create mode 100644 lib/pangea/choreographer/widgets/start_igc_button.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 6677bc3d3..f84836933 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3948,5 +3948,10 @@ "studentAnalyticsNotAvailable": "Student data not currently available", "roomDataMissing": "Some data may be missing from rooms in which you are not a member.", "updatePhoneOS": "You may need to update your device's OS version.", - "wordsPerMinute": "Words per minute" + "wordsPerMinute": "Words per minute", + "autoIGCToolName": "Run Language Assistance Automatically", + "autoIGCToolDescription": "Automatically run language assistance after typing messages", + "runGrammarCorrection": "Run grammar correction", + "grammarCorrectionFailed": "Grammar correction failed", + "grammarCorrectionComplete": "Grammar correction complete" } \ No newline at end of file diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 57255ffc9..65aac4106 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -9,6 +9,7 @@ import 'package:fluffychat/pages/chat/reactions_picker.dart'; import 'package:fluffychat/pages/chat/reply_display.dart'; import 'package:fluffychat/pangea/choreographer/widgets/has_error_button.dart'; import 'package:fluffychat/pangea/choreographer/widgets/language_permissions_warning_buttons.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/start_igc_button.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension.dart'; import 'package:fluffychat/pangea/pages/class_analytics/measure_able.dart'; import 'package:fluffychat/utils/account_config.dart'; @@ -416,8 +417,8 @@ class ChatView extends StatelessWidget { // #Pangea // if (controller.dragging) // Container( - // color: Theme.of(context) - // .scaffoldBackgroundColor + // color: Theme.of(context) + // .scaffoldBackgroundColor // .withOpacity(0.9), // alignment: Alignment.center, // child: const Icon( @@ -425,6 +426,11 @@ class ChatView extends StatelessWidget { // size: 100, // ), // ), + Positioned( + left: 20, + bottom: 75, + child: StartIGCButton(controller: controller), + ), // Pangea# ], ), diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 3a26676c6..7534529c6 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -51,7 +51,7 @@ class Choreographer { // last checked by IGC or translation String? _lastChecked; ChoreoMode choreoMode = ChoreoMode.igc; - final StreamController stateListener = StreamController(); + final StreamController stateListener = StreamController.broadcast(); StreamSubscription? trialStream; Choreographer(this.pangeaController, this.chatController) { @@ -205,14 +205,18 @@ class Choreographer { textController.editType = EditType.keyboard; } - Future getLanguageHelp([bool tokensOnly = false]) async { + Future getLanguageHelp([ + bool tokensOnly = false, + bool manual = false, + ]) async { try { if (errorService.isError) return; final CanSendStatus canSendStatus = pangeaController.subscriptionController.canSendStatus; if (canSendStatus != CanSendStatus.subscribed || - (!igcEnabled && !itEnabled)) { + (!igcEnabled && !itEnabled) || + (!isAutoIGCEnabled && !manual && choreoMode != ChoreoMode.it)) { return; } @@ -535,4 +539,40 @@ class Choreographer { pangeaController.permissionsController.isWritingAssistanceEnabled( chatController.room, ); + + bool get isAutoIGCEnabled => + pangeaController.permissionsController.isToolEnabled( + ToolSetting.autoIGC, + chatController.room, + ); + + AssistanceState get assistanceState { + if (currentText.isEmpty && itController.sourceText == null) { + return AssistanceState.noMessage; + } + + if (igc.igcTextData?.matches.isNotEmpty ?? false) { + return AssistanceState.fetched; + } + + if (isFetching) { + return AssistanceState.fetching; + } + + if (igc.igcTextData == null) { + return AssistanceState.notFetched; + } + + return AssistanceState.complete; + } +} + +// assistance state is, user has not typed a message, user has typed a message and IGC has not run, +// IGC is running, IGC has run and there are remaining steps (either IT or IGC), or all steps are done +enum AssistanceState { + noMessage, + notFetched, + fetching, + fetched, + complete, } diff --git a/lib/pangea/choreographer/widgets/send_button.dart b/lib/pangea/choreographer/widgets/send_button.dart index 045f0c516..fa3189df5 100644 --- a/lib/pangea/choreographer/widgets/send_button.dart +++ b/lib/pangea/choreographer/widgets/send_button.dart @@ -1,8 +1,7 @@ +import 'package:fluffychat/pangea/constants/colors.dart'; import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:fluffychat/pangea/constants/colors.dart'; import '../../../pages/chat/chat.dart'; class ChoreographerSendButton extends StatelessWidget { @@ -16,7 +15,8 @@ class ChoreographerSendButton extends StatelessWidget { @override Widget build(BuildContext context) { // commit for cicd - return controller.choreographer.isFetching + return controller.choreographer.isFetching && + controller.choreographer.isAutoIGCEnabled ? Container( height: 56, width: 56, @@ -28,7 +28,8 @@ class ChoreographerSendButton extends StatelessWidget { alignment: Alignment.center, child: IconButton( icon: const Icon(Icons.send_outlined), - color: controller.choreographer.igc.canSendMessage + color: controller.choreographer.igc.canSendMessage || + !controller.choreographer.isAutoIGCEnabled ? null : PangeaColors.igcError, onPressed: () { diff --git a/lib/pangea/choreographer/widgets/start_igc_button.dart b/lib/pangea/choreographer/widgets/start_igc_button.dart new file mode 100644 index 000000000..183ac690d --- /dev/null +++ b/lib/pangea/choreographer/widgets/start_igc_button.dart @@ -0,0 +1,150 @@ +import 'dart:async'; +import 'dart:math' as math; + +import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; +import 'package:fluffychat/pangea/constants/colors.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +import '../../../pages/chat/chat.dart'; + +class StartIGCButton extends StatefulWidget { + const StartIGCButton({ + super.key, + required this.controller, + }); + + final ChatController controller; + + @override + State createState() => StartIGCButtonState(); +} + +class StartIGCButtonState extends State + with SingleTickerProviderStateMixin { + AssistanceState get assistanceState => + widget.controller.choreographer.assistanceState; + AnimationController? _controller; + StreamSubscription? choreoListener; + AssistanceState? prevState; + + @override + void initState() { + _controller = AnimationController( + vsync: this, + duration: const Duration(seconds: 1), + ); + choreoListener = widget.controller.choreographer.stateListener.stream + .listen(updateSpinnerState); + super.initState(); + } + + void updateSpinnerState(_) { + if (prevState != AssistanceState.fetching && + assistanceState == AssistanceState.fetching) { + _controller?.repeat(); + } else if (prevState == AssistanceState.fetching && + assistanceState != AssistanceState.fetching) { + _controller?.stop(); + _controller?.reverse(); + } + setState(() => prevState = assistanceState); + } + + @override + Widget build(BuildContext context) { + if (widget.controller.choreographer.isAutoIGCEnabled) { + return const SizedBox.shrink(); + } + + final Widget icon = Icon( + Icons.autorenew_rounded, + size: 46, + color: assistanceState.stateColor, + ); + + return SizedBox( + height: 50, + width: 50, + child: FloatingActionButton( + tooltip: assistanceState.tooltip( + L10n.of(context)!, + ), + backgroundColor: Colors.white, + disabledElevation: 0, + shape: const CircleBorder(), + onPressed: () { + if (assistanceState != AssistanceState.complete) { + widget.controller.choreographer.getLanguageHelp( + false, + true, + ); + } + }, + child: Stack( + alignment: Alignment.center, + children: [ + _controller != null + ? RotationTransition( + turns: Tween(begin: 0.0, end: math.pi * 2) + .animate(_controller!), + child: icon, + ) + : icon, + Container( + width: 26, + height: 26, + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: Colors.white, + ), + ), + Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: assistanceState.stateColor, + ), + ), + const Icon( + size: 16, + Icons.check, + color: Colors.white, + ), + ], + ), + ), + ); + } +} + +extension AssistanceStateExtension on AssistanceState { + Color get stateColor { + switch (this) { + case AssistanceState.noMessage: + case AssistanceState.notFetched: + case AssistanceState.fetching: + return AppConfig.primaryColor; + case AssistanceState.fetched: + return PangeaColors.igcError; + case AssistanceState.complete: + return AppConfig.success; + } + } + + String tooltip(L10n l10n) { + switch (this) { + case AssistanceState.noMessage: + case AssistanceState.notFetched: + return l10n.runGrammarCorrection; + case AssistanceState.fetching: + return ""; + case AssistanceState.fetched: + return l10n.grammarCorrectionFailed; + case AssistanceState.complete: + return l10n.grammarCorrectionComplete; + } + } +} diff --git a/lib/pangea/models/class_model.dart b/lib/pangea/models/class_model.dart index 1f588980c..dc0822438 100644 --- a/lib/pangea/models/class_model.dart +++ b/lib/pangea/models/class_model.dart @@ -1,13 +1,12 @@ import 'dart:developer'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; -import 'package:matrix/matrix.dart'; - import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +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/pangea_event_types.dart'; @@ -124,6 +123,7 @@ class PangeaRoomRules { int immersionMode; int definitions; int translations; + int autoIGC; PangeaRoomRules({ this.isPublic = false, @@ -142,6 +142,7 @@ class PangeaRoomRules { this.immersionMode = ClassDefaultValues.languageToolPermissions, this.definitions = ClassDefaultValues.languageToolPermissions, this.translations = ClassDefaultValues.languageToolPermissions, + this.autoIGC = ClassDefaultValues.languageToolPermissions, }); updatePermission(String key, bool value) { @@ -201,6 +202,9 @@ class PangeaRoomRules { case ToolSetting.translations: translations = value; break; + case ToolSetting.autoIGC: + autoIGC = value; + break; default: throw Exception('Invalid key for setting permissions - $setting'); } @@ -235,6 +239,7 @@ class PangeaRoomRules { json['definitions'] ?? ClassDefaultValues.languageToolPermissions, translations: json['translations'] ?? ClassDefaultValues.languageToolPermissions, + autoIGC: json['auto_igc'] ?? ClassDefaultValues.languageToolPermissions, ); Map toJson() { @@ -256,6 +261,7 @@ class PangeaRoomRules { data['immersion_mode'] = immersionMode; data['definitions'] = definitions; data['translations'] = translations; + data['auto_igc'] = autoIGC; return data; } @@ -271,6 +277,8 @@ class PangeaRoomRules { return definitions; case ToolSetting.translations: return translations; + case ToolSetting.autoIGC: + return autoIGC; default: throw Exception('Invalid key for setting permissions - $setting'); } @@ -299,6 +307,7 @@ enum ToolSetting { immersionMode, definitions, translations, + autoIGC, } extension SettingCopy on ToolSetting { @@ -314,6 +323,8 @@ extension SettingCopy on ToolSetting { return L10n.of(context)!.definitionsToolName; case ToolSetting.translations: return L10n.of(context)!.messageTranslationsToolName; + case ToolSetting.autoIGC: + return L10n.of(context)!.autoIGCToolName; } } @@ -330,6 +341,8 @@ extension SettingCopy on ToolSetting { return L10n.of(context)!.definitionsToolDescription; case ToolSetting.translations: return L10n.of(context)!.translationsToolDescrption; + case ToolSetting.autoIGC: + return L10n.of(context)!.autoIGCToolDescription; } } } diff --git a/lib/pangea/models/user_model.dart b/lib/pangea/models/user_model.dart index 2169c6f70..10c382f9f 100644 --- a/lib/pangea/models/user_model.dart +++ b/lib/pangea/models/user_model.dart @@ -68,6 +68,7 @@ enum MatrixProfile { sourceLanguage, country, publicProfile, + autoIGC, } extension MatrixProfileExtension on MatrixProfile { @@ -89,6 +90,8 @@ extension MatrixProfileExtension on MatrixProfile { return ToolSetting.definitions.toString(); case MatrixProfile.translations: return ToolSetting.translations.toString(); + case MatrixProfile.autoIGC: + return ToolSetting.autoIGC.toString(); case MatrixProfile.showedItInstructions: return InstructionsEnum.itInstructions.toString(); case MatrixProfile.showedClickMessage: