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

View file

@ -1,7 +1,7 @@
const { Constants } = require('librechat-data-provider'); const { Constants } = require('librechat-data-provider');
const { initializeFakeClient } = require('./FakeClient'); const { initializeFakeClient } = require('./FakeClient');
jest.mock('../../../lib/db/connectDb'); jest.mock('~/lib/db/connectDb');
jest.mock('~/models', () => ({ jest.mock('~/models', () => ({
User: jest.fn(), User: jest.fn(),
Key: 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 = { module.exports = {
Conversation, 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 { try {
if (metadata && metadata?.context) {
logger.info(`[saveConvo] ${metadata.context}`);
}
const messages = await getMessages({ conversationId }, '_id'); const messages = await getMessages({ conversationId }, '_id');
const update = { ...convo, messages, user }; const update = { ...convo, messages, user: req.user.id };
if (newConversationId) { if (newConversationId) {
update.conversationId = newConversationId; update.conversationId = newConversationId;
} }
const conversation = await Conversation.findOneAndUpdate({ conversationId, user }, update, { const conversation = await Conversation.findOneAndUpdate(
new: true, { conversationId, user: req.user.id },
upsert: true, update,
}); {
new: true,
upsert: true,
},
);
return conversation.toObject(); return conversation.toObject();
} catch (error) { } catch (error) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -104,7 +104,7 @@ router.post('/update', async (req, res) => {
const update = req.body.arg; const update = req.body.arg;
try { 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); res.status(201).json(dbResponse);
} catch (error) { } catch (error) {
logger.error('Error updating conversation', error); logger.error('Error updating conversation', error);

View file

@ -30,7 +30,7 @@ router.post('/:conversationId', validateMessageReq, async (req, res) => {
if (!savedMessage) { if (!savedMessage) {
return res.status(400).json({ error: 'Message not saved' }); 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); res.status(201).json(savedMessage);
} catch (error) { } catch (error) {
logger.error('Error saving message:', 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 }); const title = await client.titleConvo({ text, responseText: response?.text });
await titleCache.set(key, title, 120000); await titleCache.set(key, title, 120000);
await saveConvo(req.user.id, { await saveConvo(
conversationId: response.conversationId, req,
title, {
}); conversationId: response.conversationId,
title,
},
{ context: 'api/server/services/Endpoints/anthropic/addTitle.js' },
);
}; };
module.exports = addTitle; 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 }); const title = await client.titleConvo({ text, conversationId, responseText });
await titleCache.set(key, title, 120000); await titleCache.set(key, title, 120000);
await saveConvo(req.user.id, { await saveConvo(
conversationId, req,
title, {
}); conversationId,
title,
},
{ context: 'api/server/services/Endpoints/assistants/addTitle.js' },
);
}; };
module.exports = addTitle; 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 }); const title = await titleClient.titleConvo({ text, responseText: response?.text });
await titleCache.set(key, title, 120000); await titleCache.set(key, title, 120000);
await saveConvo(req.user.id, { await saveConvo(
conversationId: response.conversationId, req,
title, {
}); conversationId: response.conversationId,
title,
},
{ context: 'api/server/services/Endpoints/google/addTitle.js' },
);
}; };
module.exports = addTitle; 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 }); const title = await client.titleConvo({ text, responseText: response?.text });
await titleCache.set(key, title, 120000); await titleCache.set(key, title, 120000);
await saveConvo(req.user.id, { await saveConvo(
conversationId: response.conversationId, req,
title, {
}); conversationId: response.conversationId,
title,
},
{ context: 'api/server/services/Endpoints/openAI/addTitle.js' },
);
}; };
module.exports = addTitle; 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. * 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 {Object} params - The parameters of the user message
* @param {string} params.user - The user's ID. * @param {string} params.user - The user's ID.
* @param {string} params.text - The user's prompt. * @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. * @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. * @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); const tokenCount = await countTokens(params.text);
// todo: do this on the frontend // todo: do this on the frontend
@ -110,14 +111,16 @@ async function saveUserMessage(params) {
} }
const message = await recordMessage(userMessage); const message = await recordMessage(userMessage);
await saveConvo(params.user, convo); await saveConvo(req, convo, {
context: 'api/server/services/Threads/manage.js #saveUserMessage',
});
return message; return message;
} }
/** /**
* Saves an Assistant message to the DB in the Assistants endpoint format. * 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 {Object} params - The parameters of the Assistant message
* @param {string} params.user - The user's ID. * @param {string} params.user - The user's ID.
* @param {string} params.messageId - The message 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. * @param {string} [params.promptPrefix] - Optional: from preset for `additional_instructions` field.
* @return {Promise<Run>} A promise that resolves to the created run object. * @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 tokenCount = // TODO: need to count each content part
const message = await recordMessage({ const message = await recordMessage({
@ -154,14 +157,18 @@ async function saveAssistantMessage(params) {
// tokenCount, // tokenCount,
}); });
await saveConvo(params.user, { await saveConvo(
endpoint: params.endpoint, req,
conversationId: params.conversationId, {
promptPrefix: params.promptPrefix, endpoint: params.endpoint,
instructions: params.instructions, conversationId: params.conversationId,
assistant_id: params.assistant_id, promptPrefix: params.promptPrefix,
model: params.model, instructions: params.instructions,
}); assistant_id: params.assistant_id,
model: params.model,
},
{ context: 'api/server/services/Threads/manage.js #saveAssistantMessage' },
);
return message; return message;
} }
@ -338,10 +345,14 @@ async function syncMessages({
await Promise.all(modifyPromises); await Promise.all(modifyPromises);
await Promise.all(recordPromises); await Promise.all(recordPromises);
await saveConvo(openai.req.user.id, { await saveConvo(
conversationId, openai.req,
file_ids: attached_file_ids, {
}); conversationId,
file_ids: attached_file_ids,
},
{ context: 'api/server/services/Threads/manage.js #syncMessages' },
);
return result; return result;
} }

View file

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

View file

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

View file

@ -107,6 +107,16 @@ export default function useChatFunctions({
const intermediateId = overrideUserMessageId ?? v4(); const intermediateId = overrideUserMessageId ?? v4();
parentMessageId = parentMessageId || latestMessage?.messageId || Constants.NO_PARENT; 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) { if (conversationId == Constants.NEW_CONVO) {
parentMessageId = Constants.NO_PARENT; parentMessageId = Constants.NO_PARENT;
currentMessages = []; currentMessages = [];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ import {
useSetRecoilState, useSetRecoilState,
useRecoilCallback, useRecoilCallback,
} from 'recoil'; } 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 { TMessage, TPreset, TConversation, TSubmission } from 'librechat-data-provider';
import type { TOptionSettings, ExtendedFile } from '~/common'; import type { TOptionSettings, ExtendedFile } from '~/common';
import { storeEndpointSettings, logger } from '~/utils'; import { storeEndpointSettings, logger } from '~/utils';
@ -27,6 +27,14 @@ const submissionKeysAtom = atom<(string | number)[]>({
const latestMessageFamily = atomFamily<TMessage | null, string | number | null>({ const latestMessageFamily = atomFamily<TMessage | null, string | number | null>({
key: 'latestMessageByIndex', key: 'latestMessageByIndex',
default: null, 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>({ const submissionByIndex = atomFamily<TSubmission | null, string | number>({
@ -41,7 +49,7 @@ const latestMessageKeysSelector = selector<(string | number)[]>({
return keys.filter((key) => get(latestMessageFamily(key)) !== null); return keys.filter((key) => get(latestMessageFamily(key)) !== null);
}, },
set: ({ set }, newKeys) => { set: ({ set }, newKeys) => {
logger.log('setting latestMessageKeys', newKeys); logger.log('setting latestMessageKeys', { newKeys });
set(latestMessageKeysAtom, newKeys); set(latestMessageKeysAtom, newKeys);
}, },
}); });
@ -279,19 +287,22 @@ function useClearSubmissionState() {
return clearAllSubmissions; return clearAllSubmissions;
} }
function useClearLatestMessages() { function useClearLatestMessages(context?: string) {
const clearAllLatestMessages = useRecoilCallback( const clearAllLatestMessages = useRecoilCallback(
({ reset, set, snapshot }) => ({ reset, set, snapshot }) =>
async (skipFirst?: boolean) => { async (skipFirst?: boolean) => {
const latestMessageKeys = await snapshot.getPromise(latestMessageKeysSelector); 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) { for (const key of latestMessageKeys) {
if (skipFirst && key == 0) { if (skipFirst && key == 0) {
continue; continue;
} }
logger.log('resetting latest message', key); logger.log(`[clearAllLatestMessages] resetting latest message; key: ${key}`);
reset(latestMessageFamily(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'; import type { TMessage } from 'librechat-data-provider';
export const getLengthAndFirstFiveChars = (str?: string) => { export const getLengthAndLastTenChars = (str?: string): string => {
const length = str ? str.length : 0; if (!str) {
const firstFiveChars = str ? str.substring(0, 5) : ''; return '0';
return `${length}${firstFiveChars}`; }
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) { if (!message) {
return ''; return '';
} }
@ -18,9 +22,24 @@ export const getLatestText = (message?: TMessage | null) => {
for (let i = message.content.length - 1; i >= 0; i--) { for (let i = message.content.length - 1; i >= 0; i--) {
const part = message.content[i]; const part = message.content[i];
if (part.type === ContentTypes.TEXT && part[ContentTypes.TEXT]?.value?.length > 0) { 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 ''; 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}`;
};