@tduncan / Cortana / commits / 5423825

feat: enforce model selection before sending messages by prompting the user with a picker sheet

abdulmominsakib committed Jul 27, 2026 at 13:21 UTC 542382544d0f1eb7995f7eb89a7730f9c5b3a761
14 files changed +92 -2
lib/features/chat/views/components/chat_bottom_bar.dart
+15
@@ -6,6 +6,8 @@ import 'package:localmind/features/chat/views/components/chat_input_bar.dart';
6 import 'package:localmind/features/chat/views/components/top_bar/smart_reply_chips.dart';
7 import 'package:localmind/features/chat/providers/chat_providers.dart';
8 import 'package:localmind/features/conversations/providers/conversation_providers.dart' as conv;
9 +import 'package:localmind/features/models/views/model_picker_sheet.dart';
10 +import 'package:localmind/l10n/app_localizations.dart';
11
12 class ChatBottomBar extends ConsumerWidget {
13 const ChatBottomBar({
@@ -107,6 +109,19 @@ class SmartReplyChipsWrapper extends ConsumerWidget {
109 Widget build(BuildContext context, WidgetRef ref) {
110 return SmartReplyChips(
111 onSend: (message) {
112 + if (ref.read(selectedModelProvider) == null) {
113 + final l10n = AppLocalizations.of(context)!;
114 + ScaffoldMessenger.of(context).showSnackBar(
115 + SnackBar(content: Text(l10n.model_required_toast)),
116 + );
117 + showModalBottomSheet(
118 + context: context,
119 + isScrollControlled: true,
120 + useSafeArea: true,
121 + builder: (_) => const ModelPickerSheet(),
122 + );
123 + return;
124 + }
125 ref.read(chatProvider.notifier).sendMessage(message);
126 },
127 );
lib/features/chat/views/components/chat_input_bar.dart
+23
@@ -19,6 +19,7 @@ import '../../../stt/providers/stt_providers.dart';
19 import '../../providers/chat_providers.dart';
20 import '../../utils/attachment_helpers.dart';
21 import '../../utils/image_upload_utils.dart';
22 +import '../../../models/views/model_picker_sheet.dart';
23 import 'image_preview_dialog.dart';
24
25 class ChatInputBar extends ConsumerStatefulWidget {
@@ -429,6 +430,8 @@ class ChatInputBarState extends ConsumerState<ChatInputBar>
430
431 if (text.isEmpty && _attachedFiles.isEmpty) return;
432
433 + if (!_ensureModelSelected()) return;
434 +
435 Haptics.vibrate(HapticsType.medium);
436
437 if (_sendAsAssistant) {
@@ -455,6 +458,8 @@ class ChatInputBarState extends ConsumerState<ChatInputBar>
458 final text = _controller.text.trim();
459 if (text.isEmpty && _attachedFiles.isEmpty) return;
460
461 + if (!_ensureModelSelected()) return;
462 +
463 Haptics.vibrate(HapticsType.medium);
464 ref
465 .read(chatProvider.notifier)
@@ -471,6 +476,24 @@ class ChatInputBarState extends ConsumerState<ChatInputBar>
476 });
477 }
478
479 + /// Returns true if a model is currently selected. When no model is selected,
480 + /// shows a SnackBar prompting the user to pick one and opens the model
481 + /// picker sheet, then returns false so the caller can bail out.
482 + bool _ensureModelSelected() {
483 + if (ref.read(selectedModelProvider) != null) return true;
484 + final l10n = AppLocalizations.of(context)!;
485 + ScaffoldMessenger.of(context).showSnackBar(
486 + SnackBar(content: Text(l10n.model_required_toast)),
487 + );
488 + showModalBottomSheet(
489 + context: context,
490 + isScrollControlled: true,
491 + useSafeArea: true,
492 + builder: (_) => const ModelPickerSheet(),
493 + );
494 + return false;
495 + }
496 +
497 void _handleStop() {
498 Haptics.vibrate(HapticsType.light);
499
lib/features/chat/views/components/message_list/message_area.dart
+17 -2
@@ -8,6 +8,7 @@ import 'package:localmind/features/chat/data/models/message.dart';
8 import 'package:localmind/features/chat/providers/chat_providers.dart';
9 import 'package:localmind/features/conversations/data/models/conversation.dart';
10 import 'package:localmind/features/conversations/providers/conversation_providers.dart' as conv;
11 +import 'package:localmind/features/models/views/model_picker_sheet.dart';
12 import 'package:localmind/features/personas/providers/personas_providers.dart';
13 import 'package:localmind/features/chat/views/components/model_info_sheet.dart';
14 import 'package:localmind/features/chat/views/components/edit_message_dialog.dart';
@@ -68,8 +69,22 @@ class MessageArea extends ConsumerWidget {
69
70 return EmptyState(
71 isCloudProvider: isCloudProvider,
71 - onQuickPrompt: (prompt) =>
72 - ref.read(chatProvider.notifier).sendMessage(prompt),
72 + onQuickPrompt: (prompt) {
73 + if (ref.read(selectedModelProvider) == null) {
74 + final toastL10n = AppLocalizations.of(context)!;
75 + ScaffoldMessenger.of(context).showSnackBar(
76 + SnackBar(content: Text(toastL10n.model_required_toast)),
77 + );
78 + showModalBottomSheet(
79 + context: context,
80 + isScrollControlled: true,
81 + useSafeArea: true,
82 + builder: (_) => const ModelPickerSheet(),
83 + );
84 + return;
85 + }
86 + ref.read(chatProvider.notifier).sendMessage(prompt);
87 + },
88 quickPrompts: [
89 l10n.quick_write,
90 l10n.quick_explain,
lib/l10n/app_en.arb
+4
@@ -3093,5 +3093,9 @@
3093 "folder_name_required": "Please enter a folder name",
3094 "@folder_name_required": {
3095 "description": "Validation error when a folder name is empty"
3096 + },
3097 + "model_required_toast": "You need to select a model first",
3098 + "@model_required_toast": {
3099 + "description": "Toast shown when the user tries to send a message but no model is selected"
3100 }
3101 }
lib/l10n/app_localizations.dart
+6
@@ -5260,6 +5260,12 @@ abstract class AppLocalizations {
5260 /// In en, this message translates to:
5261 /// **'Please enter a folder name'**
5262 String get folder_name_required;
5263 +
5264 + /// Toast shown when the user tries to send a message but no model is selected
5265 + ///
5266 + /// In en, this message translates to:
5267 + /// **'You need to select a model first'**
5268 + String get model_required_toast;
5269 }
5270
5271 class _AppLocalizationsDelegate
lib/l10n/app_localizations_ar.dart
+3
@@ -2796,4 +2796,7 @@ class AppLocalizationsAr extends AppLocalizations {
2796
2797 @override
2798 String get folder_name_required => 'الرجاء إدخال اسم المجلد';
2799 +
2800 + @override
2801 + String get model_required_toast => 'You need to select a model first';
2802 }
lib/l10n/app_localizations_bn.dart
+3
@@ -2816,4 +2816,7 @@ class AppLocalizationsBn extends AppLocalizations {
2816
2817 @override
2818 String get folder_name_required => 'অনুগ্রহ করে একটি ফোল্ডারের নাম লিখুন';
2819 +
2820 + @override
2821 + String get model_required_toast => 'You need to select a model first';
2822 }
lib/l10n/app_localizations_en.dart
+3
@@ -2804,4 +2804,7 @@ class AppLocalizationsEn extends AppLocalizations {
2804
2805 @override
2806 String get folder_name_required => 'Please enter a folder name';
2807 +
2808 + @override
2809 + String get model_required_toast => 'You need to select a model first';
2810 }
lib/l10n/app_localizations_es.dart
+3
@@ -2820,4 +2820,7 @@ class AppLocalizationsEs extends AppLocalizations {
2820
2821 @override
2822 String get folder_name_required => 'Introduce un nombre de carpeta';
2823 +
2824 + @override
2825 + String get model_required_toast => 'You need to select a model first';
2826 }
lib/l10n/app_localizations_hi.dart
+3
@@ -2809,4 +2809,7 @@ class AppLocalizationsHi extends AppLocalizations {
2809
2810 @override
2811 String get folder_name_required => 'कृपया फ़ोल्डर का नाम दर्ज करें';
2812 +
2813 + @override
2814 + String get model_required_toast => 'You need to select a model first';
2815 }
lib/l10n/app_localizations_it.dart
+3
@@ -2823,4 +2823,7 @@ class AppLocalizationsIt extends AppLocalizations {
2823
2824 @override
2825 String get folder_name_required => 'Inserisci un nome per la cartella';
2826 +
2827 + @override
2828 + String get model_required_toast => 'You need to select a model first';
2829 }
lib/l10n/app_localizations_ja.dart
+3
@@ -2756,4 +2756,7 @@ class AppLocalizationsJa extends AppLocalizations {
2756
2757 @override
2758 String get folder_name_required => 'フォルダ名を入力してください';
2759 +
2760 + @override
2761 + String get model_required_toast => 'You need to select a model first';
2762 }
lib/l10n/app_localizations_ru.dart
+3
@@ -2823,4 +2823,7 @@ class AppLocalizationsRu extends AppLocalizations {
2823
2824 @override
2825 String get folder_name_required => 'Введите название папки';
2826 +
2827 + @override
2828 + String get model_required_toast => 'You need to select a model first';
2829 }
lib/l10n/app_localizations_zh.dart
+3
@@ -2736,6 +2736,9 @@ class AppLocalizationsZh extends AppLocalizations {
2736
2737 @override
2738 String get folder_name_required => '请输入文件夹名称';
2739 +
2740 + @override
2741 + String get model_required_toast => 'You need to select a model first';
2742 }
2743
2744 /// The translations for Chinese, as used in Taiwan (`zh_TW`).