mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
This PR: - Breaks useAgentChatData into focused effect components (streaming, fetch, init, auto-scroll, diff sync) - Splits message list into non-last (stable) + last (streaming/error) to prevent full re-renders on each stream chunk - Adds scroll-to-bottom button and MutationObserver-based auto-scroll on thread switch - Lifts loading state from context to atoms - Adds areEqual to selector factories We could improve further but this sets up a robust architecture for further refactoring. ## Messages flow The flow of messages loading and streaming is now more solid. Everything goes out from `AgentChatAiSdkStreamEffect`, whether loaded from the DB or streaming directly, and every consumers is using only one atom `agentChatMessagesComponentFamilyState` ## Data sync effect with callbacks new hook See `packages/twenty-front/src/modules/apollo/hooks/useQueryWithCallbacks.ts` which allows to fix Apollo v4 migration leftovers and is an implementation of the pattern we talked about with @charlesBochet We could refine this pattern in another PR. # Before https://github.com/user-attachments/assets/84e7a96f-6790-405d-8a73-2dacbf783be5 # After https://github.com/user-attachments/assets/4c692e3a-2413-4513-abcc-44d0da311203 Co-authored-by: Charles Bochet <charles@twenty.com>
83 lines
2.8 KiB
TypeScript
83 lines
2.8 KiB
TypeScript
import { styled } from '@linaria/react';
|
|
import { useContext } from 'react';
|
|
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
|
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
|
|
|
import { AGENT_CHAT_NEW_THREAD_DRAFT_KEY } from '@/ai/states/agentChatDraftsByThreadIdState';
|
|
import { agentChatMessagesLoadingState } from '@/ai/states/agentChatMessagesLoadingState';
|
|
import { agentChatThreadsLoadingState } from '@/ai/states/agentChatThreadsLoadingState';
|
|
import { agentChatHasMessageComponentSelector } from '@/ai/states/agentChatHasMessageComponentSelector';
|
|
import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
|
|
import { skipMessagesSkeletonUntilLoadedState } from '@/ai/states/skipMessagesSkeletonUntilLoadedState';
|
|
import { useAtomComponentSelectorValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorValue';
|
|
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
|
|
|
const StyledSkeletonContainer = styled.div`
|
|
display: flex;
|
|
flex: 1;
|
|
flex-direction: column;
|
|
gap: ${themeCssVariables.spacing[6]};
|
|
padding: ${themeCssVariables.spacing[3]};
|
|
`;
|
|
|
|
const StyledMessageBubble = styled.div`
|
|
align-items: center;
|
|
display: flex;
|
|
gap: ${themeCssVariables.spacing[3]};
|
|
`;
|
|
|
|
const StyledMessageSkeleton = styled.div`
|
|
width: 100%;
|
|
`;
|
|
|
|
const NUMBER_OF_SKELETONS = 6;
|
|
|
|
export const AIChatSkeletonLoader = () => {
|
|
const { theme } = useContext(ThemeContext);
|
|
const agentChatThreadsLoading = useAtomStateValue(
|
|
agentChatThreadsLoadingState,
|
|
);
|
|
const agentChatMessagesLoading = useAtomStateValue(
|
|
agentChatMessagesLoadingState,
|
|
);
|
|
const skipMessagesSkeletonUntilLoaded = useAtomStateValue(
|
|
skipMessagesSkeletonUntilLoadedState,
|
|
);
|
|
const currentAIChatThread = useAtomStateValue(currentAIChatThreadState);
|
|
|
|
const hasMessages = useAtomComponentSelectorValue(
|
|
agentChatHasMessageComponentSelector,
|
|
);
|
|
|
|
const isOnNewChatSlot =
|
|
currentAIChatThread === AGENT_CHAT_NEW_THREAD_DRAFT_KEY;
|
|
const showForMessagesLoading =
|
|
agentChatMessagesLoading && !skipMessagesSkeletonUntilLoaded;
|
|
const shouldRender =
|
|
!hasMessages &&
|
|
((agentChatThreadsLoading && isOnNewChatSlot) || showForMessagesLoading);
|
|
|
|
if (!shouldRender) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<SkeletonTheme
|
|
baseColor={theme.background.tertiary}
|
|
highlightColor={theme.background.transparent.lighter}
|
|
borderRadius={4}
|
|
>
|
|
<StyledSkeletonContainer>
|
|
{Array.from({ length: NUMBER_OF_SKELETONS }).map((_, index) => (
|
|
<StyledMessageBubble key={index}>
|
|
<Skeleton width={24} height={24} borderRadius={4} />
|
|
|
|
<StyledMessageSkeleton>
|
|
<Skeleton height={20} borderRadius={8} />
|
|
</StyledMessageSkeleton>
|
|
</StyledMessageBubble>
|
|
))}
|
|
</StyledSkeletonContainer>
|
|
</SkeletonTheme>
|
|
);
|
|
};
|