File size: 6,165 Bytes
88c4c60 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | "use client";
import { useEffect, useState } from "react";
import ProviderIcon from "@/shared/components/ProviderIcon";
const CLI_TOOLS = [
{ id: "claude", name: "Claude Code", image: "/providers/claude.png" },
{ id: "codex", name: "OpenAI Codex", image: "/providers/codex.png" },
{ id: "cline", name: "Cline", image: "/providers/cline.png" },
{ id: "cursor", name: "Cursor", image: "/providers/cursor.png" },
];
const PROVIDERS = [
{
id: "openai",
name: "OpenAI",
color: "bg-emerald-500",
textColor: "text-white",
},
{
id: "anthropic",
name: "Anthropic",
color: "bg-orange-400",
textColor: "text-white",
},
{
id: "gemini",
name: "Gemini",
color: "bg-blue-500",
textColor: "text-white",
},
{
id: "github",
name: "GitHub Copilot",
color: "bg-gray-700",
textColor: "text-white",
},
];
export default function FlowAnimation() {
const [activeFlow, setActiveFlow] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setActiveFlow((prev) => (prev + 1) % PROVIDERS.length);
}, 2000);
return () => clearInterval(interval);
}, []);
return (
<div className="mt-16 w-full max-w-4xl relative h-[360px] hidden md:flex items-center justify-center animate-[float_6s_ease-in-out_infinite]">
{/* 9Router Hub - Center */}
<div className="relative z-20 w-32 h-32 rounded-full bg-[#23180f] border-2 border-[#f97815] shadow-[0_0_40px_rgba(249,120,21,0.3)] flex flex-col items-center justify-center gap-1 group cursor-pointer hover:scale-105 transition-transform duration-500">
<span className="material-symbols-outlined text-4xl text-[#f97815]">
hub
</span>
<span className="text-xs font-bold text-white tracking-widest uppercase">
9Router
</span>
<div className="absolute inset-0 rounded-full border border-[#f97815]/30 animate-ping opacity-20"></div>
</div>
{/* CLI Tools - Left side */}
<div className="absolute left-0 top-1/2 -translate-y-1/2 flex flex-col gap-7">
{CLI_TOOLS.map((tool) => (
<div
key={tool.id}
className="flex items-center gap-3 opacity-70 hover:opacity-100 transition-opacity group"
>
<div className="w-16 h-16 rounded-2xl bg-[#23180f] border border-[#3a2f27] flex items-center justify-center overflow-hidden p-2 hover:border-[#f97815]/50 transition-all hover:scale-105">
<ProviderIcon
src={tool.image}
alt={tool.name}
size={48}
className="object-contain rounded-xl max-w-[48px] max-h-[48px]"
fallbackText={tool.name.slice(0, 2).toUpperCase()}
/>
</div>
</div>
))}
</div>
{/* SVG Lines from CLI to 9Router */}
<svg
className="absolute inset-0 w-full h-full z-10 pointer-events-none stroke-yellow-700"
xmlns="http://www.w3.org/2000/svg"
>
<path
className="animate-[dash_2s_linear_infinite]"
d="M 60 50 C 250 70, 250 180, 360 180"
fill="none"
strokeDasharray="5,5"
strokeWidth="2"
></path>
<path
className="animate-[dash_2s_linear_infinite]"
d="M 60 140 C 250 140, 250 180, 360 180"
fill="none"
strokeDasharray="5,5"
strokeWidth="2"
></path>
<path
className="animate-[dash_2s_linear_infinite]"
d="M 60 210 C 250 210, 250 180, 360 180"
fill="none"
strokeDasharray="5,5"
strokeWidth="2"
></path>
<path
className="animate-[dash_2s_linear_infinite]"
d="M 60 300 C 250 280, 250 180, 360 180"
fill="none"
strokeDasharray="5,5"
strokeWidth="2"
></path>
</svg>
{/* SVG Lines from 9Router to Providers */}
<svg
className="absolute inset-0 w-full h-full z-10 pointer-events-none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M 440 180 C 550 180, 550 50, 740 50"
fill="none"
stroke={activeFlow === 0 ? "#f97815" : "rgb(75, 85, 99)"}
strokeWidth={activeFlow === 0 ? "3" : "2"}
className={activeFlow === 0 ? "animate-pulse" : ""}
></path>
<path
d="M 440 180 C 550 180, 550 130, 740 130"
fill="none"
stroke={activeFlow === 1 ? "#f97815" : "rgb(75, 85, 99)"}
strokeWidth={activeFlow === 1 ? "3" : "2"}
className={activeFlow === 1 ? "animate-pulse" : ""}
></path>
<path
d="M 440 180 C 550 180, 550 230, 740 230"
fill="none"
stroke={activeFlow === 2 ? "#f97815" : "rgb(75, 85, 99)"}
strokeWidth={activeFlow === 2 ? "3" : "2"}
className={activeFlow === 2 ? "animate-pulse" : ""}
></path>
<path
d="M 440 180 C 550 180, 550 310, 740 310"
fill="none"
stroke={activeFlow === 3 ? "#f97815" : "rgb(75, 85, 99)"}
strokeWidth={activeFlow === 3 ? "3" : "2"}
className={activeFlow === 3 ? "animate-pulse" : ""}
></path>
</svg>
{/* AI Providers - Right side */}
<div className="absolute right-0 top-0 bottom-0 flex flex-col justify-between py-6">
{PROVIDERS.map((provider, idx) => (
<div
key={provider.id}
className={`px-4 py-2 rounded-lg ${provider.color} ${provider.textColor} flex items-center justify-center font-bold text-xs shadow-lg hover:scale-110 transition-all cursor-help min-w-[140px] ${
activeFlow === idx ? "ring-4 ring-[#f97815]/50 scale-110" : ""
}`}
title={provider.name}
>
{provider.name}
</div>
))}
</div>
{/* Mobile fallback */}
<div className="md:hidden mt-8 w-full p-4 rounded-lg bg-[#23180f] border border-[#3a2f27]">
<p className="text-sm text-center text-gray-400">
Interactive diagram visible on desktop
</p>
</div>
</div>
);
}
|