feat: add support for simple speech to text #34

Merged
reya merged 2 commits from feat/speech-to-text into master 2026-07-03 08:38:59 +00:00
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 package su.reya.coop.screens
import android.app.Activity
import android.content.Intent
import android.net.Uri import android.net.Uri
import android.speech.RecognizerIntent
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts 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.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.Res
import coop.composeapp.generated.resources.ic_add_circle import coop.composeapp.generated.resources.ic_add_circle
import coop.composeapp.generated.resources.ic_arrow_back 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_cancel
import coop.composeapp.generated.resources.ic_check_circle import coop.composeapp.generated.resources.ic_check_circle
import coop.composeapp.generated.resources.ic_send import coop.composeapp.generated.resources.ic_send
@@ -153,9 +163,19 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
} }
} }
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri -> val fileLauncher =
uri?.let { sendFile(it) } 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) { LaunchedEffect(id) {
// Get messages // Get messages
@@ -356,7 +376,24 @@ fun ChatScreen(id: Long, screening: Boolean = false) {
text = "" text = ""
}, },
onUpload = { 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, value: String,
onValueChange: (String) -> Unit, onValueChange: (String) -> Unit,
onSend: () -> Unit, onSend: () -> Unit,
onUpload: () -> Unit onUpload: () -> Unit,
onMicClick: () -> Unit
) { ) {
Row( Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
@@ -576,18 +614,36 @@ fun ChatInput(
}, },
) )
Spacer(modifier = Modifier.size(8.dp)) Spacer(modifier = Modifier.size(8.dp))
FilledTonalIconButton( AnimatedContent(
onClick = onSend, targetState = value.isNotEmpty(),
modifier = Modifier.size(56.dp), transitionSpec = { (scaleIn() + fadeIn()) togetherWith (scaleOut() + fadeOut()) },
colors = IconButtonDefaults.filledTonalIconButtonColors( label = "send_mic_transition"
containerColor = MaterialTheme.colorScheme.surfaceVariant, ) { isNotEmpty ->
contentColor = MaterialTheme.colorScheme.onSurfaceVariant if (isNotEmpty) {
) IconButton(
) { onClick = onSend,
Icon( modifier = Modifier.size(56.dp),
painter = painterResource(Res.drawable.ic_send), colors = IconButtonDefaults.iconButtonColors(
contentDescription = "Send" 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"
)
}
}
} }
} }
} }