diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/MainActivity.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/MainActivity.kt index e866367a70..a5f395ea18 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/MainActivity.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/MainActivity.kt @@ -38,11 +38,11 @@ import com.example.llama.revamp.navigation.NavigationActions import com.example.llama.revamp.ui.scaffold.AnimatedNavHost import com.example.llama.revamp.ui.scaffold.AppNavigationDrawer import com.example.llama.revamp.ui.scaffold.AppScaffold -import com.example.llama.revamp.ui.scaffold.BottomBarConfig import com.example.llama.revamp.ui.scaffold.NavigationIcon import com.example.llama.revamp.ui.scaffold.ScaffoldConfig import com.example.llama.revamp.ui.scaffold.ScaffoldEvent import com.example.llama.revamp.ui.scaffold.TopBarConfig +import com.example.llama.revamp.ui.scaffold.bottombar.BottomBarConfig import com.example.llama.revamp.ui.screens.BenchmarkScreen import com.example.llama.revamp.ui.screens.ConversationScreen import com.example.llama.revamp.ui.screens.ModelLoadingScreen diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/AppScaffold.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/AppScaffold.kt index cbdc13d632..8017a555e1 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/AppScaffold.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/AppScaffold.kt @@ -7,6 +7,11 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.remember +import com.example.llama.revamp.ui.scaffold.bottombar.BenchmarkBottomBar +import com.example.llama.revamp.ui.scaffold.bottombar.BottomBarConfig +import com.example.llama.revamp.ui.scaffold.bottombar.ConversationBottomBar +import com.example.llama.revamp.ui.scaffold.bottombar.ModelSelectionBottomBar +import com.example.llama.revamp.ui.scaffold.bottombar.ModelsManagementBottomBar /** * Configuration of both [TopBarConfig] and [BottomBarConfig] diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/BottomAppBars.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/BottomAppBars.kt deleted file mode 100644 index a98d2d027c..0000000000 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/BottomAppBars.kt +++ /dev/null @@ -1,612 +0,0 @@ -package com.example.llama.revamp.ui.scaffold - -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.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.text.input.TextFieldLineLimits -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.filled.Send -import androidx.compose.material.icons.automirrored.filled.Sort -import androidx.compose.material.icons.automirrored.outlined.Backspace -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.ClearAll -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.FilterAlt -import androidx.compose.material.icons.filled.FolderOpen -import androidx.compose.material.icons.filled.Mic -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material.icons.filled.Replay -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.SearchOff -import androidx.compose.material.icons.filled.SelectAll -import androidx.compose.material.icons.filled.Share -import androidx.compose.material.icons.outlined.AddPhotoAlternate -import androidx.compose.material.icons.outlined.AttachFile -import androidx.compose.material.icons.outlined.DeleteSweep -import androidx.compose.material.icons.outlined.FilterAlt -import androidx.compose.material.icons.outlined.FilterAltOff -import androidx.compose.material3.BottomAppBar -import androidx.compose.material3.BottomAppBarDefaults -import androidx.compose.material3.Checkbox -import androidx.compose.material3.CircularProgressIndicator -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.OutlinedTextField -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.StrokeCap -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.unit.dp -import com.example.llama.R -import com.example.llama.revamp.APP_NAME -import com.example.llama.revamp.data.model.ModelFilter -import com.example.llama.revamp.data.model.ModelInfo -import com.example.llama.revamp.data.model.ModelSortOrder - -/** - * [BottomAppBar] configurations - */ -sealed class BottomBarConfig { - - object None : BottomBarConfig() - - data class ModelSelection( - val search: SearchConfig, - val sorting: SortingConfig, - val filtering: FilteringConfig, - val runAction: RunActionConfig - ) : BottomBarConfig() { - data class SearchConfig( - val isActive: Boolean, - val onToggleSearch: (Boolean) -> Unit, - val textFieldState: TextFieldState, - val onSearch: (String) -> 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, - val onToggleFilter: (ModelFilter, Boolean) -> Unit, - val onClearFilters: () -> Unit, - val isMenuVisible: Boolean, - val toggleMenu: (Boolean) -> Unit - ) - - data class RunActionConfig( - val selectedModel: ModelInfo?, - val onRun: (ModelInfo) -> Unit - ) - } - - data class ModelsManagement( - val sorting: SortingConfig, - val filtering: FilteringConfig, - val selection: SelectionConfig, - val importing: ImportConfig - ) : BottomBarConfig() { - data class SortingConfig( - val currentOrder: ModelSortOrder, - val isMenuVisible: Boolean, - val toggleMenu: (Boolean) -> Unit, - val selectOrder: (ModelSortOrder) -> Unit - ) - - data class FilteringConfig( - val isActive: Boolean, - val filters: Map, - val onToggleFilter: (ModelFilter, Boolean) -> Unit, - val onClearFilters: () -> Unit, - val isMenuVisible: Boolean, - val toggleMenu: (Boolean) -> Unit - ) - - data class SelectionConfig( - val isActive: Boolean, - val toggleMode: (Boolean) -> Unit, - val selectedModels: Map, - val toggleAllSelection: (Boolean) -> Unit, - val deleteSelected: () -> Unit - ) - - data class ImportConfig( - val isMenuVisible: Boolean, - val toggleMenu: (Boolean) -> Unit, - val importFromLocal: () -> Unit, - val importFromHuggingFace: () -> Unit - ) - } - - data class Benchmark( - val engineIdle: Boolean, - val onRerun: () -> Unit, - val onShare: () -> Unit, - ) : BottomBarConfig() - - data class Conversation( - val isEnabled: Boolean, - val textFieldState: TextFieldState, - val onSendClick: () -> Unit, - val onAttachPhotoClick: () -> Unit, - val onAttachFileClick: () -> Unit, - val onAudioInputClick: () -> Unit, - ) : BottomBarConfig() -} - -@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.selectedModel != null, - enter = scaleIn() + fadeIn(), - exit = scaleOut() + fadeOut() - ) { - FloatingActionButton( - onClick = { runAction.selectedModel?.let { runAction.onRun(it) } }, - containerColor = MaterialTheme.colorScheme.primary - ) { - Icon( - imageVector = Icons.Default.PlayArrow, - contentDescription = "Run with selected model" - ) - } - } - } - ) -} - -@Composable -fun ModelsManagementBottomBar( - sorting: BottomBarConfig.ModelsManagement.SortingConfig, - filtering: BottomBarConfig.ModelsManagement.FilteringConfig, - selection: BottomBarConfig.ModelsManagement.SelectionConfig, - importing: BottomBarConfig.ModelsManagement.ImportConfig -) { - BottomAppBar( - actions = { - if (selection.isActive) { - /* Multi-selection mode actions */ - IconButton( - onClick = selection.deleteSelected, - enabled = selection.selectedModels.isNotEmpty() - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = "Delete selected", - tint = if (selection.selectedModels.isNotEmpty()) - MaterialTheme.colorScheme.error - else - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) - ) - } - - IconButton(onClick = { selection.toggleAllSelection(false) }) { - Icon( - imageVector = Icons.Default.ClearAll, - contentDescription = "Deselect all" - ) - } - - IconButton(onClick = { selection.toggleAllSelection(true) }) { - Icon( - imageVector = Icons.Default.SelectAll, - contentDescription = "Select all" - ) - } - } else { - /* Default mode actions */ - - // Multi-selection action - IconButton(onClick = { selection.toggleMode(true) }) { - Icon( - imageVector = Icons.Outlined.DeleteSweep, - contentDescription = "Delete models" - ) - } - - // Sorting action - IconButton(onClick = { sorting.toggleMenu(true) }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.Sort, - contentDescription = "Sort models" - ) - } - - // Sorting dropdown menu - DropdownMenu( - expanded = sorting.isMenuVisible, - onDismissRequest = { sorting.toggleMenu(false) } - ) { - val sortOptions = listOf( - Triple(ModelSortOrder.NAME_ASC, "Name (A-Z)", "Sort by name in ascending order"), - Triple(ModelSortOrder.NAME_DESC, "Name (Z-A)", "Sort by name in descending order"), - Triple(ModelSortOrder.SIZE_ASC, "Size (Smallest first)", "Sort by size in ascending order"), - Triple(ModelSortOrder.SIZE_DESC, "Size (Largest first)", "Sort by size in descending order"), - Triple(ModelSortOrder.LAST_USED, "Last used", "Sort by last used") - ) - - sortOptions.forEach { (order, label, description) -> - DropdownMenuItem( - text = { Text(label) }, - trailingIcon = { - if (sorting.currentOrder == order) - Icon( - imageVector = Icons.Default.Check, - contentDescription = "$description, selected" - ) - }, - onClick = { sorting.selectOrder(order) } - ) - } - } - - // Filtering action - IconButton(onClick = { filtering.toggleMenu(true) }) { - Icon( - imageVector = - if (filtering.isActive) Icons.Default.FilterAlt - else Icons.Outlined.FilterAlt, - contentDescription = "Filter models" - ) - } - - // Filter dropdown menu - DropdownMenu( - expanded = filtering.isMenuVisible, - onDismissRequest = { filtering.toggleMenu(false) } - ) { - Text( - text = "Filter by", - style = MaterialTheme.typography.labelMedium, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) - ) - - filtering.filters.forEach { (filter, isEnabled) -> - DropdownMenuItem( - text = { Text(filter.displayName) }, - leadingIcon = { - Checkbox( - checked = isEnabled, - onCheckedChange = null - ) - }, - onClick = { filtering.onToggleFilter(filter, !isEnabled) } - ) - } - - HorizontalDivider() - - DropdownMenuItem( - text = { Text("Clear filters") }, - leadingIcon = { - Icon( - imageVector = Icons.Outlined.FilterAltOff, - contentDescription = "Clear all filters" - ) - }, - onClick = { - filtering.onClearFilters() - filtering.toggleMenu(false) - } - ) - } - } - }, - floatingActionButton = { - FloatingActionButton( - onClick = { if (selection.isActive) selection.toggleMode(false) else importing.toggleMenu(true) }, - containerColor = MaterialTheme.colorScheme.primaryContainer - ) { - Icon( - imageVector = if (selection.isActive) Icons.Default.Close else Icons.Default.Add, - contentDescription = if (selection.isActive) "Exit selection mode" else "Add model" - ) - } - - // Add model dropdown menu - DropdownMenu( - expanded = importing.isMenuVisible, - onDismissRequest = { importing.toggleMenu(false) } - ) { - DropdownMenuItem( - text = { Text("Import local model") }, - leadingIcon = { - Icon( - imageVector = Icons.Default.FolderOpen, - contentDescription = "Import a local model on the device" - ) - }, - onClick = importing.importFromLocal - ) - DropdownMenuItem( - text = { Text("Download from HuggingFace") }, - leadingIcon = { - Icon( - painter = painterResource(id = R.drawable.logo_huggingface), - contentDescription = "Browse and download a model from HuggingFace", - modifier = Modifier.size(24.dp), - tint = Color.Unspecified, - ) - }, - onClick = importing.importFromHuggingFace - ) - } - } - ) -} - -@Composable -fun BenchmarkBottomBar( - engineIdle: Boolean, - onRerun: () -> Unit, - onShare: () -> Unit, -) { - BottomAppBar( - actions = { - IconButton(onClick = onRerun) { - Icon( - imageVector = Icons.Default.Replay, - contentDescription = "Run the benchmark again", - tint = - if (engineIdle) MaterialTheme.colorScheme.onSurface - else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) - ) - } - }, - floatingActionButton = { - // Only show FAB if the benchmark result is ready - AnimatedVisibility( - visible = engineIdle, - enter = scaleIn() + fadeIn(), - exit = scaleOut() + fadeOut() - ) { - FloatingActionButton( - onClick = onShare, - containerColor = MaterialTheme.colorScheme.primary - ) { - Icon( - imageVector = Icons.Default.Share, - contentDescription = "Share the benchmark results" - ) - } - } - } - ) -} - -@Composable -fun ConversationBottomBar( - textFieldState: TextFieldState, - isReady: Boolean, - onSendClick: () -> Unit, - onAttachPhotoClick: () -> Unit, - onAttachFileClick: () -> Unit, - onAudioInputClick: () -> Unit, -) { - val placeholder = if (isReady) "Message $APP_NAME..." else "Please wait for $APP_NAME to finish" - - Column( - modifier = Modifier.fillMaxWidth() - ) { - Surface( - modifier = Modifier.fillMaxWidth(), - color = BottomAppBarDefaults.containerColor, - tonalElevation = BottomAppBarDefaults.ContainerElevation, - shape = RoundedCornerShape(topStart = 32.dp, topEnd = 32.dp) - ) { - Box( - modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 16.dp, end = 16.dp), - ) { - OutlinedTextField( - state = textFieldState, - modifier = Modifier.fillMaxWidth().padding(end = 8.dp), - enabled = isReady, - placeholder = { Text(placeholder) }, - lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 5), - colors = TextFieldDefaults.colors( - focusedContainerColor = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.5f), - unfocusedContainerColor = MaterialTheme.colorScheme.surface, - disabledContainerColor = MaterialTheme.colorScheme.surfaceDim, - ), - shape = RoundedCornerShape(16.dp), - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Send), - onKeyboardAction = { if (isReady) { onSendClick() } } - ) - } - } - - BottomAppBar( - actions = { - IconButton(onClick = onAttachPhotoClick) { - Icon( - imageVector = Icons.Outlined.AddPhotoAlternate, - contentDescription = "Attach a photo", - ) - } - - IconButton(onClick = onAttachFileClick) { - Icon( - imageVector = Icons.Outlined.AttachFile, - contentDescription = "Attach a file", - ) - } - - IconButton(onClick = onAudioInputClick) { - Icon( - imageVector = Icons.Default.Mic, - contentDescription = "Input with voice", - ) - } - }, - floatingActionButton = { - FloatingActionButton( - onClick = { if (isReady) { onSendClick() } }, - containerColor = MaterialTheme.colorScheme.primary - ) { - if (isReady) { - Icon( - imageVector = Icons.AutoMirrored.Filled.Send, - contentDescription = "Send message", - ) - } else { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - strokeCap = StrokeCap.Round, - color = MaterialTheme.colorScheme.onPrimary, - ) - } - } - } - ) - } -} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/BenchmarkBottomBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/BenchmarkBottomBar.kt new file mode 100644 index 0000000000..049c9441ab --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/BenchmarkBottomBar.kt @@ -0,0 +1,55 @@ +package com.example.llama.revamp.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.Replay +import androidx.compose.material.icons.filled.Share +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 BenchmarkBottomBar( + engineIdle: Boolean, + onRerun: () -> Unit, + onShare: () -> Unit, +) { + BottomAppBar( + actions = { + IconButton(onClick = onRerun) { + Icon( + imageVector = Icons.Default.Replay, + contentDescription = "Run the benchmark again", + tint = + if (engineIdle) MaterialTheme.colorScheme.onSurface + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + ) + } + }, + floatingActionButton = { + // Only show FAB if the benchmark result is ready + AnimatedVisibility( + visible = engineIdle, + enter = scaleIn() + fadeIn(), + exit = scaleOut() + fadeOut() + ) { + FloatingActionButton( + onClick = onShare, + containerColor = MaterialTheme.colorScheme.primary + ) { + Icon( + imageVector = Icons.Default.Share, + contentDescription = "Share the benchmark results" + ) + } + } + } + ) +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/BottomBarConfig.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/BottomBarConfig.kt new file mode 100644 index 0000000000..4dd19dd4fc --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/BottomBarConfig.kt @@ -0,0 +1,102 @@ +package com.example.llama.revamp.ui.scaffold.bottombar + +import androidx.compose.foundation.text.input.TextFieldState +import com.example.llama.revamp.data.model.ModelFilter +import com.example.llama.revamp.data.model.ModelInfo +import com.example.llama.revamp.data.model.ModelSortOrder + +/** + * [BottomAppBar] configurations + */ +sealed class BottomBarConfig { + + object None : BottomBarConfig() + + data class ModelSelection( + val search: SearchConfig, + val sorting: SortingConfig, + val filtering: FilteringConfig, + val runAction: RunActionConfig + ) : BottomBarConfig() { + data class SearchConfig( + val isActive: Boolean, + val onToggleSearch: (Boolean) -> Unit, + val textFieldState: TextFieldState, + val onSearch: (String) -> 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, + val onToggleFilter: (ModelFilter, Boolean) -> Unit, + val onClearFilters: () -> Unit, + val isMenuVisible: Boolean, + val toggleMenu: (Boolean) -> Unit + ) + + data class RunActionConfig( + val selectedModel: ModelInfo?, + val onRun: (ModelInfo) -> Unit + ) + } + + data class ModelsManagement( + val sorting: SortingConfig, + val filtering: FilteringConfig, + val selection: SelectionConfig, + val importing: ImportConfig + ) : BottomBarConfig() { + data class SortingConfig( + val currentOrder: ModelSortOrder, + val isMenuVisible: Boolean, + val toggleMenu: (Boolean) -> Unit, + val selectOrder: (ModelSortOrder) -> Unit + ) + + data class FilteringConfig( + val isActive: Boolean, + val filters: Map, + val onToggleFilter: (ModelFilter, Boolean) -> Unit, + val onClearFilters: () -> Unit, + val isMenuVisible: Boolean, + val toggleMenu: (Boolean) -> Unit + ) + + data class SelectionConfig( + val isActive: Boolean, + val toggleMode: (Boolean) -> Unit, + val selectedModels: Map, + val toggleAllSelection: (Boolean) -> Unit, + val deleteSelected: () -> Unit + ) + + data class ImportConfig( + val isMenuVisible: Boolean, + val toggleMenu: (Boolean) -> Unit, + val importFromLocal: () -> Unit, + val importFromHuggingFace: () -> Unit + ) + } + + data class Benchmark( + val engineIdle: Boolean, + val onRerun: () -> Unit, + val onShare: () -> Unit, + ) : BottomBarConfig() + + data class Conversation( + val isEnabled: Boolean, + val textFieldState: TextFieldState, + val onSendClick: () -> Unit, + val onAttachPhotoClick: () -> Unit, + val onAttachFileClick: () -> Unit, + val onAudioInputClick: () -> Unit, + ) : BottomBarConfig() +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/ConversationBottomBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/ConversationBottomBar.kt new file mode 100644 index 0000000000..6aa2b9f3bb --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/ConversationBottomBar.kt @@ -0,0 +1,131 @@ +package com.example.llama.revamp.ui.scaffold.bottombar + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Send +import androidx.compose.material.icons.filled.Mic +import androidx.compose.material.icons.outlined.AddPhotoAlternate +import androidx.compose.material.icons.outlined.AttachFile +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.BottomAppBarDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.dp +import com.example.llama.revamp.APP_NAME + +@Composable +fun ConversationBottomBar( + textFieldState: TextFieldState, + isReady: Boolean, + onSendClick: () -> Unit, + onAttachPhotoClick: () -> Unit, + onAttachFileClick: () -> Unit, + onAudioInputClick: () -> Unit, +) { + val placeholder = if (isReady) "Message ${APP_NAME}..." else "Please wait for ${APP_NAME} to finish" + + Column( + modifier = Modifier.Companion.fillMaxWidth() + ) { + Surface( + modifier = Modifier.Companion.fillMaxWidth(), + color = BottomAppBarDefaults.containerColor, + tonalElevation = BottomAppBarDefaults.ContainerElevation, + shape = RoundedCornerShape(topStart = 32.dp, topEnd = 32.dp) + ) { + Box( + modifier = Modifier.Companion.fillMaxWidth() + .padding(start = 16.dp, top = 16.dp, end = 16.dp), + ) { + OutlinedTextField( + state = textFieldState, + modifier = Modifier.Companion.fillMaxWidth().padding(end = 8.dp), + enabled = isReady, + placeholder = { Text(placeholder) }, + lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 5), + colors = TextFieldDefaults.colors( + focusedContainerColor = MaterialTheme.colorScheme.secondaryContainer.copy( + alpha = 0.5f + ), + unfocusedContainerColor = MaterialTheme.colorScheme.surface, + disabledContainerColor = MaterialTheme.colorScheme.surfaceDim, + ), + shape = androidx.compose.foundation.shape.RoundedCornerShape(16.dp), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Companion.Send), + onKeyboardAction = { + if (isReady) { + onSendClick() + } + } + ) + } + } + + BottomAppBar( + actions = { + IconButton(onClick = onAttachPhotoClick) { + Icon( + imageVector = Icons.Outlined.AddPhotoAlternate, + contentDescription = "Attach a photo", + ) + } + + IconButton(onClick = onAttachFileClick) { + Icon( + imageVector = Icons.Outlined.AttachFile, + contentDescription = "Attach a file", + ) + } + + IconButton(onClick = onAudioInputClick) { + Icon( + imageVector = Icons.Default.Mic, + contentDescription = "Input with voice", + ) + } + }, + floatingActionButton = { + FloatingActionButton( + onClick = { + if (isReady) { + onSendClick() + } + }, + containerColor = MaterialTheme.colorScheme.primary + ) { + if (isReady) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Send, + contentDescription = "Send message", + ) + } else { + CircularProgressIndicator( + modifier = Modifier.Companion.size(24.dp), + strokeCap = StrokeCap.Companion.Round, + color = MaterialTheme.colorScheme.onPrimary, + ) + } + } + } + ) + } +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/ModelSelectionBottomBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/ModelSelectionBottomBar.kt new file mode 100644 index 0000000000..d047e14e90 --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/ModelSelectionBottomBar.kt @@ -0,0 +1,192 @@ +package com.example.llama.revamp.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.revamp.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.Companion.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.selectedModel != null, + enter = scaleIn() + fadeIn(), + exit = scaleOut() + fadeOut() + ) { + FloatingActionButton( + onClick = { runAction.selectedModel?.let { runAction.onRun(it) } }, + containerColor = MaterialTheme.colorScheme.primary + ) { + Icon( + imageVector = Icons.Default.PlayArrow, + contentDescription = "Run with selected model" + ) + } + } + } + ) +} diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/ModelsManagementBottomBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/ModelsManagementBottomBar.kt new file mode 100644 index 0000000000..9d6ce59e9a --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/ModelsManagementBottomBar.kt @@ -0,0 +1,234 @@ +package com.example.llama.revamp.ui.scaffold.bottombar + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Sort +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.ClearAll +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.FilterAlt +import androidx.compose.material.icons.filled.FolderOpen +import androidx.compose.material.icons.filled.SelectAll +import androidx.compose.material.icons.outlined.DeleteSweep +import androidx.compose.material.icons.outlined.FilterAlt +import androidx.compose.material.icons.outlined.FilterAltOff +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.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.example.llama.R +import com.example.llama.revamp.data.model.ModelSortOrder + +@Composable +fun ModelsManagementBottomBar( + sorting: BottomBarConfig.ModelsManagement.SortingConfig, + filtering: BottomBarConfig.ModelsManagement.FilteringConfig, + selection: BottomBarConfig.ModelsManagement.SelectionConfig, + importing: BottomBarConfig.ModelsManagement.ImportConfig +) { + BottomAppBar( + actions = { + if (selection.isActive) { + /* Multi-selection mode actions */ + IconButton( + onClick = selection.deleteSelected, + enabled = selection.selectedModels.isNotEmpty() + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = "Delete selected", + tint = if (selection.selectedModels.isNotEmpty()) + MaterialTheme.colorScheme.error + else + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + ) + } + + IconButton(onClick = { selection.toggleAllSelection(false) }) { + Icon( + imageVector = Icons.Default.ClearAll, + contentDescription = "Deselect all" + ) + } + + IconButton(onClick = { selection.toggleAllSelection(true) }) { + Icon( + imageVector = Icons.Default.SelectAll, + contentDescription = "Select all" + ) + } + } else { + /* Default mode actions */ + + // Multi-selection action + IconButton(onClick = { selection.toggleMode(true) }) { + Icon( + imageVector = Icons.Outlined.DeleteSweep, + contentDescription = "Delete models" + ) + } + + // Sorting action + IconButton(onClick = { sorting.toggleMenu(true) }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.Sort, + contentDescription = "Sort models" + ) + } + + // Sorting dropdown menu + DropdownMenu( + expanded = sorting.isMenuVisible, + onDismissRequest = { sorting.toggleMenu(false) } + ) { + val sortOptions = listOf( + Triple( + ModelSortOrder.NAME_ASC, + "Name (A-Z)", + "Sort by name in ascending order" + ), + Triple( + ModelSortOrder.NAME_DESC, + "Name (Z-A)", + "Sort by name in descending order" + ), + Triple( + ModelSortOrder.SIZE_ASC, + "Size (Smallest first)", + "Sort by size in ascending order" + ), + Triple( + ModelSortOrder.SIZE_DESC, + "Size (Largest first)", + "Sort by size in descending order" + ), + Triple(ModelSortOrder.LAST_USED, "Last used", "Sort by last used") + ) + + sortOptions.forEach { (order, label, description) -> + DropdownMenuItem( + text = { Text(label) }, + trailingIcon = { + if (sorting.currentOrder == order) + Icon( + imageVector = Icons.Default.Check, + contentDescription = "$description, selected" + ) + }, + onClick = { sorting.selectOrder(order) } + ) + } + } + + // Filtering action + IconButton(onClick = { filtering.toggleMenu(true) }) { + Icon( + imageVector = + if (filtering.isActive) Icons.Default.FilterAlt + else Icons.Outlined.FilterAlt, + contentDescription = "Filter models" + ) + } + + // Filter dropdown menu + DropdownMenu( + expanded = filtering.isMenuVisible, + onDismissRequest = { filtering.toggleMenu(false) } + ) { + Text( + text = "Filter by", + style = MaterialTheme.typography.labelMedium, + modifier = Modifier.Companion.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 = { + FloatingActionButton( + onClick = { + if (selection.isActive) selection.toggleMode(false) else importing.toggleMenu( + true + ) + }, + containerColor = MaterialTheme.colorScheme.primaryContainer + ) { + Icon( + imageVector = if (selection.isActive) Icons.Default.Close else Icons.Default.Add, + contentDescription = if (selection.isActive) "Exit selection mode" else "Add model" + ) + } + + // Add model dropdown menu + DropdownMenu( + expanded = importing.isMenuVisible, + onDismissRequest = { importing.toggleMenu(false) } + ) { + DropdownMenuItem( + text = { Text("Import local model") }, + leadingIcon = { + Icon( + imageVector = Icons.Default.FolderOpen, + contentDescription = "Import a local model on the device" + ) + }, + onClick = importing.importFromLocal + ) + DropdownMenuItem( + text = { Text("Download from HuggingFace") }, + leadingIcon = { + Icon( + painter = painterResource(id = R.drawable.logo_huggingface), + contentDescription = "Browse and download a model from HuggingFace", + modifier = Modifier.Companion.size(24.dp), + tint = Color.Companion.Unspecified, + ) + }, + onClick = importing.importFromHuggingFace + ) + } + } + ) +}