Files
twenty/packages/twenty-front/src/modules/ai/components/AIChatScrollToBottomButton.tsx
T
Lucas Bordeau fc9723949b Fix AI chat re-renders and refactored code (#18585)
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>
2026-03-21 12:52:21 +00:00

50 lines
1.7 KiB
TypeScript

import { agentChatIsScrolledToBottomSelector } from '@/ai/states/agentChatIsScrolledToBottomSelector';
import { scrollAIChatToBottom } from '@/ai/utils/scrollAIChatToBottom';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { styled } from '@linaria/react';
import { IconArrowDown } from 'twenty-ui/display';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledScrollToBottomButton = styled.button<{ isVisible: boolean }>`
align-items: center;
background: ${themeCssVariables.background.primary};
border: 1px solid ${themeCssVariables.border.color.medium};
border-radius: ${themeCssVariables.border.radius.rounded};
bottom: ${themeCssVariables.spacing[3]};
box-shadow: ${themeCssVariables.boxShadow.light};
color: ${themeCssVariables.font.color.secondary};
cursor: pointer;
display: flex;
height: 32px;
justify-content: center;
left: 50%;
opacity: ${({ isVisible }) => (isVisible ? 1 : 0)};
pointer-events: ${({ isVisible }) => (isVisible ? 'auto' : 'none')};
position: absolute;
transform: translateX(-50%);
transition:
opacity calc(${themeCssVariables.animation.duration.normal} * 1s) ease,
background calc(${themeCssVariables.animation.duration.fast} * 1s) ease;
width: 32px;
z-index: 1;
&:hover {
background: ${themeCssVariables.background.tertiary};
}
`;
export const AIChatScrollToBottomButton = () => {
const agentChatIsScrolledToBottom = useAtomStateValue(
agentChatIsScrolledToBottomSelector,
);
return (
<StyledScrollToBottomButton
isVisible={!agentChatIsScrolledToBottom}
onClick={scrollAIChatToBottom}
>
<IconArrowDown size={16} />
</StyledScrollToBottomButton>
);
};