From 6fe2ce3e312642b72df1ceb8fc7608baaf332553 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Fri, 3 Jul 2026 10:50:15 +0700 Subject: [PATCH 1/2] add speect to text button --- .../composeResources/drawable/ic_audio.xml | 9 ++ .../kotlin/su/reya/coop/screens/ChatScreen.kt | 82 +++++++++++++++---- 2 files changed, 74 insertions(+), 17 deletions(-) create mode 100644 composeApp/src/androidMain/composeResources/drawable/ic_audio.xml diff --git a/composeApp/src/androidMain/composeResources/drawable/ic_audio.xml b/composeApp/src/androidMain/composeResources/drawable/ic_audio.xml new file mode 100644 index 0000000..2777d1e --- /dev/null +++ b/composeApp/src/androidMain/composeResources/drawable/ic_audio.xml @@ -0,0 +1,9 @@ + + + diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt index c53a642..b20fd00 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt @@ -1,6 +1,9 @@ package su.reya.coop.screens +import android.app.Activity +import android.content.Intent import android.net.Uri +import android.speech.RecognizerIntent import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable @@ -66,6 +69,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coop.composeapp.generated.resources.Res import coop.composeapp.generated.resources.ic_add_circle import coop.composeapp.generated.resources.ic_arrow_back +import coop.composeapp.generated.resources.ic_audio import coop.composeapp.generated.resources.ic_cancel import coop.composeapp.generated.resources.ic_check_circle import coop.composeapp.generated.resources.ic_send @@ -153,9 +157,19 @@ fun ChatScreen(id: Long, screening: Boolean = false) { } } - val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> - uri?.let { sendFile(it) } - } + val fileLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> + uri?.let { sendFile(it) } + } + + val sttLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val data = result.data + val results = data?.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS) + if (!results.isNullOrEmpty()) text = results[0] + } + } LaunchedEffect(id) { // Get messages @@ -356,7 +370,24 @@ fun ChatScreen(id: Long, screening: Boolean = false) { text = "" }, onUpload = { - launcher.launch("image/*") + fileLauncher.launch("image/*") + }, + onMicClick = { + val intent = + Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH).apply { + putExtra( + RecognizerIntent.EXTRA_LANGUAGE_MODEL, + RecognizerIntent.LANGUAGE_MODEL_FREE_FORM + ) + putExtra(RecognizerIntent.EXTRA_PROMPT, "Speak now...") + } + try { + sttLauncher.launch(intent) + } catch (e: Exception) { + scope.launch { + snackbarHostState.showSnackbar("Speech recognition not available") + } + } } ) } @@ -550,7 +581,8 @@ fun ChatInput( value: String, onValueChange: (String) -> Unit, onSend: () -> Unit, - onUpload: () -> Unit + onUpload: () -> Unit, + onMicClick: () -> Unit ) { Row( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), @@ -576,18 +608,34 @@ fun ChatInput( }, ) Spacer(modifier = Modifier.size(8.dp)) - FilledTonalIconButton( - onClick = onSend, - modifier = Modifier.size(56.dp), - colors = IconButtonDefaults.filledTonalIconButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant - ) - ) { - Icon( - painter = painterResource(Res.drawable.ic_send), - contentDescription = "Send" - ) + if (value.isNotEmpty()) { + FilledTonalIconButton( + onClick = onSend, + modifier = Modifier.size(56.dp), + colors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + Icon( + painter = painterResource(Res.drawable.ic_send), + contentDescription = "Send" + ) + } + } else { + FilledTonalIconButton( + onClick = onMicClick, + modifier = Modifier.size(56.dp), + colors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + Icon( + painter = painterResource(Res.drawable.ic_audio), + contentDescription = "Speech to Text" + ) + } } } } -- 2.49.1 From 51c268c45524fcbe79c0d4b13daf9bf0e3122b27 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Fri, 3 Jul 2026 11:00:31 +0700 Subject: [PATCH 2/2] update ui --- .../kotlin/su/reya/coop/screens/ChatScreen.kt | 62 +++++++++++-------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt index b20fd00..90736da 100644 --- a/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt +++ b/composeApp/src/androidMain/kotlin/su/reya/coop/screens/ChatScreen.kt @@ -6,6 +6,12 @@ import android.net.Uri import android.speech.RecognizerIntent import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -608,33 +614,35 @@ fun ChatInput( }, ) Spacer(modifier = Modifier.size(8.dp)) - if (value.isNotEmpty()) { - FilledTonalIconButton( - onClick = onSend, - modifier = Modifier.size(56.dp), - colors = IconButtonDefaults.filledTonalIconButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant - ) - ) { - Icon( - painter = painterResource(Res.drawable.ic_send), - contentDescription = "Send" - ) - } - } else { - FilledTonalIconButton( - onClick = onMicClick, - modifier = Modifier.size(56.dp), - colors = IconButtonDefaults.filledTonalIconButtonColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant, - contentColor = MaterialTheme.colorScheme.onSurfaceVariant - ) - ) { - Icon( - painter = painterResource(Res.drawable.ic_audio), - contentDescription = "Speech to Text" - ) + AnimatedContent( + targetState = value.isNotEmpty(), + transitionSpec = { (scaleIn() + fadeIn()) togetherWith (scaleOut() + fadeOut()) }, + label = "send_mic_transition" + ) { isNotEmpty -> + if (isNotEmpty) { + IconButton( + onClick = onSend, + modifier = Modifier.size(56.dp), + colors = IconButtonDefaults.iconButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + Icon( + painter = painterResource(Res.drawable.ic_send), + contentDescription = "Send" + ) + } + } else { + FilledTonalIconButton( + onClick = onMicClick, + modifier = Modifier.size(56.dp), + ) { + Icon( + painter = painterResource(Res.drawable.ic_audio), + contentDescription = "Speech to Text" + ) + } } } } -- 2.49.1