UI: merge the Model Selection and Model Management into a unified Models screen
This commit is contained in:
parent
29f263440f
commit
df16abe75e
|
|
@ -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.BenchmarkScreen
|
||||||
import com.example.llama.ui.screens.ConversationScreen
|
import com.example.llama.ui.screens.ConversationScreen
|
||||||
import com.example.llama.ui.screens.ModelLoadingScreen
|
import com.example.llama.ui.screens.ModelLoadingScreen
|
||||||
import com.example.llama.ui.screens.ModelSelectionScreen
|
import com.example.llama.ui.screens.ModelsScreen
|
||||||
import com.example.llama.ui.screens.ModelsManagementScreen
|
|
||||||
import com.example.llama.ui.screens.SettingsGeneralScreen
|
import com.example.llama.ui.screens.SettingsGeneralScreen
|
||||||
import com.example.llama.ui.theme.LlamaTheme
|
import com.example.llama.ui.theme.LlamaTheme
|
||||||
import com.example.llama.viewmodel.BenchmarkViewModel
|
import com.example.llama.viewmodel.BenchmarkViewModel
|
||||||
import com.example.llama.viewmodel.ConversationViewModel
|
import com.example.llama.viewmodel.ConversationViewModel
|
||||||
import com.example.llama.viewmodel.MainViewModel
|
import com.example.llama.viewmodel.MainViewModel
|
||||||
import com.example.llama.viewmodel.ModelLoadingViewModel
|
import com.example.llama.viewmodel.ModelLoadingViewModel
|
||||||
import com.example.llama.viewmodel.ModelSelectionViewModel
|
import com.example.llama.viewmodel.ModelsViewModel
|
||||||
import com.example.llama.viewmodel.ModelsManagementViewModel
|
|
||||||
import com.example.llama.viewmodel.SettingsViewModel
|
import com.example.llama.viewmodel.SettingsViewModel
|
||||||
|
import com.example.llama.viewmodel.ModelScreenUiMode
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
|
@ -85,11 +84,11 @@ class MainActivity : ComponentActivity() {
|
||||||
fun AppContent(
|
fun AppContent(
|
||||||
settingsViewModel: SettingsViewModel,
|
settingsViewModel: SettingsViewModel,
|
||||||
mainViewModel: MainViewModel = hiltViewModel(),
|
mainViewModel: MainViewModel = hiltViewModel(),
|
||||||
modelSelectionViewModel: ModelSelectionViewModel = hiltViewModel(),
|
modelsViewModel: ModelsViewModel = hiltViewModel(),
|
||||||
modelLoadingViewModel: ModelLoadingViewModel = hiltViewModel(),
|
modelLoadingViewModel: ModelLoadingViewModel = hiltViewModel(),
|
||||||
benchmarkViewModel: BenchmarkViewModel = hiltViewModel(),
|
benchmarkViewModel: BenchmarkViewModel = hiltViewModel(),
|
||||||
conversationViewModel: ConversationViewModel = hiltViewModel(),
|
conversationViewModel: ConversationViewModel = hiltViewModel(),
|
||||||
modelsManagementViewModel: ModelsManagementViewModel = hiltViewModel(),
|
// modelsManagementViewModel: ModelsManagementViewModel = hiltViewModel(),
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
@ -97,6 +96,9 @@ fun AppContent(
|
||||||
// Inference engine state
|
// Inference engine state
|
||||||
val engineState by mainViewModel.engineState.collectAsState()
|
val engineState by mainViewModel.engineState.collectAsState()
|
||||||
|
|
||||||
|
// Model state
|
||||||
|
val modelScreenUiMode by modelsViewModel.modelScreenUiMode.collectAsState()
|
||||||
|
|
||||||
// Metric states for scaffolds
|
// Metric states for scaffolds
|
||||||
val isMonitoringEnabled by settingsViewModel.isMonitoringEnabled.collectAsState()
|
val isMonitoringEnabled by settingsViewModel.isMonitoringEnabled.collectAsState()
|
||||||
val memoryUsage by settingsViewModel.memoryUsage.collectAsState()
|
val memoryUsage by settingsViewModel.memoryUsage.collectAsState()
|
||||||
|
|
@ -187,60 +189,157 @@ fun AppContent(
|
||||||
// Create scaffold's top & bottom bar configs based on current route
|
// Create scaffold's top & bottom bar configs based on current route
|
||||||
val scaffoldConfig = when {
|
val scaffoldConfig = when {
|
||||||
// Model selection screen
|
// Model selection screen
|
||||||
currentRoute == AppDestinations.MODEL_SELECTION_ROUTE -> {
|
currentRoute == AppDestinations.MODELS_ROUTE -> {
|
||||||
// Collect states for bottom bar
|
// Collect states for bottom bar
|
||||||
val isSearchActive by modelSelectionViewModel.isSearchActive.collectAsState()
|
val sortOrder by modelsViewModel.sortOrder.collectAsState()
|
||||||
val sortOrder by modelSelectionViewModel.sortOrder.collectAsState()
|
val showSortMenu by modelsViewModel.showSortMenu.collectAsState()
|
||||||
val showSortMenu by modelSelectionViewModel.showSortMenu.collectAsState()
|
val activeFilters by modelsViewModel.activeFilters.collectAsState()
|
||||||
val activeFilters by modelSelectionViewModel.activeFilters.collectAsState()
|
val showFilterMenu by modelsViewModel.showFilterMenu.collectAsState()
|
||||||
val showFilterMenu by modelSelectionViewModel.showFilterMenu.collectAsState()
|
val preselection by modelsViewModel.preselectedModelToRun.collectAsState()
|
||||||
val preselection by modelSelectionViewModel.preselection.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(
|
ScaffoldConfig(
|
||||||
topBarConfig =
|
topBarConfig =
|
||||||
if (isSearchActive) TopBarConfig.None()
|
when (modelScreenUiMode) {
|
||||||
else TopBarConfig.Default(
|
ModelScreenUiMode.BROWSING ->
|
||||||
title = "Pick your model",
|
TopBarConfig.ModelsBrowsing(
|
||||||
navigationIcon = NavigationIcon.Menu {
|
title = "Installed models",
|
||||||
modelSelectionViewModel.resetPreselection()
|
navigationIcon = NavigationIcon.Menu {
|
||||||
openDrawer()
|
modelsViewModel.resetPreselection()
|
||||||
}
|
openDrawer()
|
||||||
),
|
},
|
||||||
bottomBarConfig = BottomBarConfig.ModelSelection(
|
onToggleMode = modelsViewModel::toggleMode,
|
||||||
search = BottomBarConfig.ModelSelection.SearchConfig(
|
)
|
||||||
isActive = isSearchActive,
|
ModelScreenUiMode.SEARCHING ->
|
||||||
onToggleSearch = modelSelectionViewModel::toggleSearchState,
|
TopBarConfig.None()
|
||||||
textFieldState = modelSelectionViewModel.searchFieldState,
|
ModelScreenUiMode.MANAGING ->
|
||||||
onSearch = { /* No-op for now */ }
|
TopBarConfig.ModelsManagement(
|
||||||
),
|
title = "Managing models",
|
||||||
sorting = BottomBarConfig.ModelSelection.SortingConfig(
|
navigationIcon = NavigationIcon.Back {
|
||||||
currentOrder = sortOrder,
|
modelsViewModel.toggleMode(ModelScreenUiMode.BROWSING)
|
||||||
isMenuVisible = showSortMenu,
|
},
|
||||||
toggleMenu = modelSelectionViewModel::toggleSortMenu,
|
storageMetrics = if (isMonitoringEnabled) storageMetrics else null,
|
||||||
selectOrder = {
|
)
|
||||||
modelSelectionViewModel.setSortOrder(it)
|
ModelScreenUiMode.DELETING ->
|
||||||
modelSelectionViewModel.toggleSortMenu(false)
|
TopBarConfig.ModelsDeleting(
|
||||||
}
|
title = "Deleting models",
|
||||||
),
|
navigationIcon = NavigationIcon.Quit {
|
||||||
filtering = BottomBarConfig.ModelSelection.FilteringConfig(
|
modelsViewModel.toggleMode(ModelScreenUiMode.MANAGING)
|
||||||
isActive = activeFilters.any { it.value },
|
},
|
||||||
filters = activeFilters,
|
)
|
||||||
onToggleFilter = modelSelectionViewModel::toggleFilter,
|
},
|
||||||
onClearFilters = modelSelectionViewModel::clearFilters,
|
bottomBarConfig =
|
||||||
isMenuVisible = showFilterMenu,
|
when (modelScreenUiMode) {
|
||||||
toggleMenu = modelSelectionViewModel::toggleFilterMenu
|
ModelScreenUiMode.BROWSING ->
|
||||||
),
|
BottomBarConfig.Models.Browsing(
|
||||||
runAction = BottomBarConfig.ModelSelection.RunActionConfig(
|
onToggleSearching = {
|
||||||
preselection = preselection,
|
modelsViewModel.toggleMode(ModelScreenUiMode.SEARCHING)
|
||||||
onClickRun = {
|
},
|
||||||
if (modelSelectionViewModel.selectModel(it)) {
|
sorting = BottomBarConfig.Models.Browsing.SortingConfig(
|
||||||
modelSelectionViewModel.toggleSearchState(false)
|
currentOrder = sortOrder,
|
||||||
modelSelectionViewModel.resetPreselection()
|
isMenuVisible = showSortMenu,
|
||||||
navigationActions.navigateToModelLoading()
|
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
|
// Fallback for empty screen or unknown routes
|
||||||
else -> ScaffoldConfig(
|
else -> ScaffoldConfig(
|
||||||
topBarConfig = TopBarConfig.Default(title = "", navigationIcon = NavigationIcon.None)
|
topBarConfig = TopBarConfig.Default(title = "", navigationIcon = NavigationIcon.None)
|
||||||
|
|
@ -419,22 +449,22 @@ fun AppContent(
|
||||||
// AnimatedNavHost inside the scaffold content
|
// AnimatedNavHost inside the scaffold content
|
||||||
AnimatedNavHost(
|
AnimatedNavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = AppDestinations.MODEL_SELECTION_ROUTE,
|
startDestination = AppDestinations.MODELS_ROUTE,
|
||||||
modifier = Modifier.padding(paddingValues)
|
modifier = Modifier.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
// Model Selection Screen
|
// Model Selection Screen
|
||||||
composable(AppDestinations.MODEL_SELECTION_ROUTE) {
|
composable(AppDestinations.MODELS_ROUTE) {
|
||||||
ModelSelectionScreen(
|
ModelsScreen(
|
||||||
onManageModelsClicked = {
|
onManageModelsClicked = {
|
||||||
navigationActions.navigateToModelsManagement()
|
// TODO-han.yin: remove this after implementing onboarding flow
|
||||||
},
|
},
|
||||||
onConfirmSelection = { modelInfo, ramWarning ->
|
onConfirmSelection = { modelInfo, ramWarning ->
|
||||||
if (modelSelectionViewModel.confirmSelectedModel(modelInfo, ramWarning)) {
|
if (modelsViewModel.confirmSelectedModel(modelInfo, ramWarning)) {
|
||||||
navigationActions.navigateToModelLoading()
|
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
|
// General Settings Screen
|
||||||
composable(AppDestinations.SETTINGS_GENERAL_ROUTE) {
|
composable(AppDestinations.SETTINGS_GENERAL_ROUTE) {
|
||||||
SettingsGeneralScreen(
|
SettingsGeneralScreen(
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import com.example.llama.engine.ModelLoadingMetrics
|
||||||
*/
|
*/
|
||||||
object AppDestinations {
|
object AppDestinations {
|
||||||
// Primary navigation destinations
|
// Primary navigation destinations
|
||||||
const val MODEL_SELECTION_ROUTE = "model_selection"
|
const val MODELS_ROUTE = "models"
|
||||||
const val MODEL_LOADING_ROUTE = "model_loading"
|
const val MODEL_LOADING_ROUTE = "model_loading"
|
||||||
|
|
||||||
const val CONVERSATION_ROUTE = "conversation"
|
const val CONVERSATION_ROUTE = "conversation"
|
||||||
|
|
@ -19,7 +19,6 @@ object AppDestinations {
|
||||||
|
|
||||||
// Settings destinations
|
// Settings destinations
|
||||||
const val SETTINGS_GENERAL_ROUTE = "settings_general"
|
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) {
|
class NavigationActions(private val navController: NavController) {
|
||||||
|
|
||||||
fun navigateToModelSelection() {
|
fun navigateToModelSelection() {
|
||||||
navController.navigate(AppDestinations.MODEL_SELECTION_ROUTE) {
|
navController.navigate(AppDestinations.MODELS_ROUTE) {
|
||||||
// Clear back stack to start fresh
|
// 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)
|
navController.navigate(AppDestinations.SETTINGS_GENERAL_ROUTE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun navigateToModelsManagement() {
|
|
||||||
navController.navigate(AppDestinations.MODELS_MANAGEMENT_ROUTE)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun navigateUp() {
|
fun navigateUp() {
|
||||||
navController.navigateUp()
|
navController.navigateUp()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
|
@ -149,7 +148,6 @@ fun ModelCardCoreExpandable(
|
||||||
* @param isExpanded Whether additional details is expanded or shrunk
|
* @param isExpanded Whether additional details is expanded or shrunk
|
||||||
* @param onExpanded Action to perform when the card is expanded or shrunk
|
* @param onExpanded Action to perform when the card is expanded or shrunk
|
||||||
*/
|
*/
|
||||||
@OptIn(ExperimentalLayoutApi::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ModelCardFullExpandable(
|
fun ModelCardFullExpandable(
|
||||||
model: ModelInfo,
|
model: ModelInfo,
|
||||||
|
|
@ -158,10 +156,6 @@ fun ModelCardFullExpandable(
|
||||||
isExpanded: Boolean = false,
|
isExpanded: Boolean = false,
|
||||||
onExpanded: ((Boolean) -> Unit)? = null,
|
onExpanded: ((Boolean) -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(model) {
|
|
||||||
android.util.Log.w("JOJO", model.languages.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) {
|
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides Dp.Unspecified) {
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,12 @@ import androidx.compose.runtime.remember
|
||||||
import com.example.llama.ui.scaffold.bottombar.BenchmarkBottomBar
|
import com.example.llama.ui.scaffold.bottombar.BenchmarkBottomBar
|
||||||
import com.example.llama.ui.scaffold.bottombar.BottomBarConfig
|
import com.example.llama.ui.scaffold.bottombar.BottomBarConfig
|
||||||
import com.example.llama.ui.scaffold.bottombar.ConversationBottomBar
|
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.ModelsManagementBottomBar
|
||||||
|
import com.example.llama.ui.scaffold.bottombar.ModelsSearchingBottomBar
|
||||||
import com.example.llama.ui.scaffold.topbar.DefaultTopBar
|
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.NavigationIcon
|
||||||
import com.example.llama.ui.scaffold.topbar.PerformanceTopBar
|
import com.example.llama.ui.scaffold.topbar.PerformanceTopBar
|
||||||
import com.example.llama.ui.scaffold.topbar.StorageTopBar
|
import com.example.llama.ui.scaffold.topbar.StorageTopBar
|
||||||
|
|
@ -65,6 +68,25 @@ fun AppScaffold(
|
||||||
onMenuOpen = topBarconfig.navigationIcon.menuAction,
|
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(
|
is TopBarConfig.Performance -> PerformanceTopBar(
|
||||||
title = topBarconfig.title,
|
title = topBarconfig.title,
|
||||||
memoryMetrics = topBarconfig.memoryMetrics,
|
memoryMetrics = topBarconfig.memoryMetrics,
|
||||||
|
|
@ -73,13 +95,6 @@ fun AppScaffold(
|
||||||
onNavigateBack = topBarconfig.navigationIcon.backAction,
|
onNavigateBack = topBarconfig.navigationIcon.backAction,
|
||||||
onMenuOpen = topBarconfig.navigationIcon.menuAction,
|
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) {
|
when (val config = bottomBarConfig) {
|
||||||
is BottomBarConfig.None -> { /* No bottom bar */ }
|
is BottomBarConfig.None -> { /* No bottom bar */ }
|
||||||
|
|
||||||
is BottomBarConfig.ModelSelection -> {
|
is BottomBarConfig.Models.Browsing -> {
|
||||||
ModelSelectionBottomBar(
|
ModelsBrowsingBottomBar(
|
||||||
search = config.search,
|
onToggleSearching = config.onToggleSearching,
|
||||||
sorting = config.sorting,
|
sortingConfig = config.sorting,
|
||||||
filtering = config.filtering,
|
filteringConfig = config.filtering,
|
||||||
runAction = config.runAction
|
runActionConfig = config.runAction
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is BottomBarConfig.ModelsManagement -> {
|
is BottomBarConfig.Models.Searching -> {
|
||||||
ModelsManagementBottomBar(
|
ModelsSearchingBottomBar(
|
||||||
sorting = config.sorting,
|
textFieldState = config.textFieldState,
|
||||||
filtering = config.filtering,
|
onQuitSearching = config.onQuitSearching,
|
||||||
selection = config.selection,
|
onSearch = config.onSearch,
|
||||||
importing = config.importing,
|
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 -> {
|
is BottomBarConfig.Benchmark -> {
|
||||||
BenchmarkBottomBar(
|
BenchmarkBottomBar(
|
||||||
showShareFab = config.showShareFab,
|
showShareFab = config.showShareFab,
|
||||||
|
|
@ -141,8 +169,11 @@ fun AppScaffold(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions to obtain navigation actions if exist
|
// Helper functions to obtain navigation actions if exist
|
||||||
|
private val NavigationIcon.menuAction: (() -> Unit)?
|
||||||
|
get() = (this as? NavigationIcon.Menu)?.onMenuOpen
|
||||||
|
|
||||||
private val NavigationIcon.backAction: (() -> Unit)?
|
private val NavigationIcon.backAction: (() -> Unit)?
|
||||||
get() = (this as? NavigationIcon.Back)?.onNavigateBack
|
get() = (this as? NavigationIcon.Back)?.onNavigateBack
|
||||||
|
|
||||||
private val NavigationIcon.menuAction: (() -> Unit)?
|
private val NavigationIcon.quitAction: (() -> Unit)?
|
||||||
get() = (this as? NavigationIcon.Menu)?.onMenuOpen
|
get() = (this as? NavigationIcon.Quit)?.onQuit
|
||||||
|
|
|
||||||
|
|
@ -122,20 +122,21 @@ private fun DrawerContent(
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// Main Navigation Items
|
// // Main Navigation Items
|
||||||
Text(
|
// TODO-han.yin: add back once we add more features
|
||||||
text = "Navigation",
|
// Text(
|
||||||
style = MaterialTheme.typography.labelMedium,
|
// text = "Features",
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
// style = MaterialTheme.typography.labelMedium,
|
||||||
modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)
|
// color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
// modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)
|
||||||
|
// )
|
||||||
|
|
||||||
DrawerNavigationItem(
|
DrawerNavigationItem(
|
||||||
icon = Icons.Default.Home,
|
icon = Icons.Default.Home,
|
||||||
label = "Home",
|
label = "Models",
|
||||||
isSelected = currentRoute == AppDestinations.MODEL_SELECTION_ROUTE,
|
isSelected = currentRoute == AppDestinations.MODELS_ROUTE,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (currentRoute != AppDestinations.MODEL_SELECTION_ROUTE) {
|
if (currentRoute != AppDestinations.MODELS_ROUTE) {
|
||||||
onNavigate { navigationActions.navigateToModelSelection() }
|
onNavigate { navigationActions.navigateToModelSelection() }
|
||||||
} else {
|
} else {
|
||||||
onNavigate { /* No-op: simply close drawer */ }
|
onNavigate { /* No-op: simply close drawer */ }
|
||||||
|
|
@ -143,22 +144,15 @@ private fun DrawerContent(
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
// Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
// TODO-han.yin: add back once we add more features
|
||||||
// Settings Group
|
// // Settings Group
|
||||||
Text(
|
// Text(
|
||||||
text = "Settings",
|
// text = "Settings",
|
||||||
style = MaterialTheme.typography.labelMedium,
|
// style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
// color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)
|
// 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() } }
|
|
||||||
)
|
|
||||||
|
|
||||||
DrawerNavigationItem(
|
DrawerNavigationItem(
|
||||||
icon = Icons.Default.Settings,
|
icon = Icons.Default.Settings,
|
||||||
|
|
|
||||||
|
|
@ -64,10 +64,7 @@ fun BenchmarkBottomBar(
|
||||||
enter = scaleIn() + fadeIn(),
|
enter = scaleIn() + fadeIn(),
|
||||||
exit = scaleOut() + fadeOut()
|
exit = scaleOut() + fadeOut()
|
||||||
) {
|
) {
|
||||||
FloatingActionButton(
|
FloatingActionButton(onClick = onShare) {
|
||||||
onClick = onShare,
|
|
||||||
containerColor = MaterialTheme.colorScheme.primary
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Share,
|
imageVector = Icons.Default.Share,
|
||||||
contentDescription = "Share the benchmark results"
|
contentDescription = "Share the benchmark results"
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import androidx.compose.foundation.text.input.TextFieldState
|
||||||
import com.example.llama.data.model.ModelFilter
|
import com.example.llama.data.model.ModelFilter
|
||||||
import com.example.llama.data.model.ModelInfo
|
import com.example.llama.data.model.ModelInfo
|
||||||
import com.example.llama.data.model.ModelSortOrder
|
import com.example.llama.data.model.ModelSortOrder
|
||||||
import com.example.llama.viewmodel.Preselection
|
import com.example.llama.viewmodel.PreselectedModelToRun
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [BottomAppBar] configurations
|
* [BottomAppBar] configurations
|
||||||
|
|
@ -13,76 +13,78 @@ sealed class BottomBarConfig {
|
||||||
|
|
||||||
object None : BottomBarConfig()
|
object None : BottomBarConfig()
|
||||||
|
|
||||||
data class ModelSelection(
|
sealed class Models : BottomBarConfig() {
|
||||||
val search: SearchConfig,
|
|
||||||
val sorting: SortingConfig,
|
data class Browsing(
|
||||||
val filtering: FilteringConfig,
|
val onToggleSearching: () -> Unit,
|
||||||
val runAction: RunActionConfig
|
val sorting: SortingConfig,
|
||||||
) : BottomBarConfig() {
|
val filtering: FilteringConfig,
|
||||||
data class SearchConfig(
|
val runAction: RunActionConfig,
|
||||||
val isActive: Boolean,
|
) : BottomBarConfig() {
|
||||||
val onToggleSearch: (Boolean) -> Unit,
|
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<ModelFilter, Boolean>,
|
||||||
|
val onToggleFilter: (ModelFilter, Boolean) -> Unit,
|
||||||
|
val onClearFilters: () -> Unit,
|
||||||
|
val isMenuVisible: Boolean,
|
||||||
|
val toggleMenu: (Boolean) -> Unit
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Searching(
|
||||||
val textFieldState: TextFieldState,
|
val textFieldState: TextFieldState,
|
||||||
|
val onQuitSearching: () -> Unit,
|
||||||
val onSearch: (String) -> Unit,
|
val onSearch: (String) -> Unit,
|
||||||
)
|
val runAction: RunActionConfig,
|
||||||
|
) : BottomBarConfig()
|
||||||
|
|
||||||
data class SortingConfig(
|
data class Management(
|
||||||
val currentOrder: ModelSortOrder,
|
val sorting: SortingConfig,
|
||||||
val isMenuVisible: Boolean,
|
val filtering: FilteringConfig,
|
||||||
val toggleMenu: (Boolean) -> Unit,
|
val importing: ImportConfig,
|
||||||
val selectOrder: (ModelSortOrder) -> Unit
|
val onToggleDeleting: () -> Unit,
|
||||||
)
|
) : BottomBarConfig() {
|
||||||
|
data class SortingConfig(
|
||||||
|
val currentOrder: ModelSortOrder,
|
||||||
|
val isMenuVisible: Boolean,
|
||||||
|
val toggleMenu: (Boolean) -> Unit,
|
||||||
|
val selectOrder: (ModelSortOrder) -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
data class FilteringConfig(
|
data class FilteringConfig(
|
||||||
val isActive: Boolean,
|
val isActive: Boolean,
|
||||||
val filters: Map<ModelFilter, Boolean>,
|
val filters: Map<ModelFilter, Boolean>,
|
||||||
val onToggleFilter: (ModelFilter, Boolean) -> Unit,
|
val onToggleFilter: (ModelFilter, Boolean) -> Unit,
|
||||||
val onClearFilters: () -> Unit,
|
val onClearFilters: () -> Unit,
|
||||||
val isMenuVisible: Boolean,
|
val isMenuVisible: Boolean,
|
||||||
val toggleMenu: (Boolean) -> Unit
|
val toggleMenu: (Boolean) -> Unit
|
||||||
)
|
)
|
||||||
|
|
||||||
data class RunActionConfig(
|
data class ImportConfig(
|
||||||
val preselection: Preselection?,
|
val isMenuVisible: Boolean,
|
||||||
val onClickRun: (Preselection) -> Unit,
|
val toggleMenu: (Boolean) -> Unit,
|
||||||
)
|
val importFromLocal: () -> Unit,
|
||||||
}
|
val importFromHuggingFace: () -> Unit
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
data class ModelsManagement(
|
data class Deleting(
|
||||||
val sorting: SortingConfig,
|
val onQuitDeleting: () -> Unit,
|
||||||
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<ModelFilter, Boolean>,
|
|
||||||
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,
|
|
||||||
val selectedModels: Map<String, ModelInfo>,
|
val selectedModels: Map<String, ModelInfo>,
|
||||||
val toggleAllSelection: (Boolean) -> Unit,
|
val toggleAllSelection: (Boolean) -> Unit,
|
||||||
val deleteSelected: () -> Unit
|
val deleteSelected: () -> Unit
|
||||||
)
|
) : BottomBarConfig()
|
||||||
|
|
||||||
data class ImportConfig(
|
data class RunActionConfig(
|
||||||
val isMenuVisible: Boolean,
|
val preselectedModelToRun: PreselectedModelToRun?,
|
||||||
val toggleMenu: (Boolean) -> Unit,
|
val onClickRun: (PreselectedModelToRun) -> Unit,
|
||||||
val importFromLocal: () -> Unit,
|
|
||||||
val importFromHuggingFace: () -> Unit
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -6,12 +6,8 @@ import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.Sort
|
import androidx.compose.material.icons.automirrored.filled.Sort
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Check
|
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.FilterAlt
|
||||||
import androidx.compose.material.icons.filled.FolderOpen
|
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.DeleteSweep
|
||||||
import androidx.compose.material.icons.outlined.FilterAlt
|
import androidx.compose.material.icons.outlined.FilterAlt
|
||||||
import androidx.compose.material.icons.outlined.FilterAltOff
|
import androidx.compose.material.icons.outlined.FilterAltOff
|
||||||
|
|
@ -35,176 +31,138 @@ import com.example.llama.data.model.ModelSortOrder
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ModelsManagementBottomBar(
|
fun ModelsManagementBottomBar(
|
||||||
sorting: BottomBarConfig.ModelsManagement.SortingConfig,
|
sortingConfig: BottomBarConfig.Models.Management.SortingConfig,
|
||||||
filtering: BottomBarConfig.ModelsManagement.FilteringConfig,
|
filteringConfig: BottomBarConfig.Models.Management.FilteringConfig,
|
||||||
selection: BottomBarConfig.ModelsManagement.SelectionConfig,
|
importingConfig: BottomBarConfig.Models.Management.ImportConfig,
|
||||||
importing: BottomBarConfig.ModelsManagement.ImportConfig
|
onToggleDeleting: () -> Unit,
|
||||||
) {
|
) {
|
||||||
BottomAppBar(
|
BottomAppBar(
|
||||||
actions = {
|
actions = {
|
||||||
if (selection.isActive) {
|
// Batch-deletion action
|
||||||
/* Multi-selection mode actions */
|
IconButton(onClick = onToggleDeleting) {
|
||||||
IconButton(
|
Icon(
|
||||||
onClick = selection.deleteSelected,
|
imageVector = Icons.Outlined.DeleteSweep,
|
||||||
enabled = selection.selectedModels.isNotEmpty()
|
contentDescription = "Delete models"
|
||||||
) {
|
)
|
||||||
Icon(
|
}
|
||||||
imageVector = Icons.Default.Delete,
|
|
||||||
contentDescription = "Delete selected",
|
|
||||||
tint = if (selection.selectedModels.isNotEmpty())
|
|
||||||
MaterialTheme.colorScheme.error
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton(onClick = { selection.toggleAllSelection(false) }) {
|
// Sorting action
|
||||||
Icon(
|
IconButton(onClick = { sortingConfig.toggleMenu(true) }) {
|
||||||
imageVector = Icons.Default.ClearAll,
|
Icon(
|
||||||
contentDescription = "Deselect all"
|
imageVector = Icons.AutoMirrored.Filled.Sort,
|
||||||
)
|
contentDescription = "Sort models"
|
||||||
}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
IconButton(onClick = { selection.toggleAllSelection(true) }) {
|
// Sorting dropdown menu
|
||||||
Icon(
|
DropdownMenu(
|
||||||
imageVector = Icons.Default.SelectAll,
|
expanded = sortingConfig.isMenuVisible,
|
||||||
contentDescription = "Select all"
|
onDismissRequest = { sortingConfig.toggleMenu(false) }
|
||||||
)
|
) {
|
||||||
}
|
val sortOptions = listOf(
|
||||||
} else {
|
Triple(
|
||||||
/* Default mode actions */
|
ModelSortOrder.NAME_ASC,
|
||||||
|
"Name (A-Z)",
|
||||||
// Multi-selection action
|
"Sort by name in ascending order"
|
||||||
IconButton(onClick = { selection.toggleMode(true) }) {
|
),
|
||||||
Icon(
|
Triple(
|
||||||
imageVector = Icons.Outlined.DeleteSweep,
|
ModelSortOrder.NAME_DESC,
|
||||||
contentDescription = "Delete models"
|
"Name (Z-A)",
|
||||||
)
|
"Sort by name in descending order"
|
||||||
}
|
),
|
||||||
|
Triple(
|
||||||
// Sorting action
|
ModelSortOrder.SIZE_ASC,
|
||||||
IconButton(onClick = { sorting.toggleMenu(true) }) {
|
"Size (Smallest first)",
|
||||||
Icon(
|
"Sort by size in ascending order"
|
||||||
imageVector = Icons.AutoMirrored.Filled.Sort,
|
),
|
||||||
contentDescription = "Sort models"
|
Triple(
|
||||||
)
|
ModelSortOrder.SIZE_DESC,
|
||||||
}
|
"Size (Largest first)",
|
||||||
|
"Sort by size in descending order"
|
||||||
// Sorting dropdown menu
|
),
|
||||||
DropdownMenu(
|
Triple(ModelSortOrder.LAST_USED, "Last used", "Sort by last used")
|
||||||
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()
|
|
||||||
|
|
||||||
|
sortOptions.forEach { (order, label, description) ->
|
||||||
DropdownMenuItem(
|
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 = {
|
leadingIcon = {
|
||||||
Icon(
|
Checkbox(
|
||||||
imageVector = Icons.Outlined.FilterAltOff,
|
checked = isEnabled,
|
||||||
contentDescription = "Clear all filters"
|
onCheckedChange = null
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = { filteringConfig.onToggleFilter(filter, !isEnabled) }
|
||||||
filtering.onClearFilters()
|
|
||||||
filtering.toggleMenu(false)
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HorizontalDivider()
|
||||||
|
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Clear filters") },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.FilterAltOff,
|
||||||
|
contentDescription = "Clear all filters"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
filteringConfig.onClearFilters()
|
||||||
|
filteringConfig.toggleMenu(false)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = {
|
onClick = { importingConfig.toggleMenu(true) },
|
||||||
if (selection.isActive) selection.toggleMode(false) else importing.toggleMenu(
|
|
||||||
true
|
|
||||||
)
|
|
||||||
},
|
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (selection.isActive) Icons.Default.Close else Icons.Default.Add,
|
imageVector = Icons.Default.Add,
|
||||||
contentDescription = if (selection.isActive) "Exit selection mode" else "Add model"
|
contentDescription = "Add model"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add model dropdown menu
|
// Add model dropdown menu
|
||||||
DropdownMenu(
|
DropdownMenu(
|
||||||
expanded = importing.isMenuVisible,
|
expanded = importingConfig.isMenuVisible,
|
||||||
onDismissRequest = { importing.toggleMenu(false) }
|
onDismissRequest = { importingConfig.toggleMenu(false) }
|
||||||
) {
|
) {
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("Import local model") },
|
text = { Text("Import local model") },
|
||||||
|
|
@ -214,7 +172,7 @@ fun ModelsManagementBottomBar(
|
||||||
contentDescription = "Import a local model on the device"
|
contentDescription = "Import a local model on the device"
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = importing.importFromLocal
|
onClick = importingConfig.importFromLocal
|
||||||
)
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text("Download from HuggingFace") },
|
text = { Text("Download from HuggingFace") },
|
||||||
|
|
@ -226,7 +184,7 @@ fun ModelsManagementBottomBar(
|
||||||
tint = Color.Unspecified,
|
tint = Color.Unspecified,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = importing.importFromHuggingFace
|
onClick = importingConfig.importFromHuggingFace
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package com.example.llama.ui.scaffold.topbar
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
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.material.icons.filled.Menu
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
|
@ -17,12 +18,22 @@ import androidx.compose.runtime.Composable
|
||||||
fun DefaultTopBar(
|
fun DefaultTopBar(
|
||||||
title: String,
|
title: String,
|
||||||
onNavigateBack: (() -> Unit)? = null,
|
onNavigateBack: (() -> Unit)? = null,
|
||||||
|
onQuit: (() -> Unit)? = null,
|
||||||
onMenuOpen: (() -> Unit)? = null
|
onMenuOpen: (() -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(title) },
|
title = { Text(title) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
when {
|
when {
|
||||||
|
onQuit != null -> {
|
||||||
|
IconButton(onClick = onQuit) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = "Quit"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onNavigateBack != null -> {
|
onNavigateBack != null -> {
|
||||||
IconButton(onClick = onNavigateBack) {
|
IconButton(onClick = onNavigateBack) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
package com.example.llama.ui.scaffold.topbar
|
package com.example.llama.ui.scaffold.topbar
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.Memory
|
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.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.semantics.Role
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.example.llama.monitoring.MemoryMetrics
|
import com.example.llama.monitoring.MemoryMetrics
|
||||||
import com.example.llama.monitoring.TemperatureMetrics
|
import com.example.llama.monitoring.TemperatureMetrics
|
||||||
|
|
@ -92,30 +89,32 @@ private fun MemoryIndicator(
|
||||||
val availableGB = String.format(Locale.getDefault(), "%.1f", memoryUsage.availableGB)
|
val availableGB = String.format(Locale.getDefault(), "%.1f", memoryUsage.availableGB)
|
||||||
val totalGB = String.format(Locale.getDefault(), "%.1f", memoryUsage.totalGB)
|
val totalGB = String.format(Locale.getDefault(), "%.1f", memoryUsage.totalGB)
|
||||||
|
|
||||||
Row(
|
OutlinedButton(
|
||||||
modifier = Modifier.padding(end = 12.dp).clickable(role = Role.Button) {
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
|
onClick = {
|
||||||
onScaffoldEvent(ScaffoldEvent.ShowSnackbar(
|
onScaffoldEvent(ScaffoldEvent.ShowSnackbar(
|
||||||
message = "Free RAM available: $availableGB GB\nTotal RAM on your device: $totalGB GB",
|
message = "Free RAM available: $availableGB GB\nTotal RAM on your device: $totalGB GB",
|
||||||
withDismissAction = true,
|
withDismissAction = true,
|
||||||
))
|
))
|
||||||
},
|
}
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
imageVector = Icons.Default.Memory,
|
Icon(
|
||||||
contentDescription = "RAM usage",
|
imageVector = Icons.Default.Memory,
|
||||||
tint = when {
|
contentDescription = "RAM usage",
|
||||||
memoryUsage.availableGB < 1 -> MaterialTheme.colorScheme.error
|
tint = when {
|
||||||
memoryUsage.availableGB < 3 -> MaterialTheme.colorScheme.tertiary
|
memoryUsage.availableGB < 1 -> MaterialTheme.colorScheme.error
|
||||||
else -> MaterialTheme.colorScheme.onSurface
|
memoryUsage.availableGB < 3 -> MaterialTheme.colorScheme.tertiary
|
||||||
}
|
else -> MaterialTheme.colorScheme.onSurface
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(start = 4.dp),
|
modifier = Modifier.padding(start = 4.dp),
|
||||||
text = "$availableGB / $totalGB GB",
|
text = "$availableGB / $totalGB GB",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,36 +133,39 @@ private fun TemperatureIndicator(
|
||||||
}
|
}
|
||||||
val warningDismissible = temperatureMetrics.warningLevel == TemperatureWarningLevel.HIGH
|
val warningDismissible = temperatureMetrics.warningLevel == TemperatureWarningLevel.HIGH
|
||||||
|
|
||||||
Row(
|
OutlinedButton(
|
||||||
modifier = Modifier.padding(end = 12.dp).clickable(role = Role.Button) {
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
|
onClick = {
|
||||||
onScaffoldEvent(ScaffoldEvent.ShowSnackbar(
|
onScaffoldEvent(ScaffoldEvent.ShowSnackbar(
|
||||||
message = temperatureWarning,
|
message = temperatureWarning,
|
||||||
withDismissAction = warningDismissible,
|
withDismissAction = warningDismissible,
|
||||||
))
|
))
|
||||||
},
|
}
|
||||||
verticalAlignment = Alignment.CenterVertically) {
|
) {
|
||||||
Icon(
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
imageVector = when (temperatureMetrics.warningLevel) {
|
Icon(
|
||||||
TemperatureWarningLevel.HIGH -> Icons.Default.WarningAmber
|
imageVector = when (temperatureMetrics.warningLevel) {
|
||||||
else -> Icons.Default.Thermostat
|
TemperatureWarningLevel.HIGH -> Icons.Default.WarningAmber
|
||||||
},
|
else -> Icons.Default.Thermostat
|
||||||
contentDescription = "Device temperature",
|
},
|
||||||
tint = when (temperatureMetrics.warningLevel) {
|
contentDescription = "Device temperature",
|
||||||
TemperatureWarningLevel.HIGH -> MaterialTheme.colorScheme.error
|
tint = when (temperatureMetrics.warningLevel) {
|
||||||
TemperatureWarningLevel.MEDIUM -> MaterialTheme.colorScheme.tertiary
|
TemperatureWarningLevel.HIGH -> MaterialTheme.colorScheme.error
|
||||||
else -> MaterialTheme.colorScheme.onSurface
|
TemperatureWarningLevel.MEDIUM -> MaterialTheme.colorScheme.tertiary
|
||||||
}
|
else -> MaterialTheme.colorScheme.onSurface
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(start = 4.dp),
|
modifier = Modifier.padding(start = 4.dp),
|
||||||
text = temperatureDisplay,
|
text = temperatureDisplay,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = when (temperatureMetrics.warningLevel) {
|
color = when (temperatureMetrics.warningLevel) {
|
||||||
TemperatureWarningLevel.HIGH -> MaterialTheme.colorScheme.error
|
TemperatureWarningLevel.HIGH -> MaterialTheme.colorScheme.error
|
||||||
TemperatureWarningLevel.MEDIUM -> MaterialTheme.colorScheme.tertiary
|
TemperatureWarningLevel.MEDIUM -> MaterialTheme.colorScheme.tertiary
|
||||||
else -> MaterialTheme.colorScheme.onSurface
|
else -> MaterialTheme.colorScheme.onSurface
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,7 @@
|
||||||
package com.example.llama.ui.scaffold.topbar
|
package com.example.llama.ui.scaffold.topbar
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.SdStorage
|
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.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.semantics.Role
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.example.llama.monitoring.StorageMetrics
|
import com.example.llama.monitoring.StorageMetrics
|
||||||
import com.example.llama.ui.scaffold.ScaffoldEvent
|
import com.example.llama.ui.scaffold.ScaffoldEvent
|
||||||
|
|
@ -65,29 +62,31 @@ private fun StorageIndicator(
|
||||||
val usedGb = String.format(Locale.getDefault(), "%.1f", storageMetrics.usedGB)
|
val usedGb = String.format(Locale.getDefault(), "%.1f", storageMetrics.usedGB)
|
||||||
val availableGb = String.format(Locale.getDefault(), "%.1f", storageMetrics.availableGB)
|
val availableGb = String.format(Locale.getDefault(), "%.1f", storageMetrics.availableGB)
|
||||||
|
|
||||||
Row(
|
OutlinedButton(
|
||||||
modifier = Modifier.padding(end = 8.dp).clickable(role = Role.Button) {
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
|
onClick = {
|
||||||
onScaffoldEvent(ScaffoldEvent.ShowSnackbar(
|
onScaffoldEvent(ScaffoldEvent.ShowSnackbar(
|
||||||
message = "Your models occupy $usedGb GB storage\nRemaining free space available: $availableGb GB",
|
message = "Your models occupy $usedGb GB storage\nRemaining free space available: $availableGb GB",
|
||||||
withDismissAction = true,
|
withDismissAction = true,
|
||||||
))
|
))
|
||||||
},
|
}
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
imageVector = Icons.Default.SdStorage,
|
Icon(
|
||||||
contentDescription = "Storage",
|
imageVector = Icons.Default.SdStorage,
|
||||||
tint = when {
|
contentDescription = "Storage",
|
||||||
storageMetrics.availableGB < 5.0f -> MaterialTheme.colorScheme.error
|
tint = when {
|
||||||
storageMetrics.availableGB < 10.0f -> MaterialTheme.colorScheme.tertiary
|
storageMetrics.availableGB < 5.0f -> MaterialTheme.colorScheme.error
|
||||||
else -> MaterialTheme.colorScheme.onSurface
|
storageMetrics.availableGB < 10.0f -> MaterialTheme.colorScheme.tertiary
|
||||||
}
|
else -> MaterialTheme.colorScheme.onSurface
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier.padding(start = 4.dp),
|
modifier = Modifier.padding(start = 4.dp),
|
||||||
text = "$usedGb / $availableGb GB",
|
text = "$usedGb / $availableGb GB",
|
||||||
style = MaterialTheme.typography.bodySmall
|
style = MaterialTheme.typography.bodySmall
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ package com.example.llama.ui.scaffold.topbar
|
||||||
import com.example.llama.monitoring.MemoryMetrics
|
import com.example.llama.monitoring.MemoryMetrics
|
||||||
import com.example.llama.monitoring.StorageMetrics
|
import com.example.llama.monitoring.StorageMetrics
|
||||||
import com.example.llama.monitoring.TemperatureMetrics
|
import com.example.llama.monitoring.TemperatureMetrics
|
||||||
|
import com.example.llama.ui.scaffold.ScaffoldEvent
|
||||||
|
import com.example.llama.viewmodel.ModelScreenUiMode
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [TopAppBar] configurations
|
* [TopAppBar] configurations
|
||||||
|
|
@ -23,6 +25,19 @@ sealed class TopBarConfig {
|
||||||
override val navigationIcon: NavigationIcon
|
override val navigationIcon: NavigationIcon
|
||||||
) : TopBarConfig()
|
) : 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
|
// Performance monitoring top bar with RAM and optional temperature
|
||||||
data class Performance(
|
data class Performance(
|
||||||
override val title: String,
|
override val title: String,
|
||||||
|
|
@ -32,7 +47,7 @@ sealed class TopBarConfig {
|
||||||
) : TopBarConfig()
|
) : TopBarConfig()
|
||||||
|
|
||||||
// Storage management top bar with used & total storage
|
// Storage management top bar with used & total storage
|
||||||
data class Storage(
|
data class ModelsManagement(
|
||||||
override val title: String,
|
override val title: String,
|
||||||
override val navigationIcon: NavigationIcon,
|
override val navigationIcon: NavigationIcon,
|
||||||
val storageMetrics: StorageMetrics?
|
val storageMetrics: StorageMetrics?
|
||||||
|
|
@ -43,7 +58,8 @@ sealed class TopBarConfig {
|
||||||
* Helper class for navigation icon configuration
|
* Helper class for navigation icon configuration
|
||||||
*/
|
*/
|
||||||
sealed class NavigationIcon {
|
sealed class NavigationIcon {
|
||||||
data class Back(val onNavigateBack: () -> Unit) : NavigationIcon()
|
|
||||||
data class Menu(val onMenuOpen: () -> 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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@ fun ConversationScreen(
|
||||||
// UI states
|
// UI states
|
||||||
val lifecycleOwner = LocalLifecycleOwner.current
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
var isModelCardExpanded by remember { mutableStateOf(false) }
|
var isModelCardExpanded by remember { mutableStateOf(true) }
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
// Track the actual rendered size of the last message bubble
|
// Track the actual rendered size of the last message bubble
|
||||||
|
|
@ -234,7 +234,7 @@ fun ModelCardWithSystemPrompt(
|
||||||
model: ModelInfo,
|
model: ModelInfo,
|
||||||
loadingMetrics: ModelLoadingMetrics,
|
loadingMetrics: ModelLoadingMetrics,
|
||||||
systemPrompt: String?,
|
systemPrompt: String?,
|
||||||
isExpanded: Boolean = false,
|
isExpanded: Boolean = true,
|
||||||
onExpanded: ((Boolean) -> Unit)? = null,
|
onExpanded: ((Boolean) -> Unit)? = null,
|
||||||
) = ModelCardCoreExpandable(model, isExpanded, onExpanded) {
|
) = ModelCardCoreExpandable(model, isExpanded, onExpanded) {
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -67,7 +67,8 @@ import com.example.llama.viewmodel.ModelManagementState
|
||||||
import com.example.llama.viewmodel.ModelManagementState.Deletion
|
import com.example.llama.viewmodel.ModelManagementState.Deletion
|
||||||
import com.example.llama.viewmodel.ModelManagementState.Download
|
import com.example.llama.viewmodel.ModelManagementState.Download
|
||||||
import com.example.llama.viewmodel.ModelManagementState.Importation
|
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.text.SimpleDateFormat
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
|
@ -75,16 +76,16 @@ import java.util.Locale
|
||||||
* Screen for managing LLM models (view, download, delete)
|
* Screen for managing LLM models (view, download, delete)
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ModelsManagementScreen(
|
fun ModelsManagementAndDeletingScreen(
|
||||||
|
isDeleting: Boolean,
|
||||||
onScaffoldEvent: (ScaffoldEvent) -> Unit,
|
onScaffoldEvent: (ScaffoldEvent) -> Unit,
|
||||||
viewModel: ModelsManagementViewModel,
|
viewModel: ModelsViewModel,
|
||||||
) {
|
) {
|
||||||
// Data: models
|
// Data: models
|
||||||
val filteredModels by viewModel.filteredModels.collectAsState()
|
val filteredModels by viewModel.filteredModels.collectAsState()
|
||||||
|
|
||||||
// Selection state
|
// Selection state
|
||||||
val isMultiSelectionMode by viewModel.isMultiSelectionMode.collectAsState()
|
val selectedModels by viewModel.selectedModelsToDelete.collectAsState()
|
||||||
val selectedModels by viewModel.selectedModels.collectAsState()
|
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
val activeFilters by viewModel.activeFilters.collectAsState()
|
val activeFilters by viewModel.activeFilters.collectAsState()
|
||||||
|
|
@ -96,19 +97,13 @@ fun ModelsManagementScreen(
|
||||||
val managementState by viewModel.managementState.collectAsState()
|
val managementState by viewModel.managementState.collectAsState()
|
||||||
|
|
||||||
// UI states
|
// UI states
|
||||||
var expandedModels = remember { mutableStateMapOf<String, ModelInfo>() }
|
val expandedModels = remember { mutableStateMapOf<String, ModelInfo>() }
|
||||||
|
|
||||||
BackHandler(
|
BackHandler(
|
||||||
enabled = isMultiSelectionMode
|
enabled = managementState is Importation.Importing
|
||||||
|| managementState is Importation.Importing
|
|
||||||
|| managementState is Deletion.Deleting
|
|| managementState is Deletion.Deleting
|
||||||
) {
|
) {
|
||||||
if (isMultiSelectionMode) {
|
/* Ignore back press while processing model management requests */
|
||||||
// Exit selection mode if in selection mode
|
|
||||||
viewModel.toggleSelectionMode(false)
|
|
||||||
} else {
|
|
||||||
/* Ignore back press while processing model management requests */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
|
@ -134,13 +129,13 @@ fun ModelsManagementScreen(
|
||||||
) {
|
) {
|
||||||
items(items = filteredModels, key = { it.id }) { model ->
|
items(items = filteredModels, key = { it.id }) { model ->
|
||||||
val isSelected =
|
val isSelected =
|
||||||
if (isMultiSelectionMode) selectedModels.contains(model.id) else null
|
if (isDeleting) selectedModels.contains(model.id) else null
|
||||||
|
|
||||||
ModelCardFullExpandable(
|
ModelCardFullExpandable(
|
||||||
model = model,
|
model = model,
|
||||||
isSelected = isSelected,
|
isSelected = isSelected,
|
||||||
onSelected = {
|
onSelected = {
|
||||||
if (isMultiSelectionMode) {
|
if (isDeleting) {
|
||||||
viewModel.toggleModelSelectionById(model.id)
|
viewModel.toggleModelSelectionById(model.id)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -285,7 +280,7 @@ fun ModelsManagementScreen(
|
||||||
|
|
||||||
is Deletion.Success -> {
|
is Deletion.Success -> {
|
||||||
LaunchedEffect(state) {
|
LaunchedEffect(state) {
|
||||||
viewModel.toggleSelectionMode(false)
|
viewModel.toggleMode(ModelScreenUiMode.MANAGING)
|
||||||
|
|
||||||
val count = state.models.size
|
val count = state.models.size
|
||||||
onScaffoldEvent(
|
onScaffoldEvent(
|
||||||
|
|
@ -641,6 +636,9 @@ private fun BatchDeleteConfirmationDialog(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.errorContainer,
|
||||||
|
titleContentColor = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
|
textContentColor = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
TextButton(
|
TextButton(
|
||||||
onClick = onConfirm,
|
onClick = onConfirm,
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<Map<ModelFilter, Boolean>>(
|
|
||||||
ModelFilter.ALL_FILTERS.associateWith { false }
|
|
||||||
)
|
|
||||||
val activeFilters: StateFlow<Map<ModelFilter, Boolean>> = _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<List<ModelInfo>>(emptyList())
|
|
||||||
val filteredModels = _filteredModels.asStateFlow()
|
|
||||||
|
|
||||||
// Data: queried models
|
|
||||||
private val _queryResults = MutableStateFlow<List<ModelInfo>>(emptyList())
|
|
||||||
val queryResults = _queryResults.asStateFlow()
|
|
||||||
|
|
||||||
// Data: pre-selected model in expansion mode
|
|
||||||
private val _preselectedModel = MutableStateFlow<ModelInfo?>(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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -9,33 +9,43 @@ import android.content.IntentFilter
|
||||||
import android.llama.cpp.gguf.InvalidFileFormatException
|
import android.llama.cpp.gguf.InvalidFileFormatException
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.example.llama.data.model.ModelFilter
|
import com.example.llama.data.model.ModelFilter
|
||||||
import com.example.llama.data.model.ModelInfo
|
import com.example.llama.data.model.ModelInfo
|
||||||
import com.example.llama.data.model.ModelSortOrder
|
import com.example.llama.data.model.ModelSortOrder
|
||||||
import com.example.llama.data.model.filterBy
|
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.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.InsufficientStorageException
|
||||||
import com.example.llama.data.repo.ModelRepository
|
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.formatFileByteSize
|
||||||
import com.example.llama.util.getFileNameFromUri
|
import com.example.llama.util.getFileNameFromUri
|
||||||
import com.example.llama.util.getFileSizeFromUri
|
import com.example.llama.util.getFileSizeFromUri
|
||||||
import com.example.llama.viewmodel.ModelManagementState.Deletion
|
import com.example.llama.viewmodel.ModelManagementState.Deletion
|
||||||
import com.example.llama.viewmodel.ModelManagementState.Download
|
import com.example.llama.viewmodel.ModelManagementState.Download
|
||||||
import com.example.llama.viewmodel.ModelManagementState.Importation
|
import com.example.llama.viewmodel.ModelManagementState.Importation
|
||||||
|
import com.example.llama.viewmodel.PreselectedModelToRun.RamWarning
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.CancellationException
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
|
|
@ -43,70 +53,81 @@ import java.io.IOException
|
||||||
import java.net.SocketTimeoutException
|
import java.net.SocketTimeoutException
|
||||||
import java.net.UnknownHostException
|
import java.net.UnknownHostException
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ModelsManagementViewModel @Inject constructor(
|
class ModelsViewModel @Inject constructor(
|
||||||
@ApplicationContext private val context: Context,
|
@ApplicationContext private val context: Context,
|
||||||
private val modelRepository: ModelRepository
|
private val modelRepository: ModelRepository,
|
||||||
|
private val performanceMonitor: PerformanceMonitor,
|
||||||
|
private val inferenceService: InferenceService,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
// Data: models
|
// UI state: model management mode
|
||||||
private val _filteredModels = MutableStateFlow<List<ModelInfo>>(emptyList())
|
private val _modelScreenUiMode = MutableStateFlow(ModelScreenUiMode.BROWSING)
|
||||||
val filteredModels: StateFlow<List<ModelInfo>> = _filteredModels.asStateFlow()
|
val modelScreenUiMode = _modelScreenUiMode.asStateFlow()
|
||||||
|
|
||||||
// UI state: multi-selection mode
|
fun toggleMode(newMode: ModelScreenUiMode): Boolean {
|
||||||
private val _isMultiSelectionMode = MutableStateFlow(false)
|
val oldMode = _modelScreenUiMode.value
|
||||||
val isMultiSelectionMode: StateFlow<Boolean> = _isMultiSelectionMode.asStateFlow()
|
when (oldMode) {
|
||||||
|
ModelScreenUiMode.BROWSING -> {
|
||||||
fun toggleSelectionMode(enabled: Boolean) {
|
when (newMode) {
|
||||||
_isMultiSelectionMode.value = enabled
|
ModelScreenUiMode.SEARCHING -> {
|
||||||
if (!enabled) {
|
resetPreselection()
|
||||||
toggleAllSelection(selectAll = false)
|
}
|
||||||
}
|
ModelScreenUiMode.MANAGING -> {
|
||||||
}
|
resetPreselection()
|
||||||
|
}
|
||||||
// UI state: models selected in multi-selection
|
ModelScreenUiMode.DELETING -> { return false }
|
||||||
private val _selectedModels = MutableStateFlow<Map<String, ModelInfo>>(emptyMap())
|
else -> { /* No-op */ }
|
||||||
val selectedModels: StateFlow<Map<String, ModelInfo>> = _selectedModels.asStateFlow()
|
}
|
||||||
|
}
|
||||||
fun toggleModelSelectionById(modelId: String) {
|
ModelScreenUiMode.SEARCHING -> {
|
||||||
val current = _selectedModels.value.toMutableMap()
|
when (newMode) {
|
||||||
val model = _filteredModels.value.find { it.id == modelId }
|
ModelScreenUiMode.BROWSING -> {
|
||||||
|
searchFieldState.clearText()
|
||||||
if (model != null) {
|
}
|
||||||
if (current.containsKey(modelId)) {
|
else -> { return false }
|
||||||
current.remove(modelId)
|
}
|
||||||
} else {
|
}
|
||||||
current[modelId] = model
|
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) {
|
// UI state: search mode
|
||||||
if (selectAll) {
|
val searchFieldState = TextFieldState()
|
||||||
_selectedModels.value = _filteredModels.value.associateBy { it.id }
|
|
||||||
} else {
|
|
||||||
_selectedModels.value = emptyMap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// UI state: sort menu
|
// UI state: sort menu
|
||||||
private val _sortOrder = MutableStateFlow(ModelSortOrder.NAME_ASC)
|
private val _sortOrder = MutableStateFlow(ModelSortOrder.LAST_USED)
|
||||||
val sortOrder: StateFlow<ModelSortOrder> = _sortOrder.asStateFlow()
|
val sortOrder = _sortOrder.asStateFlow()
|
||||||
|
|
||||||
fun setSortOrder(order: ModelSortOrder) {
|
fun setSortOrder(order: ModelSortOrder) {
|
||||||
_sortOrder.value = order
|
_sortOrder.value = order
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _showSortMenu = MutableStateFlow(false)
|
private val _showSortMenu = MutableStateFlow(false)
|
||||||
val showSortMenu: StateFlow<Boolean> = _showSortMenu.asStateFlow()
|
val showSortMenu = _showSortMenu.asStateFlow()
|
||||||
|
|
||||||
fun toggleSortMenu(show: Boolean) {
|
fun toggleSortMenu(visible: Boolean) {
|
||||||
_showSortMenu.value = show
|
_showSortMenu.value = visible
|
||||||
}
|
}
|
||||||
|
|
||||||
// UI state: filters
|
// UI state: filter menu
|
||||||
private val _activeFilters = MutableStateFlow<Map<ModelFilter, Boolean>>(
|
private val _activeFilters = MutableStateFlow<Map<ModelFilter, Boolean>>(
|
||||||
ModelFilter.ALL_FILTERS.associateWith { false }
|
ModelFilter.ALL_FILTERS.associateWith { false }
|
||||||
)
|
)
|
||||||
|
|
@ -127,12 +148,69 @@ class ModelsManagementViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _showFilterMenu = MutableStateFlow(false)
|
private val _showFilterMenu = MutableStateFlow(false)
|
||||||
val showFilterMenu: StateFlow<Boolean> = _showFilterMenu.asStateFlow()
|
val showFilterMenu = _showFilterMenu.asStateFlow()
|
||||||
|
|
||||||
fun toggleFilterMenu(visible: Boolean) {
|
fun toggleFilterMenu(visible: Boolean) {
|
||||||
_showFilterMenu.value = visible
|
_showFilterMenu.value = visible
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Data: filtered & sorted models
|
||||||
|
private val _filteredModels = MutableStateFlow<List<ModelInfo>>(emptyList())
|
||||||
|
val filteredModels = _filteredModels.asStateFlow()
|
||||||
|
|
||||||
|
// Data: queried models
|
||||||
|
private val _queryResults = MutableStateFlow<List<ModelInfo>>(emptyList())
|
||||||
|
val queryResults = _queryResults.asStateFlow()
|
||||||
|
|
||||||
|
// Data: pre-selected model in expansion mode
|
||||||
|
private val _preselectedModelToRun = MutableStateFlow<ModelInfo?>(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<Map<String, ModelInfo>>(emptyMap())
|
||||||
|
val selectedModelsToDelete: StateFlow<Map<String, ModelInfo>> = _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
|
// UI state: import menu
|
||||||
private val _showImportModelMenu = MutableStateFlow(false)
|
private val _showImportModelMenu = MutableStateFlow(false)
|
||||||
val showImportModelMenu: StateFlow<Boolean> = _showImportModelMenu.asStateFlow()
|
val showImportModelMenu: StateFlow<Boolean> = _showImportModelMenu.asStateFlow()
|
||||||
|
|
@ -156,6 +234,17 @@ class ModelsManagementViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Internal state
|
||||||
|
private val _managementState = MutableStateFlow<ModelManagementState>(ModelManagementState.Idle)
|
||||||
|
val managementState: StateFlow<ModelManagementState> = _managementState.asStateFlow()
|
||||||
|
|
||||||
|
fun resetManagementState() {
|
||||||
|
huggingFaceQueryJob?.let {
|
||||||
|
if (it.isActive) { it.cancel() }
|
||||||
|
}
|
||||||
|
_managementState.value = ModelManagementState.Idle
|
||||||
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
combine(
|
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)
|
val filter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||||
context.registerReceiver(downloadReceiver, filter, RECEIVER_EXPORTED)
|
context.registerReceiver(downloadReceiver, filter, RECEIVER_EXPORTED)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal state
|
/**
|
||||||
private val _managementState = MutableStateFlow<ModelManagementState>(ModelManagementState.Idle)
|
* Pre-select a model to expand its details and show Run FAB
|
||||||
val managementState: StateFlow<ModelManagementState> = _managementState.asStateFlow()
|
*/
|
||||||
|
fun preselectModel(modelInfo: ModelInfo, preselected: Boolean) {
|
||||||
fun resetManagementState() {
|
_preselectedModelToRun.value = if (preselected) modelInfo else null
|
||||||
huggingFaceQueryJob?.let {
|
_showRamWarning.value = false
|
||||||
if (it.isActive) { it.cancel() }
|
|
||||||
}
|
|
||||||
_managementState.value = ModelManagementState.Idle
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* 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
|
* 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.Deleting(deleted.toFloat() / total, models)
|
||||||
}
|
}
|
||||||
_managementState.value = Deletion.Success(models.values.toList())
|
_managementState.value = Deletion.Success(models.values.toList())
|
||||||
|
toggleAllSelectedModelsToDelete(false)
|
||||||
|
|
||||||
// Reset state after a delay
|
// Reset state after a delay
|
||||||
delay(SUCCESS_RESET_TIMEOUT_MS)
|
delay(DELETE_SUCCESS_RESET_TIMEOUT_MS)
|
||||||
_managementState.value = ModelManagementState.Idle
|
_managementState.value = ModelManagementState.Idle
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
_managementState.value = Deletion.Error(
|
_managementState.value = Deletion.Error(
|
||||||
|
|
@ -371,12 +524,35 @@ class ModelsManagementViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
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 {
|
sealed class ModelManagementState {
|
||||||
object Idle : ModelManagementState()
|
object Idle : ModelManagementState()
|
||||||
|
|
||||||
Loading…
Reference in New Issue