190 lines
7.1 KiB
TypeScript
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>
|
|
);
|
|
}
|