UI: allow hide or show model card on Conversation & Benchmark screens; fix message arrangement
This commit is contained in:
parent
43d9d300aa
commit
81ad468c78
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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,94 +74,91 @@ fun BenchmarkScreen(
|
|||
viewModel.onBackPressed(onNavigateBack)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
// 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
|
||||
// Benchmark results
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(8.dp),
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
) {
|
||||
// Benchmark results
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp),
|
||||
) {
|
||||
items(items = benchmarkResults) { result ->
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = result.text,
|
||||
style = MonospacedTextStyle,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
items(items = benchmarkResults) { result ->
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().padding(8.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.surfaceVariant,
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = result.text,
|
||||
style = MonospacedTextStyle,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
ModelCardContentField("Time spent: ", formatMilliSeconds(result.duration))
|
||||
}
|
||||
ModelCardContentField("Time spent: ", formatMilliSeconds(result.duration))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loading indicator
|
||||
if (engineState is State.Benchmarking) {
|
||||
Card(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
),
|
||||
shape = MaterialTheme.shapes.extraLarge
|
||||
// Loading indicator
|
||||
if (engineState is State.Benchmarking) {
|
||||
Card(
|
||||
modifier = Modifier.align(Alignment.Center),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
),
|
||||
shape = MaterialTheme.shapes.extraLarge
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 32.dp, vertical = 48.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 32.dp, vertical = 48.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(64.dp),
|
||||
strokeWidth = ProgressIndicatorDefaults.CircularStrokeWidth * 1.5f
|
||||
)
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(64.dp),
|
||||
strokeWidth = ProgressIndicatorDefaults.CircularStrokeWidth * 1.5f
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "Running benchmark...",
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
Text(
|
||||
text = "Running benchmark...",
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = "This usually takes a few minutes",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = "This usually takes a few minutes",
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,17 +133,19 @@ fun ConversationScreen(
|
|||
listState = listState,
|
||||
)
|
||||
|
||||
selectedModel?.let {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp).align(Alignment.TopCenter)
|
||||
) {
|
||||
ModelCardWithSystemPrompt(
|
||||
model = it,
|
||||
loadingMetrics = loadingMetrics,
|
||||
systemPrompt = systemPrompt,
|
||||
isExpanded = isModelCardExpanded,
|
||||
onExpanded = { isModelCardExpanded = !isModelCardExpanded }
|
||||
)
|
||||
if (showModelCard) {
|
||||
selectedModel?.let {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(16.dp).align(Alignment.TopCenter)
|
||||
) {
|
||||
ModelCardWithSystemPrompt(
|
||||
model = it,
|
||||
loadingMetrics = loadingMetrics,
|
||||
systemPrompt = systemPrompt,
|
||||
isExpanded = isModelCardExpanded,
|
||||
onExpanded = { isModelCardExpanded = !isModelCardExpanded }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue