feat: add support for simple speech to text (#34)

Reviewed-on: #34
This commit was merged in pull request #34.
This commit is contained in:
2026-07-03 08:38:58 +00:00
parent a2d28ecc40
commit 8b2f0faa59
2 changed files with 82 additions and 17 deletions

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M280,720L280,240L360,240L360,720L280,720ZM440,880L440,80L520,80L520,880L440,880ZM120,560L120,400L200,400L200,560L120,560ZM600,720L600,240L680,240L680,720L600,720ZM760,560L760,400L840,400L840,560L760,560Z" />
</vector>

View File

@@ -1,8 +1,17 @@
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.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
@@ -66,6 +75,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 +163,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 +376,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 +587,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 +614,36 @@ 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"
)
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"
)
}
}
}
}
}