package ai.pipecat.simple_chatbot_client import ai.pipecat.simple_chatbot_client.ui.InCallLayout import ai.pipecat.simple_chatbot_client.ui.PermissionScreen import ai.pipecat.simple_chatbot_client.ui.theme.Colors import ai.pipecat.simple_chatbot_client.ui.theme.RTVIClientTheme import ai.pipecat.simple_chatbot_client.ui.theme.TextStyles import ai.pipecat.simple_chatbot_client.ui.theme.textFieldColors import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.annotation.DrawableRes import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() val voiceClientManager = VoiceClientManager(this) setContent { RTVIClientTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> Box( Modifier .fillMaxSize() .padding(innerPadding) ) { PermissionScreen() val vcState = voiceClientManager.state.value if (vcState != null) { InCallLayout(voiceClientManager) } else { ConnectSettings(voiceClientManager) } voiceClientManager.errors.firstOrNull()?.let { errorText -> val dismiss: () -> Unit = { voiceClientManager.errors.removeAt(0) } AlertDialog( shape = RectangleShape, onDismissRequest = dismiss, confirmButton = { Button( onClick = dismiss, shape = RectangleShape ) { Text( text = "OK", fontSize = 14.sp, fontWeight = FontWeight.W700, color = Color.White, style = TextStyles.base ) } }, containerColor = Color.White, title = { Text( text = "Error", fontSize = 18.sp, fontWeight = FontWeight.W700, color = Color.Black, style = TextStyles.base ) }, text = { Text( text = errorText.message, fontSize = 14.sp, fontWeight = FontWeight.W400, color = Color.Black, style = TextStyles.base ) } ) } } } } } } } @Composable fun ConnectSettings( voiceClientManager: VoiceClientManager, ) { val scrollState = rememberScrollState() val start = { val backendUrl = Preferences.backendUrl.value val apiKey = Preferences.apiKey.value val transportType = Preferences.transport.value?.let(TransportType::fromString) ?: TransportType.Daily voiceClientManager.start( transportType = transportType, params = ClientStartParams( backendUrl = backendUrl ?: "", apiKey = apiKey ?: "" ) ) } Column(Modifier.fillMaxSize()) { Row( modifier = Modifier.padding(vertical = 12.dp, horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically ) { Text( modifier = Modifier.weight(1f), text = "Pipecat Android Client", fontSize = 18.sp, fontWeight = FontWeight.W900, style = TextStyles.base, color = Color.Black ) Image( modifier = Modifier.size(48.dp), painter = painterResource(R.drawable.pipecat), contentDescription = null ) } HDivider() Box( modifier = Modifier .fillMaxWidth() .weight(1f) .verticalScroll(scrollState) .imePadding() .padding(20.dp), ) { Column( Modifier .fillMaxWidth() .padding( vertical = 24.dp, horizontal = 28.dp ) ) { @Composable fun FieldLabel(@DrawableRes icon: Int, text: String) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( modifier = Modifier.size(16.dp), painter = painterResource(icon), contentDescription = null, tint = Colors.unmutedMicBackground ) Spacer(modifier = Modifier.width(8.dp)) Text( text = text.uppercase(), fontSize = 14.sp, fontWeight = FontWeight.W700, style = TextStyles.base, color = Color.Black ) } Spacer(modifier = Modifier.height(12.dp)) } @Composable fun FieldSpacer() { Spacer(modifier = Modifier.height(48.dp)) } FieldLabel(R.drawable.network_outline, "Transport") Row { TransportButton(TransportType.Daily) Spacer(modifier = Modifier.width(8.dp)) TransportButton(TransportType.SmallWebrtc) } FieldSpacer() FieldLabel(R.drawable.link, "Backend URL") TextField( modifier = Modifier .fillMaxWidth() .border(1.dp, Colors.textFieldBorder, RectangleShape), value = Preferences.backendUrl.value ?: "", onValueChange = { Preferences.backendUrl.value = it }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Uri, imeAction = ImeAction.Next ), colors = textFieldColors(), shape = RectangleShape, textStyle = TextStyles.base ) FieldSpacer() FieldLabel(R.drawable.key_outline, "API key (optional)") TextField( modifier = Modifier .fillMaxWidth() .border(1.dp, Colors.textFieldBorder, RectangleShape), value = Preferences.apiKey.value ?: "", onValueChange = { Preferences.apiKey.value = it }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Unspecified, imeAction = ImeAction.Next ), colors = textFieldColors(), shape = RectangleShape, textStyle = TextStyles.base, ) FieldSpacer() Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { ConnectDialogButton( modifier = Modifier.weight(1f), onClick = start, text = "Connect", foreground = Color.White, background = Colors.buttonNormal, border = Colors.buttonNormal ) } } } } } @Composable private fun ConnectDialogButton( onClick: () -> Unit, text: String, foreground: Color, background: Color, border: Color, modifier: Modifier = Modifier, @DrawableRes icon: Int? = null, ) { val shape = RectangleShape Row( modifier .border(1.dp, border, shape) .clip(shape) .background(background) .clickable(onClick = onClick) .padding(vertical = 10.dp, horizontal = 24.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { if (icon != null) { Icon( modifier = Modifier.size(24.dp), painter = painterResource(icon), tint = foreground, contentDescription = null ) Spacer(modifier = Modifier.width(8.dp)) } Text( text = text.uppercase(), fontSize = 14.sp, fontWeight = FontWeight.W700, color = foreground, style = TextStyles.base ) } } @Composable private fun TransportButton( type: TransportType ) { val currentTransport = Preferences.transport.value?.let(TransportType::fromString) ?: TransportType.Daily val isSelected = type == currentTransport val shape = RectangleShape val background = if (isSelected) Color.White else Colors.chipDeselectedBackground val textColor = if (isSelected) Colors.buttonNormal else Colors.chipDeselectedText Row( Modifier .clip(shape) .clickable { Preferences.transport.value = type.toString() } .background(background) .border(width = 1.dp, color = textColor, shape = shape) .padding(horizontal = 18.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically ) { Image( modifier = Modifier.size(16.dp), painter = painterResource(type.icon), contentDescription = null, ) Spacer(Modifier.width(10.dp)) Text( text = type.label, fontSize = 16.sp, fontWeight = FontWeight.W700, color = textColor, style = TextStyles.base ) } } @Preview @Composable private fun TransportButtonPreview() { RTVIClientTheme { TransportButton(TransportType.SmallWebrtc) } }