feat(builder): add a feature agents ui (not backed yet)

This commit is contained in:
Nicholas Tindle
2024-08-01 23:45:30 -05:00
parent 18c88860ed
commit 2550c9b7b2
2 changed files with 162 additions and 101 deletions

View File

@@ -1,11 +1,12 @@
"use client";
import { useEffect, useMemo, useState, useCallback } from "react";
import React, { useEffect, useMemo, useState, useCallback } from "react";
import { useRouter } from 'next/navigation';
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import MarketplaceAPI, { AgentResponse, AgentListResponse, AgentWithRank } from "@/lib/marketplace-api";
import { ChevronLeft, ChevronRight, Search } from 'lucide-react';
import { ChevronLeft, ChevronRight, Search, Star } from 'lucide-react';
// Utility Functions
function debounce<T extends (...args: any[]) => any>(func: T, wait: number): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout | null = null;
return (...args: Parameters<T>) => {
@@ -14,11 +15,41 @@ function debounce<T extends (...args: any[]) => any>(func: T, wait: number): (..
};
}
interface AgentRowProps {
agent: AgentResponse | AgentWithRank;
}
// Types
type Agent = AgentResponse | AgentWithRank;
const AgentRow = ({ agent }: AgentRowProps) => {
// Components
const HeroSection: React.FC = () => (
<div className="relative bg-indigo-600 py-6">
<div className="absolute inset-0">
<img
className="w-full h-full object-cover opacity-20"
src="https://images.unsplash.com/photo-1562408590-e32931084e23?auto=format&fit=crop&w=2070&q=80"
alt="Marketplace background"
/>
<div className="absolute inset-0 bg-indigo-600 mix-blend-multiply" aria-hidden="true"></div>
</div>
<div className="relative max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8">
<h1 className="text-2xl font-extrabold tracking-tight text-white sm:text-3xl lg:text-4xl">AutoGPT Marketplace</h1>
<p className="mt-2 max-w-3xl text-sm sm:text-base text-indigo-100">Discover and share proven AI Agents to supercharge your business.</p>
</div>
</div>
);
const SearchInput: React.FC<{ value: string; onChange: (e: React.ChangeEvent<HTMLInputElement>) => void }> = ({ value, onChange }) => (
<div className="mb-8 relative">
<Input
placeholder="Search agents..."
type="text"
className="w-full pl-10 pr-4 py-2 rounded-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
value={value}
onChange={onChange}
/>
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
</div>
);
const AgentCard: React.FC<{ agent: Agent; featured?: boolean }> = ({ agent, featured = false }) => {
const router = useRouter();
const handleClick = () => {
@@ -26,77 +57,137 @@ const AgentRow = ({ agent }: AgentRowProps) => {
};
return (
<li
className="flex flex-col md:flex-row justify-between gap-4 py-6 px-4 cursor-pointer hover:bg-gray-50 transition-colors duration-200 rounded-lg"
<div
className={`flex flex-col justify-between p-6 cursor-pointer hover:bg-gray-50 transition-colors duration-200 rounded-lg border ${featured ? 'border-indigo-500 shadow-md' : 'border-gray-200'}`}
onClick={handleClick}
>
<div className="flex items-center gap-4">
<div className="w-16 h-16 bg-gray-200 rounded-full flex items-center justify-center text-2xl font-bold text-gray-500">
{agent.name.charAt(0)}
</div>
<div className="flex-1 min-w-0">
<div>
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-semibold text-gray-900 truncate">{agent.name}</h3>
<p className="mt-1 text-sm text-gray-500 line-clamp-2">{agent.description}</p>
{featured && <Star className="text-indigo-500" size={20} />}
</div>
<p className="text-sm text-gray-500 line-clamp-2 mb-4">{agent.description}</p>
<div className="text-xs text-gray-400 mb-2">
Categories: {agent.categories.join(', ')}
</div>
</div>
<div className="flex flex-col items-end justify-center">
<div className="text-sm text-gray-500">{agent.categories.join(', ')}</div>
<div className="mt-1 text-xs text-gray-400">
<div className="flex justify-between items-end">
<div className="text-xs text-gray-400">
Updated {new Date(agent.updatedAt).toLocaleDateString()}
</div>
{'rank' in agent && (
<div className="mt-1 text-xs text-indigo-600">
<div className="text-xs text-indigo-600">
Rank: {agent.rank.toFixed(2)}
</div>
)}
</div>
</li>
</div>
);
}
};
const Marketplace = () => {
const AgentGrid: React.FC<{ agents: Agent[]; title: string; featured?: boolean }> = ({ agents, title, featured = false }) => (
<div className="mb-12">
<h2 className="text-2xl font-bold text-gray-900 mb-4">{title}</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{agents.map((agent) => (
<AgentCard agent={agent} key={agent.id} featured={featured} />
))}
</div>
</div>
);
const Pagination: React.FC<{ page: number; totalPages: number; onPrevPage: () => void; onNextPage: () => void }> = ({ page, totalPages, onPrevPage, onNextPage }) => (
<div className="flex justify-between items-center mt-8">
<Button
onClick={onPrevPage}
disabled={page === 1}
className="flex items-center space-x-2 px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
>
<ChevronLeft size={16} />
<span>Previous</span>
</Button>
<span className="text-sm text-gray-700">
Page {page} of {totalPages}
</span>
<Button
onClick={onNextPage}
disabled={page === totalPages}
className="flex items-center space-x-2 px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
>
<span>Next</span>
<ChevronRight size={16} />
</Button>
</div>
);
// Main Component
const Marketplace: React.FC = () => {
const apiUrl = process.env.NEXT_PUBLIC_AGPT_MARKETPLACE_URL;
const api = useMemo(() => new MarketplaceAPI(apiUrl), [apiUrl]);
const [searchValue, setSearchValue] = useState("");
const [agents, setAgents] = useState<(AgentResponse | AgentWithRank)[]>([]);
const [searchResults, setSearchResults] = useState<Agent[]>([]);
const [featuredAgents, setFeaturedAgents] = useState<Agent[]>([]);
const [topAgents, setTopAgents] = useState<Agent[]>([]);
const [page, setPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const fetchAgents = useCallback(async (searchTerm: string, currentPage: number) => {
const fetchTopAgents = useCallback(async (currentPage: number) => {
setIsLoading(true);
try {
let response: AgentListResponse | AgentWithRank[];
if (searchTerm) {
response = await api.searchAgents(searchTerm, currentPage, 10);
const filteredAgents = (response as AgentWithRank[]).filter(agent => agent.rank > 0);
setAgents(filteredAgents);
setTotalPages(Math.ceil(filteredAgents.length / 10));
} else {
response = await api.getTopDownloadedAgents(currentPage, 10);
setAgents(response.agents);
setTotalPages(response.total_pages);
}
const response = await api.getTopDownloadedAgents(currentPage, 9);
setTopAgents(response.agents);
setTotalPages(response.total_pages);
} catch (error) {
console.error("Error fetching agents:", error);
console.error("Error fetching top agents:", error);
} finally {
setIsLoading(false);
}
}, [api]);
const debouncedFetchAgents = useMemo(
() => debounce(fetchAgents, 300),
[fetchAgents]
const fetchFeaturedAgents = useCallback(async () => {
try {
const featured = await api.getFeaturedAgents();
setFeaturedAgents(featured.agents);
} catch (error) {
console.error("Error fetching featured agents:", error);
}
}, [api]);
const searchAgents = useCallback(async (searchTerm: string) => {
setIsLoading(true);
try {
const response = await api.searchAgents(searchTerm, 1, 30);
const filteredAgents = response.filter(agent => agent.rank > 0);
setSearchResults(filteredAgents);
} catch (error) {
console.error("Error searching agents:", error);
} finally {
setIsLoading(false);
}
}, [api]);
const debouncedSearch = useMemo(
() => debounce(searchAgents, 300),
[searchAgents]
);
useEffect(() => {
debouncedFetchAgents(searchValue, page);
}, [searchValue, page, debouncedFetchAgents]);
if (searchValue) {
debouncedSearch(searchValue);
} else {
fetchTopAgents(page);
}
}, [searchValue, page, debouncedSearch, fetchTopAgents]);
useEffect(() => {
fetchFeaturedAgents();
}, [fetchFeaturedAgents]);
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchValue(e.target.value);
setPage(1); // Reset to first page on new search
setPage(1);
};
const handleNextPage = () => {
@@ -113,74 +204,35 @@ const Marketplace = () => {
return (
<div className="bg-gray-50 min-h-screen">
<div className="relative bg-indigo-600 py-24">
<div className="absolute inset-0">
<img
className="w-full h-full object-cover opacity-20"
src="https://images.unsplash.com/photo-1562408590-e32931084e23?auto=format&fit=crop&w=2070&q=80"
alt="Marketplace background"
/>
<div className="absolute inset-0 bg-indigo-600 mix-blend-multiply" aria-hidden="true"></div>
</div>
<div className="relative max-w-7xl mx-auto py-24 px-4 sm:px-6 lg:px-8">
<h1 className="text-4xl font-extrabold tracking-tight text-white sm:text-5xl lg:text-6xl">AutoGPT Marketplace</h1>
<p className="mt-6 max-w-3xl text-xl text-indigo-100">Discover and share proven AI Agents to supercharge your business. Explore our curated collection of powerful tools designed to enhance productivity and innovation.</p>
</div>
</div>
<HeroSection />
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="mb-8 relative">
<Input
placeholder="Search agents..."
type="text"
className="w-full pl-10 pr-4 py-2 rounded-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
value={searchValue}
onChange={handleInputChange}
/>
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
</div>
<SearchInput value={searchValue} onChange={handleInputChange} />
{isLoading ? (
<div className="text-center py-12">
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
<p className="mt-2 text-gray-600">Loading agents...</p>
</div>
) : agents.length > 0 ? (
<>
<h2 className="text-2xl font-bold text-gray-900 mb-4">
{searchValue ? "Search Results" : "Top Downloaded Agents"}
</h2>
<ul className="space-y-4">
{agents.map((agent) => (
<AgentRow agent={agent} key={agent.id} />
))}
</ul>
<div className="flex justify-between items-center mt-8">
<Button
onClick={handlePrevPage}
disabled={page === 1}
className="flex items-center space-x-2 px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
>
<ChevronLeft size={16} />
<span>Previous</span>
</Button>
<span className="text-sm text-gray-700">
Page {page} of {totalPages}
</span>
<Button
onClick={handleNextPage}
disabled={page === totalPages}
className="flex items-center space-x-2 px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50"
>
<span>Next</span>
<ChevronRight size={16} />
</Button>
) : searchValue ? (
searchResults.length > 0 ? (
<AgentGrid agents={searchResults} title="Search Results" />
) : (
<div className="text-center py-12">
<p className="text-gray-600">No agents found matching your search criteria.</p>
</div>
</>
)
) : (
<div className="text-center py-12">
<p className="text-gray-600">No agents found matching your search criteria.</p>
</div>
<>
{featuredAgents.length > 0 && (
<AgentGrid agents={featuredAgents} title="Featured Agents" featured={true} />
)}
<AgentGrid agents={topAgents} title="Top Downloaded Agents" />
<Pagination
page={page}
totalPages={totalPages}
onPrevPage={handlePrevPage}
onNextPage={handleNextPage}
/>
</>
)}
</div>
</div>

View File

@@ -32,6 +32,15 @@ export default class MarketplaceAPI {
);
}
async getFeaturedAgents(
page: number = 1,
pageSize: number = 10
): Promise<AgentListResponse> {
return this._get(
`/agents/top-downloads/agents?page=${page}&page_size=${pageSize}`
);
}
async searchAgents(
query: string,
page: number = 1,