From d7afcc41d5af80e91e44fa0f2cd3a73066b93bb8 Mon Sep 17 00:00:00 2001 From: Han Yin Date: Sun, 20 Apr 2025 17:53:42 -0700 Subject: [PATCH] UI: polish ModelLoading screen --- .../llama/revamp/ui/components/ModelCards.kt | 107 +++++++++++------- .../revamp/ui/screens/BenchmarkScreen.kt | 16 ++- .../revamp/ui/screens/ConversationScreen.kt | 1 + .../revamp/ui/screens/ModelLoadingScreen.kt | 29 +++-- .../revamp/ui/screens/ModelSelectionScreen.kt | 4 +- .../ui/screens/ModelsManagementScreen.kt | 4 +- 6 files changed, 103 insertions(+), 58 deletions(-) diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/ModelCards.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/ModelCards.kt index 59a522e26f..136e4b94b8 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/ModelCards.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/ModelCards.kt @@ -17,11 +17,16 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ExpandLess +import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material3.AssistChip import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.LocalMinimumInteractiveComponentSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -49,44 +54,70 @@ import java.util.Locale * architecture, quantization and file size in a compact card format. * * @param model The model information to display - * @param onClick Action to perform when the card is clicked - * @param isSelected Optional selection state (shows checkbox when not null) + * @param isExpanded Whether additional details is expanded or shrunk + * @param onExpanded Action to perform when the card is expanded or shrunk */ @Composable -fun ModelCardCore( +fun ModelCardCoreExpandable( model: ModelInfo, - onClick: () -> Unit, - isSelected: Boolean? = null, + isExpanded: Boolean = false, + onExpanded: ((Boolean) -> Unit)? = null, ) { Card( modifier = Modifier .fillMaxWidth() - .clickable(onClick = onClick), - colors = when (isSelected) { - true -> CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) + .clickable { onExpanded?.invoke(!isExpanded) }, + colors = when (isExpanded) { + true -> CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ) false -> CardDefaults.cardColors() - else -> CardDefaults.cardColors() }, elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { - Row( + Column( modifier = Modifier.padding(16.dp), - verticalAlignment = Alignment.CenterVertically ) { - // Show checkbox if in selection mode - if (isSelected != null) { - Checkbox( - checked = isSelected, - onCheckedChange = { onClick() }, - modifier = Modifier.padding(end = 8.dp) - ) + // Row 1: Model full name + chevron + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + ModelCardContentTitleRow(model) + + CompositionLocalProvider( + LocalMinimumInteractiveComponentSize provides Dp.Unspecified + ) { + IconButton(onClick = { onExpanded?.invoke(!isExpanded) }) { + Icon( + imageVector = if (isExpanded) Icons.Default.ExpandLess else Icons.Default.ExpandMore, + contentDescription = "Tap to ${if (isExpanded) "shrink" else "expand"} model card" + ) + } + } } - // Core model info - ModelCardContentCore( - model = model, - modifier = Modifier.weight(1f) - ) + // Expandable content + AnimatedVisibility( + visible = isExpanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Spacer(modifier = Modifier.height(8.dp)) + + // Row 2: Context length, size label + ModelCardContentContextRow(model) + + Spacer(modifier = Modifier.height(8.dp)) + + // Row 3: Architecture, quantization, formatted size + ModelCardContentArchitectureRow(model) + } + } } } } @@ -106,7 +137,7 @@ fun ModelCardCore( */ @OptIn(ExperimentalLayoutApi::class) @Composable -fun ModelCardExpandable( +fun ModelCardFullExpandable( model: ModelInfo, isSelected: Boolean? = null, onSelected: ((Boolean) -> Unit)? = null, @@ -117,11 +148,11 @@ fun ModelCardExpandable( Card( modifier = Modifier .fillMaxWidth() - .clickable { - onExpanded?.invoke(!isExpanded) - }, + .clickable { onExpanded?.invoke(!isExpanded) }, colors = when (isSelected) { - true -> CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) + true -> CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ) false -> CardDefaults.cardColors() else -> CardDefaults.cardColors() }, @@ -148,11 +179,7 @@ fun ModelCardExpandable( .padding(start = 16.dp, top = 16.dp, end = 16.dp) ) { // Row 1: Model full name - Text( - text = model.formattedFullName, - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Medium - ) + ModelCardContentTitleRow(model) } } @@ -283,11 +310,7 @@ fun ModelCardContentCore( ) = Column(modifier = modifier) { // Row 1: Model full name - Text( - text = model.formattedFullName, - style = MaterialTheme.typography.headlineSmall, // TODO - fontWeight = FontWeight.Medium - ) + ModelCardContentTitleRow(model) Spacer(modifier = Modifier.height(12.dp)) @@ -300,6 +323,14 @@ fun ModelCardContentCore( ModelCardContentArchitectureRow(model) } +@Composable +private fun ModelCardContentTitleRow(model: ModelInfo) = + Text( + text = model.formattedFullName, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Medium + ) + @Composable private fun ModelCardContentContextRow(model: ModelInfo) = Row( diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/BenchmarkScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/BenchmarkScreen.kt index 46e15a39f8..c68616528f 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/BenchmarkScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/BenchmarkScreen.kt @@ -21,26 +21,34 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.example.llama.revamp.engine.ModelLoadingMetrics -import com.example.llama.revamp.ui.components.ModelCardCore +import com.example.llama.revamp.ui.components.ModelCardCoreExpandable import com.example.llama.revamp.ui.components.ModelUnloadDialogHandler import com.example.llama.revamp.ui.theme.MonospacedTextStyle import com.example.llama.revamp.viewmodel.BenchmarkViewModel @Composable fun BenchmarkScreen( + // TODO-han.yin: Use loading metrics to show UI loadingMetrics: ModelLoadingMetrics, onNavigateBack: () -> Unit, viewModel: BenchmarkViewModel ) { + // View model states val engineState by viewModel.engineState.collectAsState() val benchmarkResults by viewModel.benchmarkResults.collectAsState() val selectedModel by viewModel.selectedModel.collectAsState() val unloadDialogState by viewModel.unloadModelState.collectAsState() + // UI states + var isModelCardExpanded by remember { mutableStateOf(false) } + // Run benchmark when entering the screen LaunchedEffect(selectedModel) { viewModel.runBenchmark() @@ -59,10 +67,10 @@ fun BenchmarkScreen( ) { // Selected model card selectedModel?.let { model -> - ModelCardCore( + ModelCardCoreExpandable( model = model, - onClick = { /* No action on click */ }, - isSelected = null + isExpanded = isModelCardExpanded, + onExpanded = { isModelCardExpanded = !isModelCardExpanded }, ) } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ConversationScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ConversationScreen.kt index 19584f2dd6..69477dd0c8 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ConversationScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ConversationScreen.kt @@ -69,6 +69,7 @@ import kotlinx.coroutines.launch */ @Composable fun ConversationScreen( + // TODO-han.yin: Use loading metrics to show UI loadingMetrics: ModelLoadingMetrics, onNavigateBack: () -> Unit, viewModel: ConversationViewModel diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelLoadingScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelLoadingScreen.kt index 666a1fc5ee..af554bdb78 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelLoadingScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelLoadingScreen.kt @@ -50,7 +50,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.example.llama.revamp.data.model.SystemPrompt import com.example.llama.revamp.engine.ModelLoadingMetrics -import com.example.llama.revamp.ui.components.ModelCardCore +import com.example.llama.revamp.ui.components.ModelCardCoreExpandable import com.example.llama.revamp.ui.components.ModelUnloadDialogHandler import com.example.llama.revamp.viewmodel.ModelLoadingViewModel @@ -72,12 +72,15 @@ fun ModelLoadingScreen( onNavigateToConversation: (ModelLoadingMetrics) -> Unit, viewModel: ModelLoadingViewModel, ) { + // View model states val engineState by viewModel.engineState.collectAsState() val selectedModel by viewModel.selectedModel.collectAsState() val presetPrompts by viewModel.presetPrompts.collectAsState() val recentPrompts by viewModel.recentPrompts.collectAsState() val unloadDialogState by viewModel.unloadModelState.collectAsState() + // UI states + var isModelCardExpanded by remember { mutableStateOf(false) } var selectedMode by remember { mutableStateOf(null) } var useSystemPrompt by remember { mutableStateOf(false) } var selectedPrompt by remember { mutableStateOf(null) } @@ -116,11 +119,13 @@ fun ModelLoadingScreen( ) { // Selected model card selectedModel?.let { model -> - ModelCardCore( + ModelCardCoreExpandable( model = model, - onClick = { /* No action on click */ }, - isSelected = null + isExpanded = isModelCardExpanded, + onExpanded = { isModelCardExpanded = !isModelCardExpanded }, ) + + Spacer(modifier = Modifier.height(16.dp)) } // Benchmark card @@ -148,7 +153,7 @@ fun ModelLoadingScreen( ) Text( text = "Benchmark", - style = MaterialTheme.typography.titleLarge, + style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(start = 8.dp) ) } @@ -159,6 +164,12 @@ fun ModelLoadingScreen( modifier = Modifier .fillMaxWidth() .padding(bottom = 4.dp) + .selectable( + selected = selectedMode == Mode.CONVERSATION, + onClick = { selectedMode = Mode.CONVERSATION }, + enabled = !isLoading, + role = Role.RadioButton + ) // Only fill height if system prompt is active .then(if (useSystemPrompt) Modifier.weight(1f) else Modifier) ) { @@ -173,12 +184,6 @@ fun ModelLoadingScreen( Row( modifier = Modifier .fillMaxWidth() - .selectable( - selected = selectedMode == Mode.CONVERSATION, - onClick = { selectedMode = Mode.CONVERSATION }, - enabled = !isLoading, - role = Role.RadioButton - ) .padding(top = 16.dp, start = 16.dp, end = 16.dp), verticalAlignment = Alignment.CenterVertically ) { @@ -188,7 +193,7 @@ fun ModelLoadingScreen( ) Text( text = "Conversation", - style = MaterialTheme.typography.titleLarge, + style = MaterialTheme.typography.titleMedium, modifier = Modifier.padding(start = 8.dp) ) } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelSelectionScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelSelectionScreen.kt index 4d271eabde..87f807a5a2 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelSelectionScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelSelectionScreen.kt @@ -34,7 +34,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.example.llama.revamp.data.model.ModelInfo -import com.example.llama.revamp.ui.components.ModelCardExpandable +import com.example.llama.revamp.ui.components.ModelCardFullExpandable import com.example.llama.revamp.viewmodel.ModelSelectionViewModel @OptIn(ExperimentalMaterial3Api::class) @@ -65,7 +65,7 @@ fun ModelSelectionScreen( verticalArrangement = Arrangement.spacedBy(12.dp) ) { items(items = models, key = { it.id }) { model -> - ModelCardExpandable( + ModelCardFullExpandable( model = model, isSelected = if (model == preselectedModel) true else null, onSelected = { selected -> diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelsManagementScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelsManagementScreen.kt index 7af1b1d02c..abe622918b 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelsManagementScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModelsManagementScreen.kt @@ -36,7 +36,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import com.example.llama.revamp.data.model.ModelInfo -import com.example.llama.revamp.ui.components.ModelCardExpandable +import com.example.llama.revamp.ui.components.ModelCardFullExpandable import com.example.llama.revamp.ui.components.ScaffoldEvent import com.example.llama.revamp.util.formatFileByteSize import com.example.llama.revamp.viewmodel.ModelManagementState @@ -84,7 +84,7 @@ fun ModelsManagementScreen( items(items = sortedModels, key = { it.id }) { model -> val isSelected = if (isMultiSelectionMode) selectedModels.contains(model.id) else null - ModelCardExpandable( + ModelCardFullExpandable( model = model, isSelected = isSelected, onSelected = {