fix(dashboard): wire scroll-to-top pagination instead of loading all messages
- Remove fetchAllMessagesInChat/fetchAllMessages helpers
- Keep limit:50 for initial load in useChat and useQuickChat
- Add IntersectionObserver sentinel at top of ChatView message list to
trigger loadMoreMessages() when user scrolls to the top
- Keep stale-session guards (activeSessionRef checks) from original PR
- Tests: revert assertions back to { limit: 50 }
This commit is contained in:
@@ -946,6 +946,8 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
|
|||||||
stopStreaming,
|
stopStreaming,
|
||||||
pendingMessage,
|
pendingMessage,
|
||||||
clearPendingMessage,
|
clearPendingMessage,
|
||||||
|
loadMoreMessages,
|
||||||
|
hasMoreMessages,
|
||||||
searchQuery,
|
searchQuery,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
filteredSessions,
|
filteredSessions,
|
||||||
@@ -1031,6 +1033,7 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
|
|||||||
}, [fileMention.mentionActive]);
|
}, [fileMention.mentionActive]);
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
|
const loadMoreSentinelRef = useRef<HTMLDivElement>(null);
|
||||||
const mobileSessionMenuRef = useRef<HTMLDivElement>(null);
|
const mobileSessionMenuRef = useRef<HTMLDivElement>(null);
|
||||||
const roomSwitcherRef = useRef<HTMLDivElement>(null);
|
const roomSwitcherRef = useRef<HTMLDivElement>(null);
|
||||||
const isUserScrollingRef = useRef(false);
|
const isUserScrollingRef = useRef(false);
|
||||||
@@ -1233,6 +1236,22 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const sentinel = loadMoreSentinelRef.current;
|
||||||
|
if (!sentinel || !hasMoreMessages) return;
|
||||||
|
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
(entries) => {
|
||||||
|
if (entries[0].isIntersecting) {
|
||||||
|
void loadMoreMessages();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.1 },
|
||||||
|
);
|
||||||
|
observer.observe(sentinel);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [hasMoreMessages, loadMoreMessages]);
|
||||||
|
|
||||||
const getActiveThreadId = useCallback(() => {
|
const getActiveThreadId = useCallback(() => {
|
||||||
return roomThreadActive ? (rooms.activeRoom?.id ?? null) : (activeSession?.id ?? null);
|
return roomThreadActive ? (rooms.activeRoom?.id ?? null) : (activeSession?.id ?? null);
|
||||||
}, [roomThreadActive, rooms.activeRoom?.id, activeSession?.id]);
|
}, [roomThreadActive, rooms.activeRoom?.id, activeSession?.id]);
|
||||||
@@ -3123,6 +3142,11 @@ export function ChatView({ projectId, addToast, experimentalFeatures }: ChatView
|
|||||||
|
|
||||||
{/* Messages */}
|
{/* Messages */}
|
||||||
<div className="chat-messages" ref={messagesContainerRef} onScroll={updateScrollState}>
|
<div className="chat-messages" ref={messagesContainerRef} onScroll={updateScrollState}>
|
||||||
|
<div ref={loadMoreSentinelRef} className="chat-load-more-sentinel">
|
||||||
|
{hasMoreMessages && messagesLoading && (
|
||||||
|
<div className="chat-loading-older">Loading older messages…</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
{isStreaming ? (
|
{isStreaming ? (
|
||||||
<>
|
<>
|
||||||
{messages.map((message) => (
|
{messages.map((message) => (
|
||||||
|
|||||||
@@ -239,26 +239,6 @@ function mapChatMessageToInfo(message: ChatMessage): ChatMessageInfo {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch all messages for a session, paginating through the API's 200-message cap.
|
|
||||||
* Used for the initial full load so sessions with >50 messages are never truncated.
|
|
||||||
*/
|
|
||||||
async function fetchAllMessagesInChat(
|
|
||||||
sessionId: string,
|
|
||||||
projectId: string | undefined,
|
|
||||||
): Promise<ChatMessageInfo[]> {
|
|
||||||
const PAGE = 200;
|
|
||||||
const all: ChatMessageInfo[] = [];
|
|
||||||
let offset = 0;
|
|
||||||
for (;;) {
|
|
||||||
const data = await fetchChatMessages(sessionId, { limit: PAGE, offset }, projectId);
|
|
||||||
all.push(...data.messages.map(mapChatMessageToInfo));
|
|
||||||
if (data.messages.length < PAGE) break;
|
|
||||||
offset += PAGE;
|
|
||||||
}
|
|
||||||
return all;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useChat(
|
export function useChat(
|
||||||
projectId?: string,
|
projectId?: string,
|
||||||
addToast?: (msg: string, type?: "success" | "error" | "warning") => void,
|
addToast?: (msg: string, type?: "success" | "error" | "warning") => void,
|
||||||
@@ -471,24 +451,18 @@ export function useChat(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const data = await fetchChatMessages(sessionId, { limit: 50, ...opts }, projectId);
|
||||||
|
const mappedMessages = data.messages.map(mapChatMessageToInfo);
|
||||||
if (isPaginationRequest) {
|
if (isPaginationRequest) {
|
||||||
// Prepend older messages (forward pagination, used when hasMoreMessages)
|
if (activeSessionRef.current?.id === sessionId) {
|
||||||
const requestSessionId = sessionId;
|
|
||||||
const data = await fetchChatMessages(sessionId, { limit: 50, ...opts }, projectId);
|
|
||||||
if (activeSessionRef.current?.id === requestSessionId) {
|
|
||||||
const mappedMessages = data.messages.map(mapChatMessageToInfo);
|
|
||||||
setMessages((prev) => [...mappedMessages, ...prev]);
|
setMessages((prev) => [...mappedMessages, ...prev]);
|
||||||
setHasMoreMessages(data.messages.length >= 50);
|
setHasMoreMessages(data.messages.length >= 50);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Initial full load — fetch all messages, never truncate at 50
|
|
||||||
const allMessages = await fetchAllMessagesInChat(sessionId, projectId);
|
|
||||||
if (activeSessionRef.current?.id === sessionId) {
|
if (activeSessionRef.current?.id === sessionId) {
|
||||||
setMessages(allMessages);
|
setMessages(mappedMessages);
|
||||||
if (cacheKey) {
|
setHasMoreMessages(data.messages.length >= 50);
|
||||||
writeCache(cacheKey, allMessages, { maxBytes: 500_000 });
|
if (cacheKey) writeCache(cacheKey, mappedMessages, { maxBytes: 500_000 });
|
||||||
}
|
|
||||||
setHasMoreMessages(false); // all messages loaded — inside guard
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -183,22 +183,6 @@ function mapChatMessageToInfo(message: ChatMessage): ChatMessageInfo {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch all messages for a session, paginating through the API's 200-message cap.
|
|
||||||
* Replaces the former hardcoded `limit: 50` that silently truncated long sessions.
|
|
||||||
*/
|
|
||||||
async function fetchAllMessages(sessionId: string, projectId?: string): Promise<ChatMessageInfo[]> {
|
|
||||||
const PAGE = 200; // API hard cap (Math.min(limit, 200))
|
|
||||||
const all: ChatMessageInfo[] = [];
|
|
||||||
let offset = 0;
|
|
||||||
for (;;) {
|
|
||||||
const data = await fetchChatMessages(sessionId, { limit: PAGE, offset }, projectId);
|
|
||||||
all.push(...data.messages.map(mapChatMessageToInfo));
|
|
||||||
if (data.messages.length < PAGE) break;
|
|
||||||
offset += PAGE;
|
|
||||||
}
|
|
||||||
return all;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Hook for the QuickChatFAB component.
|
* Hook for the QuickChatFAB component.
|
||||||
* Provides chat session management and SSE streaming for real-time AI responses.
|
* Provides chat session management and SSE streaming for real-time AI responses.
|
||||||
@@ -346,8 +330,8 @@ export function useQuickChat(
|
|||||||
isStreamingRef.current = false;
|
isStreamingRef.current = false;
|
||||||
streamRef.current = null;
|
streamRef.current = null;
|
||||||
lastAttachedGenerationRef.current = null;
|
lastAttachedGenerationRef.current = null;
|
||||||
void fetchAllMessages(sessionId, projectId).then((msgs) => {
|
void fetchChatMessages(sessionId, { limit: 50 }, projectId).then((data) => {
|
||||||
if (activeSessionRef.current?.id === sessionId) setMessages(msgs);
|
if (activeSessionRef.current?.id === sessionId) setMessages(data.messages.map(mapChatMessageToInfo));
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
flushPendingMessage();
|
flushPendingMessage();
|
||||||
},
|
},
|
||||||
@@ -363,8 +347,8 @@ export function useQuickChat(
|
|||||||
if (!options?.silent) {
|
if (!options?.silent) {
|
||||||
addToast?.(errorMessage, "error");
|
addToast?.(errorMessage, "error");
|
||||||
}
|
}
|
||||||
void fetchAllMessages(sessionId, projectId).then((msgs) => {
|
void fetchChatMessages(sessionId, { limit: 50 }, projectId).then((data) => {
|
||||||
if (activeSessionRef.current?.id === sessionId) setMessages(msgs);
|
if (activeSessionRef.current?.id === sessionId) setMessages(data.messages.map(mapChatMessageToInfo));
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
flushPendingMessage();
|
flushPendingMessage();
|
||||||
},
|
},
|
||||||
@@ -444,8 +428,8 @@ export function useQuickChat(
|
|||||||
setMessagesLoading(true);
|
setMessagesLoading(true);
|
||||||
try {
|
try {
|
||||||
const sessionId = activeSession.id;
|
const sessionId = activeSession.id;
|
||||||
const msgs = await fetchAllMessages(sessionId, projectId);
|
const data = await fetchChatMessages(sessionId, { limit: 50 }, projectId);
|
||||||
if (activeSessionRef.current?.id === sessionId) setMessages(msgs);
|
if (activeSessionRef.current?.id === sessionId) setMessages(data.messages.map(mapChatMessageToInfo));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[useQuickChat] Failed to load messages:", err);
|
console.error("[useQuickChat] Failed to load messages:", err);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -489,8 +473,8 @@ export function useQuickChat(
|
|||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
// Reload messages to pick up the completed assistant message
|
// Reload messages to pick up the completed assistant message
|
||||||
const sessionId = activeSession.id;
|
const sessionId = activeSession.id;
|
||||||
const msgs = await fetchAllMessages(sessionId, projectId);
|
const data = await fetchChatMessages(sessionId, { limit: 50 }, projectId);
|
||||||
if (activeSessionRef.current?.id === sessionId) setMessages(msgs);
|
if (activeSessionRef.current?.id === sessionId) setMessages(data.messages.map(mapChatMessageToInfo));
|
||||||
setStreamingText("");
|
setStreamingText("");
|
||||||
setStreamingThinking("");
|
setStreamingThinking("");
|
||||||
setStreamingToolCalls([]);
|
setStreamingToolCalls([]);
|
||||||
@@ -512,8 +496,8 @@ export function useQuickChat(
|
|||||||
setMessagesLoading(true);
|
setMessagesLoading(true);
|
||||||
try {
|
try {
|
||||||
const sessionId = activeSession.id;
|
const sessionId = activeSession.id;
|
||||||
const msgs = await fetchAllMessages(sessionId, projectId);
|
const data = await fetchChatMessages(sessionId, { limit: 50 }, projectId);
|
||||||
if (activeSessionRef.current?.id === sessionId) setMessages(msgs);
|
if (activeSessionRef.current?.id === sessionId) setMessages(data.messages.map(mapChatMessageToInfo));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("[useQuickChat] Failed to reload messages:", err);
|
console.error("[useQuickChat] Failed to reload messages:", err);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user