🪟 style: Agent Marketplace UI Responsiveness, a11y, and Navigation (#9068)

* refactor: Agent Marketplace Button with access control

* fix(agent-marketplace): update marketplace UI and access control

* fix(agent-card): handle optional agent description for accessibility

* fix(agent-card): remove unnecessary icon checks from tests

* chore: remove unused keys

---------

Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
Marco Beretta 2025-08-15 20:59:10 +02:00 committed by GitHub
parent c78fd0fc83
commit 4ec7bcb60f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 164 additions and 162 deletions

View file

@ -28,7 +28,7 @@ const AgentCard: React.FC<AgentCardProps> = ({ agent, onClick, className = '' })
onClick={onClick} onClick={onClick}
aria-label={localize('com_agents_agent_card_label', { aria-label={localize('com_agents_agent_card_label', {
name: agent.name, name: agent.name,
description: agent.description || localize('com_agents_no_description'), description: agent.description ?? '',
})} })}
aria-describedby={`agent-${agent.id}-description`} aria-describedby={`agent-${agent.id}-description`}
tabIndex={0} tabIndex={0}
@ -47,51 +47,48 @@ const AgentCard: React.FC<AgentCardProps> = ({ agent, onClick, className = '' })
<div className="flex-shrink-0">{renderAgentAvatar(agent, { size: 'sm' })}</div> <div className="flex-shrink-0">{renderAgentAvatar(agent, { size: 'sm' })}</div>
{/* Category tag */} {/* Category tag */}
<div className="inline-flex items-center rounded-md border-border-xheavy bg-surface-active-alt px-2 py-1 text-xs font-medium">
{agent.category && ( {agent.category && (
<div className="inline-flex items-center rounded-md border-border-xheavy bg-surface-active-alt px-2 py-1 text-xs font-medium">
<Label className="line-clamp-1 font-normal"> <Label className="line-clamp-1 font-normal">
{agent.category.charAt(0).toUpperCase() + agent.category.slice(1)} {agent.category.charAt(0).toUpperCase() + agent.category.slice(1)}
</Label> </Label>
)}
</div> </div>
)}
</div> </div>
{/* Right column: Name, description, and other content */} {/* Right column: Name, description, and other content */}
<div className="min-w-0 flex-1 space-y-1"> <div className="flex h-full min-w-0 flex-1 flex-col justify-between space-y-1">
<div className="flex items-center justify-between"> <div className="space-y-1">
{/* Agent name */} {/* Agent name */}
<Label className="mb-1 line-clamp-1 text-xl font-semibold text-text-primary"> <Label className="mb-1 line-clamp-1 text-xl font-semibold text-text-primary">
{agent.name} {agent.name}
</Label> </Label>
{/* Owner info */} {/* Agent description */}
<p
id={`agent-${agent.id}-description`}
className="line-clamp-3 text-sm leading-relaxed text-text-primary"
{...(agent.description ? { 'aria-label': `Description: ${agent.description}` } : {})}
>
{agent.description ?? ''}
</p>
</div>
{/* Owner info - moved to bottom right */}
{(() => { {(() => {
const displayName = getContactDisplayName(agent); const displayName = getContactDisplayName(agent);
if (displayName) { if (displayName) {
return ( return (
<div className="flex justify-end">
<div className="flex items-center text-sm text-text-secondary"> <div className="flex items-center text-sm text-text-secondary">
<Label className="mr-1">🔹</Label>
<Label>{displayName}</Label> <Label>{displayName}</Label>
</div> </div>
</div>
); );
} }
return null; return null;
})()} })()}
</div> </div>
{/* Agent description */}
<p
id={`agent-${agent.id}-description`}
className="line-clamp-3 text-sm leading-relaxed text-text-primary"
aria-label={`Description: ${agent.description || localize('com_agents_no_description')}`}
>
{agent.description || (
<Label className="font-normal italic text-text-primary">
{localize('com_agents_no_description')}
</Label>
)}
</p>
</div>
</div> </div>
</div> </div>
); );

