Fix AI chat scroll behavior (#15686)
This commit is contained in:
@@ -16,6 +16,7 @@ import { AgentChatContextPreview } from '@/ai/components/internal/AgentChatConte
|
||||
import { SendMessageButton } from '@/ai/components/internal/SendMessageButton';
|
||||
import { SendMessageWithRecordsContextButton } from '@/ai/components/internal/SendMessageWithRecordsContextButton';
|
||||
import { AI_CHAT_INPUT_ID } from '@/ai/constants/AiChatInputId';
|
||||
import { AI_CHAT_SCROLL_WRAPPER_ID } from '@/ai/constants/AiChatScrollWrapperId';
|
||||
import { useAIChatFileUpload } from '@/ai/hooks/useAIChatFileUpload';
|
||||
import { useAgentChatContextOrThrow } from '@/ai/hooks/useAgentChatContextOrThrow';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
@@ -62,15 +63,8 @@ const StyledButtonsContainer = styled.div`
|
||||
export const AIChatTab = () => {
|
||||
const [isDraggingFile, setIsDraggingFile] = useState(false);
|
||||
|
||||
const {
|
||||
isLoading,
|
||||
input,
|
||||
handleInputChange,
|
||||
scrollWrapperId,
|
||||
messages,
|
||||
isStreaming,
|
||||
error,
|
||||
} = useAgentChatContextOrThrow();
|
||||
const { isLoading, input, handleInputChange, messages, isStreaming, error } =
|
||||
useAgentChatContextOrThrow();
|
||||
|
||||
const contextStoreCurrentObjectMetadataItemId = useRecoilComponentValue(
|
||||
contextStoreCurrentObjectMetadataItemIdComponentState,
|
||||
@@ -94,7 +88,9 @@ export const AIChatTab = () => {
|
||||
{!isDraggingFile && (
|
||||
<>
|
||||
{messages.length !== 0 && (
|
||||
<StyledScrollWrapper componentInstanceId={scrollWrapperId}>
|
||||
<StyledScrollWrapper
|
||||
componentInstanceId={AI_CHAT_SCROLL_WRAPPER_ID}
|
||||
>
|
||||
{messages.map((message, index) => {
|
||||
const isLastMessage = index === messages.length - 1;
|
||||
const isLastMessageStreaming = isStreaming && isLastMessage;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useAgentChatScrollToBottom } from '@/ai/hooks/useAgentChatScrollToBottom';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { type ExtendedUIMessage } from 'twenty-shared/ai';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
export const AgentChatMessagesEffect = ({
|
||||
messages,
|
||||
}: {
|
||||
messages: ExtendedUIMessage[];
|
||||
}) => {
|
||||
const { scrollToBottom } = useAgentChatScrollToBottom();
|
||||
const [, setPreviousMessages] = useState<ExtendedUIMessage[] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setPreviousMessages((previousMessages) => {
|
||||
if (
|
||||
previousMessages !== null &&
|
||||
isDeeplyEqual(previousMessages, messages)
|
||||
) {
|
||||
return previousMessages;
|
||||
}
|
||||
|
||||
// We intentionally force this effect because the chat transport streams messages incrementally
|
||||
// and the only reliable way to react to those chunks is through useEffect updates.
|
||||
scrollToBottom();
|
||||
return messages;
|
||||
});
|
||||
}, [messages, scrollToBottom]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AgentChatMessagesEffect } from '@/ai/components/AgentChatMessagesEffect';
|
||||
import { AgentChatContext } from '@/ai/contexts/AgentChatContext';
|
||||
import { useAgentChat } from '@/ai/hooks/useAgentChat';
|
||||
import { useAgentChatData } from '@/ai/hooks/useAgentChatData';
|
||||
@@ -21,6 +22,7 @@ const AgentChatProviderContent = ({
|
||||
isLoading: combinedIsLoading,
|
||||
}}
|
||||
>
|
||||
<AgentChatMessagesEffect messages={chatState.messages} />
|
||||
{children}
|
||||
</AgentChatContext.Provider>
|
||||
);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const AI_CHAT_SCROLL_WRAPPER_ID = 'ai-chat-scroll-wrapper';
|
||||
@@ -13,7 +13,6 @@ export type AgentChatContextValue = {
|
||||
|
||||
handleSendMessage: (records?: ObjectRecord[]) => Promise<void>;
|
||||
|
||||
scrollWrapperId: string;
|
||||
handleRetry: () => void;
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import { tokenPairState } from '@/auth/states/tokenPairState';
|
||||
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
|
||||
import { useGetObjectMetadataItemById } from '@/object-metadata/hooks/useGetObjectMetadataItemById';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
|
||||
import { useChat } from '@ai-sdk/react';
|
||||
import { DefaultChatTransport } from 'ai';
|
||||
import { type ExtendedUIMessage } from 'twenty-shared/ai';
|
||||
@@ -46,11 +45,6 @@ export const useAgentChat = (uiMessages: ExtendedUIMessage[]) => {
|
||||
const [agentChatInput, setAgentChatInput] =
|
||||
useRecoilState(agentChatInputState);
|
||||
|
||||
const scrollWrapperId = `scroll-wrapper-ai-chat-${currentAIChatThread}`;
|
||||
|
||||
const { scrollWrapperHTMLElement } =
|
||||
useScrollWrapperHTMLElement(scrollWrapperId);
|
||||
|
||||
const retryFetchWithRenewedToken = async (
|
||||
input: RequestInfo | URL,
|
||||
init?: RequestInit,
|
||||
@@ -120,13 +114,6 @@ export const useAgentChat = (uiMessages: ExtendedUIMessage[]) => {
|
||||
|
||||
const isStreaming = status === 'streaming';
|
||||
|
||||
const scrollToBottom = () => {
|
||||
scrollWrapperHTMLElement?.scroll({
|
||||
top: scrollWrapperHTMLElement.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
|
||||
const isLoading =
|
||||
!currentAIChatThread || isStreaming || agentChatSelectedFiles.length > 0;
|
||||
|
||||
@@ -165,9 +152,7 @@ export const useAgentChat = (uiMessages: ExtendedUIMessage[]) => {
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
setAgentChatUploadedFiles([]);
|
||||
setTimeout(scrollToBottom, 100);
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -176,7 +161,6 @@ export const useAgentChat = (uiMessages: ExtendedUIMessage[]) => {
|
||||
input: agentChatInput,
|
||||
handleSendMessage,
|
||||
isLoading,
|
||||
scrollWrapperId,
|
||||
isStreaming,
|
||||
error,
|
||||
handleRetry: regenerate,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useAgentChatScrollToBottom } from '@/ai/hooks/useAgentChatScrollToBottom';
|
||||
import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
|
||||
import { mapDBMessagesToUIMessages } from '@/ai/utils/mapDBMessagesToUIMessages';
|
||||
import { useRecoilState } from 'recoil';
|
||||
@@ -12,6 +13,8 @@ export const useAgentChatData = () => {
|
||||
currentAIChatThreadState,
|
||||
);
|
||||
|
||||
const { scrollToBottom } = useAgentChatScrollToBottom();
|
||||
|
||||
const { loading: threadsLoading } = useGetChatThreadsQuery({
|
||||
skip: isDefined(currentAIChatThread),
|
||||
onCompleted: (data) => {
|
||||
@@ -24,6 +27,7 @@ export const useAgentChatData = () => {
|
||||
const { loading: messagesLoading, data } = useGetChatMessagesQuery({
|
||||
variables: { threadId: currentAIChatThread! },
|
||||
skip: !isDefined(currentAIChatThread),
|
||||
onCompleted: scrollToBottom,
|
||||
});
|
||||
|
||||
const uiMessages = mapDBMessagesToUIMessages(data?.chatMessages || []);
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { AI_CHAT_SCROLL_WRAPPER_ID } from '@/ai/constants/AiChatScrollWrapperId';
|
||||
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export const useAgentChatScrollToBottom = () => {
|
||||
const { getScrollWrapperElement } = useScrollWrapperHTMLElement(
|
||||
AI_CHAT_SCROLL_WRAPPER_ID,
|
||||
);
|
||||
|
||||
const scrollToBottom = () => {
|
||||
const { scrollWrapperElement } = getScrollWrapperElement();
|
||||
if (!isDefined(scrollWrapperElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollWrapperElement.scrollTo({
|
||||
top: scrollWrapperElement.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
|
||||
return { scrollToBottom };
|
||||
};
|
||||
Reference in New Issue
Block a user