UI: polish model cards on Benchmark and Conversation screens to show model loading metrics

This commit is contained in:
Han Yin 2025-04-20 18:37:48 -07:00
parent 10ca2fa834
commit 56a7272858
4 changed files with 158 additions and 119 deletions

View File

@ -32,10 +32,7 @@ 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.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontStyle
@ -48,10 +45,32 @@ import java.util.Date
import java.util.Locale import java.util.Locale
/** /**
* Displays model information in a card format with core details. * Displays model information in a card format expandable with core details
* such as context length, architecture, quantization and file size
* *
* This component shows essential model information like name, context length, * @param model The model information to display
* architecture, quantization and file size in a compact card format. * @param isExpanded Whether additional details is expanded or shrunk
* @param onExpanded Action to perform when the card is expanded or shrunk
*/
@Composable
fun ModelCardCoreExpandable(
model: ModelInfo,
isExpanded: Boolean = false,
onExpanded: ((Boolean) -> Unit)? = null
) = ModelCardCoreExpandable(model, isExpanded, onExpanded) {
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)
}
/**
* Displays model information in a card format expandable with customizable extra content.
* *
* @param model The model information to display * @param model The model information to display
* @param isExpanded Whether additional details is expanded or shrunk * @param isExpanded Whether additional details is expanded or shrunk
@ -62,6 +81,7 @@ fun ModelCardCoreExpandable(
model: ModelInfo, model: ModelInfo,
isExpanded: Boolean = false, isExpanded: Boolean = false,
onExpanded: ((Boolean) -> Unit)? = null, onExpanded: ((Boolean) -> Unit)? = null,
expandableSection: @Composable () -> Unit
) { ) {
Card( Card(
modifier = Modifier modifier = Modifier
@ -107,15 +127,7 @@ fun ModelCardCoreExpandable(
Column( Column(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
Spacer(modifier = Modifier.height(8.dp)) expandableSection()
// Row 2: Context length, size label
ModelCardContentContextRow(model)
Spacer(modifier = Modifier.height(8.dp))
// Row 3: Architecture, quantization, formatted size
ModelCardContentArchitectureRow(model)
} }
} }
} }
@ -234,97 +246,14 @@ fun ModelCardFullExpandable(
} }
} }
/** /*
* Expandable card that shows model info and system prompt *
* Individual components to be orchestrated into a model card's content
*
*/ */
@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
ModelCardContentCore(
model = model
)
// 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)
)
}
}
}
}
}
@Composable @Composable
fun ModelCardContentCore( fun ModelCardContentTitleRow(model: ModelInfo) =
model: ModelInfo,
modifier: Modifier = Modifier
) =
Column(modifier = modifier) {
// Row 1: Model full name
ModelCardContentTitleRow(model)
Spacer(modifier = Modifier.height(12.dp))
// Row 2: Context length, size label
ModelCardContentContextRow(model)
Spacer(modifier = Modifier.height(8.dp))
// Row 3: Architecture, quantization, formatted size
ModelCardContentArchitectureRow(model)
}
@Composable
private fun ModelCardContentTitleRow(model: ModelInfo) =
Text( Text(
text = model.formattedFullName, text = model.formattedFullName,
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleLarge,
@ -332,7 +261,7 @@ private fun ModelCardContentTitleRow(model: ModelInfo) =
) )
@Composable @Composable
private fun ModelCardContentContextRow(model: ModelInfo) = fun ModelCardContentContextRow(model: ModelInfo) =
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween horizontalArrangement = Arrangement.SpaceBetween
@ -343,7 +272,7 @@ private fun ModelCardContentContextRow(model: ModelInfo) =
} }
@Composable @Composable
private fun ModelCardContentArchitectureRow(model: ModelInfo) = fun ModelCardContentArchitectureRow(model: ModelInfo) =
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) { ) {
@ -355,7 +284,7 @@ private fun ModelCardContentArchitectureRow(model: ModelInfo) =
} }
@Composable @Composable
private fun ModelCardContentDatesRow(model: ModelInfo) { fun ModelCardContentDatesRow(model: ModelInfo) {
val dateFormatter = remember { SimpleDateFormat("MMM d, yyyy", Locale.getDefault()) } val dateFormatter = remember { SimpleDateFormat("MMM d, yyyy", Locale.getDefault()) }
Row( Row(
@ -374,7 +303,7 @@ private fun ModelCardContentDatesRow(model: ModelInfo) {
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun ModelCardContentTagsSection(tags: List<String>) = fun ModelCardContentTagsSection(tags: List<String>) =
FlowRow( FlowRow(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
@ -398,7 +327,7 @@ private fun ModelCardContentTagsSection(tags: List<String>) =
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
private fun ModelCardContentLanguagesSections(languages: List<String>) = fun ModelCardContentLanguagesSections(languages: List<String>) =
FlowRow( FlowRow(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
@ -421,7 +350,7 @@ private fun ModelCardContentLanguagesSections(languages: List<String>) =
} }
@Composable @Composable
private fun ModelCardContentField(name: String, value: String) = fun ModelCardContentField(name: String, value: String) =
Row { Row {
Text( Text(
text = name, text = name,

View File

@ -27,15 +27,19 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.example.llama.revamp.data.model.ModelInfo
import com.example.llama.revamp.engine.ModelLoadingMetrics import com.example.llama.revamp.engine.ModelLoadingMetrics
import com.example.llama.revamp.ui.components.ModelCardContentArchitectureRow
import com.example.llama.revamp.ui.components.ModelCardContentContextRow
import com.example.llama.revamp.ui.components.ModelCardContentField
import com.example.llama.revamp.ui.components.ModelCardCoreExpandable import com.example.llama.revamp.ui.components.ModelCardCoreExpandable
import com.example.llama.revamp.ui.components.ModelUnloadDialogHandler import com.example.llama.revamp.ui.components.ModelUnloadDialogHandler
import com.example.llama.revamp.ui.theme.MonospacedTextStyle import com.example.llama.revamp.ui.theme.MonospacedTextStyle
import com.example.llama.revamp.util.formatMilliSeconds
import com.example.llama.revamp.viewmodel.BenchmarkViewModel import com.example.llama.revamp.viewmodel.BenchmarkViewModel
@Composable @Composable
fun BenchmarkScreen( fun BenchmarkScreen(
// TODO-han.yin: Use loading metrics to show UI
loadingMetrics: ModelLoadingMetrics, loadingMetrics: ModelLoadingMetrics,
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
viewModel: BenchmarkViewModel viewModel: BenchmarkViewModel
@ -67,8 +71,9 @@ fun BenchmarkScreen(
) { ) {
// Selected model card // Selected model card
selectedModel?.let { model -> selectedModel?.let { model ->
ModelCardCoreExpandable( ModelCardWithLoadingMetrics(
model = model, model = model,
loadingMetrics = loadingMetrics,
isExpanded = isModelCardExpanded, isExpanded = isModelCardExpanded,
onExpanded = { isModelCardExpanded = !isModelCardExpanded }, onExpanded = { isModelCardExpanded = !isModelCardExpanded },
) )
@ -141,3 +146,26 @@ fun BenchmarkScreen(
onNavigateBack = onNavigateBack, onNavigateBack = onNavigateBack,
) )
} }
@Composable
private fun ModelCardWithLoadingMetrics(
model: ModelInfo,
loadingMetrics: ModelLoadingMetrics,
isExpanded: Boolean = false,
onExpanded: ((Boolean) -> Unit)? = null,
) = ModelCardCoreExpandable(model, isExpanded, onExpanded) {
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)
Spacer(modifier = Modifier.height(8.dp))
// Row 4: Model loading time
ModelCardContentField("Loading time", formatMilliSeconds(loadingMetrics.modelLoadingTimeMs))
}

View File

@ -30,6 +30,7 @@ import androidx.compose.material.icons.filled.Send
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
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
@ -52,14 +53,21 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.LocalLifecycleOwner
import com.example.llama.revamp.APP_NAME import com.example.llama.revamp.APP_NAME
import com.example.llama.revamp.data.model.ModelInfo
import com.example.llama.revamp.engine.ModelLoadingMetrics import com.example.llama.revamp.engine.ModelLoadingMetrics
import com.example.llama.revamp.ui.components.ModelCardWithSystemPrompt import com.example.llama.revamp.ui.components.ModelCardContentArchitectureRow
import com.example.llama.revamp.ui.components.ModelCardContentContextRow
import com.example.llama.revamp.ui.components.ModelCardContentField
import com.example.llama.revamp.ui.components.ModelCardCoreExpandable
import com.example.llama.revamp.ui.components.ModelUnloadDialogHandler import com.example.llama.revamp.ui.components.ModelUnloadDialogHandler
import com.example.llama.revamp.util.formatMilliSeconds
import com.example.llama.revamp.viewmodel.ConversationViewModel import com.example.llama.revamp.viewmodel.ConversationViewModel
import com.example.llama.revamp.viewmodel.Message import com.example.llama.revamp.viewmodel.Message
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -69,11 +77,11 @@ import kotlinx.coroutines.launch
*/ */
@Composable @Composable
fun ConversationScreen( fun ConversationScreen(
// TODO-han.yin: Use loading metrics to show UI
loadingMetrics: ModelLoadingMetrics, loadingMetrics: ModelLoadingMetrics,
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
viewModel: ConversationViewModel viewModel: ConversationViewModel
) { ) {
// View model states
val engineState by viewModel.engineState.collectAsState() val engineState by viewModel.engineState.collectAsState()
val messages by viewModel.messages.collectAsState() val messages by viewModel.messages.collectAsState()
val systemPrompt by viewModel.systemPrompt.collectAsState() val systemPrompt by viewModel.systemPrompt.collectAsState()
@ -83,10 +91,12 @@ fun ConversationScreen(
val isProcessing = engineState is State.ProcessingUserPrompt val isProcessing = engineState is State.ProcessingUserPrompt
val isGenerating = engineState is State.Generating val isGenerating = engineState is State.Generating
// UI states
val lifecycleOwner = LocalLifecycleOwner.current
val coroutineScope = rememberCoroutineScope()
var isModelCardExpanded by remember { mutableStateOf(false) }
val listState = rememberLazyListState() val listState = rememberLazyListState()
var inputText by remember { mutableStateOf("") } var inputText by remember { mutableStateOf("") }
val coroutineScope = rememberCoroutineScope()
val lifecycleOwner = LocalLifecycleOwner.current
// Auto-scroll to bottom when messages change or when typing // Auto-scroll to bottom when messages change or when typing
val shouldScrollToBottom by remember(messages.size, isGenerating) { val shouldScrollToBottom by remember(messages.size, isGenerating) {
@ -129,7 +139,15 @@ fun ConversationScreen(
) { ) {
// System prompt display (collapsible) // System prompt display (collapsible)
selectedModel?.let { selectedModel?.let {
ModelCardWithSystemPrompt(it, systemPrompt) Box(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
ModelCardWithSystemPrompt(
model = it,
loadingMetrics = loadingMetrics,
systemPrompt = systemPrompt,
isExpanded = isModelCardExpanded,
onExpanded = { isModelCardExpanded = !isModelCardExpanded }
)
}
} }
// Messages list // Messages list
@ -167,6 +185,58 @@ fun ConversationScreen(
) )
} }
@Composable
fun ModelCardWithSystemPrompt(
model: ModelInfo,
loadingMetrics: ModelLoadingMetrics,
systemPrompt: String?,
isExpanded: Boolean = false,
onExpanded: ((Boolean) -> Unit)? = null,
) = ModelCardCoreExpandable(model, isExpanded, onExpanded) {
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)
Spacer(modifier = Modifier.height(8.dp))
// Row 4: Model loading time
ModelCardContentField("Loading time", formatMilliSeconds(loadingMetrics.modelLoadingTimeMs))
if (!systemPrompt.isNullOrBlank()) {
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
Text(
text = "System Prompt",
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Normal,
color = MaterialTheme.colorScheme.onSurface,
)
Spacer(Modifier.height(6.dp))
Text(
text = systemPrompt,
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.ExtraLight,
fontStyle = FontStyle.Italic,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.height(6.dp))
loadingMetrics.systemPromptProcessingTimeMs?.let {
ModelCardContentField("Processing time", formatMilliSeconds(it))
}
}
}
@Composable @Composable
private fun ConversationMessageList( private fun ConversationMessageList(
messages: List<Message>, messages: List<Message>,

View File

@ -22,7 +22,9 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.example.llama.revamp.data.model.ModelInfo import com.example.llama.revamp.data.model.ModelInfo
import com.example.llama.revamp.ui.components.ModelCardContentCore import com.example.llama.revamp.ui.components.ModelCardContentArchitectureRow
import com.example.llama.revamp.ui.components.ModelCardContentContextRow
import com.example.llama.revamp.ui.components.ModelCardContentTitleRow
import com.example.llama.revamp.util.FileType import com.example.llama.revamp.util.FileType
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
@ -39,8 +41,18 @@ fun ModelDetailsScreen(
.padding(horizontal = 16.dp, vertical = 8.dp) .padding(horizontal = 16.dp, vertical = 8.dp)
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
// Always show the core and expanded content // Row 1: Model full name
ModelCardContentCore(model = model) ModelCardContentTitleRow(model)
Spacer(modifier = Modifier.height(12.dp))
// Row 2: Context length, size label
ModelCardContentContextRow(model)
Spacer(modifier = Modifier.height(8.dp))
// Row 3: Architecture, quantization, formatted size
ModelCardContentArchitectureRow(model)
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))