import { v4 } from 'uuid'; import { useQueryClient } from '@tanstack/react-query'; import { Constants, QueryKeys, ContentTypes, parseCompactConvo, isAssistantsEndpoint, } from 'librechat-data-provider'; import { useSetRecoilState, useResetRecoilState, useRecoilValue } from 'recoil'; import type { TMessage, TSubmission, TConversation, TEndpointOption, TEndpointsConfig, } from 'librechat-data-provider'; import type { SetterOrUpdater } from 'recoil'; import type { TAskFunction, ExtendedFile } from '~/common'; import useSetFilesToDelete from '~/hooks/Files/useSetFilesToDelete'; import useGetSender from '~/hooks/Conversations/useGetSender'; import { getArtifactsMode } from '~/utils/artifacts'; import { getEndpointField, logger } from '~/utils'; import useUserKey from '~/hooks/Input/useUserKey'; import store from '~/store'; export default function useChatFunctions({ index = 0, files, setFiles, getMessages, setMessages, isSubmitting, conversation, latestMessage, setSubmission, setLatestMessage, }: { index?: number; isSubmitting: boolean; paramId?: string | undefined; conversation: TConversation | null; latestMessage: TMessage | null; getMessages: () => TMessage[] | undefined; setMessages: (messages: TMessage[]) => void; files?: Map; setFiles?: SetterOrUpdater>; setSubmission: SetterOrUpdater; setLatestMessage?: SetterOrUpdater; }) { const codeArtifacts = useRecoilValue(store.codeArtifacts); const includeShadcnui = useRecoilValue(store.includeShadcnui); const customPromptMode = useRecoilValue(store.customPromptMode); const resetLatestMultiMessage = useResetRecoilState(store.latestMessageFamily(index + 1)); const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index)); const setFilesToDelete = useSetFilesToDelete(); const getSender = useGetSender(); const queryClient = useQueryClient(); const { getExpiry } = useUserKey(conversation?.endpoint ?? ''); const ask: TAskFunction = ( { text, overrideConvoId, overrideUserMessageId, parentMessageId = null, conversationId = null, messageId = null, }, { editedText = null, editedMessageId = null, resubmitFiles = false, isRegenerate = false, isContinued = false, isEdited = false, overrideMessages, } = {}, ) => { setShowStopButton(false); resetLatestMultiMessage(); if (!!isSubmitting || text === '') { return; } const endpoint = conversation?.endpoint; 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 = overrideMessages ?? getMessages() ?? []; // construct the query message // this is not a real messageId, it is used as placeholder before real messageId returned text = text.trim(); const intermediateId = overrideUserMessageId ?? v4(); parentMessageId = parentMessageId || latestMessage?.messageId || Constants.NO_PARENT; logger.dir('Ask function called with:', { index, latestMessage, conversationId, intermediateId, parentMessageId, currentMessages, }); logger.log('====================================='); if (conversationId == Constants.NEW_CONVO) { 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([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, thread_id, endpointType, overrideConvoId, key: getExpiry(), modelDisplayLabel, overrideUserMessageId, artifacts: getArtifactsMode({ codeArtifacts, includeShadcnui, customPromptMode }), } as TEndpointOption; const responseSender = getSender({ model: conversation?.model, ...endpointOption }); const currentMsg: TMessage = { text, sender: 'User', isCreatedByUser: true, parentMessageId, conversationId, messageId: isContinued && messageId ? messageId : intermediateId, thread_id, error: false, }; const reuseFiles = (isRegenerate || resubmitFiles) && parentMessage?.files; if (setFiles && reuseFiles && parentMessage.files?.length) { currentMsg.files = parentMessage.files; setFiles(new Map()); setFilesToDelete({}); } else if (setFiles && files && 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 : intermediateId, messageId: responseMessageId ?? `${isRegenerate ? messageId : intermediateId}_`, thread_id, conversationId, unfinished: false, isCreatedByUser: false, isEdited: isEditOrContinue, iconURL: convo.iconURL, error: false, }; if (isAssistantsEndpoint(endpoint)) { 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, userMessage: { ...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]); } if (index === 0 && setLatestMessage) { setLatestMessage(initialResponse); } setSubmission(submission); logger.dir('message_stream', submission, { depth: null }); }; 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.', ); } }; return { ask, regenerate, }; }