diff --git a/web/src/app/api/agents/route.ts b/web/src/app/api/agents/route.ts index 46f85a0cf3..b9b0ead9c2 100644 --- a/web/src/app/api/agents/route.ts +++ b/web/src/app/api/agents/route.ts @@ -6,10 +6,9 @@ import { unstable_cache } from 'next/cache' import { logger } from '@/util/logger' -// Enable static generation for API route +// ISR Configuration for API route export const revalidate = 600 // Cache for 10 minutes -export const dynamic = 'force-static' // Force static generation -export const fetchCache = 'force-cache' // Use cached data when possible +export const dynamic = 'force-static' // Cached function for expensive agent aggregations const getCachedAgents = unstable_cache( @@ -253,8 +252,8 @@ const getCachedAgents = unstable_cache( }, ['agents-data'], { - revalidate, - tags: ['agents'], + revalidate: 600, // 10 minutes + tags: ['agents', 'api'], } ) diff --git a/web/src/app/store/agents-data.ts b/web/src/app/store/agents-data.ts new file mode 100644 index 0000000000..f967463608 --- /dev/null +++ b/web/src/app/store/agents-data.ts @@ -0,0 +1,71 @@ +import { unstable_cache } from 'next/cache' + +// Types +interface AgentData { + id: string + name: string + description?: string + publisher: { + id: string + name: string + verified: boolean + avatar_url?: string | null + } + version: string + created_at: string + usage_count?: number + weekly_spent?: number + total_spent?: number + avg_cost_per_invocation?: number + unique_users?: number + last_used?: string + version_stats?: Record + tags?: string[] +} + +// Server-side data fetching function with ISR +export const getAgentsData = unstable_cache( + async (): Promise => { + const baseUrl = + process.env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'http://localhost:3000' + + try { + const response = await fetch(`${baseUrl}/api/agents`, { + headers: { + 'User-Agent': 'Codebuff-Store-Static', + }, + // Configure fetch-level caching + next: { + revalidate: 600, // 10 minutes + tags: ['agents', 'store'], + }, + }) + + if (!response.ok) { + console.error( + 'Failed to fetch agents:', + response.status, + response.statusText + ) + return [] + } + + return await response.json() + } catch (error) { + console.error('Error fetching agents data:', error) + return [] + } + }, + ['store-agents-data'], + { + revalidate: 600, // Cache for 10 minutes + tags: ['agents', 'store'], + } +) + +// Helper function for on-demand revalidation (can be used in webhooks/admin actions) +export async function revalidateAgentsData() { + const { revalidateTag } = await import('next/cache') + revalidateTag('agents') + revalidateTag('store') +} diff --git a/web/src/app/store/page.tsx b/web/src/app/store/page.tsx index 943d435ca4..84761c6fa9 100644 --- a/web/src/app/store/page.tsx +++ b/web/src/app/store/page.tsx @@ -1,6 +1,7 @@ import { Metadata } from 'next' -import { unstable_cache } from 'next/cache' +import { Suspense } from 'react' import AgentStoreClient from './store-client' +import { getAgentsData } from './agents-data' // Types interface AgentData { @@ -42,35 +43,6 @@ interface PublisherProfileResponse { avatar_url?: string | null } -// Cache the agents data with 60 second revalidation -const getCachedAgentsData = unstable_cache( - async (): Promise => { - const baseUrl = - process.env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'http://localhost:3000' - const response = await fetch(`${baseUrl}/api/agents`, { - headers: { - 'User-Agent': 'Codebuff-Store-Static', - }, - }) - - if (!response.ok) { - console.error( - 'Failed to fetch agents:', - response.status, - response.statusText - ) - return [] - } - - return await response.json() - }, - ['store-agents-data'], - { - revalidate: 60, // Revalidate every 60 seconds - tags: ['agents', 'store'], - } -) - export const metadata: Metadata = { title: 'Agent Store | Codebuff', description: 'Browse all published AI agents. Run, compose, or fork them.', @@ -81,18 +53,22 @@ export const metadata: Metadata = { }, } -// Enable static site generation with ISR -export const revalidate = 60 * 10 // Revalidate every 10 minutes +// ISR Configuration - revalidate every 10 minutes +export const revalidate = 600 export const dynamic = 'force-static' -export const fetchCache = 'force-cache' interface StorePageProps { searchParams: { [key: string]: string | string[] | undefined } } -export default async function StorePage({ searchParams }: StorePageProps) { - // Fetch agents data at build time - const agentsData = await getCachedAgentsData() +// Server Component for fetching and rendering agents data +async function AgentsDataProvider({ + searchParams, +}: { + searchParams: StorePageProps['searchParams'] +}) { + // Fetch agents data with ISR + const agentsData = await getAgentsData() // For static generation, we don't pass session data // The client will handle authentication state @@ -107,3 +83,36 @@ export default async function StorePage({ searchParams }: StorePageProps) { /> ) } + +// Loading component for better UX +function AgentsLoading() { + return ( +
+
+
+

Agent Store

+

+ Browse all published AI agents. Run, compose, or fork them. +

