From a2fe2a310e49c758d8e5e6cdb6be1c039b152abd Mon Sep 17 00:00:00 2001 From: shrimbly Date: Thu, 5 Feb 2026 22:06:44 +1300 Subject: [PATCH] fix: stabilize VideoStitchNode performance and add WaveSpeed provider badge - Stabilize thumbnail extraction deps with a string key derived from clip edge IDs, preventing excessive re-runs on unrelated node changes - Use ref-based cache for thumbnails and add cleanup abort for pending extractions - Fix clipOrder sync triggering extra render by tracking last written order - Add WaveSpeed case to ProviderBadge in GenerateImageNode and GenerateVideoNode Co-Authored-By: Claude Opus 4.5 --- src/components/nodes/GenerateImageNode.tsx | 6 +++- src/components/nodes/GenerateVideoNode.tsx | 6 +++- src/components/nodes/VideoStitchNode.tsx | 40 +++++++++++++++++++--- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/components/nodes/GenerateImageNode.tsx b/src/components/nodes/GenerateImageNode.tsx index ca891b59..db67ab33 100644 --- a/src/components/nodes/GenerateImageNode.tsx +++ b/src/components/nodes/GenerateImageNode.tsx @@ -15,7 +15,7 @@ import { getImageDimensions, calculateNodeSizePreservingHeight } from "@/utils/n // Provider badge component - shows provider icon for all providers function ProviderBadge({ provider }: { provider: ProviderType }) { - const providerName = provider === "gemini" ? "Gemini" : provider === "replicate" ? "Replicate" : provider === "kie" ? "Kie.ai" : "fal.ai"; + const providerName = provider === "gemini" ? "Gemini" : provider === "replicate" ? "Replicate" : provider === "kie" ? "Kie.ai" : provider === "wavespeed" ? "WaveSpeed" : "fal.ai"; return ( @@ -33,6 +33,10 @@ function ProviderBadge({ provider }: { provider: ProviderType }) { + ) : provider === "wavespeed" ? ( + + + ) : ( diff --git a/src/components/nodes/GenerateVideoNode.tsx b/src/components/nodes/GenerateVideoNode.tsx index 1bff8ba9..29bf7271 100644 --- a/src/components/nodes/GenerateVideoNode.tsx +++ b/src/components/nodes/GenerateVideoNode.tsx @@ -15,7 +15,7 @@ import { getVideoDimensions, calculateNodeSizePreservingHeight } from "@/utils/n // Provider badge component - shows provider icon for all providers function ProviderBadge({ provider }: { provider: ProviderType }) { - const providerName = provider === "gemini" ? "Gemini" : provider === "replicate" ? "Replicate" : provider === "kie" ? "Kie.ai" : "fal.ai"; + const providerName = provider === "gemini" ? "Gemini" : provider === "replicate" ? "Replicate" : provider === "kie" ? "Kie.ai" : provider === "wavespeed" ? "WaveSpeed" : "fal.ai"; return ( @@ -33,6 +33,10 @@ function ProviderBadge({ provider }: { provider: ProviderType }) { + ) : provider === "wavespeed" ? ( + + + ) : ( diff --git a/src/components/nodes/VideoStitchNode.tsx b/src/components/nodes/VideoStitchNode.tsx index b7747f2b..2f3ef73d 100644 --- a/src/components/nodes/VideoStitchNode.tsx +++ b/src/components/nodes/VideoStitchNode.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Handle, Position, NodeProps, Node } from "@xyflow/react"; import { BaseNode } from "./BaseNode"; import { useCommentNavigation } from "@/hooks/useCommentNavigation"; @@ -38,6 +38,7 @@ export function VideoStitchNode({ id, data, selected }: NodeProps([]); useEffect(() => { const currentEdgeIds = videoEdges.map((e) => e.id); const currentOrder = nodeData.clipOrder || []; @@ -47,10 +48,19 @@ export function VideoStitchNode({ id, data, selected }: NodeProps !currentOrder.includes(eid)); const newOrder = [...validExisting, ...newEdges]; + // Skip if we just wrote this exact order (prevents extra render cycle) + if ( + newOrder.length === lastWrittenClipOrderRef.current.length && + newOrder.every((eid, idx) => eid === lastWrittenClipOrderRef.current[idx]) + ) { + return; + } + if ( newOrder.length !== currentOrder.length || !newOrder.every((eid, idx) => eid === currentOrder[idx]) ) { + lastWrittenClipOrderRef.current = newOrder; updateNodeData(id, { clipOrder: newOrder }); } }, [videoEdges, nodeData.clipOrder, id, updateNodeData]); @@ -111,15 +121,27 @@ export function VideoStitchNode({ id, data, selected }: NodeProps orderedClips.map((c) => `${c.edgeId}:${c.videoData ? "1" : "0"}`).join(","), + [orderedClips] + ); + + // Ref-based cache so the effect doesn't read stale `thumbnails` state + const thumbnailsRef = useRef>(new Map()); + // Extract thumbnails from connected videos useEffect(() => { + let cancelled = false; + const extractThumbnails = async () => { const newThumbnails = new Map(); for (const clip of orderedClips) { + if (cancelled) return; if (!clip.videoData) continue; - if (thumbnails.has(clip.edgeId)) { - newThumbnails.set(clip.edgeId, thumbnails.get(clip.edgeId)!); + if (thumbnailsRef.current.has(clip.edgeId)) { + newThumbnails.set(clip.edgeId, thumbnailsRef.current.get(clip.edgeId)!); continue; } @@ -134,6 +156,8 @@ export function VideoStitchNode({ id, data, selected }: NodeProps reject(new Error("Failed to load video")); }); + if (cancelled) return; + const seekTime = video.duration * 0.25; video.currentTime = seekTime; @@ -141,6 +165,8 @@ export function VideoStitchNode({ id, data, selected }: NodeProps resolve(); }); + if (cancelled) return; + const canvas = document.createElement("canvas"); canvas.width = 160; canvas.height = 120; @@ -157,11 +183,15 @@ export function VideoStitchNode({ id, data, selected }: NodeProps { cancelled = true; }; + }, [clipKey]); // eslint-disable-line react-hooks/exhaustive-deps — orderedClips accessed via closure, clipKey is the stable dep // Pointer-based drag reorder (HTML5 drag doesn't work inside React Flow nodes) const [draggedClipId, setDraggedClipId] = useState(null);