| | import { v4 } from 'uuid'; |
| | import { cloneDeep } from 'lodash'; |
| | import { useQueryClient } from '@tanstack/react-query'; |
| | import { |
| | Constants, |
| | QueryKeys, |
| | ContentTypes, |
| | EModelEndpoint, |
| | getEndpointField, |
| | isAgentsEndpoint, |
| | parseCompactConvo, |
| | replaceSpecialVars, |
| | isAssistantsEndpoint, |
| | } from 'librechat-data-provider'; |
| | import { useSetRecoilState, useResetRecoilState, useRecoilValue } from 'recoil'; |
| | import type { |
| | TMessage, |
| | TSubmission, |
| | TConversation, |
| | TEndpointOption, |
| | TEndpointsConfig, |
| | EndpointSchemaKey, |
| | } 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 store, { useGetEphemeralAgent } from '~/store'; |
| | import useUserKey from '~/hooks/Input/useUserKey'; |
| | import { useNavigate } from 'react-router-dom'; |
| | import { useAuthContext } from '~/hooks'; |
| | import { logger } from '~/utils'; |
| |
|
| | const logChatRequest = (request: Record<string, unknown>) => { |
| | logger.log('=====================================\nAsk function called with:'); |
| | logger.dir(request); |
| | logger.log('====================================='); |
| | }; |
| |
|
| | export default function useChatFunctions({ |
| | index = 0, |
| | files, |
| | setFiles, |
| | getMessages, |
| | setMessages, |
| | isSubmitting, |
| | latestMessage, |
| | setSubmission, |
| | setLatestMessage, |
| | conversation: immutableConversation, |
| | }: { |
| | index?: number; |
| | isSubmitting: boolean; |
| | paramId?: string | undefined; |
| | conversation: TConversation | null; |
| | latestMessage: TMessage | null; |
| | getMessages: () => TMessage[] | undefined; |
| | setMessages: (messages: TMessage[]) => void; |
| | files?: Map<string, ExtendedFile>; |
| | setFiles?: SetterOrUpdater<Map<string, ExtendedFile>>; |
| | setSubmission: SetterOrUpdater<TSubmission | null>; |
| | setLatestMessage?: SetterOrUpdater<TMessage | null>; |
| | }) { |
| | const navigate = useNavigate(); |
| | const getSender = useGetSender(); |
| | const { user } = useAuthContext(); |
| | const queryClient = useQueryClient(); |
| | const setFilesToDelete = useSetFilesToDelete(); |
| | const getEphemeralAgent = useGetEphemeralAgent(); |
| | const isTemporary = useRecoilValue(store.isTemporary); |
| | const { getExpiry } = useUserKey(immutableConversation?.endpoint ?? ''); |
| | const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index)); |
| | const resetLatestMultiMessage = useResetRecoilState(store.latestMessageFamily(index + 1)); |
| |
|
| | const ask: TAskFunction = ( |
| | { |
| | text, |
| | overrideConvoId, |
| | overrideUserMessageId, |
| | parentMessageId = null, |
| | conversationId = null, |
| | messageId = null, |
| | }, |
| | { |
| | editedContent = null, |
| | editedMessageId = null, |
| | isRegenerate = false, |
| | isContinued = false, |
| | isEdited = false, |
| | overrideMessages, |
| | overrideFiles, |
| | } = {}, |
| | ) => { |
| | setShowStopButton(false); |
| | resetLatestMultiMessage(); |
| | if (!!isSubmitting || text === '') { |
| | return; |
| | } |
| |
|
| | const conversation = cloneDeep(immutableConversation); |
| |
|
| | 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 ephemeralAgent = getEphemeralAgent(conversationId ?? Constants.NEW_CONVO); |
| | const isEditOrContinue = isEdited || isContinued; |
| |
|
| | let currentMessages: TMessage[] | null = overrideMessages ?? getMessages() ?? []; |
| |
|
| | if (conversation?.promptPrefix) { |
| | conversation.promptPrefix = replaceSpecialVars({ |
| | text: conversation.promptPrefix, |
| | user, |
| | }); |
| | } |
| |
|
| | |
| | |
| | text = text.trim(); |
| | const intermediateId = overrideUserMessageId ?? v4(); |
| | parentMessageId = parentMessageId ?? latestMessage?.messageId ?? Constants.NO_PARENT; |
| |
|
| | logChatRequest({ |
| | index, |
| | conversation, |
| | latestMessage, |
| | conversationId, |
| | intermediateId, |
| | parentMessageId, |
| | currentMessages, |
| | }); |
| |
|
| | if (conversationId == Constants.NEW_CONVO) { |
| | parentMessageId = Constants.NO_PARENT; |
| | currentMessages = []; |
| | conversationId = null; |
| | navigate('/c/new', { state: { focusChat: true } }); |
| | } |
| |
|
| | const targetParentMessageId = isRegenerate ? messageId : latestMessage?.parentMessageId; |
| | |
| | |
| | |
| | |
| | |
| | const targetParentMessage = currentMessages.find( |
| | (msg) => msg.messageId === targetParentMessageId, |
| | ); |
| |
|
| | let thread_id = targetParentMessage?.thread_id ?? latestMessage?.thread_id; |
| | if (thread_id == null) { |
| | thread_id = currentMessages.find((message) => message.thread_id)?.thread_id; |
| | } |
| |
|
| | const endpointsConfig = queryClient.getQueryData<TEndpointsConfig>([QueryKeys.endpoints]); |
| | const endpointType = getEndpointField(endpointsConfig, endpoint, 'type'); |
| | const iconURL = conversation?.iconURL; |
| |
|
| | |
| | const convo = parseCompactConvo({ |
| | endpoint: endpoint as EndpointSchemaKey, |
| | endpointType: endpointType as EndpointSchemaKey, |
| | conversation: conversation ?? {}, |
| | }); |
| |
|
| | const { modelDisplayLabel } = endpointsConfig?.[endpoint ?? ''] ?? {}; |
| | const endpointOption = Object.assign( |
| | { |
| | endpoint, |
| | endpointType, |
| | overrideConvoId, |
| | overrideUserMessageId, |
| | }, |
| | convo, |
| | ) as TEndpointOption; |
| | if (endpoint !== EModelEndpoint.agents) { |
| | endpointOption.key = getExpiry(); |
| | endpointOption.thread_id = thread_id; |
| | endpointOption.modelDisplayLabel = modelDisplayLabel; |
| | } else { |
| | endpointOption.key = new Date(Date.now() + 60 * 60 * 1000).toISOString(); |
| | } |
| | const responseSender = getSender({ model: conversation?.model, ...endpointOption }); |
| |
|
| | const currentMsg: TMessage = { |
| | text, |
| | sender: 'User', |
| | clientTimestamp: new Date().toLocaleString('sv').replace(' ', 'T'), |
| | isCreatedByUser: true, |
| | parentMessageId, |
| | conversationId, |
| | messageId: isContinued && messageId != null && messageId ? messageId : intermediateId, |
| | thread_id, |
| | error: false, |
| | }; |
| |
|
| | const submissionFiles = overrideFiles ?? targetParentMessage?.files; |
| | const reuseFiles = |
| | (isRegenerate || (overrideFiles != null && overrideFiles.length)) && |
| | submissionFiles && |
| | submissionFiles.length > 0; |
| |
|
| | if (setFiles && reuseFiles === true) { |
| | currentMsg.files = [...submissionFiles]; |
| | 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 ?? '', |
| | height: file.height, |
| | width: file.width, |
| | })); |
| | setFiles(new Map()); |
| | setFilesToDelete({}); |
| | } |
| |
|
| | const responseMessageId = |
| | editedMessageId ?? |
| | (latestMessage?.messageId && isRegenerate |
| | ? latestMessage.messageId.replace(/_+$/, '') + '_' |
| | : null) ?? |
| | null; |
| | const initialResponseId = |
| | responseMessageId ?? `${isRegenerate ? messageId : intermediateId}`.replace(/_+$/, '') + '_'; |
| |
|
| | const initialResponse: TMessage = { |
| | sender: responseSender, |
| | text: '', |
| | endpoint: endpoint ?? '', |
| | parentMessageId: isRegenerate ? messageId : intermediateId, |
| | messageId: initialResponseId, |
| | thread_id, |
| | conversationId, |
| | unfinished: false, |
| | isCreatedByUser: false, |
| | model: convo?.model, |
| | error: false, |
| | iconURL, |
| | }; |
| |
|
| | if (isAssistantsEndpoint(endpoint)) { |
| | initialResponse.model = conversation?.assistant_id ?? ''; |
| | initialResponse.text = ''; |
| | initialResponse.content = [ |
| | { |
| | type: ContentTypes.TEXT, |
| | [ContentTypes.TEXT]: { |
| | value: '', |
| | }, |
| | }, |
| | ]; |
| | } else if (endpoint != null) { |
| | initialResponse.model = isAgentsEndpoint(endpoint) |
| | ? (conversation?.agent_id ?? '') |
| | : (conversation?.model ?? ''); |
| | initialResponse.text = ''; |
| |
|
| | if (editedContent && latestMessage?.content) { |
| | initialResponse.content = cloneDeep(latestMessage.content); |
| | const { index, type, ...part } = editedContent; |
| | if (initialResponse.content && index >= 0 && index < initialResponse.content.length) { |
| | const contentPart = initialResponse.content[index]; |
| | if (type === ContentTypes.THINK && contentPart.type === ContentTypes.THINK) { |
| | contentPart[ContentTypes.THINK] = part[ContentTypes.THINK]; |
| | } else if (type === ContentTypes.TEXT && contentPart.type === ContentTypes.TEXT) { |
| | contentPart[ContentTypes.TEXT] = part[ContentTypes.TEXT]; |
| | } |
| | } |
| | } else { |
| | initialResponse.content = [ |
| | { |
| | type: ContentTypes.TEXT, |
| | [ContentTypes.TEXT]: { |
| | value: '', |
| | }, |
| | }, |
| | ]; |
| | } |
| | setShowStopButton(true); |
| | } |
| |
|
| | if (isContinued) { |
| | currentMessages = currentMessages.filter((msg) => msg.messageId !== responseMessageId); |
| | } |
| |
|
| | logger.log('message_state', initialResponse); |
| | const submission: TSubmission = { |
| | conversation: { |
| | ...conversation, |
| | conversationId, |
| | }, |
| | endpointOption, |
| | userMessage: { |
| | ...currentMsg, |
| | responseMessageId, |
| | overrideParentMessageId: isRegenerate ? messageId : null, |
| | }, |
| | messages: currentMessages, |
| | isEdited: isEditOrContinue, |
| | isContinued, |
| | isRegenerate, |
| | initialResponse, |
| | isTemporary, |
| | ephemeralAgent, |
| | editedContent, |
| | }; |
| |
|
| | 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, |
| | }; |
| | } |
| |
|