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:
Ajax Davis 2025-12-29 13:36:57 +10:00
parent e298109e94
commit 4a13709a3d
14 changed files with 1027 additions and 17 deletions

View 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>
);
}

View 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>
);
}

View file

@ -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>
);
}

View file

@ -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>

View 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>
);
}

View file

@ -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&apos;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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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>
);
}

View file

@ -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&apos;t have installed.
</motion.div>
</motion.div>
</div>
);
}