mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-20 02:10:15 +01:00
🪟 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:
parent
c78fd0fc83
commit
4ec7bcb60f
17 changed files with 164 additions and 162 deletions
|
|
@ -28,7 +28,7 @@ const AgentCard: React.FC<AgentCardProps> = ({ agent, onClick, className = '' })
|
|||
onClick={onClick}
|
||||
aria-label={localize('com_agents_agent_card_label', {
|
||||
name: agent.name,
|
||||
description: agent.description || localize('com_agents_no_description'),
|
||||
description: agent.description ?? '',
|
||||
})}
|
||||
aria-describedby={`agent-${agent.id}-description`}
|
||||
tabIndex={0}
|
||||
|
|
@ -47,51 +47,48 @@ const AgentCard: React.FC<AgentCardProps> = ({ agent, onClick, className = '' })
|
|||
<div className="flex-shrink-0">{renderAgentAvatar(agent, { size: 'sm' })}</div>
|
||||
|
||||
{/* 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 && (
|
||||
<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">
|
||||
{agent.category.charAt(0).toUpperCase() + agent.category.slice(1)}
|
||||
</Label>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Right column: Name, description, and other content */}
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex h-full min-w-0 flex-1 flex-col justify-between space-y-1">
|
||||
<div className="space-y-1">
|
||||
{/* Agent name */}
|
||||
<Label className="mb-1 line-clamp-1 text-xl font-semibold text-text-primary">
|
||||
{agent.name}
|
||||
</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);
|
||||
if (displayName) {
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<div className="flex items-center text-sm text-text-secondary">
|
||||
<Label className="mr-1">🔹</Label>
|
||||
<Label>{displayName}</Label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
</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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -126,10 +126,7 @@ const AgentDetail: React.FC<AgentDetailProps> = ({ agent, isOpen, onClose }) =>
|
|||
|
||||
return (
|
||||
<OGDialog open={isOpen} onOpenChange={(open) => !open && onClose()}>
|
||||
<OGDialogContent
|
||||
ref={dialogRef}
|
||||
className="max-h-[90vh] overflow-y-auto py-8 sm:max-w-[450px]"
|
||||
>
|
||||
<OGDialogContent ref={dialogRef} className="max-h-[90vh] w-11/12 max-w-lg overflow-y-auto">
|
||||
{/* Copy link button - positioned next to close button */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
|
@ -161,11 +158,7 @@ const AgentDetail: React.FC<AgentDetailProps> = ({ agent, isOpen, onClose }) =>
|
|||
|
||||
{/* Agent description - below contact */}
|
||||
<div className="mt-4 whitespace-pre-wrap px-6 text-center text-base text-text-primary">
|
||||
{agent?.description || (
|
||||
<span className="italic text-text-tertiary">
|
||||
{localize('com_agents_no_description')}
|
||||
</span>
|
||||
)}
|
||||
{agent?.description}
|
||||
</div>
|
||||
|
||||
{/* Action button */}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import React from 'react';
|
||||
import type t from 'librechat-data-provider';
|
||||
import useLocalize from '~/hooks/useLocalize';
|
||||
import { useMediaQuery } from '@librechat/client';
|
||||
import { SmartLoader } from './SmartLoader';
|
||||
import { useLocalize } from '~/hooks/';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
/**
|
||||
|
|
@ -33,6 +34,7 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
|
|||
onChange,
|
||||
}) => {
|
||||
const localize = useLocalize();
|
||||
const isSmallScreen = useMediaQuery('(max-width: 768px)');
|
||||
|
||||
// Helper function to get category display name from database data
|
||||
const getCategoryDisplayName = (category: t.TCategory) => {
|
||||
|
|
@ -120,10 +122,24 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
|
|||
const tabsContent = (
|
||||
<div className="w-full pb-2">
|
||||
<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"
|
||||
aria-label={localize('com_agents_category_tabs_label')}
|
||||
aria-orientation="horizontal"
|
||||
style={
|
||||
isSmallScreen
|
||||
? {
|
||||
scrollbarWidth: 'none',
|
||||
msOverflowStyle: 'none',
|
||||
WebkitOverflowScrolling: 'touch',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{categories.map((category, index) => (
|
||||
<button
|
||||
|
|
@ -132,10 +148,11 @@ const CategoryTabs: React.FC<CategoryTabsProps> = ({
|
|||
onClick={() => onChange(category.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e, category.value)}
|
||||
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
|
||||
? '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"
|
||||
aria-selected={activeTab === category.value}
|
||||
|
|
|
|||
|
|
@ -285,10 +285,6 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
|
|||
ref={scrollContainerRef}
|
||||
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 */}
|
||||
{!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">
|
||||
|
|
@ -319,10 +315,10 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hero Section - scrolls away */}
|
||||
{!isSmallScreen && (
|
||||
<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">
|
||||
{localize('com_agents_marketplace')}
|
||||
</h1>
|
||||
|
|
@ -331,7 +327,7 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)}
|
||||
{/* Sticky wrapper for search bar and categories */}
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -341,8 +337,11 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
|
|||
>
|
||||
<div className="container mx-auto max-w-4xl px-4">
|
||||
{/* 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} />
|
||||
{/* TODO: Remove this once we have a better way to handle admin settings */}
|
||||
{/* Admin Settings */}
|
||||
<MarketplaceAdminSettings />
|
||||
</div>
|
||||
|
||||
{/* Category tabs */}
|
||||
|
|
@ -354,7 +353,6 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scrollable content area */}
|
||||
<div className="container mx-auto max-w-4xl px-4 pb-8">
|
||||
{/* 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 */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent detail dialog */}
|
||||
{isDetailOpen && selectedAgent && (
|
||||
<AgentDetail
|
||||
|
|
|
|||
|
|
@ -146,14 +146,13 @@ const MarketplaceAdminSettings = () => {
|
|||
<OGDialog>
|
||||
<OGDialogTrigger asChild>
|
||||
<Button
|
||||
variant={'outline'}
|
||||
className="btn btn-neutral border-token-border-light relative gap-1 rounded-lg font-medium"
|
||||
variant="outline"
|
||||
className="relative h-12 rounded-xl border-border-medium font-medium"
|
||||
>
|
||||
<ShieldEllipsis className="cursor-pointer" aria-hidden="true" />
|
||||
{localize('com_ui_admin_settings')}
|
||||
</Button>
|
||||
</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(
|
||||
'com_ui_marketplace',
|
||||
)}`}</OGDialogTitle>
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ const SearchBar: React.FC<SearchBarProps> = ({ value, onSearch, className = '' }
|
|||
value={searchTerm}
|
||||
onChange={handleChange}
|
||||
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-describedby="search-instructions search-results-count"
|
||||
autoComplete="off"
|
||||
|
|
|
|||
|
|
@ -53,7 +53,6 @@ const mockLocalize = jest.fn((key: string, options?: any) => {
|
|||
com_agents_search_placeholder: 'Search agents...',
|
||||
com_agents_clear_search: 'Clear search',
|
||||
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_load_more_label: `Load more agents from ${options?.category} category`,
|
||||
com_agents_error_retry: 'Try Again',
|
||||
|
|
@ -307,13 +306,6 @@ describe('Accessibility Improvements', () => {
|
|||
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', () => {
|
||||
const onClick = jest.fn();
|
||||
render(<AgentCard agent={mockAgent as t.Agent} onClick={onClick} />);
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ describe('AgentCard', () => {
|
|||
|
||||
expect(screen.getByText('Test Agent')).toBeInTheDocument();
|
||||
expect(screen.getByText('A test agent for testing purposes')).toBeInTheDocument();
|
||||
expect(screen.getByText('🔹')).toBeInTheDocument();
|
||||
expect(screen.getByText('Test Support')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -152,7 +151,6 @@ describe('AgentCard', () => {
|
|||
|
||||
render(<AgentCard agent={agentWithAuthorName} onClick={mockOnClick} />);
|
||||
|
||||
expect(screen.getByText('🔹')).toBeInTheDocument();
|
||||
expect(screen.getByText('John Doe')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -165,7 +163,6 @@ describe('AgentCard', () => {
|
|||
|
||||
render(<AgentCard agent={agentWithEmailOnly} onClick={mockOnClick} />);
|
||||
|
||||
expect(screen.getByText('🔹')).toBeInTheDocument();
|
||||
expect(screen.getByText('contact@example.com')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
|
|
@ -178,7 +175,6 @@ describe('AgentCard', () => {
|
|||
|
||||
render(<AgentCard agent={agentWithBoth} onClick={mockOnClick} />);
|
||||
|
||||
expect(screen.getByText('🔹')).toBeInTheDocument();
|
||||
expect(screen.getByText('Support Team')).toBeInTheDocument();
|
||||
expect(screen.queryByText('John Doe')).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -195,7 +191,6 @@ describe('AgentCard', () => {
|
|||
|
||||
render(<AgentCard agent={agentWithNameAndEmail} onClick={mockOnClick} />);
|
||||
|
||||
expect(screen.getByText('🔹')).toBeInTheDocument();
|
||||
expect(screen.getByText('Support Team')).toBeInTheDocument();
|
||||
expect(screen.queryByText('support@example.com')).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -179,7 +179,6 @@ describe('AgentDetail', () => {
|
|||
renderWithProviders(<AgentDetail {...defaultProps} agent={null as any} />);
|
||||
|
||||
expect(screen.getByText('com_agents_loading')).toBeInTheDocument();
|
||||
expect(screen.getByText('com_agents_no_description')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render copy link button', () => {
|
||||
|
|
|
|||
66
client/src/components/Nav/AgentMarketplaceButton.tsx
Normal file
66
client/src/components/Nav/AgentMarketplaceButton.tsx
Normal 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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -45,16 +45,9 @@ const BookmarkNav: FC<BookmarkNavProps> = ({ tags, setTags, isSmallScreen }: Boo
|
|||
data-testid="bookmark-menu"
|
||||
>
|
||||
{tags.length > 0 ? (
|
||||
<BookmarkFilledIcon
|
||||
/** `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"
|
||||
/>
|
||||
<BookmarkFilledIcon className="icon-lg text-text-primary" aria-hidden="true" />
|
||||
) : (
|
||||
<BookmarkIcon
|
||||
className={cn('text-text-primary', isSmallScreen ? 'icon-md-heavy' : 'icon-lg')}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<BookmarkIcon className="icon-lg text-text-primary" aria-hidden="true" />
|
||||
)}
|
||||
</MenuButton>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import store from '~/store';
|
|||
|
||||
const BookmarkNav = lazy(() => import('./Bookmarks/BookmarkNav'));
|
||||
const AccountSettings = lazy(() => import('./AccountSettings'));
|
||||
const AgentMarketplaceButton = lazy(() => import('./AgentMarketplaceButton'));
|
||||
|
||||
const NAV_WIDTH_DESKTOP = '260px';
|
||||
const NAV_WIDTH_MOBILE = '320px';
|
||||
|
|
@ -155,16 +156,22 @@ const Nav = memo(
|
|||
);
|
||||
|
||||
const headerButtons = useMemo(
|
||||
() =>
|
||||
hasAccessToBookmarks && (
|
||||
() => (
|
||||
<>
|
||||
<Suspense fallback={null}>
|
||||
<AgentMarketplaceButton isSmallScreen={isSmallScreen} toggleNav={toggleNavVisible} />
|
||||
</Suspense>
|
||||
{hasAccessToBookmarks && (
|
||||
<>
|
||||
<div className="mt-1.5" />
|
||||
<Suspense fallback={null}>
|
||||
<BookmarkNav tags={tags} setTags={setTags} isSmallScreen={isSmallScreen} />
|
||||
</Suspense>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
[hasAccessToBookmarks, tags, isSmallScreen],
|
||||
[hasAccessToBookmarks, tags, isSmallScreen, toggleNavVisible],
|
||||
);
|
||||
|
||||
const [isSearchLoading, setIsSearchLoading] = useState(
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
import React, { useCallback, useContext } from 'react';
|
||||
import { LayoutGrid } from 'lucide-react';
|
||||
import React, { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
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 type { TMessage } from 'librechat-data-provider';
|
||||
import { useLocalize, useNewConvo, useHasAccess, AuthContext } from '~/hooks';
|
||||
import { useLocalize, useNewConvo } from '~/hooks';
|
||||
import store from '~/store';
|
||||
|
||||
export default function NewChat({
|
||||
|
|
@ -27,15 +26,6 @@ export default function NewChat({
|
|||
const navigate = useNavigate();
|
||||
const localize = useLocalize();
|
||||
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(
|
||||
(e) => {
|
||||
|
|
@ -57,21 +47,6 @@ export default function NewChat({
|
|||
[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 (
|
||||
<>
|
||||
<div className="flex items-center justify-between py-[2px] md:py-2">
|
||||
|
|
@ -91,25 +66,7 @@ export default function NewChat({
|
|||
</Button>
|
||||
}
|
||||
/>
|
||||
<div className="flex">
|
||||
{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>
|
||||
)}
|
||||
<div className="flex gap-0.5">
|
||||
{headerButtons}
|
||||
|
||||
<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"
|
||||
onClick={clickHandler}
|
||||
>
|
||||
<NewChatIcon className="icon-md md:h-6 md:w-6" />
|
||||
<NewChatIcon className="icon-lg text-text-primary" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1179,21 +1179,12 @@
|
|||
"com_ui_permissions_failed_load": "Failed to load permissions. Please try again.",
|
||||
"com_ui_permissions_updated_success": "Permissions updated successfully",
|
||||
"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_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_no_individual_access": "No individual users or groups have access to this agent",
|
||||
"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_save_changes": "Save Changes",
|
||||
"com_ui_unsaved_changes": "You have unsaved changes",
|
||||
"com_ui_everyone_permission_level": "Everyone's permission level",
|
||||
"com_ui_at_least_one_owner_required": "At least one owner is required",
|
||||
"com_agents_marketplace": "Agent Marketplace",
|
||||
|
|
@ -1244,7 +1235,6 @@
|
|||
"com_agents_agent_card_label": "{{name}} agent. {{description}}",
|
||||
"com_agents_empty_state_heading": "No agents found",
|
||||
"com_agents_search_empty_heading": "No search results",
|
||||
"com_agents_no_description": "No description available",
|
||||
"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_no_more_results": "You've reached the end of the results",
|
||||
|
|
|
|||
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -51470,7 +51470,7 @@
|
|||
},
|
||||
"packages/client": {
|
||||
"name": "@librechat/client",
|
||||
"version": "0.2.4",
|
||||
"version": "0.2.5",
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-alias": "^5.1.0",
|
||||
"@rollup/plugin-commonjs": "^25.0.2",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@librechat/client",
|
||||
"version": "0.2.4",
|
||||
"version": "0.2.5",
|
||||
"description": "React components for LibreChat",
|
||||
"main": "dist/index.js",
|
||||
"module": "dist/index.es.js",
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const buttonVariants = cva(
|
|||
destructive:
|
||||
'bg-surface-destructive text-destructive-foreground hover:bg-surface-destructive-hover',
|
||||
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',
|
||||
ghost: 'hover:bg-surface-hover hover:text-accent-foreground',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue