UI: polish system prompt setup UI

This commit is contained in:
Han Yin 2025-04-12 11:41:54 -07:00
parent a7ee3d305f
commit 5868eaa66b
1 changed files with 254 additions and 188 deletions

View File

@ -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<Mode?>(null) }
var useSystemPrompt by remember { mutableStateOf(false) }
var selectedPrompt by remember { mutableStateOf<SystemPrompt?>(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<SystemPrompt?>(staffPickedPrompts.firstOrNull()) }
var selectedTab by remember { mutableStateOf(SystemPromptTab.PRESETS) }
var customPromptText by remember { mutableStateOf("") }
var expandedPromptId by remember { mutableStateOf<String?>(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<SystemPrompt>,
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))
}
}
}