UI: allow hide or show model card on Conversation & Benchmark screens; fix message arrangement

This commit is contained in:
Han Yin 2025-04-21 21:07:23 -07:00
parent 43d9d300aa
commit 81ad468c78
9 changed files with 156 additions and 108 deletions

View File

@ -257,6 +257,7 @@ fun AppContent(
// Benchmark screen
currentRoute.startsWith(AppDestinations.BENCHMARK_ROUTE) -> {
val engineState by benchmarkViewModel.engineState.collectAsState()
val showModelCard by benchmarkViewModel.showModelCard.collectAsState()
val benchmarkResults by benchmarkViewModel.benchmarkResults.collectAsState()
ScaffoldConfig(
@ -285,12 +286,16 @@ fun AppContent(
handleScaffoldEvent(ScaffoldEvent.ShareText(it.text))
}
},
showModelCard = showModelCard,
onToggleModelCard = benchmarkViewModel::toggleModelCard,
)
)
}
// Conversation screen
currentRoute.startsWith(AppDestinations.CONVERSATION_ROUTE) -> {
val showModelCard by conversationViewModel.showModelCard.collectAsState()
val modelThinkingOrSpeaking =
engineState is State.ProcessingUserPrompt || engineState is State.Generating
@ -313,6 +318,8 @@ fun AppContent(
isEnabled = !modelThinkingOrSpeaking,
textFieldState = conversationViewModel.inputFieldState,
onSendClick = conversationViewModel::sendMessage,
showModelCard = showModelCard,
onToggleModelCard = conversationViewModel::toggleModelCard,
onAttachPhotoClick = showStubMessage,
onAttachFileClick = showStubMessage,
onAudioInputClick = showStubMessage,

View File

@ -106,7 +106,9 @@ fun AppScaffold(
BenchmarkBottomBar(
engineIdle = config.engineIdle,
onRerun = config.onRerun,
onShare = config.onShare
onShare = config.onShare,
showModelCard = config.showModelCard,
onToggleModelCard = config.onToggleModelCard,
)
}
@ -115,6 +117,8 @@ fun AppScaffold(
isReady = config.isEnabled,
textFieldState = config.textFieldState,
onSendClick = config.onSendClick,
showModelCard = config.showModelCard,
onToggleModelCard = config.onToggleModelCard,
onAttachPhotoClick = config.onAttachPhotoClick,
onAttachFileClick = config.onAttachFileClick,
onAudioInputClick = config.onAudioInputClick,

View File

@ -6,8 +6,10 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Badge
import androidx.compose.material.icons.filled.Replay
import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.outlined.Badge
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
@ -20,6 +22,8 @@ fun BenchmarkBottomBar(
engineIdle: Boolean,
onRerun: () -> Unit,
onShare: () -> Unit,
showModelCard: Boolean,
onToggleModelCard: (Boolean) -> Unit,
) {
BottomAppBar(
actions = {
@ -32,6 +36,13 @@ fun BenchmarkBottomBar(
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f)
)
}
IconButton(onClick = { onToggleModelCard(!showModelCard) } ) {
Icon(
imageVector = if (showModelCard) Icons.Default.Badge else Icons.Outlined.Badge,
contentDescription = "${if (showModelCard) "Hide" else "Show"} model card"
)
}
},
floatingActionButton = {
// Only show FAB if the benchmark result is ready

View File

@ -89,12 +89,16 @@ sealed class BottomBarConfig {
val engineIdle: Boolean,
val onRerun: () -> Unit,
val onShare: () -> Unit,
val showModelCard: Boolean,
val onToggleModelCard: (Boolean) -> Unit,
) : BottomBarConfig()
data class Conversation(
val isEnabled: Boolean,
val textFieldState: TextFieldState,
val onSendClick: () -> Unit,
val showModelCard: Boolean,
val onToggleModelCard: (Boolean) -> Unit,
val onAttachPhotoClick: () -> Unit,
val onAttachFileClick: () -> Unit,
val onAudioInputClick: () -> Unit,

View File

@ -11,9 +11,11 @@ import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.Badge
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.outlined.AddPhotoAlternate
import androidx.compose.material.icons.outlined.AttachFile
import androidx.compose.material.icons.outlined.Badge
import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.CircularProgressIndicator
@ -37,6 +39,8 @@ fun ConversationBottomBar(
textFieldState: TextFieldState,
isReady: Boolean,
onSendClick: () -> Unit,
showModelCard: Boolean,
onToggleModelCard: (Boolean) -> Unit,
onAttachPhotoClick: () -> Unit,
onAttachFileClick: () -> Unit,
onAudioInputClick: () -> Unit,
@ -58,7 +62,7 @@ fun ConversationBottomBar(
) {
OutlinedTextField(
state = textFieldState,
modifier = Modifier.Companion.fillMaxWidth().padding(end = 8.dp),
modifier = Modifier.Companion.fillMaxWidth(),
enabled = isReady,
placeholder = { Text(placeholder) },
lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 5),
@ -102,6 +106,13 @@ fun ConversationBottomBar(
contentDescription = "Input with voice",
)
}
IconButton(onClick = { onToggleModelCard(!showModelCard) } ) {
Icon(
imageVector = if (showModelCard) Icons.Default.Badge else Icons.Outlined.Badge,
contentDescription = "${if (showModelCard) "Hide" else "Show"} model card"
)
}
},
floatingActionButton = {
FloatingActionButton(

View File

@ -54,10 +54,13 @@ fun BenchmarkScreen(
) {
// 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()
val showModelCard by viewModel.showModelCard.collectAsState()
val selectedModel by viewModel.selectedModel.collectAsState()
val benchmarkResults by viewModel.benchmarkResults.collectAsState()
// UI states
var isModelCardExpanded by remember { mutableStateOf(false) }
@ -71,36 +74,18 @@ fun BenchmarkScreen(
viewModel.onBackPressed(onNavigateBack)
}
Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
) {
// Selected model card
selectedModel?.let { model ->
Box(
modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp)
) {
ModelCardWithLoadingMetrics(
model = model,
loadingMetrics = loadingMetrics,
isExpanded = isModelCardExpanded,
onExpanded = { isModelCardExpanded = !isModelCardExpanded },
)
}
}
Box(
modifier = Modifier.fillMaxWidth().weight(1f),
contentAlignment = Alignment.Center
modifier = Modifier.fillMaxSize()
) {
// Benchmark results
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
contentPadding = PaddingValues(8.dp),
verticalArrangement = Arrangement.Bottom,
) {
items(items = benchmarkResults) { result ->
Card(
modifier = Modifier.fillMaxWidth()
modifier = Modifier.fillMaxWidth().padding(8.dp)
) {
Column(
modifier = Modifier
@ -161,6 +146,21 @@ fun BenchmarkScreen(
}
}
}
// Selected model card and loading metrics
if (showModelCard) {
selectedModel?.let { model ->
Box(
modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp)
) {
ModelCardWithLoadingMetrics(
model = model,
loadingMetrics = loadingMetrics,
isExpanded = isModelCardExpanded,
onExpanded = { isModelCardExpanded = !isModelCardExpanded },
)
}
}
}
}

View File

@ -9,6 +9,7 @@ import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@ -26,19 +27,11 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Send
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
@ -52,15 +45,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
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.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.compose.LocalLifecycleOwner
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.ui.components.ModelCardContentArchitectureRow
@ -84,11 +74,14 @@ fun ConversationScreen(
) {
// View model states
val engineState by viewModel.engineState.collectAsState()
val messages by viewModel.messages.collectAsState()
val systemPrompt by viewModel.systemPrompt.collectAsState()
val selectedModel by viewModel.selectedModel.collectAsState()
val unloadDialogState by viewModel.unloadModelState.collectAsState()
val showModelCard by viewModel.showModelCard.collectAsState()
val selectedModel by viewModel.selectedModel.collectAsState()
val systemPrompt by viewModel.systemPrompt.collectAsState()
val messages by viewModel.messages.collectAsState()
val isGenerating = engineState is State.Generating
// UI states
@ -140,6 +133,7 @@ fun ConversationScreen(
listState = listState,
)
if (showModelCard) {
selectedModel?.let {
Box(
modifier = Modifier.fillMaxWidth().padding(16.dp).align(Alignment.TopCenter)
@ -154,6 +148,7 @@ fun ConversationScreen(
}
}
}
}
// Unload confirmation dialog
ModelUnloadDialogHandler(
@ -225,6 +220,7 @@ private fun ConversationMessageList(
state = listState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
verticalArrangement = Arrangement.Bottom,
) {
items(
items = messages,

View File

@ -20,9 +20,8 @@ import javax.inject.Inject
class BenchmarkViewModel @Inject constructor(
private val benchmarkService: BenchmarkService
) : ModelUnloadingViewModel(benchmarkService) {
/**
* UI states
*/
// Data
val selectedModel: StateFlow<ModelInfo?> = benchmarkService.currentSelectedModel
private val _benchmarkDuration = MutableSharedFlow<Long>()
@ -30,6 +29,14 @@ class BenchmarkViewModel @Inject constructor(
private val _benchmarkResults = MutableStateFlow<List<BenchmarkResult>>(emptyList())
val benchmarkResults: StateFlow<List<BenchmarkResult>> = _benchmarkResults.asStateFlow()
// UI state: Model card
private val _showModelCard = MutableStateFlow(true)
val showModelCard = _showModelCard.asStateFlow()
fun toggleModelCard(show: Boolean) {
_showModelCard.value = show
}
init {
viewModelScope.launch {
benchmarkService.benchmarkResults

View File

@ -28,11 +28,19 @@ class ConversationViewModel @Inject constructor(
val selectedModel = conversationService.currentSelectedModel
val systemPrompt = conversationService.systemPrompt
// Messages state
// UI state: Model card
private val _showModelCard = MutableStateFlow(true)
val showModelCard = _showModelCard.asStateFlow()
fun toggleModelCard(show: Boolean) {
_showModelCard.value = show
}
// UI state: conversation messages
private val _messages = MutableStateFlow<List<Message>>(emptyList())
val messages: StateFlow<List<Message>> = _messages.asStateFlow()
// Input text field state
// UI state: Input text field
val inputFieldState = TextFieldState()
// Token generation job