fix: Wait for Initial Message Save & Correct Latest Message (#3399)

* chore: assistants, unsupported assistant, better logging

* chore: remove unnecessary logger in validateAssistant middleware

* fix: resolve initial conversation save/promise before saving response

* chore: Import and organize dependencies in Speech component

* fix: conversation statefulness
- Latest Message (at index 0) should not be reset if existing convo
- add debugging context for clearAllLatestMessages
- Added logging concerning latest Message updates (dev mode only)
- update latest message Set logic, also checks for change in conversation Id
- consolidated latest message helpers to client/src/utils/messages.ts
This commit is contained in:
Danny Avila 2024-07-20 01:51:59 -04:00 committed by GitHub
parent 9e7615f832
commit 2ad097647c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 275 additions and 113 deletions

View file

@ -540,6 +540,9 @@ class BaseClient {
const completionTokens = this.getTokenCount(completion);
await this.recordTokenUsage({ promptTokens, completionTokens });
}
if (this.userMessagePromise) {
await this.userMessagePromise;
}
this.responsePromise = this.saveMessageToDatabase(responseMessage, saveOptions, user);
const messageCache = getLogStores(CacheKeys.MESSAGES);
messageCache.set(
@ -620,18 +623,23 @@ class BaseClient {
unfinished: false,
user,
},
{ context: 'api/app/clients/BaseClient.js - saveMessageToDatabase' },
{ context: 'api/app/clients/BaseClient.js - saveMessageToDatabase #saveMessage' },
);
if (this.skipSaveConvo) {
return { message: savedMessage };
}
const conversation = await saveConvo(user, {
conversationId: message.conversationId,
endpoint: this.options.endpoint,
endpointType: this.options.endpointType,
...endpointOptions,
});
const conversation = await saveConvo(
this.options.req,
{
conversationId: message.conversationId,
endpoint: this.options.endpoint,
endpointType: this.options.endpointType,
...endpointOptions,
},
{ context: 'api/app/clients/BaseClient.js - saveMessageToDatabase #saveConvo' },
);
return { message: savedMessage, conversation };
}

View file

@ -1,7 +1,7 @@
const { Constants } = require('librechat-data-provider');
const { initializeFakeClient } = require('./FakeClient');
jest.mock('../../../lib/db/connectDb');
jest.mock('~/lib/db/connectDb');
jest.mock('~/models', () => ({
User: jest.fn(),
Key: jest.fn(),
@ -631,5 +631,32 @@ describe('BaseClient', () => {
}),
);
});
test('userMessagePromise is awaited before saving response message', async () => {
// Mock the saveMessageToDatabase method
TestClient.saveMessageToDatabase = jest.fn().mockImplementation(() => {
return new Promise((resolve) => setTimeout(resolve, 100)); // Simulate a delay
});
// Send a message
const messagePromise = TestClient.sendMessage('Hello, world!');
// Wait a short time to ensure the user message save has started
await new Promise((resolve) => setTimeout(resolve, 50));
// Check that saveMessageToDatabase has been called once (for the user message)
expect(TestClient.saveMessageToDatabase).toHaveBeenCalledTimes(1);
// Wait for the message to be fully processed
await messagePromise;
// Check that saveMessageToDatabase has been called twice (once for user message, once for response)
expect(TestClient.saveMessageToDatabase).toHaveBeenCalledTimes(2);
// Check the order of calls
const calls = TestClient.saveMessageToDatabase.mock.calls;
expect(calls[0][0].isCreatedByUser).toBe(true); // First call should be for user message
expect(calls[1][0].isCreatedByUser).toBe(false); // Second call should be for response message
});
});
});

View file

