🖼️ style: Improve Marketplace & Sharing Dialog UI

feat: Enhance CategoryTabs and Marketplace components for better responsiveness and navigation

feat: Refactor AgentCard and AgentGrid components for improved layout and accessibility

feat: Implement animated category transitions in AgentMarketplace and update NewChat component layout

feat: Refactor UI components for improved styling and accessibility in sharing dialogs

refactor: remove GenericManagePermissionsDialog and GrantAccessDialog components

- Deleted GenericManagePermissionsDialog and GrantAccessDialog components to streamline sharing functionality.
- Updated ManagePermissionsDialog to utilize AccessRolesPicker directly.
- Introduced UnifiedPeopleSearch for improved people selection experience.
- Enhanced PublicSharingToggle with InfoHoverCard for better user guidance.
- Adjusted AgentPanel to change error status to warning for duplicate agent versions.
- Updated translations to include new keys for search and access management.

feat: Add responsive design for SelectedPrincipalsList and improve layout in GenericGrantAccessDialog

feat: Enhance styling in SelectedPrincipalsList and SearchPicker components for improved UI consistency

feat: Improve PublicSharingToggle component with enhanced styling and accessibility features

feat: Introduce InfoHoverCard component and refactor enums for better organization

feat: Implement infinite scroll for agent grids and enhance performance

- Added `useInfiniteScroll` hook to manage infinite scrolling behavior in agent grids.
- Integrated infinite scroll functionality into `AgentGrid` and `VirtualizedAgentGrid` components.
- Updated `AgentMarketplace` to pass the scroll container to the agent grid components.
- Refactored loading indicators to show a spinner instead of a "Load More" button.
- Created `VirtualizedAgentGrid` component for optimized rendering of agent cards using virtualization.
- Added performance tests for `VirtualizedAgentGrid` to ensure efficient handling of large datasets.
- Updated translations to include new messages for end-of-results scenarios.

chore: Remove unused permission-related UI localization keys

ci: Update Agent model tests to handle duplicate support_contact updates

- Modified tests to ensure that updating an agent with the same support_contact does not create a new version and returns successfully.
- Enhanced verification for partial changes in support_contact, confirming no new version is created when content remains the same.

chore: Address ESLint, clean up unused imports and improve prop definitions in various components

ci: fix tests

ci: update tests

chore: remove unused search localization keys
This commit is contained in:
Marco Beretta 2025-08-04 18:50:54 +02:00 committed by Danny Avila
parent 9585db14ba
commit d82a63642d
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
51 changed files with 2074 additions and 1311 deletions

View file

@ -1,4 +1,4 @@
import React, { useState, useEffect, useMemo } from 'react';
import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react';
import { useRecoilState } from 'recoil';
import { useOutletContext } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
@ -43,11 +43,24 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
const { navVisible, setNavVisible } = useOutletContext<ContextType>();
const [hideSidePanel, setHideSidePanel] = useRecoilState(store.hideSidePanel);
// Get URL parameters (default to 'promoted' instead of 'all')
const activeTab = category || 'promoted';
// Get URL parameters (default to 'all' to ensure users see agents)
const activeTab = category || 'all';
const searchQuery = searchParams.get('q') || '';
const selectedAgentId = searchParams.get('agent_id') || '';
// Animation state
type Direction = 'left' | 'right';
const [displayCategory, setDisplayCategory] = useState<string>(activeTab);
const [nextCategory, setNextCategory] = useState<string | null>(null);
const [isTransitioning, setIsTransitioning] = useState<boolean>(false);
const [animationDirection, setAnimationDirection] = useState<Direction>('right');
// Keep a ref of initial mount to avoid animating first sync
const didInitRef = useRef(false);
// Ref for the scrollable container to enable infinite scroll
const scrollContainerRef = useRef<HTMLDivElement>(null);
// Local state
const [isDetailOpen, setIsDetailOpen] = useState(false);
const [selectedAgent, setSelectedAgent] = useState<t.Agent | null>(null);
@ -64,6 +77,15 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
localStorage.setItem('fullPanelCollapse', 'false');
}, [setHideSidePanel, hideSidePanel]);
// Redirect base /agents route to /agents/all for consistency
useEffect(() => {
if (!category && window.location.pathname === '/agents') {
const currentSearchParams = searchParams.toString();
const searchParamsStr = currentSearchParams ? `?${currentSearchParams}` : '';
navigate(`/agents/all${searchParamsStr}`, { replace: true });
}
}, [category, navigate, searchParams]);
// Ensure endpoints config is loaded first (required for agent queries)
useGetEndpointsQuery();
@ -101,22 +123,92 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
};
/**
* Handle category tab selection changes
*
* @param tabValue - The selected category value
* Determine ordered tabs to compute indices for direction
*/
const orderedTabs = useMemo<string[]>(() => {
const dynamic = (categoriesQuery.data || []).map((c) => c.value);
// Ensure unique and stable order - 'all' should be last to match server response
const set = new Set<string>(['promoted', ...dynamic, 'all']);
return Array.from(set);
}, [categoriesQuery.data]);
const getTabIndex = useCallback(
(tab: string): number => {
const idx = orderedTabs.indexOf(tab);
return idx >= 0 ? idx : 0;
},
[orderedTabs],
);
/**
* Handle category tab selection changes with directional animation
*/
const handleTabChange = (tabValue: string) => {
if (tabValue === activeTab || isTransitioning) {
// Ignore redundant or rapid clicks during transition
return;
}
const currentIndex = getTabIndex(displayCategory);
const newIndex = getTabIndex(tabValue);
const direction: Direction = newIndex > currentIndex ? 'right' : 'left';
setAnimationDirection(direction);
setNextCategory(tabValue);
setIsTransitioning(true);
// Update URL immediately, preserving current search params
const currentSearchParams = searchParams.toString();
const searchParamsStr = currentSearchParams ? `?${currentSearchParams}` : '';
// Navigate to the selected category
if (tabValue === 'promoted') {
navigate(`/agents${searchParamsStr}`);
} else {
navigate(`/agents/${tabValue}${searchParamsStr}`);
}
// Complete transition after 300ms
window.setTimeout(() => {
setDisplayCategory(tabValue);
setNextCategory(null);
setIsTransitioning(false);
}, 300);
};
/**
* Sync animation when URL changes externally (back/forward or deep links)
*/
useEffect(() => {
if (!didInitRef.current) {
// First render: do not animate; just set display to current active tab
didInitRef.current = true;
setDisplayCategory(activeTab);
return;
}
if (isTransitioning || activeTab === displayCategory) {
return;
}
// Compute direction vs current displayCategory and animate
const currentIndex = getTabIndex(displayCategory);
const newIndex = getTabIndex(activeTab);
const direction: Direction = newIndex > currentIndex ? 'right' : 'left';
setAnimationDirection(direction);
setNextCategory(activeTab);
setIsTransitioning(true);
const timeoutId = window.setTimeout(() => {
setDisplayCategory(activeTab);
setNextCategory(null);
setIsTransitioning(false);
}, 300);
return () => {
window.clearTimeout(timeoutId);
};
}, [activeTab, displayCategory, isTransitioning, getTabIndex]);
// No longer needed with keyframes
/**
* Handle search query changes
*
@ -207,7 +299,10 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
>
<main className="flex h-full flex-col overflow-hidden" role="main">
{/* Scrollable container */}
<div className="scrollbar-gutter-stable flex h-full flex-col overflow-y-auto overflow-x-hidden">
<div
ref={scrollContainerRef}
className="scrollbar-gutter-stable flex h-full flex-col overflow-y-auto overflow-x-hidden"
>
{/* 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">
@ -276,63 +371,158 @@ const AgentMarketplace: React.FC<AgentMarketplaceProps> = ({ className = '' }) =
{/* Scrollable content area */}
<div className="container mx-auto max-w-4xl px-4 pb-8">
{/* Category header - only show when not searching */}
{!searchQuery && (
<div className="mb-6 mt-6">
{(() => {
// Get category data for display
const getCategoryData = () => {
if (activeTab === 'promoted') {
return {
name: localize('com_agents_top_picks'),
description: localize('com_agents_recommended'),
{/* Two-pane animated container wrapping category header + grid */}
<div className="relative overflow-hidden">
{/* Current content pane */}
<div
className={cn(
isTransitioning &&
(animationDirection === 'right'
? 'motion-safe:animate-slide-out-left'
: 'motion-safe:animate-slide-out-right'),
)}
key={`pane-current-${displayCategory}`}
>
{/* Category header - only show when not searching */}
{!searchQuery && (
<div className="mb-6 mt-6">
{(() => {
// Get category data for display
const getCategoryData = () => {
if (displayCategory === 'promoted') {
return {
name: localize('com_agents_top_picks'),
description: localize('com_agents_recommended'),
};
}
if (displayCategory === '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 === displayCategory,
);
if (categoryData) {
return {
name: categoryData.label,
description: categoryData.description || '',
};
}
// Fallback for unknown categories
return {
name:
displayCategory.charAt(0).toUpperCase() +
displayCategory.slice(1),
description: '',
};
};
}
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 || '',
};
}
const { name, description } = getCategoryData();
// Fallback for unknown categories
return {
name: activeTab.charAt(0).toUpperCase() + activeTab.slice(1),
description: '',
};
};
return (
<div className="text-left">
<h2 className="text-2xl font-bold text-text-primary">{name}</h2>
{description && (
<p className="mt-2 text-text-secondary">{description}</p>
)}
</div>
);
})()}
</div>
)}
const { name, description } = getCategoryData();
return (
<div className="text-left">
<h2 className="text-2xl font-bold text-text-primary">{name}</h2>
{description && (
<p className="mt-2 text-text-secondary">{description}</p>
)}
</div>
);
})()}
{/* Agent grid */}
<AgentGrid
key={`grid-${displayCategory}`}
category={displayCategory}
searchQuery={searchQuery}
onSelectAgent={handleAgentSelect}
scrollElement={scrollContainerRef.current}
/>
</div>
)}
{/* Agent grid */}
<AgentGrid
category={activeTab}
searchQuery={searchQuery}
onSelectAgent={handleAgentSelect}
/>
{/* Next content pane, only during transition */}
{isTransitioning && nextCategory && (
<div
className={cn(
'absolute inset-0',
animationDirection === 'right'
? 'motion-safe:animate-slide-in-right'
: 'motion-safe:animate-slide-in-left',
)}
key={`pane-next-${nextCategory}-${animationDirection}`}
>
{/* Category header - only show when not searching */}
{!searchQuery && (
<div className="mb-6 mt-6">
{(() => {
// Get category data for display
const getCategoryData = () => {
if (nextCategory === 'promoted') {
return {
name: localize('com_agents_top_picks'),
description: localize('com_agents_recommended'),
};
}
if (nextCategory === '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 === nextCategory,
);
if (categoryData) {
return {
name: categoryData.label,
description: categoryData.description || '',
};
}
// Fallback for unknown categories
return {
name:
(nextCategory || '').charAt(0).toUpperCase() +
(nextCategory || '').slice(1),
description: '',
};
};
const { name, description } = getCategoryData();
return (
<div className="text-left">
<h2 className="text-2xl font-bold text-text-primary">{name}</h2>
{description && (
<p className="mt-2 text-text-secondary">{description}</p>
)}
</div>
);
})()}
</div>
)}
{/* Agent grid */}
<AgentGrid
key={`grid-${nextCategory}`}
category={nextCategory}
searchQuery={searchQuery}
onSelectAgent={handleAgentSelect}
scrollElement={scrollContainerRef.current}
/>
</div>
)}
{/* Note: Using Tailwind keyframes for slide in/out animations */}
</div>
</div>
{/* Agent detail dialog */}