feat: lazy loading for Genie conversations#163
Conversation
Load only the most recent page of messages when opening a conversation, and fetch older messages on demand when the user scrolls to the top. - Add `history_info` SSE event with pagination token - Add REST endpoint GET /:alias/conversations/:conversationId/messages - Frontend auto-triggers loading via IntersectionObserver with scroll position preservation - Consolidate to single `listConversationMessages` method in connector Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
Load only the most recent page of messages when opening a conversation, and fetch older messages on demand when the user scrolls to the top. - Add `history_info` SSE event with pagination token to existing getConversation endpoint (no new endpoints) - Frontend auto-triggers loading via IntersectionObserver with scroll position preservation using the same SSE endpoint with pageToken - Consolidate to single `listConversationMessages` method in connector Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
- Extract shared fetchConversationPage for loadHistory and loadOlderMessages - Rename processEvent to processStreamEvent (only used for sendMessage) - Pass stable React setters directly instead of recreating paginationCallbacks - Add abort signal to loadOlderMessages to prevent leaks on unmount Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
Reference the constant directly instead of hardcoding the value, preventing test failures when the default is changed. Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
Extract useScrollManagement and useLoadOlderOnScroll into custom hooks, inline StreamingIndicator, leaving the component as clean JSX. Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
- useLoadOlderOnScroll owns its sentinel ref internally - Simplify message rendering with filter+map Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
- Merge ref-sync effects into one - Extract fetchPage helper shared by loadHistory and loadOlderMessages - Simplify query_result handler with .map() instead of imperative loop Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
| hasOlderMessages, | ||
| loadOlderMessages, |
There was a problem hiding this comment.
maybe we could rename hasOlderMessages to hasPreviousPage and loadOlderMessages to fetchPreviousPage? To use a naming known from e.g. tanstack query: https://tanstack.com/query/latest/docs/framework/react/reference/useInfiniteQuery?from=reactQueryV3
What. do you think?
BTW maybe isFetchingPreviousPage would be also helpful?
There was a problem hiding this comment.
Makes sense, I made the renames and added isFetchingPreviousPage
| } | ||
|
|
||
| const includeQueryResults = req.query.includeQueryResults !== "false"; | ||
| const pageToken = req.query.pageToken as string | undefined; |
There was a problem hiding this comment.
Express query params can be [key: string]: undefined | string | ParsedQs | (string | ParsedQs)[];
There was a problem hiding this comment.
I added a check to ensure they're of type string
| convId: string, | ||
| options?: { pageToken?: string; errorMessage?: string }, | ||
| ) => { | ||
| abortControllerRef.current?.abort(); |
There was a problem hiding this comment.
pagination should have separate abortControllerRef - if a user scrolls to the top during an active stream (status "streaming"), loadOlderMessages -> fetchPage will abort the in-progress streaming connection
There was a problem hiding this comment.
separated into 2 abort controllers so that the scroll won't stop sending a message
| }); | ||
| }); | ||
|
|
||
| describe("getConversation with pageToken", () => { |
There was a problem hiding this comment.
I think we're missing a test case for pagination request failure - can you add it pls?
- Rename hasOlderMessages/loadOlderMessages to hasPreviousPage/ fetchPreviousPage following TanStack Query conventions, add isFetchingPreviousPage - Use separate AbortController for pagination to avoid aborting in-progress streams - Fix Express query param type safety (typeof check vs as cast) - Add test for paginated request failure Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
|
@calvarjorge ran
|
Consistent with getConversation handler — avoids unsafe as string cast for Express query params. Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
- useScrollManagement no longer fires on status-only changes, preventing scroll-to-bottom yank when "loading-older" spinner appears - sendMessage aborts in-flight pagination to prevent status stomping - fetchPreviousPage guards setStatus to not overwrite concurrent streams - IntersectionObserver skips initial callback after re-subscription to prevent rapid-fire pagination loop Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
- Server-sent error events during pagination now set error status (hadError flag prevents status flashing to idle) - Replace unsafe MutableRefObject cast with plain object type in fetchPage - Restore reverse-scan-with-break for query_result in processStreamEvent - Add ResizeObserver to keep scroll height ref fresh for async content Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
- Set error status directly in onError callback instead of threading hadError flag through return value - Remove freeze/unfreeze scroll momentum plumbing (not effective enough) - Clean up unused code Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
- loadHistory aborts in-flight pagination to prevent stale messages from old conversation being prepended - Replace initialFire boolean with requestAnimationFrame arming so short conversations where sentinel is genuinely visible still trigger loading automatically Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
Add MIN_PREVIOUS_PAGE_LOAD_MS to ensure scroll inertia fully settles before fetchPreviousPage prepends older messages to the chat. Signed-off-by: Jorge Calvar <jorge.calvar@databricks.com>
Summary
initialPageSize, default 20)history_infoSSE event that communicates pagination state (nextPageToken) to the frontend