diff --git a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModeSelectionScreen.kt b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModeSelectionScreen.kt index a4071e8c5f..8d14e7655b 100644 --- a/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModeSelectionScreen.kt +++ b/examples/llama.android/app/src/main/java/com/example/llama/revamp/ui/screens/ModeSelectionScreen.kt @@ -1,6 +1,12 @@ package com.example.llama.revamp.ui.screens import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -11,28 +17,27 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ExpandLess -import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.Check import androidx.compose.material3.Button import androidx.compose.material3.Card import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Divider import androidx.compose.material3.DrawerState import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.RadioButton -import androidx.compose.material3.Tab -import androidx.compose.material3.TabRow +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Switch import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -49,8 +54,15 @@ import com.example.llama.revamp.engine.InferenceEngine import com.example.llama.revamp.navigation.NavigationActions import com.example.llama.revamp.ui.components.AppScaffold import kotlinx.coroutines.launch +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale -@OptIn(ExperimentalMaterial3Api::class) +enum class SystemPromptTab { + PRESETS, CUSTOM, RECENTS +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun ModeSelectionScreen( engineState: InferenceEngine.State, @@ -65,13 +77,10 @@ fun ModeSelectionScreen( var selectedMode by remember { mutableStateOf(null) } var useSystemPrompt by remember { mutableStateOf(false) } - var selectedPrompt by remember { mutableStateOf(null) } - var tabIndex by remember { mutableStateOf(0) } - - // Custom prompt sheet state - val sheetState = rememberModalBottomSheetState() - var showCustomPromptSheet by remember { mutableStateOf(false) } + var selectedPrompt by remember { mutableStateOf(staffPickedPrompts.firstOrNull()) } + var selectedTab by remember { mutableStateOf(SystemPromptTab.PRESETS) } var customPromptText by remember { mutableStateOf("") } + var expandedPromptId by remember { mutableStateOf(null) } val coroutineScope = rememberCoroutineScope() @@ -120,106 +129,188 @@ fun ModeSelectionScreen( } } + // Conversation card with integrated system prompt Card( modifier = Modifier .fillMaxWidth() .padding(bottom = 8.dp) - .selectable( - selected = selectedMode == Mode.CONVERSATION, - onClick = { - selectedMode = Mode.CONVERSATION - }, - enabled = !isLoading, - role = Role.RadioButton - ) ) { - Row( - modifier = Modifier.padding(16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = selectedMode == Mode.CONVERSATION, - onClick = null // handled by parent selectable - ) - Text( - text = "Conversation", - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.padding(start = 8.dp) - ) - } - } - - // System prompt section (only visible when conversation mode is selected) - AnimatedVisibility(visible = selectedMode == Mode.CONVERSATION) { - Card( + Column( modifier = Modifier .fillMaxWidth() - .padding(bottom = 8.dp) ) { - Column( - modifier = Modifier.padding(16.dp) + // Conversation option + Row( + modifier = Modifier + .fillMaxWidth() + .selectable( + selected = selectedMode == Mode.CONVERSATION, + onClick = { selectedMode = Mode.CONVERSATION }, + enabled = !isLoading, + role = Role.RadioButton + ) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically ) { - Row( + RadioButton( + selected = selectedMode == Mode.CONVERSATION, + onClick = null // handled by parent selectable + ) + Text( + text = "Conversation", + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.padding(start = 8.dp) + ) + } + + // System prompt row with switch + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "System prompt", + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .padding(start = 32.dp) // Align with radio text + .weight(1f) + ) + + Switch( + checked = useSystemPrompt, + onCheckedChange = { + useSystemPrompt = it + if (it && selectedMode != Mode.CONVERSATION) { + selectedMode = Mode.CONVERSATION + } + }, + enabled = !isLoading + ) + } + + // System prompt content (visible when switch is on) + AnimatedVisibility( + visible = useSystemPrompt && selectedMode == Mode.CONVERSATION, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + Column( modifier = Modifier .fillMaxWidth() - .selectableGroup(), - verticalAlignment = Alignment.CenterVertically + .padding(horizontal = 16.dp, vertical = 8.dp) ) { - Text( - text = "Use system prompt", - style = MaterialTheme.typography.titleMedium - ) + HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp)) - Spacer(modifier = Modifier.weight(1f)) - - TextButton( - onClick = { useSystemPrompt = !useSystemPrompt } + // Tab selector using SegmentedButton + SingleChoiceSegmentedButtonRow( + modifier = Modifier.fillMaxWidth() ) { - Icon( - imageVector = if (useSystemPrompt) Icons.Default.ExpandLess else Icons.Default.ExpandMore, - contentDescription = if (useSystemPrompt) "Collapse" else "Expand" + SegmentedButton( + selected = selectedTab == SystemPromptTab.PRESETS, + onClick = { selectedTab = SystemPromptTab.PRESETS }, + shape = SegmentedButtonDefaults.itemShape(index = 0, count = 3), + icon = { + if (selectedTab == SystemPromptTab.PRESETS) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null + ) + } + }, + label = { Text("Presets") } + ) + + SegmentedButton( + selected = selectedTab == SystemPromptTab.CUSTOM, + onClick = { selectedTab = SystemPromptTab.CUSTOM }, + shape = SegmentedButtonDefaults.itemShape(index = 1, count = 3), + icon = { + if (selectedTab == SystemPromptTab.CUSTOM) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null + ) + } + }, + label = { Text("Custom") } + ) + + SegmentedButton( + selected = selectedTab == SystemPromptTab.RECENTS, + onClick = { selectedTab = SystemPromptTab.RECENTS }, + shape = SegmentedButtonDefaults.itemShape(index = 2, count = 3), + icon = { + if (selectedTab == SystemPromptTab.RECENTS) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null + ) + } + }, + label = { Text("Recents") } ) } - } - AnimatedVisibility(visible = useSystemPrompt) { - Column { - TabRow(selectedTabIndex = tabIndex) { - Tab( - selected = tabIndex == 0, - onClick = { tabIndex = 0 }, - text = { Text("Staff Picks") } - ) + Spacer(modifier = Modifier.height(16.dp)) - Tab( - selected = tabIndex == 1, - onClick = { tabIndex = 1 }, - text = { Text("Recent") } - ) - } - - // Tab content - when (tabIndex) { - 0 -> PromptList( + // Content based on selected tab + when (selectedTab) { + SystemPromptTab.PRESETS -> { + PromptList( prompts = staffPickedPrompts, - selectedPrompt = selectedPrompt, - onPromptSelected = { selectedPrompt = it } - ) - 1 -> PromptList( - prompts = recentPrompts, - selectedPrompt = selectedPrompt, - onPromptSelected = { selectedPrompt = it } + selectedPromptId = selectedPrompt?.id, + expandedPromptId = expandedPromptId, + onPromptSelected = { + selectedPrompt = it + expandedPromptId = it.id + }, + onExpandPrompt = { expandedPromptId = it } ) } - // Custom prompt button - OutlinedButton( - onClick = { showCustomPromptSheet = true }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 8.dp) - ) { - Text("Custom prompt...") + SystemPromptTab.CUSTOM -> { + // Custom prompt editor + OutlinedTextField( + value = customPromptText, + onValueChange = { + customPromptText = it + // Deselect any preset prompt if typing custom + if (it.isNotBlank()) { + selectedPrompt = null + } + }, + modifier = Modifier + .fillMaxWidth() + .height(200.dp), + label = { Text("Enter system prompt") }, + placeholder = { Text("You are a helpful assistant...") }, + minLines = 5, + maxLines = 10 + ) + } + + SystemPromptTab.RECENTS -> { + if (recentPrompts.isEmpty()) { + Text( + text = "No recent prompts found.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(16.dp) + ) + } else { + PromptList( + prompts = recentPrompts, + selectedPromptId = selectedPrompt?.id, + expandedPromptId = expandedPromptId, + onPromptSelected = { + selectedPrompt = it + expandedPromptId = it.id + }, + onExpandPrompt = { expandedPromptId = it } + ) + } } } } @@ -236,7 +327,12 @@ fun ModeSelectionScreen( Mode.BENCHMARK -> onBenchmarkSelected() Mode.CONVERSATION -> { val systemPrompt = if (useSystemPrompt) { - selectedPrompt?.content ?: customPromptText.takeIf { it.isNotBlank() } + when (selectedTab) { + SystemPromptTab.PRESETS, SystemPromptTab.RECENTS -> + selectedPrompt?.content + SystemPromptTab.CUSTOM -> + customPromptText.takeIf { it.isNotBlank() } + } } else null onConversationSelected(systemPrompt) } @@ -268,121 +364,91 @@ fun ModeSelectionScreen( } } } - - // Custom prompt bottom sheet - if (showCustomPromptSheet) { - ModalBottomSheet( - onDismissRequest = { showCustomPromptSheet = false }, - sheetState = sheetState - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Text( - text = "Custom System Prompt", - style = MaterialTheme.typography.titleLarge - ) - - Spacer(modifier = Modifier.height(16.dp)) - - OutlinedTextField( - value = customPromptText, - onValueChange = { customPromptText = it }, - modifier = Modifier - .fillMaxWidth() - .height(200.dp), - label = { Text("Enter system prompt") }, - placeholder = { Text("You are a helpful assistant...") } - ) - - Spacer(modifier = Modifier.height(16.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - TextButton( - onClick = { - coroutineScope.launch { - sheetState.hide() - showCustomPromptSheet = false - } - } - ) { - Text("Cancel") - } - - Spacer(modifier = Modifier.weight(1f)) - - Button( - onClick = { - selectedPrompt = null - coroutineScope.launch { - sheetState.hide() - showCustomPromptSheet = false - } - }, - enabled = customPromptText.isNotBlank() - ) { - Text("Use Custom Prompt") - } - } - } - } - } } } +@OptIn(ExperimentalFoundationApi::class) @Composable fun PromptList( prompts: List, - selectedPrompt: SystemPrompt?, - onPromptSelected: (SystemPrompt) -> Unit + selectedPromptId: String?, + expandedPromptId: String?, + onPromptSelected: (SystemPrompt) -> Unit, + onExpandPrompt: (String) -> Unit ) { LazyColumn( modifier = Modifier .fillMaxWidth() - .height(200.dp) - .padding(vertical = 8.dp) + .height(250.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { - items(prompts) { prompt -> - Row( + items( + items = prompts, + key = { it.id } + ) { prompt -> + val isSelected = selectedPromptId == prompt.id + val isExpanded = expandedPromptId == prompt.id + + Column( modifier = Modifier .fillMaxWidth() + .animateItemPlacement() .selectable( - selected = selectedPrompt?.id == prompt.id, - onClick = { onPromptSelected(prompt) }, - role = Role.RadioButton + selected = isSelected, + onClick = { + onPromptSelected(prompt) + onExpandPrompt(prompt.id) + } ) - .padding(vertical = 8.dp, horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically + .padding(vertical = 8.dp) ) { - RadioButton( - selected = selectedPrompt?.id == prompt.id, - onClick = null // handled by parent selectable - ) - - Column( - modifier = Modifier.padding(start = 16.dp) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() ) { - Text( - text = prompt.name, - style = MaterialTheme.typography.titleMedium + RadioButton( + selected = isSelected, + onClick = null // Handled by selectable ) - Text( - text = prompt.content, - style = MaterialTheme.typography.bodySmall, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - color = MaterialTheme.colorScheme.onSurfaceVariant + Column( + modifier = Modifier + .weight(1f) + .padding(start = 8.dp) + ) { + // Format title for recents if needed + val title = if (prompt.category == SystemPrompt.Category.USER_CREATED && prompt.lastUsed != null) { + val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()) + dateFormat.format(Date(prompt.lastUsed)) + } else { + prompt.name + } + + Text( + text = title, + style = MaterialTheme.typography.titleSmall, + color = if (isSelected) + MaterialTheme.colorScheme.primary + else + MaterialTheme.colorScheme.onSurface + ) + + Text( + text = prompt.content, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = if (isExpanded) Int.MAX_VALUE else 2, + overflow = if (isExpanded) TextOverflow.Visible else TextOverflow.Ellipsis + ) + } + } + + if (prompt != prompts.last()) { + HorizontalDivider( + modifier = Modifier.padding(top = 8.dp, start = 40.dp) ) } } - - Divider(modifier = Modifier.padding(horizontal = 16.dp)) } } }