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 ' ;
2025-09-28 23:18:24 +05:30
import ' package:riverpod_annotation/riverpod_annotation.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-10-30 22:32:59 +05:30
import ' ../models/backend_config.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-11-01 01:46:46 +05:30
import ' ../services/worker_manager.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-11-22 21:53:14 +05:30
import ' ../models/socket_transport_availability.dart ' ;
import ' storage_providers.dart ' ;
2025-08-10 01:20:45 +05:30
2025-11-22 21:53:14 +05:30
export ' storage_providers.dart ' ;
2025-09-28 23:18:24 +05:30
2025-11-22 21:53:14 +05:30
part ' app_providers.g.dart ' ;
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-11-24 16:08:55 +05:30
final parsed = _parseLocaleCode ( code ) ;
if ( parsed ! = null ) return parsed ;
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 ;
2025-11-24 16:08:55 +05:30
await _storage . setLocaleCode ( locale ? . toLanguageTag ( ) ) ;
}
Locale ? _parseLocaleCode ( String code ) {
final normalized = code . replaceAll ( ' _ ' , ' - ' ) ;
final parts = normalized . split ( ' - ' ) ;
if ( parts . isEmpty | | parts . first . isEmpty ) return null ;
final language = parts . first ;
String ? script ;
String ? country ;
for ( var i = 1 ; i < parts . length ; i + + ) {
final part = parts [ i ] ;
if ( part . length = = 4 ) {
script = ' ${ part [ 0 ] . toUpperCase ( ) } ${ part . substring ( 1 ) . toLowerCase ( ) } ' ;
} else if ( part . length = = 2 | | part . length = = 3 ) {
country = part . toUpperCase ( ) ;
}
}
return Locale . fromSubtags (
languageCode: language ,
scriptCode: script ,
countryCode: country ,
) ;
2025-08-23 20:09:43 +05:30
}
}
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 ,
) ;
} ) ;
2025-11-22 21:53:14 +05:30
@ Riverpod ( keepAlive: true )
class BackendConfigNotifier extends _ $BackendConfigNotifier {
late final OptimizedStorageService _storage ;
@ override
Future < BackendConfig ? > build ( ) async {
_storage = ref . watch ( optimizedStorageServiceProvider ) ;
final cached = await _storage . getLocalBackendConfig ( ) ;
unawaited ( _refreshBackendConfig ( ) ) ;
return cached ;
}
Future < void > refresh ( ) = > _refreshBackendConfig ( ) ;
Future < void > _refreshBackendConfig ( ) async {
final fresh = await _loadBackendConfig ( ref ) ;
if ( fresh = = null | | ! ref . mounted ) {
return ;
}
state = AsyncData ( fresh ) ;
await _storage . saveLocalBackendConfig ( fresh ) ;
// Persist resolved transport options based on backend config
if ( ! ref . mounted ) return ;
final options = _resolveTransportAvailability ( fresh ) ;
await _storage . saveLocalTransportOptions ( options ) ;
}
}
Future < BackendConfig ? > _loadBackendConfig ( Ref ref ) async {
2025-10-30 22:32:59 +05:30
final api = ref . watch ( apiServiceProvider ) ;
if ( api = = null ) {
return null ;
}
final server = await ref . watch ( activeServerProvider . future ) ;
if ( server = = null ) {
return null ;
}
try {
final config = await api . getBackendConfig ( ) ;
if ( config ! = null ) {
final forcedMode = config . enforcedTransportMode ;
if ( forcedMode ! = null ) {
final settings = ref . read ( appSettingsProvider ) ;
if ( settings . socketTransportMode ! = forcedMode ) {
Future . microtask ( ( ) {
ref
. read ( appSettingsProvider . notifier )
. setSocketTransportMode ( forcedMode ) ;
} ) ;
}
}
}
return config ;
} catch ( _ ) {
return null ;
}
}
2025-11-22 21:53:14 +05:30
/// Provides resolved socket transport options based on backend configuration.
///
/// This is a synchronous provider that:
/// - Returns cached transport options when backend config is not yet loaded
/// - Derives transport options from backend config once available
/// - Does NOT perform side effects (persistence is handled by BackendConfigNotifier)
///
/// The persistence of resolved options happens asynchronously when the
/// backend config is refreshed, ensuring the sync provider remains pure.
2025-10-30 22:32:59 +05:30
final socketTransportOptionsProvider = Provider < SocketTransportAvailability > ( (
ref ,
) {
2025-11-22 21:53:14 +05:30
final storage = ref . watch ( optimizedStorageServiceProvider ) ;
// Watch async backend config for proper invalidation
2025-10-30 22:32:59 +05:30
final backendConfigAsync = ref . watch ( backendConfigProvider ) ;
final config = backendConfigAsync . maybeWhen (
data: ( value ) = > value ,
orElse: ( ) = > null ,
) ;
if ( config = = null ) {
2025-11-22 21:53:14 +05:30
// Return cached value or defaults when config not available
return storage . getLocalTransportOptionsSync ( ) ? ?
const SocketTransportAvailability (
allowPolling: true ,
allowWebsocketOnly: true ,
) ;
2025-10-30 22:32:59 +05:30
}
2025-11-22 21:53:14 +05:30
// Determine transport availability from backend config
return _resolveTransportAvailability ( config ) ;
2025-10-30 22:32:59 +05:30
} ) ;
2025-08-10 01:20:45 +05:30
// 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 ) ;
2025-11-01 01:46:46 +05:30
final workerManager = ref . watch ( workerManagerProvider ) ;
2025-08-10 01:20:45 +05:30
return activeServer . maybeWhen (
data: ( server ) {
if ( server = = null ) return null ;
final apiService = ApiService (
serverConfig: server ,
2025-11-01 01:46:46 +05:30
workerManager: workerManager ,
2025-08-10 01:20:45 +05:30
authToken: null , // Will be set by auth state manager
) ;
// Keep callbacks in sync so interceptor can notify auth manager
apiService . setAuthCallbacks (
2025-11-12 13:23:58 +05:30
onAuthTokenInvalid: ( ) {
// Called when auth errors occur (401/403)
// Show connection issue page instead of logging out
final authManager = ref . read ( authStateManagerProvider . notifier ) ;
authManager . onAuthIssue ( ) ;
} ,
2025-08-10 01:20:45 +05:30
onTokenInvalidated: ( ) async {
2025-11-12 13:23:58 +05:30
// Called for token expiry - attempt silent re-login
2025-08-10 01:20:45 +05:30
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 = ( ) {
2025-11-12 13:23:58 +05:30
// Show connection issue page instead of logging out
final authManager = ref . read ( authStateManagerProvider . notifier ) ;
authManager . onAuthIssue ( ) ;
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-30 22:32:59 +05:30
final transportAvailability = ref . watch ( socketTransportOptionsProvider ) ;
final allowWebsocketUpgrade = transportAvailability . allowWebsocketOnly ;
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 | |
2025-10-30 22:32:59 +05:30
_service ! . websocketOnly ! = websocketOnly | |
_service ! . allowWebsocketUpgrade ! = allowWebsocketUpgrade ;
2025-09-28 23:18:24 +05:30
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-10-30 22:32:59 +05:30
allowWebsocketUpgrade: allowWebsocketUpgrade ,
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-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 ) ;
2025-11-24 17:43:05 +05:30
final authState = ref . watch ( authStateManagerProvider ) ;
final isAuthenticated = authState . maybeWhen (
data: ( state ) = > state . isAuthenticated ,
orElse: ( ) = > false ,
) ;
2025-08-10 01:20:45 +05:30
if ( api = = null | | ! isAuthenticated ) return null ;
2025-11-24 17:43:05 +05:30
// Fast path: use user already in auth state.
final authUser = authState . maybeWhen (
data: ( state ) = > state . user ,
orElse: ( ) = > null ,
) ;
if ( authUser ! = null ) return authUser ;
// Next: try cached user from storage, then refresh in the background.
final storage = ref . read ( optimizedStorageServiceProvider ) ;
final cachedUser = await _getCachedUserWithAvatar ( storage ) ;
if ( cachedUser ! = null ) {
final lastRefresh = ref . read ( _lastUserRefreshProvider ) ;
final now = DateTime . now ( ) ;
final shouldRefresh =
lastRefresh = = null | |
now . difference ( lastRefresh ) > const Duration ( minutes: 5 ) ;
if ( shouldRefresh ) {
Future . microtask ( ( ) async {
final fresh = await _refreshCurrentUser ( ref ) ;
if ( fresh ! = null & & ref . mounted ) {
ref . read ( _lastUserRefreshProvider . notifier ) . set ( now ) ;
ref . invalidate ( currentUserProvider ) ;
}
} ) ;
}
return cachedUser ;
}
// Fallback: fetch fresh.
final fresh = await _refreshCurrentUser ( ref ) ;
if ( fresh ! = null ) {
ref . read ( _lastUserRefreshProvider . notifier ) . set ( DateTime . now ( ) ) ;
}
return fresh ;
}
Future < User ? > _getCachedUserWithAvatar ( OptimizedStorageService storage ) async {
final cachedUser = await storage . getLocalUser ( ) ;
if ( cachedUser = = null ) return null ;
final cachedAvatar = await storage . getLocalUserAvatar ( ) ;
if ( cachedAvatar = = null | |
cachedAvatar . isEmpty | |
cachedUser . profileImage = = cachedAvatar ) {
return cachedUser ;
}
return cachedUser . copyWith ( profileImage: cachedAvatar ) ;
}
Future < User ? > _refreshCurrentUser ( Ref ref ) async {
final api = ref . read ( apiServiceProvider ) ;
if ( api = = null ) return null ;
2025-08-10 01:20:45 +05:30
try {
2025-11-24 17:43:05 +05:30
final user = await api . getCurrentUser ( ) ;
final storage = ref . read ( optimizedStorageServiceProvider ) ;
await storage . saveLocalUser ( user ) ;
if ( user . profileImage ! = null & & user . profileImage ! . isNotEmpty ) {
await storage . saveLocalUserAvatar ( user . profileImage ) ;
}
return user ;
} catch ( _ ) {
2025-08-10 01:20:45 +05:30
return null ;
}
2025-09-30 14:35:05 +05:30
}
2025-08-10 01:20:45 +05:30
2025-11-24 17:43:05 +05:30
@ Riverpod ( keepAlive: true )
class _LastUserRefresh extends _ $LastUserRefresh {
@ override
DateTime ? build ( ) = > null ;
void set ( DateTime ? timestamp ) = > state = timestamp ;
}
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-11-22 21:53:14 +05:30
class Models extends _ $Models {
@ override
Future < List < Model > > build ( ) async {
// Reviewer mode returns mock models
if ( ref . watch ( reviewerModeProvider ) ) {
return _demoModels ( ) ;
}
if ( ! ref . watch ( isAuthenticatedProvider2 ) ) {
DebugLogger . log ( ' skip-unauthed ' , scope: ' models ' ) ;
_persistModelsAsync ( const < Model > [ ] ) ;
return const [ ] ;
}
final storage = ref . watch ( optimizedStorageServiceProvider ) ;
try {
final cached = await storage . getLocalModels ( ) ;
if ( cached . isNotEmpty ) {
DebugLogger . log (
' cache-restored ' ,
scope: ' models/cache ' ,
data: { ' count ' : cached . length } ,
) ;
Future . microtask ( ( ) async {
try {
await refresh ( ) ;
} catch ( error , stackTrace ) {
DebugLogger . error (
' warm-refresh-failed ' ,
scope: ' models/cache ' ,
error: error ,
stackTrace: stackTrace ,
) ;
}
} ) ;
return cached ;
}
DebugLogger . log ( ' cache-empty ' , scope: ' models/cache ' ) ;
} catch ( error , stackTrace ) {
DebugLogger . error (
' cache-load-failed ' ,
scope: ' models/cache ' ,
error: error ,
stackTrace: stackTrace ,
) ;
}
final api = ref . watch ( apiServiceProvider ) ;
if ( api = = null ) {
DebugLogger . warning ( ' api-missing ' , scope: ' models ' ) ;
_persistModelsAsync ( const < Model > [ ] ) ;
return const [ ] ;
}
final fresh = await _load ( api ) ;
return fresh ;
2025-08-10 01:20:45 +05:30
}
2025-11-22 21:53:14 +05:30
Future < void > refresh ( ) async {
if ( ref . read ( reviewerModeProvider ) ) {
state = AsyncData < List < Model > > ( _demoModels ( ) ) ;
return ;
}
if ( ! ref . read ( isAuthenticatedProvider2 ) ) {
state = const AsyncData < List < Model > > ( < Model > [ ] ) ;
_persistModelsAsync ( const < Model > [ ] ) ;
return ;
}
final api = ref . read ( apiServiceProvider ) ;
if ( api = = null ) {
state = const AsyncData < List < Model > > ( < Model > [ ] ) ;
_persistModelsAsync ( const < Model > [ ] ) ;
return ;
}
final result = await AsyncValue . guard ( ( ) = > _load ( api ) ) ;
if ( ! ref . mounted ) return ;
state = result ;
}
Future < List < Model > > _load ( ApiService api ) async {
try {
DebugLogger . log ( ' fetch-start ' , scope: ' models ' ) ;
final models = await api . getModels ( ) ;
DebugLogger . log (
' fetch-ok ' ,
scope: ' models ' ,
data: { ' count ' : models . length } ,
) ;
_persistModelsAsync ( models ) ;
return models ;
} catch ( e , stackTrace ) {
DebugLogger . error (
' fetch-failed ' ,
scope: ' models ' ,
error: e ,
stackTrace: stackTrace ,
) ;
// If models endpoint returns 403, this should now clear auth token
// and redirect user to login since it's marked as a core endpoint
if ( e . toString ( ) . contains ( ' 403 ' ) ) {
DebugLogger . warning ( ' endpoint-403 ' , scope: ' models ' ) ;
}
2025-08-10 01:20:45 +05:30
2025-11-22 21:53:14 +05:30
return const [ ] ;
2025-08-10 01:20:45 +05:30
}
2025-11-22 21:53:14 +05:30
}
2025-08-10 01:20:45 +05:30
2025-11-22 21:53:14 +05:30
void _persistModelsAsync ( List < Model > models ) {
final storage = ref . read ( optimizedStorageServiceProvider ) ;
unawaited (
storage . saveLocalModels ( models ) . onError ( ( error , stack ) {
DebugLogger . error (
' Failed to persist models to cache ' ,
scope: ' models/cache ' ,
error: error ,
stackTrace: stack ,
) ;
} ) ,
) ;
2025-08-10 01:20:45 +05:30
}
2025-11-22 21:53:14 +05:30
List < Model > _demoModels ( ) = > 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 ' ] ,
) ,
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 ' ] ,
) ,
] ;
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-11-01 14:54:08 +05:30
/// Clears the in-memory timestamp cache and triggers a refresh of the
/// conversations provider. Optionally refreshes the folders provider so folder
/// metadata stays in sync.
2025-10-02 00:30:14 +05:30
void refreshConversationsCache ( dynamic ref , { bool includeFolders = false } ) {
ref . read ( _conversationsCacheTimestampProvider . notifier ) . set ( null ) ;
2025-11-01 14:54:08 +05:30
final notifier = ref . read ( conversationsProvider . notifier ) ;
unawaited ( notifier . refresh ( includeFolders: includeFolders ) ) ;
2025-11-01 15:04:50 +05:30
if ( includeFolders ) {
final foldersNotifier = ref . read ( foldersProvider . notifier ) ;
unawaited ( foldersNotifier . refresh ( ) ) ;
}
2025-10-02 00:30:14 +05:30
}
2025-11-01 14:54:08 +05:30
// Conversation providers - Now using correct OpenWebUI API with caching and
// immediate mutation helpers.
2025-09-30 23:18:06 +05:30
@ Riverpod ( keepAlive: true )
2025-11-01 14:54:08 +05:30
class Conversations extends _ $Conversations {
@ override
Future < List < Conversation > > build ( ) async {
final authed = ref . watch ( isAuthenticatedProvider2 ) ;
if ( ! authed ) {
DebugLogger . log ( ' skip-unauthed ' , scope: ' conversations ' ) ;
_updateCacheTimestamp ( null ) ;
2025-11-05 14:53:55 +05:30
_persistConversationsAsync ( const < Conversation > [ ] ) ;
2025-11-01 14:54:08 +05:30
return const [ ] ;
}
if ( ref . watch ( reviewerModeProvider ) ) {
return _demoConversations ( ) ;
}
2025-11-05 14:53:55 +05:30
final storage = ref . read ( optimizedStorageServiceProvider ) ;
try {
final cached = await storage . getLocalConversations ( ) ;
if ( cached . isNotEmpty ) {
final sortedCached = _sortByUpdatedAt ( cached ) ;
Future . microtask ( ( ) async {
try {
await refresh ( includeFolders: true ) ;
} catch ( error , stackTrace ) {
DebugLogger . error (
' warm-refresh-failed ' ,
scope: ' conversations/cache ' ,
error: error ,
stackTrace: stackTrace ,
) ;
}
} ) ;
DebugLogger . log (
' cache-restored ' ,
scope: ' conversations/cache ' ,
data: { ' count ' : sortedCached . length } ,
) ;
return sortedCached ;
}
DebugLogger . log ( ' cache-empty ' , scope: ' conversations/cache ' ) ;
} catch ( error , stackTrace ) {
DebugLogger . error (
' cache-load-failed ' ,
scope: ' conversations/cache ' ,
error: error ,
stackTrace: stackTrace ,
) ;
}
final fresh = await _loadRemoteConversations ( ) ;
_persistConversationsAsync ( fresh ) ;
return fresh ;
2025-08-17 16:11:19 +05:30
}
2025-11-01 14:54:08 +05:30
Future < void > refresh ( { bool includeFolders = false } ) async {
final authed = ref . read ( isAuthenticatedProvider2 ) ;
if ( ! authed ) {
_updateCacheTimestamp ( null ) ;
state = AsyncData < List < Conversation > > ( < Conversation > [ ] ) ;
2025-11-05 14:53:55 +05:30
_persistConversationsAsync ( const < Conversation > [ ] ) ;
2025-11-01 14:54:08 +05:30
if ( includeFolders ) {
2025-11-01 15:04:50 +05:30
unawaited ( ref . read ( foldersProvider . notifier ) . refresh ( ) ) ;
2025-11-01 14:54:08 +05:30
}
return ;
}
if ( ref . read ( reviewerModeProvider ) ) {
state = AsyncData < List < Conversation > > ( _demoConversations ( ) ) ;
if ( includeFolders ) {
2025-11-01 15:04:50 +05:30
unawaited ( ref . read ( foldersProvider . notifier ) . refresh ( ) ) ;
2025-11-01 14:54:08 +05:30
}
return ;
}
final result = await AsyncValue . guard ( _loadRemoteConversations ) ;
if ( ! ref . mounted ) return ;
2025-11-05 14:53:55 +05:30
result . when (
data: ( conversations ) {
state = AsyncData < List < Conversation > > ( conversations ) ;
_persistConversationsAsync ( conversations ) ;
} ,
error: ( error , stackTrace ) {
DebugLogger . error (
' refresh-failed ' ,
scope: ' conversations ' ,
error: error ,
stackTrace: stackTrace ,
data: { ' preservedData ' : state . asData ! = null } ,
) ;
} ,
loading: ( ) { } ,
) ;
2025-11-01 14:54:08 +05:30
if ( includeFolders ) {
2025-11-01 15:04:50 +05:30
unawaited ( ref . read ( foldersProvider . notifier ) . refresh ( ) ) ;
2025-11-01 14:54:08 +05:30
}
2025-08-10 01:20:45 +05:30
}
2025-11-01 14:54:08 +05:30
void removeConversation ( String id ) {
final current = state . asData ? . value ;
if ( current = = null ) return ;
final updated = current
. where ( ( conversation ) = > conversation . id ! = id )
. toList ( growable: true ) ;
2025-11-05 14:53:55 +05:30
_replaceState ( updated ) ;
2025-08-10 01:20:45 +05:30
}
2025-11-01 14:54:08 +05:30
void upsertConversation ( Conversation conversation ) {
final current = state . asData ? . value ? ? const < Conversation > [ ] ;
final updated = < Conversation > [ . . . current ] ;
final index = updated . indexWhere (
( element ) = > element . id = = conversation . id ,
2025-08-10 01:20:45 +05:30
) ;
2025-11-01 14:54:08 +05:30
if ( index > = 0 ) {
updated [ index ] = conversation ;
} else {
updated . add ( conversation ) ;
}
2025-11-05 14:53:55 +05:30
_replaceState ( updated ) ;
2025-11-01 14:54:08 +05:30
}
void updateConversation (
String id ,
Conversation Function ( Conversation conversation ) transform ,
) {
final current = state . asData ? . value ;
if ( current = = null ) return ;
final index = current . indexWhere ( ( conversation ) = > conversation . id = = id ) ;
if ( index < 0 ) return ;
final updated = < Conversation > [ . . . current ] ;
updated [ index ] = transform ( updated [ index ] ) ;
2025-11-05 14:53:55 +05:30
_replaceState ( updated ) ;
}
void _replaceState ( List < Conversation > conversations ) {
final sorted = _sortByUpdatedAt ( conversations ) ;
state = AsyncData < List < Conversation > > ( sorted ) ;
_persistConversationsAsync ( sorted ) ;
}
void _persistConversationsAsync ( List < Conversation > conversations ) {
final storage = ref . read ( optimizedStorageServiceProvider ) ;
unawaited (
Future < void > ( ( ) async {
try {
await storage . saveLocalConversations ( conversations ) ;
DebugLogger . log (
' cache-saved ' ,
scope: ' conversations/cache ' ,
data: { ' count ' : conversations . length } ,
) ;
} catch ( error , stackTrace ) {
DebugLogger . error (
' cache-save-failed ' ,
scope: ' conversations/cache ' ,
error: error ,
stackTrace: stackTrace ,
) ;
}
} ) ,
) ;
2025-11-01 14:54:08 +05:30
}
List < Conversation > _demoConversations ( ) = > [
Conversation (
id: ' demo-conv-1 ' ,
title: ' Welcome to Conduit (Demo) ' ,
createdAt: DateTime . now ( ) . subtract ( const Duration ( minutes: 15 ) ) ,
updatedAt: DateTime . now ( ) . subtract ( const Duration ( minutes: 10 ) ) ,
messages: [
ChatMessage (
id: ' demo-msg-1 ' ,
role: ' assistant ' ,
content:
' **Welcome to Conduit Demo Mode** \n \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. ' ,
timestamp: DateTime . now ( ) . subtract ( const Duration ( minutes: 10 ) ) ,
model: ' Gemma 2 Mini (Demo) ' ,
isStreaming: false ,
) ,
] ,
) ,
] ;
Future < List < Conversation > > _loadRemoteConversations ( ) async {
final api = ref . watch ( apiServiceProvider ) ;
if ( api = = null ) {
DebugLogger . warning ( ' api-missing ' , scope: ' conversations ' ) ;
return const [ ] ;
}
2025-08-17 16:17:39 +05:30
try {
2025-11-01 14:54:08 +05:30
DebugLogger . log ( ' fetch-start ' , scope: ' conversations ' ) ;
2025-11-13 17:02:09 +05:30
final conversationsFuture = api . getConversations ( ) ;
final foldersFuture = api . getFolders ( ) . catchError ( ( error , stackTrace ) {
DebugLogger . error (
' folders-fetch-failed ' ,
scope: ' conversations ' ,
error: error ,
stackTrace: stackTrace ,
) ;
return < Map < String , dynamic > > [ ] ;
} ) ;
final results = await Future . wait < dynamic > ( [
conversationsFuture ,
foldersFuture ,
] ) ;
final conversations = results [ 0 ] as List < Conversation > ;
final foldersData = results [ 1 ] as List < Map < String , dynamic > > ;
2025-08-20 22:15:26 +05:30
DebugLogger . log (
2025-11-01 14:54:08 +05:30
' fetch-ok ' ,
2025-09-25 22:36:42 +05:30
scope: ' conversations ' ,
2025-11-01 14:54:08 +05:30
data: { ' count ' : conversations . length } ,
2025-08-17 16:17:39 +05:30
) ;
2025-11-13 17:02:09 +05:30
DebugLogger . log (
' folders-fetched ' ,
scope: ' conversations ' ,
data: { ' count ' : foldersData . length } ,
) ;
2025-08-17 16:17:39 +05:30
2025-11-13 17:02:09 +05:30
final folders = foldersData
. map ( ( folderData ) = > Folder . fromJson ( folderData ) )
. toList ( ) ;
final conversationToFolder = < String , String > { } ;
for ( final folder in folders ) {
2025-09-25 22:36:42 +05:30
DebugLogger . log (
2025-11-13 17:02:09 +05:30
' folder ' ,
scope: ' conversations/map ' ,
data: {
' id ' : folder . id ,
' name ' : folder . name ,
' count ' : folder . conversationIds . length ,
} ,
2025-08-17 16:17:39 +05:30
) ;
2025-11-13 17:02:09 +05:30
for ( final conversationId in folder . conversationIds ) {
conversationToFolder [ conversationId ] = folder . id ;
DebugLogger . log (
' map ' ,
scope: ' conversations/map ' ,
data: { ' conversationId ' : conversationId , ' folderId ' : folder . id } ,
) ;
}
}
2025-08-17 16:17:39 +05:30
2025-11-13 17:02:09 +05:30
final conversationMap = < String , Conversation > { } ;
2025-11-01 14:54:08 +05:30
2025-11-13 17:02:09 +05:30
for ( final conversation in conversations ) {
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 (
2025-11-13 17:02:09 +05:30
' update-folder ' ,
2025-09-25 22:36:42 +05:30
scope: ' conversations/map ' ,
data: {
2025-11-13 17:02:09 +05:30
' conversationId ' : conversation . id ,
' folderId ' : folderIdToUse ,
' explicit ' : explicitFolderId ! = null ,
2025-09-25 22:36:42 +05:30
} ,
2025-08-17 16:17:39 +05:30
) ;
2025-11-13 17:02:09 +05:30
} else {
conversationMap [ conversation . id ] = conversation ;
2025-08-17 16:17:39 +05:30
}
2025-11-13 17:02:09 +05:30
}
2025-08-17 16:11:19 +05:30
2025-11-13 17:02:09 +05:30
final existingIds = conversationMap . keys . toSet ( ) ;
final missingInBase = conversationToFolder . keys
. where ( ( id ) = > ! existingIds . contains ( id ) )
. toList ( ) ;
if ( missingInBase . isNotEmpty ) {
DebugLogger . warning (
' missing-in-base ' ,
scope: ' conversations/map ' ,
data: {
' count ' : missingInBase . length ,
' preview ' : missingInBase . take ( 5 ) . toList ( ) ,
} ,
) ;
} else {
DebugLogger . log ( ' folders-synced ' , scope: ' conversations/map ' ) ;
}
2025-11-01 14:54:08 +05:30
2025-11-13 17:02:09 +05:30
for ( final folder in folders ) {
final missingIds = folder . conversationIds
. where ( ( id ) = > ! existingIds . contains ( id ) )
. toList ( ) ;
final hasKnownConversations = conversationMap . values . any (
( conversation ) = > conversation . folderId = = folder . id ,
) ;
final shouldFetchFolder =
missingIds . isNotEmpty | |
( ! hasKnownConversations & & folder . conversationIds . isEmpty ) ;
List < Conversation > folderConvs = const [ ] ;
if ( shouldFetchFolder ) {
try {
folderConvs = await api . getFolderConversationSummaries ( folder . id ) ;
2025-09-25 22:36:42 +05:30
DebugLogger . log (
2025-11-13 17:02:09 +05:30
' folder-sync ' ,
2025-09-25 22:36:42 +05:30
scope: ' conversations/map ' ,
2025-11-01 14:54:08 +05:30
data: {
2025-11-13 17:02:09 +05:30
' folderId ' : folder . id ,
' fetched ' : folderConvs . length ,
' missingIds ' : missingIds . length ,
2025-11-01 14:54:08 +05:30
} ,
2025-08-17 16:17:39 +05:30
) ;
2025-11-13 17:02:09 +05:30
} catch ( e ) {
DebugLogger . error (
' folder-fetch-failed ' ,
scope: ' conversations/map ' ,
error: e ,
data: { ' folderId ' : folder . id } ,
) ;
2025-08-17 00:05:30 +05:30
}
2025-10-02 00:06:00 +05:30
}
2025-11-13 17:02:09 +05:30
final fetchedMap = { for ( final c in folderConvs ) c . id: c } ;
2025-11-01 14:54:08 +05:30
2025-11-13 17:02:09 +05:30
for ( final convId in missingIds ) {
final fetched = fetchedMap [ convId ] ;
if ( fetched ! = null ) {
final toAdd = fetched . folderId = = null
? fetched . copyWith ( folderId: folder . id )
: fetched ;
conversationMap [ toAdd . id ] = toAdd ;
existingIds . add ( toAdd . id ) ;
DebugLogger . log (
' add-missing ' ,
scope: ' conversations/map ' ,
data: { ' conversationId ' : toAdd . id , ' folderId ' : folder . id } ,
) ;
} else {
final placeholder = Conversation (
id: convId ,
title: ' Chat ' ,
createdAt: DateTime . now ( ) ,
updatedAt: DateTime . now ( ) ,
messages: const [ ] ,
folderId: folder . id ,
) ;
conversationMap [ convId ] = placeholder ;
existingIds . add ( convId ) ;
DebugLogger . log (
' add-placeholder ' ,
scope: ' conversations/map ' ,
data: { ' conversationId ' : convId , ' folderId ' : folder . id } ,
) ;
2025-11-01 14:54:08 +05:30
}
2025-11-13 17:02:09 +05:30
}
2025-08-17 16:17:39 +05:30
2025-11-13 17:02:09 +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-11-01 14:54:08 +05:30
}
}
}
2025-11-13 17:02:09 +05:30
final sortedConversations = _sortByUpdatedAt (
conversationMap . values . toList ( ) ,
) ;
DebugLogger . log (
' sort ' ,
scope: ' conversations ' ,
data: { ' source ' : ' folder-sync ' } ,
) ;
_updateCacheTimestamp ( DateTime . now ( ) ) ;
return sortedConversations ;
2025-11-01 14:54:08 +05:30
} catch ( e , stackTrace ) {
2025-09-25 22:36:42 +05:30
DebugLogger . error (
2025-11-01 14:54:08 +05:30
' fetch-failed ' ,
2025-09-25 22:36:42 +05:30
scope: ' conversations ' ,
error: e ,
2025-11-01 14:54:08 +05:30
stackTrace: stackTrace ,
2025-09-25 22:36:42 +05:30
) ;
2025-11-01 14:54:08 +05:30
if ( e . toString ( ) . contains ( ' 403 ' ) ) {
DebugLogger . warning ( ' endpoint-403 ' , scope: ' conversations ' ) ;
}
return const [ ] ;
2025-08-17 00:05:30 +05:30
}
2025-11-01 14:54:08 +05:30
}
2025-08-10 01:20:45 +05:30
2025-11-01 14:54:08 +05:30
List < Conversation > _sortByUpdatedAt ( List < Conversation > conversations ) {
final sorted = [ . . . conversations ] ;
sorted . sort ( ( a , b ) = > b . updatedAt . compareTo ( a . updatedAt ) ) ;
return List < Conversation > . unmodifiable ( sorted ) ;
}
2025-08-10 01:20:45 +05:30
2025-11-01 14:54:08 +05:30
void _updateCacheTimestamp ( DateTime ? timestamp ) {
ref . read ( _conversationsCacheTimestampProvider . notifier ) . set ( timestamp ) ;
2025-08-10 01:20:45 +05:30
}
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 ) ;
2025-11-22 21:53:14 +05:30
final storage = ref . read ( optimizedStorageServiceProvider ) ;
2025-08-28 18:54:06 +05:30
// 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-11-22 21:53:14 +05:30
// Warm restore from cached resolved default model
try {
final cached = await storage . getLocalDefaultModel ( ) ;
if ( cached ! = null & & ! ref . read ( isManualModelSelectionProvider ) ) {
ref . read ( selectedModelProvider . notifier ) . set ( cached ) ;
DebugLogger . log (
' cached-default ' ,
scope: ' models/default ' ,
data: { ' name ' : cached . name } ,
) ;
return cached ;
}
} catch ( _ ) { }
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 ) ) {
2025-11-22 21:53:14 +05:30
final cachedMatch = await selectCachedModel ( storage , storedDefaultId ) ;
if ( cachedMatch ! = null ) {
ref . read ( selectedModelProvider . notifier ) . set ( cachedMatch ) ;
unawaited (
storage . saveLocalDefaultModel ( cachedMatch ) . onError ( (
error ,
stack ,
) {
DebugLogger . error (
' Failed to save default model to cache ' ,
scope: ' models/default ' ,
error: error ,
stackTrace: stack ,
) ;
} ) ,
) ;
DebugLogger . log (
' cache-select ' ,
scope: ' models/default ' ,
data: { ' name ' : cachedMatch . name , ' source ' : ' cache ' } ,
) ;
} else {
DebugLogger . log (
' cache-skip-missing ' ,
scope: ' models/default ' ,
data: { ' id ' : storedDefaultId } ,
) ;
}
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-11-22 21:53:14 +05:30
List < Model > models ;
final modelsAsync = ref . read ( modelsProvider ) ;
if ( modelsAsync . hasValue ) {
models = modelsAsync . value ? ? const < Model > [ ] ;
} else {
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-11-22 21:53:14 +05:30
unawaited (
storage . saveLocalDefaultModel ( resolved ) . onError ( ( error , stack ) {
DebugLogger . error (
' Failed to save default model to cache ' ,
scope: ' models/default ' ,
error: error ,
stackTrace: stack ,
) ;
} ) ,
) ;
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-11-22 21:53:14 +05:30
unawaited (
storage . saveLocalDefaultModel ( placeholder ) . onError ( ( error , stack ) {
DebugLogger . error (
' Failed to save placeholder model to cache ' ,
scope: ' models/default ' ,
error: error ,
stackTrace: stack ,
) ;
} ) ,
) ;
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-11-22 21:53:14 +05:30
unawaited (
storage . saveLocalDefaultModel ( resolved ) . onError ( ( error , stack ) {
DebugLogger . error (
' Failed to save default model to cache ' ,
scope: ' models/default ' ,
error: error ,
stackTrace: stack ,
) ;
} ) ,
) ;
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-11-22 21:53:14 +05:30
unawaited (
storage . saveLocalDefaultModel ( selectedModel ) . onError ( ( error , stack ) {
DebugLogger . error (
' Failed to save default model to cache ' ,
scope: ' models/default ' ,
error: error ,
stackTrace: stack ,
) ;
} ) ,
) ;
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-11-01 15:04:50 +05:30
class Folders extends _ $Folders {
@ override
Future < List < Folder > > build ( ) async {
if ( ! ref . watch ( isAuthenticatedProvider2 ) ) {
DebugLogger . log ( ' skip-unauthed ' , scope: ' folders ' ) ;
2025-11-10 10:44:03 +05:30
_persistFoldersAsync ( const [ ] ) ;
2025-11-01 15:04:50 +05:30
return const [ ] ;
}
2025-11-10 10:44:03 +05:30
final storage = ref . watch ( optimizedStorageServiceProvider ) ;
final cached = await storage . getLocalFolders ( ) ;
if ( cached . isNotEmpty ) {
DebugLogger . log (
' cache-restored ' ,
scope: ' folders/cache ' ,
data: { ' count ' : cached . length } ,
) ;
Future . microtask ( ( ) async {
try {
await refresh ( ) ;
} catch ( error , stackTrace ) {
DebugLogger . error (
' warm-refresh-failed ' ,
scope: ' folders/cache ' ,
error: error ,
stackTrace: stackTrace ,
) ;
}
} ) ;
return _sort ( cached ) ;
}
DebugLogger . log ( ' cache-empty ' , scope: ' folders/cache ' ) ;
2025-11-01 15:04:50 +05:30
final api = ref . watch ( apiServiceProvider ) ;
if ( api = = null ) {
DebugLogger . warning ( ' api-missing ' , scope: ' folders ' ) ;
return const [ ] ;
}
2025-11-10 10:44:03 +05:30
final fresh = await _load ( api ) ;
return fresh ;
2025-09-28 20:41:35 +05:30
}
2025-11-01 15:04:50 +05:30
Future < void > refresh ( ) async {
if ( ! ref . read ( isAuthenticatedProvider2 ) ) {
state = const AsyncData < List < Folder > > ( [ ] ) ;
2025-11-10 10:44:03 +05:30
_persistFoldersAsync ( const [ ] ) ;
2025-11-01 15:04:50 +05:30
return ;
}
final api = ref . read ( apiServiceProvider ) ;
if ( api = = null ) {
state = const AsyncData < List < Folder > > ( [ ] ) ;
2025-11-10 10:44:03 +05:30
_persistFoldersAsync ( const [ ] ) ;
2025-11-01 15:04:50 +05:30
return ;
}
final result = await AsyncValue . guard ( ( ) = > _load ( api ) ) ;
if ( ! ref . mounted ) return ;
state = result ;
2025-08-17 00:05:30 +05:30
}
2025-08-10 01:20:45 +05:30
2025-11-01 15:04:50 +05:30
void upsertFolder ( Folder folder ) {
final current = state . asData ? . value ? ? const < Folder > [ ] ;
final updated = < Folder > [ . . . current ] ;
final index = updated . indexWhere ( ( existing ) = > existing . id = = folder . id ) ;
if ( index > = 0 ) {
updated [ index ] = folder ;
} else {
updated . add ( folder ) ;
}
2025-11-10 10:44:03 +05:30
final sorted = _sort ( updated ) ;
state = AsyncData < List < Folder > > ( sorted ) ;
_persistFoldersAsync ( sorted ) ;
2025-11-01 15:04:50 +05:30
}
void updateFolder ( String id , Folder Function ( Folder folder ) transform ) {
final current = state . asData ? . value ;
if ( current = = null ) return ;
final index = current . indexWhere ( ( folder ) = > folder . id = = id ) ;
if ( index < 0 ) return ;
final updated = < Folder > [ . . . current ] ;
updated [ index ] = transform ( updated [ index ] ) ;
2025-11-10 10:44:03 +05:30
final sorted = _sort ( updated ) ;
state = AsyncData < List < Folder > > ( sorted ) ;
_persistFoldersAsync ( sorted ) ;
2025-11-01 15:04:50 +05:30
}
void removeFolder ( String id ) {
final current = state . asData ? . value ;
if ( current = = null ) return ;
final updated = current
. where ( ( folder ) = > folder . id ! = id )
. toList ( growable: true ) ;
2025-11-10 10:44:03 +05:30
final sorted = _sort ( updated ) ;
state = AsyncData < List < Folder > > ( sorted ) ;
_persistFoldersAsync ( sorted ) ;
2025-11-01 15:04:50 +05:30
}
Future < List < Folder > > _load ( ApiService api ) async {
try {
final foldersData = await api . getFolders ( ) ;
final folders = foldersData
. map ( ( folderData ) = > Folder . fromJson ( folderData ) )
. toList ( ) ;
DebugLogger . log (
' fetch-ok ' ,
scope: ' folders ' ,
data: { ' count ' : folders . length } ,
) ;
2025-11-10 10:44:03 +05:30
final sorted = _sort ( folders ) ;
_persistFoldersAsync ( sorted ) ;
return sorted ;
2025-11-01 15:04:50 +05:30
} catch ( e , stackTrace ) {
DebugLogger . error (
' fetch-failed ' ,
scope: ' folders ' ,
error: e ,
stackTrace: stackTrace ,
) ;
return const [ ] ;
}
}
2025-11-10 10:44:03 +05:30
void _persistFoldersAsync ( List < Folder > folders ) {
final storage = ref . read ( optimizedStorageServiceProvider ) ;
unawaited ( storage . saveLocalFolders ( folders ) ) ;
}
2025-11-01 15:04:50 +05:30
List < Folder > _sort ( List < Folder > input ) {
final sorted = [ . . . input ] ;
sorted . sort ( ( a , b ) = > a . name . toLowerCase ( ) . compareTo ( b . name . toLowerCase ( ) ) ) ;
return List < Folder > . unmodifiable ( sorted ) ;
2025-08-10 01:20:45 +05:30
}
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-11-01 15:15:38 +05:30
class UserFiles extends _ $UserFiles {
@ override
Future < List < FileInfo > > build ( ) async {
if ( ! ref . watch ( isAuthenticatedProvider2 ) ) {
DebugLogger . log ( ' skip-unauthed ' , scope: ' files ' ) ;
return const [ ] ;
}
final api = ref . watch ( apiServiceProvider ) ;
if ( api = = null ) return const [ ] ;
return _load ( api ) ;
2025-09-28 20:41:35 +05:30
}
2025-08-10 01:20:45 +05:30
2025-11-01 15:15:38 +05:30
Future < void > refresh ( ) async {
if ( ! ref . read ( isAuthenticatedProvider2 ) ) {
state = const AsyncData < List < FileInfo > > ( [ ] ) ;
return ;
}
final api = ref . read ( apiServiceProvider ) ;
if ( api = = null ) {
state = const AsyncData < List < FileInfo > > ( [ ] ) ;
return ;
}
final result = await AsyncValue . guard ( ( ) = > _load ( api ) ) ;
if ( ! ref . mounted ) return ;
state = result ;
}
void upsert ( FileInfo file ) {
final current = state . asData ? . value ? ? const < FileInfo > [ ] ;
final updated = < FileInfo > [ . . . current ] ;
final index = updated . indexWhere ( ( existing ) = > existing . id = = file . id ) ;
if ( index > = 0 ) {
updated [ index ] = file ;
} else {
updated . add ( file ) ;
}
state = AsyncData < List < FileInfo > > ( _sort ( updated ) ) ;
}
void remove ( String id ) {
final current = state . asData ? . value ;
if ( current = = null ) return ;
final updated = current
. where ( ( file ) = > file . id ! = id )
. toList ( growable: true ) ;
state = AsyncData < List < FileInfo > > ( _sort ( updated ) ) ;
}
Future < List < FileInfo > > _load ( ApiService api ) async {
try {
final files = await api . getUserFiles ( ) ;
return _sort ( files ) ;
} catch ( e , stackTrace ) {
DebugLogger . error (
' files-failed ' ,
scope: ' files ' ,
error: e ,
stackTrace: stackTrace ,
) ;
return const [ ] ;
}
}
List < FileInfo > _sort ( List < FileInfo > input ) {
final sorted = [ . . . input ] ;
sorted . sort ( ( a , b ) = > b . updatedAt . compareTo ( a . updatedAt ) ) ;
return List < FileInfo > . unmodifiable ( sorted ) ;
2025-08-10 01:20:45 +05:30
}
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-11-01 15:15:38 +05:30
class KnowledgeBases extends _ $KnowledgeBases {
@ override
Future < List < KnowledgeBase > > build ( ) async {
if ( ! ref . watch ( isAuthenticatedProvider2 ) ) {
DebugLogger . log ( ' skip-unauthed ' , scope: ' knowledge ' ) ;
return const [ ] ;
}
final api = ref . watch ( apiServiceProvider ) ;
if ( api = = null ) return const [ ] ;
return _load ( api ) ;
2025-09-28 20:41:35 +05:30
}
2025-08-10 01:20:45 +05:30
2025-11-01 15:15:38 +05:30
Future < void > refresh ( ) async {
if ( ! ref . read ( isAuthenticatedProvider2 ) ) {
state = const AsyncData < List < KnowledgeBase > > ( [ ] ) ;
return ;
}
final api = ref . read ( apiServiceProvider ) ;
if ( api = = null ) {
state = const AsyncData < List < KnowledgeBase > > ( [ ] ) ;
return ;
}
final result = await AsyncValue . guard ( ( ) = > _load ( api ) ) ;
if ( ! ref . mounted ) return ;
state = result ;
}
void upsert ( KnowledgeBase knowledgeBase ) {
final current = state . asData ? . value ? ? const < KnowledgeBase > [ ] ;
final updated = < KnowledgeBase > [ . . . current ] ;
final index = updated . indexWhere (
( existing ) = > existing . id = = knowledgeBase . id ,
) ;
if ( index > = 0 ) {
updated [ index ] = knowledgeBase ;
} else {
updated . add ( knowledgeBase ) ;
}
state = AsyncData < List < KnowledgeBase > > ( _sort ( updated ) ) ;
}
void remove ( String id ) {
final current = state . asData ? . value ;
if ( current = = null ) return ;
final updated = current
. where ( ( knowledgeBase ) = > knowledgeBase . id ! = id )
. toList ( growable: true ) ;
state = AsyncData < List < KnowledgeBase > > ( _sort ( updated ) ) ;
}
Future < List < KnowledgeBase > > _load ( ApiService api ) async {
try {
final knowledgeBases = await api . getKnowledgeBases ( ) ;
return _sort ( knowledgeBases ) ;
} catch ( e , stackTrace ) {
DebugLogger . error (
' knowledge-bases-failed ' ,
scope: ' knowledge ' ,
error: e ,
stackTrace: stackTrace ,
) ;
return const [ ] ;
}
}
List < KnowledgeBase > _sort ( List < KnowledgeBase > input ) {
final sorted = [ . . . input ] ;
sorted . sort ( ( a , b ) = > b . updatedAt . compareTo ( a . updatedAt ) ) ;
return List < KnowledgeBase > . unmodifiable ( sorted ) ;
2025-08-10 01:20:45 +05:30
}
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 {
2025-11-01 15:15:38 +05:30
return await api . getKnowledgeBaseItems ( kbId ) ;
2025-09-30 14:42:27 +05:30
} 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 {
2025-10-23 16:31:15 +05:30
final voices = await api . getAvailableServerVoices ( ) ;
return voices
. map ( ( v ) = > ( v [ ' name ' ] ? ? v [ ' id ' ] ? ? ' ' ) . toString ( ) )
. where ( ( s ) = > s . isNotEmpty )
. toList ( ) ;
2025-08-10 01:20:45 +05:30
} 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
}
2025-11-22 21:53:14 +05:30
/// Helper function to select cached model based on settings and available models.
/// Used by both chat page and defaultModel provider to ensure consistent behavior.
/// Returns a cached model if available, otherwise returns null.
Future < Model ? > selectCachedModel (
OptimizedStorageService storage ,
String ? desiredModelId ,
) async {
try {
final cachedModels = await storage . getLocalModels ( ) ;
if ( cachedModels . isEmpty ) return null ;
Model ? match ;
if ( desiredModelId ! = null & & desiredModelId . isNotEmpty ) {
try {
match = cachedModels . firstWhere (
( model ) = >
model . id = = desiredModelId | |
model . name . trim ( ) = = desiredModelId . trim ( ) ,
) ;
} catch ( _ ) {
match = null ;
}
}
return match ? ? cachedModels . first ;
} catch ( error , stackTrace ) {
DebugLogger . error (
' cache-select-failed ' ,
scope: ' models/cache ' ,
error: error ,
stackTrace: stackTrace ,
) ;
return null ;
}
}
/// Resolves socket transport availability from backend configuration.
///
/// Used by both the sync [socketTransportOptionsProvider] and the
/// [BackendConfigNotifier] to ensure consistent resolution logic.
SocketTransportAvailability _resolveTransportAvailability (
BackendConfig config ,
) {
if ( config . websocketOnly ) {
return const SocketTransportAvailability (
allowPolling: false ,
allowWebsocketOnly: true ,
) ;
}
if ( config . pollingOnly ) {
return const SocketTransportAvailability (
allowPolling: true ,
allowWebsocketOnly: false ,
) ;
}
return const SocketTransportAvailability (
allowPolling: true ,
allowWebsocketOnly: true ,
) ;
}