diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/components/chat/MessageList.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/components/chat/MessageList.kt index fb997b3..081db38 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/ui/components/chat/MessageList.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/components/chat/MessageList.kt @@ -11,6 +11,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -36,17 +37,57 @@ fun MessageList( ) { val listState = rememberLazyListState() - // 自动滚动到底部 - 改进:流式输出时实时滚动 - LaunchedEffect(messages.size, isStreaming, streamingText) { - val targetIndex = messages.size + if (isStreaming) 1 else 0 - if (targetIndex > 0) { - // 流式输出时使用平滑滚动,其他情况使用动画滚动 - if (isStreaming && streamingText.isNotEmpty()) { - // 流式输出时,每次文本更新都滚动到底部 - kotlinx.coroutines.delay(50) // 短暂延迟确保内容已渲染 - listState.animateScrollToItem(targetIndex) + // 计算实际的列表项数量(考虑分割消息和附加项) + val estimatedItemCount = remember(messages, isStreaming, streamingText, isTyping) { + var count = 0 + var lastTimestamp: Long? = null + + messages.forEachIndexed { index, message -> + // 时间分隔线 + if (index > 0 && lastTimestamp != null && (message.timestamp - lastTimestamp!!) > 300000) { + 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 { - 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) + } } } } diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/CreateMemoryScreen.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/CreateMemoryScreen.kt index 2f01eb5..b11d032 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/CreateMemoryScreen.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/screens/CreateMemoryScreen.kt @@ -38,7 +38,6 @@ fun CreateMemoryScreen( ) { val isRecording by viewModel.isRecording.collectAsState() val transcript by viewModel.transcript.collectAsState() - val agentResponse by viewModel.agentResponse.collectAsState() val connectionStatus by viewModel.connectionStatus.collectAsState() val userMessages by viewModel.userMessages.collectAsState() val historyMessages by viewModel.historyMessages.collectAsState() @@ -69,7 +68,8 @@ fun CreateMemoryScreen( val keyboardController = LocalSoftwareKeyboardController.current // 构建消息列表(包含历史消息和当前消息) - val messages = remember(historyMessages, userMessages, agentResponse) { + // 注意:AI回复已经直接添加到 historyMessages 中,不需要额外处理 agentResponse + val messages = remember(historyMessages, userMessages) { buildList { // 先添加历史消息 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 } // 按时间排序 } diff --git a/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModel.kt b/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModel.kt index 181871d..b5d1670 100644 --- a/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModel.kt +++ b/app-android/app/src/main/java/com/huaga/life_echo/ui/viewmodel/CreateMemoryViewModel.kt @@ -294,7 +294,7 @@ class CreateMemoryViewModel( transcript.value = message.getString("text") ?: "" } MessageType.agent_response -> { - // 处理Agent回复(可能有多条消息) + // 处理Agent回复(可能有多条消息,每条作为单独气泡显示) val text = message.getString("text") ?: "" val index = message.getInt("index") ?: 0 val total = message.getInt("total") ?: 1 @@ -302,25 +302,30 @@ class CreateMemoryViewModel( // 收到第一条回复时,隐藏打字指示器 if (index == 0) { 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 } else { - // 追加后续消息 agentResponse.value += "\n\n$text" } - // 如果是最后一条消息,结束流式状态并添加到历史消息 + // 如果是最后一条消息,结束流式状态 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 streamingText.value = "" isTyping.value = false