From 32f34ad3628c23993b4a8ade89de637c6aa440e8 Mon Sep 17 00:00:00 2001 From: Shuchang Zheng Date: Sat, 1 Feb 2025 05:14:51 +0800 Subject: [PATCH] url block (#1684) --- skyvern-frontend/src/api/types.ts | 1 - .../src/components/ProxySelector.tsx | 3 - .../routes/workflows/editor/helpContent.ts | 5 + .../editor/nodes/URLNode/URLNode.tsx | 103 ++++++++++++++++++ .../workflows/editor/nodes/URLNode/types.ts | 19 ++++ .../editor/nodes/WorkflowBlockIcon.tsx | 4 + .../routes/workflows/editor/nodes/index.ts | 6 +- .../routes/workflows/editor/nodes/types.ts | 1 + .../panels/WorkflowNodeLibraryPanel.tsx | 11 ++ .../workflows/editor/workflowEditorUtils.ts | 40 +++++++ .../routes/workflows/types/workflowTypes.ts | 9 +- .../workflows/types/workflowYamlTypes.ts | 8 +- skyvern/forge/sdk/schemas/tasks.py | 6 +- 13 files changed, 204 insertions(+), 12 deletions(-) create mode 100644 skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/URLNode.tsx create mode 100644 skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/types.ts diff --git a/skyvern-frontend/src/api/types.ts b/skyvern-frontend/src/api/types.ts index 7a9236405b..07479917ec 100644 --- a/skyvern-frontend/src/api/types.ts +++ b/skyvern-frontend/src/api/types.ts @@ -37,7 +37,6 @@ export const ProxyLocation = { ResidentialJP: "RESIDENTIAL_JP", ResidentialGB: "RESIDENTIAL_GB", ResidentialFR: "RESIDENTIAL_FR", - ResidentialDE: "RESIDENTIAL_DE", None: "NONE", } as const; diff --git a/skyvern-frontend/src/components/ProxySelector.tsx b/skyvern-frontend/src/components/ProxySelector.tsx index a55840b064..dfe1b05d79 100644 --- a/skyvern-frontend/src/components/ProxySelector.tsx +++ b/skyvern-frontend/src/components/ProxySelector.tsx @@ -39,9 +39,6 @@ function ProxySelector({ value, onChange, className }: Props) { Residential (France) - - Residential (Germany) - ); diff --git a/skyvern-frontend/src/routes/workflows/editor/helpContent.ts b/skyvern-frontend/src/routes/workflows/editor/helpContent.ts index 7b7f5e54d3..7995e855a3 100644 --- a/skyvern-frontend/src/routes/workflows/editor/helpContent.ts +++ b/skyvern-frontend/src/routes/workflows/editor/helpContent.ts @@ -101,6 +101,7 @@ export const helpTooltips = { fileUrl: "The URL from which the file will be downloaded", jsonSchema: "Specify a format for the extracted information from the file", }, + url: baseHelpTooltipContent, }; export const placeholders = { @@ -140,4 +141,8 @@ export const placeholders = { fileUrl: basePlaceholderContent, wait: basePlaceholderContent, pdfParser: basePlaceholderContent, + url: { + ...basePlaceholderContent, + url: "(required) Navigate to this URL: https://...", + }, }; diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/URLNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/URLNode.tsx new file mode 100644 index 0000000000..02cbae5d22 --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/URLNode.tsx @@ -0,0 +1,103 @@ +import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react"; +import type { URLNode } from "./types"; +import { useIsFirstBlockInWorkflow } from "../../hooks/useIsFirstNodeInWorkflow"; +import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback"; +import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler"; +import { useState } from "react"; +import { WorkflowBlockIcon } from "../WorkflowBlockIcon"; +import { WorkflowBlockTypes } from "@/routes/workflows/types/workflowTypes"; +import { EditableNodeTitle } from "../components/EditableNodeTitle"; +import { NodeActionMenu } from "../NodeActionMenu"; +import { Label } from "@/components/ui/label"; +import { WorkflowBlockInputTextarea } from "@/components/WorkflowBlockInputTextarea"; +import { placeholders } from "../../helpContent"; + +function URLNode({ id, data, type }: NodeProps) { + const { updateNodeData } = useReactFlow(); + const { editable } = data; + const deleteNodeCallback = useDeleteNodeCallback(); + const [label, setLabel] = useNodeLabelChangeHandler({ + id, + initialValue: data.label, + }); + const isFirstWorkflowBlock = useIsFirstBlockInWorkflow({ id }); + + const [inputs, setInputs] = useState({ + url: data.url, + }); + + function handleChange(key: string, value: unknown) { + if (!editable) { + return; + } + setInputs({ ...inputs, [key]: value }); + updateNodeData(id, { [key]: value }); + } + + return ( +
+ + +
+
+
+
+ +
+
+ + Go to URL Block +
+
+ { + deleteNodeCallback(id); + }} + /> +
+
+
+
+ + {isFirstWorkflowBlock ? ( +
+ Tip: Use the {"+"} button to add parameters! +
+ ) : null} +
+ { + handleChange("url", value); + }} + value={inputs.url} + placeholder={placeholders[type]["url"]} + className="nopan text-xs" + /> +
+
+
+
+ ); +} + +export { URLNode }; diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/types.ts new file mode 100644 index 0000000000..937108c1d6 --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/URLNode/types.ts @@ -0,0 +1,19 @@ +import { Node } from "@xyflow/react"; +import { NodeBaseData } from "../types"; + +export type URLNodeData = NodeBaseData & { + url: string; +}; + +export type URLNode = Node; + +export const urlNodeDefaultData: URLNodeData = { + label: "", + continueOnFailure: false, + url: "", + editable: true, +}; + +export function isUrlNode(node: Node): node is URLNode { + return node.type === "url"; +} diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/WorkflowBlockIcon.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/WorkflowBlockIcon.tsx index 735bafaa48..a0f1d621a8 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/WorkflowBlockIcon.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/WorkflowBlockIcon.tsx @@ -6,6 +6,7 @@ import { CursorTextIcon, DownloadIcon, EnvelopeClosedIcon, + ExternalLinkIcon, FileTextIcon, ListBulletIcon, LockOpen1Icon, @@ -72,6 +73,9 @@ function WorkflowBlockIcon({ workflowBlockType, className }: Props) { case "pdf_parser": { return ; } + case "goto_url": { + return ; + } } } diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/index.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/index.ts index 9228e69ce0..664dbef7ae 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/index.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/index.ts @@ -37,6 +37,8 @@ import { PDFParserNode } from "./PDFParserNode/types"; import { PDFParserNode as PDFParserNodeComponent } from "./PDFParserNode/PDFParserNode"; import { Taskv2Node } from "./Taskv2Node/types"; import { Taskv2Node as Taskv2NodeComponent } from "./Taskv2Node/Taskv2Node"; +import { URLNode } from "./URLNode/types"; +import { URLNode as URLNodeComponent } from "./URLNode/URLNode"; export type UtilityNode = StartNode | NodeAdderNode; @@ -57,7 +59,8 @@ export type WorkflowBlockNode = | WaitNode | FileDownloadNode | PDFParserNode - | Taskv2Node; + | Taskv2Node + | URLNode; export function isUtilityNode(node: AppNode): node is UtilityNode { return node.type === "nodeAdder" || node.type === "start"; @@ -89,4 +92,5 @@ export const nodeTypes = { fileDownload: memo(FileDownloadNodeComponent), pdfParser: memo(PDFParserNodeComponent), taskv2: memo(Taskv2NodeComponent), + url: memo(URLNodeComponent), } as const; diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/types.ts b/skyvern-frontend/src/routes/workflows/editor/nodes/types.ts index 34161db126..c6f1e7843d 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/types.ts +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/types.ts @@ -47,4 +47,5 @@ export const workflowBlockTitle: { wait: "Wait", pdf_parser: "PDF Parser", task_v2: "Task v2", + goto_url: "Go to URL", }; diff --git a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx index 9bccac3417..8cf7eda328 100644 --- a/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/panels/WorkflowNodeLibraryPanel.tsx @@ -78,6 +78,17 @@ const nodeLibraryItems: Array<{ title: "Task v2 Block", description: "Runs a Skyvern v2 Task", }, + { + nodeType: "url", + icon: ( + + ), + title: "Go to URL Block", + description: "Navigates to a URL", + }, { nodeType: "textPrompt", icon: ( diff --git a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts index 7d36ae8f1d..fa3a085e24 100644 --- a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts +++ b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts @@ -34,6 +34,7 @@ import { FileDownloadBlockYAML, PDFParserBlockYAML, Taskv2BlockYAML, + URLBlockYAML, } from "../types/workflowYamlTypes"; import { EMAIL_BLOCK_SENDER, @@ -88,6 +89,7 @@ import { fileDownloadNodeDefaultData } from "./nodes/FileDownloadNode/types"; import { ProxyLocation } from "@/api/types"; import { pdfParserNodeDefaultData } from "./nodes/PDFParserNode/types"; import { taskv2NodeDefaultData } from "./nodes/Taskv2Node/types"; +import { urlNodeDefaultData } from "./nodes/URLNode/types"; export const NEW_NODE_LABEL_PREFIX = "block_"; @@ -450,6 +452,18 @@ function convertToNode( }, }; } + + case "goto_url": { + return { + ...identifiers, + ...common, + type: "url", + data: { + ...commonData, + url: block.url, + }, + }; + } } } @@ -843,6 +857,17 @@ function createNode( }, }; } + case "url": { + return { + ...identifiers, + ...common, + type: "url", + data: { + ...urlNodeDefaultData, + label, + }, + }; + } } } @@ -1094,6 +1119,13 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML { json_schema: JSONParseSafe(node.data.jsonSchema), }; } + case "url": { + return { + ...base, + block_type: "goto_url", + url: node.data.url, + }; + } default: { throw new Error("Invalid node type for getWorkflowBlock"); } @@ -1754,6 +1786,14 @@ function convertBlocksToBlockYAML( }; return blockYaml; } + case "goto_url": { + const blockYaml: URLBlockYAML = { + ...base, + block_type: "goto_url", + url: block.url, + }; + return blockYaml; + } } }); } diff --git a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts index 577a5a0110..d3784f7a04 100644 --- a/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts +++ b/skyvern-frontend/src/routes/workflows/types/workflowTypes.ts @@ -156,7 +156,8 @@ export type WorkflowBlock = | WaitBlock | FileDownloadBlock | PDFParserBlock - | Taskv2Block; + | Taskv2Block + | URLBlock; export const WorkflowBlockTypes = { Task: "task", @@ -176,6 +177,7 @@ export const WorkflowBlockTypes = { FileDownload: "file_download", PDFParser: "pdf_parser", Taskv2: "task_v2", + URL: "goto_url", } as const; export function isTaskVariantBlock(item: { @@ -389,6 +391,11 @@ export type PDFParserBlock = WorkflowBlockBase & { json_schema: Record | null; }; +export type URLBlock = WorkflowBlockBase & { + block_type: "goto_url"; + url: string; +}; + export type WorkflowDefinition = { parameters: Array; blocks: Array; diff --git a/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts index 339c81bb90..d2ad353bf6 100644 --- a/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts +++ b/skyvern-frontend/src/routes/workflows/types/workflowYamlTypes.ts @@ -99,7 +99,8 @@ export type BlockYAML = | WaitBlockYAML | FileDownloadBlockYAML | PDFParserBlockYAML - | Taskv2BlockYAML; + | Taskv2BlockYAML + | URLBlockYAML; export type BlockYAMLBase = { block_type: WorkflowBlockType; @@ -283,3 +284,8 @@ export type PDFParserBlockYAML = BlockYAMLBase & { file_url: string; json_schema: Record | null; }; + +export type URLBlockYAML = BlockYAMLBase & { + block_type: "goto_url"; + url: string; +}; diff --git a/skyvern/forge/sdk/schemas/tasks.py b/skyvern/forge/sdk/schemas/tasks.py index 104383a7a3..47e0da2f46 100644 --- a/skyvern/forge/sdk/schemas/tasks.py +++ b/skyvern/forge/sdk/schemas/tasks.py @@ -25,7 +25,6 @@ class ProxyLocation(StrEnum): RESIDENTIAL_IN = "RESIDENTIAL_IN" RESIDENTIAL_JP = "RESIDENTIAL_JP" RESIDENTIAL_FR = "RESIDENTIAL_FR" - RESIDENTIAL_DE = "RESIDENTIAL_DE" NONE = "NONE" @@ -64,14 +63,11 @@ def get_tzinfo_from_proxy(proxy_location: ProxyLocation) -> ZoneInfo | None: return ZoneInfo("Asia/Kolkata") if proxy_location == ProxyLocation.RESIDENTIAL_JP: - return ZoneInfo("Asia/Tokyo") + return ZoneInfo("Asia/Kolkata") if proxy_location == ProxyLocation.RESIDENTIAL_FR: return ZoneInfo("Europe/Paris") - if proxy_location == ProxyLocation.RESIDENTIAL_DE: - return ZoneInfo("Europe/Berlin") - return None