Files
iiEsaywebUIapp/lib/core/providers/app_providers.dart
2025-08-21 14:37:49 +05:30

1040 lines
34 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart' as foundation;
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../services/storage_service.dart';
// (removed duplicate) import '../services/optimized_storage_service.dart';
import '../services/api_service.dart';
import '../auth/auth_state_manager.dart';
import '../../features/auth/providers/unified_auth_providers.dart';
import '../services/attachment_upload_queue.dart';
import '../models/server_config.dart';
import '../models/user.dart';
import '../models/model.dart';
import '../models/conversation.dart';
import '../models/chat_message.dart';
import '../models/folder.dart';
import '../models/user_settings.dart';
import '../models/file_info.dart';
import '../models/knowledge_base.dart';
import '../services/settings_service.dart';
import '../services/optimized_storage_service.dart';
import '../utils/debug_logger.dart';
// Storage providers
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
throw UnimplementedError();
});
final secureStorageProvider = Provider<FlutterSecureStorage>((ref) {
return const FlutterSecureStorage();
});
final storageServiceProvider = Provider<StorageService>((ref) {
return StorageService(
secureStorage: ref.watch(secureStorageProvider),
prefs: ref.watch(sharedPreferencesProvider),
);
});
// Optimized storage service provider
final optimizedStorageServiceProvider = Provider<OptimizedStorageService>((
ref,
) {
return OptimizedStorageService(
secureStorage: ref.watch(secureStorageProvider),
prefs: ref.watch(sharedPreferencesProvider),
);
});
// Theme provider
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeMode>((
ref,
) {
final storage = ref.watch(optimizedStorageServiceProvider);
return ThemeModeNotifier(storage);
});
class ThemeModeNotifier extends StateNotifier<ThemeMode> {
final OptimizedStorageService _storage;
ThemeModeNotifier(this._storage) : super(ThemeMode.system) {
_loadTheme();
}
void _loadTheme() {
final mode = _storage.getThemeMode();
if (mode != null) {
state = ThemeMode.values.firstWhere(
(e) => e.toString() == mode,
orElse: () => ThemeMode.system,
);
}
}
void setTheme(ThemeMode mode) {
state = mode;
_storage.setThemeMode(mode.toString());
}
}
// Server connection providers - optimized with caching
final serverConfigsProvider = FutureProvider<List<ServerConfig>>((ref) async {
final storage = ref.watch(optimizedStorageServiceProvider);
return storage.getServerConfigs();
});
final activeServerProvider = FutureProvider<ServerConfig?>((ref) async {
final storage = ref.watch(optimizedStorageServiceProvider);
final configs = await ref.watch(serverConfigsProvider.future);
final activeId = await storage.getActiveServerId();
if (activeId == null || configs.isEmpty) return null;
return configs.firstWhere(
(config) => config.id == activeId,
orElse: () => configs.first,
);
});
final serverConnectionStateProvider = Provider<bool>((ref) {
final activeServer = ref.watch(activeServerProvider);
return activeServer.maybeWhen(
data: (server) => server != null,
orElse: () => false,
);
});
// API Service provider with unified auth integration
final apiServiceProvider = Provider<ApiService?>((ref) {
// If reviewer mode is enabled, skip creating ApiService
final reviewerMode = ref.watch(reviewerModeProvider);
if (reviewerMode) {
return null;
}
final activeServer = ref.watch(activeServerProvider);
return activeServer.maybeWhen(
data: (server) {
if (server == null) return null;
final apiService = ApiService(
serverConfig: server,
authToken: null, // Will be set by auth state manager
);
// Keep callbacks in sync so interceptor can notify auth manager
apiService.setAuthCallbacks(
onAuthTokenInvalid: () {},
onTokenInvalidated: () async {
final authManager = ref.read(authStateManagerProvider.notifier);
await authManager.onTokenInvalidated();
},
);
// Set up callback for unified auth state manager
// (legacy properties kept during transition)
apiService.onTokenInvalidated = () async {
final authManager = ref.read(authStateManagerProvider.notifier);
await authManager.onTokenInvalidated();
};
// Keep legacy callback for backward compatibility during transition
apiService.onAuthTokenInvalid = () {
// This will be removed once migration is complete
foundation.debugPrint(
'DEBUG: Legacy auth invalidation callback triggered',
);
};
// Initialize with any existing token immediately
final token = ref.read(authTokenProvider3);
if (token != null && token.isNotEmpty) {
apiService.updateAuthToken(token);
}
return apiService;
},
orElse: () => null,
);
});
// Attachment upload queue provider
final attachmentUploadQueueProvider = Provider<AttachmentUploadQueue?>((ref) {
final api = ref.watch(apiServiceProvider);
if (api == null) return null;
final queue = AttachmentUploadQueue();
// Initialize once; subsequent calls are no-ops due to singleton
queue.initialize(
onUpload: (filePath, fileName) => api.uploadFile(filePath, fileName),
);
return queue;
});
// Auth providers
// Auth token integration with API service - using unified auth system
final apiTokenUpdaterProvider = Provider<void>((ref) {
// Listen to unified auth token changes and update API service
ref.listen(authTokenProvider3, (previous, next) {
final api = ref.read(apiServiceProvider);
if (api != null && next != null && next.isNotEmpty) {
api.updateAuthToken(next);
foundation.debugPrint(
'DEBUG: Updated API service with unified auth token',
);
}
});
});
final currentUserProvider = FutureProvider<User?>((ref) async {
final api = ref.read(apiServiceProvider);
final isAuthenticated = ref.watch(isAuthenticatedProvider2);
if (api == null || !isAuthenticated) return null;
try {
return await api.getCurrentUser();
} catch (e) {
return null;
}
});
// Helper provider to force refresh auth state - now using unified system
final refreshAuthStateProvider = Provider<void>((ref) {
// This provider can be invalidated to force refresh the unified auth system
ref.read(refreshAuthProvider);
return;
});
// Model providers
final modelsProvider = FutureProvider<List<Model>>((ref) async {
// Reviewer mode returns mock models
final reviewerMode = ref.watch(reviewerModeProvider);
if (reviewerMode) {
return [
const Model(
id: 'demo/gemma-2-mini',
name: 'Gemma 2 Mini (Demo)',
description: 'Demo model for reviewer mode',
isMultimodal: true,
supportsStreaming: true,
supportedParameters: ['max_tokens', 'stream'],
),
const Model(
id: 'demo/llama-3-8b',
name: 'Llama 3 8B (Demo)',
description: 'Fast text model for demo',
isMultimodal: false,
supportsStreaming: true,
supportedParameters: ['max_tokens', 'stream'],
),
];
}
final api = ref.watch(apiServiceProvider);
if (api == null) return [];
try {
DebugLogger.log('Fetching models from server');
final models = await api.getModels();
DebugLogger.log('Successfully fetched ${models.length} models');
return models;
} catch (e) {
foundation.debugPrint('ERROR: Failed to fetch models: $e');
// If models endpoint returns 403, this should now clear auth token
// and redirect user to login since it's marked as a core endpoint
if (e.toString().contains('403')) {
DebugLogger.warning(
'Models endpoint returned 403 - authentication may be invalid',
);
}
return [];
}
});
final selectedModelProvider = StateProvider<Model?>((ref) => null);
// Track if the current model selection is manual (user-selected) or automatic (default)
final isManualModelSelectionProvider = StateProvider<bool>((ref) => false);
// Listen for settings changes and reset manual selection when default model changes
final _settingsWatcherProvider = Provider<void>((ref) {
ref.listen<AppSettings>(appSettingsProvider, (previous, next) {
if (previous?.defaultModel != next.defaultModel) {
// Reset manual selection when default model changes
ref.read(isManualModelSelectionProvider.notifier).state = false;
}
});
});
// Cache timestamp for conversations to prevent rapid re-fetches
final _conversationsCacheTimestamp = StateProvider<DateTime?>((ref) => null);
// Conversation providers - Now using correct OpenWebUI API with caching
final conversationsProvider = FutureProvider<List<Conversation>>((ref) async {
// Check if we have a recent cache (within 5 seconds)
final lastFetch = ref.read(_conversationsCacheTimestamp);
if (lastFetch != null && DateTime.now().difference(lastFetch).inSeconds < 5) {
DebugLogger.log(
'Using cached conversations (fetched ${DateTime.now().difference(lastFetch).inSeconds}s ago)',
);
// Note: Can't read our own provider here, would cause a cycle
// The caching is handled by Riverpod's built-in mechanism
}
final reviewerMode = ref.watch(reviewerModeProvider);
if (reviewerMode) {
// Provide a simple local demo conversation list
return [
Conversation(
id: 'demo-conv-1',
title: 'Welcome to Conduit (Demo)',
createdAt: DateTime.now().subtract(const Duration(minutes: 15)),
updatedAt: DateTime.now().subtract(const Duration(minutes: 10)),
messages: [
ChatMessage(
id: 'demo-msg-1',
role: 'assistant',
content:
'**Welcome to Conduit Demo Mode**\n\nThis is a demo for app review - responses are pre-written, not from real AI.\n\nTry these features:\n• Send messages\n• Attach images\n• Use voice input\n• Switch models (tap header)\n• Create new chats (menu)\n\nAll features work offline. No server needed.',
timestamp: DateTime.now().subtract(const Duration(minutes: 10)),
model: 'Gemma 2 Mini (Demo)',
isStreaming: false,
),
],
),
];
}
final api = ref.watch(apiServiceProvider);
if (api == null) {
DebugLogger.log('No API service available');
return [];
}
try {
DebugLogger.log('Fetching conversations from OpenWebUI API...');
final conversations = await api
.getConversations(); // Fetch all conversations
DebugLogger.log(
'Successfully fetched ${conversations.length} conversations',
);
// Also fetch folder information and update conversations with folder IDs
try {
final foldersData = await api.getFolders();
DebugLogger.log(
'Fetched ${foldersData.length} folders for conversation mapping',
);
// Parse folder data into Folder objects
final folders = foldersData
.map((folderData) => Folder.fromJson(folderData))
.toList();
// Create a map of conversation ID to folder ID
final conversationToFolder = <String, String>{};
for (final folder in folders) {
foundation.debugPrint(
'DEBUG: Folder "${folder.name}" (${folder.id}) has ${folder.conversationIds.length} conversations',
);
for (final conversationId in folder.conversationIds) {
conversationToFolder[conversationId] = folder.id;
foundation.debugPrint(
'DEBUG: Mapping conversation $conversationId to folder ${folder.id}',
);
}
}
// Update conversations with folder IDs, preferring explicit folder_id from chat if present
// Use a map to ensure uniqueness by ID throughout the merge process
final conversationMap = <String, Conversation>{};
for (final conversation in conversations) {
// Prefer server-provided folderId on the chat itself
final explicitFolderId = conversation.folderId;
final mappedFolderId = conversationToFolder[conversation.id];
final folderIdToUse = explicitFolderId ?? mappedFolderId;
if (folderIdToUse != null) {
conversationMap[conversation.id] = conversation.copyWith(
folderId: folderIdToUse,
);
foundation.debugPrint(
'DEBUG: Updated conversation ${conversation.id.substring(0, 8)} with folderId: $folderIdToUse (explicit: ${explicitFolderId != null})',
);
} else {
conversationMap[conversation.id] = conversation;
}
}
// Merge conversations that are in folders but missing from the main list
// Build a set of existing IDs from the fetched list
final existingIds = conversationMap.keys.toSet();
// Diagnostics: count how many folder-mapped IDs are missing from the main list
final missingInBase = conversationToFolder.keys
.where((id) => !existingIds.contains(id))
.toList();
if (missingInBase.isNotEmpty) {
foundation.debugPrint(
'DEBUG: ${missingInBase.length} conversations referenced by folders are missing from base list',
);
final preview = missingInBase.take(10).toList();
foundation.debugPrint(
'DEBUG: Missing IDs sample: $preview${missingInBase.length > 10 ? ' ...' : ''}',
);
} else {
foundation.debugPrint(
'DEBUG: All folder-referenced conversations are present in base list',
);
}
// Attempt to fetch missing conversations per-folder to construct accurate entries
// If per-folder fetch fails, fall back to creating minimal placeholder entries
final apiSvc = ref.read(apiServiceProvider);
for (final folder in folders) {
// Collect IDs in this folder that are missing
final missingIds = folder.conversationIds
.where((id) => !existingIds.contains(id))
.toList();
if (missingIds.isEmpty) continue;
List<Conversation> folderConvs = const [];
try {
if (apiSvc != null) {
folderConvs = await apiSvc.getConversationsInFolder(folder.id);
}
} catch (e) {
foundation.debugPrint(
'DEBUG: getConversationsInFolder failed for ${folder.id}: $e',
);
}
// Index fetched folder conversations for quick lookup
final fetchedMap = {for (final c in folderConvs) c.id: c};
for (final convId in missingIds) {
final fetched = fetchedMap[convId];
if (fetched != null) {
final toAdd = fetched.folderId == null
? fetched.copyWith(folderId: folder.id)
: fetched;
// Use map to prevent duplicates - this will overwrite if ID already exists
conversationMap[toAdd.id] = toAdd;
existingIds.add(toAdd.id);
foundation.debugPrint(
'DEBUG: Added missing conversation from folder fetch: ${toAdd.id.substring(0, 8)} -> folder ${folder.id}',
);
} else {
// Create a minimal placeholder if not returned by folder API
final placeholder = Conversation(
id: convId,
title: 'Chat',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
messages: const [],
folderId: folder.id,
);
// Use map to prevent duplicates
conversationMap[convId] = placeholder;
existingIds.add(convId);
foundation.debugPrint(
'DEBUG: Added placeholder conversation for missing ID: ${convId.substring(0, 8)} -> folder ${folder.id}',
);
}
}
}
// Convert map back to list - this ensures no duplicates by ID
final sortedConversations = conversationMap.values.toList();
// Sort conversations by updatedAt in descending order (most recent first)
sortedConversations.sort((a, b) => b.updatedAt.compareTo(a.updatedAt));
foundation.debugPrint(
'DEBUG: Sorted conversations by updatedAt (most recent first)',
);
// Update cache timestamp
ref.read(_conversationsCacheTimestamp.notifier).state = DateTime.now();
return sortedConversations;
} catch (e) {
foundation.debugPrint('DEBUG: Failed to fetch folder information: $e');
// Sort conversations even when folder fetch fails
conversations.sort((a, b) => b.updatedAt.compareTo(a.updatedAt));
foundation.debugPrint(
'DEBUG: Sorted conversations by updatedAt (fallback case)',
);
// Update cache timestamp
ref.read(_conversationsCacheTimestamp.notifier).state = DateTime.now();
return conversations; // Return original conversations if folder fetch fails
}
} catch (e, stackTrace) {
foundation.debugPrint('DEBUG: Error fetching conversations: $e');
foundation.debugPrint('DEBUG: Stack trace: $stackTrace');
// If conversations endpoint returns 403, this should now clear auth token
// and redirect user to login since it's marked as a core endpoint
if (e.toString().contains('403')) {
foundation.debugPrint(
'DEBUG: Conversations endpoint returned 403 - authentication may be invalid',
);
}
// Return empty list instead of re-throwing to allow app to continue functioning
return [];
}
});
final activeConversationProvider = StateProvider<Conversation?>((ref) => null);
// Provider to load full conversation with messages
final loadConversationProvider = FutureProvider.family<Conversation, String>((
ref,
conversationId,
) async {
final api = ref.watch(apiServiceProvider);
if (api == null) {
throw Exception('No API service available');
}
foundation.debugPrint('DEBUG: Loading full conversation: $conversationId');
final fullConversation = await api.getConversation(conversationId);
foundation.debugPrint(
'DEBUG: Loaded conversation with ${fullConversation.messages.length} messages',
);
return fullConversation;
});
// Provider to automatically load and set the default model from user settings or OpenWebUI
final defaultModelProvider = FutureProvider<Model?>((ref) async {
// Initialize the settings watcher
ref.watch(_settingsWatcherProvider);
// Watch user settings to refresh when default model changes
ref.watch(appSettingsProvider);
// Handle reviewer mode first
final reviewerMode = ref.watch(reviewerModeProvider);
if (reviewerMode) {
// Check if a model is manually selected
final currentSelected = ref.read(selectedModelProvider);
final isManualSelection = ref.read(isManualModelSelectionProvider);
if (currentSelected != null && isManualSelection) {
foundation.debugPrint(
'DEBUG: Manual model selected in reviewer mode: ${currentSelected.name}',
);
return currentSelected;
}
// Get demo models and select the first one
final models = await ref.read(modelsProvider.future);
if (models.isNotEmpty) {
final defaultModel = models.first;
Future.microtask(() {
if (!ref.read(isManualModelSelectionProvider)) {
ref.read(selectedModelProvider.notifier).state = defaultModel;
foundation.debugPrint(
'DEBUG: Auto-selected demo model: ${defaultModel.name}',
);
}
});
return defaultModel;
}
return null;
}
final api = ref.watch(apiServiceProvider);
if (api == null) return null;
try {
// Get all available models first
final models = await ref.read(modelsProvider.future);
if (models.isEmpty) {
foundation.debugPrint('DEBUG: No models available');
return null;
}
Model? selectedModel;
// First check user's preferred default model
final userSettings = ref.read(appSettingsProvider);
final userDefaultModelId = userSettings.defaultModel;
if (userDefaultModelId != null && userDefaultModelId.isNotEmpty) {
try {
selectedModel = models.firstWhere(
(model) =>
model.id == userDefaultModelId ||
model.name == userDefaultModelId ||
model.id.contains(userDefaultModelId) ||
model.name.contains(userDefaultModelId),
);
foundation.debugPrint(
'DEBUG: Found user default model: ${selectedModel.name}',
);
} catch (e) {
foundation.debugPrint(
'DEBUG: User default model "$userDefaultModelId" not found in available models',
);
selectedModel = null; // Will fall back to server default or first model
}
}
// If no user default or user default not found, try server's default model
if (selectedModel == null) {
try {
final defaultModelId = await api.getDefaultModel();
if (defaultModelId != null && defaultModelId.isNotEmpty) {
// Find the model that matches the default model ID
try {
selectedModel = models.firstWhere(
(model) =>
model.id == defaultModelId ||
model.name == defaultModelId ||
model.id.contains(defaultModelId) ||
model.name.contains(defaultModelId),
);
foundation.debugPrint(
'DEBUG: Found server default model: ${selectedModel.name}',
);
} catch (e) {
foundation.debugPrint(
'DEBUG: Server default model "$defaultModelId" not found in available models',
);
selectedModel = models.first;
}
} else {
// No server default, use first available model
selectedModel = models.first;
foundation.debugPrint(
'DEBUG: No server default model, using first available: ${selectedModel.name}',
);
}
} catch (apiError) {
foundation.debugPrint(
'DEBUG: Failed to get default model from server: $apiError',
);
// Use first available model as fallback
selectedModel = models.first;
foundation.debugPrint(
'DEBUG: Using first available model as fallback: ${selectedModel.name}',
);
}
}
// Defer the state update to avoid modifying providers during initialization
final modelToSet = selectedModel;
Future.microtask(() {
// Only update if this is not a manual selection
if (!ref.read(isManualModelSelectionProvider)) {
ref.read(selectedModelProvider.notifier).state = modelToSet;
foundation.debugPrint('DEBUG: Set default model: ${modelToSet.name}');
}
});
return selectedModel;
} catch (e) {
foundation.debugPrint('DEBUG: Error setting default model: $e');
// Final fallback: try to select any available model
try {
final models = await ref.read(modelsProvider.future);
if (models.isNotEmpty) {
final fallbackModel = models.first;
// Defer the state update
Future.microtask(() {
if (!ref.read(isManualModelSelectionProvider)) {
ref.read(selectedModelProvider.notifier).state = fallbackModel;
foundation.debugPrint(
'DEBUG: Fallback to first available model: ${fallbackModel.name}',
);
}
});
return fallbackModel;
}
} catch (fallbackError) {
foundation.debugPrint(
'DEBUG: Error in fallback model selection: $fallbackError',
);
}
return null;
}
});
// Background model loading provider that doesn't block UI
// This just schedules the loading, doesn't wait for it
final backgroundModelLoadProvider = Provider<void>((ref) {
// Ensure API token updater is initialized
ref.watch(apiTokenUpdaterProvider);
// Schedule background loading without blocking
Future.microtask(() async {
// Wait a bit to ensure auth is complete
await Future.delayed(const Duration(milliseconds: 1500));
foundation.debugPrint('DEBUG: Starting background model loading');
// Load default model in background
try {
await ref.read(defaultModelProvider.future);
foundation.debugPrint('DEBUG: Background model loading completed');
} catch (e) {
// Ignore errors in background loading
foundation.debugPrint('DEBUG: Background model loading failed: $e');
}
});
// Return immediately, don't block the UI
return;
});
// Search query provider
final searchQueryProvider = StateProvider<String>((ref) => '');
// Server-side search provider for chats
final serverSearchProvider = FutureProvider.family<List<Conversation>, String>((
ref,
query,
) async {
if (query.trim().isEmpty) {
// Return empty list for empty query instead of all conversations
return [];
}
final api = ref.watch(apiServiceProvider);
if (api == null) return [];
try {
foundation.debugPrint('DEBUG: Performing server-side search for: "$query"');
// Use the new server-side search API
final searchResult = await api.searchChats(
query: query.trim(),
archived: false, // Only search non-archived conversations
limit: 50,
sortBy: 'updated_at',
sortOrder: 'desc',
);
// Extract conversations from search result
final List<dynamic> conversationsData = searchResult['conversations'] ?? [];
// Convert to Conversation objects
final List<Conversation> conversations = conversationsData.map((data) {
return Conversation.fromJson(data as Map<String, dynamic>);
}).toList();
foundation.debugPrint(
'DEBUG: Server search returned ${conversations.length} results',
);
return conversations;
} catch (e) {
foundation.debugPrint('DEBUG: Server search failed, fallback to local: $e');
// Fallback to local search if server search fails
final allConversations = await ref.read(conversationsProvider.future);
return allConversations.where((conv) {
return !conv.archived &&
(conv.title.toLowerCase().contains(query.toLowerCase()) ||
conv.messages.any(
(msg) =>
msg.content.toLowerCase().contains(query.toLowerCase()),
));
}).toList();
}
});
final filteredConversationsProvider = Provider<List<Conversation>>((ref) {
final conversations = ref.watch(conversationsProvider);
final query = ref.watch(searchQueryProvider);
// Use server-side search when there's a query
if (query.trim().isNotEmpty) {
final searchResults = ref.watch(serverSearchProvider(query));
return searchResults.maybeWhen(
data: (results) => results,
loading: () {
// While server search is loading, show local filtered results
return conversations.maybeWhen(
data: (convs) => convs.where((conv) {
return !conv.archived &&
(conv.title.toLowerCase().contains(query.toLowerCase()) ||
conv.messages.any(
(msg) => msg.content.toLowerCase().contains(
query.toLowerCase(),
),
));
}).toList(),
orElse: () => [],
);
},
error: (_, stackTrace) {
// On error, fallback to local search
return conversations.maybeWhen(
data: (convs) => convs.where((conv) {
return !conv.archived &&
(conv.title.toLowerCase().contains(query.toLowerCase()) ||
conv.messages.any(
(msg) => msg.content.toLowerCase().contains(
query.toLowerCase(),
),
));
}).toList(),
orElse: () => [],
);
},
orElse: () => [],
);
}
// When no search query, show all non-archived conversations
return conversations.maybeWhen(
data: (convs) {
if (ref.watch(reviewerModeProvider)) {
return convs; // Already filtered above for demo
}
// Filter out archived conversations (they should be in a separate view)
final filtered = convs.where((conv) => !conv.archived).toList();
// Sort: pinned conversations first, then by updated date
filtered.sort((a, b) {
// Pinned conversations come first
if (a.pinned && !b.pinned) return -1;
if (!a.pinned && b.pinned) return 1;
// Within same pin status, sort by updated date (newest first)
return b.updatedAt.compareTo(a.updatedAt);
});
return filtered;
},
orElse: () => [],
);
});
// Provider for archived conversations
final archivedConversationsProvider = Provider<List<Conversation>>((ref) {
final conversations = ref.watch(conversationsProvider);
return conversations.maybeWhen(
data: (convs) {
if (ref.watch(reviewerModeProvider)) {
return convs.where((c) => c.archived).toList();
}
// Only show archived conversations
final archived = convs.where((conv) => conv.archived).toList();
// Sort by updated date (newest first)
archived.sort((a, b) => b.updatedAt.compareTo(a.updatedAt));
return archived;
},
orElse: () => [],
);
});
// Reviewer mode provider (persisted)
final reviewerModeProvider = StateNotifierProvider<ReviewerModeNotifier, bool>(
(ref) => ReviewerModeNotifier(ref.watch(optimizedStorageServiceProvider)),
);
class ReviewerModeNotifier extends StateNotifier<bool> {
final OptimizedStorageService _storage;
ReviewerModeNotifier(this._storage) : super(false) {
_load();
}
Future<void> _load() async {
final enabled = await _storage.getReviewerMode();
state = enabled;
}
Future<void> setEnabled(bool enabled) async {
state = enabled;
await _storage.setReviewerMode(enabled);
}
Future<void> toggle() => setEnabled(!state);
}
// User Settings providers
final userSettingsProvider = FutureProvider<UserSettings>((ref) async {
final api = ref.watch(apiServiceProvider);
if (api == null) {
// Return default settings if no API
return const UserSettings();
}
try {
final settingsData = await api.getUserSettings();
return UserSettings.fromJson(settingsData);
} catch (e) {
foundation.debugPrint('DEBUG: Error fetching user settings: $e');
// Return default settings on error
return const UserSettings();
}
});
// Conversation Suggestions provider
final conversationSuggestionsProvider = FutureProvider<List<String>>((
ref,
) async {
final api = ref.watch(apiServiceProvider);
if (api == null) return [];
try {
return await api.getSuggestions();
} catch (e) {
foundation.debugPrint('DEBUG: Error fetching suggestions: $e');
return [];
}
});
// Server features and permissions
final userPermissionsProvider = FutureProvider<Map<String, dynamic>>((
ref,
) async {
final api = ref.watch(apiServiceProvider);
if (api == null) return {};
try {
return await api.getUserPermissions();
} catch (e) {
foundation.debugPrint('DEBUG: Error fetching user permissions: $e');
return {};
}
});
final imageGenerationAvailableProvider = Provider<bool>((ref) {
final perms = ref.watch(userPermissionsProvider);
return perms.maybeWhen(
data: (data) {
final features = data['features'];
if (features is Map<String, dynamic>) {
final value = features['image_generation'];
if (value is bool) return value;
if (value is String) return value.toLowerCase() == 'true';
}
return false;
},
orElse: () => false,
);
});
// Folders provider
final foldersProvider = FutureProvider<List<Folder>>((ref) async {
final api = ref.watch(apiServiceProvider);
if (api == null) {
foundation.debugPrint('DEBUG: No API service available for folders');
return [];
}
try {
foundation.debugPrint('DEBUG: Fetching folders from API...');
final foldersData = await api.getFolders();
foundation.debugPrint('DEBUG: Raw folders data received successfully');
final folders = foldersData
.map((folderData) => Folder.fromJson(folderData))
.toList();
foundation.debugPrint('DEBUG: Parsed ${folders.length} folders');
return folders;
} catch (e) {
foundation.debugPrint('DEBUG: Error fetching folders: $e');
return [];
}
});
// Files provider
final userFilesProvider = FutureProvider<List<FileInfo>>((ref) async {
final api = ref.watch(apiServiceProvider);
if (api == null) return [];
try {
final filesData = await api.getUserFiles();
return filesData.map((fileData) => FileInfo.fromJson(fileData)).toList();
} catch (e) {
foundation.debugPrint('DEBUG: Error fetching files: $e');
return [];
}
});
// File content provider
final fileContentProvider = FutureProvider.family<String, String>((
ref,
fileId,
) async {
final api = ref.watch(apiServiceProvider);
if (api == null) throw Exception('No API service available');
try {
return await api.getFileContent(fileId);
} catch (e) {
foundation.debugPrint('DEBUG: Error fetching file content: $e');
throw Exception('Failed to load file content: $e');
}
});
// Knowledge Base providers
final knowledgeBasesProvider = FutureProvider<List<KnowledgeBase>>((ref) async {
final api = ref.watch(apiServiceProvider);
if (api == null) return [];
try {
final kbData = await api.getKnowledgeBases();
return kbData.map((data) => KnowledgeBase.fromJson(data)).toList();
} catch (e) {
foundation.debugPrint('DEBUG: Error fetching knowledge bases: $e');
return [];
}
});
final knowledgeBaseItemsProvider =
FutureProvider.family<List<KnowledgeBaseItem>, String>((ref, kbId) async {
final api = ref.watch(apiServiceProvider);
if (api == null) return [];
try {
final itemsData = await api.getKnowledgeBaseItems(kbId);
return itemsData
.map((data) => KnowledgeBaseItem.fromJson(data))
.toList();
} catch (e) {
foundation.debugPrint('DEBUG: Error fetching knowledge base items: $e');
return [];
}
});
// Audio providers
final availableVoicesProvider = FutureProvider<List<String>>((ref) async {
final api = ref.watch(apiServiceProvider);
if (api == null) return [];
try {
return await api.getAvailableVoices();
} catch (e) {
foundation.debugPrint('DEBUG: Error fetching voices: $e');
return [];
}
});
// Image Generation providers
final imageModelsProvider = FutureProvider<List<Map<String, dynamic>>>((
ref,
) async {
final api = ref.watch(apiServiceProvider);
if (api == null) return [];
try {
return await api.getImageModels();
} catch (e) {
foundation.debugPrint('DEBUG: Error fetching image models: $e');
return [];
}
});