LibreChat/client/src/store/families.ts
Danny Avila 63926cb874
🪦 refactor: Remove Legacy Code (#10533)
* 🗑️ chore: Remove unused Legacy Provider clients and related helpers

* Deleted OpenAIClient and GoogleClient files along with their associated tests.
* Removed references to these clients in the clients index file.
* Cleaned up typedefs by removing the OpenAISpecClient export.
* Updated chat controllers to use the OpenAI SDK directly instead of the removed client classes.

* chore/remove-openapi-specs

* 🗑️ chore: Remove unused mergeSort and misc utility functions

* Deleted mergeSort.js and misc.js files as they are no longer needed.
* Removed references to cleanUpPrimaryKeyValue in messages.js and adjusted related logic.
* Updated mongoMeili.ts to eliminate local implementations of removed functions.

* chore: remove legacy endpoints

* chore: remove all plugins endpoint related code

* chore: remove unused prompt handling code and clean up imports

* Deleted handleInputs.js and instructions.js files as they are no longer needed.
* Removed references to these files in the prompts index.js.
* Updated docker-compose.yml to simplify reverse proxy configuration.

* chore: remove unused LightningIcon import from Icons.tsx

* chore: clean up translation.json by removing deprecated and unused keys

* chore: update Jest configuration and remove unused mock file

    * Simplified the setupFiles array in jest.config.js by removing the fetchEventSource mock.
    * Deleted the fetchEventSource.js mock file as it is no longer needed.

* fix: simplify endpoint type check in Landing and ConversationStarters components

    * Updated the endpoint type check to use strict equality for better clarity and performance.
    * Ensured consistency in the handling of the azureOpenAI endpoint across both components.

* chore: remove unused dependencies from package.json and package-lock.json

* chore: remove legacy EditController, associated routes and imports

* chore: update banResponse logic to refine request handling for banned users

* chore: remove unused validateEndpoint middleware and its references

* chore: remove unused 'res' parameter from initializeClient in multiple endpoint files

* chore: remove unused 'isSmallScreen' prop from BookmarkNav and NewChat components; clean up imports in ArchivedChatsTable and useSetIndexOptions hooks; enhance localization in PromptVersions

* chore: remove unused import of Constants and TMessage from MobileNav; retain only necessary QueryKeys import

* chore: remove unused TResPlugin type and related references; clean up imports in types and schemas
2025-11-26 11:47:28 -05:00

421 lines
12 KiB
TypeScript

import { useEffect } from 'react';
import {
atom,
selector,
atomFamily,
DefaultValue,
selectorFamily,
useRecoilState,
useRecoilValue,
useSetRecoilState,
useRecoilCallback,
} from 'recoil';
import { LocalStorageKeys, Constants } from 'librechat-data-provider';
import type { TMessage, TPreset, TConversation, TSubmission } from 'librechat-data-provider';
import type { TOptionSettings, ExtendedFile } from '~/common';
import { useSetConvoContext } from '~/Providers/SetConvoContext';
import { storeEndpointSettings, logger, createChatSearchParams } from '~/utils';
import { createSearchParams } from 'react-router-dom';
const latestMessageKeysAtom = atom<(string | number)[]>({
key: 'latestMessageKeys',
default: [],
});
const submissionKeysAtom = atom<(string | number)[]>({
key: 'submissionKeys',
default: [],
});
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>({
key: 'submissionByIndex',
default: null,
});
const latestMessageKeysSelector = selector<(string | number)[]>({
key: 'latestMessageKeysSelector',
get: ({ get }) => {
const keys = get(conversationKeysAtom);
return keys.filter((key) => get(latestMessageFamily(key)) !== null);
},
set: ({ set }, newKeys) => {
logger.log('setting latestMessageKeys', { newKeys });
set(latestMessageKeysAtom, newKeys);
},
});
const submissionKeysSelector = selector<(string | number)[]>({
key: 'submissionKeysSelector',
get: ({ get }) => {
const keys = get(conversationKeysAtom);
return keys.filter((key) => get(submissionByIndex(key)) !== null);
},
set: ({ set }, newKeys) => {
logger.log('setting submissionKeysAtom', newKeys);
set(submissionKeysAtom, newKeys);
},
});
const conversationByIndex = atomFamily<TConversation | null, string | number>({
key: 'conversationByIndex',
default: null,
effects: [
({ onSet, node }) => {
onSet(async (newValue, oldValue) => {
const index = Number(node.key.split('__')[1]);
logger.log('conversation', 'Setting conversation:', { index, newValue, oldValue });
if (newValue?.assistant_id != null && newValue.assistant_id) {
localStorage.setItem(
`${LocalStorageKeys.ASST_ID_PREFIX}${index}${newValue.endpoint}`,
newValue.assistant_id,
);
}
if (newValue?.agent_id != null && newValue.agent_id) {
localStorage.setItem(`${LocalStorageKeys.AGENT_ID_PREFIX}${index}`, newValue.agent_id);
}
if (newValue?.spec != null && newValue.spec) {
localStorage.setItem(LocalStorageKeys.LAST_SPEC, newValue.spec);
}
if (newValue?.tools && Array.isArray(newValue.tools)) {
localStorage.setItem(
LocalStorageKeys.LAST_TOOLS,
JSON.stringify(newValue.tools.filter((el) => !!el)),
);
}
if (!newValue) {
return;
}
storeEndpointSettings(newValue);
localStorage.setItem(
`${LocalStorageKeys.LAST_CONVO_SETUP}_${index}`,
JSON.stringify(newValue),
);
const disableParams = newValue.disableParams === true;
const shouldUpdateParams =
index === 0 &&
!disableParams &&
newValue.createdAt === '' &&
JSON.stringify(newValue) !== JSON.stringify(oldValue) &&
(oldValue as TConversation)?.conversationId === Constants.NEW_CONVO;
if (shouldUpdateParams) {
const newParams = createChatSearchParams(newValue);
const searchParams = createSearchParams(newParams);
const url = `${window.location.pathname}?${searchParams.toString()}`;
window.history.pushState({}, '', url);
}
});
},
] as const,
});
const filesByIndex = atomFamily<Map<string, ExtendedFile>, string | number>({
key: 'filesByIndex',
default: new Map(),
});
const conversationKeysAtom = atom<(string | number)[]>({
key: 'conversationKeys',
default: [],
});
const allConversationsSelector = selector({
key: 'allConversationsSelector',
get: ({ get }) => {
const keys = get(conversationKeysAtom);
return keys.map((key) => get(conversationByIndex(key))).map((convo) => convo?.conversationId);
},
});
const presetByIndex = atomFamily<TPreset | null, string | number>({
key: 'presetByIndex',
default: null,
});
const textByIndex = atomFamily<string, string | number>({
key: 'textByIndex',
default: '',
});
const showStopButtonByIndex = atomFamily<boolean, string | number>({
key: 'showStopButtonByIndex',
default: false,
});
const abortScrollFamily = atomFamily<boolean, string | number>({
key: 'abortScrollByIndex',
default: false,
effects: [
({ onSet, node }) => {
onSet(async (newValue) => {
const key = Number(node.key.split(Constants.COMMON_DIVIDER)[1]);
logger.log('message_scrolling', 'Recoil Effect: Setting abortScrollByIndex', {
key,
newValue,
});
});
},
] as const,
});
const isSubmittingFamily = atomFamily({
key: 'isSubmittingByIndex',
default: false,
effects: [
({ onSet, node }) => {
onSet(async (newValue) => {
const key = Number(node.key.split(Constants.COMMON_DIVIDER)[1]);
logger.log('message_stream', 'Recoil Effect: Setting isSubmittingByIndex', {
key,
newValue,
});
});
},
],
});
const anySubmittingSelector = selector<boolean>({
key: 'anySubmittingSelector',
get: ({ get }) => {
const keys = get(conversationKeysAtom);
return keys.some((key) => get(isSubmittingFamily(key)) === true);
},
});
const optionSettingsFamily = atomFamily<TOptionSettings, string | number>({
key: 'optionSettingsByIndex',
default: {},
});
const showPopoverFamily = atomFamily({
key: 'showPopoverByIndex',
default: false,
});
const activePromptByIndex = atomFamily<string | undefined, string | number | null>({
key: 'activePromptByIndex',
default: undefined,
});
const showMentionPopoverFamily = atomFamily<boolean, string | number | null>({
key: 'showMentionPopoverByIndex',
default: false,
});
const showPlusPopoverFamily = atomFamily<boolean, string | number | null>({
key: 'showPlusPopoverByIndex',
default: false,
});
const showPromptsPopoverFamily = atomFamily<boolean, string | number | null>({
key: 'showPromptsPopoverByIndex',
default: false,
});
const globalAudioURLFamily = atomFamily<string | null, string | number | null>({
key: 'globalAudioURLByIndex',
default: null,
});
const globalAudioFetchingFamily = atomFamily<boolean, string | number | null>({
key: 'globalAudioisFetchingByIndex',
default: false,
});
const globalAudioPlayingFamily = atomFamily<boolean, string | number | null>({
key: 'globalAudioisPlayingByIndex',
default: false,
});
const activeRunFamily = atomFamily<string | null, string | number | null>({
key: 'activeRunByIndex',
default: null,
});
const audioRunFamily = atomFamily<string | null, string | number | null>({
key: 'audioRunByIndex',
default: null,
});
const messagesSiblingIdxFamily = atomFamily<number, string | null | undefined>({
key: 'messagesSiblingIdx',
default: 0,
});
function useCreateConversationAtom(key: string | number) {
const hasSetConversation = useSetConvoContext();
const [keys, setKeys] = useRecoilState(conversationKeysAtom);
const setConversation = useSetRecoilState(conversationByIndex(key));
const conversation = useRecoilValue(conversationByIndex(key));
useEffect(() => {
if (!keys.includes(key)) {
setKeys([...keys, key]);
}
}, [key, keys, setKeys]);
return { hasSetConversation, conversation, setConversation };
}
function useClearConvoState() {
/** Clears all active conversations. Pass `true` to skip the first or root conversation */
const clearAllConversations = useRecoilCallback(
({ reset, snapshot }) =>
async (skipFirst?: boolean) => {
const conversationKeys = await snapshot.getPromise(conversationKeysAtom);
for (const conversationKey of conversationKeys) {
if (skipFirst === true && conversationKey == 0) {
continue;
}
reset(conversationByIndex(conversationKey));
const conversation = await snapshot.getPromise(conversationByIndex(conversationKey));
if (conversation) {
reset(latestMessageFamily(conversationKey));
}
}
reset(conversationKeysAtom);
},
[],
);
return clearAllConversations;
}
const conversationByKeySelector = selectorFamily({
key: 'conversationByKeySelector',
get:
(index: string | number) =>
({ get }) => {
const conversation = get(conversationByIndex(index));
return conversation;
},
});
function useClearSubmissionState() {
const clearAllSubmissions = useRecoilCallback(
({ reset, set, snapshot }) =>
async (skipFirst?: boolean) => {
const submissionKeys = await snapshot.getPromise(submissionKeysSelector);
logger.log('submissionKeys', submissionKeys);
for (const key of submissionKeys) {
if (skipFirst === true && key == 0) {
continue;
}
logger.log('resetting submission', key);
reset(submissionByIndex(key));
}
set(submissionKeysSelector, []);
},
[],
);
return clearAllSubmissions;
}
function useClearLatestMessages(context?: string) {
const clearAllLatestMessages = useRecoilCallback(
({ reset, set, snapshot }) =>
async (skipFirst?: boolean) => {
const latestMessageKeys = await snapshot.getPromise(latestMessageKeysSelector);
logger.log('[clearAllLatestMessages] latestMessageKeys', latestMessageKeys);
if (context != null && context) {
logger.log(`[clearAllLatestMessages] context: ${context}`);
}
for (const key of latestMessageKeys) {
if (skipFirst === true && key == 0) {
continue;
}
logger.log(`[clearAllLatestMessages] resetting latest message; key: ${key}`);
reset(latestMessageFamily(key));
}
set(latestMessageKeysSelector, []);
},
[],
);
return clearAllLatestMessages;
}
const updateConversationSelector = selectorFamily({
key: 'updateConversationSelector',
get: () => () => null as Partial<TConversation> | null,
set:
(conversationId: string) =>
({ set, get }, newPartialConversation) => {
if (newPartialConversation instanceof DefaultValue) {
return;
}
const keys = get(conversationKeysAtom);
keys.forEach((key) => {
set(conversationByIndex(key), (prevConversation) => {
if (prevConversation && prevConversation.conversationId === conversationId) {
return {
...prevConversation,
...newPartialConversation,
};
}
return prevConversation;
});
});
},
});
export default {
conversationKeysAtom,
conversationByIndex,
filesByIndex,
presetByIndex,
submissionByIndex,
textByIndex,
showStopButtonByIndex,
abortScrollFamily,
isSubmittingFamily,
optionSettingsFamily,
showPopoverFamily,
latestMessageFamily,
messagesSiblingIdxFamily,
anySubmittingSelector,
allConversationsSelector,
conversationByKeySelector,
useClearConvoState,
useCreateConversationAtom,
showMentionPopoverFamily,
globalAudioURLFamily,
activeRunFamily,
audioRunFamily,
globalAudioPlayingFamily,
globalAudioFetchingFamily,
showPlusPopoverFamily,
activePromptByIndex,
useClearSubmissionState,
useClearLatestMessages,
showPromptsPopoverFamily,
updateConversationSelector,
};