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
+
{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);