refactor(android): unify chat status label

This commit is contained in:
Peter Steinberger
2025-12-18 00:20:19 +01:00
parent 693215723a
commit ac4a65ddfd
3 changed files with 37 additions and 93 deletions

View File

@@ -15,6 +15,7 @@ import androidx.compose.foundation.horizontalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowUpward import androidx.compose.material.icons.filled.ArrowUpward
import androidx.compose.material.icons.filled.AttachFile import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Stop import androidx.compose.material.icons.filled.Stop
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
@@ -41,6 +42,7 @@ 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,
@@ -49,6 +51,7 @@ fun ChatComposer(
onPickImages: () -> Unit, onPickImages: () -> Unit,
onRemoveAttachment: (id: String) -> Unit, onRemoveAttachment: (id: String) -> Unit,
onSetThinkingLevel: (level: String) -> Unit, onSetThinkingLevel: (level: String) -> Unit,
onShowSessions: () -> Unit,
onRefresh: () -> Unit, onRefresh: () -> Unit,
onAbort: () -> Unit, onAbort: () -> Unit,
onSend: (text: String) -> Unit, onSend: (text: String) -> Unit,
@@ -88,6 +91,10 @@ fun ChatComposer(
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
FilledTonalIconButton(onClick = onShowSessions, modifier = Modifier.size(42.dp)) {
Icon(Icons.Default.FolderOpen, contentDescription = "Sessions")
}
FilledTonalIconButton(onClick = onRefresh, modifier = Modifier.size(42.dp)) { FilledTonalIconButton(onClick = onRefresh, modifier = Modifier.size(42.dp)) {
Icon(Icons.Default.Refresh, contentDescription = "Refresh") Icon(Icons.Default.Refresh, contentDescription = "Refresh")
} }
@@ -111,6 +118,7 @@ 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) {
@@ -147,6 +155,32 @@ fun ChatComposer(
} }
} }
@Composable
private fun ConnectionPill(sessionKey: String, healthOk: Boolean) {
Surface(
shape = RoundedCornerShape(999.dp),
color = MaterialTheme.colorScheme.surfaceContainerHighest,
) {
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,
)
}
}
}
@Composable @Composable
private fun ThinkingMenuItem( private fun ThinkingMenuItem(
value: String, value: String,

View File

@@ -3,45 +3,32 @@ package com.steipete.clawdis.node.ui.chat
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowCircleDown import androidx.compose.material.icons.filled.ArrowCircleDown
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.steipete.clawdis.node.chat.ChatMessage import com.steipete.clawdis.node.chat.ChatMessage
import com.steipete.clawdis.node.chat.ChatPendingToolCall import com.steipete.clawdis.node.chat.ChatPendingToolCall
@Composable @Composable
fun ChatMessageListCard( fun ChatMessageListCard(
sessionKey: String,
isBridgeConnected: Boolean,
healthOk: Boolean,
messages: List<ChatMessage>, messages: List<ChatMessage>,
pendingRunCount: Int, pendingRunCount: Int,
pendingToolCalls: List<ChatPendingToolCall>, pendingToolCalls: List<ChatPendingToolCall>,
streamingAssistantText: String?, streamingAssistantText: String?,
onShowSessions: () -> Unit,
onRefresh: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val listState = rememberLazyListState() val listState = rememberLazyListState()
@@ -70,7 +57,7 @@ fun ChatMessageListCard(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
state = listState, state = listState,
verticalArrangement = Arrangement.spacedBy(14.dp), verticalArrangement = Arrangement.spacedBy(14.dp),
contentPadding = androidx.compose.foundation.layout.PaddingValues(top = 44.dp, bottom = 12.dp, start = 12.dp, end = 12.dp), contentPadding = androidx.compose.foundation.layout.PaddingValues(top = 12.dp, bottom = 12.dp, start = 12.dp, end = 12.dp),
) { ) {
items(count = messages.size, key = { idx -> messages[idx].id }) { idx -> items(count = messages.size, key = { idx -> messages[idx].id }) { idx ->
ChatMessageBubble(message = messages[idx]) ChatMessageBubble(message = messages[idx])
@@ -96,15 +83,6 @@ fun ChatMessageListCard(
} }
} }
ChatStatusPill(
sessionKey = sessionKey,
isBridgeConnected = isBridgeConnected,
healthOk = healthOk,
onShowSessions = onShowSessions,
onRefresh = onRefresh,
modifier = Modifier.align(Alignment.TopStart).padding(10.dp),
)
if (messages.isEmpty() && pendingRunCount == 0 && pendingToolCalls.isEmpty() && streamingAssistantText.isNullOrBlank()) { if (messages.isEmpty() && pendingRunCount == 0 && pendingToolCalls.isEmpty() && streamingAssistantText.isNullOrBlank()) {
EmptyChatHint(modifier = Modifier.align(Alignment.Center)) EmptyChatHint(modifier = Modifier.align(Alignment.Center))
} }
@@ -112,70 +90,6 @@ fun ChatMessageListCard(
} }
} }
@Composable
private fun ChatStatusPill(
sessionKey: String,
isBridgeConnected: Boolean,
healthOk: Boolean,
onShowSessions: () -> Unit,
onRefresh: () -> Unit,
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(
modifier = modifier,
shape = MaterialTheme.shapes.large,
color = MaterialTheme.colorScheme.surfaceContainerHighest.copy(alpha = 0.96f),
shadowElevation = 1.dp,
tonalElevation = 0.dp,
) {
Row(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Surface(
modifier = Modifier.size(7.dp),
shape = androidx.compose.foundation.shape.CircleShape,
color = statusColor,
) {}
Text(
text = sessionKey,
style = MaterialTheme.typography.labelMedium,
)
Text(
text = statusText,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.alpha(0.9f),
)
Spacer(modifier = Modifier.weight(1f))
FilledTonalIconButton(onClick = onShowSessions, modifier = Modifier.size(34.dp)) {
Icon(Icons.Default.FolderOpen, contentDescription = "Sessions")
}
FilledTonalIconButton(onClick = onRefresh, modifier = Modifier.size(34.dp)) {
Icon(Icons.Default.Refresh, contentDescription = "Refresh")
}
}
}
}
@Composable @Composable
private fun EmptyChatHint(modifier: Modifier = Modifier) { private fun EmptyChatHint(modifier: Modifier = Modifier) {
Row( Row(

View File

@@ -33,7 +33,6 @@ 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,19 +78,15 @@ fun ChatSheetContent(viewModel: MainViewModel) {
verticalArrangement = Arrangement.spacedBy(10.dp), verticalArrangement = Arrangement.spacedBy(10.dp),
) { ) {
ChatMessageListCard( ChatMessageListCard(
sessionKey = sessionKey,
isBridgeConnected = isBridgeConnected,
healthOk = healthOk,
messages = messages, messages = messages,
pendingRunCount = pendingRunCount, pendingRunCount = pendingRunCount,
pendingToolCalls = pendingToolCalls, pendingToolCalls = pendingToolCalls,
streamingAssistantText = streamingAssistantText, streamingAssistantText = streamingAssistantText,
onShowSessions = { showSessions = true },
onRefresh = { viewModel.refreshChat() },
modifier = Modifier.weight(1f, fill = true), modifier = Modifier.weight(1f, fill = true),
) )
ChatComposer( ChatComposer(
sessionKey = sessionKey,
healthOk = healthOk, healthOk = healthOk,
thinkingLevel = thinkingLevel, thinkingLevel = thinkingLevel,
pendingRunCount = pendingRunCount, pendingRunCount = pendingRunCount,
@@ -100,6 +95,7 @@ fun ChatSheetContent(viewModel: MainViewModel) {
onPickImages = { pickImages.launch("image/*") }, onPickImages = { pickImages.launch("image/*") },
onRemoveAttachment = { id -> attachments.removeAll { it.id == id } }, onRemoveAttachment = { id -> attachments.removeAll { it.id == id } },
onSetThinkingLevel = { level -> viewModel.setChatThinkingLevel(level) }, onSetThinkingLevel = { level -> viewModel.setChatThinkingLevel(level) },
onShowSessions = { showSessions = true },
onRefresh = { viewModel.refreshChat() }, onRefresh = { viewModel.refreshChat() },
onAbort = { viewModel.abortChat() }, onAbort = { viewModel.abortChat() },
onSend = { text -> onSend = { text ->