tpmjs/apps/web/src/app/page.tsx
Ajax Davis 3c5c218207 feat(web): add scenarios UI - explorer, detail, and homepage section
- Add global scenarios explorer page at /scenarios
- Add scenario detail page with run history at /scenarios/[id]
- Add collection-scoped scenario detail page
- Add featured scenarios section to homepage
- Add useScenarios hook for data fetching
- Regenerate CLI manifest
2026-01-19 05:57:29 +10:00

555 lines
24 KiB
TypeScript

import { prisma } from '@tpmjs/db';
import { Badge } from '@tpmjs/ui/Badge/Badge';
import { Button } from '@tpmjs/ui/Button/Button';
import { Container } from '@tpmjs/ui/Container/Container';
import { Icon } from '@tpmjs/ui/Icon/Icon';
import Link from 'next/link';
import { AppHeader } from '../components/AppHeader';
// import { ArchitectureDiagramWrapper } from '../components/home/ArchitectureDiagramWrapper';
// import { FeaturesSection } from '../components/home/FeaturesSection';
import { HeroSection } from '../components/home/HeroSection';
export const dynamic = 'force-dynamic';
async function getHomePageData() {
try {
// Fetch stats in parallel
const [packageCount, toolCount, featuredTools, categoryStats, featuredScenarios] =
await Promise.all([
// Total package count
prisma.package.count(),
// Total tool count
prisma.tool.count(),
// Top 6 featured tools by quality score
prisma.tool.findMany({
orderBy: [{ qualityScore: 'desc' }, { package: { npmDownloadsLastMonth: 'desc' } }],
take: 6,
select: {
id: true,
name: true,
description: true,
qualityScore: true,
package: {
select: {
npmPackageName: true,
category: true,
npmDownloadsLastMonth: true,
isOfficial: true,
},
},
},
}),
// Category distribution for stats (group by package category)
prisma.package.groupBy({
by: ['category'],
_count: {
_all: true,
},
}),
// Featured scenarios - mix of high quality, diverse, and fresh
(async () => {
// Get high quality scenarios
const highQuality = await prisma.scenario.findMany({
where: {
collection: { isPublic: true },
qualityScore: { gte: 0.3 },
totalRuns: { gte: 1 },
},
orderBy: { qualityScore: 'desc' },
take: 3,
include: {
collection: {
select: {
id: true,
name: true,
slug: true,
user: { select: { username: true } },
},
},
},
});
// Get fresh scenarios (excluding already selected)
const seenIds = new Set(highQuality.map((s) => s.id));
const fresh = await prisma.scenario.findMany({
where: {
collection: { isPublic: true },
id: { notIn: Array.from(seenIds) },
},
orderBy: { createdAt: 'desc' },
take: 3,
include: {
collection: {
select: {
id: true,
name: true,
slug: true,
user: { select: { username: true } },
},
},
},
});
return [...highQuality, ...fresh].slice(0, 6);
})(),
]);
return {
stats: {
packageCount,
toolCount,
categoryCount: categoryStats.length,
},
featuredTools,
categories: categoryStats.slice(0, 5).map((c) => ({
name: c.category,
count: c._count._all,
})),
featuredScenarios,
};
} catch (error) {
console.error('Failed to fetch homepage data:', error);
return {
stats: {
packageCount: 0,
toolCount: 0,
categoryCount: 0,
},
featuredTools: [],
categories: [],
featuredScenarios: [],
};
}
}
export default async function HomePage(): Promise<React.ReactElement> {
const data = await getHomePageData();
return (
<>
<AppHeader />
<main>
{/* Hero Section - Dithered Design */}
<HeroSection stats={data.stats} />
{/* Features Section - Interactive & Animated - temporarily disabled
<FeaturesSection />
*/}
{/* Architecture Diagram Section - temporarily disabled
<section className="py-16 bg-background border-b border-border">
<Container size="xl" padding="lg">
<div className="text-center mb-10">
<h2 className="text-3xl md:text-4xl font-bold mb-4 text-foreground">
How It Works
</h2>
<p className="text-lg text-foreground-secondary max-w-2xl mx-auto">
Publish once to npm, reach every AI agent. Your tool goes live in minutes.
</p>
</div>
<ArchitectureDiagramWrapper />
</Container>
</section>
*/}
{/* Featured Tools Section */}
<section className="py-16 bg-background">
<Container size="xl" padding="lg">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold mb-4 text-foreground">
Featured Tools
</h2>
<p className="text-lg text-foreground-secondary max-w-2xl mx-auto mb-8">
Production-ready tools you can use today. Add to your AI agent in one line.
</p>
</div>
{data.featuredTools.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6 mb-12">
{data.featuredTools.map((tool) => (
<Link
key={tool.id}
href={`/tool/${tool.package.npmPackageName}/${tool.name}`}
className="group"
>
<div className="p-6 border border-border rounded-lg bg-surface hover:border-foreground transition-colors h-full flex flex-col">
<div className="flex items-start justify-between gap-2 mb-3">
<h3 className="text-lg font-semibold text-foreground group-hover:text-brutalist-accent transition-colors min-w-0 break-words">
{tool.package.npmPackageName}
<span className="text-xs text-foreground-tertiary ml-2">
({tool.name})
</span>
</h3>
{tool.package.isOfficial && (
<Badge variant="default" size="sm" className="flex-shrink-0">
Official
</Badge>
)}
</div>
<p className="text-sm text-foreground-secondary mb-4 flex-1 line-clamp-3">
{tool.description}
</p>
<div className="flex items-center gap-2 flex-wrap">
<Badge variant="outline" size="sm">
{tool.package.category}
</Badge>
</div>
<div className="mt-4 pt-4 border-t border-border flex items-center justify-between text-xs text-foreground-tertiary">
{tool.qualityScore && Number(tool.qualityScore) > 0 ? (
<span className="flex items-center gap-1">
<span className="text-brutalist-accent"></span>
{Number(tool.qualityScore).toFixed(2)}
</span>
) : (
<span />
)}
<span>
{(tool.package.npmDownloadsLastMonth ?? 0) > 0
? `${tool.package.npmDownloadsLastMonth?.toLocaleString()} downloads/mo`
: 'New'}
</span>
</div>
</div>
</Link>
))}
</div>
) : (
<div className="text-center py-12">
<p className="text-foreground-secondary">
No tools available yet. Check back soon!
</p>
</div>
)}
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
<Link href="/tool/tool-search">
<Button size="lg" variant="default">
Browse All {data.stats.toolCount} Tools
</Button>
</Link>
<Link href="/tool/tool-search">
<Button size="lg" variant="outline">
Search by Category
</Button>
</Link>
</div>
</Container>
</section>
{/* Featured Scenarios Section */}
{data.featuredScenarios.length > 0 && (
<section className="py-16 bg-surface border-t border-border">
<Container size="xl" padding="lg">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold mb-4 text-foreground">
Test Scenarios
</h2>
<p className="text-lg text-foreground-secondary max-w-2xl mx-auto mb-8">
See how tool collections are tested with AI-generated scenarios. Real execution,
real results.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 md:gap-6 mb-12">
{data.featuredScenarios.map((scenario) => {
const qualityPercent = Math.round(scenario.qualityScore * 100);
const qualityColor =
qualityPercent >= 70
? 'text-success'
: qualityPercent >= 40
? 'text-warning'
: 'text-foreground-tertiary';
return (
<Link
key={scenario.id}
href={
scenario.collection
? `/@${scenario.collection.user.username}/collections/${scenario.collection.slug}/scenarios/${scenario.id}`
: `/scenarios/${scenario.id}`
}
className="group"
>
<div className="p-6 border border-border rounded-lg bg-background hover:border-foreground transition-colors h-full flex flex-col">
<div className="flex items-start justify-between gap-2 mb-3">
<h3 className="text-lg font-semibold text-foreground group-hover:text-brutalist-accent transition-colors min-w-0 break-words line-clamp-1">
{scenario.name ||
(scenario.prompt.length > 50
? `${scenario.prompt.slice(0, 50)}...`
: scenario.prompt)}
</h3>
{scenario.lastRunStatus === 'pass' && (
<Badge
size="sm"
className="flex-shrink-0 bg-success/10 text-success border-success/20"
>
<Icon icon="check" className="w-3 h-3 mr-1" />
Pass
</Badge>
)}
{scenario.lastRunStatus === 'fail' && (
<Badge
size="sm"
className="flex-shrink-0 bg-error/10 text-error border-error/20"
>
<Icon icon="x" className="w-3 h-3 mr-1" />
Fail
</Badge>
)}
</div>
<p className="text-sm text-foreground-secondary mb-4 flex-1 line-clamp-2">
{scenario.prompt}
</p>
{scenario.collection && (
<div className="flex items-center gap-2 mb-4">
<Badge variant="outline" size="sm">
{scenario.collection.name}
</Badge>
<span className="text-xs text-foreground-tertiary">
by @{scenario.collection.user.username}
</span>
</div>
)}
{scenario.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mb-4">
{scenario.tags.slice(0, 3).map((tag) => (
<Badge key={tag} variant="secondary" size="sm" className="text-xs">
{tag}
</Badge>
))}
</div>
)}
<div className="mt-auto pt-4 border-t border-border flex items-center justify-between text-xs text-foreground-tertiary">
<span className={`flex items-center gap-1 ${qualityColor}`}>
<Icon icon="star" className="w-3.5 h-3.5" />
{qualityPercent}% quality
</span>
<span>{scenario.totalRuns} runs</span>
</div>
</div>
</Link>
);
})}
</div>
<div className="flex justify-center">
<Link href="/scenarios">
<Button size="lg" variant="outline">
Browse All Scenarios
</Button>
</Link>
</div>
</Container>
</section>
)}
{/* Integration Section */}
<section className="py-20 bg-surface border-y border-border">
<Container size="xl" padding="lg">
{/* Section Header */}
<div className="text-center mb-16">
<p className="font-mono text-xs text-primary uppercase tracking-widest mb-3">
universal compatibility
</p>
<h2 className="font-mono text-3xl md:text-4xl font-semibold mb-4 text-foreground lowercase">
works everywhere
</h2>
<p className="text-base text-foreground-secondary max-w-xl mx-auto font-sans">
Use TPMJS tools with any MCP-compatible AI client. One URL, instant access.
</p>
</div>
{/* Client Cards - Fieldset Style */}
<fieldset className="border border-dashed border-border p-8 mb-12">
<legend className="font-mono text-sm text-foreground-secondary px-3 lowercase">
supported clients
</legend>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="group p-6 border border-dashed border-border hover:border-primary hover:bg-primary/5 transition-all">
<div className="w-10 h-10 bg-primary/10 flex items-center justify-center mb-4">
<span className="font-mono text-lg text-primary">C</span>
</div>
<div className="font-mono text-lg font-medium text-foreground group-hover:text-primary transition-colors">
Claude
</div>
<p className="font-mono text-xs text-foreground-tertiary mt-1">
desktop / projects / api
</p>
</div>
<div className="group p-6 border border-dashed border-border hover:border-primary hover:bg-primary/5 transition-all">
<div className="w-10 h-10 bg-primary/10 flex items-center justify-center mb-4">
<span className="font-mono text-lg text-primary">{'>_'}</span>
</div>
<div className="font-mono text-lg font-medium text-foreground group-hover:text-primary transition-colors">
Cursor
</div>
<p className="font-mono text-xs text-foreground-tertiary mt-1">
ai-powered code editor
</p>
</div>
<div className="group p-6 border border-dashed border-border hover:border-primary hover:bg-primary/5 transition-all">
<div className="w-10 h-10 bg-primary/10 flex items-center justify-center mb-4">
<span className="font-mono text-lg text-primary">W</span>
</div>
<div className="font-mono text-lg font-medium text-foreground group-hover:text-primary transition-colors">
Windsurf
</div>
<p className="font-mono text-xs text-foreground-tertiary mt-1">agentic ide</p>
</div>
<div className="group p-6 border border-dashed border-border hover:border-primary hover:bg-primary/5 transition-all">
<div className="w-10 h-10 bg-primary/10 flex items-center justify-center mb-4">
<span className="font-mono text-lg text-primary">*</span>
</div>
<div className="font-mono text-lg font-medium text-foreground group-hover:text-primary transition-colors">
Any MCP
</div>
<p className="font-mono text-xs text-foreground-tertiary mt-1">
open protocol support
</p>
</div>
</div>
</fieldset>
{/* Code Example - Fieldset Style */}
<fieldset className="border border-dashed border-border p-8 max-w-2xl mx-auto">
<legend className="font-mono text-sm text-foreground-secondary px-3 lowercase">
quick start
</legend>
<div className="mb-4">
<p className="font-mono text-xs text-foreground-tertiary uppercase tracking-wider mb-2">
claude_desktop_config.json
</p>
<div className="bg-background border border-border p-4 font-mono text-sm overflow-x-auto">
<pre className="text-foreground">
<span className="text-foreground-tertiary">{'{'}</span>
{'\n '}
<span className="text-primary">"mcpServers"</span>:{' '}
<span className="text-foreground-tertiary">{'{'}</span>
{'\n '}
<span className="text-primary">"tpmjs"</span>:{' '}
<span className="text-foreground-tertiary">{'{'}</span>
{'\n '}
<span className="text-primary">"command"</span>:{' '}
<span className="text-success">"npx"</span>,{'\n '}
<span className="text-primary">"args"</span>: [
<span className="text-success">"-y"</span>,{' '}
<span className="text-success">"@anthropic/mcp-remote"</span>,{'\n '}
<span className="text-success">
"https://tpmjs.com/api/mcp/ajax/ajax-collection/sse"
</span>
]{'\n '}
<span className="text-foreground-tertiary">{'}'}</span>
{'\n '}
<span className="text-foreground-tertiary">{'}'}</span>
{'\n'}
<span className="text-foreground-tertiary">{'}'}</span>
</pre>
</div>
</div>
<div className="flex items-center gap-3 pt-4 border-t border-dashed border-border">
<div className="w-2 h-2 bg-success rounded-full animate-pulse" />
<p className="font-mono text-xs text-foreground-secondary">
add to config instant access to{' '}
<span className="text-primary font-medium">170+ tools</span>
</p>
</div>
</fieldset>
</Container>
</section>
{/* Publish Your Tool Section */}
<section className="py-16 bg-surface">
<Container size="xl" padding="lg">
<div className="text-center max-w-3xl mx-auto">
<h2 className="text-3xl md:text-4xl font-bold mb-4 text-foreground">
Publish Your AI Tool
</h2>
<p className="text-lg text-foreground-secondary mb-8">
Share your tool with the AI community. Automatic discovery, quality scoring, and
integration with Vercel AI SDK, LangChain, and more.
</p>
{/* Generator Highlight Box */}
<div className="mb-12 p-6 border-2 border-primary/50 rounded-lg bg-primary/5 text-left">
<div className="flex flex-col sm:flex-row items-start gap-4">
<div className="text-3xl sm:text-4xl" aria-hidden="true">
</div>
<div className="flex-1">
<h3 className="text-xl font-bold mb-2 text-foreground">
Start with Our Package Generator
</h3>
<p className="text-sm text-foreground-secondary mb-4">
Create a production-ready TPMJS tool package in seconds with our CLI
generator. Includes 2-3 tools, complete setup, and best practices.
</p>
<div className="flex flex-col sm:flex-row gap-3">
<code className="text-sm bg-surface px-4 py-2 rounded text-foreground border border-border">
npx @tpmjs/create-basic-tools
</code>
<a
href="https://github.com/tpmjs/tpmjs/tree/main/packages/tools/create-basic-tools#readme"
target="_blank"
rel="noopener noreferrer"
>
<Button size="sm" variant="outline">
View Docs
</Button>
</a>
</div>
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 md:gap-6 mb-8">
<div className="p-4">
<div className="text-3xl mb-2" aria-hidden="true">
🚀
</div>
<h3 className="font-semibold mb-1 text-foreground">Quick Setup</h3>
<p className="text-sm text-foreground-secondary">
Add one keyword to package.json and publish to NPM
</p>
</div>
<div className="p-4">
<div className="text-3xl mb-2" aria-hidden="true">
</div>
<h3 className="font-semibold mb-1 text-foreground">Auto Discovery</h3>
<p className="text-sm text-foreground-secondary">
Your tool appears on tpmjs.com within 15 minutes
</p>
</div>
<div className="p-4">
<div className="text-3xl mb-2" aria-hidden="true">
📊
</div>
<h3 className="font-semibold mb-1 text-foreground">Quality Metrics</h3>
<p className="text-sm text-foreground-secondary">
Automatic scoring based on docs, downloads, and stars
</p>
</div>
</div>
<Link href="/publish">
<Button size="lg" variant="default">
Learn How to Publish
</Button>
</Link>
</div>
</Container>
</section>
</main>
</>
);
}