View file

@ -126,10 +126,7 @@ const AgentDetail: React.FC<AgentDetailProps> = ({ agent, isOpen, onClose }) =>
return ( return (
<OGDialog open={isOpen} onOpenChange={(open) => !open && onClose()}> <OGDialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
<OGDialogContent <OGDialogContent ref={dialogRef} className="max-h-[90vh] w-11/12 max-w-lg overflow-y-auto">
ref={dialogRef}
className="max-h-[90vh] overflow-y-auto py-8 sm:max-w-[450px]"
>
{/* Copy link button - positioned next to close button */} {/* Copy link button - positioned next to close button */}
<Button <Button
variant="ghost" variant="ghost"
@ -161,11 +158,7 @@ const AgentDetail: React.FC<AgentDetailProps> = ({ agent, isOpen, onClose }) =>
{/* Agent description - below contact */} {/* Agent description - below contact */}
<div className="mt-4 whitespace-pre-wrap px-6 text-center text-base text-text-primary"> <div className="mt-4 whitespace-pre-wrap px-6 text-center text-base text-text-primary">
{agent?.description || ( {agent?.description}
<span className="italic text-text-tertiary">
{localize('com_agents_no_description')}
</span>
)}
</div> </div>
{/* Action button */} {/* Action button */}

View file

@ -1,7 +1,8 @@
import React from 'react'; import React from 'react';
import type t from 'librechat-data-provider'; import type t from 'librechat-data-provider';
import useLocalize from '~/hooks/useLocalize'; import { useMediaQuery } from '@librechat/client';
import { SmartLoader } from './SmartLoader'; import { SmartLoader } from './SmartLoader';
import { useLocalize } from '~/hooks/';
import { cn } from '~/utils'; import { cn } from '~/utils';
/** /**
@ -33,6 +34,7 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
onChange, onChange,
}) => { }) => {
const localize = useLocalize(); const localize = useLocalize();
const isSmallScreen = useMediaQuery('(max-width: 768px)');
// Helper function to get category display name from database data // Helper function to get category display name from database data
const getCategoryDisplayName = (category: t.TCategory) => { const getCategoryDisplayName = (category: t.TCategory) => {
@ -120,10 +122,24 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
const tabsContent = ( const tabsContent = (
<div className="w-full pb-2"> <div className="w-full pb-2">
<div <div
className="flex flex-wrap justify-center gap-1.5 px-4" className={cn(
'px-4',
isSmallScreen
? 'scrollbar-hide flex gap-2 overflow-x-auto scroll-smooth'
: 'flex flex-wrap justify-center gap-1.5',
)}
role="tablist" role="tablist"
aria-label={localize('com_agents_category_tabs_label')} aria-label={localize('com_agents_category_tabs_label')}
aria-orientation="horizontal" aria-orientation="horizontal"
style={
isSmallScreen
? {
scrollbarWidth: 'none',
msOverflowStyle: 'none',
WebkitOverflowScrolling: 'touch',
}
: undefined
}
> >
{categories.map((category, index) => ( {categories.map((category, index) => (
<button <button
@ -132,10 +148,11 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
onClick={() => onChange(category.value)} onClick={() => onChange(category.value)}
onKeyDown={(e) => handleKeyDown(e, category.value)} onKeyDown={(e) => handleKeyDown(e, category.value)}
className={cn( className={cn(
'relative cursor-pointer select-none whitespace-nowrap px-3 py-2 transition-colors', 'relative cursor-pointer select-none whitespace-nowrap px-3 py-2 transition-all duration-200',
isSmallScreen ? 'min-w-fit flex-shrink-0' : '',
activeTab === category.value activeTab === category.value
? 'rounded-t-lg bg-surface-hover text-text-primary' ? 'rounded-t-lg bg-surface-hover text-text-primary'
: 'rounded-lg bg-surface-secondary text-text-secondary hover:bg-surface-hover hover:text-text-primary', : 'rounded-lg bg-surface-secondary text-text-secondary hover:bg-surface-hover hover:text-text-primary active:scale-95',
)} )}
role="tab" role="tab"
aria-selected={activeTab === category.value} aria-selected={activeTab === category.value}

View file

@ -285,10 +285,6 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
ref={scrollContainerRef} ref={scrollContainerRef}
className="scrollbar-gutter-stable relative flex h-full flex-col overflow-y-auto overflow-x-hidden" className="scrollbar-gutter-stable relative flex h-full flex-col overflow-y-auto overflow-x-hidden"
> >
{/* Admin Settings */}
<div className="absolute right-4 top-4 z-30">
<MarketplaceAdminSettings />
</div>
{/* Simplified header for agents marketplace - only show nav controls when needed */} {/* Simplified header for agents marketplace - only show nav controls when needed */}
{!isSmallScreen && ( {!isSmallScreen && (
<div className="sticky top-0 z-20 flex items-center justify-between bg-surface-secondary p-2 font-semibold text-text-primary md:h-14"> <div className="sticky top-0 z-20 flex items-center justify-between bg-surface-secondary p-2 font-semibold text-text-primary md:h-14">
@ -319,10 +315,10 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
</div> </div>
</div> </div>
)} )}
{/* Hero Section - scrolls away */} {/* Hero Section - scrolls away */}
{!isSmallScreen && (
<div className="container mx-auto max-w-4xl"> <div className="container mx-auto max-w-4xl">
<div className={cn('mb-8 text-center', isSmallScreen ? 'mt-6' : 'mt-12')}> <div className={cn('mb-8 text-center', 'mt-12')}>
<h1 className="mb-3 text-3xl font-bold tracking-tight text-text-primary md:text-5xl"> <h1 className="mb-3 text-3xl font-bold tracking-tight text-text-primary md:text-5xl">
{localize('com_agents_marketplace')} {localize('com_agents_marketplace')}
</h1> </h1>
@ -331,7 +327,7 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
</p> </p>
</div> </div>
</div> </div>
)}
{/* Sticky wrapper for search bar and categories */} {/* Sticky wrapper for search bar and categories */}
<div <div
className={cn( className={cn(
@ -341,8 +337,11 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
> >
<div className="container mx-auto max-w-4xl px-4"> <div className="container mx-auto max-w-4xl px-4">
{/* Search bar */} {/* Search bar */}
<div className="mx-auto max-w-2xl pb-6"> <div className="mx-auto flex max-w-2xl gap-2 pb-6">
<SearchBar value={searchQuery} onSearch={handleSearch} /> <SearchBar value={searchQuery} onSearch={handleSearch} />
{/* TODO: Remove this once we have a better way to handle admin settings */}
{/* Admin Settings */}
<MarketplaceAdminSettings />
</div> </div>
{/* Category tabs */} {/* Category tabs */}
@ -354,7 +353,6 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
/> />
</div> </div>
</div> </div>
{/* Scrollable content area */} {/* Scrollable content area */}
<div className="container mx-auto max-w-4xl px-4 pb-8"> <div className="container mx-auto max-w-4xl px-4 pb-8">
{/* Two-pane animated container wrapping category header + grid */} {/* Two-pane animated container wrapping category header + grid */}
@ -510,7 +508,6 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
{/* Note: Using Tailwind keyframes for slide in/out animations */} {/* Note: Using Tailwind keyframes for slide in/out animations */}
</div> </div>
</div> </div>
{/* Agent detail dialog */} {/* Agent detail dialog */}
{isDetailOpen && selectedAgent && ( {isDetailOpen && selectedAgent && (
<AgentDetail <AgentDetail

View file

@ -146,14 +146,13 @@ const MarketplaceAdminSettings = () => {
<OGDialog> <OGDialog>
<OGDialogTrigger asChild> <OGDialogTrigger asChild>
<Button <Button
variant={'outline'} variant="outline"
className="btn btn-neutral border-token-border-light relative gap-1 rounded-lg font-medium" className="relative h-12 rounded-xl border-border-medium font-medium"
> >
<ShieldEllipsis className="cursor-pointer" aria-hidden="true" /> <ShieldEllipsis className="cursor-pointer" aria-hidden="true" />
{localize('com_ui_admin_settings')}
</Button> </Button>
</OGDialogTrigger> </OGDialogTrigger>
<OGDialogContent className="w-full border-border-light bg-surface-primary text-text-primary md:w-1/4"> <OGDialogContent className="w-11/12 max-w-md border-border-light bg-surface-primary text-text-primary">
<OGDialogTitle>{`${localize('com_ui_admin_settings')} - ${localize( <OGDialogTitle>{`${localize('com_ui_admin_settings')} - ${localize(
'com_ui_marketplace', 'com_ui_marketplace',
)}`}</OGDialogTitle> )}`}</OGDialogTitle>

View file

@ -73,7 +73,7 @@ const SearchBar: React.FC<SearchBarProps> = ({ value, onSearch, className = '' }
value={searchTerm} value={searchTerm}
onChange={handleChange} onChange={handleChange}
placeholder={localize('com_agents_search_placeholder')} placeholder={localize('com_agents_search_placeholder')}
className="h-14 rounded-2xl border-border-medium bg-transparent pl-12 pr-12 text-lg text-text-primary shadow-md transition-[border-color,box-shadow] duration-200 placeholder:text-text-secondary focus:border-border-heavy focus:shadow-lg focus:ring-0" className="h-12 rounded-xl border-border-medium bg-transparent pl-12 pr-12 text-lg text-text-primary shadow-md transition-[border-color,box-shadow] duration-200 placeholder:text-text-secondary focus:border-border-heavy focus:shadow-lg focus:ring-0"
aria-label={localize('com_agents_search_aria')} aria-label={localize('com_agents_search_aria')}
aria-describedby="search-instructions search-results-count" aria-describedby="search-instructions search-results-count"
autoComplete="off" autoComplete="off"

View file

@ -53,7 +53,6 @@ const mockLocalize = jest.fn((key: string, options?: any) => {
com_agents_search_placeholder: 'Search agents...', com_agents_search_placeholder: 'Search agents...',
com_agents_clear_search: 'Clear search', com_agents_clear_search: 'Clear search',
com_agents_agent_card_label: `${options?.name} agent. ${options?.description}`, com_agents_agent_card_label: `${options?.name} agent. ${options?.description}`,
com_agents_no_description: 'No description available',
com_agents_grid_announcement: `Showing ${options?.count} agents in ${options?.category} category`, com_agents_grid_announcement: `Showing ${options?.count} agents in ${options?.category} category`,
com_agents_load_more_label: `Load more agents from ${options?.category} category`, com_agents_load_more_label: `Load more agents from ${options?.category} category`,
com_agents_error_retry: 'Try Again', com_agents_error_retry: 'Try Again',
@ -307,13 +306,6 @@ describe('Accessibility Improvements', () => {
expect(card).toHaveAttribute('role', 'button'); expect(card).toHaveAttribute('role', 'button');
}); });
it('handles agents without descriptions', () => {
const agentWithoutDesc = { ...mockAgent, description: undefined };
render(<AgentCard agent={agentWithoutDesc as any as t.Agent} onClick={jest.fn()} />);
expect(screen.getByText('No description available')).toBeInTheDocument();
});
it('supports keyboard interaction', () => { it('supports keyboard interaction', () => {
const onClick = jest.fn(); const onClick = jest.fn();
render(<AgentCard agent={mockAgent as t.Agent} onClick={onClick} />); render(<AgentCard agent={mockAgent as t.Agent} onClick={onClick} />);

View file

@ -48,7 +48,6 @@ describe('AgentCard', () => {
expect(screen.getByText('Test Agent')).toBeInTheDocument(); expect(screen.getByText('Test Agent')).toBeInTheDocument();
expect(screen.getByText('A test agent for testing purposes')).toBeInTheDocument(); expect(screen.getByText('A test agent for testing purposes')).toBeInTheDocument();
expect(screen.getByText('🔹')).toBeInTheDocument();
expect(screen.getByText('Test Support')).toBeInTheDocument(); expect(screen.getByText('Test Support')).toBeInTheDocument();
}); });
@ -152,7 +151,6 @@ describe('AgentCard', () => {
render(<AgentCard agent={agentWithAuthorName} onClick={mockOnClick} />); render(<AgentCard agent={agentWithAuthorName} onClick={mockOnClick} />);
expect(screen.getByText('🔹')).toBeInTheDocument();
expect(screen.getByText('John Doe')).toBeInTheDocument(); expect(screen.getByText('John Doe')).toBeInTheDocument();
}); });
@ -165,7 +163,6 @@ describe('AgentCard', () => {
render(<AgentCard agent={agentWithEmailOnly} onClick={mockOnClick} />); render(<AgentCard agent={agentWithEmailOnly} onClick={mockOnClick} />);
expect(screen.getByText('🔹')).toBeInTheDocument();
expect(screen.getByText('contact@example.com')).toBeInTheDocument(); expect(screen.getByText('contact@example.com')).toBeInTheDocument();
}); });
@ -178,7 +175,6 @@ describe('AgentCard', () => {
render(<AgentCard agent={agentWithBoth} onClick={mockOnClick} />); render(<AgentCard agent={agentWithBoth} onClick={mockOnClick} />);
expect(screen.getByText('🔹')).toBeInTheDocument();
expect(screen.getByText('Support Team')).toBeInTheDocument(); expect(screen.getByText('Support Team')).toBeInTheDocument();
expect(screen.queryByText('John Doe')).not.toBeInTheDocument(); expect(screen.queryByText('John Doe')).not.toBeInTheDocument();
}); });
@ -195,7 +191,6 @@ describe('AgentCard', () => {
render(<AgentCard agent={agentWithNameAndEmail} onClick={mockOnClick} />); render(<AgentCard agent={agentWithNameAndEmail} onClick={mockOnClick} />);
expect(screen.getByText('🔹')).toBeInTheDocument();
expect(screen.getByText('Support Team')).toBeInTheDocument(); expect(screen.getByText('Support Team')).toBeInTheDocument();
expect(screen.queryByText('support@example.com')).not.toBeInTheDocument(); expect(screen.queryByText('support@example.com')).not.toBeInTheDocument();
}); });

View file

@ -179,7 +179,6 @@ describe('AgentDetail', () => {
renderWithProviders(<AgentDetail {...defaultProps} agent={null as any} />); renderWithProviders(<AgentDetail {...defaultProps} agent={null as any} />);
expect(screen.getByText('com_agents_loading')).toBeInTheDocument(); expect(screen.getByText('com_agents_loading')).toBeInTheDocument();
expect(screen.getByText('com_agents_no_description')).toBeInTheDocument();
}); });
it('should render copy link button', () => { it('should render copy link button', () => {

View file

@ -0,0 +1,66 @@
import React, { useCallback, useContext } from 'react';
import { LayoutGrid } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import { TooltipAnchor, Button } from '@librechat/client';
import { useLocalize, useHasAccess, AuthContext } from '~/hooks';
interface AgentMarketplaceButtonProps {
isSmallScreen?: boolean;
toggleNav: () => void;
}
export default function AgentMarketplaceButton({
isSmallScreen,
toggleNav,
}: AgentMarketplaceButtonProps) {
const navigate = useNavigate();
const localize = useLocalize();
const authContext = useContext(AuthContext);
const hasAccessToAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.USE,
});
const hasAccessToMarketplace = useHasAccess({
permissionType: PermissionTypes.MARKETPLACE,
permission: Permissions.USE,
});
const handleAgentMarketplace = useCallback(() => {
navigate('/agents');
if (isSmallScreen) {
toggleNav();
}
}, [navigate, isSmallScreen, toggleNav]);
// Check if auth is ready (avoid race conditions)
const authReady =
authContext?.isAuthenticated !== undefined &&
(authContext?.isAuthenticated === false || authContext?.user !== undefined);
// Show agent marketplace when marketplace permission is enabled, auth is ready, and user has access to agents
const showAgentMarketplace = authReady && hasAccessToAgents && hasAccessToMarketplace;
if (!showAgentMarketplace) {
return null;
}
return (
<TooltipAnchor
description={localize('com_agents_marketplace')}
render={
<Button
variant="outline"
data-testid="nav-agents-marketplace-button"
aria-label={localize('com_agents_marketplace')}
className="rounded-full border-none bg-transparent p-2 hover:bg-surface-hover md:rounded-xl"
onClick={handleAgentMarketplace}
>
<LayoutGrid className="icon-lg text-text-primary" />
</Button>
}
/>
);
}

View file

@ -45,16 +45,9 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags, isSmallScreen }: Boo
data-testid="bookmark-menu" data-testid="bookmark-menu"
> >
{tags.length > 0 ? ( {tags.length > 0 ? (
<BookmarkFilledIcon <BookmarkFilledIcon className="icon-lg text-text-primary" aria-hidden="true" />
/** `isSmallScreen` is used because lazy loading is not influencing `md:` prefix for some reason */
className={cn('text-text-primary', isSmallScreen ? 'icon-md-heavy' : 'icon-lg')}
aria-hidden="true"
/>
) : ( ) : (
<BookmarkIcon <BookmarkIcon className="icon-lg text-text-primary" aria-hidden="true" />
className={cn('text-text-primary', isSmallScreen ? 'icon-md-heavy' : 'icon-lg')}
aria-hidden="true"
/>
)} )}
</MenuButton> </MenuButton>
} }

View file

@ -20,6 +20,7 @@ import store from '~/store';
const BookmarkNav = lazy(() => import('./Bookmarks/BookmarkNav')); const BookmarkNav = lazy(() => import('./Bookmarks/BookmarkNav'));
const AccountSettings = lazy(() => import('./AccountSettings')); const AccountSettings = lazy(() => import('./AccountSettings'));
const AgentMarketplaceButton = lazy(() => import('./AgentMarketplaceButton'));
const NAV_WIDTH_DESKTOP = '260px'; const NAV_WIDTH_DESKTOP = '260px';
const NAV_WIDTH_MOBILE = '320px'; const NAV_WIDTH_MOBILE = '320px';
@ -155,16 +156,22 @@ const Nav = memo(
); );
const headerButtons = useMemo( const headerButtons = useMemo(
() => () => (
hasAccessToBookmarks && ( <>
<Suspense fallback={null}>
<AgentMarketplaceButton isSmallScreen={isSmallScreen} toggleNav={toggleNavVisible} />
</Suspense>
{hasAccessToBookmarks && (
<> <>
<div className="mt-1.5" /> <div className="mt-1.5" />
<Suspense fallback={null}> <Suspense fallback={null}>
<BookmarkNav tags={tags} setTags={setTags} isSmallScreen={isSmallScreen} /> <BookmarkNav tags={tags} setTags={setTags} isSmallScreen={isSmallScreen} />
</Suspense> </Suspense>
</> </>
)}
</>
), ),
[hasAccessToBookmarks, tags, isSmallScreen], [hasAccessToBookmarks, tags, isSmallScreen, toggleNavVisible],
); );
const [isSearchLoading, setIsSearchLoading] = useState( const [isSearchLoading, setIsSearchLoading] = useState(

View file

@ -1,11 +1,10 @@
import React, { useCallback, useContext } from 'react'; import React, { useCallback } from 'react';
import { LayoutGrid } from 'lucide-react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, Constants, PermissionTypes, Permissions } from 'librechat-data-provider'; import { QueryKeys, Constants } from 'librechat-data-provider';
import { TooltipAnchor, NewChatIcon, MobileSidebar, Sidebar, Button } from '@librechat/client'; import { TooltipAnchor, NewChatIcon, MobileSidebar, Sidebar, Button } from '@librechat/client';
import type { TMessage } from 'librechat-data-provider'; import type { TMessage } from 'librechat-data-provider';
import { useLocalize, useNewConvo, useHasAccess, AuthContext } from '~/hooks'; import { useLocalize, useNewConvo } from '~/hooks';
import store from '~/store'; import store from '~/store';
export default function NewChat({ export default function NewChat({
@ -27,15 +26,6 @@ export default function NewChat({
const navigate = useNavigate(); const navigate = useNavigate();
const localize = useLocalize(); const localize = useLocalize();
const { conversation } = store.useCreateConversationAtom(index); const { conversation } = store.useCreateConversationAtom(index);
const authContext = useContext(AuthContext);
const hasAccessToAgents = useHasAccess({
permissionType: PermissionTypes.AGENTS,
permission: Permissions.USE,
});
const hasAccessToMarketplace = useHasAccess({
permissionType: PermissionTypes.MARKETPLACE,
permission: Permissions.USE,
});
const clickHandler: React.MouseEventHandler<HTMLButtonElement> = useCallback( const clickHandler: React.MouseEventHandler<HTMLButtonElement> = useCallback(
(e) => { (e) => {
@ -57,21 +47,6 @@ export default function NewChat({
[queryClient, conversation, newConvo, navigate, toggleNav, isSmallScreen], [queryClient, conversation, newConvo, navigate, toggleNav, isSmallScreen],
); );
const handleAgentMarketplace = useCallback(() => {
navigate('/agents');
if (isSmallScreen) {
toggleNav();
}
}, [navigate, isSmallScreen, toggleNav]);
// Check if auth is ready (avoid race conditions)
const authReady =
authContext?.isAuthenticated !== undefined &&
(authContext?.isAuthenticated === false || authContext?.user !== undefined);
// Show agent marketplace when marketplace permission is enabled, auth is ready, and user has access to agents
const showAgentMarketplace = authReady && hasAccessToAgents && hasAccessToMarketplace;
return ( return (
<> <>
<div className="flex items-center justify-between py-[2px] md:py-2"> <div className="flex items-center justify-between py-[2px] md:py-2">
@ -91,25 +66,7 @@ export default function NewChat({
</Button> </Button>
} }
/> />
<div className="flex"> <div className="flex gap-0.5">
{showAgentMarketplace && (
<div className="flex">
<TooltipAnchor
description={localize('com_agents_marketplace')}
render={
<Button
variant="outline"
data-testid="nav-agents-marketplace-button"
aria-label={localize('com_agents_marketplace')}
className="rounded-full border-none bg-transparent p-2 hover:bg-surface-hover md:rounded-xl"
onClick={handleAgentMarketplace}
>
<LayoutGrid className="icon-md md:h-6 md:w-6" />
</Button>
}
/>
</div>
)}
{headerButtons} {headerButtons}
<TooltipAnchor <TooltipAnchor
@ -123,7 +80,7 @@ export default function NewChat({
className="rounded-full border-none bg-transparent p-2 hover:bg-surface-hover md:rounded-xl" className="rounded-full border-none bg-transparent p-2 hover:bg-surface-hover md:rounded-xl"
onClick={clickHandler} onClick={clickHandler}
> >
<NewChatIcon className="icon-md md:h-6 md:w-6" /> <NewChatIcon className="icon-lg text-text-primary" />
</Button> </Button>
} }
/> />

View file

@ -1179,21 +1179,12 @@
"com_ui_permissions_failed_load": "Failed to load permissions. Please try again.", "com_ui_permissions_failed_load": "Failed to load permissions. Please try again.",
"com_ui_permissions_updated_success": "Permissions updated successfully", "com_ui_permissions_updated_success": "Permissions updated successfully",
"com_ui_permissions_failed_update": "Failed to update permissions. Please try again.", "com_ui_permissions_failed_update": "Failed to update permissions. Please try again.",
"com_ui_manage_permissions_for": "Manage Permissions for",
"com_ui_current_access": "Current Access",
"com_ui_no_users_groups_access": "No users or groups have access",
"com_ui_shared_with_count": "Shared with {{0}} {{1}}{{2}}",
"com_ui_person": "person",
"com_ui_people": "people", "com_ui_people": "people",
"com_ui_and_public": " and public",
"com_ui_revoke_all": "Revoke All",
"com_ui_loading_permissions": "Loading permissions...",
"com_ui_user_group_permissions": "User & Group Permissions", "com_ui_user_group_permissions": "User & Group Permissions",
"com_ui_no_individual_access": "No individual users or groups have access to this agent", "com_ui_no_individual_access": "No individual users or groups have access to this agent",
"com_ui_share_everyone": "Share with everyone", "com_ui_share_everyone": "Share with everyone",
"com_ui_share_everyone_description_var": "This {{resource}} will be available to everyone. Please make sure the {{resource}} is really meant to be shared with everyone. Be careful with your data.", "com_ui_share_everyone_description_var": "This {{resource}} will be available to everyone. Please make sure the {{resource}} is really meant to be shared with everyone. Be careful with your data.",
"com_ui_save_changes": "Save Changes", "com_ui_save_changes": "Save Changes",
"com_ui_unsaved_changes": "You have unsaved changes",
"com_ui_everyone_permission_level": "Everyone's permission level", "com_ui_everyone_permission_level": "Everyone's permission level",
"com_ui_at_least_one_owner_required": "At least one owner is required", "com_ui_at_least_one_owner_required": "At least one owner is required",
"com_agents_marketplace": "Agent Marketplace", "com_agents_marketplace": "Agent Marketplace",
@ -1244,7 +1235,6 @@
"com_agents_agent_card_label": "{{name}} agent. {{description}}", "com_agents_agent_card_label": "{{name}} agent. {{description}}",
"com_agents_empty_state_heading": "No agents found", "com_agents_empty_state_heading": "No agents found",
"com_agents_search_empty_heading": "No search results", "com_agents_search_empty_heading": "No search results",
"com_agents_no_description": "No description available",
"com_agents_results_for": "Results for '{{query}}'", "com_agents_results_for": "Results for '{{query}}'",
"com_agents_marketplace_subtitle": "Discover and use powerful AI agents to enhance your workflows and productivity", "com_agents_marketplace_subtitle": "Discover and use powerful AI agents to enhance your workflows and productivity",
"com_agents_no_more_results": "You've reached the end of the results", "com_agents_no_more_results": "You've reached the end of the results",

2
package-lock.json generated
View file

@ -51470,7 +51470,7 @@
}, },
"packages/client": { "packages/client": {
"name": "@librechat/client", "name": "@librechat/client",
"version": "0.2.4", "version": "0.2.5",
"devDependencies": { "devDependencies": {
"@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-alias": "^5.1.0",
"@rollup/plugin-commonjs": "^25.0.2", "@rollup/plugin-commonjs": "^25.0.2",

View file

@ -1,6 +1,6 @@
{ {
"name": "@librechat/client", "name": "@librechat/client",
"version": "0.2.4", "version": "0.2.5",
"description": "React components for LibreChat", "description": "React components for LibreChat",
"main": "dist/index.js", "main": "dist/index.js",
"module": "dist/index.es.js", "module": "dist/index.es.js",

View file

@ -12,7 +12,7 @@ const buttonVariants = cva(
destructive: destructive:
'bg-surface-destructive text-destructive-foreground hover:bg-surface-destructive-hover', 'bg-surface-destructive text-destructive-foreground hover:bg-surface-destructive-hover',
outline: outline:
'text-text-primary border border-border-light bg-background hover:bg-accent hover:text-accent-foreground', 'text-text-primary border border-border-light bg-transparent hover:bg-accent hover:text-accent-foreground',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost: 'hover:bg-surface-hover hover:text-accent-foreground', ghost: 'hover:bg-surface-hover hover:text-accent-foreground',
link: 'text-primary underline-offset-4 hover:underline', link: 'text-primary underline-offset-4 hover:underline',