"use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { CloseOutlined, PictureOutlined, PlusOutlined } from "@ant-design/icons"; import { useWorkflowStore } from "@/store/workflowStore"; import { NodeType } from "@/types"; import { useReactFlow } from "@xyflow/react"; import { useI18n } from "@/i18n"; import { buildDefaultGenerationNodeData } from "@/utils/defaultGenerationNodeModel"; import { GenerateNodeMenu } from "./GenerateNodeMenu"; import { ModelSearchDialog } from "./modals/ModelSearchDialog"; import { AssetPickerModal, PopiAssetItem } from "./AssetPickerModal"; import { getPopiImageAssetPreview, resolvePopiImageAsset } from "@/lib/popiAssetApi"; import { useFTUXStore, TutorialStep } from "@/store/ftuxStore"; // 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 }; } function GenerateComboButton() { const { t } = useI18n(); const [isOpen, setIsOpen] = useState(false); const menuRef = useRef(null); const addNode = useWorkflowStore((state) => state.addNode); const selectSingleNode = useWorkflowStore((state) => state.selectSingleNode); 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 = async (type: NodeType) => { const center = getPaneCenter(); const position = screenToFlowPosition({ x: center.x + Math.random() * 100 - 50, y: center.y + Math.random() * 100 - 50, }); const nodeId = addNode(type, position, await buildDefaultGenerationNodeData(type)); selectSingleNode(nodeId); 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 ResourceImageButton() { const { t } = useI18n(); const [isAssetPickerOpen, setIsAssetPickerOpen] = useState(false); const addNode = useWorkflowStore((state) => state.addNode); const updateNodeData = useWorkflowStore((state) => state.updateNodeData); const selectSingleNode = useWorkflowStore((state) => state.selectSingleNode); const { screenToFlowPosition } = useReactFlow(); const handleSelectAsset = (asset: PopiAssetItem) => { const preview = getPopiImageAssetPreview(asset); if (!preview) return; const center = getPaneCenter(); const position = screenToFlowPosition({ x: center.x + Math.random() * 100 - 50, y: center.y + Math.random() * 100 - 50, }); const nodeId = addNode("imageInput", position, { image: preview.imageUrl || preview.previewImageUrl, imageRef: undefined, assetId: preview.assetId, previewImage: preview.previewImageUrl, assetDetailLoading: true, assetDetailError: null, filename: preview.filename, dimensions: preview.dimensions, }); selectSingleNode(nodeId); setIsAssetPickerOpen(false); const applyResolvedAsset = ( resolved: NonNullable>>, dimensions: { width: number; height: number } | null ) => { updateNodeData(nodeId, { image: resolved.imageUrl || resolved.previewImageUrl, imageRef: undefined, assetId: resolved.assetId, previewImage: resolved.previewImageUrl, assetDetailLoading: false, assetDetailError: null, filename: resolved.filename, dimensions, }); }; void resolvePopiImageAsset(asset) .then((resolved) => { if (!resolved) { updateNodeData(nodeId, { assetDetailLoading: false, assetDetailError: null, }); return; } if (resolved.dimensions || typeof Image === "undefined") { applyResolvedAsset(resolved, resolved.dimensions); return; } const image = new Image(); image.onload = () => applyResolvedAsset(resolved, { width: image.width, height: image.height }); image.onerror = () => applyResolvedAsset(resolved, preview.dimensions); image.src = resolved.previewImageUrl; }) .catch((error: unknown) => { updateNodeData(nodeId, { assetDetailLoading: false, assetDetailError: error instanceof Error ? error.message : "Failed to load asset detail", }); }); }; return ( <> {isAssetPickerOpen && ( setIsAssetPickerOpen(false)} onSelect={handleSelectAsset} /> )} ); } export function FloatingActionBar() { const { t } = useI18n(); const nodes = useWorkflowStore((state) => state.nodes) ?? []; const isRunning = useWorkflowStore((state) => state.isRunning) ?? false; const currentNodeIds = useWorkflowStore((state) => state.currentNodeIds) ?? []; const executeWorkflow = useWorkflowStore((state) => state.executeWorkflow) ?? (async () => {}); const regenerateNode = useWorkflowStore((state) => state.regenerateNode) ?? (async () => {}); const executeSelectedNodes = useWorkflowStore((state) => state.executeSelectedNodes) ?? (async () => {}); const stopWorkflow = useWorkflowStore((state) => state.stopWorkflow) ?? (() => {}); const mockTutorialExecution = useWorkflowStore((state) => state.mockTutorialExecution) ?? (async () => {}); const validateWorkflow = useWorkflowStore((state) => state.validateWorkflow); const edgeStyle = useWorkflowStore((state) => state.edgeStyle); const setEdgeStyle = useWorkflowStore((state) => state.setEdgeStyle); const setModelSearchOpen = useWorkflowStore((state) => state.setModelSearchOpen); const modelSearchOpen = useWorkflowStore((state) => state.modelSearchOpen); const modelSearchProvider = useWorkflowStore((state) => state.modelSearchProvider); const [tutorialActive, setTutorialActive] = useState(false); const [currentTutorialStep, setCurrentTutorialStep] = useState(0); const [tutorialSteps, setTutorialSteps] = useState([]); const [runMenuOpen, setRunMenuOpen] = useState(false); const runMenuRef = useRef(null); const { valid, errors } = validateWorkflow?.() ?? { valid: true, errors: [] }; useEffect(() => { const unsubscribe = useFTUXStore.subscribe((state) => { setTutorialActive(state.tutorialActive); setCurrentTutorialStep(state.currentTutorialStep); setTutorialSteps(state.tutorialSteps); }); const currentState = useFTUXStore.getState(); setTutorialActive(currentState.tutorialActive); setCurrentTutorialStep(currentState.currentTutorialStep); setTutorialSteps(currentState.tutorialSteps); return unsubscribe; }, []); const selectedNodes = useMemo(() => nodes.filter((node) => node.selected), [nodes]); const selectedNode = useMemo(() => selectedNodes.length === 1 ? selectedNodes[0] : null, [selectedNodes]); const isRunOptionsTutorialStep = useMemo(() => { if (!tutorialActive || tutorialSteps.length === 0) return false; const currentStep = tutorialSteps[currentTutorialStep]; return currentStep?.id === "explain-run-options"; }, [currentTutorialStep, tutorialActive, tutorialSteps]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (runMenuRef.current && !runMenuRef.current.contains(event.target as Node) && !isRunOptionsTutorialStep) { setRunMenuOpen(false); } }; if (runMenuOpen) { document.addEventListener("mousedown", handleClickOutside); } return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [isRunOptionsTutorialStep, runMenuOpen]); useEffect(() => { if (isRunOptionsTutorialStep) { setRunMenuOpen(true); } }, [isRunOptionsTutorialStep]); useEffect(() => { if (!tutorialActive || tutorialSteps.length === 0) return; const currentStep = tutorialSteps[currentTutorialStep]; if ( currentStep?.id === "run-workflow" || currentStep?.id === "demonstrate-downstream" || currentStep?.id === "demonstrate-complete" ) { setRunMenuOpen(false); } }, [currentTutorialStep, tutorialActive, tutorialSteps]); const runningNodeCount = currentNodeIds.length; const getRunningLabel = useCallback(() => { if (runningNodeCount === 0) return t("toolbar.running"); if (runningNodeCount === 1) { const node = nodes.find((item) => item.id === currentNodeIds[0]); const nodeName = node?.data?.customTitle || node?.type || "node"; return t("toolbar.runningNode", { node: nodeName }); } return t("toolbar.runningNodes", { count: runningNodeCount }); }, [currentNodeIds, nodes, runningNodeCount, t]); const toggleEdgeStyle = () => { setEdgeStyle(edgeStyle === "angular" ? "curved" : "angular"); }; const handleRunClick = useCallback(() => { const ftuxState = useFTUXStore.getState(); const currentStep = ftuxState.tutorialSteps[ftuxState.currentTutorialStep]; if (isRunning) { stopWorkflow(); } else if (ftuxState.tutorialActive && currentStep?.id === "run-workflow") { void mockTutorialExecution(); } else { void executeWorkflow(); } }, [executeWorkflow, isRunning, mockTutorialExecution, stopWorkflow]); const handleRunFromSelected = useCallback(() => { if (!selectedNode) return; void executeWorkflow(selectedNode.id); setRunMenuOpen(false); }, [executeWorkflow, selectedNode]); const handleRunSelectedOnly = useCallback(() => { if (!selectedNode) return; void regenerateNode(selectedNode.id); setRunMenuOpen(false); }, [regenerateNode, selectedNode]); const handleRunSelectedNodes = useCallback(() => { if (selectedNodes.length === 0) return; void executeSelectedNodes(selectedNodes.map((node) => node.id)); setRunMenuOpen(false); }, [executeSelectedNodes, selectedNodes]); return (
{!isRunning && valid && ( )} {runMenuOpen && !isRunning && (
)}
setModelSearchOpen(false)} initialProvider={modelSearchProvider} />
); }