From c507c7b54b673617000213a4d236535dc95de292 Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Tue, 30 Dec 2025 09:07:16 -0500 Subject: [PATCH] feat: allow token feedback for word card in vocab analytics (#4900) * feat: allow token feedback for word card in vocab analytics * fix: remove duplicate global keys --- lib/pages/chat/chat.dart | 32 +++---------- .../vocab_analytics_details_view.dart | 40 +++++++++++++++- .../lemmas/lemma_highlight_emoji_row.dart | 23 ++++++---- .../phonetic_transcription_widget.dart | 15 +++--- .../show_token_feedback_dialog.dart | 43 +++++++++++++++++ .../token_info_feedback_dialog.dart | 46 ++++++++++--------- .../token_info_feedback_request.dart | 8 ++-- .../word_card/lemma_reaction_picker.dart | 18 ++++---- 8 files changed, 146 insertions(+), 79 deletions(-) create mode 100644 lib/pangea/token_info_feedback/show_token_feedback_dialog.dart diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 8a989e812..1a2bf8eec 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -67,8 +67,7 @@ import 'package:fluffychat/pangea/learning_settings/language_mismatch_repo.dart' import 'package:fluffychat/pangea/learning_settings/p_language_dialog.dart'; import 'package:fluffychat/pangea/spaces/load_participants_builder.dart'; import 'package:fluffychat/pangea/subscription/widgets/paywall_card.dart'; -import 'package:fluffychat/pangea/token_info_feedback/token_info_feedback_dialog.dart'; -import 'package:fluffychat/pangea/token_info_feedback/token_info_feedback_notification.dart'; +import 'package:fluffychat/pangea/token_info_feedback/show_token_feedback_dialog.dart'; import 'package:fluffychat/pangea/token_info_feedback/token_info_feedback_request.dart'; import 'package:fluffychat/pangea/toolbar/message_practice/message_practice_mode_enum.dart'; import 'package:fluffychat/pangea/toolbar/message_selection_overlay.dart'; @@ -2304,31 +2303,12 @@ class ChatController extends State PangeaMessageEvent event, ) async { clearSelectedEvents(); - final resp = await showDialog( - context: context, - builder: (context) => TokenInfoFeedbackDialog( - requestData: requestData, - langCode: langCode, - event: event, - ), + await TokenFeedbackUtil.showTokenFeedbackDialog( + context, + requestData: requestData, + langCode: langCode, + event: event, ); - - if (resp != null && resp is String) { - OverlayUtil.showOverlay( - overlayKey: "token_feedback_snackbar", - context: context, - child: TokenFeedbackNotification(message: resp), - transformTargetId: '', - position: OverlayPositionEnum.top, - backDropToDismiss: false, - closePrevOverlay: false, - canPop: false, - ); - - Future.delayed(const Duration(seconds: 10), () { - MatrixState.pAnyState.closeOverlay("token_feedback_snackbar"); - }); - } } void toggleShowDropdown() { diff --git a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart index c2c532852..e18489d30 100644 --- a/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart +++ b/lib/pangea/analytics_details_popup/vocab_analytics_details_view.dart @@ -10,7 +10,12 @@ import 'package:fluffychat/pangea/analytics_misc/analytics_navigation_util.dart' import 'package:fluffychat/pangea/analytics_summary/progress_indicators_enum.dart'; import 'package:fluffychat/pangea/constructs/construct_identifier.dart'; import 'package:fluffychat/pangea/constructs/construct_level_enum.dart'; +import 'package:fluffychat/pangea/events/models/pangea_token_model.dart'; import 'package:fluffychat/pangea/events/models/pangea_token_text_model.dart'; +import 'package:fluffychat/pangea/lemmas/lemma.dart'; +import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; +import 'package:fluffychat/pangea/token_info_feedback/show_token_feedback_dialog.dart'; +import 'package:fluffychat/pangea/token_info_feedback/token_info_feedback_request.dart'; import 'package:fluffychat/pangea/toolbar/word_card/word_zoom_widget.dart'; import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; @@ -72,6 +77,18 @@ class VocabDetailsView extends StatelessWidget { .toList() ?? []; + final tokenText = PangeaTokenText.fromString(constructId.lemma); + final token = PangeaToken( + text: tokenText, + pos: constructId.category, + morph: {}, + lemma: Lemma( + text: constructId.lemma, + form: constructId.lemma, + saveVocab: true, + ), + ); + return MaxWidthBody( maxWidth: 600.0, showBorder: false, @@ -82,11 +99,32 @@ class VocabDetailsView extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ WordZoomWidget( - token: PangeaTokenText.fromString(constructId.lemma), + token: tokenText, langCode: MatrixState.pangeaController.userController.userL2Code!, construct: constructId, onClose: Navigator.of(context).pop, + onFlagTokenInfo: + (LemmaInfoResponse lemmaInfo, String phonetics) { + final requestData = TokenInfoFeedbackRequestData( + userId: Matrix.of(context).client.userID!, + detectedLanguage: MatrixState + .pangeaController.userController.userL2Code!, + tokens: [token], + selectedToken: 0, + wordCardL1: MatrixState + .pangeaController.userController.userL1Code!, + lemmaInfo: lemmaInfo, + phonetics: phonetics, + ); + + TokenFeedbackUtil.showTokenFeedbackDialog( + context, + requestData: requestData, + langCode: MatrixState + .pangeaController.userController.userL2Code!, + ); + }, ), ], ), diff --git a/lib/pangea/lemmas/lemma_highlight_emoji_row.dart b/lib/pangea/lemmas/lemma_highlight_emoji_row.dart index c9e63a6c9..587bc0395 100644 --- a/lib/pangea/lemmas/lemma_highlight_emoji_row.dart +++ b/lib/pangea/lemmas/lemma_highlight_emoji_row.dart @@ -15,8 +15,9 @@ import 'package:fluffychat/widgets/matrix.dart'; class LemmaHighlightEmojiRow extends StatefulWidget { final ConstructIdentifier cId; final String langCode; + final String targetId; - final Function(String) onEmojiSelected; + final Function(String, String) onEmojiSelected; final Map messageInfo; final String? emoji; @@ -26,6 +27,7 @@ class LemmaHighlightEmojiRow extends StatefulWidget { super.key, required this.cId, required this.langCode, + required this.targetId, required this.onEmojiSelected, required this.messageInfo, this.emoji, @@ -73,22 +75,23 @@ class LemmaHighlightEmojiRowState extends State ), ), ) - : emojis - .map( - (emoji) => EmojiChoiceItem( + : emojis.map( + (emoji) { + final targetId = "${widget.targetId}-$emoji"; + return EmojiChoiceItem( cId: widget.cId, emoji: emoji, - onSelectEmoji: () => widget.onEmojiSelected(emoji), + onSelectEmoji: () => + widget.onEmojiSelected(emoji, targetId), selected: widget.emoji == emoji, - transformTargetId: - "emoji-choice-item-$emoji-${widget.cId.lemma}", + transformTargetId: targetId, badge: widget.emoji == emoji ? widget.selectedEmojiBadge : null, showShimmer: widget.emoji == null, - ), - ) - .toList(), + ); + }, + ).toList(), ), ); }, diff --git a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart index 42bafda33..eb62b7682 100644 --- a/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart +++ b/lib/pangea/phonetic_transcription/phonetic_transcription_widget.dart @@ -41,7 +41,7 @@ class _PhoneticTranscriptionWidgetState extends State { bool _isPlaying = false; - Future _handleAudioTap() async { + Future _handleAudioTap(String targetId) async { if (_isPlaying) { await TtsController.stop(); setState(() => _isPlaying = false); @@ -49,7 +49,7 @@ class _PhoneticTranscriptionWidgetState await TtsController.tryToSpeak( widget.text, context: context, - targetID: 'phonetic-transcription-${widget.text}', + targetID: targetId, langCode: widget.textLanguage.langCode, onStart: () { if (mounted) setState(() => _isPlaying = true); @@ -63,10 +63,11 @@ class _PhoneticTranscriptionWidgetState @override Widget build(BuildContext context) { + final targetId = 'phonetic-transcription-${widget.text}-$hashCode'; return HoverBuilder( builder: (context, hovering) { return GestureDetector( - onTap: _handleAudioTap, + onTap: () => _handleAudioTap(targetId), child: AnimatedContainer( duration: const Duration(milliseconds: 150), decoration: BoxDecoration( @@ -77,13 +78,9 @@ class _PhoneticTranscriptionWidgetState ), padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: CompositedTransformTarget( - link: MatrixState.pAnyState - .layerLinkAndKey("phonetic-transcription-${widget.text}") - .link, + link: MatrixState.pAnyState.layerLinkAndKey(targetId).link, child: PhoneticTranscriptionBuilder( - key: MatrixState.pAnyState - .layerLinkAndKey("phonetic-transcription-${widget.text}") - .key, + key: MatrixState.pAnyState.layerLinkAndKey(targetId).key, textLanguage: widget.textLanguage, text: widget.text, builder: (context, controller) { diff --git a/lib/pangea/token_info_feedback/show_token_feedback_dialog.dart b/lib/pangea/token_info_feedback/show_token_feedback_dialog.dart new file mode 100644 index 000000000..365509e2f --- /dev/null +++ b/lib/pangea/token_info_feedback/show_token_feedback_dialog.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/pangea/common/utils/overlay.dart'; +import 'package:fluffychat/pangea/events/event_wrappers/pangea_message_event.dart'; +import 'package:fluffychat/pangea/token_info_feedback/token_info_feedback_dialog.dart'; +import 'package:fluffychat/pangea/token_info_feedback/token_info_feedback_notification.dart'; +import 'package:fluffychat/pangea/token_info_feedback/token_info_feedback_request.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class TokenFeedbackUtil { + static Future showTokenFeedbackDialog( + BuildContext context, { + required TokenInfoFeedbackRequestData requestData, + required String langCode, + PangeaMessageEvent? event, + }) async { + final resp = await showDialog( + context: context, + builder: (context) => TokenInfoFeedbackDialog( + requestData: requestData, + langCode: langCode, + event: event, + ), + ); + + if (resp != null && resp is String) { + OverlayUtil.showOverlay( + overlayKey: "token_feedback_snackbar", + context: context, + child: TokenFeedbackNotification(message: resp), + transformTargetId: '', + position: OverlayPositionEnum.top, + backDropToDismiss: false, + closePrevOverlay: false, + canPop: false, + ); + + Future.delayed(const Duration(seconds: 10), () { + MatrixState.pAnyState.closeOverlay("token_feedback_snackbar"); + }); + } + } +} diff --git a/lib/pangea/token_info_feedback/token_info_feedback_dialog.dart b/lib/pangea/token_info_feedback/token_info_feedback_dialog.dart index a715dc1fc..fefd83cbe 100644 --- a/lib/pangea/token_info_feedback/token_info_feedback_dialog.dart +++ b/lib/pangea/token_info_feedback/token_info_feedback_dialog.dart @@ -24,13 +24,13 @@ import 'package:fluffychat/widgets/matrix.dart'; class TokenInfoFeedbackDialog extends StatelessWidget { final TokenInfoFeedbackRequestData requestData; final String langCode; - final PangeaMessageEvent event; + final PangeaMessageEvent? event; const TokenInfoFeedbackDialog({ super.key, required this.requestData, required this.langCode, - required this.event, + this.event, }); Future _submitFeedback(String feedback) async { @@ -60,7 +60,7 @@ class TokenInfoFeedbackDialog extends StatelessWidget { ); } - final originalSent = event.originalSent; + final originalSent = event?.originalSent; // if no other changes, just return the message final hasTokenUpdate = response.updatedToken != null; @@ -82,23 +82,25 @@ class TokenInfoFeedbackDialog extends StatelessWidget { originalSent.content.langCode = response.updatedLanguage!; } - await event.room.pangeaSendTextEvent( - requestData.fullText, - editEventId: event.eventId, - originalSent: originalSent?.content, - originalWritten: event.originalWritten?.content, - tokensSent: PangeaMessageTokens( - tokens: tokens, - ), - tokensWritten: event.originalWritten?.tokens != null - ? PangeaMessageTokens( - tokens: event.originalWritten!.tokens!, - detections: event.originalWritten?.detections, - ) - : null, - choreo: originalSent?.choreo, - messageTag: ModelKey.tokenFeedbackEdit, - ); + if (requestData.fullText != null && event != null) { + await event!.room.pangeaSendTextEvent( + requestData.fullText!, + editEventId: event!.eventId, + originalSent: originalSent?.content, + originalWritten: event!.originalWritten?.content, + tokensSent: PangeaMessageTokens( + tokens: tokens, + ), + tokensWritten: event!.originalWritten?.tokens != null + ? PangeaMessageTokens( + tokens: event!.originalWritten!.tokens!, + detections: event!.originalWritten?.detections, + ) + : null, + choreo: originalSent?.choreo, + messageTag: ModelKey.tokenFeedbackEdit, + ); + } return response.userFriendlyMessage; } @@ -119,7 +121,9 @@ class TokenInfoFeedbackDialog extends StatelessWidget { LemmaInfoResponse response, ) => LemmaInfoRepo.set( - token.vocabConstructID.lemmaInfoRequest(event.event.content), + token.vocabConstructID.lemmaInfoRequest( + event?.event.content ?? {}, + ), response, ); diff --git a/lib/pangea/token_info_feedback/token_info_feedback_request.dart b/lib/pangea/token_info_feedback/token_info_feedback_request.dart index 815979493..c7f7636a8 100644 --- a/lib/pangea/token_info_feedback/token_info_feedback_request.dart +++ b/lib/pangea/token_info_feedback/token_info_feedback_request.dart @@ -3,8 +3,8 @@ import 'package:fluffychat/pangea/lemmas/lemma_info_response.dart'; class TokenInfoFeedbackRequestData { final String userId; - final String roomId; - final String fullText; + final String? roomId; + final String? fullText; final String detectedLanguage; final List tokens; final int selectedToken; @@ -14,14 +14,14 @@ class TokenInfoFeedbackRequestData { TokenInfoFeedbackRequestData({ required this.userId, - required this.roomId, - required this.fullText, required this.detectedLanguage, required this.tokens, required this.selectedToken, required this.lemmaInfo, required this.phonetics, required this.wordCardL1, + this.roomId, + this.fullText, }); @override diff --git a/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart b/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart index cc4071c9b..050c64370 100644 --- a/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart +++ b/lib/pangea/toolbar/word_card/lemma_reaction_picker.dart @@ -36,12 +36,12 @@ class LemmaReactionPicker extends StatelessWidget with LemmaEmojiSetter { ); } - Future _setEmoji(String emoji, BuildContext context) async { - await setLemmaEmoji( - constructId, - emoji, - "emoji-choice-item-$emoji-${constructId.lemma}", - ); + Future _setEmoji( + String emoji, + BuildContext context, + String targetId, + ) async { + await setLemmaEmoji(constructId, emoji, targetId); showLemmaEmojiSnackbar(context, constructId, emoji); } @@ -78,6 +78,7 @@ class LemmaReactionPicker extends StatelessWidget with LemmaEmojiSetter { .updateDispatcher .lemmaUpdateStream(constructId); + final targetId = "emoji-choice-item-${constructId.lemma}-$hashCode"; return StreamBuilder( stream: stream, builder: (context, snapshot) { @@ -87,8 +88,9 @@ class LemmaReactionPicker extends StatelessWidget with LemmaEmojiSetter { return LemmaHighlightEmojiRow( cId: constructId, langCode: langCode, - onEmojiSelected: (emoji) => emoji != selectedEmoji - ? _setEmoji(emoji, context) + targetId: targetId, + onEmojiSelected: (emoji, target) => emoji != selectedEmoji + ? _setEmoji(emoji, context, target) : _sendOrRedactReaction(emoji, context), emoji: selectedEmoji, messageInfo: event?.content ?? {},