feat(chat): 思维链保留 10 分钟并自动折叠;输入栏下方留白承载忙碌提示
流式结束后把思维链挂到落库消息上,10 分钟后自动清除;ThinkingCard 在 active 变 false 时自动折叠,历史思维链默认折叠。输入栏底部预留 22dp 避开屏幕圆角,顶栏副标题只留连接状态,忙碌提示移到输入栏下方(带左缩进)。
This commit is contained in:
1 parent
5975827aea
commit
5a5b02f6fb
3 files changed
+80
-27
No files matched your search
@@ -3,11 +3,13 @@ package top.yeij.cyrene.ui.chat
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
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.width
|
||||
@@ -45,6 +47,9 @@ import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
/** 判定「贴着底部」的容差(px)。 */
|
||||
private const val BOTTOM_TOLERANCE = 64
|
||||
|
||||
/** 输入栏下方预留的高度:避开屏幕圆角,同时承载忙碌提示。 */
|
||||
private val InputBottomSpacer = 22.dp
|
||||
|
||||
@Composable
|
||||
fun ChatScreen(
|
||||
container: AppContainer,
|
||||
@@ -96,7 +101,7 @@ fun ChatScreen(
|
||||
topBar = {
|
||||
SmallTopAppBar(
|
||||
title = "与昔涟",
|
||||
subtitle = statusLabel(state),
|
||||
subtitle = connectionLabel(state),
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onOpenDrawer) {
|
||||
Icon(imageVector = MiuixIcons.Basic.Sidebar, contentDescription = "菜单")
|
||||
@@ -107,6 +112,7 @@ fun ChatScreen(
|
||||
bottomBar = {
|
||||
InputBar(
|
||||
value = state.draft,
|
||||
busyText = busyLabel(state),
|
||||
onValueChange = viewModel::onDraftChange,
|
||||
onSend = viewModel::send,
|
||||
)
|
||||
@@ -172,30 +178,46 @@ fun ChatScreen(
|
||||
@Composable
|
||||
private fun InputBar(
|
||||
value: String,
|
||||
busyText: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
onSend: () -> Unit,
|
||||
) {
|
||||
Row(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.imePadding()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
.padding(start = 12.dp, end = 12.dp, top = 8.dp, bottom = 4.dp),
|
||||
) {
|
||||
TextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
label = "和昔涟说点什么…",
|
||||
useLabelAsPlaceholder = true,
|
||||
maxLines = 5,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Button(
|
||||
onClick = onSend,
|
||||
enabled = value.isNotBlank(),
|
||||
Row(verticalAlignment = Alignment.Bottom) {
|
||||
TextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
label = "和昔涟说点什么…",
|
||||
useLabelAsPlaceholder = true,
|
||||
maxLines = 5,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Button(
|
||||
onClick = onSend,
|
||||
enabled = value.isNotBlank(),
|
||||
) {
|
||||
Text("发送")
|
||||
}
|
||||
}
|
||||
// 输入栏下方预留的空间:让输入框/按钮避开屏幕圆角,同时放忙碌提示
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(InputBottomSpacer)
|
||||
.padding(start = 12.dp, top = 4.dp),
|
||||
contentAlignment = Alignment.CenterStart,
|
||||
) {
|
||||
Text("发送")
|
||||
Text(
|
||||
text = busyText,
|
||||
style = MiuixTheme.textStyles.footnote2,
|
||||
color = MiuixTheme.colorScheme.onSurfaceVariantSummary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,14 +233,18 @@ private fun CenterHint(text: String) {
|
||||
}
|
||||
}
|
||||
|
||||
private fun statusLabel(state: ChatUiState): String = when {
|
||||
/** 顶栏副标题:只反映连接状态。 */
|
||||
private fun connectionLabel(state: ChatUiState): String = when (state.connection) {
|
||||
WsState.Connected -> "已连接"
|
||||
WsState.Connecting -> "连接中…"
|
||||
WsState.Reconnecting -> "重连中…"
|
||||
WsState.Offline -> "离线"
|
||||
WsState.Idle -> ""
|
||||
}
|
||||
|
||||
/** 输入栏下方的忙碌提示。 */
|
||||
private fun busyLabel(state: ChatUiState): String = when {
|
||||
state.busyMode != null && state.queueDepth > 0 -> "正在思考… 已排队 ${state.queueDepth} 条"
|
||||
state.busyMode != null -> "正在思考…"
|
||||
else -> when (state.connection) {
|
||||
WsState.Connected -> "已连接"
|
||||
WsState.Connecting -> "连接中…"
|
||||
WsState.Reconnecting -> "重连中…"
|
||||
WsState.Offline -> "离线"
|
||||
WsState.Idle -> ""
|
||||
}
|
||||
else -> ""
|
||||
}
|
||||
@@ -304,6 +304,13 @@ class ChatViewModel(
|
||||
if (msg.sessionId != sessionId) return
|
||||
|
||||
val clearStreaming = msg.role == "assistant"
|
||||
// 流式结束:把这一轮的思维链挂到落库消息上,保留 10 分钟后再自动消失
|
||||
val streamedThinking = if (clearStreaming) {
|
||||
_state.value.streaming?.thinking?.takeIf { it.isNotBlank() }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val persisted = if (streamedThinking != null) msg.copy(thinking = streamedThinking) else msg
|
||||
if (clearStreaming) streamParser = null
|
||||
|
||||
_state.update { state ->
|
||||
@@ -314,11 +321,24 @@ class ChatViewModel(
|
||||
state.pending
|
||||
}
|
||||
state.copy(
|
||||
messages = mergeMessages(state.messages, listOf(msg)),
|
||||
messages = mergeMessages(state.messages, listOf(persisted)),
|
||||
pending = pending,
|
||||
streaming = if (clearStreaming) null else state.streaming,
|
||||
)
|
||||
}
|
||||
|
||||
if (streamedThinking != null) {
|
||||
viewModelScope.launch {
|
||||
kotlinx.coroutines.delay(THINKING_LINGER_MILLIS)
|
||||
_state.update { state ->
|
||||
state.copy(
|
||||
messages = state.messages.map {
|
||||
if (it.id == persisted.id) it.copy(thinking = null) else it
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 上拉加载更早的一页。 */
|
||||
@@ -358,3 +378,6 @@ class ChatViewModel(
|
||||
|
||||
private fun Throwable.userMessage(): String =
|
||||
(this as? CyreneException)?.error?.userMessage ?: "加载失败,请稍后重试"
|
||||
|
||||
/** 思维链在流式结束后继续保留的时长。 */
|
||||
private const val THINKING_LINGER_MILLIS = 10 * 60 * 1000L
|
||||
@@ -33,7 +33,11 @@ fun ThinkingCard(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (text.isBlank() && !active) return
|
||||
var expanded by remember { mutableStateOf(true) }
|
||||
// 流式期间展开(方便观察),流式结束自动折叠;历史思维链默认折叠。
|
||||
var expanded by remember { mutableStateOf(active) }
|
||||
LaunchedEffect(active) {
|
||||
if (!active) expanded = false
|
||||
}
|
||||
|
||||
FoldCard(modifier = modifier, onClick = { expanded = !expanded }) {
|
||||
FoldCardHeader(
|
||||
|
||||
Reference in new issue
Block a user