523 lines
19 KiB
TypeScript

'use client';
import { ComponentProps, ReactElement, useState } from 'react';
import { ButtonLink, SectionHeading, VideoModal } from '@nx/nx-dev/ui-common';
import {
PlayIcon,
CommandLineIcon,
CpuChipIcon,
} from '@heroicons/react/24/outline';
import { sendCustomEvent } from '@nx/nx-dev/feature-analytics';
import { cx } from '@nx/nx-dev/ui-primitives';
import { MovingBorder } from '@nx/nx-dev/ui-animations';
import { motion, AnimatePresence } from 'framer-motion';
import Image from 'next/image';
function PlayButton({
className,
...props
}: ComponentProps<'div'>): ReactElement {
const parent = {
initial: {
width: 82,
transition: {
when: 'afterChildren',
},
},
hover: {
width: 296,
transition: {
duration: 0.125,
type: 'tween',
ease: 'easeOut',
},
},
};
const child = {
initial: {
opacity: 0,
x: -6,
},
hover: {
x: 0,
opacity: 1,
transition: {
duration: 0.015,
type: 'tween',
ease: 'easeOut',
},
},
};
return (
<div
className={cx(
'group relative overflow-hidden rounded-full bg-transparent p-[1px] shadow-md',
className
)}
{...props}
>
<div className="absolute inset-0">
<MovingBorder duration={5000} rx="5%" ry="5%">
<div className="size-20 bg-[radial-gradient(var(--blue-500)_40%,transparent_60%)] opacity-[0.8] dark:bg-[radial-gradient(var(--cyan-500)_40%,transparent_60%)]" />
</MovingBorder>
</div>
<motion.div
initial="initial"
whileHover="hover"
variants={parent}
className="relative isolate flex size-20 cursor-pointer items-center justify-center gap-6 rounded-full border-2 border-slate-100 bg-white/10 p-6 text-white antialiased backdrop-blur-xl"
role="button"
aria-label="Play video about Nx MCP"
tabIndex={0}
>
<PlayIcon aria-hidden="true" className="absolute left-6 top-6 size-8" />
<motion.div variants={child} className="absolute left-20 top-4 w-48">
<p className="text-base font-medium">Watch the video</p>
<p className="text-xs">See Nx AI in action.</p>
</motion.div>
</motion.div>
</div>
);
}
interface AIFeature {
id: string;
title: string;
description: string;
icon: React.ComponentType<React.ComponentProps<'svg'>>;
videoUrl: string;
thumbnailUrl: string;
eventId: string;
blogUrl?: string;
}
const aiFeatures: AIFeature[] = [
{
id: 'vscode-copilot',
title: 'Integrate with your LLM via MCP',
description:
'Connect your AI assistants directly to your Nx workspace for deep project understanding.',
icon: CpuChipIcon,
videoUrl: 'https://youtu.be/RNilYmJJzdk',
thumbnailUrl: '/images/ai/nx-copilot-mcp-yt-thumb.avif',
eventId: 'nx-ai-vscode-video-click',
blogUrl: '/blog/nx-mcp-vscode-copilot',
},
{
id: 'ci-fixes',
title: 'CI integration and AI-powered fixes',
description:
'Your LLM automatically diagnoses CI failures and suggests targeted fixes.',
icon: ({ className, ...props }: React.ComponentProps<'svg'>) => (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M12 2L2 7L12 12L22 7L12 2Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M2 17L12 22L22 17"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M2 12L12 17L22 12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
),
videoUrl: 'https://youtu.be/fPqPh4h8RJg',
thumbnailUrl: '/images/ai/ai-ci-fix-thumb.avif',
eventId: 'nx-ai-ci-video-click',
blogUrl: '/blog/nx-editor-ci-llm-integration',
},
{
id: 'terminal-integration',
title: 'Active terminal task and log awareness',
description:
'Give your LLM real-time visibility into running tasks and build outputs.',
icon: CommandLineIcon,
videoUrl: 'https://youtu.be/Cbc9_W5J6DA',
thumbnailUrl: '/images/ai/terminal-llm-comm-thumb.avif',
eventId: 'nx-ai-terminal-video-click',
// blogUrl: '/blog/nx-editor-ci-llm-integration',
},
{
id: 'code-generation',
title: 'Predictable code generation that works',
description:
'Generate workspace-aware code that follows your patterns and architecture.',
icon: ({ className, ...props }: React.ComponentProps<'svg'>) => (
<svg
className={className}
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M14 3V7C14 7.55228 14.4477 8 15 8H19"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M17 21H7C5.89543 21 5 20.1046 5 19V5C5 3.89543 5.89543 3 7 3H14L19 8V19C19 20.1046 18.1046 21 17 21Z"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M9 17H15"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<path
d="M9 13H15"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
),
videoUrl: 'https://youtu.be/PXNjedYhZDs',
thumbnailUrl: '/images/ai/video-code-gen-and-ai-thumb.avif',
eventId: 'nx-ai-codegen-video-click',
blogUrl: '/blog/nx-generators-ai-integration',
},
];
export interface HeroProps {
className?: string;
}
export function Hero(): JSX.Element {
const [isOpen, setIsOpen] = useState(false);
const [selectedFeature, setSelectedFeature] = useState<AIFeature>(
aiFeatures[0]
);
const headingVariants = {
hidden: { opacity: 0, y: 20 },
visible: {
opacity: 1,
y: 0,
transition: {
duration: 0.6,
ease: 'easeOut',
},
},
};
return (
<section className="relative overflow-hidden py-10 sm:py-16 md:py-20">
<div className="mx-auto max-w-7xl px-6 lg:px-8">
{/* Header Section */}
<div className="mx-auto max-w-4xl text-center">
<motion.div
initial="hidden"
animate="visible"
variants={headingVariants}
className="mb-16"
>
<SectionHeading
as="h1"
variant="display"
className="text-pretty tracking-tight"
>
AI that{' '}
<span className="rounded-lg bg-gradient-to-r from-cyan-500 to-blue-500 bg-clip-text text-transparent">
actually works
</span>{' '}
<br className="hidden md:block" />
for large codebases
</SectionHeading>
</motion.div>
</div>
{/* Interactive Video + Features Section */}
<div className="lg:grid lg:grid-cols-2 lg:items-center lg:gap-12">
{/* Video Section */}
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.3, duration: 0.6 }}
className="mb-12 lg:mb-0"
>
<div className="relative">
<div className="absolute bottom-0 start-0 -z-10 -translate-x-14 translate-y-10">
<svg
className="h-auto max-w-40 text-slate-200 dark:text-slate-800"
width="696"
height="653"
viewBox="0 0 696 653"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<circle cx="72.5" cy="29.5" r="29.5" fill="currentColor" />
<circle cx="171.5" cy="29.5" r="29.5" fill="currentColor" />
<circle cx="270.5" cy="29.5" r="29.5" fill="currentColor" />
<circle cx="369.5" cy="29.5" r="29.5" fill="currentColor" />
<circle cx="468.5" cy="29.5" r="29.5" fill="currentColor" />
<circle cx="567.5" cy="29.5" r="29.5" fill="currentColor" />
<circle cx="666.5" cy="29.5" r="29.5" fill="currentColor" />
<circle cx="29.5" cy="128.5" r="29.5" fill="currentColor" />
<circle cx="128.5" cy="128.5" r="29.5" fill="currentColor" />
<circle cx="227.5" cy="128.5" r="29.5" fill="currentColor" />
<circle cx="326.5" cy="128.5" r="29.5" fill="currentColor" />
<circle cx="425.5" cy="128.5" r="29.5" fill="currentColor" />
<circle cx="524.5" cy="128.5" r="29.5" fill="currentColor" />
<circle cx="623.5" cy="128.5" r="29.5" fill="currentColor" />
<circle cx="72.5" cy="227.5" r="29.5" fill="currentColor" />
<circle cx="171.5" cy="227.5" r="29.5" fill="currentColor" />
<circle cx="270.5" cy="227.5" r="29.5" fill="currentColor" />
<circle cx="369.5" cy="227.5" r="29.5" fill="currentColor" />
<circle cx="468.5" cy="227.5" r="29.5" fill="currentColor" />
<circle cx="567.5" cy="227.5" r="29.5" fill="currentColor" />
<circle cx="666.5" cy="227.5" r="29.5" fill="currentColor" />
<circle cx="29.5" cy="326.5" r="29.5" fill="currentColor" />
<circle cx="128.5" cy="326.5" r="29.5" fill="currentColor" />
<circle cx="227.5" cy="326.5" r="29.5" fill="currentColor" />
<circle cx="326.5" cy="326.5" r="29.5" fill="currentColor" />
<circle cx="425.5" cy="326.5" r="29.5" fill="currentColor" />
<circle cx="524.5" cy="326.5" r="29.5" fill="currentColor" />
<circle cx="623.5" cy="326.5" r="29.5" fill="currentColor" />
<circle cx="72.5" cy="425.5" r="29.5" fill="currentColor" />
<circle cx="171.5" cy="425.5" r="29.5" fill="currentColor" />
<circle cx="270.5" cy="425.5" r="29.5" fill="currentColor" />
<circle cx="369.5" cy="425.5" r="29.5" fill="currentColor" />
<circle cx="468.5" cy="425.5" r="29.5" fill="currentColor" />
<circle cx="567.5" cy="425.5" r="29.5" fill="currentColor" />
<circle cx="666.5" cy="425.5" r="29.5" fill="currentColor" />
<circle cx="29.5" cy="524.5" r="29.5" fill="currentColor" />
<circle cx="128.5" cy="524.5" r="29.5" fill="currentColor" />
<circle cx="227.5" cy="524.5" r="29.5" fill="currentColor" />
<circle cx="326.5" cy="524.5" r="29.5" fill="currentColor" />
<circle cx="425.5" cy="524.5" r="29.5" fill="currentColor" />
<circle cx="524.5" cy="524.5" r="29.5" fill="currentColor" />
<circle cx="623.5" cy="524.5" r="29.5" fill="currentColor" />
<circle cx="72.5" cy="623.5" r="29.5" fill="currentColor" />
<circle cx="171.5" cy="623.5" r="29.5" fill="currentColor" />
<circle cx="270.5" cy="623.5" r="29.5" fill="currentColor" />
<circle cx="369.5" cy="623.5" r="29.5" fill="currentColor" />
<circle cx="468.5" cy="623.5" r="29.5" fill="currentColor" />
<circle cx="567.5" cy="623.5" r="29.5" fill="currentColor" />
<circle cx="666.5" cy="623.5" r="29.5" fill="currentColor" />
</svg>
</div>
<div className="overflow-hidden rounded-xl shadow-2xl">
<div className="absolute inset-0 z-0 rounded-xl bg-gradient-to-tr from-blue-500/10 to-cyan-500/10"></div>
<AnimatePresence mode="wait">
<motion.div
key={selectedFeature.id}
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 1.05 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
>
<Image
src={selectedFeature.thumbnailUrl}
alt={`${selectedFeature.title} video thumbnail`}
width={960}
height={540}
loading="lazy"
unoptimized
className="relative w-full transform rounded-xl transition-transform duration-300 hover:scale-[1.02]"
/>
</motion.div>
</AnimatePresence>
</div>
<AnimatePresence mode="wait">
<motion.div
key={selectedFeature.id}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="absolute inset-0 grid h-full w-full items-center justify-center"
>
<PlayButton
onClick={() => {
setIsOpen(true);
sendCustomEvent(
selectedFeature.eventId,
'ai-landing-hero-video',
'ai-landing'
);
}}
/>
</motion.div>
</AnimatePresence>
</div>
{selectedFeature.blogUrl && (
<AnimatePresence mode="wait">
<motion.div
key={selectedFeature.id}
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3, delay: 0.1 }}
className="mt-6 flex justify-center"
>
<a
href={selectedFeature.blogUrl}
className="group inline-flex items-center gap-2 text-center text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
onClick={() =>
sendCustomEvent(
`${selectedFeature.eventId}-learn-more`,
'ai-landing-hero-learn-more',
'ai-landing'
)
}
>
Learn more about {selectedFeature.title.toLowerCase()}
<motion.span
className="inline-block"
animate={{ x: 0 }}
whileHover={{ x: 4 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
>
</motion.span>
</a>
</motion.div>
</AnimatePresence>
)}
</motion.div>
{/* Features Section */}
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.5, duration: 0.6 }}
className="w-full flex-auto"
>
<div className="space-y-2">
{aiFeatures.map((feature, index) => {
const Icon = feature.icon;
const isSelected = selectedFeature.id === feature.id;
return (
<button
key={feature.id}
onClick={() => setSelectedFeature(feature)}
className={cx(
'group flex w-full gap-4 rounded-lg p-4 text-left transition-all duration-200',
{
'bg-blue-50 ring-2 ring-blue-500 dark:bg-blue-950/50 dark:ring-blue-400':
isSelected,
'hover:bg-slate-100 dark:hover:bg-slate-800':
!isSelected,
}
)}
>
<motion.div
animate={{
scale: isSelected ? 1.1 : 1,
rotate: isSelected ? 5 : 0,
}}
transition={{ duration: 0.2, ease: 'easeOut' }}
>
<Icon
aria-hidden="true"
className={cx(
'size-6 shrink-0 transition-colors',
isSelected
? 'text-blue-600 dark:text-blue-400'
: 'text-blue-500'
)}
/>
</motion.div>
<div className="min-w-0 flex-1">
<h4
className={cx(
'relative text-base font-medium leading-6 transition-colors',
isSelected
? 'text-blue-900 dark:text-blue-100'
: 'text-slate-900 group-hover:text-blue-600 dark:text-slate-100 dark:group-hover:text-blue-400'
)}
>
{feature.title}
<motion.span
className={cx(
'ml-1 transition-opacity',
isSelected
? 'opacity-100'
: 'opacity-0 group-hover:opacity-100'
)}
animate={{ x: isSelected ? 4 : 0 }}
transition={{ duration: 0.2 }}
>
</motion.span>
</h4>
<p
className={cx(
'mt-2 text-sm leading-6 transition-colors',
isSelected
? 'text-blue-700 dark:text-blue-200'
: 'text-slate-600 dark:text-slate-400'
)}
>
{feature.description}
</p>
</div>
</button>
);
})}
</div>
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8, duration: 0.5 }}
className="mt-8 flex justify-start"
>
<ButtonLink
href="/features/enhance-AI#setting-up-nx-mcp"
variant="primary"
size="default"
title="Enhance your AI assistant"
onClick={() =>
sendCustomEvent(
'ai-landing-enhance-click',
'ai-landing-hero',
'ai-landing'
)
}
>
Enhance your AI assistant
</ButtonLink>
</motion.div>
</motion.div>
</div>
</div>
<VideoModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
videoUrl={selectedFeature.videoUrl}
/>
</section>
);
}