From 550c7cc68a626280006a0f1d4d74d903662c7494 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 27 Apr 2025 14:03:25 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=AD=20refactor:=20Modernize=20Nav/Head?= =?UTF-8?q?er=20(#7094)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: streamline model preset handling in conversation setup * refactor: integrate navigation and location hooks in chat functions and event handlers, prevent cache from fetching on final event handling * fix: prevent adding code interpreter non-image output to file list on message attachment event, fix all unhandled edge cases when this is done (treating the file download as an image attachment, undefined fields, message tokenCount issues, use of `startsWith` on undefined "text") although it is now prevent altogether * chore: remove unused jailbreak prop from MinimalIcon component in EndpointIcon * feat: add new SVG icons (MobileSidebar, Sidebar, XAIcon), fix: xAI styling in dark vs. light modes, adjust styling of Landing icons * fix: open conversation in new tab on navigation with ctrl/meta key * refactor: update Nav & Header to use close/open sidebar buttons, as well as redesign "New Chat"/"Bookmarks" buttons to the top of the Nav, matching the latest design of ChatGPT for simplicity and to free up space * chore: remove unused isToggleHovering state and simplify opacity logic in Nav component * style: match mobile nav to mobile header --- api/app/clients/AnthropicClient.js | 3 + api/app/clients/GoogleClient.js | 3 + api/app/clients/OpenAIClient.js | 3 + api/models/Message.js | 12 +- api/server/controllers/agents/client.js | 4 +- client/public/assets/xai.svg | 1 - client/src/common/types.ts | 5 +- client/src/components/Chat/Header.tsx | 5 +- .../Chat/Input/Files/DragDropModal.tsx | 2 +- .../Chat/Input/Files/Table/Columns.tsx | 7 +- client/src/components/Chat/Landing.tsx | 6 +- .../components/Chat/Menus/HeaderNewChat.tsx | 53 +++--- .../src/components/Chat/Menus/OpenSidebar.tsx | 33 ++++ client/src/components/Chat/Menus/index.ts | 1 + .../Chat/Messages/Content/Markdown.tsx | 2 +- client/src/components/Conversations/Convo.tsx | 3 + .../src/components/Endpoints/EndpointIcon.tsx | 1 - .../components/Nav/Bookmarks/BookmarkNav.tsx | 61 ++++--- client/src/components/Nav/Nav.tsx | 56 +++--- client/src/components/Nav/NewChat.tsx | 163 +++++++----------- .../Prompts/Groups/GroupSidePanel.tsx | 2 +- .../SidePanel/Files/PanelFileCell.tsx | 2 +- .../components/svg/AnthropicMinimalIcon.tsx | 2 +- client/src/components/svg/MobileSidebar.tsx | 19 ++ client/src/components/svg/Panel.tsx | 43 ----- client/src/components/svg/Sidebar.tsx | 19 ++ client/src/components/svg/XAIcon.tsx | 16 ++ client/src/components/svg/index.ts | 4 +- client/src/components/ui/ModelParameters.tsx | 3 +- client/src/hooks/Chat/useChatFunctions.ts | 3 + client/src/hooks/Chat/useChatHelpers.ts | 6 +- client/src/hooks/Endpoint/UnknownIcon.tsx | 18 +- client/src/hooks/SSE/useAttachmentHandler.ts | 2 +- client/src/hooks/SSE/useEventHandlers.ts | 55 ++++-- client/src/hooks/useNewConvo.ts | 8 +- client/src/routes/ChatRoute.tsx | 22 +-- client/src/utils/endpoints.ts | 11 ++ 37 files changed, 361 insertions(+), 298 deletions(-) delete mode 100644 client/public/assets/xai.svg create mode 100644 client/src/components/Chat/Menus/OpenSidebar.tsx create mode 100644 client/src/components/svg/MobileSidebar.tsx delete mode 100644 client/src/components/svg/Panel.tsx create mode 100644 client/src/components/svg/Sidebar.tsx create mode 100644 client/src/components/svg/XAIcon.tsx 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