Files
nearle_web_nextjs/src/components/why-nearle/ProductLLMPipeline.tsx
2026-07-19 23:07:18 +05:30

190 lines
7.1 KiB
TypeScript

"use client";
import { motion, useReducedMotion } from "framer-motion";
import { MaterialIcon } from "@/components/ui/MaterialIcon";
import { fadeUp, staggerContainer, viewport } from "@/lib/animations";
const EASE = [0.22, 1, 0.36, 1] as [number, number, number, number];
const STAGES = [
{ icon: "photo_camera", label: "Shelf Scan" },
{ icon: "barcode_scanner", label: "Barcode Detection" },
{ icon: "center_focus_strong", label: "Recognition" },
{ icon: "category", label: "Category" },
{ icon: "nutrition", label: "Nutrition" },
{ icon: "science", label: "Ingredients" },
{ icon: "sell", label: "Pricing" },
{ icon: "inventory_2", label: "Inventory" },
{ icon: "check_circle", label: "Ready", done: true },
];
const nodeVariant = {
hidden: { opacity: 0, scale: 0.7, y: 12 },
visible: { opacity: 1, scale: 1, y: 0, transition: { duration: 0.45, ease: EASE } },
};
function StageNode({
stage,
index,
reduce,
size = "md",
}: {
stage: (typeof STAGES)[number];
index: number;
reduce: boolean | null;
size?: "md" | "sm";
}) {
const dim = size === "md" ? "h-[68px] w-[68px]" : "h-14 w-14";
return (
<motion.div variants={nodeVariant} className="relative">
{/* light-up glow */}
<div
aria-hidden="true"
className={`pointer-events-none absolute inset-0 rounded-2xl blur-xl ${
stage.done ? "opacity-90" : "opacity-50"
}`}
style={{
background: stage.done
? "radial-gradient(circle, rgba(34,197,94,0.5), rgba(34,197,94,0) 70%)"
: "radial-gradient(circle, rgba(124,58,237,0.35), rgba(124,58,237,0) 70%)",
}}
/>
{stage.done && !reduce && (
<motion.span
aria-hidden="true"
className="absolute inset-0 rounded-2xl border-2 border-[#22C55E]"
animate={{ scale: [1, 1.25], opacity: [0.6, 0] }}
transition={{ repeat: Infinity, duration: 1.9, ease: "easeOut" }}
/>
)}
<div
className={`relative flex ${dim} items-center justify-center rounded-2xl border ${
stage.done
? "border-transparent bg-gradient-to-br from-[#22C55E] to-[#16A34A] text-white shadow-[0_14px_34px_rgba(34,197,94,0.4)]"
: "border-[#6330D6]/12 bg-white text-[#6330D6] shadow-[0_10px_26px_rgba(99,48,214,0.12)]"
}`}
>
<MaterialIcon icon={stage.icon} size={size === "md" ? 30 : 26} />
<span
className={`absolute -top-2 -right-2 flex h-6 w-6 items-center justify-center rounded-full text-[11px] font-black ${
stage.done ? "bg-white text-[#16A34A]" : "bg-[#6330D6] text-white"
} shadow-md`}
>
{index + 1}
</span>
</div>
</motion.div>
);
}
export function ProductLLMPipeline() {
const reduce = useReducedMotion();
return (
<section className="section bg-white w-full border-b border-[#6330D6]/10 overflow-hidden">
<div className="page-container">
<motion.div
variants={staggerContainer}
initial="hidden"
whileInView="visible"
viewport={viewport}
className="text-center max-w-[720px] mx-auto"
>
<motion.span
variants={fadeUp}
className="inline-flex items-center rounded-full bg-[#6330D6]/[0.08] px-3.5 py-1 text-xs font-black uppercase tracking-widest text-[#6330D6]"
>
ProductLLM
</motion.span>
<motion.h2
variants={fadeUp}
className="font-display font-black text-[#16001D] mt-4 tracking-tight"
style={{ fontSize: "clamp(30px, 3vw, 48px)", lineHeight: 1.1 }}
>
From a shelf photo to a live catalog
</motion.h2>
<motion.p
variants={fadeUp}
className="text-[#5E5866] mt-4"
style={{ fontSize: 18, lineHeight: 1.65 }}
>
One scan runs the full pipeline no manual data entry, no
spreadsheets.
</motion.p>
</motion.div>
{/* ── Desktop: horizontal processing rail ── */}
<motion.div
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.12 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
className="relative mt-16 hidden lg:block"
>
{/* base rail */}
<div className="absolute left-[5%] right-[5%] top-[34px] h-1 rounded-full bg-[#6330D6]/10" />
{/* filling gradient */}
<motion.div
className="absolute left-[5%] top-[34px] h-1 origin-left rounded-full bg-gradient-to-r from-[#7C3AED] via-[#6330D6] to-[#22C55E]"
style={{ width: "90%" }}
variants={{
hidden: { scaleX: 0 },
visible: { scaleX: 1, transition: { duration: 1.8, ease: EASE } },
}}
/>
{/* travelling data pulse */}
{!reduce && (
<motion.span
aria-hidden="true"
className="absolute top-[30px] left-[5%] h-2.5 w-2.5 rounded-full bg-white shadow-[0_0_12px_3px_rgba(124,58,237,0.7)]"
animate={{ left: ["5%", "95%"], opacity: [0, 1, 1, 0] }}
transition={{ repeat: Infinity, duration: 3, ease: "easeInOut" }}
/>
)}
<div className="relative grid grid-cols-9">
{STAGES.map((stage, i) => (
<div key={stage.label} className="flex flex-col items-center px-1 text-center">
<StageNode stage={stage} index={i} reduce={reduce} />
<p className="mt-4 text-[13px] font-bold leading-tight text-[#16001D]">
{stage.label}
</p>
</div>
))}
</div>
</motion.div>
{/* ── Mobile / tablet: vertical pipeline ── */}
<motion.div
variants={{ hidden: {}, visible: { transition: { staggerChildren: 0.1 } } }}
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.1 }}
className="relative mx-auto mt-12 max-w-md lg:hidden"
>
{/* vertical rail */}
<div className="absolute left-7 top-8 bottom-8 w-1 -translate-x-1/2 rounded-full bg-[#6330D6]/10" />
<motion.div
className="absolute left-7 top-8 w-1 -translate-x-1/2 origin-top rounded-full bg-gradient-to-b from-[#7C3AED] via-[#6330D6] to-[#22C55E]"
style={{ bottom: "2rem" }}
variants={{
hidden: { scaleY: 0 },
visible: { scaleY: 1, transition: { duration: 1.6, ease: EASE } },
}}
/>
<div className="flex flex-col gap-4">
{STAGES.map((stage, i) => (
<div key={stage.label} className="flex items-center gap-5">
<StageNode stage={stage} index={i} reduce={reduce} size="sm" />
<p className="font-display font-black text-[#16001D] text-base leading-tight">
{stage.label}
</p>
</div>
))}
</div>
</motion.div>
</div>
</section>
);
}