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

View File

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

View File

@ -6,8 +6,10 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut import androidx.compose.animation.scaleOut
import androidx.compose.material.icons.Icons 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.Replay
import androidx.compose.material.icons.filled.Share import androidx.compose.material.icons.filled.Share
import androidx.compose.material.icons.outlined.Badge
import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -20,6 +22,8 @@ fun BenchmarkBottomBar(
engineIdle: Boolean, engineIdle: Boolean,
onRerun: () -> Unit, onRerun: () -> Unit,
onShare: () -> Unit, onShare: () -> Unit,
showModelCard: Boolean,
onToggleModelCard: (Boolean) -> Unit,
) { ) {
BottomAppBar( BottomAppBar(
actions = { actions = {
@ -32,6 +36,13 @@ fun BenchmarkBottomBar(
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) 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 = { floatingActionButton = {
// Only show FAB if the benchmark result is ready // Only show FAB if the benchmark result is ready

View File

@ -89,12 +89,16 @@ sealed class BottomBarConfig {
val engineIdle: Boolean, val engineIdle: Boolean,
val onRerun: () -> Unit, val onRerun: () -> Unit,
val onShare: () -> Unit, val onShare: () -> Unit,
val showModelCard: Boolean,
val onToggleModelCard: (Boolean) -> Unit,
) : BottomBarConfig() ) : BottomBarConfig()
data class Conversation( data class Conversation(
val isEnabled: Boolean, val isEnabled: Boolean,
val textFieldState: TextFieldState, val textFieldState: TextFieldState,
val onSendClick: () -> Unit, val onSendClick: () -> Unit,
val showModelCard: Boolean,
val onToggleModelCard: (Boolean) -> Unit,
val onAttachPhotoClick: () -> Unit, val onAttachPhotoClick: () -> Unit,
val onAttachFileClick: () -> Unit, val onAttachFileClick: () -> Unit,
val onAudioInputClick: () -> 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.foundation.text.input.TextFieldState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send 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.filled.Mic
import androidx.compose.material.icons.outlined.AddPhotoAlternate import androidx.compose.material.icons.outlined.AddPhotoAlternate
import androidx.compose.material.icons.outlined.AttachFile import androidx.compose.material.icons.outlined.AttachFile
import androidx.compose.material.icons.outlined.Badge
import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomAppBar
import androidx.compose.material3.BottomAppBarDefaults import androidx.compose.material3.BottomAppBarDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
@ -37,6 +39,8 @@ fun ConversationBottomBar(
textFieldState: TextFieldState, textFieldState: TextFieldState,
isReady: Boolean, isReady: Boolean,
onSendClick: () -> Unit, onSendClick: () -> Unit,
showModelCard: Boolean,
onToggleModelCard: (Boolean) -> Unit,
onAttachPhotoClick: () -> Unit, onAttachPhotoClick: () -> Unit,
onAttachFileClick: () -> Unit, onAttachFileClick: () -> Unit,
onAudioInputClick: () -> Unit, onAudioInputClick: () -> Unit,
@ -58,7 +62,7 @@ fun ConversationBottomBar(
) { ) {
OutlinedTextField( OutlinedTextField(
state = textFieldState, state = textFieldState,
modifier = Modifier.Companion.fillMaxWidth().padding(end = 8.dp), modifier = Modifier.Companion.fillMaxWidth(),
enabled = isReady, enabled = isReady,
placeholder = { Text(placeholder) }, placeholder = { Text(placeholder) },
lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 5), lineLimits = TextFieldLineLimits.MultiLine(maxHeightInLines = 5),
@ -102,6 +106,13 @@ fun ConversationBottomBar(
contentDescription = "Input with voice", 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 = {
FloatingActionButton( FloatingActionButton(

View File

@ -54,10 +54,13 @@ fun BenchmarkScreen(
) { ) {
// View model states // View model states
val engineState by viewModel.engineState.collectAsState() 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 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 // UI states
var isModelCardExpanded by remember { mutableStateOf(false) } var isModelCardExpanded by remember { mutableStateOf(false) }
@ -71,36 +74,18 @@ fun BenchmarkScreen(
viewModel.onBackPressed(onNavigateBack) viewModel.onBackPressed(onNavigateBack)
} }
Column(
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
) {
// Selected model card
selectedModel?.let { model ->
Box( Box(
modifier = Modifier.padding(start = 16.dp, top = 16.dp, end = 16.dp) modifier = Modifier.fillMaxSize()
) {
ModelCardWithLoadingMetrics(
model = model,
loadingMetrics = loadingMetrics,
isExpanded = isModelCardExpanded,
onExpanded = { isModelCardExpanded = !isModelCardExpanded },
)
}
}
Box(
modifier = Modifier.fillMaxWidth().weight(1f),
contentAlignment = Alignment.Center
) { ) {
// Benchmark results // Benchmark results
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(16.dp), contentPadding = PaddingValues(8.dp),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp), verticalArrangement = Arrangement.Bottom,
) { ) {
items(items = benchmarkResults) { result -> items(items = benchmarkResults) { result ->
Card( Card(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth().padding(8.dp)
) { ) {
Column( Column(
modifier = Modifier 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.rememberInfiniteTransition
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues 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.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape 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.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -52,15 +45,12 @@ 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.draw.clip 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.FontStyle
import androidx.compose.ui.text.font.FontWeight 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.data.model.ModelInfo 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.ModelCardContentArchitectureRow
@ -84,11 +74,14 @@ fun ConversationScreen(
) { ) {
// View model states // View model states
val engineState by viewModel.engineState.collectAsState() 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 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 val isGenerating = engineState is State.Generating
// UI states // UI states
@ -140,6 +133,7 @@ fun ConversationScreen(
listState = listState, listState = listState,
) )
if (showModelCard) {
selectedModel?.let { selectedModel?.let {
Box( Box(
modifier = Modifier.fillMaxWidth().padding(16.dp).align(Alignment.TopCenter) modifier = Modifier.fillMaxWidth().padding(16.dp).align(Alignment.TopCenter)
@ -154,6 +148,7 @@ fun ConversationScreen(
} }
} }
} }
}
// Unload confirmation dialog // Unload confirmation dialog
ModelUnloadDialogHandler( ModelUnloadDialogHandler(
@ -225,6 +220,7 @@ private fun ConversationMessageList(
state = listState, state = listState,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp), contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
verticalArrangement = Arrangement.Bottom,
) { ) {
items( items(
items = messages, items = messages,

View File

@ -20,9 +20,8 @@ import javax.inject.Inject
class BenchmarkViewModel @Inject constructor( class BenchmarkViewModel @Inject constructor(
private val benchmarkService: BenchmarkService private val benchmarkService: BenchmarkService
) : ModelUnloadingViewModel(benchmarkService) { ) : ModelUnloadingViewModel(benchmarkService) {
/**
* UI states // Data
*/
val selectedModel: StateFlow<ModelInfo?> = benchmarkService.currentSelectedModel val selectedModel: StateFlow<ModelInfo?> = benchmarkService.currentSelectedModel
private val _benchmarkDuration = MutableSharedFlow<Long>() private val _benchmarkDuration = MutableSharedFlow<Long>()
@ -30,6 +29,14 @@ class BenchmarkViewModel @Inject constructor(
private val _benchmarkResults = MutableStateFlow<List<BenchmarkResult>>(emptyList()) private val _benchmarkResults = MutableStateFlow<List<BenchmarkResult>>(emptyList())
val benchmarkResults: StateFlow<List<BenchmarkResult>> = _benchmarkResults.asStateFlow() 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 { init {
viewModelScope.launch { viewModelScope.launch {
benchmarkService.benchmarkResults benchmarkService.benchmarkResults

View File

@ -28,11 +28,19 @@ class ConversationViewModel @Inject constructor(
val selectedModel = conversationService.currentSelectedModel val selectedModel = conversationService.currentSelectedModel
val systemPrompt = conversationService.systemPrompt 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()) private val _messages = MutableStateFlow<List<Message>>(emptyList())
val messages: StateFlow<List<Message>> = _messages.asStateFlow() val messages: StateFlow<List<Message>> = _messages.asStateFlow()
// Input text field state // UI state: Input text field
val inputFieldState = TextFieldState() val inputFieldState = TextFieldState()
// Token generation job // Token generation job