From 240b039ae702e1d3aac0feac20e6c46f801a8274 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Sat, 5 Oct 2024 15:51:17 -0400 Subject: [PATCH] several toolbar UI tweaks --- assets/l10n/intl_en.arb | 4 +- lib/main.dart | 2 +- lib/pangea/choreographer/widgets/it_bar.dart | 4 +- .../controllers/my_analytics_controller.dart | 14 +++ lib/pangea/enum/construct_use_type_enum.dart | 11 +-- .../widgets/chat/message_audio_card.dart | 3 + .../chat/message_selection_overlay.dart | 24 ++++- lib/pangea/widgets/chat/message_toolbar.dart | 3 + .../chat/message_translation_card.dart | 3 + lib/pangea/widgets/content_issue_button.dart | 89 +++++++++++++++++++ lib/pangea/widgets/igc/span_card.dart | 2 + lib/pangea/widgets/igc/word_data_card.dart | 25 +----- .../no_more_practice_card.dart | 11 ++- .../practice_activity_card.dart | 79 +++------------- lib/pangea/widgets/select_to_define.dart | 26 ++++++ 15 files changed, 195 insertions(+), 105 deletions(-) create mode 100644 lib/pangea/widgets/content_issue_button.dart create mode 100644 lib/pangea/widgets/select_to_define.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index e8e2aa585..14627f227 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4231,7 +4231,7 @@ "inviteChat": "📨 Invite chat", "chatName": "Chat name", "reportContentIssueTitle": "Report content issue", - "feedback": "Your feedback (optional)", - "reportContentIssueDescription": "Sorry! AI can make personalized experiences but also may have issues. Please provide any feedback you have and we'll generate a new activity.", + "feedback": "Optional feedback", + "reportContentIssueDescription": "Uh oh! AI can faciliate personalized learning experiences but... also hallucinates. Please provide any feedback you have and we'll try again.", "clickTheWordAgainToDeselect": "Click the selected word to deselect it." } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 6be6edc91..36add47dc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,7 +22,7 @@ void main() async { // #Pangea try { - await dotenv.load(fileName: ".env"); + await dotenv.load(fileName: ".env.local_choreo"); } catch (e) { Logs().e('Failed to load .env file', e); } diff --git a/lib/pangea/choreographer/widgets/it_bar.dart b/lib/pangea/choreographer/widgets/it_bar.dart index 2fc20531f..9e37112ea 100644 --- a/lib/pangea/choreographer/widgets/it_bar.dart +++ b/lib/pangea/choreographer/widgets/it_bar.dart @@ -225,6 +225,7 @@ class OriginalText extends StatelessWidget { controller.sourceText != null ? Flexible(child: Text(controller.sourceText!)) : const LinearProgressIndicator(), + const SizedBox(width: 4), if (controller.isEditingSourceText) Expanded( child: TextField( @@ -243,7 +244,7 @@ class OriginalText extends StatelessWidget { if (!controller.isEditingSourceText && controller.sourceText != null) AnimatedOpacity( duration: const Duration(milliseconds: 500), - opacity: controller.nextITStep != null ? 1.0 : 0.0, + opacity: controller.nextITStep != null ? 0.7 : 0.0, child: IconButton( onPressed: () => { if (controller.nextITStep != null) @@ -252,6 +253,7 @@ class OriginalText extends StatelessWidget { }, }, icon: const Icon(Icons.edit_outlined), + iconSize: 20, ), ), ], diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index cd6991864..f8fae3457 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -113,6 +113,11 @@ class MyAnalyticsController extends BaseController { _pangeaController.analytics .filterConstructs(unfilteredConstructs: constructs) .then((filtered) { + for (final use in filtered) { + debugPrint( + "_onNewAnalyticsData filtered use: ${use.constructType.string} ${use.useType.string} ${use.lemma} ${use.useType.pointValue}", + ); + } if (filtered.isEmpty) return; // @ggurdin - are we sure this isn't happening twice? it's also above @@ -166,6 +171,14 @@ class MyAnalyticsController extends BaseController { } } + if (kDebugMode) { + for (final use in uses) { + debugPrint( + "Draft use: ${use.constructType.string} ${use.useType.string} ${use.lemma} ${use.useType.pointValue}", + ); + } + } + // @ggurdin - if the point of draft uses is that we don't want to send them twice, // then, if this is triggered here, couldn't that make a problem? final level = _pangeaController.analytics.level; @@ -189,6 +202,7 @@ class MyAnalyticsController extends BaseController { /// cache of recently sent messages Future _addLocalMessage( String eventID, + // @ggurdin - why is this an eventID and not a roomID? List constructs, ) async { try { diff --git a/lib/pangea/enum/construct_use_type_enum.dart b/lib/pangea/enum/construct_use_type_enum.dart index c42f7f4a9..ab953d24d 100644 --- a/lib/pangea/enum/construct_use_type_enum.dart +++ b/lib/pangea/enum/construct_use_type_enum.dart @@ -29,13 +29,14 @@ enum ConstructUseTypeEnum { /// encountered as distractor in IGC flow and selected it incIGC, - /// selected correctly in practice activity flow + /// selected correctly in word meaning in context practice activity corPA, - /// encountered as distractor in practice activity flow and correctly ignored it + /// encountered as distractor in word meaning in context practice activity and correctly ignored it + /// Currently not used ignPA, - /// was target construct in practice activity but user did not select correctly + /// was target construct in word meaning in context practice activity and incorrectly selected incPA, } @@ -125,9 +126,9 @@ extension ConstructUseTypeExtension on ConstructUseTypeEnum { case ConstructUseTypeEnum.unk: return 0; case ConstructUseTypeEnum.corPA: - return 2; + return 5; case ConstructUseTypeEnum.incPA: - return -1; + return -2; case ConstructUseTypeEnum.ignPA: return 1; } diff --git a/lib/pangea/widgets/chat/message_audio_card.dart b/lib/pangea/widgets/chat/message_audio_card.dart index e9cf67dd7..b190da291 100644 --- a/lib/pangea/widgets/chat/message_audio_card.dart +++ b/lib/pangea/widgets/chat/message_audio_card.dart @@ -2,6 +2,7 @@ import 'package:fluffychat/pages/chat/events/audio_player.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart'; import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart'; import 'package:flutter/material.dart'; @@ -86,6 +87,8 @@ class MessageAudioCardState extends State { Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(8), + constraints: const BoxConstraints(minHeight: minCardHeight), + alignment: Alignment.center, child: _isLoading ? const ToolbarContentLoadingIndicator() : localAudioEvent != null || audioFile != null diff --git a/lib/pangea/widgets/chat/message_selection_overlay.dart b/lib/pangea/widgets/chat/message_selection_overlay.dart index d94790925..5c41a3c33 100644 --- a/lib/pangea/widgets/chat/message_selection_overlay.dart +++ b/lib/pangea/widgets/chat/message_selection_overlay.dart @@ -16,6 +16,7 @@ import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:matrix/matrix.dart'; class MessageSelectionOverlay extends StatefulWidget { @@ -73,6 +74,26 @@ class MessageOverlayController extends State setInitialToolbarMode(); } + /// We need to check if the setState call is safe to call immediately + /// Kept getting the error: setState() or markNeedsBuild() called during build. + /// This is a workaround to prevent that error + @override + void setState(VoidCallback fn) { + if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.idle || + SchedulerBinding.instance.schedulerPhase == + SchedulerPhase.postFrameCallbacks) { + // It's safe to call setState immediately + super.setState(fn); + } else { + // Defer the setState call to after the current frame + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + super.setState(fn); + } + }); + } + } + bool get isPracticeComplete => activitiesLeftToComplete <= 0; /// When an activity is completed, we need to update the state @@ -137,7 +158,8 @@ class MessageOverlayController extends State void onClickOverlayMessageToken( PangeaToken token, ) { - if (toolbarMode == MessageMode.practiceActivity) { + if ([MessageMode.practiceActivity, MessageMode.textToSpeech] + .contains(toolbarMode)) { return; } diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 63a202f8e..f0c0dde80 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -13,10 +13,13 @@ import 'package:fluffychat/pangea/widgets/chat/message_translation_card.dart'; import 'package:fluffychat/pangea/widgets/chat/message_unsubscribed_card.dart'; import 'package:fluffychat/pangea/widgets/igc/word_data_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; +import 'package:fluffychat/pangea/widgets/select_to_define.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +const double minCardHeight = 70; + class MessageToolbar extends StatefulWidget { final PangeaMessageEvent pangeaMessageEvent; final MessageOverlayController overLayController; diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart index 9c648fa5b..5e66d9966 100644 --- a/lib/pangea/widgets/chat/message_translation_card.dart +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -6,6 +6,7 @@ import 'package:fluffychat/pangea/repo/full_text_translation_repo.dart'; import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/inline_tooltip.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart'; import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart'; import 'package:fluffychat/widgets/matrix.dart'; @@ -135,6 +136,8 @@ class MessageTranslationCardState extends State { return Container( padding: const EdgeInsets.all(8), + constraints: const BoxConstraints(minHeight: minCardHeight), + alignment: Alignment.center, child: _fetchingTranslation ? const ToolbarContentLoadingIndicator() : Column( diff --git a/lib/pangea/widgets/content_issue_button.dart b/lib/pangea/widgets/content_issue_button.dart new file mode 100644 index 000000000..7df2565c0 --- /dev/null +++ b/lib/pangea/widgets/content_issue_button.dart @@ -0,0 +1,89 @@ +import 'package:fluffychat/pangea/widgets/common/bot_face_svg.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class ContentIssueButton extends StatelessWidget { + final bool isActive; + final void Function(String) submitFeedback; + + const ContentIssueButton({ + super.key, + required this.isActive, + required this.submitFeedback, + }); + + @override + Widget build(BuildContext context) { + return Opacity( + opacity: 0.8, // Slight opacity + child: Tooltip( + message: L10n.of(context)!.reportContentIssueTitle, + child: IconButton( + icon: const Icon(Icons.flag), + iconSize: 16, + onPressed: () { + if (!isActive) { + return; + } + final TextEditingController feedbackController = + TextEditingController(); + + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text( + L10n.of(context)!.reportContentIssueTitle, + textAlign: TextAlign.center, + ), + content: Container( + constraints: const BoxConstraints(maxWidth: 300), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const BotFace( + width: 60, + expression: BotExpression.addled, + ), + const SizedBox(height: 10), + Text(L10n.of(context)!.reportContentIssueDescription), + const SizedBox(height: 10), + SizedBox( + width: double.infinity, + child: TextField( + controller: feedbackController, + decoration: InputDecoration( + labelText: L10n.of(context)!.feedback, + border: const OutlineInputBorder(), + ), + maxLines: 4, + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); // Close the dialog + }, + child: Text(L10n.of(context)!.cancel), + ), + ElevatedButton( + onPressed: () { + // Call the additional callback function + submitFeedback(feedbackController.text); + Navigator.of(context).pop(); // Close the dialog + }, + child: Text(L10n.of(context)!.submit), + ), + ], + ); + }, + ); + }, + ), + ), + ); + } +} diff --git a/lib/pangea/widgets/igc/span_card.dart b/lib/pangea/widgets/igc/span_card.dart index 97fb2cfe4..757f8f0ea 100644 --- a/lib/pangea/widgets/igc/span_card.dart +++ b/lib/pangea/widgets/igc/span_card.dart @@ -143,6 +143,8 @@ class SpanCardState extends State { } } + /// @ggurdin - this seems like it would be including the correct answer as well + /// we only want to give this kind of points for ignored distractors /// Returns the list of choices that are not selected List? get ignoredMatches => widget.scm.pangeaMatch?.match.choices ?.where((choice) => !choice.selected) diff --git a/lib/pangea/widgets/igc/word_data_card.dart b/lib/pangea/widgets/igc/word_data_card.dart index c35829264..6f1492a75 100644 --- a/lib/pangea/widgets/igc/word_data_card.dart +++ b/lib/pangea/widgets/igc/word_data_card.dart @@ -7,6 +7,7 @@ import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/pangea/widgets/common/p_circular_loader.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/foundation.dart'; @@ -176,6 +177,8 @@ class WordDataCardView extends StatelessWidget { return Container( padding: const EdgeInsets.all(8), + constraints: const BoxConstraints(minHeight: minCardHeight), + alignment: Alignment.center, child: Scrollbar( thumbVisibility: true, controller: scrollController, @@ -400,25 +403,3 @@ class PartOfSpeechBlock extends StatelessWidget { ); } } - -class SelectToDefine extends StatelessWidget { - const SelectToDefine({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return Center( - child: Container( - height: 80, - padding: const EdgeInsets.all(8), - child: Center( - child: Text( - L10n.of(context)!.selectToDefine, - style: BotStyle.text(context), - ), - ), - ), - ); - } -} diff --git a/lib/pangea/widgets/practice_activity/no_more_practice_card.dart b/lib/pangea/widgets/practice_activity/no_more_practice_card.dart index 46df44ce5..1cef6c174 100644 --- a/lib/pangea/widgets/practice_activity/no_more_practice_card.dart +++ b/lib/pangea/widgets/practice_activity/no_more_practice_card.dart @@ -1,3 +1,4 @@ +import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:flutter/material.dart'; class StarAnimationWidget extends StatefulWidget { @@ -42,8 +43,8 @@ class _StarAnimationWidgetState extends State Widget build(BuildContext context) { return SizedBox( // Set constant height and width for the star container - height: 80.0, - width: 80.0, + height: 60.0, + width: 60.0, child: Center( child: AnimatedBuilder( animation: _controller, @@ -74,6 +75,7 @@ class GamifiedTextWidget extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, // Adjusts the size to fit children children: [ + const SizedBox(height: 10), // Spacing between the star and text // Star animation above the text const StarAnimationWidget(), const SizedBox(height: 10), // Spacing between the star and text @@ -84,10 +86,7 @@ class GamifiedTextWidget extends StatelessWidget { padding: const EdgeInsets.all(8), child: Text( userMessage, - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), + style: BotStyle.text(context), textAlign: TextAlign.center, // Center-align the text ), ), diff --git a/lib/pangea/widgets/practice_activity/practice_activity_card.dart b/lib/pangea/widgets/practice_activity/practice_activity_card.dart index 5ebb4ec38..517cbcebe 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_card.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_card.dart @@ -13,6 +13,8 @@ import 'package:fluffychat/pangea/utils/bot_style.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/pangea/widgets/animations/gain_points.dart'; import 'package:fluffychat/pangea/widgets/chat/message_selection_overlay.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; +import 'package:fluffychat/pangea/widgets/content_issue_button.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/no_more_practice_card.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/target_tokens_controller.dart'; @@ -54,7 +56,7 @@ class MessagePracticeActivityCardState extends State { // Used to show an animation when the user completes an activity // while simultaneously fetching a new activity and not showing the loading spinner // until the appropriate time has passed to 'savor the joy' - Duration appropriateTimeForJoy = const Duration(milliseconds: 1000); + Duration appropriateTimeForJoy = const Duration(milliseconds: 1500); bool savoringTheJoy = false; @override @@ -65,7 +67,7 @@ class MessagePracticeActivityCardState extends State { void _updateFetchingActivity(bool value) { if (fetchingActivity == value) return; - setState(() => fetchingActivity = value); + if (mounted) setState(() => fetchingActivity = value); } void _setPracticeActivity(PracticeActivityEvent? activity) { @@ -177,19 +179,13 @@ class MessagePracticeActivityCardState extends State { ); Future _savorTheJoy() async { - if (savoringTheJoy) { - //should not happen - debugger(when: kDebugMode); - } - savoringTheJoy = true; + debugger(when: savoringTheJoy && kDebugMode); - debugPrint('Savoring the joy'); + setState(() => savoringTheJoy = true); await Future.delayed(appropriateTimeForJoy); - savoringTheJoy = false; - - debugPrint('Savoring the joy is over'); + if (mounted) setState(() => savoringTheJoy = false); } /// Called when the user finishes an activity. @@ -233,6 +229,7 @@ class MessagePracticeActivityCardState extends State { widget.overlayController.onActivityFinish(); + // final Iterable result = await Future.wait([ _savorTheJoy(), _fetchNewActivity(), @@ -254,50 +251,6 @@ class MessagePracticeActivityCardState extends State { } } - void onFlagClick(BuildContext context) { - final TextEditingController feedbackController = TextEditingController(); - - showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text(L10n.of(context)!.reportContentIssueTitle), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(L10n.of(context)!.reportContentIssueDescription), - const SizedBox(height: 10), - TextField( - controller: feedbackController, - decoration: InputDecoration( - labelText: L10n.of(context)!.feedback, - border: const OutlineInputBorder(), - ), - maxLines: 3, - ), - ], - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); // Close the dialog - }, - child: Text(L10n.of(context)!.cancel), - ), - ElevatedButton( - onPressed: () { - // Call the additional callback function - submitFeedback(feedbackController.text); - Navigator.of(context).pop(); // Close the dialog - }, - child: Text(L10n.of(context)!.submit), - ), - ], - ); - }, - ); - } - /// clear the current activity, record, and selection /// fetch a new activity, including the offending activity in the request void submitFeedback(String feedback) { @@ -385,6 +338,7 @@ class MessagePracticeActivityCardState extends State { constraints: const BoxConstraints( maxWidth: 350, minWidth: 350, + minHeight: minCardHeight, ), child: Stack( alignment: Alignment.center, @@ -412,18 +366,9 @@ class MessagePracticeActivityCardState extends State { Positioned( top: 0, right: 0, - child: Opacity( - opacity: 0.65, // Slight opacity - child: Tooltip( - message: L10n.of(context)!.reportContentIssueTitle, - child: IconButton( - padding: const EdgeInsets.all(2), - icon: const Icon(Icons.flag), - iconSize: 16, - onPressed: () => - currentActivity == null ? null : onFlagClick(context), - ), - ), + child: ContentIssueButton( + isActive: currentActivity != null, + submitFeedback: submitFeedback, ), ), ], diff --git a/lib/pangea/widgets/select_to_define.dart b/lib/pangea/widgets/select_to_define.dart new file mode 100644 index 000000000..7020e5e77 --- /dev/null +++ b/lib/pangea/widgets/select_to_define.dart @@ -0,0 +1,26 @@ +import 'package:fluffychat/pangea/utils/bot_style.dart'; +import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class SelectToDefine extends StatelessWidget { + const SelectToDefine({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + constraints: const BoxConstraints(minHeight: minCardHeight), + padding: const EdgeInsets.all(8), + child: Center( + child: Text( + L10n.of(context)!.selectToDefine, + style: BotStyle.text(context), + ), + ), + ), + ); + } +}