From c2426a42e5c00184ddbd770a12c55d45ce8b417c Mon Sep 17 00:00:00 2001 From: Han Yin Date: Tue, 15 Apr 2025 22:31:57 -0700 Subject: [PATCH] UI: unify Model Card components --- .../llama/revamp/ui/components/ModelCard.kt | 152 ----------- .../llama/revamp/ui/components/ModelCards.kt | 249 ++++++++++++++++++ .../revamp/ui/screens/BenchmarkScreen.kt | 29 +- .../revamp/ui/screens/ConversationScreen.kt | 84 +----- .../revamp/ui/screens/ModelLoadingScreen.kt | 18 +- 5 files changed, 264 insertions(+), 268 deletions(-) delete mode 100644 examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/ModelCard.kt create mode 100644 examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/ModelCards.kt diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/ModelCard.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/ModelCard.kt deleted file mode 100644 index 4361861478..0000000000 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/ModelCard.kt +++ /dev/null @@ -1,152 +0,0 @@ -package com.example.llama.revamp.ui.components - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -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.Info -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Checkbox -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.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import com.example.llama.revamp.data.model.ModelInfo -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale - -/** - * Reusable card component for displaying model information. - * Can be configured for selection mode or normal display mode. - */ -@Composable -fun ModelCard( - model: ModelInfo, - onClick: () -> Unit, - modifier: Modifier = Modifier, - isSelected: Boolean? = null, // `null`: not in selection mode, otherwise true/false - actionButton: @Composable (() -> Unit)? = null -) { - Card( - modifier = modifier - .fillMaxWidth() - .clickable(onClick = onClick), - colors = when { - isSelected == true -> CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) - isSelected == false -> CardDefaults.cardColors() - else -> CardDefaults.cardColors() // Not in selection mode - }, - elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) - ) { - Row( - 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) - ) - } - - // Model info - Column(modifier = Modifier.weight(1f)) { - Text( - text = model.name, - style = MaterialTheme.typography.titleMedium - ) - - Spacer(modifier = Modifier.height(4.dp)) - - // Model details row (parameters, quantization, size) - Row { - if (model.parameters != null) { - Text( - text = model.parameters, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - if (model.quantization != null) { - Text( - text = " • ${model.quantization}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - Text( - text = " • ${model.formattedSize}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - Spacer(modifier = Modifier.height(4.dp)) - - // Context length - if (model.contextLength != null) { - Text( - text = "Context Length: ${model.contextLength}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - // Last used date - model.lastUsed?.let { lastUsed -> - val dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.getDefault()) - Text( - text = "Last used: ${dateFormat.format(Date(lastUsed))}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - // Custom action button or built-in ones - actionButton?.invoke() ?: Spacer(modifier = Modifier.width(8.dp)) - } - } -} - -/** - * Predefined action buttons for ModelCard - */ -object ModelCardActions { - @Composable - fun PlayButton(onClick: () -> Unit) { - IconButton(onClick = onClick) { - Icon( - imageVector = Icons.Default.PlayArrow, - contentDescription = "Select model", - tint = MaterialTheme.colorScheme.primary - ) - } - } - - @Composable - fun InfoButton(onClick: () -> Unit) { - IconButton(onClick = onClick) { - Icon( - imageVector = Icons.Default.Info, - contentDescription = "Model details" - ) - } - } -} 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 new file mode 100644 index 0000000000..8d0c1d45b0 --- /dev/null +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/components/ModelCards.kt @@ -0,0 +1,249 @@ +package com.example.llama.revamp.ui.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.Info +import androidx.compose.material.icons.filled.PlayArrow +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.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.data.model.ModelInfo +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +/** + * Standard model card for selection lists + */ +@Composable +fun ModelCard( + model: ModelInfo, + onClick: () -> Unit, + modifier: Modifier = Modifier, + isSelected: Boolean? = null, + actionButton: @Composable (() -> Unit)? = null +) { + Card( + modifier = modifier + .fillMaxWidth() + .clickable(onClick = onClick), + colors = when { + isSelected == true -> CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) + isSelected == false -> CardDefaults.cardColors() + else -> CardDefaults.cardColors() // Not in selection mode + }, + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Row( + 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) + ) + } + + // Model info + ModelInfoContent( + model = model, + modifier = Modifier.weight(1f), + contentPadding = PaddingValues(0.dp) + ) + + // Custom action button or built-in ones + actionButton?.invoke() ?: Spacer(modifier = Modifier.width(8.dp)) + } + } +} + +/** + * Expandable card that shows model info and system prompt + */ +@Composable +fun ModelCardWithSystemPrompt( + model: ModelInfo, + systemPrompt: String?, + modifier: Modifier = Modifier, + initiallyExpanded: Boolean = false +) { + var expanded by remember { mutableStateOf(initiallyExpanded) } + + Card( + modifier = modifier + .fillMaxWidth(), + onClick = { + if (systemPrompt != null) { + expanded = !expanded + } + } + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + // Model info section + ModelInfoContent( + model = model, + contentPadding = PaddingValues(0.dp) + ) + + // Add divider between model info and system prompt + if (!systemPrompt.isNullOrBlank()) { + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "System Prompt", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.weight(1f) + ) + + Text( + text = if (expanded) "Hide" else "Show", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary + ) + } + + AnimatedVisibility( + visible = expanded, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + Text( + text = systemPrompt, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) + ) + } + } + } + } +} + + +/** + * Core model info display component that can be used by other card variants + */ +@Composable +private fun ModelInfoContent( + model: ModelInfo, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(16.dp) +) { + Column(modifier = modifier.padding(contentPadding)) { + Text( + text = model.name, + style = MaterialTheme.typography.titleMedium + ) + + Spacer(modifier = Modifier.height(4.dp)) + + // Model details row (parameters, quantization, size) + Row { + if (model.parameters != null) { + Text( + text = model.parameters, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + if (model.quantization != null) { + Text( + text = " • ${model.quantization}", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Text( + text = " • ${model.formattedSize}", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Spacer(modifier = Modifier.height(4.dp)) + + // Context length + if (model.contextLength != null) { + Text( + text = "Context Length: ${model.contextLength}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + // Last used date + model.lastUsed?.let { lastUsed -> + val dateFormat = SimpleDateFormat("MMM d, yyyy", Locale.getDefault()) + Text( + text = "Last used: ${dateFormat.format(Date(lastUsed))}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +/** + * Predefined action buttons for ModelCard + */ +object ModelCardActions { + @Composable + fun PlayButton(onClick: () -> Unit) { + IconButton(onClick = onClick) { + Icon( + imageVector = Icons.Default.PlayArrow, + contentDescription = "Select model", + tint = MaterialTheme.colorScheme.primary + ) + } + } + + @Composable + fun InfoButton(onClick: () -> Unit) { + IconButton(onClick = onClick) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = "Model details" + ) + } + } +} 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 42c46a59cc..28e2661d69 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 @@ -24,6 +24,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import com.example.llama.revamp.engine.InferenceEngine +import com.example.llama.revamp.ui.components.ModelCard import com.example.llama.revamp.ui.components.PerformanceAppScaffold import com.example.llama.revamp.ui.theme.MonospacedTextStyle import com.example.llama.revamp.viewmodel.BenchmarkViewModel @@ -54,28 +55,14 @@ fun BenchmarkScreen( .padding(16.dp) .verticalScroll(rememberScrollState()) ) { - // Model info + // Selected model card selectedModel?.let { model -> - Card( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp) - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - Text( - text = model.name, - style = MaterialTheme.typography.titleLarge - ) - - Text( - text = "${model.parameters} [${model.quantization}]", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } + ModelCard( + model = model, + onClick = { /* No action on click */ }, + modifier = Modifier.padding(bottom = 16.dp), + isSelected = null + ) } // Benchmark results or loading indicator 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 a73e791b86..54e8f22b93 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 @@ -63,6 +63,7 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import com.example.llama.revamp.data.model.ModelInfo import com.example.llama.revamp.engine.InferenceEngine +import com.example.llama.revamp.ui.components.ModelCardWithSystemPrompt import com.example.llama.revamp.ui.components.PerformanceAppScaffold import com.example.llama.revamp.viewmodel.ConversationViewModel import com.example.llama.revamp.viewmodel.Message @@ -162,89 +163,6 @@ fun ConversationScreen( } } -@Composable -private fun ModelCardWithSystemPrompt( - selectedModel: ModelInfo, - systemPrompt: String? -) { - var expanded by remember { mutableStateOf(false) } - - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 8.dp), - onClick = { - expanded = !expanded - } - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - // Show model info first - Text( - text = selectedModel.name, - style = MaterialTheme.typography.titleMedium - ) - - Text( - text = "${selectedModel.parameters ?: ""} ${selectedModel.quantization ?: ""} • ${selectedModel.formattedSize}", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - - if (selectedModel.contextLength != null) { - Text( - text = "Context: ${selectedModel.contextLength}", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - // Add divider between model info and system prompt - if (!systemPrompt.isNullOrBlank()) { - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp) - ) - } - - // Only show system prompt section if one exists - if (!systemPrompt.isNullOrBlank()) { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "System Prompt", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.weight(1f) - ) - - Text( - text = if (expanded) "Hide" else "Show", - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.primary - ) - } - - AnimatedVisibility( - visible = expanded, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - Text( - text = systemPrompt, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp) - ) - } - } - } - } -} - @Composable fun ConversationMessageList( messages: List, 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 19db6dc413..412726a8cc 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 @@ -131,18 +131,12 @@ fun ModelLoadingScreen( ) { // Selected model card selectedModel?.let { model -> - Card( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp) - ) { - ModelCard( - model = model, - onClick = { /* TODO-han.yin: expand & shrink */ }, - isSelected = null, - modifier = Modifier.padding(vertical = 0.dp) - ) - } + ModelCard( + model = model, + onClick = { /* No action on click */ }, + modifier = Modifier.padding(bottom = 16.dp), + isSelected = null + ) } // Benchmark card