From dc55796ea6f8e8ba7c163856feecfe0f27c1273a Mon Sep 17 00:00:00 2001 From: ggurdin <46800240+ggurdin@users.noreply.github.com> Date: Thu, 2 Oct 2025 13:29:49 -0400 Subject: [PATCH] feat: show warning popup on l2/activity language mixup (#4229) --- lib/l10n/intl_en.arb | 4 +- lib/pages/chat/chat.dart | 38 +++++++++ .../controllers/choreographer.dart | 5 ++ .../utils/language_mismatch_repo.dart | 42 ++++++++++ .../widgets/igc/language_mismatch_popup.dart | 79 +++++++++++++++++++ .../widgets/start_igc_button.dart | 8 +- .../common/controllers/pangea_controller.dart | 1 + 7 files changed, 174 insertions(+), 3 deletions(-) create mode 100644 lib/pangea/choreographer/utils/language_mismatch_repo.dart create mode 100644 lib/pangea/choreographer/widgets/igc/language_mismatch_popup.dart diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index a496db14d..77a0b19fa 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -5300,5 +5300,7 @@ "playWithAI": "Play with AI for now", "courseStartDesc": "Pangea Bot is ready to go anytime!\n\n...but learning is better with friends!", "activityDropdownDesc": "When you’re done with this activity, click below", - "activityAnalyticsListBody": "These are your completed activities! After finishing activities, you can view them here." + "activityAnalyticsListBody": "These are your completed activities! After finishing activities, you can view them here.", + "languageMismatchTitle": "Language mismatch", + "languageMismatchDesc": "Your target language doesn't match the language of this activity. Update your target language?" } diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index ed3b27336..dd77c9272 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -42,6 +42,8 @@ import 'package:fluffychat/pangea/chat/widgets/event_too_large_dialog.dart'; import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; import 'package:fluffychat/pangea/choreographer/enums/edit_type.dart'; import 'package:fluffychat/pangea/choreographer/models/choreo_record.dart'; +import 'package:fluffychat/pangea/choreographer/utils/language_mismatch_repo.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/igc/language_mismatch_popup.dart'; import 'package:fluffychat/pangea/choreographer/widgets/igc/message_analytics_feedback.dart'; import 'package:fluffychat/pangea/choreographer/widgets/igc/pangea_text_controller.dart'; import 'package:fluffychat/pangea/common/constants/model_keys.dart'; @@ -2158,6 +2160,42 @@ class ChatController extends State } } + bool get shouldShowLanguageMismatchPopup { + if (!LanguageMismatchRepo.shouldShow) { + return false; + } + + final l2 = choreographer.l2Lang?.langCodeShort; + final activityLang = room.activityPlan?.req.targetLanguage.split('-').first; + return activityLang != null && l2 != null && l2 != activityLang; + } + + Future showLanguageMismatchPopup() async { + if (!shouldShowLanguageMismatchPopup) { + return; + } + + final targetLanguage = room.activityPlan!.req.targetLanguage; + LanguageMismatchRepo.set(); + OverlayUtil.showPositionedCard( + context: context, + cardToShow: LanguageMismatchPopup( + targetLanguage: targetLanguage, + choreographer: choreographer, + onUpdate: () async { + await choreographer.getLanguageHelp(manual: true); + final matches = choreographer.igc.igcTextData?.matches; + if (matches?.isNotEmpty == true) { + choreographer.igc.showFirstMatch(context); + } + }, + ), + maxHeight: 325, + maxWidth: 325, + transformTargetId: choreographer.inputTransformTargetKey, + ); + } + void _showAnalyticsFeedback( List constructs, String eventId, diff --git a/lib/pangea/choreographer/controllers/choreographer.dart b/lib/pangea/choreographer/controllers/choreographer.dart index 70ee420f4..b6ae1c447 100644 --- a/lib/pangea/choreographer/controllers/choreographer.dart +++ b/lib/pangea/choreographer/controllers/choreographer.dart @@ -122,6 +122,11 @@ class Choreographer { return; } + if (chatController.shouldShowLanguageMismatchPopup) { + chatController.showLanguageMismatchPopup(); + return; + } + if (!igc.hasRelevantIGCTextData && !itController.dismissed) { getLanguageHelp().then((value) => _sendWithIGC(context)); } else { diff --git a/lib/pangea/choreographer/utils/language_mismatch_repo.dart b/lib/pangea/choreographer/utils/language_mismatch_repo.dart new file mode 100644 index 000000000..6863164a7 --- /dev/null +++ b/lib/pangea/choreographer/utils/language_mismatch_repo.dart @@ -0,0 +1,42 @@ +import 'package:get_storage/get_storage.dart'; + +class LanguageMismatchRepo { + static final GetStorage _storage = GetStorage('language_mismatch'); + static const String key = 'shown_timestamp'; + static const Duration displayInterval = Duration(minutes: 30); + + static Future set() async { + await _storage.write(key, DateTime.now().toIso8601String()); + } + + static DateTime? _get() { + final entry = _storage.read(key); + if (entry == null) return null; + + try { + final value = DateTime.tryParse(entry); + if (value != null) { + final timeSince = DateTime.now().difference(value); + if (timeSince > displayInterval) { + _delete(); + return null; + } + return value; + } + } catch (_) { + _delete(); + } + + return null; + } + + static Future _delete() async { + await _storage.remove(key); + } + + static bool get shouldShow { + final lastShown = _get(); + if (lastShown == null) return true; + return DateTime.now().difference(lastShown) >= displayInterval; + } +} diff --git a/lib/pangea/choreographer/widgets/igc/language_mismatch_popup.dart b/lib/pangea/choreographer/widgets/igc/language_mismatch_popup.dart new file mode 100644 index 000000000..62edb4798 --- /dev/null +++ b/lib/pangea/choreographer/widgets/igc/language_mismatch_popup.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/l10n/l10n.dart'; +import 'package:fluffychat/pangea/bot/utils/bot_style.dart'; +import 'package:fluffychat/pangea/bot/widgets/bot_face_svg.dart'; +import 'package:fluffychat/pangea/choreographer/controllers/choreographer.dart'; +import 'package:fluffychat/pangea/choreographer/widgets/igc/card_header.dart'; +import 'package:fluffychat/widgets/future_loading_dialog.dart'; +import 'package:fluffychat/widgets/matrix.dart'; + +class LanguageMismatchPopup extends StatelessWidget { + final String targetLanguage; + final Choreographer choreographer; + final VoidCallback onUpdate; + + const LanguageMismatchPopup({ + super.key, + required this.targetLanguage, + required this.choreographer, + required this.onUpdate, + }); + + Future _onConfirm(BuildContext context) async { + await MatrixState.pangeaController.userController.updateProfile( + (profile) { + profile.userSettings.targetLanguage = targetLanguage; + return profile; + }, + waitForDataInSync: true, + ); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + CardHeader( + text: L10n.of(context).languageMismatchTitle, + botExpression: BotExpression.addled, + ), + Padding( + padding: const EdgeInsets.all(8), + child: Column( + spacing: 12.0, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + L10n.of(context).languageMismatchDesc, + style: BotStyle.text(context), + textAlign: TextAlign.center, + ), + SizedBox( + width: double.infinity, + child: TextButton( + onPressed: () async { + await showFutureLoadingDialog( + context: context, + future: () => _onConfirm(context), + ); + MatrixState.pAnyState.closeOverlay(); + onUpdate(); + }, + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + (Theme.of(context).colorScheme.primary).withAlpha(25), + ), + ), + child: Text(L10n.of(context).confirm), + ), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pangea/choreographer/widgets/start_igc_button.dart b/lib/pangea/choreographer/widgets/start_igc_button.dart index 6daac042a..ceef200c5 100644 --- a/lib/pangea/choreographer/widgets/start_igc_button.dart +++ b/lib/pangea/choreographer/widgets/start_igc_button.dart @@ -100,8 +100,12 @@ class StartIGCButtonState extends State ); return; case AssistanceState.notFetched: - await widget.controller.choreographer.getLanguageHelp(manual: true); - _showFirstMatch(); + if (widget.controller.shouldShowLanguageMismatchPopup) { + widget.controller.showLanguageMismatchPopup(); + } else { + await widget.controller.choreographer.getLanguageHelp(manual: true); + _showFirstMatch(); + } return; case AssistanceState.fetched: _showFirstMatch(); diff --git a/lib/pangea/common/controllers/pangea_controller.dart b/lib/pangea/common/controllers/pangea_controller.dart index afeac1a1b..fa7b2f611 100644 --- a/lib/pangea/common/controllers/pangea_controller.dart +++ b/lib/pangea/common/controllers/pangea_controller.dart @@ -126,6 +126,7 @@ class PangeaController { 'course_location_storage', 'course_activity_storage', 'course_location_media_storage', + 'language_mismatch', ]; Future clearCache({List exclude = const []}) async {