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 12:37:41 +02:00
committed by GitHub
co-authored by Félix Malfait
parent 329af2b670
commit 0648dc5c65
41 changed files with 385 additions and 415 deletions
@@ -70,7 +70,6 @@ export type Agent = {
export type AgentChatMessage = {
__typename?: 'AgentChatMessage';
createdAt: Scalars['DateTime'];
files: Array<File>;
id: Scalars['UUID'];
parts: Array<AgentChatMessagePart>;
role: Scalars['String'];
@@ -1317,7 +1316,6 @@ export type File = {
createdAt: Scalars['DateTime'];
fullPath: Scalars['String'];
id: Scalars['UUID'];
messageId?: Maybe<Scalars['UUID']>;
name: Scalars['String'];
size: Scalars['Float'];
type: Scalars['String'];
@@ -4674,7 +4672,7 @@ export type GetAgentChatMessagesQueryVariables = Exact<{
}>;
export type GetAgentChatMessagesQuery = { __typename?: 'Query', agentChatMessages: Array<{ __typename?: 'AgentChatMessage', id: string, threadId: string, role: string, createdAt: string, parts: Array<{ __typename?: 'AgentChatMessagePart', id: string, messageId: string, orderIndex: number, type: string, textContent?: string | null, reasoningContent?: string | null, toolName?: string | null, toolCallId?: string | null, toolInput?: any | null, toolOutput?: any | null, state?: string | null, errorMessage?: string | null, errorDetails?: any | null, sourceUrlSourceId?: string | null, sourceUrlUrl?: string | null, sourceUrlTitle?: string | null, sourceDocumentSourceId?: string | null, sourceDocumentMediaType?: string | null, sourceDocumentTitle?: string | null, sourceDocumentFilename?: string | null, fileMediaType?: string | null, fileFilename?: string | null, fileUrl?: string | null, providerMetadata?: any | null, createdAt: string }>, files: Array<{ __typename?: 'File', id: string, name: string, fullPath: string, size: number, type: string, createdAt: string }> }> };
export type GetAgentChatMessagesQuery = { __typename?: 'Query', agentChatMessages: Array<{ __typename?: 'AgentChatMessage', id: string, threadId: string, role: string, createdAt: string, parts: Array<{ __typename?: 'AgentChatMessagePart', id: string, messageId: string, orderIndex: number, type: string, textContent?: string | null, reasoningContent?: string | null, toolName?: string | null, toolCallId?: string | null, toolInput?: any | null, toolOutput?: any | null, state?: string | null, errorMessage?: string | null, errorDetails?: any | null, sourceUrlSourceId?: string | null, sourceUrlUrl?: string | null, sourceUrlTitle?: string | null, sourceDocumentSourceId?: string | null, sourceDocumentMediaType?: string | null, sourceDocumentTitle?: string | null, sourceDocumentFilename?: string | null, fileMediaType?: string | null, fileFilename?: string | null, fileUrl?: string | null, providerMetadata?: any | null, createdAt: string }> }> };
export type GetAgentChatThreadsQueryVariables = Exact<{
agentId: Scalars['UUID'];
@@ -6963,14 +6961,6 @@ export const GetAgentChatMessagesDocument = gql`
providerMetadata
createdAt
}
files {
id
name
fullPath
size
type
createdAt
}
}
}
`;
@@ -70,7 +70,6 @@ export type Agent = {
export type AgentChatMessage = {
__typename?: 'AgentChatMessage';
createdAt: Scalars['DateTime'];
files: Array<File>;
id: Scalars['UUID'];
parts: Array<AgentChatMessagePart>;
role: Scalars['String'];
@@ -1281,7 +1280,6 @@ export type File = {
createdAt: Scalars['DateTime'];
fullPath: Scalars['String'];
id: Scalars['UUID'];
messageId?: Maybe<Scalars['UUID']>;
name: Scalars['String'];
size: Scalars['Float'];
type: Scalars['String'];
@@ -14,12 +14,15 @@ import { AIChatMessage } from '@/ai/components/AIChatMessage';
import { AIChatSkeletonLoader } from '@/ai/components/internal/AIChatSkeletonLoader';
import { AgentChatContextPreview } from '@/ai/components/internal/AgentChatContextPreview';
import { SendMessageButton } from '@/ai/components/internal/SendMessageButton';
import { SendMessageWithRecordsContextButton } from '@/ai/components/internal/SendMessageWithRecordsContextButton';
import { AI_CHAT_INPUT_ID } from '@/ai/constants/AiChatInputId';
import { useAIChatFileUpload } from '@/ai/hooks/useAIChatFileUpload';
import { type UIMessageWithMetadata } from '@/ai/types/UIMessageWithMetadata';
import { useAgentChatContextOrThrow } from '@/ai/hooks/useAgentChatContextOrThrow';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { Button } from 'twenty-ui/input';
import { useAgentChat } from '../hooks/useAgentChat';
const StyledContainer = styled.div<{ isDraggingFile: boolean }>`
background: ${({ theme }) => theme.background.primary};
@@ -56,13 +59,7 @@ const StyledButtonsContainer = styled.div`
gap: ${({ theme }) => theme.spacing(2)};
`;
export const AIChatTab = ({
agentId,
uiMessages,
}: {
agentId: string;
uiMessages: UIMessageWithMetadata[];
}) => {
export const AIChatTab = ({ agentId }: { agentId: string }) => {
const [isDraggingFile, setIsDraggingFile] = useState(false);
const {
@@ -71,11 +68,14 @@ export const AIChatTab = ({
handleInputChange,
scrollWrapperId,
messages,
handleSendMessage,
isStreaming,
} = useAgentChat(agentId, uiMessages);
const { uploadFiles } = useAIChatFileUpload({ agentId });
} = useAgentChatContextOrThrow();
const contextStoreCurrentObjectMetadataItemId = useRecoilComponentValue(
contextStoreCurrentObjectMetadataItemIdComponentState,
);
const { uploadFiles } = useAIChatFileUpload();
const { createAgentChatThread } = useCreateNewAIChatThread({ agentId });
const { navigateCommandMenu } = useCommandMenu();
@@ -110,9 +110,9 @@ export const AIChatTab = ({
{isLoading && messages.length === 0 && <AIChatSkeletonLoader />}
<StyledInputArea>
<AgentChatContextPreview agentId={agentId} />
<AgentChatContextPreview />
<TextArea
textAreaId={`${agentId}-chat-input`}
textAreaId={AI_CHAT_INPUT_ID}
placeholder={t`Enter a question...`}
value={input}
onChange={handleInputChange}
@@ -138,13 +138,12 @@ export const AIChatTab = ({
Icon={IconMessageCirclePlus}
onClick={() => createAgentChatThread()}
/>
<AgentChatFileUploadButton agentId={agentId} />
<SendMessageButton
agentId={agentId}
handleSendMessage={handleSendMessage}
input={input}
isLoading={isLoading}
/>
<AgentChatFileUploadButton />
{contextStoreCurrentObjectMetadataItemId ? (
<SendMessageWithRecordsContextButton />
) : (
<SendMessageButton />
)}
</StyledButtonsContainer>
</StyledInputArea>
</>
@@ -1,9 +1,9 @@
import { currentAIChatThreadComponentState } from '@/ai/states/currentAIChatThreadComponentState';
import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useRecoilState } from 'recoil';
import { IconSparkles } from 'twenty-ui/display';
import { type AgentChatThread } from '~/generated-metadata/graphql';
@@ -69,18 +69,13 @@ const StyledThreadTitle = styled.div`
export const AIChatThreadGroup = ({
threads,
title,
agentId,
}: {
threads: AgentChatThread[];
agentId: string;
title: string;
}) => {
const { t } = useLingui();
const theme = useTheme();
const [, setCurrentThreadId] = useRecoilComponentState(
currentAIChatThreadComponentState,
agentId,
);
const [, setCurrentAIChatThread] = useRecoilState(currentAIChatThreadState);
const { openAskAIPage } = useOpenAskAIPageInCommandMenu();
if (threads.length === 0) {
@@ -94,7 +89,7 @@ export const AIChatThreadGroup = ({
{threads.map((thread) => (
<StyledThreadItem
onClick={() => {
setCurrentThreadId(thread.id);
setCurrentAIChatThread(thread.id);
openAskAIPage(thread.title);
}}
key={thread.id}
@@ -2,10 +2,10 @@ import styled from '@emotion/styled';
import { AIChatThreadGroup } from '@/ai/components/AIChatThreadGroup';
import { AIChatThreadsListEffect } from '@/ai/components/AIChatThreadsListEffect';
import { AIChatSkeletonLoader } from '@/ai/components/internal/AIChatSkeletonLoader';
import { useCreateNewAIChatThread } from '@/ai/hooks/useCreateNewAIChatThread';
import { groupThreadsByDate } from '@/ai/utils/groupThreadsByDate';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { AIChatSkeletonLoader } from '@/ai/components/internal/AIChatSkeletonLoader';
import { Key } from 'ts-key-enum';
import { capitalize } from 'twenty-shared/utils';
import { Button } from 'twenty-ui/input';
@@ -65,7 +65,6 @@ export const AIChatThreadsList = ({ agentId }: { agentId: string }) => {
<AIChatThreadGroup
key={title}
title={capitalize(title)}
agentId={agentId}
threads={threads}
/>
))}
@@ -0,0 +1,57 @@
import { AgentChatContext } from '@/ai/contexts/AgentChatContext';
import { useAgentChat } from '@/ai/hooks/useAgentChat';
import { useAgentChatData } from '@/ai/hooks/useAgentChatData';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { Suspense } from 'react';
import { useRecoilValue } from 'recoil';
import { FeatureFlagKey } from '~/generated/graphql';
const AgentChatProviderContent = ({
agentId,
children,
}: {
agentId: string;
children: React.ReactNode;
}) => {
const { uiMessages, isLoading } = useAgentChatData(agentId);
const chatState = useAgentChat(agentId, uiMessages);
const combinedIsLoading = chatState.isLoading || isLoading;
return (
<AgentChatContext.Provider
value={{
...chatState,
isLoading: combinedIsLoading,
}}
>
{children}
</AgentChatContext.Provider>
);
};
export const AgentChatProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const agentId = currentWorkspace?.defaultAgent?.id;
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
if (!isAiEnabled || !agentId) {
return (
<AgentChatContext.Provider value={null}>
{children}
</AgentChatContext.Provider>
);
}
return (
<Suspense fallback={null}>
<AgentChatProviderContent agentId={agentId}>
{children}
</AgentChatProviderContent>
</Suspense>
);
};
@@ -0,0 +1,54 @@
import { AgentChatContext } from '@/ai/contexts/AgentChatContext';
import { useAgentChat } from '@/ai/hooks/useAgentChat';
import { useAgentChatData } from '@/ai/hooks/useAgentChatData';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { Suspense } from 'react';
import { useRecoilValue } from 'recoil';
const AgentChatProviderContent = ({
agentId,
children,
}: {
agentId: string;
children: React.ReactNode;
}) => {
const { uiMessages, isLoading } = useAgentChatData(agentId);
const chatState = useAgentChat(agentId, uiMessages);
const combinedIsLoading = chatState.isLoading || isLoading;
return (
<AgentChatContext.Provider
value={{
...chatState,
isLoading: combinedIsLoading,
}}
>
{children}
</AgentChatContext.Provider>
);
};
export const AgentChatProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const agentId = currentWorkspace?.defaultAgent?.id;
if (!agentId) {
return (
<AgentChatContext.Provider value={null}>
{children}
</AgentChatContext.Provider>
);
}
return (
<Suspense fallback={null}>
<AgentChatProviderContent agentId={agentId}>
{children}
</AgentChatProviderContent>
</Suspense>
);
};
@@ -1,23 +0,0 @@
import { AIChatSkeletonLoader } from '@/ai/components/internal/AIChatSkeletonLoader';
import { type UIMessageWithMetadata } from '@/ai/types/UIMessageWithMetadata';
import { lazy, Suspense } from 'react';
const AIChatTab = lazy(() =>
import('./AIChatTab').then((module) => ({
default: module.AIChatTab,
})),
);
export const LazyAIChatTab = ({
agentId,
uiMessages,
}: {
agentId: string;
uiMessages: UIMessageWithMetadata[];
}) => {
return (
<Suspense fallback={<AIChatSkeletonLoader />}>
<AIChatTab agentId={agentId} uiMessages={uiMessages} />
</Suspense>
);
};
@@ -1,10 +1,10 @@
import { AgentChatContextRecordPreview } from '@/ai/components/internal/AgentChatContextRecordPreview';
import { agentChatSelectedFilesComponentState } from '@/ai/states/agentChatSelectedFilesComponentState';
import { agentChatUploadedFilesComponentState } from '@/ai/states/agentChatUploadedFilesComponentState';
import { agentChatSelectedFilesState } from '@/ai/states/agentChatSelectedFilesState';
import { agentChatUploadedFilesState } from '@/ai/states/agentChatUploadedFilesState';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { AgentChatFilePreview } from './AgentChatFilePreview';
const StyledContainer = styled.div`
@@ -21,11 +21,13 @@ const StyledPreviewsContainer = styled.div`
gap: ${({ theme }) => theme.spacing(1)};
`;
export const AgentChatContextPreview = ({ agentId }: { agentId: string }) => {
const [agentChatSelectedFiles, setAgentChatSelectedFiles] =
useRecoilComponentState(agentChatSelectedFilesComponentState, agentId);
const [agentChatUploadedFiles, setAgentChatUploadedFiles] =
useRecoilComponentState(agentChatUploadedFilesComponentState, agentId);
export const AgentChatContextPreview = () => {
const [agentChatSelectedFiles, setAgentChatSelectedFiles] = useRecoilState(
agentChatSelectedFilesState,
);
const [agentChatUploadedFiles, setAgentChatUploadedFiles] = useRecoilState(
agentChatUploadedFilesState,
);
const handleRemoveUploadedFile = async (fileIndex: number) => {
setAgentChatUploadedFiles(
@@ -62,7 +64,6 @@ export const AgentChatContextPreview = ({ agentId }: { agentId: string }) => {
))}
{contextStoreCurrentObjectMetadataItemId && (
<AgentChatContextRecordPreview
agentId={agentId}
contextStoreCurrentObjectMetadataItemId={
contextStoreCurrentObjectMetadataItemId
}
@@ -3,10 +3,10 @@ import { CommandMenuContextRecordChipAvatars } from '@/command-menu/components/C
import { getSelectedRecordsContextText } from '@/command-menu/utils/getRecordContextText';
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useRecoilState } from 'recoil';
import { MultipleAvatarChip } from 'twenty-ui/components';
import { IconReload, IconX } from 'twenty-ui/display';
@@ -24,10 +24,8 @@ const StyledChipWrapper = styled.div<{ isActive: boolean }>`
`;
export const AgentChatContextRecordPreview = ({
agentId,
contextStoreCurrentObjectMetadataItemId,
}: {
agentId: string;
contextStoreCurrentObjectMetadataItemId: string;
}) => {
const theme = useTheme();
@@ -41,7 +39,7 @@ export const AgentChatContextRecordPreview = ({
});
const [isAgentChatCurrentContextActive, setIsAgentChatCurrentContextActive] =
useRecoilComponentState(isAgentChatCurrentContextActiveState, agentId);
useRecoilState(isAgentChatCurrentContextActiveState);
const Avatars = records.map((record) => (
// @todo move this components to be less specific. (Outside of CommandMenu
@@ -1,8 +1,8 @@
import { useAIChatFileUpload } from '@/ai/hooks/useAIChatFileUpload';
import { agentChatSelectedFilesComponentState } from '@/ai/states/agentChatSelectedFilesComponentState';
import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
import { agentChatSelectedFilesState } from '@/ai/states/agentChatSelectedFilesState';
import styled from '@emotion/styled';
import React, { useRef } from 'react';
import { useSetRecoilState } from 'recoil';
import { IconPaperclip } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
@@ -16,13 +16,12 @@ const StyledFileInput = styled.input`
display: none;
`;
export const AgentChatFileUploadButton = ({ agentId }: { agentId: string }) => {
const setAgentChatSelectedFiles = useSetRecoilComponentState(
agentChatSelectedFilesComponentState,
agentId,
export const AgentChatFileUploadButton = () => {
const setAgentChatSelectedFiles = useSetRecoilState(
agentChatSelectedFilesState,
);
const fileInputRef = useRef<HTMLInputElement>(null);
const { uploadFiles } = useAIChatFileUpload({ agentId });
const { uploadFiles } = useAIChatFileUpload();
const handleFileInputChange = (
event: React.ChangeEvent<HTMLInputElement>,
@@ -1,25 +1,42 @@
import { AI_CHAT_INPUT_ID } from '@/ai/constants/AiChatInputId';
import { useAgentChatContextOrThrow } from '@/ai/hooks/useAgentChatContextOrThrow';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { t } from '@lingui/core/macro';
import { Key } from 'ts-key-enum';
import { Button } from 'twenty-ui/input';
export const SendMessageButton = ({
handleSendMessage,
input,
isLoading,
records,
}: {
agentId: string;
handleSendMessage: () => void;
input: string;
isLoading: boolean;
records?: ObjectRecord[];
}) => {
const { handleSendMessage, isLoading, input } = useAgentChatContextOrThrow();
useHotkeysOnFocusedElement({
keys: [Key.Enter],
callback: (event: KeyboardEvent) => {
if (!event.ctrlKey && !event.metaKey) {
event.preventDefault();
handleSendMessage(records);
}
},
focusId: AI_CHAT_INPUT_ID,
dependencies: [input, isLoading],
options: {
enableOnFormTags: true,
},
});
return (
<Button
hotkeys={input && !isLoading ? ['⏎'] : undefined}
onClick={() => handleSendMessage(records)}
disabled={!input || isLoading}
variant="primary"
accent="blue"
size="small"
hotkeys={input && !isLoading ? ['⏎'] : undefined}
disabled={!input || isLoading}
title={t`Send`}
onClick={handleSendMessage}
/>
);
};
@@ -0,0 +1,10 @@
import { SendMessageButton } from '@/ai/components/internal/SendMessageButton';
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
export const SendMessageWithRecordsContextButton = () => {
const { records } = useFindManyRecordsSelectedInContextStore({
limit: 10,
});
return <SendMessageButton records={records} />;
};
@@ -0,0 +1 @@
export const AI_CHAT_INPUT_ID = 'ai-chat-input';
@@ -0,0 +1,26 @@
import { type AIChatObjectMetadataAndRecordContext } from '@/ai/states/agentChatContextState';
import { type UIMessageWithMetadata } from '@/ai/types/UIMessageWithMetadata';
import { createContext } from 'react';
import { type ObjectRecord } from '../../object-record/types/ObjectRecord';
export type AgentChatContextValue = {
messages: UIMessageWithMetadata[];
isStreaming: boolean;
isLoading: boolean;
error?: Error;
input: string;
handleInputChange: (value: string) => void;
handleSendMessage: (records?: ObjectRecord[]) => Promise<void>;
handleSetContext: (
items: Array<AIChatObjectMetadataAndRecordContext>,
) => Promise<void>;
scrollWrapperId: string;
context: Array<AIChatObjectMetadataAndRecordContext>;
};
export const AgentChatContext = createContext<AgentChatContextValue | null>(
null,
);
@@ -34,14 +34,6 @@ export const GET_AGENT_CHAT_MESSAGES = gql`
providerMetadata
createdAt
}
files {
id
name
fullPath
size
type
createdAt
}
}
}
`;
@@ -1,24 +1,26 @@
import { agentChatSelectedFilesComponentState } from '@/ai/states/agentChatSelectedFilesComponentState';
import { agentChatUploadedFilesComponentState } from '@/ai/states/agentChatUploadedFilesComponentState';
import { agentChatSelectedFilesState } from '@/ai/states/agentChatSelectedFilesState';
import { agentChatUploadedFilesState } from '@/ai/states/agentChatUploadedFilesState';
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useLingui } from '@lingui/react/macro';
import { type FileUIPart } from 'ai';
import { useRecoilState } from 'recoil';
import { buildSignedPath, isDefined } from 'twenty-shared/utils';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useUploadFileMutation } from '~/generated-metadata/graphql';
import { FileFolder } from '~/generated/graphql';
export const useAIChatFileUpload = ({ agentId }: { agentId: string }) => {
export const useAIChatFileUpload = () => {
const coreClient = useApolloCoreClient();
const [uploadFile] = useUploadFileMutation({ client: coreClient });
const { t } = useLingui();
const { enqueueErrorSnackBar } = useSnackBar();
const [agentChatSelectedFiles, setAgentChatSelectedFiles] =
useRecoilComponentState(agentChatSelectedFilesComponentState, agentId);
const [agentChatUploadedFiles, setAgentChatUploadedFiles] =
useRecoilComponentState(agentChatUploadedFilesComponentState, agentId);
const [agentChatSelectedFiles, setAgentChatSelectedFiles] = useRecoilState(
agentChatSelectedFilesState,
);
const [agentChatUploadedFiles, setAgentChatUploadedFiles] = useRecoilState(
agentChatUploadedFilesState,
);
const sendFile = async (file: File): Promise<FileUIPart | null> => {
try {
@@ -1,24 +1,21 @@
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useRecoilState } from 'recoil';
import { Key } from 'ts-key-enum';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useRecoilState, useRecoilValue } from 'recoil';
import {
type AIChatObjectMetadataAndRecordContext,
agentChatObjectMetadataAndRecordContextState,
} from '@/ai/states/agentChatObjectMetadataAndRecordContextState';
import { agentChatSelectedFilesComponentState } from '@/ai/states/agentChatSelectedFilesComponentState';
import { agentChatUploadedFilesComponentState } from '@/ai/states/agentChatUploadedFilesComponentState';
import { currentAIChatThreadComponentState } from '@/ai/states/currentAIChatThreadComponentState';
agentChatContextState,
} from '@/ai/states/agentChatContextState';
import { agentChatSelectedFilesState } from '@/ai/states/agentChatSelectedFilesState';
import { agentChatUploadedFilesState } from '@/ai/states/agentChatUploadedFilesState';
import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
import { isAgentChatCurrentContextActiveState } from '@/ai/states/isAgentChatCurrentContextActiveState';
import { type UIMessageWithMetadata } from '@/ai/types/UIMessageWithMetadata';
import { getTokenPair } from '@/apollo/utils/getTokenPair';
import { useFindManyRecordsSelectedInContextStore } from '@/context-store/hooks/useFindManyRecordsSelectedInContextStore';
import { contextStoreCurrentObjectMetadataItemIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataItemIdComponentState';
import { useGetObjectMetadataItemById } from '@/object-metadata/hooks/useGetObjectMetadataItemById';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { isDefined } from 'twenty-shared/utils';
@@ -35,33 +32,22 @@ export const useAgentChat = (
contextStoreCurrentObjectMetadataItemIdComponentState,
);
const { records } = useFindManyRecordsSelectedInContextStore({
limit: 10,
});
const isAgentChatCurrentContextActive = useRecoilComponentValue(
const isAgentChatCurrentContextActive = useRecoilValue(
isAgentChatCurrentContextActiveState,
agentId,
);
const agentChatSelectedFiles = useRecoilComponentValue(
agentChatSelectedFilesComponentState,
agentId,
const agentChatSelectedFiles = useRecoilValue(agentChatSelectedFilesState);
const [agentChatContext, setAgentChatContext] = useRecoilState(
agentChatContextState,
);
const [agentChatContext, setAgentChatContext] = useRecoilComponentState(
agentChatObjectMetadataAndRecordContextState,
agentId,
);
const currentAIChatThread = useRecoilValue(currentAIChatThreadState);
const currentThreadId = useRecoilComponentValue(
currentAIChatThreadComponentState,
agentId,
const [agentChatUploadedFiles, setAgentChatUploadedFiles] = useRecoilState(
agentChatUploadedFilesState,
);
const [agentChatUploadedFiles, setAgentChatUploadedFiles] =
useRecoilComponentState(agentChatUploadedFilesComponentState, agentId);
const [agentChatInput, setAgentChatInput] =
useRecoilState(agentChatInputState);
@@ -80,7 +66,7 @@ export const useAgentChat = (
}),
}),
messages: uiMessages,
id: currentThreadId as string,
id: `${currentAIChatThread}-${uiMessages.length}`,
onError: (error) => {
enqueueErrorSnackBar({ message: error.message });
},
@@ -96,9 +82,16 @@ export const useAgentChat = (
};
const isLoading =
!currentThreadId || isStreaming || agentChatSelectedFiles.length > 0;
!currentAIChatThread || isStreaming || agentChatSelectedFiles.length > 0;
const handleSendMessage = async (records?: ObjectRecord[]) => {
if (agentChatInput.trim() === '' || isLoading === true) {
return;
}
const content = agentChatInput.trim();
setAgentChatInput('');
const sendChatMessage = async (content: string) => {
const recordIdsByObjectMetadataNameSingular = [];
if (
@@ -121,7 +114,7 @@ export const useAgentChat = (
},
{
body: {
threadId: currentThreadId,
threadId: currentAIChatThread,
recordIdsByObjectMetadataNameSingular,
},
},
@@ -131,36 +124,12 @@ export const useAgentChat = (
setTimeout(scrollToBottom, 100);
};
const handleSendMessage = async () => {
if (agentChatInput.trim() === '' || isLoading === true) {
return;
}
const content = agentChatInput.trim();
setAgentChatInput('');
await sendChatMessage(content);
};
const handleSetContext = async (
items: Array<AIChatObjectMetadataAndRecordContext>,
) => {
setAgentChatContext(items);
};
useHotkeysOnFocusedElement({
keys: [Key.Enter],
callback: (event: KeyboardEvent) => {
if (!event.ctrlKey && !event.metaKey) {
event.preventDefault();
handleSendMessage();
}
},
focusId: `${agentId}-chat-input`,
dependencies: [agentChatInput, isLoading],
options: {
enableOnFormTags: true,
},
});
return {
handleInputChange: (value: string) => setAgentChatInput(value),
messages,
@@ -0,0 +1,14 @@
import { AgentChatContext } from '@/ai/contexts/AgentChatContext';
import { useContext } from 'react';
export const useAgentChatContextOrThrow = () => {
const context = useContext(AgentChatContext);
if (!context) {
throw new Error(
'AgentChatContext not found. Please wrap your component tree with <AgentChatContextProvider> before using useAgentChatContextOrThrow().',
);
}
return context;
};
@@ -0,0 +1,37 @@
import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
import { mapDBMessagesToUIMessages } from '@/ai/utils/mapDBMessagesToUIMessages';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import {
useGetAgentChatMessagesQuery,
useGetAgentChatThreadsQuery,
} from '~/generated-metadata/graphql';
export const useAgentChatData = (agentId: string) => {
const [currentAIChatThread, setCurrentAIChatThread] = useRecoilState(
currentAIChatThreadState,
);
const { loading: threadsLoading } = useGetAgentChatThreadsQuery({
variables: { agentId },
skip: isDefined(currentAIChatThread) || !isDefined(agentId),
onCompleted: (data) => {
if (data.agentChatThreads.length > 0) {
setCurrentAIChatThread(data.agentChatThreads[0].id);
}
},
});
const { loading: messagesLoading, data } = useGetAgentChatMessagesQuery({
variables: { threadId: currentAIChatThread! },
skip: !isDefined(currentAIChatThread),
});
const uiMessages = mapDBMessagesToUIMessages(data?.agentChatMessages || []);
const isLoading = messagesLoading || threadsLoading;
return {
uiMessages,
isLoading,
};
};
@@ -1,19 +1,16 @@
import { currentAIChatThreadComponentState } from '@/ai/states/currentAIChatThreadComponentState';
import { currentAIChatThreadState } from '@/ai/states/currentAIChatThreadState';
import { useOpenAskAIPageInCommandMenu } from '@/command-menu/hooks/useOpenAskAIPageInCommandMenu';
import { useRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentState';
import { useRecoilState } from 'recoil';
import { useCreateAgentChatThreadMutation } from '~/generated-metadata/graphql';
export const useCreateNewAIChatThread = ({ agentId }: { agentId: string }) => {
const [, setCurrentThreadId] = useRecoilComponentState(
currentAIChatThreadComponentState,
agentId,
);
const [, setCurrentAIChatThread] = useRecoilState(currentAIChatThreadState);
const { openAskAIPage } = useOpenAskAIPageInCommandMenu();
const [createAgentChatThread] = useCreateAgentChatThreadMutation({
variables: { input: { agentId } },
onCompleted: (data) => {
setCurrentThreadId(data.createAgentChatThread.id);
setCurrentAIChatThread(data.createAgentChatThread.id);
openAskAIPage();
},
});
@@ -0,0 +1,13 @@
import { atom } from 'recoil';
export type AIChatObjectMetadataAndRecordContext = {
type: 'objectMetadataId' | 'recordId';
id: string;
};
export const agentChatContextState = atom<
Array<AIChatObjectMetadataAndRecordContext>
>({
key: 'ai/agentChatContextState',
default: [],
});
@@ -1,14 +0,0 @@
import { AgentChatMessagesComponentInstanceContext } from '@/ai/states/agentChatMessagesComponentState';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export type AIChatObjectMetadataAndRecordContext = {
type: 'objectMetadataId' | 'recordId';
id: string;
};
export const agentChatObjectMetadataAndRecordContextState =
createComponentState<Array<AIChatObjectMetadataAndRecordContext>>({
defaultValue: [],
key: 'agentChatObjectMetadataAndRecordContextState',
componentInstanceContext: AgentChatMessagesComponentInstanceContext,
});
@@ -1,10 +0,0 @@
import { AgentChatMessagesComponentInstanceContext } from '@/ai/states/agentChatMessagesComponentState';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const agentChatSelectedFilesComponentState = createComponentState<
File[]
>({
key: 'agentChatSelectedFilesComponentState',
defaultValue: [],
componentInstanceContext: AgentChatMessagesComponentInstanceContext,
});
@@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const agentChatSelectedFilesState = atom<File[]>({
key: 'ai/agentChatSelectedFilesState',
default: [],
});
@@ -1,11 +0,0 @@
import { AgentChatMessagesComponentInstanceContext } from '@/ai/states/agentChatMessagesComponentState';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { type FileUIPart } from 'ai';
export const agentChatUploadedFilesComponentState = createComponentState<
FileUIPart[]
>({
key: 'agentChatUploadedFilesComponentState',
defaultValue: [],
componentInstanceContext: AgentChatMessagesComponentInstanceContext,
});
@@ -0,0 +1,7 @@
import { type FileUIPart } from 'ai';
import { atom } from 'recoil';
export const agentChatUploadedFilesState = atom<FileUIPart[]>({
key: 'ai/agentChatUploadedFilesState',
default: [],
});
@@ -1,10 +0,0 @@
import { CommandMenuPageComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuPageComponentInstanceContext';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
export const currentAIChatThreadComponentState = createComponentState<
string | null
>({
key: 'currentAIChatThreadComponentState',
defaultValue: null,
componentInstanceContext: CommandMenuPageComponentInstanceContext,
});
@@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const currentAIChatThreadState = atom<string | null>({
key: 'ai/currentAIChatThreadState',
default: null,
});
@@ -1,12 +1,6 @@
import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
import { atom } from 'recoil';
export const IsAgentChatCurrentContextActiveInstanceContext =
createComponentInstanceContext();
export const isAgentChatCurrentContextActiveState =
createComponentState<boolean>({
defaultValue: true,
key: 'isAgentChatCurrentContextActiveState',
componentInstanceContext: IsAgentChatCurrentContextActiveInstanceContext,
});
export const isAgentChatCurrentContextActiveState = atom<boolean>({
key: 'ai/isAgentChatCurrentContextActiveState',
default: true,
});
@@ -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>
);
};
@@ -2,7 +2,6 @@ import { Logger } from '@nestjs/common';
import { Command, CommandRunner } from 'nest-commander';
import { CleanupOrphanedFilesCronCommand } from 'src/engine/core-modules/file/crons/commands/cleanup-orphaned-files.cron.command';
import { CheckPublicDomainsValidRecordsCronCommand } from 'src/engine/core-modules/public-domain/crons/commands/check-public-domains-valid-records.cron.command';
import { CheckCustomDomainValidRecordsCronCommand } from 'src/engine/core-modules/workspace/crons/commands/check-custom-domain-valid-records.cron.command';
import { CronTriggerCronCommand } from 'src/engine/metadata-modules/cron-trigger/crons/commands/cron-trigger.cron.command';
@@ -35,7 +34,6 @@ export class CronRegisterAllCommand extends CommandRunner {
private readonly calendarEventsImportCronCommand: CalendarEventsImportCronCommand,
private readonly calendarOngoingStaleCronCommand: CalendarOngoingStaleCronCommand,
private readonly workflowCronTriggerCronCommand: WorkflowCronTriggerCronCommand,
private readonly cleanupOrphanedFilesCronCommand: CleanupOrphanedFilesCronCommand,
private readonly checkCustomDomainValidRecordsCronCommand: CheckCustomDomainValidRecordsCronCommand,
private readonly checkPublicDomainsValidRecordsCronCommand: CheckPublicDomainsValidRecordsCronCommand,
private readonly workflowRunEnqueueCronCommand: WorkflowRunEnqueueCronCommand,
@@ -76,10 +74,6 @@ export class CronRegisterAllCommand extends CommandRunner {
name: 'CalendarOngoingStale',
command: this.calendarOngoingStaleCronCommand,
},
{
name: 'CleanupOrphanedFiles',
command: this.cleanupOrphanedFilesCronCommand,
},
{
name: 'CheckCustomDomainValidRecords',
command: this.checkCustomDomainValidRecordsCronCommand,
@@ -0,0 +1,23 @@
import { type MigrationInterface, type QueryRunner } from 'typeorm';
export class RemoveMessageIdFromFileTable1759378531410
implements MigrationInterface
{
name = 'RemoveMessageIdFromFileTable1759378531410';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "core"."file" DROP CONSTRAINT "FK_a78a68c3f577a485dd4c741909f"`,
);
await queryRunner.query(
`ALTER TABLE "core"."file" DROP COLUMN "messageId"`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "core"."file" ADD "messageId" uuid`);
await queryRunner.query(
`ALTER TABLE "core"."file" ADD CONSTRAINT "FK_a78a68c3f577a485dd4c741909f" FOREIGN KEY ("messageId") REFERENCES "core"."agentChatMessage"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
);
}
}
@@ -1,32 +0,0 @@
import { Command, CommandRunner } from 'nest-commander';
import {
CLEANUP_ORPHANED_FILES_CRON_PATTERN,
CleanupOrphanedFilesCronJob,
} from 'src/engine/core-modules/file/crons/jobs/cleanup-orphaned-files.cron.job';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
@Command({
name: 'cron:file:cleanup-orphaned-files',
description: 'Starts a cron job to clean up orphaned files (no messageId)',
})
export class CleanupOrphanedFilesCronCommand extends CommandRunner {
constructor(
@InjectMessageQueue(MessageQueue.cronQueue)
private readonly messageQueueService: MessageQueueService,
) {
super();
}
async run(): Promise<void> {
await this.messageQueueService.addCron<undefined>({
jobName: CleanupOrphanedFilesCronJob.name,
data: undefined,
options: {
repeat: { pattern: CLEANUP_ORPHANED_FILES_CRON_PATTERN },
},
});
}
}
@@ -1,67 +0,0 @@
import { InjectRepository } from '@nestjs/typeorm';
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
import { IsNull, LessThan, Repository } from 'typeorm';
import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { FileMetadataService } from 'src/engine/core-modules/file/services/file-metadata.service';
import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator';
import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
export const CLEANUP_ORPHANED_FILES_CRON_PATTERN = '0 2 * * *';
@Processor(MessageQueue.cronQueue)
export class CleanupOrphanedFilesCronJob {
constructor(
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
@InjectRepository(FileEntity)
private readonly fileRepository: Repository<FileEntity>,
private readonly fileMetadataService: FileMetadataService,
) {}
@Process(CleanupOrphanedFilesCronJob.name)
@SentryCronMonitor(
CleanupOrphanedFilesCronJob.name,
CLEANUP_ORPHANED_FILES_CRON_PATTERN,
)
async handle(): Promise<void> {
const activeWorkspaces = await this.workspaceRepository.find({
where: {
activationStatus: WorkspaceActivationStatus.ACTIVE,
},
select: ['id'],
});
if (activeWorkspaces.length === 0) {
return;
}
const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
const orphanedFiles = await this.fileRepository.find({
select: ['id', 'workspaceId', 'fullPath'],
where: {
messageId: IsNull(),
createdAt: LessThan(oneHourAgo),
},
});
if (orphanedFiles.length === 0) {
return;
}
for (const file of orphanedFiles) {
await this.fileMetadataService
.deleteFileById(file.id, file.workspaceId)
.catch((error) => {
throw new Error(
`[${CleanupOrphanedFilesCronJob.name}] Cannot delete orphaned file - ${file.fullPath}: ${error.message}`,
);
});
}
}
}
@@ -19,9 +19,6 @@ export class FileDTO {
@Field()
type: string;
@Field(() => UUIDScalarType, { nullable: true })
messageId?: string;
@Field(() => Date, { nullable: false })
createdAt: Date;
}
@@ -10,7 +10,6 @@ import {
} from 'typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AgentChatMessageEntity } from 'src/engine/metadata-modules/agent/agent-chat-message.entity';
@Entity('file')
@Index('IDX_FILE_WORKSPACE_ID', ['workspaceId'])
@@ -39,15 +38,6 @@ export class FileEntity {
@JoinColumn({ name: 'workspaceId' })
workspace: Relation<Workspace>;
@Column({ nullable: true, type: 'uuid' })
messageId: string;
@ManyToOne(() => AgentChatMessageEntity, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'messageId' })
message: Relation<AgentChatMessageEntity>;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
}
@@ -11,8 +11,6 @@ import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { FileController } from './controllers/file.controller';
import { CleanupOrphanedFilesCronCommand } from './crons/commands/cleanup-orphaned-files.cron.command';
import { CleanupOrphanedFilesCronJob } from './crons/jobs/cleanup-orphaned-files.cron.job';
import { FileEntity } from './entities/file.entity';
import { FileUploadService } from './file-upload/services/file-upload.service';
import { FileResolver } from './resolvers/file.resolver';
@@ -34,11 +32,9 @@ import { FileService } from './services/file.service';
FileWorkspaceMemberListener,
FileWorkspaceFolderDeletionJob,
FileDeletionJob,
CleanupOrphanedFilesCronJob,
CleanupOrphanedFilesCronCommand,
FileUploadService,
],
exports: [FileService, FileMetadataService, CleanupOrphanedFilesCronCommand],
exports: [FileService, FileMetadataService],
controllers: [FileController],
})
export class FileModule {}
@@ -10,7 +10,6 @@ import {
Relation,
} from 'typeorm';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import { AgentChatThreadEntity } from 'src/engine/metadata-modules/agent/agent-chat-thread.entity';
import { AgentChatMessagePartEntity } from './agent-chat-message-part.entity';
@@ -41,9 +40,6 @@ export class AgentChatMessageEntity {
@OneToMany(() => AgentChatMessagePartEntity, (part) => part.message)
parts: Relation<AgentChatMessagePartEntity[]>;
@OneToMany(() => FileEntity, (file) => file.message)
files: Relation<FileEntity[]>;
@CreateDateColumn()
createdAt: Date;
}
@@ -1,7 +1,6 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { FileDTO } from 'src/engine/core-modules/file/dtos/file.dto';
import { AgentChatMessagePartDTO } from './agent-chat-message-part.dto';
@@ -19,9 +18,6 @@ export class AgentChatMessageDTO {
@Field(() => [AgentChatMessagePartDTO])
parts: AgentChatMessagePartDTO[];
@Field(() => [FileDTO])
files: FileDTO[];
@Field()
createdAt: Date;
}