diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx index 520bbf70..8706e915 100644 --- a/src/components/WorkflowCanvas.tsx +++ b/src/components/WorkflowCanvas.tsx @@ -57,7 +57,6 @@ import { GroupBackgroundsPortal, GroupControlsOverlay } from "./GroupsOverlay"; import { NodeType, NanoBananaNodeData, HandleType } from "@/types"; import { defaultNodeDimensions } from "@/store/utils/nodeDefaults"; import { FloatingNodeHeader } from "./nodes/FloatingNodeHeader"; -import { ProviderBadge } from "./nodes/ProviderBadge"; import { ControlPanel } from "./nodes/ControlPanel"; import { detectAndSplitGrid } from "@/utils/gridSplitter"; import { logger } from "@/utils/logger"; @@ -366,14 +365,6 @@ export function WorkflowCanvas() { return NODE_TITLES[node.type || ""] || "Node"; }, []); - // Helper to get title prefix (provider badge for generate/LLM nodes) - const getNodeTitlePrefix = useCallback((node: Node): React.ReactNode => { - const provider = (node.data as any)?.selectedModel?.provider; - if (provider) { - return ; - } - return null; - }, []); // Wire comment/title change callbacks for FloatingNodeHeaders const handleCustomTitleChange = useCallback((nodeId: string, title: string) => { @@ -384,33 +375,22 @@ export function WorkflowCanvas() { updateNodeData(nodeId, { comment: comment || undefined }); }, [updateNodeData]); - // Create onRun callback for runnable nodes - const getOnRun = useCallback((nodeId: string, nodeType: string) => { - const runnableTypes = ['nanoBanana', 'generateVideo', 'generate3d', 'generateAudio', 'llmGenerate']; - if (runnableTypes.includes(nodeType)) { - return () => regenerateNode(nodeId); - } - return undefined; + // Stable callback for running a node from its header + const handleRunNode = useCallback((nodeId: string) => { + regenerateNode(nodeId); }, [regenerateNode]); - // Create onExpand callback for expandable nodes - const getOnExpand = useCallback((nodeId: string, nodeType: string) => { + // Stable callback for expanding a node from its header + const handleExpandNode = useCallback((nodeId: string, nodeType: string) => { if (nodeType === 'annotation') { - // Annotation uses annotationStore's openModal - return () => { - const node = getNodeById(nodeId); - if (!node) return; - const imageToEdit = (node.data as any)?.outputImage || (node.data as any)?.image; - if (!imageToEdit) return; - openAnnotationModal(nodeId, imageToEdit, (node.data as any)?.annotations); - }; - } - - const expandableTypes = ['prompt', 'promptConstructor', 'splitGrid']; - if (expandableTypes.includes(nodeType)) { - return () => setExpandingNode({ id: nodeId, type: nodeType }); + const node = getNodeById(nodeId); + if (!node) return; + const imageToEdit = (node.data as any)?.outputImage || (node.data as any)?.image; + if (!imageToEdit) return; + openAnnotationModal(nodeId, imageToEdit, (node.data as any)?.annotations); + } else { + setExpandingNode({ id: nodeId, type: nodeType }); } - return undefined; }, [getNodeById, openAnnotationModal]); @@ -2072,18 +2052,20 @@ export function WorkflowCanvas() { key={`header-${node.id}`} id={node.id} type={node.type as NodeType} - data={node.data} + isInLockedGroup={!!(node.data as any)?.isInLockedGroup} + isExecuting={!!(node.data as any)?.isExecuting} + focusedCommentNodeId={(node.data as any)?.focusedCommentNodeId} position={node.position} width={headerWidth} selected={!!node.selected} title={getNodeTitle(node)} customTitle={node.data?.customTitle} comment={node.data?.comment} - titlePrefix={getNodeTitlePrefix(node)} - onCustomTitleChange={(title) => handleCustomTitleChange(node.id, title)} - onCommentChange={(comment) => handleCommentChange(node.id, comment)} - onRun={getOnRun(node.id, node.type as string)} - onExpand={getOnExpand(node.id, node.type as string)} + provider={(node.data as any)?.selectedModel?.provider} + onCustomTitleChange={handleCustomTitleChange} + onCommentChange={handleCommentChange} + onRunNode={handleRunNode} + onExpandNode={handleExpandNode} /> ); })} diff --git a/src/components/nodes/FloatingNodeHeader.tsx b/src/components/nodes/FloatingNodeHeader.tsx index acb41238..bd5cb5d6 100644 --- a/src/components/nodes/FloatingNodeHeader.tsx +++ b/src/components/nodes/FloatingNodeHeader.tsx @@ -1,11 +1,12 @@ "use client"; -import { ReactNode, useState, useEffect, useRef, useCallback } from "react"; +import { ReactNode, useState, useEffect, useRef, useCallback, memo } from "react"; import { createPortal } from "react-dom"; import { useReactFlow } from "@xyflow/react"; -import { NodeType } from "@/types"; +import { NodeType, ProviderType } from "@/types"; import { useWorkflowStore } from "@/store/workflowStore"; import { defaultNodeDimensions } from "@/store/utils/nodeDefaults"; +import { ProviderBadge } from "./ProviderBadge"; export interface CommentNavigationProps { currentIndex: number; @@ -14,38 +15,45 @@ export interface CommentNavigationProps { onNext: () => void; } +const RUNNABLE_TYPES = new Set(['nanoBanana', 'generateVideo', 'generate3d', 'generateAudio', 'llmGenerate']); +const EXPANDABLE_TYPES = new Set(['prompt', 'promptConstructor', 'splitGrid', 'annotation']); + interface FloatingNodeHeaderProps { id: string; type: NodeType; - data: any; + isInLockedGroup?: boolean; + isExecuting?: boolean; + focusedCommentNodeId?: string | null; position: { x: number; y: number }; width: number; selected: boolean; - onExpand?: () => void; - onRun?: () => void; + onExpandNode?: (nodeId: string, nodeType: string) => void; + onRunNode?: (nodeId: string) => void; headerAction?: ReactNode; headerButtons?: ReactNode; - titlePrefix?: ReactNode; + provider?: ProviderType; title: string; customTitle?: string; comment?: string; - onCustomTitleChange?: (title: string) => void; - onCommentChange?: (comment: string) => void; + onCustomTitleChange?: (nodeId: string, title: string) => void; + onCommentChange?: (nodeId: string, comment: string) => void; commentNavigation?: CommentNavigationProps; } -export function FloatingNodeHeader({ +export const FloatingNodeHeader = memo(function FloatingNodeHeader({ id, type, - data, + isInLockedGroup = false, + isExecuting = false, + focusedCommentNodeId, position, width, selected, - onExpand, - onRun, + onExpandNode, + onRunNode, headerAction, headerButtons, - titlePrefix, + provider, title, customTitle, comment, @@ -53,6 +61,8 @@ export function FloatingNodeHeader({ onCommentChange, commentNavigation, }: FloatingNodeHeaderProps) { + const canRun = RUNNABLE_TYPES.has(type); + const canExpand = EXPANDABLE_TYPES.has(type); const [isHeaderHovered, setIsHeaderHovered] = useState(false); const isBodyHovered = useWorkflowStore((state) => state.hoveredNodeId === id); const isHovered = isHeaderHovered || isBodyHovered; @@ -68,14 +78,8 @@ export function FloatingNodeHeader({ const commentButtonRef = useRef(null); const tooltipRef = useRef(null); - // Check if this node is in a locked group - const isInLockedGroup = data?.isInLockedGroup || false; - // Check if comment is focused for navigation - const isCommentFocused = data?.focusedCommentNodeId === id; - - // Check if node is executing - const isExecuting = data?.isExecuting || false; + const isCommentFocused = focusedCommentNodeId === id; // Sync state with props useEffect(() => { @@ -133,10 +137,10 @@ export function FloatingNodeHeader({ const handleTitleSubmit = useCallback(() => { const trimmed = editTitleValue.trim(); if (trimmed !== (customTitle || "")) { - onCustomTitleChange?.(trimmed); + onCustomTitleChange?.(id, trimmed); } setIsEditingTitle(false); - }, [editTitleValue, customTitle, onCustomTitleChange]); + }, [editTitleValue, customTitle, onCustomTitleChange, id]); const handleTitleKeyDown = useCallback( (e: React.KeyboardEvent) => { @@ -154,10 +158,10 @@ export function FloatingNodeHeader({ const handleCommentSubmit = useCallback(() => { const trimmed = editCommentValue.trim(); if (trimmed !== (comment || "")) { - onCommentChange?.(trimmed); + onCommentChange?.(id, trimmed); } setIsEditingComment(false); - }, [editCommentValue, comment, onCommentChange]); + }, [editCommentValue, comment, onCommentChange, id]); const handleCommentKeyDown = useCallback( (e: React.KeyboardEvent) => { @@ -342,7 +346,7 @@ export function FloatingNodeHeader({ > {/* Title Section */}
- {titlePrefix} + {provider && } {isEditingTitle ? ( {/* Expand Button */} - {onExpand && ( + {canExpand && onExpandNode && (
); -} +});