feat(#125): add free-form text input and option tap/long-press behavior to ClarifyDialog

This commit is contained in:
Hy4ri committed 2026-06-20 12:28:14 +03:00
1 parent 42f8cfe509
commit 8d31e37baa
2 files changed
+90 -8

No files matched your search

@@ -18,6 +18,7 @@ import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
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.graphics.Brush
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.font.FontWeight
@@ -788,24 +790,63 @@ private fun ClarifyDialog(
onOptionSelected: (String) -> Unit,
onDismiss: () -> Unit,
) {
var typedText by rememberSaveable { mutableStateOf("") }
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Clarification Needed") },
text = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(text = clarify.text)
Spacer(modifier = Modifier.height(8.dp))
clarify.options.forEach { option ->
FilledTonalButton(
onClick = { onOptionSelected(option) },
modifier = Modifier.fillMaxWidth(),
) {
Text(option)
if (clarify.options.isNotEmpty()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Tap an option to copy to input, or long-press to send directly.",
style = MaterialTheme.typography.bodySmall,
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 = {
TextButton(onClick = onDismiss) {
Text("Dismiss")
@@ -323,6 +323,47 @@ class ChatViewModelTest {
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
fun testSendMessage() =
runTest {