UI: polish ModelLoading screen

This commit is contained in:
Han Yin 2025-04-20 17:53:42 -07:00
parent 57b5001f5c
commit d7afcc41d5
6 changed files with 103 additions and 58 deletions

View File

@ -17,11 +17,16 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width 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.AssistChip
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalMinimumInteractiveComponentSize import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -49,44 +54,70 @@ import java.util.Locale
* architecture, quantization and file size in a compact card format. * architecture, quantization and file size in a compact card format.
* *
* @param model The model information to display * @param model The model information to display
* @param onClick Action to perform when the card is clicked * @param isExpanded Whether additional details is expanded or shrunk
* @param isSelected Optional selection state (shows checkbox when not null) * @param onExpanded Action to perform when the card is expanded or shrunk
*/ */
@Composable @Composable
fun ModelCardCore( fun ModelCardCoreExpandable(
model: ModelInfo, model: ModelInfo,
onClick: () -> Unit, isExpanded: Boolean = false,
isSelected: Boolean? = null, onExpanded: ((Boolean) -> Unit)? = null,
) { ) {
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable(onClick = onClick), .clickable { onExpanded?.invoke(!isExpanded) },
colors = when (isSelected) { colors = when (isExpanded) {
true -> CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) true -> CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
false -> CardDefaults.cardColors() false -> CardDefaults.cardColors()
else -> CardDefaults.cardColors()
}, },
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) { ) {
Row( Column(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) { ) {
// Show checkbox if in selection mode // Row 1: Model full name + chevron
if (isSelected != null) { Row(
Checkbox( modifier = Modifier.fillMaxWidth(),
checked = isSelected, horizontalArrangement = Arrangement.SpaceBetween,
onCheckedChange = { onClick() }, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(end = 8.dp) ) {
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 // Expandable content
ModelCardContentCore( AnimatedVisibility(
model = model, visible = isExpanded,
enter = expandVertically() + fadeIn(),
exit = shrinkVertically() + fadeOut()
) {
Column(
modifier = Modifier.weight(1f) 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) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun ModelCardExpandable( fun ModelCardFullExpandable(
model: ModelInfo, model: ModelInfo,
isSelected: Boolean? = null, isSelected: Boolean? = null,
onSelected: ((Boolean) -> Unit)? = null, onSelected: ((Boolean) -> Unit)? = null,
@ -117,11 +148,11 @@ fun ModelCardExpandable(
Card( Card(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable { onExpanded?.invoke(!isExpanded) },
onExpanded?.invoke(!isExpanded)
},
colors = when (isSelected) { colors = when (isSelected) {
true -> CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer) true -> CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
false -> CardDefaults.cardColors() false -> CardDefaults.cardColors()
else -> CardDefaults.cardColors() else -> CardDefaults.cardColors()
}, },
@ -148,11 +179,7 @@ fun ModelCardExpandable(
.padding(start = 16.dp, top = 16.dp, end = 16.dp) .padding(start = 16.dp, top = 16.dp, end = 16.dp)
) { ) {
// Row 1: Model full name // Row 1: Model full name
Text( ModelCardContentTitleRow(model)
text = model.formattedFullName,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Medium
)
} }
} }
@ -283,11 +310,7 @@ fun ModelCardContentCore(
) = ) =
Column(modifier = modifier) { Column(modifier = modifier) {
// Row 1: Model full name // Row 1: Model full name
Text( ModelCardContentTitleRow(model)
text = model.formattedFullName,
style = MaterialTheme.typography.headlineSmall, // TODO
fontWeight = FontWeight.Medium
)
Spacer(modifier = Modifier.height(12.dp)) Spacer(modifier = Modifier.height(12.dp))
@ -300,6 +323,14 @@ fun ModelCardContentCore(
ModelCardContentArchitectureRow(model) ModelCardContentArchitectureRow(model)
} }
@Composable
private fun ModelCardContentTitleRow(model: ModelInfo) =
Text(
text = model.formattedFullName,
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Medium
)
@Composable @Composable
private fun ModelCardContentContextRow(model: ModelInfo) = private fun ModelCardContentContextRow(model: ModelInfo) =
Row( Row(

View File

@ -21,26 +21,34 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue 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.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.engine.ModelLoadingMetrics 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.components.ModelUnloadDialogHandler
import com.example.llama.revamp.ui.theme.MonospacedTextStyle import com.example.llama.revamp.ui.theme.MonospacedTextStyle
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
) { ) {
// View model states
val engineState by viewModel.engineState.collectAsState() val engineState by viewModel.engineState.collectAsState()
val benchmarkResults by viewModel.benchmarkResults.collectAsState() val benchmarkResults by viewModel.benchmarkResults.collectAsState()
val selectedModel by viewModel.selectedModel.collectAsState() val selectedModel by viewModel.selectedModel.collectAsState()
val unloadDialogState by viewModel.unloadModelState.collectAsState() val unloadDialogState by viewModel.unloadModelState.collectAsState()
// UI states
var isModelCardExpanded by remember { mutableStateOf(false) }
// Run benchmark when entering the screen // Run benchmark when entering the screen
LaunchedEffect(selectedModel) { LaunchedEffect(selectedModel) {
viewModel.runBenchmark() viewModel.runBenchmark()
@ -59,10 +67,10 @@ fun BenchmarkScreen(
) { ) {
// Selected model card // Selected model card
selectedModel?.let { model -> selectedModel?.let { model ->
ModelCardCore( ModelCardCoreExpandable(
model = model, model = model,
onClick = { /* No action on click */ }, isExpanded = isModelCardExpanded,
isSelected = null onExpanded = { isModelCardExpanded = !isModelCardExpanded },
) )
} }

View File

@ -69,6 +69,7 @@ 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 File

@ -50,7 +50,7 @@ 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.SystemPrompt import com.example.llama.revamp.data.model.SystemPrompt
import com.example.llama.revamp.engine.ModelLoadingMetrics 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.components.ModelUnloadDialogHandler
import com.example.llama.revamp.viewmodel.ModelLoadingViewModel import com.example.llama.revamp.viewmodel.ModelLoadingViewModel
@ -72,12 +72,15 @@ fun ModelLoadingScreen(
onNavigateToConversation: (ModelLoadingMetrics) -> Unit, onNavigateToConversation: (ModelLoadingMetrics) -> Unit,
viewModel: ModelLoadingViewModel, viewModel: ModelLoadingViewModel,
) { ) {
// View model states
val engineState by viewModel.engineState.collectAsState() val engineState by viewModel.engineState.collectAsState()
val selectedModel by viewModel.selectedModel.collectAsState() val selectedModel by viewModel.selectedModel.collectAsState()
val presetPrompts by viewModel.presetPrompts.collectAsState() val presetPrompts by viewModel.presetPrompts.collectAsState()
val recentPrompts by viewModel.recentPrompts.collectAsState() val recentPrompts by viewModel.recentPrompts.collectAsState()
val unloadDialogState by viewModel.unloadModelState.collectAsState() val unloadDialogState by viewModel.unloadModelState.collectAsState()
// UI states
var isModelCardExpanded by remember { mutableStateOf(false) }
var selectedMode by remember { mutableStateOf<Mode?>(null) } var selectedMode by remember { mutableStateOf<Mode?>(null) }
var useSystemPrompt by remember { mutableStateOf(false) } var useSystemPrompt by remember { mutableStateOf(false) }
var selectedPrompt by remember { mutableStateOf<SystemPrompt?>(null) } var selectedPrompt by remember { mutableStateOf<SystemPrompt?>(null) }
@ -116,11 +119,13 @@ fun ModelLoadingScreen(
) { ) {
// Selected model card // Selected model card
selectedModel?.let { model -> selectedModel?.let { model ->
ModelCardCore( ModelCardCoreExpandable(
model = model, model = model,
onClick = { /* No action on click */ }, isExpanded = isModelCardExpanded,
isSelected = null onExpanded = { isModelCardExpanded = !isModelCardExpanded },
) )
Spacer(modifier = Modifier.height(16.dp))
} }
// Benchmark card // Benchmark card
@ -148,7 +153,7 @@ fun ModelLoadingScreen(
) )
Text( Text(
text = "Benchmark", text = "Benchmark",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(start = 8.dp) modifier = Modifier.padding(start = 8.dp)
) )
} }
@ -159,6 +164,12 @@ fun ModelLoadingScreen(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(bottom = 4.dp) .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 // Only fill height if system prompt is active
.then(if (useSystemPrompt) Modifier.weight(1f) else Modifier) .then(if (useSystemPrompt) Modifier.weight(1f) else Modifier)
) { ) {
@ -173,12 +184,6 @@ fun ModelLoadingScreen(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .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), .padding(top = 16.dp, start = 16.dp, end = 16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@ -188,7 +193,7 @@ fun ModelLoadingScreen(
) )
Text( Text(
text = "Conversation", text = "Conversation",
style = MaterialTheme.typography.titleLarge, style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(start = 8.dp) modifier = Modifier.padding(start = 8.dp)
) )
} }

View File

@ -34,7 +34,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
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.ModelCardExpandable import com.example.llama.revamp.ui.components.ModelCardFullExpandable
import com.example.llama.revamp.viewmodel.ModelSelectionViewModel import com.example.llama.revamp.viewmodel.ModelSelectionViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -65,7 +65,7 @@ fun ModelSelectionScreen(
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
items(items = models, key = { it.id }) { model -> items(items = models, key = { it.id }) { model ->
ModelCardExpandable( ModelCardFullExpandable(
model = model, model = model,
isSelected = if (model == preselectedModel) true else null, isSelected = if (model == preselectedModel) true else null,
onSelected = { selected -> onSelected = { selected ->

View File

@ -36,7 +36,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import com.example.llama.revamp.data.model.ModelInfo 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.ui.components.ScaffoldEvent
import com.example.llama.revamp.util.formatFileByteSize import com.example.llama.revamp.util.formatFileByteSize
import com.example.llama.revamp.viewmodel.ModelManagementState import com.example.llama.revamp.viewmodel.ModelManagementState
@ -84,7 +84,7 @@ fun ModelsManagementScreen(
items(items = sortedModels, key = { it.id }) { model -> items(items = sortedModels, key = { it.id }) { model ->
val isSelected = if (isMultiSelectionMode) selectedModels.contains(model.id) else null val isSelected = if (isMultiSelectionMode) selectedModels.contains(model.id) else null
ModelCardExpandable( ModelCardFullExpandable(
model = model, model = model,
isSelected = isSelected, isSelected = isSelected,
onSelected = { onSelected = {