diff --git a/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt b/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt index bb0122bc5b..2a55b31033 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/MainActivity.kt @@ -46,17 +46,16 @@ import com.example.llama.ui.scaffold.topbar.TopBarConfig import com.example.llama.ui.screens.BenchmarkScreen import com.example.llama.ui.screens.ConversationScreen import com.example.llama.ui.screens.ModelLoadingScreen -import com.example.llama.ui.screens.ModelSelectionScreen -import com.example.llama.ui.screens.ModelsManagementScreen +import com.example.llama.ui.screens.ModelsScreen import com.example.llama.ui.screens.SettingsGeneralScreen import com.example.llama.ui.theme.LlamaTheme import com.example.llama.viewmodel.BenchmarkViewModel import com.example.llama.viewmodel.ConversationViewModel import com.example.llama.viewmodel.MainViewModel import com.example.llama.viewmodel.ModelLoadingViewModel -import com.example.llama.viewmodel.ModelSelectionViewModel -import com.example.llama.viewmodel.ModelsManagementViewModel +import com.example.llama.viewmodel.ModelsViewModel import com.example.llama.viewmodel.SettingsViewModel +import com.example.llama.viewmodel.ModelScreenUiMode import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @@ -85,11 +84,11 @@ class MainActivity : ComponentActivity() { fun AppContent( settingsViewModel: SettingsViewModel, mainViewModel: MainViewModel = hiltViewModel(), - modelSelectionViewModel: ModelSelectionViewModel = hiltViewModel(), + modelsViewModel: ModelsViewModel = hiltViewModel(), modelLoadingViewModel: ModelLoadingViewModel = hiltViewModel(), benchmarkViewModel: BenchmarkViewModel = hiltViewModel(), conversationViewModel: ConversationViewModel = hiltViewModel(), - modelsManagementViewModel: ModelsManagementViewModel = hiltViewModel(), +// modelsManagementViewModel: ModelsManagementViewModel = hiltViewModel(), ) { val coroutineScope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } @@ -97,6 +96,9 @@ fun AppContent( // Inference engine state val engineState by mainViewModel.engineState.collectAsState() + // Model state + val modelScreenUiMode by modelsViewModel.modelScreenUiMode.collectAsState() + // Metric states for scaffolds val isMonitoringEnabled by settingsViewModel.isMonitoringEnabled.collectAsState() val memoryUsage by settingsViewModel.memoryUsage.collectAsState() @@ -187,60 +189,157 @@ fun AppContent( // Create scaffold's top & bottom bar configs based on current route val scaffoldConfig = when { // Model selection screen - currentRoute == AppDestinations.MODEL_SELECTION_ROUTE -> { + currentRoute == AppDestinations.MODELS_ROUTE -> { // Collect states for bottom bar - val isSearchActive by modelSelectionViewModel.isSearchActive.collectAsState() - val sortOrder by modelSelectionViewModel.sortOrder.collectAsState() - val showSortMenu by modelSelectionViewModel.showSortMenu.collectAsState() - val activeFilters by modelSelectionViewModel.activeFilters.collectAsState() - val showFilterMenu by modelSelectionViewModel.showFilterMenu.collectAsState() - val preselection by modelSelectionViewModel.preselection.collectAsState() + val sortOrder by modelsViewModel.sortOrder.collectAsState() + val showSortMenu by modelsViewModel.showSortMenu.collectAsState() + val activeFilters by modelsViewModel.activeFilters.collectAsState() + val showFilterMenu by modelsViewModel.showFilterMenu.collectAsState() + val preselection by modelsViewModel.preselectedModelToRun.collectAsState() + + val selectedModelsToDelete by modelsViewModel.selectedModelsToDelete.collectAsState() + val showImportModelMenu by modelsViewModel.showImportModelMenu.collectAsState() + + // Create file launcher for importing local models + val fileLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.OpenDocument() + ) { uri -> uri?.let { modelsViewModel.importLocalModelFileSelected(it) } } ScaffoldConfig( topBarConfig = - if (isSearchActive) TopBarConfig.None() - else TopBarConfig.Default( - title = "Pick your model", - navigationIcon = NavigationIcon.Menu { - modelSelectionViewModel.resetPreselection() - openDrawer() - } - ), - bottomBarConfig = BottomBarConfig.ModelSelection( - search = BottomBarConfig.ModelSelection.SearchConfig( - isActive = isSearchActive, - onToggleSearch = modelSelectionViewModel::toggleSearchState, - textFieldState = modelSelectionViewModel.searchFieldState, - onSearch = { /* No-op for now */ } - ), - sorting = BottomBarConfig.ModelSelection.SortingConfig( - currentOrder = sortOrder, - isMenuVisible = showSortMenu, - toggleMenu = modelSelectionViewModel::toggleSortMenu, - selectOrder = { - modelSelectionViewModel.setSortOrder(it) - modelSelectionViewModel.toggleSortMenu(false) - } - ), - filtering = BottomBarConfig.ModelSelection.FilteringConfig( - isActive = activeFilters.any { it.value }, - filters = activeFilters, - onToggleFilter = modelSelectionViewModel::toggleFilter, - onClearFilters = modelSelectionViewModel::clearFilters, - isMenuVisible = showFilterMenu, - toggleMenu = modelSelectionViewModel::toggleFilterMenu - ), - runAction = BottomBarConfig.ModelSelection.RunActionConfig( - preselection = preselection, - onClickRun = { - if (modelSelectionViewModel.selectModel(it)) { - modelSelectionViewModel.toggleSearchState(false) - modelSelectionViewModel.resetPreselection() - navigationActions.navigateToModelLoading() - } - } - ) - ) + when (modelScreenUiMode) { + ModelScreenUiMode.BROWSING -> + TopBarConfig.ModelsBrowsing( + title = "Installed models", + navigationIcon = NavigationIcon.Menu { + modelsViewModel.resetPreselection() + openDrawer() + }, + onToggleMode = modelsViewModel::toggleMode, + ) + ModelScreenUiMode.SEARCHING -> + TopBarConfig.None() + ModelScreenUiMode.MANAGING -> + TopBarConfig.ModelsManagement( + title = "Managing models", + navigationIcon = NavigationIcon.Back { + modelsViewModel.toggleMode(ModelScreenUiMode.BROWSING) + }, + storageMetrics = if (isMonitoringEnabled) storageMetrics else null, + ) + ModelScreenUiMode.DELETING -> + TopBarConfig.ModelsDeleting( + title = "Deleting models", + navigationIcon = NavigationIcon.Quit { + modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING) + }, + ) + }, + bottomBarConfig = + when (modelScreenUiMode) { + ModelScreenUiMode.BROWSING -> + BottomBarConfig.Models.Browsing( + onToggleSearching = { + modelsViewModel.toggleMode(ModelScreenUiMode.SEARCHING) + }, + sorting = BottomBarConfig.Models.Browsing.SortingConfig( + currentOrder = sortOrder, + isMenuVisible = showSortMenu, + toggleMenu = modelsViewModel::toggleSortMenu, + selectOrder = { + modelsViewModel.setSortOrder(it) + modelsViewModel.toggleSortMenu(false) + } + ), + filtering = BottomBarConfig.Models.Browsing.FilteringConfig( + isActive = activeFilters.any { it.value }, + filters = activeFilters, + onToggleFilter = modelsViewModel::toggleFilter, + onClearFilters = modelsViewModel::clearFilters, + isMenuVisible = showFilterMenu, + toggleMenu = modelsViewModel::toggleFilterMenu + ), + runAction = BottomBarConfig.Models.RunActionConfig( + preselectedModelToRun = preselection, + onClickRun = { + if (modelsViewModel.selectModel(it)) { + modelsViewModel.resetPreselection() + navigationActions.navigateToModelLoading() + } + } + ), + ) + + ModelScreenUiMode.SEARCHING -> + BottomBarConfig.Models.Searching( + textFieldState = modelsViewModel.searchFieldState, + onQuitSearching = { + modelsViewModel.toggleMode(ModelScreenUiMode.BROWSING) + }, + onSearch = { /* No-op for now */ }, + runAction = BottomBarConfig.Models.RunActionConfig( + preselectedModelToRun = preselection, + onClickRun = { + if (modelsViewModel.selectModel(it)) { + modelsViewModel.resetPreselection() + navigationActions.navigateToModelLoading() + } + } + ), + ) + + ModelScreenUiMode.MANAGING -> + BottomBarConfig.Models.Management( + sorting = BottomBarConfig.Models.Management.SortingConfig( + currentOrder = sortOrder, + isMenuVisible = showSortMenu, + toggleMenu = { modelsViewModel.toggleSortMenu(it) }, + selectOrder = { + modelsViewModel.setSortOrder(it) + modelsViewModel.toggleSortMenu(false) + } + ), + filtering = BottomBarConfig.Models.Management.FilteringConfig( + isActive = activeFilters.any { it.value }, + filters = activeFilters, + onToggleFilter = modelsViewModel::toggleFilter, + onClearFilters = modelsViewModel::clearFilters, + isMenuVisible = showFilterMenu, + toggleMenu = modelsViewModel::toggleFilterMenu + ), + importing = BottomBarConfig.Models.Management.ImportConfig( + isMenuVisible = showImportModelMenu, + toggleMenu = { show -> modelsViewModel.toggleImportMenu(show) }, + importFromLocal = { + fileLauncher.launch(arrayOf("application/octet-stream", "*/*")) + modelsViewModel.toggleImportMenu(false) + }, + importFromHuggingFace = { + modelsViewModel.queryModelsFromHuggingFace() + modelsViewModel.toggleImportMenu(false) + } + ), + onToggleDeleting = { + modelsViewModel.toggleMode(ModelScreenUiMode.DELETING) + } + ) + + ModelScreenUiMode.DELETING -> + BottomBarConfig.Models.Deleting( + onQuitDeleting = { + modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING) + }, + selectedModels = selectedModelsToDelete, + toggleAllSelection = { modelsViewModel.toggleAllSelectedModelsToDelete(it) }, + deleteSelected = { + selectedModelsToDelete.let { + if (it.isNotEmpty()) { + modelsViewModel.batchDeletionClicked(it) + } + } + }, + ) + } ) } @@ -327,75 +426,6 @@ fun AppContent( ) ) - // Storage management screen - currentRoute == AppDestinations.MODELS_MANAGEMENT_ROUTE -> { - // Collect the needed states - val sortOrder by modelsManagementViewModel.sortOrder.collectAsState() - val isMultiSelectionMode by modelsManagementViewModel.isMultiSelectionMode.collectAsState() - val selectedModels by modelsManagementViewModel.selectedModels.collectAsState() - val showSortMenu by modelsManagementViewModel.showSortMenu.collectAsState() - val activeFilters by modelsManagementViewModel.activeFilters.collectAsState() - val showFilterMenu by modelsManagementViewModel.showFilterMenu.collectAsState() - val showImportModelMenu by modelsManagementViewModel.showImportModelMenu.collectAsState() - - // Create file launcher for importing local models - val fileLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.OpenDocument() - ) { uri -> uri?.let { modelsManagementViewModel.importLocalModelFileSelected(it) } } - - val bottomBarConfig = BottomBarConfig.ModelsManagement( - sorting = BottomBarConfig.ModelsManagement.SortingConfig( - currentOrder = sortOrder, - isMenuVisible = showSortMenu, - toggleMenu = { modelsManagementViewModel.toggleSortMenu(it) }, - selectOrder = { - modelsManagementViewModel.setSortOrder(it) - modelsManagementViewModel.toggleSortMenu(false) - } - ), - filtering = BottomBarConfig.ModelsManagement.FilteringConfig( - isActive = activeFilters.any { it.value }, - filters = activeFilters, - onToggleFilter = modelsManagementViewModel::toggleFilter, - onClearFilters = modelsManagementViewModel::clearFilters, - isMenuVisible = showFilterMenu, - toggleMenu = modelsManagementViewModel::toggleFilterMenu - ), - selection = BottomBarConfig.ModelsManagement.SelectionConfig( - isActive = isMultiSelectionMode, - toggleMode = { modelsManagementViewModel.toggleSelectionMode(it) }, - selectedModels = selectedModels, - toggleAllSelection = { modelsManagementViewModel.toggleAllSelection(it) }, - deleteSelected = { - if (selectedModels.isNotEmpty()) { - modelsManagementViewModel.batchDeletionClicked(selectedModels) - } - }, - ), - importing = BottomBarConfig.ModelsManagement.ImportConfig( - isMenuVisible = showImportModelMenu, - toggleMenu = { show -> modelsManagementViewModel.toggleImportMenu(show) }, - importFromLocal = { - fileLauncher.launch(arrayOf("application/octet-stream", "*/*")) - modelsManagementViewModel.toggleImportMenu(false) - }, - importFromHuggingFace = { - modelsManagementViewModel.queryModelsFromHuggingFace() - modelsManagementViewModel.toggleImportMenu(false) - } - ) - ) - - ScaffoldConfig( - topBarConfig = TopBarConfig.Storage( - title = "Models Management", - navigationIcon = NavigationIcon.Back { navigationActions.navigateUp() }, - storageMetrics = if (isMonitoringEnabled) storageMetrics else null, - ), - bottomBarConfig = bottomBarConfig - ) - } - // Fallback for empty screen or unknown routes else -> ScaffoldConfig( topBarConfig = TopBarConfig.Default(title = "", navigationIcon = NavigationIcon.None) @@ -419,22 +449,22 @@ fun AppContent( // AnimatedNavHost inside the scaffold content AnimatedNavHost( navController = navController, - startDestination = AppDestinations.MODEL_SELECTION_ROUTE, + startDestination = AppDestinations.MODELS_ROUTE, modifier = Modifier.padding(paddingValues) ) { // Model Selection Screen - composable(AppDestinations.MODEL_SELECTION_ROUTE) { - ModelSelectionScreen( + composable(AppDestinations.MODELS_ROUTE) { + ModelsScreen( onManageModelsClicked = { - navigationActions.navigateToModelsManagement() + // TODO-han.yin: remove this after implementing onboarding flow }, onConfirmSelection = { modelInfo, ramWarning -> - if (modelSelectionViewModel.confirmSelectedModel(modelInfo, ramWarning)) { + if (modelsViewModel.confirmSelectedModel(modelInfo, ramWarning)) { navigationActions.navigateToModelLoading() - modelSelectionViewModel.toggleSearchState(false) } }, - viewModel = modelSelectionViewModel + onScaffoldEvent = handleScaffoldEvent, + viewModel = modelsViewModel ) } @@ -502,14 +532,6 @@ fun AppContent( ) } - // Models Management Screen - composable(AppDestinations.MODELS_MANAGEMENT_ROUTE) { - ModelsManagementScreen( - onScaffoldEvent = handleScaffoldEvent, - viewModel = modelsManagementViewModel - ) - } - // General Settings Screen composable(AppDestinations.SETTINGS_GENERAL_ROUTE) { SettingsGeneralScreen( diff --git a/examples/llama.android/app/src/main/java/com/example/llama/navigation/AppDestinations.kt b/examples/llama.android/app/src/main/java/com/example/llama/navigation/AppDestinations.kt index 38e7fad41a..78d5a4dde8 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/navigation/AppDestinations.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/navigation/AppDestinations.kt @@ -8,7 +8,7 @@ import com.example.llama.engine.ModelLoadingMetrics */ object AppDestinations { // Primary navigation destinations - const val MODEL_SELECTION_ROUTE = "model_selection" + const val MODELS_ROUTE = "models" const val MODEL_LOADING_ROUTE = "model_loading" const val CONVERSATION_ROUTE = "conversation" @@ -19,7 +19,6 @@ object AppDestinations { // Settings destinations const val SETTINGS_GENERAL_ROUTE = "settings_general" - const val MODELS_MANAGEMENT_ROUTE = "models_management" } /** @@ -28,9 +27,9 @@ object AppDestinations { class NavigationActions(private val navController: NavController) { fun navigateToModelSelection() { - navController.navigate(AppDestinations.MODEL_SELECTION_ROUTE) { + navController.navigate(AppDestinations.MODELS_ROUTE) { // Clear back stack to start fresh - popUpTo(AppDestinations.MODEL_SELECTION_ROUTE) { inclusive = true } + popUpTo(AppDestinations.MODELS_ROUTE) { inclusive = true } } } @@ -55,10 +54,6 @@ class NavigationActions(private val navController: NavController) { navController.navigate(AppDestinations.SETTINGS_GENERAL_ROUTE) } - fun navigateToModelsManagement() { - navController.navigate(AppDestinations.MODELS_MANAGEMENT_ROUTE) - } - fun navigateUp() { navController.navigateUp() } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/components/ModelCards.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/components/ModelCards.kt index 6d36ecf240..f81bf954bb 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/components/ModelCards.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/components/ModelCards.kt @@ -32,7 +32,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -149,7 +148,6 @@ fun ModelCardCoreExpandable( * @param isExpanded Whether additional details is expanded or shrunk * @param onExpanded Action to perform when the card is expanded or shrunk */ -@OptIn(ExperimentalLayoutApi::class) @Composable fun ModelCardFullExpandable( model: ModelInfo, @@ -158,10 +156,6 @@ fun ModelCardFullExpandable( isExpanded: Boolean = false, onExpanded: ((Boolean) -> Unit)? = null, ) { - LaunchedEffect(model) { - android.util.Log.w("JOJO", model.languages.toString()) - } - CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) { Card( modifier = Modifier diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/AppScaffold.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/AppScaffold.kt index f67076509a..98ad2c5cd3 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/AppScaffold.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/AppScaffold.kt @@ -10,9 +10,12 @@ import androidx.compose.runtime.remember import com.example.llama.ui.scaffold.bottombar.BenchmarkBottomBar import com.example.llama.ui.scaffold.bottombar.BottomBarConfig import com.example.llama.ui.scaffold.bottombar.ConversationBottomBar -import com.example.llama.ui.scaffold.bottombar.ModelSelectionBottomBar +import com.example.llama.ui.scaffold.bottombar.ModelsBrowsingBottomBar +import com.example.llama.ui.scaffold.bottombar.ModelsDeletingBottomBar import com.example.llama.ui.scaffold.bottombar.ModelsManagementBottomBar +import com.example.llama.ui.scaffold.bottombar.ModelsSearchingBottomBar import com.example.llama.ui.scaffold.topbar.DefaultTopBar +import com.example.llama.ui.scaffold.topbar.ModelsBrowsingTopBar import com.example.llama.ui.scaffold.topbar.NavigationIcon import com.example.llama.ui.scaffold.topbar.PerformanceTopBar import com.example.llama.ui.scaffold.topbar.StorageTopBar @@ -65,6 +68,25 @@ fun AppScaffold( onMenuOpen = topBarconfig.navigationIcon.menuAction, ) + is TopBarConfig.ModelsBrowsing -> ModelsBrowsingTopBar( + title = topBarconfig.title, + onToggleMode = topBarconfig.onToggleMode, + onNavigateBack = topBarconfig.navigationIcon.backAction, + onMenuOpen = topBarconfig.navigationIcon.menuAction + ) + + is TopBarConfig.ModelsDeleting -> DefaultTopBar( + title = topBarconfig.title, + onQuit = topBarconfig.navigationIcon.quitAction + ) + + is TopBarConfig.ModelsManagement -> StorageTopBar( + title = topBarconfig.title, + storageMetrics = topBarconfig.storageMetrics, + onScaffoldEvent = onScaffoldEvent, + onNavigateBack = topBarconfig.navigationIcon.backAction, + ) + is TopBarConfig.Performance -> PerformanceTopBar( title = topBarconfig.title, memoryMetrics = topBarconfig.memoryMetrics, @@ -73,13 +95,6 @@ fun AppScaffold( onNavigateBack = topBarconfig.navigationIcon.backAction, onMenuOpen = topBarconfig.navigationIcon.menuAction, ) - - is TopBarConfig.Storage -> StorageTopBar( - title = topBarconfig.title, - storageMetrics = topBarconfig.storageMetrics, - onScaffoldEvent = onScaffoldEvent, - onNavigateBack = topBarconfig.navigationIcon.backAction, - ) } } @@ -87,24 +102,37 @@ fun AppScaffold( when (val config = bottomBarConfig) { is BottomBarConfig.None -> { /* No bottom bar */ } - is BottomBarConfig.ModelSelection -> { - ModelSelectionBottomBar( - search = config.search, - sorting = config.sorting, - filtering = config.filtering, - runAction = config.runAction + is BottomBarConfig.Models.Browsing -> { + ModelsBrowsingBottomBar( + onToggleSearching = config.onToggleSearching, + sortingConfig = config.sorting, + filteringConfig = config.filtering, + runActionConfig = config.runAction ) } - is BottomBarConfig.ModelsManagement -> { - ModelsManagementBottomBar( - sorting = config.sorting, - filtering = config.filtering, - selection = config.selection, - importing = config.importing, + is BottomBarConfig.Models.Searching -> { + ModelsSearchingBottomBar( + textFieldState = config.textFieldState, + onQuitSearching = config.onQuitSearching, + onSearch = config.onSearch, + runActionConfig = config.runAction, ) } + is BottomBarConfig.Models.Management -> { + ModelsManagementBottomBar( + sortingConfig = config.sorting, + filteringConfig = config.filtering, + importingConfig = config.importing, + onToggleDeleting = config.onToggleDeleting + ) + } + + is BottomBarConfig.Models.Deleting -> { + ModelsDeletingBottomBar(config) + } + is BottomBarConfig.Benchmark -> { BenchmarkBottomBar( showShareFab = config.showShareFab, @@ -141,8 +169,11 @@ fun AppScaffold( } // Helper functions to obtain navigation actions if exist +private val NavigationIcon.menuAction: (() -> Unit)? + get() = (this as? NavigationIcon.Menu)?.onMenuOpen + private val NavigationIcon.backAction: (() -> Unit)? get() = (this as? NavigationIcon.Back)?.onNavigateBack -private val NavigationIcon.menuAction: (() -> Unit)? - get() = (this as? NavigationIcon.Menu)?.onMenuOpen +private val NavigationIcon.quitAction: (() -> Unit)? + get() = (this as? NavigationIcon.Quit)?.onQuit diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/NavigationDrawer.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/NavigationDrawer.kt index 9eed128e3b..de23b065c3 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/NavigationDrawer.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/NavigationDrawer.kt @@ -122,20 +122,21 @@ private fun DrawerContent( Spacer(modifier = Modifier.height(16.dp)) - // Main Navigation Items - Text( - text = "Navigation", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(start = 8.dp, bottom = 8.dp) - ) +// // Main Navigation Items + // TODO-han.yin: add back once we add more features +// Text( +// text = "Features", +// style = MaterialTheme.typography.labelMedium, +// color = MaterialTheme.colorScheme.onSurfaceVariant, +// modifier = Modifier.padding(start = 8.dp, bottom = 8.dp) +// ) DrawerNavigationItem( icon = Icons.Default.Home, - label = "Home", - isSelected = currentRoute == AppDestinations.MODEL_SELECTION_ROUTE, + label = "Models", + isSelected = currentRoute == AppDestinations.MODELS_ROUTE, onClick = { - if (currentRoute != AppDestinations.MODEL_SELECTION_ROUTE) { + if (currentRoute != AppDestinations.MODELS_ROUTE) { onNavigate { navigationActions.navigateToModelSelection() } } else { onNavigate { /* No-op: simply close drawer */ } @@ -143,22 +144,15 @@ private fun DrawerContent( } ) - Spacer(modifier = Modifier.height(24.dp)) - - // Settings Group - Text( - text = "Settings", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(start = 8.dp, bottom = 8.dp) - ) - - DrawerNavigationItem( - icon = Icons.Default.Folder, - label = "Models", - isSelected = currentRoute == AppDestinations.MODELS_MANAGEMENT_ROUTE, - onClick = { onNavigate { navigationActions.navigateToModelsManagement() } } - ) +// Spacer(modifier = Modifier.height(24.dp)) + // TODO-han.yin: add back once we add more features +// // Settings Group +// Text( +// text = "Settings", +// style = MaterialTheme.typography.labelMedium, +// color = MaterialTheme.colorScheme.onSurfaceVariant, +// modifier = Modifier.padding(start = 8.dp, bottom = 8.dp) +// ) DrawerNavigationItem( icon = Icons.Default.Settings, diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/BenchmarkBottomBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/BenchmarkBottomBar.kt index bc52e4ac8c..adf5f403f5 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/BenchmarkBottomBar.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/BenchmarkBottomBar.kt @@ -64,10 +64,7 @@ fun BenchmarkBottomBar( enter = scaleIn() + fadeIn(), exit = scaleOut() + fadeOut() ) { - FloatingActionButton( - onClick = onShare, - containerColor = MaterialTheme.colorScheme.primary - ) { + FloatingActionButton(onClick = onShare) { Icon( imageVector = Icons.Default.Share, contentDescription = "Share the benchmark results" diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/BottomBarConfig.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/BottomBarConfig.kt index 7331ec929c..73eea87b67 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/BottomBarConfig.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/BottomBarConfig.kt @@ -4,7 +4,7 @@ import androidx.compose.foundation.text.input.TextFieldState import com.example.llama.data.model.ModelFilter import com.example.llama.data.model.ModelInfo import com.example.llama.data.model.ModelSortOrder -import com.example.llama.viewmodel.Preselection +import com.example.llama.viewmodel.PreselectedModelToRun /** * [BottomAppBar] configurations @@ -13,76 +13,78 @@ sealed class BottomBarConfig { object None : BottomBarConfig() - data class ModelSelection( - val search: SearchConfig, - val sorting: SortingConfig, - val filtering: FilteringConfig, - val runAction: RunActionConfig - ) : BottomBarConfig() { - data class SearchConfig( - val isActive: Boolean, - val onToggleSearch: (Boolean) -> Unit, + sealed class Models : BottomBarConfig() { + + data class Browsing( + val onToggleSearching: () -> Unit, + val sorting: SortingConfig, + val filtering: FilteringConfig, + val runAction: RunActionConfig, + ) : BottomBarConfig() { + data class SortingConfig( + val currentOrder: ModelSortOrder, + val isMenuVisible: Boolean, + val toggleMenu: (Boolean) -> Unit, + val selectOrder: (ModelSortOrder) -> Unit + ) + + data class FilteringConfig( + val isActive: Boolean, + val filters: Map, + val onToggleFilter: (ModelFilter, Boolean) -> Unit, + val onClearFilters: () -> Unit, + val isMenuVisible: Boolean, + val toggleMenu: (Boolean) -> Unit + ) + } + + data class Searching( val textFieldState: TextFieldState, + val onQuitSearching: () -> Unit, val onSearch: (String) -> Unit, - ) + val runAction: RunActionConfig, + ) : BottomBarConfig() - data class SortingConfig( - val currentOrder: ModelSortOrder, - val isMenuVisible: Boolean, - val toggleMenu: (Boolean) -> Unit, - val selectOrder: (ModelSortOrder) -> Unit - ) + data class Management( + val sorting: SortingConfig, + val filtering: FilteringConfig, + val importing: ImportConfig, + val onToggleDeleting: () -> Unit, + ) : BottomBarConfig() { + data class SortingConfig( + val currentOrder: ModelSortOrder, + val isMenuVisible: Boolean, + val toggleMenu: (Boolean) -> Unit, + val selectOrder: (ModelSortOrder) -> Unit + ) - data class FilteringConfig( - val isActive: Boolean, - val filters: Map, - val onToggleFilter: (ModelFilter, Boolean) -> Unit, - val onClearFilters: () -> Unit, - val isMenuVisible: Boolean, - val toggleMenu: (Boolean) -> Unit - ) + data class FilteringConfig( + val isActive: Boolean, + val filters: Map, + val onToggleFilter: (ModelFilter, Boolean) -> Unit, + val onClearFilters: () -> Unit, + val isMenuVisible: Boolean, + val toggleMenu: (Boolean) -> Unit + ) - data class RunActionConfig( - val preselection: Preselection?, - val onClickRun: (Preselection) -> Unit, - ) - } + data class ImportConfig( + val isMenuVisible: Boolean, + val toggleMenu: (Boolean) -> Unit, + val importFromLocal: () -> Unit, + val importFromHuggingFace: () -> Unit + ) + } - data class ModelsManagement( - val sorting: SortingConfig, - val filtering: FilteringConfig, - val selection: SelectionConfig, - val importing: ImportConfig - ) : BottomBarConfig() { - data class SortingConfig( - val currentOrder: ModelSortOrder, - val isMenuVisible: Boolean, - val toggleMenu: (Boolean) -> Unit, - val selectOrder: (ModelSortOrder) -> Unit - ) - - data class FilteringConfig( - val isActive: Boolean, - val filters: Map, - val onToggleFilter: (ModelFilter, Boolean) -> Unit, - val onClearFilters: () -> Unit, - val isMenuVisible: Boolean, - val toggleMenu: (Boolean) -> Unit - ) - - data class SelectionConfig( - val isActive: Boolean, - val toggleMode: (Boolean) -> Unit, + data class Deleting( + val onQuitDeleting: () -> Unit, val selectedModels: Map, val toggleAllSelection: (Boolean) -> Unit, val deleteSelected: () -> Unit - ) + ) : BottomBarConfig() - data class ImportConfig( - val isMenuVisible: Boolean, - val toggleMenu: (Boolean) -> Unit, - val importFromLocal: () -> Unit, - val importFromHuggingFace: () -> Unit + data class RunActionConfig( + val preselectedModelToRun: PreselectedModelToRun?, + val onClickRun: (PreselectedModelToRun) -> Unit, ) } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelSelectionBottomBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelSelectionBottomBar.kt deleted file mode 100644 index 9eb296660c..0000000000 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelSelectionBottomBar.kt +++ /dev/null @@ -1,192 +0,0 @@ -package com.example.llama.ui.scaffold.bottombar - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleIn -import androidx.compose.animation.scaleOut -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.input.clearText -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Sort -import androidx.compose.material.icons.automirrored.outlined.Backspace -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.FilterAlt -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.SearchOff -import androidx.compose.material.icons.outlined.FilterAlt -import androidx.compose.material.icons.outlined.FilterAltOff -import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.Checkbox -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.example.llama.data.model.ModelSortOrder - -@Composable -fun ModelSelectionBottomBar( - search: BottomBarConfig.ModelSelection.SearchConfig, - sorting: BottomBarConfig.ModelSelection.SortingConfig, - filtering: BottomBarConfig.ModelSelection.FilteringConfig, - runAction: BottomBarConfig.ModelSelection.RunActionConfig -) { - BottomAppBar( - actions = { - if (search.isActive) { - // Quit search action - IconButton(onClick = { search.onToggleSearch(false) }) { - Icon( - imageVector = Icons.Default.SearchOff, - contentDescription = "Quit search mode" - ) - } - - // Clear query action - IconButton(onClick = { search.textFieldState.clearText() }) { - Icon( - imageVector = Icons.AutoMirrored.Outlined.Backspace, - contentDescription = "Clear query text" - ) - } - } else { - // Enter search action - IconButton(onClick = { search.onToggleSearch(true) }) { - Icon( - imageVector = Icons.Default.Search, - contentDescription = "Search models" - ) - } - - // Sorting action - IconButton(onClick = { sorting.toggleMenu(true) }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.Sort, - contentDescription = "Sort models" - ) - } - - // Sorting dropdown menu - DropdownMenu( - expanded = sorting.isMenuVisible, - onDismissRequest = { sorting.toggleMenu(false) } - ) { - val sortOptions = listOf( - Triple( - ModelSortOrder.NAME_ASC, - "Name (A-Z)", - "Sort by name in ascending order" - ), - Triple( - ModelSortOrder.NAME_DESC, - "Name (Z-A)", - "Sort by name in descending order" - ), - Triple( - ModelSortOrder.SIZE_ASC, - "Size (Smallest first)", - "Sort by size in ascending order" - ), - Triple( - ModelSortOrder.SIZE_DESC, - "Size (Largest first)", - "Sort by size in descending order" - ), - Triple(ModelSortOrder.LAST_USED, "Last used", "Sort by last used") - ) - - sortOptions.forEach { (order, label, description) -> - DropdownMenuItem( - text = { Text(label) }, - trailingIcon = { - if (sorting.currentOrder == order) - Icon( - imageVector = Icons.Default.Check, - contentDescription = "$description, selected" - ) - }, - onClick = { sorting.selectOrder(order) } - ) - } - } - - // Filter action - IconButton(onClick = { filtering.toggleMenu(true) }) { - Icon( - imageVector = - if (filtering.isActive) Icons.Default.FilterAlt - else Icons.Outlined.FilterAlt, - contentDescription = "Filter models" - ) - } - - // Filter dropdown menu - DropdownMenu( - expanded = filtering.isMenuVisible, - onDismissRequest = { filtering.toggleMenu(false) } - ) { - Text( - text = "Filter by", - style = MaterialTheme.typography.labelMedium, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) - ) - - filtering.filters.forEach { (filter, isEnabled) -> - DropdownMenuItem( - text = { Text(filter.displayName) }, - leadingIcon = { - Checkbox( - checked = isEnabled, - onCheckedChange = null - ) - }, - onClick = { filtering.onToggleFilter(filter, !isEnabled) } - ) - } - - HorizontalDivider() - - DropdownMenuItem( - text = { Text("Clear filters") }, - leadingIcon = { - Icon( - imageVector = Icons.Outlined.FilterAltOff, - contentDescription = "Clear all filters" - ) - }, - onClick = { - filtering.onClearFilters() - filtering.toggleMenu(false) - } - ) - } - } - }, - floatingActionButton = { - // Only show FAB if a model is selected - AnimatedVisibility( - visible = runAction.preselection != null, - enter = scaleIn() + fadeIn(), - exit = scaleOut() + fadeOut() - ) { - FloatingActionButton( - onClick = { runAction.preselection?.let { runAction.onClickRun(it) } }, - containerColor = MaterialTheme.colorScheme.primary - ) { - Icon( - imageVector = Icons.Default.PlayArrow, - contentDescription = "Run with selected model" - ) - } - } - } - ) -} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsBrowsingBottomBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsBrowsingBottomBar.kt new file mode 100644 index 0000000000..55d82efc2d --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsBrowsingBottomBar.kt @@ -0,0 +1,174 @@ +package com.example.llama.ui.scaffold.bottombar + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Sort +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.FilterAlt +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.outlined.FilterAlt +import androidx.compose.material.icons.outlined.FilterAltOff +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.Checkbox +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.example.llama.data.model.ModelSortOrder + +@Composable +fun ModelsBrowsingBottomBar( + onToggleSearching: () -> Unit, + sortingConfig: BottomBarConfig.Models.Browsing.SortingConfig, + filteringConfig: BottomBarConfig.Models.Browsing.FilteringConfig, + runActionConfig: BottomBarConfig.Models.RunActionConfig, +) { + BottomAppBar( + actions = { + // Enter search action + IconButton(onClick = onToggleSearching) { + Icon( + imageVector = Icons.Default.Search, + contentDescription = "Search models" + ) + } + + // Sorting action + IconButton(onClick = { sortingConfig.toggleMenu(true) }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Sort, + contentDescription = "Sort models" + ) + } + + // Sorting dropdown menu + DropdownMenu( + expanded = sortingConfig.isMenuVisible, + onDismissRequest = { sortingConfig.toggleMenu(false) } + ) { + val sortOptions = listOf( + Triple( + ModelSortOrder.NAME_ASC, + "Name (A-Z)", + "Sort by name in ascending order" + ), + Triple( + ModelSortOrder.NAME_DESC, + "Name (Z-A)", + "Sort by name in descending order" + ), + Triple( + ModelSortOrder.SIZE_ASC, + "Size (Smallest first)", + "Sort by size in ascending order" + ), + Triple( + ModelSortOrder.SIZE_DESC, + "Size (Largest first)", + "Sort by size in descending order" + ), + Triple(ModelSortOrder.LAST_USED, "Last used", "Sort by last used") + ) + + sortOptions.forEach { (order, label, description) -> + DropdownMenuItem( + text = { Text(label) }, + trailingIcon = { + if (sortingConfig.currentOrder == order) + Icon( + imageVector = Icons.Default.Check, + contentDescription = "$description, selected" + ) + }, + onClick = { sortingConfig.selectOrder(order) } + ) + } + } + + // Filter action + IconButton(onClick = { filteringConfig.toggleMenu(true) }) { + Icon( + imageVector = + if (filteringConfig.isActive) Icons.Default.FilterAlt + else Icons.Outlined.FilterAlt, + contentDescription = "Filter models" + ) + } + + // Filter dropdown menu + DropdownMenu( + expanded = filteringConfig.isMenuVisible, + onDismissRequest = { filteringConfig.toggleMenu(false) } + ) { + Text( + text = "Filter by", + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + + filteringConfig.filters.forEach { (filter, isEnabled) -> + DropdownMenuItem( + text = { Text(filter.displayName) }, + leadingIcon = { + Checkbox( + checked = isEnabled, + onCheckedChange = null + ) + }, + onClick = { filteringConfig.onToggleFilter(filter, !isEnabled) } + ) + } + + HorizontalDivider() + + DropdownMenuItem( + text = { Text("Clear filters") }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.FilterAltOff, + contentDescription = "Clear all filters" + ) + }, + onClick = { + filteringConfig.onClearFilters() + filteringConfig.toggleMenu(false) + } + ) + } + }, + floatingActionButton = { + // Only show FAB if a model is selected + AnimatedVisibility( + visible = runActionConfig.preselectedModelToRun != null, + enter = scaleIn() + fadeIn(), + exit = scaleOut() + fadeOut() + ) { + FloatingActionButton( + onClick = { + runActionConfig.preselectedModelToRun?.let { + runActionConfig.onClickRun(it) + } + }, + ) { + Icon( + imageVector = Icons.Default.PlayArrow, + contentDescription = "Run with selected model" + ) + } + } + } + ) +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsDeletingBottomBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsDeletingBottomBar.kt new file mode 100644 index 0000000000..dd8426d9b8 --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsDeletingBottomBar.kt @@ -0,0 +1,61 @@ +package com.example.llama.ui.scaffold.bottombar + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ClearAll +import androidx.compose.material.icons.filled.DeleteForever +import androidx.compose.material.icons.filled.SelectAll +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable + + +@Composable +fun ModelsDeletingBottomBar( + deleting: BottomBarConfig.Models.Deleting, +) { + BottomAppBar( + actions = { + IconButton(onClick = { deleting.toggleAllSelection(false) }) { + Icon( + imageVector = Icons.Default.ClearAll, + contentDescription = "Deselect all" + ) + } + + IconButton(onClick = { deleting.toggleAllSelection(true) }) { + Icon( + imageVector = Icons.Default.SelectAll, + contentDescription = "Select all" + ) + } + }, + floatingActionButton = { + AnimatedVisibility( + visible = deleting.selectedModels.isNotEmpty(), + enter = scaleIn() + fadeIn(), + exit = scaleOut() + fadeOut() + ) { + FloatingActionButton( + onClick = { + deleting.deleteSelected() + }, + containerColor = MaterialTheme.colorScheme.error + ) { + Icon( + imageVector = Icons.Default.DeleteForever, + contentDescription = "Delete selected models", + tint = MaterialTheme.colorScheme.onError, + ) + } + } + } + ) +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsManagementBottomBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsManagementBottomBar.kt index e3bb3fe1a3..79003e0eca 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsManagementBottomBar.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsManagementBottomBar.kt @@ -6,12 +6,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Sort import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.ClearAll -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.FilterAlt import androidx.compose.material.icons.filled.FolderOpen -import androidx.compose.material.icons.filled.SelectAll import androidx.compose.material.icons.outlined.DeleteSweep import androidx.compose.material.icons.outlined.FilterAlt import androidx.compose.material.icons.outlined.FilterAltOff @@ -35,176 +31,138 @@ import com.example.llama.data.model.ModelSortOrder @Composable fun ModelsManagementBottomBar( - sorting: BottomBarConfig.ModelsManagement.SortingConfig, - filtering: BottomBarConfig.ModelsManagement.FilteringConfig, - selection: BottomBarConfig.ModelsManagement.SelectionConfig, - importing: BottomBarConfig.ModelsManagement.ImportConfig + sortingConfig: BottomBarConfig.Models.Management.SortingConfig, + filteringConfig: BottomBarConfig.Models.Management.FilteringConfig, + importingConfig: BottomBarConfig.Models.Management.ImportConfig, + onToggleDeleting: () -> Unit, ) { BottomAppBar( actions = { - if (selection.isActive) { - /* Multi-selection mode actions */ - IconButton( - onClick = selection.deleteSelected, - enabled = selection.selectedModels.isNotEmpty() - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = "Delete selected", - tint = if (selection.selectedModels.isNotEmpty()) - MaterialTheme.colorScheme.error - else - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) - ) - } + // Batch-deletion action + IconButton(onClick = onToggleDeleting) { + Icon( + imageVector = Icons.Outlined.DeleteSweep, + contentDescription = "Delete models" + ) + } - IconButton(onClick = { selection.toggleAllSelection(false) }) { - Icon( - imageVector = Icons.Default.ClearAll, - contentDescription = "Deselect all" - ) - } + // Sorting action + IconButton(onClick = { sortingConfig.toggleMenu(true) }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Sort, + contentDescription = "Sort models" + ) + } - IconButton(onClick = { selection.toggleAllSelection(true) }) { - Icon( - imageVector = Icons.Default.SelectAll, - contentDescription = "Select all" - ) - } - } else { - /* Default mode actions */ - - // Multi-selection action - IconButton(onClick = { selection.toggleMode(true) }) { - Icon( - imageVector = Icons.Outlined.DeleteSweep, - contentDescription = "Delete models" - ) - } - - // Sorting action - IconButton(onClick = { sorting.toggleMenu(true) }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.Sort, - contentDescription = "Sort models" - ) - } - - // Sorting dropdown menu - DropdownMenu( - expanded = sorting.isMenuVisible, - onDismissRequest = { sorting.toggleMenu(false) } - ) { - val sortOptions = listOf( - Triple( - ModelSortOrder.NAME_ASC, - "Name (A-Z)", - "Sort by name in ascending order" - ), - Triple( - ModelSortOrder.NAME_DESC, - "Name (Z-A)", - "Sort by name in descending order" - ), - Triple( - ModelSortOrder.SIZE_ASC, - "Size (Smallest first)", - "Sort by size in ascending order" - ), - Triple( - ModelSortOrder.SIZE_DESC, - "Size (Largest first)", - "Sort by size in descending order" - ), - Triple(ModelSortOrder.LAST_USED, "Last used", "Sort by last used") - ) - - sortOptions.forEach { (order, label, description) -> - DropdownMenuItem( - text = { Text(label) }, - trailingIcon = { - if (sorting.currentOrder == order) - Icon( - imageVector = Icons.Default.Check, - contentDescription = "$description, selected" - ) - }, - onClick = { sorting.selectOrder(order) } - ) - } - } - - // Filtering action - IconButton(onClick = { filtering.toggleMenu(true) }) { - Icon( - imageVector = - if (filtering.isActive) Icons.Default.FilterAlt - else Icons.Outlined.FilterAlt, - contentDescription = "Filter models" - ) - } - - // Filter dropdown menu - DropdownMenu( - expanded = filtering.isMenuVisible, - onDismissRequest = { filtering.toggleMenu(false) } - ) { - Text( - text = "Filter by", - style = MaterialTheme.typography.labelMedium, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) - ) - - filtering.filters.forEach { (filter, isEnabled) -> - DropdownMenuItem( - text = { Text(filter.displayName) }, - leadingIcon = { - Checkbox( - checked = isEnabled, - onCheckedChange = null - ) - }, - onClick = { filtering.onToggleFilter(filter, !isEnabled) } - ) - } - - HorizontalDivider() + // Sorting dropdown menu + DropdownMenu( + expanded = sortingConfig.isMenuVisible, + onDismissRequest = { sortingConfig.toggleMenu(false) } + ) { + val sortOptions = listOf( + Triple( + ModelSortOrder.NAME_ASC, + "Name (A-Z)", + "Sort by name in ascending order" + ), + Triple( + ModelSortOrder.NAME_DESC, + "Name (Z-A)", + "Sort by name in descending order" + ), + Triple( + ModelSortOrder.SIZE_ASC, + "Size (Smallest first)", + "Sort by size in ascending order" + ), + Triple( + ModelSortOrder.SIZE_DESC, + "Size (Largest first)", + "Sort by size in descending order" + ), + Triple(ModelSortOrder.LAST_USED, "Last used", "Sort by last used") + ) + sortOptions.forEach { (order, label, description) -> DropdownMenuItem( - text = { Text("Clear filters") }, + text = { Text(label) }, + trailingIcon = { + if (sortingConfig.currentOrder == order) + Icon( + imageVector = Icons.Default.Check, + contentDescription = "$description, selected" + ) + }, + onClick = { sortingConfig.selectOrder(order) } + ) + } + } + + // Filtering action + IconButton(onClick = { filteringConfig.toggleMenu(true) }) { + Icon( + imageVector = + if (filteringConfig.isActive) Icons.Default.FilterAlt + else Icons.Outlined.FilterAlt, + contentDescription = "Filter models" + ) + } + + // Filter dropdown menu + DropdownMenu( + expanded = filteringConfig.isMenuVisible, + onDismissRequest = { filteringConfig.toggleMenu(false) } + ) { + Text( + text = "Filter by", + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + + filteringConfig.filters.forEach { (filter, isEnabled) -> + DropdownMenuItem( + text = { Text(filter.displayName) }, leadingIcon = { - Icon( - imageVector = Icons.Outlined.FilterAltOff, - contentDescription = "Clear all filters" + Checkbox( + checked = isEnabled, + onCheckedChange = null ) }, - onClick = { - filtering.onClearFilters() - filtering.toggleMenu(false) - } + onClick = { filteringConfig.onToggleFilter(filter, !isEnabled) } ) } + + HorizontalDivider() + + DropdownMenuItem( + text = { Text("Clear filters") }, + leadingIcon = { + Icon( + imageVector = Icons.Outlined.FilterAltOff, + contentDescription = "Clear all filters" + ) + }, + onClick = { + filteringConfig.onClearFilters() + filteringConfig.toggleMenu(false) + } + ) } }, floatingActionButton = { FloatingActionButton( - onClick = { - if (selection.isActive) selection.toggleMode(false) else importing.toggleMenu( - true - ) - }, - containerColor = MaterialTheme.colorScheme.primaryContainer + onClick = { importingConfig.toggleMenu(true) }, ) { Icon( - imageVector = if (selection.isActive) Icons.Default.Close else Icons.Default.Add, - contentDescription = if (selection.isActive) "Exit selection mode" else "Add model" + imageVector = Icons.Default.Add, + contentDescription = "Add model" ) } // Add model dropdown menu DropdownMenu( - expanded = importing.isMenuVisible, - onDismissRequest = { importing.toggleMenu(false) } + expanded = importingConfig.isMenuVisible, + onDismissRequest = { importingConfig.toggleMenu(false) } ) { DropdownMenuItem( text = { Text("Import local model") }, @@ -214,7 +172,7 @@ fun ModelsManagementBottomBar( contentDescription = "Import a local model on the device" ) }, - onClick = importing.importFromLocal + onClick = importingConfig.importFromLocal ) DropdownMenuItem( text = { Text("Download from HuggingFace") }, @@ -226,7 +184,7 @@ fun ModelsManagementBottomBar( tint = Color.Unspecified, ) }, - onClick = importing.importFromHuggingFace + onClick = importingConfig.importFromHuggingFace ) } } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsSearchingBottomBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsSearchingBottomBar.kt new file mode 100644 index 0000000000..99571cd196 --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/bottombar/ModelsSearchingBottomBar.kt @@ -0,0 +1,67 @@ +package com.example.llama.ui.scaffold.bottombar + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.Backspace +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.SearchOff +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable + +@Composable +fun ModelsSearchingBottomBar( + textFieldState: TextFieldState, + onQuitSearching: () -> Unit, + onSearch: (String) -> Unit, // TODO-han.yin: somehow this is unused? + runActionConfig: BottomBarConfig.Models.RunActionConfig, +) { + BottomAppBar( + actions = { + // Quit search action + IconButton(onClick = onQuitSearching) { + Icon( + imageVector = Icons.Default.SearchOff, + contentDescription = "Quit search mode" + ) + } + + // Clear query action + IconButton(onClick = { textFieldState.clearText() }) { + Icon( + imageVector = Icons.AutoMirrored.Outlined.Backspace, + contentDescription = "Clear query text" + ) + } + }, + floatingActionButton = { + // Only show FAB if a model is selected + AnimatedVisibility( + visible = runActionConfig.preselectedModelToRun != null, + enter = scaleIn() + fadeIn(), + exit = scaleOut() + fadeOut() + ) { + FloatingActionButton( + onClick = { + runActionConfig.preselectedModelToRun?.let { + runActionConfig.onClickRun(it) + } + }, + ) { + Icon( + imageVector = Icons.Default.PlayArrow, + contentDescription = "Run with selected model" + ) + } + } + } + ) +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/DefaultTopBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/DefaultTopBar.kt index 06b1854e32..95d02409a1 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/DefaultTopBar.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/DefaultTopBar.kt @@ -2,6 +2,7 @@ package com.example.llama.ui.scaffold.topbar import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Menu import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -17,12 +18,22 @@ import androidx.compose.runtime.Composable fun DefaultTopBar( title: String, onNavigateBack: (() -> Unit)? = null, + onQuit: (() -> Unit)? = null, onMenuOpen: (() -> Unit)? = null ) { TopAppBar( title = { Text(title) }, navigationIcon = { when { + onQuit != null -> { + IconButton(onClick = onQuit) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Quit" + ) + } + } + onNavigateBack != null -> { IconButton(onClick = onNavigateBack) { Icon( diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/ModelsBrowsingTopBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/ModelsBrowsingTopBar.kt new file mode 100644 index 0000000000..5409595026 --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/ModelsBrowsingTopBar.kt @@ -0,0 +1,88 @@ +package com.example.llama.ui.scaffold.topbar + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Build +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.example.llama.viewmodel.ModelScreenUiMode + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ModelsBrowsingTopBar( + title: String, + onToggleMode: (ModelScreenUiMode) -> Unit, + onNavigateBack: (() -> Unit)? = null, + onMenuOpen: (() -> Unit)? = null, +) { + TopAppBar( + title = { Text(title) }, + navigationIcon = { + when { + onNavigateBack != null -> { + IconButton(onClick = onNavigateBack) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + } + + onMenuOpen != null -> { + IconButton(onClick = onMenuOpen) { + Icon( + imageVector = Icons.Default.Menu, + contentDescription = "Menu" + ) + } + } + } + }, + actions = { + ModelManageActionToggle(onToggleManageMode = { + onToggleMode(ModelScreenUiMode.MANAGING) + }) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + titleContentColor = MaterialTheme.colorScheme.onSurface + ) + ) +} + +@Composable +private fun ModelManageActionToggle( + onToggleManageMode: () -> Unit, +) { + FilledTonalButton( + modifier = Modifier.padding(end = 12.dp), + onClick = onToggleManageMode + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Default.Build, + contentDescription = "Manage models", + tint = MaterialTheme.colorScheme.onSurface, + ) + + Text( + modifier = Modifier.padding(start = 4.dp), + text = "Manage", + style = MaterialTheme.typography.bodySmall + ) + } + } +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/PerformanceTopBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/PerformanceTopBar.kt index 46c4aa8cb9..05458058eb 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/PerformanceTopBar.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/PerformanceTopBar.kt @@ -1,10 +1,7 @@ package com.example.llama.ui.scaffold.topbar -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Memory @@ -15,13 +12,13 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import com.example.llama.monitoring.MemoryMetrics import com.example.llama.monitoring.TemperatureMetrics @@ -92,30 +89,32 @@ private fun MemoryIndicator( val availableGB = String.format(Locale.getDefault(), "%.1f", memoryUsage.availableGB) val totalGB = String.format(Locale.getDefault(), "%.1f", memoryUsage.totalGB) - Row( - modifier = Modifier.padding(end = 12.dp).clickable(role = Role.Button) { + OutlinedButton( + modifier = Modifier.padding(end = 8.dp), + onClick = { onScaffoldEvent(ScaffoldEvent.ShowSnackbar( message = "Free RAM available: $availableGB GB\nTotal RAM on your device: $totalGB GB", withDismissAction = true, )) - }, - verticalAlignment = Alignment.CenterVertically, + } ) { - Icon( - imageVector = Icons.Default.Memory, - contentDescription = "RAM usage", - tint = when { - memoryUsage.availableGB < 1 -> MaterialTheme.colorScheme.error - memoryUsage.availableGB < 3 -> MaterialTheme.colorScheme.tertiary - else -> MaterialTheme.colorScheme.onSurface - } - ) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Default.Memory, + contentDescription = "RAM usage", + tint = when { + memoryUsage.availableGB < 1 -> MaterialTheme.colorScheme.error + memoryUsage.availableGB < 3 -> MaterialTheme.colorScheme.tertiary + else -> MaterialTheme.colorScheme.onSurface + } + ) - Text( - modifier = Modifier.padding(start = 4.dp), - text = "$availableGB / $totalGB GB", - style = MaterialTheme.typography.bodySmall, - ) + Text( + modifier = Modifier.padding(start = 4.dp), + text = "$availableGB / $totalGB GB", + style = MaterialTheme.typography.bodySmall, + ) + } } } @@ -134,36 +133,39 @@ private fun TemperatureIndicator( } val warningDismissible = temperatureMetrics.warningLevel == TemperatureWarningLevel.HIGH - Row( - modifier = Modifier.padding(end = 12.dp).clickable(role = Role.Button) { + OutlinedButton( + modifier = Modifier.padding(end = 8.dp), + onClick = { onScaffoldEvent(ScaffoldEvent.ShowSnackbar( message = temperatureWarning, withDismissAction = warningDismissible, )) - }, - verticalAlignment = Alignment.CenterVertically) { - Icon( - imageVector = when (temperatureMetrics.warningLevel) { - TemperatureWarningLevel.HIGH -> Icons.Default.WarningAmber - else -> Icons.Default.Thermostat - }, - contentDescription = "Device temperature", - tint = when (temperatureMetrics.warningLevel) { - TemperatureWarningLevel.HIGH -> MaterialTheme.colorScheme.error - TemperatureWarningLevel.MEDIUM -> MaterialTheme.colorScheme.tertiary - else -> MaterialTheme.colorScheme.onSurface - } - ) + } + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = when (temperatureMetrics.warningLevel) { + TemperatureWarningLevel.HIGH -> Icons.Default.WarningAmber + else -> Icons.Default.Thermostat + }, + contentDescription = "Device temperature", + tint = when (temperatureMetrics.warningLevel) { + TemperatureWarningLevel.HIGH -> MaterialTheme.colorScheme.error + TemperatureWarningLevel.MEDIUM -> MaterialTheme.colorScheme.tertiary + else -> MaterialTheme.colorScheme.onSurface + } + ) - Text( - modifier = Modifier.padding(start = 4.dp), - text = temperatureDisplay, - style = MaterialTheme.typography.bodySmall, - color = when (temperatureMetrics.warningLevel) { - TemperatureWarningLevel.HIGH -> MaterialTheme.colorScheme.error - TemperatureWarningLevel.MEDIUM -> MaterialTheme.colorScheme.tertiary - else -> MaterialTheme.colorScheme.onSurface - } - ) + Text( + modifier = Modifier.padding(start = 4.dp), + text = temperatureDisplay, + style = MaterialTheme.typography.bodySmall, + color = when (temperatureMetrics.warningLevel) { + TemperatureWarningLevel.HIGH -> MaterialTheme.colorScheme.error + TemperatureWarningLevel.MEDIUM -> MaterialTheme.colorScheme.tertiary + else -> MaterialTheme.colorScheme.onSurface + } + ) + } } } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/StorageTopBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/StorageTopBar.kt index 7c896ae950..e8efc23d64 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/StorageTopBar.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/StorageTopBar.kt @@ -1,10 +1,7 @@ package com.example.llama.ui.scaffold.topbar -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.SdStorage @@ -12,13 +9,13 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import com.example.llama.monitoring.StorageMetrics import com.example.llama.ui.scaffold.ScaffoldEvent @@ -65,29 +62,31 @@ private fun StorageIndicator( val usedGb = String.format(Locale.getDefault(), "%.1f", storageMetrics.usedGB) val availableGb = String.format(Locale.getDefault(), "%.1f", storageMetrics.availableGB) - Row( - modifier = Modifier.padding(end = 8.dp).clickable(role = Role.Button) { + OutlinedButton( + modifier = Modifier.padding(end = 8.dp), + onClick = { onScaffoldEvent(ScaffoldEvent.ShowSnackbar( message = "Your models occupy $usedGb GB storage\nRemaining free space available: $availableGb GB", withDismissAction = true, )) - }, - verticalAlignment = Alignment.CenterVertically + } ) { - Icon( - imageVector = Icons.Default.SdStorage, - contentDescription = "Storage", - tint = when { - storageMetrics.availableGB < 5.0f -> MaterialTheme.colorScheme.error - storageMetrics.availableGB < 10.0f -> MaterialTheme.colorScheme.tertiary - else -> MaterialTheme.colorScheme.onSurface - } - ) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Default.SdStorage, + contentDescription = "Storage", + tint = when { + storageMetrics.availableGB < 5.0f -> MaterialTheme.colorScheme.error + storageMetrics.availableGB < 10.0f -> MaterialTheme.colorScheme.tertiary + else -> MaterialTheme.colorScheme.onSurface + } + ) - Text( - modifier = Modifier.padding(start = 4.dp), - text = "$usedGb / $availableGb GB", - style = MaterialTheme.typography.bodySmall - ) + Text( + modifier = Modifier.padding(start = 4.dp), + text = "$usedGb / $availableGb GB", + style = MaterialTheme.typography.bodySmall + ) + } } } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/TopBarConfig.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/TopBarConfig.kt index 043d1ae59e..d9076460fd 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/TopBarConfig.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/scaffold/topbar/TopBarConfig.kt @@ -3,6 +3,8 @@ package com.example.llama.ui.scaffold.topbar import com.example.llama.monitoring.MemoryMetrics import com.example.llama.monitoring.StorageMetrics import com.example.llama.monitoring.TemperatureMetrics +import com.example.llama.ui.scaffold.ScaffoldEvent +import com.example.llama.viewmodel.ModelScreenUiMode /** * [TopAppBar] configurations @@ -23,6 +25,19 @@ sealed class TopBarConfig { override val navigationIcon: NavigationIcon ) : TopBarConfig() + // Model management top bar with a toggle to turn on/off manage mode + data class ModelsBrowsing( + override val title: String, + override val navigationIcon: NavigationIcon, + val onToggleMode: (ModelScreenUiMode) -> Unit, + ) : TopBarConfig() + + // Model batch-deletion top bar with a toggle to turn on/off manage mode + data class ModelsDeleting( + override val title: String, + override val navigationIcon: NavigationIcon, + ) : TopBarConfig() + // Performance monitoring top bar with RAM and optional temperature data class Performance( override val title: String, @@ -32,7 +47,7 @@ sealed class TopBarConfig { ) : TopBarConfig() // Storage management top bar with used & total storage - data class Storage( + data class ModelsManagement( override val title: String, override val navigationIcon: NavigationIcon, val storageMetrics: StorageMetrics? @@ -43,7 +58,8 @@ sealed class TopBarConfig { * Helper class for navigation icon configuration */ sealed class NavigationIcon { - data class Back(val onNavigateBack: () -> Unit) : NavigationIcon() data class Menu(val onMenuOpen: () -> Unit) : NavigationIcon() - object None : NavigationIcon() + data class Back(val onNavigateBack: () -> Unit) : NavigationIcon() + data class Quit(val onQuit: () -> Unit) : NavigationIcon() + data object None : NavigationIcon() } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ConversationScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ConversationScreen.kt index ce175ad506..5b85aa5ee4 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ConversationScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ConversationScreen.kt @@ -110,7 +110,7 @@ fun ConversationScreen( // UI states val lifecycleOwner = LocalLifecycleOwner.current val coroutineScope = rememberCoroutineScope() - var isModelCardExpanded by remember { mutableStateOf(false) } + var isModelCardExpanded by remember { mutableStateOf(true) } val listState = rememberLazyListState() // Track the actual rendered size of the last message bubble @@ -234,7 +234,7 @@ fun ModelCardWithSystemPrompt( model: ModelInfo, loadingMetrics: ModelLoadingMetrics, systemPrompt: String?, - isExpanded: Boolean = false, + isExpanded: Boolean = true, onExpanded: ((Boolean) -> Unit)? = null, ) = ModelCardCoreExpandable(model, isExpanded, onExpanded) { Spacer(modifier = Modifier.height(8.dp)) diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelSelectionScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelSelectionScreen.kt deleted file mode 100644 index 9282e09f61..0000000000 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelSelectionScreen.kt +++ /dev/null @@ -1,315 +0,0 @@ -package com.example.llama.ui.screens - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.input.clearText -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.FolderOpen -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.SearchOff -import androidx.compose.material.icons.filled.Warning -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.DockedSearchBar -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SearchBarDefaults -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.platform.LocalSoftwareKeyboardController -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.example.llama.data.model.ModelInfo -import com.example.llama.ui.components.InfoAction -import com.example.llama.ui.components.InfoView -import com.example.llama.ui.components.ModelCardFullExpandable -import com.example.llama.util.formatFileByteSize -import com.example.llama.viewmodel.ModelSelectionViewModel -import com.example.llama.viewmodel.Preselection.RamWarning - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ModelSelectionScreen( - onManageModelsClicked: () -> Unit, - onConfirmSelection: (ModelInfo, RamWarning) -> Unit, - viewModel: ModelSelectionViewModel, -) { - // Data: models - val filteredModels by viewModel.filteredModels.collectAsState() - val preselection by viewModel.preselection.collectAsState() - - // Query states - val textFieldState = viewModel.searchFieldState - val isSearchActive by viewModel.isSearchActive.collectAsState() - val searchQuery by remember(textFieldState) { - derivedStateOf { textFieldState.text.toString() } - } - val queryResults by viewModel.queryResults.collectAsState() - - // Filter states - val activeFilters by viewModel.activeFilters.collectAsState() - val activeFiltersCount by remember(activeFilters) { - derivedStateOf { activeFilters.count { it.value } } - } - - // UI states - val focusRequester = remember { FocusRequester() } - val keyboardController = LocalSoftwareKeyboardController.current - val toggleSearchFocusAndIme: (Boolean) -> Unit = { show -> - if (show) { - focusRequester.requestFocus() - keyboardController?.show() - } else { - focusRequester.freeFocus() - keyboardController?.hide() - } - } - - // Handle back button press - BackHandler(preselection != null || isSearchActive) { - if (isSearchActive) { - viewModel.toggleSearchState(false) - } else { - viewModel.onBackPressed() - } - } - - LaunchedEffect (isSearchActive) { - if (isSearchActive) { - toggleSearchFocusAndIme(true) - } - } - - Box( - modifier = Modifier.fillMaxSize() - ) { - if (isSearchActive) { - DockedSearchBar( - modifier = Modifier.align(Alignment.TopCenter), - inputField = { - SearchBarDefaults.InputField( - modifier = Modifier.focusRequester(focusRequester), - query = textFieldState.text.toString(), - onQueryChange = { textFieldState.edit { replace(0, length, it) } }, - onSearch = {}, - expanded = true, - onExpandedChange = { expanded -> - viewModel.toggleSearchState(expanded) - textFieldState.clearText() - }, - leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, - trailingIcon = { Icon(Icons.Default.MoreVert, contentDescription = null) }, - placeholder = { Text("Type to search your models") } - ) - }, - expanded = true, - onExpandedChange = { - viewModel.toggleSearchState(it) - } - ) { - if (queryResults.isEmpty()) { - if (searchQuery.isNotBlank()) { - // If no results under current query, show "no results" message - EmptySearchResultsView( - onClearSearch = { - textFieldState.clearText() - toggleSearchFocusAndIme(true) - } - ) - } - } else { - LazyColumn( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(12.dp), - contentPadding = PaddingValues(vertical = 12.dp, horizontal = 16.dp), - ) { - items(items = queryResults, key = { it.id }) { model -> - ModelCardFullExpandable( - model = model, - isSelected = if (model == preselection?.modelInfo) true else null, - onSelected = { selected -> - if (selected) { - toggleSearchFocusAndIme(false) - } else { - viewModel.resetPreselection() - toggleSearchFocusAndIme(true) - } - }, - isExpanded = model == preselection?.modelInfo, - onExpanded = { expanded -> - viewModel.preselectModel(model, expanded) - toggleSearchFocusAndIme(!expanded) - } - ) - } - } - } - } - } else { - if (filteredModels.isEmpty()) { - // Empty model prompt - EmptyModelsView(activeFiltersCount, onManageModelsClicked) - } else { - // Model cards - LazyColumn( - Modifier.fillMaxSize(), // .padding(horizontal = 16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - contentPadding = PaddingValues(vertical = 12.dp, horizontal = 16.dp), - ) { - items(items = filteredModels, key = { it.id }) { model -> - ModelCardFullExpandable( - model = model, - isSelected = if (model == preselection?.modelInfo) true else null, - onSelected = { selected -> - if (!selected) viewModel.resetPreselection() - }, - isExpanded = model == preselection?.modelInfo, - onExpanded = { expanded -> - viewModel.preselectModel(model, expanded) - } - ) - } - } - } - } - - // Show insufficient RAM warning - preselection?.let { - it.ramWarning?.let { warning -> - if (warning.showing) { - RamErrorDialog( - warning, - onDismiss = { viewModel.dismissRamWarning() }, - onConfirm = { onConfirmSelection(it.modelInfo, warning) } - ) - } - } - } - } -} - -@Composable -private fun EmptyModelsView( - activeFiltersCount: Int, - onManageModelsClicked: () -> Unit -) { - val message = when (activeFiltersCount) { - 0 -> "Import some models to get started!" - 1 -> "No models match the selected filter" - else -> "No models match the selected filters" - } - InfoView( - modifier = Modifier.fillMaxSize(), - title = "No Models Available", - icon = Icons.Default.FolderOpen, - message = message, - action = InfoAction( - label = "Add Models", - icon = Icons.Default.Add, - onAction = onManageModelsClicked - ) - ) -} - -@Composable -private fun EmptySearchResultsView( - onClearSearch: () -> Unit -) { - Column( - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 32.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Icon( - imageVector = Icons.Default.SearchOff, - contentDescription = null, - modifier = Modifier.size(64.dp), - tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - text = "No matching models found", - style = MaterialTheme.typography.headlineSmall - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = "Try a different search term", - style = MaterialTheme.typography.bodyLarge, - textAlign = TextAlign.Center, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - Spacer(modifier = Modifier.height(24.dp)) - - Button(onClick = onClearSearch) { - Text("Clear Search") - } - } -} - -@Composable -private fun RamErrorDialog( - ramError: RamWarning, - onDismiss: () -> Unit, - onConfirm: () -> Unit, -) { - val requiredRam = formatFileByteSize(ramError.requiredRam) - val availableRam = formatFileByteSize(ramError.availableRam) - - AlertDialog( - text = { - InfoView( - modifier = Modifier.fillMaxWidth(), - title = "Insufficient RAM", - icon = Icons.Default.Warning, - message = "You are trying to run a $requiredRam size model, " + - "but currently there's only $availableRam memory available!", - ) - }, - containerColor = MaterialTheme.colorScheme.errorContainer, - titleContentColor = MaterialTheme.colorScheme.onErrorContainer, - textContentColor = MaterialTheme.colorScheme.onErrorContainer, - dismissButton = { - TextButton(onClick = onDismiss) { - Text("Cancel") - } - }, - onDismissRequest = onDismiss, - confirmButton = { - TextButton(onClick = onConfirm) { - Text( - text = "Proceed", - color = MaterialTheme.colorScheme.error - ) - } - } - ) -} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsBrowsingScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsBrowsingScreen.kt new file mode 100644 index 0000000000..6d1b005dea --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsBrowsingScreen.kt @@ -0,0 +1,88 @@ +package com.example.llama.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.FolderOpen +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.example.llama.ui.components.InfoAction +import com.example.llama.ui.components.InfoView +import com.example.llama.ui.components.ModelCardFullExpandable +import com.example.llama.viewmodel.ModelsViewModel + +@Composable +fun ModelsBrowsingScreen( + onManageModelsClicked: () -> Unit, + viewModel: ModelsViewModel, +) { + // Data: models + val filteredModels by viewModel.filteredModels.collectAsState() + val preselection by viewModel.preselectedModelToRun.collectAsState() + + // Filter states + val activeFilters by viewModel.activeFilters.collectAsState() + val activeFiltersCount by remember(activeFilters) { + derivedStateOf { activeFilters.count { it.value } } + } + + + if (filteredModels.isEmpty()) { + // Empty model prompt + EmptyModelsView(activeFiltersCount, onManageModelsClicked) + } else { + // Model cards + LazyColumn( + Modifier.fillMaxSize(), // .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(vertical = 12.dp, horizontal = 16.dp), + ) { + items(items = filteredModels, key = { it.id }) { model -> + ModelCardFullExpandable( + model = model, + isSelected = if (model == preselection?.modelInfo) true else null, + onSelected = { selected -> + if (!selected) viewModel.resetPreselection() + }, + isExpanded = model == preselection?.modelInfo, + onExpanded = { expanded -> + viewModel.preselectModel(model, expanded) + } + ) + } + } + } +} + + +@Composable +private fun EmptyModelsView( + activeFiltersCount: Int, + onManageModelsClicked: () -> Unit +) { + val message = when (activeFiltersCount) { + 0 -> "Import some models to get started!" + 1 -> "No models match the selected filter" + else -> "No models match the selected filters" + } + InfoView( + modifier = Modifier.fillMaxSize(), + title = "No Models Available", + icon = Icons.Default.FolderOpen, + message = message, + action = InfoAction( + label = "Add Models", + icon = Icons.Default.Add, + onAction = onManageModelsClicked + ) + ) +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsManagementScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsManagementAndDeletingScreen.kt similarity index 96% rename from examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsManagementScreen.kt rename to examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsManagementAndDeletingScreen.kt index df9a0e8d8a..3728bd48ea 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsManagementScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsManagementAndDeletingScreen.kt @@ -67,7 +67,8 @@ import com.example.llama.viewmodel.ModelManagementState import com.example.llama.viewmodel.ModelManagementState.Deletion import com.example.llama.viewmodel.ModelManagementState.Download import com.example.llama.viewmodel.ModelManagementState.Importation -import com.example.llama.viewmodel.ModelsManagementViewModel +import com.example.llama.viewmodel.ModelScreenUiMode +import com.example.llama.viewmodel.ModelsViewModel import java.text.SimpleDateFormat import java.util.Locale @@ -75,16 +76,16 @@ import java.util.Locale * Screen for managing LLM models (view, download, delete) */ @Composable -fun ModelsManagementScreen( +fun ModelsManagementAndDeletingScreen( + isDeleting: Boolean, onScaffoldEvent: (ScaffoldEvent) -> Unit, - viewModel: ModelsManagementViewModel, + viewModel: ModelsViewModel, ) { // Data: models val filteredModels by viewModel.filteredModels.collectAsState() // Selection state - val isMultiSelectionMode by viewModel.isMultiSelectionMode.collectAsState() - val selectedModels by viewModel.selectedModels.collectAsState() + val selectedModels by viewModel.selectedModelsToDelete.collectAsState() // Filter state val activeFilters by viewModel.activeFilters.collectAsState() @@ -96,19 +97,13 @@ fun ModelsManagementScreen( val managementState by viewModel.managementState.collectAsState() // UI states - var expandedModels = remember { mutableStateMapOf() } + val expandedModels = remember { mutableStateMapOf() } BackHandler( - enabled = isMultiSelectionMode - || managementState is Importation.Importing + enabled = managementState is Importation.Importing || managementState is Deletion.Deleting ) { - if (isMultiSelectionMode) { - // Exit selection mode if in selection mode - viewModel.toggleSelectionMode(false) - } else { - /* Ignore back press while processing model management requests */ - } + /* Ignore back press while processing model management requests */ } Box(modifier = Modifier.fillMaxSize()) { @@ -134,13 +129,13 @@ fun ModelsManagementScreen( ) { items(items = filteredModels, key = { it.id }) { model -> val isSelected = - if (isMultiSelectionMode) selectedModels.contains(model.id) else null + if (isDeleting) selectedModels.contains(model.id) else null ModelCardFullExpandable( model = model, isSelected = isSelected, onSelected = { - if (isMultiSelectionMode) { + if (isDeleting) { viewModel.toggleModelSelectionById(model.id) } }, @@ -285,7 +280,7 @@ fun ModelsManagementScreen( is Deletion.Success -> { LaunchedEffect(state) { - viewModel.toggleSelectionMode(false) + viewModel.toggleMode(ModelScreenUiMode.MANAGING) val count = state.models.size onScaffoldEvent( @@ -641,6 +636,9 @@ private fun BatchDeleteConfirmationDialog( } } }, + containerColor = MaterialTheme.colorScheme.errorContainer, + titleContentColor = MaterialTheme.colorScheme.onErrorContainer, + textContentColor = MaterialTheme.colorScheme.onErrorContainer, confirmButton = { TextButton( onClick = onConfirm, diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsScreen.kt new file mode 100644 index 0000000000..e4fd990edc --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsScreen.kt @@ -0,0 +1,132 @@ +package com.example.llama.ui.screens + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import com.example.llama.data.model.ModelInfo +import com.example.llama.ui.components.InfoView +import com.example.llama.ui.scaffold.ScaffoldEvent +import com.example.llama.util.formatFileByteSize +import com.example.llama.viewmodel.ModelScreenUiMode +import com.example.llama.viewmodel.ModelsViewModel +import com.example.llama.viewmodel.PreselectedModelToRun.RamWarning + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ModelsScreen( + onManageModelsClicked: () -> Unit, + onConfirmSelection: (ModelInfo, RamWarning) -> Unit, + onScaffoldEvent: (ScaffoldEvent) -> Unit, + viewModel: ModelsViewModel, +) { + // Data + val preselection by viewModel.preselectedModelToRun.collectAsState() + + // UI states + val currentMode by viewModel.modelScreenUiMode.collectAsState() + + // Handle back button press + BackHandler { + when (currentMode) { + ModelScreenUiMode.BROWSING -> { + if (preselection != null) { + viewModel.resetPreselection() + } + } + ModelScreenUiMode.SEARCHING -> { + viewModel.toggleMode(ModelScreenUiMode.BROWSING) + } + ModelScreenUiMode.MANAGING -> { + viewModel.toggleMode(ModelScreenUiMode.BROWSING) + } + ModelScreenUiMode.DELETING -> { + viewModel.toggleAllSelectedModelsToDelete(false) + viewModel.toggleMode(ModelScreenUiMode.MANAGING) + } + } + } + + Box( + modifier = Modifier.fillMaxSize() + ) { + when (currentMode) { + ModelScreenUiMode.BROWSING -> + ModelsBrowsingScreen( + onManageModelsClicked = { /* TODO-han.yin */ }, + viewModel = viewModel + ) + ModelScreenUiMode.SEARCHING -> + ModelsSearchingScreen(viewModel = viewModel) + ModelScreenUiMode.MANAGING, ModelScreenUiMode.DELETING -> + ModelsManagementAndDeletingScreen( + isDeleting = currentMode == ModelScreenUiMode.DELETING, + onScaffoldEvent = onScaffoldEvent, + viewModel = viewModel + ) + } + + // Show insufficient RAM warning + preselection?.let { + it.ramWarning?.let { warning -> + if (warning.showing) { + RamErrorDialog( + warning, + onDismiss = { viewModel.dismissRamWarning() }, + onConfirm = { onConfirmSelection(it.modelInfo, warning) } + ) + } + } + } + } +} + +@Composable +private fun RamErrorDialog( + ramError: RamWarning, + onDismiss: () -> Unit, + onConfirm: () -> Unit, +) { + val requiredRam = formatFileByteSize(ramError.requiredRam) + val availableRam = formatFileByteSize(ramError.availableRam) + + AlertDialog( + text = { + InfoView( + modifier = Modifier.fillMaxWidth(), + title = "Insufficient RAM", + icon = Icons.Default.Warning, + message = "You are trying to run a $requiredRam size model, " + + "but currently there's only $availableRam memory available!", + ) + }, + containerColor = MaterialTheme.colorScheme.errorContainer, + titleContentColor = MaterialTheme.colorScheme.onErrorContainer, + textContentColor = MaterialTheme.colorScheme.onErrorContainer, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + }, + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = onConfirm) { + Text( + text = "Proceed", + color = MaterialTheme.colorScheme.error + ) + } + } + ) +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsSearchingScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsSearchingScreen.kt new file mode 100644 index 0000000000..1f96058419 --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsSearchingScreen.kt @@ -0,0 +1,186 @@ +package com.example.llama.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.input.clearText +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.SearchOff +import androidx.compose.material3.Button +import androidx.compose.material3.DockedSearchBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.example.llama.ui.components.ModelCardFullExpandable +import com.example.llama.viewmodel.ModelScreenUiMode +import com.example.llama.viewmodel.ModelsViewModel + +@ExperimentalMaterial3Api +@Composable +fun ModelsSearchingScreen( + + viewModel: ModelsViewModel, +) { + val preselection by viewModel.preselectedModelToRun.collectAsState() + + // Query states + val textFieldState = viewModel.searchFieldState + val searchQuery by remember(textFieldState) { + derivedStateOf { textFieldState.text.toString() } + } + val queryResults by viewModel.queryResults.collectAsState() + + // Local UI states + val focusRequester = remember { FocusRequester() } + val keyboardController = LocalSoftwareKeyboardController.current + val toggleSearchFocusAndIme: (Boolean) -> Unit = { show -> + if (show) { + focusRequester.requestFocus() + keyboardController?.show() + } else { + focusRequester.freeFocus() + keyboardController?.hide() + } + } + + +// LaunchedEffect (isSearchActive) { +// if (isSearchActive) { +// toggleSearchFocusAndIme(true) +// } +// } + + val handleExpanded: (Boolean) -> Unit = { expanded -> + viewModel.toggleMode( + if (expanded) ModelScreenUiMode.SEARCHING + else ModelScreenUiMode.BROWSING + ) + textFieldState.clearText() + } + + Box(modifier = Modifier.fillMaxSize()) { + DockedSearchBar( + modifier = Modifier.align(Alignment.TopCenter), + inputField = { + SearchBarDefaults.InputField( + modifier = Modifier.focusRequester(focusRequester), + query = textFieldState.text.toString(), + onQueryChange = { textFieldState.edit { replace(0, length, it) } }, + onSearch = {}, + expanded = true, + onExpandedChange = handleExpanded, + leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, + trailingIcon = { Icon(Icons.Default.MoreVert, contentDescription = null) }, + placeholder = { Text("Type to search your models") } + ) + }, + expanded = true, + onExpandedChange = handleExpanded + ) { + if (queryResults.isEmpty()) { + if (searchQuery.isNotBlank()) { + // If no results under current query, show "no results" message + EmptySearchResultsView( + onClearSearch = { + textFieldState.clearText() + toggleSearchFocusAndIme(true) + } + ) + } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = PaddingValues(vertical = 12.dp, horizontal = 16.dp), + ) { + items(items = queryResults, key = { it.id }) { model -> + ModelCardFullExpandable( + model = model, + isSelected = if (model == preselection?.modelInfo) true else null, + onSelected = { selected -> + if (selected) { + toggleSearchFocusAndIme(false) + } else { + viewModel.resetPreselection() + toggleSearchFocusAndIme(true) + } + }, + isExpanded = model == preselection?.modelInfo, + onExpanded = { expanded -> + viewModel.preselectModel(model, expanded) + toggleSearchFocusAndIme(!expanded) + } + ) + } + } + } + } + } +} + +@Composable +private fun EmptySearchResultsView( + onClearSearch: () -> Unit +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + imageVector = Icons.Default.SearchOff, + contentDescription = null, + modifier = Modifier.size(64.dp), + tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "No matching models found", + style = MaterialTheme.typography.headlineSmall + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Try a different search term", + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Button(onClick = onClearSearch) { + Text("Clear Search") + } + } +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/viewmodel/ModelSelectionViewModel.kt b/examples/llama.android/app/src/main/java/com/example/llama/viewmodel/ModelSelectionViewModel.kt deleted file mode 100644 index 7c4bea1b49..0000000000 --- a/examples/llama.android/app/src/main/java/com/example/llama/viewmodel/ModelSelectionViewModel.kt +++ /dev/null @@ -1,241 +0,0 @@ -package com.example.llama.viewmodel - -import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.foundation.text.input.clearText -import androidx.compose.runtime.snapshotFlow -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import com.example.llama.data.model.ModelFilter -import com.example.llama.data.model.ModelInfo -import com.example.llama.data.model.ModelSortOrder -import com.example.llama.data.model.filterBy -import com.example.llama.data.model.queryBy -import com.example.llama.data.model.sortByOrder -import com.example.llama.data.repo.ModelRepository -import com.example.llama.engine.InferenceService -import com.example.llama.monitoring.PerformanceMonitor -import com.example.llama.viewmodel.Preselection.RamWarning -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import javax.inject.Inject - - -@OptIn(FlowPreview::class) -@HiltViewModel -class ModelSelectionViewModel @Inject constructor( - modelRepository: ModelRepository, - private val performanceMonitor: PerformanceMonitor, - private val inferenceService: InferenceService, -) : ViewModel() { - - // UI state: search mode - private val _isSearchActive = MutableStateFlow(false) - val isSearchActive = _isSearchActive.asStateFlow() - - fun toggleSearchState(active: Boolean) { - _isSearchActive.value = active - if (active) { - resetPreselection() - } else { - searchFieldState.clearText() - } - } - - val searchFieldState = TextFieldState() - - // UI state: sort menu - private val _sortOrder = MutableStateFlow(ModelSortOrder.LAST_USED) - val sortOrder = _sortOrder.asStateFlow() - - fun setSortOrder(order: ModelSortOrder) { - _sortOrder.value = order - } - - private val _showSortMenu = MutableStateFlow(false) - val showSortMenu = _showSortMenu.asStateFlow() - - fun toggleSortMenu(visible: Boolean) { - _showSortMenu.value = visible - } - - // UI state: filter menu - private val _activeFilters = MutableStateFlow>( - ModelFilter.ALL_FILTERS.associateWith { false } - ) - val activeFilters: StateFlow> = _activeFilters.asStateFlow() - - fun toggleFilter(filter: ModelFilter, enabled: Boolean) { - _activeFilters.update { current -> - current.toMutableMap().apply { - this[filter] = enabled - } - } - } - - fun clearFilters() { - _activeFilters.update { current -> - current.mapValues { false } - } - } - - private val _showFilterMenu = MutableStateFlow(false) - val showFilterMenu = _showFilterMenu.asStateFlow() - - fun toggleFilterMenu(visible: Boolean) { - _showFilterMenu.value = visible - } - - // Data: filtered & sorted models - private val _filteredModels = MutableStateFlow>(emptyList()) - val filteredModels = _filteredModels.asStateFlow() - - // Data: queried models - private val _queryResults = MutableStateFlow>(emptyList()) - val queryResults = _queryResults.asStateFlow() - - // Data: pre-selected model in expansion mode - private val _preselectedModel = MutableStateFlow(null) - private val _showRamWarning = MutableStateFlow(false) - val preselection = combine( - _preselectedModel, - performanceMonitor.monitorMemoryUsage(), - _showRamWarning, - ) { model, memory, show -> - if (model == null) { - null - } else { - if (memory.availableMem >= model.sizeInBytes + RAM_LOAD_MODEL_BUFFER_BYTES) { - Preselection(model, null) - } else { - Preselection(model, RamWarning(model.sizeInBytes, memory.availableMem, show)) - } - } - }.stateIn( - scope = viewModelScope, - started = SharingStarted.WhileSubscribed(SUBSCRIPTION_TIMEOUT_MS), - initialValue = null - ) - - init { - viewModelScope.launch { - combine( - modelRepository.getModels(), - _activeFilters, - _sortOrder, - ) { models, filters, sortOrder -> - models.filterBy(filters).sortByOrder(sortOrder) - }.collectLatest { - _filteredModels.value = it - } - } - - viewModelScope.launch { - combine( - modelRepository.getModels(), - snapshotFlow { searchFieldState.text }.debounce(QUERY_DEBOUNCE_TIMEOUT_MS) - ) { models, query -> - if (query.isBlank()) { - emptyList() - } else { - models.queryBy(query.toString()).sortedBy { it.dateLastUsed ?: it.dateAdded } - } - }.collectLatest { - _queryResults.value = it - } - } - } - - /** - * Pre-select a model to expand its details and show Run FAB - */ - fun preselectModel(modelInfo: ModelInfo, preselected: Boolean) { - _preselectedModel.value = if (preselected) modelInfo else null - _showRamWarning.value = false - } - - /** - * Reset preselected model to none (before navigating away) - */ - fun resetPreselection() { - _preselectedModel.value = null - _showRamWarning.value = false - } - - /** - * Select the currently pre-selected model - * - * @return True if RAM enough, otherwise False. - */ - fun selectModel(preselection: Preselection) = - when (preselection.ramWarning?.showing) { - null -> { - inferenceService.setCurrentModel(preselection.modelInfo) - true - } - false -> { - _showRamWarning.value = true - false - } - else -> false - } - - /** - * Dismiss the RAM warnings - */ - fun dismissRamWarning() { - _showRamWarning.value = false - } - - /** - * Acknowledge RAM warnings and confirm currently pre-selected model - * - * @return True if confirmed, otherwise False. - */ - fun confirmSelectedModel(modelInfo: ModelInfo, ramWarning: RamWarning): Boolean = - if (ramWarning.showing) { - inferenceService.setCurrentModel(modelInfo) - _showRamWarning.value = false - true - } else { - false - } - - /** - * Handle back press from both back button and top bar - */ - fun onBackPressed() { - if (_preselectedModel.value != null) { - resetPreselection() - } - } - - companion object { - private val TAG = ModelSelectionViewModel::class.java.simpleName - - private const val SUBSCRIPTION_TIMEOUT_MS = 5000L - private const val QUERY_DEBOUNCE_TIMEOUT_MS = 500L - - private const val RAM_LOAD_MODEL_BUFFER_BYTES = 300 * 1024 - } -} - -data class Preselection( - val modelInfo: ModelInfo, - val ramWarning: RamWarning?, -) { - data class RamWarning( - val requiredRam: Long, - val availableRam: Long, - val showing: Boolean, - ) -} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/viewmodel/ModelsManagementViewModel.kt b/examples/llama.android/app/src/main/java/com/example/llama/viewmodel/ModelsViewModel.kt similarity index 66% rename from examples/llama.android/app/src/main/java/com/example/llama/viewmodel/ModelsManagementViewModel.kt rename to examples/llama.android/app/src/main/java/com/example/llama/viewmodel/ModelsViewModel.kt index ff5090d784..b26f77c9b0 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/viewmodel/ModelsManagementViewModel.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/viewmodel/ModelsViewModel.kt @@ -9,33 +9,43 @@ import android.content.IntentFilter import android.llama.cpp.gguf.InvalidFileFormatException import android.net.Uri import android.util.Log +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.clearText +import androidx.compose.runtime.snapshotFlow import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.example.llama.data.model.ModelFilter import com.example.llama.data.model.ModelInfo import com.example.llama.data.model.ModelSortOrder import com.example.llama.data.model.filterBy +import com.example.llama.data.model.queryBy import com.example.llama.data.model.sortByOrder -import com.example.llama.data.source.remote.HuggingFaceDownloadInfo -import com.example.llama.data.source.remote.HuggingFaceModel import com.example.llama.data.repo.InsufficientStorageException import com.example.llama.data.repo.ModelRepository +import com.example.llama.data.source.remote.HuggingFaceDownloadInfo +import com.example.llama.data.source.remote.HuggingFaceModel +import com.example.llama.engine.InferenceService +import com.example.llama.monitoring.PerformanceMonitor import com.example.llama.util.formatFileByteSize import com.example.llama.util.getFileNameFromUri import com.example.llama.util.getFileSizeFromUri import com.example.llama.viewmodel.ModelManagementState.Deletion import com.example.llama.viewmodel.ModelManagementState.Download import com.example.llama.viewmodel.ModelManagementState.Importation +import com.example.llama.viewmodel.PreselectedModelToRun.RamWarning import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.io.FileNotFoundException @@ -43,70 +53,81 @@ import java.io.IOException import java.net.SocketTimeoutException import java.net.UnknownHostException import javax.inject.Inject +import kotlin.coroutines.cancellation.CancellationException + +@OptIn(FlowPreview::class) @HiltViewModel -class ModelsManagementViewModel @Inject constructor( +class ModelsViewModel @Inject constructor( @ApplicationContext private val context: Context, - private val modelRepository: ModelRepository + private val modelRepository: ModelRepository, + private val performanceMonitor: PerformanceMonitor, + private val inferenceService: InferenceService, ) : ViewModel() { - // Data: models - private val _filteredModels = MutableStateFlow>(emptyList()) - val filteredModels: StateFlow> = _filteredModels.asStateFlow() + // UI state: model management mode + private val _modelScreenUiMode = MutableStateFlow(ModelScreenUiMode.BROWSING) + val modelScreenUiMode = _modelScreenUiMode.asStateFlow() - // UI state: multi-selection mode - private val _isMultiSelectionMode = MutableStateFlow(false) - val isMultiSelectionMode: StateFlow = _isMultiSelectionMode.asStateFlow() - - fun toggleSelectionMode(enabled: Boolean) { - _isMultiSelectionMode.value = enabled - if (!enabled) { - toggleAllSelection(selectAll = false) - } - } - - // UI state: models selected in multi-selection - private val _selectedModels = MutableStateFlow>(emptyMap()) - val selectedModels: StateFlow> = _selectedModels.asStateFlow() - - fun toggleModelSelectionById(modelId: String) { - val current = _selectedModels.value.toMutableMap() - val model = _filteredModels.value.find { it.id == modelId } - - if (model != null) { - if (current.containsKey(modelId)) { - current.remove(modelId) - } else { - current[modelId] = model + fun toggleMode(newMode: ModelScreenUiMode): Boolean { + val oldMode = _modelScreenUiMode.value + when (oldMode) { + ModelScreenUiMode.BROWSING -> { + when (newMode) { + ModelScreenUiMode.SEARCHING -> { + resetPreselection() + } + ModelScreenUiMode.MANAGING -> { + resetPreselection() + } + ModelScreenUiMode.DELETING -> { return false } + else -> { /* No-op */ } + } + } + ModelScreenUiMode.SEARCHING -> { + when (newMode) { + ModelScreenUiMode.BROWSING -> { + searchFieldState.clearText() + } + else -> { return false } + } + } + ModelScreenUiMode.MANAGING -> { + when (newMode) { + ModelScreenUiMode.SEARCHING -> { return false } + else -> { /* No-op */ } + } + } + ModelScreenUiMode.DELETING -> { + when (newMode) { + ModelScreenUiMode.BROWSING, ModelScreenUiMode.SEARCHING -> { return false } + else -> { /* No-op */ } + } } - _selectedModels.value = current } + _modelScreenUiMode.value = newMode + return true } - fun toggleAllSelection(selectAll: Boolean) { - if (selectAll) { - _selectedModels.value = _filteredModels.value.associateBy { it.id } - } else { - _selectedModels.value = emptyMap() - } - } + // UI state: search mode + val searchFieldState = TextFieldState() // UI state: sort menu - private val _sortOrder = MutableStateFlow(ModelSortOrder.NAME_ASC) - val sortOrder: StateFlow = _sortOrder.asStateFlow() + private val _sortOrder = MutableStateFlow(ModelSortOrder.LAST_USED) + val sortOrder = _sortOrder.asStateFlow() fun setSortOrder(order: ModelSortOrder) { _sortOrder.value = order } private val _showSortMenu = MutableStateFlow(false) - val showSortMenu: StateFlow = _showSortMenu.asStateFlow() + val showSortMenu = _showSortMenu.asStateFlow() - fun toggleSortMenu(show: Boolean) { - _showSortMenu.value = show + fun toggleSortMenu(visible: Boolean) { + _showSortMenu.value = visible } - // UI state: filters + // UI state: filter menu private val _activeFilters = MutableStateFlow>( ModelFilter.ALL_FILTERS.associateWith { false } ) @@ -127,12 +148,69 @@ class ModelsManagementViewModel @Inject constructor( } private val _showFilterMenu = MutableStateFlow(false) - val showFilterMenu: StateFlow = _showFilterMenu.asStateFlow() + val showFilterMenu = _showFilterMenu.asStateFlow() fun toggleFilterMenu(visible: Boolean) { _showFilterMenu.value = visible } + // Data: filtered & sorted models + private val _filteredModels = MutableStateFlow>(emptyList()) + val filteredModels = _filteredModels.asStateFlow() + + // Data: queried models + private val _queryResults = MutableStateFlow>(emptyList()) + val queryResults = _queryResults.asStateFlow() + + // Data: pre-selected model in expansion mode + private val _preselectedModelToRun = MutableStateFlow(null) + private val _showRamWarning = MutableStateFlow(false) + val preselectedModelToRun = combine( + _preselectedModelToRun, + performanceMonitor.monitorMemoryUsage(), + _showRamWarning, + ) { model, memory, show -> + if (model == null) { + null + } else { + if (memory.availableMem >= model.sizeInBytes + RAM_LOAD_MODEL_BUFFER_BYTES) { + PreselectedModelToRun(model, null) + } else { + PreselectedModelToRun(model, RamWarning(model.sizeInBytes, memory.availableMem, show)) + } + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(SUBSCRIPTION_TIMEOUT_MS), + initialValue = null + ) + + // UI state: models selected in deleting mode + private val _selectedModelsToDelete = MutableStateFlow>(emptyMap()) + val selectedModelsToDelete: StateFlow> = _selectedModelsToDelete.asStateFlow() + + fun toggleModelSelectionById(modelId: String) { + val current = _selectedModelsToDelete.value.toMutableMap() + val model = _filteredModels.value.find { it.id == modelId } + + if (model != null) { + if (current.containsKey(modelId)) { + current.remove(modelId) + } else { + current[modelId] = model + } + _selectedModelsToDelete.value = current + } + } + + fun toggleAllSelectedModelsToDelete(selectAll: Boolean) { + if (selectAll) { + _selectedModelsToDelete.value = _filteredModels.value.associateBy { it.id } + } else { + _selectedModelsToDelete.value = emptyMap() + } + } + // UI state: import menu private val _showImportModelMenu = MutableStateFlow(false) val showImportModelMenu: StateFlow = _showImportModelMenu.asStateFlow() @@ -156,6 +234,17 @@ class ModelsManagementViewModel @Inject constructor( } } + // Internal state + private val _managementState = MutableStateFlow(ModelManagementState.Idle) + val managementState: StateFlow = _managementState.asStateFlow() + + fun resetManagementState() { + huggingFaceQueryJob?.let { + if (it.isActive) { it.cancel() } + } + _managementState.value = ModelManagementState.Idle + } + init { viewModelScope.launch { combine( @@ -169,21 +258,83 @@ class ModelsManagementViewModel @Inject constructor( } } + viewModelScope.launch { + combine( + modelRepository.getModels(), + snapshotFlow { searchFieldState.text }.debounce(QUERY_DEBOUNCE_TIMEOUT_MS) + ) { models, query -> + if (query.isBlank()) { + emptyList() + } else { + models.queryBy(query.toString()).sortedBy { it.dateLastUsed ?: it.dateAdded } + } + }.collectLatest { + _queryResults.value = it + } + } + val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE) context.registerReceiver(downloadReceiver, filter, RECEIVER_EXPORTED) } - // Internal state - private val _managementState = MutableStateFlow(ModelManagementState.Idle) - val managementState: StateFlow = _managementState.asStateFlow() - - fun resetManagementState() { - huggingFaceQueryJob?.let { - if (it.isActive) { it.cancel() } - } - _managementState.value = ModelManagementState.Idle + /** + * Pre-select a model to expand its details and show Run FAB + */ + fun preselectModel(modelInfo: ModelInfo, preselected: Boolean) { + _preselectedModelToRun.value = if (preselected) modelInfo else null + _showRamWarning.value = false } + /** + * Reset preselected model to none (before navigating away) + */ + fun resetPreselection() { + _preselectedModelToRun.value = null + _showRamWarning.value = false + } + + /** + * Select the currently pre-selected model + * + * @return True if RAM enough, otherwise False. + */ + fun selectModel(preselectedModelToRun: PreselectedModelToRun) = + when (preselectedModelToRun.ramWarning?.showing) { + null -> { + inferenceService.setCurrentModel(preselectedModelToRun.modelInfo) + true + } + false -> { + _showRamWarning.value = true + false + } + else -> false + } + + /** + * Dismiss the RAM warnings + */ + fun dismissRamWarning() { + _showRamWarning.value = false + } + + /** + * Acknowledge RAM warnings and confirm currently pre-selected model + * + * @return True if confirmed, otherwise False. + */ + fun confirmSelectedModel(modelInfo: ModelInfo, ramWarning: RamWarning): Boolean = + if (ramWarning.showing) { + inferenceService.setCurrentModel(modelInfo) + _showRamWarning.value = false + + resetPreselection() + + true + } else { + false + } + /** * First show confirmation instead of starting import local file immediately */ @@ -199,6 +350,7 @@ class ModelsManagementViewModel @Inject constructor( } } + /** * Import a local model file from device storage while updating UI states with realtime progress */ @@ -359,9 +511,10 @@ class ModelsManagementViewModel @Inject constructor( _managementState.value = Deletion.Deleting(deleted.toFloat() / total, models) } _managementState.value = Deletion.Success(models.values.toList()) + toggleAllSelectedModelsToDelete(false) // Reset state after a delay - delay(SUCCESS_RESET_TIMEOUT_MS) + delay(DELETE_SUCCESS_RESET_TIMEOUT_MS) _managementState.value = ModelManagementState.Idle } catch (e: Exception) { _managementState.value = Deletion.Error( @@ -371,12 +524,35 @@ class ModelsManagementViewModel @Inject constructor( } companion object { - private val TAG = ModelsManagementViewModel::class.java.simpleName + private val TAG = ModelsViewModel::class.java.simpleName - private const val SUCCESS_RESET_TIMEOUT_MS = 1000L + private const val SUBSCRIPTION_TIMEOUT_MS = 5000L + private const val QUERY_DEBOUNCE_TIMEOUT_MS = 500L + + private const val DELETE_SUCCESS_RESET_TIMEOUT_MS = 1000L + + private const val RAM_LOAD_MODEL_BUFFER_BYTES = 300 * 1024 } } +enum class ModelScreenUiMode { + BROWSING, + SEARCHING, + MANAGING, + DELETING +} + +data class PreselectedModelToRun( + val modelInfo: ModelInfo, + val ramWarning: RamWarning?, +) { + data class RamWarning( + val requiredRam: Long, + val availableRam: Long, + val showing: Boolean, + ) +} + sealed class ModelManagementState { object Idle : ModelManagementState()