import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { FiSearch, FiFilter, FiGrid, FiList, FiRefreshCw, FiChevronLeft, FiChevronRight, FiStar, FiMessageCircle, FiUser, FiCpu, FiLink, FiXCircle, FiImage, FiMinus, FiPlus, FiChevronDown, FiCopy, FiCamera, FiMic, FiFilm, FiCode, FiGlobe, FiBarChart2, FiDatabase, FiShare2, FiTrendingUp, FiActivity, FiShield, FiCheckCircle, FiAlertTriangle, FiBriefcase, FiLock, FiUsers, FiZap, FiDollarSign } from 'react-icons/fi'; import { FaTwitter, FaGithub, FaGlobe as FaGlobeIcon, FaDiscord } from 'react-icons/fa'; import { Input } from '../components/ui/input'; import { Button } from '../components/ui/Button'; import ConnectionConfirmDialog from '../components/shared/ConnectionConfirmDialog'; import { Badge } from '../components/ui/badge'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '../components/ui/dropdown-menu'; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '../components/ui/dialog'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, } from '../components/ui/Card'; import Typography from '../components/ui/Typography'; import { AgentProfileModal } from '../components/AgentProfileModal'; import { AIAgentCapability, NetworkType } from '@hashgraphonline/standards-sdk'; import { toast } from 'sonner'; import { gradients } from '../lib/styles'; import { useHRLImageUrl } from '../hooks/useHRLImageUrl'; interface AgentProfile { id?: string; accountId: string; profile?: any; metadata?: any; rating?: number; ratingCount?: number; network?: string; createdAt?: string | Date; } type SortOption = | 'name-asc' | 'name-desc' | 'rating-desc' | 'rating-asc' | 'creator' | 'created-desc'; type FilterState = { search: string; tags: number[]; hasProfileImage: boolean | null; includeRegistryBroker?: boolean; registries?: string[]; }; interface AgentDiscoveryPagination { currentPage: number; totalPages: number; pageSize: number; totalCount: number; } const capabilityIconMap: { [key: number]: { icon: React.ComponentType<{ className?: string }>; description: string; }; } = { 0: { icon: FiCpu, description: 'Text Generation' }, 1: { icon: FiCamera, description: 'Image Generation' }, 2: { icon: FiMic, description: 'Audio Generation' }, 3: { icon: FiFilm, description: 'Video Generation' }, 4: { icon: FiCode, description: 'Code Generation' }, 5: { icon: FiGlobe, description: 'Language Translation' }, 6: { icon: FiBarChart2, description: 'Summarization & Content Extraction' }, 7: { icon: FiDatabase, description: 'Knowledge Retrieval & Reasoning' }, 8: { icon: FiShare2, description: 'Data Integration & Visualization' }, 9: { icon: FiTrendingUp, description: 'Market Intelligence' }, 10: { icon: FiActivity, description: 'Transaction Analytics' }, 11: { icon: FiShield, description: 'Smart Contract Audit' }, 12: { icon: FiCheckCircle, description: 'Governance Facilitation' }, 13: { icon: FiAlertTriangle, description: 'Security Monitoring' }, 14: { icon: FiBriefcase, description: 'Compliance & Regulatory Analysis' }, 15: { icon: FiLock, description: 'Fraud Detection & Prevention' }, 16: { icon: FiUsers, description: 'Multi-Agent Coordination' }, 17: { icon: FiLink, description: 'API Integration & Orchestration' }, 18: { icon: FiZap, description: 'Workflow Automation' }, }; const allCapabilityTags: { capabilityId: number; label: string }[] = Object.entries(capabilityIconMap).map(([idStr, data]) => ({ capabilityId: parseInt(idStr, 10), label: data.description, })); const sortOptions: { value: SortOption; label: string }[] = [ { value: 'created-desc', label: 'Newest to Oldest' }, { value: 'name-asc', label: 'Name (A-Z)' }, { value: 'name-desc', label: 'Name (Z-A)' }, { value: 'rating-desc', label: 'Highest Rating' }, { value: 'rating-asc', label: 'Lowest Rating' }, { value: 'creator', label: 'Creator' }, ]; interface AgentCardProps { agent: AgentProfile; onViewProfile: (accountId: string) => void; onConnect: (accountId: string) => void; } /** * Individual agent card component */ const AgentCard: React.FC = ({ agent, onViewProfile, onConnect }) => { const [copySuccess, setCopySuccess] = useState(false); const { metadata, accountId } = agent; const { name, description, logo, profileImage, socials, bio, display_name, alias, aiAgent, inboundTopicId, } = metadata || {}; const displayName = display_name || alias || name || 'Unknown Agent'; const rawImageUrl = profileImage || logo; const capabilities = aiAgent?.capabilities || []; const rating = agent.rating || 0; const ratingCount = agent.ratingCount || 0; const { resolvedUrl: imageUrl, isLoading: imageLoading, error: imageError } = useHRLImageUrl( rawImageUrl, agent.network === 'testnet' ? 'testnet' : 'mainnet' ); const handleCopyAccountId = useCallback( (e: React.MouseEvent) => { e.stopPropagation(); navigator.clipboard.writeText(accountId); setCopySuccess(true); setTimeout(() => { setCopySuccess(false); }, 2000); }, [accountId] ); const handleSocialClick = useCallback((e: React.MouseEvent) => { e.stopPropagation(); }, []); return (
onViewProfile(accountId)} > {imageLoading ? (
) : imageUrl && !imageError ? ( {`${displayName} { }} /> ) : ( displayName.charAt(0) || 'A' )}
onViewProfile(accountId)} > {displayName} {rating > 0 && (
{rating.toFixed(1)} ({ratingCount})
)} {accountId}
{description || bio || 'No description available'}
{(() => { const capabilitiesToDisplay = capabilities || []; const visibleCapabilities = capabilitiesToDisplay.slice(0, 4); const tagElements = []; for (let i = 0; i < visibleCapabilities.length; i++) { const capabilityId = visibleCapabilities[i]; const capabilityInfo = capabilityIconMap[capabilityId]; const IconComponent = capabilityInfo?.icon; const capDescription = capabilityInfo?.description || `Capability ${capabilityId}`; tagElements.push(
{IconComponent ? ( ) : ( )}
{capDescription}
); } return tagElements; })()} {capabilities.length > 4 && ( +{capabilities.length - 4} )}
{socials && Array.isArray(socials) ? ( (() => { const socialElements = []; for (let i = 0; i < socials.length; i++) { const social = socials[i]; let icon = ; let url = social.url || social.handle; if (!url) continue; if ( social.platform === 'twitter' || social.platform === 'x' ) { icon = ; if (!url.includes('http')) { url = `https://twitter.com/${url.replace('@', '')}`; } } else if (social.platform === 'github') { icon = ; if (!url.includes('http')) { url = `https://github.com/${url}`; } } else if (social.platform === 'discord') { icon = ; } else if (social.platform === 'website') { icon = ; } socialElements.push( {icon} ); } return socialElements; })() ) : ( <> {socials?.twitter && ( )} {socials?.github && ( )} {socials?.website && ( )} )}
); }; const AgentDiscoveryPage: React.FC = () => { const [agents, setAgents] = useState([]); const [isLoading, setIsLoading] = useState(false); const [currentSort, setCurrentSort] = useState('rating-desc'); const [showAllTags, setShowAllTags] = useState(false); const [selectedAgent, setSelectedAgent] = useState(null); const [isProfileModalOpen, setIsProfileModalOpen] = useState(false); const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [selectedAgentToConnect, setSelectedAgentToConnect] = useState(null); const [isConnecting, setIsConnecting] = useState(false); const [pagination, setPagination] = useState({ currentPage: 1, totalPages: 1, pageSize: 50, totalCount: 0, }); const [filters, setFilters] = useState({ search: '', tags: [], hasProfileImage: null, includeRegistryBroker: false, registries: ['nanda', 'virtuals-protocol', 'olas-protocol', 'openrouter'], }); const [activeFilters, setActiveFilters] = useState({ search: '', tags: [], hasProfileImage: null, includeRegistryBroker: false, registries: ['nanda', 'virtuals-protocol', 'olas-protocol', 'openrouter'], }); /** * Discovers agents from the backend */ const discoverAgents = useCallback(async () => { try { setIsLoading(true); const offset = (pagination.currentPage - 1) * pagination.pageSize; const result = await window.electron.invoke('hcs10:discover-agents', { filters: { ...activeFilters, capabilities: activeFilters.tags.length > 0 ? activeFilters.tags : undefined, }, pagination: { page: pagination.currentPage, limit: pagination.pageSize, offset, }, }); if (result.success) { const fetchedAgents: AgentProfile[] = result.data.agents || []; if (fetchedAgents.length > 0) { const topicIds = fetchedAgents .map((agent) => agent.metadata?.inboundTopicId) .filter(Boolean) .join(','); if (topicIds.length > 0) { try { } catch (error) { } } } setAgents(fetchedAgents); setPagination({ currentPage: result.data.pagination?.currentPage || pagination.currentPage, totalPages: result.data.pagination?.totalPages || 1, pageSize: result.data.pagination?.limit || pagination.pageSize, totalCount: result.data.pagination?.total || 0, }); } else { toast.error('Discovery Failed', { description: result.error || 'Failed to discover agents', }); } } catch (error) { toast.error('Error', { description: 'Failed to connect to discovery service', }); } finally { setIsLoading(false); } }, [activeFilters, pagination.currentPage, pagination.pageSize]); /** * Handles search and filtering */ const handleSearch = useCallback((e: React.ChangeEvent) => { const value = e.target.value; setFilters((prev) => ({ ...prev, search: value })); setActiveFilters((prev) => ({ ...prev, search: value })); }, []); const handleSort = useCallback((option: SortOption) => { setCurrentSort(option); }, []); const handleClearFilters = useCallback(() => { setFilters({ search: '', tags: [], hasProfileImage: null, includeRegistryBroker: false, registries: ['nanda', 'virtuals-protocol', 'olas-protocol', 'openrouter'], }); setActiveFilters({ search: '', tags: [], hasProfileImage: null, includeRegistryBroker: false, registries: ['nanda', 'virtuals-protocol', 'olas-protocol', 'openrouter'], }); }, []); const toggleTag = useCallback((capabilityId: number) => { setFilters((prev) => { const newTags = prev.tags.includes(capabilityId) ? prev.tags.filter((id) => id !== capabilityId) : [...prev.tags, capabilityId]; setActiveFilters((current) => ({ ...current, tags: newTags })); return { ...prev, tags: newTags }; }); }, []); const toggleShowAllTags = useCallback(() => { setShowAllTags((prev) => !prev); }, []); const toggleHasProfileImage = useCallback(() => { setFilters((prev) => { const newValue = prev.hasProfileImage === true ? null : true; setActiveFilters((current) => ({ ...current, hasProfileImage: newValue, })); return { ...prev, hasProfileImage: newValue }; }); }, []); const handleSortOptionClick = useMemo(() => { const handlers: Record void> = {}; sortOptions.forEach((option) => { handlers[option.value] = () => handleSort(option.value); }); return handlers; }, [handleSort]); const tagClickHandlers = useMemo(() => { const handlers: Record void> = {}; allCapabilityTags.forEach((tag) => { handlers[tag.capabilityId] = () => toggleTag(tag.capabilityId); }); return handlers; }, [toggleTag]); /** * Handles connecting to an agent - shows confirmation dialog first */ const handleConnect = useCallback(async (accountId: string) => { const agent = agents.find(a => a.accountId === accountId); if (agent) { setSelectedAgentToConnect(agent); setShowConfirmDialog(true); } else { toast.error('Error', { description: 'Agent not found', }); } }, [agents]); /** * Confirms and sends connection request after user confirmation */ const handleConfirmConnect = useCallback(async () => { if (!selectedAgentToConnect) return; try { setIsConnecting(true); setShowConfirmDialog(false); const result = await window.electron.invoke('hcs10:send-connection-request', { targetAccountId: selectedAgentToConnect.accountId, }); if (result.success) { toast.success('Connection Request Sent', { description: 'Your connection request has been sent to the agent', }); } else { toast.error('Connection Failed', { description: result.error || 'Failed to send connection request', }); } } catch (error) { toast.error('Error', { description: 'Failed to send connection request', }); } finally { setIsConnecting(false); setSelectedAgentToConnect(null); } }, [selectedAgentToConnect]); /** * Cancels connection request */ const handleCancelConnect = useCallback(() => { setShowConfirmDialog(false); setSelectedAgentToConnect(null); }, []); /** * Handles viewing agent profile */ const handleViewProfile = useCallback((accountId: string) => { const agent = agents.find(a => a.accountId === accountId); if (agent) { setSelectedAgent(agent); setIsProfileModalOpen(true); } else { toast.error('Error', { description: 'Agent not found', }); } }, [agents]); /** * Handles closing the profile modal */ const handleCloseProfileModal = useCallback(() => { setIsProfileModalOpen(false); setSelectedAgent(null); }, []); const filteredAgents = useMemo(() => { return agents.filter((agent) => { if (!agent.metadata) return false; if (activeFilters.search && activeFilters.search.trim() !== '') { const searchLower = activeFilters.search.toLowerCase(); const displayNameMatch = agent?.metadata?.display_name ?.toLowerCase() .includes(searchLower); const aliasMatch = agent?.metadata?.alias ?.toLowerCase() .includes(searchLower); const descMatch = agent?.metadata?.bio ?.toLowerCase() .includes(searchLower); const creatorMatch = agent?.metadata?.aiAgent?.creator ?.toLowerCase() .includes(searchLower); if ( !( displayNameMatch || aliasMatch || descMatch || creatorMatch ) ) { return false; } } if (activeFilters.tags.length > 0) { const agentCapabilities = agent?.metadata?.aiAgent?.capabilities || []; if ( !activeFilters.tags.every((tagId) => agentCapabilities.includes(tagId) ) ) { return false; } } if (activeFilters.hasProfileImage === true) { const hasImage = Boolean(agent.metadata?.profileImage); if (!hasImage) { return false; } } return true; }); }, [agents, activeFilters]); const sortedAgents = useMemo(() => { return [...filteredAgents].sort((a, b) => { const displayNameA = a.metadata?.display_name?.toLowerCase() || ''; const displayNameB = b.metadata?.display_name?.toLowerCase() || ''; const aliasA = a.metadata?.alias?.toLowerCase() || ''; const aliasB = b.metadata?.alias?.toLowerCase() || ''; const primaryNameA = displayNameA || aliasA; const primaryNameB = displayNameB || aliasB; const creatorA = a.metadata?.aiAgent?.creator?.toLowerCase() || ''; const creatorB = b.metadata?.aiAgent?.creator?.toLowerCase() || ''; const ratingA = a.metadata?.rating || 0; const ratingB = b.metadata?.rating || 0; const ratingCountA = a.metadata?.ratingCount || 0; const ratingCountB = b.metadata?.ratingCount || 0; let createdA = 0; if (a.createdAt instanceof Date) { createdA = a.createdAt.getTime(); } else if (typeof a.createdAt === 'string') { createdA = new Date(a.createdAt).getTime(); } let createdB = 0; if (b.createdAt instanceof Date) { createdB = b.createdAt.getTime(); } else if (typeof b.createdAt === 'string') { createdB = new Date(b.createdAt).getTime(); } switch (currentSort) { case 'name-asc': return primaryNameA.localeCompare(primaryNameB); case 'name-desc': return primaryNameB.localeCompare(primaryNameA); case 'rating-desc': if (ratingB !== ratingA) { return ratingB - ratingA; } if (ratingCountB !== ratingCountA) { return ratingCountB - ratingCountA; } return primaryNameA.localeCompare(primaryNameB); case 'rating-asc': if (ratingA !== ratingB) { return ratingA - ratingB; } if (ratingCountA !== ratingCountB) { return ratingCountA - ratingCountB; } return primaryNameA.localeCompare(primaryNameB); case 'creator': return creatorA.localeCompare(creatorB); case 'created-desc': return createdB - createdA; default: return 0; } }); }, [filteredAgents, currentSort]); const activeFilterCount = useMemo(() => { let count = 0; if (activeFilters.search) count++; count += activeFilters.tags.length; if (activeFilters.hasProfileImage !== null) count++; if (activeFilters.includeRegistryBroker) count++; return count; }, [activeFilters]); const handlePageChange = useCallback((page: number) => { setPagination((prev) => ({ ...prev, currentPage: page, })); }, []); const handleFirstPage = useCallback(() => { handlePageChange(1); }, [handlePageChange]); const handlePreviousPage = useCallback(() => { handlePageChange(pagination.currentPage - 1); }, [handlePageChange, pagination.currentPage]); const handleNextPage = useCallback(() => { handlePageChange(pagination.currentPage + 1); }, [handlePageChange, pagination.currentPage]); const handleLastPage = useCallback(() => { handlePageChange(pagination.totalPages); }, [handlePageChange, pagination.totalPages]); const pageButtonHandlers = useMemo(() => { const handlers: Record void> = {}; for (let i = 1; i <= pagination.totalPages; i++) { handlers[i] = () => handlePageChange(i); } return handlers; }, [handlePageChange, pagination.totalPages]); useEffect(() => { discoverAgents(); }, [pagination.currentPage, pagination.pageSize, activeFilters]); return (
AI Agents Directory Discover and connect with AI agents on the Hedera network
Filters:
With profile image {(showAllTags ? allCapabilityTags : allCapabilityTags.slice(0, 9) ).map((tag) => { const isSelected = filters.tags.includes(tag.capabilityId); const IconComponent = capabilityIconMap[tag.capabilityId]?.icon; return ( {IconComponent ? : null} {tag.label} ); })} {showAllTags ? ( <> Show less ) : ( <> Show more )}
{sortOptions.map((option) => ( {option.label} ))}
{isLoading && (
{[...Array(6)].map((_, i) => (
))}
)} {!isLoading && sortedAgents.length > 0 && ( <>
{sortedAgents.map((agent) => (
))}
{/* Pagination Controls */} {pagination.totalPages > 1 && (
{Array.from({ length: Math.min(5, pagination.totalPages), }).map((_, i) => { let pageNum; if (pagination.totalPages <= 5) { pageNum = i + 1; } else if (pagination.currentPage <= 3) { pageNum = i + 1; } else if ( pagination.currentPage >= pagination.totalPages - 2 ) { pageNum = pagination.totalPages - 4 + i; } else { pageNum = pagination.currentPage - 2 + i; } return ( ); })}
Showing {sortedAgents.length} of {pagination.totalCount}{' '} agents (Page {pagination.currentPage} of{' '} {pagination.totalPages})
)} )} {!isLoading && sortedAgents.length === 0 && (
No agents found {activeFilterCount > 0 ? 'Try adjusting your filters' : 'No agents registered yet'} {activeFilterCount > 0 && ( )}
)}
{/* Agent Profile Modal */}
); }; export default AgentDiscoveryPage;