diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt index 868e3099..b98fff6c 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/ChatScreen.kt @@ -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") diff --git a/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt index 8a170310..75d1906d 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/chat/ChatViewModelTest.kt @@ -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 {