feat: add two-tutorial structure with landing page
- Create landing page at / with cards linking to both tutorials - Add /agents route for agent developers (search & execute) - Add /authors route for package authors (publish tools) - Create 9 author slides covering the publishing workflow - Rename Slideshow.tsx to AgentSlideshow.tsx - Add back navigation buttons to both slideshows - Fix array key lint errors using unique slide IDs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
e298109e94
commit
4a13709a3d
14 changed files with 1027 additions and 17 deletions
11
apps/tutorial/src/app/agents/page.tsx
Normal file
11
apps/tutorial/src/app/agents/page.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
'use client';
|
||||
|
||||
import { AgentSlideshow } from '@/components/AgentSlideshow';
|
||||
|
||||
export default function AgentsTutorial(): React.ReactElement {
|
||||
return (
|
||||
<main className="h-screen w-screen overflow-hidden">
|
||||
<AgentSlideshow />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
11
apps/tutorial/src/app/authors/page.tsx
Normal file
11
apps/tutorial/src/app/authors/page.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
'use client';
|
||||
|
||||
import { AuthorSlideshow } from '@/components/AuthorSlideshow';
|
||||
|
||||
export default function AuthorsTutorial(): React.ReactElement {
|
||||
return (
|
||||
<main className="h-screen w-screen overflow-hidden">
|
||||
<AuthorSlideshow />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,11 +1,182 @@
|
|||
'use client';
|
||||
|
||||
import { Slideshow } from '@/components/Slideshow';
|
||||
import { motion } from 'framer-motion';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function TutorialPage(): React.ReactElement {
|
||||
const tutorials = [
|
||||
{
|
||||
id: 'agents',
|
||||
title: 'For Agent Developers',
|
||||
subtitle: 'Search & Execute',
|
||||
description: 'Give your agent access to 200+ tools at runtime. No imports. No bundling.',
|
||||
href: '/agents',
|
||||
gradient: 'from-cyan-500 to-blue-600',
|
||||
icon: (
|
||||
<svg
|
||||
className="w-8 h-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714c0 .597.237 1.17.659 1.591L19.8 15.3M14.25 3.104c.251.023.501.05.75.082M19.8 15.3l-1.57.393A9.065 9.065 0 0112 15a9.065 9.065 0 00-6.23.693L5 14.5m14.8.8l1.402 1.402c1.232 1.232.65 3.318-1.067 3.611A48.309 48.309 0 0112 21c-2.773 0-5.491-.235-8.135-.687-1.718-.293-2.3-2.379-1.067-3.61L5 14.5"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
features: ['searchTpmjsToolsTool', 'registryExecuteTool', 'Sandbox execution'],
|
||||
},
|
||||
{
|
||||
id: 'authors',
|
||||
title: 'For Package Authors',
|
||||
subtitle: 'Publish Your Tools',
|
||||
description:
|
||||
'Get your AI SDK tools discovered by agents worldwide. npm publish is all it takes.',
|
||||
href: '/authors',
|
||||
gradient: 'from-purple-500 to-pink-600',
|
||||
icon: (
|
||||
<svg
|
||||
className="w-8 h-8"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.5}
|
||||
d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
features: ['tpmjs-tool keyword', 'Schema extraction', 'Quality scoring'],
|
||||
},
|
||||
];
|
||||
|
||||
export default function TutorialHome(): React.ReactElement {
|
||||
return (
|
||||
<main className="h-screen w-screen overflow-hidden">
|
||||
<Slideshow />
|
||||
<main className="min-h-screen bg-[#0a0a0f] flex flex-col items-center justify-center px-6 py-12">
|
||||
{/* Background */}
|
||||
<div className="fixed inset-0 bg-gradient-to-br from-[#0a0a0f] via-[#0d1117] to-[#0a0a0f]" />
|
||||
<div
|
||||
className="fixed inset-0 opacity-[0.03]"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: '50px 50px',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 max-w-4xl w-full">
|
||||
{/* Header */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
className="text-center mb-16"
|
||||
>
|
||||
<div className="inline-flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 mb-6">
|
||||
<span className="text-cyan-400 font-mono text-sm">tpmjs.com</span>
|
||||
<span className="text-white/30">/</span>
|
||||
<span className="text-white/60 font-mono text-sm">tutorials</span>
|
||||
</div>
|
||||
<h1 className="text-4xl md:text-6xl font-bold text-white mb-4">Learn TPMJS</h1>
|
||||
<p className="text-xl text-white/50 max-w-2xl mx-auto">
|
||||
The tool registry for the Vercel AI SDK. Choose your path.
|
||||
</p>
|
||||
</motion.div>
|
||||
|
||||
{/* Tutorial Cards */}
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{tutorials.map((tutorial, index) => (
|
||||
<motion.div
|
||||
key={tutorial.id}
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 + index * 0.1 }}
|
||||
>
|
||||
<Link href={tutorial.href} className="block group">
|
||||
<div className="relative p-8 rounded-2xl bg-white/5 border border-white/10 hover:border-white/20 transition-all duration-300 hover:bg-white/[0.07] overflow-hidden">
|
||||
{/* Gradient glow on hover */}
|
||||
<div
|
||||
className={`absolute inset-0 bg-gradient-to-br ${tutorial.gradient} opacity-0 group-hover:opacity-5 transition-opacity duration-300`}
|
||||
/>
|
||||
|
||||
{/* Icon */}
|
||||
<div
|
||||
className={`inline-flex p-3 rounded-xl bg-gradient-to-br ${tutorial.gradient} text-white mb-6`}
|
||||
>
|
||||
{tutorial.icon}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative">
|
||||
<div className="text-xs font-mono text-white/40 mb-2">{tutorial.subtitle}</div>
|
||||
<h2 className="text-2xl font-bold text-white mb-3">{tutorial.title}</h2>
|
||||
<p className="text-white/50 mb-6">{tutorial.description}</p>
|
||||
|
||||
{/* Features */}
|
||||
<div className="flex flex-wrap gap-2 mb-6">
|
||||
{tutorial.features.map((feature) => (
|
||||
<span
|
||||
key={feature}
|
||||
className="px-2 py-1 rounded text-xs font-mono bg-white/5 text-white/60"
|
||||
>
|
||||
{feature}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* CTA */}
|
||||
<div
|
||||
className={`inline-flex items-center gap-2 text-sm font-medium bg-gradient-to-r ${tutorial.gradient} bg-clip-text text-transparent group-hover:gap-3 transition-all`}
|
||||
>
|
||||
Start Tutorial
|
||||
<svg
|
||||
className="w-4 h-4 text-white/60 group-hover:translate-x-1 transition-transform"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 5l7 7-7 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="text-center mt-12"
|
||||
>
|
||||
<a
|
||||
href="https://tpmjs.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-white/30 hover:text-white/50 text-sm transition-colors"
|
||||
>
|
||||
Back to tpmjs.com
|
||||
</a>
|
||||
</motion.div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useSlideshow } from '@/hooks/useSlideshow';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import Link from 'next/link';
|
||||
import { Slide } from './Slide';
|
||||
import { SlideNavigation } from './SlideNavigation';
|
||||
import { SlideProgress } from './SlideProgress';
|
||||
|
|
@ -16,18 +17,18 @@ import { ToolDetailSlide } from './slides/ToolDetailSlide';
|
|||
import { WelcomeSlide } from './slides/WelcomeSlide';
|
||||
|
||||
const SLIDES = [
|
||||
WelcomeSlide,
|
||||
ProblemSlide,
|
||||
SolutionSlide,
|
||||
HowItWorksSlide,
|
||||
DiscoverySlide,
|
||||
IntegrationSlide,
|
||||
QualitySlide,
|
||||
ToolDetailSlide,
|
||||
GetStartedSlide,
|
||||
{ id: 'welcome', Component: WelcomeSlide },
|
||||
{ id: 'problem', Component: ProblemSlide },
|
||||
{ id: 'solution', Component: SolutionSlide },
|
||||
{ id: 'how-it-works', Component: HowItWorksSlide },
|
||||
{ id: 'discovery', Component: DiscoverySlide },
|
||||
{ id: 'integration', Component: IntegrationSlide },
|
||||
{ id: 'quality', Component: QualitySlide },
|
||||
{ id: 'tool-detail', Component: ToolDetailSlide },
|
||||
{ id: 'get-started', Component: GetStartedSlide },
|
||||
];
|
||||
|
||||
export function Slideshow(): React.ReactElement {
|
||||
export function AgentSlideshow(): React.ReactElement {
|
||||
const { currentSlide, totalSlides, next, prev, goTo, direction, isFirst, isLast } = useSlideshow({
|
||||
totalSlides: SLIDES.length,
|
||||
});
|
||||
|
|
@ -49,11 +50,28 @@ export function Slideshow(): React.ReactElement {
|
|||
}}
|
||||
/>
|
||||
|
||||
{/* Back button */}
|
||||
<Link
|
||||
href="/"
|
||||
className="fixed top-6 left-6 z-50 flex items-center gap-2 px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white/50 hover:text-white/80 hover:bg-white/10 transition-all text-sm"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Tutorials
|
||||
</Link>
|
||||
|
||||
{/* Slides */}
|
||||
<AnimatePresence mode="wait" custom={direction}>
|
||||
{SLIDES.map((SlideContent, index) => (
|
||||
<Slide key={index} isActive={index === currentSlide} direction={direction}>
|
||||
<SlideContent />
|
||||
{SLIDES.map((slide, index) => (
|
||||
<Slide key={slide.id} isActive={index === currentSlide} direction={direction}>
|
||||
<slide.Component />
|
||||
</Slide>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
91
apps/tutorial/src/components/AuthorSlideshow.tsx
Normal file
91
apps/tutorial/src/components/AuthorSlideshow.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
'use client';
|
||||
|
||||
import { useSlideshow } from '@/hooks/useSlideshow';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import Link from 'next/link';
|
||||
import { Slide } from './Slide';
|
||||
import { SlideNavigation } from './SlideNavigation';
|
||||
import { SlideProgress } from './SlideProgress';
|
||||
import { AuthorNextStepsSlide } from './author-slides/AuthorNextStepsSlide';
|
||||
import { AuthorWelcomeSlide } from './author-slides/AuthorWelcomeSlide';
|
||||
import { GetDiscoveredSlide } from './author-slides/GetDiscoveredSlide';
|
||||
import { HealthChecksSlide } from './author-slides/HealthChecksSlide';
|
||||
import { HowSyncWorksSlide } from './author-slides/HowSyncWorksSlide';
|
||||
import { PackageSetupSlide } from './author-slides/PackageSetupSlide';
|
||||
import { QualityScoringSlide } from './author-slides/QualityScoringSlide';
|
||||
import { ToolDefinitionSlide } from './author-slides/ToolDefinitionSlide';
|
||||
import { WhatIsRegistrySlide } from './author-slides/WhatIsRegistrySlide';
|
||||
|
||||
const SLIDES = [
|
||||
{ id: 'welcome', Component: AuthorWelcomeSlide },
|
||||
{ id: 'what-is', Component: WhatIsRegistrySlide },
|
||||
{ id: 'sync', Component: HowSyncWorksSlide },
|
||||
{ id: 'setup', Component: PackageSetupSlide },
|
||||
{ id: 'definition', Component: ToolDefinitionSlide },
|
||||
{ id: 'quality', Component: QualityScoringSlide },
|
||||
{ id: 'health', Component: HealthChecksSlide },
|
||||
{ id: 'discovered', Component: GetDiscoveredSlide },
|
||||
{ id: 'next-steps', Component: AuthorNextStepsSlide },
|
||||
];
|
||||
|
||||
export function AuthorSlideshow(): React.ReactElement {
|
||||
const { currentSlide, totalSlides, next, prev, goTo, direction, isFirst, isLast } = useSlideshow({
|
||||
totalSlides: SLIDES.length,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="relative h-full w-full bg-[#0a0a0f] overflow-hidden">
|
||||
{/* Background gradient */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-[#0a0a0f] via-[#0d1117] to-[#0a0a0f]" />
|
||||
|
||||
{/* Subtle grid pattern */}
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.03]"
|
||||
style={{
|
||||
backgroundImage: `
|
||||
linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px)
|
||||
`,
|
||||
backgroundSize: '50px 50px',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Back button */}
|
||||
<Link
|
||||
href="/"
|
||||
className="fixed top-6 left-6 z-50 flex items-center gap-2 px-3 py-2 rounded-lg bg-white/5 border border-white/10 text-white/50 hover:text-white/80 hover:bg-white/10 transition-all text-sm"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
Tutorials
|
||||
</Link>
|
||||
|
||||
{/* Slides */}
|
||||
<AnimatePresence mode="wait" custom={direction}>
|
||||
{SLIDES.map((slide, index) => (
|
||||
<Slide key={slide.id} isActive={index === currentSlide} direction={direction}>
|
||||
<slide.Component />
|
||||
</Slide>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Navigation */}
|
||||
<SlideNavigation onNext={next} onPrev={prev} isFirst={isFirst} isLast={isLast} />
|
||||
|
||||
{/* Progress */}
|
||||
<SlideProgress currentSlide={currentSlide} totalSlides={totalSlides} goTo={goTo} />
|
||||
|
||||
{/* Keyboard hint */}
|
||||
<div className="fixed bottom-8 right-8 z-50 text-white/30 text-xs font-mono">
|
||||
Use arrow keys or click to navigate
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const steps = [
|
||||
{ num: '1', text: 'Add "tpmjs-tool" to your package.json keywords' },
|
||||
{ num: '2', text: 'Define your tools in the "tpmjs" field' },
|
||||
{ num: '3', text: 'npm publish' },
|
||||
];
|
||||
|
||||
export function AuthorNextStepsSlide(): React.ReactElement {
|
||||
return (
|
||||
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<div
|
||||
className="absolute w-[600px] h-[600px] rounded-full bg-gradient-to-r from-purple-500/10 to-pink-500/10 blur-[100px]"
|
||||
style={{ top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="relative z-10 max-w-3xl w-full"
|
||||
>
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">Ready to Publish?</h2>
|
||||
<p className="text-xl text-white/50 mb-10">Three steps. That's it.</p>
|
||||
|
||||
<div className="space-y-4 max-w-md mx-auto mb-10">
|
||||
{steps.map((step, i) => (
|
||||
<motion.div
|
||||
key={step.num}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.3 + i * 0.1 }}
|
||||
className="flex items-center gap-4 p-4 rounded-xl bg-white/5 border border-white/10 text-left"
|
||||
>
|
||||
<span className="flex-shrink-0 w-10 h-10 rounded-full bg-purple-500/20 text-purple-400 flex items-center justify-center font-mono text-lg">
|
||||
{step.num}
|
||||
</span>
|
||||
<span className="text-white/70">{step.text}</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.8 }}
|
||||
className="flex flex-wrap justify-center gap-4"
|
||||
>
|
||||
<a
|
||||
href="https://tpmjs.com/tool-search"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-6 py-3 rounded-lg bg-purple-500/20 text-purple-400 text-sm font-medium hover:bg-purple-500/30 transition-colors"
|
||||
>
|
||||
Browse Existing Tools
|
||||
</a>
|
||||
<a
|
||||
href="https://tpmjs.com/playground"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-6 py-3 rounded-lg bg-cyan-500/20 text-cyan-400 text-sm font-medium hover:bg-cyan-500/30 transition-colors"
|
||||
>
|
||||
Try the Playground
|
||||
</a>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1 }}
|
||||
className="mt-10 text-white/30 text-sm"
|
||||
>
|
||||
Indexed within 2 minutes. No registration required.
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export function AuthorWelcomeSlide(): React.ReactElement {
|
||||
return (
|
||||
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-purple-900/20 via-transparent to-pink-900/20 pointer-events-none" />
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.9 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ duration: 0.8, ease: 'easeOut' }}
|
||||
className="relative z-10"
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="mb-6"
|
||||
>
|
||||
<span className="px-4 py-2 rounded-full bg-purple-500/20 text-purple-400 text-sm font-mono">
|
||||
For Package Authors
|
||||
</span>
|
||||
</motion.div>
|
||||
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="text-5xl md:text-7xl font-bold text-white mb-6"
|
||||
>
|
||||
Publish Your Tools
|
||||
</motion.h1>
|
||||
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="text-xl md:text-2xl text-white/50 max-w-2xl mx-auto mb-8"
|
||||
>
|
||||
Get your AI SDK tools discovered by agents worldwide.
|
||||
<br />
|
||||
<span className="text-purple-400">npm publish</span> is all it takes.
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 0.8 }}
|
||||
className="text-white/30 text-sm font-mono"
|
||||
>
|
||||
Press arrow keys or click to continue
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const searchExample = [
|
||||
{ id: 'agent', line: 'Agent: "I need to format some markdown"', color: 'white' },
|
||||
{ id: 'search', line: 'searchTpmjsToolsTool({ query: "markdown format" })', color: 'cyan' },
|
||||
{ id: 'result', line: '→ Found: @your-org/tools::formatMarkdownTable', color: 'emerald' },
|
||||
{ id: 'execute', line: 'registryExecuteTool({ toolId: "...", params })', color: 'purple' },
|
||||
{ id: 'done', line: '→ Your tool runs. Agent gets result.', color: 'yellow' },
|
||||
];
|
||||
|
||||
export function GetDiscoveredSlide(): React.ReactElement {
|
||||
return (
|
||||
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-cyan-900/10 via-transparent to-purple-900/10 pointer-events-none" />
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="relative z-10 max-w-3xl w-full"
|
||||
>
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">How Agents Find You</h2>
|
||||
<p className="text-xl text-white/50 mb-10">Your tool in action</p>
|
||||
|
||||
<div className="space-y-3 text-left max-w-xl mx-auto">
|
||||
{searchExample.map((item, i) => (
|
||||
<motion.div
|
||||
key={item.id}
|
||||
initial={{ opacity: 0, x: item.id === 'agent' ? -20 : 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.3 + i * 0.15 }}
|
||||
className={`p-3 rounded-lg font-mono text-sm ${
|
||||
item.id === 'agent' ? 'bg-white/5' : 'bg-white/5 ml-4'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
item.color === 'cyan'
|
||||
? 'text-cyan-400'
|
||||
: item.color === 'purple'
|
||||
? 'text-purple-400'
|
||||
: item.color === 'emerald'
|
||||
? 'text-emerald-400'
|
||||
: item.color === 'yellow'
|
||||
? 'text-yellow-400'
|
||||
: 'text-white/70'
|
||||
}
|
||||
>
|
||||
{item.line}
|
||||
</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.2 }}
|
||||
className="mt-10 p-4 rounded-xl bg-white/5 border border-white/10"
|
||||
>
|
||||
<div className="text-sm text-white/60">
|
||||
The agent never installed your package.
|
||||
<br />
|
||||
<span className="text-cyan-400">They found and ran it at runtime.</span>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const checks = [
|
||||
{
|
||||
status: 'HEALTHY',
|
||||
icon: '✓',
|
||||
title: 'Import Health',
|
||||
desc: 'We require() your package to verify it loads',
|
||||
color: 'emerald',
|
||||
},
|
||||
{
|
||||
status: 'HEALTHY',
|
||||
icon: '✓',
|
||||
title: 'Execution Health',
|
||||
desc: 'We call your tool with test params',
|
||||
color: 'emerald',
|
||||
},
|
||||
{
|
||||
status: 'BROKEN',
|
||||
icon: '!',
|
||||
title: 'Labeled, Not Hidden',
|
||||
desc: 'Failed tools show warning, still searchable',
|
||||
color: 'yellow',
|
||||
},
|
||||
];
|
||||
|
||||
export function HealthChecksSlide(): React.ReactElement {
|
||||
return (
|
||||
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-emerald-900/10 via-transparent to-yellow-900/10 pointer-events-none" />
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="relative z-10 max-w-3xl w-full"
|
||||
>
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">Health Checks</h2>
|
||||
<p className="text-xl text-white/50 mb-12">We verify your tools actually work</p>
|
||||
|
||||
<div className="space-y-4 max-w-xl mx-auto">
|
||||
{checks.map((check, i) => (
|
||||
<motion.div
|
||||
key={check.title}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.3 + i * 0.15 }}
|
||||
className="flex items-start gap-4 p-4 rounded-xl bg-white/5 border border-white/10 text-left"
|
||||
>
|
||||
<div
|
||||
className={`flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center text-lg font-bold ${
|
||||
check.color === 'emerald'
|
||||
? 'bg-emerald-500/20 text-emerald-400'
|
||||
: 'bg-yellow-500/20 text-yellow-400'
|
||||
}`}
|
||||
>
|
||||
{check.icon}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-medium text-white">{check.title}</span>
|
||||
<span
|
||||
className={`text-xs font-mono px-2 py-0.5 rounded ${
|
||||
check.status === 'HEALTHY'
|
||||
? 'bg-emerald-500/20 text-emerald-400'
|
||||
: 'bg-yellow-500/20 text-yellow-400'
|
||||
}`}
|
||||
>
|
||||
{check.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-sm text-white/50">{check.desc}</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1 }}
|
||||
className="mt-8 text-white/40 text-sm"
|
||||
>
|
||||
Health checks run hourly. Fix issues anytime by publishing a new version.
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const syncSteps = [
|
||||
{ step: '1', text: 'You publish to npm with tpmjs-tool keyword', color: 'purple' },
|
||||
{ step: '2', text: 'Registry detects your package within 2 minutes', color: 'cyan' },
|
||||
{ step: '3', text: 'We extract schemas from your package.json', color: 'emerald' },
|
||||
{ step: '4', text: 'Your tool appears in search results', color: 'yellow' },
|
||||
];
|
||||
|
||||
export function HowSyncWorksSlide(): React.ReactElement {
|
||||
return (
|
||||
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-purple-900/10 via-transparent to-cyan-900/10 pointer-events-none" />
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="relative z-10 max-w-3xl w-full"
|
||||
>
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">Automatic Discovery</h2>
|
||||
<p className="text-xl text-white/50 mb-12">Publish once. We handle the rest.</p>
|
||||
|
||||
<div className="space-y-4 text-left max-w-xl mx-auto">
|
||||
{syncSteps.map((item, i) => (
|
||||
<motion.div
|
||||
key={item.step}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.3 + i * 0.15 }}
|
||||
className="flex items-start gap-4"
|
||||
>
|
||||
<span
|
||||
className={`flex-shrink-0 w-10 h-10 rounded-full flex items-center justify-center text-lg font-mono ${
|
||||
item.color === 'purple'
|
||||
? 'bg-purple-500/20 text-purple-400'
|
||||
: item.color === 'cyan'
|
||||
? 'bg-cyan-500/20 text-cyan-400'
|
||||
: item.color === 'emerald'
|
||||
? 'bg-emerald-500/20 text-emerald-400'
|
||||
: 'bg-yellow-500/20 text-yellow-400'
|
||||
}`}
|
||||
>
|
||||
{item.step}
|
||||
</span>
|
||||
<span className="text-white/70 pt-2">{item.text}</span>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1.2 }}
|
||||
className="mt-10 p-4 rounded-xl bg-purple-500/10 border border-purple-500/20"
|
||||
>
|
||||
<code className="text-purple-400 font-mono text-sm">npm publish</code>
|
||||
<div className="text-white/40 text-sm mt-1">No registration. No API keys. Just npm.</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const packageJson = [
|
||||
{ id: 'open', line: '{' },
|
||||
{ id: 'name', line: ' "name": "@your-org/your-tools",' },
|
||||
{ id: 'version', line: ' "version": "1.0.0",' },
|
||||
{ id: 'keywords-open', line: ' "keywords": [' },
|
||||
{ id: 'keyword', line: ' "tpmjs-tool"' },
|
||||
{ id: 'keywords-close', line: ' ],' },
|
||||
{ id: 'tpmjs-open', line: ' "tpmjs": {' },
|
||||
{ id: 'tools-open', line: ' "tools": [{ ... }]' },
|
||||
{ id: 'tpmjs-close', line: ' }' },
|
||||
{ id: 'close', line: '}' },
|
||||
];
|
||||
|
||||
export function PackageSetupSlide(): React.ReactElement {
|
||||
return (
|
||||
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-emerald-900/10 via-transparent to-cyan-900/10 pointer-events-none" />
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="relative z-10 max-w-4xl w-full"
|
||||
>
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">Package Setup</h2>
|
||||
<p className="text-xl text-white/50 mb-10">Two things: a keyword and a tpmjs field</p>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8 max-w-3xl mx-auto">
|
||||
{/* package.json */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="text-left"
|
||||
>
|
||||
<div className="text-xs text-emerald-400/60 font-mono mb-2">package.json</div>
|
||||
<div className="bg-[#1e1e2e] p-4 rounded-xl font-mono text-sm">
|
||||
{packageJson.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={
|
||||
item.line.includes('tpmjs-tool')
|
||||
? 'text-cyan-400'
|
||||
: item.line.includes('"tpmjs"')
|
||||
? 'text-purple-400'
|
||||
: item.line.includes('tools')
|
||||
? 'text-emerald-400'
|
||||
: 'text-white/60'
|
||||
}
|
||||
>
|
||||
{item.line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Explanation */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="text-left space-y-4"
|
||||
>
|
||||
<div className="p-4 rounded-lg bg-white/5 border border-cyan-500/20">
|
||||
<div className="text-sm font-medium text-cyan-400 mb-1">tpmjs-tool keyword</div>
|
||||
<div className="text-xs text-white/50">Required. This is how we find you on npm.</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg bg-white/5 border border-purple-500/20">
|
||||
<div className="text-sm font-medium text-purple-400 mb-1">tpmjs field</div>
|
||||
<div className="text-xs text-white/50">
|
||||
Defines your tools: name, description, schemas.
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg bg-white/5 border border-emerald-500/20">
|
||||
<div className="text-sm font-medium text-emerald-400 mb-1">tools array</div>
|
||||
<div className="text-xs text-white/50">One entry per exported tool function.</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const factors = [
|
||||
{
|
||||
name: 'Schema Completeness',
|
||||
weight: '40%',
|
||||
desc: 'Rich parameter definitions with types and descriptions',
|
||||
color: 'cyan',
|
||||
},
|
||||
{
|
||||
name: 'npm Downloads',
|
||||
weight: '30%',
|
||||
desc: 'Community adoption signals quality',
|
||||
color: 'purple',
|
||||
},
|
||||
{
|
||||
name: 'Health Status',
|
||||
weight: '20%',
|
||||
desc: 'Can it import and execute successfully?',
|
||||
color: 'emerald',
|
||||
},
|
||||
{
|
||||
name: 'Documentation',
|
||||
weight: '10%',
|
||||
desc: 'Good descriptions help agents choose',
|
||||
color: 'yellow',
|
||||
},
|
||||
];
|
||||
|
||||
export function QualityScoringSlide(): React.ReactElement {
|
||||
return (
|
||||
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-yellow-900/10 via-transparent to-cyan-900/10 pointer-events-none" />
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="relative z-10 max-w-4xl w-full"
|
||||
>
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">Quality Score</h2>
|
||||
<p className="text-xl text-white/50 mb-10">Higher scores = better search ranking</p>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4 max-w-3xl mx-auto">
|
||||
{factors.map((factor, i) => (
|
||||
<motion.div
|
||||
key={factor.name}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 + i * 0.1 }}
|
||||
className="p-4 rounded-xl bg-white/5 border border-white/10 text-left"
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span
|
||||
className={`text-sm font-medium ${
|
||||
factor.color === 'cyan'
|
||||
? 'text-cyan-400'
|
||||
: factor.color === 'purple'
|
||||
? 'text-purple-400'
|
||||
: factor.color === 'emerald'
|
||||
? 'text-emerald-400'
|
||||
: 'text-yellow-400'
|
||||
}`}
|
||||
>
|
||||
{factor.name}
|
||||
</span>
|
||||
<span className="text-xs font-mono text-white/40">{factor.weight}</span>
|
||||
</div>
|
||||
<div className="text-xs text-white/50">{factor.desc}</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1 }}
|
||||
className="mt-8 p-4 rounded-xl bg-emerald-500/10 border border-emerald-500/20 max-w-md mx-auto"
|
||||
>
|
||||
<div className="text-emerald-400 font-mono text-sm">qualityScore: 0.92</div>
|
||||
<div className="text-white/40 text-xs mt-1">
|
||||
Rich schemas + healthy + documented = high score
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const toolDef = [
|
||||
{ id: 'open', line: '{' },
|
||||
{ id: 'name', line: ' "name": "formatMarkdownTable",' },
|
||||
{ id: 'desc', line: ' "description": "Format CSV data as markdown table",' },
|
||||
{ id: 'params-open', line: ' "parameters": [{' },
|
||||
{ id: 'param-name', line: ' "name": "csvData",' },
|
||||
{ id: 'param-type', line: ' "type": "string",' },
|
||||
{ id: 'param-req', line: ' "required": true,' },
|
||||
{ id: 'param-desc', line: ' "description": "CSV data to format"' },
|
||||
{ id: 'params-close', line: ' }],' },
|
||||
{ id: 'returns', line: ' "returns": { "type": "string" }' },
|
||||
{ id: 'close', line: '}' },
|
||||
];
|
||||
|
||||
export function ToolDefinitionSlide(): React.ReactElement {
|
||||
return (
|
||||
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-cyan-900/10 via-transparent to-emerald-900/10 pointer-events-none" />
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="relative z-10 max-w-4xl w-full"
|
||||
>
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">Tool Definition</h2>
|
||||
<p className="text-xl text-white/50 mb-10">
|
||||
Each tool needs a name, description, and parameter schema
|
||||
</p>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||||
{/* Tool JSON */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="text-left"
|
||||
>
|
||||
<div className="text-xs text-cyan-400/60 font-mono mb-2">tpmjs.tools[0]</div>
|
||||
<div className="bg-[#1e1e2e] p-4 rounded-xl font-mono text-xs">
|
||||
{toolDef.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className={
|
||||
item.line.includes('"name"')
|
||||
? 'text-emerald-400'
|
||||
: item.line.includes('"description"')
|
||||
? 'text-cyan-400'
|
||||
: item.line.includes('"parameters"') || item.line.includes('"returns"')
|
||||
? 'text-purple-400'
|
||||
: item.line.includes('"type"') || item.line.includes('"required"')
|
||||
? 'text-yellow-400'
|
||||
: 'text-white/60'
|
||||
}
|
||||
>
|
||||
{item.line}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* Field explanations */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
className="text-left space-y-3"
|
||||
>
|
||||
<div className="p-3 rounded-lg bg-white/5">
|
||||
<div className="text-xs font-mono text-emerald-400 mb-1">name</div>
|
||||
<div className="text-xs text-white/50">
|
||||
Function export name. Must match your code.
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-white/5">
|
||||
<div className="text-xs font-mono text-cyan-400 mb-1">description</div>
|
||||
<div className="text-xs text-white/50">
|
||||
How agents understand what your tool does.
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-white/5">
|
||||
<div className="text-xs font-mono text-purple-400 mb-1">parameters</div>
|
||||
<div className="text-xs text-white/50">
|
||||
Input schema. Each param needs name, type, required.
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-3 rounded-lg bg-white/5">
|
||||
<div className="text-xs font-mono text-purple-400 mb-1">returns</div>
|
||||
<div className="text-xs text-white/50">
|
||||
Output type. Helps agents understand responses.
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1 }}
|
||||
className="mt-8 text-white/40 text-sm"
|
||||
>
|
||||
Schemas are converted to Zod for AI SDK compatibility.
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
'use client';
|
||||
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
const features = [
|
||||
{ icon: '🔍', title: 'Discoverable', desc: 'Agents search by natural language query' },
|
||||
{ icon: '📦', title: 'npm-native', desc: 'Synced from npm every 2 minutes' },
|
||||
{ icon: '⚡', title: 'Instant Execution', desc: 'Run in sandbox, no install needed' },
|
||||
];
|
||||
|
||||
export function WhatIsRegistrySlide(): React.ReactElement {
|
||||
return (
|
||||
<div className="relative flex flex-col items-center justify-center h-full px-8 text-center">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-cyan-900/10 via-transparent to-purple-900/10 pointer-events-none" />
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8 }}
|
||||
className="relative z-10 max-w-4xl w-full"
|
||||
>
|
||||
<h2 className="text-4xl md:text-5xl font-bold text-white mb-4">What is TPMJS?</h2>
|
||||
<p className="text-xl text-white/50 mb-12">A tool registry for the Vercel AI SDK</p>
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-6 max-w-3xl mx-auto">
|
||||
{features.map((feature, index) => (
|
||||
<motion.div
|
||||
key={feature.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.3 + index * 0.15 }}
|
||||
className="p-6 rounded-xl bg-white/5 border border-white/10"
|
||||
>
|
||||
<div className="text-4xl mb-4">{feature.icon}</div>
|
||||
<div className="text-lg font-semibold text-white mb-2">{feature.title}</div>
|
||||
<div className="text-sm text-white/50">{feature.desc}</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{ delay: 1 }}
|
||||
className="mt-10 text-white/40 text-sm"
|
||||
>
|
||||
AI agents use TPMJS to find and run tools they don't have installed.
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue