From 0513bf393fb0eedd2baecdadb54b6129976a706c Mon Sep 17 00:00:00 2001 From: decolua Date: Sun, 19 Jul 2026 13:19:13 +0700 Subject: [PATCH] Flow animatopn --- .../usage/components/ProviderTopology.js | 169 ++++++++++++++++-- src/app/globals.css | 62 +++++++ 2 files changed, 221 insertions(+), 10 deletions(-) diff --git a/src/app/(dashboard)/dashboard/usage/components/ProviderTopology.js b/src/app/(dashboard)/dashboard/usage/components/ProviderTopology.js index 57b217a3..7db34791 100644 --- a/src/app/(dashboard)/dashboard/usage/components/ProviderTopology.js +++ b/src/app/(dashboard)/dashboard/usage/components/ProviderTopology.js @@ -7,6 +7,8 @@ import { Handle, Position, Controls, + BaseEdge, + getBezierPath, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; import { AI_PROVIDERS } from "@/shared/constants/providers"; @@ -16,6 +18,10 @@ import { getProviderIconSrc, markProviderIconMissing } from "@/shared/utils/prov const FE_ACTIVE_TIMEOUT_MS = 60000; const FE_ACTIVE_TICK_MS = 1000; +// Kame + electric particles along active edges +const KAME_PARTICLE_COUNT = 6; +const SPARK_COUNT = 5; + function getProviderConfig(providerId) { return AI_PROVIDERS[providerId] || { color: "#6b7280", name: providerId }; } @@ -88,19 +94,34 @@ ProviderNode.propTypes = { data: PropTypes.object.isRequired, }; -// Center 9Router node +// Center 9Router node — pulse/glow on card only (no expanding rings) function RouterNode({ data }) { + const powering = (data.activeCount || 0) > 0; return ( -
+
- 9Router - 9Router + 9Router + + 9Router + {data.activeCount > 0 && ( - + {data.activeCount} )} @@ -112,7 +133,131 @@ RouterNode.propTypes = { data: PropTypes.object.isRequired, }; +// Active: electric kame beam (multi-layer stroke + sparks). Idle/last/error: solid BaseEdge. +function TopologyEdge({ + id, + sourceX, + sourceY, + targetX, + targetY, + sourcePosition, + targetPosition, + style = {}, + data, +}) { + const [edgePath] = getBezierPath({ + sourceX, + sourceY, + sourcePosition, + targetX, + targetY, + targetPosition, + }); + const active = !!data?.active; + const stroke = style.stroke || "var(--color-border)"; + const filterId = `topo-electric-${id}`; + + if (!active) { + return ; + } + + return ( + + + + + + + + + + {/* Outer electric halo */} + + {/* Mid plasma */} + + {/* Hot white core */} + + {/* Energy orbs */} + {Array.from({ length: KAME_PARTICLE_COUNT }, (_, i) => ( + + + + ))} + {/* Electric sparks (short-lived blink along path) */} + {Array.from({ length: SPARK_COUNT }, (_, i) => ( + + + + + ))} + + ); +} + +TopologyEdge.propTypes = { + id: PropTypes.string, + sourceX: PropTypes.number, + sourceY: PropTypes.number, + targetX: PropTypes.number, + targetY: PropTypes.number, + sourcePosition: PropTypes.string, + targetPosition: PropTypes.string, + style: PropTypes.object, + data: PropTypes.object, +}; + const nodeTypes = { provider: ProviderNode, router: RouterNode }; +const edgeTypes = { topology: TopologyEdge }; // Place N nodes evenly along an ellipse around the router center. function buildLayout(providers, activeSet, lastSet, errorSet) { @@ -146,9 +291,9 @@ function buildLayout(providers, activeSet, lastSet, errorSet) { draggable: false, }); - const edgeStyle = (active, last, error, color) => { + const edgeStyle = (active, last, error) => { if (error) return { stroke: "#ef4444", strokeWidth: 2.5, opacity: 0.9 }; - if (active) return { stroke: "#22c55e", strokeWidth: 2.5, opacity: 0.9 }; + if (active) return { stroke: "#22d3ee", strokeWidth: 3.5, opacity: 1 }; if (last) return { stroke: "#f59e0b", strokeWidth: 2, opacity: 0.7 }; return { stroke: "var(--color-border)", strokeWidth: 1, opacity: 0.3 }; }; @@ -194,12 +339,15 @@ function buildLayout(providers, activeSet, lastSet, errorSet) { edges.push({ id: `e-${nodeId}`, + type: "topology", source: "router", sourceHandle, target: nodeId, targetHandle, - animated: active, - style: edgeStyle(active, last, error, config.color), + // Built-in animated uses stroke-dasharray (CPU-heavy); use particle beam instead + animated: false, + data: { active }, + style: edgeStyle(active, last, error), }); }); @@ -252,7 +400,7 @@ export default function ProviderTopology({ providers = [], activeRequests = [], const { nodes, edges } = useMemo( () => buildLayout(providers, activeSet, lastSet, errorSet), - [providers, activeSet, lastKey, errorKey] + [providers, activeSet, lastSet, errorSet] ); // Stable key — only remount when provider list changes @@ -300,6 +448,7 @@ export default function ProviderTopology({ providers = [], activeRequests = [], nodes={nodes} edges={edges} nodeTypes={nodeTypes} + edgeTypes={edgeTypes} fitView fitViewOptions={fitOpts} minZoom={0.1} diff --git a/src/app/globals.css b/src/app/globals.css index fdc219ba..5fd9d0b5 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -442,6 +442,68 @@ button:disabled, opacity: 0.04; } +/* Topology: router card pulse (no flying rings) + electric edge beam */ +@keyframes topology-router-pulse { + 0%, 100% { + transform: scale(1); + box-shadow: + 0 0 8px #fde047, + 0 0 20px color-mix(in srgb, var(--color-primary) 65%, transparent), + 0 0 32px rgba(34, 211, 238, 0.4), + inset 0 0 10px rgba(253, 224, 71, 0.3); + } + 50% { + transform: scale(1.06); + box-shadow: + 0 0 14px #fef08a, + 0 0 28px #facc15, + 0 0 44px rgba(34, 211, 238, 0.6), + inset 0 0 14px rgba(255, 255, 255, 0.25); + } +} +@keyframes topology-router-icon-shake { + 0%, 100% { transform: rotate(0deg) scale(1); filter: drop-shadow(0 0 2px #fde047); } + 25% { transform: rotate(-5deg) scale(1.08); filter: drop-shadow(0 0 6px #facc15); } + 75% { transform: rotate(5deg) scale(1.08); filter: drop-shadow(0 0 6px #22d3ee); } +} +@keyframes topology-router-label-flicker { + 0%, 100% { text-shadow: 0 0 6px #fde047, 0 0 12px #22d3ee; } + 50% { text-shadow: 0 0 10px #fff, 0 0 18px #facc15; } +} +@keyframes topology-edge-dash { + to { stroke-dashoffset: -36; } +} +@keyframes topology-edge-flicker { + 0%, 100% { opacity: 0.85; } + 40% { opacity: 1; } + 55% { opacity: 0.55; } + 70% { opacity: 1; } +} +.topology-router-core { + animation: topology-router-pulse 0.75s ease-in-out infinite; + border-color: #fde047 !important; +} +.topology-router-icon { + animation: topology-router-icon-shake 0.45s ease-in-out infinite; +} +.topology-router-label { + animation: topology-router-label-flicker 0.7s ease-in-out infinite; +} +.topology-router-badge { + box-shadow: 0 0 10px #fde047, 0 0 16px rgba(34, 211, 238, 0.6); +} +.topology-edge-kame { + stroke-dasharray: 8 6; + animation: topology-edge-dash 0.22s linear infinite; +} +.topology-edge-halo { + animation: topology-edge-flicker 0.18s steps(2) infinite; +} +.topology-edge-plasma { + stroke-dasharray: 14 10; + animation: topology-edge-dash 0.18s linear infinite, topology-edge-flicker 0.22s steps(3) infinite; +} + /* React Flow controls: match app theme */ .react-flow-controls-custom { background: var(--color-surface);