5
import 'package:go_router/go_router.dart';
6
import 'package:hugeicons/hugeicons.dart';
7
import 'package:shadcn_ui/shadcn_ui.dart';
8
-import 'package:localmind/core/theme/colors.dart';
8
import 'package:localmind/core/models/enums.dart';
9
import 'package:localmind/core/providers/app_providers.dart';
10
import 'package:localmind/core/routes/app_routes.dart';
25
import 'components/chat_input_bar.dart';
26
import 'components/chat_settings_sheet.dart';
27
import 'components/edit_message_dialog.dart';
28
+import 'components/persona_picker_sheet.dart';
29
import 'components/notification_permission_banner.dart';
30
import 'components/message_list/message_list.dart';
31
import 'components/message_list/empty_state.dart';
114
case 'new_chat':
115
ref.read(chatProvider.notifier).startNewConversation();
116
case 'persona':
117
- _showPersonaPicker(context);
117
+ showPersonaPickerSheet(context);
118
case 'remove_persona':
119
final activeConv = ref.read(conv.activeConversationProvider);
120
if (activeConv != null) {
149
}
150
}
151
152
- void _showPersonaPicker(BuildContext context) {
153
- final personasAsync = ref.read(personasNotifierProvider);
154
- final personas = personasAsync.value ?? [];
155
- final isDark = Theme.of(context).brightness == Brightness.dark;
156
- final activeConv = ref.read(conv.activeConversationProvider);
157
- final currentPersonaId = activeConv?.personaId;
158
-
159
- showModalBottomSheet(
160
- context: context,
161
- isScrollControlled: true,
162
- builder: (ctx) {
163
- final sheetL10n = AppLocalizations.of(context)!;
164
- return SafeArea(
165
- child: Container(
166
- padding: const EdgeInsets.symmetric(vertical: 16),
167
- child: Column(
168
- mainAxisSize: MainAxisSize.min,
169
- children: [
170
- Center(
171
- child: Container(
172
- width: 40,
173
- height: 4,
174
- decoration: BoxDecoration(
175
- color: isDark ? Colors.grey[600] : Colors.grey[300],
176
- borderRadius: BorderRadius.circular(2),
177
- ),
178
- ),
179
- ),
180
- const SizedBox(height: 16),
181
- Text(
182
- sheetL10n.select_persona,
183
- style: TextStyle(
184
- fontSize: 18,
185
- fontWeight: FontWeight.bold,
186
- color: isDark ? Colors.white : Colors.black,
187
- ),
188
- ),
189
- const SizedBox(height: 8),
190
- Flexible(
191
- child: ListView.builder(
192
- shrinkWrap: true,
193
- itemCount: personas.length,
194
- itemBuilder: (context, index) {
195
- final p = personas[index];
196
- final isSelected = p.id == currentPersonaId;
197
- final accent = isDark
198
- ? AppColors.darkAccent
199
- : AppColors.lightAccent;
200
- return ListTile(
201
- leading: Text(
202
- p.emoji,
203
- style: const TextStyle(fontSize: 22),
204
- ),
205
- title: Text(
206
- p.name,
207
- style: TextStyle(
208
- fontWeight: isSelected
209
- ? FontWeight.w600
210
- : FontWeight.normal,
211
- color: isDark ? Colors.white : Colors.black,
212
- ),
213
- ),
214
- subtitle: p.description != null
215
- ? Text(
216
- p.description!,
217
- style: TextStyle(
218
- fontSize: 12,
219
- color: isDark
220
- ? AppColors.darkMutedText
221
- : AppColors.lightMutedText,
222
- ),
223
- maxLines: 1,
224
- overflow: TextOverflow.ellipsis,
225
- )
226
- : null,
227
- trailing: isSelected
228
- ? Icon(Icons.check_circle, color: accent)
229
- : null,
230
- onTap: () {
231
- if (activeConv != null) {
232
- ref
233
- .read(conv.conversationsProvider.notifier)
234
- .updatePersona(
235
- activeConv.id,
236
- p.id,
237
- p.systemPrompt,
238
- );
239
- }
240
- Navigator.pop(ctx);
241
- },
242
- );
243
- },
244
- ),
245
- ),
246
- ],
247
- ),
248
- ),
249
- );
250
- },
251
- );
252
- }
253
-
152
void _showToolApprovalDialog(
153
BuildContext context,
154
PendingToolApproval approval,
265
isDark: isDark,
266
persona: persona,
267
onMenuAction: onMenuAction,
370
- onPersonaPicker: () => _openPersonaPicker(context, ref),
268
+ onPersonaPicker: () => showPersonaPickerSheet(context),
269
),
270
const NotificationPermissionBanner(),
271
if (connectionStatus == ConnectionStatus.disconnected ||
274
if (persona != null)
275
PersonaIndicator(
276
persona: persona,
379
- onTap: () => _openPersonaPicker(context, ref),
277
+ onTap: () => showPersonaPickerSheet(context),
278
onRemove: () {
279
final activeConv = ref.read(conv.activeConversationProvider);
280
if (activeConv != null) {
313
}
314
}
315
418
-void _openPersonaPicker(BuildContext context, WidgetRef ref) {
419
- final personasAsync = ref.read(personasNotifierProvider);
420
- final personas = personasAsync.value ?? [];
421
- final isDark = Theme.of(context).brightness == Brightness.dark;
422
- final activeConv = ref.read(conv.activeConversationProvider);
423
- final currentPersonaId = activeConv?.personaId;
424
-
425
- showModalBottomSheet(
426
- context: context,
427
- isScrollControlled: true,
428
- builder: (ctx) {
429
- final sheetL10n = AppLocalizations.of(context)!;
430
- return SafeArea(
431
- child: Container(
432
- padding: const EdgeInsets.symmetric(vertical: 16),
433
- child: Column(
434
- mainAxisSize: MainAxisSize.min,
435
- children: [
436
- Center(
437
- child: Container(
438
- width: 40,
439
- height: 4,
440
- decoration: BoxDecoration(
441
- color: isDark ? Colors.grey[600] : Colors.grey[300],
442
- borderRadius: BorderRadius.circular(2),
443
- ),
444
- ),
445
- ),
446
- const SizedBox(height: 16),
447
- Text(
448
- sheetL10n.select_persona,
449
- style: TextStyle(
450
- fontSize: 18,
451
- fontWeight: FontWeight.bold,
452
- color: isDark ? Colors.white : Colors.black,
453
- ),
454
- ),
455
- const SizedBox(height: 8),
456
- Flexible(
457
- child: ListView.builder(
458
- shrinkWrap: true,
459
- itemCount: personas.length,
460
- itemBuilder: (context, index) {
461
- final p = personas[index];
462
- final isSelected = p.id == currentPersonaId;
463
- final accent = isDark
464
- ? AppColors.darkAccent
465
- : AppColors.lightAccent;
466
- return ListTile(
467
- leading: Text(
468
- p.emoji,
469
- style: const TextStyle(fontSize: 22),
470
- ),
471
- title: Text(
472
- p.name,
473
- style: TextStyle(
474
- fontWeight: isSelected
475
- ? FontWeight.w600
476
- : FontWeight.normal,
477
- color: isDark ? Colors.white : Colors.black,
478
- ),
479
- ),
480
- subtitle: p.description != null
481
- ? Text(
482
- p.description!,
483
- style: TextStyle(
484
- fontSize: 12,
485
- color: isDark
486
- ? AppColors.darkMutedText
487
- : AppColors.lightMutedText,
488
- ),
489
- maxLines: 1,
490
- overflow: TextOverflow.ellipsis,
491
- )
492
- : null,
493
- trailing: isSelected
494
- ? Icon(Icons.check_circle, color: accent)
495
- : null,
496
- onTap: () {
497
- if (activeConv != null) {
498
- ref
499
- .read(conv.conversationsProvider.notifier)
500
- .updatePersona(
501
- activeConv.id,
502
- p.id,
503
- p.systemPrompt,
504
- );
505
- }
506
- Navigator.pop(ctx);
507
- },
508
- );
509
- },
510
- ),
511
- ),
512
- ],
513
- ),
514
- ),
515
- );
516
- },
517
- ).then((_) {});
518
-}
519
-
316
class _MessageArea extends ConsumerWidget {
317
const _MessageArea({
318
required this.isLoading,
370
selectedModel: selectedModel,
371
onModelTap: onModelPicker,
372
selectedPersona: ref.watch(selectedPersonaProvider),
577
- onPersonaTap: () => _openPersonaPickerForPreselection(context, ref),
373
+ onPersonaTap: () => showPersonaPickerSheet(
374
+ context,
375
+ mode: PersonaPickerMode.preselection,
376
+ ),
377
);
378
}
379
396
.editMessage(messageId, newContent);
397
}
398
},
399
+ onEditAssistant: (messageId, currentContent) async {
400
+ final l10n = AppLocalizations.of(context)!;
401
+ final newContent = await EditMessageDialog.show(
402
+ context,
403
+ initialContent: currentContent,
404
+ description: l10n.edit_assistant_message_desc,
405
+ saveLabel: l10n.save,
406
+ );
407
+ if (newContent != null && newContent != currentContent) {
408
+ await ref
409
+ .read(chatProvider.notifier)
410
+ .editAssistantMessage(messageId, newContent);
411
+ }
412
+ },
413
+ onBranch: (messageId) =>
414
+ ref.read(chatProvider.notifier).branchFromMessage(messageId),
415
+ onModelPicker: onModelPicker,
416
hasSmartReplies:
417
!isStreaming &&
418
keyboardBottomInset == 0 &&
422
}
423
}
424
609
-void _openPersonaPickerForPreselection(BuildContext context, WidgetRef ref) {
610
- final personasAsync = ref.read(personasNotifierProvider);
611
- final personas = personasAsync.value ?? [];
612
- final isDark = Theme.of(context).brightness == Brightness.dark;
613
- final currentPersona = ref.watch(selectedPersonaProvider);
614
-
615
- showModalBottomSheet(
616
- context: context,
617
- isScrollControlled: true,
618
- builder: (ctx) {
619
- final sheetL10n = AppLocalizations.of(context)!;
620
- return SafeArea(
621
- child: Container(
622
- padding: const EdgeInsets.symmetric(vertical: 16),
623
- child: Column(
624
- mainAxisSize: MainAxisSize.min,
625
- children: [
626
- Center(
627
- child: Container(
628
- width: 40,
629
- height: 4,
630
- decoration: BoxDecoration(
631
- color: isDark ? Colors.grey[600] : Colors.grey[300],
632
- borderRadius: BorderRadius.circular(2),
633
- ),
634
- ),
635
- ),
636
- const SizedBox(height: 16),
637
- Text(
638
- sheetL10n.select_persona,
639
- style: TextStyle(
640
- fontSize: 18,
641
- fontWeight: FontWeight.bold,
642
- color: isDark ? Colors.white : Colors.black,
643
- ),
644
- ),
645
- const SizedBox(height: 8),
646
- Flexible(
647
- child: ListView.builder(
648
- shrinkWrap: true,
649
- itemCount: personas.length,
650
- itemBuilder: (context, index) {
651
- final p = personas[index];
652
- final isSelected = p.id == currentPersona?.id;
653
- final accent = isDark
654
- ? AppColors.darkAccent
655
- : AppColors.lightAccent;
656
- return ListTile(
657
- leading: Text(
658
- p.emoji,
659
- style: const TextStyle(fontSize: 22),
660
- ),
661
- title: Text(
662
- p.name,
663
- style: TextStyle(
664
- fontWeight: isSelected
665
- ? FontWeight.w600
666
- : FontWeight.normal,
667
- color: isDark ? Colors.white : Colors.black,
668
- ),
669
- ),
670
- subtitle: p.description != null
671
- ? Text(
672
- p.description!,
673
- style: TextStyle(
674
- fontSize: 12,
675
- color: isDark
676
- ? AppColors.darkMutedText
677
- : AppColors.lightMutedText,
678
- ),
679
- maxLines: 1,
680
- overflow: TextOverflow.ellipsis,
681
- )
682
- : null,
683
- trailing: isSelected
684
- ? Icon(Icons.check_circle, color: accent)
685
- : null,
686
- onTap: () {
687
- ref.read(selectedPersonaProvider.notifier).select(p);
688
- Navigator.pop(ctx);
689
- },
690
- );
691
- },
692
- ),
693
- ),
694
- ],
695
- ),
696
- ),
697
- );
698
- },
699
- ).then((_) {});
700
-}
701
-
425
class _ScreenAppBar extends ConsumerWidget {
426
const _ScreenAppBar({
427
required this.activeServer,