UI: unify Model Card components

This commit is contained in:
Han Yin 2025-04-15 22:31:57 -07:00
parent 434933f5b3
commit c2426a42e5
5 changed files with 264 additions and 268 deletions

View File

@ -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"
)
}
}
}

View File

@ -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"
)
}
}
}

View File

@ -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

View File

@ -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<Message>,

View File

@ -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