UI: add a info button to explain token metrics

This commit is contained in:
Han Yin 2025-08-29 19:24:37 -07:00
parent 027c68db64
commit 5794d7ae6c
1 changed files with 55 additions and 32 deletions

View File

@ -1,6 +1,8 @@
package com.example.llama.ui.screens package com.example.llama.ui.screens
import android.content.Intent
import android.llama.cpp.InferenceEngine.State import android.llama.cpp.InferenceEngine.State
import android.widget.Toast
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode import androidx.compose.animation.core.RepeatMode
@ -28,12 +30,14 @@ 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.Icons
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
import androidx.compose.material.icons.filled.Timer import androidx.compose.material.icons.filled.Timer
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TonalToggleButton import androidx.compose.material3.TonalToggleButton
@ -52,9 +56,11 @@ import androidx.compose.runtime.snapshotFlow
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.platform.LocalContext
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.core.net.toUri
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
@ -279,6 +285,13 @@ private fun ConversationMessageList(
messages: List<Message>, messages: List<Message>,
listState: LazyListState, listState: LazyListState,
) { ) {
val context = LocalContext.current
val onInfoClick = {
Toast.makeText(context, "Please refer to this guide for more details on the metrics", Toast.LENGTH_SHORT).show()
val intent = Intent(Intent.ACTION_VIEW, "https://docs.nvidia.com/nim/benchmarking/llm/latest/metrics.html".toUri())
context.startActivity(intent)
}
LazyColumn( LazyColumn(
state = listState, state = listState,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
@ -289,18 +302,6 @@ private fun ConversationMessageList(
items = messages, items = messages,
key = { "${it::class.simpleName}_${it.timestamp}" } key = { "${it::class.simpleName}_${it.timestamp}" }
) { message -> ) { message ->
MessageBubble(message = message)
}
// Add extra space at the bottom for better UX and a scroll target
item(key = "bottom-spacer") {
Spacer(modifier = Modifier.height(36.dp))
}
}
}
@Composable
private fun MessageBubble(message: Message) {
when (message) { when (message) {
is Message.User -> UserMessageBubble( is Message.User -> UserMessageBubble(
formattedTime = message.formattedTime, formattedTime = message.formattedTime,
@ -312,7 +313,8 @@ private fun MessageBubble(message: Message) {
content = message.content, content = message.content,
isThinking = message.content.isBlank(), isThinking = message.content.isBlank(),
isGenerating = true, isGenerating = true,
metrics = null metrics = null,
onInfoClick = onInfoClick,
) )
is Message.Assistant.Stopped -> AssistantMessageBubble( is Message.Assistant.Stopped -> AssistantMessageBubble(
@ -320,11 +322,19 @@ private fun MessageBubble(message: Message) {
content = message.content, content = message.content,
isThinking = false, isThinking = false,
isGenerating = false, isGenerating = false,
metrics = message.metrics metrics = message.metrics,
onInfoClick = onInfoClick,
) )
} }
} }
// Add extra space at the bottom for better UX and a scroll target
item(key = "bottom-spacer") {
Spacer(modifier = Modifier.height(36.dp))
}
}
}
@Composable @Composable
private fun UserMessageBubble(content: String, formattedTime: String) { private fun UserMessageBubble(content: String, formattedTime: String) {
Column( Column(
@ -367,9 +377,11 @@ private fun AssistantMessageBubble(
content: String, content: String,
isThinking: Boolean, isThinking: Boolean,
isGenerating: Boolean, isGenerating: Boolean,
metrics: TokenMetrics? = null metrics: TokenMetrics? = null,
onInfoClick: () -> Unit,
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.Top, verticalAlignment = Alignment.Top,
) { ) {
// Assistant avatar // Assistant avatar
@ -435,7 +447,7 @@ private fun AssistantMessageBubble(
} else { } else {
// Show metrics when message is complete // Show metrics when message is complete
metrics?.let { metrics?.let {
ExpandableTokenMetricsBubble(metrics) ExpandableTokenMetricsBubble(metrics, onInfoClick)
} }
} }
} }
@ -484,10 +496,13 @@ private fun PulsatingDots(small: Boolean = false) {
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
private fun ExpandableTokenMetricsBubble(metrics: TokenMetrics) { private fun ExpandableTokenMetricsBubble(
metrics: TokenMetrics,
onInfoClick: () -> Unit
) {
var showMetrics by remember { mutableStateOf(false) } var showMetrics by remember { mutableStateOf(false) }
Column { Column(Modifier.fillMaxWidth(0.9f)) {
TonalToggleButton( TonalToggleButton(
checked = showMetrics, checked = showMetrics,
onCheckedChange = { showMetrics = !showMetrics } onCheckedChange = { showMetrics = !showMetrics }
@ -512,7 +527,6 @@ private fun ExpandableTokenMetricsBubble(metrics: TokenMetrics) {
), ),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) { ) {
Row( Row(
modifier = Modifier.padding(12.dp), modifier = Modifier.padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(18.dp) horizontalArrangement = Arrangement.spacedBy(18.dp)
@ -525,6 +539,15 @@ private fun ExpandableTokenMetricsBubble(metrics: TokenMetrics) {
val duration = formatMilliSecondstructured(metrics.duration) val duration = formatMilliSecondstructured(metrics.duration)
TokenMetricSection("Duration", duration.value.toString(), duration.unit.toEnglishName()) TokenMetricSection("Duration", duration.value.toString(), duration.unit.toEnglishName())
IconButton(onClick = onInfoClick) {
Icon(
modifier = Modifier.size(24.dp),
imageVector = Icons.AutoMirrored.Outlined.HelpOutline,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
contentDescription = "Information on token metrics"
)
}
} }
} }
} }