195 lines
6.9 KiB
TypeScript
195 lines
6.9 KiB
TypeScript
'use client';
|
|
|
|
import { motion } from 'framer-motion';
|
|
import { useLocale } from '@/lib/i18n/locale-context';
|
|
import { SectionHeader } from '@/components/ui/SectionHeader';
|
|
|
|
/**
|
|
* Animated RAG pipeline: ingest → embed → retrieve → rerank → generate.
|
|
*
|
|
* The diagram itself is always laid out left-to-right (dir="ltr") regardless of
|
|
* page locale — a data pipeline reads forward in both languages — while the
|
|
* labels/descriptions come from the localized dictionary. The flowing dashes
|
|
* are pure SVG (animated stroke-dashoffset), so there is no per-frame JS.
|
|
*/
|
|
|
|
type Accent = 'electric' | 'violet' | 'cyan' | 'magenta' | 'emerald';
|
|
|
|
const ACCENT_HEX: Record<Accent, string> = {
|
|
electric: '#38bdf8',
|
|
violet: '#818cf8',
|
|
cyan: '#22d3ee',
|
|
magenta: '#e879f9',
|
|
emerald: '#34d399',
|
|
};
|
|
|
|
// Literal class maps so Tailwind's JIT scanner can see every variant.
|
|
const ACCENT_TEXT: Record<Accent, string> = {
|
|
electric: 'text-electric',
|
|
violet: 'text-violet',
|
|
cyan: 'text-cyan',
|
|
magenta: 'text-magenta',
|
|
emerald: 'text-emerald',
|
|
};
|
|
const ACCENT_BORDER: Record<Accent, string> = {
|
|
electric: 'border-electric/40',
|
|
violet: 'border-violet/40',
|
|
cyan: 'border-cyan/40',
|
|
magenta: 'border-magenta/40',
|
|
emerald: 'border-emerald/40',
|
|
};
|
|
const ACCENT_HOVER_SHADOW: Record<Accent, string> = {
|
|
electric: 'hover:shadow-[0_0_30px_-12px_#38bdf8]',
|
|
violet: 'hover:shadow-[0_0_30px_-12px_#818cf8]',
|
|
cyan: 'hover:shadow-[0_0_30px_-12px_#22d3ee]',
|
|
magenta: 'hover:shadow-[0_0_30px_-12px_#e879f9]',
|
|
emerald: 'hover:shadow-[0_0_30px_-12px_#34d399]',
|
|
};
|
|
|
|
function asAccent(value: string | undefined): Accent {
|
|
return value === 'violet' ||
|
|
value === 'cyan' ||
|
|
value === 'magenta' ||
|
|
value === 'emerald' ||
|
|
value === 'electric'
|
|
? value
|
|
: 'electric';
|
|
}
|
|
|
|
export function DataFlow() {
|
|
const { t } = useLocale();
|
|
const data = t.dataflow;
|
|
const nodes = data.nodes;
|
|
|
|
return (
|
|
<section id="dataflow" className="relative px-5 py-28 sm:px-8">
|
|
<div className="mx-auto max-w-7xl">
|
|
<SectionHeader eyebrow={data.eyebrow} title={data.title} sub={data.sub} />
|
|
|
|
{/* Diagram canvas — fixed LTR reading order. */}
|
|
<div dir="ltr" className="relative mt-14">
|
|
{/* SVG connectors sit behind the cards on md+ (horizontal flow). */}
|
|
<svg
|
|
aria-hidden
|
|
viewBox="0 0 1000 120"
|
|
preserveAspectRatio="none"
|
|
className="pointer-events-none absolute inset-x-0 top-1/2 hidden h-28 -translate-y-1/2 md:block"
|
|
>
|
|
<defs>
|
|
<linearGradient id="flow-line" x1="0" y1="0" x2="1" y2="0">
|
|
<stop offset="0%" stopColor="#38bdf8" />
|
|
<stop offset="25%" stopColor="#818cf8" />
|
|
<stop offset="50%" stopColor="#22d3ee" />
|
|
<stop offset="75%" stopColor="#e879f9" />
|
|
<stop offset="100%" stopColor="#34d399" />
|
|
</linearGradient>
|
|
</defs>
|
|
{/* Static base rail */}
|
|
<line
|
|
x1="40"
|
|
y1="60"
|
|
x2="960"
|
|
y2="60"
|
|
stroke="url(#flow-line)"
|
|
strokeWidth="1.5"
|
|
strokeOpacity="0.28"
|
|
/>
|
|
{/* Animated travelling packets */}
|
|
<line
|
|
x1="40"
|
|
y1="60"
|
|
x2="960"
|
|
y2="60"
|
|
stroke="url(#flow-line)"
|
|
strokeWidth="2.5"
|
|
strokeLinecap="round"
|
|
strokeDasharray="6 60"
|
|
className="animate-flow-dash"
|
|
/>
|
|
</svg>
|
|
|
|
<ol className="relative grid grid-cols-1 gap-5 sm:grid-cols-2 md:grid-cols-5 md:gap-3">
|
|
{nodes.map((node, i) => {
|
|
const accent = asAccent(node.accent);
|
|
return (
|
|
<motion.li
|
|
key={node.id}
|
|
initial={{ opacity: 0, y: 22 }}
|
|
whileInView={{ opacity: 1, y: 0 }}
|
|
viewport={{ once: true, margin: '-60px' }}
|
|
transition={{
|
|
duration: 0.5,
|
|
ease: [0.22, 1, 0.36, 1],
|
|
delay: 0.08 * i,
|
|
}}
|
|
className="relative"
|
|
>
|
|
<div
|
|
className={`glass group relative flex h-full flex-col gap-3 rounded-2xl border ${ACCENT_BORDER[accent]} bg-white/[0.02] p-5 transition-shadow duration-500 ${ACCENT_HOVER_SHADOW[accent]}`}
|
|
>
|
|
{/* Step index + pulsing node dot */}
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-mono text-[0.7rem] text-slate-500">
|
|
{String(i + 1).padStart(2, '0')}
|
|
</span>
|
|
<span className="relative flex h-2.5 w-2.5">
|
|
<span
|
|
className="absolute inline-flex h-full w-full animate-ping rounded-full opacity-60"
|
|
style={{ backgroundColor: ACCENT_HEX[accent] }}
|
|
/>
|
|
<span
|
|
className="relative inline-flex h-2.5 w-2.5 rounded-full"
|
|
style={{ backgroundColor: ACCENT_HEX[accent] }}
|
|
/>
|
|
</span>
|
|
</div>
|
|
|
|
<h3
|
|
className={`font-display text-lg font-semibold ${ACCENT_TEXT[accent]}`}
|
|
>
|
|
{node.label}
|
|
</h3>
|
|
<p className="text-sm leading-relaxed text-slate-400">
|
|
{node.desc}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Arrow connector for stacked (mobile / sm) layouts */}
|
|
{i < nodes.length - 1 && (
|
|
<span
|
|
aria-hidden
|
|
className="absolute left-1/2 top-full z-10 -translate-x-1/2 text-slate-600 sm:hidden"
|
|
>
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
|
|
<path
|
|
d="M12 4v16m0 0l6-6m-6 6l-6-6"
|
|
stroke="currentColor"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</svg>
|
|
</span>
|
|
)}
|
|
</motion.li>
|
|
);
|
|
})}
|
|
</ol>
|
|
|
|
{data.caption && (
|
|
<motion.p
|
|
initial={{ opacity: 0 }}
|
|
whileInView={{ opacity: 1 }}
|
|
viewport={{ once: true }}
|
|
transition={{ duration: 0.6, delay: 0.4 }}
|
|
className="mt-10 text-center font-mono text-[0.72rem] uppercase tracking-[0.18em] text-slate-500"
|
|
>
|
|
{data.caption}
|
|
</motion.p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|