diff --git a/android/app/src/main/java/com/tiangong/aiagent/ui/chat/ChatScreen.kt b/android/app/src/main/java/com/tiangong/aiagent/ui/chat/ChatScreen.kt index f19b163..1c5cda1 100644 --- a/android/app/src/main/java/com/tiangong/aiagent/ui/chat/ChatScreen.kt +++ b/android/app/src/main/java/com/tiangong/aiagent/ui/chat/ChatScreen.kt @@ -85,7 +85,7 @@ fun ChatScreen( } } - // Scroll-to-top detection for loading more history + // Scroll-to-top detection for loading more history (throttled to prevent double-fire) val isAtTop by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 && @@ -93,8 +93,14 @@ fun ChatScreen( } } + var lastLoadMoreTime by remember { mutableStateOf(0L) } + LaunchedEffect(isAtTop) { - if (isAtTop && uiState.hasMoreMessages && !uiState.isLoadingMore && initialScrollDone) { + val now = System.currentTimeMillis() + if (isAtTop && uiState.hasMoreMessages && !uiState.isLoadingMore && initialScrollDone + && (now - lastLoadMoreTime) > 800 + ) { + lastLoadMoreTime = now viewModel.loadMoreHistory() } } diff --git a/android/app/src/main/java/com/tiangong/aiagent/ui/chat/ChatViewModel.kt b/android/app/src/main/java/com/tiangong/aiagent/ui/chat/ChatViewModel.kt index 3f4eeab..768abad 100644 --- a/android/app/src/main/java/com/tiangong/aiagent/ui/chat/ChatViewModel.kt +++ b/android/app/src/main/java/com/tiangong/aiagent/ui/chat/ChatViewModel.kt @@ -124,6 +124,7 @@ class ChatViewModel @Inject constructor( private var sseJob: Job? = null private var historyFlowJob: Job? = null + private var isLoadingHistory = false companion object { private const val KEY_SESSION_STATE = "chat_session_state" @@ -181,9 +182,9 @@ class ChatViewModel @Inject constructor( val currentAgentId = _uiState.value.currentAgent?.id historyFlowJob = viewModelScope.launch { chatRepository.getMessages(sessionId).collect { messages -> - if (messages.isNotEmpty() && _uiState.value.currentAgent?.id == currentAgentId) { + if (messages.isNotEmpty() && _uiState.value.currentAgent?.id == currentAgentId && !isLoadingHistory) { _uiState.value = _uiState.value.copy( - messages = messages.map { it.toUiMessage() }.distinctBy { it.id }.distinctBy { it.id }, + messages = messages.map { it.toUiMessage() }.distinctBy { it.id }, sessionId = sessionId ) } @@ -243,7 +244,7 @@ class ChatViewModel @Inject constructor( val latestConversation = conversations.firstOrNull() if (latestConversation != null) { chatRepository.getMessages(latestConversation.sessionId).collect { messages -> - if (messages.isNotEmpty() && _uiState.value.currentAgent?.id == agentId) { + if (messages.isNotEmpty() && _uiState.value.currentAgent?.id == agentId && !isLoadingHistory) { _uiState.value = _uiState.value.copy( messages = messages.map { it.toUiMessage() }.distinctBy { it.id }, sessionId = latestConversation.sessionId @@ -312,7 +313,7 @@ class ChatViewModel @Inject constructor( historyFlowJob?.cancel() historyFlowJob = viewModelScope.launch { chatRepository.getMessages(sessionId).collect { messages -> - if (messages.isNotEmpty()) { + if (messages.isNotEmpty() && !isLoadingHistory) { _uiState.value = _uiState.value.copy( messages = messages.map { it.toUiMessage() }.distinctBy { it.id } ) @@ -350,10 +351,15 @@ class ChatViewModel @Inject constructor( val oldestMsg = state.messages.firstOrNull() ?: return viewModelScope.launch { + isLoadingHistory = true _uiState.value = _uiState.value.copy(isLoadingMore = true) - // Capture current scroll anchor (first visible item id + offset) - val anchorId = oldestMsg.id + // Use a server-resolvable ID as cursor: local IDs (user_/asst_/tool_/system_ prefix) + // are not in the server DB, so the server would ignore the cursor and return ALL messages. + // Find the oldest message whose ID looks like a server UUID. + val serverIdPattern = Regex("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", RegexOption.IGNORE_CASE) + val anchorId = state.messages.firstOrNull { serverIdPattern.matches(it.id) }?.id + ?: oldestMsg.id val result = chatRepository.fetchOlderMessages( agentId = agentId, @@ -377,6 +383,7 @@ class ChatViewModel @Inject constructor( hasMoreMessages = hasMore, isLoadingMore = false ) + isLoadingHistory = false } else { // API failed — try Room fallback val oldestTimestamp = oldestMsg.createdAt @@ -398,11 +405,13 @@ class ChatViewModel @Inject constructor( hasMoreMessages = roomMsgs.size >= 50, isLoadingMore = false ) + isLoadingHistory = false } else { _uiState.value = _uiState.value.copy( hasMoreMessages = false, isLoadingMore = false ) + isLoadingHistory = false } } }