Android: polish settings UI
This commit is contained in:
@@ -62,7 +62,8 @@ class ChatController(
|
|||||||
|
|
||||||
fun onDisconnected(message: String) {
|
fun onDisconnected(message: String) {
|
||||||
_healthOk.value = false
|
_healthOk.value = false
|
||||||
_errorText.value = message
|
// Not an error; keep connection status in the UI pill.
|
||||||
|
_errorText.value = null
|
||||||
clearPendingRuns()
|
clearPendingRuns()
|
||||||
pendingToolCallsById.clear()
|
pendingToolCallsById.clear()
|
||||||
publishPendingToolCalls()
|
publishPendingToolCalls()
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import android.Manifest
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
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.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
@@ -11,19 +13,24 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
|
||||||
import androidx.compose.foundation.layout.only
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ExpandLess
|
||||||
|
import androidx.compose.material.icons.filled.ExpandMore
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -35,6 +42,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.steipete.clawdis.node.MainViewModel
|
import com.steipete.clawdis.node.MainViewModel
|
||||||
@@ -56,9 +64,11 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
val serverName by viewModel.serverName.collectAsState()
|
val serverName by viewModel.serverName.collectAsState()
|
||||||
val remoteAddress by viewModel.remoteAddress.collectAsState()
|
val remoteAddress by viewModel.remoteAddress.collectAsState()
|
||||||
val bridges by viewModel.bridges.collectAsState()
|
val bridges by viewModel.bridges.collectAsState()
|
||||||
val listState = rememberLazyListState()
|
|
||||||
|
|
||||||
|
val listState = rememberLazyListState()
|
||||||
val (wakeWordsText, setWakeWordsText) = remember { mutableStateOf("") }
|
val (wakeWordsText, setWakeWordsText) = remember { mutableStateOf("") }
|
||||||
|
val (advancedExpanded, setAdvancedExpanded) = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(wakeWords) { setWakeWordsText(wakeWords.joinToString(", ")) }
|
LaunchedEffect(wakeWords) { setWakeWordsText(wakeWords.joinToString(", ")) }
|
||||||
|
|
||||||
val permissionLauncher =
|
val permissionLauncher =
|
||||||
@@ -67,6 +77,29 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
viewModel.setCameraEnabled(cameraOk)
|
viewModel.setCameraEnabled(cameraOk)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setCameraEnabledChecked(checked: Boolean) {
|
||||||
|
if (!checked) {
|
||||||
|
viewModel.setCameraEnabled(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val cameraOk =
|
||||||
|
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==
|
||||||
|
PackageManager.PERMISSION_GRANTED
|
||||||
|
if (cameraOk) {
|
||||||
|
viewModel.setCameraEnabled(true)
|
||||||
|
} else {
|
||||||
|
permissionLauncher.launch(arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val bridgeDiscoveryFooterText =
|
||||||
|
if (bridges.isEmpty()) {
|
||||||
|
"Searching for bridges…"
|
||||||
|
} else {
|
||||||
|
"Discovery active • ${bridges.size} bridge${if (bridges.size == 1) "" else "s"} found"
|
||||||
|
}
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier =
|
modifier =
|
||||||
@@ -76,9 +109,9 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
.imePadding()
|
.imePadding()
|
||||||
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)),
|
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Bottom)),
|
||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(14.dp),
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
) {
|
) {
|
||||||
item { Text("Node") }
|
item { Text("Node", style = MaterialTheme.typography.titleSmall) }
|
||||||
item {
|
item {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = displayName,
|
value = displayName,
|
||||||
@@ -87,11 +120,11 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item { Text("Instance ID: $instanceId") }
|
item { Text("Instance ID: $instanceId", color = MaterialTheme.colorScheme.onSurfaceVariant) }
|
||||||
|
|
||||||
item { HorizontalDivider() }
|
item { HorizontalDivider() }
|
||||||
|
|
||||||
item { Text("Wake Words") }
|
item { Text("Wake Words", style = MaterialTheme.typography.titleSmall) }
|
||||||
item {
|
item {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = wakeWordsText,
|
value = wakeWordsText,
|
||||||
@@ -123,56 +156,50 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
} else {
|
} else {
|
||||||
"Connect to a gateway to sync wake words globally."
|
"Connect to a gateway to sync wake words globally."
|
||||||
},
|
},
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item { HorizontalDivider() }
|
item { HorizontalDivider() }
|
||||||
|
|
||||||
item { Text("Camera") }
|
item { Text("Camera", style = MaterialTheme.typography.titleSmall) }
|
||||||
item {
|
item {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth()) {
|
ListItem(
|
||||||
Switch(
|
headlineContent = { Text("Allow Camera") },
|
||||||
checked = cameraEnabled,
|
supportingContent = { Text("Allows the bridge to request photos or short video clips (foreground only).") },
|
||||||
onCheckedChange = { enabled ->
|
trailingContent = { Switch(checked = cameraEnabled, onCheckedChange = ::setCameraEnabledChecked) },
|
||||||
if (!enabled) {
|
)
|
||||||
viewModel.setCameraEnabled(false)
|
}
|
||||||
return@Switch
|
item {
|
||||||
}
|
Text(
|
||||||
|
"Tip: grant Microphone permission for video clips with audio.",
|
||||||
val cameraOk =
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) ==
|
)
|
||||||
PackageManager.PERMISSION_GRANTED
|
|
||||||
if (cameraOk) {
|
|
||||||
viewModel.setCameraEnabled(true)
|
|
||||||
} else {
|
|
||||||
permissionLauncher.launch(arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
Text(if (cameraEnabled) "Allow Camera" else "Camera Disabled")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
item { Text("Tip: grant Microphone permission for video clips with audio.") }
|
|
||||||
|
|
||||||
item { HorizontalDivider() }
|
item { HorizontalDivider() }
|
||||||
|
|
||||||
item { Text("Screen") }
|
item { Text("Screen", style = MaterialTheme.typography.titleSmall) }
|
||||||
item {
|
item {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth()) {
|
ListItem(
|
||||||
Switch(checked = preventSleep, onCheckedChange = viewModel::setPreventSleep)
|
headlineContent = { Text("Prevent Sleep") },
|
||||||
Text(if (preventSleep) "Prevent Sleep" else "Allow Sleep")
|
supportingContent = { Text("Keeps the screen awake while Clawdis is open.") },
|
||||||
}
|
trailingContent = { Switch(checked = preventSleep, onCheckedChange = viewModel::setPreventSleep) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
item { Text("Keeps the screen awake while Clawdis is open.") }
|
|
||||||
|
|
||||||
item { HorizontalDivider() }
|
item { HorizontalDivider() }
|
||||||
|
|
||||||
item { Text("Bridge") }
|
item { Text("Bridge", style = MaterialTheme.typography.titleSmall) }
|
||||||
item { Text("Status: $statusText") }
|
item { ListItem(headlineContent = { Text("Status") }, supportingContent = { Text(statusText) }) }
|
||||||
item { if (serverName != null) Text("Server: $serverName") }
|
if (serverName != null) {
|
||||||
item { if (remoteAddress != null) Text("Address: $remoteAddress") }
|
item { ListItem(headlineContent = { Text("Server") }, supportingContent = { Text(serverName!!) }) }
|
||||||
|
}
|
||||||
|
if (remoteAddress != null) {
|
||||||
|
item { ListItem(headlineContent = { Text("Address") }, supportingContent = { Text(remoteAddress!!) }) }
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
if (isConnected) {
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.disconnect()
|
viewModel.disconnect()
|
||||||
@@ -186,72 +213,95 @@ fun SettingsSheet(viewModel: MainViewModel) {
|
|||||||
|
|
||||||
item { HorizontalDivider() }
|
item { HorizontalDivider() }
|
||||||
|
|
||||||
item { Text("Advanced") }
|
item { Text("Discovered Bridges", style = MaterialTheme.typography.titleSmall) }
|
||||||
item {
|
if (bridges.isEmpty()) {
|
||||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth()) {
|
item { Text("No bridges found yet.", color = MaterialTheme.colorScheme.onSurfaceVariant) }
|
||||||
Switch(checked = manualEnabled, onCheckedChange = viewModel::setManualEnabled)
|
} else {
|
||||||
Text(if (manualEnabled) "Manual Bridge Enabled" else "Manual Bridge Disabled")
|
items(items = bridges, key = { it.stableId }) { bridge ->
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(bridge.name) },
|
||||||
|
supportingContent = { Text("${bridge.host}:${bridge.port}") },
|
||||||
|
trailingContent = {
|
||||||
|
Button(
|
||||||
|
onClick = {
|
||||||
|
NodeForegroundService.start(context)
|
||||||
|
viewModel.connect(bridge)
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text("Connect")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
OutlinedTextField(
|
Text(
|
||||||
value = manualHost,
|
bridgeDiscoveryFooterText,
|
||||||
onValueChange = viewModel::setManualHost,
|
|
||||||
label = { Text("Host") },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
enabled = manualEnabled,
|
textAlign = TextAlign.Center,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
|
||||||
OutlinedTextField(
|
|
||||||
value = manualPort.toString(),
|
|
||||||
onValueChange = { v -> viewModel.setManualPort(v.toIntOrNull() ?: 0) },
|
|
||||||
label = { Text("Port") },
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
enabled = manualEnabled,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
Button(
|
|
||||||
onClick = {
|
|
||||||
NodeForegroundService.start(context)
|
|
||||||
viewModel.connectManual()
|
|
||||||
},
|
|
||||||
enabled = manualEnabled,
|
|
||||||
) {
|
|
||||||
Text("Connect (Manual)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item { HorizontalDivider() }
|
item { HorizontalDivider() }
|
||||||
|
|
||||||
item { Text("Discovered Bridges") }
|
item {
|
||||||
if (bridges.isEmpty()) {
|
ListItem(
|
||||||
item { Text("No bridges found yet.") }
|
headlineContent = { Text("Advanced") },
|
||||||
} else {
|
supportingContent = { Text("Manual bridge connection") },
|
||||||
items(items = bridges, key = { it.stableId }) { bridge ->
|
trailingContent = {
|
||||||
Row(
|
Icon(
|
||||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
imageVector = if (advancedExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
contentDescription = if (advancedExpanded) "Collapse" else "Expand",
|
||||||
) {
|
)
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
},
|
||||||
Text(bridge.name)
|
modifier =
|
||||||
Text("${bridge.host}:${bridge.port}")
|
Modifier.clickable {
|
||||||
}
|
setAdvancedExpanded(!advancedExpanded)
|
||||||
Spacer(modifier = Modifier.padding(4.dp))
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
AnimatedVisibility(visible = advancedExpanded) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth()) {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text("Use Manual Bridge") },
|
||||||
|
supportingContent = { Text("Use this when discovery is blocked.") },
|
||||||
|
trailingContent = { Switch(checked = manualEnabled, onCheckedChange = viewModel::setManualEnabled) },
|
||||||
|
)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = manualHost,
|
||||||
|
onValueChange = viewModel::setManualHost,
|
||||||
|
label = { Text("Host") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = manualEnabled,
|
||||||
|
)
|
||||||
|
OutlinedTextField(
|
||||||
|
value = manualPort.toString(),
|
||||||
|
onValueChange = { v -> viewModel.setManualPort(v.toIntOrNull() ?: 0) },
|
||||||
|
label = { Text("Port") },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
enabled = manualEnabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
val hostOk = manualHost.trim().isNotEmpty()
|
||||||
|
val portOk = manualPort in 1..65535
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
NodeForegroundService.start(context)
|
NodeForegroundService.start(context)
|
||||||
viewModel.connect(bridge)
|
viewModel.connectManual()
|
||||||
},
|
},
|
||||||
|
enabled = manualEnabled && hostOk && portOk,
|
||||||
) {
|
) {
|
||||||
Text("Connect")
|
Text("Connect (Manual)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
HorizontalDivider()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item { Spacer(modifier = Modifier.height(20.dp)) }
|
item { Spacer(modifier = Modifier.height(20.dp)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ import androidx.compose.ui.unit.dp
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChatComposer(
|
fun ChatComposer(
|
||||||
sessionKey: String,
|
|
||||||
healthOk: Boolean,
|
healthOk: Boolean,
|
||||||
thinkingLevel: String,
|
thinkingLevel: String,
|
||||||
pendingRunCount: Int,
|
pendingRunCount: Int,
|
||||||
@@ -112,7 +111,6 @@ fun ChatComposer(
|
|||||||
)
|
)
|
||||||
|
|
||||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||||
ConnectionPill(sessionKey = sessionKey, healthOk = healthOk)
|
|
||||||
Spacer(modifier = Modifier.weight(1f))
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
if (pendingRunCount > 0) {
|
if (pendingRunCount > 0) {
|
||||||
@@ -220,29 +218,3 @@ private fun AttachmentChip(fileName: String, onRemove: () -> Unit) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ConnectionPill(sessionKey: String, healthOk: Boolean) {
|
|
||||||
Surface(
|
|
||||||
shape = RoundedCornerShape(999.dp),
|
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerLow,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier.size(7.dp),
|
|
||||||
shape = androidx.compose.foundation.shape.CircleShape,
|
|
||||||
color = if (healthOk) Color(0xFF2ECC71) else Color(0xFFF39C12),
|
|
||||||
) {}
|
|
||||||
Text(sessionKey, style = MaterialTheme.typography.labelSmall)
|
|
||||||
Text(
|
|
||||||
if (healthOk) "Connected" else "Connecting…",
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import com.steipete.clawdis.node.chat.ChatPendingToolCall
|
|||||||
@Composable
|
@Composable
|
||||||
fun ChatMessageListCard(
|
fun ChatMessageListCard(
|
||||||
sessionKey: String,
|
sessionKey: String,
|
||||||
|
isBridgeConnected: Boolean,
|
||||||
healthOk: Boolean,
|
healthOk: Boolean,
|
||||||
messages: List<ChatMessage>,
|
messages: List<ChatMessage>,
|
||||||
pendingRunCount: Int,
|
pendingRunCount: Int,
|
||||||
@@ -97,6 +98,7 @@ fun ChatMessageListCard(
|
|||||||
|
|
||||||
ChatStatusPill(
|
ChatStatusPill(
|
||||||
sessionKey = sessionKey,
|
sessionKey = sessionKey,
|
||||||
|
isBridgeConnected = isBridgeConnected,
|
||||||
healthOk = healthOk,
|
healthOk = healthOk,
|
||||||
onShowSessions = onShowSessions,
|
onShowSessions = onShowSessions,
|
||||||
onRefresh = onRefresh,
|
onRefresh = onRefresh,
|
||||||
@@ -113,11 +115,25 @@ fun ChatMessageListCard(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ChatStatusPill(
|
private fun ChatStatusPill(
|
||||||
sessionKey: String,
|
sessionKey: String,
|
||||||
|
isBridgeConnected: Boolean,
|
||||||
healthOk: Boolean,
|
healthOk: Boolean,
|
||||||
onShowSessions: () -> Unit,
|
onShowSessions: () -> Unit,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val statusText =
|
||||||
|
when {
|
||||||
|
!isBridgeConnected -> "Offline"
|
||||||
|
healthOk -> "Connected"
|
||||||
|
else -> "Connecting…"
|
||||||
|
}
|
||||||
|
val statusColor =
|
||||||
|
when {
|
||||||
|
!isBridgeConnected -> Color(0xFF9E9E9E)
|
||||||
|
healthOk -> Color(0xFF2ECC71)
|
||||||
|
else -> Color(0xFFF39C12)
|
||||||
|
}
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
shape = MaterialTheme.shapes.large,
|
shape = MaterialTheme.shapes.large,
|
||||||
@@ -133,7 +149,7 @@ private fun ChatStatusPill(
|
|||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.size(7.dp),
|
modifier = Modifier.size(7.dp),
|
||||||
shape = androidx.compose.foundation.shape.CircleShape,
|
shape = androidx.compose.foundation.shape.CircleShape,
|
||||||
color = if (healthOk) Color(0xFF2ECC71) else Color(0xFFF39C12),
|
color = statusColor,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
@@ -141,7 +157,7 @@ private fun ChatStatusPill(
|
|||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = if (healthOk) "Connected" else "Connecting…",
|
text = statusText,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.alpha(0.9f),
|
modifier = Modifier.alpha(0.9f),
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ fun ChatSheetContent(viewModel: MainViewModel) {
|
|||||||
val messages by viewModel.chatMessages.collectAsState()
|
val messages by viewModel.chatMessages.collectAsState()
|
||||||
val errorText by viewModel.chatError.collectAsState()
|
val errorText by viewModel.chatError.collectAsState()
|
||||||
val pendingRunCount by viewModel.pendingRunCount.collectAsState()
|
val pendingRunCount by viewModel.pendingRunCount.collectAsState()
|
||||||
|
val isBridgeConnected by viewModel.isConnected.collectAsState()
|
||||||
val healthOk by viewModel.chatHealthOk.collectAsState()
|
val healthOk by viewModel.chatHealthOk.collectAsState()
|
||||||
val sessionKey by viewModel.chatSessionKey.collectAsState()
|
val sessionKey by viewModel.chatSessionKey.collectAsState()
|
||||||
val thinkingLevel by viewModel.chatThinkingLevel.collectAsState()
|
val thinkingLevel by viewModel.chatThinkingLevel.collectAsState()
|
||||||
@@ -79,6 +80,7 @@ fun ChatSheetContent(viewModel: MainViewModel) {
|
|||||||
) {
|
) {
|
||||||
ChatMessageListCard(
|
ChatMessageListCard(
|
||||||
sessionKey = sessionKey,
|
sessionKey = sessionKey,
|
||||||
|
isBridgeConnected = isBridgeConnected,
|
||||||
healthOk = healthOk,
|
healthOk = healthOk,
|
||||||
messages = messages,
|
messages = messages,
|
||||||
pendingRunCount = pendingRunCount,
|
pendingRunCount = pendingRunCount,
|
||||||
@@ -90,7 +92,6 @@ fun ChatSheetContent(viewModel: MainViewModel) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
ChatComposer(
|
ChatComposer(
|
||||||
sessionKey = sessionKey,
|
|
||||||
healthOk = healthOk,
|
healthOk = healthOk,
|
||||||
thinkingLevel = thinkingLevel,
|
thinkingLevel = thinkingLevel,
|
||||||
pendingRunCount = pendingRunCount,
|
pendingRunCount = pendingRunCount,
|
||||||
|
|||||||
Reference in New Issue
Block a user