From 72fd3b07e748f078a213e8335978e8b8c1f977de Mon Sep 17 00:00:00 2001 From: Abdul Rahman <81605929+abdulrahmancodes@users.noreply.github.com> Date: Tue, 15 Jul 2025 12:27:10 +0530 Subject: [PATCH] Add file support to agent chat (#13187) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://github.com/user-attachments/assets/911d5d8d-cc2e-4c18-9f93-2663d84ff9ef --------- Co-authored-by: Raphaël Bosi <71827178+bosiraphael@users.noreply.github.com> Co-authored-by: neo773 <62795688+neo773@users.noreply.github.com> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Félix Malfait Co-authored-by: Félix Malfait Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions Co-authored-by: MD Readul Islam <99027968+readul-islam@users.noreply.github.com> Co-authored-by: readul-islam Co-authored-by: Thomas des Francs Co-authored-by: Guillim Co-authored-by: Lucas Bordeau --- packages/twenty-front/codegen-metadata.cjs | 1 + .../src/generated-metadata/graphql.ts | 114 ++++++++++ .../twenty-front/src/generated/graphql.ts | 24 ++ .../blocks/components/FileBlock.tsx | 10 +- .../files/components/AttachmentRow.tsx | 4 +- .../apollo/constant/rest-api-base-url.ts | 3 + .../modules/apollo/services/apollo.factory.ts | 8 +- .../components/FileIcon.tsx} | 15 +- .../file/graphql/mutations/createFile.ts | 14 ++ .../file/graphql/mutations/deleteFile.ts | 14 ++ .../utils/__tests__/formatFileSize.test.ts | 23 ++ .../src/modules/file/utils/formatFileSize.ts | 9 + .../api/agent-chat-apollo.api.ts | 1 + .../ai-agent-action/components/AIChatTab.tsx | 56 +++-- .../components/AgentChatFilePreview.tsx | 103 +++++++++ .../components/AgentChatFileUpload.tsx | 127 +++++++++++ .../AgentChatSelectedFilesPreview.tsx | 75 +++++++ .../components/WorkflowEditActionAiAgent.tsx | 2 +- .../ai-agent-action/hooks/useAgentChat.ts | 27 ++- .../hooks/useAgentChatMessages.ts | 2 + .../agentChatSelectedFilesComponentState.ts | 10 + .../agentChatUploadedFilesComponentState.ts | 11 + .../parseAgentStreamingChunk.test.ts | 211 ++++++++++++++++++ .../commands/cron-register-all.command.ts | 6 + .../commands/database-command.module.ts | 2 + .../common/1752207396042-create-file-table.ts | 31 +++ .../cleanup-orphaned-files.cron.command.ts | 32 +++ .../jobs/cleanup-orphaned-files.cron.job.ts | 69 ++++++ .../engine/core-modules/file/dtos/file.dto.ts | 25 +++ .../core-modules/file/entities/file.entity.ts | 53 +++++ .../engine/core-modules/file/file.module.ts | 22 +- .../file/interfaces/file-folder.interface.ts | 4 + .../file/jobs/file-deletion.job.ts | 4 +- .../file/resolvers/file.resolver.ts | 55 +++++ .../file/services/file-metadata.service.ts | 94 ++++++++ .../file/services/file.service.ts | 14 +- .../extract-file-id-from-path.utils.spec.ts | 37 --- .../utils/extract-file-id-from-path.utils.ts | 17 -- .../extract-folderpath-and-filename.utils.ts | 13 ++ .../agent/agent-chat-message.entity.ts | 5 + .../agent/agent-chat.controller.ts | 3 +- .../agent/agent-chat.service.ts | 18 +- .../agent/agent-execution.service.ts | 95 +++++++- .../agent/agent-streaming.service.ts | 36 ++- .../metadata-modules/agent/agent.module.ts | 6 + .../agent/dtos/agent-chat-message.dto.ts | 5 + packages/twenty-server/src/main.ts | 1 + ...workflow-version-step.workspace-service.ts | 12 +- 48 files changed, 1387 insertions(+), 136 deletions(-) create mode 100644 packages/twenty-front/src/modules/apollo/constant/rest-api-base-url.ts rename packages/twenty-front/src/modules/{activities/files/components/AttachmentIcon.tsx => file/components/FileIcon.tsx} (85%) create mode 100644 packages/twenty-front/src/modules/file/graphql/mutations/createFile.ts create mode 100644 packages/twenty-front/src/modules/file/graphql/mutations/deleteFile.ts create mode 100644 packages/twenty-front/src/modules/file/utils/__tests__/formatFileSize.test.ts create mode 100644 packages/twenty-front/src/modules/file/utils/formatFileSize.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AgentChatFilePreview.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AgentChatFileUpload.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AgentChatSelectedFilesPreview.tsx create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/states/agentChatSelectedFilesComponentState.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/states/agentChatUploadedFilesComponentState.ts create mode 100644 packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/utils/__tests__/parseAgentStreamingChunk.test.ts create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1752207396042-create-file-table.ts create mode 100644 packages/twenty-server/src/engine/core-modules/file/crons/commands/cleanup-orphaned-files.cron.command.ts create mode 100644 packages/twenty-server/src/engine/core-modules/file/crons/jobs/cleanup-orphaned-files.cron.job.ts create mode 100644 packages/twenty-server/src/engine/core-modules/file/dtos/file.dto.ts create mode 100644 packages/twenty-server/src/engine/core-modules/file/entities/file.entity.ts create mode 100644 packages/twenty-server/src/engine/core-modules/file/resolvers/file.resolver.ts create mode 100644 packages/twenty-server/src/engine/core-modules/file/services/file-metadata.service.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/file/utils/__tests__/extract-file-id-from-path.utils.spec.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/file/utils/extract-file-id-from-path.utils.ts create mode 100644 packages/twenty-server/src/engine/core-modules/file/utils/extract-folderpath-and-filename.utils.ts diff --git a/packages/twenty-front/codegen-metadata.cjs b/packages/twenty-front/codegen-metadata.cjs index 5f4150073..ee6da2070 100644 --- a/packages/twenty-front/codegen-metadata.cjs +++ b/packages/twenty-front/codegen-metadata.cjs @@ -20,6 +20,7 @@ module.exports = { './src/modules/analytics/graphql/**/*.{ts,tsx}', './src/modules/object-metadata/graphql/**/*.{ts,tsx}', './src/modules/attachments/graphql/**/*.{ts,tsx}', + './src/modules/file/graphql/**/*.{ts,tsx}', './src/modules/onboarding/graphql/**/*.{ts,tsx}', '!./src/**/*.test.{ts,tsx}', diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 3c76b4a93..fb8fa4eaf 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -812,8 +812,20 @@ export type FieldPermissionInput = { objectMetadataId: Scalars['String']; }; +export type File = { + __typename?: 'File'; + createdAt: Scalars['DateTime']; + fullPath: Scalars['String']; + id: Scalars['ID']; + messageId?: Maybe; + name: Scalars['String']; + size: Scalars['Float']; + type: Scalars['String']; +}; + export enum FileFolder { Attachment = 'Attachment', + File = 'File', PersonPicture = 'PersonPicture', ProfilePicture = 'ProfilePicture', ServerlessFunction = 'ServerlessFunction', @@ -1058,6 +1070,7 @@ export type Mutation = { createApprovedAccessDomain: ApprovedAccessDomain; createDatabaseConfigVariable: Scalars['Boolean']; createDraftFromWorkflowVersion: WorkflowVersion; + createFile: File; createOIDCIdentityProvider: SetupSsoOutput; createObjectEvent: Analytics; createOneAppToken: AppToken; @@ -1073,6 +1086,7 @@ export type Mutation = { deleteApprovedAccessDomain: Scalars['Boolean']; deleteCurrentWorkspace: Workspace; deleteDatabaseConfigVariable: Scalars['Boolean']; + deleteFile: File; deleteOneField: Field; deleteOneObject: Object; deleteOneRemoteServer: RemoteServer; @@ -1203,6 +1217,11 @@ export type MutationCreateDraftFromWorkflowVersionArgs = { }; +export type MutationCreateFileArgs = { + file: Scalars['Upload']; +}; + + export type MutationCreateOidcIdentityProviderArgs = { input: SetupOidcSsoInput; }; @@ -1276,6 +1295,11 @@ export type MutationDeleteDatabaseConfigVariableArgs = { }; +export type MutationDeleteFileArgs = { + fileId: Scalars['String']; +}; + + export type MutationDeleteOneFieldArgs = { input: DeleteOneFieldInput; }; @@ -3194,6 +3218,20 @@ export type GetOneDatabaseConnectionQueryVariables = Exact<{ export type GetOneDatabaseConnectionQuery = { __typename?: 'Query', findOneRemoteServerById: { __typename?: 'RemoteServer', id: string, createdAt: string, foreignDataWrapperId: string, foreignDataWrapperOptions?: any | null, foreignDataWrapperType: string, updatedAt: string, schema?: string | null, label: string, userMappingOptions?: { __typename?: 'UserMappingOptionsUser', user?: string | null } | null } }; +export type CreateFileMutationVariables = Exact<{ + file: Scalars['Upload']; +}>; + + +export type CreateFileMutation = { __typename?: 'Mutation', createFile: { __typename?: 'File', id: string, name: string, fullPath: string, size: number, type: string, createdAt: string } }; + +export type DeleteFileMutationVariables = Exact<{ + fileId: Scalars['String']; +}>; + + +export type DeleteFileMutation = { __typename?: 'Mutation', deleteFile: { __typename?: 'File', id: string, name: string, fullPath: string, size: number, type: string, createdAt: string } }; + export type CreateOneObjectMetadataItemMutationVariables = Exact<{ input: CreateOneObjectInput; }>; @@ -5482,6 +5520,82 @@ export function useGetOneDatabaseConnectionLazyQuery(baseOptions?: Apollo.LazyQu export type GetOneDatabaseConnectionQueryHookResult = ReturnType; export type GetOneDatabaseConnectionLazyQueryHookResult = ReturnType; export type GetOneDatabaseConnectionQueryResult = Apollo.QueryResult; +export const CreateFileDocument = gql` + mutation CreateFile($file: Upload!) { + createFile(file: $file) { + id + name + fullPath + size + type + createdAt + } +} + `; +export type CreateFileMutationFn = Apollo.MutationFunction; + +/** + * __useCreateFileMutation__ + * + * To run a mutation, you first call `useCreateFileMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateFileMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [createFileMutation, { data, loading, error }] = useCreateFileMutation({ + * variables: { + * file: // value for 'file' + * }, + * }); + */ +export function useCreateFileMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateFileDocument, options); + } +export type CreateFileMutationHookResult = ReturnType; +export type CreateFileMutationResult = Apollo.MutationResult; +export type CreateFileMutationOptions = Apollo.BaseMutationOptions; +export const DeleteFileDocument = gql` + mutation DeleteFile($fileId: String!) { + deleteFile(fileId: $fileId) { + id + name + fullPath + size + type + createdAt + } +} + `; +export type DeleteFileMutationFn = Apollo.MutationFunction; + +/** + * __useDeleteFileMutation__ + * + * To run a mutation, you first call `useDeleteFileMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useDeleteFileMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [deleteFileMutation, { data, loading, error }] = useDeleteFileMutation({ + * variables: { + * fileId: // value for 'fileId' + * }, + * }); + */ +export function useDeleteFileMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(DeleteFileDocument, options); + } +export type DeleteFileMutationHookResult = ReturnType; +export type DeleteFileMutationResult = Apollo.MutationResult; +export type DeleteFileMutationOptions = Apollo.BaseMutationOptions; export const CreateOneObjectMetadataItemDocument = gql` mutation CreateOneObjectMetadataItem($input: CreateOneObjectInput!) { createOneObject(input: $input) { diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 33cae512d..ba8887689 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -776,8 +776,20 @@ export type FieldPermissionInput = { objectMetadataId: Scalars['String']; }; +export type File = { + __typename?: 'File'; + createdAt: Scalars['DateTime']; + fullPath: Scalars['String']; + id: Scalars['ID']; + messageId?: Maybe; + name: Scalars['String']; + size: Scalars['Float']; + type: Scalars['String']; +}; + export enum FileFolder { Attachment = 'Attachment', + File = 'File', PersonPicture = 'PersonPicture', ProfilePicture = 'ProfilePicture', ServerlessFunction = 'ServerlessFunction', @@ -1015,6 +1027,7 @@ export type Mutation = { createApprovedAccessDomain: ApprovedAccessDomain; createDatabaseConfigVariable: Scalars['Boolean']; createDraftFromWorkflowVersion: WorkflowVersion; + createFile: File; createOIDCIdentityProvider: SetupSsoOutput; createObjectEvent: Analytics; createOneAppToken: AppToken; @@ -1029,6 +1042,7 @@ export type Mutation = { deleteApprovedAccessDomain: Scalars['Boolean']; deleteCurrentWorkspace: Workspace; deleteDatabaseConfigVariable: Scalars['Boolean']; + deleteFile: File; deleteOneField: Field; deleteOneObject: Object; deleteOneRole: Scalars['String']; @@ -1154,6 +1168,11 @@ export type MutationCreateDraftFromWorkflowVersionArgs = { }; +export type MutationCreateFileArgs = { + file: Scalars['Upload']; +}; + + export type MutationCreateOidcIdentityProviderArgs = { input: SetupOidcSsoInput; }; @@ -1212,6 +1231,11 @@ export type MutationDeleteDatabaseConfigVariableArgs = { }; +export type MutationDeleteFileArgs = { + fileId: Scalars['String']; +}; + + export type MutationDeleteOneFieldArgs = { input: DeleteOneFieldInput; }; diff --git a/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx b/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx index 20390712f..d10cfbc20 100644 --- a/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx +++ b/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx @@ -4,11 +4,11 @@ import { isNonEmptyString } from '@sniptt/guards'; import { ChangeEvent, useRef } from 'react'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; -import { AttachmentIcon } from '../../files/components/AttachmentIcon'; -import { AttachmentType } from '../../files/types/Attachment'; -import { getFileType } from '../../files/utils/getFileType'; +import { FileIcon } from '@/file/components/FileIcon'; import { isDefined } from 'twenty-shared/utils'; import { Button } from 'twenty-ui/input'; +import { AttachmentType } from '../../files/types/Attachment'; +import { getFileType } from '../../files/utils/getFileType'; const StyledFileInput = styled.input` display: none; @@ -89,9 +89,7 @@ export const FileBlock = createReactBlockSpec( if (isNonEmptyString(block.props.url)) { return ( - + {block.props.name} diff --git a/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx b/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx index 5f126b43a..88029e022 100644 --- a/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx @@ -1,6 +1,5 @@ import { ActivityRow } from '@/activities/components/ActivityRow'; import { AttachmentDropdown } from '@/activities/files/components/AttachmentDropdown'; -import { AttachmentIcon } from '@/activities/files/components/AttachmentIcon'; import { Attachment } from '@/activities/files/types/Attachment'; import { downloadFile } from '@/activities/files/utils/downloadFile'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; @@ -17,6 +16,7 @@ import { useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; import { PREVIEWABLE_EXTENSIONS } from '@/activities/files/const/previewable-extensions.const'; +import { FileIcon } from '@/file/components/FileIcon'; import { IconCalendar, OverflowingTextWithTooltip } from 'twenty-ui/display'; import { isNavigationModifierPressed } from 'twenty-ui/utilities'; import { formatToHumanReadableDate } from '~/utils/date-utils'; @@ -162,7 +162,7 @@ export const AttachmentRow = ({ > - + {isEditing ? ( extends ApolloClientOptions { isDebugMode?: boolean; } -const REST_API_BASE_URL = `${REACT_APP_SERVER_BASE_URL}/rest`; - export class ApolloFactory implements ApolloManager { private client: ApolloClient; private currentWorkspaceMember: CurrentWorkspaceMember | null = null; @@ -76,7 +74,7 @@ export class ApolloFactory implements ApolloManager { this.currentWorkspace = currentWorkspace; const buildApolloLink = (): ApolloLink => { - const httpLink = createUploadLink({ + const uploadLink = createUploadLink({ uri, }); @@ -252,7 +250,7 @@ export class ApolloFactory implements ApolloManager { retryLink, streamingRestLink, restLink, - httpLink, + uploadLink, ].filter(isDefined), ); }; diff --git a/packages/twenty-front/src/modules/activities/files/components/AttachmentIcon.tsx b/packages/twenty-front/src/modules/file/components/FileIcon.tsx similarity index 85% rename from packages/twenty-front/src/modules/activities/files/components/AttachmentIcon.tsx rename to packages/twenty-front/src/modules/file/components/FileIcon.tsx index cd1bc13e4..4492ac7a6 100644 --- a/packages/twenty-front/src/modules/activities/files/components/AttachmentIcon.tsx +++ b/packages/twenty-front/src/modules/file/components/FileIcon.tsx @@ -1,7 +1,7 @@ +import { AttachmentType } from '@/activities/files/types/Attachment'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { AttachmentType } from '@/activities/files/types/Attachment'; import { IconComponent, IconFile, @@ -21,9 +21,8 @@ const StyledIconContainer = styled.div<{ background: string }>` color: ${({ theme }) => theme.grayScale.gray0}; display: flex; flex-shrink: 0; - height: 20px; justify-content: center; - width: 20px; + padding: ${({ theme }) => theme.spacing(1.25)}; `; const IconMapping: { [key in AttachmentType]: IconComponent } = { @@ -37,11 +36,7 @@ const IconMapping: { [key in AttachmentType]: IconComponent } = { Other: IconFile, }; -export const AttachmentIcon = ({ - attachmentType, -}: { - attachmentType: AttachmentType; -}) => { +export const FileIcon = ({ fileType }: { fileType: AttachmentType }) => { const theme = useTheme(); const IconColors: { [key in AttachmentType]: string } = { @@ -55,10 +50,10 @@ export const AttachmentIcon = ({ Other: theme.color.gray, }; - const Icon = IconMapping[attachmentType]; + const Icon = IconMapping[fileType]; return ( - + {Icon && } ); diff --git a/packages/twenty-front/src/modules/file/graphql/mutations/createFile.ts b/packages/twenty-front/src/modules/file/graphql/mutations/createFile.ts new file mode 100644 index 000000000..8fcfb17c4 --- /dev/null +++ b/packages/twenty-front/src/modules/file/graphql/mutations/createFile.ts @@ -0,0 +1,14 @@ +import { gql } from '@apollo/client'; + +export const CREATE_FILE = gql` + mutation CreateFile($file: Upload!) { + createFile(file: $file) { + id + name + fullPath + size + type + createdAt + } + } +`; diff --git a/packages/twenty-front/src/modules/file/graphql/mutations/deleteFile.ts b/packages/twenty-front/src/modules/file/graphql/mutations/deleteFile.ts new file mode 100644 index 000000000..c663350ec --- /dev/null +++ b/packages/twenty-front/src/modules/file/graphql/mutations/deleteFile.ts @@ -0,0 +1,14 @@ +import { gql } from '@apollo/client'; + +export const DELETE_FILE = gql` + mutation DeleteFile($fileId: String!) { + deleteFile(fileId: $fileId) { + id + name + fullPath + size + type + createdAt + } + } +`; diff --git a/packages/twenty-front/src/modules/file/utils/__tests__/formatFileSize.test.ts b/packages/twenty-front/src/modules/file/utils/__tests__/formatFileSize.test.ts new file mode 100644 index 000000000..e98485766 --- /dev/null +++ b/packages/twenty-front/src/modules/file/utils/__tests__/formatFileSize.test.ts @@ -0,0 +1,23 @@ +import { formatFileSize } from '../formatFileSize'; + +describe('formatFileSize', () => { + it('should format bytes correctly', () => { + expect(formatFileSize(0)).toBe('0 B'); + expect(formatFileSize(512)).toBe('512 B'); + expect(formatFileSize(1024)).toBe('1024 B'); + }); + + it('should format kilobytes correctly', () => { + expect(formatFileSize(1025)).toBe('1.0 KB'); + expect(formatFileSize(2048)).toBe('2.0 KB'); + expect(formatFileSize(1536)).toBe('1.5 KB'); + expect(formatFileSize(1024 * 1024)).toBe('1024.0 KB'); + }); + + it('should format megabytes correctly', () => { + expect(formatFileSize(1024 * 1024 + 1)).toBe('1.0 MB'); + expect(formatFileSize(2 * 1024 * 1024)).toBe('2.0 MB'); + expect(formatFileSize(1.5 * 1024 * 1024)).toBe('1.5 MB'); + expect(formatFileSize(10 * 1024 * 1024)).toBe('10.0 MB'); + }); +}); diff --git a/packages/twenty-front/src/modules/file/utils/formatFileSize.ts b/packages/twenty-front/src/modules/file/utils/formatFileSize.ts new file mode 100644 index 000000000..c42967c21 --- /dev/null +++ b/packages/twenty-front/src/modules/file/utils/formatFileSize.ts @@ -0,0 +1,9 @@ +export const formatFileSize = (size: number) => { + if (size > 1024 * 1024) { + return `${(size / (1024 * 1024)).toFixed(1)} MB`; + } + if (size > 1024) { + return `${(size / 1024).toFixed(1)} KB`; + } + return `${size} B`; +}; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/api/agent-chat-apollo.api.ts b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/api/agent-chat-apollo.api.ts index 8967488cb..daaf40604 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/api/agent-chat-apollo.api.ts +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/api/agent-chat-apollo.api.ts @@ -27,6 +27,7 @@ export const GET_AGENT_CHAT_MESSAGES = gql` role content createdAt + files } } `; diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AIChatTab.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AIChatTab.tsx index 5948ae5d0..181234fe4 100644 --- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AIChatTab.tsx +++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AIChatTab.tsx @@ -1,11 +1,12 @@ import { TextArea } from '@/ui/input/components/TextArea'; import { keyframes, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import React from 'react'; import { Avatar, IconDotsVertical, IconSparkles } from 'twenty-ui/display'; import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; +import { AgentChatFilePreview } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AgentChatFilePreview'; +import { AgentChatFileUpload } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AgentChatFileUpload'; import { AgentChatMessageRole } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/agent-chat-message-role'; import { t } from '@lingui/core/macro'; import { Button } from 'twenty-ui/input'; @@ -13,6 +14,7 @@ import { beautifyPastDateRelativeToNow } from '~/utils/date-utils'; import { useAgentChat } from '../hooks/useAgentChat'; import { AgentChatMessage } from '../hooks/useAgentChatMessages'; import { AIChatSkeletonLoader } from './AIChatSkeletonLoader'; +import { AgentChatSelectedFilesPreview } from './AgentChatSelectedFilesPreview'; const StyledContainer = styled.div` background: ${({ theme }) => theme.background.primary}; @@ -172,11 +174,21 @@ const StyledToolCallContainer = styled.div` } `; -type AIChatTabProps = { - agentId: string; -}; +const StyledButtonsContainer = styled.div` + display: flex; + flex-direction: row; + gap: ${({ theme }) => theme.spacing(2)}; +`; -export const AIChatTab: React.FC = ({ agentId }) => { +const StyledFilesContainer = styled.div` + display: flex; + flex-direction: row; + gap: ${({ theme }) => theme.spacing(2)}; + flex-wrap: wrap; + margin-top: ${({ theme }) => theme.spacing(2)}; +`; + +export const AIChatTab = ({ agentId }: { agentId: string }) => { const theme = useTheme(); const { @@ -186,6 +198,7 @@ export const AIChatTab: React.FC = ({ agentId }) => { input, handleInputChange, agentStreamingMessage, + scrollWrapperId, } = useAgentChat(agentId); const getAssistantMessageContent = (message: AgentChatMessage) => { @@ -215,9 +228,7 @@ export const AIChatTab: React.FC = ({ agentId }) => { return ( {messages.length !== 0 && ( - + {messages.map((msg) => ( = ({ agentId }) => { ? getAssistantMessageContent(msg) : msg.content} + {msg.files.length > 0 && ( + + {msg.files.map((file) => ( + + ))} + + )} {msg.content && ( @@ -282,21 +300,25 @@ export const AIChatTab: React.FC = ({ agentId }) => { {isLoading && messages.length === 0 && } +