import { LocalStorageKeys } from 'librechat-data-provider'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import type { UseMutationResult } from '@tanstack/react-query'; import type t from 'librechat-data-provider'; import type { TFile, BatchFile, TFileUpload, TImportStartResponse, AssistantListResponse, UploadMutationOptions, UploadConversationsMutationOptions, DeleteFilesResponse, DeleteFilesBody, DeleteMutationOptions, UpdatePresetOptions, DeletePresetOptions, PresetDeleteResponse, LogoutOptions, TPreset, UploadAvatarOptions, AvatarUploadResponse, TConversation, Assistant, AssistantCreateParams, AssistantUpdateParams, UploadAssistantAvatarOptions, AssistantAvatarVariables, CreateAssistantMutationOptions, UpdateAssistantMutationOptions, DeleteAssistantMutationOptions, DeleteAssistantBody, DeleteConversationOptions, UpdateActionOptions, UpdateActionVariables, UpdateActionResponse, DeleteActionOptions, DeleteActionVariables, Action, } from 'librechat-data-provider'; import { dataService, MutationKeys, QueryKeys, defaultOrderQuery } from 'librechat-data-provider'; import { updateConversation, deleteConversation, updateConvoFields } from '~/utils'; import { useSetRecoilState } from 'recoil'; import store from '~/store'; /** Conversations */ export const useGenTitleMutation = (): UseMutationResult< t.TGenTitleResponse, unknown, t.TGenTitleRequest, unknown > => { const queryClient = useQueryClient(); return useMutation((payload: t.TGenTitleRequest) => dataService.genTitle(payload), { onSuccess: (response, vars) => { queryClient.setQueryData( [QueryKeys.conversation, vars.conversationId], (convo: TConversation | undefined) => { if (!convo) { return convo; } return { ...convo, title: response.title }; }, ); queryClient.setQueryData([QueryKeys.allConversations], (convoData) => { if (!convoData) { return convoData; } return updateConvoFields(convoData, { conversationId: vars.conversationId, title: response.title, } as TConversation); }); document.title = response.title; }, }); }; export const useUpdateConversationMutation = ( id: string, ): UseMutationResult< t.TUpdateConversationResponse, unknown, t.TUpdateConversationRequest, unknown > => { const queryClient = useQueryClient(); return useMutation( (payload: t.TUpdateConversationRequest) => dataService.updateConversation(payload), { onSuccess: (updatedConvo) => { queryClient.setQueryData([QueryKeys.conversation, id], updatedConvo); queryClient.setQueryData([QueryKeys.allConversations], (convoData) => { if (!convoData) { return convoData; } return updateConversation(convoData, updatedConvo); }); }, }, ); }; export const useDeleteConversationMutation = ( options?: DeleteConversationOptions, ): UseMutationResult< t.TDeleteConversationResponse, unknown, t.TDeleteConversationRequest, unknown > => { const queryClient = useQueryClient(); const { onSuccess, ..._options } = options || {}; return useMutation( (payload: t.TDeleteConversationRequest) => dataService.deleteConversation(payload), { onSuccess: (_data, vars, context) => { if (!vars.conversationId) { return; } queryClient.setQueryData([QueryKeys.conversation, vars.conversationId], null); queryClient.setQueryData([QueryKeys.allConversations], (convoData) => { if (!convoData) { return convoData; } const update = deleteConversation(convoData, vars.conversationId as string); return update; }); onSuccess?.(_data, vars, context); }, ...(_options || {}), }, ); }; export const useUploadConversationsMutation = (_options?: UploadConversationsMutationOptions) => { const queryClient = useQueryClient(); const { onSuccess, onError } = _options || {}; // returns the job status or reason of failure const checkJobStatus = async (jobId) => { try { const response = await dataService.queryImportConversationJobStatus(jobId); return response; } catch (error) { throw new Error('Failed to check job status'); } }; // Polls the job status until it is completed, failed, or timed out const pollJobStatus = (jobId, onSuccess, onError) => { let timeElapsed = 0; const timeout = 60000; // Timeout after a minute const pollInterval = 500; // Poll every 500ms const intervalId = setInterval(async () => { try { const statusResponse = await checkJobStatus(jobId); console.log('Polling job status', statusResponse); if (statusResponse.status === 'completed' || statusResponse.status === 'failed') { clearInterval(intervalId); if (statusResponse.status === 'completed') { onSuccess && onSuccess(statusResponse); } else { onError && onError( new Error( statusResponse.failReason ? statusResponse.failReason : 'Failed to import conversations', ), ); } } timeElapsed += pollInterval; // Increment time elapsed by polling interval if (timeElapsed >= timeout) { clearInterval(intervalId); onError && onError(new Error('Polling timed out')); } } catch (error) { clearInterval(intervalId); onError && onError(error); } }, pollInterval); }; return useMutation({ mutationFn: (formData: FormData) => dataService.importConversationsFile(formData), onSuccess: (data, variables, context) => { queryClient.invalidateQueries([QueryKeys.allConversations]); // Assuming the job ID is in the response data const jobId = data.jobId; if (jobId) { // Start polling for job status pollJobStatus( jobId, (statusResponse) => { // This is the final success callback when the job is completed queryClient.invalidateQueries([QueryKeys.allConversations]); // Optionally refresh conversations query if (onSuccess) { onSuccess(statusResponse, variables, context); } }, (error) => { // This is the error callback for job failure or polling errors if (onError) { onError(error, variables, context); } }, ); } }, onError: (err, variables, context) => { if (onError) { onError(err, variables, context); } }, }); }; export const useUploadFileMutation = ( _options?: UploadMutationOptions, ): UseMutationResult< TFileUpload, // response data unknown, // error FormData, // request unknown // context > => { const queryClient = useQueryClient(); const { onSuccess, ...options } = _options || {}; return useMutation([MutationKeys.fileUpload], { mutationFn: (body: FormData) => { const height = body.get('height'); const width = body.get('width'); if (height && width) { return dataService.uploadImage(body); } return dataService.uploadFile(body); }, ...(options || {}), onSuccess: (data, formData, context) => { queryClient.setQueryData([QueryKeys.files], (_files) => [ data, ...(_files ?? []), ]); const assistant_id = formData.get('assistant_id'); const message_file = formData.get('message_file'); if (!assistant_id || message_file === 'true') { onSuccess?.(data, formData, context); return; } queryClient.setQueryData( [QueryKeys.assistants, defaultOrderQuery], (prev) => { if (!prev) { return prev; } return { ...prev, data: prev?.data.map((assistant) => { if (assistant.id === assistant_id) { return { ...assistant, file_ids: [...assistant.file_ids, data.file_id], }; } return assistant; }), }; }, ); onSuccess?.(data, formData, context); }, }); }; export const useDeleteFilesMutation = ( _options?: DeleteMutationOptions, ): UseMutationResult< DeleteFilesResponse, // response data unknown, // error DeleteFilesBody, // request unknown // context > => { const queryClient = useQueryClient(); const { onSuccess, ...options } = _options || {}; return useMutation([MutationKeys.fileDelete], { mutationFn: (body: DeleteFilesBody) => dataService.deleteFiles(body.files, body.assistant_id), ...(options || {}), onSuccess: (data, ...args) => { queryClient.setQueryData([QueryKeys.files], (cachefiles) => { const { files: filesDeleted } = args[0]; const fileMap = filesDeleted.reduce((acc, file) => { acc.set(file.file_id, file); return acc; }, new Map()); return (cachefiles ?? []).filter((file) => !fileMap.has(file.file_id)); }); onSuccess?.(data, ...args); }, }); }; export const useUpdatePresetMutation = ( options?: UpdatePresetOptions, ): UseMutationResult< TPreset, // response data unknown, TPreset, unknown > => { return useMutation([MutationKeys.updatePreset], { mutationFn: (preset: TPreset) => dataService.updatePreset(preset), ...(options || {}), }); }; export const useDeletePresetMutation = ( options?: DeletePresetOptions, ): UseMutationResult< PresetDeleteResponse, // response data unknown, TPreset | undefined, unknown > => { return useMutation([MutationKeys.deletePreset], { mutationFn: (preset: TPreset | undefined) => dataService.deletePreset(preset), ...(options || {}), }); }; /* login/logout */ export const useLogoutUserMutation = ( options?: LogoutOptions, ): UseMutationResult => { const queryClient = useQueryClient(); const setDefaultPreset = useSetRecoilState(store.defaultPreset); return useMutation([MutationKeys.logoutUser], { mutationFn: () => dataService.logout(), ...(options || {}), onSuccess: (...args) => { options?.onSuccess?.(...args); }, onMutate: (...args) => { setDefaultPreset(null); queryClient.removeQueries(); localStorage.removeItem(LocalStorageKeys.LAST_CONVO_SETUP); localStorage.removeItem(LocalStorageKeys.LAST_MODEL); localStorage.removeItem(LocalStorageKeys.LAST_TOOLS); localStorage.removeItem(LocalStorageKeys.FILES_TO_DELETE); // localStorage.removeItem('lastAssistant'); options?.onMutate?.(...args); }, }); }; /* Avatar upload */ export const useUploadAvatarMutation = ( options?: UploadAvatarOptions, ): UseMutationResult< AvatarUploadResponse, // response data unknown, // error FormData, // request unknown // context > => { return useMutation([MutationKeys.avatarUpload], { mutationFn: (variables: FormData) => dataService.uploadAvatar(variables), ...(options || {}), }); }; /** * ASSISTANTS */ /** * Create a new assistant */ export const useCreateAssistantMutation = ( options?: CreateAssistantMutationOptions, ): UseMutationResult => { const queryClient = useQueryClient(); return useMutation( (newAssistantData: AssistantCreateParams) => dataService.createAssistant(newAssistantData), { onMutate: (variables) => options?.onMutate?.(variables), onError: (error, variables, context) => options?.onError?.(error, variables, context), onSuccess: (newAssistant, variables, context) => { const listRes = queryClient.getQueryData([ QueryKeys.assistants, defaultOrderQuery, ]); if (!listRes) { return options?.onSuccess?.(newAssistant, variables, context); } const currentAssistants = [newAssistant, ...JSON.parse(JSON.stringify(listRes.data))]; queryClient.setQueryData([QueryKeys.assistants, defaultOrderQuery], { ...listRes, data: currentAssistants, }); return options?.onSuccess?.(newAssistant, variables, context); }, }, ); }; /** * Hook for updating an assistant */ export const useUpdateAssistantMutation = ( options?: UpdateAssistantMutationOptions, ): UseMutationResult => { const queryClient = useQueryClient(); return useMutation( ({ assistant_id, data }: { assistant_id: string; data: AssistantUpdateParams }) => dataService.updateAssistant(assistant_id, data), { onMutate: (variables) => options?.onMutate?.(variables), onError: (error, variables, context) => options?.onError?.(error, variables, context), onSuccess: (updatedAssistant, variables, context) => { const listRes = queryClient.getQueryData([ QueryKeys.assistants, defaultOrderQuery, ]); if (!listRes) { return options?.onSuccess?.(updatedAssistant, variables, context); } queryClient.setQueryData([QueryKeys.assistants, defaultOrderQuery], { ...listRes, data: listRes.data.map((assistant) => { if (assistant.id === variables.assistant_id) { return updatedAssistant; } return assistant; }), }); return options?.onSuccess?.(updatedAssistant, variables, context); }, }, ); }; /** * Hook for deleting an assistant */ export const useDeleteAssistantMutation = ( options?: DeleteAssistantMutationOptions, ): UseMutationResult => { const queryClient = useQueryClient(); return useMutation( ({ assistant_id, model }: DeleteAssistantBody) => dataService.deleteAssistant(assistant_id, model), { onMutate: (variables) => options?.onMutate?.(variables), onError: (error, variables, context) => options?.onError?.(error, variables, context), onSuccess: (_data, variables, context) => { const listRes = queryClient.getQueryData([ QueryKeys.assistants, defaultOrderQuery, ]); if (!listRes) { return options?.onSuccess?.(_data, variables, context); } const data = listRes.data.filter((assistant) => assistant.id !== variables.assistant_id); queryClient.setQueryData([QueryKeys.assistants, defaultOrderQuery], { ...listRes, data, }); return options?.onSuccess?.(_data, variables, data); }, }, ); }; /** * Hook for uploading an assistant avatar */ export const useUploadAssistantAvatarMutation = ( options?: UploadAssistantAvatarOptions, ): UseMutationResult< Assistant, // response data unknown, // error AssistantAvatarVariables, // request unknown // context > => { return useMutation([MutationKeys.assistantAvatarUpload], { // eslint-disable-next-line @typescript-eslint/no-unused-vars mutationFn: ({ postCreation, ...variables }: AssistantAvatarVariables) => dataService.uploadAssistantAvatar(variables), ...(options || {}), }); }; /** * Hook for updating Assistant Actions */ export const useUpdateAction = ( options?: UpdateActionOptions, ): UseMutationResult< UpdateActionResponse, // response data unknown, // error UpdateActionVariables, // request unknown // context > => { const queryClient = useQueryClient(); return useMutation([MutationKeys.updateAction], { mutationFn: (variables: UpdateActionVariables) => dataService.updateAction(variables), onMutate: (variables) => options?.onMutate?.(variables), onError: (error, variables, context) => options?.onError?.(error, variables, context), onSuccess: (updateActionResponse, variables, context) => { const listRes = queryClient.getQueryData([ QueryKeys.assistants, defaultOrderQuery, ]); if (!listRes) { return options?.onSuccess?.(updateActionResponse, variables, context); } const updatedAssistant = updateActionResponse[1]; queryClient.setQueryData([QueryKeys.assistants, defaultOrderQuery], { ...listRes, data: listRes.data.map((assistant) => { if (assistant.id === variables.assistant_id) { return updatedAssistant; } return assistant; }), }); queryClient.setQueryData([QueryKeys.actions], (prev) => { return prev ?.map((action) => { if (action.action_id === variables.action_id) { return updateActionResponse[2]; } return action; }) .concat(variables.action_id ? [] : [updateActionResponse[2]]); }); return options?.onSuccess?.(updateActionResponse, variables, context); }, }); }; /** * Hook for deleting an Assistant Action */ export const useDeleteAction = ( options?: DeleteActionOptions, ): UseMutationResult< void, // response data for a delete operation is typically void Error, // error type DeleteActionVariables, // request variables unknown // context > => { const queryClient = useQueryClient(); return useMutation([MutationKeys.deleteAction], { mutationFn: (variables: DeleteActionVariables) => dataService.deleteAction(variables.assistant_id, variables.action_id, variables.model), onMutate: (variables) => options?.onMutate?.(variables), onError: (error, variables, context) => options?.onError?.(error, variables, context), onSuccess: (_data, variables, context) => { let domain: string | undefined = ''; queryClient.setQueryData([QueryKeys.actions], (prev) => { return prev?.filter((action) => { domain = action.metadata.domain; return action.action_id !== variables.action_id; }); }); queryClient.setQueryData( [QueryKeys.assistants, defaultOrderQuery], (prev) => { if (!prev) { return prev; } return { ...prev, data: prev?.data.map((assistant) => { if (assistant.id === variables.assistant_id) { return { ...assistant, tools: assistant.tools.filter( (tool) => !tool.function?.name.includes(domain ?? ''), ), }; } return assistant; }), }; }, ); return options?.onSuccess?.(_data, variables, context); }, }); };