2025-08-10 01:20:45 +05:30
import ' package:flutter/material.dart ' ;
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 ' ;
2025-08-17 16:11:19 +05:30
import ' ../models/chat_message.dart ' ;
2025-08-17 00:05:30 +05:30
import ' ../models/folder.dart ' ;
2025-08-10 01:20:45 +05:30
import ' ../models/user_settings.dart ' ;
import ' ../models/file_info.dart ' ;
import ' ../models/knowledge_base.dart ' ;
2025-08-17 17:01:06 +05:30
import ' ../services/settings_service.dart ' ;
2025-08-10 01:20:45 +05:30
import ' ../services/optimized_storage_service.dart ' ;
2025-08-31 14:02:44 +05:30
import ' ../services/socket_service.dart ' ;
2025-08-20 22:15:26 +05:30
import ' ../utils/debug_logger.dart ' ;
2025-08-10 01:20:45 +05:30
// Storage providers
final sharedPreferencesProvider = Provider < SharedPreferences > ( ( ref ) {
throw UnimplementedError ( ) ;
} ) ;
final secureStorageProvider = Provider < FlutterSecureStorage > ( ( ref ) {
2025-09-09 13:00:47 +05:30
// Single, shared instance with explicit platform options
return const FlutterSecureStorage (
aOptions: AndroidOptions (
encryptedSharedPreferences: true ,
sharedPreferencesName: ' conduit_secure_prefs ' ,
preferencesKeyPrefix: ' conduit_ ' ,
// Avoid auto-wipe on transient errors; we handle errors in code
resetOnError: false ,
) ,
iOptions: IOSOptions (
accountName: ' conduit_secure_storage ' ,
synchronizable: false ,
) ,
) ;
2025-08-10 01:20:45 +05:30
} ) ;
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
2025-09-21 22:31:44 +05:30
final themeModeProvider = NotifierProvider < ThemeModeNotifier , ThemeMode > (
ThemeModeNotifier . new ,
) ;
2025-08-10 01:20:45 +05:30
2025-09-21 22:31:44 +05:30
class ThemeModeNotifier extends Notifier < ThemeMode > {
late final OptimizedStorageService _storage ;
2025-08-10 01:20:45 +05:30
2025-09-21 22:31:44 +05:30
@ override
ThemeMode build ( ) {
_storage = ref . watch ( optimizedStorageServiceProvider ) ;
final storedMode = _storage . getThemeMode ( ) ;
if ( storedMode ! = null ) {
return ThemeMode . values . firstWhere (
( e ) = > e . toString ( ) = = storedMode ,
2025-08-10 01:20:45 +05:30
orElse: ( ) = > ThemeMode . system ,
) ;
}
2025-09-21 22:31:44 +05:30
return ThemeMode . system ;
2025-08-10 01:20:45 +05:30
}
void setTheme ( ThemeMode mode ) {
state = mode ;
_storage . setThemeMode ( mode . toString ( ) ) ;
}
}
2025-08-23 20:09:43 +05:30
// Locale provider
2025-09-21 22:31:44 +05:30
final localeProvider = NotifierProvider < LocaleNotifier , Locale ? > (
LocaleNotifier . new ,
) ;
2025-08-23 20:09:43 +05:30
2025-09-21 22:31:44 +05:30
class LocaleNotifier extends Notifier < Locale ? > {
late final OptimizedStorageService _storage ;
2025-08-23 20:09:43 +05:30
2025-09-21 22:31:44 +05:30
@ override
Locale ? build ( ) {
_storage = ref . watch ( optimizedStorageServiceProvider ) ;
2025-08-23 20:09:43 +05:30
final code = _storage . getLocaleCode ( ) ;
if ( code ! = null & & code . isNotEmpty ) {
2025-09-21 22:31:44 +05:30
return Locale ( code ) ;
2025-08-23 20:09:43 +05:30
}
2025-09-21 22:31:44 +05:30
return null ; // system default
2025-08-23 20:09:43 +05:30
}
Future < void > setLocale ( Locale ? locale ) async {
state = locale ;
await _storage . setLocaleCode ( locale ? . languageCode ) ;
}
}
2025-08-10 01:20:45 +05:30
// 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 ;
2025-09-23 00:58:58 +05:30
for ( final config in configs ) {
if ( config . id = = activeId ) {
return config ;
}
}
return null ;
2025-08-10 01:20:45 +05:30
} ) ;
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
2025-09-25 22:36:42 +05:30
DebugLogger . auth ( ' legacy-token-callback ' , scope: ' auth/api ' ) ;
2025-08-10 01:20:45 +05:30
} ;
return apiService ;
} ,
orElse: ( ) = > null ,
) ;
} ) ;
2025-08-31 14:02:44 +05:30
// Socket.IO service provider
final socketServiceProvider = Provider < SocketService ? > ( ( ref ) {
final reviewerMode = ref . watch ( reviewerModeProvider ) ;
if ( reviewerMode ) return null ;
final activeServer = ref . watch ( activeServerProvider ) ;
2025-09-23 13:43:01 +05:30
final token = ref . watch ( authTokenProvider3 . select ( ( t ) = > t ) ) ;
final transportMode = ref . watch (
appSettingsProvider . select ( ( s ) = > s . socketTransportMode ) ,
) ;
2025-08-31 14:02:44 +05:30
return activeServer . maybeWhen (
data: ( server ) {
if ( server = = null ) return null ;
2025-09-07 11:13:05 +05:30
final s = SocketService (
serverConfig: server ,
authToken: token ,
websocketOnly: transportMode = = ' ws ' ,
) ;
2025-08-31 14:02:44 +05:30
// best-effort connect; errors handled internally
// ignore unawaited_futures
s . connect ( ) ;
2025-09-23 13:43:01 +05:30
// Keep socket token up-to-date without reconstructing the service
ref . listen < String ? > ( authTokenProvider3 , ( prev , next ) {
s . updateAuthToken ( next ) ;
} ) ;
2025-09-02 21:19:07 +05:30
ref . onDispose ( ( ) {
2025-09-16 18:15:44 +05:30
try {
s . dispose ( ) ;
} catch ( _ ) { }
2025-09-02 21:19:07 +05:30
} ) ;
2025-08-31 14:02:44 +05:30
return s ;
} ,
orElse: ( ) = > null ,
) ;
} ) ;
2025-08-10 01:20:45 +05:30
// 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
2025-09-23 13:43:01 +05:30
ref . listen < String ? > ( authTokenProvider3 , ( previous , next ) {
2025-08-10 01:20:45 +05:30
final api = ref . read ( apiServiceProvider ) ;
2025-09-23 13:43:01 +05:30
if ( api ! = null ) {
2025-09-24 10:52:15 +05:30
api . updateAuthToken ( next ) ;
final length = next ? . length ? ? 0 ;
2025-09-25 22:36:42 +05:30
DebugLogger . auth (
' token-updated ' ,
scope: ' auth/api ' ,
data: { ' length ' : length } ,
) ;
2025-08-10 01:20:45 +05:30
}
} ) ;
} ) ;
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
2025-08-29 12:58:56 +05:30
Future . microtask ( ( ) = > ref . read ( authActionsProvider ) . refresh ( ) ) ;
2025-08-10 01:20:45 +05:30
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 {
2025-09-25 22:36:42 +05:30
DebugLogger . log ( ' fetch-start ' , scope: ' models ' ) ;
2025-08-10 01:20:45 +05:30
final models = await api . getModels ( ) ;
2025-09-25 22:36:42 +05:30
DebugLogger . log (
' fetch-ok ' ,
scope: ' models ' ,
data: { ' count ' : models . length } ,
) ;
2025-08-10 01:20:45 +05:30
return models ;
} catch ( e ) {
2025-09-25 22:36:42 +05:30
DebugLogger . error ( ' fetch-failed ' , scope: ' models ' , error: e ) ;
2025-08-10 01:20:45 +05:30
// 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 ' ) ) {
2025-09-25 22:36:42 +05:30
DebugLogger . warning ( ' endpoint-403 ' , scope: ' models ' ) ;
2025-08-10 01:20:45 +05:30
}
return [ ] ;
}
} ) ;
2025-09-21 22:31:44 +05:30
final selectedModelProvider = NotifierProvider < SelectedModelNotifier , Model ? > (
SelectedModelNotifier . new ,
) ;
2025-08-10 01:20:45 +05:30
2025-08-17 17:43:19 +05:30
// Track if the current model selection is manual (user-selected) or automatic (default)
2025-09-21 22:31:44 +05:30
final isManualModelSelectionProvider =
NotifierProvider < IsManualModelSelectionNotifier , bool > (
IsManualModelSelectionNotifier . new ,
) ;
class SelectedModelNotifier extends Notifier < Model ? > {
@ override
Model ? build ( ) = > null ;
void set ( Model ? model ) = > state = model ;
void clear ( ) = > state = null ;
}
class IsManualModelSelectionNotifier extends Notifier < bool > {
@ override
bool build ( ) = > false ;
void set ( bool value ) = > state = value ;
}
2025-08-17 17:43:19 +05:30
// 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
2025-09-21 22:31:44 +05:30
ref . read ( isManualModelSelectionProvider . notifier ) . set ( false ) ;
2025-08-17 17:43:19 +05:30
}
} ) ;
} ) ;
2025-08-28 19:17:05 +05:30
// Auto-apply default model from settings when it changes (and not manually overridden)
final defaultModelAutoSelectionProvider = Provider < void > ( ( ref ) {
ref . listen < AppSettings > ( appSettingsProvider , ( previous , next ) {
// Only react when default model value changes
if ( previous ? . defaultModel = = next . defaultModel ) return ;
// Do not override manual selections
if ( ref . read ( isManualModelSelectionProvider ) ) return ;
final desired = next . defaultModel ;
if ( desired = = null | | desired . isEmpty ) return ;
2025-09-01 18:49:43 +05:30
// Resolve the desired model against available models (by ID only)
2025-08-28 19:17:05 +05:30
Future ( ( ) async {
try {
// Prefer already-loaded models to avoid unnecessary fetches
List < Model > models ;
final modelsAsync = ref . read ( modelsProvider ) ;
if ( modelsAsync . hasValue ) {
models = modelsAsync . value ! ;
} else {
models = await ref . read ( modelsProvider . future ) ;
}
Model ? selected ;
try {
2025-09-01 18:49:43 +05:30
selected = models . firstWhere ( ( model ) = > model . id = = desired ) ;
} catch ( _ ) {
selected = null ;
}
2025-08-28 19:17:05 +05:30
// Fallback: keep current selection or pick first available
2025-09-16 18:15:44 +05:30
selected ? ? =
ref . read ( selectedModelProvider ) ? ?
2025-08-28 19:17:05 +05:30
( models . isNotEmpty ? models . first : null ) ;
if ( selected ! = null ) {
2025-09-21 22:31:44 +05:30
ref . read ( selectedModelProvider . notifier ) . set ( selected ) ;
2025-09-25 22:36:42 +05:30
DebugLogger . log (
' auto-apply ' ,
scope: ' models/default ' ,
data: { ' name ' : selected . name } ,
2025-08-28 19:17:05 +05:30
) ;
}
} catch ( e ) {
2025-09-25 22:36:42 +05:30
DebugLogger . error (
' auto-select-failed ' ,
scope: ' models/default ' ,
error: e ,
2025-08-28 19:17:05 +05:30
) ;
}
} ) ;
} ) ;
} ) ;
2025-08-17 16:11:19 +05:30
// Cache timestamp for conversations to prevent rapid re-fetches
2025-09-21 22:31:44 +05:30
final _conversationsCacheTimestamp =
NotifierProvider < _ConversationsCacheTimestampNotifier , DateTime ? > (
_ConversationsCacheTimestampNotifier . new ,
) ;
class _ConversationsCacheTimestampNotifier extends Notifier < DateTime ? > {
@ override
DateTime ? build ( ) = > null ;
void set ( DateTime ? timestamp ) = > state = timestamp ;
}
2025-08-17 16:11:19 +05:30
// Conversation providers - Now using correct OpenWebUI API with caching
2025-08-10 01:20:45 +05:30
final conversationsProvider = FutureProvider < List < Conversation > > ( ( ref ) async {
2025-08-17 16:11:19 +05:30
// Check if we have a recent cache (within 5 seconds)
final lastFetch = ref . read ( _conversationsCacheTimestamp ) ;
if ( lastFetch ! = null & & DateTime . now ( ) . difference ( lastFetch ) . inSeconds < 5 ) {
2025-08-20 22:15:26 +05:30
DebugLogger . log (
2025-09-25 22:36:42 +05:30
' cache-hit ' ,
scope: ' conversations ' ,
data: { ' ageSecs ' : DateTime . now ( ) . difference ( lastFetch ) . inSeconds } ,
2025-08-17 16:17:39 +05:30
) ;
2025-08-17 16:11:19 +05:30
// Note: Can't read our own provider here, would cause a cycle
// The caching is handled by Riverpod's built-in mechanism
}
2025-08-10 01:20:45 +05:30
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 ) ) ,
2025-08-17 16:11:19 +05:30
messages: [
ChatMessage (
id: ' demo-msg-1 ' ,
role: ' assistant ' ,
2025-08-17 16:17:39 +05:30
content:
' **Welcome to Conduit Demo Mode** \n \n This is a demo for app review - responses are pre-written, not from real AI. \n \n Try these features: \n • Send messages \n • Attach images \n • Use voice input \n • Switch models (tap header) \n • Create new chats (menu) \n \n All features work offline. No server needed. ' ,
2025-08-17 16:11:19 +05:30
timestamp: DateTime . now ( ) . subtract ( const Duration ( minutes: 10 ) ) ,
model: ' Gemma 2 Mini (Demo) ' ,
isStreaming: false ,
) ,
] ,
2025-08-10 01:20:45 +05:30
) ,
] ;
}
final api = ref . watch ( apiServiceProvider ) ;
if ( api = = null ) {
2025-09-25 22:36:42 +05:30
DebugLogger . warning ( ' api-missing ' , scope: ' conversations ' ) ;
2025-08-10 01:20:45 +05:30
return [ ] ;
}
try {
2025-09-25 22:36:42 +05:30
DebugLogger . log ( ' fetch-start ' , scope: ' conversations ' ) ;
2025-08-17 16:17:39 +05:30
final conversations = await api
. getConversations ( ) ; // Fetch all conversations
2025-08-20 22:15:26 +05:30
DebugLogger . log (
2025-09-25 22:36:42 +05:30
' fetch-ok ' ,
scope: ' conversations ' ,
data: { ' count ' : conversations . length } ,
2025-08-10 01:20:45 +05:30
) ;
2025-08-17 16:17:39 +05:30
// Also fetch folder information and update conversations with folder IDs
try {
final foldersData = await api . getFolders ( ) ;
2025-08-20 22:15:26 +05:30
DebugLogger . log (
2025-09-25 22:36:42 +05:30
' folders-fetched ' ,
scope: ' conversations ' ,
data: { ' count ' : foldersData . length } ,
2025-08-17 16:17:39 +05:30
) ;
// 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 ) {
2025-09-25 22:36:42 +05:30
DebugLogger . log (
' folder ' ,
scope: ' conversations/map ' ,
data: {
' id ' : folder . id ,
' name ' : folder . name ,
' count ' : folder . conversationIds . length ,
} ,
2025-08-17 16:17:39 +05:30
) ;
for ( final conversationId in folder . conversationIds ) {
conversationToFolder [ conversationId ] = folder . id ;
2025-09-25 22:36:42 +05:30
DebugLogger . log (
' map ' ,
scope: ' conversations/map ' ,
data: { ' conversationId ' : conversationId , ' folderId ' : folder . id } ,
2025-08-17 16:17:39 +05:30
) ;
2025-08-17 16:11:19 +05:30
}
2025-08-17 16:17:39 +05:30
}
// 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 ,
) ;
2025-09-25 22:36:42 +05:30
DebugLogger . log (
' update-folder ' ,
scope: ' conversations/map ' ,
data: {
' conversationId ' : conversation . id ,
' folderId ' : folderIdToUse ,
' explicit ' : explicitFolderId ! = null ,
} ,
2025-08-17 16:17:39 +05:30
) ;
2025-08-17 00:05:30 +05:30
} else {
2025-08-17 16:17:39 +05:30
conversationMap [ conversation . id ] = conversation ;
2025-08-17 00:05:30 +05:30
}
2025-08-17 16:17:39 +05:30
}
// 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 ( ) ;
2025-08-17 16:11:19 +05:30
2025-08-17 16:17:39 +05:30
// 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 ) {
2025-09-25 22:36:42 +05:30
DebugLogger . warning (
' missing-in-base ' ,
scope: ' conversations/map ' ,
data: {
' count ' : missingInBase . length ,
' preview ' : missingInBase . take ( 5 ) . toList ( ) ,
} ,
2025-08-17 16:17:39 +05:30
) ;
} else {
2025-09-25 22:36:42 +05:30
DebugLogger . log ( ' folders-synced ' , scope: ' conversations/map ' ) ;
2025-08-17 16:17:39 +05:30
}
// 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 ) ;
2025-08-17 16:11:19 +05:30
}
2025-08-17 16:17:39 +05:30
} catch ( e ) {
2025-09-25 22:36:42 +05:30
DebugLogger . error (
' folder-fetch-failed ' ,
scope: ' conversations/map ' ,
error: e ,
data: { ' folderId ' : folder . id } ,
2025-08-17 16:17:39 +05:30
) ;
}
2025-08-17 16:11:19 +05:30
2025-08-17 16:17:39 +05:30
// 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 ) ;
2025-09-25 22:36:42 +05:30
DebugLogger . log (
' add-missing ' ,
scope: ' conversations/map ' ,
data: { ' conversationId ' : toAdd . id , ' folderId ' : folder . id } ,
2025-08-17 16:17:39 +05:30
) ;
} 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 ) ;
2025-09-25 22:36:42 +05:30
DebugLogger . log (
' add-placeholder ' ,
scope: ' conversations/map ' ,
data: { ' conversationId ' : convId , ' folderId ' : folder . id } ,
2025-08-17 16:17:39 +05:30
) ;
2025-08-17 00:05:30 +05:30
}
}
2025-08-17 16:17:39 +05:30
}
2025-08-17 16:11:19 +05:30
// Convert map back to list - this ensures no duplicates by ID
final sortedConversations = conversationMap . values . toList ( ) ;
2025-08-17 00:26:12 +05:30
// Sort conversations by updatedAt in descending order (most recent first)
2025-08-17 16:11:19 +05:30
sortedConversations . sort ( ( a , b ) = > b . updatedAt . compareTo ( a . updatedAt ) ) ;
2025-09-25 22:36:42 +05:30
DebugLogger . log (
' sort ' ,
scope: ' conversations ' ,
data: { ' source ' : ' folder-sync ' } ,
2025-08-17 16:17:39 +05:30
) ;
2025-08-17 16:11:19 +05:30
// Update cache timestamp
2025-09-21 22:31:44 +05:30
ref . read ( _conversationsCacheTimestamp . notifier ) . set ( DateTime . now ( ) ) ;
2025-08-17 16:17:39 +05:30
2025-08-17 16:11:19 +05:30
return sortedConversations ;
2025-08-17 00:05:30 +05:30
} catch ( e ) {
2025-09-25 22:36:42 +05:30
DebugLogger . error (
' folders-fetch-failed ' ,
scope: ' conversations ' ,
error: e ,
) ;
2025-08-17 00:26:12 +05:30
// Sort conversations even when folder fetch fails
conversations . sort ( ( a , b ) = > b . updatedAt . compareTo ( a . updatedAt ) ) ;
2025-09-25 22:36:42 +05:30
DebugLogger . log (
' sort ' ,
scope: ' conversations ' ,
data: { ' source ' : ' fallback ' } ,
2025-08-17 16:17:39 +05:30
) ;
2025-08-17 16:11:19 +05:30
// Update cache timestamp
2025-09-21 22:31:44 +05:30
ref . read ( _conversationsCacheTimestamp . notifier ) . set ( DateTime . now ( ) ) ;
2025-08-17 16:17:39 +05:30
2025-08-17 00:05:30 +05:30
return conversations ; // Return original conversations if folder fetch fails
}
2025-08-10 01:20:45 +05:30
} catch ( e , stackTrace ) {
2025-09-25 22:36:42 +05:30
DebugLogger . error (
' fetch-failed ' ,
scope: ' conversations ' ,
error: e ,
stackTrace: stackTrace ,
) ;
2025-08-10 01:20:45 +05:30
// 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 ' ) ) {
2025-09-25 22:36:42 +05:30
DebugLogger . warning ( ' endpoint-403 ' , scope: ' conversations ' ) ;
2025-08-10 01:20:45 +05:30
}
// Return empty list instead of re-throwing to allow app to continue functioning
return [ ] ;
}
} ) ;
2025-09-21 22:31:44 +05:30
final activeConversationProvider =
NotifierProvider < ActiveConversationNotifier , Conversation ? > (
ActiveConversationNotifier . new ,
) ;
class ActiveConversationNotifier extends Notifier < Conversation ? > {
@ override
Conversation ? build ( ) = > null ;
void set ( Conversation ? conversation ) = > state = conversation ;
void clear ( ) = > state = null ;
}
2025-08-10 01:20:45 +05:30
// 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 ' ) ;
}
2025-09-25 22:36:42 +05:30
DebugLogger . log (
' load-start ' ,
scope: ' conversation ' ,
data: { ' id ' : conversationId } ,
) ;
2025-08-10 01:20:45 +05:30
final fullConversation = await api . getConversation ( conversationId ) ;
2025-09-25 22:36:42 +05:30
DebugLogger . log (
' load-ok ' ,
scope: ' conversation ' ,
data: { ' messages ' : fullConversation . messages . length } ,
2025-08-10 01:20:45 +05:30
) ;
return fullConversation ;
} ) ;
2025-08-17 17:01:06 +05:30
// Provider to automatically load and set the default model from user settings or OpenWebUI
2025-08-10 01:20:45 +05:30
final defaultModelProvider = FutureProvider < Model ? > ( ( ref ) async {
2025-08-28 18:54:06 +05:30
// 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 ) ;
2025-08-17 16:17:39 +05:30
if ( reviewerMode ) {
2025-08-17 17:43:19 +05:30
// Check if a model is manually selected
2025-08-17 16:17:39 +05:30
final currentSelected = ref . read ( selectedModelProvider ) ;
2025-08-17 17:43:19 +05:30
final isManualSelection = ref . read ( isManualModelSelectionProvider ) ;
2025-08-20 22:15:26 +05:30
2025-08-17 17:43:19 +05:30
if ( currentSelected ! = null & & isManualSelection ) {
2025-09-25 22:36:42 +05:30
DebugLogger . log (
' manual ' ,
scope: ' models/default ' ,
data: { ' name ' : currentSelected . name } ,
2025-08-17 16:17:39 +05:30
) ;
return currentSelected ;
}
// Get demo models and select the first one
final models = await ref . read ( modelsProvider . future ) ;
if ( models . isNotEmpty ) {
final defaultModel = models . first ;
2025-08-28 18:54:06 +05:30
if ( ! ref . read ( isManualModelSelectionProvider ) ) {
2025-09-21 22:31:44 +05:30
ref . read ( selectedModelProvider . notifier ) . set ( defaultModel ) ;
2025-09-25 22:36:42 +05:30
DebugLogger . log (
' auto-select ' ,
scope: ' models/default ' ,
data: { ' name ' : defaultModel . name } ,
2025-08-28 18:54:06 +05:30
) ;
}
2025-08-17 16:17:39 +05:30
return defaultModel ;
}
return null ;
}
2025-08-28 18:54:06 +05:30
final api = ref . read ( apiServiceProvider ) ;
2025-08-10 01:20:45 +05:30
if ( api = = null ) return null ;
try {
2025-09-16 20:10:53 +05:30
// Respect manual selection if present
if ( ref . read ( isManualModelSelectionProvider ) ) {
final current = ref . read ( selectedModelProvider ) ;
if ( current ! = null ) return current ;
2025-08-10 01:20:45 +05:30
}
2025-09-16 20:10:53 +05:30
// 1) Fast path: read stored default model ID directly and select optimistically
try {
final storedDefaultId = await SettingsService . getDefaultModel ( ) ;
if ( storedDefaultId ! = null & & storedDefaultId . isNotEmpty ) {
if ( ! ref . read ( isManualModelSelectionProvider ) ) {
final placeholder = Model (
id: storedDefaultId ,
name: storedDefaultId ,
supportsStreaming: true ,
2025-09-01 18:49:43 +05:30
) ;
2025-09-21 22:31:44 +05:30
ref . read ( selectedModelProvider . notifier ) . set ( placeholder ) ;
2025-09-01 18:49:43 +05:30
}
2025-09-16 20:10:53 +05:30
// Reconcile against real models in background
Future . microtask ( ( ) async {
2025-08-17 17:01:06 +05:30
try {
2025-09-16 20:10:53 +05:30
final models = await ref . read ( modelsProvider . future ) ;
Model ? resolved ;
try {
resolved = models . firstWhere ( ( m ) = > m . id = = storedDefaultId ) ;
} catch ( _ ) {
final byName = models
. where ( ( m ) = > m . name = = storedDefaultId )
. toList ( ) ;
if ( byName . length = = 1 ) resolved = byName . first ;
}
resolved ? ? = models . isNotEmpty ? models . first : null ;
if ( resolved ! = null & & ! ref . read ( isManualModelSelectionProvider ) ) {
2025-09-21 22:31:44 +05:30
ref . read ( selectedModelProvider . notifier ) . set ( resolved ) ;
2025-09-25 22:36:42 +05:30
DebugLogger . log (
' reconcile ' ,
scope: ' models/default ' ,
data: { ' name ' : resolved . name , ' source ' : ' stored ' } ,
2025-09-01 18:49:43 +05:30
) ;
}
2025-09-16 20:10:53 +05:30
} catch ( _ ) { }
} ) ;
return ref . read ( selectedModelProvider ) ;
2025-08-10 01:20:45 +05:30
}
2025-09-16 20:10:53 +05:30
} catch ( _ ) { }
2025-08-10 01:20:45 +05:30
2025-09-16 20:10:53 +05:30
// 2) Fast server path: query server default ID without listing all models
2025-08-10 01:20:45 +05:30
try {
2025-09-16 20:10:53 +05:30
final serverDefault = await api . getDefaultModel ( ) ;
if ( serverDefault ! = null & & serverDefault . isNotEmpty ) {
2025-08-28 18:54:06 +05:30
if ( ! ref . read ( isManualModelSelectionProvider ) ) {
2025-09-16 20:10:53 +05:30
final placeholder = Model (
id: serverDefault ,
name: serverDefault ,
supportsStreaming: true ,
2025-08-28 18:54:06 +05:30
) ;
2025-09-21 22:31:44 +05:30
ref . read ( selectedModelProvider . notifier ) . set ( placeholder ) ;
2025-08-28 18:54:06 +05:30
}
2025-09-16 20:10:53 +05:30
// Reconcile against real models in background
Future . microtask ( ( ) async {
try {
final models = await ref . read ( modelsProvider . future ) ;
Model ? resolved ;
try {
resolved = models . firstWhere ( ( m ) = > m . id = = serverDefault ) ;
} catch ( _ ) {
final byName = models
. where ( ( m ) = > m . name = = serverDefault )
. toList ( ) ;
if ( byName . length = = 1 ) resolved = byName . first ;
}
resolved ? ? = models . isNotEmpty ? models . first : null ;
if ( resolved ! = null & & ! ref . read ( isManualModelSelectionProvider ) ) {
2025-09-21 22:31:44 +05:30
ref . read ( selectedModelProvider . notifier ) . set ( resolved ) ;
2025-09-25 22:36:42 +05:30
DebugLogger . log (
' reconcile ' ,
scope: ' models/default ' ,
data: { ' name ' : resolved . name , ' source ' : ' server ' } ,
2025-09-16 20:10:53 +05:30
) ;
}
} catch ( _ ) { }
} ) ;
return ref . read ( selectedModelProvider ) ;
2025-08-10 01:20:45 +05:30
}
2025-09-16 20:10:53 +05:30
} catch ( _ ) { }
// 3) Fallback: fetch models and pick first available
final models = await ref . read ( modelsProvider . future ) ;
if ( models . isEmpty ) {
2025-09-25 22:36:42 +05:30
DebugLogger . warning ( ' no-models ' , scope: ' models/default ' ) ;
2025-09-16 20:10:53 +05:30
return null ;
}
final selectedModel = models . first ;
if ( ! ref . read ( isManualModelSelectionProvider ) ) {
2025-09-21 22:31:44 +05:30
ref . read ( selectedModelProvider . notifier ) . set ( selectedModel ) ;
2025-09-25 22:36:42 +05:30
DebugLogger . log (
' fallback ' ,
scope: ' models/default ' ,
data: { ' name ' : selectedModel . name } ,
2025-08-17 16:17:39 +05:30
) ;
2025-08-10 01:20:45 +05:30
}
2025-09-16 20:10:53 +05:30
return selectedModel ;
} catch ( e ) {
2025-09-25 22:36:42 +05:30
DebugLogger . error ( ' set-default-failed ' , scope: ' models/default ' , error: e ) ;
2025-08-10 01:20:45 +05:30
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
2025-09-16 20:10:53 +05:30
await Future . delayed ( const Duration ( milliseconds: 200 ) ) ;
2025-08-10 01:20:45 +05:30
2025-09-25 22:36:42 +05:30
DebugLogger . log ( ' bg-start ' , scope: ' models/background ' ) ;
2025-08-10 01:20:45 +05:30
// Load default model in background
try {
await ref . read ( defaultModelProvider . future ) ;
2025-09-25 22:36:42 +05:30
DebugLogger . log ( ' bg-complete ' , scope: ' models/background ' ) ;
2025-08-10 01:20:45 +05:30
} catch ( e ) {
// Ignore errors in background loading
2025-09-25 22:36:42 +05:30
DebugLogger . error ( ' bg-failed ' , scope: ' models/background ' , error: e ) ;
2025-08-10 01:20:45 +05:30
}
} ) ;
// Return immediately, don't block the UI
return ;
} ) ;
// Search query provider
2025-09-21 22:31:44 +05:30
final searchQueryProvider = NotifierProvider < SearchQueryNotifier , String > (
SearchQueryNotifier . new ,
) ;
class SearchQueryNotifier extends Notifier < String > {
@ override
String build ( ) = > ' ' ;
void set ( String query ) = > state = query ;
}
2025-08-10 01:20:45 +05:30
// 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 {
2025-09-25 22:36:42 +05:30
final trimmedQuery = query . trim ( ) ;
DebugLogger . log (
' server-search ' ,
scope: ' search ' ,
data: { ' length ' : trimmedQuery . length } ,
) ;
2025-08-10 01:20:45 +05:30
// Use the new server-side search API
2025-08-26 21:19:06 +05:30
final chatHits = await api . searchChats (
2025-09-25 22:36:42 +05:30
query: trimmedQuery ,
2025-08-10 01:20:45 +05:30
archived: false , // Only search non-archived conversations
limit: 50 ,
sortBy: ' updated_at ' ,
sortOrder: ' desc ' ,
) ;
2025-08-26 21:19:06 +05:30
// chatHits is already List<Conversation>
final List < Conversation > conversations = List . of ( chatHits ) ;
2025-08-10 01:20:45 +05:30
2025-08-26 21:19:06 +05:30
// Perform message-level search and merge chat hits
try {
final messageHits = await api . searchMessages (
2025-09-25 22:36:42 +05:30
query: trimmedQuery ,
2025-08-26 21:19:06 +05:30
limit: 100 ,
) ;
// Build a set of conversation IDs already present from chat search
final existingIds = conversations . map ( ( c ) = > c . id ) . toSet ( ) ;
// Extract chat ids from message hits (supporting multiple key casings)
final messageChatIds = < String > { } ;
for ( final hit in messageHits ) {
final chatId =
( hit [ ' chat_id ' ] ? ? hit [ ' chatId ' ] ? ? hit [ ' chatID ' ] ) as String ? ;
if ( chatId ! = null & & chatId . isNotEmpty ) {
messageChatIds . add ( chatId ) ;
}
}
2025-08-10 01:20:45 +05:30
2025-08-26 21:19:06 +05:30
// Determine which chat ids we still need to fetch
final idsToFetch = messageChatIds
. where ( ( id ) = > ! existingIds . contains ( id ) )
. toList ( ) ;
// Fetch conversations for those ids in parallel (cap to avoid overload)
const maxFetch = 50 ;
final fetchList = idsToFetch . take ( maxFetch ) . toList ( ) ;
if ( fetchList . isNotEmpty ) {
2025-09-25 22:36:42 +05:30
DebugLogger . log (
' fetch-from-messages ' ,
scope: ' search ' ,
data: { ' count ' : fetchList . length } ,
2025-08-26 21:19:06 +05:30
) ;
final fetched = await Future . wait (
fetchList . map ( ( id ) async {
try {
return await api . getConversation ( id ) ;
} catch ( _ ) {
return null ;
}
} ) ,
) ;
// Merge fetched conversations
for ( final conv in fetched ) {
if ( conv ! = null & & ! existingIds . contains ( conv . id ) ) {
conversations . add ( conv ) ;
existingIds . add ( conv . id ) ;
}
}
// Optional: sort by updated date desc to keep results consistent
conversations . sort ( ( a , b ) = > b . updatedAt . compareTo ( a . updatedAt ) ) ;
}
} catch ( e ) {
2025-09-25 22:36:42 +05:30
DebugLogger . error ( ' message-search-failed ' , scope: ' search ' , error: e ) ;
2025-08-26 21:19:06 +05:30
}
2025-08-10 01:20:45 +05:30
2025-09-25 22:36:42 +05:30
DebugLogger . log (
' server-results ' ,
scope: ' search ' ,
data: { ' count ' : conversations . length } ,
2025-08-17 16:17:39 +05:30
) ;
2025-08-10 01:20:45 +05:30
return conversations ;
} catch ( e ) {
2025-09-25 22:36:42 +05:30
DebugLogger . error ( ' server-search-failed ' , scope: ' search ' , error: e ) ;
2025-08-10 01:20:45 +05:30
// Fallback to local search if server search fails
final allConversations = await ref . read ( conversationsProvider . future ) ;
2025-09-25 22:36:42 +05:30
DebugLogger . log ( ' fallback-local ' , scope: ' search ' ) ;
2025-08-10 01:20:45 +05:30
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)
2025-09-21 22:31:44 +05:30
final reviewerModeProvider = NotifierProvider < ReviewerModeNotifier , bool > (
ReviewerModeNotifier . new ,
2025-08-10 01:20:45 +05:30
) ;
2025-09-21 22:31:44 +05:30
class ReviewerModeNotifier extends Notifier < bool > {
late final OptimizedStorageService _storage ;
bool _initialized = false ;
@ override
bool build ( ) {
_storage = ref . watch ( optimizedStorageServiceProvider ) ;
if ( ! _initialized ) {
_initialized = true ;
Future . microtask ( _load ) ;
}
return false ;
2025-08-10 01:20:45 +05:30
}
2025-09-21 22:31:44 +05:30
2025-08-10 01:20:45 +05:30
Future < void > _load ( ) async {
final enabled = await _storage . getReviewerMode ( ) ;
2025-09-21 22:31:44 +05:30
if ( ! ref . mounted ) {
return ;
}
2025-08-10 01:20:45 +05:30
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 ) {
2025-09-25 22:36:42 +05:30
DebugLogger . error ( ' user-settings-failed ' , scope: ' settings ' , error: e ) ;
2025-08-10 01:20:45 +05:30
// 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 ) {
2025-09-25 22:36:42 +05:30
DebugLogger . error ( ' suggestions-failed ' , scope: ' suggestions ' , error: e ) ;
2025-08-10 01:20:45 +05:30
return [ ] ;
}
} ) ;
2025-08-21 14:37:49 +05:30
// 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 ) {
2025-09-25 22:36:42 +05:30
DebugLogger . error ( ' permissions-failed ' , scope: ' permissions ' , error: e ) ;
2025-08-21 14:37:49 +05:30
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 ,
) ;
} ) ;
2025-08-24 20:55:51 +05:30
final webSearchAvailableProvider = 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 [ ' web_search ' ] ;
if ( value is bool ) return value ;
if ( value is String ) return value . toLowerCase ( ) = = ' true ' ;
}
return false ;
} ,
orElse: ( ) = > false ,
) ;
} ) ;
2025-08-10 01:20:45 +05:30
// Folders provider
final foldersProvider = FutureProvider < List < Folder > > ( ( ref ) async {
final api = ref . watch ( apiServiceProvider ) ;
2025-08-17 00:05:30 +05:30
if ( api = = null ) {
2025-09-25 22:36:42 +05:30
DebugLogger . warning ( ' api-missing ' , scope: ' folders ' ) ;
2025-08-17 00:05:30 +05:30
return [ ] ;
}
2025-08-10 01:20:45 +05:30
try {
final foldersData = await api . getFolders ( ) ;
2025-08-17 00:05:30 +05:30
final folders = foldersData
2025-08-10 01:20:45 +05:30
. map ( ( folderData ) = > Folder . fromJson ( folderData ) )
. toList ( ) ;
2025-09-25 22:36:42 +05:30
DebugLogger . log (
' fetch-ok ' ,
scope: ' folders ' ,
data: { ' count ' : folders . length } ,
) ;
2025-08-17 00:05:30 +05:30
return folders ;
2025-08-10 01:20:45 +05:30
} catch ( e ) {
2025-09-25 22:36:42 +05:30
DebugLogger . error ( ' fetch-failed ' , scope: ' folders ' , error: e ) ;
2025-08-10 01:20:45 +05:30
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 ) {
2025-09-25 22:36:42 +05:30
DebugLogger . error ( ' files-failed ' , scope: ' files ' , error: e ) ;
2025-08-10 01:20:45 +05:30
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 ) {
2025-09-25 22:36:42 +05:30
DebugLogger . error (
' file-content-failed ' ,
scope: ' files ' ,
error: e ,
data: { ' fileId ' : fileId } ,
) ;
2025-08-10 01:20:45 +05:30
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 ) {
2025-09-25 22:36:42 +05:30
DebugLogger . error ( ' knowledge-bases-failed ' , scope: ' knowledge ' , error: e ) ;
2025-08-10 01:20:45 +05:30
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 ) {
2025-09-25 22:36:42 +05:30
DebugLogger . error (
' knowledge-items-failed ' ,
scope: ' knowledge ' ,
error: e ,
) ;
2025-08-10 01:20:45 +05:30
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 ) {
2025-09-25 22:36:42 +05:30
DebugLogger . error ( ' voices-failed ' , scope: ' voices ' , error: e ) ;
2025-08-10 01:20:45 +05:30
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 ) {
2025-09-25 22:36:42 +05:30
DebugLogger . error ( ' image-models-failed ' , scope: ' image-models ' , error: e ) ;
2025-08-10 01:20:45 +05:30
return [ ] ;
}
} ) ;