mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
* chore: bump vite, vitejs/plugin-react, mark client package as esm, move react-query as a peer dep in data-provider * chore: import changes due to new data-provider export strategy, also fix type imports where applicable * chore: export react-query services as separate to avoid react dependencies in /api/ * chore: suppress sourcemap warnings and polyfill node:path which is used by filenamify TODO: replace filenamify with an alternative and REMOVE polyfill * chore: /api/ changes to support `librechat-data-provider` * refactor: rewrite Dockerfile.multi in light of /api/ changes to support `librechat-data-provider` * chore: remove volume mapping to node_modules directories in default compose file * chore: remove schemas from /api/ as is no longer needed with use of `librechat-data-provider` * fix(ci): jest `librechat-data-provider/react-query` module resolution
370 lines
12 KiB
TypeScript
370 lines
12 KiB
TypeScript
import { v4 } from 'uuid';
|
|
import { useCallback, useState } from 'react';
|
|
import { useQueryClient } from '@tanstack/react-query';
|
|
import { useRecoilState, useResetRecoilState, useSetRecoilState } from 'recoil';
|
|
import { QueryKeys, parseCompactConvo, getResponseSender } from 'librechat-data-provider';
|
|
import { useGetMessagesByConvoId } from 'librechat-data-provider/react-query';
|
|
import type {
|
|
TMessage,
|
|
TSubmission,
|
|
TEndpointOption,
|
|
TConversation,
|
|
TGetConversationsResponse,
|
|
} from 'librechat-data-provider';
|
|
import type { TAskFunction } from '~/common';
|
|
import useSetFilesToDelete from './useSetFilesToDelete';
|
|
import { useAuthContext } from './AuthContext';
|
|
import useUserKey from './Input/useUserKey';
|
|
import useNewConvo from './useNewConvo';
|
|
import store from '~/store';
|
|
|
|
// this to be set somewhere else
|
|
export default function useChatHelpers(index = 0, paramId: string | undefined) {
|
|
const [files, setFiles] = useRecoilState(store.filesByIndex(index));
|
|
const [showStopButton, setShowStopButton] = useState(true);
|
|
const [filesLoading, setFilesLoading] = useState(false);
|
|
const setFilesToDelete = useSetFilesToDelete();
|
|
|
|
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 addConvo = useCallback(
|
|
(convo: TConversation) => {
|
|
const convoData = queryClient.getQueryData<TGetConversationsResponse>([
|
|
QueryKeys.allConversations,
|
|
{ pageNumber: '1', active: true },
|
|
]) ?? { conversations: [] as TConversation[], pageNumber: '1', pages: 1, pageSize: 14 };
|
|
|
|
let { conversations: convos, pageSize = 14 } = convoData;
|
|
pageSize = Number(pageSize);
|
|
convos = convos.filter((c) => c.conversationId !== convo.conversationId);
|
|
convos = convos.length < pageSize ? convos : convos.slice(0, -1);
|
|
|
|
const conversations = [
|
|
{
|
|
...convo,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
},
|
|
...convos,
|
|
];
|
|
|
|
queryClient.setQueryData<TGetConversationsResponse>(
|
|
[QueryKeys.allConversations, { pageNumber: '1', active: true }],
|
|
{
|
|
...convoData,
|
|
conversations,
|
|
},
|
|
);
|
|
},
|
|
[queryClient],
|
|
);
|
|
|
|
const invalidateConvos = useCallback(() => {
|
|
queryClient.invalidateQueries([QueryKeys.allConversations, { active: true }]);
|
|
}, [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(true);
|
|
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;
|
|
|
|
// set the endpoint option
|
|
const convo = parseCompactConvo(endpoint, conversation ?? {});
|
|
const endpointOption = {
|
|
...convo,
|
|
endpoint,
|
|
key: getExpiry(),
|
|
} as TEndpointOption;
|
|
const responseSender = getResponseSender({ model: conversation?.model, ...endpointOption });
|
|
|
|
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 || '00000000-0000-0000-0000-000000000000';
|
|
|
|
if (conversationId == 'new') {
|
|
parentMessageId = '00000000-0000-0000-0000-000000000000';
|
|
currentMessages = [];
|
|
conversationId = null;
|
|
}
|
|
const currentMsg: TMessage = {
|
|
text,
|
|
sender: 'User',
|
|
isCreatedByUser: true,
|
|
parentMessageId,
|
|
conversationId,
|
|
messageId: isContinued && messageId ? messageId : fakeMessageId,
|
|
error: false,
|
|
};
|
|
|
|
const parentMessage = currentMessages?.find(
|
|
(msg) => msg.messageId === latestMessage?.parentMessageId,
|
|
);
|
|
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
|
|
: '<span className="result-streaming">█</span>';
|
|
|
|
const responseMessageId = editedMessageId ?? latestMessage?.messageId ?? null;
|
|
const initialResponse: TMessage = {
|
|
sender: responseSender,
|
|
text: responseText,
|
|
parentMessageId: isRegenerate ? messageId : fakeMessageId,
|
|
messageId: responseMessageId ?? `${isRegenerate ? messageId : fakeMessageId}_`,
|
|
conversationId,
|
|
unfinished: false,
|
|
submitting: true,
|
|
isCreatedByUser: false,
|
|
isEdited: isEditOrContinue,
|
|
error: false,
|
|
};
|
|
|
|
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,
|
|
addConvo,
|
|
// 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,
|
|
invalidateConvos,
|
|
filesLoading,
|
|
setFilesLoading,
|
|
showStopButton,
|
|
setShowStopButton,
|
|
};
|
|
}
|