"use client"; import { useRef, useState, useEffect, useMemo } from "react"; import { useWorkflowStore } from "@/store/workflowStore"; import { NodeType, ProviderType } from "@/types"; import { useReactFlow } from "@xyflow/react"; import { ModelSearchDialog } from "./modals/ModelSearchDialog"; import { EnvStatusResponse } from "@/app/api/env-status/route"; // 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 ( ); } 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 (
{isOpen && (
)}
); } function ProviderIconButton({ provider, onClick }: { provider: ProviderType; onClick: () => void }) { return ( ); } export function FloatingActionBar() { const { nodes, isRunning, executeWorkflow, regenerateNode, stopWorkflow, validateWorkflow, edgeStyle, setEdgeStyle, providerSettings, setModelSearchOpen, modelSearchOpen, modelSearchProvider, } = useWorkflowStore(); const [runMenuOpen, setRunMenuOpen] = useState(false); const runMenuRef = useRef(null); const [mounted, setMounted] = useState(false); const [envStatus, setEnvStatus] = useState(null); // Defer client-only rendering to avoid hydration mismatch useEffect(() => { setMounted(true); }, []); // Fetch environment status to check for API keys in .env useEffect(() => { fetch("/api/env-status") .then((res) => res.json()) .then((data: EnvStatusResponse) => setEnvStatus(data)) .catch(() => setEnvStatus(null)); }, []); const { valid, errors } = validateWorkflow(); // Get the selected node (if exactly one is selected) const selectedNode = useMemo(() => { const selected = nodes.filter((n) => n.selected); return selected.length === 1 ? selected[0] : null; }, [nodes]); // 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); } }; return (
{/* Provider model browser icons */}
{/* Replicate icon - show if API key is configured via env or project settings */} {mounted && (providerSettings.providers.replicate?.apiKey || envStatus?.replicate) && ( setModelSearchOpen(true, "replicate")} /> )} {/* fal.ai icon - always show (works without key but rate limited) */} setModelSearchOpen(true, "fal")} />
{/* Dropdown chevron button */} {!isRunning && valid && ( )} {/* Dropdown menu */} {runMenuOpen && !isRunning && (
)}
{/* Model search dialog */} setModelSearchOpen(false)} initialProvider={modelSearchProvider} />
); }