150 lines
6.1 KiB
TypeScript
150 lines
6.1 KiB
TypeScript
"use client";
|
|
|
|
import { motion, useInView } from "framer-motion";
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { landingHowItWorksNarrative } from "../lib/product-narrative";
|
|
|
|
const demoMessages = landingHowItWorksNarrative.demoMessages;
|
|
|
|
const logTone: Record<(typeof demoMessages)[number]["role"], string> = {
|
|
system: "text-[var(--muted-strong)]",
|
|
};
|
|
|
|
const dotTone: Record<(typeof demoMessages)[number]["role"], string> = {
|
|
system: "bg-[var(--muted-strong)]",
|
|
};
|
|
|
|
const steps = landingHowItWorksNarrative.steps;
|
|
|
|
function DemoPanel() {
|
|
const anchorRef = useRef<HTMLDivElement | null>(null);
|
|
const isVisible = useInView(anchorRef, { once: true, amount: 0.4 });
|
|
const [visibleCount, setVisibleCount] = useState(0);
|
|
const logRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!isVisible) return;
|
|
|
|
const timers = demoMessages.map((message, index) =>
|
|
window.setTimeout(() => setVisibleCount(index + 1), message.delay),
|
|
);
|
|
|
|
return () => timers.forEach((timer) => window.clearTimeout(timer));
|
|
}, [isVisible]);
|
|
|
|
useEffect(() => {
|
|
if (logRef.current) {
|
|
logRef.current.scrollTop = logRef.current.scrollHeight;
|
|
}
|
|
}, [visibleCount]);
|
|
|
|
return (
|
|
<div ref={anchorRef} className="relative">
|
|
<div className="rounded-[22px] overflow-hidden border border-[var(--border)] bg-[var(--panel)] shadow-[0_8px_40px_-8px_rgba(0,0,0,0.06)]">
|
|
<div className="flex items-center gap-2 px-4 py-3 bg-[var(--panel-soft)] border-b border-[var(--border)]">
|
|
<span className="w-3 h-3 rounded-full bg-[#ff5f57]" aria-hidden="true" />
|
|
<span className="w-3 h-3 rounded-full bg-[#febc2e]" aria-hidden="true" />
|
|
<span className="w-3 h-3 rounded-full bg-[#28c840]" aria-hidden="true" />
|
|
<span className="ml-2 text-xs text-[var(--muted)] font-mono">AgentCompany</span>
|
|
</div>
|
|
|
|
<div ref={logRef} role="log" aria-label="Agent activity demonstration" className="p-5 lg:p-6 font-mono text-sm leading-relaxed h-[340px] overflow-y-auto">
|
|
{demoMessages.slice(0, visibleCount).map((message, index) => (
|
|
<motion.div
|
|
key={`${message.agent}-${index}`}
|
|
initial={{ opacity: 0, y: 8 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
transition={{ duration: 0.3, ease: [0.16, 1, 0.3, 1] }}
|
|
className="flex gap-3 mb-3 last:mb-0"
|
|
>
|
|
<span className={`shrink-0 font-semibold inline-flex items-center gap-1.5 ${logTone[message.role]}`}>
|
|
<span
|
|
className={`w-1.5 h-1.5 rounded-full ${dotTone[message.role]}`}
|
|
aria-hidden="true"
|
|
/>
|
|
{message.agent}
|
|
</span>
|
|
<span className="text-[var(--text)]">{message.text}</span>
|
|
</motion.div>
|
|
))}
|
|
|
|
{visibleCount < demoMessages.length && isVisible && (
|
|
<span className="inline-block w-2 h-4 bg-[var(--accent)] animate-pulse rounded-sm" aria-hidden="true" />
|
|
)}
|
|
|
|
{visibleCount >= demoMessages.length && (
|
|
<motion.div
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: 1 }}
|
|
transition={{ delay: 0.3 }}
|
|
className="mt-4 pt-4 border-t border-[var(--border)] flex items-center gap-2 text-[#059669]"
|
|
>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
<path d="m9 12 2 2 4-4" />
|
|
<circle cx="12" cy="12" r="10" />
|
|
</svg>
|
|
<span className="font-semibold">{landingHowItWorksNarrative.completionText}</span>
|
|
</motion.div>
|
|
)}
|
|
|
|
<div />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function HowItWorks() {
|
|
return (
|
|
<section id="how-it-works" className="py-24 lg:py-40 px-4 sm:px-6 lg:px-8">
|
|
<div className="mx-auto max-w-6xl">
|
|
<div className="text-center mb-20 lg:mb-24">
|
|
<p className="font-mono text-xs font-medium uppercase tracking-[0.25em] text-[var(--accent)] mb-5">
|
|
How It Works
|
|
</p>
|
|
<h2 className="font-serif text-[clamp(2rem,1.5rem+2.5vw,3.25rem)] tracking-[-0.01em] text-[var(--text)]">
|
|
{landingHowItWorksNarrative.heading}
|
|
</h2>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-5 gap-12 lg:gap-16 items-start">
|
|
<div className="lg:col-span-2 relative">
|
|
<div className="absolute left-[15px] top-10 bottom-4 w-px bg-[var(--border)] hidden lg:block" aria-hidden="true" />
|
|
|
|
<div className="space-y-7">
|
|
{steps.map((step, index) => (
|
|
<motion.div
|
|
key={step.number}
|
|
initial={{ opacity: 0, x: -24 }}
|
|
whileInView={{ opacity: 1, x: 0 }}
|
|
viewport={{ once: true, amount: 0.5 }}
|
|
transition={{ duration: 0.5, delay: 0.12 * index, ease: [0.16, 1, 0.3, 1] }}
|
|
className="flex gap-5"
|
|
>
|
|
<div className="relative z-10 shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-[var(--accent)] text-[var(--text-on-accent)] text-sm font-bold shadow-[0_2px_12px_-2px_var(--accent)]">
|
|
{step.number}
|
|
</div>
|
|
<div className="pt-0.5">
|
|
<h3 className="text-base font-bold text-[var(--text)] mb-1.5 tracking-tight">{step.title}</h3>
|
|
<p className="text-sm leading-relaxed text-[var(--muted)]">{step.description}</p>
|
|
</div>
|
|
</motion.div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<motion.div
|
|
className="lg:col-span-3"
|
|
initial={{ opacity: 0, y: 32 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true, amount: 0.3 }}
|
|
transition={{ duration: 0.7, ease: [0.16, 1, 0.3, 1] }}
|
|
>
|
|
<DemoPanel />
|
|
</motion.div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|