"use client"; import { useRef, useState, useEffect, useMemo } from "react"; import { useWorkflowStore } from "@/store/workflowStore"; import { NodeType } from "@/types"; import { useReactFlow } from "@xyflow/react"; import { ModelSearchDialog } from "./modals/ModelSearchDialog"; // Get the center of the React Flow pane in screen coordinates function getPaneCenter() { const pane = document.querySelector('.react-flow'); if (pane) { const rect = pane.getBoundingClientRect(); return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, }; } return { x: window.innerWidth / 2, y: window.innerHeight / 2 }; } interface NodeButtonProps { type: NodeType; label: string; } function NodeButton({ type, label }: NodeButtonProps) { const addNode = useWorkflowStore((state) => state.addNode); const { screenToFlowPosition } = useReactFlow(); const handleClick = () => { const center = getPaneCenter(); const position = screenToFlowPosition({ x: center.x + Math.random() * 100 - 50, y: center.y + Math.random() * 100 - 50, }); addNode(type, position); }; const handleDragStart = (event: React.DragEvent) => { event.dataTransfer.setData("application/node-type", type); event.dataTransfer.effectAllowed = "copy"; }; return ( {label} ); } function GenerateComboButton() { const [isOpen, setIsOpen] = useState(false); const menuRef = useRef(null); const addNode = useWorkflowStore((state) => state.addNode); const { screenToFlowPosition } = useReactFlow(); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(event.target as Node)) { setIsOpen(false); } }; if (isOpen) { document.addEventListener("mousedown", handleClickOutside); } return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [isOpen]); const handleAddNode = (type: NodeType) => { const center = getPaneCenter(); const position = screenToFlowPosition({ x: center.x + Math.random() * 100 - 50, y: center.y + Math.random() * 100 - 50, }); addNode(type, position); setIsOpen(false); }; const handleDragStart = (event: React.DragEvent, type: NodeType) => { event.dataTransfer.setData("application/node-type", type); event.dataTransfer.effectAllowed = "copy"; setIsOpen(false); }; return ( setIsOpen(!isOpen)} className="px-2.5 py-1.5 text-[11px] font-medium text-neutral-400 hover:text-neutral-100 hover:bg-neutral-700 rounded transition-colors flex items-center gap-1" > Generate {isOpen && ( handleAddNode("nanoBanana")} draggable onDragStart={(e) => handleDragStart(e, "nanoBanana")} className="w-full px-3 py-2 text-left text-[11px] font-medium text-neutral-300 hover:bg-neutral-700 hover:text-neutral-100 transition-colors flex items-center gap-2 cursor-grab active:cursor-grabbing" > Image handleAddNode("generateVideo")} draggable onDragStart={(e) => handleDragStart(e, "generateVideo")} className="w-full px-3 py-2 text-left text-[11px] font-medium text-neutral-300 hover:bg-neutral-700 hover:text-neutral-100 transition-colors flex items-center gap-2 cursor-grab active:cursor-grabbing" > Video handleAddNode("llmGenerate")} draggable onDragStart={(e) => handleDragStart(e, "llmGenerate")} className="w-full px-3 py-2 text-left text-[11px] font-medium text-neutral-300 hover:bg-neutral-700 hover:text-neutral-100 transition-colors flex items-center gap-2 cursor-grab active:cursor-grabbing" > Text (LLM) )} ); } export function FloatingActionBar() { const { nodes, isRunning, currentNodeIds, executeWorkflow, regenerateNode, executeSelectedNodes, stopWorkflow, validateWorkflow, edgeStyle, setEdgeStyle, setModelSearchOpen, modelSearchOpen, modelSearchProvider, } = useWorkflowStore(); // Get display text for running nodes const runningNodeCount = currentNodeIds.length; const getRunningLabel = () => { if (runningNodeCount === 0) return "Running..."; if (runningNodeCount === 1) { const node = nodes.find((n) => n.id === currentNodeIds[0]); const nodeName = node?.data?.customTitle || node?.type || "node"; return `Running ${nodeName}...`; } return `Running ${runningNodeCount} nodes...`; }; const [runMenuOpen, setRunMenuOpen] = useState(false); const runMenuRef = useRef(null); const { valid, errors } = validateWorkflow(); // Get the selected nodes const selectedNodes = useMemo(() => { return nodes.filter((n) => n.selected); }, [nodes]); // Get the selected node (if exactly one is selected) const selectedNode = useMemo(() => { return selectedNodes.length === 1 ? selectedNodes[0] : null; }, [selectedNodes]); // Close run menu when clicking outside useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (runMenuRef.current && !runMenuRef.current.contains(event.target as Node)) { setRunMenuOpen(false); } }; if (runMenuOpen) { document.addEventListener("mousedown", handleClickOutside); } return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [runMenuOpen]); const toggleEdgeStyle = () => { setEdgeStyle(edgeStyle === "angular" ? "curved" : "angular"); }; const handleRunClick = () => { if (isRunning) { stopWorkflow(); } else { executeWorkflow(); } }; const handleRunFromSelected = () => { if (selectedNode) { executeWorkflow(selectedNode.id); setRunMenuOpen(false); } }; const handleRunSelectedOnly = () => { if (selectedNode) { regenerateNode(selectedNode.id); setRunMenuOpen(false); } }; const handleRunSelectedNodes = () => { if (selectedNodes.length > 0) { executeSelectedNodes(selectedNodes.map((n) => n.id)); setRunMenuOpen(false); } }; return ( {/* Browse models button */} setModelSearchOpen(true)} title="Browse models" className="p-1.5 text-neutral-400 hover:text-neutral-100 hover:bg-neutral-700 rounded transition-colors" > {edgeStyle === "angular" ? ( ) : ( )} {isRunning ? ( <> {runningNodeCount > 1 ? `${runningNodeCount} nodes` : "Stop"} > ) : ( <> Run > )} {/* Dropdown chevron button */} {!isRunning && valid && ( setRunMenuOpen(!runMenuOpen)} className="flex items-center self-stretch px-1.5 rounded-r bg-white text-neutral-900 hover:bg-neutral-200 border-l border-neutral-200 transition-colors" title="Run options" > )} {/* Dropdown menu */} {runMenuOpen && !isRunning && ( { executeWorkflow(); setRunMenuOpen(false); }} className="w-full px-3 py-2 text-left text-[11px] font-medium text-neutral-300 hover:bg-neutral-700 hover:text-neutral-100 transition-colors flex items-center gap-2" > Run entire workflow Run from selected node Run selected node only 0 ? "text-neutral-300 hover:bg-neutral-700 hover:text-neutral-100" : "text-neutral-500 cursor-not-allowed" }`} title={selectedNodes.length === 0 ? "Select one or more nodes first" : `Run ${selectedNodes.length} selected node${selectedNodes.length > 1 ? 's' : ''}`} > {selectedNodes.length > 0 ? `Run ${selectedNodes.length} selected node${selectedNodes.length !== 1 ? 's' : ''}` : 'Run selected nodes'} )} {/* Model search dialog */} setModelSearchOpen(false)} initialProvider={modelSearchProvider} /> ); }