2025-09-28 23:18:24 +05:30
import ' dart:async ' ;
2025-10-13 15:25:20 +05:30
import ' package:flutter/foundation.dart ' ;
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 ' ;
2025-09-28 23:18:24 +05:30
import ' package:riverpod_annotation/riverpod_annotation.dart ' ;
2025-10-01 16:55:44 +05:30
import ' ../persistence/persistence_providers.dart ' ;
2025-08-10 01:20:45 +05:30
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 ' ;
2025-10-13 15:25:20 +05:30
import ' ../models/tool.dart ' ;
2025-08-10 01:20:45 +05:30
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-09-29 00:22:12 +05:30
import ' ../models/socket_event.dart ' ;
2025-10-18 13:58:15 +05:30
import ' ../../shared/theme/tweakcn_themes.dart ' ;
2025-10-02 01:58:12 +05:30
import ' ../../shared/theme/app_theme.dart ' ;
2025-10-11 13:27:39 +05:30
import ' ../../features/tools/providers/tools_providers.dart ' ;
2025-08-10 01:20:45 +05:30
2025-09-28 23:18:24 +05:30
part ' app_providers.g.dart ' ;
2025-08-10 01:20:45 +05:30
// Storage providers
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
} ) ;
// Optimized storage service provider
final optimizedStorageServiceProvider = Provider < OptimizedStorageService > ( (
ref ,
) {
return OptimizedStorageService (
secureStorage: ref . watch ( secureStorageProvider ) ,
2025-10-01 16:55:44 +05:30
boxes: ref . watch ( hiveBoxesProvider ) ,
2025-08-10 01:20:45 +05:30
) ;
} ) ;
// Theme provider
2025-10-01 18:32:16 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 14:54:24 +05:30
class AppThemeMode extends _ $AppThemeMode {
2025-09-21 22:31:44 +05:30
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-10-02 01:58:12 +05:30
@ Riverpod ( keepAlive: true )
class AppThemePalette extends _ $AppThemePalette {
late final OptimizedStorageService _storage ;
@ override
2025-10-18 13:58:15 +05:30
TweakcnThemeDefinition build ( ) {
2025-10-02 01:58:12 +05:30
_storage = ref . watch ( optimizedStorageServiceProvider ) ;
final storedId = _storage . getThemePaletteId ( ) ;
2025-10-18 13:58:15 +05:30
return TweakcnThemes . byId ( storedId ) ;
2025-10-02 01:58:12 +05:30
}
Future < void > setPalette ( String paletteId ) async {
2025-10-18 13:58:15 +05:30
final palette = TweakcnThemes . byId ( paletteId ) ;
2025-10-02 01:58:12 +05:30
state = palette ;
await _storage . setThemePaletteId ( palette . id ) ;
}
}
@ Riverpod ( keepAlive: true )
class AppLightTheme extends _ $AppLightTheme {
@ override
ThemeData build ( ) {
final palette = ref . watch ( appThemePaletteProvider ) ;
return AppTheme . light ( palette ) ;
}
}
@ Riverpod ( keepAlive: true )
class AppDarkTheme extends _ $AppDarkTheme {
@ override
ThemeData build ( ) {
final palette = ref . watch ( appThemePaletteProvider ) ;
return AppTheme . dark ( palette ) ;
}
}
2025-08-23 20:09:43 +05:30
// Locale provider
2025-10-01 18:32:16 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 14:54:24 +05:30
class AppLocale extends _ $AppLocale {
2025-09-21 22:31:44 +05:30
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
2025-09-30 23:18:06 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 14:35:33 +05:30
Future < List < ServerConfig > > serverConfigs ( Ref ref ) async {
2025-08-10 01:20:45 +05:30
final storage = ref . watch ( optimizedStorageServiceProvider ) ;
return storage . getServerConfigs ( ) ;
2025-09-30 14:35:05 +05:30
}
2025-08-10 01:20:45 +05:30
2025-09-30 23:18:06 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 14:35:33 +05:30
Future < ServerConfig ? > activeServer ( Ref ref ) async {
2025-08-10 01:20:45 +05:30
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-09-30 14:35:05 +05:30
}
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
2025-09-28 23:18:24 +05:30
@ Riverpod ( keepAlive: true )
class SocketServiceManager extends _ $SocketServiceManager {
SocketService ? _service ;
ProviderSubscription < String ? > ? _tokenSubscription ;
2025-08-31 14:02:44 +05:30
2025-09-28 23:18:24 +05:30
@ override
FutureOr < SocketService ? > build ( ) async {
final reviewerMode = ref . watch ( reviewerModeProvider ) ;
if ( reviewerMode ) {
_disposeService ( ) ;
return null ;
}
2025-08-31 14:02:44 +05:30
2025-09-28 23:18:24 +05:30
final server = await ref . watch ( activeServerProvider . future ) ;
if ( server = = null ) {
_disposeService ( ) ;
return null ;
}
final transportMode = ref . watch (
appSettingsProvider . select ( ( settings ) = > settings . socketTransportMode ) ,
) ;
final websocketOnly = transportMode = = ' ws ' ;
2025-10-09 20:51:29 +05:30
// Don't watch authTokenProvider3 here to avoid rebuilding on token changes
// Token updates are handled via the subscription below
final token = ref . read ( authTokenProvider3 ) ;
2025-09-28 23:18:24 +05:30
final requiresNewService =
_service = = null | |
_service ! . serverConfig . id ! = server . id | |
_service ! . websocketOnly ! = websocketOnly ;
if ( requiresNewService ) {
_disposeService ( ) ;
_service = SocketService (
2025-09-07 11:13:05 +05:30
serverConfig: server ,
authToken: token ,
2025-09-28 23:18:24 +05:30
websocketOnly: websocketOnly ,
2025-09-07 11:13:05 +05:30
) ;
2025-09-28 23:18:24 +05:30
_scheduleConnect ( _service ! ) ;
} else {
_service ! . updateAuthToken ( token ) ;
}
_tokenSubscription ? ? = ref . listen < String ? > ( authTokenProvider3 , (
previous ,
next ,
) {
_service ? . updateAuthToken ( next ) ;
} ) ;
ref . onDispose ( ( ) {
_tokenSubscription ? . close ( ) ;
_tokenSubscription = null ;
_disposeService ( ) ;
} ) ;
return _service ;
}
void _scheduleConnect ( SocketService service ) {
WidgetsBinding . instance . addPostFrameCallback ( ( _ ) async {
await Future . delayed ( const Duration ( milliseconds: 150 ) ) ;
if ( ! ref . mounted ) return ;
try {
unawaited ( service . connect ( ) ) ;
} catch ( _ ) { }
} ) ;
}
void _disposeService ( ) {
if ( _service = = null ) return ;
try {
_service ! . dispose ( ) ;
} catch ( _ ) { }
_service = null ;
}
}
final socketServiceProvider = Provider < SocketService ? > ( ( ref ) {
final asyncService = ref . watch ( socketServiceManagerProvider ) ;
return asyncService . maybeWhen ( data: ( service ) = > service , orElse: ( ) = > null ) ;
2025-08-31 14:02:44 +05:30
} ) ;
2025-09-28 23:18:24 +05:30
enum SocketConnectionState { disconnected , connecting , connected }
@ Riverpod ( keepAlive: true )
class SocketConnectionStream extends _ $SocketConnectionStream {
StreamController < SocketConnectionState > ? _controller ;
ProviderSubscription < AsyncValue < SocketService ? > > ? _serviceSubscription ;
2025-09-29 00:22:12 +05:30
VoidCallback ? _cancelConnectListener ;
VoidCallback ? _cancelDisconnectListener ;
2025-10-01 18:44:54 +05:30
SocketConnectionState _latestState = SocketConnectionState . connecting ;
2025-09-28 23:18:24 +05:30
@ override
Stream < SocketConnectionState > build ( ) {
2025-09-29 00:22:12 +05:30
final controller = StreamController < SocketConnectionState > . broadcast (
sync : true ,
) ;
controller
. . onListen = _primeState
. . onCancel = _maybeNotifyDisconnected ;
2025-09-28 23:18:24 +05:30
_controller = controller ;
2025-09-29 00:22:12 +05:30
final initialService = ref
. watch ( socketServiceManagerProvider )
. maybeWhen ( data: ( service ) = > service , orElse: ( ) = > null ) ;
_handleServiceChange ( initialService ) ;
2025-09-28 23:18:24 +05:30
_serviceSubscription = ref . listen < AsyncValue < SocketService ? > > (
socketServiceManagerProvider ,
2025-09-29 00:22:12 +05:30
( _ , next ) = > _handleServiceChange (
next . maybeWhen ( data: ( service ) = > service , orElse: ( ) = > null ) ,
) ,
2025-09-28 23:18:24 +05:30
) ;
ref . onDispose ( ( ) {
_serviceSubscription ? . close ( ) ;
_serviceSubscription = null ;
_unbindSocket ( ) ;
_controller ? . close ( ) ;
_controller = null ;
} ) ;
return controller . stream ;
}
2025-09-29 00:22:12 +05:30
/// Publishes a disconnected state when the final listener cancels.
void _maybeNotifyDisconnected ( ) {
try {
_controller ? . add ( SocketConnectionState . disconnected ) ;
_latestState = SocketConnectionState . disconnected ;
} catch ( _ ) { }
}
/// Replays the cached state to new listeners.
void _primeState ( ) {
try {
_controller ? . add ( _latestState ) ;
} catch ( _ ) { }
}
void _handleServiceChange ( SocketService ? service ) {
if ( service = = null ) {
_unbindSocket ( ) ;
2025-10-01 18:44:54 +05:30
_emit ( SocketConnectionState . connecting ) ;
2025-09-29 00:22:12 +05:30
return ;
}
_emit (
service . isConnected
? SocketConnectionState . connected
: SocketConnectionState . connecting ,
) ;
_bindSocket ( service ) ;
}
2025-09-28 23:18:24 +05:30
void _bindSocket ( SocketService service ) {
_unbindSocket ( ) ;
void handleConnect ( dynamic _ ) {
2025-09-29 00:22:12 +05:30
_emit ( SocketConnectionState . connected ) ;
2025-09-28 23:18:24 +05:30
}
void handleDisconnect ( dynamic _ ) {
2025-09-29 00:22:12 +05:30
_emit ( SocketConnectionState . disconnected ) ;
2025-09-28 23:18:24 +05:30
}
service . socket ? . on ( ' connect ' , handleConnect ) ;
service . socket ? . on ( ' disconnect ' , handleDisconnect ) ;
_cancelConnectListener = ( ) {
service . socket ? . off ( ' connect ' , handleConnect ) ;
} ;
_cancelDisconnectListener = ( ) {
service . socket ? . off ( ' disconnect ' , handleDisconnect ) ;
} ;
}
2025-09-29 00:22:12 +05:30
void _emit ( SocketConnectionState next ) {
if ( _latestState = = next ) {
return ;
}
_latestState = next ;
try {
_controller ? . add ( next ) ;
} catch ( _ ) { }
}
2025-09-28 23:18:24 +05:30
void _unbindSocket ( ) {
_cancelConnectListener ? . call ( ) ;
_cancelDisconnectListener ? . call ( ) ;
_cancelConnectListener = null ;
_cancelDisconnectListener = null ;
}
}
2025-09-29 00:22:12 +05:30
@ Riverpod ( keepAlive: true )
2025-10-01 00:47:29 +05:30
class ConversationDeltaStream extends _ $ConversationDeltaStream {
StreamController < ConversationDelta > ? _controller ;
ProviderSubscription < AsyncValue < SocketService ? > > ? _serviceSubscription ;
SocketEventSubscription ? _socketSubscription ;
@ override
Stream < ConversationDelta > build ( ConversationDeltaRequest request ) {
final controller = StreamController < ConversationDelta > . broadcast (
sync : true ,
onCancel: _maybeTearDownSocket ,
) ;
_controller = controller ;
final initialService = ref
. watch ( socketServiceManagerProvider )
. maybeWhen ( data: ( service ) = > service , orElse: ( ) = > null ) ;
_bindSocket ( initialService , request ) ;
_serviceSubscription = ref . listen < AsyncValue < SocketService ? > > (
socketServiceManagerProvider ,
( _ , next ) = > _bindSocket (
next . maybeWhen ( data: ( service ) = > service , orElse: ( ) = > null ) ,
request ,
) ,
) ;
ref . onDispose ( ( ) {
_serviceSubscription ? . close ( ) ;
_serviceSubscription = null ;
_socketSubscription ? . dispose ( ) ;
_socketSubscription = null ;
_controller ? . close ( ) ;
_controller = null ;
} ) ;
2025-09-29 00:22:12 +05:30
2025-10-01 00:47:29 +05:30
return controller . stream ;
}
2025-09-29 00:22:12 +05:30
2025-10-01 00:47:29 +05:30
void _bindSocket ( SocketService ? service , ConversationDeltaRequest request ) {
_socketSubscription ? . dispose ( ) ;
_socketSubscription = null ;
2025-09-29 00:22:12 +05:30
if ( service = = null ) {
return ;
}
switch ( request . source ) {
case ConversationDeltaSource . chat:
2025-10-01 00:47:29 +05:30
_socketSubscription = service . addChatEventHandler (
2025-09-29 00:22:12 +05:30
conversationId: request . conversationId ,
sessionId: request . sessionId ,
requireFocus: request . requireFocus ,
handler: ( event , ack ) {
2025-10-01 00:47:29 +05:30
_controller ? . add (
ConversationDelta . fromSocketEvent (
ConversationDeltaSource . chat ,
event ,
ack ,
) ,
) ;
2025-09-29 00:22:12 +05:30
} ,
) ;
break ;
case ConversationDeltaSource . channel:
2025-10-01 00:47:29 +05:30
_socketSubscription = service . addChannelEventHandler (
2025-09-29 00:22:12 +05:30
conversationId: request . conversationId ,
sessionId: request . sessionId ,
requireFocus: request . requireFocus ,
handler: ( event , ack ) {
2025-10-01 00:47:29 +05:30
_controller ? . add (
ConversationDelta . fromSocketEvent (
ConversationDeltaSource . channel ,
event ,
ack ,
) ,
) ;
2025-09-29 00:22:12 +05:30
} ,
) ;
break ;
}
}
2025-10-01 00:47:29 +05:30
void _maybeTearDownSocket ( ) {
if ( _controller ? . hasListener = = true ) {
return ;
}
_socketSubscription ? . dispose ( ) ;
_socketSubscription = null ;
}
2025-10-01 00:35:56 +05:30
}
2025-09-29 00:22:12 +05:30
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 ) {
2025-10-04 13:37:47 +05:30
void syncToken ( ApiService ? api , String ? token ) {
if ( api = = null ) return ;
api . updateAuthToken ( token ! = null & & token . isNotEmpty ? token : null ) ;
final length = token ? . length ? ? 0 ;
DebugLogger . auth (
' token-updated ' ,
scope: ' auth/api ' ,
data: { ' length ' : length } ,
) ;
}
syncToken ( ref . read ( apiServiceProvider ) , ref . read ( authTokenProvider3 ) ) ;
ref . listen < ApiService ? > ( apiServiceProvider , ( previous , next ) {
syncToken ( next , ref . read ( authTokenProvider3 ) ) ;
} ) ;
2025-09-23 13:43:01 +05:30
ref . listen < String ? > ( authTokenProvider3 , ( previous , next ) {
2025-10-04 13:37:47 +05:30
syncToken ( ref . read ( apiServiceProvider ) , next ) ;
2025-08-10 01:20:45 +05:30
} ) ;
} ) ;
2025-10-01 18:32:16 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 14:35:33 +05:30
Future < User ? > currentUser ( Ref ref ) async {
2025-08-10 01:20:45 +05:30
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 ;
}
2025-09-30 14:35:05 +05:30
}
2025-08-10 01:20:45 +05:30
// 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
2025-09-30 23:18:06 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 14:35:33 +05:30
Future < List < Model > > models ( Ref ref ) async {
2025-08-10 01:20:45 +05:30
// 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-30 14:35:05 +05:30
}
2025-08-10 01:20:45 +05:30
2025-09-30 23:18:06 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 14:31:56 +05:30
class SelectedModel extends _ $SelectedModel {
2025-09-21 22:31:44 +05:30
@ override
Model ? build ( ) = > null ;
void set ( Model ? model ) = > state = model ;
void clear ( ) = > state = null ;
}
2025-09-30 14:31:56 +05:30
// Track if the current model selection is manual (user-selected) or automatic (default)
2025-09-30 23:18:06 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 14:31:56 +05:30
class IsManualModelSelection extends _ $IsManualModelSelection {
2025-09-21 22:31:44 +05:30
@ 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
2025-09-30 23:18:06 +05:30
// keepAlive to maintain listener throughout app lifecycle
2025-08-17 17:43:19 +05:30
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-10-13 15:25:20 +05:30
// Auto-apply model-specific tools when model changes or tools load
2025-10-11 13:27:39 +05:30
final modelToolsAutoSelectionProvider = Provider < void > ( ( ref ) {
2025-10-13 15:25:20 +05:30
Future < void > applyTools ( Model ? model ) async {
if ( model = = null ) {
final current = ref . read ( selectedToolIdsProvider ) ;
if ( current . isNotEmpty ) {
ref . read ( selectedToolIdsProvider . notifier ) . set ( [ ] ) ;
}
return ;
}
final modelToolIds = model . toolIds ? ? [ ] ;
if ( modelToolIds . isEmpty ) {
final current = ref . read ( selectedToolIdsProvider ) ;
if ( current . isNotEmpty ) {
ref . read ( selectedToolIdsProvider . notifier ) . set ( [ ] ) ;
}
return ;
}
void updateSelection ( List < Tool > availableTools ) {
final validToolIds = modelToolIds
. where ( ( id ) = > availableTools . any ( ( tool ) = > tool . id = = id ) )
. toList ( ) ;
final currentSelection = ref . read ( selectedToolIdsProvider ) ;
if ( validToolIds . isEmpty ) {
if ( currentSelection . isNotEmpty ) {
ref . read ( selectedToolIdsProvider . notifier ) . set ( [ ] ) ;
2025-10-11 13:27:39 +05:30
}
2025-10-13 15:25:20 +05:30
return ;
}
if ( listEquals ( currentSelection , validToolIds ) ) return ;
ref . read ( selectedToolIdsProvider . notifier ) . set ( validToolIds ) ;
DebugLogger . log (
' auto-apply-tools ' ,
scope: ' models/tools ' ,
data: { ' modelId ' : model . id , ' toolCount ' : validToolIds . length } ,
) ;
}
final toolsAsync = ref . read ( toolsListProvider ) ;
if ( toolsAsync . hasValue ) {
updateSelection ( toolsAsync . value ? ? const < Tool > [ ] ) ;
return ;
}
try {
final availableTools = await ref . read ( toolsListProvider . future ) ;
if ( ! ref . mounted ) return ;
updateSelection ( availableTools ) ;
} catch ( error , stackTrace ) {
DebugLogger . error (
' auto-apply-tools-failed ' ,
scope: ' models/tools ' ,
error: error ,
stackTrace: stackTrace ,
) ;
}
}
Future < void > scheduleApply ( Model ? model ) async {
await applyTools ( model ) ;
}
Future . microtask ( ( ) = > scheduleApply ( ref . read ( selectedModelProvider ) ) ) ;
ref . listen < Model ? > ( selectedModelProvider , ( previous , next ) {
if ( previous ? . id = = next ? . id & & previous ! = null ) {
return ;
2025-10-11 13:27:39 +05:30
}
2025-10-13 15:25:20 +05:30
Future . microtask ( ( ) = > scheduleApply ( next ) ) ;
} ) ;
ref . listen ( toolsListProvider , ( previous , next ) {
if ( ! next . hasValue ) return ;
Future . microtask ( ( ) = > scheduleApply ( ref . read ( selectedModelProvider ) ) ) ;
2025-10-11 13:27:39 +05:30
} ) ;
} ) ;
2025-08-28 19:17:05 +05:30
// Auto-apply default model from settings when it changes (and not manually overridden)
2025-09-30 23:18:06 +05:30
// keepAlive to maintain listener throughout app lifecycle
2025-08-28 19:17:05 +05:30
final defaultModelAutoSelectionProvider = Provider < void > ( ( ref ) {
2025-10-11 13:27:39 +05:30
// Initialize the model tools auto-selection
ref . watch ( modelToolsAutoSelectionProvider ) ;
2025-08-28 19:17:05 +05:30
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-30 23:18:06 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 14:48:44 +05:30
class _ConversationsCacheTimestamp extends _ $ConversationsCacheTimestamp {
2025-09-21 22:31:44 +05:30
@ override
DateTime ? build ( ) = > null ;
void set ( DateTime ? timestamp ) = > state = timestamp ;
}
2025-08-17 16:11:19 +05:30
2025-10-02 00:30:14 +05:30
/// Clears the in-memory timestamp cache and invalidates the conversations
/// provider so the next read forces a refetch. Optionally invalidates the
/// folders provider when folder metadata must stay in sync with conversations.
void refreshConversationsCache ( dynamic ref , { bool includeFolders = false } ) {
ref . read ( _conversationsCacheTimestampProvider . notifier ) . set ( null ) ;
ref . invalidate ( conversationsProvider ) ;
if ( includeFolders ) {
ref . invalidate ( foldersProvider ) ;
}
}
2025-08-17 16:11:19 +05:30
// Conversation providers - Now using correct OpenWebUI API with caching
2025-09-30 23:18:06 +05:30
// keepAlive to maintain cache during authenticated session
@ Riverpod ( keepAlive: true )
2025-09-30 15:00:55 +05:30
Future < List < Conversation > > conversations ( Ref ref ) async {
2025-09-28 21:33:49 +05:30
// Do not fetch protected data until authenticated. Use watch so we refetch
// when the auth state transitions in either direction.
final authed = ref . watch ( isAuthenticatedProvider2 ) ;
2025-09-28 20:41:35 +05:30
if ( ! authed ) {
DebugLogger . log ( ' skip-unauthed ' , scope: ' conversations ' ) ;
return [ ] ;
}
2025-08-17 16:11:19 +05:30
// Check if we have a recent cache (within 5 seconds)
2025-09-30 14:49:07 +05:30
final lastFetch = ref . read ( _conversationsCacheTimestampProvider ) ;
2025-08-17 16:11:19 +05:30
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 ( ) ;
2025-10-02 00:06:00 +05:30
final hasKnownConversations = conversationMap . values . any (
( conversation ) = > conversation . folderId = = folder . id ,
) ;
final shouldFetchFolder =
apiSvc ! = null & &
( missingIds . isNotEmpty | |
( ! hasKnownConversations & & folder . conversationIds . isEmpty ) ) ;
2025-08-17 16:17:39 +05:30
List < Conversation > folderConvs = const [ ] ;
2025-10-02 00:06:00 +05:30
if ( shouldFetchFolder ) {
try {
2025-08-17 16:17:39 +05:30
folderConvs = await apiSvc . getConversationsInFolder ( folder . id ) ;
2025-10-02 00:06:00 +05:30
DebugLogger . log (
' folder-sync ' ,
scope: ' conversations/map ' ,
data: {
' folderId ' : folder . id ,
' fetched ' : folderConvs . length ,
' missingIds ' : missingIds . length ,
} ,
) ;
} catch ( e ) {
DebugLogger . error (
' folder-fetch-failed ' ,
scope: ' conversations/map ' ,
error: e ,
data: { ' folderId ' : folder . id } ,
) ;
2025-08-17 16:11:19 +05:30
}
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-10-02 00:06:00 +05:30
}
if ( folderConvs . isNotEmpty & & folder . conversationIds . isEmpty ) {
for ( final conv in folderConvs ) {
final toAdd = conv . folderId = = null
? conv . copyWith ( folderId: folder . id )
: conv ;
conversationMap [ toAdd . id ] = toAdd ;
existingIds . add ( toAdd . id ) ;
DebugLogger . log (
' add-folder-fetch ' ,
scope: ' conversations/map ' ,
data: { ' conversationId ' : toAdd . id , ' folderId ' : folder . id } ,
) ;
}
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-30 15:20:08 +05:30
ref
. read ( _conversationsCacheTimestampProvider . 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-30 15:20:08 +05:30
ref
. read ( _conversationsCacheTimestampProvider . 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-30 15:00:55 +05:30
}
2025-08-10 01:20:45 +05:30
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
2025-09-30 14:40:55 +05:30
@ riverpod
Future < Conversation > loadConversation ( Ref ref , String conversationId ) async {
2025-08-10 01:20:45 +05:30
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-09-30 14:40:55 +05:30
}
2025-08-10 01:20:45 +05:30
2025-08-17 17:01:06 +05:30
// Provider to automatically load and set the default model from user settings or OpenWebUI
2025-09-30 15:20:08 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 15:00:19 +05:30
Future < Model ? > defaultModel ( Ref ref ) async {
2025-09-30 15:20:08 +05:30
DebugLogger . log ( ' provider-called ' , scope: ' models/default ' ) ;
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-09-30 15:20:08 +05:30
DebugLogger . log ( ' reviewer-mode ' , scope: ' models/default ' ) ;
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 ;
}
2025-09-30 15:20:08 +05:30
DebugLogger . warning ( ' no-demo-models ' , scope: ' models/default ' ) ;
2025-08-17 16:17:39 +05:30
return null ;
}
2025-09-27 17:29:15 +05:30
final api = ref . watch ( apiServiceProvider ) ;
2025-09-30 15:20:08 +05:30
if ( api = = null ) {
DebugLogger . warning ( ' no-api ' , scope: ' models/default ' ) ;
return null ;
}
DebugLogger . log ( ' api-available ' , scope: ' models/default ' ) ;
2025-08-10 01:20:45 +05:30
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-30 15:20:08 +05:30
if ( ! ref . mounted ) return ;
2025-09-16 20:10:53 +05:30
final models = await ref . read ( modelsProvider . future ) ;
2025-09-30 15:20:08 +05:30
if ( ! ref . mounted ) return ;
2025-09-16 20:10:53 +05:30
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 ;
2025-09-30 15:20:08 +05:30
if ( ! ref . mounted ) return ;
2025-09-16 20:10:53 +05:30
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-30 15:20:08 +05:30
} catch ( e ) {
DebugLogger . error (
' reconcile-failed ' ,
scope: ' models/default ' ,
error: e ,
) ;
}
2025-09-16 20:10:53 +05:30
} ) ;
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 {
2025-09-30 15:20:08 +05:30
if ( ! ref . mounted ) return ;
2025-09-16 20:10:53 +05:30
final models = await ref . read ( modelsProvider . future ) ;
2025-09-30 15:20:08 +05:30
if ( ! ref . mounted ) return ;
2025-09-16 20:10:53 +05:30
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 ;
2025-09-30 15:20:08 +05:30
if ( ! ref . mounted ) return ;
2025-09-16 20:10:53 +05:30
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
) ;
}
2025-09-30 15:20:08 +05:30
} catch ( e ) {
DebugLogger . error (
' reconcile-failed ' ,
scope: ' models/default ' ,
error: e ,
) ;
}
2025-09-16 20:10:53 +05:30
} ) ;
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
2025-09-30 15:20:08 +05:30
DebugLogger . log ( ' fallback-path ' , scope: ' models/default ' ) ;
2025-09-16 20:10:53 +05:30
final models = await ref . read ( modelsProvider . future ) ;
2025-09-30 15:20:08 +05:30
DebugLogger . log (
' models-loaded ' ,
scope: ' models/default ' ,
data: { ' count ' : models . length } ,
) ;
2025-09-16 20:10:53 +05:30
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 (
2025-09-30 15:20:08 +05:30
' fallback-selected ' ,
2025-09-25 22:36:42 +05:30
scope: ' models/default ' ,
2025-09-30 15:20:08 +05:30
data: { ' name ' : selectedModel . name , ' id ' : selectedModel . id } ,
2025-08-17 16:17:39 +05:30
) ;
2025-09-30 15:20:08 +05:30
} else {
DebugLogger . log ( ' skip-manual-override ' , scope: ' models/default ' ) ;
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 ;
}
2025-09-30 15:00:19 +05:30
}
2025-08-10 01:20:45 +05:30
// 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 ) ;
2025-09-30 15:20:08 +05:30
// Watch auth state to trigger model loading when authenticated
final navState = ref . watch ( authNavigationStateProvider ) ;
2025-09-28 20:59:19 +05:30
if ( navState ! = AuthNavigationState . authenticated ) {
2025-09-30 15:20:08 +05:30
DebugLogger . log ( ' skip-not-authed ' , scope: ' models/background ' ) ;
2025-09-28 20:59:19 +05:30
return ;
}
2025-08-10 01:20:45 +05:30
2025-09-30 15:20:08 +05:30
// Use a flag to prevent multiple concurrent loads
var isLoading = false ;
2025-09-28 20:59:19 +05:30
WidgetsBinding . instance . addPostFrameCallback ( ( _ ) {
2025-09-30 15:20:08 +05:30
if ( isLoading ) return ;
isLoading = true ;
2025-09-28 20:59:19 +05:30
// Schedule background loading without blocking startup frame
Future . microtask ( ( ) async {
2025-09-30 15:20:08 +05:30
// Reduced delay for faster startup model selection
await Future . delayed ( const Duration ( milliseconds: 100 ) ) ;
if ( ! ref . mounted ) {
DebugLogger . log ( ' cancelled-unmounted ' , scope: ' models/background ' ) ;
return ;
}
2025-08-10 01:20:45 +05:30
2025-09-28 20:59:19 +05:30
DebugLogger . log ( ' bg-start ' , scope: ' models/background ' ) ;
try {
2025-09-30 15:20:08 +05:30
final model = await ref . read ( defaultModelProvider . future ) ;
if ( ! ref . mounted ) {
DebugLogger . log ( ' complete-unmounted ' , scope: ' models/background ' ) ;
return ;
}
DebugLogger . log (
' bg-complete ' ,
scope: ' models/background ' ,
data: { ' model ' : model ? . name ? ? ' null ' } ,
) ;
2025-09-28 20:59:19 +05:30
} catch ( e ) {
DebugLogger . error ( ' bg-failed ' , scope: ' models/background ' , error: e ) ;
2025-09-30 15:20:08 +05:30
} finally {
isLoading = false ;
2025-09-28 20:59:19 +05:30
}
} ) ;
2025-08-10 01:20:45 +05:30
} ) ;
return ;
} ) ;
// Search query provider
2025-10-01 18:32:16 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 14:28:56 +05:30
class SearchQuery extends _ $SearchQuery {
2025-09-21 22:31:44 +05:30
@ override
String build ( ) = > ' ' ;
void set ( String query ) = > state = query ;
}
2025-08-10 01:20:45 +05:30
// Server-side search provider for chats
2025-09-30 14:42:27 +05:30
@ riverpod
Future < List < Conversation > > serverSearch ( Ref ref , String query ) async {
2025-08-10 01:20:45 +05:30
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 ( ) ;
}
2025-09-30 14:42:27 +05:30
}
2025-08-10 01:20:45 +05:30
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-30 23:18:06 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 14:31:56 +05:30
class ReviewerMode extends _ $ReviewerMode {
2025-09-21 22:31:44 +05:30
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
2025-10-01 18:32:16 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 14:39:22 +05:30
Future < UserSettings > userSettings ( Ref ref ) async {
2025-08-10 01:20:45 +05:30
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 ( ) ;
}
2025-09-30 14:39:22 +05:30
}
2025-08-10 01:20:45 +05:30
// Conversation Suggestions provider
2025-10-01 18:32:16 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 14:39:22 +05:30
Future < List < String > > conversationSuggestions ( Ref ref ) async {
2025-08-10 01:20:45 +05:30
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-09-30 14:39:22 +05:30
}
2025-08-10 01:20:45 +05:30
2025-08-21 14:37:49 +05:30
// Server features and permissions
2025-10-01 18:32:16 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 14:39:22 +05:30
Future < Map < String , dynamic > > userPermissions ( Ref ref ) async {
2025-08-21 14:37:49 +05:30
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 { } ;
}
2025-09-30 14:39:22 +05:30
}
2025-08-21 14:37:49 +05:30
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
2025-10-01 18:08:03 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 14:39:22 +05:30
Future < List < Folder > > folders ( Ref ref ) async {
2025-09-28 20:41:35 +05:30
// Protected: require authentication
if ( ! ref . read ( isAuthenticatedProvider2 ) ) {
DebugLogger . log ( ' skip-unauthed ' , scope: ' folders ' ) ;
return [ ] ;
}
2025-08-10 01:20:45 +05:30
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 [ ] ;
}
2025-09-30 14:39:22 +05:30
}
2025-08-10 01:20:45 +05:30
// Files provider
2025-10-01 18:32:16 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 14:39:22 +05:30
Future < List < FileInfo > > userFiles ( Ref ref ) async {
2025-09-28 20:41:35 +05:30
// Protected: require authentication
if ( ! ref . read ( isAuthenticatedProvider2 ) ) {
DebugLogger . log ( ' skip-unauthed ' , scope: ' files ' ) ;
return [ ] ;
}
2025-08-10 01:20:45 +05:30
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 [ ] ;
}
2025-09-30 14:39:22 +05:30
}
2025-08-10 01:20:45 +05:30
// File content provider
2025-09-30 14:42:27 +05:30
@ riverpod
Future < String > fileContent ( Ref ref , String fileId ) async {
2025-09-28 20:41:35 +05:30
// Protected: require authentication
if ( ! ref . read ( isAuthenticatedProvider2 ) ) {
DebugLogger . log ( ' skip-unauthed ' , scope: ' files/content ' ) ;
throw Exception ( ' Not authenticated ' ) ;
}
2025-08-10 01:20:45 +05:30
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 ' ) ;
}
2025-09-30 14:42:27 +05:30
}
2025-08-10 01:20:45 +05:30
// Knowledge Base providers
2025-10-01 18:32:16 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 14:39:22 +05:30
Future < List < KnowledgeBase > > knowledgeBases ( Ref ref ) async {
2025-09-28 20:41:35 +05:30
// Protected: require authentication
if ( ! ref . read ( isAuthenticatedProvider2 ) ) {
DebugLogger . log ( ' skip-unauthed ' , scope: ' knowledge ' ) ;
return [ ] ;
}
2025-08-10 01:20:45 +05:30
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 [ ] ;
}
2025-09-30 14:39:22 +05:30
}
2025-08-10 01:20:45 +05:30
2025-09-30 14:42:27 +05:30
@ riverpod
2025-09-30 15:20:08 +05:30
Future < List < KnowledgeBaseItem > > knowledgeBaseItems ( Ref ref , String kbId ) async {
2025-09-30 14:42:27 +05:30
// Protected: require authentication
if ( ! ref . read ( isAuthenticatedProvider2 ) ) {
DebugLogger . log ( ' skip-unauthed ' , scope: ' knowledge/items ' ) ;
return [ ] ;
}
final api = ref . watch ( apiServiceProvider ) ;
if ( api = = null ) return [ ] ;
2025-08-10 01:20:45 +05:30
2025-09-30 14:42:27 +05:30
try {
final itemsData = await api . getKnowledgeBaseItems ( kbId ) ;
return itemsData . map ( ( data ) = > KnowledgeBaseItem . fromJson ( data ) ) . toList ( ) ;
} catch ( e ) {
2025-09-30 15:20:08 +05:30
DebugLogger . error ( ' knowledge-items-failed ' , scope: ' knowledge ' , error: e ) ;
2025-09-30 14:42:27 +05:30
return [ ] ;
}
}
2025-08-10 01:20:45 +05:30
// Audio providers
2025-10-01 18:32:16 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 14:39:22 +05:30
Future < List < String > > availableVoices ( Ref ref ) async {
2025-09-28 20:41:35 +05:30
// Protected: require authentication
if ( ! ref . read ( isAuthenticatedProvider2 ) ) {
DebugLogger . log ( ' skip-unauthed ' , scope: ' voices ' ) ;
return [ ] ;
}
2025-08-10 01:20:45 +05:30
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 [ ] ;
}
2025-09-30 14:39:22 +05:30
}
2025-08-10 01:20:45 +05:30
// Image Generation providers
2025-10-01 18:32:16 +05:30
@ Riverpod ( keepAlive: true )
2025-09-30 14:39:22 +05:30
Future < List < Map < String , dynamic > > > imageModels ( Ref ref ) async {
2025-08-10 01:20:45 +05:30
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 [ ] ;
}
2025-09-30 14:39:22 +05:30
}