From 97993cbedeb4d69e389649ecba2e227a9234a023 Mon Sep 17 00:00:00 2001 From: Jonathan Dobson Date: Wed, 13 Aug 2025 14:13:00 -0400 Subject: [PATCH] Jon/workspace (#3175) --- .../src/components/FloatingWindow.tsx | 32 +- .../routes/workflows/editor/FlowRenderer.tsx | 377 ++++-------- .../workflows/editor/WorkflowDebugger.tsx | 257 +-------- .../workflows/editor/WorkflowEditor.tsx | 30 +- .../workflows/editor/WorkflowHeader.tsx | 3 + .../src/routes/workflows/editor/Workspace.tsx | 543 ++++++++++++++++++ .../editor/nodes/components/NodeHeader.tsx | 4 + .../panels/WorkflowNodeLibraryPanel.tsx | 14 +- .../editor/panels/WorkflowParametersPanel.tsx | 11 +- .../src/routes/workflows/hooks/useRanker.ts | 46 ++ 10 files changed, 754 insertions(+), 563 deletions(-) create mode 100644 skyvern-frontend/src/routes/workflows/editor/Workspace.tsx create mode 100644 skyvern-frontend/src/routes/workflows/hooks/useRanker.ts diff --git a/skyvern-frontend/src/components/FloatingWindow.tsx b/skyvern-frontend/src/components/FloatingWindow.tsx index 03dceeda..06f5b413 100644 --- a/skyvern-frontend/src/components/FloatingWindow.tsx +++ b/skyvern-frontend/src/components/FloatingWindow.tsx @@ -158,6 +158,8 @@ function FloatingWindow({ zIndex, // -- onCycle, + onFocus, + onBlur, onInteract, }: { bounded?: boolean; @@ -172,9 +174,11 @@ function FloatingWindow({ showPowerButton?: boolean; showReloadButton?: boolean; title: string; - zIndex?: string; + zIndex?: number; // -- onCycle?: () => void; + onFocus?: () => void; + onBlur?: () => void; onInteract?: () => void; }) { const [reloadKey, setReloadKey] = useState(0); @@ -217,6 +221,7 @@ function FloatingWindow({ } | undefined >(undefined); + const hasInitialized = useRef(false); const os = getOs(); @@ -284,9 +289,10 @@ function FloatingWindow({ ); useEffect(() => { - if (!initialWidth || !initialHeight) { + if (hasInitialized.current || !initialWidth || !initialHeight) { return; } + hasInitialized.current = true; setSize({ left: initialPosition?.x ?? 0, top: initialPosition?.y ?? 0, @@ -533,6 +539,16 @@ function FloatingWindow({ className={cn("border-2 border-gray-700", { "hover:border-slate-500": !isMaximized, })} + handleStyles={{ + bottomLeft: { + width: "40px", + height: "40px", + }, + bottomRight: { + width: "40px", + height: "40px", + }, + }} minHeight={Constants.MinHeight} minWidth={Constants.MinWidth} // TODO: turn back on; turning off clears a resize bug atm @@ -556,6 +572,7 @@ function FloatingWindow({ return; } + onFocus?.(); setIsMinimized(false); setIsResizing(true); setDragStartSize({ ...size, left: position.x, top: position.y }); @@ -565,6 +582,7 @@ function FloatingWindow({ return; } + onFocus?.(); onResize({ delta, direction, size }); }} onResizeStop={() => { @@ -581,7 +599,8 @@ function FloatingWindow({
onInteract?.()} + onFocus={onFocus} + onBlur={onBlur} + onMouseDownCapture={(e) => { + onInteract?.(); + e.currentTarget.focus(); + }} onDoubleClick={() => { toggleMaximized(); }} diff --git a/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx b/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx index ecb757a8..37ebde26 100644 --- a/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/FlowRenderer.tsx @@ -7,7 +7,6 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { toast } from "@/components/ui/use-toast"; import { useOnChange } from "@/hooks/useOnChange"; import { useShouldNotifyWhenClosingTab } from "@/hooks/useShouldNotifyWhenClosingTab"; import { BlockActionContext } from "@/store/BlockActionContext"; @@ -17,7 +16,6 @@ import { useWorkflowSave, type WorkflowSaveData, } from "@/store/WorkflowHasChangesStore"; -import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore"; import { useWorkflowTitleStore } from "@/store/WorkflowTitleStore"; import { ReloadIcon } from "@radix-ui/react-icons"; import { @@ -25,17 +23,15 @@ import { BackgroundVariant, Controls, Edge, - Panel, PanOnScrollMode, ReactFlow, Viewport, - useEdgesState, useNodesInitialized, - useNodesState, useReactFlow, + NodeChange, + EdgeChange, } from "@xyflow/react"; import "@xyflow/react/dist/style.css"; -import { nanoid } from "nanoid"; import { useCallback, useEffect, useRef, useState } from "react"; import { useBlocker } from "react-router-dom"; import { @@ -56,22 +52,13 @@ import { ParameterYAML, WorkflowParameterYAML, } from "../types/workflowYamlTypes"; -import { WorkflowHeader } from "./WorkflowHeader"; -import { WorkflowParametersStateContext } from "./WorkflowParametersStateContext"; import { BITWARDEN_CLIENT_ID_AWS_SECRET_KEY, BITWARDEN_CLIENT_SECRET_AWS_SECRET_KEY, BITWARDEN_MASTER_PASSWORD_AWS_SECRET_KEY, } from "./constants"; import { edgeTypes } from "./edges"; -import { - AppNode, - isWorkflowBlockNode, - nodeTypes, - WorkflowBlockNode, -} from "./nodes"; -import { WorkflowNodeLibraryPanel } from "./panels/WorkflowNodeLibraryPanel"; -import { WorkflowParametersPanel } from "./panels/WorkflowParametersPanel"; +import { AppNode, isWorkflowBlockNode, nodeTypes } from "./nodes"; import { ParametersState, parameterIsSkyvernCredential, @@ -81,22 +68,14 @@ import { import "./reactFlowOverrideStyles.css"; import { convertEchoParameters, - createNode, - defaultEdge, descendants, - generateNodeLabel, getAdditionalParametersForEmailBlock, getOrderedChildrenBlocks, getOutputParameterKey, getWorkflowBlocks, - getWorkflowErrors, getWorkflowSettings, layout, - nodeAdderNode, - startNode, } from "./workflowEditorUtils"; -import { cn } from "@/util/utils"; -import { WorkflowDebuggerRun } from "@/routes/workflows/editor/WorkflowDebuggerRun"; import { useAutoPan } from "./useAutoPan"; function convertToParametersYAML( @@ -237,38 +216,38 @@ function convertToParametersYAML( } type Props = { + nodes: Array; + edges: Array; + setNodes: (nodes: Array) => void; + setEdges: (edges: Array) => void; + onNodesChange: (changes: Array>) => void; + onEdgesChange: (changes: Array) => void; initialTitle: string; - initialNodes: Array; - initialEdges: Array; initialParameters: ParametersState; workflow: WorkflowApiResponse; -}; - -export type AddNodeProps = { - nodeType: NonNullable; - previous: string | null; - next: string | null; - parent?: string; - connectingEdgeType: string; + onDebuggableBlockCountChange: (count: number) => void; + onMouseDownCapture?: () => void; + zIndex?: number; }; function FlowRenderer({ + nodes, + edges, + setNodes, + setEdges, + onNodesChange, + onEdgesChange, initialTitle, - initialEdges, - initialNodes, initialParameters, workflow, + onDebuggableBlockCountChange, + onMouseDownCapture, + zIndex, }: Props) { const reactFlowInstance = useReactFlow(); const debugStore = useDebugStore(); - const { workflowPanelState, setWorkflowPanelState, closeWorkflowPanel } = - useWorkflowPanelStore(); const { title, initializeTitle } = useWorkflowTitleStore(); - const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); - const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); - const [parameters, setParameters] = - useState(initialParameters); - const [debuggableBlockCount, setDebuggableBlockCount] = useState(0); + const [parameters] = useState(initialParameters); const nodesInitialized = useNodesInitialized(); const [shouldConstrainPan, setShouldConstrainPan] = useState(false); const onNodesChangeTimeoutRef = useRef(null); @@ -324,8 +303,8 @@ function FlowRenderer({ } } - setDebuggableBlockCount(debuggable.length); - }, [nodes, edges]); + onDebuggableBlockCountChange(debuggable.length); + }, [nodes, edges, onDebuggableBlockCountChange]); const constructSaveData = useCallback((): WorkflowSaveData => { const blocks = getWorkflowBlocks(nodes, edges); @@ -371,88 +350,6 @@ function FlowRenderer({ return await saveWorkflow.mutateAsync(); } - function addNode({ - nodeType, - previous, - next, - parent, - connectingEdgeType, - }: AddNodeProps) { - const newNodes: Array = []; - const newEdges: Array = []; - const id = nanoid(); - const existingLabels = nodes - .filter(isWorkflowBlockNode) - .map((node) => node.data.label); - const node = createNode( - { id, parentId: parent }, - nodeType, - generateNodeLabel(existingLabels), - ); - newNodes.push(node); - if (previous) { - const newEdge = { - id: nanoid(), - type: "edgeWithAddButton", - source: previous, - target: id, - style: { - strokeWidth: 2, - }, - }; - newEdges.push(newEdge); - } - if (next) { - const newEdge = { - id: nanoid(), - type: connectingEdgeType, - source: id, - target: next, - style: { - strokeWidth: 2, - }, - }; - newEdges.push(newEdge); - } - - if (nodeType === "loop") { - // when loop node is first created it needs an adder node so nodes can be added inside the loop - const startNodeId = nanoid(); - const adderNodeId = nanoid(); - newNodes.push( - startNode( - startNodeId, - { - withWorkflowSettings: false, - editable: true, - }, - id, - ), - ); - newNodes.push(nodeAdderNode(adderNodeId, id)); - newEdges.push(defaultEdge(startNodeId, adderNodeId)); - } - - const editedEdges = previous - ? edges.filter((edge) => edge.source !== previous) - : edges; - - const previousNode = nodes.find((node) => node.id === previous); - const previousNodeIndex = previousNode - ? nodes.indexOf(previousNode) - : nodes.length - 1; - - // creating some memory for no reason, maybe check it out later - const newNodesAfter = [ - ...nodes.slice(0, previousNodeIndex + 1), - ...newNodes, - ...nodes.slice(previousNodeIndex + 1), - ]; - - workflowChangesStore.setHasChanges(true); - doLayout(newNodesAfter, [...editedEdges, ...newEdges]); - } - function deleteNode(id: string) { const node = nodes.find((node) => node.id === id); if (!node || !isWorkflowBlockNode(node)) { @@ -629,7 +526,11 @@ function FlowRenderer({ }; return ( - <> +
onMouseDownCapture?.()} + > { @@ -671,155 +572,83 @@ function FlowRenderer({ - - { + const dimensionChanges = changes.filter( + (change) => change.type === "dimensions", + ); + const tempNodes = [...nodes]; + dimensionChanges.forEach((change) => { + const node = tempNodes.find((node) => node.id === change.id); + if (node) { + if (node.measured?.width) { + node.measured.width = change.dimensions?.width; + } + if (node.measured?.height) { + node.measured.height = change.dimensions?.height; + } + } + }); + if (dimensionChanges.length > 0) { + doLayout(tempNodes, edges); + } + if ( + changes.some((change) => { + return ( + change.type === "add" || + change.type === "remove" || + change.type === "replace" + ); + }) + ) { + workflowChangesStore.setHasChanges(true); + } + // throttle onNodesChange to prevent cascading React updates + if (onNodesChangeTimeoutRef.current === null) { + onNodesChange(changes); + onNodesChangeTimeoutRef.current = setTimeout(() => { + onNodesChangeTimeoutRef.current = null; + }, 33); // ~30fps throttle + } }} + onEdgesChange={onEdgesChange} + nodeTypes={nodeTypes} + edgeTypes={edgeTypes} + colorMode="dark" + fitView={true} + fitViewOptions={{ + maxZoom: 1, + }} + deleteKeyCode={null} + onMove={(_, viewport) => { + if (debugStore.isDebugMode && shouldConstrainPan) { + constrainPan(viewport); + } + }} + maxZoom={debugStore.isDebugMode ? 1 : 2} + minZoom={debugStore.isDebugMode ? 1 : 0.5} + panOnDrag={true} + panOnScroll={true} + panOnScrollMode={PanOnScrollMode.Vertical} + zoomOnDoubleClick={!debugStore.isDebugMode} + zoomOnPinch={!debugStore.isDebugMode} + zoomOnScroll={!debugStore.isDebugMode} > - { - const dimensionChanges = changes.filter( - (change) => change.type === "dimensions", - ); - const tempNodes = [...nodes]; - dimensionChanges.forEach((change) => { - const node = tempNodes.find((node) => node.id === change.id); - if (node) { - if (node.measured?.width) { - node.measured.width = change.dimensions?.width; - } - if (node.measured?.height) { - node.measured.height = change.dimensions?.height; - } - } - }); - if (dimensionChanges.length > 0) { - doLayout(tempNodes, edges); - } - if ( - changes.some((change) => { - return ( - change.type === "add" || - change.type === "remove" || - change.type === "replace" - ); - }) - ) { - workflowChangesStore.setHasChanges(true); - } - - // throttle onNodesChange to prevent cascading React updates - if (onNodesChangeTimeoutRef.current === null) { - onNodesChange(changes); - onNodesChangeTimeoutRef.current = setTimeout(() => { - onNodesChangeTimeoutRef.current = null; - }, 33); // ~30fps throttle - } - }} - onEdgesChange={onEdgesChange} - nodeTypes={nodeTypes} - edgeTypes={edgeTypes} - colorMode="dark" - fitView={true} - fitViewOptions={{ - maxZoom: 1, - }} - deleteKeyCode={null} - onMove={(_, viewport) => { - if (debugStore.isDebugMode && shouldConstrainPan) { - constrainPan(viewport); - } - }} - maxZoom={debugStore.isDebugMode ? 1 : 2} - minZoom={debugStore.isDebugMode ? 1 : 0.5} - panOnDrag={true} - panOnScroll={true} - panOnScrollMode={PanOnScrollMode.Vertical} - zoomOnDoubleClick={!debugStore.isDebugMode} - zoomOnPinch={!debugStore.isDebugMode} - zoomOnScroll={!debugStore.isDebugMode} - > - - - {debugStore.isDebugMode && ( - -
-
- -
-
-
- )} - - { - if ( - workflowPanelState.active && - workflowPanelState.content === "parameters" - ) { - closeWorkflowPanel(); - } else { - setWorkflowPanelState({ - active: true, - content: "parameters", - }); - } - }} - onSave={async () => { - const errors = getWorkflowErrors(nodes); - if (errors.length > 0) { - toast({ - title: "Can not save workflow because of errors:", - description: ( -
- {errors.map((error) => ( -

{error}

- ))} -
- ), - variant: "destructive", - }); - return; - } - await handleSave(); - }} - /> -
- {workflowPanelState.active && ( - - {workflowPanelState.content === "parameters" && ( - - )} - {workflowPanelState.content === "nodeLibrary" && ( - { - addNode(props); - }} - /> - )} - - )} -
-
-
- + + + + +
); } -export { FlowRenderer }; +export { FlowRenderer, type Props as FlowRendererProps }; diff --git a/skyvern-frontend/src/routes/workflows/editor/WorkflowDebugger.tsx b/skyvern-frontend/src/routes/workflows/editor/WorkflowDebugger.tsx index f53c0762..b49c99c9 100644 --- a/skyvern-frontend/src/routes/workflows/editor/WorkflowDebugger.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/WorkflowDebugger.tsx @@ -1,175 +1,18 @@ -import { AxiosError } from "axios"; -import { ReloadIcon } from "@radix-ui/react-icons"; -import { useEffect, useRef, useState } from "react"; import { useParams } from "react-router-dom"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; import { ReactFlowProvider } from "@xyflow/react"; -import { getClient } from "@/api/AxiosClient"; -import { DebugSessionApiResponse } from "@/api/types"; -import { AnimatedWave } from "@/components/AnimatedWave"; -import { BrowserStream } from "@/components/BrowserStream"; -import { FloatingWindow } from "@/components/FloatingWindow"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogClose, -} from "@/components/ui/dialog"; -import { toast } from "@/components/ui/use-toast"; -import { useCredentialGetter } from "@/hooks/useCredentialGetter"; -import { useMountEffect } from "@/hooks/useMountEffect"; -import { statusIsFinalized } from "@/routes/tasks/types.ts"; -import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery"; -import { useBlockScriptStore } from "@/store/BlockScriptStore"; -import { useSidebarStore } from "@/store/SidebarStore"; -import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore"; -import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery"; import { useWorkflowQuery } from "../hooks/useWorkflowQuery"; -import { useDebugSessionQuery } from "../hooks/useDebugSessionQuery"; import { WorkflowSettings } from "../types/workflowTypes"; -import { FlowRenderer } from "./FlowRenderer"; import { getElements } from "./workflowEditorUtils"; import { getInitialParameters } from "./utils"; - -const Constants = { - NewBrowserCooldown: 30000, -} as const; +import { Workspace } from "./Workspace"; function WorkflowDebugger() { - const { blockLabel, workflowPermanentId } = useParams(); - const [openDialogue, setOpenDialogue] = useState(false); - const [activeDebugSession, setActiveDebugSession] = - useState(null); - const [showPowerButton, setShowPowerButton] = useState(true); - const credentialGetter = useCredentialGetter(); - const queryClient = useQueryClient(); - const [shouldFetchDebugSession, setShouldFetchDebugSession] = useState(false); - const blockScriptStore = useBlockScriptStore(); - - const { data: workflowRun } = useWorkflowRunQuery(); - + const { workflowPermanentId } = useParams(); const { data: workflow } = useWorkflowQuery({ workflowPermanentId, }); - const { data: blockScripts } = useBlockScriptsQuery({ - workflowPermanentId, - }); - - const { data: debugSession } = useDebugSessionQuery({ - workflowPermanentId, - enabled: shouldFetchDebugSession && !!workflowPermanentId, - }); - - const setCollapsed = useSidebarStore((state) => { - return state.setCollapsed; - }); - - const workflowChangesStore = useWorkflowHasChangesStore(); - - const handleOnCycle = () => { - setOpenDialogue(true); - }; - - useMountEffect(() => { - setCollapsed(true); - workflowChangesStore.setHasChanges(false); - - if (workflowPermanentId) { - queryClient.removeQueries({ - queryKey: ["debugSession", workflowPermanentId], - }); - setShouldFetchDebugSession(true); - } - }); - - useEffect(() => { - blockScriptStore.setScripts(blockScripts ?? {}); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [blockScripts]); - - const afterCycleBrowser = () => { - setOpenDialogue(false); - setShowPowerButton(false); - - if (powerButtonTimeoutRef.current) { - clearTimeout(powerButtonTimeoutRef.current); - } - - powerButtonTimeoutRef.current = setTimeout(() => { - setShowPowerButton(true); - }, Constants.NewBrowserCooldown); - }; - - const cycleBrowser = useMutation({ - mutationFn: async (id: string) => { - const client = await getClient(credentialGetter, "sans-api-v1"); - return client.post(`/debug-session/${id}/new`); - }, - onSuccess: (response) => { - const newDebugSession = response.data; - setActiveDebugSession(newDebugSession); - - queryClient.invalidateQueries({ - queryKey: ["debugSession", workflowPermanentId], - }); - - toast({ - title: "Browser cycled", - variant: "success", - description: "Your browser has been cycled.", - }); - - afterCycleBrowser(); - }, - onError: (error: AxiosError) => { - toast({ - variant: "destructive", - title: "Failed to cycle browser", - description: error.message, - }); - - afterCycleBrowser(); - }, - }); - - const intervalRef = useRef(null); - const powerButtonTimeoutRef = useRef(null); - - useEffect(() => { - if ( - (!debugSession || !debugSession.browser_session_id) && - shouldFetchDebugSession && - workflowPermanentId - ) { - intervalRef.current = setInterval(() => { - queryClient.invalidateQueries({ - queryKey: ["debugSession", workflowPermanentId], - }); - }, 2000); - } else { - if (intervalRef.current) { - clearInterval(intervalRef.current); - intervalRef.current = null; - } - - if (debugSession) { - setActiveDebugSession(debugSession); - } - } - - return () => { - if (intervalRef.current) { - clearInterval(intervalRef.current); - } - }; - }, [debugSession, shouldFetchDebugSession, workflowPermanentId, queryClient]); - if (!workflow) { return null; } @@ -193,110 +36,18 @@ function WorkflowDebugger() { true, ); - const isFinalized = workflowRun ? statusIsFinalized(workflowRun) : null; - const interactor = workflowRun && isFinalized === false ? "agent" : "human"; - const browserTitle = interactor === "agent" ? `Browser [๐Ÿค–]` : `Browser [๐Ÿ‘ค]`; - - // ---start fya: https://github.com/frontyardart - const initialBrowserPosition = { - x: 600, - y: 132, - }; - - const windowWidth = window.innerWidth; - const rightPadding = 567; - const initialWidth = Math.max( - 512, - windowWidth - initialBrowserPosition.x - rightPadding, - ); - const initialHeight = (initialWidth / 16) * 9; - // ---end fya - return (
- { - if (!open && cycleBrowser.isPending) { - return; - } - setOpenDialogue(open); - }} - > - - - Cycle (Get a new browser) - -
- {cycleBrowser.isPending ? ( - <> - Cooking you up a fresh browser... - - - ) : ( - "Abandon this browser for a new one. Are you sure?" - )} -
-
-
- - {!cycleBrowser.isPending && ( - - - - )} - - -
-
- - - - {activeDebugSession && - activeDebugSession.browser_session_id && - !cycleBrowser.isPending ? ( - - ) : ( -
- Connecting to your browser... - -
- )} -
); } diff --git a/skyvern-frontend/src/routes/workflows/editor/WorkflowEditor.tsx b/skyvern-frontend/src/routes/workflows/editor/WorkflowEditor.tsx index c1425b4d..05dc1c25 100644 --- a/skyvern-frontend/src/routes/workflows/editor/WorkflowEditor.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/WorkflowEditor.tsx @@ -1,26 +1,15 @@ -import { useEffect } from "react"; -import { useMountEffect } from "@/hooks/useMountEffect"; -import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery"; -import { useBlockScriptStore } from "@/store/BlockScriptStore"; -import { useSidebarStore } from "@/store/SidebarStore"; -import { useWorkflowHasChangesStore } from "@/store/WorkflowHasChangesStore"; import { ReactFlowProvider } from "@xyflow/react"; import { useParams } from "react-router-dom"; import { useWorkflowQuery } from "../hooks/useWorkflowQuery"; -import { FlowRenderer } from "./FlowRenderer"; import { getElements } from "./workflowEditorUtils"; import { LogoMinimized } from "@/components/LogoMinimized"; import { WorkflowSettings } from "../types/workflowTypes"; import { useGlobalWorkflowsQuery } from "../hooks/useGlobalWorkflowsQuery"; import { getInitialParameters } from "./utils"; +import { Workspace } from "./Workspace"; function WorkflowEditor() { const { workflowPermanentId } = useParams(); - const setCollapsed = useSidebarStore((state) => { - return state.setCollapsed; - }); - const workflowChangesStore = useWorkflowHasChangesStore(); - const blockScriptStore = useBlockScriptStore(); const { data: workflow, isLoading } = useWorkflowQuery({ workflowPermanentId, @@ -29,20 +18,6 @@ function WorkflowEditor() { const { data: globalWorkflows, isLoading: isGlobalWorkflowsLoading } = useGlobalWorkflowsQuery(); - const { data: blockScripts } = useBlockScriptsQuery({ - workflowPermanentId, - }); - - useMountEffect(() => { - setCollapsed(true); - workflowChangesStore.setHasChanges(false); - }); - - useEffect(() => { - blockScriptStore.setScripts(blockScripts ?? {}); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [blockScripts]); - if (isLoading || isGlobalWorkflowsLoading) { return (
@@ -82,11 +57,12 @@ function WorkflowEditor() { return (
- diff --git a/skyvern-frontend/src/routes/workflows/editor/WorkflowHeader.tsx b/skyvern-frontend/src/routes/workflows/editor/WorkflowHeader.tsx index e077bcc9..8ab77701 100644 --- a/skyvern-frontend/src/routes/workflows/editor/WorkflowHeader.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/WorkflowHeader.tsx @@ -29,6 +29,7 @@ type Props = { parametersPanelOpen: boolean; onParametersClick: () => void; onSave: () => void; + onRun?: () => void; saving: boolean; }; @@ -37,6 +38,7 @@ function WorkflowHeader({ parametersPanelOpen, onParametersClick, onSave, + onRun, saving, }: Props) { const { title, setTitle } = useWorkflowTitleStore(); @@ -148,6 +150,7 @@ function WorkflowHeader({ + + )} + + + + + + {/* header panel */} +
{ + promote("header"); + }} + > + { + if ( + workflowPanelState.active && + workflowPanelState.content === "parameters" + ) { + closeWorkflowPanel(); + promote("header"); + } else { + setWorkflowPanelState({ + active: true, + content: "parameters", + }); + promote("dropdown"); + } + }} + onSave={async () => { + const errors = getWorkflowErrors(nodes); + if (errors.length > 0) { + toast({ + title: "Can not save workflow because of errors:", + description: ( +
+ {errors.map((error) => ( +

{error}

+ ))} +
+ ), + variant: "destructive", + }); + return; + } + await saveWorkflow.mutateAsync(); + }} + onRun={() => { + closeWorkflowPanel(); + promote("header"); + }} + /> +
+ + {/* sub panels */} + {workflowPanelState.active && ( +
{ + promote("dropdown"); + }} + > + {workflowPanelState.content === "parameters" && ( + { + promote("dropdown"); + }} + /> + )} + {workflowPanelState.content === "nodeLibrary" && ( + { + promote("dropdown"); + }} + onNodeClick={(props) => { + addNode(props); + }} + /> + )} +
+ )} + + {debugStore.isDebugMode && ( +
{ + closeWorkflowPanel(); + promote("history"); + }} + > +
+
+ +
+
+
+ )} + + {/* infinite canvas */} + setDebuggableBlockCount(c)} + onMouseDownCapture={() => promote("infiniteCanvas")} + zIndex={rankedItems.infiniteCanvas} + /> + + {/* browser */} + {showBrowser && ( + promote("browserWindow")} + > + {activeDebugSession && + activeDebugSession.browser_session_id && + !cycleBrowser.isPending ? ( + + ) : ( +
+ Connecting to your browser... + +
+ )} +
+ )} +
+ + ); +} + +export { Workspace }; diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx index 3a6e5e97..dfbac596 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx @@ -22,6 +22,7 @@ import { } from "@/routes/workflows/types/workflowTypes"; import { getInitialValues } from "@/routes/workflows/utils"; import { useDebugStore } from "@/store/useDebugStore"; +import { useWorkflowPanelStore } from "@/store/WorkflowPanelStore"; import { useWorkflowSave } from "@/store/WorkflowHasChangesStore"; import { useWorkflowSettingsStore, @@ -135,6 +136,7 @@ function NodeHeader({ workflowRunId, } = useParams(); const debugStore = useDebugStore(); + const { closeWorkflowPanel } = useWorkflowPanelStore(); const thisBlockIsPlaying = urlBlockLabel !== undefined && urlBlockLabel === blockLabel; const anyBlockIsPlaying = @@ -194,6 +196,8 @@ function NodeHeader({ const runBlock = useMutation({ mutationFn: async () => { + closeWorkflowPanel(); + await saveWorkflow.mutateAsync(); if (!workflowPermanentId) { diff --git a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx index 0dc6a262..7f71197f 100644 --- a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx @@ -7,9 +7,9 @@ import { MagnifyingGlassIcon, } from "@radix-ui/react-icons"; import { WorkflowBlockTypes } from "../../types/workflowTypes"; -import { AddNodeProps } from "../FlowRenderer"; import { WorkflowBlockNode } from "../nodes"; import { WorkflowBlockIcon } from "../nodes/WorkflowBlockIcon"; +import { AddNodeProps } from "../Workspace"; import { Input } from "@/components/ui/input"; const enableCodeBlock = @@ -243,11 +243,16 @@ const nodeLibraryItems: Array<{ ]; type Props = { + onMouseDownCapture?: () => void; onNodeClick: (props: AddNodeProps) => void; first?: boolean; }; -function WorkflowNodeLibraryPanel({ onNodeClick, first }: Props) { +function WorkflowNodeLibraryPanel({ + onMouseDownCapture, + onNodeClick, + first, +}: Props) { const workflowPanelData = useWorkflowPanelStore( (state) => state.workflowPanelState.data, ); @@ -311,7 +316,10 @@ function WorkflowNodeLibraryPanel({ onNodeClick, first }: Props) { }); return ( -
+
onMouseDownCapture?.()} + >
diff --git a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowParametersPanel.tsx b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowParametersPanel.tsx index a292ac34..3b017b5a 100644 --- a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowParametersPanel.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowParametersPanel.tsx @@ -36,7 +36,11 @@ import { getLabelForWorkflowParameterType } from "../workflowEditorUtils"; const WORKFLOW_EDIT_PANEL_WIDTH = 20 * 16; const WORKFLOW_EDIT_PANEL_GAP = 1 * 16; -function WorkflowParametersPanel() { +interface Props { + onMouseDownCapture?: () => void; +} + +function WorkflowParametersPanel({ onMouseDownCapture }: Props) { const setHasChanges = useWorkflowHasChangesStore( (state) => state.setHasChanges, ); @@ -56,7 +60,10 @@ function WorkflowParametersPanel() { const { setNodes } = useReactFlow(); return ( -
+
onMouseDownCapture?.()} + >

Parameters

diff --git a/skyvern-frontend/src/routes/workflows/hooks/useRanker.ts b/skyvern-frontend/src/routes/workflows/hooks/useRanker.ts new file mode 100644 index 00000000..33614ac0 --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/hooks/useRanker.ts @@ -0,0 +1,46 @@ +import { useState, useMemo, useCallback } from "react"; + +interface RankedItems { + [key: string]: number; +} + +interface UseRankerReturn { + rankedItems: RankedItems; + promote: (name: string) => void; + orderedNames: string[]; +} + +function useRanker(initialNames: string[]): UseRankerReturn { + const [orderedNames, setOrderedNames] = useState(initialNames); + + const rankedItems = useMemo(() => { + const items: RankedItems = {}; + const maxRank = orderedNames.length; + + orderedNames.forEach((name, index) => { + items[name] = maxRank - index; + }); + + return items; + }, [orderedNames]); + + const promote = useCallback((name: string) => { + setOrderedNames((prevNames) => { + if (!prevNames.includes(name)) { + console.warn(`Name "${name}" not found in ranked list`); + return prevNames; + } + + const filteredNames = prevNames.filter((n) => n !== name); + return [name, ...filteredNames]; + }); + }, []); + + return { + rankedItems, + promote, + orderedNames, + }; +} + +export { useRanker, type RankedItems };