diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/MainActivity.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/MainActivity.kt index ca57a7a002..0c860128bb 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/MainActivity.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/MainActivity.kt @@ -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, diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/AppScaffold.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/AppScaffold.kt index 1a74ec076f..3d2dd2916d 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/AppScaffold.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/AppScaffold.kt @@ -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, diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/BenchmarkBottomBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/BenchmarkBottomBar.kt index 049c9441ab..48e9d0c735 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/BenchmarkBottomBar.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/BenchmarkBottomBar.kt @@ -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 diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/BottomBarConfig.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/BottomBarConfig.kt index 4dd19dd4fc..a837487bf8 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/BottomBarConfig.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/BottomBarConfig.kt @@ -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, diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/ConversationBottomBar.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/ConversationBottomBar.kt index 6aa2b9f3bb..14bbd36f13 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/ConversationBottomBar.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/scaffold/bottombar/ConversationBottomBar.kt @@ -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( diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/BenchmarkScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/BenchmarkScreen.kt index 106581cc4b..6d9acea34f 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/BenchmarkScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/BenchmarkScreen.kt @@ -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 }, + ) } } } diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ConversationScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ConversationScreen.kt index 583d363667..fe3141ed33 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ConversationScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ConversationScreen.kt @@ -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, diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/viewmodel/BenchmarkViewModel.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/viewmodel/BenchmarkViewModel.kt index e066389b4e..48dc310640 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/viewmodel/BenchmarkViewModel.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/viewmodel/BenchmarkViewModel.kt @@ -20,9 +20,8 @@ import javax.inject.Inject class BenchmarkViewModel @Inject constructor( private val benchmarkService: BenchmarkService ) : ModelUnloadingViewModel(benchmarkService) { - /** - * UI states - */ + + // Data val selectedModel: StateFlow = benchmarkService.currentSelectedModel private val _benchmarkDuration = MutableSharedFlow() @@ -30,6 +29,14 @@ class BenchmarkViewModel @Inject constructor( private val _benchmarkResults = MutableStateFlow>(emptyList()) val benchmarkResults: StateFlow> = _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 diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/viewmodel/ConversationViewModel.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/viewmodel/ConversationViewModel.kt index 99a2ec86e0..51777ed8f0 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/viewmodel/ConversationViewModel.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/viewmodel/ConversationViewModel.kt @@ -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>(emptyList()) val messages: StateFlow> = _messages.asStateFlow() - // Input text field state + // UI state: Input text field val inputFieldState = TextFieldState() // Token generation job