refactor(android): unify chat status label
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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 ->
|
||||||
|
|||||||
Reference in New Issue
Block a user