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