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