refactor: migrate to riverpod 3
This commit is contained in:
@@ -23,90 +23,155 @@ const bool kSocketVerboseLogging = false;
|
||||
|
||||
// Chat messages for current conversation
|
||||
final chatMessagesProvider =
|
||||
StateNotifierProvider<ChatMessagesNotifier, List<ChatMessage>>((ref) {
|
||||
return ChatMessagesNotifier(ref);
|
||||
});
|
||||
NotifierProvider<ChatMessagesNotifier, List<ChatMessage>>(
|
||||
ChatMessagesNotifier.new,
|
||||
);
|
||||
|
||||
// Loading state for conversation (used to show chat skeletons during fetch)
|
||||
final isLoadingConversationProvider = StateProvider<bool>((ref) => false);
|
||||
final isLoadingConversationProvider =
|
||||
NotifierProvider<IsLoadingConversationNotifier, bool>(
|
||||
IsLoadingConversationNotifier.new,
|
||||
);
|
||||
|
||||
// Prefilled input text (e.g., when sharing text from other apps)
|
||||
final prefilledInputTextProvider = StateProvider<String?>((ref) => null);
|
||||
final prefilledInputTextProvider =
|
||||
NotifierProvider<PrefilledInputTextNotifier, String?>(
|
||||
PrefilledInputTextNotifier.new,
|
||||
);
|
||||
|
||||
// Trigger to request focus on the chat input (increment to signal)
|
||||
final inputFocusTriggerProvider = StateProvider<int>((ref) => 0);
|
||||
final inputFocusTriggerProvider =
|
||||
NotifierProvider<InputFocusTriggerNotifier, int>(
|
||||
InputFocusTriggerNotifier.new,
|
||||
);
|
||||
|
||||
// Whether the chat composer currently has focus
|
||||
final composerHasFocusProvider = StateProvider<bool>((ref) => false);
|
||||
final composerHasFocusProvider = NotifierProvider<ComposerFocusNotifier, bool>(
|
||||
ComposerFocusNotifier.new,
|
||||
);
|
||||
|
||||
class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
|
||||
final Ref _ref;
|
||||
class IsLoadingConversationNotifier extends Notifier<bool> {
|
||||
@override
|
||||
bool build() => false;
|
||||
|
||||
void set(bool value) => state = value;
|
||||
}
|
||||
|
||||
class PrefilledInputTextNotifier extends Notifier<String?> {
|
||||
@override
|
||||
String? build() => null;
|
||||
|
||||
void set(String? value) => state = value;
|
||||
|
||||
void clear() => state = null;
|
||||
}
|
||||
|
||||
class InputFocusTriggerNotifier extends Notifier<int> {
|
||||
@override
|
||||
int build() => 0;
|
||||
|
||||
void set(int value) => state = value;
|
||||
|
||||
int increment() {
|
||||
final next = state + 1;
|
||||
state = next;
|
||||
return next;
|
||||
}
|
||||
}
|
||||
|
||||
class ComposerFocusNotifier extends Notifier<bool> {
|
||||
@override
|
||||
bool build() => false;
|
||||
|
||||
void set(bool value) => state = value;
|
||||
}
|
||||
|
||||
class ChatMessagesNotifier extends Notifier<List<ChatMessage>> {
|
||||
StreamSubscription? _messageStream;
|
||||
ProviderSubscription? _conversationListener;
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
// Activity-based watchdog to prevent stuck typing indicator
|
||||
InactivityWatchdog? _typingWatchdog;
|
||||
|
||||
ChatMessagesNotifier(this._ref) : super([]) {
|
||||
// Load messages when conversation changes with proper cleanup
|
||||
_conversationListener = _ref.listen(activeConversationProvider, (
|
||||
previous,
|
||||
next,
|
||||
) {
|
||||
debugPrint('Conversation changed: ${previous?.id} -> ${next?.id}');
|
||||
bool _initialized = false;
|
||||
|
||||
// Only react when the conversation actually changes
|
||||
if (previous?.id == next?.id) {
|
||||
// If same conversation but server updated it (e.g., title/content), avoid overwriting
|
||||
// locally streamed assistant content with an outdated server copy.
|
||||
if (previous?.updatedAt != next?.updatedAt) {
|
||||
final serverMessages = next?.messages ?? const [];
|
||||
// Primary rule: adopt server messages when there are strictly more of them.
|
||||
if (serverMessages.length > state.length) {
|
||||
state = serverMessages;
|
||||
return;
|
||||
}
|
||||
@override
|
||||
List<ChatMessage> build() {
|
||||
if (!_initialized) {
|
||||
_initialized = true;
|
||||
_conversationListener = ref.listen(activeConversationProvider, (
|
||||
previous,
|
||||
next,
|
||||
) {
|
||||
debugPrint('Conversation changed: ${previous?.id} -> ${next?.id}');
|
||||
|
||||
// Secondary rule: if counts are equal but the last assistant message grew,
|
||||
// adopt the server copy to recover from missed socket events.
|
||||
if (serverMessages.isNotEmpty && state.isNotEmpty) {
|
||||
final serverLast = serverMessages.last;
|
||||
final localLast = state.last;
|
||||
final serverText = serverLast.content.trim();
|
||||
final localText = localLast.content.trim();
|
||||
final sameLastId = serverLast.id == localLast.id;
|
||||
final isAssistant = serverLast.role == 'assistant';
|
||||
final serverHasMore =
|
||||
serverText.isNotEmpty && serverText.length > localText.length;
|
||||
final localEmptyButServerHas =
|
||||
localText.isEmpty && serverText.isNotEmpty;
|
||||
if (sameLastId &&
|
||||
isAssistant &&
|
||||
(serverHasMore || localEmptyButServerHas)) {
|
||||
// Only react when the conversation actually changes
|
||||
if (previous?.id == next?.id) {
|
||||
// If same conversation but server updated it (e.g., title/content), avoid overwriting
|
||||
// locally streamed assistant content with an outdated server copy.
|
||||
if (previous?.updatedAt != next?.updatedAt) {
|
||||
final serverMessages = next?.messages ?? const [];
|
||||
// Primary rule: adopt server messages when there are strictly more of them.
|
||||
if (serverMessages.length > state.length) {
|
||||
state = serverMessages;
|
||||
return;
|
||||
}
|
||||
|
||||
// Secondary rule: if counts are equal but the last assistant message grew,
|
||||
// adopt the server copy to recover from missed socket events.
|
||||
if (serverMessages.isNotEmpty && state.isNotEmpty) {
|
||||
final serverLast = serverMessages.last;
|
||||
final localLast = state.last;
|
||||
final serverText = serverLast.content.trim();
|
||||
final localText = localLast.content.trim();
|
||||
final sameLastId = serverLast.id == localLast.id;
|
||||
final isAssistant = serverLast.role == 'assistant';
|
||||
final serverHasMore =
|
||||
serverText.isNotEmpty && serverText.length > localText.length;
|
||||
final localEmptyButServerHas =
|
||||
localText.isEmpty && serverText.isNotEmpty;
|
||||
if (sameLastId &&
|
||||
isAssistant &&
|
||||
(serverHasMore || localEmptyButServerHas)) {
|
||||
state = serverMessages;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel any existing message stream when switching conversations
|
||||
_cancelMessageStream();
|
||||
// Also cancel typing guard on conversation switch
|
||||
_cancelTypingGuard();
|
||||
// Cancel any existing message stream when switching conversations
|
||||
_cancelMessageStream();
|
||||
// Also cancel typing guard on conversation switch
|
||||
_cancelTypingGuard();
|
||||
|
||||
if (next != null) {
|
||||
state = next.messages;
|
||||
if (next != null) {
|
||||
state = next.messages;
|
||||
|
||||
// Update selected model if conversation has a different model
|
||||
_updateModelForConversation(next);
|
||||
} else {
|
||||
state = [];
|
||||
}
|
||||
});
|
||||
// Update selected model if conversation has a different model
|
||||
_updateModelForConversation(next);
|
||||
} else {
|
||||
state = [];
|
||||
}
|
||||
});
|
||||
|
||||
// ProviderSubscription will be cleaned up in dispose method
|
||||
ref.onDispose(() {
|
||||
for (final subscription in _subscriptions) {
|
||||
subscription.cancel();
|
||||
}
|
||||
_subscriptions.clear();
|
||||
|
||||
_cancelMessageStream();
|
||||
_cancelTypingGuard();
|
||||
|
||||
_conversationListener?.close();
|
||||
_conversationListener = null;
|
||||
});
|
||||
}
|
||||
|
||||
final activeConversation = ref.read(activeConversationProvider);
|
||||
return activeConversation?.messages ?? const [];
|
||||
}
|
||||
|
||||
void _addSubscription(StreamSubscription subscription) {
|
||||
@@ -137,8 +202,8 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
|
||||
// Attempt a soft recovery: if content is still empty, try fetching final content from server
|
||||
if ((last.content).trim().isEmpty) {
|
||||
try {
|
||||
final apiSvc = _ref.read(apiServiceProvider);
|
||||
final activeConv = _ref.read(activeConversationProvider);
|
||||
final apiSvc = ref.read(apiServiceProvider);
|
||||
final activeConv = ref.read(activeConversationProvider);
|
||||
final msgId = last.id;
|
||||
final chatId = activeConv?.id;
|
||||
if (apiSvc != null && chatId != null && chatId.isNotEmpty) {
|
||||
@@ -228,9 +293,9 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
|
||||
final isImageGenFlow = (meta['imageGenerationFlow'] == true);
|
||||
|
||||
// Also consult global toggles if metadata not present
|
||||
final globalWebSearch = _ref.read(webSearchEnabledProvider);
|
||||
final webSearchAvailable = _ref.read(webSearchAvailableProvider);
|
||||
final globalImageGen = _ref.read(imageGenerationEnabledProvider);
|
||||
final globalWebSearch = ref.read(webSearchEnabledProvider);
|
||||
final webSearchAvailable = ref.read(webSearchAvailableProvider);
|
||||
final globalImageGen = ref.read(imageGenerationEnabledProvider);
|
||||
|
||||
// Extend guard windows to tolerate long reasoning/tools (> 1 min)
|
||||
if (isWebSearchFlow || (globalWebSearch && webSearchAvailable)) {
|
||||
@@ -262,13 +327,13 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
|
||||
return;
|
||||
}
|
||||
|
||||
final currentSelectedModel = _ref.read(selectedModelProvider);
|
||||
final currentSelectedModel = ref.read(selectedModelProvider);
|
||||
|
||||
// If the conversation's model is different from the currently selected one
|
||||
if (currentSelectedModel?.id != conversation.model) {
|
||||
// Get available models to find the matching one
|
||||
try {
|
||||
final models = await _ref.read(modelsProvider.future);
|
||||
final models = await ref.read(modelsProvider.future);
|
||||
|
||||
if (models.isEmpty) {
|
||||
return;
|
||||
@@ -281,7 +346,7 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
|
||||
|
||||
if (conversationModel != null) {
|
||||
// Update the selected model
|
||||
_ref.read(selectedModelProvider.notifier).state = conversationModel;
|
||||
ref.read(selectedModelProvider.notifier).set(conversationModel);
|
||||
} else {
|
||||
// Model not found in available models - silently continue
|
||||
}
|
||||
@@ -447,33 +512,9 @@ class ChatMessagesNotifier extends StateNotifier<List<ChatMessage>> {
|
||||
// can pick up updated titles and ordering once streaming completes.
|
||||
// Best-effort: ignore if ref lifecycle/context prevents invalidation.
|
||||
try {
|
||||
_ref.invalidate(conversationsProvider);
|
||||
ref.invalidate(conversationsProvider);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
debugPrint(
|
||||
'ChatMessagesNotifier disposing - ${_subscriptions.length} subscriptions',
|
||||
);
|
||||
|
||||
// Cancel all tracked subscriptions
|
||||
for (final subscription in _subscriptions) {
|
||||
subscription.cancel();
|
||||
}
|
||||
_subscriptions.clear();
|
||||
|
||||
// Cancel message stream specifically
|
||||
_cancelMessageStream();
|
||||
// Cancel any active typing guard
|
||||
_cancelTypingGuard();
|
||||
|
||||
// Cancel conversation listener specifically
|
||||
_conversationListener?.close();
|
||||
_conversationListener = null;
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-seed an assistant skeleton message (with a given id or a new one),
|
||||
@@ -526,8 +567,8 @@ Future<String> _preseedAssistantAndPersist(
|
||||
// Persist the skeleton to the server so the web client sees a correct chain
|
||||
try {
|
||||
if (api != null && activeConv != null) {
|
||||
final resolvedSystemPrompt = (systemPrompt != null &&
|
||||
systemPrompt.trim().isNotEmpty)
|
||||
final resolvedSystemPrompt =
|
||||
(systemPrompt != null && systemPrompt.trim().isNotEmpty)
|
||||
? systemPrompt.trim()
|
||||
: activeConv.systemPrompt;
|
||||
final current = ref.read(chatMessagesProvider);
|
||||
@@ -567,45 +608,92 @@ String? _extractSystemPromptFromSettings(Map<String, dynamic>? settings) {
|
||||
// Start a new chat (unified function for both "New Chat" button and home screen)
|
||||
void startNewChat(dynamic ref) {
|
||||
// Clear active conversation
|
||||
ref.read(activeConversationProvider.notifier).state = null;
|
||||
ref.read(activeConversationProvider.notifier).clear();
|
||||
|
||||
// Clear messages
|
||||
ref.read(chatMessagesProvider.notifier).clearMessages();
|
||||
}
|
||||
|
||||
// Available tools provider
|
||||
final availableToolsProvider = StateProvider<List<String>>((ref) => []);
|
||||
final availableToolsProvider =
|
||||
NotifierProvider<AvailableToolsNotifier, List<String>>(
|
||||
AvailableToolsNotifier.new,
|
||||
);
|
||||
|
||||
// Web search enabled state for API-based web search
|
||||
final webSearchEnabledProvider = StateProvider<bool>((ref) => false);
|
||||
final webSearchEnabledProvider =
|
||||
NotifierProvider<WebSearchEnabledNotifier, bool>(
|
||||
WebSearchEnabledNotifier.new,
|
||||
);
|
||||
|
||||
// Image generation enabled state - behaves like web search
|
||||
final imageGenerationEnabledProvider = StateProvider<bool>((ref) => false);
|
||||
final imageGenerationEnabledProvider =
|
||||
NotifierProvider<ImageGenerationEnabledNotifier, bool>(
|
||||
ImageGenerationEnabledNotifier.new,
|
||||
);
|
||||
|
||||
// Vision capable models provider
|
||||
final visionCapableModelsProvider = StateProvider<List<String>>((ref) {
|
||||
final selectedModel = ref.watch(selectedModelProvider);
|
||||
if (selectedModel == null) return [];
|
||||
|
||||
// Check if the model supports vision (multimodal)
|
||||
if (selectedModel.isMultimodal == true) {
|
||||
return [selectedModel.id];
|
||||
}
|
||||
|
||||
// For now, assume all models support vision unless explicitly marked
|
||||
// This can be enhanced with proper model capability detection
|
||||
return [selectedModel.id];
|
||||
});
|
||||
final visionCapableModelsProvider =
|
||||
NotifierProvider<VisionCapableModelsNotifier, List<String>>(
|
||||
VisionCapableModelsNotifier.new,
|
||||
);
|
||||
|
||||
// File upload capable models provider
|
||||
final fileUploadCapableModelsProvider = StateProvider<List<String>>((ref) {
|
||||
final selectedModel = ref.watch(selectedModelProvider);
|
||||
if (selectedModel == null) return [];
|
||||
final fileUploadCapableModelsProvider =
|
||||
NotifierProvider<FileUploadCapableModelsNotifier, List<String>>(
|
||||
FileUploadCapableModelsNotifier.new,
|
||||
);
|
||||
|
||||
// For now, assume all models support file upload
|
||||
// This can be enhanced with proper model capability detection
|
||||
return [selectedModel.id];
|
||||
});
|
||||
class AvailableToolsNotifier extends Notifier<List<String>> {
|
||||
@override
|
||||
List<String> build() => [];
|
||||
|
||||
void set(List<String> tools) => state = List<String>.from(tools);
|
||||
}
|
||||
|
||||
class WebSearchEnabledNotifier extends Notifier<bool> {
|
||||
@override
|
||||
bool build() => false;
|
||||
|
||||
void set(bool value) => state = value;
|
||||
}
|
||||
|
||||
class ImageGenerationEnabledNotifier extends Notifier<bool> {
|
||||
@override
|
||||
bool build() => false;
|
||||
|
||||
void set(bool value) => state = value;
|
||||
}
|
||||
|
||||
class VisionCapableModelsNotifier extends Notifier<List<String>> {
|
||||
@override
|
||||
List<String> build() {
|
||||
final selectedModel = ref.watch(selectedModelProvider);
|
||||
if (selectedModel == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (selectedModel.isMultimodal == true) {
|
||||
return [selectedModel.id];
|
||||
}
|
||||
|
||||
// For now, assume all models support vision unless explicitly marked
|
||||
return [selectedModel.id];
|
||||
}
|
||||
}
|
||||
|
||||
class FileUploadCapableModelsNotifier extends Notifier<List<String>> {
|
||||
@override
|
||||
List<String> build() {
|
||||
final selectedModel = ref.watch(selectedModelProvider);
|
||||
if (selectedModel == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// For now, assume all models support file upload
|
||||
return [selectedModel.id];
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to validate file size
|
||||
bool validateFileSize(int fileSize, int? maxSizeMB) {
|
||||
@@ -790,9 +878,10 @@ Future<void> regenerateMessage(
|
||||
if ((activeConversation.systemPrompt == null ||
|
||||
activeConversation.systemPrompt!.trim().isEmpty) &&
|
||||
(userSystemPrompt?.isNotEmpty ?? false)) {
|
||||
final updated =
|
||||
activeConversation.copyWith(systemPrompt: userSystemPrompt);
|
||||
ref.read(activeConversationProvider.notifier).state = updated;
|
||||
final updated = activeConversation.copyWith(
|
||||
systemPrompt: userSystemPrompt,
|
||||
);
|
||||
ref.read(activeConversationProvider.notifier).set(updated);
|
||||
activeConversation = updated;
|
||||
}
|
||||
|
||||
@@ -831,7 +920,8 @@ Future<void> regenerateMessage(
|
||||
}
|
||||
|
||||
final conversationSystemPrompt = activeConversation.systemPrompt?.trim();
|
||||
final effectiveSystemPrompt = (conversationSystemPrompt != null &&
|
||||
final effectiveSystemPrompt =
|
||||
(conversationSystemPrompt != null &&
|
||||
conversationSystemPrompt.isNotEmpty)
|
||||
? conversationSystemPrompt
|
||||
: userSystemPrompt;
|
||||
@@ -840,10 +930,10 @@ Future<void> regenerateMessage(
|
||||
(m) => (m['role']?.toString().toLowerCase() ?? '') == 'system',
|
||||
);
|
||||
if (!hasSystemMessage) {
|
||||
conversationMessages.insert(
|
||||
0,
|
||||
{'role': 'system', 'content': effectiveSystemPrompt},
|
||||
);
|
||||
conversationMessages.insert(0, {
|
||||
'role': 'system',
|
||||
'content': effectiveSystemPrompt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -974,7 +1064,9 @@ Future<void> regenerateMessage(
|
||||
// Resolve tool servers from user settings (if any)
|
||||
List<Map<String, dynamic>>? toolServers;
|
||||
final uiSettings = userSettingsData?['ui'] as Map<String, dynamic>?;
|
||||
final rawServers = uiSettings != null ? (uiSettings['toolServers'] as List?) : null;
|
||||
final rawServers = uiSettings != null
|
||||
? (uiSettings['toolServers'] as List?)
|
||||
: null;
|
||||
if (rawServers != null && rawServers.isNotEmpty) {
|
||||
try {
|
||||
toolServers = await _resolveToolServers(rawServers, api);
|
||||
@@ -1151,7 +1243,7 @@ Future<void> _sendMessageInternal(
|
||||
);
|
||||
|
||||
// Set as active conversation locally
|
||||
ref.read(activeConversationProvider.notifier).state = localConversation;
|
||||
ref.read(activeConversationProvider.notifier).set(localConversation);
|
||||
activeConversation = localConversation;
|
||||
|
||||
if (!reviewerMode) {
|
||||
@@ -1170,8 +1262,7 @@ Future<void> _sendMessageInternal(
|
||||
? serverConversation.messages
|
||||
: [userMessage],
|
||||
);
|
||||
ref.read(activeConversationProvider.notifier).state =
|
||||
updatedConversation;
|
||||
ref.read(activeConversationProvider.notifier).set(updatedConversation);
|
||||
activeConversation = updatedConversation;
|
||||
|
||||
// Set messages in the messages provider to keep UI in sync
|
||||
@@ -1208,7 +1299,7 @@ Future<void> _sendMessageInternal(
|
||||
activeConversation.systemPrompt!.trim().isEmpty) &&
|
||||
(userSystemPrompt?.isNotEmpty ?? false)) {
|
||||
final updated = activeConversation.copyWith(systemPrompt: userSystemPrompt);
|
||||
ref.read(activeConversationProvider.notifier).state = updated;
|
||||
ref.read(activeConversationProvider.notifier).set(updated);
|
||||
activeConversation = updated;
|
||||
}
|
||||
|
||||
@@ -1316,8 +1407,8 @@ Future<void> _sendMessageInternal(
|
||||
}
|
||||
|
||||
final conversationSystemPrompt = activeConversation?.systemPrompt?.trim();
|
||||
final effectiveSystemPrompt = (conversationSystemPrompt != null &&
|
||||
conversationSystemPrompt.isNotEmpty)
|
||||
final effectiveSystemPrompt =
|
||||
(conversationSystemPrompt != null && conversationSystemPrompt.isNotEmpty)
|
||||
? conversationSystemPrompt
|
||||
: userSystemPrompt;
|
||||
if (effectiveSystemPrompt != null && effectiveSystemPrompt.isNotEmpty) {
|
||||
@@ -1325,10 +1416,10 @@ Future<void> _sendMessageInternal(
|
||||
(m) => (m['role']?.toString().toLowerCase() ?? '') == 'system',
|
||||
);
|
||||
if (!hasSystemMessage) {
|
||||
conversationMessages.insert(
|
||||
0,
|
||||
{'role': 'system', 'content': effectiveSystemPrompt},
|
||||
);
|
||||
conversationMessages.insert(0, {
|
||||
'role': 'system',
|
||||
'content': effectiveSystemPrompt,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1478,7 +1569,9 @@ Future<void> _sendMessageInternal(
|
||||
// Resolve tool servers from user settings (if any)
|
||||
List<Map<String, dynamic>>? toolServers;
|
||||
final uiSettings = userSettingsData?['ui'] as Map<String, dynamic>?;
|
||||
final rawServers = uiSettings != null ? (uiSettings['toolServers'] as List?) : null;
|
||||
final rawServers = uiSettings != null
|
||||
? (uiSettings['toolServers'] as List?)
|
||||
: null;
|
||||
if (rawServers != null && rawServers.isNotEmpty) {
|
||||
try {
|
||||
toolServers = await _resolveToolServers(rawServers, api);
|
||||
@@ -2377,8 +2470,9 @@ Future<void> _sendMessageInternal(
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
ref.read(activeConversationProvider.notifier).state =
|
||||
updatedConversation;
|
||||
ref
|
||||
.read(activeConversationProvider.notifier)
|
||||
.set(updatedConversation);
|
||||
} else {
|
||||
// Keep local messages and only refresh conversations list
|
||||
ref.invalidate(conversationsProvider);
|
||||
@@ -2614,7 +2708,7 @@ Future<void> _checkForTitleInBackground(
|
||||
title: updatedConv.title,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
ref.read(activeConversationProvider.notifier).state = updated;
|
||||
ref.read(activeConversationProvider.notifier).set(updated);
|
||||
|
||||
// Refresh the conversations list
|
||||
ref.invalidate(conversationsProvider);
|
||||
@@ -2679,7 +2773,7 @@ Future<void> _saveConversationLocally(dynamic ref) async {
|
||||
}
|
||||
|
||||
await storage.setString('conversations', jsonEncode(conversations));
|
||||
ref.read(activeConversationProvider.notifier).state = updatedConversation;
|
||||
ref.read(activeConversationProvider.notifier).set(updatedConversation);
|
||||
ref.invalidate(conversationsProvider);
|
||||
} catch (e) {
|
||||
// Handle local storage errors silently
|
||||
@@ -2723,8 +2817,9 @@ Future<void> pinConversation(
|
||||
// Update active conversation if it's the one being pinned
|
||||
final activeConversation = ref.read(activeConversationProvider);
|
||||
if (activeConversation?.id == conversationId) {
|
||||
ref.read(activeConversationProvider.notifier).state = activeConversation!
|
||||
.copyWith(pinned: pinned);
|
||||
ref
|
||||
.read(activeConversationProvider.notifier)
|
||||
.set(activeConversation!.copyWith(pinned: pinned));
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error ${pinned ? 'pinning' : 'unpinning'} conversation: $e');
|
||||
@@ -2743,7 +2838,7 @@ Future<void> archiveConversation(
|
||||
|
||||
// Update local state first
|
||||
if (activeConversation?.id == conversationId && archived) {
|
||||
ref.read(activeConversationProvider.notifier).state = null;
|
||||
ref.read(activeConversationProvider.notifier).clear();
|
||||
ref.read(chatMessagesProvider.notifier).clearMessages();
|
||||
}
|
||||
|
||||
@@ -2761,7 +2856,7 @@ Future<void> archiveConversation(
|
||||
|
||||
// If server operation failed and we archived locally, restore the conversation
|
||||
if (activeConversation?.id == conversationId && archived) {
|
||||
ref.read(activeConversationProvider.notifier).state = activeConversation;
|
||||
ref.read(activeConversationProvider.notifier).set(activeConversation);
|
||||
// Messages will be restored through the listener
|
||||
}
|
||||
|
||||
@@ -2796,7 +2891,7 @@ Future<void> cloneConversation(WidgetRef ref, String conversationId) async {
|
||||
final clonedConversation = await api.cloneConversation(conversationId);
|
||||
|
||||
// Set the cloned conversation as active
|
||||
ref.read(activeConversationProvider.notifier).state = clonedConversation;
|
||||
ref.read(activeConversationProvider.notifier).set(clonedConversation);
|
||||
// Load messages through the listener mechanism
|
||||
// The ChatMessagesNotifier will automatically load messages when activeConversation changes
|
||||
|
||||
@@ -2841,7 +2936,7 @@ final regenerateLastMessageProvider = Provider<Future<void> Function()>((ref) {
|
||||
final prev = ref.read(imageGenerationEnabledProvider);
|
||||
try {
|
||||
// Force image generation enabled during regeneration
|
||||
ref.read(imageGenerationEnabledProvider.notifier).state = true;
|
||||
ref.read(imageGenerationEnabledProvider.notifier).set(true);
|
||||
await regenerateMessage(
|
||||
ref,
|
||||
lastUserMessage.content,
|
||||
@@ -2849,7 +2944,7 @@ final regenerateLastMessageProvider = Provider<Future<void> Function()>((ref) {
|
||||
);
|
||||
} finally {
|
||||
// restore previous state
|
||||
ref.read(imageGenerationEnabledProvider.notifier).state = prev;
|
||||
ref.read(imageGenerationEnabledProvider.notifier).set(prev);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -49,21 +49,32 @@ class TextToSpeechState {
|
||||
}
|
||||
}
|
||||
|
||||
class TextToSpeechController extends StateNotifier<TextToSpeechState> {
|
||||
TextToSpeechController(this._service) : super(const TextToSpeechState()) {
|
||||
_service.bindHandlers(
|
||||
onStart: _handleStart,
|
||||
onComplete: _handleCompletion,
|
||||
onCancel: _handleCancellation,
|
||||
onPause: _handlePause,
|
||||
onContinue: _handleContinue,
|
||||
onError: _handleError,
|
||||
);
|
||||
}
|
||||
|
||||
final TextToSpeechService _service;
|
||||
class TextToSpeechController extends Notifier<TextToSpeechState> {
|
||||
late final TextToSpeechService _service;
|
||||
bool _handlersBound = false;
|
||||
Future<bool>? _initializationFuture;
|
||||
|
||||
@override
|
||||
TextToSpeechState build() {
|
||||
_service = ref.watch(textToSpeechServiceProvider);
|
||||
if (!_handlersBound) {
|
||||
_handlersBound = true;
|
||||
_service.bindHandlers(
|
||||
onStart: _handleStart,
|
||||
onComplete: _handleCompletion,
|
||||
onCancel: _handleCancellation,
|
||||
onPause: _handlePause,
|
||||
onContinue: _handleContinue,
|
||||
onError: _handleError,
|
||||
);
|
||||
|
||||
ref.onDispose(() {
|
||||
unawaited(_service.stop());
|
||||
});
|
||||
}
|
||||
return const TextToSpeechState();
|
||||
}
|
||||
|
||||
Future<bool> _ensureInitialized() {
|
||||
final existing = _initializationFuture;
|
||||
if (existing != null) {
|
||||
@@ -78,7 +89,7 @@ class TextToSpeechController extends StateNotifier<TextToSpeechState> {
|
||||
final future = _service
|
||||
.initialize()
|
||||
.then((available) {
|
||||
if (!mounted) {
|
||||
if (!ref.mounted) {
|
||||
return available;
|
||||
}
|
||||
|
||||
@@ -90,7 +101,7 @@ class TextToSpeechController extends StateNotifier<TextToSpeechState> {
|
||||
return available;
|
||||
})
|
||||
.catchError((error, _) {
|
||||
if (!mounted) {
|
||||
if (!ref.mounted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -132,7 +143,7 @@ class TextToSpeechController extends StateNotifier<TextToSpeechState> {
|
||||
|
||||
final available = await _ensureInitialized();
|
||||
if (!available) {
|
||||
if (!mounted) {
|
||||
if (!ref.mounted) {
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(
|
||||
@@ -151,14 +162,14 @@ class TextToSpeechController extends StateNotifier<TextToSpeechState> {
|
||||
|
||||
try {
|
||||
await _service.speak(text);
|
||||
if (!mounted) {
|
||||
if (!ref.mounted) {
|
||||
return;
|
||||
}
|
||||
if (state.status == TtsPlaybackStatus.loading) {
|
||||
state = state.copyWith(status: TtsPlaybackStatus.speaking);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) {
|
||||
if (!ref.mounted) {
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(
|
||||
@@ -178,7 +189,7 @@ class TextToSpeechController extends StateNotifier<TextToSpeechState> {
|
||||
|
||||
Future<void> stop() async {
|
||||
await _service.stop();
|
||||
if (!mounted) {
|
||||
if (!ref.mounted) {
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(
|
||||
@@ -189,14 +200,14 @@ class TextToSpeechController extends StateNotifier<TextToSpeechState> {
|
||||
}
|
||||
|
||||
void _handleStart() {
|
||||
if (!mounted) {
|
||||
if (!ref.mounted) {
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(status: TtsPlaybackStatus.speaking);
|
||||
}
|
||||
|
||||
void _handleCompletion() {
|
||||
if (!mounted) {
|
||||
if (!ref.mounted) {
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(
|
||||
@@ -206,7 +217,7 @@ class TextToSpeechController extends StateNotifier<TextToSpeechState> {
|
||||
}
|
||||
|
||||
void _handleCancellation() {
|
||||
if (!mounted) {
|
||||
if (!ref.mounted) {
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(
|
||||
@@ -216,21 +227,21 @@ class TextToSpeechController extends StateNotifier<TextToSpeechState> {
|
||||
}
|
||||
|
||||
void _handlePause() {
|
||||
if (!mounted) {
|
||||
if (!ref.mounted) {
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(status: TtsPlaybackStatus.paused);
|
||||
}
|
||||
|
||||
void _handleContinue() {
|
||||
if (!mounted) {
|
||||
if (!ref.mounted) {
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(status: TtsPlaybackStatus.speaking);
|
||||
}
|
||||
|
||||
void _handleError(String message) {
|
||||
if (!mounted) {
|
||||
if (!ref.mounted) {
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(
|
||||
@@ -239,12 +250,6 @@ class TextToSpeechController extends StateNotifier<TextToSpeechState> {
|
||||
clearActiveMessageId: true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
unawaited(_service.stop());
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
final textToSpeechServiceProvider = Provider<TextToSpeechService>((ref) {
|
||||
@@ -256,7 +261,6 @@ final textToSpeechServiceProvider = Provider<TextToSpeechService>((ref) {
|
||||
});
|
||||
|
||||
final textToSpeechControllerProvider =
|
||||
StateNotifierProvider<TextToSpeechController, TextToSpeechState>((ref) {
|
||||
final service = ref.watch(textToSpeechServiceProvider);
|
||||
return TextToSpeechController(service);
|
||||
});
|
||||
NotifierProvider<TextToSpeechController, TextToSpeechState>(
|
||||
TextToSpeechController.new,
|
||||
);
|
||||
|
||||
@@ -498,8 +498,9 @@ final fileAttachmentServiceProvider = Provider<dynamic>((ref) {
|
||||
});
|
||||
|
||||
// State notifier for managing attached files
|
||||
class AttachedFilesNotifier extends StateNotifier<List<FileUploadState>> {
|
||||
AttachedFilesNotifier() : super([]);
|
||||
class AttachedFilesNotifier extends Notifier<List<FileUploadState>> {
|
||||
@override
|
||||
List<FileUploadState> build() => [];
|
||||
|
||||
void addFiles(List<File> files) {
|
||||
final newStates = files
|
||||
@@ -536,6 +537,6 @@ class AttachedFilesNotifier extends StateNotifier<List<FileUploadState>> {
|
||||
}
|
||||
|
||||
final attachedFilesProvider =
|
||||
StateNotifierProvider<AttachedFilesNotifier, List<FileUploadState>>((ref) {
|
||||
return AttachedFilesNotifier();
|
||||
});
|
||||
NotifierProvider<AttachedFilesNotifier, List<FileUploadState>>(
|
||||
AttachedFilesNotifier.new,
|
||||
);
|
||||
|
||||
@@ -523,16 +523,41 @@ final messageBatchServiceProvider = Provider<MessageBatchService>((ref) {
|
||||
});
|
||||
|
||||
/// Provider for selected messages (for batch operations)
|
||||
final selectedMessagesProvider = StateProvider<Set<String>>((ref) {
|
||||
return <String>{};
|
||||
});
|
||||
final selectedMessagesProvider =
|
||||
NotifierProvider<SelectedMessagesNotifier, Set<String>>(
|
||||
SelectedMessagesNotifier.new,
|
||||
);
|
||||
|
||||
/// Provider for batch operation mode
|
||||
final batchModeProvider = StateProvider<bool>((ref) {
|
||||
return false;
|
||||
});
|
||||
final batchModeProvider = NotifierProvider<BatchModeNotifier, bool>(
|
||||
BatchModeNotifier.new,
|
||||
);
|
||||
|
||||
/// Provider for message filter
|
||||
final messageFilterProvider = StateProvider<MessageFilter?>((ref) {
|
||||
return null;
|
||||
});
|
||||
final messageFilterProvider =
|
||||
NotifierProvider<MessageFilterNotifier, MessageFilter?>(
|
||||
MessageFilterNotifier.new,
|
||||
);
|
||||
|
||||
class SelectedMessagesNotifier extends Notifier<Set<String>> {
|
||||
@override
|
||||
Set<String> build() => <String>{};
|
||||
|
||||
void set(Set<String> messages) => state = Set<String>.from(messages);
|
||||
|
||||
void clear() => state = <String>{};
|
||||
}
|
||||
|
||||
class BatchModeNotifier extends Notifier<bool> {
|
||||
@override
|
||||
bool build() => false;
|
||||
|
||||
void set(bool value) => state = value;
|
||||
}
|
||||
|
||||
class MessageFilterNotifier extends Notifier<MessageFilter?> {
|
||||
@override
|
||||
MessageFilter? build() => null;
|
||||
|
||||
void set(MessageFilter? filter) => state = filter;
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
void startNewChat() {
|
||||
// Clear current conversation
|
||||
ref.read(chatMessagesProvider.notifier).clearMessages();
|
||||
ref.read(activeConversationProvider.notifier).state = null;
|
||||
ref.read(activeConversationProvider.notifier).clear();
|
||||
|
||||
// Scroll to top
|
||||
if (_scrollController.hasClients) {
|
||||
@@ -140,7 +140,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
'Default provider failed, selecting first model directly',
|
||||
);
|
||||
// Fallback: select the first available model
|
||||
ref.read(selectedModelProvider.notifier).state = models.first;
|
||||
ref.read(selectedModelProvider.notifier).set(models.first);
|
||||
DebugLogger.log('Fallback model selected: ${models.first.name}');
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -220,7 +220,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
ref.read(activeConversationProvider.notifier).state = welcomeConv;
|
||||
ref.read(activeConversationProvider.notifier).set(welcomeConv);
|
||||
debugPrint('Auto-loaded demo conversation');
|
||||
return;
|
||||
}
|
||||
@@ -296,7 +296,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
}
|
||||
if (models.isNotEmpty) {
|
||||
selectedModel = models.first;
|
||||
ref.read(selectedModelProvider.notifier).state = selectedModel;
|
||||
ref.read(selectedModelProvider.notifier).set(selectedModel);
|
||||
}
|
||||
} catch (_) {
|
||||
// If models cannot be resolved, bail out without sending
|
||||
@@ -1007,12 +1007,14 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
if (!mounted) return;
|
||||
final current = ref.read(inputFocusTriggerProvider);
|
||||
// Immediate focus bump
|
||||
ref.read(inputFocusTriggerProvider.notifier).state = current + 1;
|
||||
ref
|
||||
.read(inputFocusTriggerProvider.notifier)
|
||||
.set(current + 1);
|
||||
// Second bump shortly after to overcome route/IME timing
|
||||
Future.delayed(const Duration(milliseconds: 120), () {
|
||||
if (!mounted) return;
|
||||
final cur2 = ref.read(inputFocusTriggerProvider);
|
||||
ref.read(inputFocusTriggerProvider.notifier).state = cur2 + 1;
|
||||
ref.read(inputFocusTriggerProvider.notifier).set(cur2 + 1);
|
||||
});
|
||||
});
|
||||
_didStartupFocus = true;
|
||||
@@ -1326,9 +1328,8 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
try {
|
||||
final full = await api.getConversation(active.id);
|
||||
ref
|
||||
.read(activeConversationProvider.notifier)
|
||||
.state =
|
||||
full;
|
||||
.read(activeConversationProvider.notifier)
|
||||
.set(full);
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'DEBUG: Failed to refresh conversation: $e',
|
||||
@@ -1507,7 +1508,7 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
||||
if (hadFocus) {
|
||||
// Bump focus trigger to restore composer focus + IME
|
||||
final cur = ref.read(inputFocusTriggerProvider);
|
||||
ref.read(inputFocusTriggerProvider.notifier).state = cur + 1;
|
||||
ref.read(inputFocusTriggerProvider.notifier).set(cur + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1789,11 +1790,8 @@ class _ModelSelectorSheetState extends ConsumerState<_ModelSelectorSheet> {
|
||||
onTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
widget.ref
|
||||
.read(
|
||||
selectedModelProvider.notifier,
|
||||
)
|
||||
.state =
|
||||
model;
|
||||
.read(selectedModelProvider.notifier)
|
||||
.set(model);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -115,7 +115,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
_controller.text = text;
|
||||
_controller.selection = TextSelection.collapsed(offset: text.length);
|
||||
// Clear after applying so it doesn't re-apply on rebuilds
|
||||
ref.read(prefilledInputTextProvider.notifier).state = null;
|
||||
ref.read(prefilledInputTextProvider.notifier).clear();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -131,7 +131,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
final hasFocus = _focusNode.hasFocus;
|
||||
// Publish composer focus state
|
||||
try {
|
||||
ref.read(composerHasFocusProvider.notifier).state = hasFocus;
|
||||
ref.read(composerHasFocusProvider.notifier).set(hasFocus);
|
||||
} catch (_) {}
|
||||
});
|
||||
});
|
||||
@@ -142,7 +142,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
@override
|
||||
void dispose() {
|
||||
try {
|
||||
ref.read(composerHasFocusProvider.notifier).state = false;
|
||||
ref.read(composerHasFocusProvider.notifier).set(false);
|
||||
} catch (_) {}
|
||||
_controller.removeListener(_handleComposerChanged);
|
||||
_controller.dispose();
|
||||
@@ -583,7 +583,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
offset: incoming.length,
|
||||
);
|
||||
try {
|
||||
ref.read(prefilledInputTextProvider.notifier).state = null;
|
||||
ref.read(prefilledInputTextProvider.notifier).clear();
|
||||
} catch (_) {}
|
||||
});
|
||||
});
|
||||
@@ -658,7 +658,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
: Icons.search;
|
||||
void handleTap() {
|
||||
final notifier = ref.read(webSearchEnabledProvider.notifier);
|
||||
notifier.state = !webSearchEnabled;
|
||||
notifier.set(!webSearchEnabled);
|
||||
}
|
||||
|
||||
quickPills.add(
|
||||
@@ -676,7 +676,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
: Icons.image;
|
||||
void handleTap() {
|
||||
final notifier = ref.read(imageGenerationEnabledProvider.notifier);
|
||||
notifier.state = !imageGenEnabled;
|
||||
notifier.set(!imageGenEnabled);
|
||||
}
|
||||
|
||||
quickPills.add(
|
||||
@@ -709,7 +709,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
} else {
|
||||
current.add(id);
|
||||
}
|
||||
ref.read(selectedToolIdsProvider.notifier).state = current;
|
||||
ref.read(selectedToolIdsProvider.notifier).set(current);
|
||||
}
|
||||
|
||||
quickPills.add(
|
||||
@@ -1459,7 +1459,7 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
subtitle: l10n.webSearchDescription,
|
||||
value: webSearchEnabled,
|
||||
onChanged: (next) {
|
||||
modalRef.read(webSearchEnabledProvider.notifier).state = next;
|
||||
modalRef.read(webSearchEnabledProvider.notifier).set(next);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -1479,8 +1479,9 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
subtitle: l10n.imageGenerationDescription,
|
||||
value: imageGenEnabled,
|
||||
onChanged: (next) {
|
||||
modalRef.read(imageGenerationEnabledProvider.notifier).state =
|
||||
next;
|
||||
modalRef
|
||||
.read(imageGenerationEnabledProvider.notifier)
|
||||
.set(next);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -1507,8 +1508,9 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
|
||||
} else {
|
||||
current.add(tool.id);
|
||||
}
|
||||
modalRef.read(selectedToolIdsProvider.notifier).state =
|
||||
current;
|
||||
modalRef
|
||||
.read(selectedToolIdsProvider.notifier)
|
||||
.set(current);
|
||||
},
|
||||
);
|
||||
}).toList();
|
||||
|
||||
@@ -43,10 +43,12 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
bool _draggingHasFolder = false;
|
||||
|
||||
// UI state providers for sections
|
||||
static final _showArchivedProvider = StateProvider<bool>((ref) => false);
|
||||
static final _expandedFoldersProvider = StateProvider<Map<String, bool>>(
|
||||
(ref) => {},
|
||||
);
|
||||
static final _showArchivedProvider =
|
||||
NotifierProvider<_ShowArchivedNotifier, bool>(_ShowArchivedNotifier.new);
|
||||
static final _expandedFoldersProvider =
|
||||
NotifierProvider<_ExpandedFoldersNotifier, Map<String, bool>>(
|
||||
_ExpandedFoldersNotifier.new,
|
||||
);
|
||||
|
||||
Future<void> _refreshChats() async {
|
||||
try {
|
||||
@@ -694,7 +696,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
onTap: () {
|
||||
final current = {...ref.read(_expandedFoldersProvider)};
|
||||
current[folderId] = !isExpanded;
|
||||
ref.read(_expandedFoldersProvider.notifier).state = current;
|
||||
ref.read(_expandedFoldersProvider.notifier).set(current);
|
||||
},
|
||||
onLongPress: () {
|
||||
HapticFeedback.selectionClick();
|
||||
@@ -1065,7 +1067,7 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.zero,
|
||||
onTap: () => ref.read(_showArchivedProvider.notifier).state = !show,
|
||||
onTap: () => ref.read(_showArchivedProvider.notifier).set(!show),
|
||||
overlayColor: WidgetStateProperty.resolveWith((states) {
|
||||
if (states.contains(WidgetState.pressed)) {
|
||||
return theme.buttonPrimary.withValues(
|
||||
@@ -1163,11 +1165,11 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
final container = ProviderScope.containerOf(context, listen: false);
|
||||
try {
|
||||
// Mark global loading to show skeletons in chat
|
||||
container.read(chat.isLoadingConversationProvider.notifier).state = true;
|
||||
container.read(chat.isLoadingConversationProvider.notifier).set(true);
|
||||
_pendingConversationId = id;
|
||||
|
||||
// Immediately clear current chat to show loading skeleton in the chat view
|
||||
container.read(activeConversationProvider.notifier).state = null;
|
||||
container.read(activeConversationProvider.notifier).clear();
|
||||
container.read(chat.chatMessagesProvider.notifier).clearMessages();
|
||||
|
||||
// Close the drawer immediately for faster perceived performance
|
||||
@@ -1185,21 +1187,22 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
final api = container.read(apiServiceProvider);
|
||||
if (api != null) {
|
||||
final full = await api.getConversation(id);
|
||||
container.read(activeConversationProvider.notifier).state = full;
|
||||
container.read(activeConversationProvider.notifier).set(full);
|
||||
} else {
|
||||
// Fallback: use the lightweight item to update the active conversation
|
||||
container
|
||||
.read(activeConversationProvider.notifier)
|
||||
.state = (await container.read(
|
||||
conversationsProvider.future,
|
||||
)).firstWhere((c) => c.id == id);
|
||||
container.read(activeConversationProvider.notifier).set(
|
||||
(await container.read(
|
||||
conversationsProvider.future,
|
||||
))
|
||||
.firstWhere((c) => c.id == id),
|
||||
);
|
||||
}
|
||||
|
||||
// Clear loading after data is ready
|
||||
container.read(chat.isLoadingConversationProvider.notifier).state = false;
|
||||
container.read(chat.isLoadingConversationProvider.notifier).set(false);
|
||||
_pendingConversationId = null;
|
||||
} catch (_) {
|
||||
container.read(chat.isLoadingConversationProvider.notifier).state = false;
|
||||
container.read(chat.isLoadingConversationProvider.notifier).set(false);
|
||||
_pendingConversationId = null;
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoadingConversation = false);
|
||||
@@ -1311,6 +1314,20 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
|
||||
}
|
||||
}
|
||||
|
||||
class _ShowArchivedNotifier extends Notifier<bool> {
|
||||
@override
|
||||
bool build() => false;
|
||||
|
||||
void set(bool value) => state = value;
|
||||
}
|
||||
|
||||
class _ExpandedFoldersNotifier extends Notifier<Map<String, bool>> {
|
||||
@override
|
||||
Map<String, bool> build() => {};
|
||||
|
||||
void set(Map<String, bool> value) => state = Map<String, bool>.from(value);
|
||||
}
|
||||
|
||||
class _DragConversationData {
|
||||
final String id;
|
||||
final String title;
|
||||
|
||||
@@ -9,4 +9,14 @@ final promptsListProvider = FutureProvider<List<Prompt>>((ref) async {
|
||||
return promptsService.getPrompts();
|
||||
});
|
||||
|
||||
final activePromptCommandProvider = StateProvider<String?>((ref) => null);
|
||||
final activePromptCommandProvider =
|
||||
NotifierProvider<ActivePromptCommandNotifier, String?>(
|
||||
ActivePromptCommandNotifier.new,
|
||||
);
|
||||
|
||||
class ActivePromptCommandNotifier extends Notifier<String?> {
|
||||
@override
|
||||
String? build() => null;
|
||||
|
||||
void set(String? command) => state = command;
|
||||
}
|
||||
|
||||
@@ -8,4 +8,14 @@ final toolsListProvider = FutureProvider<List<Tool>>((ref) async {
|
||||
return await toolsService.getTools();
|
||||
});
|
||||
|
||||
final selectedToolIdsProvider = StateProvider<List<String>>((ref) => []);
|
||||
final selectedToolIdsProvider =
|
||||
NotifierProvider<SelectedToolIdsNotifier, List<String>>(
|
||||
SelectedToolIdsNotifier.new,
|
||||
);
|
||||
|
||||
class SelectedToolIdsNotifier extends Notifier<List<String>> {
|
||||
@override
|
||||
List<String> build() => [];
|
||||
|
||||
void set(List<String> ids) => state = List<String>.from(ids);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user