From cf306db85517649210f313f7bb208958f106430d Mon Sep 17 00:00:00 2001 From: Han Yin Date: Sat, 30 Aug 2025 20:00:44 -0700 Subject: [PATCH] UI: polish the bottom bars and info view when no models found; show loading in progress while fetching models --- .../java/com/example/llama/MainActivity.kt | 21 ++++---- .../example/llama/ui/components/InfoView.kt | 4 +- .../example/llama/ui/scaffold/AppScaffold.kt | 3 +- .../ui/scaffold/bottombar/BottomBarConfig.kt | 9 ++-- .../bottombar/ModelsBrowsingBottomBar.kt | 28 +++++++--- .../bottombar/ModelsManagementBottomBar.kt | 27 ++++++---- .../llama/ui/screens/ModelsBrowsingScreen.kt | 51 ++++++++++--------- .../ModelsManagementAndDeletingScreen.kt | 11 ++-- .../example/llama/ui/screens/ModelsScreen.kt | 1 - 9 files changed, 93 insertions(+), 62 deletions(-) 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 6a2dbb33cc..b4242884e8 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 @@ -192,6 +192,7 @@ fun AppContent( // Model selection screen currentRoute == AppDestinations.MODELS_ROUTE -> { // Collect states for bottom bar + val allModels by modelsViewModel.allModels.collectAsState() val filteredModels by modelsViewModel.filteredModels.collectAsState() val sortOrder by modelsViewModel.sortOrder.collectAsState() val showSortMenu by modelsViewModel.showSortMenu.collectAsState() @@ -202,6 +203,8 @@ fun AppContent( val selectedModelsToDelete by modelsManagementViewModel.selectedModelsToDelete.collectAsState() val showImportModelMenu by modelsManagementViewModel.showImportModelMenu.collectAsState() + val hasModelsInstalled = allModels?.isNotEmpty() == true + // Create file launcher for importing local models val fileLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.OpenDocument() @@ -242,10 +245,12 @@ fun AppContent( when (modelScreenUiMode) { ModelScreenUiMode.BROWSING -> BottomBarConfig.Models.Browsing( + isSearchingEnabled = hasModelsInstalled, onToggleSearching = { modelsViewModel.toggleMode(ModelScreenUiMode.SEARCHING) }, sorting = BottomBarConfig.Models.Browsing.SortingConfig( + isEnabled = hasModelsInstalled, currentOrder = sortOrder, isMenuVisible = showSortMenu, toggleMenu = modelsViewModel::toggleSortMenu, @@ -255,7 +260,7 @@ fun AppContent( } ), filtering = BottomBarConfig.Models.Browsing.FilteringConfig( - isActive = activeFilters.any { it.value }, + isEnabled = hasModelsInstalled, filters = activeFilters, onToggleFilter = modelsViewModel::toggleFilter, onClearFilters = modelsViewModel::clearFilters, @@ -292,12 +297,13 @@ fun AppContent( ) ModelScreenUiMode.MANAGING -> - BottomBarConfig.Models.Management( + BottomBarConfig.Models.Managing( isDeletionEnabled = filteredModels?.isNotEmpty() == true, onToggleDeleting = { modelsViewModel.toggleMode(ModelScreenUiMode.DELETING) }, - sorting = BottomBarConfig.Models.Management.SortingConfig( + sorting = BottomBarConfig.Models.Managing.SortingConfig( + isEnabled = hasModelsInstalled, currentOrder = sortOrder, isMenuVisible = showSortMenu, toggleMenu = { modelsViewModel.toggleSortMenu(it) }, @@ -306,15 +312,15 @@ fun AppContent( modelsViewModel.toggleSortMenu(false) } ), - filtering = BottomBarConfig.Models.Management.FilteringConfig( - isActive = activeFilters.any { it.value }, + filtering = BottomBarConfig.Models.Managing.FilteringConfig( + isEnabled = hasModelsInstalled, filters = activeFilters, onToggleFilter = modelsViewModel::toggleFilter, onClearFilters = modelsViewModel::clearFilters, isMenuVisible = showFilterMenu, toggleMenu = modelsViewModel::toggleFilterMenu ), - importing = BottomBarConfig.Models.Management.ImportConfig( + importing = BottomBarConfig.Models.Managing.ImportConfig( isMenuVisible = showImportModelMenu, toggleMenu = { show -> modelsManagementViewModel.toggleImportMenu(show) }, importFromLocal = { @@ -466,9 +472,6 @@ fun AppContent( // Model Selection Screen composable(AppDestinations.MODELS_ROUTE) { ModelsScreen( - onManageModelsClicked = { - // TODO-han.yin: remove this after implementing onboarding flow - }, onConfirmSelection = { modelInfo, ramWarning -> if (modelsViewModel.confirmSelectedModel(modelInfo, ramWarning)) { navigationActions.navigateToModelLoading() diff --git a/examples/llama.android/app/src/main/java/com/example/llama/ui/components/InfoView.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/components/InfoView.kt index 0a1eb166b0..5d72a877dc 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/components/InfoView.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/components/InfoView.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -68,7 +69,8 @@ fun InfoView( Text( text = title, style = MaterialTheme.typography.headlineSmall, - textAlign = TextAlign.Center + textAlign = TextAlign.Center, + fontWeight = FontWeight.SemiBold ) message?.let { 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 888b57de24..0c26e90c6e 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 @@ -107,6 +107,7 @@ fun AppScaffold( is BottomBarConfig.Models.Browsing -> { ModelsBrowsingBottomBar( + isSearchingEnabled = config.isSearchingEnabled, onToggleSearching = config.onToggleSearching, sortingConfig = config.sorting, filteringConfig = config.filtering, @@ -123,7 +124,7 @@ fun AppScaffold( ) } - is BottomBarConfig.Models.Management -> { + is BottomBarConfig.Models.Managing -> { ModelsManagementBottomBar( isDeletionEnabled = config.isDeletionEnabled, onToggleDeleting = config.onToggleDeleting, 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 d34f5411c9..b176799038 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 @@ -16,12 +16,14 @@ sealed class BottomBarConfig { sealed class Models : BottomBarConfig() { data class Browsing( + val isSearchingEnabled: Boolean, val onToggleSearching: () -> Unit, val sorting: SortingConfig, val filtering: FilteringConfig, val runAction: RunActionConfig, ) : BottomBarConfig() { data class SortingConfig( + val isEnabled: Boolean, val currentOrder: ModelSortOrder, val isMenuVisible: Boolean, val toggleMenu: (Boolean) -> Unit, @@ -29,7 +31,7 @@ sealed class BottomBarConfig { ) data class FilteringConfig( - val isActive: Boolean, + val isEnabled: Boolean, val filters: Map, val onToggleFilter: (ModelFilter, Boolean) -> Unit, val onClearFilters: () -> Unit, @@ -45,7 +47,7 @@ sealed class BottomBarConfig { val runAction: RunActionConfig, ) : BottomBarConfig() - data class Management( + data class Managing( val isDeletionEnabled: Boolean, val onToggleDeleting: () -> Unit, val sorting: SortingConfig, @@ -53,6 +55,7 @@ sealed class BottomBarConfig { val importing: ImportConfig, ) : BottomBarConfig() { data class SortingConfig( + val isEnabled: Boolean, val currentOrder: ModelSortOrder, val isMenuVisible: Boolean, val toggleMenu: (Boolean) -> Unit, @@ -60,7 +63,7 @@ sealed class BottomBarConfig { ) data class FilteringConfig( - val isActive: Boolean, + val isEnabled: Boolean, val filters: Map, val onToggleFilter: (ModelFilter, Boolean) -> Unit, val onClearFilters: () -> Unit, 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 index daed872042..949c9ab766 100644 --- 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 @@ -22,15 +22,18 @@ import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.test.isEnabled import androidx.compose.ui.unit.dp import com.example.llama.data.model.ModelSortOrder @Composable fun ModelsBrowsingBottomBar( + isSearchingEnabled: Boolean, onToggleSearching: () -> Unit, sortingConfig: BottomBarConfig.Models.Browsing.SortingConfig, filteringConfig: BottomBarConfig.Models.Browsing.FilteringConfig, @@ -39,7 +42,10 @@ fun ModelsBrowsingBottomBar( BottomAppBar( actions = { // Enter search action - IconButton(onClick = onToggleSearching) { + IconButton( + enabled = isSearchingEnabled, + onClick = onToggleSearching + ) { Icon( imageVector = Icons.Default.Search, contentDescription = "Search models" @@ -47,7 +53,10 @@ fun ModelsBrowsingBottomBar( } // Sorting action - IconButton(onClick = { sortingConfig.toggleMenu(true) }) { + IconButton( + enabled = sortingConfig.isEnabled, + onClick = { sortingConfig.toggleMenu(true) } + ) { Icon( imageVector = Icons.AutoMirrored.Filled.Sort, contentDescription = "Sort models" @@ -99,15 +108,20 @@ fun ModelsBrowsingBottomBar( } // Filter action - IconButton(onClick = { filteringConfig.toggleMenu(true) }) { + val hasFilters = filteringConfig.filters.any { it.value } + IconButton( + enabled = filteringConfig.isEnabled, + colors = IconButtonDefaults.iconButtonColors().copy( + contentColor = if (hasFilters) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant + ), + onClick = { filteringConfig.toggleMenu(true) } + ) { Icon( imageVector = - if (filteringConfig.isActive) Icons.Default.FilterAlt + if (hasFilters) Icons.Default.FilterAlt else Icons.Outlined.FilterAlt, contentDescription = "Filter models", - tint = - if (filteringConfig.isActive) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.onSurfaceVariant ) } 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 cb97d3e92a..44c4da521c 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 @@ -19,6 +19,7 @@ import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -33,9 +34,9 @@ import com.example.llama.data.model.ModelSortOrder fun ModelsManagementBottomBar( isDeletionEnabled: Boolean, onToggleDeleting: () -> Unit, - sortingConfig: BottomBarConfig.Models.Management.SortingConfig, - filteringConfig: BottomBarConfig.Models.Management.FilteringConfig, - importingConfig: BottomBarConfig.Models.Management.ImportConfig, + sortingConfig: BottomBarConfig.Models.Managing.SortingConfig, + filteringConfig: BottomBarConfig.Models.Managing.FilteringConfig, + importingConfig: BottomBarConfig.Models.Managing.ImportConfig, ) { BottomAppBar( actions = { @@ -47,7 +48,10 @@ fun ModelsManagementBottomBar( } // Sorting action - IconButton(onClick = { sortingConfig.toggleMenu(true) }) { + IconButton( + enabled = sortingConfig.isEnabled, + onClick = { sortingConfig.toggleMenu(true) } + ) { Icon( imageVector = Icons.AutoMirrored.Filled.Sort, contentDescription = "Sort models" @@ -99,15 +103,20 @@ fun ModelsManagementBottomBar( } // Filtering action - IconButton(onClick = { filteringConfig.toggleMenu(true) }) { + val hasFilters = filteringConfig.filters.any { it.value } + IconButton( + enabled = filteringConfig.isEnabled, + onClick = { filteringConfig.toggleMenu(true) }, + colors = IconButtonDefaults.iconButtonColors().copy( + contentColor = if (hasFilters) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant + ), + ) { Icon( imageVector = - if (filteringConfig.isActive) Icons.Default.FilterAlt + if (hasFilters) Icons.Default.FilterAlt else Icons.Outlined.FilterAlt, contentDescription = "Filter models", - tint = - if (filteringConfig.isActive) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.onSurfaceVariant ) } 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 index ee484a2cec..55cd1ef4e7 100644 --- 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 @@ -1,14 +1,16 @@ package com.example.llama.ui.screens import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box 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.automirrored.filled.ArrowForward import androidx.compose.material.icons.filled.FolderOpen 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.data.model.ModelInfo @@ -30,7 +32,29 @@ fun ModelsBrowsingScreen( ModelsLoadingInProgressView() } else if (filteredModels.isEmpty()) { // Empty model prompt - EmptyModelsView(activeFiltersCount, onManageModelsClicked) + val title = when (activeFiltersCount) { + 0 -> "No models installed yet" + 1 -> "No models match your filter" + else -> "No models match your filters" + } + val message = when (activeFiltersCount) { + 0 -> "Tap the button below to install your first Large Language Model!" + 1 -> "Try removing your filter to see more results" + else -> "Try removing some filters to see more results" + } + Box(modifier = Modifier.fillMaxSize()) { + InfoView( + modifier = Modifier.fillMaxSize(0.9f).align(Alignment.Center), + title = title, + icon = Icons.Default.FolderOpen, + message = message, + action = InfoAction( + label = "Get Started", + icon = Icons.AutoMirrored.Default.ArrowForward, + onAction = onManageModelsClicked + ) + ) + } } else { // Model cards LazyColumn( @@ -54,26 +78,3 @@ fun ModelsBrowsingScreen( } } } - -@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/ModelsManagementAndDeletingScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsManagementAndDeletingScreen.kt index 0c6a5bb316..61897a6abd 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsManagementAndDeletingScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/ui/screens/ModelsManagementAndDeletingScreen.kt @@ -56,7 +56,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import androidx.core.net.toUri -import com.example.llama.data.model.ModelFilter import com.example.llama.data.model.ModelInfo import com.example.llama.data.source.remote.HuggingFaceModel import com.example.llama.ui.components.InfoAction @@ -110,18 +109,18 @@ fun ModelsManagementAndDeletingScreen( ModelsLoadingInProgressView() } else if (filteredModels.isEmpty()) { // Import model prompt - val message = when (activeFiltersCount) { - 0 -> "Tap the \"+\" button\n to import a model" + val title = when (activeFiltersCount) { + 0 -> "Tap the \"+\" button\n to install a model" 1 -> "No models match\n the selected filter" else -> "No models match\n the selected filters" } InfoView( - modifier = Modifier.fillMaxSize(0.8f).align(Alignment.Center), - title = message, + modifier = Modifier.fillMaxSize(0.9f).align(Alignment.Center), + title = title, icon = Icons.Default.FolderOpen, message = "Import a local GGUF model file, or download directly from HuggingFace!", action = InfoAction( - label = "Learn more", + label = "Learn More", icon = Icons.AutoMirrored.Default.Help, onAction = { val url = "https://huggingface.co/docs/hub/en/gguf" 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 index abdce9fbe2..1c4484384b 100644 --- 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 @@ -33,7 +33,6 @@ import com.example.llama.viewmodel.PreselectedModelToRun.RamWarning @OptIn(ExperimentalMaterial3Api::class) @Composable fun ModelsScreen( - onManageModelsClicked: () -> Unit, onConfirmSelection: (ModelInfo, RamWarning) -> Unit, onScaffoldEvent: (ScaffoldEvent) -> Unit, modelsViewModel: ModelsViewModel,