feat(#125): add free-form text input and option tap/long-press behavior to ClarifyDialog
This commit is contained in:
@@ -18,6 +18,7 @@ import androidx.compose.animation.slideOutVertically
|
|||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -82,6 +83,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -788,24 +790,63 @@ private fun ClarifyDialog(
|
|||||||
onOptionSelected: (String) -> Unit,
|
onOptionSelected: (String) -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
var typedText by rememberSaveable { mutableStateOf("") }
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text("Clarification Needed") },
|
title = { Text("Clarification Needed") },
|
||||||
text = {
|
text = {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
Text(text = clarify.text)
|
Text(text = clarify.text)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
|
||||||
clarify.options.forEach { option ->
|
if (clarify.options.isNotEmpty()) {
|
||||||
FilledTonalButton(
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
onClick = { onOptionSelected(option) },
|
Text(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
text = "Tap an option to copy to input, or long-press to send directly.",
|
||||||
) {
|
style = MaterialTheme.typography.bodySmall,
|
||||||
Text(option)
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
clarify.options.forEach { option ->
|
||||||
|
FilledTonalButton(
|
||||||
|
onClick = { /* onTap handled by pointerInput */ },
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.pointerInput(option) {
|
||||||
|
detectTapGestures(
|
||||||
|
onTap = { typedText = option },
|
||||||
|
onLongPress = { onOptionSelected(option) },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(option)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
OutlinedTextField(
|
||||||
|
value = typedText,
|
||||||
|
onValueChange = { typedText = it },
|
||||||
|
label = { Text("Response") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
if (typedText.isNotBlank()) {
|
||||||
|
onOptionSelected(typedText)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled = typedText.isNotBlank(),
|
||||||
|
) {
|
||||||
|
Text("Send")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {},
|
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = onDismiss) {
|
TextButton(onClick = onDismiss) {
|
||||||
Text("Dismiss")
|
Text("Dismiss")
|
||||||
|
|||||||
@@ -323,6 +323,47 @@ class ChatViewModelTest {
|
|||||||
assertEquals(MessageRole.USER, state.messages[1].role)
|
assertEquals(MessageRole.USER, state.messages[1].role)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testClarifyRequestCustomResponse() =
|
||||||
|
runTest {
|
||||||
|
var createReqId = ""
|
||||||
|
every { HermesWsClient.send(WsMethods.SESSION_CREATE, any(), any()) } answers {
|
||||||
|
createReqId = "create-req-clarify-custom"
|
||||||
|
val onSent = arg<((String) -> Unit)?>(2)
|
||||||
|
onSent?.invoke(createReqId)
|
||||||
|
createReqId
|
||||||
|
}
|
||||||
|
|
||||||
|
val viewModel = ChatViewModel(app, startCleanup = false)
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// Set session ID
|
||||||
|
mockEventsFlow.emit(WsEvent.GatewayReady(null))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
mockEventsFlow.emit(WsEvent.RpcResult(createReqId, mapOf("session_id" to "session-123")))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
mockEventsFlow.emit(WsEvent.ClarifyRequest("Please explain:", emptyList()))
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
var state = viewModel.uiState.value
|
||||||
|
assertEquals("Please explain:", state.clarifyRequest?.text)
|
||||||
|
assertTrue(state.clarifyRequest?.options.isNullOrEmpty())
|
||||||
|
|
||||||
|
// Respond to clarify with custom text
|
||||||
|
viewModel.respondToClarify("This is my custom response text")
|
||||||
|
advanceUntilIdle()
|
||||||
|
|
||||||
|
// verify clarify request is dismissed, and user message is sent
|
||||||
|
state = viewModel.uiState.value
|
||||||
|
assertNull(state.clarifyRequest)
|
||||||
|
assertEquals(2, state.messages.size)
|
||||||
|
assertEquals("Session created", state.messages[0].content)
|
||||||
|
assertEquals("This is my custom response text", state.messages[1].content)
|
||||||
|
assertEquals(MessageRole.USER, state.messages[1].role)
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testSendMessage() =
|
fun testSendMessage() =
|
||||||
runTest {
|
runTest {
|
||||||
|
|||||||
Reference in New Issue
Block a user