Skip to content

Commit 95883eb

Browse files
committed
Simple store page
1 parent 5c8c14c commit 95883eb

File tree

4 files changed

+372
-4
lines changed

4 files changed

+372
-4
lines changed

npm-app/src/cli-handlers/agents.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,6 @@ import { green, yellow, cyan, magenta, bold, gray, red } from 'picocolors'
1212
import { loadLocalAgents, getLoadedAgentNames } from '../agents/load-agents'
1313
import { CLI } from '../cli'
1414
import { getProjectRoot } from '../project-files'
15-
import {
16-
startAgentCreationChat,
17-
createAgentFromRequirements,
18-
} from './agent-creation-chat'
1915
import { Spinner } from '../utils/spinner'
2016
import {
2117
ENTER_ALT_BUFFER,

web/src/app/agents/page.tsx

Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
'use client'
2+
3+
import { useState, useMemo } from 'react'
4+
import { useQuery } from '@tanstack/react-query'
5+
import { motion } from 'framer-motion'
6+
import {
7+
Search,
8+
TrendingUp,
9+
Clock,
10+
Star,
11+
Users,
12+
ChevronRight,
13+
} from 'lucide-react'
14+
import Link from 'next/link'
15+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
16+
import { Badge } from '@/components/ui/badge'
17+
import { Skeleton } from '@/components/ui/skeleton'
18+
import { Input } from '@/components/ui/input'
19+
import {
20+
Select,
21+
SelectContent,
22+
SelectItem,
23+
SelectTrigger,
24+
SelectValue,
25+
} from '@/components/ui/select'
26+
import { AnimatedElement } from '@/components/ui/landing/animated-element'
27+
28+
interface AgentData {
29+
id: string
30+
name: string
31+
description?: string
32+
publisher: {
33+
id: string
34+
name: string
35+
verified: boolean
36+
}
37+
version: string
38+
created_at: string
39+
usage_count?: number
40+
total_spent?: number
41+
avg_cost_per_invocation?: number
42+
avg_response_time?: number
43+
44+
tags?: string[]
45+
}
46+
47+
const AgentStorePage = () => {
48+
const [searchQuery, setSearchQuery] = useState('')
49+
const [sortBy, setSortBy] = useState('usage')
50+
51+
// Fetch agents from the API
52+
const { data: agents = [], isLoading } = useQuery<AgentData[]>({
53+
queryKey: ['agents'],
54+
queryFn: async () => {
55+
const response = await fetch('/api/agents')
56+
if (!response.ok) {
57+
throw new Error('Failed to fetch agents')
58+
}
59+
return await response.json()
60+
},
61+
})
62+
63+
const filteredAndSortedAgents = useMemo(() => {
64+
let filtered = agents.filter((agent) => {
65+
const matchesSearch =
66+
agent.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
67+
agent.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
68+
agent.tags?.some((tag) =>
69+
tag.toLowerCase().includes(searchQuery.toLowerCase())
70+
)
71+
return matchesSearch
72+
})
73+
74+
return filtered.sort((a, b) => {
75+
switch (sortBy) {
76+
case 'usage':
77+
return (b.usage_count || 0) - (a.usage_count || 0)
78+
case 'newest':
79+
return (
80+
new Date(b.created_at).getTime() - new Date(a.created_at).getTime()
81+
)
82+
case 'name':
83+
return a.name.localeCompare(b.name)
84+
case 'cost':
85+
return (b.total_spent || 0) - (a.total_spent || 0)
86+
default:
87+
return 0
88+
}
89+
})
90+
}, [agents, searchQuery, sortBy])
91+
92+
const formatCurrency = (amount?: number) => {
93+
if (!amount) return '$0.00'
94+
return `${amount.toFixed(2)}`
95+
}
96+
97+
const formatUsageCount = (count?: number) => {
98+
if (!count) return '0'
99+
if (count >= 1000000) return `${(count / 1000000).toFixed(1)}M`
100+
if (count >= 1000) return `${(count / 1000).toFixed(1)}K`
101+
return count.toString()
102+
}
103+
104+
return (
105+
<div className="container mx-auto py-8 px-4">
106+
<div className="max-w-7xl mx-auto">
107+
{' '}
108+
{/* Header */}
109+
<AnimatedElement type="fade" className="text-center mb-12">
110+
<h1 className="text-4xl font-bold mb-4 text-white">Agent Store</h1>
111+
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
112+
Browse all published AI agents. Run, compose, or fork them.
113+
</p>
114+
</AnimatedElement>
115+
{/* Search and Filters */}
116+
<AnimatedElement type="slide" delay={0.1} className="mb-8">
117+
<div className="flex flex-col md:flex-row gap-4 items-center justify-end">
118+
<div className="relative flex-1 max-w-[200px]">
119+
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground h-4 w-4" />
120+
<Input
121+
placeholder=""
122+
value={searchQuery}
123+
onChange={(e) => setSearchQuery(e.target.value)}
124+
className="pl-10"
125+
/>
126+
</div>
127+
<div className="flex gap-3">
128+
<Select value={sortBy} onValueChange={setSortBy}>
129+
<SelectTrigger className="w-40">
130+
<TrendingUp className="h-4 w-4 mr-2" />
131+
<SelectValue placeholder="Sort by" />
132+
</SelectTrigger>
133+
<SelectContent>
134+
<SelectItem value="usage">Most Used</SelectItem>
135+
<SelectItem value="newest">Newest</SelectItem>
136+
<SelectItem value="name">Name</SelectItem>
137+
<SelectItem value="cost">Total Spent</SelectItem>
138+
</SelectContent>
139+
</Select>
140+
</div>
141+
</div>
142+
</AnimatedElement>
143+
{/* Agent Grid */}
144+
{isLoading ? (
145+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
146+
{Array.from({ length: 6 }).map((_, i) => (
147+
<Card key={i} className="h-64">
148+
<CardHeader>
149+
<Skeleton className="h-6 w-3/4" />
150+
<Skeleton className="h-4 w-1/2" />
151+
</CardHeader>
152+
<CardContent>
153+
<Skeleton className="h-4 w-full mb-2" />
154+
<Skeleton className="h-4 w-2/3 mb-4" />
155+
<div className="flex gap-2">
156+
<Skeleton className="h-6 w-16" />
157+
<Skeleton className="h-6 w-20" />
158+
</div>
159+
</CardContent>
160+
</Card>
161+
))}
162+
</div>
163+
) : (
164+
<motion.div
165+
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"
166+
layout
167+
>
168+
{filteredAndSortedAgents.map((agent, index) => (
169+
<motion.div
170+
key={agent.id}
171+
layout
172+
initial={{ opacity: 0, y: 20 }}
173+
animate={{ opacity: 1, y: 0 }}
174+
transition={{ delay: index * 0.1, duration: 0.5 }}
175+
whileHover={{ y: -4, transition: { duration: 0.2 } }}
176+
>
177+
<Link
178+
href={`/publishers/${agent.publisher.id}/agents/${agent.id}/${agent.version || '1.0.0'}`}
179+
>
180+
<Card className="h-full hover:shadow-lg transition-all duration-300 cursor-pointer group border-2 hover:border-gray-300 dark:hover:border-gray-600">
181+
<CardHeader className="pb-3">
182+
<div className="flex items-start justify-between">
183+
<div className="flex-1">
184+
<CardTitle className="text-lg transition-colors">
185+
{agent.name}
186+
</CardTitle>
187+
<div className="flex items-center gap-2 mt-1">
188+
<span className="text-sm text-muted-foreground">
189+
by @{agent.publisher.id}
190+
</span>
191+
{agent.publisher.verified && (
192+
<Badge
193+
variant="secondary"
194+
className="text-xs px-1.5 py-0"
195+
>
196+
197+
</Badge>
198+
)}
199+
</div>
200+
</div>
201+
<ChevronRight className="h-4 w-4 text-muted-foreground transition-colors" />
202+
</div>
203+
</CardHeader>
204+
<CardContent className="pt-0">
205+
<p className="text-sm text-muted-foreground mb-4 line-clamp-2">
206+
{agent.description}
207+
</p>{' '}
208+
{/* Usage Stats */}
209+
<div className="grid grid-cols-2 gap-3 mb-4 text-xs">
210+
<div className="flex items-center gap-1">
211+
<Users className="h-3 w-3 text-blue-500" />
212+
<span className="font-medium">
213+
{formatUsageCount(agent.usage_count)}
214+
</span>
215+
<span className="text-muted-foreground">uses</span>
216+
</div>
217+
<div className="flex items-center gap-1">
218+
<Star className="h-3 w-3 text-green-500" />
219+
<span className="font-medium text-green-600">
220+
{formatCurrency(agent.total_spent)}
221+
</span>
222+
<span className="text-muted-foreground">spent</span>
223+
</div>
224+
<div className="flex items-center gap-1">
225+
<Clock className="h-3 w-3 text-orange-500" />
226+
<span className="font-medium">
227+
{formatCurrency(agent.avg_cost_per_invocation)}
228+
</span>
229+
<span className="text-muted-foreground">per use</span>
230+
</div>
231+
<div className="flex items-center gap-1">
232+
<Badge
233+
variant="outline"
234+
className="text-xs px-1.5 py-0"
235+
>
236+
v{agent.version}
237+
</Badge>
238+
</div>
239+
</div>
240+
{/* Tags */}
241+
{agent.tags && agent.tags.length > 0 && (
242+
<div className="flex flex-wrap gap-1">
243+
{agent.tags.slice(0, 3).map((tag) => (
244+
<Badge
245+
key={tag}
246+
variant="secondary"
247+
className="text-xs px-2 py-0"
248+
>
249+
{tag}
250+
</Badge>
251+
))}
252+
{agent.tags.length > 3 && (
253+
<Badge
254+
variant="secondary"
255+
className="text-xs px-2 py-0"
256+
>
257+
+{agent.tags.length - 3}
258+
</Badge>
259+
)}
260+
</div>
261+
)}
262+
</CardContent>
263+
</Card>
264+
</Link>
265+
</motion.div>
266+
))}
267+
</motion.div>
268+
)}
269+
{filteredAndSortedAgents.length === 0 && !isLoading && (
270+
<AnimatedElement type="fade" className="text-center py-12">
271+
<div className="text-muted-foreground">
272+
<Search className="h-12 w-12 mx-auto mb-4 opacity-50" />
273+
<h3 className="text-lg font-medium mb-2">No agents found</h3>
274+
<p>Try adjusting your search or filter criteria</p>
275+
</div>
276+
</AnimatedElement>
277+
)}
278+
</div>
279+
</div>
280+
)
281+
}
282+
283+
export default AgentStorePage

web/src/app/api/agents/route.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import db from '@codebuff/common/db'
2+
import * as schema from '@codebuff/common/db/schema'
3+
import { sql } from 'drizzle-orm'
4+
import { NextResponse } from 'next/server'
5+
6+
import { logger } from '@/util/logger'
7+
8+
export async function GET() {
9+
try {
10+
// Get all published agents with their publisher info
11+
const agents = await db
12+
.select({
13+
id: schema.agentConfig.id,
14+
version: schema.agentConfig.version,
15+
data: schema.agentConfig.data,
16+
created_at: schema.agentConfig.created_at,
17+
publisher: {
18+
id: schema.publisher.id,
19+
name: schema.publisher.name,
20+
verified: schema.publisher.verified,
21+
},
22+
})
23+
.from(schema.agentConfig)
24+
.innerJoin(
25+
schema.publisher,
26+
sql`${schema.agentConfig.publisher_id} = ${schema.publisher.id}`
27+
)
28+
.orderBy(sql`${schema.agentConfig.created_at} DESC`) // Sort by date descending
29+
.limit(100) // Limit for performance
30+
31+
// Transform the data to include parsed agent data and mock usage metrics
32+
const transformedAgents = agents.map((agent) => {
33+
const agentData = typeof agent.data === 'string' ? JSON.parse(agent.data) : agent.data
34+
35+
// Mock usage metrics (in a real app, these would come from analytics/usage tables)
36+
const mockUsageCount = Math.floor(Math.random() * 50000) + 1000
37+
const mockTotalSpent = Math.floor(Math.random() * 5000) + 100 // $100-$5100
38+
const mockAvgCostPerInvocation = mockTotalSpent / mockUsageCount
39+
const mockResponseTime = Math.floor(Math.random() * 3000) + 500 // 500-3500ms
40+
41+
return {
42+
id: agent.id,
43+
name: agentData.name || agent.id,
44+
description: agentData.description,
45+
publisher: agent.publisher,
46+
version: agent.version,
47+
created_at: agent.created_at,
48+
usage_count: mockUsageCount,
49+
total_spent: mockTotalSpent,
50+
avg_cost_per_invocation: mockAvgCostPerInvocation,
51+
avg_response_time: mockResponseTime,
52+
53+
tags: agentData.tags || [],
54+
}
55+
})
56+
57+
// Group by agent name and keep only the latest version of each
58+
const latestAgents = new Map()
59+
transformedAgents.forEach((agent) => {
60+
const key = `${agent.publisher.id}/${agent.name}`
61+
if (!latestAgents.has(key)) { // Since it's sorted, the first one is the latest
62+
latestAgents.set(key, agent)
63+
}
64+
})
65+
66+
const result = Array.from(latestAgents.values())
67+
68+
return NextResponse.json(result)
69+
} catch (error) {
70+
logger.error({ error }, 'Error fetching agents')
71+
return NextResponse.json(
72+
{ error: 'Internal server error' },
73+
{ status: 500 }
74+
)
75+
}
76+
}

0 commit comments

Comments
 (0)