twenty/packages/twenty-front/src/modules/ai/hooks/useUpdateStreamingPartsWithDiff.ts
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

55 lines
1.9 KiB
TypeScript

import { useProcessStreamingMessageUpdate } from '@/ai/hooks/useProcessStreamingMessageUpdate';
import { agentChatMessageComponentFamilyState } from '@/ai/states/agentChatMessageComponentFamilyState';
import { useAtomComponentFamilyStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentFamilyStateCallbackState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { useCallback } from 'react';
import { type ExtendedUIMessage } from 'twenty-shared/ai';
import { isDefined } from 'twenty-shared/utils';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
export const useUpdateStreamingPartsWithDiff = () => {
const agentChatMessageFamilyCallbackState =
useAtomComponentFamilyStateCallbackState(
agentChatMessageComponentFamilyState,
);
const { processStreamingMessageUpdate } = useProcessStreamingMessageUpdate();
const updateStreamingPartsWithDiff = useCallback(
(incomingMessages: ExtendedUIMessage[]) => {
for (const incomingMessage of incomingMessages) {
const alreadyExistingMessage = jotaiStore.get(
agentChatMessageFamilyCallbackState(incomingMessage.id),
);
const messageContentHasChanged = !isDeeplyEqual(
alreadyExistingMessage,
incomingMessage,
);
const messageAlreadyExists = isDefined(alreadyExistingMessage);
const shouldProcessMessage =
!messageAlreadyExists || messageContentHasChanged;
if (!shouldProcessMessage) {
continue;
}
const clonedMessage = structuredClone(incomingMessage);
jotaiStore.set(
agentChatMessageFamilyCallbackState(incomingMessage.id),
clonedMessage,
);
processStreamingMessageUpdate(incomingMessage);
}
},
[agentChatMessageFamilyCallbackState, processStreamingMessageUpdate],
);
return {
updateStreamingPartsWithDiff,
};
};