UI: polish system prompt setup UI
This commit is contained in:
parent
a7ee3d305f
commit
5868eaa66b
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue