diff --git a/api/app/clients/AnthropicClient.js b/api/app/clients/AnthropicClient.js index ebd94ca9b1..60b9c64d1e 100644 --- a/api/app/clients/AnthropicClient.js +++ b/api/app/clients/AnthropicClient.js @@ -418,6 +418,9 @@ class AnthropicClient extends BaseClient { this.contextHandlers?.processFile(file); continue; } + if (file.metadata?.fileIdentifier) { + continue; + } orderedMessages[i].tokenCount += this.calculateImageTokenCost({ width: file.width, diff --git a/api/app/clients/GoogleClient.js b/api/app/clients/GoogleClient.js index 575065d879..4a919876af 100644 --- a/api/app/clients/GoogleClient.js +++ b/api/app/clients/GoogleClient.js @@ -318,6 +318,9 @@ class GoogleClient extends BaseClient { this.contextHandlers?.processFile(file); continue; } + if (file.metadata?.fileIdentifier) { + continue; + } } this.augmentedPrompt = await this.contextHandlers.createContext(); diff --git a/api/app/clients/OpenAIClient.js b/api/app/clients/OpenAIClient.js index dd437f0b9e..c6a6bcc68e 100644 --- a/api/app/clients/OpenAIClient.js +++ b/api/app/clients/OpenAIClient.js @@ -455,6 +455,9 @@ class OpenAIClient extends BaseClient { this.contextHandlers?.processFile(file); continue; } + if (file.metadata?.fileIdentifier) { + continue; + } orderedMessages[i].tokenCount += this.calculateImageTokenCost({ width: file.width, diff --git a/api/models/Message.js b/api/models/Message.js index 58068813ef..86fd2fd549 100644 --- a/api/models/Message.js +++ b/api/models/Message.js @@ -61,6 +61,14 @@ async function saveMessage(req, params, metadata) { update.expiredAt = null; } + if (update.tokenCount != null && isNaN(update.tokenCount)) { + logger.warn( + `Resetting invalid \`tokenCount\` for message \`${params.messageId}\`: ${update.tokenCount}`, + ); + logger.info(`---\`saveMessage\` context: ${metadata?.context}`); + update.tokenCount = 0; + } + const message = await Message.findOneAndUpdate( { messageId: params.messageId, user: req.user.id }, update, @@ -97,7 +105,9 @@ async function saveMessage(req, params, metadata) { }; } catch (findError) { // If the findOne also fails, log it but don't crash - logger.warn(`Could not retrieve existing message with ID ${params.messageId}: ${findError.message}`); + logger.warn( + `Could not retrieve existing message with ID ${params.messageId}: ${findError.message}`, + ); return { ...params, messageId: params.messageId, diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index b462a8a0c8..cb4a9347cb 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -364,7 +364,9 @@ class AgentClient extends BaseClient { this.contextHandlers?.processFile(file); continue; } - + if (file.metadata?.fileIdentifier) { + continue; + } // orderedMessages[i].tokenCount += this.calculateImageTokenCost({ // width: file.width, // height: file.height, diff --git a/client/public/assets/xai.svg b/client/public/assets/xai.svg deleted file mode 100644 index 2aca45ed4f..0000000000 --- a/client/public/assets/xai.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/src/common/types.ts b/client/src/common/types.ts index 7bbb786548..cd8b45f6b7 100644 --- a/client/src/common/types.ts +++ b/client/src/common/types.ts @@ -508,7 +508,10 @@ export interface ModelItemProps { className?: string; } -export type ContextType = { navVisible: boolean; setNavVisible: (visible: boolean) => void }; +export type ContextType = { + navVisible: boolean; + setNavVisible: React.Dispatch>; +}; export interface SwitcherProps { endpoint?: t.EModelEndpoint | null; diff --git a/client/src/components/Chat/Header.tsx b/client/src/components/Chat/Header.tsx index b5379c72aa..4f491f4b05 100644 --- a/client/src/components/Chat/Header.tsx +++ b/client/src/components/Chat/Header.tsx @@ -3,7 +3,7 @@ import { useOutletContext } from 'react-router-dom'; import { getConfigDefaults, PermissionTypes, Permissions } from 'librechat-data-provider'; import type { ContextType } from '~/common'; import ModelSelector from './Menus/Endpoints/ModelSelector'; -import { PresetsMenu, HeaderNewChat } from './Menus'; +import { PresetsMenu, HeaderNewChat, OpenSidebar } from './Menus'; import { useGetStartupConfig } from '~/data-provider'; import ExportAndShareMenu from './ExportAndShareMenu'; import { useMediaQuery, useHasAccess } from '~/hooks'; @@ -15,7 +15,7 @@ const defaultInterface = getConfigDefaults().interface; export default function Header() { const { data: startupConfig } = useGetStartupConfig(); - const { navVisible } = useOutletContext(); + const { navVisible, setNavVisible } = useOutletContext(); const interfaceConfig = useMemo( () => startupConfig?.interface ?? defaultInterface, [startupConfig], @@ -37,6 +37,7 @@ export default function Header() {
+ {!navVisible && } {!navVisible && } {} {interfaceConfig.presets === true && interfaceConfig.modelSelect && } diff --git a/client/src/components/Chat/Input/Files/DragDropModal.tsx b/client/src/components/Chat/Input/Files/DragDropModal.tsx index 2abc15a45b..784116dc65 100644 --- a/client/src/components/Chat/Input/Files/DragDropModal.tsx +++ b/client/src/components/Chat/Input/Files/DragDropModal.tsx @@ -34,7 +34,7 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD label: localize('com_ui_upload_image_input'), value: undefined, icon: , - condition: files.every((file) => file.type.startsWith('image/')), + condition: files.every((file) => file.type?.startsWith('image/')), }, ]; for (const capability of capabilities) { diff --git a/client/src/components/Chat/Input/Files/Table/Columns.tsx b/client/src/components/Chat/Input/Files/Table/Columns.tsx index 8b8f52d8e7..bc60de361f 100644 --- a/client/src/components/Chat/Input/Files/Table/Columns.tsx +++ b/client/src/components/Chat/Input/Files/Table/Columns.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react-hooks/rules-of-hooks */ import { ArrowUpDown, Database } from 'lucide-react'; import { FileSources, FileContext } from 'librechat-data-provider'; @@ -68,7 +69,7 @@ export const columns: ColumnDef[] = [ }, cell: ({ row }) => { const file = row.original; - if (file.type.startsWith('image')) { + if (file.type?.startsWith('image')) { return (
[] = [ className="relative h-10 w-10 shrink-0 overflow-hidden rounded-md" source={file.source} /> - {file.filename} + {file.filename}
); } @@ -212,4 +213,4 @@ export const columns: ColumnDef[] = [ return `${value}${suffix}`; }, }, -]; \ No newline at end of file +]; diff --git a/client/src/components/Chat/Landing.tsx b/client/src/components/Chat/Landing.tsx index 032c59f538..655fa67401 100644 --- a/client/src/components/Chat/Landing.tsx +++ b/client/src/components/Chat/Landing.tsx @@ -9,7 +9,7 @@ import { useLocalize, useAuthContext } from '~/hooks'; import { getIconEndpoint, getEntity } from '~/utils'; const containerClassName = - 'shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white text-black'; + 'shadow-stroke relative flex h-full items-center justify-center rounded-full bg-white dark:bg-presentation dark:text-white text-black dark:after:shadow-none '; function getTextSizeClass(text: string | undefined | null) { if (!text) { @@ -149,7 +149,7 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding: >
{startupConfig?.showBirthdayIcon && ( diff --git a/client/src/components/Chat/Menus/HeaderNewChat.tsx b/client/src/components/Chat/Menus/HeaderNewChat.tsx index f59f570ec8..e417d6d6d8 100644 --- a/client/src/components/Chat/Menus/HeaderNewChat.tsx +++ b/client/src/components/Chat/Menus/HeaderNewChat.tsx @@ -1,36 +1,43 @@ import { useQueryClient } from '@tanstack/react-query'; import { QueryKeys, Constants } from 'librechat-data-provider'; import type { TMessage } from 'librechat-data-provider'; -import { useMediaQuery, useLocalize } from '~/hooks'; -import { Button, NewChatIcon } from '~/components'; +import { TooltipAnchor, Button } from '~/components/ui'; +import { NewChatIcon } from '~/components/svg'; import { useChatContext } from '~/Providers'; +import { useLocalize } from '~/hooks'; export default function HeaderNewChat() { + const localize = useLocalize(); const queryClient = useQueryClient(); const { conversation, newConversation } = useChatContext(); - const isSmallScreen = useMediaQuery('(max-width: 768px)'); - const localize = useLocalize(); - if (isSmallScreen) { - return null; - } + const clickHandler: React.MouseEventHandler = (e) => { + if (e.button === 0 && (e.ctrlKey || e.metaKey)) { + window.open('/c/new', '_blank'); + return; + } + queryClient.setQueryData( + [QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO], + [], + ); + newConversation(); + }; return ( - + + + + } + /> ); } diff --git a/client/src/components/Chat/Menus/OpenSidebar.tsx b/client/src/components/Chat/Menus/OpenSidebar.tsx new file mode 100644 index 0000000000..4c4f29414f --- /dev/null +++ b/client/src/components/Chat/Menus/OpenSidebar.tsx @@ -0,0 +1,33 @@ +import { TooltipAnchor, Button } from '~/components/ui'; +import { Sidebar } from '~/components/svg'; +import { useLocalize } from '~/hooks'; + +export default function OpenSidebar({ + setNavVisible, +}: { + setNavVisible: React.Dispatch>; +}) { + const localize = useLocalize(); + return ( + + setNavVisible((prev) => { + localStorage.setItem('navVisible', JSON.stringify(!prev)); + return !prev; + }) + } + > + + + } + /> + ); +} diff --git a/client/src/components/Chat/Menus/index.ts b/client/src/components/Chat/Menus/index.ts index 33b7e7d498..79ae61315b 100644 --- a/client/src/components/Chat/Menus/index.ts +++ b/client/src/components/Chat/Menus/index.ts @@ -1,2 +1,3 @@ export { default as PresetsMenu } from './PresetsMenu'; +export { default as OpenSidebar } from './OpenSidebar'; export { default as HeaderNewChat } from './HeaderNewChat'; diff --git a/client/src/components/Chat/Messages/Content/Markdown.tsx b/client/src/components/Chat/Messages/Content/Markdown.tsx index ee134b0e53..c0664adfe7 100644 --- a/client/src/components/Chat/Messages/Content/Markdown.tsx +++ b/client/src/components/Chat/Messages/Content/Markdown.tsx @@ -150,7 +150,7 @@ export const a: React.ElementType = memo(({ href, children }: TAnchorProps) => { return ( {children} diff --git a/client/src/components/Conversations/Convo.tsx b/client/src/components/Conversations/Convo.tsx index 510a0f407e..825fc54032 100644 --- a/client/src/components/Conversations/Convo.tsx +++ b/client/src/components/Conversations/Convo.tsx @@ -102,6 +102,9 @@ export default function Conversation({ const handleNavigation = (ctrlOrMetaKey: boolean) => { if (ctrlOrMetaKey) { toggleNav(); + const baseUrl = window.location.origin; + const path = `/c/${conversationId}`; + window.open(baseUrl + path, '_blank'); return; } diff --git a/client/src/components/Endpoints/EndpointIcon.tsx b/client/src/components/Endpoints/EndpointIcon.tsx index b727b28bf8..f635388f0e 100644 --- a/client/src/components/Endpoints/EndpointIcon.tsx +++ b/client/src/components/Endpoints/EndpointIcon.tsx @@ -63,7 +63,6 @@ export default function EndpointIcon({ isCreatedByUser={false} chatGptLabel={undefined} modelLabel={undefined} - jailbreak={undefined} /> ); } diff --git a/client/src/components/Nav/Bookmarks/BookmarkNav.tsx b/client/src/components/Nav/Bookmarks/BookmarkNav.tsx index fa8ab06d6a..c67ddd12fb 100644 --- a/client/src/components/Nav/Bookmarks/BookmarkNav.tsx +++ b/client/src/components/Nav/Bookmarks/BookmarkNav.tsx @@ -1,10 +1,12 @@ -import { type FC } from 'react'; +import { useMemo } from 'react'; +import type { FC } from 'react'; import { useRecoilValue } from 'recoil'; import { Menu, MenuButton, MenuItems } from '@headlessui/react'; import { BookmarkFilledIcon, BookmarkIcon } from '@radix-ui/react-icons'; import { BookmarkContext } from '~/Providers/BookmarkContext'; import { useGetConversationTags } from '~/data-provider'; import BookmarkNavItems from './BookmarkNavItems'; +import { TooltipAnchor } from '~/components/ui'; import { useLocalize } from '~/hooks'; import { cn } from '~/utils'; import store from '~/store'; @@ -19,33 +21,48 @@ const BookmarkNav: FC = ({ tags, setTags, isSmallScreen }: Boo const localize = useLocalize(); const { data } = useGetConversationTags(); const conversation = useRecoilValue(store.conversationByIndex(0)); + const label = useMemo( + () => (tags.length > 0 ? tags.join(', ') : localize('com_ui_bookmarks')), + [tags, localize], + ); return ( {({ open }) => ( <> - -
-
- {tags.length > 0 ? ( -
-
- {tags.length > 0 ? tags.join(', ') : localize('com_ui_bookmarks')} -
- - + data-testid="bookmark-menu" + > + {tags.length > 0 ? ( +
- - - - - )} - - ), - [search.enabled, hasAccessToBookmarks, isSmallScreen, tags, setTags], + () => search.enabled === true && , + [search.enabled, isSmallScreen], + ); + + const headerButtons = useMemo( + () => + hasAccessToBookmarks && ( + <> +
+ + + + + ), + [hasAccessToBookmarks, tags, isSmallScreen], ); const [isSearchLoading, setIsSearchLoading] = useState( @@ -198,23 +197,19 @@ const Nav = memo( >
-
+
- - - {isSmallScreen && } ); diff --git a/client/src/components/Nav/NewChat.tsx b/client/src/components/Nav/NewChat.tsx index a891fe9386..a8a75c8b04 100644 --- a/client/src/components/Nav/NewChat.tsx +++ b/client/src/components/Nav/NewChat.tsx @@ -1,88 +1,27 @@ -import React, { useMemo, useCallback } from 'react'; -import { Search } from 'lucide-react'; +import React, { useCallback } from 'react'; import { useRecoilValue } from 'recoil'; import { useNavigate } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; import { QueryKeys, Constants } from 'librechat-data-provider'; -import type { TConversation, TMessage } from 'librechat-data-provider'; -import { getEndpointField, getIconEndpoint, getIconKey } from '~/utils'; -import ConvoIconURL from '~/components/Endpoints/ConvoIconURL'; -import { useGetEndpointsQuery } from '~/data-provider'; +import type { TMessage, TStartupConfig } from 'librechat-data-provider'; +import { NewChatIcon, MobileSidebar, Sidebar } from '~/components/svg'; +import { getDefaultModelSpec, getModelSpecPreset } from '~/utils'; +import { TooltipAnchor, Button } from '~/components/ui'; import { useLocalize, useNewConvo } from '~/hooks'; -import { icons } from '~/hooks/Endpoint/Icons'; -import { NewChatIcon } from '~/components/svg'; -import { cn } from '~/utils'; import store from '~/store'; -const NewChatButtonIcon = React.memo(({ conversation }: { conversation: TConversation | null }) => { - const { data: endpointsConfig } = useGetEndpointsQuery(); - const search = useRecoilValue(store.search); - const searchQuery = search.debouncedQuery; - - const computedIcon = useMemo(() => { - if (searchQuery) { - return null; - } - let { endpoint = '' } = conversation ?? {}; - const iconURL = conversation?.iconURL ?? ''; - endpoint = getIconEndpoint({ endpointsConfig, iconURL, endpoint }); - const endpointType = getEndpointField(endpointsConfig, endpoint, 'type'); - const endpointIconURL = getEndpointField(endpointsConfig, endpoint, 'iconURL'); - const iconKey = getIconKey({ endpoint, endpointsConfig, endpointType, endpointIconURL }); - const Icon = icons[iconKey]; - return { iconURL, endpoint, endpointType, endpointIconURL, Icon }; - }, [searchQuery, conversation, endpointsConfig]); - - if (searchQuery) { - return ( -
- -
- ); - } - - if (!computedIcon) { - return null; - } - - const { iconURL, endpoint, endpointIconURL, Icon } = computedIcon; - - return ( -
- {iconURL && iconURL.includes('http') ? ( - - ) : ( -
- {endpoint && Icon && ( - - )} -
- )} -
- ); -}); - export default function NewChat({ index = 0, toggleNav, subHeaders, isSmallScreen, + headerButtons, }: { index?: number; toggleNav: () => void; + isSmallScreen?: boolean; subHeaders?: React.ReactNode; - isSmallScreen: boolean; + headerButtons?: React.ReactNode; }) { const queryClient = useQueryClient(); /** Note: this component needs an explicit index passed if using more than one */ @@ -91,48 +30,64 @@ export default function NewChat({ const localize = useLocalize(); const { conversation } = store.useCreateConversationAtom(index); - const clickHandler = useCallback( - (event: React.MouseEvent) => { - if (event.button === 0 && !(event.ctrlKey || event.metaKey)) { - event.preventDefault(); - queryClient.setQueryData( - [QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO], - [], - ); - newConvo(); - navigate('/c/new'); + const clickHandler: React.MouseEventHandler = useCallback( + (e) => { + if (e.button === 0 && (e.ctrlKey || e.metaKey)) { + window.open('/c/new', '_blank'); + return; + } + queryClient.setQueryData( + [QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO], + [], + ); + newConvo(); + navigate('/c/new'); + if (isSmallScreen) { toggleNav(); } }, - [queryClient, conversation, newConvo, navigate, toggleNav], + [queryClient, conversation, newConvo, navigate, toggleNav, isSmallScreen], ); return ( -
-
- - -
- {localize('com_ui_new_chat')} -
-
- - - -
-
+ <> +
+ + + + + } + /> +
+ {headerButtons} + + + + } + /> +
{subHeaders != null ? subHeaders : null} -
+ ); } diff --git a/client/src/components/Prompts/Groups/GroupSidePanel.tsx b/client/src/components/Prompts/Groups/GroupSidePanel.tsx index 84a5c3a7a5..5cfd77ec2a 100644 --- a/client/src/components/Prompts/Groups/GroupSidePanel.tsx +++ b/client/src/components/Prompts/Groups/GroupSidePanel.tsx @@ -24,7 +24,7 @@ export default function GroupSidePanel({ } & ReturnType) { const location = useLocation(); const isSmallerScreen = useMediaQuery('(max-width: 1024px)'); - const isChatRoute = useMemo(() => location.pathname.startsWith('/c/'), [location.pathname]); + const isChatRoute = useMemo(() => location.pathname?.startsWith('/c/'), [location.pathname]); return (
}) const file = row.original; return (
- {file?.type.startsWith('image') === true ? ( + {file?.type?.startsWith('image') === true ? ( + + + ); +} diff --git a/client/src/components/svg/Panel.tsx b/client/src/components/svg/Panel.tsx deleted file mode 100644 index bb62833de9..0000000000 --- a/client/src/components/svg/Panel.tsx +++ /dev/null @@ -1,43 +0,0 @@ -export default function Panel({ open = false }) { - const openPanel = ( - - - - - ); - - const closePanel = ( - - - - - ); - - if (open) { - return openPanel; - } else { - return closePanel; - } -} diff --git a/client/src/components/svg/Sidebar.tsx b/client/src/components/svg/Sidebar.tsx new file mode 100644 index 0000000000..152215f635 --- /dev/null +++ b/client/src/components/svg/Sidebar.tsx @@ -0,0 +1,19 @@ +export default function Sidebar({ className }: { className?: string }) { + return ( + + + + ); +} diff --git a/client/src/components/svg/XAIcon.tsx b/client/src/components/svg/XAIcon.tsx new file mode 100644 index 0000000000..2ee679dcd3 --- /dev/null +++ b/client/src/components/svg/XAIcon.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +export default function XAIcon({ className = '' }) { + return ( + + ); +} diff --git a/client/src/components/svg/index.ts b/client/src/components/svg/index.ts index 745c5210bd..f0cf35f6c3 100644 --- a/client/src/components/svg/index.ts +++ b/client/src/components/svg/index.ts @@ -4,7 +4,8 @@ export { default as Plugin } from './Plugin'; export { default as GPTIcon } from './GPTIcon'; export { default as EditIcon } from './EditIcon'; export { default as DataIcon } from './DataIcon'; -export { default as Panel } from './Panel'; +export { default as Sidebar } from './Sidebar'; +export { default as MobileSidebar } from './MobileSidebar'; export { default as Spinner } from './Spinner'; export { default as Clipboard } from './Clipboard'; export { default as CheckMark } from './CheckMark'; @@ -56,3 +57,4 @@ export { default as SpeechIcon } from './SpeechIcon'; export { default as SaveIcon } from './SaveIcon'; export { default as CircleHelpIcon } from './CircleHelpIcon'; export { default as BedrockIcon } from './BedrockIcon'; +export { default as XAIcon } from './XAIcon'; diff --git a/client/src/components/ui/ModelParameters.tsx b/client/src/components/ui/ModelParameters.tsx index 881717e096..17fd8c14be 100644 --- a/client/src/components/ui/ModelParameters.tsx +++ b/client/src/components/ui/ModelParameters.tsx @@ -33,7 +33,8 @@ const ModelParameters: React.FC = ({ const rangeRef = useRef(null); const id = `model-parameter-${ariaLabel.toLowerCase().replace(/\s+/g, '-')}`; - const displayLabel = label.startsWith('com_') ? localize(label as TranslationKeys) : label; + const displayLabel = + label && label.startsWith('com_') ? localize(label as TranslationKeys) : label; const getDecimalPlaces = (num: number) => { const match = ('' + num).match(/(?:\.(\d+))?(?:[eE]([+-]?\d+))?$/); diff --git a/client/src/hooks/Chat/useChatFunctions.ts b/client/src/hooks/Chat/useChatFunctions.ts index 770c8127d8..b75d97d6a2 100644 --- a/client/src/hooks/Chat/useChatFunctions.ts +++ b/client/src/hooks/Chat/useChatFunctions.ts @@ -25,6 +25,7 @@ import store, { useGetEphemeralAgent } from '~/store'; import { getArtifactsMode } from '~/utils/artifacts'; import { getEndpointField, logger } from '~/utils'; import useUserKey from '~/hooks/Input/useUserKey'; +import { useNavigate } from 'react-router-dom'; const logChatRequest = (request: Record) => { logger.log('=====================================\nAsk function called with:'); @@ -69,6 +70,7 @@ export default function useChatFunctions({ const codeArtifacts = useRecoilValue(store.codeArtifacts); const includeShadcnui = useRecoilValue(store.includeShadcnui); const customPromptMode = useRecoilValue(store.customPromptMode); + const navigate = useNavigate(); const resetLatestMultiMessage = useResetRecoilState(store.latestMessageFamily(index + 1)); const setShowStopButton = useSetRecoilState(store.showStopButtonByIndex(index)); const setFilesToDelete = useSetFilesToDelete(); @@ -146,6 +148,7 @@ export default function useChatFunctions({ parentMessageId = Constants.NO_PARENT; currentMessages = []; conversationId = null; + navigate('/c/new'); } const targetParentMessageId = isRegenerate ? messageId : latestMessage?.parentMessageId; diff --git a/client/src/hooks/Chat/useChatHelpers.ts b/client/src/hooks/Chat/useChatHelpers.ts index a79b860c57..c4e491c7df 100644 --- a/client/src/hooks/Chat/useChatHelpers.ts +++ b/client/src/hooks/Chat/useChatHelpers.ts @@ -23,10 +23,10 @@ export default function useChatHelpers(index = 0, paramId?: string) { const { conversation, setConversation } = useCreateConversationAtom(index); const { conversationId } = conversation ?? {}; - const queryParam = paramId === 'new' ? paramId : conversationId ?? paramId ?? ''; + const queryParam = paramId === 'new' ? paramId : (conversationId ?? paramId ?? ''); /* Messages: here simply to fetch, don't export and use `getMessages()` instead */ - // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { data: _messages } = useGetMessagesByConvoId(conversationId ?? '', { enabled: isAuthenticated, }); @@ -41,7 +41,7 @@ export default function useChatHelpers(index = 0, paramId?: string) { const setMessages = useCallback( (messages: TMessage[]) => { queryClient.setQueryData([QueryKeys.messages, queryParam], messages); - if (queryParam === 'new') { + if (queryParam === 'new' && conversationId && conversationId !== 'new') { queryClient.setQueryData([QueryKeys.messages, conversationId], messages); } }, diff --git a/client/src/hooks/Endpoint/UnknownIcon.tsx b/client/src/hooks/Endpoint/UnknownIcon.tsx index 04bac0bdeb..6c69954a2d 100644 --- a/client/src/hooks/Endpoint/UnknownIcon.tsx +++ b/client/src/hooks/Endpoint/UnknownIcon.tsx @@ -1,6 +1,6 @@ import { memo } from 'react'; import { EModelEndpoint, KnownEndpoints } from 'librechat-data-provider'; -import { CustomMinimalIcon } from '~/components/svg'; +import { CustomMinimalIcon, XAIcon } from '~/components/svg'; import { IconContext } from '~/common'; import { cn } from '~/utils'; @@ -20,7 +20,6 @@ const knownEndpointAssets = { [KnownEndpoints.shuttleai]: '/assets/shuttleai.png', [KnownEndpoints['together.ai']]: '/assets/together.png', [KnownEndpoints.unify]: '/assets/unify.webp', - [KnownEndpoints.xai]: '/assets/xai.svg', }; const knownEndpointClasses = { @@ -29,9 +28,6 @@ const knownEndpointClasses = { }, [KnownEndpoints.xai]: { [IconContext.landing]: 'p-2', - [IconContext.menuItem]: 'bg-white', - [IconContext.message]: 'bg-white', - [IconContext.nav]: 'bg-white', }, }; @@ -72,6 +68,18 @@ function UnknownIcon({ const currentEndpoint = endpoint.toLowerCase(); + if (currentEndpoint === KnownEndpoints.xai) { + return ( + + ); + } + if (iconURL) { return {`${endpoint}; } diff --git a/client/src/hooks/SSE/useAttachmentHandler.ts b/client/src/hooks/SSE/useAttachmentHandler.ts index 3610b9798e..2eb748ab1f 100644 --- a/client/src/hooks/SSE/useAttachmentHandler.ts +++ b/client/src/hooks/SSE/useAttachmentHandler.ts @@ -10,7 +10,7 @@ export default function useAttachmentHandler(queryClient?: QueryClient) { return ({ data }: { data: TAttachment; submission: EventSubmission }) => { const { messageId } = data; - if (queryClient) { + if (queryClient && !data?.filepath?.startsWith('/api/files')) { queryClient.setQueryData([QueryKeys.files], (oldData: TAttachment[] | undefined) => { return [data, ...(oldData || [])]; }); diff --git a/client/src/hooks/SSE/useEventHandlers.ts b/client/src/hooks/SSE/useEventHandlers.ts index 257331e485..003d5547df 100644 --- a/client/src/hooks/SSE/useEventHandlers.ts +++ b/client/src/hooks/SSE/useEventHandlers.ts @@ -1,7 +1,7 @@ import { v4 } from 'uuid'; import { useCallback, useRef } from 'react'; import { useSetRecoilState } from 'recoil'; -import { useParams } from 'react-router-dom'; +import { useParams, useNavigate, useLocation } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; import { QueryKeys, @@ -172,6 +172,8 @@ export default function useEventHandlers({ const { announcePolite } = useLiveAnnouncer(); const applyAgentTemplate = useApplyNewAgentTemplate(); const setAbortScroll = useSetRecoilState(store.abortScroll); + const navigate = useNavigate(); + const location = useLocation(); const lastAnnouncementTimeRef = useRef(Date.now()); const { conversationId: paramId } = useParams(); @@ -421,6 +423,7 @@ export default function useEventHandlers({ announcePolite, setConversation, resetLatestMessage, + applyAgentTemplate, ], ); @@ -449,12 +452,20 @@ export default function useEventHandlers({ announcePolite({ message: getAllContentText(responseMessage) }); /* Update messages; if assistants endpoint, client doesn't receive responseMessage */ + let finalMessages: TMessage[] = []; if (runMessages) { - setMessages([...runMessages]); + finalMessages = [...runMessages]; } else if (isRegenerate && responseMessage) { - setMessages([...messages, responseMessage]); + finalMessages = [...messages, responseMessage]; } else if (requestMessage != null && responseMessage != null) { - setMessages([...messages, requestMessage, responseMessage]); + finalMessages = [...messages, requestMessage, responseMessage]; + } + if (finalMessages.length > 0) { + setMessages(finalMessages); + queryClient.setQueryData( + [QueryKeys.messages, conversation.conversationId], + finalMessages, + ); } const isNewConvo = conversation.conversationId !== submissionConvo.conversationId; @@ -476,8 +487,8 @@ export default function useEventHandlers({ } if (setConversation && isAddedRequest !== true) { - if (window.location.pathname === '/c/new') { - window.history.pushState({}, '', '/c/' + conversation.conversationId); + if (location.pathname === '/c/new') { + navigate(`/c/${conversation.conversationId}`, { replace: true }); } setConversation((prevState) => { @@ -502,16 +513,18 @@ export default function useEventHandlers({ setIsSubmitting(false); }, [ - genTitle, - queryClient, - getMessages, - setMessages, - setCompleted, - isAddedRequest, - announcePolite, - setConversation, - setIsSubmitting, setShowStopButton, + setCompleted, + getMessages, + announcePolite, + genTitle, + setConversation, + isAddedRequest, + setIsSubmitting, + setMessages, + queryClient, + location.pathname, + navigate, ], ); @@ -599,7 +612,7 @@ export default function useEventHandlers({ setIsSubmitting(false); return; }, - [setMessages, paramId, setIsSubmitting, setCompleted, newConversation], + [setCompleted, setMessages, paramId, newConversation, setIsSubmitting, getMessages], ); const abortConversation = useCallback( @@ -698,7 +711,15 @@ export default function useEventHandlers({ setIsSubmitting(false); } }, - [token, setIsSubmitting, finalHandler, cancelHandler, setMessages, newConversation], + [ + finalHandler, + newConversation, + setIsSubmitting, + token, + cancelHandler, + getMessages, + setMessages, + ], ); return { diff --git a/client/src/hooks/useNewConvo.ts b/client/src/hooks/useNewConvo.ts index f5933cc547..d27f80a306 100644 --- a/client/src/hooks/useNewConvo.ts +++ b/client/src/hooks/useNewConvo.ts @@ -22,8 +22,8 @@ import { getEndpointField, buildDefaultConvo, getDefaultEndpoint, + getModelSpecPreset, getDefaultModelSpec, - getModelSpecIconURL, updateLastSelectedModel, } from '~/utils'; import { useDeleteFilesMutation, useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider'; @@ -231,11 +231,7 @@ const useNewConvo = (index = 0) => { (startupConfig.interface?.modelSelect ?? true) !== true) && defaultModelSpec ) { - preset = { - ...defaultModelSpec.preset, - iconURL: getModelSpecIconURL(defaultModelSpec), - spec: defaultModelSpec.name, - } as TConversation; + preset = getModelSpecPreset(defaultModelSpec); } if (conversation.conversationId === 'new' && !modelsData) { diff --git a/client/src/routes/ChatRoute.tsx b/client/src/routes/ChatRoute.tsx index 4ce9cdef9e..48382fc548 100644 --- a/client/src/routes/ChatRoute.tsx +++ b/client/src/routes/ChatRoute.tsx @@ -9,8 +9,8 @@ import { useGetStartupConfig, useGetEndpointsQuery, } from '~/data-provider'; +import { getDefaultModelSpec, getModelSpecPreset, logger } from '~/utils'; import { useNewConvo, useAppStartup, useAssistantListMap } from '~/hooks'; -import { getDefaultModelSpec, getModelSpecIconURL, logger } from '~/utils'; import { ToolCallsMapProvider } from '~/Providers'; import ChatView from '~/components/Chat/ChatView'; import useAuthRedirect from './useAuthRedirect'; @@ -65,15 +65,7 @@ export default function ChatRoute() { newConversation({ modelsData: modelsQuery.data, template: conversation ? conversation : undefined, - ...(spec - ? { - preset: { - ...spec.preset, - iconURL: getModelSpecIconURL(spec), - spec: spec.name, - }, - } - : {}), + ...(spec ? { preset: getModelSpecPreset(spec) } : {}), }); hasSetConversation.current = true; @@ -97,15 +89,7 @@ export default function ChatRoute() { newConversation({ modelsData: modelsQuery.data, template: conversation ? conversation : undefined, - ...(spec - ? { - preset: { - ...spec.preset, - iconURL: getModelSpecIconURL(spec), - spec: spec.name, - }, - } - : {}), + ...(spec ? { preset: getModelSpecPreset(spec) } : {}), }); hasSetConversation.current = true; } else if ( diff --git a/client/src/utils/endpoints.ts b/client/src/utils/endpoints.ts index f86f81682e..497c76aca6 100644 --- a/client/src/utils/endpoints.ts +++ b/client/src/utils/endpoints.ts @@ -203,6 +203,17 @@ export function getDefaultModelSpec(startupConfig?: t.TStartupConfig) { return list?.find((spec) => spec.name === lastConversationSetup.spec); } +export function getModelSpecPreset(modelSpec?: t.TModelSpec) { + if (!modelSpec) { + return; + } + return { + ...modelSpec.preset, + spec: modelSpec.name, + iconURL: getModelSpecIconURL(modelSpec), + }; +} + /** Gets the default spec iconURL by order or definition. * * First, the admin defined default, then last selected spec, followed by first spec