Fix AI chat streaming persistence when command menu closes (#14854)

Closes [#1583](https://github.com/twentyhq/core-team-issues/issues/1583)


- Removed `messageId` column from `File` table and its references in
code (unrelated to this PR)
- Updated AI chat to use React Context API with persistent provider in
`CommandMenuContainer`
- Converted all AI chat component states to regular Recoil atoms (no
instance context needed)

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
Abdul Rahman
2025-10-03 16:07:41 +05:30
committed by GitHub
parent 329af2b670
commit 0648dc5c65
41 changed files with 385 additions and 415 deletions
@@ -1,4 +1,5 @@
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
import { AgentChatProvider } from '@/ai/components/AgentChatProvider';
import { CommandMenuOpenContainer } from '@/command-menu/components/CommandMenuOpenContainer';
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { useCommandMenuCloseAnimationCompleteCleanup } from '@/command-menu/hooks/useCommandMenuCloseAnimationCompleteCleanup';
@@ -55,14 +56,16 @@ export const CommandMenuContainer = ({
<ActionMenuComponentInstanceContext.Provider
value={{ instanceId: COMMAND_MENU_COMPONENT_INSTANCE_ID }}
>
<AnimatePresence
mode="wait"
onExitComplete={commandMenuCloseAnimationCompleteCleanup}
>
{isCommandMenuOpened && (
<CommandMenuOpenContainer>{children}</CommandMenuOpenContainer>
)}
</AnimatePresence>
<AgentChatProvider>
<AnimatePresence
mode="wait"
onExitComplete={commandMenuCloseAnimationCompleteCleanup}
>
{isCommandMenuOpened && (
<CommandMenuOpenContainer>{children}</CommandMenuOpenContainer>
)}
</AnimatePresence>
</AgentChatProvider>
</ActionMenuComponentInstanceContext.Provider>
</ContextStoreComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>
@@ -1,17 +1,8 @@
import { LazyAIChatTab } from '@/ai/components/LazyAIChatTab';
import { AIChatSkeletonLoader } from '@/ai/components/internal/AIChatSkeletonLoader';
import { currentAIChatThreadComponentState } from '@/ai/states/currentAIChatThreadComponentState';
import { mapDBMessagesToUIMessages } from '@/ai/utils/mapDBMessagesToUIMessages';
import { AIChatTab } from '@/ai/components/AIChatTab';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import {
useGetAgentChatMessagesQuery,
useGetAgentChatThreadsQuery,
} from '~/generated-metadata/graphql';
const StyledContainer = styled.div`
height: 100%;
@@ -31,32 +22,6 @@ export const CommandMenuAskAIPage = () => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const agentId = currentWorkspace?.defaultAgent?.id;
const [currentThreadId, setCurrentThreadId] = useRecoilComponentState(
currentAIChatThreadComponentState,
agentId,
);
const { loading: threadsLoading } = useGetAgentChatThreadsQuery({
variables: { agentId: agentId! },
skip: isDefined(currentThreadId) || !isDefined(agentId),
onCompleted: (data) => {
if (data.agentChatThreads.length > 0) {
setCurrentThreadId(data.agentChatThreads[0].id);
}
},
});
const { loading, data } = useGetAgentChatMessagesQuery({
variables: { threadId: currentThreadId ?? '' },
skip: !isDefined(currentThreadId),
});
const isLoading = loading || !currentThreadId || threadsLoading;
if (isLoading) {
return <AIChatSkeletonLoader />;
}
if (!agentId) {
return (
<StyledContainer>
@@ -67,11 +32,7 @@ export const CommandMenuAskAIPage = () => {
return (
<StyledContainer>
<LazyAIChatTab
agentId={agentId}
key={currentThreadId}
uiMessages={mapDBMessagesToUIMessages(data?.agentChatMessages || [])}
/>
<AIChatTab agentId={agentId} />
</StyledContainer>
);
};