diff --git a/packages/twenty-front/src/modules/ai/components/AIChatEmptyState.tsx b/packages/twenty-front/src/modules/ai/components/AIChatEmptyState.tsx
new file mode 100644
index 000000000..87f51f4aa
--- /dev/null
+++ b/packages/twenty-front/src/modules/ai/components/AIChatEmptyState.tsx
@@ -0,0 +1,51 @@
+import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+import { t } from '@lingui/core/macro';
+import { IconSparkles } from 'twenty-ui/display';
+const StyledEmptyState = styled.div`
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ gap: ${({ theme }) => theme.spacing(2)};
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+`;
+
+const StyledSparkleIcon = styled.div`
+ align-items: center;
+ background: ${({ theme }) => theme.background.transparent.blue};
+ border-radius: ${({ theme }) => theme.border.radius.sm};
+ padding: ${({ theme }) => theme.spacing(2.5)};
+ display: flex;
+ justify-content: center;
+ margin-bottom: ${({ theme }) => theme.spacing(2)};
+`;
+
+const StyledTitle = styled.div`
+ font-size: ${({ theme }) => theme.font.size.lg};
+ font-weight: 600;
+`;
+
+const StyledDescription = styled.div`
+ color: ${({ theme }) => theme.font.color.secondary};
+ text-align: center;
+ max-width: 85%;
+ font-size: ${({ theme }) => theme.font.size.md};
+`;
+
+export const AIChatEmptyState = () => {
+ const theme = useTheme();
+
+ return (
+
+
+
+
+ {t`Chat`}
+
+ {t`Start a conversation with your AI agent to get workflow insights, task assistance, and process guidance`}
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx b/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx
new file mode 100644
index 000000000..05f894bb7
--- /dev/null
+++ b/packages/twenty-front/src/modules/ai/components/AIChatMessage.tsx
@@ -0,0 +1,204 @@
+import { keyframes, useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+import { Avatar, IconDotsVertical, IconSparkles } from 'twenty-ui/display';
+
+import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton';
+import { AgentChatFilePreview } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AgentChatFilePreview';
+import { AgentChatMessageRole } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/constants/agent-chat-message-role';
+
+import { AgentChatMessage } from '~/generated/graphql';
+import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
+
+const StyledMessageBubble = styled.div<{ isUser?: boolean }>`
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ position: relative;
+ width: 100%;
+
+ &:hover .message-footer {
+ opacity: 1;
+ pointer-events: auto;
+ }
+`;
+
+const StyledMessageRow = styled.div<{ isShowingToolCall?: boolean }>`
+ display: flex;
+ flex-direction: row;
+ align-items: ${({ isShowingToolCall }) =>
+ isShowingToolCall ? 'center' : 'flex-start'};
+ gap: ${({ theme }) => theme.spacing(3)};
+ width: 100%;
+`;
+
+const StyledMessageText = styled.div<{ isUser?: boolean }>`
+ background: ${({ theme, isUser }) =>
+ isUser ? theme.background.secondary : theme.background.transparent};
+ border-radius: ${({ theme }) => theme.border.radius.md};
+ padding: ${({ theme, isUser }) => (isUser ? theme.spacing(1, 2) : 0)};
+ border: ${({ isUser, theme }) =>
+ !isUser ? 'none' : `1px solid ${theme.border.color.light}`};
+ color: ${({ theme, isUser }) =>
+ isUser ? theme.font.color.light : theme.font.color.primary};
+ font-weight: ${({ isUser }) => (isUser ? 500 : 400)};
+ width: fit-content;
+ white-space: pre-line;
+`;
+
+const StyledMessageFooter = styled.div`
+ align-items: center;
+ color: ${({ theme }) => theme.font.color.secondary};
+ display: flex;
+ font-size: ${({ theme }) => theme.font.size.sm};
+ justify-content: space-between;
+ margin-top: ${({ theme }) => theme.spacing(1)};
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.3s ease-in-out;
+ width: 100%;
+`;
+
+const StyledAvatarContainer = styled.div<{ isUser?: boolean }>`
+ align-items: center;
+ background: ${({ theme, isUser }) =>
+ isUser
+ ? theme.background.transparent.light
+ : theme.background.transparent.blue};
+ display: flex;
+ justify-content: center;
+ height: 24px;
+ min-width: 24px;
+ border-radius: ${({ theme }) => theme.border.radius.sm};
+ padding: 1px;
+`;
+
+const StyledMessageContainer = styled.div`
+ width: 100%;
+`;
+
+const StyledFilesContainer = styled.div`
+ display: flex;
+ flex-direction: row;
+ gap: ${({ theme }) => theme.spacing(2)};
+ flex-wrap: wrap;
+ margin-top: ${({ theme }) => theme.spacing(2)};
+`;
+
+const dots = keyframes`
+ 0% { content: ''; }
+ 33% { content: '.'; }
+ 66% { content: '..'; }
+ 100% { content: '...'; }
+`;
+
+const StyledToolCallContainer = styled.div`
+ &::after {
+ display: inline-block;
+ content: '';
+ animation: ${dots} 750ms steps(3, end) infinite;
+ width: 2ch;
+ text-align: left;
+ }
+`;
+
+const StyledDotsIconContainer = styled.div`
+ align-items: center;
+ border: ${({ theme }) => `1px solid ${theme.border.color.light}`};
+ border-radius: ${({ theme }) => theme.border.radius.md};
+ display: flex;
+ justify-content: center;
+ padding-inline: ${({ theme }) => theme.spacing(1)};
+`;
+
+const StyledDotsIcon = styled(IconDotsVertical)`
+ color: ${({ theme }) => theme.font.color.light};
+ transform: rotate(90deg);
+`;
+
+export const AIChatMessage = ({
+ message,
+ agentStreamingMessage,
+}: {
+ message: AgentChatMessage;
+ agentStreamingMessage: { streamingText: string; toolCall: string };
+}) => {
+ const theme = useTheme();
+
+ const getAssistantMessageContent = (message: AgentChatMessage) => {
+ if (message.content !== '') {
+ return message.content;
+ }
+
+ if (agentStreamingMessage.streamingText !== '') {
+ return agentStreamingMessage.streamingText;
+ }
+
+ if (agentStreamingMessage.toolCall !== '') {
+ return (
+
+ {agentStreamingMessage.toolCall}
+
+ );
+ }
+
+ return (
+
+
+
+ );
+ };
+
+ return (
+
+
+ {message.role === AgentChatMessageRole.ASSISTANT && (
+
+
+
+ )}
+ {message.role === AgentChatMessageRole.USER && (
+
+
+
+ )}
+
+
+ {message.role === AgentChatMessageRole.ASSISTANT
+ ? getAssistantMessageContent(message)
+ : message.content}
+
+ {message.files.length > 0 && (
+
+ {message.files.map((file) => (
+
+ ))}
+
+ )}
+ {message.content && (
+
+ {beautifyPastDateRelativeToNow(message.createdAt)}
+
+
+ )}
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/ai/hooks/useAIChatFileUpload.ts b/packages/twenty-front/src/modules/ai/hooks/useAIChatFileUpload.ts
new file mode 100644
index 000000000..dd9de38f8
--- /dev/null
+++ b/packages/twenty-front/src/modules/ai/hooks/useAIChatFileUpload.ts
@@ -0,0 +1,82 @@
+import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
+import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
+import { agentChatSelectedFilesComponentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/agentChatSelectedFilesComponentState';
+import { agentChatUploadedFilesComponentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/agentChatUploadedFilesComponentState';
+import { useLingui } from '@lingui/react/macro';
+import { isDefined } from 'twenty-shared/utils';
+import {
+ File as FileDocument,
+ useCreateFileMutation,
+} from '~/generated-metadata/graphql';
+
+export const useAIChatFileUpload = ({ agentId }: { agentId: string }) => {
+ const coreClient = useApolloCoreClient();
+ const [createFile] = useCreateFileMutation({ client: coreClient });
+ const { t } = useLingui();
+ const { enqueueErrorSnackBar } = useSnackBar();
+ const [agentChatSelectedFiles, setAgentChatSelectedFiles] =
+ useRecoilComponentStateV2(agentChatSelectedFilesComponentState, agentId);
+ const [agentChatUploadedFiles, setAgentChatUploadedFiles] =
+ useRecoilComponentStateV2(agentChatUploadedFilesComponentState, agentId);
+
+ const sendFile = async (file: File) => {
+ try {
+ const result = await createFile({
+ variables: {
+ file,
+ },
+ });
+
+ const uploadedFile = result?.data?.createFile;
+
+ if (!isDefined(uploadedFile)) {
+ throw new Error(t`Couldn't upload the file.`);
+ }
+ setAgentChatSelectedFiles(
+ agentChatSelectedFiles.filter((f) => f.name !== file.name),
+ );
+ return uploadedFile;
+ } catch (error) {
+ const fileName = file.name;
+ enqueueErrorSnackBar({
+ message: t`Failed to upload file: ${fileName}`,
+ });
+ return null;
+ }
+ };
+
+ const uploadFiles = async (files: File[]) => {
+ const uploadResults = await Promise.allSettled(
+ files.map((file) => sendFile(file)),
+ );
+
+ const successfulUploads = uploadResults.reduce(
+ (acc, result) => {
+ if (result.status === 'fulfilled' && isDefined(result.value)) {
+ acc.push(result.value);
+ }
+ return acc;
+ },
+ [],
+ );
+
+ if (successfulUploads.length > 0) {
+ setAgentChatUploadedFiles([
+ ...agentChatUploadedFiles,
+ ...successfulUploads,
+ ]);
+ }
+
+ const failedCount = uploadResults.filter(
+ (result) => result.status === 'rejected',
+ ).length;
+ if (failedCount > 0) {
+ enqueueErrorSnackBar({
+ message: t`${failedCount} file(s) failed to upload`,
+ });
+ }
+ };
+
+ return { uploadFiles };
+};
diff --git a/packages/twenty-front/src/modules/ai/utils/__tests__/groupThreadsByDate.test.ts b/packages/twenty-front/src/modules/ai/utils/__tests__/groupThreadsByDate.test.ts
new file mode 100644
index 000000000..d50b95ef2
--- /dev/null
+++ b/packages/twenty-front/src/modules/ai/utils/__tests__/groupThreadsByDate.test.ts
@@ -0,0 +1,39 @@
+import { AgentChatThread } from '~/generated-metadata/graphql';
+import { groupThreadsByDate } from '../groupThreadsByDate';
+
+describe('groupThreadsByDate', () => {
+ const baseThread: Omit = {
+ agentId: 'agent-1',
+ title: 'Test Thread',
+ updatedAt: new Date().toISOString(),
+ };
+
+ const today = new Date();
+ const yesterday = new Date(today);
+ yesterday.setDate(today.getDate() - 1);
+ const twoDaysAgo = new Date(today);
+ twoDaysAgo.setDate(today.getDate() - 2);
+
+ const threads: AgentChatThread[] = [
+ { ...baseThread, id: '1', createdAt: today.toISOString() },
+ { ...baseThread, id: '2', createdAt: yesterday.toISOString() },
+ { ...baseThread, id: '3', createdAt: twoDaysAgo.toISOString() },
+ ];
+
+ it('groups threads into today, yesterday, and older', () => {
+ const result = groupThreadsByDate(threads);
+ expect(result.today).toHaveLength(1);
+ expect(result.today[0].id).toBe('1');
+ expect(result.yesterday).toHaveLength(1);
+ expect(result.yesterday[0].id).toBe('2');
+ expect(result.older).toHaveLength(1);
+ expect(result.older[0].id).toBe('3');
+ });
+
+ it('returns empty arrays if no threads', () => {
+ const result = groupThreadsByDate([]);
+ expect(result.today).toEqual([]);
+ expect(result.yesterday).toEqual([]);
+ expect(result.older).toEqual([]);
+ });
+});
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 12ef8beb1..ca3d5aac4 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,77 +1,39 @@
import { TextArea } from '@/ui/input/components/TextArea';
-import { keyframes, useTheme } from '@emotion/react';
import styled from '@emotion/styled';
-import {
- Avatar,
- IconDotsVertical,
- IconHistory,
- IconMessageCirclePlus,
- IconSparkles,
-} from 'twenty-ui/display';
+import { IconHistory, IconMessageCirclePlus } from 'twenty-ui/display';
+import { DropZone } from '@/activities/files/components/DropZone';
import { useCreateNewAIChatThread } from '@/ai/hooks/useCreateNewAIChatThread';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
-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 { AIChatEmptyState } from '@/ai/components/AIChatEmptyState';
+import { AIChatMessage } from '@/ai/components/AIChatMessage';
+import { useAIChatFileUpload } from '@/ai/hooks/useAIChatFileUpload';
import { t } from '@lingui/core/macro';
+import { useState } from 'react';
import { Button } from 'twenty-ui/input';
-import { AgentChatMessage } from '~/generated/graphql';
-import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
import { useAgentChat } from '../hooks/useAgentChat';
import { AIChatSkeletonLoader } from './AIChatSkeletonLoader';
import { AgentChatSelectedFilesPreview } from './AgentChatSelectedFilesPreview';
-const StyledContainer = styled.div`
+const StyledContainer = styled.div<{ isDraggingFile: boolean }>`
background: ${({ theme }) => theme.background.primary};
- height: calc(100% - 154px);
-`;
-
-const StyledEmptyState = styled.div`
+ height: ${({ isDraggingFile }) =>
+ isDraggingFile ? `calc(100% - 24px)` : '100%'};
+ padding: ${({ isDraggingFile, theme }) =>
+ isDraggingFile ? theme.spacing(3) : '0'};
display: flex;
flex-direction: column;
- flex: 1;
- gap: ${({ theme }) => theme.spacing(2)};
- align-items: center;
- justify-content: center;
- height: 100%;
-`;
-
-const StyledSparkleIcon = styled.div`
- align-items: center;
- background: ${({ theme }) => theme.background.transparent.blue};
- border-radius: ${({ theme }) => theme.border.radius.sm};
- display: flex;
- height: 40px;
- justify-content: center;
- margin-bottom: ${({ theme }) => theme.spacing(2)};
- width: 40px;
-`;
-
-const StyledTitle = styled.div`
- font-size: ${({ theme }) => theme.font.size.lg};
- font-weight: 600;
-`;
-
-const StyledDescription = styled.div`
- color: ${({ theme }) => theme.font.color.secondary};
- text-align: center;
- max-width: 85%;
- font-size: ${({ theme }) => theme.font.size.md};
`;
const StyledInputArea = styled.div`
align-items: flex-end;
- bottom: 0;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(2)};
- position: absolute;
- width: calc(100% - 24px);
padding: ${({ theme }) => theme.spacing(3)};
background: ${({ theme }) => theme.background.primary};
`;
@@ -86,118 +48,12 @@ const StyledScrollWrapper = styled(ScrollWrapper)`
width: calc(100% - 24px);
`;
-const StyledMessageBubble = styled.div<{ isUser?: boolean }>`
- display: flex;
- flex-direction: column;
- align-items: flex-start;
- position: relative;
- width: 100%;
-
- &:hover .message-footer {
- opacity: 1;
- pointer-events: auto;
- }
-`;
-
-const StyledMessageRow = styled.div<{ isShowingToolCall?: boolean }>`
- display: flex;
- flex-direction: row;
- align-items: ${({ isShowingToolCall }) =>
- isShowingToolCall ? 'center' : 'flex-start'};
- gap: ${({ theme }) => theme.spacing(3)};
- width: 100%;
-`;
-
-const StyledMessageText = styled.div<{ isUser?: boolean }>`
- background: ${({ theme, isUser }) =>
- isUser ? theme.background.secondary : theme.background.transparent};
- border-radius: ${({ theme }) => theme.border.radius.md};
- padding: ${({ theme, isUser }) => (isUser ? theme.spacing(1, 2) : 0)};
- border: ${({ isUser, theme }) =>
- !isUser ? 'none' : `1px solid ${theme.border.color.light}`};
- color: ${({ theme, isUser }) =>
- isUser ? theme.font.color.light : theme.font.color.primary};
- font-weight: ${({ isUser }) => (isUser ? 500 : 400)};
- width: fit-content;
- white-space: pre-line;
-`;
-
-const StyledMessageFooter = styled.div`
- align-items: center;
- color: ${({ theme }) => theme.font.color.secondary};
- display: flex;
- font-size: ${({ theme }) => theme.font.size.sm};
- justify-content: space-between;
- margin-top: ${({ theme }) => theme.spacing(1)};
- opacity: 0;
- pointer-events: none;
- transition: opacity 0.3s ease-in-out;
- width: 100%;
-`;
-
-const StyledAvatarContainer = styled.div<{ isUser?: boolean }>`
- align-items: center;
- background: ${({ theme, isUser }) =>
- isUser
- ? theme.background.transparent.light
- : theme.background.transparent.blue};
- display: flex;
- justify-content: center;
- height: 24px;
- min-width: 24px;
- border-radius: ${({ theme }) => theme.border.radius.sm};
- padding: 1px;
-`;
-
-const StyledDotsIconContainer = styled.div`
- align-items: center;
- border: ${({ theme }) => `1px solid ${theme.border.color.light}`};
- border-radius: ${({ theme }) => theme.border.radius.md};
- display: flex;
- justify-content: center;
- padding-inline: ${({ theme }) => theme.spacing(1)};
-`;
-
-const StyledDotsIcon = styled(IconDotsVertical)`
- color: ${({ theme }) => theme.font.color.light};
- transform: rotate(90deg);
-`;
-
-const StyledMessageContainer = styled.div`
- width: 100%;
-`;
-
-const dots = keyframes`
- 0% { content: ''; }
- 33% { content: '.'; }
- 66% { content: '..'; }
- 100% { content: '...'; }
-`;
-
-const StyledToolCallContainer = styled.div`
- &::after {
- display: inline-block;
- content: '';
- animation: ${dots} 750ms steps(3, end) infinite;
- width: 2ch;
- text-align: left;
- }
-`;
-
const StyledButtonsContainer = styled.div`
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
`;
-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,
isWorkflowAgentNodeChat,
@@ -205,7 +61,7 @@ export const AIChatTab = ({
agentId: string;
isWorkflowAgentNodeChat?: boolean;
}) => {
- const theme = useTheme();
+ const [isDraggingFile, setIsDraggingFile] = useState(false);
const {
messages,
@@ -216,151 +72,83 @@ export const AIChatTab = ({
agentStreamingMessage,
scrollWrapperId,
} = useAgentChat(agentId);
+ const { uploadFiles } = useAIChatFileUpload({ agentId });
const { createAgentChatThread } = useCreateNewAIChatThread({ agentId });
const { navigateCommandMenu } = useCommandMenu();
- const getAssistantMessageContent = (message: AgentChatMessage) => {
- if (message.content !== '') {
- return message.content;
- }
-
- if (agentStreamingMessage.streamingText !== '') {
- return agentStreamingMessage.streamingText;
- }
-
- if (agentStreamingMessage.toolCall !== '') {
- return (
-
- {agentStreamingMessage.toolCall}
-
- );
- }
-
- return (
-
-
-
- );
- };
-
return (
-
- {messages.length !== 0 && (
-
- {messages.map((msg) => (
-
-
- {msg.role === AgentChatMessageRole.ASSISTANT && (
-
-
-
- )}
- {msg.role === AgentChatMessageRole.USER && (
-
-
-
- )}
-
-
- {msg.role === AgentChatMessageRole.ASSISTANT
- ? getAssistantMessageContent(msg)
- : msg.content}
-
- {msg.files.length > 0 && (
-
- {msg.files.map((file) => (
-
- ))}
-
- )}
- {msg.content && (
-
-
- {beautifyPastDateRelativeToNow(msg.createdAt)}
-
-
-
- )}
-
-
-
- ))}
-
- )}
- {messages.length === 0 && !isLoading && (
-
-
-
-
- {t`Chat`}
-
- {t`Start a conversation with your AI agent to get workflow insights, task assistance, and process guidance`}
-
-
- )}
- {isLoading && messages.length === 0 && }
-
-
-
-
+ {messages.length === 0 && !isLoading && }
+ {isLoading && messages.length === 0 && }
+
+
+
+
+
+ {!isWorkflowAgentNodeChat && (
+ <>
+
+
+ >
+ )}
);
};
diff --git a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AgentChatFileUpload.tsx b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AgentChatFileUpload.tsx
index d40d60b4b..67142e4f7 100644
--- a/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AgentChatFileUpload.tsx
+++ b/packages/twenty-front/src/modules/workflow/workflow-steps/workflow-actions/ai-agent-action/components/AgentChatFileUpload.tsx
@@ -1,18 +1,10 @@
-import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
-import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+import { useAIChatFileUpload } from '@/ai/hooks/useAIChatFileUpload';
import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2';
import { agentChatSelectedFilesComponentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/agentChatSelectedFilesComponentState';
-import { agentChatUploadedFilesComponentState } from '@/workflow/workflow-steps/workflow-actions/ai-agent-action/states/agentChatUploadedFilesComponentState';
import styled from '@emotion/styled';
-import { useLingui } from '@lingui/react/macro';
import React, { useRef } from 'react';
-import { isDefined } from 'twenty-shared/utils';
import { IconPaperclip } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
-import {
- File as FileDocument,
- useCreateFileMutation,
-} from '~/generated-metadata/graphql';
const StyledFileUploadContainer = styled.div`
display: flex;
@@ -25,73 +17,12 @@ const StyledFileInput = styled.input`
`;
export const AgentChatFileUpload = ({ agentId }: { agentId: string }) => {
- const coreClient = useApolloCoreClient();
- const [createFile] = useCreateFileMutation({ client: coreClient });
- const { t } = useLingui();
- const { enqueueErrorSnackBar } = useSnackBar();
- const [agentChatSelectedFiles, setAgentChatSelectedFiles] =
- useRecoilComponentStateV2(agentChatSelectedFilesComponentState, agentId);
- const [agentChatUploadedFiles, setAgentChatUploadedFiles] =
- useRecoilComponentStateV2(agentChatUploadedFilesComponentState, agentId);
+ const [, setAgentChatSelectedFiles] = useRecoilComponentStateV2(
+ agentChatSelectedFilesComponentState,
+ agentId,
+ );
const fileInputRef = useRef(null);
-
- const sendFile = async (file: File) => {
- try {
- const result = await createFile({
- variables: {
- file,
- },
- });
-
- const uploadedFile = result?.data?.createFile;
-
- if (!isDefined(uploadedFile)) {
- throw new Error("Couldn't upload the file.");
- }
- setAgentChatSelectedFiles(
- agentChatSelectedFiles.filter((f) => f.name !== file.name),
- );
- return uploadedFile;
- } catch (error) {
- const fileName = file.name;
- enqueueErrorSnackBar({
- message: t`Failed to upload file: ${fileName}`,
- });
- return null;
- }
- };
-
- const uploadFiles = async (files: File[]) => {
- const uploadResults = await Promise.allSettled(
- files.map((file) => sendFile(file)),
- );
-
- const successfulUploads = uploadResults.reduce(
- (acc, result) => {
- if (result.status === 'fulfilled' && isDefined(result.value)) {
- acc.push(result.value);
- }
- return acc;
- },
- [],
- );
-
- if (successfulUploads.length > 0) {
- setAgentChatUploadedFiles([
- ...agentChatUploadedFiles,
- ...successfulUploads,
- ]);
- }
-
- const failedCount = uploadResults.filter(
- (result) => result.status === 'rejected',
- ).length;
- if (failedCount > 0) {
- enqueueErrorSnackBar({
- message: t`${failedCount} file(s) failed to upload`,
- });
- }
- };
+ const { uploadFiles } = useAIChatFileUpload({ agentId });
const handleFileInputChange = (
event: React.ChangeEvent,