🧭 refactor: Modernize Nav/Header (#7094)

* 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
This commit is contained in:
Danny Avila 2025-04-27 14:03:25 -04:00 committed by GitHub
parent c0ebb434a6
commit 550c7cc68a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 361 additions and 298 deletions

View file

@ -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<ContextType>();
const { navVisible, setNavVisible } = useOutletContext<ContextType>();
const interfaceConfig = useMemo(
() => startupConfig?.interface ?? defaultInterface,
[startupConfig],
@ -37,6 +37,7 @@ export default function Header() {
<div className="sticky top-0 z-10 flex h-14 w-full items-center justify-between bg-white p-2 font-semibold text-text-primary dark:bg-gray-800">
<div className="hide-scrollbar flex w-full items-center justify-between gap-2 overflow-x-auto">
<div className="mx-2 flex items-center gap-2">
{!navVisible && <OpenSidebar setNavVisible={setNavVisible} />}
{!navVisible && <HeaderNewChat />}
{<ModelSelector startupConfig={startupConfig} />}
{interfaceConfig.presets === true && interfaceConfig.modelSelect && <PresetsMenu />}

View file

@ -34,7 +34,7 @@ const DragDropModal = ({ onOptionSelect, setShowModal, files, isVisible }: DragD
label: localize('com_ui_upload_image_input'),
value: undefined,
icon: <ImageUpIcon className="icon-md" />,
condition: files.every((file) => file.type.startsWith('image/')),
condition: files.every((file) => file.type?.startsWith('image/')),
},
];
for (const capability of capabilities) {

View file

@ -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<TFile>[] = [
},
cell: ({ row }) => {
const file = row.original;
if (file.type.startsWith('image')) {
if (file.type?.startsWith('image')) {
return (
<div className="flex gap-2">
<ImagePreview
@ -76,7 +77,7 @@ export const columns: ColumnDef<TFile>[] = [
className="relative h-10 w-10 shrink-0 overflow-hidden rounded-md"
source={file.source}
/>
<span className="self-center truncate ">{file.filename}</span>
<span className="self-center truncate">{file.filename}</span>
</div>
);
}
@ -212,4 +213,4 @@ export const columns: ColumnDef<TFile>[] = [
return `${value}${suffix}`;
},
},
];
];

View file

@ -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:
>
<div ref={contentRef} className="flex flex-col items-center gap-0 p-2">
<div
className={`flex ${textHasMultipleLines ? 'flex-col' : 'flex-col md:flex-row'} items-center justify-center gap-4`}
className={`flex ${textHasMultipleLines ? 'flex-col' : 'flex-col md:flex-row'} items-center justify-center gap-2`}
>
<div className={`relative size-10 justify-center ${textHasMultipleLines ? 'mb-2' : ''}`}>
<ConvoIcon
@ -159,7 +159,7 @@ export default function Landing({ centerFormOnLanding }: { centerFormOnLanding:
endpointsConfig={endpointsConfig}
containerClassName={containerClassName}
context="landing"
className="h-2/3 w-2/3"
className="h-2/3 w-2/3 text-black dark:text-white"
size={41}
/>
{startupConfig?.showBirthdayIcon && (

View file

@ -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<HTMLButtonElement> = (e) => {
if (e.button === 0 && (e.ctrlKey || e.metaKey)) {
window.open('/c/new', '_blank');
return;
}
queryClient.setQueryData<TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
newConversation();
};
return (
<Button
size="icon"
variant="outline"
data-testid="wide-header-new-chat-button"
aria-label={localize('com_ui_new_chat')}
className="rounded-xl border border-border-light bg-surface-secondary p-2 hover:bg-surface-hover"
onClick={() => {
queryClient.setQueryData<TMessage[]>(
[QueryKeys.messages, conversation?.conversationId ?? Constants.NEW_CONVO],
[],
);
newConversation();
}}
>
<NewChatIcon />
</Button>
<TooltipAnchor
description={localize('com_ui_new_chat')}
render={
<Button
size="icon"
variant="outline"
data-testid="wide-header-new-chat-button"
aria-label={localize('com_ui_new_chat')}
className="rounded-xl border border-border-light bg-surface-secondary p-2 hover:bg-surface-hover max-md:hidden"
onClick={clickHandler}
>
<NewChatIcon />
</Button>
}
/>
);
}

View file

@ -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<React.SetStateAction<boolean>>;
}) {
const localize = useLocalize();
return (
<TooltipAnchor
description={localize('com_nav_open_sidebar')}
render={
<Button
size="icon"
variant="outline"
data-testid="open-sidebar-button"
aria-label={localize('com_nav_open_sidebar')}
className="rounded-xl border border-border-light bg-surface-secondary p-2 hover:bg-surface-hover max-md:hidden"
onClick={() =>
setNavVisible((prev) => {
localStorage.setItem('navVisible', JSON.stringify(!prev));
return !prev;
})
}
>
<Sidebar />
</Button>
}
/>
);
}

View file

@ -1,2 +1,3 @@
export { default as PresetsMenu } from './PresetsMenu';
export { default as OpenSidebar } from './OpenSidebar';
export { default as HeaderNewChat } from './HeaderNewChat';

View file

@ -150,7 +150,7 @@ export const a: React.ElementType = memo(({ href, children }: TAnchorProps) => {
return (
<a
href={filepath.startsWith('files/') ? `/api/${filepath}` : `/api/files/${filepath}`}
href={filepath?.startsWith('files/') ? `/api/${filepath}` : `/api/files/${filepath}`}
{...props}
>
{children}