LibreChat/client/src/hooks/useChatHelpers.ts
Danny Avila 18edd2660b
👥 fix(assistants): Improve Error handling (#2012)
* feat: make assistants endpoint appendable since message state is not managed by LibreChat

* fix(ask): search currentMessages for thread_id if it's not defined

* refactor(abortMiddleware): remove use of `overrideProps` and spread unknown fields instead

* chore: remove console.log in `abortConversation`

* refactor(assistants): improve error handling/cancellation flow
2024-03-07 10:50:01 -05:00

371 lines
11 KiB
TypeScript

import { v4 } from 'uuid';
import { useCallback, useState } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import {
Constants,
EModelEndpoint,
QueryKeys,
parseCompactConvo,
ContentTypes,
} from 'librechat-data-provider';
import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query';
import type {
TMessage,
TSubmission,
TEndpointOption,
TEndpointsConfig,
} from 'librechat-data-provider';
import type { TAskFunction } from '~/common';
import useSetFilesToDelete from './Files/useSetFilesToDelete';
import useGetSender from './Conversations/useGetSender';
import { useAuthContext } from './AuthContext';
import useUserKey from './Input/useUserKey';
import { getEndpointField } from '~/utils';
import useNewConvo from './useNewConvo';
import store from '~/store';
// this to be set somewhere else
export default function useChatHelpers(index = 0, paramId: string | undefined) {
const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index));
const [files, setFiles] = useRecoilState(store.filesByIndex(index));
const [filesLoading, setFilesLoading] = useState(false);
const setFilesToDelete = useSetFilesToDelete();
const getSender = useGetSender();
const queryClient = useQueryClient();
const { isAuthenticated } = useAuthContext();
const { newConversation } = useNewConvo(index);
const { useCreateConversationAtom } = store;
const { conversation, setConversation } = useCreateConversationAtom(index);
const { conversationId, endpoint } = conversation ?? {};
const queryParam = paramId === 'new' ? paramId : conversationId ?? paramId ?? '';
/* Messages: here simply to fetch, don't export and use `getMessages()` instead */
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { data: _messages } = useGetMessagesByConvoId(conversationId ?? '', {
enabled: isAuthenticated,
});
const resetLatestMessage = useResetRecoilState(store.latestMessageFamily(index));
const [isSubmitting, setIsSubmitting] = useRecoilState(store.isSubmittingFamily(index));
const [latestMessage, setLatestMessage] = useRecoilState(store.latestMessageFamily(index));
const setSiblingIdx = useSetRecoilState(
store.messagesSiblingIdxFamily(latestMessage?.parentMessageId ?? null),
);
const setMessages = useCallback(
(messages: TMessage[]) => {
queryClient.setQueryData<TMessage[]>([QueryKeys.messages, queryParam], messages);
},
// [conversationId, queryClient],
[queryParam, queryClient],
);
const getMessages = useCallback(() => {
return queryClient.getQueryData<TMessage[]>([QueryKeys.messages, queryParam]);
}, [queryParam, queryClient]);
/* Conversation */
// const setActiveConvos = useSetRecoilState(store.activeConversations);
// const setConversation = useCallback(
// (convoUpdate: TConversation) => {
// _setConversation(prev => {
// const { conversationId: convoId } = prev ?? { conversationId: null };
// const { conversationId: currentId } = convoUpdate;
// if (currentId && convoId && convoId !== 'new' && convoId !== currentId) {
// // for now, we delete the prev convoId from activeConversations
// const newActiveConvos = { [currentId]: true };
// setActiveConvos(newActiveConvos);
// }
// return convoUpdate;
// });
// },
// [_setConversation, setActiveConvos],
// );
const { getExpiry } = useUserKey(endpoint ?? '');
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
const ask: TAskFunction = (
{ text, parentMessageId = null, conversationId = null, messageId = null },
{
editedText = null,
editedMessageId = null,
isRegenerate = false,
isContinued = false,
isEdited = false,
} = {},
) => {
setShowStopButton(false);
if (!!isSubmitting || text === '') {
return;
}
if (endpoint === null) {
console.error('No endpoint available');
return;
}
conversationId = conversationId ?? conversation?.conversationId ?? null;
if (conversationId == 'search') {
console.error('cannot send any message under search view!');
return;
}
if (isContinued && !latestMessage) {
console.error('cannot continue AI message without latestMessage!');
return;
}
const isEditOrContinue = isEdited || isContinued;
let currentMessages: TMessage[] | null = getMessages() ?? [];
// construct the query message
// this is not a real messageId, it is used as placeholder before real messageId returned
text = text.trim();
const fakeMessageId = v4();
parentMessageId = parentMessageId || latestMessage?.messageId || Constants.NO_PARENT;
if (conversationId == 'new') {
parentMessageId = Constants.NO_PARENT;
currentMessages = [];
conversationId = null;
}
const parentMessage = currentMessages?.find(
(msg) => msg.messageId === latestMessage?.parentMessageId,
);
let thread_id = parentMessage?.thread_id ?? latestMessage?.thread_id;
if (!thread_id) {
thread_id = currentMessages.find((message) => message.thread_id)?.thread_id;
}
const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]);
const endpointType = getEndpointField(endpointsConfig, endpoint, 'type');
// set the endpoint option
const convo = parseCompactConvo({
endpoint,
endpointType,
conversation: conversation ?? {},
});
const { modelDisplayLabel } = endpointsConfig?.[endpoint ?? ''] ?? {};
const endpointOption = {
...convo,
endpoint,
endpointType,
modelDisplayLabel,
key: getExpiry(),
thread_id,
} as TEndpointOption;
const responseSender = getSender({ model: conversation?.model, ...endpointOption });
const currentMsg: TMessage = {
text,
sender: 'User',
isCreatedByUser: true,
parentMessageId,
conversationId,
messageId: isContinued && messageId ? messageId : fakeMessageId,
thread_id,
error: false,
};
const reuseFiles = isRegenerate && parentMessage?.files;
if (reuseFiles && parentMessage.files?.length) {
currentMsg.files = parentMessage.files;
setFiles(new Map());
setFilesToDelete({});
} else if (files.size > 0) {
currentMsg.files = Array.from(files.values()).map((file) => ({
file_id: file.file_id,
filepath: file.filepath,
type: file.type || '', // Ensure type is not undefined
height: file.height,
width: file.width,
}));
setFiles(new Map());
setFilesToDelete({});
}
// construct the placeholder response message
const generation = editedText ?? latestMessage?.text ?? '';
const responseText = isEditOrContinue ? generation : '';
const responseMessageId = editedMessageId ?? latestMessage?.messageId ?? null;
const initialResponse: TMessage = {
sender: responseSender,
text: responseText,
endpoint: endpoint ?? '',
parentMessageId: isRegenerate ? messageId : fakeMessageId,
messageId: responseMessageId ?? `${isRegenerate ? messageId : fakeMessageId}_`,
thread_id,
conversationId,
unfinished: false,
isCreatedByUser: false,
isEdited: isEditOrContinue,
error: false,
};
if (endpoint === EModelEndpoint.assistants) {
initialResponse.model = conversation?.assistant_id ?? '';
initialResponse.text = '';
initialResponse.content = [
{
type: ContentTypes.TEXT,
[ContentTypes.TEXT]: {
value: responseText,
},
},
];
} else {
setShowStopButton(true);
}
if (isContinued) {
currentMessages = currentMessages.filter((msg) => msg.messageId !== responseMessageId);
}
const submission: TSubmission = {
conversation: {
...conversation,
conversationId,
},
endpointOption,
message: {
...currentMsg,
generation,
responseMessageId,
overrideParentMessageId: isRegenerate ? messageId : null,
},
messages: currentMessages,
isEdited: isEditOrContinue,
isContinued,
isRegenerate,
initialResponse,
};
if (isRegenerate) {
setMessages([...submission.messages, initialResponse]);
} else {
setMessages([...submission.messages, currentMsg, initialResponse]);
}
setLatestMessage(initialResponse);
setSubmission(submission);
};
const regenerate = ({ parentMessageId }) => {
const messages = getMessages();
const parentMessage = messages?.find((element) => element.messageId == parentMessageId);
if (parentMessage && parentMessage.isCreatedByUser) {
ask({ ...parentMessage }, { isRegenerate: true });
} else {
console.error(
'Failed to regenerate the message: parentMessage not found or not created by user.',
);
}
};
const continueGeneration = () => {
if (!latestMessage) {
console.error('Failed to regenerate the message: latestMessage not found.');
return;
}
const messages = getMessages();
const parentMessage = messages?.find(
(element) => element.messageId == latestMessage.parentMessageId,
);
if (parentMessage && parentMessage.isCreatedByUser) {
ask({ ...parentMessage }, { isContinued: true, isRegenerate: true, isEdited: true });
} else {
console.error(
'Failed to regenerate the message: parentMessage not found, or not created by user.',
);
}
};
const stopGenerating = () => {
setSubmission(null);
};
const handleStopGenerating = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
stopGenerating();
};
const handleRegenerate = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
const parentMessageId = latestMessage?.parentMessageId;
if (!parentMessageId) {
console.error('Failed to regenerate the message: parentMessageId not found.');
return;
}
regenerate({ parentMessageId });
};
const handleContinue = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
continueGeneration();
setSiblingIdx(0);
};
const [showBingToneSetting, setShowBingToneSetting] = useRecoilState(
store.showBingToneSettingFamily(index),
);
const [showPopover, setShowPopover] = useRecoilState(store.showPopoverFamily(index));
const [abortScroll, setAbortScroll] = useRecoilState(store.abortScrollFamily(index));
const [preset, setPreset] = useRecoilState(store.presetByIndex(index));
const [optionSettings, setOptionSettings] = useRecoilState(store.optionSettingsFamily(index));
const [showAgentSettings, setShowAgentSettings] = useRecoilState(
store.showAgentSettingsFamily(index),
);
return {
newConversation,
conversation,
setConversation,
// getConvos,
// setConvos,
isSubmitting,
setIsSubmitting,
getMessages,
setMessages,
setSiblingIdx,
latestMessage,
setLatestMessage,
resetLatestMessage,
ask,
index,
regenerate,
stopGenerating,
handleStopGenerating,
handleRegenerate,
handleContinue,
showPopover,
setShowPopover,
abortScroll,
setAbortScroll,
showBingToneSetting,
setShowBingToneSetting,
preset,
setPreset,
optionSettings,
setOptionSettings,
showAgentSettings,
setShowAgentSettings,
files,
setFiles,
filesLoading,
setFilesLoading,
};
}