diff --git a/src/components/FloatingActionBar.tsx b/src/components/FloatingActionBar.tsx index 2aa4b05c..be230f1c 100644 --- a/src/components/FloatingActionBar.tsx +++ b/src/components/FloatingActionBar.tsx @@ -1,11 +1,68 @@ "use client"; -import { useRef, useState, useEffect, useMemo } from "react"; +import { useRef, useState, useEffect, useMemo, useCallback } from "react"; import { useWorkflowStore } from "@/store/workflowStore"; import { NodeType } from "@/types"; import { useReactFlow } from "@xyflow/react"; import { ModelSearchDialog } from "./modals/ModelSearchDialog"; +// All nodes menu categories +const ALL_NODES_CATEGORIES: { label: string; nodes: { type: NodeType; label: string }[] }[] = [ + { + label: "Input", + nodes: [ + { type: "imageInput", label: "Image Input" }, + { type: "audioInput", label: "Audio Input" }, + { type: "glbViewer", label: "3D Viewer" }, + ], + }, + { + label: "Text", + nodes: [ + { type: "prompt", label: "Prompt" }, + { type: "promptConstructor", label: "Prompt Constructor" }, + { type: "array", label: "Array" }, + ], + }, + { + label: "Generate", + nodes: [ + { type: "nanoBanana", label: "Generate Image" }, + { type: "generateVideo", label: "Generate Video" }, + { type: "generate3d", label: "Generate 3D" }, + { type: "generateAudio", label: "Generate Audio" }, + { type: "llmGenerate", label: "LLM Generate" }, + ], + }, + { + label: "Process", + nodes: [ + { type: "annotation", label: "Annotate" }, + { type: "splitGrid", label: "Split Grid" }, + { type: "videoStitch", label: "Video Stitch" }, + { type: "videoTrim", label: "Video Trim" }, + { type: "easeCurve", label: "Ease Curve" }, + { type: "videoFrameGrab", label: "Frame Grab" }, + { type: "imageCompare", label: "Image Compare" }, + ], + }, + { + label: "Route", + nodes: [ + { type: "router", label: "Router" }, + { type: "switch", label: "Switch" }, + { type: "conditionalSwitch", label: "Conditional Switch" }, + ], + }, + { + label: "Output", + nodes: [ + { type: "output", label: "Output" }, + { type: "outputGallery", label: "Output Gallery" }, + ], + }, +]; + // Get the center of the React Flow pane in screen coordinates function getPaneCenter() { const pane = document.querySelector('.react-flow'); @@ -165,6 +222,89 @@ function GenerateComboButton() { } +function AllNodesMenu() { + 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 = useCallback((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); + }, [addNode, screenToFlowPosition]); + + const handleDragStart = useCallback((event: React.DragEvent, type: NodeType) => { + event.dataTransfer.setData("application/node-type", type); + event.dataTransfer.effectAllowed = "copy"; + setIsOpen(false); + }, []); + + return ( +
+ + + {isOpen && ( +
+ {ALL_NODES_CATEGORIES.map((category, catIndex) => ( +
+
0 ? " border-t border-neutral-700" : ""}`}> + {category.label} +
+ {category.nodes.map((node) => ( + + ))} +
+ ))} +
+ )} +
+ ); +} + export function FloatingActionBar() { const { nodes, @@ -261,21 +401,19 @@ export function FloatingActionBar() {
- + - {/* Browse models button */} + {/* All models button */}