From 589901150dccb2f647d25fc6d0847158a107f7c3 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Thu, 27 Jun 2024 09:53:20 -0400 Subject: [PATCH 01/26] error handling for error found by blue --- lib/pangea/utils/error_handler.dart | 5 +++++ lib/pangea/widgets/igc/pangea_rich_text.dart | 23 ++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/lib/pangea/utils/error_handler.dart b/lib/pangea/utils/error_handler.dart index d66c29186..2144e9f87 100644 --- a/lib/pangea/utils/error_handler.dart +++ b/lib/pangea/utils/error_handler.dart @@ -8,6 +8,11 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:http/http.dart' as http; import 'package:sentry_flutter/sentry_flutter.dart'; +class PangeaWarningError implements Exception { + final String message; + PangeaWarningError(message) : message = "Pangea Warning Error: $message"; +} + class ErrorHandler { ErrorHandler(); diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index bbd6868bf..afb72c0d9 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -57,13 +57,22 @@ class PangeaRichTextState extends State { } void _setTextSpan(String newTextSpan) { - widget.toolbarController?.toolbar?.textSelection.setMessageText( - newTextSpan, - ); - if (mounted) { - setState(() { - textSpan = newTextSpan; - }); + try { + widget.toolbarController?.toolbar?.textSelection.setMessageText( + newTextSpan, + ); + if (mounted) { + setState(() { + textSpan = newTextSpan; + }); + } + } catch (err, stack) { + ErrorHandler.logError( + e: PangeaWarningError( + err.toString(), + ), + s: stack, + ); } } From d975e52a04f58f9793d728dbd82287254f7cb35e Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Thu, 27 Jun 2024 12:30:25 -0400 Subject: [PATCH 02/26] inlined tooltip --- lib/pages/chat/chat_event_list.dart | 2 +- .../choreographer/widgets/it_bar_buttons.dart | 9 +-- .../controllers/my_analytics_controller.dart | 1 + lib/pangea/utils/instructions.dart | 56 +++++++++++++++++-- .../chat/message_speech_to_text_card.dart | 6 +- lib/pangea/widgets/igc/pangea_rich_text.dart | 2 +- 6 files changed, 64 insertions(+), 12 deletions(-) diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 048b54443..114aadd84 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -46,7 +46,7 @@ class ChatEventList extends StatelessWidget { // card, attach it on top of the first shown message WidgetsBinding.instance.addPostFrameCallback((_) { if (events.isEmpty) return; - controller.pangeaController.instructions.show( + controller.pangeaController.instructions.showInstructionsPopup( context, InstructionsEnum.clickMessage, events[0].eventId, diff --git a/lib/pangea/choreographer/widgets/it_bar_buttons.dart b/lib/pangea/choreographer/widgets/it_bar_buttons.dart index 815020d17..e06855f26 100644 --- a/lib/pangea/choreographer/widgets/it_bar_buttons.dart +++ b/lib/pangea/choreographer/widgets/it_bar_buttons.dart @@ -1,8 +1,8 @@ -import 'package:flutter/material.dart'; - import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/utils/instructions.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; + import '../../widgets/common/bot_face_svg.dart'; import '../controllers/choreographer.dart'; import '../controllers/it_controller.dart'; @@ -37,7 +37,7 @@ class ITBotButton extends StatelessWidget { @override Widget build(BuildContext context) { - choreographer.pangeaController.instructions.show( + choreographer.pangeaController.instructions.showInstructionsPopup( context, InstructionsEnum.itInstructions, choreographer.itBotTransformTargetKey, @@ -46,7 +46,8 @@ class ITBotButton extends StatelessWidget { return IconButton( icon: const BotFace(width: 40.0, expression: BotExpression.right), - onPressed: () => choreographer.pangeaController.instructions.show( + onPressed: () => + choreographer.pangeaController.instructions.showInstructionsPopup( context, InstructionsEnum.itInstructions, choreographer.itBotTransformTargetKey, diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 614fcf9db..067083e38 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -305,6 +305,7 @@ class MyAnalyticsController extends BaseController { // if there's new content to be sent, or if lastUpdated hasn't been // set yet for this room, send the analytics events + if (summaryContent.isNotEmpty || lastUpdated == null) { await SummaryAnalyticsEvent.sendSummaryAnalyticsEvent( analyticsRoom, diff --git a/lib/pangea/utils/instructions.dart b/lib/pangea/utils/instructions.dart index b9eecd799..4fe3ea627 100644 --- a/lib/pangea/utils/instructions.dart +++ b/lib/pangea/utils/instructions.dart @@ -14,17 +14,26 @@ import 'overlay.dart'; class InstructionsController { late PangeaController _pangeaController; + /// Instruction popup was closed by the user final Map _instructionsClosed = {}; + + /// Instructions that were shown in that session final Map _instructionsShown = {}; + /// Returns true if the instructions were turned off by the user via the toggle switch + bool? toggledOff(InstructionsEnum key) => + _pangeaController.pStoreService.read(key.toString()); + + /// We have these three methods to make sure that the instructions are not shown too much + InstructionsController(PangeaController pangeaController) { _pangeaController = pangeaController; } + /// Returns true if the instructions were turned off by the user + /// via the toggle switch bool wereInstructionsTurnedOff(InstructionsEnum key) => - _pangeaController.pStoreService.read(key.toString()) ?? - _instructionsClosed[key] ?? - false; + toggledOff(key) ?? _instructionsClosed[key] ?? false; Future updateEnableInstructions( InstructionsEnum key, @@ -35,7 +44,30 @@ class InstructionsController { value, ); - Future show( + // return a text widget with constainer that expands to fill a parent container + // and displays instructions text defined in the enum extension + Future getInlineTooltip( + BuildContext context, + InstructionsEnum key, + ) async { + if (wereInstructionsTurnedOff(key)) { + return const SizedBox(); + } + if (L10n.of(context) == null) { + ErrorHandler.logError( + m: "null context in ITBotButton.showCard", + s: StackTrace.current, + ); + return const SizedBox(); + } + if (_instructionsShown[key] ?? false) { + return const SizedBox(); + } + + return key.inlineTooltip(context); + } + + Future showInstructionsPopup( BuildContext context, InstructionsEnum key, String transformTargetKey, [ @@ -135,6 +167,22 @@ extension Copy on InstructionsEnum { : L10n.of(context)!.tooltipInstructionsBrowserBody; } } + + Widget inlineTooltip(BuildContext context) { + switch (this) { + case InstructionsEnum.itInstructions: + return Padding( + padding: const EdgeInsets.all(6.0), + child: Text( + body(context), + style: BotStyle.text(context), + ), + ); + default: + print('inlineTooltip not implemented for $this'); + return const SizedBox(); + } + } } class InstructionsToggle extends StatefulWidget { diff --git a/lib/pangea/widgets/chat/message_speech_to_text_card.dart b/lib/pangea/widgets/chat/message_speech_to_text_card.dart index 9099b9b95..68295789a 100644 --- a/lib/pangea/widgets/chat/message_speech_to_text_card.dart +++ b/lib/pangea/widgets/chat/message_speech_to_text_card.dart @@ -172,7 +172,8 @@ class MessageSpeechToTextCardState extends State { number: "${selectedToken?.confidence ?? speechToTextResponse!.transcript.confidence}%", toolTip: L10n.of(context)!.accuracy, - onPressed: () => MatrixState.pangeaController.instructions.show( + onPressed: () => MatrixState.pangeaController.instructions + .showInstructionsPopup( context, InstructionsEnum.tooltipInstructions, widget.messageEvent.eventId, @@ -184,7 +185,8 @@ class MessageSpeechToTextCardState extends State { number: wordsPerMinuteString != null ? "$wordsPerMinuteString" : "??", toolTip: L10n.of(context)!.wordsPerMinute, - onPressed: () => MatrixState.pangeaController.instructions.show( + onPressed: () => MatrixState.pangeaController.instructions + .showInstructionsPopup( context, InstructionsEnum.tooltipInstructions, widget.messageEvent.eventId, diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index bbd6868bf..05875c25c 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -115,7 +115,7 @@ class PangeaRichTextState extends State { @override Widget build(BuildContext context) { if (blur > 0) { - pangeaController.instructions.show( + pangeaController.instructions.showInstructionsPopup( context, InstructionsEnum.blurMeansTranslate, widget.pangeaMessageEvent.eventId, From e35be8f8628a6f2c225f31e8410c3ad8c97049f2 Mon Sep 17 00:00:00 2001 From: bluearevalo <90929912+bluearevalo@users.noreply.github.com> Date: Thu, 27 Jun 2024 13:19:59 -0400 Subject: [PATCH 03/26] added error handling messages for Sentry --- lib/pangea/widgets/igc/pangea_rich_text.dart | 109 ++++++++---------- .../fcm_shared_isolate/pubspec.lock | 12 ++ pubspec.yaml | 4 +- 3 files changed, 61 insertions(+), 64 deletions(-) diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index afb72c0d9..7391edb05 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -56,70 +56,55 @@ class PangeaRichTextState extends State { setTextSpan(); } - void _setTextSpan(String newTextSpan) { - try { - widget.toolbarController?.toolbar?.textSelection.setMessageText( - newTextSpan, - ); - if (mounted) { - setState(() { - textSpan = newTextSpan; +void _setTextSpan(String newTextSpan) { + try { + if (!mounted) return; // Early exit if the widget is no longer in the tree + + widget.toolbarController?.toolbar?.textSelection.setMessageText( + newTextSpan, + ); + setState(() { + textSpan = newTextSpan; + }); + } catch (error, stackTrace) { + ErrorHandler.logError(e: error, s: stackTrace, m: "Error setting text span in PangeaRichText"); + } +} + +void setTextSpan() { + if (_fetchingRepresentation) { + _setTextSpan(widget.pangeaMessageEvent.event.getDisplayEvent(widget.pangeaMessageEvent.timeline).body); + return; + } + + if (widget.pangeaMessageEvent.eventId.contains("webdebug")) { + debugger(when: kDebugMode); + } + + repEvent = widget.pangeaMessageEvent + .representationByLanguage(widget.pangeaMessageEvent.messageDisplayLangCode) + ?.content; + + if (repEvent == null) { + setState(() => _fetchingRepresentation = true); + widget.pangeaMessageEvent + .representationByLanguageGlobal(langCode: widget.pangeaMessageEvent.messageDisplayLangCode) + .onError((error, stackTrace) => ErrorHandler.logError(e: error, s: stackTrace, m: "Error fetching representation")) + .then((event) { + if (!mounted) return; + repEvent = event; + _setTextSpan(repEvent?.text ?? widget.pangeaMessageEvent.body); + }).whenComplete(() { + if (mounted) { + setState(() => _fetchingRepresentation = false); + } }); - } - } catch (err, stack) { - ErrorHandler.logError( - e: PangeaWarningError( - err.toString(), - ), - s: stack, - ); - } - } - - void setTextSpan() { - if (_fetchingRepresentation == true) { - _setTextSpan( - textSpan = widget.pangeaMessageEvent.event - .getDisplayEvent(widget.pangeaMessageEvent.timeline) - .body, - ); - return; - } - - if (widget.pangeaMessageEvent.eventId.contains("webdebug")) { - debugger(when: kDebugMode); - } - - repEvent = widget.pangeaMessageEvent - .representationByLanguage( - widget.pangeaMessageEvent.messageDisplayLangCode, - ) - ?.content; - - if (repEvent == null) { - setState(() => _fetchingRepresentation = true); - widget.pangeaMessageEvent - .representationByLanguageGlobal( - langCode: widget.pangeaMessageEvent.messageDisplayLangCode, - ) - .onError( - (error, stackTrace) => - ErrorHandler.logError(e: error, s: stackTrace), - ) - .then((event) { - repEvent = event; - _setTextSpan(repEvent?.text ?? widget.pangeaMessageEvent.body); - }).whenComplete(() { - if (mounted) { - setState(() => _fetchingRepresentation = false); - } - }); - - _setTextSpan(widget.pangeaMessageEvent.body); - } else { - _setTextSpan(repEvent!.text); - } + + _setTextSpan(widget.pangeaMessageEvent.body); + } else { + _setTextSpan(repEvent!.text); } +} @override Widget build(BuildContext context) { diff --git a/pangea_packages/fcm_shared_isolate/pubspec.lock b/pangea_packages/fcm_shared_isolate/pubspec.lock index 55c0cb16e..7f3457dbe 100644 --- a/pangea_packages/fcm_shared_isolate/pubspec.lock +++ b/pangea_packages/fcm_shared_isolate/pubspec.lock @@ -149,33 +149,41 @@ packages: description: name: matcher sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted version: "0.12.16+1" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted version: "0.8.0" + version: "0.8.0" meta: dependency: transitive description: name: meta sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" url: "https://pub.dev" source: hosted version: "1.12.0" + version: "1.12.0" path: dependency: transitive description: name: path sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted version: "1.9.0" + version: "1.9.0" pedantic: dependency: "direct dev" description: @@ -242,9 +250,11 @@ packages: description: name: test_api sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" url: "https://pub.dev" source: hosted version: "0.7.0" + version: "0.7.0" vector_math: dependency: transitive description: @@ -272,3 +282,5 @@ packages: sdks: dart: ">=3.3.0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/pubspec.yaml b/pubspec.yaml index ce6ef99dc..85cfeb074 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -148,8 +148,8 @@ flutter: # #Pangea # uncomment this to enable mobile builds # causes error with github actions - # - .env - # - assets/.env + - .env + - assets/.env - assets/pangea/ - assets/pangea/bot_faces/ # Pangea# From 929e30a499f388ca3f879dae205aec181ee934b3 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Fri, 28 Jun 2024 08:54:37 -0400 Subject: [PATCH 04/26] Design and insert inline tooltip --- assets/l10n/intl_en.arb | 4 +- lib/pages/chat/chat_event_list.dart | 2 +- .../choreographer/widgets/it_bar_buttons.dart | 2 +- lib/pangea/enum/instructions_enum.dart | 68 +++++++++ lib/pangea/models/user_model.dart | 2 +- lib/pangea/utils/instructions.dart | 133 +++++++----------- .../chat/message_speech_to_text_card.dart | 11 +- lib/pangea/widgets/chat/message_toolbar.dart | 2 + lib/pangea/widgets/igc/pangea_rich_text.dart | 2 +- 9 files changed, 141 insertions(+), 85 deletions(-) create mode 100644 lib/pangea/enum/instructions_enum.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index cb319c9c7..134f46d0a 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4058,5 +4058,7 @@ "suggestToSpaceDesc": "Suggested spaces will appear in the chat lists for their parent spaces", "practice": "Practice", "noLanguagesSet": "No languages set", - "noActivitiesFound": "No practice activities found for this message" + "noActivitiesFound": "No practice activities found for this message", + "hintTitle": "Hint:", + "speechToTextBody": "See how well you did by looking at your Accuracy and Words Per Minute scores" } \ No newline at end of file diff --git a/lib/pages/chat/chat_event_list.dart b/lib/pages/chat/chat_event_list.dart index 114aadd84..24508bb62 100644 --- a/lib/pages/chat/chat_event_list.dart +++ b/lib/pages/chat/chat_event_list.dart @@ -4,8 +4,8 @@ import 'package:fluffychat/pages/chat/events/message.dart'; import 'package:fluffychat/pages/chat/seen_by_row.dart'; import 'package:fluffychat/pages/chat/typing_indicators.dart'; import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; +import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; -import 'package:fluffychat/pangea/utils/instructions.dart'; import 'package:fluffychat/pangea/widgets/chat/locked_chat_message.dart'; import 'package:fluffychat/utils/account_config.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; diff --git a/lib/pangea/choreographer/widgets/it_bar_buttons.dart b/lib/pangea/choreographer/widgets/it_bar_buttons.dart index e06855f26..77de39818 100644 --- a/lib/pangea/choreographer/widgets/it_bar_buttons.dart +++ b/lib/pangea/choreographer/widgets/it_bar_buttons.dart @@ -1,5 +1,5 @@ import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/utils/instructions.dart'; +import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; diff --git a/lib/pangea/enum/instructions_enum.dart b/lib/pangea/enum/instructions_enum.dart new file mode 100644 index 000000000..8bcd3f3f9 --- /dev/null +++ b/lib/pangea/enum/instructions_enum.dart @@ -0,0 +1,68 @@ +import 'package:fluffychat/pangea/utils/bot_style.dart'; +import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +enum InstructionsEnum { + itInstructions, + clickMessage, + blurMeansTranslate, + tooltipInstructions, + speechToText, +} + +extension Copy on InstructionsEnum { + String title(BuildContext context) { + switch (this) { + case InstructionsEnum.itInstructions: + return L10n.of(context)!.itInstructionsTitle; + case InstructionsEnum.clickMessage: + return L10n.of(context)!.clickMessageTitle; + case InstructionsEnum.blurMeansTranslate: + return L10n.of(context)!.blurMeansTranslateTitle; + case InstructionsEnum.tooltipInstructions: + return L10n.of(context)!.tooltipInstructionsTitle; + case InstructionsEnum.speechToText: + return L10n.of(context)!.hintTitle; + } + } + + String body(BuildContext context) { + switch (this) { + case InstructionsEnum.itInstructions: + return L10n.of(context)!.itInstructionsBody; + case InstructionsEnum.clickMessage: + return L10n.of(context)!.clickMessageBody; + case InstructionsEnum.blurMeansTranslate: + return L10n.of(context)!.blurMeansTranslateBody; + case InstructionsEnum.speechToText: + return L10n.of(context)!.speechToTextBody; + case InstructionsEnum.tooltipInstructions: + return PlatformInfos.isMobile + ? L10n.of(context)!.tooltipInstructionsMobileBody + : L10n.of(context)!.tooltipInstructionsBrowserBody; + } + } + + Widget inlineTooltip(BuildContext context) { + switch (this) { + case InstructionsEnum.speechToText: + return Column( + children: [ + Text( + title(context), + style: BotStyle.text(context), + ), + Text( + body(context), + style: BotStyle.text(context), + ), + // ), + ], + ); + default: + debugPrint('inlineTooltip not implemented for $this'); + return const SizedBox(); + } + } +} diff --git a/lib/pangea/models/user_model.dart b/lib/pangea/models/user_model.dart index f8e929a13..afe910610 100644 --- a/lib/pangea/models/user_model.dart +++ b/lib/pangea/models/user_model.dart @@ -4,8 +4,8 @@ import 'package:country_picker/country_picker.dart'; import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/models/space_model.dart'; -import 'package:fluffychat/pangea/utils/instructions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; diff --git a/lib/pangea/utils/instructions.dart b/lib/pangea/utils/instructions.dart index 4fe3ea627..a21b664a9 100644 --- a/lib/pangea/utils/instructions.dart +++ b/lib/pangea/utils/instructions.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -14,24 +14,24 @@ import 'overlay.dart'; class InstructionsController { late PangeaController _pangeaController; + // We have these three methods to make sure that the instructions are not shown too much + /// Instruction popup was closed by the user final Map _instructionsClosed = {}; - /// Instructions that were shown in that session + /// Instruction popup has already been shown this session final Map _instructionsShown = {}; - /// Returns true if the instructions were turned off by the user via the toggle switch + /// Returns true if the user requested this popup not be shown again bool? toggledOff(InstructionsEnum key) => _pangeaController.pStoreService.read(key.toString()); - /// We have these three methods to make sure that the instructions are not shown too much - InstructionsController(PangeaController pangeaController) { _pangeaController = pangeaController; } - /// Returns true if the instructions were turned off by the user - /// via the toggle switch + /// Returns true if the instructions were closed + /// or turned off by the user via the toggle switch bool wereInstructionsTurnedOff(InstructionsEnum key) => toggledOff(key) ?? _instructionsClosed[key] ?? false; @@ -44,29 +44,6 @@ class InstructionsController { value, ); - // return a text widget with constainer that expands to fill a parent container - // and displays instructions text defined in the enum extension - Future getInlineTooltip( - BuildContext context, - InstructionsEnum key, - ) async { - if (wereInstructionsTurnedOff(key)) { - return const SizedBox(); - } - if (L10n.of(context) == null) { - ErrorHandler.logError( - m: "null context in ITBotButton.showCard", - s: StackTrace.current, - ); - return const SizedBox(); - } - if (_instructionsShown[key] ?? false) { - return const SizedBox(); - } - - return key.inlineTooltip(context); - } - Future showInstructionsPopup( BuildContext context, InstructionsEnum key, @@ -130,58 +107,56 @@ class InstructionsController { ), ); } -} -enum InstructionsEnum { - itInstructions, - clickMessage, - blurMeansTranslate, - tooltipInstructions, -} - -extension Copy on InstructionsEnum { - String title(BuildContext context) { - switch (this) { - case InstructionsEnum.itInstructions: - return L10n.of(context)!.itInstructionsTitle; - case InstructionsEnum.clickMessage: - return L10n.of(context)!.clickMessageTitle; - case InstructionsEnum.blurMeansTranslate: - return L10n.of(context)!.blurMeansTranslateTitle; - case InstructionsEnum.tooltipInstructions: - return L10n.of(context)!.tooltipInstructionsTitle; + /// Returns a widget that will be added to existing widget + /// that displays hint text defined in the enum extension + Widget getInlineTooltip( + BuildContext context, + InstructionsEnum key, + Function refreshOnClose, + ) { + if (wereInstructionsTurnedOff(key)) { + // Uncomment this line to make hint viewable again + // _instructionsClosed[key] = false; + return const SizedBox(); } - } - - String body(BuildContext context) { - switch (this) { - case InstructionsEnum.itInstructions: - return L10n.of(context)!.itInstructionsBody; - case InstructionsEnum.clickMessage: - return L10n.of(context)!.clickMessageBody; - case InstructionsEnum.blurMeansTranslate: - return L10n.of(context)!.blurMeansTranslateBody; - case InstructionsEnum.tooltipInstructions: - return PlatformInfos.isMobile - ? L10n.of(context)!.tooltipInstructionsMobileBody - : L10n.of(context)!.tooltipInstructionsBrowserBody; - } - } - - Widget inlineTooltip(BuildContext context) { - switch (this) { - case InstructionsEnum.itInstructions: - return Padding( - padding: const EdgeInsets.all(6.0), - child: Text( - body(context), - style: BotStyle.text(context), - ), - ); - default: - print('inlineTooltip not implemented for $this'); - return const SizedBox(); + if (L10n.of(context) == null) { + ErrorHandler.logError( + m: "null context in ITBotButton.showCard", + s: StackTrace.current, + ); + return const SizedBox(); } + return Column( + children: [ + Row( + children: [ + const Expanded( + child: Divider(), + ), + CircleAvatar( + radius: 10, + backgroundColor: + Theme.of(context).colorScheme.primary.withAlpha(50), + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon( + Icons.close_outlined, + size: 15, + ), + onPressed: () { + _instructionsClosed[key] = true; + refreshOnClose(); + }, + ), + ), + ], + ), + key.inlineTooltip(context), + const SizedBox(height: 9), + const Divider(), + ], + ); } } diff --git a/lib/pangea/widgets/chat/message_speech_to_text_card.dart b/lib/pangea/widgets/chat/message_speech_to_text_card.dart index 68295789a..9f76e060a 100644 --- a/lib/pangea/widgets/chat/message_speech_to_text_card.dart +++ b/lib/pangea/widgets/chat/message_speech_to_text_card.dart @@ -1,9 +1,9 @@ import 'dart:developer'; +import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/speech_to_text_models.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:fluffychat/pangea/utils/instructions.dart'; import 'package:fluffychat/pangea/widgets/chat/toolbar_content_loading_indicator.dart'; import 'package:fluffychat/pangea/widgets/common/icon_number_widget.dart'; import 'package:fluffychat/pangea/widgets/igc/card_error_widget.dart'; @@ -65,6 +65,10 @@ class MessageSpeechToTextCardState extends State { } } + void refreshOnCloseHint() { + setState(() {}); + } + TextSpan _buildTranscriptText(BuildContext context) { final Transcript transcript = speechToTextResponse!.transcript; final List spans = []; @@ -195,6 +199,11 @@ class MessageSpeechToTextCardState extends State { ), ], ), + MatrixState.pangeaController.instructions.getInlineTooltip( + context, + InstructionsEnum.speechToText, + refreshOnCloseHint, + ), ], ); } diff --git a/lib/pangea/widgets/chat/message_toolbar.dart b/lib/pangea/widgets/chat/message_toolbar.dart index 2468d6b96..13ebd3103 100644 --- a/lib/pangea/widgets/chat/message_toolbar.dart +++ b/lib/pangea/widgets/chat/message_toolbar.dart @@ -134,6 +134,8 @@ class ToolbarDisplayController { ? Alignment.bottomLeft : Alignment.topLeft, backgroundColor: const Color.fromRGBO(0, 0, 0, 1).withAlpha(100), + closePrevOverlay: + MatrixState.pangeaController.subscriptionController.isSubscribed, ); if (MatrixState.pAnyState.entries.isNotEmpty) { diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index 05875c25c..1dd1cd16e 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -3,10 +3,10 @@ import 'dart:ui'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; +import 'package:fluffychat/pangea/enum/instructions_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; -import 'package:fluffychat/pangea/utils/instructions.dart'; import 'package:fluffychat/pangea/widgets/chat/message_context_menu.dart'; import 'package:fluffychat/pangea/widgets/chat/message_toolbar.dart'; import 'package:fluffychat/widgets/matrix.dart'; From 493146120913155e2a92a06b666b10e4782fd309 Mon Sep 17 00:00:00 2001 From: Kelrap Date: Fri, 28 Jun 2024 10:56:42 -0400 Subject: [PATCH 05/26] Adjust hint box design --- lib/pangea/enum/instructions_enum.dart | 18 +++++-- lib/pangea/utils/instructions.dart | 66 +++++++++++++++----------- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/lib/pangea/enum/instructions_enum.dart b/lib/pangea/enum/instructions_enum.dart index 8bcd3f3f9..16564ef07 100644 --- a/lib/pangea/enum/instructions_enum.dart +++ b/lib/pangea/enum/instructions_enum.dart @@ -49,9 +49,21 @@ extension Copy on InstructionsEnum { case InstructionsEnum.speechToText: return Column( children: [ - Text( - title(context), - style: BotStyle.text(context), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.record_voice_over_outlined, + size: 20, + ), + const SizedBox( + width: 7, + ), + Text( + title(context), + style: BotStyle.text(context), + ), + ], ), Text( body(context), diff --git a/lib/pangea/utils/instructions.dart b/lib/pangea/utils/instructions.dart index a21b664a9..fb917dedd 100644 --- a/lib/pangea/utils/instructions.dart +++ b/lib/pangea/utils/instructions.dart @@ -44,6 +44,8 @@ class InstructionsController { value, ); + /// Instruction Card gives users tips on + /// how to use Pangea Chat's features Future showInstructionsPopup( BuildContext context, InstructionsEnum key, @@ -109,7 +111,7 @@ class InstructionsController { } /// Returns a widget that will be added to existing widget - /// that displays hint text defined in the enum extension + /// which displays hint text defined in the enum extension Widget getInlineTooltip( BuildContext context, InstructionsEnum key, @@ -127,39 +129,45 @@ class InstructionsController { ); return const SizedBox(); } - return Column( - children: [ - Row( - children: [ - const Expanded( - child: Divider(), - ), - CircleAvatar( - radius: 10, - backgroundColor: - Theme.of(context).colorScheme.primary.withAlpha(50), - child: IconButton( - padding: EdgeInsets.zero, - icon: const Icon( - Icons.close_outlined, - size: 15, - ), - onPressed: () { - _instructionsClosed[key] = true; - refreshOnClose(); - }, - ), - ), - ], + return Badge( + offset: const Offset(0, -7), + backgroundColor: Colors.transparent, + label: CircleAvatar( + radius: 10, + backgroundColor: Theme.of(context).colorScheme.primary.withAlpha(20), + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon( + Icons.close_outlined, + size: 15, + ), + onPressed: () { + _instructionsClosed[key] = true; + refreshOnClose(); + }, ), - key.inlineTooltip(context), - const SizedBox(height: 9), - const Divider(), - ], + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + 10, + ), + color: Theme.of(context).colorScheme.primary.withAlpha(20), + // border: Border.all( + // color: Theme.of(context).colorScheme.primary.withAlpha(50), + // ), + ), + child: Padding( + padding: const EdgeInsets.all(10), + child: key.inlineTooltip(context), + ), + ), ); } } +/// User can toggle on to prevent Instruction Card +/// from appearing in future sessions class InstructionsToggle extends StatefulWidget { const InstructionsToggle({ super.key, From ffbc62ba555a1faa378a00268da5072a527c33a4 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Fri, 28 Jun 2024 11:34:55 -0400 Subject: [PATCH 06/26] consolidating language constants and using for unknown detection instance --- lib/pages/chat/chat_input_row.dart | 2 +- .../controllers/choreographer.dart | 114 ++++++++++-------- .../controllers/it_controller.dart | 3 - .../controllers/message_options.dart | 2 +- lib/pangea/constants/keys.dart | 4 - lib/pangea/constants/language_constants.dart | 24 ++++ lib/pangea/constants/language_keys.dart | 6 - lib/pangea/constants/language_level_type.dart | 3 - lib/pangea/constants/language_list_keys.dart | 4 - .../controllers/language_controller.dart | 2 +- .../language_detection_controller.dart | 18 ++- .../controllers/language_list_controller.dart | 3 +- .../controllers/my_analytics_controller.dart | 2 +- lib/pangea/controllers/user_controller.dart | 2 +- .../controllers/word_net_controller.dart | 2 +- .../pangea_room_extension.dart | 2 +- .../pangea_message_event.dart | 2 +- .../pangea_representation_event.dart | 2 +- lib/pangea/models/language_model.dart | 2 +- lib/pangea/models/space_model.dart | 2 +- lib/pangea/models/user_model.dart | 2 +- .../student_analytics/student_analytics.dart | 2 +- .../utils/get_chat_list_item_subtitle.dart | 2 +- lib/pangea/widgets/igc/word_data_card.dart | 2 +- .../space/language_level_dropdown.dart | 2 +- .../user_settings/p_language_dialog.dart | 2 +- lib/utils/background_push.dart | 2 +- 27 files changed, 113 insertions(+), 102 deletions(-) delete mode 100644 lib/pangea/constants/keys.dart create mode 100644 lib/pangea/constants/language_constants.dart delete mode 100644 lib/pangea/constants/language_keys.dart delete mode 100644 lib/pangea/constants/language_level_type.dart delete mode 100644 lib/pangea/constants/language_list_keys.dart diff --git a/lib/pages/chat/chat_input_row.dart b/lib/pages/chat/chat_input_row.dart index 6bf19a850..0577d6094 100644 --- a/lib/pages/chat/chat_input_row.dart +++ b/lib/pages/chat/chat_input_row.dart @@ -3,7 +3,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/choreographer/widgets/it_bar.dart'; import 'package:fluffychat/pangea/choreographer/widgets/send_button.dart'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/utils/platform_infos.dart'; import 'package:fluffychat/widgets/avatar.dart'; import 'package:fluffychat/widgets/matrix.dart'; diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index a0c6a5f6b..9a7f38428 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -5,13 +5,12 @@ import 'package:fluffychat/pages/chat/chat.dart'; import 'package:fluffychat/pangea/choreographer/controllers/alternative_translator.dart'; import 'package:fluffychat/pangea/choreographer/controllers/igc_controller.dart'; import 'package:fluffychat/pangea/choreographer/controllers/message_options.dart'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; import 'package:fluffychat/pangea/enum/assistance_state_enum.dart'; import 'package:fluffychat/pangea/enum/edit_type.dart'; import 'package:fluffychat/pangea/models/it_step.dart'; -import 'package:fluffychat/pangea/models/language_detection_model.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/models/space_model.dart'; import 'package:fluffychat/pangea/models/tokens_event_content_model.dart'; @@ -94,63 +93,67 @@ class Choreographer { } Future _sendWithIGC(BuildContext context) async { - if (igc.canSendMessage) { - final PangeaRepresentation? originalWritten = - choreoRecord.includedIT && itController.sourceText != null - ? PangeaRepresentation( - langCode: l1LangCode ?? LanguageKeys.unknownLanguage, - text: itController.sourceText!, - originalWritten: true, - originalSent: false, - ) - : null; + if (!igc.canSendMessage) { + igc.showFirstMatch(context); + return; + } - final PangeaRepresentation originalSent = PangeaRepresentation( - langCode: langCodeOfCurrentText ?? LanguageKeys.unknownLanguage, - text: currentText, - originalSent: true, - originalWritten: originalWritten == null, - ); - final ChoreoRecord? applicableChoreo = - isITandIGCEnabled && igc.igcTextData != null ? choreoRecord : null; + final PangeaRepresentation? originalWritten = + choreoRecord.includedIT && itController.sourceText != null + ? PangeaRepresentation( + langCode: l1LangCode ?? LanguageKeys.unknownLanguage, + text: itController.sourceText!, + originalWritten: true, + originalSent: false, + ) + : null; + //TODO - confirm that IT is indeed making sure the message is in the user's L1 - // if the message has not been processed to determine its language - // then run it through the language detection endpoint. If the detection - // confidence is high enough, use that language code as the message's language - // to save that pangea representation - if (applicableChoreo == null) { - final resp = await pangeaController.languageDetection.detectLanguage( + // if the message has not been processed to determine its language + // then run it through the language detection endpoint. If the detection + // confidence is high enough, use that language code as the message's language + // to save that pangea representation + // TODO - move this to somewhere such that the message can be cleared from the input field + // before the language detection is complete. Otherwise, user is going to be waiting + // in cases of slow internet or slow language detection + final String originalSentLangCode = langCodeOfCurrentText ?? + (await pangeaController.languageDetection.detectLanguage( currentText, pangeaController.languageController.userL2?.langCode, pangeaController.languageController.userL1?.langCode, - ); - final LanguageDetection? bestDetection = resp.bestDetection(); - if (bestDetection != null) { - originalSent.langCode = bestDetection.langCode; - } - } + )) + .bestDetection() + .langCode; - final UseType useType = useTypeCalculator(applicableChoreo); - debugPrint("use type in choreographer $useType"); + final PangeaRepresentation originalSent = PangeaRepresentation( + langCode: originalSentLangCode, + text: currentText, + originalSent: true, + originalWritten: originalWritten == null, + ); - chatController.send( - // PTODO - turn this back on in conjunction with saving tokens - // we need to save those tokens as well, in order for exchanges to work - // properly. in an exchange, the other user will want - // originalWritten: originalWritten, - originalSent: originalSent, - tokensSent: igc.igcTextData?.tokens != null - ? PangeaMessageTokens(tokens: igc.igcTextData!.tokens) - : null, - //TODO - save originalwritten tokens - choreo: applicableChoreo, - useType: useType, - ); + // TODO - why does both it and igc need to be enabled for choreo to be applicable? + final ChoreoRecord? applicableChoreo = + isITandIGCEnabled && igc.igcTextData != null ? choreoRecord : null; - clear(); - } else { - igc.showFirstMatch(context); - } + final UseType useType = useTypeCalculator(applicableChoreo); + debugPrint("use type in choreographer $useType"); + + chatController.send( + // PTODO - turn this back on in conjunction with saving tokens + // we need to save those tokens as well, in order for exchanges to work + // properly. in an exchange, the other user will want + // originalWritten: originalWritten, + originalSent: originalSent, + tokensSent: igc.igcTextData?.tokens != null + ? PangeaMessageTokens(tokens: igc.igcTextData!.tokens) + : null, + //TODO - save originalwritten tokens + choreo: applicableChoreo, + useType: useType, + ); + + clear(); } _resetDebounceTimer() { @@ -481,10 +484,17 @@ class Choreographer { bool get editTypeIsKeyboard => EditType.keyboard == _textController.editType; + /// If there is applicable igcTextData, return the detected langCode + /// Otherwise, if the IT controller is open, return the user's L2 langCode + /// This second piece assumes that IT is being used to translate into the user's L2 + /// and could be spotty. It's a bit of a hack, and should be tested more. String? get langCodeOfCurrentText { if (igc.detectedLangCode != null) return igc.detectedLangCode!; - if (itController.isOpen) return l2LangCode!; + // 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) + return l2LangCode!; return null; } diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart index 632db7e9b..a79f2d6fa 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -340,9 +340,6 @@ class ITController { bool get isLoading => choreographer.isFetching; - bool get correctChoicesSelected => - completedITSteps.every((ITStep step) => step.isCorrect); - String latestChoiceFeedback(BuildContext context) => completedITSteps.isNotEmpty ? completedITSteps.last.choiceFeedback(context) diff --git a/lib/pangea/choreographer/controllers/message_options.dart b/lib/pangea/choreographer/controllers/message_options.dart index 0e5e68461..96df5d921 100644 --- a/lib/pangea/choreographer/controllers/message_options.dart +++ b/lib/pangea/choreographer/controllers/message_options.dart @@ -1,7 +1,7 @@ import 'package:flutter/cupertino.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/utils/firebase_analytics.dart'; diff --git a/lib/pangea/constants/keys.dart b/lib/pangea/constants/keys.dart deleted file mode 100644 index 092b1b0a9..000000000 --- a/lib/pangea/constants/keys.dart +++ /dev/null @@ -1,4 +0,0 @@ -class PrefKey { - static const lastFetched = 'LAST_FETCHED'; - static const flags = 'flags'; -} diff --git a/lib/pangea/constants/language_constants.dart b/lib/pangea/constants/language_constants.dart new file mode 100644 index 000000000..73137a300 --- /dev/null +++ b/lib/pangea/constants/language_constants.dart @@ -0,0 +1,24 @@ +import 'package:fluffychat/pangea/models/language_detection_model.dart'; + +class LanguageKeys { + static const unknownLanguage = "unk"; + static const mixedLanguage = "mixed"; + static const defaultLanguage = "en"; + static const multiLanguage = "multi"; +} + +class LanguageLevelType { + static List get allInts => [0, 1, 2, 3, 4, 5, 6]; +} + +class PrefKey { + static const lastFetched = 'p_lang_lastfetched'; + static const flags = 'p_lang_flag'; +} + +final LanguageDetection unknownLanguageDetection = LanguageDetection( + langCode: LanguageKeys.unknownLanguage, + confidence: 0.5, +); + +const double languageDetectionConfidenceThreshold = 0.95; diff --git a/lib/pangea/constants/language_keys.dart b/lib/pangea/constants/language_keys.dart deleted file mode 100644 index cfe0c96c0..000000000 --- a/lib/pangea/constants/language_keys.dart +++ /dev/null @@ -1,6 +0,0 @@ -class LanguageKeys { - static const unknownLanguage = "unk"; - static const mixedLanguage = "mixed"; - static const defaultLanguage = "en"; - static const multiLanguage = "multi"; -} diff --git a/lib/pangea/constants/language_level_type.dart b/lib/pangea/constants/language_level_type.dart deleted file mode 100644 index 49136ca77..000000000 --- a/lib/pangea/constants/language_level_type.dart +++ /dev/null @@ -1,3 +0,0 @@ -class LanguageLevelType { - static List get allInts => [0, 1, 2, 3, 4, 5, 6]; -} diff --git a/lib/pangea/constants/language_list_keys.dart b/lib/pangea/constants/language_list_keys.dart deleted file mode 100644 index 7fff924a9..000000000 --- a/lib/pangea/constants/language_list_keys.dart +++ /dev/null @@ -1,4 +0,0 @@ -class PrefKey { - static const lastFetched = 'p_lang_lastfetched'; - static const flags = 'p_lang_flag'; -} diff --git a/lib/pangea/controllers/language_controller.dart b/lib/pangea/controllers/language_controller.dart index c906e6b4e..b1bc02380 100644 --- a/lib/pangea/controllers/language_controller.dart +++ b/lib/pangea/controllers/language_controller.dart @@ -1,6 +1,6 @@ import 'dart:developer'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; diff --git a/lib/pangea/controllers/language_detection_controller.dart b/lib/pangea/controllers/language_detection_controller.dart index 4ddfabc88..08f38ea2c 100644 --- a/lib/pangea/controllers/language_detection_controller.dart +++ b/lib/pangea/controllers/language_detection_controller.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/models/language_detection_model.dart'; import 'package:fluffychat/pangea/network/urls.dart'; @@ -75,19 +76,16 @@ class LanguageDetectionResponse { }; } - LanguageDetection? get _bestDetection { + LanguageDetection get _bestDetection { detections.sort((a, b) => b.confidence.compareTo(a.confidence)); - return detections.isNotEmpty ? detections.first : null; + return detections.firstOrNull ?? unknownLanguageDetection; } - final double _confidenceThreshold = 0.95; - - LanguageDetection? bestDetection({double? threshold}) { - threshold ??= _confidenceThreshold; - return (_bestDetection?.confidence ?? 0) >= _confidenceThreshold - ? _bestDetection! - : null; - } + LanguageDetection bestDetection({double? threshold}) => + _bestDetection.confidence >= + (threshold ?? languageDetectionConfidenceThreshold) + ? _bestDetection + : unknownLanguageDetection; } class _LanguageDetectionCacheItem { diff --git a/lib/pangea/controllers/language_list_controller.dart b/lib/pangea/controllers/language_list_controller.dart index 59c3cce88..31e4513fa 100644 --- a/lib/pangea/controllers/language_list_controller.dart +++ b/lib/pangea/controllers/language_list_controller.dart @@ -1,13 +1,12 @@ import 'dart:async'; import 'dart:developer'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; import 'package:fluffychat/pangea/repo/language_repo.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; -import '../constants/language_list_keys.dart'; import '../utils/shared_prefs.dart'; class PangeaLanguage { diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 614fcf9db..99fdb14c7 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:developer'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/base_controller.dart'; diff --git a/lib/pangea/controllers/user_controller.dart b/lib/pangea/controllers/user_controller.dart index c7a35b3e8..390f17b9d 100644 --- a/lib/pangea/controllers/user_controller.dart +++ b/lib/pangea/controllers/user_controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:collection/collection.dart'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/controllers/base_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; diff --git a/lib/pangea/controllers/word_net_controller.dart b/lib/pangea/controllers/word_net_controller.dart index ce8324ebe..a67941465 100644 --- a/lib/pangea/controllers/word_net_controller.dart +++ b/lib/pangea/controllers/word_net_controller.dart @@ -1,7 +1,7 @@ import 'package:collection/collection.dart'; import 'package:http/http.dart' as http; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/repo/word_repo.dart'; import '../models/word_data_model.dart'; import 'base_controller.dart'; diff --git a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart index b8dae7ab3..298a519ad 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -4,7 +4,7 @@ import 'dart:developer'; import 'package:adaptive_dialog/adaptive_dialog.dart'; import 'package:collection/collection.dart'; import 'package:fluffychat/pangea/constants/class_default_values.dart'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/constants/pangea_room_types.dart'; import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 951b77dfc..4ead9982c 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -22,7 +22,7 @@ import 'package:matrix/matrix.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import '../../widgets/matrix.dart'; -import '../constants/language_keys.dart'; +import '../constants/language_constants.dart'; import '../constants/pangea_event_types.dart'; import '../enum/use_type.dart'; import '../utils/error_handler.dart'; diff --git a/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart b/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart index e774dc4f2..e6e45b756 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_representation_event.dart @@ -12,7 +12,7 @@ import 'package:matrix/src/utils/markdown.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import '../../widgets/matrix.dart'; -import '../constants/language_keys.dart'; +import '../constants/language_constants.dart'; import '../constants/pangea_event_types.dart'; import '../models/choreo_record.dart'; import '../models/representation_content_model.dart'; diff --git a/lib/pangea/models/language_model.dart b/lib/pangea/models/language_model.dart index ae960b212..be3cc71ba 100644 --- a/lib/pangea/models/language_model.dart +++ b/lib/pangea/models/language_model.dart @@ -1,6 +1,6 @@ import 'dart:developer'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; diff --git a/lib/pangea/models/space_model.dart b/lib/pangea/models/space_model.dart index f17507fa8..946da6dc7 100644 --- a/lib/pangea/models/space_model.dart +++ b/lib/pangea/models/space_model.dart @@ -7,7 +7,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; import '../constants/class_default_values.dart'; -import '../constants/language_keys.dart'; +import '../constants/language_constants.dart'; import '../constants/pangea_event_types.dart'; import 'language_model.dart'; diff --git a/lib/pangea/models/user_model.dart b/lib/pangea/models/user_model.dart index f8e929a13..10b7ae747 100644 --- a/lib/pangea/models/user_model.dart +++ b/lib/pangea/models/user_model.dart @@ -9,7 +9,7 @@ import 'package:fluffychat/pangea/utils/instructions.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; -import '../constants/language_keys.dart'; +import '../constants/language_constants.dart'; import 'language_model.dart'; PUserModel pUserModelFromJson(String str) => diff --git a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart index d6bc9d766..a345243ff 100644 --- a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart +++ b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:developer'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart'; import 'package:fluffychat/pangea/extensions/client_extension/client_extension.dart'; diff --git a/lib/pangea/utils/get_chat_list_item_subtitle.dart b/lib/pangea/utils/get_chat_list_item_subtitle.dart index 5adc0040c..bd8d064f5 100644 --- a/lib/pangea/utils/get_chat_list_item_subtitle.dart +++ b/lib/pangea/utils/get_chat_list_item_subtitle.dart @@ -1,5 +1,5 @@ import 'package:collection/collection.dart'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; diff --git a/lib/pangea/widgets/igc/word_data_card.dart b/lib/pangea/widgets/igc/word_data_card.dart index 5a785c795..1d789424d 100644 --- a/lib/pangea/widgets/igc/word_data_card.dart +++ b/lib/pangea/widgets/igc/word_data_card.dart @@ -1,6 +1,6 @@ import 'dart:developer'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/controllers/contextual_definition_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; diff --git a/lib/pangea/widgets/space/language_level_dropdown.dart b/lib/pangea/widgets/space/language_level_dropdown.dart index 3b2678e29..aeb1cfd36 100644 --- a/lib/pangea/widgets/space/language_level_dropdown.dart +++ b/lib/pangea/widgets/space/language_level_dropdown.dart @@ -1,4 +1,4 @@ -import 'package:fluffychat/pangea/constants/language_level_type.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/utils/language_level_copy.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; diff --git a/lib/pangea/widgets/user_settings/p_language_dialog.dart b/lib/pangea/widgets/user_settings/p_language_dialog.dart index 51082a7e6..8b7ae33b5 100644 --- a/lib/pangea/widgets/user_settings/p_language_dialog.dart +++ b/lib/pangea/widgets/user_settings/p_language_dialog.dart @@ -1,6 +1,6 @@ import 'dart:developer'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/models/language_model.dart'; diff --git a/lib/utils/background_push.dart b/lib/utils/background_push.dart index 00ebd3cb5..093b78f81 100644 --- a/lib/utils/background_push.dart +++ b/lib/utils/background_push.dart @@ -24,7 +24,7 @@ import 'dart:io'; import 'package:fcm_shared_isolate/fcm_shared_isolate.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:fluffychat/pangea/constants/language_keys.dart'; +import 'package:fluffychat/pangea/constants/language_constants.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:fluffychat/utils/push_helper.dart'; import 'package:fluffychat/widgets/fluffy_chat_app.dart'; From 8ceb7851e5923ec7455f2930507d53065e071e05 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Fri, 28 Jun 2024 11:36:10 -0400 Subject: [PATCH 07/26] refactoring of my analytics controller and related flows --- assets/l10n/intl_en.arb | 2 +- assets/l10n/intl_es.arb | 2 +- lib/pages/chat_list/chat_list.dart | 2 +- .../controllers/my_analytics_controller.dart | 304 +++++++++--------- .../client_extension/client_extension.dart | 4 +- .../client_extension/space_extension.dart | 12 + .../events_extension.dart | 34 +- .../pangea_room_extension.dart | 1 + .../room_analytics_extension.dart | 34 +- .../models/analytics/analytics_event.dart | 30 -- .../models/analytics/constructs_event.dart | 15 - .../analytics/summary_analytics_event.dart | 14 - .../practice_activity_record_model.dart | 22 +- .../student_analytics/student_analytics.dart | 35 +- 14 files changed, 241 insertions(+), 270 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index f0aba0219..986cb9cb0 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -3109,7 +3109,7 @@ "prettyGood": "Pretty good! Here's what I would have said.", "letMeThink": "Hmm, let's see how you did!", "clickMessageTitle": "Need help?", - "clickMessageBody": "Click messages to access definitions, translations, and audio!", + "clickMessageBody": "Click a message for language help! Click and hold to react 😀.", "understandingMessagesTitle": "Definitions and translations!", "understandingMessagesBody": "Click underlined words for definitions. Translate with message options (upper right).", "allDone": "All done!", diff --git a/assets/l10n/intl_es.arb b/assets/l10n/intl_es.arb index dc328294a..e771c86a3 100644 --- a/assets/l10n/intl_es.arb +++ b/assets/l10n/intl_es.arb @@ -4512,7 +4512,7 @@ "definitions": "definiciones", "subscribedToUnlockTools": "Suscríbase para desbloquear herramientas lingüísticas, como", "clickMessageTitle": "¿Necesitas ayuda?", - "clickMessageBody": "Haga clic en los mensajes para acceder a las definiciones, traducciones y audio.", + "clickMessageBody": "¡Lame un mensaje para obtener ayuda con el idioma! Haz clic y mantén presionado para reaccionar 😀", "more": "Más", "translationTooltip": "Traducir", "audioTooltip": "Reproducir audio", diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 569eba880..89f31fd68 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -917,7 +917,7 @@ class ChatListController extends State if (mounted) { GoogleAnalytics.analyticsUserUpdate(client.userID); await pangeaController.subscriptionController.initialize(); - await pangeaController.myAnalytics.addEventsListener(); + await pangeaController.myAnalytics.initialize(); pangeaController.afterSyncAndFirstLoginInitialization(context); await pangeaController.inviteBotToExistingSpaces(); await pangeaController.setPangeaPushRules(); diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 99fdb14c7..82b7af3c7 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -1,16 +1,17 @@ import 'dart:async'; import 'dart:developer'; +<<<<<<< Updated upstream import 'package:fluffychat/pangea/constants/language_constants.dart'; +======= +>>>>>>> Stashed changes import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; -import 'package:fluffychat/pangea/controllers/base_controller.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; -import 'package:fluffychat/pangea/models/analytics/constructs_event.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; -import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart'; import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; @@ -18,11 +19,18 @@ import 'package:matrix/matrix.dart'; import '../extensions/client_extension/client_extension.dart'; import '../extensions/pangea_room_extension/pangea_room_extension.dart'; -// controls the sending of analytics events -class MyAnalyticsController extends BaseController { +/// handles the processing of analytics for +/// 1) messages sent by the user and +/// 2) constructs used by the user, both in sending messages and doing practice activities +class MyAnalyticsController { late PangeaController _pangeaController; Timer? _updateTimer; + + /// the max number of messages that will be cached before + /// an automatic update is triggered final int _maxMessagesCached = 10; + + /// the number of minutes before an automatic update is triggered final int _minutesBeforeUpdate = 5; /// the time since the last update that will trigger an automatic update @@ -33,41 +41,50 @@ class MyAnalyticsController extends BaseController { } /// adds the listener that handles when to run automatic updates - /// to analytics - either after a certain number of messages sent/ + /// to analytics - either after a certain number of messages sent /// received or after a certain amount of time [_timeSinceUpdate] without an update - Future addEventsListener() async { - final Client client = _pangeaController.matrixState.client; + Future initialize() async { + final lastUpdated = await _refreshAnalyticsIfOutdated(); - // if analytics haven't been updated in the last day, update them - DateTime? lastUpdated = await _pangeaController.analytics - .myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics); - final DateTime yesterday = DateTime.now().subtract(_timeSinceUpdate); - if (lastUpdated?.isBefore(yesterday) ?? true) { - debugPrint("analytics out-of-date, updating"); - await updateAnalytics(); - lastUpdated = await _pangeaController.analytics - .myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics); - } - - client.onSync.stream + // listen for new messages and updateAnalytics timer + // we are doing this in an attempt to update analytics when activitiy is low + // both in messages sent by this client and other clients that you're connected with + // doesn't account for messages sent by other clients that you're not connected with + _client.onSync.stream .where((SyncUpdate update) => update.rooms?.join != null) .listen((update) { updateAnalyticsTimer(update, lastUpdated); }); } - /// given an update from sync stream, check if the update contains + /// If analytics haven't been updated in the last day, update them + Future _refreshAnalyticsIfOutdated() async { + DateTime? lastUpdated = await _pangeaController.analytics + .myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics); + final DateTime yesterday = DateTime.now().subtract(_timeSinceUpdate); + + if (lastUpdated?.isBefore(yesterday) ?? true) { + debugPrint("analytics out-of-date, updating"); + await updateAnalytics(); + lastUpdated = await _pangeaController.analytics + .myAnalyticsLastUpdated(PangeaEventTypes.summaryAnalytics); + } + return lastUpdated; + } + + Client get _client => _pangeaController.matrixState.client; + + /// Given an update from sync stream, check if the update contains /// messages for which analytics will be saved. If so, reset the timer /// and add the event ID to the cache of un-added event IDs void updateAnalyticsTimer(SyncUpdate update, DateTime? lastUpdated) { for (final entry in update.rooms!.join!.entries) { - final Room room = - _pangeaController.matrixState.client.getRoomById(entry.key)!; + final Room room = _client.getRoomById(entry.key)!; // get the new events in this sync that are messages final List? events = entry.value.timeline?.events ?.map((event) => Event.fromMatrixEvent(event, room)) - .where((event) => eventHasAnalytics(event, lastUpdated)) + .where((event) => hasUserAnalyticsToCache(event, lastUpdated)) .toList(); // add their event IDs to the cache of un-added event IDs @@ -87,8 +104,9 @@ class MyAnalyticsController extends BaseController { } // checks if event from sync update is a message that should have analytics - bool eventHasAnalytics(Event event, DateTime? lastUpdated) { - return (lastUpdated == null || event.originServerTs.isAfter(lastUpdated)) && + bool hasUserAnalyticsToCache(Event event, DateTime? lastUpdated) { + return event.senderId == _client.userID && + (lastUpdated == null || event.originServerTs.isAfter(lastUpdated)) && event.type == EventTypes.Message && event.messageType == MessageTypes.Text && !(event.eventId.contains("web") && @@ -176,21 +194,18 @@ class MyAnalyticsController extends BaseController { } } + String? get userL2 => _pangeaController.languageController.activeL2Code(); + // top level analytics sending function. Send analytics // for each type of analytics event // to each of the applicable analytics rooms Future _updateAnalytics() async { - // if the user's l2 is not sent, don't send analytics - final String? userL2 = _pangeaController.languageController.activeL2Code(); - if (userL2 == null) { + // if missing important info, don't send analytics + if (userL2 == null || _client.userID == null) { + debugger(when: kDebugMode); return; } - // fetch a list of all the chats that the user is studying - // and a list of all the spaces in which the user is studying - await setStudentChats(); - await setStudentSpaces(); - // get the last updated time for each analytics room // and the least recent update, which will be used to determine // how far to go back in the chat history to get messages @@ -217,151 +232,128 @@ class MyAnalyticsController extends BaseController { lastUpdates.isNotEmpty ? lastUpdates.first : null; } - // for each chat the user is studying in, get all the messages - // since the least recent update analytics update, and sort them - // by their langCodes - final Map> langCodeToMsgs = - await getLangCodesToMsgs( - userL2, + final List chats = await _client.chatsImAStudentIn; + + final List recentMsgs = + await _getMessagesWithUnsavedAnalytics( l2AnalyticsLastUpdated, + chats, ); - final List langCodes = langCodeToMsgs.keys.toList(); - for (final String langCode in langCodes) { - // for each of the langs that the user has sent message in, get - // the corresponding analytics room (or create it) - final Room analyticsRoom = await _pangeaController.matrixState.client - .getMyAnalyticsRoom(langCode); + final List recentActivities = + await getRecentActivities(userL2!, l2AnalyticsLastUpdated, chats); - // if there is no analytics room for this langCode, then user hadn't sent - // message in this language at the time of the last analytics update - // so fallback to the least recent update time - final DateTime? lastUpdated = - lastUpdatedMap[analyticsRoom.id] ?? l2AnalyticsLastUpdated; + // FOR DISCUSSION: + // we want to make sure we save something for every message send + // however, we're currently saving analytics for messages not in the userL2 + // based on bad language detection results. maybe it would be better to + // save the analytics for these messages in the userL2 analytics room, but + // with useType of unknown - // get the corresponding list of recent messages for this langCode - final List recentMsgs = - langCodeToMsgs[langCode] ?? []; + final Room analyticsRoom = await _client.getMyAnalyticsRoom(userL2!); - // finally, send the analytics events to the analytics room - await sendAnalyticsEvents( - analyticsRoom, - recentMsgs, - lastUpdated, + // if there is no analytics room for this langCode, then user hadn't sent + // message in this language at the time of the last analytics update + // so fallback to the least recent update time + final DateTime? lastUpdated = + lastUpdatedMap[analyticsRoom.id] ?? l2AnalyticsLastUpdated; + + // final String msgLangCode = (msg.originalSent?.langCode != null && + // msg.originalSent?.langCode != LanguageKeys.unknownLanguage) + // ? msg.originalSent!.langCode + // : userL2; + + // finally, send the analytics events to the analytics room + await _sendAnalyticsEvents( + analyticsRoom, + recentMsgs, + lastUpdated, + recentActivities, + ); + } + + Future> getRecentActivities( + String userL2, + DateTime? lastUpdated, + List chats, + ) async { + final List>> recentActivityFutures = []; + for (final Room chat in chats) { + recentActivityFutures.add( + chat.getEventsBySender( + type: PangeaEventTypes.activityRecord, + sender: _client.userID!, + since: lastUpdated, + ), ); } + final List> recentActivityLists = + await Future.wait(recentActivityFutures); + + return recentActivityLists + .expand((e) => e) + .map((e) => ActivityRecordResponse.fromJson(e.content)) + .toList(); } - Future>> getLangCodesToMsgs( - String userL2, + /// Returns the new messages that have not yet been saved to analytics. + /// The keys in the map correspond to different categories or groups of messages, + /// while the values are lists of [PangeaMessageEvent] objects belonging to each category. + Future> _getMessagesWithUnsavedAnalytics( DateTime? since, + List chats, ) async { - // get a map of langCodes to messages for each chat the user is studying in - final Map> langCodeToMsgs = {}; - for (final Room chat in _studentChats) { - List? recentMsgs; - try { - recentMsgs = await chat.myMessageEventsInChat( + // get the recent messages for each chat + final List>> futures = []; + for (final Room chat in chats) { + futures.add( + chat.myMessageEventsInChat( since: since, - ); - } catch (err) { - debugPrint("failed to fetch messages for chat ${chat.id}"); - continue; - } - - // sort those messages by their langCode - // langCode is hopefully based on the original sent rep, but if that - // is null or unk, it will be based on the user's current l2 - for (final msg in recentMsgs) { - final String msgLangCode = (msg.originalSent?.langCode != null && - msg.originalSent?.langCode != LanguageKeys.unknownLanguage) - ? msg.originalSent!.langCode - : userL2; - langCodeToMsgs[msgLangCode] ??= []; - langCodeToMsgs[msgLangCode]!.add(msg); - } + ), + ); } - return langCodeToMsgs; + final List> recentMsgLists = + await Future.wait(futures); + + // flatten the list of lists of messages + return recentMsgLists.expand((e) => e).toList(); } - Future sendAnalyticsEvents( + Future _sendAnalyticsEvents( Room analyticsRoom, List recentMsgs, DateTime? lastUpdated, + List recentActivities, ) async { - // remove messages that were sent before the last update - if (recentMsgs.isEmpty) return; - if (lastUpdated != null) { - recentMsgs.removeWhere( - (msg) => msg.event.originServerTs.isBefore(lastUpdated), - ); + final List constructContent = []; + + if (recentMsgs.isNotEmpty) { + // remove messages that were sent before the last update + + // format the analytics data + final List summaryContent = + SummaryAnalyticsModel.formatSummaryContent(recentMsgs); + // if there's new content to be sent, or if lastUpdated hasn't been + // set yet for this room, send the analytics events + if (summaryContent.isNotEmpty || lastUpdated == null) { + await analyticsRoom.sendSummaryAnalyticsEvent( + summaryContent, + ); + } + + constructContent + .addAll(ConstructAnalyticsModel.formatConstructsContent(recentMsgs)); } - // format the analytics data - final List summaryContent = - SummaryAnalyticsModel.formatSummaryContent(recentMsgs); - final List constructContent = - ConstructAnalyticsModel.formatConstructsContent(recentMsgs); - - // if there's new content to be sent, or if lastUpdated hasn't been - // set yet for this room, send the analytics events - if (summaryContent.isNotEmpty || lastUpdated == null) { - await SummaryAnalyticsEvent.sendSummaryAnalyticsEvent( - analyticsRoom, - summaryContent, - ); + if (recentActivities.isNotEmpty) { + // TODO - Concert recentActivities into list of constructUse objects. + // First, We need to get related practiceActivityEvent from timeline in order to get its related constructs. Alternatively we + // could search for completed practice activities and see which have been completed by the user. + // It's not clear which is the best approach at the moment and we should consider both. } - if (constructContent.isNotEmpty) { - await ConstructAnalyticsEvent.sendConstructsEvent( - analyticsRoom, - constructContent, - ); - } - } - - List _studentChats = []; - - Future setStudentChats() async { - final List teacherRoomIds = - await _pangeaController.matrixState.client.teacherRoomIds; - _studentChats = _pangeaController.matrixState.client.rooms - .where( - (r) => - !r.isSpace && - !r.isAnalyticsRoom && - !teacherRoomIds.contains(r.id), - ) - .toList(); - setState(data: _studentChats); - } - - List get studentChats { - try { - if (_studentChats.isNotEmpty) return _studentChats; - setStudentChats(); - return _studentChats; - } catch (err) { - debugger(when: kDebugMode); - return []; - } - } - - List _studentSpaces = []; - - Future setStudentSpaces() async { - _studentSpaces = - await _pangeaController.matrixState.client.spacesImStudyingIn; - } - - List get studentSpaces { - try { - if (_studentSpaces.isNotEmpty) return _studentSpaces; - setStudentSpaces(); - return _studentSpaces; - } catch (err) { - debugger(when: kDebugMode); - return []; - } + await analyticsRoom.sendConstructsEvent( + constructContent, + ); } } diff --git a/lib/pangea/extensions/client_extension/client_extension.dart b/lib/pangea/extensions/client_extension/client_extension.dart index 779f8ee0a..3dda99237 100644 --- a/lib/pangea/extensions/client_extension/client_extension.dart +++ b/lib/pangea/extensions/client_extension/client_extension.dart @@ -51,7 +51,9 @@ extension PangeaClient on Client { Future> get spacesImTeaching async => await _spacesImTeaching; - Future> get spacesImStudyingIn async => await _spacesImStudyingIn; + Future> get chatsImAStudentIn async => await _chatsImAStudentIn; + + Future> get spaceImAStudentIn async => await _spacesImStudyingIn; List get spacesImIn => _spacesImIn; diff --git a/lib/pangea/extensions/client_extension/space_extension.dart b/lib/pangea/extensions/client_extension/space_extension.dart index 3bef46e45..0adc70469 100644 --- a/lib/pangea/extensions/client_extension/space_extension.dart +++ b/lib/pangea/extensions/client_extension/space_extension.dart @@ -19,6 +19,18 @@ extension SpaceClientExtension on Client { return spaces; } + Future> get _chatsImAStudentIn async { + final List nowteacherRoomIds = await teacherRoomIds; + return rooms + .where( + (r) => + !r.isSpace && + !r.isAnalyticsRoom && + !nowteacherRoomIds.contains(r.id), + ) + .toList(); + } + Future> get _spacesImStudyingIn async { final List joinedSpaces = rooms .where( diff --git a/lib/pangea/extensions/pangea_room_extension/events_extension.dart b/lib/pangea/extensions/pangea_room_extension/events_extension.dart index 2d67db5b2..6cdde1ce2 100644 --- a/lib/pangea/extensions/pangea_room_extension/events_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/events_extension.dart @@ -429,21 +429,27 @@ extension EventsRoomExtension on Room { Future> myMessageEventsInChat({ DateTime? since, }) async { - final List msgEvents = await getEventsBySender( - type: EventTypes.Message, - sender: client.userID!, - since: since, - ); - final Timeline timeline = await getTimeline(); - return msgEvents - .where((event) => (event.content['msgtype'] == MessageTypes.Text)) - .map((event) { - return PangeaMessageEvent( - event: event, - timeline: timeline, - ownMessage: true, + try { + final List msgEvents = await getEventsBySender( + type: EventTypes.Message, + sender: client.userID!, + since: since, ); - }).toList(); + final Timeline timeline = await getTimeline(); + return msgEvents + .where((event) => (event.content['msgtype'] == MessageTypes.Text)) + .map((event) { + return PangeaMessageEvent( + event: event, + timeline: timeline, + ownMessage: true, + ); + }).toList(); + } catch (err, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: err, s: s); + return []; + } } // fetch event of a certain type by a certain sender diff --git a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart index 298a519ad..78ecb9cc0 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -11,6 +11,7 @@ import 'package:fluffychat/pangea/controllers/language_list_controller.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/analytics/analytics_event.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_event.dart'; +import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart'; import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart'; import 'package:fluffychat/pangea/models/bot_options_model.dart'; diff --git a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart index e3d00f8a8..04e168612 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -99,7 +99,7 @@ extension AnalyticsRoomExtension on Room { return; } - for (final Room space in (await client.spacesImStudyingIn)) { + for (final Room space in (await client.spaceImAStudentIn)) { if (space.spaceChildren.any((sc) => sc.roomId == id)) continue; await space.addAnalyticsRoomToSpace(this); } @@ -175,7 +175,7 @@ extension AnalyticsRoomExtension on Room { return; } - for (final Room space in (await client.spacesImStudyingIn)) { + for (final Room space in (await client.spaceImAStudentIn)) { await space.inviteSpaceTeachersToAnalyticsRoom(this); } } @@ -249,4 +249,34 @@ extension AnalyticsRoomExtension on Room { return creationContent?.tryGet(ModelKey.langCode) == langCode || creationContent?.tryGet(ModelKey.oldLangCode) == langCode; } + + Future sendSummaryAnalyticsEvent( + List records, + ) async { + if (records.isEmpty) return null; + + final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel( + messages: records, + ); + final String? eventId = await sendEvent( + analyticsModel.toJson(), + type: PangeaEventTypes.summaryAnalytics, + ); + return eventId; + } + + Future sendConstructsEvent( + List uses, + ) async { + if (uses.isEmpty) return null; + final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel( + uses: uses, + ); + + final String? eventId = await sendEvent( + constructsModel.toJson(), + type: PangeaEventTypes.construct, + ); + return eventId; + } } diff --git a/lib/pangea/models/analytics/analytics_event.dart b/lib/pangea/models/analytics/analytics_event.dart index 2453e62ef..7010d3591 100644 --- a/lib/pangea/models/analytics/analytics_event.dart +++ b/lib/pangea/models/analytics/analytics_event.dart @@ -1,8 +1,6 @@ import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/models/analytics/analytics_model.dart'; -import 'package:fluffychat/pangea/models/analytics/constructs_event.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; -import 'package:fluffychat/pangea/models/analytics/summary_analytics_event.dart'; import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart'; import 'package:matrix/matrix.dart'; @@ -28,32 +26,4 @@ abstract class AnalyticsEvent { } return contentCache!; } - - static List analyticsEventTypes = [ - PangeaEventTypes.summaryAnalytics, - PangeaEventTypes.construct, - ]; - - static Future sendEvent( - Room analyticsRoom, - String type, - List analyticsContent, - ) async { - String? eventId; - switch (type) { - case PangeaEventTypes.summaryAnalytics: - eventId = await SummaryAnalyticsEvent.sendSummaryAnalyticsEvent( - analyticsRoom, - analyticsContent.cast(), - ); - break; - case PangeaEventTypes.construct: - eventId = await ConstructAnalyticsEvent.sendConstructsEvent( - analyticsRoom, - analyticsContent.cast(), - ); - break; - } - return eventId; - } } diff --git a/lib/pangea/models/analytics/constructs_event.dart b/lib/pangea/models/analytics/constructs_event.dart index c2930faba..481051b1c 100644 --- a/lib/pangea/models/analytics/constructs_event.dart +++ b/lib/pangea/models/analytics/constructs_event.dart @@ -18,19 +18,4 @@ class ConstructAnalyticsEvent extends AnalyticsEvent { contentCache ??= ConstructAnalyticsModel.fromJson(event.content); return contentCache as ConstructAnalyticsModel; } - - static Future sendConstructsEvent( - Room analyticsRoom, - List uses, - ) async { - final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel( - uses: uses, - ); - - final String? eventId = await analyticsRoom.sendEvent( - constructsModel.toJson(), - type: PangeaEventTypes.construct, - ); - return eventId; - } } diff --git a/lib/pangea/models/analytics/summary_analytics_event.dart b/lib/pangea/models/analytics/summary_analytics_event.dart index e7034eaa4..a764d5597 100644 --- a/lib/pangea/models/analytics/summary_analytics_event.dart +++ b/lib/pangea/models/analytics/summary_analytics_event.dart @@ -18,18 +18,4 @@ class SummaryAnalyticsEvent extends AnalyticsEvent { contentCache ??= SummaryAnalyticsModel.fromJson(event.content); return contentCache as SummaryAnalyticsModel; } - - static Future sendSummaryAnalyticsEvent( - Room analyticsRoom, - List records, - ) async { - final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel( - messages: records, - ); - final String? eventId = await analyticsRoom.sendEvent( - analyticsModel.toJson(), - type: PangeaEventTypes.summaryAnalytics, - ); - return eventId; - } } diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart index 3fe3e859d..e5c3a1c18 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart @@ -7,14 +7,14 @@ import 'dart:typed_data'; class PracticeActivityRecordModel { final String? question; - late List responses; + late List responses; PracticeActivityRecordModel({ required this.question, - List? responses, + List? responses, }) { if (responses == null) { - this.responses = List.empty(growable: true); + this.responses = List.empty(growable: true); } else { this.responses = responses; } @@ -26,7 +26,9 @@ class PracticeActivityRecordModel { return PracticeActivityRecordModel( question: json['question'] as String, responses: (json['responses'] as List) - .map((e) => ActivityResponse.fromJson(e as Map)) + .map( + (e) => ActivityRecordResponse.fromJson(e as Map), + ) .toList(), ); } @@ -55,7 +57,7 @@ class PracticeActivityRecordModel { }) { try { responses.add( - ActivityResponse( + ActivityRecordResponse( text: text, audioBytes: audioBytes, imageBytes: imageBytes, @@ -84,7 +86,7 @@ class PracticeActivityRecordModel { int get hashCode => question.hashCode ^ responses.hashCode; } -class ActivityResponse { +class ActivityRecordResponse { // the user's response // has nullable string, nullable audio bytes, nullable image bytes, and timestamp final String? text; @@ -92,15 +94,15 @@ class ActivityResponse { final Uint8List? imageBytes; final DateTime timestamp; - ActivityResponse({ + ActivityRecordResponse({ this.text, this.audioBytes, this.imageBytes, required this.timestamp, }); - factory ActivityResponse.fromJson(Map json) { - return ActivityResponse( + factory ActivityRecordResponse.fromJson(Map json) { + return ActivityRecordResponse( text: json['text'] as String?, audioBytes: json['audio'] as Uint8List?, imageBytes: json['image'] as Uint8List?, @@ -121,7 +123,7 @@ class ActivityResponse { bool operator ==(Object other) { if (identical(this, other)) return true; - return other is ActivityResponse && + return other is ActivityRecordResponse && other.text == text && other.audioBytes == audioBytes && other.imageBytes == imageBytes && diff --git a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart index a345243ff..5c694b6ca 100644 --- a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart +++ b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:developer'; import 'package:fluffychat/pangea/constants/language_constants.dart'; @@ -29,49 +28,35 @@ class StudentAnalyticsPage extends StatefulWidget { class StudentAnalyticsController extends State { final PangeaController _pangeaController = MatrixState.pangeaController; AnalyticsSelected? selected; - StreamSubscription? stateSub; @override void initState() { super.initState(); - - final listFutures = [ - _pangeaController.myAnalytics.setStudentChats(), - _pangeaController.myAnalytics.setStudentSpaces(), - ]; - Future.wait(listFutures).then((_) => setState(() {})); - - stateSub = _pangeaController.myAnalytics.stateStream.listen((_) { - setState(() {}); - }); } @override void dispose() { - stateSub?.cancel(); super.dispose(); } + List _chats = []; List get chats { - if (_pangeaController.myAnalytics.studentChats.isEmpty) { - _pangeaController.myAnalytics.setStudentChats().then((_) { - if (_pangeaController.myAnalytics.studentChats.isNotEmpty) { - setState(() {}); - } + if (_chats.isEmpty) { + _pangeaController.matrixState.client.chatsImAStudentIn.then((result) { + setState(() => _chats = result); }); } - return _pangeaController.myAnalytics.studentChats; + return _chats; } + List _spaces = []; List get spaces { - if (_pangeaController.myAnalytics.studentSpaces.isEmpty) { - _pangeaController.myAnalytics.setStudentSpaces().then((_) { - if (_pangeaController.myAnalytics.studentSpaces.isNotEmpty) { - setState(() {}); - } + if (_spaces.isEmpty) { + _pangeaController.matrixState.client.spaceImAStudentIn.then((result) { + setState(() => _spaces = result); }); } - return _pangeaController.myAnalytics.studentSpaces; + return _spaces; } String? get userId { From 5c8666b3e27b3391353f7781493868eeaae85501 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Fri, 28 Jun 2024 15:30:57 -0400 Subject: [PATCH 08/26] rough draft complete --- .../message_analytics_controller.dart | 12 +- .../controllers/my_analytics_controller.dart | 208 +++++++----------- ...actice_activity_generation_controller.dart | 2 +- lib/pangea/enum/construct_type_enum.dart | 16 +- lib/pangea/enum/construct_use_type_enum.dart | 93 ++++++++ .../events_extension.dart | 26 --- .../pangea_message_event.dart | 151 ++++++++++++- .../practice_acitivity_record_event.dart | 24 -- .../practice_activity_event.dart | 4 +- .../practice_activity_record_event.dart | 89 ++++++++ .../models/analytics/analytics_model.dart | 6 +- .../models/analytics/constructs_model.dart | 127 +---------- lib/pangea/models/choreo_record.dart | 134 ----------- lib/pangea/models/headwords.dart | 24 +- .../practice_activity_model.dart | 4 +- .../practice_activity_record_model.dart | 21 +- .../pages/analytics/base_analytics_view.dart | 2 +- .../pages/analytics/construct_list.dart | 4 +- .../practice_activity_content.dart | 11 +- 19 files changed, 484 insertions(+), 474 deletions(-) create mode 100644 lib/pangea/enum/construct_use_type_enum.dart delete mode 100644 lib/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart create mode 100644 lib/pangea/matrix_event_wrappers/practice_activity_record_event.dart diff --git a/lib/pangea/controllers/message_analytics_controller.dart b/lib/pangea/controllers/message_analytics_controller.dart index 1475c0e6e..3739b2596 100644 --- a/lib/pangea/controllers/message_analytics_controller.dart +++ b/lib/pangea/controllers/message_analytics_controller.dart @@ -641,7 +641,7 @@ class AnalyticsController extends BaseController { List? getConstructsLocal({ required TimeSpan timeSpan, - required ConstructType constructType, + required ConstructTypeEnum constructType, required AnalyticsSelected defaultSelected, AnalyticsSelected? selected, DateTime? lastUpdated, @@ -669,7 +669,7 @@ class AnalyticsController extends BaseController { } void cacheConstructs({ - required ConstructType constructType, + required ConstructTypeEnum constructType, required List events, required AnalyticsSelected defaultSelected, AnalyticsSelected? selected, @@ -687,7 +687,7 @@ class AnalyticsController extends BaseController { Future> getMyConstructs({ required AnalyticsSelected defaultSelected, - required ConstructType constructType, + required ConstructTypeEnum constructType, AnalyticsSelected? selected, }) async { final List unfilteredConstructs = @@ -706,7 +706,7 @@ class AnalyticsController extends BaseController { } Future> getSpaceConstructs({ - required ConstructType constructType, + required ConstructTypeEnum constructType, required Room space, required AnalyticsSelected defaultSelected, AnalyticsSelected? selected, @@ -768,7 +768,7 @@ class AnalyticsController extends BaseController { } Future?> getConstructs({ - required ConstructType constructType, + required ConstructTypeEnum constructType, required AnalyticsSelected defaultSelected, AnalyticsSelected? selected, bool removeIT = true, @@ -898,7 +898,7 @@ abstract class CacheEntry { } class ConstructCacheEntry extends CacheEntry { - final ConstructType type; + final ConstructTypeEnum type; final List events; ConstructCacheEntry({ diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 82b7af3c7..ef6baad1e 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -1,17 +1,13 @@ import 'dart:async'; import 'dart:developer'; -<<<<<<< Updated upstream -import 'package:fluffychat/pangea/constants/language_constants.dart'; -======= ->>>>>>> Stashed changes import 'package:fluffychat/pangea/constants/local.key.dart'; import 'package:fluffychat/pangea/constants/pangea_event_types.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/analytics/summary_analytics_model.dart'; -import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; @@ -196,9 +192,8 @@ class MyAnalyticsController { String? get userL2 => _pangeaController.languageController.activeL2Code(); - // top level analytics sending function. Send analytics - // for each type of analytics event - // to each of the applicable analytics rooms + /// top level analytics sending function. Gather recent messages and activity records, + /// convert them into the correct formats, and send them to the analytics room Future _updateAnalytics() async { // if missing important info, don't send analytics if (userL2 == null || _client.userID == null) { @@ -206,151 +201,108 @@ class MyAnalyticsController { return; } - // get the last updated time for each analytics room - // and the least recent update, which will be used to determine - // how far to go back in the chat history to get messages - final Map lastUpdatedMap = await _pangeaController - .matrixState.client - .allAnalyticsRoomsLastUpdated(); - final List lastUpdates = lastUpdatedMap.values - .where((lastUpdate) => lastUpdate != null) - .cast() - .toList(); - - /// Get the last time that analytics to for current target language - /// were updated. This my present a problem is the user has analytics - /// rooms for multiple languages, and a non-target language was updated - /// less recently than the target language. In this case, some data may - /// be missing, but a case like that seems relatively rare, and could - /// result in unnecessaily going too far back in the chat history - DateTime? l2AnalyticsLastUpdated = lastUpdatedMap[userL2]; - if (l2AnalyticsLastUpdated == null) { - /// if the target language has never been updated, use the least - /// recent update time - lastUpdates.sort((a, b) => a.compareTo(b)); - l2AnalyticsLastUpdated = - lastUpdates.isNotEmpty ? lastUpdates.first : null; - } - - final List chats = await _client.chatsImAStudentIn; - - final List recentMsgs = - await _getMessagesWithUnsavedAnalytics( - l2AnalyticsLastUpdated, - chats, - ); - - final List recentActivities = - await getRecentActivities(userL2!, l2AnalyticsLastUpdated, chats); - - // FOR DISCUSSION: - // we want to make sure we save something for every message send - // however, we're currently saving analytics for messages not in the userL2 - // based on bad language detection results. maybe it would be better to - // save the analytics for these messages in the userL2 analytics room, but - // with useType of unknown - + // analytics room for the user and current target language final Room analyticsRoom = await _client.getMyAnalyticsRoom(userL2!); - // if there is no analytics room for this langCode, then user hadn't sent - // message in this language at the time of the last analytics update - // so fallback to the least recent update time - final DateTime? lastUpdated = - lastUpdatedMap[analyticsRoom.id] ?? l2AnalyticsLastUpdated; - - // final String msgLangCode = (msg.originalSent?.langCode != null && - // msg.originalSent?.langCode != LanguageKeys.unknownLanguage) - // ? msg.originalSent!.langCode - // : userL2; - - // finally, send the analytics events to the analytics room - await _sendAnalyticsEvents( - analyticsRoom, - recentMsgs, - lastUpdated, - recentActivities, + // get the last time analytics were updated for this room + final DateTime? l2AnalyticsLastUpdated = + await analyticsRoom.analyticsLastUpdated( + PangeaEventTypes.summaryAnalytics, + _client.userID!, ); - } - Future> getRecentActivities( - String userL2, - DateTime? lastUpdated, - List chats, - ) async { + // all chats in which user is a student + final List chats = await _client.chatsImAStudentIn; + + // get the recent message events and activity records for each chat + final List>> recentMsgFutures = []; final List>> recentActivityFutures = []; for (final Room chat in chats) { + chat.getEventsBySender( + type: EventTypes.Message, + sender: _client.userID!, + since: l2AnalyticsLastUpdated, + ); recentActivityFutures.add( chat.getEventsBySender( type: PangeaEventTypes.activityRecord, sender: _client.userID!, - since: lastUpdated, + since: l2AnalyticsLastUpdated, ), ); } - final List> recentActivityLists = - await Future.wait(recentActivityFutures); + final List> recentMsgs = + (await Future.wait(recentMsgFutures)).toList(); + final List recentActivityReconds = + (await Future.wait(recentActivityFutures)) + .expand((e) => e) + .map((event) => PracticeActivityRecordEvent(event: event)) + .toList(); - return recentActivityLists - .expand((e) => e) - .map((e) => ActivityRecordResponse.fromJson(e.content)) - .toList(); - } + // get the timelines for each chat + final List> timelineFutures = []; + for (final chat in chats) { + timelineFutures.add(chat.getTimeline()); + } + final List timelines = await Future.wait(timelineFutures); + final Map timelineMap = + Map.fromIterables(chats.map((e) => e.id), timelines); - /// Returns the new messages that have not yet been saved to analytics. - /// The keys in the map correspond to different categories or groups of messages, - /// while the values are lists of [PangeaMessageEvent] objects belonging to each category. - Future> _getMessagesWithUnsavedAnalytics( - DateTime? since, - List chats, - ) async { - // get the recent messages for each chat - final List>> futures = []; - for (final Room chat in chats) { - futures.add( - chat.myMessageEventsInChat( - since: since, - ), + //convert into PangeaMessageEvents + final List> recentPangeaMessageEvents = []; + for (final (index, eventList) in recentMsgs.indexed) { + recentPangeaMessageEvents.add( + eventList + .map( + (event) => PangeaMessageEvent( + event: event, + timeline: timelines[index], + ownMessage: true, + ), + ) + .toList(), ); } - final List> recentMsgLists = - await Future.wait(futures); - // flatten the list of lists of messages - return recentMsgLists.expand((e) => e).toList(); - } + final List allRecentMessages = + recentPangeaMessageEvents.expand((e) => e).toList(); - Future _sendAnalyticsEvents( - Room analyticsRoom, - List recentMsgs, - DateTime? lastUpdated, - List recentActivities, - ) async { + final List summaryContent = + SummaryAnalyticsModel.formatSummaryContent(allRecentMessages); + // if there's new content to be sent, or if lastUpdated hasn't been + // set yet for this room, send the analytics events + if (summaryContent.isNotEmpty || l2AnalyticsLastUpdated == null) { + await analyticsRoom.sendSummaryAnalyticsEvent( + summaryContent, + ); + } + + // get constructs for messages final List constructContent = []; + for (final PangeaMessageEvent message in allRecentMessages) { + constructContent.addAll(message.allConstructUses); + } - if (recentMsgs.isNotEmpty) { - // remove messages that were sent before the last update - - // format the analytics data - final List summaryContent = - SummaryAnalyticsModel.formatSummaryContent(recentMsgs); - // if there's new content to be sent, or if lastUpdated hasn't been - // set yet for this room, send the analytics events - if (summaryContent.isNotEmpty || lastUpdated == null) { - await analyticsRoom.sendSummaryAnalyticsEvent( - summaryContent, + // get constructs for practice activities + final List>> constructFutures = []; + for (final PracticeActivityRecordEvent activity in recentActivityReconds) { + final Timeline? timeline = timelineMap[activity.event.roomId!]; + if (timeline == null) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "PracticeActivityRecordEvent has null timeline", + data: activity.event.toJson(), ); + continue; } - - constructContent - .addAll(ConstructAnalyticsModel.formatConstructsContent(recentMsgs)); + constructFutures.add(activity.uses(timeline)); } + final List> constructLists = + await Future.wait(constructFutures); - if (recentActivities.isNotEmpty) { - // TODO - Concert recentActivities into list of constructUse objects. - // First, We need to get related practiceActivityEvent from timeline in order to get its related constructs. Alternatively we - // could search for completed practice activities and see which have been completed by the user. - // It's not clear which is the best approach at the moment and we should consider both. - } + constructContent.addAll(constructLists.expand((e) => e)); + + debugger(when: kDebugMode); await analyticsRoom.sendConstructsEvent( constructContent, diff --git a/lib/pangea/controllers/practice_activity_generation_controller.dart b/lib/pangea/controllers/practice_activity_generation_controller.dart index 29047d0c4..8ecdc8740 100644 --- a/lib/pangea/controllers/practice_activity_generation_controller.dart +++ b/lib/pangea/controllers/practice_activity_generation_controller.dart @@ -88,7 +88,7 @@ class PracticeGenerationController { PracticeActivityModel dummyModel(PangeaMessageEvent event) => PracticeActivityModel( tgtConstructs: [ - ConstructIdentifier(lemma: "be", type: ConstructType.vocab), + ConstructIdentifier(lemma: "be", type: ConstructTypeEnum.vocab), ], activityType: ActivityTypeEnum.multipleChoice, langCode: event.messageDisplayLangCode, diff --git a/lib/pangea/enum/construct_type_enum.dart b/lib/pangea/enum/construct_type_enum.dart index 2a7d5583d..7db7f9cd5 100644 --- a/lib/pangea/enum/construct_type_enum.dart +++ b/lib/pangea/enum/construct_type_enum.dart @@ -1,30 +1,30 @@ -enum ConstructType { +enum ConstructTypeEnum { grammar, vocab, } -extension ConstructExtension on ConstructType { +extension ConstructExtension on ConstructTypeEnum { String get string { switch (this) { - case ConstructType.grammar: + case ConstructTypeEnum.grammar: return 'grammar'; - case ConstructType.vocab: + case ConstructTypeEnum.vocab: return 'vocab'; } } } class ConstructTypeUtil { - static ConstructType fromString(String? string) { + static ConstructTypeEnum fromString(String? string) { switch (string) { case 'g': case 'grammar': - return ConstructType.grammar; + return ConstructTypeEnum.grammar; case 'v': case 'vocab': - return ConstructType.vocab; + return ConstructTypeEnum.vocab; default: - return ConstructType.vocab; + return ConstructTypeEnum.vocab; } } } diff --git a/lib/pangea/enum/construct_use_type_enum.dart b/lib/pangea/enum/construct_use_type_enum.dart new file mode 100644 index 000000000..0e3c52bbb --- /dev/null +++ b/lib/pangea/enum/construct_use_type_enum.dart @@ -0,0 +1,93 @@ +import 'package:flutter/material.dart'; + +enum ConstructUseTypeEnum { + /// produced in chat by user, igc was run, and we've judged it to be a correct use + wa, + + /// produced in chat by user, igc was run, and we've judged it to be a incorrect use + /// Note: if the IGC match is ignored, this is not counted as an incorrect use + ga, + + /// produced in chat by user and igc was not run + unk, + + /// selected correctly in IT flow + corIt, + + /// encountered as IT distractor and correctly ignored it + ignIt, + + /// encountered as it distractor and selected it + incIt, + + /// encountered in igc match and ignored match + ignIGC, + + /// selected correctly in IGC flow + corIGC, + + /// encountered as distractor in IGC flow and selected it + incIGC, + + /// selected correctly in practice activity flow + corPA, + + /// was target construct in practice activity but user did not select correctly + incPA, +} + +extension ConstructUseTypeExtension on ConstructUseTypeEnum { + String get string { + switch (this) { + case ConstructUseTypeEnum.ga: + return 'ga'; + case ConstructUseTypeEnum.wa: + return 'wa'; + case ConstructUseTypeEnum.corIt: + return 'corIt'; + case ConstructUseTypeEnum.incIt: + return 'incIt'; + case ConstructUseTypeEnum.ignIt: + return 'ignIt'; + case ConstructUseTypeEnum.ignIGC: + return 'ignIGC'; + case ConstructUseTypeEnum.corIGC: + return 'corIGC'; + case ConstructUseTypeEnum.incIGC: + return 'incIGC'; + case ConstructUseTypeEnum.unk: + return 'unk'; + case ConstructUseTypeEnum.corPA: + return 'corPA'; + case ConstructUseTypeEnum.incPA: + return 'incPA'; + } + } + + IconData get icon { + switch (this) { + case ConstructUseTypeEnum.ga: + return Icons.check; + case ConstructUseTypeEnum.wa: + return Icons.thumb_up_sharp; + case ConstructUseTypeEnum.corIt: + return Icons.check; + case ConstructUseTypeEnum.incIt: + return Icons.close; + case ConstructUseTypeEnum.ignIt: + return Icons.close; + case ConstructUseTypeEnum.ignIGC: + return Icons.close; + case ConstructUseTypeEnum.corIGC: + return Icons.check; + case ConstructUseTypeEnum.incIGC: + return Icons.close; + case ConstructUseTypeEnum.corPA: + return Icons.check; + case ConstructUseTypeEnum.incPA: + return Icons.close; + case ConstructUseTypeEnum.unk: + return Icons.help; + } + } +} diff --git a/lib/pangea/extensions/pangea_room_extension/events_extension.dart b/lib/pangea/extensions/pangea_room_extension/events_extension.dart index 6cdde1ce2..0f40da5c7 100644 --- a/lib/pangea/extensions/pangea_room_extension/events_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/events_extension.dart @@ -426,32 +426,6 @@ extension EventsRoomExtension on Room { // } // } - Future> myMessageEventsInChat({ - DateTime? since, - }) async { - try { - final List msgEvents = await getEventsBySender( - type: EventTypes.Message, - sender: client.userID!, - since: since, - ); - final Timeline timeline = await getTimeline(); - return msgEvents - .where((event) => (event.content['msgtype'] == MessageTypes.Text)) - .map((event) { - return PangeaMessageEvent( - event: event, - timeline: timeline, - ownMessage: true, - ); - }).toList(); - } catch (err, s) { - debugger(when: kDebugMode); - ErrorHandler.logError(e: err, s: s); - return []; - } - } - // fetch event of a certain type by a certain sender // since a certain time or up to a certain amount Future> getEventsBySender({ diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 4ead9982c..080b07617 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -2,14 +2,20 @@ import 'dart:convert'; import 'dart:developer'; import 'package:collection/collection.dart'; +import 'package:fluffychat/pangea/constants/choreo_constants.dart'; import 'package:fluffychat/pangea/constants/model_keys.dart'; import 'package:fluffychat/pangea/controllers/text_to_speech_controller.dart'; import 'package:fluffychat/pangea/enum/audio_encoding_enum.dart'; +import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; +import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_representation_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; +import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; +import 'package:fluffychat/pangea/models/lemma.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; +import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/models/representation_content_model.dart'; import 'package:fluffychat/pangea/models/space_model.dart'; import 'package:fluffychat/pangea/models/speech_to_text_models.dart'; @@ -658,14 +664,145 @@ class PangeaMessageEvent { } } - // 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 + List get allConstructUses => + [...grammarConstructUses, ..._vocabUses]; - // replication of logic from message_content.dart - // bool get isHtml => - // AppConfig.renderHtml && !_event.redacted && _event.isRichMessage; + /// [tokens] is the final list of tokens that were sent + /// if no ga or ta, + /// make wa use for each and return + /// else + /// for each saveable vocab in the final message + /// if vocab is contained in an accepted replacement, make ga use + /// if vocab is contained in ta choice, + /// if selected as choice, corIt + /// if written as customInput, corIt? (account for score in this) + /// for each it step + /// for each continuance + /// if not within the final message, save ignIT/incIT + List get _vocabUses { + 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, + ), + ); + } + } + 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 + if (originalSent?.tokens != null) { + for (final token in originalSent!.tokens!) { + uses.addAll(getVocabUseForToken(token)); + } + } + + if (originalSent?.choreo == null) return uses; + + for (final itStep in originalSent!.choreo!.itSteps) { + for (final continuance in itStep.continuances) { + // this seems to always be false for continuances right now + + if (originalSent!.choreo!.finalMessage.contains(continuance.text)) { + continue; + } + if (continuance.wasClicked) { + //PTODO - account for end of flow score + if (continuance.level != ChoreoConstants.levelThresholdForGreen) { + uses.addAll( + lemmasToVocabUses(continuance.lemmas, ConstructUseTypeEnum.incIt), + ); + } + } else { + if (continuance.level != ChoreoConstants.levelThresholdForGreen) { + uses.addAll( + lemmasToVocabUses(continuance.lemmas, ConstructUseTypeEnum.ignIt), + ); + } + } + } + } + + return uses; + } + + List get grammarConstructUses { + final List uses = []; + + if (originalSent?.choreo == null || event.roomId == null) return uses; + + for (final step in originalSent!.choreo!.choreoSteps) { + if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted) { + final String name = step.acceptedOrIgnoredMatch!.match.rule?.id ?? + step.acceptedOrIgnoredMatch!.match.shortMessage ?? + step.acceptedOrIgnoredMatch!.match.type.typeName.name; + uses.add( + OneConstructUse( + useType: ConstructUseTypeEnum.ga, + chatId: event.roomId!, + timeStamp: event.originServerTs, + lemma: name, + form: name, + msgId: event.eventId, + constructType: ConstructTypeEnum.grammar, + id: "${event.eventId}_${step.acceptedOrIgnoredMatch!.match.offset}_${step.acceptedOrIgnoredMatch!.match.length}", + ), + ); + } + } + return uses; + } } class URLFinder { diff --git a/lib/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart b/lib/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart deleted file mode 100644 index d4b9cde23..000000000 --- a/lib/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; -import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; -import 'package:matrix/matrix.dart'; - -import '../constants/pangea_event_types.dart'; - -class PracticeActivityRecordEvent { - Event event; - - PracticeActivityRecordModel? _content; - - PracticeActivityRecordEvent({required this.event}) { - if (event.type != PangeaEventTypes.activityRecord) { - throw Exception( - "${event.type} should not be used to make a PracticeActivityRecordEvent", - ); - } - } - - PracticeActivityRecordModel? get record { - _content ??= event.getPangeaContent(); - return _content!; - } -} diff --git a/lib/pangea/matrix_event_wrappers/practice_activity_event.dart b/lib/pangea/matrix_event_wrappers/practice_activity_event.dart index c5f35be91..9d7b17ccc 100644 --- a/lib/pangea/matrix_event_wrappers/practice_activity_event.dart +++ b/lib/pangea/matrix_event_wrappers/practice_activity_event.dart @@ -1,7 +1,7 @@ import 'dart:developer'; import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; -import 'package:fluffychat/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; import 'package:flutter/foundation.dart'; import 'package:matrix/matrix.dart'; @@ -61,6 +61,8 @@ class PracticeActivityEvent { ) .toList(); + String get parentMessageId => event.relationshipEventId!; + /// Checks if there are any user records in the list for this activity, /// and, if so, then the activity is complete bool get isComplete => userRecords.isNotEmpty; diff --git a/lib/pangea/matrix_event_wrappers/practice_activity_record_event.dart b/lib/pangea/matrix_event_wrappers/practice_activity_record_event.dart new file mode 100644 index 000000000..77b4948fd --- /dev/null +++ b/lib/pangea/matrix_event_wrappers/practice_activity_record_event.dart @@ -0,0 +1,89 @@ +import 'dart:developer'; + +import 'package:fluffychat/pangea/extensions/pangea_event_extension.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; +import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_model.dart'; +import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; +import 'package:fluffychat/pangea/utils/error_handler.dart'; +import 'package:flutter/foundation.dart'; +import 'package:matrix/matrix.dart'; + +import '../constants/pangea_event_types.dart'; + +class PracticeActivityRecordEvent { + Event event; + + PracticeActivityRecordModel? _content; + + PracticeActivityRecordEvent({required this.event}) { + if (event.type != PangeaEventTypes.activityRecord) { + throw Exception( + "${event.type} should not be used to make a PracticeActivityRecordEvent", + ); + } + } + + PracticeActivityRecordModel get record { + _content ??= event.getPangeaContent(); + return _content!; + } + + Future> uses(Timeline timeline) async { + try { + final String? parent = event.relationshipEventId; + if (parent == null) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "PracticeActivityRecordEvent has null event.relationshipEventId", + data: event.toJson(), + ); + return []; + } + + final Event? practiceEvent = + await timeline.getEventById(event.relationshipEventId!); + + if (practiceEvent == null) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "PracticeActivityRecordEvent has null practiceActivityEvent with id $parent", + data: event.toJson(), + ); + return []; + } + + final PracticeActivityEvent practiceActivity = PracticeActivityEvent( + event: practiceEvent, + timeline: timeline, + ); + + final List uses = []; + + final List constructIds = + practiceActivity.practiceActivity.tgtConstructs; + + for (final construct in constructIds) { + uses.add( + OneConstructUse( + lemma: construct.lemma, + constructType: construct.type, + useType: record.useType, + //TODO - find form of construct within the message + //this is related to the feature of highlighting the target construct in the message + form: construct.lemma, + chatId: event.roomId ?? practiceEvent.roomId ?? timeline.room.id, + msgId: practiceActivity.parentMessageId, + timeStamp: event.originServerTs, + ), + ); + } + + return uses; + } catch (e, s) { + debugger(when: kDebugMode); + ErrorHandler.logError(e: e, s: s, data: event.toJson()); + rethrow; + } + } +} diff --git a/lib/pangea/models/analytics/analytics_model.dart b/lib/pangea/models/analytics/analytics_model.dart index bdb3bc6d5..d8732ad97 100644 --- a/lib/pangea/models/analytics/analytics_model.dart +++ b/lib/pangea/models/analytics/analytics_model.dart @@ -12,7 +12,11 @@ abstract class AnalyticsModel { case PangeaEventTypes.summaryAnalytics: return SummaryAnalyticsModel.formatSummaryContent(recentMsgs); case PangeaEventTypes.construct: - return ConstructAnalyticsModel.formatConstructsContent(recentMsgs); + final List uses = []; + for (final msg in recentMsgs) { + uses.addAll(msg.allConstructUses); + } + return uses; } return []; } diff --git a/lib/pangea/models/analytics/constructs_model.dart b/lib/pangea/models/analytics/constructs_model.dart index 18c6d3d5a..54e81789f 100644 --- a/lib/pangea/models/analytics/constructs_model.dart +++ b/lib/pangea/models/analytics/constructs_model.dart @@ -1,11 +1,9 @@ import 'dart:developer'; -import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/models/analytics/analytics_model.dart'; -import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; import 'package:matrix/matrix.dart'; import '../../enum/construct_type_enum.dart'; @@ -24,7 +22,7 @@ class ConstructAnalyticsModel extends AnalyticsModel { if (json[_usesKey] is List) { // This is the new format uses.addAll( - json[_usesKey] + (json[_usesKey] as List) .map((use) => OneConstructUse.fromJson(use)) .cast() .toList(), @@ -39,13 +37,13 @@ class ConstructAnalyticsModel extends AnalyticsModel { final lemmaUses = useValue[_usesKey]; for (final useData in lemmaUses) { final use = OneConstructUse( - useType: ConstructUseType.ga, + useType: ConstructUseTypeEnum.ga, chatId: useData["chatId"], timeStamp: DateTime.parse(useData["timeStamp"]), lemma: lemma, form: useData["form"], msgId: useData["msgId"], - constructType: ConstructType.grammar, + constructType: ConstructTypeEnum.grammar, ); uses.add(use); } @@ -70,122 +68,13 @@ class ConstructAnalyticsModel extends AnalyticsModel { _usesKey: uses.map((use) => use.toJson()).toList(), }; } - - static List formatConstructsContent( - List recentMsgs, - ) { - final List filtered = List.from(recentMsgs); - final List uses = []; - - for (final msg in filtered) { - if (msg.originalSent?.choreo == null) continue; - uses.addAll( - msg.originalSent!.choreo!.toGrammarConstructUse( - msg.eventId, - msg.room.id, - msg.originServerTs, - ), - ); - - final List? tokens = msg.originalSent?.tokens; - if (tokens == null) continue; - uses.addAll( - msg.originalSent!.choreo!.toVocabUse( - tokens, - msg.room.id, - msg.eventId, - msg.originServerTs, - ), - ); - } - - return uses; - } -} - -enum ConstructUseType { - /// produced in chat by user, igc was run, and we've judged it to be a correct use - wa, - - /// produced in chat by user, igc was run, and we've judged it to be a incorrect use - /// Note: if the IGC match is ignored, this is not counted as an incorrect use - ga, - - /// produced in chat by user and igc was not run - unk, - - /// selected correctly in IT flow - corIt, - - /// encountered as IT distractor and correctly ignored it - ignIt, - - /// encountered as it distractor and selected it - incIt, - - /// encountered in igc match and ignored match - ignIGC, - - /// selected correctly in IGC flow - corIGC, - - /// encountered as distractor in IGC flow and selected it - incIGC, -} - -extension on ConstructUseType { - String get string { - switch (this) { - case ConstructUseType.ga: - return 'ga'; - case ConstructUseType.wa: - return 'wa'; - case ConstructUseType.corIt: - return 'corIt'; - case ConstructUseType.incIt: - return 'incIt'; - case ConstructUseType.ignIt: - return 'ignIt'; - case ConstructUseType.ignIGC: - return 'ignIGC'; - case ConstructUseType.corIGC: - return 'corIGC'; - case ConstructUseType.incIGC: - return 'incIGC'; - case ConstructUseType.unk: - return 'unk'; - } - } - - IconData get icon { - switch (this) { - case ConstructUseType.ga: - return Icons.check; - case ConstructUseType.wa: - return Icons.thumb_up_sharp; - case ConstructUseType.corIt: - return Icons.check; - case ConstructUseType.incIt: - return Icons.close; - case ConstructUseType.ignIt: - return Icons.close; - case ConstructUseType.ignIGC: - return Icons.close; - case ConstructUseType.corIGC: - return Icons.check; - case ConstructUseType.incIGC: - return Icons.close; - case ConstructUseType.unk: - return Icons.help; - } - } } class OneConstructUse { String? lemma; - ConstructType? constructType; + ConstructTypeEnum? constructType; String? form; - ConstructUseType useType; + ConstructUseTypeEnum useType; String chatId; String? msgId; DateTime timeStamp; @@ -204,7 +93,7 @@ class OneConstructUse { factory OneConstructUse.fromJson(Map json) { return OneConstructUse( - useType: ConstructUseType.values + useType: ConstructUseTypeEnum.values .firstWhere((e) => e.string == json['useType']), chatId: json['chatId'], timeStamp: DateTime.parse(json['timeStamp']), @@ -248,7 +137,7 @@ class OneConstructUse { class ConstructUses { final List uses; - final ConstructType constructType; + final ConstructTypeEnum constructType; final String lemma; ConstructUses({ diff --git a/lib/pangea/models/choreo_record.dart b/lib/pangea/models/choreo_record.dart index 413f00716..3586fcee1 100644 --- a/lib/pangea/models/choreo_record.dart +++ b/lib/pangea/models/choreo_record.dart @@ -1,13 +1,8 @@ import 'dart:convert'; -import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; -import 'package:fluffychat/pangea/models/pangea_token_model.dart'; -import '../constants/choreo_constants.dart'; -import '../enum/construct_type_enum.dart'; import 'it_step.dart'; -import 'lemma.dart'; /// this class lives within a [PangeaIGCEvent] /// it always has a [RepresentationEvent] parent @@ -111,135 +106,6 @@ class ChoreoRecord { openMatches: [], ); - /// [tokens] is the final list of tokens that were sent - /// if no ga or ta, - /// make wa use for each and return - /// else - /// for each saveable vocab in the final message - /// if vocab is contained in an accepted replacement, make ga use - /// if vocab is contained in ta choice, - /// if selected as choice, corIt - /// if written as customInput, corIt? (account for score in this) - /// for each it step - /// for each continuance - /// if not within the final message, save ignIT/incIT - List toVocabUse( - List tokens, - String chatId, - String msgId, - DateTime timestamp, - ) { - final List uses = []; - final DateTime now = DateTime.now(); - List lemmasToVocabUses( - List lemmas, - ConstructUseType type, - ) { - final List uses = []; - for (final lemma in lemmas) { - if (lemma.saveVocab) { - uses.add( - OneConstructUse( - useType: type, - chatId: chatId, - timeStamp: timestamp, - lemma: lemma.text, - form: lemma.form, - msgId: msgId, - constructType: ConstructType.vocab, - ), - ); - } - } - return uses; - } - - List getVocabUseForToken(PangeaToken token) { - for (final step in choreoSteps) { - /// if 1) accepted match 2) token is in the replacement and 3) replacement - /// is in the overall step text, then token was a ga - if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted && - (step.acceptedOrIgnoredMatch!.match.choices?.any( - (r) => - r.value.contains(token.text.content) && - step.text.contains(r.value), - ) ?? - false)) { - return lemmasToVocabUses(token.lemmas, ConstructUseType.ga); - } - if (step.itStep != null) { - final bool pickedThroughIT = step.itStep!.chosenContinuance?.text - .contains(token.text.content) ?? - false; - if (pickedThroughIT) { - return lemmasToVocabUses(token.lemmas, ConstructUseType.corIt); - //PTODO - check if added via custom input in IT flow - } - } - } - return lemmasToVocabUses(token.lemmas, ConstructUseType.wa); - } - - /// for each token, record whether selected in ga, ta, or wa - for (final token in tokens) { - uses.addAll(getVocabUseForToken(token)); - } - - for (final itStep in itSteps) { - for (final continuance in itStep.continuances) { - // this seems to always be false for continuances right now - - if (finalMessage.contains(continuance.text)) { - continue; - } - if (continuance.wasClicked) { - //PTODO - account for end of flow score - if (continuance.level != ChoreoConstants.levelThresholdForGreen) { - uses.addAll( - lemmasToVocabUses(continuance.lemmas, ConstructUseType.incIt), - ); - } - } else { - if (continuance.level != ChoreoConstants.levelThresholdForGreen) { - uses.addAll( - lemmasToVocabUses(continuance.lemmas, ConstructUseType.ignIt), - ); - } - } - } - } - - return uses; - } - - List toGrammarConstructUse( - String msgId, - String chatId, - DateTime timestamp, - ) { - final List uses = []; - for (final step in choreoSteps) { - if (step.acceptedOrIgnoredMatch?.status == PangeaMatchStatus.accepted) { - final String name = step.acceptedOrIgnoredMatch!.match.rule?.id ?? - step.acceptedOrIgnoredMatch!.match.shortMessage ?? - step.acceptedOrIgnoredMatch!.match.type.typeName.name; - uses.add( - OneConstructUse( - useType: ConstructUseType.ga, - chatId: chatId, - timeStamp: timestamp, - lemma: name, - form: name, - msgId: msgId, - constructType: ConstructType.grammar, - id: "${msgId}_${step.acceptedOrIgnoredMatch!.match.offset}_${step.acceptedOrIgnoredMatch!.match.length}", - ), - ); - } - } - return uses; - } - List get itSteps => choreoSteps.where((e) => e.itStep != null).map((e) => e.itStep!).toList(); diff --git a/lib/pangea/models/headwords.dart b/lib/pangea/models/headwords.dart index 497381fa1..a55eeb188 100644 --- a/lib/pangea/models/headwords.dart +++ b/lib/pangea/models/headwords.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:developer'; +import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; import 'package:fluffychat/pangea/models/analytics/constructs_model.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; @@ -154,32 +155,37 @@ class VocabTotals { void addVocabUseBasedOnUseType(List uses) { for (final use in uses) { switch (use.useType) { - case ConstructUseType.ga: + case ConstructUseTypeEnum.ga: ga++; break; - case ConstructUseType.wa: + case ConstructUseTypeEnum.wa: wa++; break; - case ConstructUseType.corIt: + case ConstructUseTypeEnum.corIt: corIt++; break; - case ConstructUseType.incIt: + case ConstructUseTypeEnum.incIt: incIt++; break; - case ConstructUseType.ignIt: + case ConstructUseTypeEnum.ignIt: ignIt++; break; //TODO - these shouldn't be counted as such - case ConstructUseType.ignIGC: + case ConstructUseTypeEnum.ignIGC: ignIt++; break; - case ConstructUseType.corIGC: + case ConstructUseTypeEnum.corIGC: corIt++; break; - case ConstructUseType.incIGC: + case ConstructUseTypeEnum.incIGC: incIt++; break; - case ConstructUseType.unk: + //TODO if we bring back Headwords then we need to add these + case ConstructUseTypeEnum.corPA: + break; + case ConstructUseTypeEnum.incPA: + break; + case ConstructUseTypeEnum.unk: break; } } diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart index ae8455c7f..bd597b6a2 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_model.dart @@ -8,7 +8,7 @@ import 'package:flutter/foundation.dart'; class ConstructIdentifier { final String lemma; - final ConstructType type; + final ConstructTypeEnum type; ConstructIdentifier({required this.lemma, required this.type}); @@ -16,7 +16,7 @@ class ConstructIdentifier { try { return ConstructIdentifier( lemma: json['lemma'] as String, - type: ConstructType.values.firstWhere( + type: ConstructTypeEnum.values.firstWhere( (e) => e.string == json['type'], ), ); diff --git a/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart b/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart index e5c3a1c18..0c4ea52bf 100644 --- a/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart +++ b/lib/pangea/models/practice_activities.dart/practice_activity_record_model.dart @@ -5,6 +5,8 @@ import 'dart:developer'; import 'dart:typed_data'; +import 'package:fluffychat/pangea/enum/construct_use_type_enum.dart'; + class PracticeActivityRecordModel { final String? question; late List responses; @@ -42,18 +44,25 @@ class PracticeActivityRecordModel { /// get the latest response index according to the response timeStamp /// sort the responses by timestamp and get the index of the last response - String? get latestResponse { + ActivityRecordResponse? get latestResponse { if (responses.isEmpty) { return null; } responses.sort((a, b) => a.timestamp.compareTo(b.timestamp)); - return responses[responses.length - 1].text; + return responses[responses.length - 1]; } + ConstructUseTypeEnum get useType => latestResponse?.score != null + ? (latestResponse!.score > 0 + ? ConstructUseTypeEnum.corPA + : ConstructUseTypeEnum.incPA) + : ConstructUseTypeEnum.unk; + void addResponse({ String? text, Uint8List? audioBytes, Uint8List? imageBytes, + required double score, }) { try { responses.add( @@ -62,6 +71,7 @@ class PracticeActivityRecordModel { audioBytes: audioBytes, imageBytes: imageBytes, timestamp: DateTime.now(), + score: score, ), ); } catch (e) { @@ -93,11 +103,13 @@ class ActivityRecordResponse { final Uint8List? audioBytes; final Uint8List? imageBytes; final DateTime timestamp; + final double score; ActivityRecordResponse({ this.text, this.audioBytes, this.imageBytes, + required this.score, required this.timestamp, }); @@ -107,6 +119,10 @@ class ActivityRecordResponse { audioBytes: json['audio'] as Uint8List?, imageBytes: json['image'] as Uint8List?, timestamp: DateTime.parse(json['timestamp'] as String), + // this has a default of 1 to make this backwards compatible + // score was added later and is not present in all records + // currently saved to Matrix + score: json['score'] ?? 1.0, ); } @@ -116,6 +132,7 @@ class ActivityRecordResponse { 'audio': audioBytes, 'image': imageBytes, 'timestamp': timestamp.toIso8601String(), + 'score': score, }; } diff --git a/lib/pangea/pages/analytics/base_analytics_view.dart b/lib/pangea/pages/analytics/base_analytics_view.dart index 3d70c9b4c..cca0c7f4e 100644 --- a/lib/pangea/pages/analytics/base_analytics_view.dart +++ b/lib/pangea/pages/analytics/base_analytics_view.dart @@ -35,7 +35,7 @@ class BaseAnalyticsView extends StatelessWidget { ); case BarChartViewSelection.grammar: return ConstructList( - constructType: ConstructType.grammar, + constructType: ConstructTypeEnum.grammar, defaultSelected: controller.widget.defaultSelected, selected: controller.selected, controller: controller, diff --git a/lib/pangea/pages/analytics/construct_list.dart b/lib/pangea/pages/analytics/construct_list.dart index 8651b7a74..d46936c86 100644 --- a/lib/pangea/pages/analytics/construct_list.dart +++ b/lib/pangea/pages/analytics/construct_list.dart @@ -19,7 +19,7 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:matrix/matrix.dart'; class ConstructList extends StatefulWidget { - final ConstructType constructType; + final ConstructTypeEnum constructType; final AnalyticsSelected defaultSelected; final AnalyticsSelected? selected; final BaseAnalyticsController controller; @@ -94,7 +94,7 @@ class ConstructListView extends StatefulWidget { } class ConstructListViewState extends State { - final ConstructType constructType = ConstructType.grammar; + final ConstructTypeEnum constructType = ConstructTypeEnum.grammar; final Map _timelinesCache = {}; final Map _msgEventCache = {}; final List _msgEvents = []; diff --git a/lib/pangea/widgets/practice_activity/practice_activity_content.dart b/lib/pangea/widgets/practice_activity/practice_activity_content.dart index 8080c27ee..af43081f3 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_content.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_content.dart @@ -2,7 +2,7 @@ import 'package:collection/collection.dart'; import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; -import 'package:fluffychat/pangea/matrix_event_wrappers/practice_acitivity_record_event.dart'; +import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; @@ -11,6 +11,7 @@ import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_ca import 'package:fluffychat/widgets/matrix.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; +import 'package:get_storage/get_storage.dart'; class PracticeActivityContent extends StatefulWidget { final PracticeActivityEvent practiceEvent; @@ -65,9 +66,9 @@ class MessagePracticeActivityContentState recordModel = recordEvent!.record; //Note that only MultipleChoice activities will have this so we probably should move this logic to the MultipleChoiceActivity widget - selectedChoiceIndex = recordModel?.latestResponse != null + selectedChoiceIndex = recordModel?.latestResponse?.text != null ? widget.practiceEvent.practiceActivity.multipleChoice - ?.choiceIndex(recordModel!.latestResponse!) + ?.choiceIndex(recordModel!.latestResponse!.text!) : null; recordSubmittedPreviousSession = true; @@ -80,6 +81,10 @@ class MessagePracticeActivityContentState setState(() { selectedChoiceIndex = index; recordModel!.addResponse( + score: widget.practiceEvent.practiceActivity.multipleChoice! + .isCorrect(index) + ? 1 + : 0, text: widget .practiceEvent.practiceActivity.multipleChoice!.choices[index], ); From 919cfc4bd39b8f8b61c2a003f2cf0f6a20eaf38b Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Sun, 30 Jun 2024 10:36:09 -0400 Subject: [PATCH 09/26] improving documentation --- .../controllers/choreographer.dart | 41 ++-- .../controllers/igc_controller.dart | 67 +++--- .../controllers/it_controller.dart | 5 +- .../controllers/span_data_controller.dart | 0 .../widgets/start_igc_button.dart | 4 +- lib/pangea/config/environment.dart | 2 +- .../controllers/my_analytics_controller.dart | 14 +- .../pangea_message_event.dart | 194 ++++++++++-------- lib/pangea/repo/igc_repo.dart | 3 - 9 files changed, 181 insertions(+), 149 deletions(-) rename lib/pangea/{ => choreographer}/controllers/span_data_controller.dart (100%) 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, }; } From 25263317068e058e0f84d58f7cf1d252974f62c0 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Sun, 30 Jun 2024 12:08:30 -0400 Subject: [PATCH 10/26] using getIGCTextData to fetch tokens and languages if not present --- .../controllers/choreographer.dart | 57 +++----- .../controllers/igc_controller.dart | 133 +++++++----------- .../controllers/it_controller.dart | 13 +- .../controllers/my_analytics_controller.dart | 5 +- .../pangea_message_event.dart | 2 - lib/pangea/models/igc_text_data_model.dart | 26 ++-- lib/pangea/repo/igc_repo.dart | 6 +- 7 files changed, 103 insertions(+), 139 deletions(-) diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 77130f635..42661b81e 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -117,34 +117,39 @@ class Choreographer { // TODO - move this to somewhere such that the message can be cleared from the input field // before the language detection is complete. Otherwise, user is going to be waiting // in cases of slow internet or slow language detection - final String originalSentLangCode = langCodeOfCurrentText ?? - (await pangeaController.languageDetection.detectLanguage( - currentText, - pangeaController.languageController.userL2?.langCode, - pangeaController.languageController.userL1?.langCode, - )) - .bestDetection() - .langCode; - - final PangeaRepresentation originalSent = PangeaRepresentation( - langCode: originalSentLangCode, - text: currentText, - originalSent: true, - originalWritten: originalWritten == null, - ); + final String? originalSentLangCode = igc.igcTextData?.detectedLanguage; // TODO - why does both it and igc need to be enabled for choreo to be applicable? final ChoreoRecord? applicableChoreo = isITandIGCEnabled && igc.igcTextData != null ? choreoRecord : null; final UseType useType = useTypeCalculator(applicableChoreo); - debugPrint("use type in choreographer $useType"); + + // if tokens or language detection are not available, get them + // note that we probably need to move this to after we clear the input field + // or the user could experience some lag here. note that this call is being + // made after we've determined if we have an applicable choreo in order to + // say whether correction was run on the message. we may eventually want + // to edit the useType after + if (igc.igcTextData?.tokens == null || + igc.igcTextData?.detectedLanguage == null) { + await igc.getIGCTextData(onlyTokensAndLanguageDetection: true); + } + + final PangeaRepresentation originalSent = PangeaRepresentation( + langCode: originalSentLangCode ?? LanguageKeys.unknownLanguage, + text: currentText, + originalSent: true, + originalWritten: originalWritten == null, + ); + debugger(when: kDebugMode); chatController.send( // PTODO - turn this back on in conjunction with saving tokens // we need to save those tokens as well, in order for exchanges to work // properly. in an exchange, the other user will want // originalWritten: originalWritten, + originalSent: originalSent, tokensSent: igc.igcTextData?.tokens != null ? PangeaMessageTokens(tokens: igc.igcTextData!.tokens) @@ -170,7 +175,7 @@ class Choreographer { } choreoMode = ChoreoMode.it; itController.initializeIT( - ITStartData(_textController.text, igc.detectedLangCode), + ITStartData(_textController.text, igc.igcTextData?.detectedLanguage), ); itMatch.status = PangeaMatchStatus.accepted; @@ -195,7 +200,7 @@ class Choreographer { // 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(); + // 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 @@ -499,22 +504,6 @@ class Choreographer { bool get editTypeIsKeyboard => EditType.keyboard == _textController.editType; - /// If there is applicable igcTextData, return the detected langCode - /// Otherwise, if the IT controller is open, return the user's L2 langCode - /// This second piece assumes that IT is being used to translate into the user's L2 - /// and could be spotty. It's a bit of a hack, and should be tested more. - String? get langCodeOfCurrentText { - if (igc.detectedLangCode != null) return igc.detectedLangCode!; - - // 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) { - return l2LangCode!; - } - - return null; - } - setState() { if (!stateListener.isClosed) { stateListener.add(0); diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index a694c48a5..fed4a3167 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -10,11 +10,8 @@ import 'package:fluffychat/pangea/repo/igc_repo.dart'; import 'package:fluffychat/pangea/widgets/igc/span_card.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:sentry_flutter/sentry_flutter.dart'; -import '../../models/language_detection_model.dart'; import '../../models/span_card_model.dart'; -import '../../repo/tokens_repo.dart'; import '../../utils/error_handler.dart'; import '../../utils/overlay.dart'; @@ -64,22 +61,6 @@ class IgcController { return; } - //TO-DO: in api call, specify turning off IT and/or grammar checking - // 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, @@ -106,61 +87,61 @@ class IgcController { } } - Future justGetTokensAndAddThemToIGCTextData() async { - try { - if (igcTextData == null) { - debugger(when: kDebugMode); - choreographer.getLanguageHelp(); - return; - } - igcTextData!.loading = true; - choreographer.startLoading(); - if (igcTextData!.originalInput != choreographer.textController.text) { - debugger(when: kDebugMode); - ErrorHandler.logError( - m: "igcTextData fullText does not match current text", - s: StackTrace.current, - data: igcTextData!.toJson(), - ); - } + // Future justGetTokensAndAddThemToIGCTextData() async { + // try { + // if (igcTextData == null) { + // debugger(when: kDebugMode); + // choreographer.getLanguageHelp(); + // return; + // } + // igcTextData!.loading = true; + // choreographer.startLoading(); + // if (igcTextData!.originalInput != choreographer.textController.text) { + // debugger(when: kDebugMode); + // ErrorHandler.logError( + // m: "igcTextData fullText does not match current text", + // s: StackTrace.current, + // data: igcTextData!.toJson(), + // ); + // } - if (choreographer.l1LangCode == null || - choreographer.l2LangCode == null) { - debugger(when: kDebugMode); - ErrorHandler.logError( - m: "l1LangCode and/or l2LangCode is null", - s: StackTrace.current, - data: { - "l1LangCode": choreographer.l1LangCode, - "l2LangCode": choreographer.l2LangCode, - }, - ); - return; - } + // if (choreographer.l1LangCode == null || + // choreographer.l2LangCode == null) { + // debugger(when: kDebugMode); + // ErrorHandler.logError( + // m: "l1LangCode and/or l2LangCode is null", + // s: StackTrace.current, + // data: { + // "l1LangCode": choreographer.l1LangCode, + // "l2LangCode": choreographer.l2LangCode, + // }, + // ); + // return; + // } - final TokensResponseModel res = await TokensRepo.tokenize( - await choreographer.pangeaController.userController.accessToken, - TokensRequestModel( - fullText: igcTextData!.originalInput, - userL1: choreographer.l1LangCode!, - userL2: choreographer.l2LangCode!, - ), - ); - igcTextData?.tokens = res.tokens; - } catch (err, stack) { - debugger(when: kDebugMode); - choreographer.errorService.setError( - ChoreoError(type: ChoreoErrorType.unknown, raw: err), - ); - Sentry.addBreadcrumb( - Breadcrumb.fromJson({"igctextDdata": igcTextData?.toJson()}), - ); - ErrorHandler.logError(e: err, s: stack); - } finally { - igcTextData?.loading = false; - choreographer.stopLoading(); - } - } + // final TokensResponseModel res = await TokensRepo.tokenize( + // await choreographer.pangeaController.userController.accessToken, + // TokensRequestModel( + // fullText: igcTextData!.originalInput, + // userL1: choreographer.l1LangCode!, + // userL2: choreographer.l2LangCode!, + // ), + // ); + // igcTextData?.tokens = res.tokens; + // } catch (err, stack) { + // debugger(when: kDebugMode); + // choreographer.errorService.setError( + // ChoreoError(type: ChoreoErrorType.unknown, raw: err), + // ); + // Sentry.addBreadcrumb( + // Breadcrumb.fromJson({"igctextDdata": igcTextData?.toJson()}), + // ); + // ErrorHandler.logError(e: err, s: stack); + // } finally { + // igcTextData?.loading = false; + // choreographer.stopLoading(); + // } + // } void showFirstMatch(BuildContext context) { if (igcTextData == null || igcTextData!.matches.isEmpty) { @@ -218,14 +199,6 @@ class IgcController { return true; } - String? get detectedLangCode { - if (!hasRelevantIGCTextData) return null; - - final LanguageDetection first = igcTextData!.detections.first; - - return first.langCode; - } - clear() { igcTextData = null; spanDataController.clearCache(); diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart index 225d2fec6..9e70287cd 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -72,8 +72,6 @@ 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( Breadcrumb( @@ -98,21 +96,12 @@ class ITController { request: FullTextTranslationRequestModel( text: _itStartData!.text, tgtLang: choreographer.l1LangCode!, - srcLang: choreographer.l2LangCode, + srcLang: _itStartData!.langCode, userL1: choreographer.l1LangCode!, userL2: choreographer.l2LangCode!, ), ); sourceText = res.bestTranslation; - // } catch (err, stack) { - // debugger(when: kDebugMode); - // if (_itStartData?.text.isNotEmpty ?? false) { - // ErrorHandler.logError(e: err, s: stack); - // sourceText = _itStartData!.text; - // } else { - // rethrow; - // } - // } } // used 1) at very beginning (with custom input = null) diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 75a18ad74..14b3555a1 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -309,7 +309,10 @@ class MyAnalyticsController { recentConstructUses.addAll(constructLists.expand((e) => e)); //TODO - confirm that this is the correct construct content - debugger(when: kDebugMode && recentConstructUses.isNotEmpty); + debugger( + when: kDebugMode && + (recentPangeaMessageEvents.isNotEmpty || + recentActivityRecords.isNotEmpty)); await analyticsRoom.sendConstructsEvent( recentConstructUses, diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 28253d419..2f529e528 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -666,7 +666,6 @@ class PangeaMessageEvent { /// get construct uses of type vocab for the message List get _vocabUses { - debugger(); final List uses = []; // missing vital info so return. should not happen @@ -739,7 +738,6 @@ class PangeaMessageEvent { /// 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( diff --git a/lib/pangea/models/igc_text_data_model.dart b/lib/pangea/models/igc_text_data_model.dart index 6a3eec96e..fb9388514 100644 --- a/lib/pangea/models/igc_text_data_model.dart +++ b/lib/pangea/models/igc_text_data_model.dart @@ -1,5 +1,6 @@ import 'dart:developer'; +import 'package:fluffychat/pangea/controllers/language_detection_controller.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; import 'package:fluffychat/pangea/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/models/span_card_model.dart'; @@ -13,12 +14,11 @@ import 'package:matrix/matrix.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import '../constants/model_keys.dart'; -import 'language_detection_model.dart'; // import 'package:language_tool/language_tool.dart'; class IGCTextData { - List detections; + LanguageDetectionResponse detections; String originalInput; String? fullTextCorrection; List tokens; @@ -42,6 +42,17 @@ class IGCTextData { }); factory IGCTextData.fromJson(Map json) { + // changing this to allow for use of the LanguageDetectionResponse methods + // TODO - change API after we're sure all clients are updated. not urgent. + final LanguageDetectionResponse detections = + json[_detectionsKey] is Iterable + ? LanguageDetectionResponse.fromJson({ + "detections": json[_detectionsKey], + "full_text": json["original_input"], + }) + : LanguageDetectionResponse.fromJson( + json[_detectionsKey] as Map); + return IGCTextData( tokens: (json[_tokensKey] as Iterable) .map( @@ -59,12 +70,7 @@ class IGCTextData { .toList() .cast() : [], - detections: (json[_detectionsKey] as Iterable) - .map( - (e) => LanguageDetection.fromJson(e as Map), - ) - .toList() - .cast(), + detections: detections, originalInput: json["original_input"], fullTextCorrection: json["full_text_correction"], userL1: json[ModelKey.userL1], @@ -79,7 +85,7 @@ class IGCTextData { static const String _detectionsKey = "detections"; Map toJson() => { - _detectionsKey: detections.map((e) => e.toJson()).toList(), + _detectionsKey: detections.toJson(), "original_input": originalInput, "full_text_correction": fullTextCorrection, _tokensKey: tokens.map((e) => e.toJson()).toList(), @@ -90,6 +96,8 @@ class IGCTextData { "enable_igc": enableIGC, }; + String get detectedLanguage => detections.bestDetection().langCode; + // reconstruct fullText based on accepted match //update offsets in existing matches to reflect the change //if existing matches overlap with the accepted one, remove them?? diff --git a/lib/pangea/repo/igc_repo.dart b/lib/pangea/repo/igc_repo.dart index 5f281abe6..d3dbbcb6b 100644 --- a/lib/pangea/repo/igc_repo.dart +++ b/lib/pangea/repo/igc_repo.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:fluffychat/pangea/config/environment.dart'; +import 'package:fluffychat/pangea/controllers/language_detection_controller.dart'; import 'package:fluffychat/pangea/models/language_detection_model.dart'; import 'package:fluffychat/pangea/models/lemma.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; @@ -39,7 +40,10 @@ class IgcRepo { await Future.delayed(const Duration(seconds: 2)); final IGCTextData igcTextData = IGCTextData( - detections: [LanguageDetection(langCode: "en", confidence: 0.99)], + detections: LanguageDetectionResponse( + detections: [LanguageDetection(langCode: "en", confidence: 0.99)], + fullText: "This be a sample text", + ), tokens: [ PangeaToken( text: PangeaTokenText(content: "This", offset: 0, length: 4), From c6d3f36805eaf8140915b77f8012abcaedd67b08 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Sun, 30 Jun 2024 18:31:53 -0400 Subject: [PATCH 11/26] saving of tokens, changing of top language calculation, documentation --- lib/pages/chat/chat.dart | 4 -- lib/pages/chat/events/message.dart | 2 +- .../controllers/choreographer.dart | 46 +++++--------- lib/pangea/constants/model_keys.dart | 1 - .../language_detection_controller.dart | 13 ++-- .../controllers/my_analytics_controller.dart | 13 ++-- lib/pangea/enum/use_type.dart | 16 ----- .../events_extension.dart | 4 +- .../pangea_room_extension.dart | 3 - .../pangea_message_event.dart | 61 +++++++++++-------- .../analytics/summary_analytics_model.dart | 2 +- lib/pangea/models/igc_text_data_model.dart | 15 ++++- lib/pangea/models/lemma.dart | 7 +++ lib/pangea/utils/firebase_analytics.dart | 4 +- lib/pangea/widgets/chat/overlay_message.dart | 2 +- lib/pangea/widgets/new_group/vocab_list.dart | 11 ++-- 16 files changed, 96 insertions(+), 108 deletions(-) diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 13f09ffb6..5629d809e 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -16,7 +16,6 @@ import 'package:fluffychat/pages/chat/recording_dialog.dart'; import 'package:fluffychat/pages/chat_details/chat_details.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/enum/use_type.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/pangea_message_event.dart'; import 'package:fluffychat/pangea/models/choreo_record.dart'; @@ -586,7 +585,6 @@ class ChatController extends State PangeaMessageTokens? tokensSent, PangeaMessageTokens? tokensWritten, ChoreoRecord? choreo, - UseType? useType, }) async { // Pangea# if (sendController.text.trim().isEmpty) return; @@ -630,7 +628,6 @@ class ChatController extends State tokensSent: tokensSent, tokensWritten: tokensWritten, choreo: choreo, - useType: useType, ) .then( (String? msgEventId) async { @@ -644,7 +641,6 @@ class ChatController extends State GoogleAnalytics.sendMessage( room.id, room.classCode, - useType ?? UseType.un, ); if (msgEventId == null) { diff --git a/lib/pages/chat/events/message.dart b/lib/pages/chat/events/message.dart index c5756438c..3b2c1b2fb 100644 --- a/lib/pages/chat/events/message.dart +++ b/lib/pages/chat/events/message.dart @@ -470,7 +470,7 @@ class Message extends StatelessWidget { ?.showUseType ?? false) ...[ pangeaMessageEvent! - .useType + .msgUseType .iconView( context, textColor diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 42661b81e..3e7668323 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -24,7 +24,6 @@ import 'package:flutter/material.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import '../../../widgets/matrix.dart'; -import '../../enum/use_type.dart'; import '../../models/choreo_record.dart'; import '../../models/language_model.dart'; import '../../models/pangea_match_model.dart'; @@ -108,27 +107,16 @@ class Choreographer { originalSent: false, ) : null; - //TODO - confirm that IT is indeed making sure the message is in the user's L1 - - // if the message has not been processed to determine its language - // then run it through the language detection endpoint. If the detection - // confidence is high enough, use that language code as the message's language - // to save that pangea representation - // TODO - move this to somewhere such that the message can be cleared from the input field - // before the language detection is complete. Otherwise, user is going to be waiting - // in cases of slow internet or slow language detection - final String? originalSentLangCode = igc.igcTextData?.detectedLanguage; // TODO - why does both it and igc need to be enabled for choreo to be applicable? - final ChoreoRecord? applicableChoreo = - isITandIGCEnabled && igc.igcTextData != null ? choreoRecord : null; + // final ChoreoRecord? applicableChoreo = + // isITandIGCEnabled && igc.igcTextData != null ? choreoRecord : null; - final UseType useType = useTypeCalculator(applicableChoreo); - - // if tokens or language detection are not available, get them - // note that we probably need to move this to after we clear the input field - // or the user could experience some lag here. note that this call is being - // made after we've determined if we have an applicable choreo in order to + // if tokens or language detection are not available, we should get them + // notes + // 1) we probably need to move this to after we clear the input field + // or the user could experience some lag here. + // 2) that this call is being made after we've determined if we have an applicable choreo in order to // say whether correction was run on the message. we may eventually want // to edit the useType after if (igc.igcTextData?.tokens == null || @@ -137,26 +125,24 @@ class Choreographer { } final PangeaRepresentation originalSent = PangeaRepresentation( - langCode: originalSentLangCode ?? LanguageKeys.unknownLanguage, + langCode: + igc.igcTextData?.detectedLanguage ?? LanguageKeys.unknownLanguage, text: currentText, originalSent: true, originalWritten: originalWritten == null, ); - debugger(when: kDebugMode); + + final PangeaMessageTokens? tokensSent = igc.igcTextData?.tokens != null + ? PangeaMessageTokens(tokens: igc.igcTextData!.tokens) + : null; chatController.send( - // PTODO - turn this back on in conjunction with saving tokens - // we need to save those tokens as well, in order for exchanges to work - // properly. in an exchange, the other user will want // originalWritten: originalWritten, - originalSent: originalSent, - tokensSent: igc.igcTextData?.tokens != null - ? PangeaMessageTokens(tokens: igc.igcTextData!.tokens) - : null, + tokensSent: tokensSent, //TODO - save originalwritten tokens - choreo: applicableChoreo, - useType: useType, + // choreo: applicableChoreo, + choreo: choreoRecord, ); clear(); diff --git a/lib/pangea/constants/model_keys.dart b/lib/pangea/constants/model_keys.dart index ca59d89b7..372e72606 100644 --- a/lib/pangea/constants/model_keys.dart +++ b/lib/pangea/constants/model_keys.dart @@ -66,7 +66,6 @@ class ModelKey { static const String tokensSent = "tokens_sent"; static const String tokensWritten = "tokens_written"; static const String choreoRecord = "choreo_record"; - static const String useType = "use_type"; static const String baseDefinition = "base_definition"; static const String targetDefinition = "target_definition"; diff --git a/lib/pangea/controllers/language_detection_controller.dart b/lib/pangea/controllers/language_detection_controller.dart index 08f38ea2c..a3e07b0a3 100644 --- a/lib/pangea/controllers/language_detection_controller.dart +++ b/lib/pangea/controllers/language_detection_controller.dart @@ -76,15 +76,20 @@ class LanguageDetectionResponse { }; } - LanguageDetection get _bestDetection { + /// Return the highest confidence detection. + /// If there are no detections, the unknown language detection is returned. + LanguageDetection get highestConfidenceDetection { detections.sort((a, b) => b.confidence.compareTo(a.confidence)); return detections.firstOrNull ?? unknownLanguageDetection; } - LanguageDetection bestDetection({double? threshold}) => - _bestDetection.confidence >= + /// Returns the highest validated detection based on the confidence threshold. + /// If the highest confidence detection is below the threshold, the unknown language + /// detection is returned. + LanguageDetection highestValidatedDetection({double? threshold}) => + highestConfidenceDetection.confidence >= (threshold ?? languageDetectionConfidenceThreshold) - ? _bestDetection + ? highestConfidenceDetection : unknownLanguageDetection; } diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 14b3555a1..1ae6f2a5b 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -216,8 +216,6 @@ class MyAnalyticsController { .where((room) => !room.isSpace && !room.isAnalyticsRoom) .toList(); - final DateTime now = DateTime.now(); - // get the recent message events and activity records for each chat final List>> recentMsgFutures = []; final List>> recentActivityFutures = []; @@ -309,10 +307,13 @@ class MyAnalyticsController { recentConstructUses.addAll(constructLists.expand((e) => e)); //TODO - confirm that this is the correct construct content - debugger( - when: kDebugMode && - (recentPangeaMessageEvents.isNotEmpty || - recentActivityRecords.isNotEmpty)); + // debugger( + // when: kDebugMode, + // ); + // ; debugger( + // when: kDebugMode && + // (allRecentMessages.isNotEmpty || recentActivityRecords.isNotEmpty), + // ); await analyticsRoom.sendConstructsEvent( recentConstructUses, diff --git a/lib/pangea/enum/use_type.dart b/lib/pangea/enum/use_type.dart index 771b26220..56a4fa3b0 100644 --- a/lib/pangea/enum/use_type.dart +++ b/lib/pangea/enum/use_type.dart @@ -1,8 +1,6 @@ import 'package:flutter/material.dart'; - import 'package:flutter_gen/gen_l10n/l10n.dart'; -import '../models/choreo_record.dart'; import '../utils/bot_style.dart'; enum UseType { wa, ta, ga, un } @@ -93,17 +91,3 @@ extension UseTypeMethods on UseType { } } } - -UseType useTypeCalculator( - ChoreoRecord? choreoRecord, -) { - if (choreoRecord == null) { - return UseType.un; - } else if (choreoRecord.includedIT) { - return UseType.ta; - } else if (choreoRecord.hasAcceptedMatches) { - return UseType.ga; - } else { - return UseType.wa; - } -} diff --git a/lib/pangea/extensions/pangea_room_extension/events_extension.dart b/lib/pangea/extensions/pangea_room_extension/events_extension.dart index 0f40da5c7..ce9d3451c 100644 --- a/lib/pangea/extensions/pangea_room_extension/events_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/events_extension.dart @@ -229,7 +229,6 @@ extension EventsRoomExtension on Room { PangeaMessageTokens? tokensSent, PangeaMessageTokens? tokensWritten, ChoreoRecord? choreo, - UseType? useType, }) { // if (parseCommands) { // return client.parseAndRunCommand(this, message, @@ -247,7 +246,6 @@ extension EventsRoomExtension on Room { ModelKey.originalWritten: originalWritten?.toJson(), ModelKey.tokensSent: tokensSent?.toJson(), ModelKey.tokensWritten: tokensWritten?.toJson(), - ModelKey.useType: useType?.string, }; if (parseMarkdown) { final html = markdown( @@ -347,7 +345,7 @@ extension EventsRoomExtension on Room { RecentMessageRecord( eventId: event.eventId, chatId: id, - useType: pMsgEvent.useType, + useType: pMsgEvent.msgUseType, time: event.originServerTs, ), ); diff --git a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart index 78ecb9cc0..21f5abac5 100644 --- a/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/pangea_room_extension.dart @@ -34,7 +34,6 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import '../../../config/app_config.dart'; import '../../constants/pangea_event_types.dart'; -import '../../enum/use_type.dart'; import '../../models/choreo_record.dart'; import '../../models/representation_content_model.dart'; import '../client_extension/client_extension.dart'; @@ -181,7 +180,6 @@ extension PangeaRoom on Room { PangeaMessageTokens? tokensSent, PangeaMessageTokens? tokensWritten, ChoreoRecord? choreo, - UseType? useType, }) => _pangeaSendTextEvent( message, @@ -198,7 +196,6 @@ extension PangeaRoom on Room { tokensSent: tokensSent, tokensWritten: tokensWritten, choreo: choreo, - useType: useType, ); Future updateStateEvent(Event stateEvent) => diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index 2f529e528..e0820d665 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -37,7 +37,6 @@ class PangeaMessageEvent { late Event _event; final Timeline timeline; final bool ownMessage; - bool _isValidPangeaMessageEvent = true; PangeaMessageEvent({ required Event event, @@ -45,7 +44,7 @@ class PangeaMessageEvent { required this.ownMessage, }) { if (event.type != EventTypes.Message) { - _isValidPangeaMessageEvent = false; + debugger(when: kDebugMode); ErrorHandler.logError( m: "${event.type} should not be used to make a PangeaMessageEvent", ); @@ -548,7 +547,18 @@ class PangeaMessageEvent { originalWritten: false, ); - UseType get useType => useTypeCalculator(originalSent?.choreo); + UseType get msgUseType { + final ChoreoRecord? choreoRecord = originalSent?.choreo; + if (choreoRecord == null) { + return UseType.un; + } else if (choreoRecord.includedIT) { + return UseType.ta; + } else if (choreoRecord.hasAcceptedMatches) { + return UseType.ga; + } else { + return UseType.wa; + } + } bool get showUseType => !ownMessage && @@ -662,30 +672,7 @@ class PangeaMessageEvent { /// all construct uses for the message, including vocab and grammar List get allConstructUses => - [..._grammarConstructUses, ..._vocabUses]; - - /// get construct uses of type vocab for the message - List get _vocabUses { - final List uses = []; - - // missing vital info so return. should not happen - if (event.roomId == null) { - debugger(when: kDebugMode); - return uses; - } - - // 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)); - } - } - - // add construct uses related to IT use - uses.addAll(_itStepsToConstructUses); - - return uses; - } + [..._grammarConstructUses, ..._vocabUses, ..._itStepsToConstructUses]; /// Returns a list of [OneConstructUse] from itSteps for which the continuance /// was selected or ignored. Correct selections are considered in the tokens @@ -696,6 +683,8 @@ class PangeaMessageEvent { /// are actually in the final message. List get _itStepsToConstructUses { final List uses = []; + if (originalSent?.choreo == null) return uses; + for (final itStep in originalSent!.choreo!.itSteps) { for (final continuance in itStep.continuances) { // this seems to always be false for continuances right now @@ -728,6 +717,24 @@ class PangeaMessageEvent { return uses; } + /// get construct uses of type vocab for the message + List get _vocabUses { + final List uses = []; + + // missing vital info so return + if (event.roomId == null || originalSent?.tokens == null) { + debugger(when: kDebugMode); + return uses; + } + + // for each token, record whether selected in ga, ta, or wa + for (final token in originalSent!.tokens!) { + uses.addAll(_getVocabUseForToken(token)); + } + + return uses; + } + /// Returns a list of [OneConstructUse] objects for the given [token] /// If there is no [originalSent] or [originalSent.choreo], the [token] is /// considered to be a [ConstructUseTypeEnum.wa] as long as it matches the target language. diff --git a/lib/pangea/models/analytics/summary_analytics_model.dart b/lib/pangea/models/analytics/summary_analytics_model.dart index b09d0a870..0b8e4b27c 100644 --- a/lib/pangea/models/analytics/summary_analytics_model.dart +++ b/lib/pangea/models/analytics/summary_analytics_model.dart @@ -50,7 +50,7 @@ class SummaryAnalyticsModel extends AnalyticsModel { (msg) => RecentMessageRecord( eventId: msg.eventId, chatId: msg.room.id, - useType: msg.useType, + useType: msg.msgUseType, time: msg.originServerTs, ), ) diff --git a/lib/pangea/models/igc_text_data_model.dart b/lib/pangea/models/igc_text_data_model.dart index fb9388514..442bf4a60 100644 --- a/lib/pangea/models/igc_text_data_model.dart +++ b/lib/pangea/models/igc_text_data_model.dart @@ -51,7 +51,8 @@ class IGCTextData { "full_text": json["original_input"], }) : LanguageDetectionResponse.fromJson( - json[_detectionsKey] as Map); + json[_detectionsKey] as Map, + ); return IGCTextData( tokens: (json[_tokensKey] as Iterable) @@ -96,7 +97,17 @@ class IGCTextData { "enable_igc": enableIGC, }; - String get detectedLanguage => detections.bestDetection().langCode; + /// if we haven't run IGC or IT or there are no matches, we use the highest validated detection + /// from [LanguageDetectionResponse.highestValidatedDetection] + /// if we have run igc/it and there are no matches, we can relax the threshold + /// and use the highest confidence detection + String get detectedLanguage { + if (!(enableIGC && enableIT) || matches.isNotEmpty) { + return detections.highestValidatedDetection().langCode; + } else { + return detections.highestConfidenceDetection.langCode; + } + } // reconstruct fullText based on accepted match //update offsets in existing matches to reflect the change diff --git a/lib/pangea/models/lemma.dart b/lib/pangea/models/lemma.dart index 2ad0b2950..56f23d876 100644 --- a/lib/pangea/models/lemma.dart +++ b/lib/pangea/models/lemma.dart @@ -1,6 +1,13 @@ +/// Represents a lemma object class Lemma { + /// [text] ex "ir" - text of the lemma of the word final String text; + + /// [form] ex "vamos" - conjugated form of the lemma and as it appeared in some original text final String form; + + /// [saveVocab] true - whether to save the lemma to the user's vocabulary + /// vocab that are not saved: emails, urls, numbers, punctuation, etc. final bool saveVocab; Lemma({required this.text, required this.saveVocab, required this.form}); diff --git a/lib/pangea/utils/firebase_analytics.dart b/lib/pangea/utils/firebase_analytics.dart index 59485d35b..323a7a172 100644 --- a/lib/pangea/utils/firebase_analytics.dart +++ b/lib/pangea/utils/firebase_analytics.dart @@ -4,7 +4,6 @@ import 'package:fluffychat/pangea/controllers/subscription_controller.dart'; import 'package:flutter/widgets.dart'; import '../../config/firebase_options.dart'; -import '../enum/use_type.dart'; // PageRoute import @@ -90,13 +89,12 @@ class GoogleAnalytics { logEvent('join_group', parameters: {'group_id': classCode}); } - static sendMessage(String chatRoomId, String classCode, UseType useType) { + static sendMessage(String chatRoomId, String classCode) { logEvent( 'sent_message', parameters: { "chat_id": chatRoomId, 'group_id': classCode, - "message_type": useType.toString(), }, ); } diff --git a/lib/pangea/widgets/chat/overlay_message.dart b/lib/pangea/widgets/chat/overlay_message.dart index d7c99f07b..5f3d46c7e 100644 --- a/lib/pangea/widgets/chat/overlay_message.dart +++ b/lib/pangea/widgets/chat/overlay_message.dart @@ -166,7 +166,7 @@ class OverlayMessage extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ if (pangeaMessageEvent.showUseType) ...[ - pangeaMessageEvent.useType.iconView( + pangeaMessageEvent.msgUseType.iconView( context, textColor.withAlpha(164), ), diff --git a/lib/pangea/widgets/new_group/vocab_list.dart b/lib/pangea/widgets/new_group/vocab_list.dart index 240e99a85..67089145e 100644 --- a/lib/pangea/widgets/new_group/vocab_list.dart +++ b/lib/pangea/widgets/new_group/vocab_list.dart @@ -1,9 +1,8 @@ -import 'package:flutter/material.dart'; - -import 'package:flutter_gen/gen_l10n/l10n.dart'; - import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + import '../../models/chat_topic_model.dart'; import '../../models/lemma.dart'; import '../../repo/topic_data_repo.dart'; @@ -76,7 +75,7 @@ class ChatVocabularyList extends StatelessWidget { for (final word in topic.vocab) Chip( labelStyle: Theme.of(context).textTheme.bodyMedium, - label: Text(word.form), + label: Text(word.text), onDeleted: () { onChanged(topic.vocab..remove(word)); }, @@ -464,7 +463,7 @@ class PromptsFieldState extends State { // button to call API ElevatedButton.icon( - icon: BotFace( + icon: const BotFace( width: 50.0, expression: BotExpression.idle, ), From 61b7583ad259bd373aeef344893a52eb4a09e028 Mon Sep 17 00:00:00 2001 From: Brord van Wierst Date: Mon, 1 Jul 2024 16:09:06 +0200 Subject: [PATCH 12/26] added pub get for sentry --- .github/workflows/main_deploy.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/main_deploy.yaml b/.github/workflows/main_deploy.yaml index 6bda0c1db..46f909e1f 100644 --- a/.github/workflows/main_deploy.yaml +++ b/.github/workflows/main_deploy.yaml @@ -79,5 +79,7 @@ jobs: with: name: web path: build/web + - name: Update packages + run: flutter pub get - name: Update sentry run: flutter packages pub run sentry_dart_plugin From e6fa2b1b4b6c361fdf08c370eb4ad469ec689a35 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 1 Jul 2024 11:57:17 -0400 Subject: [PATCH 13/26] changed messages since update back to 10, re-enabled getting tokens after accepting matching in IGC, and allowed saving of initial empty analytics event --- .../controllers/choreographer.dart | 2 +- .../controllers/igc_controller.dart | 114 +++++++++--------- .../controllers/my_analytics_controller.dart | 10 +- .../room_analytics_extension.dart | 3 - .../practice_activity_content.dart | 66 +--------- 5 files changed, 64 insertions(+), 131 deletions(-) diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 3e7668323..66e11808b 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -186,7 +186,7 @@ class Choreographer { // 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(); + 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 diff --git a/lib/pangea/choreographer/controllers/igc_controller.dart b/lib/pangea/choreographer/controllers/igc_controller.dart index fed4a3167..533b36c83 100644 --- a/lib/pangea/choreographer/controllers/igc_controller.dart +++ b/lib/pangea/choreographer/controllers/igc_controller.dart @@ -7,9 +7,11 @@ import 'package:fluffychat/pangea/choreographer/controllers/span_data_controller import 'package:fluffychat/pangea/models/igc_text_data_model.dart'; import 'package:fluffychat/pangea/models/pangea_match_model.dart'; import 'package:fluffychat/pangea/repo/igc_repo.dart'; +import 'package:fluffychat/pangea/repo/tokens_repo.dart'; import 'package:fluffychat/pangea/widgets/igc/span_card.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import '../../models/span_card_model.dart'; import '../../utils/error_handler.dart'; @@ -32,12 +34,6 @@ class IgcController { 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 = $onlyTokensAndLanguageDetection', @@ -87,61 +83,61 @@ class IgcController { } } - // Future justGetTokensAndAddThemToIGCTextData() async { - // try { - // if (igcTextData == null) { - // debugger(when: kDebugMode); - // choreographer.getLanguageHelp(); - // return; - // } - // igcTextData!.loading = true; - // choreographer.startLoading(); - // if (igcTextData!.originalInput != choreographer.textController.text) { - // debugger(when: kDebugMode); - // ErrorHandler.logError( - // m: "igcTextData fullText does not match current text", - // s: StackTrace.current, - // data: igcTextData!.toJson(), - // ); - // } + Future justGetTokensAndAddThemToIGCTextData() async { + try { + if (igcTextData == null) { + debugger(when: kDebugMode); + choreographer.getLanguageHelp(); + return; + } + igcTextData!.loading = true; + choreographer.startLoading(); + if (igcTextData!.originalInput != choreographer.textController.text) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "igcTextData fullText does not match current text", + s: StackTrace.current, + data: igcTextData!.toJson(), + ); + } - // if (choreographer.l1LangCode == null || - // choreographer.l2LangCode == null) { - // debugger(when: kDebugMode); - // ErrorHandler.logError( - // m: "l1LangCode and/or l2LangCode is null", - // s: StackTrace.current, - // data: { - // "l1LangCode": choreographer.l1LangCode, - // "l2LangCode": choreographer.l2LangCode, - // }, - // ); - // return; - // } + if (choreographer.l1LangCode == null || + choreographer.l2LangCode == null) { + debugger(when: kDebugMode); + ErrorHandler.logError( + m: "l1LangCode and/or l2LangCode is null", + s: StackTrace.current, + data: { + "l1LangCode": choreographer.l1LangCode, + "l2LangCode": choreographer.l2LangCode, + }, + ); + return; + } - // final TokensResponseModel res = await TokensRepo.tokenize( - // await choreographer.pangeaController.userController.accessToken, - // TokensRequestModel( - // fullText: igcTextData!.originalInput, - // userL1: choreographer.l1LangCode!, - // userL2: choreographer.l2LangCode!, - // ), - // ); - // igcTextData?.tokens = res.tokens; - // } catch (err, stack) { - // debugger(when: kDebugMode); - // choreographer.errorService.setError( - // ChoreoError(type: ChoreoErrorType.unknown, raw: err), - // ); - // Sentry.addBreadcrumb( - // Breadcrumb.fromJson({"igctextDdata": igcTextData?.toJson()}), - // ); - // ErrorHandler.logError(e: err, s: stack); - // } finally { - // igcTextData?.loading = false; - // choreographer.stopLoading(); - // } - // } + final TokensResponseModel res = await TokensRepo.tokenize( + await choreographer.pangeaController.userController.accessToken, + TokensRequestModel( + fullText: igcTextData!.originalInput, + userL1: choreographer.l1LangCode!, + userL2: choreographer.l2LangCode!, + ), + ); + igcTextData?.tokens = res.tokens; + } catch (err, stack) { + debugger(when: kDebugMode); + choreographer.errorService.setError( + ChoreoError(type: ChoreoErrorType.unknown, raw: err), + ); + Sentry.addBreadcrumb( + Breadcrumb.fromJson({"igctextDdata": igcTextData?.toJson()}), + ); + ErrorHandler.logError(e: err, s: stack); + } finally { + igcTextData?.loading = false; + choreographer.stopLoading(); + } + } void showFirstMatch(BuildContext context) { if (igcTextData == null || igcTextData!.matches.isEmpty) { diff --git a/lib/pangea/controllers/my_analytics_controller.dart b/lib/pangea/controllers/my_analytics_controller.dart index 1ae6f2a5b..535af6b8b 100644 --- a/lib/pangea/controllers/my_analytics_controller.dart +++ b/lib/pangea/controllers/my_analytics_controller.dart @@ -24,7 +24,7 @@ class MyAnalyticsController { /// the max number of messages that will be cached before /// an automatic update is triggered - final int _maxMessagesCached = 1; + final int _maxMessagesCached = 10; /// the number of minutes before an automatic update is triggered final int _minutesBeforeUpdate = 5; @@ -315,8 +315,10 @@ class MyAnalyticsController { // (allRecentMessages.isNotEmpty || recentActivityRecords.isNotEmpty), // ); - await analyticsRoom.sendConstructsEvent( - recentConstructUses, - ); + if (recentConstructUses.isNotEmpty) { + await analyticsRoom.sendConstructsEvent( + recentConstructUses, + ); + } } } diff --git a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart index 34370306e..a27526a2b 100644 --- a/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart +++ b/lib/pangea/extensions/pangea_room_extension/room_analytics_extension.dart @@ -253,8 +253,6 @@ extension AnalyticsRoomExtension on Room { Future sendSummaryAnalyticsEvent( List records, ) async { - if (records.isEmpty) return null; - final SummaryAnalyticsModel analyticsModel = SummaryAnalyticsModel( messages: records, ); @@ -268,7 +266,6 @@ extension AnalyticsRoomExtension on Room { Future sendConstructsEvent( List uses, ) async { - if (uses.isEmpty) return null; final ConstructAnalyticsModel constructsModel = ConstructAnalyticsModel( uses: uses, ); diff --git a/lib/pangea/widgets/practice_activity/practice_activity_content.dart b/lib/pangea/widgets/practice_activity/practice_activity_content.dart index 9bdc95eea..6de31829c 100644 --- a/lib/pangea/widgets/practice_activity/practice_activity_content.dart +++ b/lib/pangea/widgets/practice_activity/practice_activity_content.dart @@ -1,7 +1,5 @@ import 'package:fluffychat/pangea/enum/activity_type_enum.dart'; import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_event.dart'; -import 'package:fluffychat/pangea/matrix_event_wrappers/practice_activity_record_event.dart'; -import 'package:fluffychat/pangea/models/practice_activities.dart/practice_activity_record_model.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/multiple_choice_activity.dart'; import 'package:fluffychat/pangea/widgets/practice_activity/practice_activity_card.dart'; import 'package:flutter/material.dart'; @@ -18,70 +16,10 @@ class PracticeActivity extends StatefulWidget { }); @override - MessagePracticeActivityContentState createState() => - MessagePracticeActivityContentState(); + PracticeActivityContentState createState() => PracticeActivityContentState(); } -class MessagePracticeActivityContentState extends State { - int? selectedChoiceIndex; - PracticeActivityRecordModel? recordModel; - bool recordSubmittedThisSession = false; - bool recordSubmittedPreviousSession = false; - - PracticeActivityEvent get practiceEvent => widget.practiceEvent; - - @override - void initState() { - super.initState(); - initalizeActivity(); - } - - @override - void didUpdateWidget(covariant PracticeActivity oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.practiceEvent.event.eventId != - widget.practiceEvent.event.eventId) { - initalizeActivity(); - } - } - - void initalizeActivity() { - final PracticeActivityRecordEvent? recordEvent = - widget.practiceEvent.userRecord; - if (recordEvent?.record == null) { - recordModel = PracticeActivityRecordModel( - question: - widget.practiceEvent.practiceActivity.multipleChoice!.question, - ); - } else { - recordModel = recordEvent!.record; - - //Note that only MultipleChoice activities will have this so we probably should move this logic to the MultipleChoiceActivity widget - selectedChoiceIndex = recordModel?.latestResponse?.text != null - ? widget.practiceEvent.practiceActivity.multipleChoice - ?.choiceIndex(recordModel!.latestResponse!.text!) - : null; - - recordSubmittedPreviousSession = true; - recordSubmittedThisSession = true; - } - setState(() {}); - } - - void updateChoice(int index) { - setState(() { - selectedChoiceIndex = index; - recordModel!.addResponse( - score: widget.practiceEvent.practiceActivity.multipleChoice! - .isCorrect(index) - ? 1 - : 0, - text: widget - .practiceEvent.practiceActivity.multipleChoice!.choices[index], - ); - }); - } - +class PracticeActivityContentState extends State { Widget get activityWidget { switch (widget.practiceEvent.practiceActivity.activityType) { case ActivityTypeEnum.multipleChoice: From 275e5c94f370253ae9ea5e89d78c25e76ba78acc Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 1 Jul 2024 12:04:11 -0400 Subject: [PATCH 14/26] formatted pangea_rich_text file --- lib/pangea/widgets/igc/pangea_rich_text.dart | 108 +++++++++++-------- 1 file changed, 63 insertions(+), 45 deletions(-) diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index 7391edb05..451423bb2 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -56,56 +56,74 @@ class PangeaRichTextState extends State { setTextSpan(); } -void _setTextSpan(String newTextSpan) { - try { - if (!mounted) return; // Early exit if the widget is no longer in the tree + void _setTextSpan(String newTextSpan) { + try { + if (!mounted) return; // Early exit if the widget is no longer in the tree - widget.toolbarController?.toolbar?.textSelection.setMessageText( - newTextSpan, - ); - setState(() { - textSpan = newTextSpan; - }); - } catch (error, stackTrace) { - ErrorHandler.logError(e: error, s: stackTrace, m: "Error setting text span in PangeaRichText"); - } -} - -void setTextSpan() { - if (_fetchingRepresentation) { - _setTextSpan(widget.pangeaMessageEvent.event.getDisplayEvent(widget.pangeaMessageEvent.timeline).body); - return; + widget.toolbarController?.toolbar?.textSelection.setMessageText( + newTextSpan, + ); + setState(() { + textSpan = newTextSpan; + }); + } catch (error, stackTrace) { + ErrorHandler.logError( + e: error, + s: stackTrace, + m: "Error setting text span in PangeaRichText", + ); + } } - if (widget.pangeaMessageEvent.eventId.contains("webdebug")) { - debugger(when: kDebugMode); + void setTextSpan() { + if (_fetchingRepresentation) { + _setTextSpan( + widget.pangeaMessageEvent.event + .getDisplayEvent(widget.pangeaMessageEvent.timeline) + .body, + ); + return; + } + + if (widget.pangeaMessageEvent.eventId.contains("webdebug")) { + debugger(when: kDebugMode); + } + + repEvent = widget.pangeaMessageEvent + .representationByLanguage( + widget.pangeaMessageEvent.messageDisplayLangCode, + ) + ?.content; + + if (repEvent == null) { + setState(() => _fetchingRepresentation = true); + widget.pangeaMessageEvent + .representationByLanguageGlobal( + langCode: widget.pangeaMessageEvent.messageDisplayLangCode, + ) + .onError( + (error, stackTrace) => ErrorHandler.logError( + e: error, + s: stackTrace, + m: "Error fetching representation", + ), + ) + .then((event) { + if (!mounted) return; + repEvent = event; + _setTextSpan(repEvent?.text ?? widget.pangeaMessageEvent.body); + }).whenComplete(() { + if (mounted) { + setState(() => _fetchingRepresentation = false); + } + }); + + _setTextSpan(widget.pangeaMessageEvent.body); + } else { + _setTextSpan(repEvent!.text); + } } - repEvent = widget.pangeaMessageEvent - .representationByLanguage(widget.pangeaMessageEvent.messageDisplayLangCode) - ?.content; - - if (repEvent == null) { - setState(() => _fetchingRepresentation = true); - widget.pangeaMessageEvent - .representationByLanguageGlobal(langCode: widget.pangeaMessageEvent.messageDisplayLangCode) - .onError((error, stackTrace) => ErrorHandler.logError(e: error, s: stackTrace, m: "Error fetching representation")) - .then((event) { - if (!mounted) return; - repEvent = event; - _setTextSpan(repEvent?.text ?? widget.pangeaMessageEvent.body); - }).whenComplete(() { - if (mounted) { - setState(() => _fetchingRepresentation = false); - } - }); - - _setTextSpan(widget.pangeaMessageEvent.body); - } else { - _setTextSpan(repEvent!.text); - } -} - @override Widget build(BuildContext context) { if (blur > 0) { From f2c1a4579c7842500aa2b271b6cd35ead4c63f13 Mon Sep 17 00:00:00 2001 From: bluearevalo <90929912+bluearevalo@users.noreply.github.com> Date: Mon, 1 Jul 2024 14:45:42 -0400 Subject: [PATCH 15/26] reformatted, added PangeaWarningError, and commented back .env in pubsec --- lib/pangea/widgets/igc/pangea_rich_text.dart | 35 ++++++++------------ pubspec.yaml | 4 +-- 2 files changed, 16 insertions(+), 23 deletions(-) diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index 451423bb2..8a02c7a74 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -68,20 +68,17 @@ class PangeaRichTextState extends State { }); } catch (error, stackTrace) { ErrorHandler.logError( - e: error, - s: stackTrace, - m: "Error setting text span in PangeaRichText", - ); + e: PangeaWarningError(error), + s: stackTrace, + m: "Error setting text span in PangeaRichText"); } } void setTextSpan() { if (_fetchingRepresentation) { - _setTextSpan( - widget.pangeaMessageEvent.event - .getDisplayEvent(widget.pangeaMessageEvent.timeline) - .body, - ); + _setTextSpan(widget.pangeaMessageEvent.event + .getDisplayEvent(widget.pangeaMessageEvent.timeline) + .body); return; } @@ -91,24 +88,20 @@ class PangeaRichTextState extends State { repEvent = widget.pangeaMessageEvent .representationByLanguage( - widget.pangeaMessageEvent.messageDisplayLangCode, - ) + widget.pangeaMessageEvent.messageDisplayLangCode) ?.content; if (repEvent == null) { setState(() => _fetchingRepresentation = true); widget.pangeaMessageEvent .representationByLanguageGlobal( - langCode: widget.pangeaMessageEvent.messageDisplayLangCode, - ) - .onError( - (error, stackTrace) => ErrorHandler.logError( - e: error, - s: stackTrace, - m: "Error fetching representation", - ), - ) - .then((event) { + langCode: widget.pangeaMessageEvent.messageDisplayLangCode) + .onError((error, stackTrace) { + ErrorHandler.logError( + e: PangeaWarningError(error), + s: stackTrace, + m: "Error fetching representation"); + }).then((event) { if (!mounted) return; repEvent = event; _setTextSpan(repEvent?.text ?? widget.pangeaMessageEvent.body); diff --git a/pubspec.yaml b/pubspec.yaml index 85cfeb074..ce6ef99dc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -148,8 +148,8 @@ flutter: # #Pangea # uncomment this to enable mobile builds # causes error with github actions - - .env - - assets/.env + # - .env + # - assets/.env - assets/pangea/ - assets/pangea/bot_faces/ # Pangea# From 183247035dce1bc407f8677cd07c34d692d06454 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 1 Jul 2024 15:06:11 -0400 Subject: [PATCH 16/26] removed view selection page in analytics --- assets/l10n/intl_en.arb | 3 +- lib/config/routes.dart | 53 +-- lib/pangea/enum/bar_chart_view_enum.dart | 11 - .../analytics/analytics_language_button.dart | 4 +- .../pages/analytics/analytics_list_tile.dart | 5 +- .../analytics/analytics_view_button.dart | 49 +++ .../pages/analytics/base_analytics.dart | 12 +- .../pages/analytics/base_analytics_view.dart | 383 +++++++----------- .../space_analytics/space_analytics.dart | 4 +- .../student_analytics/student_analytics.dart | 4 +- 10 files changed, 224 insertions(+), 304 deletions(-) create mode 100644 lib/pangea/pages/analytics/analytics_view_button.dart diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 964107cf8..7f44188a6 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -4084,5 +4084,6 @@ "@interactiveTranslatorAutoPlayDesc": { "type": "text", "placeholders": {} - } + }, + "changeAnalyticsView": "Change Analytics View" } \ No newline at end of file diff --git a/lib/config/routes.dart b/lib/config/routes.dart index eba1ae511..51c631ff3 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -170,31 +170,11 @@ abstract class AppRoutes { pageBuilder: (context, state) => defaultPageBuilder( context, state, - const StudentAnalyticsPage(), + const StudentAnalyticsPage( + selectedView: BarChartViewSelection.messages, + ), ), redirect: loggedOutRedirect, - routes: [ - GoRoute( - path: 'messages', - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - const StudentAnalyticsPage( - selectedView: BarChartViewSelection.messages, - ), - ), - ), - GoRoute( - path: 'errors', - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - const StudentAnalyticsPage( - selectedView: BarChartViewSelection.grammar, - ), - ), - ), - ], ), GoRoute( path: 'analytics', @@ -207,34 +187,13 @@ abstract class AppRoutes { routes: [ GoRoute( path: ':spaceid', - redirect: loggedOutRedirect, pageBuilder: (context, state) => defaultPageBuilder( context, state, - const SpaceAnalyticsPage(), + const SpaceAnalyticsPage( + selectedView: BarChartViewSelection.messages, + ), ), - routes: [ - GoRoute( - path: 'messages', - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - const SpaceAnalyticsPage( - selectedView: BarChartViewSelection.messages, - ), - ), - ), - GoRoute( - path: 'errors', - pageBuilder: (context, state) => defaultPageBuilder( - context, - state, - const SpaceAnalyticsPage( - selectedView: BarChartViewSelection.grammar, - ), - ), - ), - ], ), ], ), diff --git a/lib/pangea/enum/bar_chart_view_enum.dart b/lib/pangea/enum/bar_chart_view_enum.dart index 3fe812634..aba0652af 100644 --- a/lib/pangea/enum/bar_chart_view_enum.dart +++ b/lib/pangea/enum/bar_chart_view_enum.dart @@ -29,15 +29,4 @@ extension BarChartViewSelectionExtension on BarChartViewSelection { return Icons.spellcheck_outlined; } } - - String get route { - switch (this) { - case BarChartViewSelection.messages: - return 'messages'; - // case BarChartViewSelection.vocab: - // return 'vocab'; - case BarChartViewSelection.grammar: - return 'errors'; - } - } } diff --git a/lib/pangea/pages/analytics/analytics_language_button.dart b/lib/pangea/pages/analytics/analytics_language_button.dart index 2c3923fb4..08b3220b0 100644 --- a/lib/pangea/pages/analytics/analytics_language_button.dart +++ b/lib/pangea/pages/analytics/analytics_language_button.dart @@ -34,9 +34,7 @@ class AnalyticsLanguageButton extends StatelessWidget { }).toList(), child: TextButton.icon( label: Text( - L10n.of(context)!.languageButtonLabel( - value.getDisplayName(context) ?? value.langCode, - ), + value.getDisplayName(context) ?? value.langCode, style: TextStyle( color: Theme.of(context).colorScheme.onSurface, ), diff --git a/lib/pangea/pages/analytics/analytics_list_tile.dart b/lib/pangea/pages/analytics/analytics_list_tile.dart index 8b1e1ba49..a49ef4cb4 100644 --- a/lib/pangea/pages/analytics/analytics_list_tile.dart +++ b/lib/pangea/pages/analytics/analytics_list_tile.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:fluffychat/pangea/controllers/pangea_controller.dart'; -import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart'; import 'package:fluffychat/pangea/extensions/pangea_room_extension/pangea_room_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; @@ -141,9 +140,7 @@ class AnalyticsListTileState extends State { return; } if ((room?.isSpace ?? false) && widget.allowNavigateOnSelect) { - final String selectedView = - widget.controller!.widget.selectedView!.route; - context.go('/rooms/analytics/${room!.id}/$selectedView'); + context.go('/rooms/analytics/${room!.id}'); return; } widget.onTap(widget.selected); diff --git a/lib/pangea/pages/analytics/analytics_view_button.dart b/lib/pangea/pages/analytics/analytics_view_button.dart new file mode 100644 index 000000000..c98bf47fc --- /dev/null +++ b/lib/pangea/pages/analytics/analytics_view_button.dart @@ -0,0 +1,49 @@ +import 'package:fluffychat/pangea/enum/bar_chart_view_enum.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/l10n.dart'; + +class AnalyticsViewButton extends StatelessWidget { + final BarChartViewSelection value; + final void Function(BarChartViewSelection) onChange; + const AnalyticsViewButton({ + super.key, + required this.value, + required this.onChange, + }); + + @override + Widget build(BuildContext context) { + return PopupMenuButton( + tooltip: L10n.of(context)!.changeAnalyticsView, + initialValue: value, + onSelected: (BarChartViewSelection? view) { + if (view == null) { + debugPrint("when is view null?"); + return; + } + onChange(view); + }, + itemBuilder: (BuildContext context) => BarChartViewSelection.values + .map>( + (BarChartViewSelection view) { + return PopupMenuItem( + value: view, + child: Text(view.string(context)), + ); + }).toList(), + child: TextButton.icon( + label: Text( + value.string(context), + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + icon: Icon( + value.icon, + color: Theme.of(context).colorScheme.onSurface, + ), + onPressed: null, + ), + ); + } +} diff --git a/lib/pangea/pages/analytics/base_analytics.dart b/lib/pangea/pages/analytics/base_analytics.dart index 146a5ac70..cd41d2312 100644 --- a/lib/pangea/pages/analytics/base_analytics.dart +++ b/lib/pangea/pages/analytics/base_analytics.dart @@ -20,7 +20,7 @@ import '../../models/analytics/chart_analytics_model.dart'; class BaseAnalyticsPage extends StatefulWidget { final String pageTitle; final List tabs; - final BarChartViewSelection? selectedView; + final BarChartViewSelection selectedView; final AnalyticsSelected defaultSelected; final AnalyticsSelected? alwaysSelected; @@ -33,7 +33,7 @@ class BaseAnalyticsPage extends StatefulWidget { required this.tabs, required this.alwaysSelected, required this.defaultSelected, - this.selectedView, + required this.selectedView, this.myAnalyticsController, targetLanguages, }) : targetLanguages = (targetLanguages?.isNotEmpty ?? false) @@ -50,6 +50,7 @@ class BaseAnalyticsController extends State { String? currentLemma; ChartAnalyticsModel? chartData; StreamController refreshStream = StreamController.broadcast(); + BarChartViewSelection currentView = BarChartViewSelection.messages; bool isSelected(String chatOrStudentId) => chatOrStudentId == selected?.id; @@ -63,6 +64,7 @@ class BaseAnalyticsController extends State { @override void initState() { super.initState(); + currentView = widget.selectedView; if (widget.defaultSelected.type == AnalyticsEntryType.student) { runFirstRefresh(); } @@ -168,6 +170,12 @@ class BaseAnalyticsController extends State { refreshStream.add(false); } + Future toggleView(BarChartViewSelection view) async { + currentView = view; + await setChartData(); + refreshStream.add(false); + } + void setCurrentLemma(String? lemma) { currentLemma = lemma; setState(() {}); diff --git a/lib/pangea/pages/analytics/base_analytics_view.dart b/lib/pangea/pages/analytics/base_analytics_view.dart index cca0c7f4e..13e49aa38 100644 --- a/lib/pangea/pages/analytics/base_analytics_view.dart +++ b/lib/pangea/pages/analytics/base_analytics_view.dart @@ -5,6 +5,7 @@ import 'package:fluffychat/pangea/enum/construct_type_enum.dart'; import 'package:fluffychat/pangea/enum/time_span.dart'; import 'package:fluffychat/pangea/pages/analytics/analytics_language_button.dart'; import 'package:fluffychat/pangea/pages/analytics/analytics_list_tile.dart'; +import 'package:fluffychat/pangea/pages/analytics/analytics_view_button.dart'; import 'package:fluffychat/pangea/pages/analytics/base_analytics.dart'; import 'package:fluffychat/pangea/pages/analytics/construct_list.dart'; import 'package:fluffychat/pangea/pages/analytics/messages_bar_chart.dart'; @@ -24,11 +25,7 @@ class BaseAnalyticsView extends StatelessWidget { final BaseAnalyticsController controller; Widget chartView(BuildContext context) { - if (controller.widget.selectedView == null) { - return const SizedBox(); - } - - switch (controller.widget.selectedView!) { + switch (controller.currentView) { case BarChartViewSelection.messages: return MessagesBarChart( chartAnalytics: controller.chartData, @@ -75,27 +72,13 @@ class BaseAnalyticsView extends StatelessWidget { if (controller.activeSpace != null) TextSpan( text: controller.activeSpace!.getLocalizedDisplayname(), - style: const TextStyle(decoration: TextDecoration.underline), - recognizer: TapGestureRecognizer() - ..onTap = () { - if (controller.widget.selectedView == null) return; - String route = - "/rooms/${controller.widget.defaultSelected.type.route}"; - if (controller.widget.defaultSelected.type == - AnalyticsEntryType.space) { - route += "/${controller.widget.defaultSelected.id}"; - } - context.go(route); - }, - ), - if (controller.widget.selectedView != null) - const TextSpan( - text: " > ", - ), - if (controller.widget.selectedView != null) - TextSpan( - text: controller.widget.selectedView!.string(context), ), + const TextSpan( + text: " > ", + ), + TextSpan( + text: controller.currentView.string(context), + ), ], ), overflow: TextOverflow.ellipsis, @@ -104,220 +87,156 @@ class BaseAnalyticsView extends StatelessWidget { ), body: MaxWidthBody( withScrolling: false, - child: controller.widget.selectedView != null - ? Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - // if (controller.widget.defaultSelected.type == - // AnalyticsEntryType.student) - // IconButton( - // icon: const Icon(Icons.refresh), - // onPressed: controller.onRefresh, - // tooltip: L10n.of(context)!.refresh, - // ), - TimeSpanMenuButton( - value: controller.currentTimeSpan, - onChange: (TimeSpan value) => - controller.toggleTimeSpan(context, value), - ), - AnalyticsLanguageButton( - value: controller - .pangeaController.analytics.currentAnalyticsLang, - onChange: (lang) => controller.toggleSpaceLang(lang), - languages: controller.widget.targetLanguages, - ), - ], - ), - Expanded( - flex: 1, - child: chartView(context), - ), - Expanded( - flex: 1, - child: DefaultTabController( - length: 2, - child: Column( - children: [ - TabBar( - tabs: [ - ...controller.widget.tabs.map( - (tab) => Tab( - icon: Icon( - tab.icon, - color: Theme.of(context) - .colorScheme - .onSurfaceVariant, + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TimeSpanMenuButton( + value: controller.currentTimeSpan, + onChange: (TimeSpan value) => + controller.toggleTimeSpan(context, value), + ), + AnalyticsViewButton( + value: controller.currentView, + onChange: controller.toggleView, + ), + AnalyticsLanguageButton( + value: controller + .pangeaController.analytics.currentAnalyticsLang, + onChange: (lang) => controller.toggleSpaceLang(lang), + languages: controller.widget.targetLanguages, + ), + ], + ), + const SizedBox( + height: 10, + ), + Expanded( + flex: 1, + child: chartView(context), + ), + Expanded( + flex: 1, + child: DefaultTabController( + length: 2, + child: Column( + children: [ + TabBar( + tabs: [ + ...controller.widget.tabs.map( + (tab) => Tab( + icon: Icon( + tab.icon, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + ), + ], + ), + Expanded( + child: SingleChildScrollView( + child: SizedBox( + height: max( + controller.widget.tabs[0].items.length + 1, + controller.widget.tabs[1].items.length, + ) * + 72, + child: TabBarView( + physics: const NeverScrollableScrollPhysics(), + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + ...controller.widget.tabs[0].items.map( + (item) => AnalyticsListTile( + refreshStream: controller.refreshStream, + avatar: item.avatar, + defaultSelected: + controller.widget.defaultSelected, + selected: AnalyticsSelected( + item.id, + controller.widget.tabs[0].type, + item.displayName, + ), + isSelected: + controller.isSelected(item.id), + onTap: (_) => controller.toggleSelection( + AnalyticsSelected( + item.id, + controller.widget.tabs[0].type, + item.displayName, + ), + ), + allowNavigateOnSelect: controller + .widget.tabs[0].allowNavigateOnSelect, + pangeaController: + controller.pangeaController, + controller: controller, + ), ), - ), + if (controller.widget.defaultSelected.type == + AnalyticsEntryType.space) + AnalyticsListTile( + refreshStream: controller.refreshStream, + defaultSelected: + controller.widget.defaultSelected, + avatar: null, + selected: AnalyticsSelected( + controller.widget.defaultSelected.id, + AnalyticsEntryType.privateChats, + L10n.of(context)!.allPrivateChats, + ), + allowNavigateOnSelect: false, + isSelected: controller.isSelected( + controller.widget.defaultSelected.id, + ), + onTap: controller.toggleSelection, + pangeaController: + controller.pangeaController, + controller: controller, + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: controller.widget.tabs[1].items + .map( + (item) => AnalyticsListTile( + refreshStream: controller.refreshStream, + avatar: item.avatar, + defaultSelected: + controller.widget.defaultSelected, + selected: AnalyticsSelected( + item.id, + controller.widget.tabs[1].type, + item.displayName, + ), + isSelected: + controller.isSelected(item.id), + onTap: controller.toggleSelection, + allowNavigateOnSelect: controller.widget + .tabs[1].allowNavigateOnSelect, + pangeaController: + controller.pangeaController, + controller: controller, + ), + ) + .toList(), ), ], ), - Expanded( - child: SingleChildScrollView( - child: SizedBox( - height: max( - controller.widget.tabs[0].items.length + - 1, - controller.widget.tabs[1].items.length, - ) * - 72, - child: TabBarView( - physics: const NeverScrollableScrollPhysics(), - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - ...controller.widget.tabs[0].items.map( - (item) => AnalyticsListTile( - refreshStream: - controller.refreshStream, - avatar: item.avatar, - defaultSelected: controller - .widget.defaultSelected, - selected: AnalyticsSelected( - item.id, - controller.widget.tabs[0].type, - item.displayName, - ), - isSelected: - controller.isSelected(item.id), - onTap: (_) => - controller.toggleSelection( - AnalyticsSelected( - item.id, - controller.widget.tabs[0].type, - item.displayName, - ), - ), - allowNavigateOnSelect: controller - .widget - .tabs[0] - .allowNavigateOnSelect, - pangeaController: - controller.pangeaController, - controller: controller, - ), - ), - if (controller - .widget.defaultSelected.type == - AnalyticsEntryType.space) - AnalyticsListTile( - refreshStream: - controller.refreshStream, - defaultSelected: controller - .widget.defaultSelected, - avatar: null, - selected: AnalyticsSelected( - controller - .widget.defaultSelected.id, - AnalyticsEntryType.privateChats, - L10n.of(context)!.allPrivateChats, - ), - allowNavigateOnSelect: false, - isSelected: controller.isSelected( - controller - .widget.defaultSelected.id, - ), - onTap: controller.toggleSelection, - pangeaController: - controller.pangeaController, - controller: controller, - ), - ], - ), - Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: controller.widget.tabs[1].items - .map( - (item) => AnalyticsListTile( - refreshStream: - controller.refreshStream, - avatar: item.avatar, - defaultSelected: controller - .widget.defaultSelected, - selected: AnalyticsSelected( - item.id, - controller.widget.tabs[1].type, - item.displayName, - ), - isSelected: controller - .isSelected(item.id), - onTap: controller.toggleSelection, - allowNavigateOnSelect: controller - .widget - .tabs[1] - .allowNavigateOnSelect, - pangeaController: - controller.pangeaController, - controller: controller, - ), - ) - .toList(), - ), - ], - ), - ), - ), - ), - ], + ), ), ), - ), - ], - ) - : Column( - children: [ - const Divider(height: 1), - ListTile( - title: Text(L10n.of(context)!.grammarAnalytics), - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: - Theme.of(context).textTheme.bodyLarge!.color, - child: Icon(BarChartViewSelection.grammar.icon), - ), - trailing: const Icon(Icons.chevron_right), - onTap: () { - String route = - "/rooms/${controller.widget.defaultSelected.type.route}"; - if (controller.widget.defaultSelected.type == - AnalyticsEntryType.space) { - route += "/${controller.widget.defaultSelected.id}"; - } - route += "/${BarChartViewSelection.grammar.route}"; - context.go(route); - }, - ), - const Divider(height: 1), - ListTile( - title: Text(L10n.of(context)!.messageAnalytics), - leading: CircleAvatar( - backgroundColor: - Theme.of(context).scaffoldBackgroundColor, - foregroundColor: - Theme.of(context).textTheme.bodyLarge!.color, - child: Icon(BarChartViewSelection.messages.icon), - ), - trailing: const Icon(Icons.chevron_right), - onTap: () { - String route = - "/rooms/${controller.widget.defaultSelected.type.route}"; - if (controller.widget.defaultSelected.type == - AnalyticsEntryType.space) { - route += "/${controller.widget.defaultSelected.id}"; - } - route += "/${BarChartViewSelection.messages.route}"; - context.go(route); - }, - ), - const Divider(height: 1), - ], + ], + ), ), + ), + ], + ), ), ); } diff --git a/lib/pangea/pages/analytics/space_analytics/space_analytics.dart b/lib/pangea/pages/analytics/space_analytics/space_analytics.dart index b32780761..2db1acb4c 100644 --- a/lib/pangea/pages/analytics/space_analytics/space_analytics.dart +++ b/lib/pangea/pages/analytics/space_analytics/space_analytics.dart @@ -18,8 +18,8 @@ import '../../../utils/sync_status_util_v2.dart'; import 'space_analytics_view.dart'; class SpaceAnalyticsPage extends StatefulWidget { - final BarChartViewSelection? selectedView; - const SpaceAnalyticsPage({super.key, this.selectedView}); + final BarChartViewSelection selectedView; + const SpaceAnalyticsPage({super.key, required this.selectedView}); @override State createState() => SpaceAnalyticsV2Controller(); diff --git a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart index 5c694b6ca..e06c6d0ba 100644 --- a/lib/pangea/pages/analytics/student_analytics/student_analytics.dart +++ b/lib/pangea/pages/analytics/student_analytics/student_analytics.dart @@ -18,8 +18,8 @@ import '../base_analytics.dart'; import 'student_analytics_view.dart'; class StudentAnalyticsPage extends StatefulWidget { - final BarChartViewSelection? selectedView; - const StudentAnalyticsPage({super.key, this.selectedView}); + final BarChartViewSelection selectedView; + const StudentAnalyticsPage({super.key, required this.selectedView}); @override State createState() => StudentAnalyticsController(); From e7568c1c720efb3765e5b2a1b7307c59264f498f Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 1 Jul 2024 15:10:17 -0400 Subject: [PATCH 17/26] added inline tooltip file --- lib/pangea/utils/inline_tooltip.dart | 62 ++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 lib/pangea/utils/inline_tooltip.dart diff --git a/lib/pangea/utils/inline_tooltip.dart b/lib/pangea/utils/inline_tooltip.dart new file mode 100644 index 000000000..253288a84 --- /dev/null +++ b/lib/pangea/utils/inline_tooltip.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +class InlineTooltip extends StatelessWidget { + final String body; + final VoidCallback onClose; + + const InlineTooltip({ + super.key, + required this.body, + required this.onClose, + }); + + @override + Widget build(BuildContext context) { + return Badge( + offset: const Offset(0, -7), + backgroundColor: Colors.transparent, + label: CircleAvatar( + radius: 10, + child: IconButton( + padding: EdgeInsets.zero, + icon: const Icon( + Icons.close_outlined, + size: 15, + ), + onPressed: onClose, + ), + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Theme.of(context).colorScheme.primary.withAlpha(20), + ), + child: Padding( + padding: const EdgeInsets.all(10), + child: RichText( + textAlign: TextAlign.justify, + text: TextSpan( + children: [ + const WidgetSpan( + child: Icon( + Icons.lightbulb, + size: 16, + ), + ), + const WidgetSpan( + child: SizedBox(width: 5), + ), + TextSpan( + text: body, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurface, + ), + ), + ], + ), + ), + ), + ), + ); + } +} From 3c4ed1a821613768e5913359d1dfe62221025b38 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Mon, 1 Jul 2024 15:23:36 -0400 Subject: [PATCH 18/26] formatting pangea rich text file --- lib/pangea/widgets/igc/pangea_rich_text.dart | 29 ++++++++++++-------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/lib/pangea/widgets/igc/pangea_rich_text.dart b/lib/pangea/widgets/igc/pangea_rich_text.dart index 5b33f66a9..abf583b3b 100644 --- a/lib/pangea/widgets/igc/pangea_rich_text.dart +++ b/lib/pangea/widgets/igc/pangea_rich_text.dart @@ -68,17 +68,20 @@ class PangeaRichTextState extends State { }); } catch (error, stackTrace) { ErrorHandler.logError( - e: PangeaWarningError(error), - s: stackTrace, - m: "Error setting text span in PangeaRichText"); + e: PangeaWarningError(error), + s: stackTrace, + m: "Error setting text span in PangeaRichText", + ); } } void setTextSpan() { if (_fetchingRepresentation) { - _setTextSpan(widget.pangeaMessageEvent.event - .getDisplayEvent(widget.pangeaMessageEvent.timeline) - .body); + _setTextSpan( + widget.pangeaMessageEvent.event + .getDisplayEvent(widget.pangeaMessageEvent.timeline) + .body, + ); return; } @@ -88,19 +91,23 @@ class PangeaRichTextState extends State { repEvent = widget.pangeaMessageEvent .representationByLanguage( - widget.pangeaMessageEvent.messageDisplayLangCode) + widget.pangeaMessageEvent.messageDisplayLangCode, + ) ?.content; if (repEvent == null) { setState(() => _fetchingRepresentation = true); widget.pangeaMessageEvent .representationByLanguageGlobal( - langCode: widget.pangeaMessageEvent.messageDisplayLangCode) + langCode: widget.pangeaMessageEvent.messageDisplayLangCode, + ) .onError((error, stackTrace) { ErrorHandler.logError( - e: PangeaWarningError(error), - s: stackTrace, - m: "Error fetching representation"); + e: PangeaWarningError(error), + s: stackTrace, + m: "Error fetching representation", + ); + return null; }).then((event) { if (!mounted) return; repEvent = event; From 6ca6b19b3962460313936785b3b48e7b454acce9 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Tue, 2 Jul 2024 09:56:26 -0400 Subject: [PATCH 19/26] don't set source text to l1 translation --- .../controllers/it_controller.dart | 33 +++++++++---------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/lib/pangea/choreographer/controllers/it_controller.dart b/lib/pangea/choreographer/controllers/it_controller.dart index 9e70287cd..3187d4a6a 100644 --- a/lib/pangea/choreographer/controllers/it_controller.dart +++ b/lib/pangea/choreographer/controllers/it_controller.dart @@ -3,7 +3,6 @@ import 'dart:developer'; import 'package:fluffychat/pangea/choreographer/controllers/error_service.dart'; import 'package:fluffychat/pangea/constants/choreo_constants.dart'; -import 'package:fluffychat/pangea/repo/full_text_translation_repo.dart'; import 'package:fluffychat/pangea/utils/error_handler.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -85,23 +84,23 @@ class ITController { throw Exception("null _itStartData or empty text in _setSourceText"); } debugPrint("_setSourceText with detectedLang ${_itStartData!.langCode}"); - if (_itStartData!.langCode == choreographer.l1LangCode) { - sourceText = _itStartData!.text; - return; - } + // if (_itStartData!.langCode == choreographer.l1LangCode) { + sourceText = _itStartData!.text; + return; + // } - final FullTextTranslationResponseModel res = - await FullTextTranslationRepo.translate( - accessToken: await choreographer.accessToken, - request: FullTextTranslationRequestModel( - text: _itStartData!.text, - tgtLang: choreographer.l1LangCode!, - srcLang: _itStartData!.langCode, - userL1: choreographer.l1LangCode!, - userL2: choreographer.l2LangCode!, - ), - ); - sourceText = res.bestTranslation; + // final FullTextTranslationResponseModel res = + // await FullTextTranslationRepo.translate( + // accessToken: await choreographer.accessToken, + // request: FullTextTranslationRequestModel( + // text: _itStartData!.text, + // tgtLang: choreographer.l1LangCode!, + // srcLang: _itStartData!.langCode, + // userL1: choreographer.l1LangCode!, + // userL2: choreographer.l2LangCode!, + // ), + // ); + // sourceText = res.bestTranslation; } // used 1) at very beginning (with custom input = null) From 1163c1d5cdeaf12d8bc8cfdc2520c65096112dd4 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 3 Jul 2024 12:01:22 -0400 Subject: [PATCH 20/26] adding pos and morph to lemmas --- .../pangea_message_event.dart | 2 +- lib/pangea/models/lemma.dart | 27 ++++++++++++++++--- lib/pangea/models/pangea_token_model.dart | 5 ---- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart index e0820d665..334c0fa78 100644 --- a/lib/pangea/matrix_event_wrappers/pangea_message_event.dart +++ b/lib/pangea/matrix_event_wrappers/pangea_message_event.dart @@ -723,7 +723,7 @@ class PangeaMessageEvent { // missing vital info so return if (event.roomId == null || originalSent?.tokens == null) { - debugger(when: kDebugMode); + // debugger(when: kDebugMode); return uses; } diff --git a/lib/pangea/models/lemma.dart b/lib/pangea/models/lemma.dart index 56f23d876..d07ac8ff0 100644 --- a/lib/pangea/models/lemma.dart +++ b/lib/pangea/models/lemma.dart @@ -10,22 +10,41 @@ class Lemma { /// vocab that are not saved: emails, urls, numbers, punctuation, etc. final bool saveVocab; - Lemma({required this.text, required this.saveVocab, required this.form}); + /// [pos] ex "v" - part of speech of the lemma + /// https://universaldependencies.org/u/pos/ + final String pos; + + /// [morph] ex {} - morphological features of the lemma + /// https://universaldependencies.org/u/feat/ + final Map morph; + + Lemma( + {required this.text, + required this.saveVocab, + required this.form, + this.pos = '', + this.morph = const {}}); factory Lemma.fromJson(Map json) { return Lemma( text: json['text'], saveVocab: json['save_vocab'] ?? json['saveVocab'] ?? false, form: json["form"] ?? json['text'], + pos: json['pos'] ?? '', + morph: json['morph'] ?? {}, ); } toJson() { - return {'text': text, 'save_vocab': saveVocab, 'form': form}; + return { + 'text': text, + 'save_vocab': saveVocab, + 'form': form, + 'pos': pos, + 'morph': morph + }; } - static Lemma get empty => Lemma(text: '', saveVocab: true, form: ''); - static Lemma create(String form) => Lemma(text: '', saveVocab: true, form: form); } diff --git a/lib/pangea/models/pangea_token_model.dart b/lib/pangea/models/pangea_token_model.dart index a671256ab..9dddd149b 100644 --- a/lib/pangea/models/pangea_token_model.dart +++ b/lib/pangea/models/pangea_token_model.dart @@ -9,12 +9,10 @@ import 'lemma.dart'; class PangeaToken { PangeaTokenText text; - bool hasInfo; List lemmas; PangeaToken({ required this.text, - required this.hasInfo, required this.lemmas, }); @@ -37,7 +35,6 @@ class PangeaToken { PangeaTokenText.fromJson(json[_textKey] as Map); return PangeaToken( text: text, - hasInfo: json[_hasInfoKey] ?? text.length > 2, lemmas: getLemmas(text.content, json[_lemmaKey]), ); } catch (err, s) { @@ -56,12 +53,10 @@ class PangeaToken { } static const String _textKey = "text"; - static const String _hasInfoKey = "has_info"; static const String _lemmaKey = ModelKey.lemma; Map toJson() => { _textKey: text.toJson(), - _hasInfoKey: hasInfo, _lemmaKey: lemmas.map((e) => e.toJson()).toList(), }; From 4d34b390d5aff2d6ecce7c983164944a1f8d2124 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 3 Jul 2024 14:00:11 -0400 Subject: [PATCH 21/26] simplify logic of message translation --- .../chat/message_translation_card.dart | 30 ++++++------------- 1 file changed, 9 insertions(+), 21 deletions(-) diff --git a/lib/pangea/widgets/chat/message_translation_card.dart b/lib/pangea/widgets/chat/message_translation_card.dart index 094f481ec..31b6addb2 100644 --- a/lib/pangea/widgets/chat/message_translation_card.dart +++ b/lib/pangea/widgets/chat/message_translation_card.dart @@ -29,41 +29,26 @@ class MessageTranslationCardState extends State { PangeaRepresentation? repEvent; String? selectionTranslation; String? oldSelectedText; - String? l1Code; - String? l2Code; bool _fetchingRepresentation = false; - String? translationLangCode() { - if (widget.immersionMode) return l1Code; - final String? originalSentCode = - widget.messageEvent.originalSent?.content.langCode; - return (l1Code == originalSentCode || originalSentCode == null) - ? l2Code - : l1Code; - } - Future fetchRepresentation(BuildContext context) async { - final String? langCode = translationLangCode(); - if (langCode == null) return; + if (l1Code == null) return; repEvent = widget.messageEvent .representationByLanguage( - langCode, + l1Code!, ) ?.content; if (repEvent == null && mounted) { repEvent = await widget.messageEvent.representationByLanguageGlobal( - langCode: langCode, + langCode: l1Code!, ); } } Future translateSelection() async { - final String? targetLang = translationLangCode(); - if (widget.selection.selectedText == null || - targetLang == null || l1Code == null || l2Code == null) { selectionTranslation = null; @@ -78,7 +63,7 @@ class MessageTranslationCardState extends State { accessToken: accessToken, request: FullTextTranslationRequestModel( text: widget.selection.messageText, - tgtLang: translationLangCode()!, + tgtLang: l1Code!, userL1: l1Code!, userL2: l2Code!, srcLang: widget.messageEvent.messageDisplayLangCode, @@ -106,11 +91,14 @@ class MessageTranslationCardState extends State { } } + String? get l1Code => + MatrixState.pangeaController.languageController.activeL1Code(); + String? get l2Code => + MatrixState.pangeaController.languageController.activeL2Code(); + @override void initState() { super.initState(); - l1Code = MatrixState.pangeaController.languageController.activeL1Code(); - l2Code = MatrixState.pangeaController.languageController.activeL2Code(); if (mounted) { setState(() {}); } From 266826fd0ee435c23bb4f6dd6ac023aa952fd23e Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 3 Jul 2024 14:01:54 -0400 Subject: [PATCH 22/26] take out hasInfo --- lib/pangea/repo/igc_repo.dart | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/pangea/repo/igc_repo.dart b/lib/pangea/repo/igc_repo.dart index d3dbbcb6b..d6271470a 100644 --- a/lib/pangea/repo/igc_repo.dart +++ b/lib/pangea/repo/igc_repo.dart @@ -47,27 +47,22 @@ class IgcRepo { tokens: [ PangeaToken( text: PangeaTokenText(content: "This", offset: 0, length: 4), - hasInfo: true, lemmas: [Lemma(form: "This", text: "this", saveVocab: true)], ), PangeaToken( text: PangeaTokenText(content: "be", offset: 5, length: 2), - hasInfo: true, lemmas: [Lemma(form: "be", text: "be", saveVocab: true)], ), PangeaToken( text: PangeaTokenText(content: "a", offset: 8, length: 1), - hasInfo: false, lemmas: [], ), PangeaToken( text: PangeaTokenText(content: "sample", offset: 10, length: 6), - hasInfo: false, lemmas: [], ), PangeaToken( text: PangeaTokenText(content: "text", offset: 17, length: 4), - hasInfo: false, lemmas: [], ), ], From 8ab62fed3b9040f6155956662af9d6a642c4afc6 Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 3 Jul 2024 14:42:51 -0400 Subject: [PATCH 23/26] update lemma factory --- .gitignore | 2 ++ lib/main.dart | 2 +- lib/pangea/models/lemma.dart | 19 ++++++++++--------- pubspec.yaml | 2 ++ 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 3c47c0cc2..1071f4410 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ prime *.env !/public/.env +*.env.local_choreo +*.env.prod # libolm package /assets/js/package diff --git a/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/models/lemma.dart b/lib/pangea/models/lemma.dart index d07ac8ff0..017a7ab88 100644 --- a/lib/pangea/models/lemma.dart +++ b/lib/pangea/models/lemma.dart @@ -16,14 +16,15 @@ class Lemma { /// [morph] ex {} - morphological features of the lemma /// https://universaldependencies.org/u/feat/ - final Map morph; + final Map morph; - Lemma( - {required this.text, - required this.saveVocab, - required this.form, - this.pos = '', - this.morph = const {}}); + Lemma({ + required this.text, + required this.saveVocab, + required this.form, + this.pos = '', + this.morph = const {}, + }); factory Lemma.fromJson(Map json) { return Lemma( @@ -31,7 +32,7 @@ class Lemma { saveVocab: json['save_vocab'] ?? json['saveVocab'] ?? false, form: json["form"] ?? json['text'], pos: json['pos'] ?? '', - morph: json['morph'] ?? {}, + morph: json['morph'] ?? '{}', ); } @@ -41,7 +42,7 @@ class Lemma { 'save_vocab': saveVocab, 'form': form, 'pos': pos, - 'morph': morph + 'morph': morph, }; } diff --git a/pubspec.yaml b/pubspec.yaml index a24dfdff3..c78c7dbba 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -151,6 +151,8 @@ flutter: # causes error with github actions # - .env # - assets/.env + - .env.local_choreo + - assets/.env.local_choreo - assets/pangea/ - assets/pangea/bot_faces/ # Pangea# From 9362f9c915645cef5e8b7d19419fda34a53f11c0 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 3 Jul 2024 15:09:16 -0400 Subject: [PATCH 24/26] uncommented message info button --- lib/pages/chat/chat_view.dart | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 5b4f96606..3c819f412 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -89,19 +89,17 @@ class ChatView extends StatelessWidget { } }, itemBuilder: (context) => [ - // #Pangea - // PopupMenuItem( - // value: _EventContextAction.info, - // child: Row( - // mainAxisSize: MainAxisSize.min, - // children: [ - // const Icon(Icons.info_outlined), - // const SizedBox(width: 12), - // Text(L10n.of(context)!.messageInfo), - // ], - // ), - // ), - // Pangea# + PopupMenuItem( + value: _EventContextAction.info, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.info_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.messageInfo), + ], + ), + ), if (controller.selectedEvents.single.status.isSent) PopupMenuItem( value: _EventContextAction.report, @@ -442,7 +440,8 @@ class ChatView extends StatelessWidget { children: [ const ConnectionStatusHeader(), ITBar( - choreographer: controller.choreographer, + choreographer: + controller.choreographer, ), ReactionsPicker(controller), ReplyDisplay(controller), From 347adaf059d230c800c921331ee882ef3b837bf1 Mon Sep 17 00:00:00 2001 From: ggurdin Date: Wed, 3 Jul 2024 15:33:09 -0400 Subject: [PATCH 25/26] removed incorrect env file references --- lib/main.dart | 2 +- lib/pangea/config/environment.dart | 2 +- pubspec.yaml | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 36add47dc..6be6edc91 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,7 +22,7 @@ void main() async { // #Pangea try { - await dotenv.load(fileName: ".env.local_choreo"); + await dotenv.load(fileName: ".env"); } catch (e) { Logs().e('Failed to load .env file', e); } diff --git a/lib/pangea/config/environment.dart b/lib/pangea/config/environment.dart index de7039f9d..4d4378999 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 ".local_choreo.env"; + return ".env"; } static bool get isStaging => synapsURL.contains("staging"); diff --git a/pubspec.yaml b/pubspec.yaml index c78c7dbba..a24dfdff3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -151,8 +151,6 @@ flutter: # causes error with github actions # - .env # - assets/.env - - .env.local_choreo - - assets/.env.local_choreo - assets/pangea/ - assets/pangea/bot_faces/ # Pangea# From 6113b901ce8d256a5b535eaacf8f031af2c65cbb Mon Sep 17 00:00:00 2001 From: William Jordan-Cooley Date: Wed, 3 Jul 2024 19:48:24 -0400 Subject: [PATCH 26/26] lemma factory hot-fix --- lib/pangea/models/lemma.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pangea/models/lemma.dart b/lib/pangea/models/lemma.dart index 017a7ab88..81c8f911d 100644 --- a/lib/pangea/models/lemma.dart +++ b/lib/pangea/models/lemma.dart @@ -32,7 +32,7 @@ class Lemma { saveVocab: json['save_vocab'] ?? json['saveVocab'] ?? false, form: json["form"] ?? json['text'], pos: json['pos'] ?? '', - morph: json['morph'] ?? '{}', + morph: json['morph'] ?? {}, ); }