feat: 优化消息列表和创建记忆屏幕逻辑

- 在MessageList组件中改进自动滚动逻辑,支持流式消息和时间分隔线的处理
- 在CreateMemoryScreen中移除冗余的agentResponse处理,直接使用historyMessages构建消息列表
- 在CreateMemoryViewModel中优化Agent回复处理,确保每条消息作为单独气泡显示并更新历史消息
This commit is contained in:
penghanyuan
2026-01-29 20:18:06 +01:00
parent 41ceb3dad8
commit c3c8eb2e6e
3 changed files with 72 additions and 38 deletions

View File

@@ -11,6 +11,7 @@ import androidx.compose.material3.MaterialTheme
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.runtime.remember
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.clip import androidx.compose.ui.draw.clip
@@ -36,17 +37,57 @@ fun MessageList(
) { ) {
val listState = rememberLazyListState() val listState = rememberLazyListState()
// 自动滚动到底部 - 改进:流式输出时实时滚动 // 计算实际的列表项数量(考虑分割消息和附加项)
LaunchedEffect(messages.size, isStreaming, streamingText) { val estimatedItemCount = remember(messages, isStreaming, streamingText, isTyping) {
val targetIndex = messages.size + if (isStreaming) 1 else 0 var count = 0
if (targetIndex > 0) { var lastTimestamp: Long? = null
// 流式输出时使用平滑滚动,其他情况使用动画滚动
if (isStreaming && streamingText.isNotEmpty()) { messages.forEachIndexed { index, message ->
// 流式输出时,每次文本更新都滚动到底部 // 时间分隔线
kotlinx.coroutines.delay(50) // 短暂延迟确保内容已渲染 if (index > 0 && lastTimestamp != null && (message.timestamp - lastTimestamp!!) > 300000) {
listState.animateScrollToItem(targetIndex) count++
}
// 消息气泡(考虑分割)
if (message.senderType == "assistant") {
val parts = message.content.split("[SPLIT]").filter { it.trim().isNotEmpty() }
count += if (parts.size > 1) parts.size else 1
} else { } else {
listState.animateScrollToItem(targetIndex) count++
}
lastTimestamp = message.timestamp
}
// 流式消息
if (isStreaming) {
val streamingParts = streamingText.split("[SPLIT]").filter { it.trim().isNotEmpty() }
count += if (streamingParts.size > 1) streamingParts.size else 1
}
// 输入指示器
if (isTyping || (isStreaming && streamingText.isEmpty())) {
count++
}
count
}
// 自动滚动到底部 - 当消息变化或流式内容更新时滚动
LaunchedEffect(messages.size, messages.lastOrNull()?.id, isStreaming, streamingText, isTyping) {
// 短暂延迟确保内容已渲染
delay(100)
// 滚动到最后一项
if (estimatedItemCount > 0) {
try {
listState.animateScrollToItem(estimatedItemCount - 1)
} catch (e: Exception) {
// 如果索引超出范围,尝试滚动到实际的最后一项
val actualCount = listState.layoutInfo.totalItemsCount
if (actualCount > 0) {
listState.animateScrollToItem(actualCount - 1)
}
} }
} }
} }

View File

@@ -38,7 +38,6 @@ fun CreateMemoryScreen(
) { ) {
val isRecording by viewModel.isRecording.collectAsState() val isRecording by viewModel.isRecording.collectAsState()
val transcript by viewModel.transcript.collectAsState() val transcript by viewModel.transcript.collectAsState()
val agentResponse by viewModel.agentResponse.collectAsState()
val connectionStatus by viewModel.connectionStatus.collectAsState() val connectionStatus by viewModel.connectionStatus.collectAsState()
val userMessages by viewModel.userMessages.collectAsState() val userMessages by viewModel.userMessages.collectAsState()
val historyMessages by viewModel.historyMessages.collectAsState() val historyMessages by viewModel.historyMessages.collectAsState()
@@ -69,7 +68,8 @@ fun CreateMemoryScreen(
val keyboardController = LocalSoftwareKeyboardController.current val keyboardController = LocalSoftwareKeyboardController.current
// 构建消息列表(包含历史消息和当前消息) // 构建消息列表(包含历史消息和当前消息)
val messages = remember(historyMessages, userMessages, agentResponse) { // 注意AI回复已经直接添加到 historyMessages 中,不需要额外处理 agentResponse
val messages = remember(historyMessages, userMessages) {
buildList { buildList {
// 先添加历史消息 // 先添加历史消息
addAll(historyMessages) addAll(historyMessages)
@@ -88,18 +88,6 @@ fun CreateMemoryScreen(
)) ))
} }
} }
// 添加AI回复如果不在历史消息中
if (agentResponse.isNotEmpty() && !historyMessages.any { it.content == agentResponse && it.senderType == "assistant" }) {
add(MessageDto(
id = "ai_response_${historyMessages.size + userMessages.size}",
conversationId = conversationId,
content = agentResponse,
senderType = "assistant",
timestamp = System.currentTimeMillis(),
messageType = "text"
))
}
}.sortedBy { it.timestamp } // 按时间排序 }.sortedBy { it.timestamp } // 按时间排序
} }

View File

@@ -294,7 +294,7 @@ class CreateMemoryViewModel(
transcript.value = message.getString("text") ?: "" transcript.value = message.getString("text") ?: ""
} }
MessageType.agent_response -> { MessageType.agent_response -> {
// 处理Agent回复可能有多条消息 // 处理Agent回复可能有多条消息,每条作为单独气泡显示
val text = message.getString("text") ?: "" val text = message.getString("text") ?: ""
val index = message.getInt("index") ?: 0 val index = message.getInt("index") ?: 0
val total = message.getInt("total") ?: 1 val total = message.getInt("total") ?: 1
@@ -302,25 +302,30 @@ class CreateMemoryViewModel(
// 收到第一条回复时,隐藏打字指示器 // 收到第一条回复时,隐藏打字指示器
if (index == 0) { if (index == 0) {
isTyping.value = false isTyping.value = false
}
// 每条消息立即作为单独的气泡添加到历史消息
conversationId.value?.let { id ->
val aiMessage = MessageDto(
id = "ai_${System.currentTimeMillis()}_$index",
conversationId = id,
content = text,
senderType = "assistant",
timestamp = System.currentTimeMillis(),
messageType = "text"
)
historyMessages.value = historyMessages.value + aiMessage
}
// 更新 agentResponse用于显示最新回复
if (index == 0) {
agentResponse.value = text agentResponse.value = text
} else { } else {
// 追加后续消息
agentResponse.value += "\n\n$text" agentResponse.value += "\n\n$text"
} }
// 如果是最后一条消息,结束流式状态并添加到历史消息 // 如果是最后一条消息,结束流式状态
if (index >= total - 1) { if (index >= total - 1) {
conversationId.value?.let { id ->
val aiMessage = MessageDto(
id = "ai_${System.currentTimeMillis()}",
conversationId = id,
content = agentResponse.value,
senderType = "assistant",
timestamp = System.currentTimeMillis(),
messageType = "text"
)
historyMessages.value = historyMessages.value + aiMessage
}
isStreaming.value = false isStreaming.value = false
streamingText.value = "" streamingText.value = ""
isTyping.value = false isTyping.value = false