6 files changed
+141
-49
lib/features/chat/providers/chat_providers.dart
+1
@@ -730,6 +730,7 @@ class ChatNotifier extends Notifier<ChatState> {
730
void cancelStream() {
731
_streamSubscription?.cancel();
732
_streamSubscription = null;
733
+ ref.read(chatServiceProvider).cancelStream();
734
state = state.copyWith(isStreaming: false, clearStreaming: true);
735
}
736
lib/features/chat/views/chat_screen.dart
+40
-24
@@ -776,12 +776,16 @@ class _EmptyStateState extends State<_EmptyState>
776
color: theme.colorScheme.primary,
777
),
778
const SizedBox(width: 8),
779
- Text(
780
- widget.selectedModel?.displayName ?? 'Select Model',
781
- style: TextStyle(
782
- fontSize: 15,
783
- fontWeight: FontWeight.w600,
784
- color: isDark ? Colors.white : Colors.black,
779
+ Flexible(
780
+ child: Text(
781
+ widget.selectedModel?.displayName ?? 'Select Model',
782
+ style: TextStyle(
783
+ fontSize: 15,
784
+ fontWeight: FontWeight.w600,
785
+ color: isDark ? Colors.white : Colors.black,
786
+ ),
787
+ maxLines: 2,
788
+ overflow: TextOverflow.ellipsis,
789
),
790
),
791
const SizedBox(width: 4),
@@ -820,12 +824,16 @@ class _EmptyStateState extends State<_EmptyState>
824
style: const TextStyle(fontSize: 18),
825
),
826
const SizedBox(width: 8),
823
- Text(
824
- widget.selectedPersona!.name,
825
- style: TextStyle(
826
- fontSize: 15,
827
- fontWeight: FontWeight.w600,
828
- color: isDark ? Colors.white : Colors.black,
827
+ Flexible(
828
+ child: Text(
829
+ widget.selectedPersona!.name,
830
+ style: TextStyle(
831
+ fontSize: 15,
832
+ fontWeight: FontWeight.w600,
833
+ color: isDark ? Colors.white : Colors.black,
834
+ ),
835
+ maxLines: 2,
836
+ overflow: TextOverflow.ellipsis,
837
),
838
),
839
] else ...[
@@ -835,12 +843,16 @@ class _EmptyStateState extends State<_EmptyState>
843
color: theme.colorScheme.primary,
844
),
845
const SizedBox(width: 8),
838
- Text(
839
- 'Select Persona',
840
- style: TextStyle(
841
- fontSize: 15,
842
- fontWeight: FontWeight.w600,
843
- color: isDark ? Colors.white : Colors.black,
846
+ Flexible(
847
+ child: Text(
848
+ 'Select Persona',
849
+ style: TextStyle(
850
+ fontSize: 15,
851
+ fontWeight: FontWeight.w600,
852
+ color: isDark ? Colors.white : Colors.black,
853
+ ),
854
+ maxLines: 2,
855
+ overflow: TextOverflow.ellipsis,
856
),
857
),
858
],
@@ -1316,12 +1328,16 @@ class _PersonaIndicator extends StatelessWidget {
1328
children: [
1329
Text(persona.emoji, style: const TextStyle(fontSize: 16)),
1330
const SizedBox(width: 6),
1319
- Text(
1320
- persona.name,
1321
- style: TextStyle(
1322
- fontSize: 13,
1323
- fontWeight: FontWeight.w600,
1324
- color: theme.colorScheme.primary,
1331
+ Flexible(
1332
+ child: Text(
1333
+ persona.name,
1334
+ style: TextStyle(
1335
+ fontSize: 13,
1336
+ fontWeight: FontWeight.w600,
1337
+ color: theme.colorScheme.primary,
1338
+ ),
1339
+ maxLines: 2,
1340
+ overflow: TextOverflow.ellipsis,
1341
),
1342
),
1343
],
lib/features/chat/views/components/chat_input_bar.dart
+1
-1
@@ -275,7 +275,7 @@ class _ChatInputBarState extends ConsumerState<ChatInputBar>
275
color: isDark ? Colors.white : Colors.black87,
276
),
277
decoration: InputDecoration(
278
- hintText: 'Reply',
278
+ hintText: 'Ask anything',
279
hintStyle: TextStyle(
280
color: isDark ? Colors.white38 : Colors.black38,
281
fontSize: 15,
lib/features/conversations/data/models/conversation.dart
+4
-2
@@ -58,11 +58,13 @@ class Conversation extends HiveObject {
58
DateTime? updatedAt,
59
bool? isPinned,
60
String? personaId,
61
+ bool clearPersona = false,
62
String? serverId,
63
String? modelId,
64
int? messageCount,
65
String? lastMessagePreview,
66
String? systemPrompt,
67
+ bool clearSystemPrompt = false,
68
}) {
69
return Conversation(
70
id: id ?? this.id,
@@ -70,12 +72,12 @@ class Conversation extends HiveObject {
72
createdAt: createdAt ?? this.createdAt,
73
updatedAt: updatedAt ?? this.updatedAt,
74
isPinned: isPinned ?? this.isPinned,
73
- personaId: personaId ?? this.personaId,
75
+ personaId: clearPersona ? null : (personaId ?? this.personaId),
76
serverId: serverId ?? this.serverId,
77
modelId: modelId ?? this.modelId,
78
messageCount: messageCount ?? this.messageCount,
79
lastMessagePreview: lastMessagePreview ?? this.lastMessagePreview,
78
- systemPrompt: systemPrompt ?? this.systemPrompt,
80
+ systemPrompt: clearSystemPrompt ? null : (systemPrompt ?? this.systemPrompt),
81
);
82
}
83
}
lib/features/conversations/providers/conversation_providers.dart
+24
-2
@@ -125,7 +125,9 @@ class ConversationsNotifier extends Notifier<List<Conversation>> {
125
if (conversation != null) {
126
final updated = conversation.copyWith(
127
personaId: personaId,
128
+ clearPersona: personaId == null,
129
systemPrompt: systemPrompt,
130
+ clearSystemPrompt: systemPrompt == null,
131
updatedAt: DateTime.now(),
132
);
133
await box.put(id, updated);
@@ -154,14 +156,34 @@ final activeConversationProvider =
156
});
157
158
class ActiveConversationNotifier extends Notifier<Conversation?> {
159
+ String? _activeConversationId;
160
+
161
@override
162
Conversation? build() {
163
final conversations = ref.watch(conversationsProvider);
160
- if (conversations.isEmpty) return null;
161
- return conversations.first;
164
+ if (conversations.isEmpty) {
165
+ _activeConversationId = null;
166
+ return null;
167
+ }
168
+
169
+ // Attempt to maintain the currently active conversation
170
+ if (_activeConversationId != null) {
171
+ final conversation = conversations
172
+ .where((c) => c.id == _activeConversationId)
173
+ .firstOrNull;
174
+ if (conversation != null) {
175
+ return conversation;
176
+ }
177
+ }
178
+
179
+ // Default to the first available conversation if none active or active one was deleted
180
+ final first = conversations.first;
181
+ _activeConversationId = first.id;
182
+ return first;
183
}
184
185
void setActiveConversation(Conversation? conversation) {
186
+ _activeConversationId = conversation?.id;
187
if (conversation != null) {
188
final conversations = ref.read(conversationsProvider);
189
final index = conversations.indexWhere((c) => c.id == conversation.id);
lib/features/models/screens/model_picker_sheet.dart
+71
-20
@@ -346,6 +346,39 @@ class _ModelList extends ConsumerWidget {
346
Navigator.pop(context);
347
}
348
},
349
+ onUnload: () async {
350
+ final activeServer = ref.read(activeServerProvider);
351
+ if (activeServer == null) return;
352
+
353
+ try {
354
+ final apiService = ref.read(serverApiServiceProvider);
355
+ await apiService.unloadModel(activeServer, model.id);
356
+ ref.invalidate(loadedModelsProvider(activeServer));
357
+
358
+ // If the model being unloaded is the currently selected one, clear it
359
+ final selectedModel = ref.read(selectedModelProvider);
360
+ if (selectedModel?.id == model.id) {
361
+ ref.read(selectedModelProvider.notifier).clear();
362
+ }
363
+
364
+ if (context.mounted) {
365
+ ScaffoldMessenger.of(context).showSnackBar(
366
+ SnackBar(
367
+ content: Text('${model.name} unloaded successfully'),
368
+ ),
369
+ );
370
+ }
371
+ } catch (e) {
372
+ if (context.mounted) {
373
+ ScaffoldMessenger.of(context).showSnackBar(
374
+ SnackBar(
375
+ content: Text('Failed to unload model: $e'),
376
+ backgroundColor: Colors.red,
377
+ ),
378
+ );
379
+ }
380
+ }
381
+ },
382
);
383
},
384
);
@@ -361,6 +394,7 @@ class _ModelTile extends StatelessWidget {
394
required this.isLoaded,
395
required this.isDark,
396
required this.onTap,
397
+ this.onUnload,
398
});
399
400
final ModelInfo model;
@@ -368,6 +402,7 @@ class _ModelTile extends StatelessWidget {
402
final bool isLoaded;
403
final bool isDark;
404
final VoidCallback onTap;
405
+ final Future<void> Function()? onUnload;
406
407
@override
408
Widget build(BuildContext context) {
@@ -396,28 +431,20 @@ class _ModelTile extends StatelessWidget {
431
children: [
432
Row(
433
children: [
399
- Text(
400
- model.displayName,
401
- style: TextStyle(
402
- fontSize: 15,
403
- fontWeight: isSelected
404
- ? FontWeight.w600
405
- : FontWeight.w500,
406
- color: isDark ? Colors.white : Colors.black,
407
- ),
408
- maxLines: 1,
409
- overflow: TextOverflow.ellipsis,
410
- ),
411
- if (isLoaded)
412
- Container(
413
- width: 8,
414
- height: 8,
415
- margin: const EdgeInsets.only(left: 8),
416
- decoration: const BoxDecoration(
417
- color: Color(0xFF4CAF50),
418
- shape: BoxShape.circle,
434
+ Expanded(
435
+ child: Text(
436
+ model.displayName,
437
+ style: TextStyle(
438
+ fontSize: 15,
439
+ fontWeight: isSelected
440
+ ? FontWeight.w600
441
+ : FontWeight.w500,
442
+ color: isDark ? Colors.white : Colors.black,
443
),
444
+ maxLines: 1,
445
+ overflow: TextOverflow.ellipsis,
446
),
447
+ ),
448
],
449
),
450
const SizedBox(height: 4),
@@ -450,6 +477,30 @@ class _ModelTile extends StatelessWidget {
477
],
478
),
479
),
480
+ if (isLoaded) ...[
481
+ Container(
482
+ width: 15,
483
+ height: 15,
484
+ decoration: const BoxDecoration(
485
+ color: Color(0xFF4CAF50),
486
+ shape: BoxShape.circle,
487
+ ),
488
+ ),
489
+ ],
490
+ if (isLoaded) ...[
491
+ IconButton(
492
+ icon: Icon(
493
+ Icons.power_settings_new_outlined,
494
+ size: 18,
495
+ color: Colors.red[400],
496
+ ),
497
+ onPressed: onUnload,
498
+ padding: EdgeInsets.zero,
499
+ constraints: const BoxConstraints(),
500
+ tooltip: 'Unload from server',
501
+ ),
502
+ const SizedBox(width: 8),
503
+ ],
504
if (isSelected)
505
Icon(Icons.check_circle, color: accent, size: 22)
506
else