From ff6d33abdfc46d42079d50a12ba0c6468cd3785e Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:20:08 +0530 Subject: [PATCH] refactor: enhance model loading and error handling in providers - Improved handling of asynchronous states in the model loading process. - Added debug logging for better traceability of model loading failures. - Ensured proper checks for mounted state to prevent updates after disposal. - Cleaned up code formatting for better readability. - Updated the `defaultModel` provider to include more detailed logging and error handling. --- lib/core/providers/app_providers.dart | 104 +++++++++++++++++++------ lib/features/chat/views/chat_page.dart | 40 +++++++++- 2 files changed, 117 insertions(+), 27 deletions(-) diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index 064ad2e..c785399 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -924,7 +924,9 @@ Future> conversations(Ref ref) async { ); // Update cache timestamp - ref.read(_conversationsCacheTimestampProvider.notifier).set(DateTime.now()); + ref + .read(_conversationsCacheTimestampProvider.notifier) + .set(DateTime.now()); return sortedConversations; } catch (e) { @@ -942,7 +944,9 @@ Future> conversations(Ref ref) async { ); // Update cache timestamp - ref.read(_conversationsCacheTimestampProvider.notifier).set(DateTime.now()); + ref + .read(_conversationsCacheTimestampProvider.notifier) + .set(DateTime.now()); return conversations; // Return original conversations if folder fetch fails } @@ -1003,13 +1007,16 @@ Future loadConversation(Ref ref, String conversationId) async { } // Provider to automatically load and set the default model from user settings or OpenWebUI -@riverpod +@Riverpod(keepAlive: true) Future defaultModel(Ref ref) async { + DebugLogger.log('provider-called', scope: 'models/default'); + // Initialize the settings watcher (side-effect only) ref.read(_settingsWatcherProvider); // Read settings without subscribing to rebuilds to avoid watch/await hazards final reviewerMode = ref.read(reviewerModeProvider); if (reviewerMode) { + DebugLogger.log('reviewer-mode', scope: 'models/default'); // Check if a model is manually selected final currentSelected = ref.read(selectedModelProvider); final isManualSelection = ref.read(isManualModelSelectionProvider); @@ -1037,11 +1044,17 @@ Future defaultModel(Ref ref) async { } return defaultModel; } + DebugLogger.warning('no-demo-models', scope: 'models/default'); return null; } final api = ref.watch(apiServiceProvider); - if (api == null) return null; + if (api == null) { + DebugLogger.warning('no-api', scope: 'models/default'); + return null; + } + + DebugLogger.log('api-available', scope: 'models/default'); try { // Respect manual selection if present @@ -1065,7 +1078,10 @@ Future defaultModel(Ref ref) async { // Reconcile against real models in background Future.microtask(() async { try { + if (!ref.mounted) return; final models = await ref.read(modelsProvider.future); + if (!ref.mounted) return; + Model? resolved; try { resolved = models.firstWhere((m) => m.id == storedDefaultId); @@ -1076,6 +1092,8 @@ Future defaultModel(Ref ref) async { if (byName.length == 1) resolved = byName.first; } resolved ??= models.isNotEmpty ? models.first : null; + + if (!ref.mounted) return; if (resolved != null && !ref.read(isManualModelSelectionProvider)) { ref.read(selectedModelProvider.notifier).set(resolved); DebugLogger.log( @@ -1084,7 +1102,13 @@ Future defaultModel(Ref ref) async { data: {'name': resolved.name, 'source': 'stored'}, ); } - } catch (_) {} + } catch (e) { + DebugLogger.error( + 'reconcile-failed', + scope: 'models/default', + error: e, + ); + } }); return ref.read(selectedModelProvider); } @@ -1105,7 +1129,10 @@ Future defaultModel(Ref ref) async { // Reconcile against real models in background Future.microtask(() async { try { + if (!ref.mounted) return; final models = await ref.read(modelsProvider.future); + if (!ref.mounted) return; + Model? resolved; try { resolved = models.firstWhere((m) => m.id == serverDefault); @@ -1116,6 +1143,8 @@ Future defaultModel(Ref ref) async { if (byName.length == 1) resolved = byName.first; } resolved ??= models.isNotEmpty ? models.first : null; + + if (!ref.mounted) return; if (resolved != null && !ref.read(isManualModelSelectionProvider)) { ref.read(selectedModelProvider.notifier).set(resolved); DebugLogger.log( @@ -1124,14 +1153,26 @@ Future defaultModel(Ref ref) async { data: {'name': resolved.name, 'source': 'server'}, ); } - } catch (_) {} + } catch (e) { + DebugLogger.error( + 'reconcile-failed', + scope: 'models/default', + error: e, + ); + } }); return ref.read(selectedModelProvider); } } catch (_) {} // 3) Fallback: fetch models and pick first available + DebugLogger.log('fallback-path', scope: 'models/default'); final models = await ref.read(modelsProvider.future); + DebugLogger.log( + 'models-loaded', + scope: 'models/default', + data: {'count': models.length}, + ); if (models.isEmpty) { DebugLogger.warning('no-models', scope: 'models/default'); return null; @@ -1140,10 +1181,12 @@ Future defaultModel(Ref ref) async { if (!ref.read(isManualModelSelectionProvider)) { ref.read(selectedModelProvider.notifier).set(selectedModel); DebugLogger.log( - 'fallback', + 'fallback-selected', scope: 'models/default', - data: {'name': selectedModel.name}, + data: {'name': selectedModel.name, 'id': selectedModel.id}, ); + } else { + DebugLogger.log('skip-manual-override', scope: 'models/default'); } return selectedModel; } catch (e) { @@ -1158,24 +1201,46 @@ final backgroundModelLoadProvider = Provider((ref) { // Ensure API token updater is initialized ref.watch(apiTokenUpdaterProvider); - // Only run when authenticated, and defer until after first frame - final navState = ref.read(authNavigationStateProvider); + // Watch auth state to trigger model loading when authenticated + final navState = ref.watch(authNavigationStateProvider); if (navState != AuthNavigationState.authenticated) { + DebugLogger.log('skip-not-authed', scope: 'models/background'); return; } + // Use a flag to prevent multiple concurrent loads + var isLoading = false; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (isLoading) return; + isLoading = true; + // Schedule background loading without blocking startup frame Future.microtask(() async { - // Small delay to allow initial build/layout to settle - await Future.delayed(const Duration(milliseconds: 250)); + // Reduced delay for faster startup model selection + await Future.delayed(const Duration(milliseconds: 100)); + + if (!ref.mounted) { + DebugLogger.log('cancelled-unmounted', scope: 'models/background'); + return; + } DebugLogger.log('bg-start', scope: 'models/background'); try { - await ref.read(defaultModelProvider.future); - DebugLogger.log('bg-complete', scope: 'models/background'); + final model = await ref.read(defaultModelProvider.future); + if (!ref.mounted) { + DebugLogger.log('complete-unmounted', scope: 'models/background'); + return; + } + DebugLogger.log( + 'bg-complete', + scope: 'models/background', + data: {'model': model?.name ?? 'null'}, + ); } catch (e) { DebugLogger.error('bg-failed', scope: 'models/background', error: e); + } finally { + isLoading = false; } }); }); @@ -1600,10 +1665,7 @@ Future> knowledgeBases(Ref ref) async { } @riverpod -Future> knowledgeBaseItems( - Ref ref, - String kbId, -) async { +Future> knowledgeBaseItems(Ref ref, String kbId) async { // Protected: require authentication if (!ref.read(isAuthenticatedProvider2)) { DebugLogger.log('skip-unauthed', scope: 'knowledge/items'); @@ -1616,11 +1678,7 @@ Future> knowledgeBaseItems( final itemsData = await api.getKnowledgeBaseItems(kbId); return itemsData.map((data) => KnowledgeBaseItem.fromJson(data)).toList(); } catch (e) { - DebugLogger.error( - 'knowledge-items-failed', - scope: 'knowledge', - error: e, - ); + DebugLogger.error('knowledge-items-failed', scope: 'knowledge', error: e); return []; } } diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 61dae92..e3a7ec8 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -1170,11 +1170,43 @@ class _ChatPageState extends ConsumerState { ), ) : GestureDetector( - onTap: () { + onTap: () async { final modelsAsync = ref.read(modelsProvider); - modelsAsync.whenData( - (models) => _showModelDropdown(context, ref, models), - ); + + // Handle all async states properly + if (modelsAsync.isLoading) { + // If still loading, wait for it to complete + try { + final models = await ref.read(modelsProvider.future); + if (mounted) { + _showModelDropdown(context, ref, models); + } + } catch (e) { + DebugLogger.error( + 'model-load-failed', + scope: 'chat/model-selector', + error: e, + ); + } + } else if (modelsAsync.hasValue) { + // If we have data, show immediately + _showModelDropdown(context, ref, modelsAsync.value!); + } else if (modelsAsync.hasError) { + // If there's an error, try to refresh and load + try { + ref.invalidate(modelsProvider); + final models = await ref.read(modelsProvider.future); + if (mounted) { + _showModelDropdown(context, ref, models); + } + } catch (e) { + DebugLogger.error( + 'model-refresh-failed', + scope: 'chat/model-selector', + error: e, + ); + } + } }, onLongPress: () { final conversation = ref.read(activeConversationProvider);