From 9562c661083a27c89932d9ffa8419cc568bb76f9 Mon Sep 17 00:00:00 2001 From: TianYun Date: Tue, 9 Jun 2026 13:38:34 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BA=BF=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../__tests__/EditableEdge.test.tsx | 44 ++++++++++++++++++- src/components/edges/EditableEdge.tsx | 17 ++++--- src/components/nodes/GroupNode.tsx | 2 +- 3 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/components/__tests__/EditableEdge.test.tsx b/src/components/__tests__/EditableEdge.test.tsx index 8c12a45e..3aef6e6d 100644 --- a/src/components/__tests__/EditableEdge.test.tsx +++ b/src/components/__tests__/EditableEdge.test.tsx @@ -58,6 +58,8 @@ const createDefaultProps = (overrides = {}) => ({ const createDefaultState = (overrides = {}) => ({ edgeStyle: "angular" as const, nodes: [], + runningNodeIds: new Set(), + currentNodeIds: [] as string[], ...overrides, }); @@ -287,6 +289,23 @@ describe("EditableEdge", () => { expect(stroke).toContain("-active"); }); + it("should show pulse animation when connected to selected node", () => { + mockUseWorkflowStore.mockImplementation((selector) => { + return selector(createDefaultState({ + nodes: [{ id: "node-1", selected: true }], + })); + }); + + const { container } = render( + + + + ); + + const paths = container.querySelectorAll("path"); + expect(paths.length).toBeGreaterThan(2); + }); + it("should have dimmed opacity when not connected to selected node", () => { mockUseWorkflowStore.mockImplementation((selector) => { return selector(createDefaultState({ @@ -308,12 +327,33 @@ describe("EditableEdge", () => { }); describe("Loading Animation", () => { - it("should show pulse animation when target node is loading", () => { + it("should show pulse animation when target node is running", () => { + mockUseWorkflowStore.mockImplementation((selector) => { + return selector(createDefaultState({ + nodes: [ + { id: "node-1", type: "prompt", selected: false }, + { id: "node-2", type: "generateVideo", selected: false, data: { status: "idle" } }, + ], + runningNodeIds: new Set(["node-2"]), + })); + }); + + const { container } = render( + + + + ); + + const paths = container.querySelectorAll("path"); + expect(paths.length).toBeGreaterThan(2); + }); + + it("should show pulse animation when target node data is loading", () => { mockUseWorkflowStore.mockImplementation((selector) => { return selector(createDefaultState({ nodes: [ { id: "node-1", type: "prompt", selected: false }, - { id: "node-2", type: "nanoBanana", selected: false, data: { status: "loading" } }, + { id: "node-2", type: "generateAudio", selected: false, data: { status: "loading" } }, ], })); }); diff --git a/src/components/edges/EditableEdge.tsx b/src/components/edges/EditableEdge.tsx index 246c84ab..836eace1 100644 --- a/src/components/edges/EditableEdge.tsx +++ b/src/components/edges/EditableEdge.tsx @@ -9,7 +9,7 @@ import { useReactFlow, } from "@xyflow/react"; import { useWorkflowStore } from "@/store/workflowStore"; -import { NanoBananaNodeData, WorkflowEdgeData } from "@/types"; +import { WorkflowEdgeData } from "@/types"; import { getSharedGradientId } from "./SharedEdgeGradients"; interface EdgeData extends WorkflowEdgeData { @@ -65,12 +65,15 @@ export function EditableEdge({ const offsetY = edgeData?.offsetY ?? 0; const hasPause = edgeData?.hasPause ?? false; - // Narrow selector: only re-renders when target loading status changes - const isTargetLoading = useWorkflowStore((state) => { + // Animate incoming edges for the node-level task state, with node data status as a fallback. + const isTargetRunning = useWorkflowStore((state) => { + if (state.runningNodeIds.has(target) || state.currentNodeIds.includes(target)) { + return true; + } const targetNode = state.nodes.find((n) => n.id === target); - if (targetNode?.type !== "nanoBanana") return false; - return (targetNode.data as NanoBananaNodeData).status === "loading"; + return (targetNode?.data as { status?: unknown } | undefined)?.status === "loading"; }); + const shouldAnimateEdge = isConnectedToSelection || isTargetRunning; // Determine edge color based on handle type (magenta for loop edges, orange if paused) const edgeColor = useMemo(() => { @@ -233,8 +236,8 @@ export function EditableEdge({ }} /> - {/* Animated pulse overlay when target is loading */} - {isTargetLoading && ( + {/* Animated pulse overlay for selected-node context and running targets */} + {shouldAnimateEdge && ( <> {/* Outer glow — replaces blur(6px) filter for better perf on Windows */} ) { type="button" className={`nodrag nopan flex h-8 w-[96px] shrink-0 items-center justify-center gap-1 rounded px-2 text-[11px] font-medium transition-colors focus:outline-none focus:ring-1 ${isGroupRunning ? "text-red-300 hover:bg-red-500/10 hover:text-red-200 focus:ring-red-500/60" - : "text-emerald-300 hover:bg-emerald-500/10 hover:text-emerald-200 focus:ring-emerald-500/60" + : "text-neutral-100 hover:bg-neutral-700 hover:text-white focus:ring-neutral-500" }`} onClick={handleRunGroup} title={t("group.runWholeGroup")}