+
+ +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+ ))} +
+
+
+ ) +} + +export default async function StorePage({ searchParams }: StorePageProps) { + return ( + }> + + + ) +} diff --git a/web/src/components/navbar/client-auth-nav.tsx b/web/src/components/navbar/client-auth-nav.tsx deleted file mode 100644 index a7e5de1710..0000000000 --- a/web/src/components/navbar/client-auth-nav.tsx +++ /dev/null @@ -1,96 +0,0 @@ -'use client' - -import { useSession } from 'next-auth/react' -import Link from 'next/link' -import { LogIn, BarChart2 } from 'lucide-react' -import { UserDropdown } from './user-dropdown' -import { Button } from '../ui/button' -import { cn } from '@/lib/utils' -import { DropdownMenuItem } from '../ui/dropdown-menu' - -interface ClientAuthNavProps { - className?: string - isMobile?: boolean -} - -export function ClientAuthNav({ - className, - isMobile = false, -}: ClientAuthNavProps) { - const { data: session, status } = useSession() - - // Server container handles spacing, no need for placeholder here - if (status === 'loading') { - return null - } - - if (isMobile) { - // Mobile dropdown item - if (!session) { - return ( - - - - Log in - - - ) - } - return null // Usage link is handled separately in mobile menu - } - - // Desktop version - if (session) { - return - } - - return ( - -
- - - ) -} - -export function ClientUsageLink() { - const { data: session, status } = useSession() - - // Server container handles spacing, just return null when not logged in - if (!session) { - return null - } - - return ( - - Usage - - ) -} - -export function ClientMobileUsageLink() { - const { data: session } = useSession() - - if (!session) return null - - return ( - - - - Usage - - - ) -} diff --git a/web/src/components/navbar/navbar.tsx b/web/src/components/navbar/navbar.tsx index 54310fc926..0bd273d6ec 100644 --- a/web/src/components/navbar/navbar.tsx +++ b/web/src/components/navbar/navbar.tsx @@ -1,8 +1,17 @@ -import { Menu, DollarSign, BookHeart, Bot } from 'lucide-react' +import { + Menu, + DollarSign, + LogIn, + BarChart2, + BookHeart, + User, + Bot, +} from 'lucide-react' import Image from 'next/image' import Link from 'next/link' -import dynamic from 'next/dynamic' +import { getServerSession } from 'next-auth' +import { UserDropdown } from './user-dropdown' import { Button } from '../ui/button' import { DropdownMenu, @@ -12,47 +21,12 @@ import { } from '../ui/dropdown-menu' import { Icons } from '../icons' -// TODO: This dynamic pattern might not be the best way to handle the navbar. Reconsider from first principles. - -// Dynamically import client auth components to prevent SSR and enable SSG -const ClientAuthNav = dynamic( - () => - import('./client-auth-nav').then((mod) => ({ default: mod.ClientAuthNav })), - { - ssr: false, - loading: () =>
, // Placeholder to prevent layout shift - } -) - -const ClientUsageLink = dynamic( - () => - import('./client-auth-nav').then((mod) => ({ - default: mod.ClientUsageLink, - })), - { - ssr: false, - } -) - -const ClientMobileUsageLink = dynamic( - () => - import('./client-auth-nav').then((mod) => ({ - default: mod.ClientMobileUsageLink, - })), - { - ssr: false, - } -) +import { authOptions } from '@/app/api/auth/[...nextauth]/auth-options' +import { cn } from '@/lib/utils' -const ClientMobileAuthNav = dynamic( - () => - import('./client-auth-nav').then((mod) => ({ default: mod.ClientAuthNav })), - { - ssr: false, - } -) +export const Navbar = async () => { + const session = await getServerSession(authOptions) -export const Navbar = () => { return (
{ Agent Store -
- -
+ + {session && ( + + Usage + + )}
@@ -139,13 +119,43 @@ export const Navbar = () => { Agent Store - - + + {session && ( + + + + Usage + + + )} + {!session && ( + + + + Log in + + + )} -
- -
+ {session ? ( + + ) : ( + +
+ + + )} {/* */}
diff --git a/web/src/lib/revalidation.ts b/web/src/lib/revalidation.ts new file mode 100644 index 0000000000..6401d1690e --- /dev/null +++ b/web/src/lib/revalidation.ts @@ -0,0 +1,45 @@ +'use server' + +import { revalidatePath, revalidateTag } from 'next/cache' + +/** + * Revalidate all agent-related data across the application + * Use this when agent data is updated via admin actions or webhooks + */ +export async function revalidateAgents() { + // Revalidate specific pages + revalidatePath('/store') + revalidatePath('/api/agents') + + // Revalidate by tags (affects all cached data with these tags) + revalidateTag('agents') + revalidateTag('store') + revalidateTag('api') +} + +/** + * Revalidate a specific agent's data + * Use this when a single agent is updated + */ +export async function revalidateAgent(publisherId: string, agentId: string) { + // Revalidate specific agent pages + revalidatePath(`/publishers/${publisherId}/agents/${agentId}`) + revalidatePath(`/publishers/${publisherId}`) + + // Also revalidate the store to reflect changes + revalidatePath('/store') + revalidateTag('agents') +} + +/** + * Revalidate publisher-related data + * Use this when publisher information is updated + */ +export async function revalidatePublisher(publisherId: string) { + revalidatePath(`/publishers/${publisherId}`) + revalidatePath('/publishers') + revalidateTag('publishers') + + // Also revalidate agents since publisher info appears in agent cards + revalidateTag('agents') +}