From 399fd4ea74aea57f33fbecebdf03d922ed9554c0 Mon Sep 17 00:00:00 2001 From: Jonathan Dobson Date: Wed, 13 Aug 2025 15:17:04 -0400 Subject: [PATCH] =?UTF-8?q?make=20other=20script=20enabled=20blocks=20flip?= =?UTF-8?q?-to-script;=20add=20type-checked=20lis=E2=80=A6=20(#3179)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../editor/nodes/ActionNode/ActionNode.tsx | 568 +++++++------- .../nodes/ExtractionNode/ExtractionNode.tsx | 384 +++++----- .../FileDownloadNode/FileDownloadNode.tsx | 520 +++++++------ .../editor/nodes/LoginNode/LoginNode.tsx | 538 ++++++------- .../workflows/editor/nodes/NodeActionMenu.tsx | 31 +- .../editor/nodes/TaskNode/TaskNode.tsx | 706 +++++++++--------- .../editor/nodes/URLNode/URLNode.tsx | 118 +-- .../nodes/ValidationNode/ValidationNode.tsx | 296 ++++---- .../editor/nodes/components/NodeHeader.tsx | 3 + .../routes/workflows/types/workflowTypes.ts | 11 + 10 files changed, 1667 insertions(+), 1508 deletions(-) diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/ActionNode/ActionNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/ActionNode/ActionNode.tsx index e5a4ba73..21a24988 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/ActionNode/ActionNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/ActionNode/ActionNode.tsx @@ -1,3 +1,5 @@ +import { useEffect } from "react"; +import { Flippable } from "@/components/Flippable"; import { Accordion, AccordionContent, @@ -23,6 +25,7 @@ import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; import { Switch } from "@/components/ui/switch"; import { placeholders, helpTooltips } from "../../helpContent"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; +import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor"; import { WorkflowBlockInput } from "@/components/WorkflowBlockInput"; import { AppNode } from ".."; import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils"; @@ -31,6 +34,7 @@ import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow" import { RunEngineSelector } from "@/components/EngineSelector"; import { ModelSelector } from "@/components/ModelSelector"; import { useDebugStore } from "@/store/useDebugStore"; +import { useBlockScriptStore } from "@/store/BlockScriptStore"; import { cn } from "@/util/utils"; import { useParams } from "react-router-dom"; import { NodeHeader } from "../components/NodeHeader"; @@ -44,7 +48,10 @@ const navigationGoalPlaceholder = 'Input {{ name }} into "Name" field.'; function ActionNode({ id, data, type }: NodeProps) { const { updateNodeData } = useReactFlow(); + const [facing, setFacing] = useState<"front" | "back">("front"); + const blockScriptStore = useBlockScriptStore(); const { editable, debuggable, label } = data; + const script = blockScriptStore.scripts[label]; const [inputs, setInputs] = useState({ url: data.url, navigationGoal: data.navigationGoal, @@ -78,308 +85,319 @@ function ActionNode({ id, data, type }: NodeProps) { const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id }); + useEffect(() => { + setFacing(data.showCode ? "back" : "front"); + }, [data.showCode]); + return ( -
- - -
- +
+ +
-
-
-
- - -
- {isFirstWorkflowBlock ? ( -
- Tip: Use the {"+"} button to add parameters! + +
+
+
+
+ +
- ) : null} -
- - { - handleChange("url", value); - }} - value={inputs.url} - placeholder={placeholders["action"]["url"]} - className="nopan text-xs" - /> -
-
-
- - -
- { - handleChange("navigationGoal", value); - }} - value={inputs.navigationGoal} - placeholder={navigationGoalPlaceholder} - className="nopan text-xs" - /> -
-
-
- Tip: While executing the action block, Skyvern will only take one - action. -
-
-
- - - - - Advanced Settings - - -
-
- { - handleChange("model", value); - }} - /> - { - updateNodeData(id, { parameterKeys }); - }} - /> -
-
-
- + {isFirstWorkflowBlock ? ( +
+ Tip: Use the {"+"} button to add parameters!
- { - handleChange("engine", value); - }} - className="nopan w-52 text-xs" - /> -
-
-
-
- - -
- { - if (!editable) { - return; - } - handleChange( - "errorCodeMapping", - checked - ? JSON.stringify(errorMappingExampleValue, null, 2) - : "null", - ); + ) : null} +
+ + { + handleChange("url", value); + }} + value={inputs.url} + placeholder={placeholders["action"]["url"]} + className="nopan text-xs" + /> +
+
+
+ + +
+ { + handleChange("navigationGoal", value); + }} + value={inputs.navigationGoal} + placeholder={navigationGoalPlaceholder} + className="nopan text-xs" + /> +
+
+
+ Tip: While executing the action block, Skyvern will only take + one action. +
+
+
+ + + + + Advanced Settings + + +
+
+ { + handleChange("model", value); + }} + /> + { + updateNodeData(id, { parameterKeys }); }} />
- {inputs.errorCodeMapping !== "null" && ( -
- { +
+
+ +
+ { + handleChange("engine", value); + }} + className="nopan w-52 text-xs" + /> +
+
+
+
+ + +
+ { if (!editable) { return; } - handleChange("errorCodeMapping", value); + handleChange( + "errorCodeMapping", + checked + ? JSON.stringify( + errorMappingExampleValue, + null, + 2, + ) + : "null", + ); }} - className="nowheel nopan" - fontSize={8} />
- )} -
- -
-
- - + {inputs.errorCodeMapping !== "null" && ( +
+ { + if (!editable) { + return; + } + handleChange("errorCodeMapping", value); + }} + className="nowheel nopan" + fontSize={8} + /> +
+ )}
-
- { - if (!editable) { - return; - } - handleChange("continueOnFailure", checked); + +
+
+ + +
+
+ { + if (!editable) { + return; + } + handleChange("continueOnFailure", checked); + }} + /> +
+
+
+
+ + +
+
+ { + if (!editable) { + return; + } + handleChange("cacheActions", checked); + }} + /> +
+
+ +
+
+ + +
+
+ { + if (!editable) { + return; + } + handleChange("allowDownloads", checked); + }} + /> +
+
+
+
+ + +
+ { + handleChange("downloadSuffix", value); }} />
-
-
-
- - -
-
- { - if (!editable) { - return; - } - handleChange("cacheActions", checked); + +
+
+ + +
+ { + handleChange("totpIdentifier", value); }} + value={inputs.totpIdentifier ?? ""} + placeholder={placeholders["action"]["totpIdentifier"]} + className="nopan text-xs" />
-
- -
-
- - -
-
- { - if (!editable) { - return; - } - handleChange("allowDownloads", checked); +
+
+ + +
+ { + handleChange("totpVerificationUrl", value); }} + value={inputs.totpVerificationUrl ?? ""} + placeholder={placeholders["task"]["totpVerificationUrl"]} + className="nopan text-xs" />
-
-
- - -
- { - handleChange("downloadSuffix", value); - }} - /> -
- -
-
- - -
- { - handleChange("totpIdentifier", value); - }} - value={inputs.totpIdentifier ?? ""} - placeholder={placeholders["action"]["totpIdentifier"]} - className="nopan text-xs" - /> -
-
-
- - -
- { - handleChange("totpVerificationUrl", value); - }} - value={inputs.totpVerificationUrl ?? ""} - placeholder={placeholders["task"]["totpVerificationUrl"]} - className="nopan text-xs" - /> -
-
- - - + + + +
-
+ + ); } diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx index d53f529a..95c8aa85 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx @@ -1,3 +1,5 @@ +import { useEffect } from "react"; +import { Flippable } from "@/components/Flippable"; import { HelpTooltip } from "@/components/HelpTooltip"; import { Accordion, @@ -22,6 +24,7 @@ import { dataSchemaExampleValue } from "../types"; import type { ExtractionNode } from "./types"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; +import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor"; import { helpTooltips, placeholders } from "../../helpContent"; import { AppNode } from ".."; import { getAvailableOutputParameterKeys } from "../../workflowEditorUtils"; @@ -31,13 +34,17 @@ import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow" import { RunEngineSelector } from "@/components/EngineSelector"; import { ModelSelector } from "@/components/ModelSelector"; import { useDebugStore } from "@/store/useDebugStore"; +import { useBlockScriptStore } from "@/store/BlockScriptStore"; import { cn } from "@/util/utils"; import { NodeHeader } from "../components/NodeHeader"; import { useParams } from "react-router-dom"; function ExtractionNode({ id, data, type }: NodeProps) { const { updateNodeData } = useReactFlow(); + const [facing, setFacing] = useState<"front" | "back">("front"); + const blockScriptStore = useBlockScriptStore(); const { debuggable, editable, label } = data; + const script = blockScriptStore.scripts[label]; const debugStore = useDebugStore(); const elideFromDebugging = debugStore.isDebugMode && !debuggable; const { blockLabel: urlBlockLabel } = useParams(); @@ -67,193 +74,204 @@ function ExtractionNode({ id, data, type }: NodeProps) { updateNodeData(id, { [key]: value }); } - return ( -
- - -
- -
-
-
- - -
- {isFirstWorkflowBlock ? ( -
- Tip: Use the {"+"} button to add parameters! -
- ) : null} -
+ useEffect(() => { + setFacing(data.showCode ? "back" : "front"); + }, [data.showCode]); - { - if (!editable) { - return; - } - handleChange("dataExtractionGoal", value); - }} - value={inputs.dataExtractionGoal} - placeholder={placeholders["extraction"]["dataExtractionGoal"]} - className="nopan text-xs" - /> -
- { - handleChange("dataSchema", value); - }} - exampleValue={dataSchemaExampleValue} - suggestionContext={{ - data_extraction_goal: inputs.dataExtractionGoal, - current_schema: inputs.dataSchema, - }} + return ( + +
+ - - - - - Advanced Settings - - -
-
- { - handleChange("model", value); - }} - /> - { - updateNodeData(id, { parameterKeys }); - }} - /> -
-
-
- -
- { - handleChange("engine", value); - }} - className="nopan w-52 text-xs" - /> -
-
-
- - -
- { - if (!editable) { - return; - } - const value = - event.target.value === "" - ? null - : Number(event.target.value); - handleChange("maxStepsOverride", value); - }} - /> -
- -
-
- - -
-
- { - if (!editable) { - return; - } - handleChange("continueOnFailure", checked); - }} - /> -
-
-
-
- - -
-
- { - if (!editable) { - return; - } - handleChange("cacheActions", checked); - }} - /> -
-
+ +
+ +
+
+
+ +
- - - + {isFirstWorkflowBlock ? ( +
+ Tip: Use the {"+"} button to add parameters! +
+ ) : null} +
+ + { + if (!editable) { + return; + } + handleChange("dataExtractionGoal", value); + }} + value={inputs.dataExtractionGoal} + placeholder={placeholders["extraction"]["dataExtractionGoal"]} + className="nopan text-xs" + /> +
+ { + handleChange("dataSchema", value); + }} + exampleValue={dataSchemaExampleValue} + suggestionContext={{ + data_extraction_goal: inputs.dataExtractionGoal, + current_schema: inputs.dataSchema, + }} + /> + + + + + Advanced Settings + + +
+
+ { + handleChange("model", value); + }} + /> + { + updateNodeData(id, { parameterKeys }); + }} + /> +
+
+
+ +
+ { + handleChange("engine", value); + }} + className="nopan w-52 text-xs" + /> +
+
+
+ + +
+ { + if (!editable) { + return; + } + const value = + event.target.value === "" + ? null + : Number(event.target.value); + handleChange("maxStepsOverride", value); + }} + /> +
+ +
+
+ + +
+
+ { + if (!editable) { + return; + } + handleChange("continueOnFailure", checked); + }} + /> +
+
+
+
+ + +
+
+ { + if (!editable) { + return; + } + handleChange("cacheActions", checked); + }} + /> +
+
+
+
+
+
+
-
+ +
); } diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/FileDownloadNode/FileDownloadNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/FileDownloadNode/FileDownloadNode.tsx index 73bc8e52..aad4a6bb 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/FileDownloadNode/FileDownloadNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/FileDownloadNode/FileDownloadNode.tsx @@ -1,3 +1,5 @@ +import { useEffect } from "react"; +import { Flippable } from "@/components/Flippable"; import { HelpTooltip } from "@/components/HelpTooltip"; import { Accordion, @@ -11,7 +13,9 @@ import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; +import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; +import { useBlockScriptStore } from "@/store/BlockScriptStore"; import { Handle, NodeProps, @@ -44,7 +48,10 @@ const navigationGoalPlaceholder = "Tell Skyvern which file to download."; function FileDownloadNode({ id, data }: NodeProps) { const { updateNodeData } = useReactFlow(); + const [facing, setFacing] = useState<"front" | "back">("front"); + const blockScriptStore = useBlockScriptStore(); const { debuggable, editable, label } = data; + const script = blockScriptStore.scripts[label]; const debugStore = useDebugStore(); const { blockLabel: urlBlockLabel } = useParams(); const thisBlockIsPlaying = @@ -75,282 +82,297 @@ function FileDownloadNode({ id, data }: NodeProps) { updateNodeData(id, { [key]: value }); } + useEffect(() => { + setFacing(data.showCode ? "back" : "front"); + }, [data.showCode]); + return ( -
- - -
- +
+ -
-
-
-
- - -
- {isFirstWorkflowBlock ? ( -
- Tip: Use the {"+"} button to add parameters! + +
+ +
+
+
+
+ +
- ) : null} -
- { - handleChange("url", value); - }} - value={inputs.url} - placeholder={urlPlaceholder} - className="nopan text-xs" - /> -
-
-
- - -
- { - handleChange("navigationGoal", value); - }} - value={inputs.navigationGoal} - placeholder={navigationGoalPlaceholder} - className="nopan text-xs" - /> -
-
- Once the file is downloaded, this block will complete. -
-
- - - - - Advanced Settings - - -
-
- { - handleChange("model", value); - }} - /> - { - updateNodeData(id, { parameterKeys }); - }} - /> -
-
-
- + {isFirstWorkflowBlock ? ( +
+ Tip: Use the {"+"} button to add parameters!
- { - handleChange("engine", value); - }} - className="nopan w-52 text-xs" - /> -
-
-
- - + { + handleChange("url", value); + }} + value={inputs.url} + placeholder={urlPlaceholder} + className="nopan text-xs" + /> +
+
+
+ + +
+ { + handleChange("navigationGoal", value); + }} + value={inputs.navigationGoal} + placeholder={navigationGoalPlaceholder} + className="nopan text-xs" + /> +
+
+ Once the file is downloaded, this block will complete. +
+
+ + + + + Advanced Settings + + +
+
+ { + handleChange("model", value); + }} + /> + { + updateNodeData(id, { parameterKeys }); + }} />
- { - const value = - event.target.value === "" - ? null - : Number(event.target.value); - handleChange("maxStepsOverride", value); - }} - /> -
-
-
+
+
+ { + handleChange("engine", value); + }} + className="nopan w-52 text-xs" + /> +
+
+
+
- { - handleChange( - "errorCodeMapping", - checked - ? JSON.stringify(errorMappingExampleValue, null, 2) - : "null", - ); + { + const value = + event.target.value === "" + ? null + : Number(event.target.value); + handleChange("maxStepsOverride", value); }} />
- {inputs.errorCodeMapping !== "null" && ( -
- { - handleChange("errorCodeMapping", value); +
+
+
+ + +
+ { + handleChange( + "errorCodeMapping", + checked + ? JSON.stringify( + errorMappingExampleValue, + null, + 2, + ) + : "null", + ); }} - className="nowheel nopan" - fontSize={8} />
- )} -
- -
-
- - + {inputs.errorCodeMapping !== "null" && ( +
+ { + handleChange("errorCodeMapping", value); + }} + className="nowheel nopan" + fontSize={8} + /> +
+ )}
-
- { - handleChange("continueOnFailure", checked); + +
+
+ + +
+
+ { + handleChange("continueOnFailure", checked); + }} + /> +
+
+
+
+ + +
+
+ { + handleChange("cacheActions", checked); + }} + /> +
+
+ +
+
+ + +
+ { + handleChange("downloadSuffix", event.target.value); }} />
-
-
-
- - -
-
- { - handleChange("cacheActions", checked); + +
+
+ + +
+ { + handleChange("totpIdentifier", value); }} + value={inputs.totpIdentifier ?? ""} + placeholder={placeholders["download"]["totpIdentifier"]} + className="nopan text-xs" + /> +
+
+
+ + +
+ { + handleChange("totpVerificationUrl", value); + }} + value={inputs.totpVerificationUrl ?? ""} + placeholder={placeholders["task"]["totpVerificationUrl"]} + className="nopan text-xs" />
- -
-
- - -
- { - handleChange("downloadSuffix", event.target.value); - }} - /> -
- -
-
- - -
- { - handleChange("totpIdentifier", value); - }} - value={inputs.totpIdentifier ?? ""} - placeholder={placeholders["download"]["totpIdentifier"]} - className="nopan text-xs" - /> -
-
-
- - -
- { - handleChange("totpVerificationUrl", value); - }} - value={inputs.totpVerificationUrl ?? ""} - placeholder={placeholders["task"]["totpVerificationUrl"]} - className="nopan text-xs" - /> -
-
- - - + + + +
-
+ + ); } diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/LoginNode/LoginNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/LoginNode/LoginNode.tsx index 055f626c..7bdf1579 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/LoginNode/LoginNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/LoginNode/LoginNode.tsx @@ -1,3 +1,5 @@ +import { useEffect } from "react"; +import { Flippable } from "@/components/Flippable"; import { HelpTooltip } from "@/components/HelpTooltip"; import { Accordion, @@ -11,7 +13,9 @@ import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; +import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; +import { useBlockScriptStore } from "@/store/BlockScriptStore"; import { Handle, NodeProps, @@ -38,7 +42,10 @@ import { useParams } from "react-router-dom"; function LoginNode({ id, data, type }: NodeProps) { const { updateNodeData } = useReactFlow(); + const [facing, setFacing] = useState<"front" | "back">("front"); + const blockScriptStore = useBlockScriptStore(); const { debuggable, editable, label } = data; + const script = blockScriptStore.scripts[label]; const debugStore = useDebugStore(); const elideFromDebugging = debugStore.isDebugMode && !debuggable; const { blockLabel: urlBlockLabel } = useParams(); @@ -72,291 +79,304 @@ function LoginNode({ id, data, type }: NodeProps) { updateNodeData(id, { [key]: value }); } - return ( -
- - -
- -
-
-
-
- - -
- {isFirstWorkflowBlock ? ( -
- Tip: Use the {"+"} button to add parameters! -
- ) : null} -
+ useEffect(() => { + setFacing(data.showCode ? "back" : "front"); + }, [data.showCode]); - { - handleChange("url", value); - }} - value={inputs.url} - placeholder={placeholders["login"]["url"]} - className="nopan text-xs" - /> -
-
-
- - -
- { - handleChange("navigationGoal", value); - }} - value={inputs.navigationGoal} - placeholder={placeholders["login"]["navigationGoal"]} - className="nopan text-xs" - /> -
-
- - 0 - ? data.parameterKeys[0] - : undefined - } - onChange={(value) => { - if (!editable) { - return; - } - updateNodeData(id, { parameterKeys: [value] }); - }} - /> -
-
- - - - - Advanced Settings - - -
-
- { - handleChange("model", value); - }} - /> - { - updateNodeData(id, { parameterKeys }); - }} - /> + return ( + +
+ + +
+ +
+
+
+
+ +
-
- - { - handleChange("completeCriterion", value); - }} - value={inputs.completeCriterion} - className="nopan text-xs" - /> -
- -
-
- + {isFirstWorkflowBlock ? ( +
+ Tip: Use the {"+"} button to add parameters!
- { - handleChange("engine", value); - }} - className="nopan w-52 text-xs" - /> -
-
-
- - + + { + handleChange("url", value); + }} + value={inputs.url} + placeholder={placeholders["login"]["url"]} + className="nopan text-xs" + /> +
+
+
+ + +
+ { + handleChange("navigationGoal", value); + }} + value={inputs.navigationGoal} + placeholder={placeholders["login"]["navigationGoal"]} + className="nopan text-xs" + /> +
+
+ + 0 + ? data.parameterKeys[0] + : undefined + } + onChange={(value) => { + if (!editable) { + return; + } + updateNodeData(id, { parameterKeys: [value] }); + }} + /> +
+
+ + + + + Advanced Settings + + +
+
+ { + handleChange("model", value); + }} + /> + { + updateNodeData(id, { parameterKeys }); + }} />
- { - const value = - event.target.value === "" - ? null - : Number(event.target.value); - handleChange("maxStepsOverride", value); - }} - /> -
-
-
+
+ + { + handleChange("completeCriterion", value); + }} + value={inputs.completeCriterion} + className="nopan text-xs" + /> +
+ +
+
+ { + handleChange("engine", value); + }} + className="nopan w-52 text-xs" + /> +
+
+
+
- { - handleChange( - "errorCodeMapping", - checked - ? JSON.stringify(errorMappingExampleValue, null, 2) - : "null", - ); + { + const value = + event.target.value === "" + ? null + : Number(event.target.value); + handleChange("maxStepsOverride", value); }} />
- {inputs.errorCodeMapping !== "null" && ( -
- { - handleChange("errorCodeMapping", value); +
+
+
+ + +
+ { + handleChange( + "errorCodeMapping", + checked + ? JSON.stringify( + errorMappingExampleValue, + null, + 2, + ) + : "null", + ); }} - className="nowheel nopan" - fontSize={8} />
- )} -
- -
-
- - + {inputs.errorCodeMapping !== "null" && ( +
+ { + handleChange("errorCodeMapping", value); + }} + className="nowheel nopan" + fontSize={8} + /> +
+ )}
-
- { - handleChange("continueOnFailure", checked); + +
+
+ + +
+
+ { + handleChange("continueOnFailure", checked); + }} + /> +
+
+
+
+ + +
+
+ { + handleChange("cacheActions", checked); + }} + /> +
+
+ +
+
+ + +
+ { + handleChange("totpIdentifier", value); }} + value={inputs.totpIdentifier ?? ""} + placeholder={placeholders["login"]["totpIdentifier"]} + className="nopan text-xs" />
-
-
-
- - -
-
- { - handleChange("cacheActions", checked); +
+
+ + +
+ { + handleChange("totpVerificationUrl", value); }} + value={inputs.totpVerificationUrl ?? ""} + placeholder={placeholders["login"]["totpVerificationUrl"]} + className="nopan text-xs" />
- -
-
- - -
- { - handleChange("totpIdentifier", value); - }} - value={inputs.totpIdentifier ?? ""} - placeholder={placeholders["login"]["totpIdentifier"]} - className="nopan text-xs" - /> -
-
-
- - -
- { - handleChange("totpVerificationUrl", value); - }} - value={inputs.totpVerificationUrl ?? ""} - placeholder={placeholders["login"]["totpVerificationUrl"]} - className="nopan text-xs" - /> -
-
- - - + + + +
-
+ + ); } diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/NodeActionMenu.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/NodeActionMenu.tsx index b2df436d..3b0a119c 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/NodeActionMenu.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/NodeActionMenu.tsx @@ -10,11 +10,16 @@ import { DotsHorizontalIcon } from "@radix-ui/react-icons"; import { OrgWalled } from "@/components/Orgwalled"; type Props = { + isScriptable?: boolean; onDelete: () => void; onShowScript?: () => void; }; -function NodeActionMenu({ onDelete, onShowScript }: Props) { +function NodeActionMenu({ + isScriptable = false, + onDelete, + onShowScript, +}: Props) { return ( @@ -30,17 +35,19 @@ function NodeActionMenu({ onDelete, onShowScript }: Props) { > Delete Block - - {onShowScript && ( - { - onShowScript(); - }} - > - Show Script - - )} - + {isScriptable && ( + + {onShowScript && ( + { + onShowScript(); + }} + > + Show Script + + )} + + )} ); diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx index 89a25e31..a54ea132 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/TaskNode/TaskNode.tsx @@ -1,3 +1,5 @@ +import { useEffect } from "react"; +import { Flippable } from "@/components/Flippable"; import { HelpTooltip } from "@/components/HelpTooltip"; import { Accordion, @@ -12,7 +14,9 @@ import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; import { WorkflowBlockInput } from "@/components/WorkflowBlockInput"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; +import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; +import { useBlockScriptStore } from "@/store/BlockScriptStore"; import { Handle, NodeProps, @@ -39,7 +43,10 @@ import { useParams } from "react-router-dom"; function TaskNode({ id, data, type }: NodeProps) { const { updateNodeData } = useReactFlow(); + const [facing, setFacing] = useState<"front" | "back">("front"); + const blockScriptStore = useBlockScriptStore(); const { debuggable, editable, label } = data; + const script = blockScriptStore.scripts[label]; const debugStore = useDebugStore(); const elideFromDebugging = debugStore.isDebugMode && !debuggable; const { blockLabel: urlBlockLabel } = useParams(); @@ -78,377 +85,390 @@ function TaskNode({ id, data, type }: NodeProps) { updateNodeData(id, { [key]: value }); } + useEffect(() => { + setFacing(data.showCode ? "back" : "front"); + }, [data.showCode]); + return ( -
- - -
- +
+ - - - Content - -
-
-
-
- - -
- {isFirstWorkflowBlock ? ( -
- Tip: Use the {"+"} button to add parameters! + +
+ + + + Content + +
+
+
+
+ +
- ) : null} -
- { - handleChange("url", value); - }} - value={inputs.url} - placeholder={placeholders["task"]["url"]} - className="nopan text-xs" - /> -
-
-
- - + Tip: Use the {"+"} button to add parameters! +
+ ) : null} +
+ { + handleChange("url", value); + }} + value={inputs.url} + placeholder={placeholders["task"]["url"]} + className="nopan text-xs" />
- { - handleChange("navigationGoal", value); - }} - value={inputs.navigationGoal} - placeholder={placeholders["task"]["navigationGoal"]} - className="nopan text-xs" - /> -
-
- { - updateNodeData(id, { parameterKeys }); - }} - /> -
-
- - - - Extraction - -
-
-
- - -
- { - handleChange("dataExtractionGoal", value); - }} - value={inputs.dataExtractionGoal} - placeholder={placeholders["task"]["dataExtractionGoal"]} - className="nopan text-xs" - /> -
- { - handleChange("dataSchema", value); - }} - value={inputs.dataSchema} - suggestionContext={{ - data_extraction_goal: inputs.dataExtractionGoal, - current_schema: inputs.dataSchema, - navigation_goal: inputs.navigationGoal, - }} - /> -
-
-
- - Advanced Settings - -
-
- - { - handleChange("completeCriterion", value); - }} - value={inputs.completeCriterion} - className="nopan text-xs" - /> -
- - { - handleChange("model", value); - }} - /> -
-
- -
- { - handleChange("engine", value); - }} - className="nopan w-52 text-xs" - /> -
-
-
- - -
- { - const value = - event.target.value === "" - ? null - : Number(event.target.value); - handleChange("maxStepsOverride", value); - }} - /> -
-
-
+
-
+ { + handleChange("navigationGoal", value); + }} + value={inputs.navigationGoal} + placeholder={placeholders["task"]["navigationGoal"]} + className="nopan text-xs" + /> +
+
+ { + updateNodeData(id, { parameterKeys }); + }} + /> +
+
+ + + + Extraction + +
+
+
+
- { - handleChange( - "errorCodeMapping", - checked - ? JSON.stringify(errorMappingExampleValue, null, 2) - : "null", - ); + { + handleChange("dataExtractionGoal", value); }} + value={inputs.dataExtractionGoal} + placeholder={placeholders["task"]["dataExtractionGoal"]} + className="nopan text-xs" />
- {inputs.errorCodeMapping !== "null" && ( -
- { - handleChange("errorCodeMapping", value); - }} - className="nowheel nopan" - fontSize={8} - /> -
- )} + { + handleChange("dataSchema", value); + }} + value={inputs.dataSchema} + suggestionContext={{ + data_extraction_goal: inputs.dataExtractionGoal, + current_schema: inputs.dataSchema, + navigation_goal: inputs.navigationGoal, + }} + />
- -
-
-
- - - + + + +
-
+ + ); } diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/URLNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/URLNode.tsx index be057278..848caed3 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/URLNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/URLNode.tsx @@ -1,10 +1,14 @@ +import { useEffect } from "react"; +import { Flippable } from "@/components/Flippable"; import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react"; import type { URLNode } from "./types"; import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; import { useState } from "react"; import { Label } from "@/components/ui/label"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; +import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor"; import { placeholders } from "../../helpContent"; +import { useBlockScriptStore } from "@/store/BlockScriptStore"; import { useDebugStore } from "@/store/useDebugStore"; import { cn } from "@/util/utils"; import { NodeHeader } from "../components/NodeHeader"; @@ -12,7 +16,10 @@ import { useParams } from "react-router-dom"; function URLNode({ id, data, type }: NodeProps) { const { updateNodeData } = useReactFlow(); + const [facing, setFacing] = useState<"front" | "back">("front"); + const blockScriptStore = useBlockScriptStore(); const { debuggable, editable, label } = data; + const script = blockScriptStore.scripts[label]; const debugStore = useDebugStore(); const elideFromDebugging = debugStore.isDebugMode && !debuggable; const { blockLabel: urlBlockLabel } = useParams(); @@ -32,62 +39,73 @@ function URLNode({ id, data, type }: NodeProps) { updateNodeData(id, { [key]: value }); } + useEffect(() => { + setFacing(data.showCode ? "back" : "front"); + }, [data.showCode]); + return ( -
- - -
- +
+ -
-
-
- - {isFirstWorkflowBlock ? ( -
- Tip: Use the {"+"} button to add parameters! -
- ) : null} + +
+ +
+
+
+ + {isFirstWorkflowBlock ? ( +
+ Tip: Use the {"+"} button to add parameters! +
+ ) : null} +
+ { + handleChange("url", value); + }} + value={inputs.url} + placeholder={placeholders[type]["url"]} + className="nopan text-xs" + />
- { - handleChange("url", value); - }} - value={inputs.url} - placeholder={placeholders[type]["url"]} - className="nopan text-xs" - />
-
+ + ); } diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/ValidationNode/ValidationNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/ValidationNode/ValidationNode.tsx index c8ba95ad..ead409c8 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/ValidationNode/ValidationNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/ValidationNode/ValidationNode.tsx @@ -1,3 +1,5 @@ +import { useEffect } from "react"; +import { Flippable } from "@/components/Flippable"; import { HelpTooltip } from "@/components/HelpTooltip"; import { Accordion, @@ -10,7 +12,9 @@ import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { Switch } from "@/components/ui/switch"; import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; +import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor"; import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; +import { useBlockScriptStore } from "@/store/BlockScriptStore"; import { Handle, NodeProps, @@ -35,7 +39,10 @@ import { useParams } from "react-router-dom"; function ValidationNode({ id, data, type }: NodeProps) { const { updateNodeData } = useReactFlow(); + const [facing, setFacing] = useState<"front" | "back">("front"); + const blockScriptStore = useBlockScriptStore(); const { debuggable, editable, label } = data; + const script = blockScriptStore.scripts[label]; const debugStore = useDebugStore(); const elideFromDebugging = debugStore.isDebugMode && !debuggable; const { blockLabel: urlBlockLabel } = useParams(); @@ -61,162 +68,177 @@ function ValidationNode({ id, data, type }: NodeProps) { const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id }); + useEffect(() => { + setFacing(data.showCode ? "back" : "front"); + }, [data.showCode]); + return ( -
- - -
- +
+ -
-
- - {isFirstWorkflowBlock ? ( -
- Tip: Use the {"+"} button to add parameters! -
- ) : null} -
- +
+ { - handleChange("completeCriterion", value); - }} - value={inputs.completeCriterion} - className="nopan text-xs" + totpIdentifier={null} + totpUrl={null} + type={type} /> -
-
- - { - handleChange("terminateCriterion", value); - }} - value={inputs.terminateCriterion} - className="nopan text-xs" - /> -
- - - - - Advanced Settings - - -
-
- { - handleChange("model", value); - }} - /> - { - updateNodeData(id, { parameterKeys }); - }} - /> +
+
+ + {isFirstWorkflowBlock ? ( +
+ Tip: Use the {"+"} button to add parameters!
-
-
-
- - -
- { - if (!editable) { - return; - } - handleChange( - "errorCodeMapping", - checked - ? JSON.stringify(errorMappingExampleValue, null, 2) - : "null", - ); + ) : null} +
+ { + handleChange("completeCriterion", value); + }} + value={inputs.completeCriterion} + className="nopan text-xs" + /> +
+
+ + { + handleChange("terminateCriterion", value); + }} + value={inputs.terminateCriterion} + className="nopan text-xs" + /> +
+ + + + + Advanced Settings + + +
+
+ { + handleChange("model", value); + }} + /> + { + updateNodeData(id, { parameterKeys }); }} />
- {inputs.errorCodeMapping !== "null" && ( -
- { +
+
+
+ + +
+ { if (!editable) { return; } - handleChange("errorCodeMapping", value); + handleChange( + "errorCodeMapping", + checked + ? JSON.stringify( + errorMappingExampleValue, + null, + 2, + ) + : "null", + ); }} - className="nowheel nopan" - fontSize={8} />
- )} -
- -
-
- - + {inputs.errorCodeMapping !== "null" && ( +
+ { + if (!editable) { + return; + } + handleChange("errorCodeMapping", value); + }} + className="nowheel nopan" + fontSize={8} + /> +
+ )}
-
- { - if (!editable) { - return; + +
+
+ + + /> +
+
+ { + if (!editable) { + return; + } + updateNodeData(id, { continueOnFailure: checked }); + }} + /> +
-
- - - + + + +
-
+ + ); } 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 dfbac596..73b74ba8 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeHeader.tsx @@ -17,6 +17,7 @@ import { useDebugSessionQuery } from "@/routes/workflows/hooks/useDebugSessionQu import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery"; import { debuggableWorkflowBlockTypes, + scriptableWorkflowBlockTypes, type WorkflowBlockType, type WorkflowApiResponse, } from "@/routes/workflows/types/workflowTypes"; @@ -154,6 +155,7 @@ function NodeHeader({ const queryClient = useQueryClient(); const location = useLocation(); const isDebuggable = debuggableWorkflowBlockTypes.has(type); + const isScriptable = scriptableWorkflowBlockTypes.has(type); const { data: workflowRun } = useWorkflowRunQuery(); const workflowRunIsRunningOrQueued = workflowRun && statusIsRunningOrQueued(workflowRun); @@ -414,6 +416,7 @@ function NodeHeader({ })} > { deleteNodeCallback(nodeId); }} diff --git a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts index ad04ba38..667ccc8a 100644 --- a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts +++ b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts @@ -228,6 +228,17 @@ export const debuggableWorkflowBlockTypes: Set = new Set([ "validation", ]); +export const scriptableWorkflowBlockTypes: Set = new Set([ + "action", + "extraction", + "file_download", + "goto_url", + "login", + "navigation", + "task", + "validation", +]); + export function isTaskVariantBlock(item: { block_type: WorkflowBlockType; }): boolean {