Files
twenty/packages/twenty-front/src/modules/ai/hooks/useAIChatFileUpload.ts
T
0648dc5c65 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>
2025-10-03 12:37:41 +02:00

97 lines
2.9 KiB
TypeScript

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 { 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 = () => {
const coreClient = useApolloCoreClient();
const [uploadFile] = useUploadFileMutation({ client: coreClient });
const { t } = useLingui();
const { enqueueErrorSnackBar } = useSnackBar();
const [agentChatSelectedFiles, setAgentChatSelectedFiles] = useRecoilState(
agentChatSelectedFilesState,
);
const [agentChatUploadedFiles, setAgentChatUploadedFiles] = useRecoilState(
agentChatUploadedFilesState,
);
const sendFile = async (file: File): Promise<FileUIPart | null> => {
try {
const result = await uploadFile({
variables: {
file,
fileFolder: FileFolder.AgentChat,
},
});
const response = result?.data?.uploadFile;
if (!isDefined(response)) {
throw new Error(t`Couldn't upload the file.`);
}
const signedPath = buildSignedPath({
path: response.path,
token: response.token,
});
setAgentChatSelectedFiles(
agentChatSelectedFiles.filter((f) => f.name !== file.name),
);
return {
filename: file.name,
mediaType: file.type,
url: `${REACT_APP_SERVER_BASE_URL}/files/${signedPath}`,
type: 'file',
};
} catch {
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<FileUIPart[]>(
(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 };
};