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
This commit is contained in:
ggurdin 2025-12-30 09:07:16 -05:00 committed by GitHub
parent 99336960d2
commit c507c7b54b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 146 additions and 79 deletions

View file

@ -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<ChatPageWithRoom>
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() {

View file

@ -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!,
);
},
),
],
),

View file

@ -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<String, dynamic> 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<LemmaHighlightEmojiRow>
),
),
)
: 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(),
),
);
},

View file

@ -41,7 +41,7 @@ class _PhoneticTranscriptionWidgetState
extends State<PhoneticTranscriptionWidget> {
bool _isPlaying = false;
Future<void> _handleAudioTap() async {
Future<void> _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) {

View file

@ -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<void> 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");
});
}
}
}

View file

@ -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<String> _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,
);

View file

@ -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<PangeaToken> 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

View file

@ -36,12 +36,12 @@ class LemmaReactionPicker extends StatelessWidget with LemmaEmojiSetter {
);
}
Future<void> _setEmoji(String emoji, BuildContext context) async {
await setLemmaEmoji(
constructId,
emoji,
"emoji-choice-item-$emoji-${constructId.lemma}",
);
Future<void> _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 ?? {},