diff --git a/client/src/components/Chat/Header.tsx b/client/src/components/Chat/Header.tsx index 567ae8fb0..bf04a2e4f 100644 --- a/client/src/components/Chat/Header.tsx +++ b/client/src/components/Chat/Header.tsx @@ -48,7 +48,7 @@ export default function Header() { : 'pointer-events-none translate-x-[-100px] opacity-0' }`} > - +
>; + className?: string; }) { const localize = useLocalize(); return ( @@ -16,7 +19,10 @@ export default function OpenSidebar({ 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" + className={cn( + 'rounded-xl border border-border-light bg-surface-secondary p-2 hover:bg-surface-hover', + className, + )} onClick={() => setNavVisible((prev) => { localStorage.setItem('navVisible', JSON.stringify(!prev)); diff --git a/client/src/components/Nav/NewChat.tsx b/client/src/components/Nav/NewChat.tsx index 548f20980..82b7d78be 100644 --- a/client/src/components/Nav/NewChat.tsx +++ b/client/src/components/Nav/NewChat.tsx @@ -113,20 +113,20 @@ export default function NewChat({ {/* Agent Marketplace button - separate row like ChatGPT */} {showAgentMarketplace && ( -
+
- - {localize('com_nav_agents_marketplace')} + + {localize('com_agents_marketplace')} } diff --git a/client/src/components/SidePanel/Agents/AgentCard.tsx b/client/src/components/SidePanel/Agents/AgentCard.tsx index 03d1e491c..292aa5957 100644 --- a/client/src/components/SidePanel/Agents/AgentCard.tsx +++ b/client/src/components/SidePanel/Agents/AgentCard.tsx @@ -24,7 +24,7 @@ const AgentCard: React.FC = ({ agent, onClick, className = '' }) 'group relative flex overflow-hidden rounded-2xl', 'cursor-pointer transition-colors duration-200', 'aspect-[5/2.5] w-full', - 'bg-gray-50 hover:bg-gray-100 dark:bg-gray-700 dark:hover:bg-gray-600', + 'bg-surface-tertiary hover:bg-surface-hover-alt', className, )} onClick={onClick} @@ -51,7 +51,7 @@ const AgentCard: React.FC = ({ agent, onClick, className = '' }) {/* Agent info section - right side, responsive */}
{/* Agent name - responsive text sizing */} -

+

{agent.name}

@@ -59,13 +59,15 @@ const AgentCard: React.FC = ({ agent, onClick, className = '' })

{agent.description || ( - {localize('com_agents_no_description')} + + {localize('com_agents_no_description')} + )}

@@ -75,7 +77,7 @@ const AgentCard: React.FC = ({ agent, onClick, className = '' }) if (displayName) { return ( -
+
{localize('com_agents_created_by')} {displayName}
diff --git a/client/src/components/SidePanel/Agents/AgentDetail.tsx b/client/src/components/SidePanel/Agents/AgentDetail.tsx index 874001174..57a05ec52 100644 --- a/client/src/components/SidePanel/Agents/AgentDetail.tsx +++ b/client/src/components/SidePanel/Agents/AgentDetail.tsx @@ -1,6 +1,7 @@ -import React, { useRef, useState, useEffect } from 'react'; +import React, { useRef } from 'react'; +import { Link } from 'lucide-react'; import { useQueryClient } from '@tanstack/react-query'; -import { Dialog, DialogContent, Button, DotsIcon, useToastContext } from '@librechat/client'; +import { OGDialog, OGDialogContent, Button, useToastContext } from '@librechat/client'; import { QueryKeys, Constants, @@ -37,26 +38,7 @@ const AgentDetail: React.FC = ({ agent, isOpen, onClose }) => const { conversation, newConversation } = useChatContext(); const { showToast } = useToastContext(); const dialogRef = useRef(null); - const dropdownRef = useRef(null); - const [dropdownOpen, setDropdownOpen] = useState(false); const queryClient = useQueryClient(); - // Close dropdown when clicking outside the dropdown menu - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if ( - dropdownOpen && - dropdownRef.current && - !dropdownRef.current.contains(event.target as Node) - ) { - setDropdownOpen(false); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [dropdownOpen]); /** * Navigate to chat with the selected agent @@ -143,63 +125,46 @@ const AgentDetail: React.FC = ({ agent, isOpen, onClose }) => }; return ( - !open && onClose()}> - - {/* Context menu - top right */} -
- - - {/* Simple dropdown menu */} - {dropdownOpen && ( -
- -
- )} -
+ !open && onClose()}> + + {/* Copy link button - positioned next to close button */} + {/* Agent avatar - top center */}
{renderAgentAvatar(agent, { size: 'xl' })}
{/* Agent name - center aligned below image */}
-

+

{agent?.name || localize('com_agents_loading')}

{/* Contact info - center aligned below name */} {agent?.support_contact && formatContact() && ( -
+
{localize('com_agents_contact')}: {formatContact()}
)} {/* Agent description - below contact */} -
+
{agent?.description || ( - {localize('com_agents_no_description')} + + {localize('com_agents_no_description')} + )}
@@ -209,8 +174,8 @@ const AgentDetail: React.FC = ({ agent, isOpen, onClose }) => {localize('com_agents_start_chat')}
- -
+ + ); }; diff --git a/client/src/components/SidePanel/Agents/AgentGrid.tsx b/client/src/components/SidePanel/Agents/AgentGrid.tsx index 16a7d2a1c..1b07b3969 100644 --- a/client/src/components/SidePanel/Agents/AgentGrid.tsx +++ b/client/src/components/SidePanel/Agents/AgentGrid.tsx @@ -3,8 +3,7 @@ import { Button, Spinner } from '@librechat/client'; import { PERMISSION_BITS } from 'librechat-data-provider'; import type t from 'librechat-data-provider'; import { useMarketplaceAgentsInfiniteQuery } from '~/data-provider/Agents'; -import { useAgentCategories } from '~/hooks/Agents'; -import useLocalize from '~/hooks/useLocalize'; +import { useAgentCategories, useLocalize } from '~/hooks'; import { useHasData } from './SmartLoader'; import ErrorDisplay from './ErrorDisplay'; import AgentCard from './AgentCard'; @@ -124,8 +123,8 @@ const AgentGrid: React.FC = ({ category, searchQuery, onSelectAg const loadingSkeleton = (
-
-
+
+
{Array(6) @@ -134,15 +133,21 @@ const AgentGrid: React.FC = ({ category, searchQuery, onSelectAg
-
-
-
-
-
+
+ {/* Avatar skeleton */} +
+
+
+ {/* Content skeleton */} +
+
+
+
))} @@ -178,7 +183,7 @@ const AgentGrid: React.FC = ({ category, searchQuery, onSelectAg {searchQuery && (

@@ -190,7 +195,7 @@ const AgentGrid: React.FC = ({ category, searchQuery, onSelectAg {/* Handle empty results with enhanced accessibility */} {(!currentAgents || currentAgents.length === 0) && !isLoading && !isFetching ? (
= ({ category, searchQuery, onSelectAg variant="outline" onClick={handleLoadMore} className={cn( - 'min-w-[160px] border-2 border-gray-300 bg-white px-6 py-3 font-medium text-gray-700', - 'shadow-sm transition-all duration-200 hover:border-gray-400 hover:bg-gray-50', - 'hover:shadow-md focus:ring-2 focus:ring-blue-500 focus:ring-offset-2', - 'dark:border-gray-600 dark:bg-gray-800 dark:text-gray-200', - 'dark:hover:border-gray-500 dark:hover:bg-gray-700 dark:focus:ring-blue-400', + 'min-w-[160px] border-2 border-border-medium bg-surface-primary px-6 py-3 font-medium text-text-primary', + 'shadow-sm transition-all duration-200 hover:border-border-heavy hover:bg-surface-hover', + 'hover:shadow-md focus:ring-2 focus:ring-ring focus:ring-offset-2', )} aria-label={localize('com_agents_load_more_label', { category: getCategoryDisplayName(category), diff --git a/client/src/components/SidePanel/Agents/AgentMarketplace.tsx b/client/src/components/SidePanel/Agents/AgentMarketplace.tsx index 339412d45..ed4088eac 100644 --- a/client/src/components/SidePanel/Agents/AgentMarketplace.tsx +++ b/client/src/components/SidePanel/Agents/AgentMarketplace.tsx @@ -1,9 +1,9 @@ import React, { useState, useEffect, useMemo } from 'react'; +import { useRecoilState } from 'recoil'; import { useOutletContext } from 'react-router-dom'; import { useQueryClient } from '@tanstack/react-query'; -import { useSetRecoilState, useRecoilValue } from 'recoil'; -import { TooltipAnchor, Button, NewChatIcon } from '@librechat/client'; import { useSearchParams, useParams, useNavigate } from 'react-router-dom'; +import { TooltipAnchor, Button, NewChatIcon, useMediaQuery } from '@librechat/client'; import { PermissionTypes, Permissions, QueryKeys, Constants } from 'librechat-data-provider'; import type t from 'librechat-data-provider'; import type { ContextType } from '~/common'; @@ -17,6 +17,7 @@ import CategoryTabs from './CategoryTabs'; import AgentDetail from './AgentDetail'; import SearchBar from './SearchBar'; import AgentGrid from './AgentGrid'; +import { cn } from '~/utils'; import store from '~/store'; interface AgentMarketplaceProps { @@ -33,13 +34,14 @@ interface AgentMarketplaceProps { const AgentMarketplace: React.FC = ({ className = '' }) => { const localize = useLocalize(); const navigate = useNavigate(); - const queryClient = useQueryClient(); - const { conversation, newConversation } = useChatContext(); - const [searchParams, setSearchParams] = useSearchParams(); const { category } = useParams(); - const setHideSidePanel = useSetRecoilState(store.hideSidePanel); - const hideSidePanel = useRecoilValue(store.hideSidePanel); + const queryClient = useQueryClient(); + const [searchParams, setSearchParams] = useSearchParams(); + const { conversation, newConversation } = useChatContext(); + + const isSmallScreen = useMediaQuery('(max-width: 768px)'); const { navVisible, setNavVisible } = useOutletContext(); + const [hideSidePanel, setHideSidePanel] = useRecoilState(store.hideSidePanel); // Get URL parameters (default to 'promoted' instead of 'all') const activeTab = category || 'promoted'; @@ -203,123 +205,145 @@ const AgentMarketplace: React.FC = ({ className = '' }) = fullPanelCollapse={fullCollapse} defaultCollapsed={defaultCollapsed} > -
- {/* Simplified header for agents marketplace - only show nav controls when needed */} -
-
- {!navVisible && } - {!navVisible && ( - - - - } - /> - )} -
-
-
- {/* Hero Section - ChatGPT Style */} -
-

- {localize('com_agents_marketplace')} -

-

- {localize('com_agents_marketplace_subtitle')} -

- - {/* Search bar */} -
- -
-
- - {/* Category tabs */} - - - {/* Category header - only show when not searching */} - {!searchQuery && ( -
- {(() => { - // Get category data for display - const getCategoryData = () => { - if (activeTab === 'promoted') { - return { - name: localize('com_agents_top_picks'), - description: localize('com_agents_recommended'), - }; - } - if (activeTab === 'all') { - return { - name: 'All Agents', - description: 'Browse all shared agents across all categories', - }; - } - - // Find the category in the API data - const categoryData = categoriesQuery.data?.find( - (cat) => cat.value === activeTab, - ); - if (categoryData) { - return { - name: categoryData.label, - description: categoryData.description || '', - }; - } - - // Fallback for unknown categories - return { - name: activeTab.charAt(0).toUpperCase() + activeTab.slice(1), - description: '', - }; - }; - - const { name, description } = getCategoryData(); - - return ( -
-

- {name} -

- {description && ( -

{description}

- )} -
- ); - })()} +
+ {/* Scrollable container */} +
+ {/* Simplified header for agents marketplace - only show nav controls when needed */} + {!isSmallScreen && ( +
+
+ {!navVisible ? ( + <> + + + + + } + /> + + ) : ( + // Invisible placeholder to maintain height +
+ )} +
)} - {/* Agent grid */} - -
+ {/* Hero Section - scrolls away */} +
+
+

+ {localize('com_agents_marketplace')} +

+

+ {localize('com_agents_marketplace_subtitle')} +

+
+
- {/* Agent detail dialog */} - {isDetailOpen && selectedAgent && ( - - )} + {/* Sticky wrapper for search bar and categories */} +
+
+ {/* Search bar */} +
+ +
+ + {/* Category tabs */} + +
+
+ + {/* Scrollable content area */} +
+ {/* Category header - only show when not searching */} + {!searchQuery && ( +
+ {(() => { + // Get category data for display + const getCategoryData = () => { + if (activeTab === 'promoted') { + return { + name: localize('com_agents_top_picks'), + description: localize('com_agents_recommended'), + }; + } + if (activeTab === 'all') { + return { + name: 'All Agents', + description: 'Browse all shared agents across all categories', + }; + } + + // Find the category in the API data + const categoryData = categoriesQuery.data?.find( + (cat) => cat.value === activeTab, + ); + if (categoryData) { + return { + name: categoryData.label, + description: categoryData.description || '', + }; + } + + // Fallback for unknown categories + return { + name: activeTab.charAt(0).toUpperCase() + activeTab.slice(1), + description: '', + }; + }; + + const { name, description } = getCategoryData(); + + return ( +
+

{name}

+ {description && ( +

{description}

+ )} +
+ ); + })()} +
+ )} + + {/* Agent grid */} + +
+ + {/* Agent detail dialog */} + {isDetailOpen && selectedAgent && ( + + )} +
diff --git a/client/src/components/SidePanel/Agents/CategoryTabs.tsx b/client/src/components/SidePanel/Agents/CategoryTabs.tsx index ef11472ef..f232c7ad0 100644 --- a/client/src/components/SidePanel/Agents/CategoryTabs.tsx +++ b/client/src/components/SidePanel/Agents/CategoryTabs.tsx @@ -1,7 +1,5 @@ import React from 'react'; - import type t from 'librechat-data-provider'; - import useLocalize from '~/hooks/useLocalize'; import { SmartLoader } from './SmartLoader'; import { cn } from '~/utils'; @@ -50,16 +48,14 @@ const CategoryTabs: React.FC = ({ // Loading skeleton component const loadingSkeleton = ( -
-
-
- {[...Array(6)].map((_, i) => ( -
- ))} -
+
+
+ {[...Array(6)].map((_, i) => ( +
+ ))}
); @@ -106,52 +102,54 @@ const CategoryTabs: React.FC = ({ // Early return if no categories available if (!isLoading && (!categories || categories.length === 0)) { return ( -
{localize('com_agents_no_categories')}
+
{localize('com_ui_no_categories')}
); } // Main tabs content const tabsContent = ( -
-
- {/* Accessible tab navigation with proper ARIA attributes */} -
- {categories.map((category, index) => ( - - ))} -
+
+
+ {categories.map((category, index) => ( + + ))}
); diff --git a/client/src/components/SidePanel/Agents/SearchBar.tsx b/client/src/components/SidePanel/Agents/SearchBar.tsx index 46c2cca0a..4c473b216 100644 --- a/client/src/components/SidePanel/Agents/SearchBar.tsx +++ b/client/src/components/SidePanel/Agents/SearchBar.tsx @@ -73,7 +73,7 @@ const SearchBar: React.FC = ({ value, onSearch, className = '' } value={searchTerm} onChange={handleChange} placeholder={localize('com_agents_search_placeholder')} - className="h-14 rounded-2xl border-2 border-gray-200 bg-white pl-12 pr-12 text-lg text-gray-900 shadow-lg placeholder:text-gray-500 focus:border-gray-300 focus:ring-0 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 dark:placeholder:text-gray-400 dark:focus:border-gray-500" + className="h-14 rounded-2xl border-2 border-border-medium bg-transparent pl-12 pr-12 text-lg text-text-primary shadow-lg placeholder:text-text-tertiary focus:border-border-heavy focus:ring-0" aria-label={localize('com_agents_search_aria')} aria-describedby="search-instructions search-results-count" autoComplete="off" @@ -82,7 +82,7 @@ const SearchBar: React.FC = ({ value, onSearch, className = '' } {/* Search icon with proper accessibility */} {/* Hidden instructions for screen readers */} @@ -95,7 +95,7 @@ const SearchBar: React.FC = ({ value, onSearch, className = '' } + ), + }; + }); + return (
- { - const localizedInfo = getLocalizedRoleInfo(role.accessRoleId); - return { - value: role.accessRoleId, - label: localizedInfo.name, - description: localizedInfo.description, - }; - })} - showLabel={false} - value={ - selectedRole - ? (() => { - const localizedInfo = getLocalizedRoleInfo(selectedRole.accessRoleId); - return { - value: selectedRole.accessRoleId, - label: localizedInfo.name, - description: localizedInfo.description, - }; - })() - : null + + + {selectedRoleInfo?.name || localize('com_ui_select')} + + + } - setValue={onRoleChange} + items={dropdownItems} + className="w-[280px]" />
); diff --git a/client/src/components/SidePanel/Agents/__tests__/CategoryTabs.spec.tsx b/client/src/components/SidePanel/Agents/__tests__/CategoryTabs.spec.tsx index cbcf9c601..be30849c7 100644 --- a/client/src/components/SidePanel/Agents/__tests__/CategoryTabs.spec.tsx +++ b/client/src/components/SidePanel/Agents/__tests__/CategoryTabs.spec.tsx @@ -10,7 +10,7 @@ jest.mock('~/hooks/useLocalize', () => () => (key: string) => { const mockTranslations: Record = { com_agents_top_picks: 'Top Picks', com_agents_all: 'All', - com_agents_no_categories: 'No categories available', + com_ui_no_categories: 'No categories available', com_agents_category_tabs_label: 'Agent Categories', com_ui_agent_category_general: 'General', com_ui_agent_category_hr: 'HR', @@ -83,7 +83,7 @@ describe('CategoryTabs', () => { ); const generalTab = screen.getByText('General').closest('button'); - expect(generalTab).toHaveClass('text-gray-900'); + expect(generalTab).toHaveClass('bg-surface-tertiary'); // Should have active underline const underline = generalTab?.querySelector('.absolute.bottom-0'); @@ -149,7 +149,8 @@ describe('CategoryTabs', () => { ); const generalTab = screen.getByText('General').closest('button'); - expect(generalTab).toHaveClass('text-gray-600'); + expect(generalTab).toHaveClass('bg-surface-secondary'); + expect(generalTab).toHaveClass('text-text-secondary'); // Should not have active underline const underline = generalTab?.querySelector('.absolute.bottom-0'); diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index cfb5e1edf..c099f2396 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -935,6 +935,7 @@ "com_ui_no": "No", "com_ui_no_backup_codes": "No backup codes available. Please generate new ones", "com_ui_no_bookmarks": "it seems like you have no bookmarks yet. Click on a chat and add a new one", + "com_ui_no_categories": "No categories available", "com_ui_no_category": "No category", "com_ui_no_data": "something needs to go here. was empty", "com_ui_no_personalization_available": "No personalization options are currently available", @@ -1246,7 +1247,6 @@ "com_agents_none_in_category": "No agents found in this category", "com_agents_no_results": "No agents found. Try another search term.", "com_agents_results_for": "Results for '{{query}}'", - "com_nav_agents_marketplace": "Agent Marketplace", "com_agents_marketplace_subtitle": "Discover and use powerful AI agents to enhance your workflows and productivity", "com_ui_agent_name_is_required": "Agent name is required", "com_agents_missing_name": "Please type in a name before creating an agent." diff --git a/client/src/style.css b/client/src/style.css index a0aae5e5d..5f3b2df31 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -2801,6 +2801,17 @@ html { padding: 12px; } +/* Hide scrollbar for Chrome, Safari and Opera */ +.no-scrollbar::-webkit-scrollbar { + display: none; +} + +/* Hide scrollbar for IE, Edge and Firefox */ +.no-scrollbar { + -ms-overflow-style: none; /* IE and Edge */ + scrollbar-width: none; /* Firefox */ +} + .sharepoint-picker-bg{ background-color: #F5F5F5; }