@ -19,18 +19,32 @@ const getConvo = async (user, conversationId) => {
module.exports = {
Conversation,
saveConvo: async (user, { conversationId, newConversationId, ...convo }) => {
/**
* Saves a conversation to the database.
* @param {Object} req - The request object.
* @param {string} conversationId - The conversation's ID.
* @param {Object} metadata - Additional metadata to log for operation.
* @returns {Promise<TConversation>} The conversation object.
*/
saveConvo: async (req, { conversationId, newConversationId, ...convo }, metadata) => {
try {
if (metadata && metadata?.context) {
logger.info(`[saveConvo] ${metadata.context}`);
}
const messages = await getMessages({ conversationId }, '_id');
const update = { ...convo, messages, user };
const update = { ...convo, messages, user: req.user.id };
if (newConversationId) {
update.conversationId = newConversationId;
}
const conversation = await Conversation.findOneAndUpdate({ conversationId, user }, update, {
new: true,
upsert: true,
});
const conversation = await Conversation.findOneAndUpdate(
{ conversationId, user: req.user.id },
update,
{
new: true,
upsert: true,
},
);
return conversation.toObject();
} catch (error) {

View file

@ -383,6 +383,9 @@ const chatV1 = async (req, res) => {
return files;
};
/** @type {Promise<Run>|undefined} */
let userMessagePromise;
const initializeThread = async () => {
/** @type {[ undefined | MongoFile[]]}*/
const [processedFiles] = await Promise.all([addVisionPrompt(), getRequestFileIds()]);
@ -439,7 +442,7 @@ const chatV1 = async (req, res) => {
previousMessages.push(requestMessage);
/* asynchronous */
saveUserMessage({ ...requestMessage, model });
userMessagePromise = saveUserMessage(req, { ...requestMessage, model });
conversation = {
conversationId,
@ -583,7 +586,10 @@ const chatV1 = async (req, res) => {
});
res.end();
await saveAssistantMessage({ ...responseMessage, model });
if (userMessagePromise) {
await userMessagePromise;
}
await saveAssistantMessage(req, { ...responseMessage, model });
if (parentMessageId === Constants.NO_PARENT && !_thread_id) {
addTitle(req, {

View file

@ -246,6 +246,9 @@ const chatV2 = async (req, res) => {
}
};
/** @type {Promise<Run>|undefined} */
let userMessagePromise;
const initializeThread = async () => {
await getRequestFileIds();
@ -288,7 +291,7 @@ const chatV2 = async (req, res) => {
previousMessages.push(requestMessage);
/* asynchronous */
saveUserMessage({ ...requestMessage, model });
userMessagePromise = saveUserMessage(req, { ...requestMessage, model });
conversation = {
conversationId,
@ -449,7 +452,10 @@ const chatV2 = async (req, res) => {
});
res.end();
await saveAssistantMessage({ ...responseMessage, model });
if (userMessagePromise) {
await userMessagePromise;
}
await saveAssistantMessage(req, { ...responseMessage, model });
if (parentMessageId === Constants.NO_PARENT && !_thread_id) {
addTitle(req, {

View file

@ -19,7 +19,8 @@ const validateAssistant = async (req, res, next) => {
}
const { supportedIds, excludedIds } = assistantsConfig;
const error = { message: 'Assistant not supported' };
const error = { message: 'validateAssistant: Assistant not supported' };
if (supportedIds?.length && !supportedIds.includes(assistant_id)) {
return await handleAbortError(res, req, error, {
sender: 'System',

View file

@ -52,7 +52,7 @@ router.post('/', setHeaders, async (req, res) => {
if (!overrideParentMessageId) {
await saveMessage(req, { ...userMessage, user: req.user.id });
await saveConvo(req.user.id, {
await saveConvo(req, {
...userMessage,
...endpointOption,
conversationId,
@ -183,7 +183,7 @@ const ask = async ({
}
}
await saveConvo(user, conversationUpdate);
await saveConvo(req, conversationUpdate);
conversationId = newConversationId;
// STEP3 update the user message
@ -213,7 +213,7 @@ const ask = async ({
if (userParentMessageId == Constants.NO_PARENT) {
// const title = await titleConvo({ endpoint: endpointOption?.endpoint, text, response: responseMessage });
const title = await response.details.title;
await saveConvo(user, {
await saveConvo(req, {
conversationId: conversationId,
title,
});

View file

@ -71,7 +71,7 @@ router.post('/', setHeaders, async (req, res) => {
if (!overrideParentMessageId) {
await saveMessage(req, { ...userMessage, user: req.user.id });
await saveConvo(req.user.id, {
await saveConvo(req, {
...userMessage,
...endpointOption,
conversationId,
@ -216,7 +216,7 @@ const ask = async ({
conversationUpdate.invocationId = response.invocationId;
}
await saveConvo(user, conversationUpdate);
await saveConvo(req, conversationUpdate);
userMessage.messageId = newUserMessageId;
// If response has parentMessageId, the fake userMessage.messageId should be updated to the real one.
@ -245,7 +245,7 @@ const ask = async ({
response: responseMessage,
});
await saveConvo(user, {
await saveConvo(req, {
conversationId: conversationId,
title,
});

View file

@ -104,7 +104,7 @@ router.post('/update', async (req, res) => {
const update = req.body.arg;
try {
const dbResponse = await saveConvo(req.user.id, update);
const dbResponse = await saveConvo(req, update, { context: 'POST /api/convos/update' });
res.status(201).json(dbResponse);
} catch (error) {
logger.error('Error updating conversation', error);

View file

@ -30,7 +30,7 @@ router.post('/:conversationId', validateMessageReq, async (req, res) => {
if (!savedMessage) {
return res.status(400).json({ error: 'Message not saved' });
}
await saveConvo(req.user.id, savedMessage);
await saveConvo(req, savedMessage, { context: 'POST /api/messages/:conversationId' });
res.status(201).json(savedMessage);
} catch (error) {
logger.error('Error saving message:', error);

View file

@ -23,10 +23,14 @@ const addTitle = async (req, { text, response, client }) => {
const title = await client.titleConvo({ text, responseText: response?.text });
await titleCache.set(key, title, 120000);
await saveConvo(req.user.id, {
conversationId: response.conversationId,
title,
});
await saveConvo(
req,
{
conversationId: response.conversationId,
title,
},
{ context: 'api/server/services/Endpoints/anthropic/addTitle.js' },
);
};
module.exports = addTitle;

View file

@ -19,10 +19,14 @@ const addTitle = async (req, { text, responseText, conversationId, client }) =>
const title = await client.titleConvo({ text, conversationId, responseText });
await titleCache.set(key, title, 120000);
await saveConvo(req.user.id, {
conversationId,
title,
});
await saveConvo(
req,
{
conversationId,
title,
},
{ context: 'api/server/services/Endpoints/assistants/addTitle.js' },
);
};
module.exports = addTitle;

View file

@ -49,10 +49,14 @@ const addTitle = async (req, { text, response, client }) => {
const title = await titleClient.titleConvo({ text, responseText: response?.text });
await titleCache.set(key, title, 120000);
await saveConvo(req.user.id, {
conversationId: response.conversationId,
title,
});
await saveConvo(
req,
{
conversationId: response.conversationId,
title,
},
{ context: 'api/server/services/Endpoints/google/addTitle.js' },
);
};
module.exports = addTitle;

View file

@ -23,10 +23,14 @@ const addTitle = async (req, { text, response, client }) => {
const title = await client.titleConvo({ text, responseText: response?.text });
await titleCache.set(key, title, 120000);
await saveConvo(req.user.id, {
conversationId: response.conversationId,
title,
});
await saveConvo(
req,
{
conversationId: response.conversationId,
title,
},
{ context: 'api/server/services/Endpoints/openAI/addTitle.js' },
);
};
module.exports = addTitle;

View file

@ -41,6 +41,7 @@ async function initThread({ openai, body, thread_id: _thread_id }) {
/**
* Saves a user message to the DB in the Assistants endpoint format.
*
* @param {Object} req - The request object.
* @param {Object} params - The parameters of the user message
* @param {string} params.user - The user's ID.
* @param {string} params.text - The user's prompt.
@ -59,7 +60,7 @@ async function initThread({ openai, body, thread_id: _thread_id }) {
* @param {string[]} [params.file_ids] - Optional. List of File IDs attached to the userMessage.
* @return {Promise<Run>} A promise that resolves to the created run object.
*/
async function saveUserMessage(params) {
async function saveUserMessage(req, params) {
const tokenCount = await countTokens(params.text);
// todo: do this on the frontend
@ -110,14 +111,16 @@ async function saveUserMessage(params) {
}
const message = await recordMessage(userMessage);
await saveConvo(params.user, convo);
await saveConvo(req, convo, {
context: 'api/server/services/Threads/manage.js #saveUserMessage',
});
return message;
}
/**
* Saves an Assistant message to the DB in the Assistants endpoint format.
*
* @param {Object} req - The request object.
* @param {Object} params - The parameters of the Assistant message
* @param {string} params.user - The user's ID.
* @param {string} params.messageId - The message Id.
@ -134,7 +137,7 @@ async function saveUserMessage(params) {
* @param {string} [params.promptPrefix] - Optional: from preset for `additional_instructions` field.
* @return {Promise<Run>} A promise that resolves to the created run object.
*/
async function saveAssistantMessage(params) {
async function saveAssistantMessage(req, params) {
// const tokenCount = // TODO: need to count each content part
const message = await recordMessage({
@ -154,14 +157,18 @@ async function saveAssistantMessage(params) {
// tokenCount,
});
await saveConvo(params.user, {
endpoint: params.endpoint,
conversationId: params.conversationId,
promptPrefix: params.promptPrefix,
instructions: params.instructions,
assistant_id: params.assistant_id,
model: params.model,
});
await saveConvo(
req,
{
endpoint: params.endpoint,
conversationId: params.conversationId,
promptPrefix: params.promptPrefix,
instructions: params.instructions,
assistant_id: params.assistant_id,
model: params.model,
},
{ context: 'api/server/services/Threads/manage.js #saveAssistantMessage' },
);
return message;
}
@ -338,10 +345,14 @@ async function syncMessages({
await Promise.all(modifyPromises);
await Promise.all(recordPromises);
await saveConvo(openai.req.user.id, {
conversationId,
file_ids: attached_file_ids,
});
await saveConvo(
openai.req,
{
conversationId,
file_ids: attached_file_ids,
},
{ context: 'api/server/services/Threads/manage.js #syncMessages' },
);
return result;
}

View file

@ -1,6 +1,7 @@
import { useRecoilValue } from 'recoil';
import { useParams } from 'react-router-dom';
import { useState, useRef, useMemo } from 'react';
import { Constants } from 'librechat-data-provider';
import { useGetEndpointsQuery, useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react';
import { useUpdateConversationMutation } from '~/data-provider';
@ -9,14 +10,14 @@ import { useConversations, useNavigateToConvo } from '~/hooks';
import { NotificationSeverity } from '~/common';
import { ArchiveIcon } from '~/components/svg';
import { useToastContext } from '~/Providers';
import DropDownMenu from './DropDownMenu';
import ArchiveButton from './ArchiveButton';
import DropDownMenu from './DropDownMenu';
import DeleteButton from './DeleteButton';
import RenameButton from './RenameButton';
import HoverToggle from './HoverToggle';
import ShareButton from './ShareButton';
import { cn } from '~/utils';
import store from '~/store';
import ShareButton from './ShareButton';
type KeyEvent = KeyboardEvent<HTMLInputElement>;
@ -51,7 +52,8 @@ export default function Conversation({ conversation, retainView, toggleNav, isLa
// set document title
document.title = title;
navigateWithLastTools(conversation);
/* Note: Latest Message should not be reset if existing convo */
navigateWithLastTools(conversation, !conversationId || conversationId === Constants.NEW_CONVO);
};
const renameHandler = (e: MouseEvent<HTMLButtonElement>) => {

View file

@ -1,12 +1,9 @@
import { useRecoilState } from 'recoil';
import * as Tabs from '@radix-ui/react-tabs';
import { Lightbulb, Cog } from 'lucide-react';
import { SettingsTabValues } from 'librechat-data-provider';
import React, { useState, useRef, useEffect, useCallback } from 'react';
import { useRecoilState } from 'recoil';
import { Lightbulb, Cog } from 'lucide-react';
import { useOnClickOutside, useMediaQuery } from '~/hooks';
import store from '~/store';
import { cn } from '~/utils';
import ConversationModeSwitch from './ConversationModeSwitch';
import { useGetCustomConfigSpeechQuery } from 'librechat-data-provider/react-query';
import {
CloudBrowserVoicesSwitch,
AutomaticPlaybackSwitch,
@ -24,7 +21,10 @@ import {
EngineSTTDropdown,
DecibelSelector,
} from './STT';
import { useGetCustomConfigSpeechQuery } from 'librechat-data-provider/react-query';
import ConversationModeSwitch from './ConversationModeSwitch';
import { useOnClickOutside, useMediaQuery } from '~/hooks';
import { cn, logger } from '~/utils';
import store from '~/store';
function Speech() {
const [confirmClear, setConfirmClear] = useState(false);
@ -131,8 +131,7 @@ function Speech() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
console.log(sttExternal);
console.log(ttsExternal);
logger.log({ sttExternal, ttsExternal });
const contentRef = useRef(null);
useOnClickOutside(contentRef, () => confirmClear && setConfirmClear(false), []);

View file

@ -107,6 +107,16 @@ export default function useChatFunctions({
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 = [];

View file

@ -12,7 +12,7 @@ import type {
TModelsConfig,
TEndpointsConfig,
} from 'librechat-data-provider';
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField, logger } from '~/utils';
import store from '~/store';
const useConversation = () => {
@ -60,6 +60,10 @@ const useConversation = () => {
setMessages(messages);
setSubmission({} as TSubmission);
resetLatestMessage();
logger.log(
'[useConversation] Switched to conversation and reset Latest Message',
conversation,
);
if (conversation.conversationId === 'new' && !modelsData) {
queryClient.invalidateQueries([QueryKeys.messages, 'new']);

View file

@ -1,7 +1,7 @@
import { useSetRecoilState } from 'recoil';
import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, EModelEndpoint, LocalStorageKeys } from 'librechat-data-provider';
import { QueryKeys, EModelEndpoint, LocalStorageKeys, Constants } from 'librechat-data-provider';
import type { TConversation, TEndpointsConfig, TModelsConfig } from 'librechat-data-provider';
import { buildDefaultConvo, getDefaultEndpoint, getEndpointField } from '~/utils';
import store from '~/store';
@ -10,7 +10,7 @@ const useNavigateToConvo = (index = 0) => {
const navigate = useNavigate();
const queryClient = useQueryClient();
const clearAllConversations = store.useClearConvoState();
const clearAllLatestMessages = store.useClearLatestMessages();
const clearAllLatestMessages = store.useClearLatestMessages(`useNavigateToConvo ${index}`);
const setSubmission = useSetRecoilState(store.submissionByIndex(index));
const { setConversation } = store.useCreateConversationAtom(index);
@ -50,10 +50,10 @@ const useNavigateToConvo = (index = 0) => {
}
clearAllConversations(true);
setConversation(convo);
navigate(`/c/${convo.conversationId ?? 'new'}`);
navigate(`/c/${convo.conversationId ?? Constants.NEW_CONVO}`);
};
const navigateWithLastTools = (conversation: TConversation) => {
const navigateWithLastTools = (conversation: TConversation, _resetLatestMessage?: boolean) => {
// set conversation to the new conversation
if (conversation?.endpoint === EModelEndpoint.gptPlugins) {
let lastSelectedTools = [];
@ -63,12 +63,15 @@ const useNavigateToConvo = (index = 0) => {
} catch (e) {
// console.error(e);
}
navigateToConvo({
...conversation,
tools: conversation?.tools?.length ? conversation?.tools : lastSelectedTools,
});
navigateToConvo(
{
...conversation,
tools: conversation?.tools?.length ? conversation?.tools : lastSelectedTools,
},
_resetLatestMessage,
);
} else {
navigateToConvo(conversation);
navigateToConvo(conversation, _resetLatestMessage);
}
};

View file

@ -1,9 +1,9 @@
import { useEffect, useRef, useCallback } from 'react';
import { isAssistantsEndpoint } from 'librechat-data-provider';
import { Constants, isAssistantsEndpoint } from 'librechat-data-provider';
import type { TMessageProps } from '~/common';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
import { getLatestText, getLengthAndFirstFiveChars } from '~/utils';
import useCopyToClipboard from './useCopyToClipboard';
import { getTextKey, logger } from '~/utils';
export default function useMessageHelpers(props: TMessageProps) {
const latestText = useRef<string | number>('');
@ -27,7 +27,8 @@ export default function useMessageHelpers(props: TMessageProps) {
const isLast = !children?.length;
useEffect(() => {
if (conversation?.conversationId === 'new') {
const convoId = conversation?.conversationId;
if (convoId === Constants.NEW_CONVO) {
return;
}
if (!message) {
@ -37,15 +38,25 @@ export default function useMessageHelpers(props: TMessageProps) {
return;
}
const text = getLatestText(message);
const textKey = `${message?.messageId ?? ''}${getLengthAndFirstFiveChars(text)}`;
const textKey = getTextKey(message, convoId);
if (textKey === latestText.current) {
return;
// Check for text/conversation change
const logInfo = {
textKey,
'latestText.current': latestText.current,
messageId: message?.messageId,
convoId,
};
if (
textKey !== latestText.current ||
(latestText.current && convoId !== latestText.current.split(Constants.COMMON_DIVIDER)[2])
) {
logger.log('[useMessageHelpers] Setting latest message: ', logInfo);
latestText.current = textKey;
setLatestMessage({ ...message });
} else {
logger.log('No change in latest message', logInfo);
}
latestText.current = textKey;
setLatestMessage({ ...message });
}, [isLast, message, setLatestMessage, conversation?.conversationId]);
const enterEdit = useCallback(

View file

@ -1,8 +1,9 @@
import { useRecoilValue } from 'recoil';
import { Constants } from 'librechat-data-provider';
import { useEffect, useRef, useCallback, useMemo, useState } from 'react';
import type { TMessage } from 'librechat-data-provider';
import { useChatContext, useAddedChatContext } from '~/Providers';
import { getLatestText, getLengthAndFirstFiveChars } from '~/utils';
import { getTextKey, logger } from '~/utils';
import store from '~/store';
export default function useMessageProcess({ message }: { message?: TMessage | null }) {
@ -26,7 +27,8 @@ export default function useMessageProcess({ message }: { message?: TMessage | nu
);
useEffect(() => {
if (conversation?.conversationId === 'new') {
const convoId = conversation?.conversationId;
if (convoId === Constants.NEW_CONVO) {
return;
}
if (!message) {
@ -36,15 +38,27 @@ export default function useMessageProcess({ message }: { message?: TMessage | nu
return;
}
const text = getLatestText(message);
const textKey = `${message?.messageId ?? ''}${getLengthAndFirstFiveChars(text)}`;
const textKey = getTextKey(message, convoId);
if (textKey === latestText.current) {
return;
// Check for text/conversation change
const logInfo = {
textKey,
'latestText.current': latestText.current,
messageId: message?.messageId,
convoId,
};
if (
textKey !== latestText.current ||
(convoId &&
latestText.current &&
convoId !== latestText.current.split(Constants.COMMON_DIVIDER)[2])
) {
logger.log('[useMessageProcess] Setting latest message: ', logInfo);
latestText.current = textKey;
setLatestMessage({ ...message });
} else {
logger.log('No change in latest message', logInfo);
}
latestText.current = textKey;
setLatestMessage({ ...message });
}, [hasNoChildren, message, setLatestMessage, conversation?.conversationId]);
const handleScroll = useCallback(() => {

View file

@ -34,9 +34,9 @@ const useNewConvo = (index = 0) => {
const { data: startupConfig } = useGetStartupConfig();
const clearAllConversations = store.useClearConvoState();
const defaultPreset = useRecoilValue(store.defaultPreset);
const clearAllLatestMessages = store.useClearLatestMessages();
const { setConversation } = store.useCreateConversationAtom(index);
const [files, setFiles] = useRecoilState(store.filesByIndex(index));
const clearAllLatestMessages = store.useClearLatestMessages(`useNewConvo ${index}`);
const setSubmission = useSetRecoilState<TSubmission | null>(store.submissionByIndex(index));
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();

View file

@ -8,7 +8,7 @@ import {
useSetRecoilState,
useRecoilCallback,
} from 'recoil';
import { LocalStorageKeys } from 'librechat-data-provider';
import { LocalStorageKeys, Constants } from 'librechat-data-provider';
import type { TMessage, TPreset, TConversation, TSubmission } from 'librechat-data-provider';
import type { TOptionSettings, ExtendedFile } from '~/common';
import { storeEndpointSettings, logger } from '~/utils';
@ -27,6 +27,14 @@ const submissionKeysAtom = atom<(string | number)[]>({
const latestMessageFamily = atomFamily<TMessage | null, string | number | null>({
key: 'latestMessageByIndex',
default: null,
effects: [
({ onSet, node }) => {
onSet(async (newValue) => {
const key = Number(node.key.split(Constants.COMMON_DIVIDER)[1]);
logger.log('Recoil Effect: Setting latestMessage', { key, newValue });
});
},
] as const,
});
const submissionByIndex = atomFamily<TSubmission | null, string | number>({
@ -41,7 +49,7 @@ const latestMessageKeysSelector = selector<(string | number)[]>({
return keys.filter((key) => get(latestMessageFamily(key)) !== null);
},
set: ({ set }, newKeys) => {
logger.log('setting latestMessageKeys', newKeys);
logger.log('setting latestMessageKeys', { newKeys });
set(latestMessageKeysAtom, newKeys);
},
});
@ -279,19 +287,22 @@ function useClearSubmissionState() {
return clearAllSubmissions;
}
function useClearLatestMessages() {
function useClearLatestMessages(context?: string) {
const clearAllLatestMessages = useRecoilCallback(
({ reset, set, snapshot }) =>
async (skipFirst?: boolean) => {
const latestMessageKeys = await snapshot.getPromise(latestMessageKeysSelector);
logger.log('latestMessageKeys', latestMessageKeys);
logger.log('[clearAllLatestMessages] latestMessageKeys', latestMessageKeys);
if (context) {
logger.log(`[clearAllLatestMessages] context: ${context}`);
}
for (const key of latestMessageKeys) {
if (skipFirst && key == 0) {
continue;
}
logger.log('resetting latest message', key);
logger.log(`[clearAllLatestMessages] resetting latest message; key: ${key}`);
reset(latestMessageFamily(key));
}

View file

@ -1,13 +1,17 @@
import { ContentTypes } from 'librechat-data-provider';
import { ContentTypes, Constants } from 'librechat-data-provider';
import type { TMessage } from 'librechat-data-provider';
export const getLengthAndFirstFiveChars = (str?: string) => {
const length = str ? str.length : 0;
const firstFiveChars = str ? str.substring(0, 5) : '';
return `${length}${firstFiveChars}`;
export const getLengthAndLastTenChars = (str?: string): string => {
if (!str) {
return '0';
}
const length = str.length;
const lastTenChars = str.slice(-10);
return `${length}${lastTenChars}`;
};
export const getLatestText = (message?: TMessage | null) => {
export const getLatestText = (message?: TMessage | null, includeIndex?: boolean) => {
if (!message) {
return '';
}
@ -18,9 +22,24 @@ export const getLatestText = (message?: TMessage | null) => {
for (let i = message.content.length - 1; i >= 0; i--) {
const part = message.content[i];
if (part.type === ContentTypes.TEXT && part[ContentTypes.TEXT]?.value?.length > 0) {
return part[ContentTypes.TEXT].value;
const text = part[ContentTypes.TEXT].value;
if (includeIndex) {
return `${text}-${i}`;
} else {
return text;
}
}
}
}
return '';
};
export const getTextKey = (message?: TMessage | null, convoId?: string | null) => {
if (!message) {
return '';
}
const text = getLatestText(message, true);
return `${message.messageId ?? ''}${Constants.COMMON_DIVIDER}${getLengthAndLastTenChars(text)}${
Constants.COMMON_DIVIDER
}${message.conversationId ?? convoId}`;
};