diff --git a/client/src/Providers/SetConvoContext.tsx b/client/src/Providers/SetConvoContext.tsx new file mode 100644 index 000000000..bdbc95baa --- /dev/null +++ b/client/src/Providers/SetConvoContext.tsx @@ -0,0 +1,14 @@ +import { createContext, useContext, useRef } from 'react'; +import type { MutableRefObject } from 'react'; + +type SetConvoContext = MutableRefObject; + +export const SetConvoContext = createContext({} as SetConvoContext); + +export const SetConvoProvider = ({ children }: { children: React.ReactNode }) => { + const hasSetConversation = useRef(false); + + return {children}; +}; + +export const useSetConvoContext = () => useContext(SetConvoContext); diff --git a/client/src/Providers/index.ts b/client/src/Providers/index.ts index d777b5bb7..a11da6504 100644 --- a/client/src/Providers/index.ts +++ b/client/src/Providers/index.ts @@ -18,3 +18,4 @@ export * from './AnnouncerContext'; export * from './AgentsMapContext'; export * from './CodeBlockContext'; export * from './ToolCallsMapContext'; +export * from './SetConvoContext'; diff --git a/client/src/components/Auth/__tests__/Login.spec.tsx b/client/src/components/Auth/__tests__/Login.spec.tsx index 308d584a9..7eebb5779 100644 --- a/client/src/components/Auth/__tests__/Login.spec.tsx +++ b/client/src/components/Auth/__tests__/Login.spec.tsx @@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event'; import { getByTestId, render, waitFor } from 'test/layout-test-utils'; import * as mockDataProvider from 'librechat-data-provider/react-query'; import type { TStartupConfig } from 'librechat-data-provider'; +import * as authDataProvider from '~/data-provider/Auth/mutations'; import AuthLayout from '~/components/Auth/AuthLayout'; import Login from '~/components/Auth/Login'; @@ -61,7 +62,7 @@ const setup = ({ }, } = {}) => { const mockUseLoginUser = jest - .spyOn(mockDataProvider, 'useLoginUserMutation') + .spyOn(authDataProvider, 'useLoginUserMutation') //@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult .mockReturnValue(useLoginUserReturnValue); const mockUseGetUserQuery = jest diff --git a/client/src/components/Auth/__tests__/LoginForm.spec.tsx b/client/src/components/Auth/__tests__/LoginForm.spec.tsx index 81d9df96a..913257a00 100644 --- a/client/src/components/Auth/__tests__/LoginForm.spec.tsx +++ b/client/src/components/Auth/__tests__/LoginForm.spec.tsx @@ -2,6 +2,7 @@ import { render, getByTestId } from 'test/layout-test-utils'; import userEvent from '@testing-library/user-event'; import * as mockDataProvider from 'librechat-data-provider/react-query'; import type { TStartupConfig } from 'librechat-data-provider'; +import * as authDataProvider from '~/data-provider/Auth/mutations'; import Login from '../LoginForm'; jest.mock('librechat-data-provider/react-query'); @@ -66,7 +67,7 @@ const setup = ({ }, } = {}) => { const mockUseLoginUser = jest - .spyOn(mockDataProvider, 'useLoginUserMutation') + .spyOn(authDataProvider, 'useLoginUserMutation') //@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult .mockReturnValue(useLoginUserReturnValue); const mockUseGetUserQuery = jest diff --git a/client/src/components/Chat/Messages/SearchButtons.tsx b/client/src/components/Chat/Messages/SearchButtons.tsx index eba93e1b4..c9feada2f 100644 --- a/client/src/components/Chat/Messages/SearchButtons.tsx +++ b/client/src/components/Chat/Messages/SearchButtons.tsx @@ -8,33 +8,34 @@ export default function SearchButtons({ message }: { message: TMessage }) { const localize = useLocalize(); const { searchQueryRes } = useSearchContext(); const { navigateWithLastTools } = useNavigateToConvo(); + const conversationId = message.conversationId ?? ''; - if (!message.conversationId) { + if (!conversationId) { return null; } - const clickHandler = (event: React.MouseEvent) => { + const clickHandler = (event: React.MouseEvent) => { event.preventDefault(); - const conversation = getConversationById(searchQueryRes?.data, message.conversationId); + const conversation = getConversationById(searchQueryRes?.data, conversationId); if (!conversation) { return; } document.title = message.title ?? ''; - navigateWithLastTools(conversation); + navigateWithLastTools(conversation, true, true); }; return ( - ); } diff --git a/client/src/components/Conversations/Convo.tsx b/client/src/components/Conversations/Convo.tsx index 16abb9943..42d878128 100644 --- a/client/src/components/Conversations/Convo.tsx +++ b/client/src/components/Conversations/Convo.tsx @@ -6,7 +6,7 @@ import { Constants } from 'librechat-data-provider'; import { useGetEndpointsQuery } from 'librechat-data-provider/react-query'; import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react'; import type { TConversation } from 'librechat-data-provider'; -import { useConversations, useNavigateToConvo, useMediaQuery, useLocalize } from '~/hooks'; +import { useNavigateToConvo, useMediaQuery, useLocalize } from '~/hooks'; import { useUpdateConversationMutation } from '~/data-provider'; import EndpointIcon from '~/components/Endpoints/EndpointIcon'; import { NotificationSeverity } from '~/common'; @@ -36,7 +36,6 @@ export default function Conversation({ const activeConvos = useRecoilValue(store.allConversationsSelector); const { data: endpointsConfig } = useGetEndpointsQuery(); const { navigateWithLastTools } = useNavigateToConvo(); - const { refreshConversations } = useConversations(); const { showToast } = useToastContext(); const { conversationId, title } = conversation; const inputRef = useRef(null); @@ -97,7 +96,6 @@ export default function Conversation({ updateConvoMutation.mutate( { conversationId, title: titleInput ?? '' }, { - onSuccess: () => refreshConversations(), onError: () => { setTitleInput(title); showToast({ @@ -109,7 +107,7 @@ export default function Conversation({ }, ); }, - [title, titleInput, conversationId, showToast, refreshConversations, updateConvoMutation], + [title, titleInput, conversationId, showToast, updateConvoMutation], ); const handleKeyDown = useCallback( diff --git a/client/src/components/Nav/MobileNav.tsx b/client/src/components/Nav/MobileNav.tsx index 7cbef8c7d..b25809a3b 100644 --- a/client/src/components/Nav/MobileNav.tsx +++ b/client/src/components/Nav/MobileNav.tsx @@ -14,7 +14,7 @@ export default function MobileNav({ }) { const localize = useLocalize(); const queryClient = useQueryClient(); - const { newConversation } = useNewConvo(0); + const { newConversation } = useNewConvo(); const conversation = useRecoilValue(store.conversationByIndex(0)); const { title = 'New Chat' } = conversation || {}; diff --git a/client/src/components/Nav/Nav.tsx b/client/src/components/Nav/Nav.tsx index ec8b1b475..98b630ae6 100644 --- a/client/src/components/Nav/Nav.tsx +++ b/client/src/components/Nav/Nav.tsx @@ -1,6 +1,5 @@ import { useCallback, useEffect, useState, useMemo, memo } from 'react'; import { useRecoilValue } from 'recoil'; -import { useParams } from 'react-router-dom'; import { PermissionTypes, Permissions } from 'librechat-data-provider'; import type { ConversationListResponse } from 'librechat-data-provider'; import { @@ -8,10 +7,8 @@ import { useHasAccess, useMediaQuery, useAuthContext, - useConversation, useLocalStorage, useNavScrolling, - useConversations, } from '~/hooks'; import { useConversationsInfiniteQuery } from '~/data-provider'; import { Conversations } from '~/components/Conversations'; @@ -33,7 +30,6 @@ const Nav = ({ setNavVisible: React.Dispatch>; }) => { const localize = useLocalize(); - const { conversationId } = useParams(); const { isAuthenticated } = useAuthContext(); const [navWidth, setNavWidth] = useState('260px'); @@ -67,11 +63,9 @@ const Nav = ({ } }, [isSmallScreen]); - const { newConversation } = useConversation(); const [showLoading, setShowLoading] = useState(false); const isSearchEnabled = useRecoilValue(store.isSearchEnabled); - const { refreshConversations } = useConversations(); const { pageNumber, searchQuery, setPageNumber, searchQueryRes } = useSearchContext(); const [tags, setTags] = useState([]); const { data, fetchNextPage, hasNextPage, isFetchingNextPage, refetch } = @@ -104,14 +98,6 @@ const Nav = ({ [data, searchQuery, searchQueryRes?.data], ); - const clearSearch = () => { - setPageNumber(1); - refreshConversations(); - if (conversationId == 'search') { - newConversation(); - } - }; - const toggleNavVisible = () => { setNavVisible((prev: boolean) => { localStorage.setItem('navVisible', JSON.stringify(!prev)); @@ -174,7 +160,10 @@ const Nav = ({ subHeaders={ <> {isSearchEnabled === true && ( - + )} {hasAccessToBookmarks === true && ( <> diff --git a/client/src/components/Nav/SearchBar.tsx b/client/src/components/Nav/SearchBar.tsx index 677036ab7..0142f9a12 100644 --- a/client/src/components/Nav/SearchBar.tsx +++ b/client/src/components/Nav/SearchBar.tsx @@ -1,27 +1,39 @@ import debounce from 'lodash/debounce'; import { Search, X } from 'lucide-react'; import { useSetRecoilState } from 'recoil'; +import { useLocation } from 'react-router-dom'; import { QueryKeys } from 'librechat-data-provider'; import { useQueryClient } from '@tanstack/react-query'; import { forwardRef, useState, useCallback, useMemo, Ref } from 'react'; -import { useLocalize } from '~/hooks'; +import { useLocalize, useNewConvo } from '~/hooks'; import { cn } from '~/utils'; import store from '~/store'; type SearchBarProps = { - clearSearch: () => void; isSmallScreen?: boolean; + setPageNumber: React.Dispatch>; }; const SearchBar = forwardRef((props: SearchBarProps, ref: Ref) => { - const { clearSearch, isSmallScreen } = props; + const localize = useLocalize(); + const location = useLocation(); const queryClient = useQueryClient(); + const { setPageNumber, isSmallScreen } = props; + + const [text, setText] = useState(''); + const [showClearIcon, setShowClearIcon] = useState(false); + + const { newConversation } = useNewConvo(); const clearConvoState = store.useClearConvoState(); const setSearchQuery = useSetRecoilState(store.searchQuery); - const [showClearIcon, setShowClearIcon] = useState(false); - const [text, setText] = useState(''); const setIsSearching = useSetRecoilState(store.isSearching); - const localize = useLocalize(); + + const clearSearch = useCallback(() => { + setPageNumber(1); + if (location.pathname.includes('/search')) { + newConversation(); + } + }, [newConversation, setPageNumber, location.pathname]); const clearText = useCallback(() => { setShowClearIcon(false); diff --git a/client/src/components/Nav/SettingsTabs/Data/ClearChats.tsx b/client/src/components/Nav/SettingsTabs/Data/ClearChats.tsx index 745768518..dd7aacc2a 100644 --- a/client/src/components/Nav/SettingsTabs/Data/ClearChats.tsx +++ b/client/src/components/Nav/SettingsTabs/Data/ClearChats.tsx @@ -1,14 +1,13 @@ import React, { useState } from 'react'; import { useClearConversationsMutation } from 'librechat-data-provider/react-query'; import { Label, Button, OGDialog, OGDialogTrigger, Spinner } from '~/components'; -import { useConversation, useConversations, useLocalize } from '~/hooks'; +import { useLocalize, useNewConvo } from '~/hooks'; import OGDialogTemplate from '~/components/ui/OGDialogTemplate'; export const ClearChats = () => { const localize = useLocalize(); const [open, setOpen] = useState(false); - const { newConversation } = useConversation(); - const { refreshConversations } = useConversations(); + const { newConversation } = useNewConvo(); const clearConvosMutation = useClearConversationsMutation(); const clearConvos = () => { @@ -17,7 +16,6 @@ export const ClearChats = () => { { onSuccess: () => { newConversation(); - refreshConversations(); }, }, ); diff --git a/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx b/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx index 7b318e649..c39e8351e 100644 --- a/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx +++ b/client/src/components/Nav/SettingsTabs/Data/ImportConversations.tsx @@ -2,9 +2,9 @@ import { useState, useRef } from 'react'; import { Import } from 'lucide-react'; import type { TError } from 'librechat-data-provider'; import { useUploadConversationsMutation } from '~/data-provider'; -import { useLocalize, useConversations } from '~/hooks'; import { useToastContext } from '~/Providers'; import { Spinner } from '~/components/svg'; +import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; function ImportConversations() { @@ -15,11 +15,9 @@ function ImportConversations() { const [, setErrors] = useState([]); const [allowImport, setAllowImport] = useState(true); const setError = (error: string) => setErrors((prevErrors) => [...prevErrors, error]); - const { refreshConversations } = useConversations(); const uploadFile = useUploadConversationsMutation({ onSuccess: () => { - refreshConversations(); showToast({ message: localize('com_ui_import_conversation_success') }); setAllowImport(true); }, @@ -29,7 +27,7 @@ function ImportConversations() { setError( (error as TError).response?.data?.message ?? 'An error occurred while uploading the file.', ); - if (error?.toString().includes('Unsupported import type')) { + if (error?.toString().includes('Unsupported import type') === true) { showToast({ message: localize('com_ui_import_conversation_file_type_error'), status: 'error', diff --git a/client/src/components/ui/TermsAndConditionsModal.tsx b/client/src/components/ui/TermsAndConditionsModal.tsx index ec0d5c534..bdf6e8fb3 100644 --- a/client/src/components/ui/TermsAndConditionsModal.tsx +++ b/client/src/components/ui/TermsAndConditionsModal.tsx @@ -1,3 +1,5 @@ +import { useMemo } from 'react'; +import type { TTermsOfService } from 'librechat-data-provider'; import MarkdownLite from '~/components/Chat/Messages/Content/MarkdownLite'; import DialogTemplate from '~/components/ui/DialogTemplate'; import { useAcceptTermsMutation } from '~/data-provider'; @@ -19,7 +21,7 @@ const TermsAndConditionsModal = ({ onDecline: () => void; title?: string; contentUrl?: string; - modalContent?: string; + modalContent?: TTermsOfService['modalContent']; }) => { const localize = useLocalize(); const { showToast } = useToastContext(); @@ -49,6 +51,18 @@ const TermsAndConditionsModal = ({ onOpenChange(isOpen); }; + const content = useMemo(() => { + if (typeof modalContent === 'string') { + return modalContent; + } + + if (Array.isArray(modalContent)) { + return modalContent.join('\n'); + } + + return ''; + }, [modalContent]); + return (
- {modalContent != null && modalContent ? ( - + {content !== '' ? ( + ) : (

{localize('com_ui_no_terms_content')}

)} diff --git a/client/src/data-provider/Auth/index.ts b/client/src/data-provider/Auth/index.ts new file mode 100644 index 000000000..d5bc710cf --- /dev/null +++ b/client/src/data-provider/Auth/index.ts @@ -0,0 +1 @@ +export * from './mutations'; diff --git a/client/src/data-provider/Auth/mutations.ts b/client/src/data-provider/Auth/mutations.ts new file mode 100644 index 000000000..28c349e48 --- /dev/null +++ b/client/src/data-provider/Auth/mutations.ts @@ -0,0 +1,65 @@ +import { useResetRecoilState } from 'recoil'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { MutationKeys, dataService } from 'librechat-data-provider'; +import type { UseMutationResult } from '@tanstack/react-query'; +import type * as t from 'librechat-data-provider'; +import useClearStates from '~/hooks/Config/useClearStates'; +import store from '~/store'; + +/* login/logout */ +export const useLogoutUserMutation = ( + options?: t.LogoutOptions, +): UseMutationResult => { + const queryClient = useQueryClient(); + const clearStates = useClearStates(); + const resetDefaultPreset = useResetRecoilState(store.defaultPreset); + + return useMutation([MutationKeys.logoutUser], { + mutationFn: () => dataService.logout(), + ...(options || {}), + onSuccess: (...args) => { + resetDefaultPreset(); + clearStates(); + queryClient.removeQueries(); + options?.onSuccess?.(...args); + }, + }); +}; + +export const useLoginUserMutation = (): UseMutationResult< + t.TLoginResponse, + unknown, + t.TLoginUser, + unknown +> => { + const queryClient = useQueryClient(); + const clearStates = useClearStates(); + const resetDefaultPreset = useResetRecoilState(store.defaultPreset); + return useMutation((payload: t.TLoginUser) => dataService.login(payload), { + onMutate: () => { + resetDefaultPreset(); + clearStates(); + queryClient.removeQueries(); + }, + }); +}; + +/* User */ +export const useDeleteUserMutation = ( + options?: t.MutationOptions, +): UseMutationResult => { + const queryClient = useQueryClient(); + const clearStates = useClearStates(); + const resetDefaultPreset = useResetRecoilState(store.defaultPreset); + + return useMutation([MutationKeys.deleteUser], { + mutationFn: () => dataService.deleteUser(), + ...(options || {}), + onSuccess: (...args) => { + resetDefaultPreset(); + clearStates(); + queryClient.removeQueries(); + options?.onSuccess?.(...args); + }, + }); +}; diff --git a/client/src/data-provider/index.ts b/client/src/data-provider/index.ts index 3d5039f1d..5e56a113c 100644 --- a/client/src/data-provider/index.ts +++ b/client/src/data-provider/index.ts @@ -1,3 +1,4 @@ +export * from './Auth'; export * from './Agents'; export * from './Files'; export * from './Tools'; diff --git a/client/src/data-provider/mutations.ts b/client/src/data-provider/mutations.ts index e0c717e5e..6d952bb06 100644 --- a/client/src/data-provider/mutations.ts +++ b/client/src/data-provider/mutations.ts @@ -1,11 +1,9 @@ import { Constants, - LocalStorageKeys, InfiniteCollections, defaultAssistantsVersion, ConversationListResponse, } from 'librechat-data-provider'; -import { useSetRecoilState } from 'recoil'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { dataService, MutationKeys, QueryKeys, defaultOrderQuery } from 'librechat-data-provider'; import type * as t from 'librechat-data-provider'; @@ -13,7 +11,6 @@ import type { InfiniteData, UseMutationResult } from '@tanstack/react-query'; import useUpdateTagsInConvo from '~/hooks/Conversations/useUpdateTagsInConvo'; import { updateConversationTag } from '~/utils/conversationTags'; import { normalizeData } from '~/utils/collection'; -import store from '~/store'; import { useConversationTagsQuery, useConversationsInfiniteQuery, @@ -692,34 +689,6 @@ export const useDeletePresetMutation = ( }); }; -/* login/logout */ -export const useLogoutUserMutation = ( - options?: t.LogoutOptions, -): UseMutationResult => { - const queryClient = useQueryClient(); - const setDefaultPreset = useSetRecoilState(store.defaultPreset); - return useMutation([MutationKeys.logoutUser], { - mutationFn: () => dataService.logout(), - - ...(options || {}), - onSuccess: (...args) => { - options?.onSuccess?.(...args); - }, - onMutate: (...args) => { - setDefaultPreset(null); - queryClient.removeQueries(); - localStorage.removeItem(LocalStorageKeys.LAST_CONVO_SETUP); - localStorage.removeItem(`${LocalStorageKeys.LAST_CONVO_SETUP}_0`); - localStorage.removeItem(`${LocalStorageKeys.LAST_CONVO_SETUP}_1`); - localStorage.removeItem(LocalStorageKeys.LAST_MODEL); - localStorage.removeItem(LocalStorageKeys.LAST_TOOLS); - localStorage.removeItem(LocalStorageKeys.FILES_TO_DELETE); - // localStorage.removeItem('lastAssistant'); - options?.onMutate?.(...args); - }, - }); -}; - /* Avatar upload */ export const useUploadAvatarMutation = ( options?: t.UploadAvatarOptions, @@ -735,32 +704,6 @@ export const useUploadAvatarMutation = ( }); }; -export const useDeleteUserMutation = ( - options?: t.MutationOptions, -): UseMutationResult => { - const queryClient = useQueryClient(); - const setDefaultPreset = useSetRecoilState(store.defaultPreset); - return useMutation([MutationKeys.deleteUser], { - mutationFn: () => dataService.deleteUser(), - - ...(options || {}), - onSuccess: (...args) => { - options?.onSuccess?.(...args); - }, - onMutate: (...args) => { - setDefaultPreset(null); - queryClient.removeQueries(); - localStorage.removeItem(LocalStorageKeys.LAST_CONVO_SETUP); - localStorage.removeItem(`${LocalStorageKeys.LAST_CONVO_SETUP}_0`); - localStorage.removeItem(`${LocalStorageKeys.LAST_CONVO_SETUP}_1`); - localStorage.removeItem(LocalStorageKeys.LAST_MODEL); - localStorage.removeItem(LocalStorageKeys.LAST_TOOLS); - localStorage.removeItem(LocalStorageKeys.FILES_TO_DELETE); - options?.onMutate?.(...args); - }, - }); -}; - /* Speech to text */ export const useSpeechToTextMutation = ( options?: t.SpeechToTextOptions, diff --git a/client/src/hooks/AuthContext.tsx b/client/src/hooks/AuthContext.tsx index 5624df138..9c94a0bce 100644 --- a/client/src/hooks/AuthContext.tsx +++ b/client/src/hooks/AuthContext.tsx @@ -10,14 +10,10 @@ import { import { useRecoilState } from 'recoil'; import { useNavigate } from 'react-router-dom'; import { setTokenHeader, SystemRoles } from 'librechat-data-provider'; -import { - useGetUserQuery, - useLoginUserMutation, - useRefreshTokenMutation, -} from 'librechat-data-provider/react-query'; +import { useGetUserQuery, useRefreshTokenMutation } from 'librechat-data-provider/react-query'; import type { TLoginResponse, TLoginUser } from 'librechat-data-provider'; +import { useLoginUserMutation, useLogoutUserMutation, useGetRole } from '~/data-provider'; import { TAuthConfig, TUserContext, TAuthContext, TResError } from '~/common'; -import { useLogoutUserMutation, useGetRole } from '~/data-provider'; import useTimeout from './useTimeout'; import store from '~/store'; diff --git a/client/src/hooks/Config/index.ts b/client/src/hooks/Config/index.ts index 8be1beefd..88e4a5716 100644 --- a/client/src/hooks/Config/index.ts +++ b/client/src/hooks/Config/index.ts @@ -1 +1,2 @@ export { default as useAppStartup } from './useAppStartup'; +export { default as useClearStates } from './useClearStates'; diff --git a/client/src/hooks/Config/useClearStates.ts b/client/src/hooks/Config/useClearStates.ts new file mode 100644 index 000000000..d832d08af --- /dev/null +++ b/client/src/hooks/Config/useClearStates.ts @@ -0,0 +1,52 @@ +import { useRecoilCallback } from 'recoil'; +import { clearLocalStorage } from '~/utils/localStorage'; +import store from '~/store'; + +export default function useClearStates() { + const clearConversations = store.useClearConvoState(); + const clearSubmissions = store.useClearSubmissionState(); + const clearLatestMessages = store.useClearLatestMessages(); + + const clearStates = useRecoilCallback( + ({ reset, snapshot }) => + async (skipFirst?: boolean) => { + await clearSubmissions(skipFirst); + await clearConversations(skipFirst); + await clearLatestMessages(skipFirst); + + const keys = await snapshot.getPromise(store.conversationKeysAtom); + + for (const key of keys) { + if (skipFirst === true && key === 0) { + continue; + } + + reset(store.filesByIndex(key)); + reset(store.presetByIndex(key)); + reset(store.textByIndex(key)); + reset(store.showStopButtonByIndex(key)); + reset(store.abortScrollFamily(key)); + reset(store.isSubmittingFamily(key)); + reset(store.optionSettingsFamily(key)); + reset(store.showAgentSettingsFamily(key)); + reset(store.showBingToneSettingFamily(key)); + reset(store.showPopoverFamily(key)); + reset(store.showMentionPopoverFamily(key)); + reset(store.showPlusPopoverFamily(key)); + reset(store.showPromptsPopoverFamily(key)); + reset(store.activePromptByIndex(key)); + reset(store.globalAudioURLFamily(key)); + reset(store.globalAudioFetchingFamily(key)); + reset(store.globalAudioPlayingFamily(key)); + reset(store.activeRunFamily(key)); + reset(store.audioRunFamily(key)); + reset(store.messagesSiblingIdxFamily(key.toString())); + } + + clearLocalStorage(skipFirst); + }, + [], + ); + + return clearStates; +} diff --git a/client/src/hooks/Config/useConfigOverride.ts b/client/src/hooks/Config/useConfigOverride.ts index f31a55c25..977cb8661 100644 --- a/client/src/hooks/Config/useConfigOverride.ts +++ b/client/src/hooks/Config/useConfigOverride.ts @@ -8,7 +8,7 @@ import store from '~/store'; type TempOverrideType = Record & { endpointsConfig: TEndpointsConfig; - modelsConfig: TModelsConfig; + modelsConfig?: TModelsConfig; combinedOptions: unknown[]; combined: boolean; }; @@ -38,7 +38,7 @@ export default function useConfigOverride() { ); useEffect(() => { - if (overrideQuery.data) { + if (overrideQuery.data != null) { handleOverride(overrideQuery.data); } }, [overrideQuery.data, handleOverride]); diff --git a/client/src/hooks/Conversations/index.ts b/client/src/hooks/Conversations/index.ts index 4d14b42b9..b580e504f 100644 --- a/client/src/hooks/Conversations/index.ts +++ b/client/src/hooks/Conversations/index.ts @@ -2,9 +2,7 @@ export { default as useSearch } from './useSearch'; export { default as usePresets } from './usePresets'; export { default as useGetSender } from './useGetSender'; export { default as useDefaultConvo } from './useDefaultConvo'; -export { default as useConversation } from './useConversation'; export { default as useGenerateConvo } from './useGenerateConvo'; -export { default as useConversations } from './useConversations'; export { default as useArchiveHandler } from './useArchiveHandler'; export { default as useDebouncedInput } from './useDebouncedInput'; export { default as useBookmarkSuccess } from './useBookmarkSuccess'; diff --git a/client/src/hooks/Conversations/useArchiveHandler.ts b/client/src/hooks/Conversations/useArchiveHandler.ts index a0a1fd742..d4bf3510e 100644 --- a/client/src/hooks/Conversations/useArchiveHandler.ts +++ b/client/src/hooks/Conversations/useArchiveHandler.ts @@ -1,7 +1,6 @@ import { useParams, useNavigate } from 'react-router-dom'; import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react'; import { useArchiveConversationMutation } from '~/data-provider'; -import useConversations from './useConversations'; import { NotificationSeverity } from '~/common'; import { useToastContext } from '~/Providers'; import useLocalize from '../useLocalize'; @@ -16,7 +15,6 @@ export default function useArchiveHandler( const navigate = useNavigate(); const { showToast } = useToastContext(); const { newConversation } = useNewConvo(); - const { refreshConversations } = useConversations(); const { conversationId: currentConvoId } = useParams(); const archiveConvoMutation = useArchiveConversationMutation(conversationId ?? ''); @@ -38,7 +36,6 @@ export default function useArchiveHandler( newConversation(); navigate('/c/new', { replace: true }); } - refreshConversations(); retainView(); }, onError: () => { diff --git a/client/src/hooks/Conversations/useConversation.ts b/client/src/hooks/Conversations/useConversation.ts deleted file mode 100644 index 3dc99f80a..000000000 --- a/client/src/hooks/Conversations/useConversation.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { useCallback } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { QueryKeys } from 'librechat-data-provider'; -import { useQueryClient } from '@tanstack/react-query'; -import { useSetRecoilState, useResetRecoilState, useRecoilCallback } from 'recoil'; -import { useGetEndpointsQuery, useGetModelsQuery } from 'librechat-data-provider/react-query'; -import type { - TConversation, - TMessagesAtom, - TSubmission, - TPreset, - TModelsConfig, - TEndpointsConfig, -} from 'librechat-data-provider'; -import { buildDefaultConvo, getDefaultEndpoint, getEndpointField, logger } from '~/utils'; -import store from '~/store'; - -const useConversation = () => { - const navigate = useNavigate(); - const queryClient = useQueryClient(); - const setConversation = useSetRecoilState(store.conversation); - const resetLatestMessage = useResetRecoilState(store.latestMessage); - const setMessages = useSetRecoilState(store.messages); - const setSubmission = useSetRecoilState(store.submission); - const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery(); - const modelsQuery = useGetModelsQuery(); - - const switchToConversation = useRecoilCallback( - () => - async ( - conversation: TConversation, - messages: TMessagesAtom = null, - preset: TPreset | null = null, - modelsData?: TModelsConfig, - ) => { - const modelsConfig = modelsData ?? modelsQuery.data; - const { endpoint = null } = conversation; - - if (endpoint === null) { - const defaultEndpoint = getDefaultEndpoint({ - convoSetup: preset ?? conversation, - endpointsConfig, - }); - - const endpointType = getEndpointField(endpointsConfig, defaultEndpoint, 'type'); - if (!conversation.endpointType && endpointType) { - conversation.endpointType = endpointType; - } - - const models = modelsConfig?.[defaultEndpoint] ?? []; - conversation = buildDefaultConvo({ - conversation, - lastConversationSetup: preset as TConversation, - endpoint: defaultEndpoint, - models, - }); - } - - setConversation(conversation); - 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']); - navigate('/c/new'); - } - }, - [endpointsConfig, modelsQuery.data], - ); - - const newConversation = useCallback( - (template = {}, preset?: TPreset, modelsData?: TModelsConfig) => { - switchToConversation( - { - conversationId: 'new', - title: 'New Chat', - ...template, - endpoint: null, - createdAt: '', - updatedAt: '', - }, - [], - preset, - modelsData, - ); - }, - [switchToConversation], - ); - - const searchPlaceholderConversation = useCallback(() => { - switchToConversation( - { - conversationId: 'search', - title: 'Search', - endpoint: null, - createdAt: '', - updatedAt: '', - }, - [], - ); - }, [switchToConversation]); - - return { - switchToConversation, - newConversation, - searchPlaceholderConversation, - }; -}; - -export default useConversation; diff --git a/client/src/hooks/Conversations/useConversations.ts b/client/src/hooks/Conversations/useConversations.ts deleted file mode 100644 index 1f74bf1c7..000000000 --- a/client/src/hooks/Conversations/useConversations.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { useSetRecoilState } from 'recoil'; -import { useCallback } from 'react'; -import store from '~/store'; - -const useConversations = () => { - const setRefreshConversationsHint = useSetRecoilState(store.refreshConversationsHint); - - const refreshConversations = useCallback(() => { - setRefreshConversationsHint((prevState) => prevState + 1); - }, [setRefreshConversationsHint]); - - return { refreshConversations }; -}; - -export default useConversations; diff --git a/client/src/hooks/Conversations/useNavigateToConvo.tsx b/client/src/hooks/Conversations/useNavigateToConvo.tsx index c86fd70d7..5b5a8c033 100644 --- a/client/src/hooks/Conversations/useNavigateToConvo.tsx +++ b/client/src/hooks/Conversations/useNavigateToConvo.tsx @@ -12,20 +12,29 @@ const useNavigateToConvo = (index = 0) => { const clearAllConversations = store.useClearConvoState(); const clearAllLatestMessages = store.useClearLatestMessages(`useNavigateToConvo ${index}`); const setSubmission = useSetRecoilState(store.submissionByIndex(index)); - const { setConversation } = store.useCreateConversationAtom(index); + const { hasSetConversation, setConversation } = store.useCreateConversationAtom(index); - const navigateToConvo = (conversation: TConversation, _resetLatestMessage = true) => { + const navigateToConvo = ( + conversation?: TConversation | null, + _resetLatestMessage = true, + invalidateMessages = false, + ) => { if (!conversation) { console.log('Conversation not provided'); return; } + hasSetConversation.current = true; setSubmission(null); if (_resetLatestMessage) { clearAllLatestMessages(); } + if (invalidateMessages && conversation.conversationId != null && conversation.conversationId) { + queryClient.setQueryData([QueryKeys.messages, Constants.NEW_CONVO], []); + queryClient.invalidateQueries([QueryKeys.messages, conversation.conversationId]); + } let convo = { ...conversation }; - if (!convo?.endpoint) { + if (!convo.endpoint) { /* undefined endpoint edge case */ const modelsConfig = queryClient.getQueryData([QueryKeys.models]); const endpointsConfig = queryClient.getQueryData([QueryKeys.endpoints]); @@ -53,9 +62,17 @@ const useNavigateToConvo = (index = 0) => { navigate(`/c/${convo.conversationId ?? Constants.NEW_CONVO}`); }; - const navigateWithLastTools = (conversation: TConversation, _resetLatestMessage?: boolean) => { + const navigateWithLastTools = ( + conversation?: TConversation | null, + _resetLatestMessage?: boolean, + invalidateMessages?: boolean, + ) => { + if (!conversation) { + console.log('Conversation not provided'); + return; + } // set conversation to the new conversation - if (conversation?.endpoint === EModelEndpoint.gptPlugins) { + if (conversation.endpoint === EModelEndpoint.gptPlugins) { let lastSelectedTools = []; try { lastSelectedTools = @@ -63,15 +80,17 @@ const useNavigateToConvo = (index = 0) => { } catch (e) { // console.error(e); } + const hasTools = (conversation.tools?.length ?? 0) > 0; navigateToConvo( { ...conversation, - tools: conversation?.tools?.length ? conversation?.tools : lastSelectedTools, + tools: hasTools ? conversation.tools : lastSelectedTools, }, _resetLatestMessage, + invalidateMessages, ); } else { - navigateToConvo(conversation, _resetLatestMessage); + navigateToConvo(conversation, _resetLatestMessage, invalidateMessages); } }; diff --git a/client/src/hooks/Conversations/useSearch.ts b/client/src/hooks/Conversations/useSearch.ts index 5bcefd429..75b15148a 100644 --- a/client/src/hooks/Conversations/useSearch.ts +++ b/client/src/hooks/Conversations/useSearch.ts @@ -5,14 +5,23 @@ import { useGetSearchEnabledQuery } from 'librechat-data-provider/react-query'; import type { UseInfiniteQueryResult } from '@tanstack/react-query'; import type { ConversationListResponse } from 'librechat-data-provider'; import { useSearchInfiniteQuery } from '~/data-provider'; -import useConversation from './useConversation'; +import useNewConvo from '~/hooks/useNewConvo'; import store from '~/store'; export default function useSearchMessages({ isAuthenticated }: { isAuthenticated: boolean }) { const navigate = useNavigate(); const location = useLocation(); const [pageNumber, setPageNumber] = useState(1); - const { searchPlaceholderConversation } = useConversation(); + const { switchToConversation } = useNewConvo(); + const searchPlaceholderConversation = useCallback(() => { + switchToConversation({ + conversationId: 'search', + title: 'Search', + endpoint: null, + createdAt: '', + updatedAt: '', + }); + }, [switchToConversation]); const searchQuery = useRecoilValue(store.searchQuery); const setIsSearchEnabled = useSetRecoilState(store.isSearchEnabled); diff --git a/client/src/hooks/SSE/useSSE.ts b/client/src/hooks/SSE/useSSE.ts index 3ee0e9d50..8b81247d5 100644 --- a/client/src/hooks/SSE/useSSE.ts +++ b/client/src/hooks/SSE/useSSE.ts @@ -81,7 +81,7 @@ export default function useSSE( }); useEffect(() => { - if (submission === null || Object.keys(submission).length === 0) { + if (submission == null || Object.keys(submission).length === 0) { return; } diff --git a/client/src/hooks/useNewConvo.ts b/client/src/hooks/useNewConvo.ts index 9dbb98ebb..2c02a84a6 100644 --- a/client/src/hooks/useNewConvo.ts +++ b/client/src/hooks/useNewConvo.ts @@ -117,7 +117,11 @@ const useNewConvo = (index = 0) => { ) ?? assistants[0]?.id; } - if (currentAssistantId && isAssistantEndpoint && conversation.conversationId === Constants.NEW_CONVO) { + if ( + currentAssistantId && + isAssistantEndpoint && + conversation.conversationId === Constants.NEW_CONVO + ) { const assistant = assistants.find((asst) => asst.id === currentAssistantId); conversation.model = assistant?.model; updateLastSelectedModel({ @@ -168,7 +172,7 @@ const useNewConvo = (index = 0) => { ); const newConversation = useCallback( - ({ + function createNewConvo({ template: _template = {}, preset: _preset, modelsData, @@ -182,7 +186,7 @@ const useNewConvo = (index = 0) => { buildDefault?: boolean; keepLatestMessage?: boolean; keepAddedConvos?: boolean; - } = {}) => { + } = {}) { pauseGlobalAudio(); const templateConvoId = _template.conversationId ?? ''; diff --git a/client/src/routes/ChatRoute.tsx b/client/src/routes/ChatRoute.tsx index e266af23b..81ff3dbd5 100644 --- a/client/src/routes/ChatRoute.tsx +++ b/client/src/routes/ChatRoute.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef } from 'react'; +import { useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { Constants, EModelEndpoint } from 'librechat-data-provider'; import { @@ -25,9 +25,8 @@ export default function ChatRoute() { const index = 0; const { conversationId = '' } = useParams(); - const { conversation } = store.useCreateConversationAtom(index); + const { hasSetConversation, conversation } = store.useCreateConversationAtom(index); const { newConversation } = useNewConvo(); - const hasSetConversation = useRef(false); const modelsQuery = useGetModelsQuery({ enabled: isAuthenticated, diff --git a/client/src/routes/Root.tsx b/client/src/routes/Root.tsx index d980ce2f8..242825b45 100644 --- a/client/src/routes/Root.tsx +++ b/client/src/routes/Root.tsx @@ -1,30 +1,35 @@ import React, { useState, useEffect } from 'react'; import { Outlet, useNavigate } from 'react-router-dom'; import { useGetStartupConfig } from 'librechat-data-provider/react-query'; -import { useUserTermsQuery } from '~/data-provider'; - import type { ContextType } from '~/common'; -import { AgentsMapContext, AssistantsMapContext, FileMapContext, SearchContext } from '~/Providers'; +import { + AgentsMapContext, + AssistantsMapContext, + FileMapContext, + SearchContext, + SetConvoProvider, +} from '~/Providers'; import { useAuthContext, useAssistantsMap, useAgentsMap, useFileMap, useSearch } from '~/hooks'; -import { Nav, MobileNav } from '~/components/Nav'; import TermsAndConditionsModal from '~/components/ui/TermsAndConditionsModal'; +import { useUserTermsQuery } from '~/data-provider'; +import { Nav, MobileNav } from '~/components/Nav'; import { Banner } from '~/components/Banners'; export default function Root() { - const { isAuthenticated, logout } = useAuthContext(); const navigate = useNavigate(); + const [showTerms, setShowTerms] = useState(false); + const [bannerHeight, setBannerHeight] = useState(0); const [navVisible, setNavVisible] = useState(() => { const savedNavVisible = localStorage.getItem('navVisible'); return savedNavVisible !== null ? JSON.parse(savedNavVisible) : true; }); - const [bannerHeight, setBannerHeight] = useState(0); - const search = useSearch({ isAuthenticated }); - const fileMap = useFileMap({ isAuthenticated }); + const { isAuthenticated, logout } = useAuthContext(); const assistantsMap = useAssistantsMap({ isAuthenticated }); const agentsMap = useAgentsMap({ isAuthenticated }); + const fileMap = useFileMap({ isAuthenticated }); + const search = useSearch({ isAuthenticated }); - const [showTerms, setShowTerms] = useState(false); const { data: config } = useGetStartupConfig(); const { data: termsData } = useUserTermsQuery({ enabled: isAuthenticated && config?.interface?.termsOfService?.modalAcceptance === true, @@ -51,33 +56,35 @@ export default function Root() { } return ( - - - - - -
-
-