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.
This commit is contained in:
@@ -924,7 +924,9 @@ Future<List<Conversation>> conversations(Ref ref) async {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Update cache timestamp
|
// Update cache timestamp
|
||||||
ref.read(_conversationsCacheTimestampProvider.notifier).set(DateTime.now());
|
ref
|
||||||
|
.read(_conversationsCacheTimestampProvider.notifier)
|
||||||
|
.set(DateTime.now());
|
||||||
|
|
||||||
return sortedConversations;
|
return sortedConversations;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -942,7 +944,9 @@ Future<List<Conversation>> conversations(Ref ref) async {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Update cache timestamp
|
// 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
|
return conversations; // Return original conversations if folder fetch fails
|
||||||
}
|
}
|
||||||
@@ -1003,13 +1007,16 @@ Future<Conversation> loadConversation(Ref ref, String conversationId) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Provider to automatically load and set the default model from user settings or OpenWebUI
|
// Provider to automatically load and set the default model from user settings or OpenWebUI
|
||||||
@riverpod
|
@Riverpod(keepAlive: true)
|
||||||
Future<Model?> defaultModel(Ref ref) async {
|
Future<Model?> defaultModel(Ref ref) async {
|
||||||
|
DebugLogger.log('provider-called', scope: 'models/default');
|
||||||
|
|
||||||
// Initialize the settings watcher (side-effect only)
|
// Initialize the settings watcher (side-effect only)
|
||||||
ref.read(_settingsWatcherProvider);
|
ref.read(_settingsWatcherProvider);
|
||||||
// Read settings without subscribing to rebuilds to avoid watch/await hazards
|
// Read settings without subscribing to rebuilds to avoid watch/await hazards
|
||||||
final reviewerMode = ref.read(reviewerModeProvider);
|
final reviewerMode = ref.read(reviewerModeProvider);
|
||||||
if (reviewerMode) {
|
if (reviewerMode) {
|
||||||
|
DebugLogger.log('reviewer-mode', scope: 'models/default');
|
||||||
// Check if a model is manually selected
|
// Check if a model is manually selected
|
||||||
final currentSelected = ref.read(selectedModelProvider);
|
final currentSelected = ref.read(selectedModelProvider);
|
||||||
final isManualSelection = ref.read(isManualModelSelectionProvider);
|
final isManualSelection = ref.read(isManualModelSelectionProvider);
|
||||||
@@ -1037,11 +1044,17 @@ Future<Model?> defaultModel(Ref ref) async {
|
|||||||
}
|
}
|
||||||
return defaultModel;
|
return defaultModel;
|
||||||
}
|
}
|
||||||
|
DebugLogger.warning('no-demo-models', scope: 'models/default');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final api = ref.watch(apiServiceProvider);
|
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 {
|
try {
|
||||||
// Respect manual selection if present
|
// Respect manual selection if present
|
||||||
@@ -1065,7 +1078,10 @@ Future<Model?> defaultModel(Ref ref) async {
|
|||||||
// Reconcile against real models in background
|
// Reconcile against real models in background
|
||||||
Future.microtask(() async {
|
Future.microtask(() async {
|
||||||
try {
|
try {
|
||||||
|
if (!ref.mounted) return;
|
||||||
final models = await ref.read(modelsProvider.future);
|
final models = await ref.read(modelsProvider.future);
|
||||||
|
if (!ref.mounted) return;
|
||||||
|
|
||||||
Model? resolved;
|
Model? resolved;
|
||||||
try {
|
try {
|
||||||
resolved = models.firstWhere((m) => m.id == storedDefaultId);
|
resolved = models.firstWhere((m) => m.id == storedDefaultId);
|
||||||
@@ -1076,6 +1092,8 @@ Future<Model?> defaultModel(Ref ref) async {
|
|||||||
if (byName.length == 1) resolved = byName.first;
|
if (byName.length == 1) resolved = byName.first;
|
||||||
}
|
}
|
||||||
resolved ??= models.isNotEmpty ? models.first : null;
|
resolved ??= models.isNotEmpty ? models.first : null;
|
||||||
|
|
||||||
|
if (!ref.mounted) return;
|
||||||
if (resolved != null && !ref.read(isManualModelSelectionProvider)) {
|
if (resolved != null && !ref.read(isManualModelSelectionProvider)) {
|
||||||
ref.read(selectedModelProvider.notifier).set(resolved);
|
ref.read(selectedModelProvider.notifier).set(resolved);
|
||||||
DebugLogger.log(
|
DebugLogger.log(
|
||||||
@@ -1084,7 +1102,13 @@ Future<Model?> defaultModel(Ref ref) async {
|
|||||||
data: {'name': resolved.name, 'source': 'stored'},
|
data: {'name': resolved.name, 'source': 'stored'},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (e) {
|
||||||
|
DebugLogger.error(
|
||||||
|
'reconcile-failed',
|
||||||
|
scope: 'models/default',
|
||||||
|
error: e,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return ref.read(selectedModelProvider);
|
return ref.read(selectedModelProvider);
|
||||||
}
|
}
|
||||||
@@ -1105,7 +1129,10 @@ Future<Model?> defaultModel(Ref ref) async {
|
|||||||
// Reconcile against real models in background
|
// Reconcile against real models in background
|
||||||
Future.microtask(() async {
|
Future.microtask(() async {
|
||||||
try {
|
try {
|
||||||
|
if (!ref.mounted) return;
|
||||||
final models = await ref.read(modelsProvider.future);
|
final models = await ref.read(modelsProvider.future);
|
||||||
|
if (!ref.mounted) return;
|
||||||
|
|
||||||
Model? resolved;
|
Model? resolved;
|
||||||
try {
|
try {
|
||||||
resolved = models.firstWhere((m) => m.id == serverDefault);
|
resolved = models.firstWhere((m) => m.id == serverDefault);
|
||||||
@@ -1116,6 +1143,8 @@ Future<Model?> defaultModel(Ref ref) async {
|
|||||||
if (byName.length == 1) resolved = byName.first;
|
if (byName.length == 1) resolved = byName.first;
|
||||||
}
|
}
|
||||||
resolved ??= models.isNotEmpty ? models.first : null;
|
resolved ??= models.isNotEmpty ? models.first : null;
|
||||||
|
|
||||||
|
if (!ref.mounted) return;
|
||||||
if (resolved != null && !ref.read(isManualModelSelectionProvider)) {
|
if (resolved != null && !ref.read(isManualModelSelectionProvider)) {
|
||||||
ref.read(selectedModelProvider.notifier).set(resolved);
|
ref.read(selectedModelProvider.notifier).set(resolved);
|
||||||
DebugLogger.log(
|
DebugLogger.log(
|
||||||
@@ -1124,14 +1153,26 @@ Future<Model?> defaultModel(Ref ref) async {
|
|||||||
data: {'name': resolved.name, 'source': 'server'},
|
data: {'name': resolved.name, 'source': 'server'},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (e) {
|
||||||
|
DebugLogger.error(
|
||||||
|
'reconcile-failed',
|
||||||
|
scope: 'models/default',
|
||||||
|
error: e,
|
||||||
|
);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return ref.read(selectedModelProvider);
|
return ref.read(selectedModelProvider);
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
// 3) Fallback: fetch models and pick first available
|
// 3) Fallback: fetch models and pick first available
|
||||||
|
DebugLogger.log('fallback-path', scope: 'models/default');
|
||||||
final models = await ref.read(modelsProvider.future);
|
final models = await ref.read(modelsProvider.future);
|
||||||
|
DebugLogger.log(
|
||||||
|
'models-loaded',
|
||||||
|
scope: 'models/default',
|
||||||
|
data: {'count': models.length},
|
||||||
|
);
|
||||||
if (models.isEmpty) {
|
if (models.isEmpty) {
|
||||||
DebugLogger.warning('no-models', scope: 'models/default');
|
DebugLogger.warning('no-models', scope: 'models/default');
|
||||||
return null;
|
return null;
|
||||||
@@ -1140,10 +1181,12 @@ Future<Model?> defaultModel(Ref ref) async {
|
|||||||
if (!ref.read(isManualModelSelectionProvider)) {
|
if (!ref.read(isManualModelSelectionProvider)) {
|
||||||
ref.read(selectedModelProvider.notifier).set(selectedModel);
|
ref.read(selectedModelProvider.notifier).set(selectedModel);
|
||||||
DebugLogger.log(
|
DebugLogger.log(
|
||||||
'fallback',
|
'fallback-selected',
|
||||||
scope: 'models/default',
|
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;
|
return selectedModel;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1158,24 +1201,46 @@ final backgroundModelLoadProvider = Provider<void>((ref) {
|
|||||||
// Ensure API token updater is initialized
|
// Ensure API token updater is initialized
|
||||||
ref.watch(apiTokenUpdaterProvider);
|
ref.watch(apiTokenUpdaterProvider);
|
||||||
|
|
||||||
// Only run when authenticated, and defer until after first frame
|
// Watch auth state to trigger model loading when authenticated
|
||||||
final navState = ref.read(authNavigationStateProvider);
|
final navState = ref.watch(authNavigationStateProvider);
|
||||||
if (navState != AuthNavigationState.authenticated) {
|
if (navState != AuthNavigationState.authenticated) {
|
||||||
|
DebugLogger.log('skip-not-authed', scope: 'models/background');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use a flag to prevent multiple concurrent loads
|
||||||
|
var isLoading = false;
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (isLoading) return;
|
||||||
|
isLoading = true;
|
||||||
|
|
||||||
// Schedule background loading without blocking startup frame
|
// Schedule background loading without blocking startup frame
|
||||||
Future.microtask(() async {
|
Future.microtask(() async {
|
||||||
// Small delay to allow initial build/layout to settle
|
// Reduced delay for faster startup model selection
|
||||||
await Future.delayed(const Duration(milliseconds: 250));
|
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');
|
DebugLogger.log('bg-start', scope: 'models/background');
|
||||||
try {
|
try {
|
||||||
await ref.read(defaultModelProvider.future);
|
final model = await ref.read(defaultModelProvider.future);
|
||||||
DebugLogger.log('bg-complete', scope: 'models/background');
|
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) {
|
} catch (e) {
|
||||||
DebugLogger.error('bg-failed', scope: 'models/background', error: e);
|
DebugLogger.error('bg-failed', scope: 'models/background', error: e);
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1600,10 +1665,7 @@ Future<List<KnowledgeBase>> knowledgeBases(Ref ref) async {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<List<KnowledgeBaseItem>> knowledgeBaseItems(
|
Future<List<KnowledgeBaseItem>> knowledgeBaseItems(Ref ref, String kbId) async {
|
||||||
Ref ref,
|
|
||||||
String kbId,
|
|
||||||
) async {
|
|
||||||
// Protected: require authentication
|
// Protected: require authentication
|
||||||
if (!ref.read(isAuthenticatedProvider2)) {
|
if (!ref.read(isAuthenticatedProvider2)) {
|
||||||
DebugLogger.log('skip-unauthed', scope: 'knowledge/items');
|
DebugLogger.log('skip-unauthed', scope: 'knowledge/items');
|
||||||
@@ -1616,11 +1678,7 @@ Future<List<KnowledgeBaseItem>> knowledgeBaseItems(
|
|||||||
final itemsData = await api.getKnowledgeBaseItems(kbId);
|
final itemsData = await api.getKnowledgeBaseItems(kbId);
|
||||||
return itemsData.map((data) => KnowledgeBaseItem.fromJson(data)).toList();
|
return itemsData.map((data) => KnowledgeBaseItem.fromJson(data)).toList();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
DebugLogger.error(
|
DebugLogger.error('knowledge-items-failed', scope: 'knowledge', error: e);
|
||||||
'knowledge-items-failed',
|
|
||||||
scope: 'knowledge',
|
|
||||||
error: e,
|
|
||||||
);
|
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1170,11 +1170,43 @@ class _ChatPageState extends ConsumerState<ChatPage> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: GestureDetector(
|
: GestureDetector(
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
final modelsAsync = ref.read(modelsProvider);
|
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: () {
|
onLongPress: () {
|
||||||
final conversation = ref.read(activeConversationProvider);
|
final conversation = ref.read(activeConversationProvider);
|
||||||
|
|||||||
Reference in New Issue
Block a user