url block (#1684)
This commit is contained in:
@@ -37,7 +37,6 @@ export const ProxyLocation = {
|
|||||||
ResidentialJP: "RESIDENTIAL_JP",
|
ResidentialJP: "RESIDENTIAL_JP",
|
||||||
ResidentialGB: "RESIDENTIAL_GB",
|
ResidentialGB: "RESIDENTIAL_GB",
|
||||||
ResidentialFR: "RESIDENTIAL_FR",
|
ResidentialFR: "RESIDENTIAL_FR",
|
||||||
ResidentialDE: "RESIDENTIAL_DE",
|
|
||||||
None: "NONE",
|
None: "NONE",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -39,9 +39,6 @@ function ProxySelector({ value, onChange, className }: Props) {
|
|||||||
<SelectItem value={ProxyLocation.ResidentialFR}>
|
<SelectItem value={ProxyLocation.ResidentialFR}>
|
||||||
Residential (France)
|
Residential (France)
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value={ProxyLocation.ResidentialDE}>
|
|
||||||
Residential (Germany)
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ export const helpTooltips = {
|
|||||||
fileUrl: "The URL from which the file will be downloaded",
|
fileUrl: "The URL from which the file will be downloaded",
|
||||||
jsonSchema: "Specify a format for the extracted information from the file",
|
jsonSchema: "Specify a format for the extracted information from the file",
|
||||||
},
|
},
|
||||||
|
url: baseHelpTooltipContent,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const placeholders = {
|
export const placeholders = {
|
||||||
@@ -140,4 +141,8 @@ export const placeholders = {
|
|||||||
fileUrl: basePlaceholderContent,
|
fileUrl: basePlaceholderContent,
|
||||||
wait: basePlaceholderContent,
|
wait: basePlaceholderContent,
|
||||||
pdfParser: basePlaceholderContent,
|
pdfParser: basePlaceholderContent,
|
||||||
|
url: {
|
||||||
|
...basePlaceholderContent,
|
||||||
|
url: "(required) Navigate to this URL: https://...",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<URLNode>) {
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Bottom}
|
||||||
|
id="a"
|
||||||
|
className="opacity-0"
|
||||||
|
/>
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Top}
|
||||||
|
id="b"
|
||||||
|
className="opacity-0"
|
||||||
|
/>
|
||||||
|
<div className="w-[30rem] space-y-4 rounded-lg bg-slate-elevation3 px-6 py-4">
|
||||||
|
<div className="flex h-[2.75rem] justify-between">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="flex h-[2.75rem] w-[2.75rem] items-center justify-center rounded border border-slate-600">
|
||||||
|
<WorkflowBlockIcon
|
||||||
|
workflowBlockType={WorkflowBlockTypes.URL}
|
||||||
|
className="size-6"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<EditableNodeTitle
|
||||||
|
value={label}
|
||||||
|
editable={editable}
|
||||||
|
onChange={setLabel}
|
||||||
|
titleClassName="text-base"
|
||||||
|
inputClassName="text-base"
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-slate-400">Go to URL Block</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<NodeActionMenu
|
||||||
|
onDelete={() => {
|
||||||
|
deleteNodeCallback(id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<Label className="text-xs text-slate-300">URL</Label>
|
||||||
|
{isFirstWorkflowBlock ? (
|
||||||
|
<div className="flex justify-end text-xs text-slate-400">
|
||||||
|
Tip: Use the {"+"} button to add parameters!
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<WorkflowBlockInputTextarea
|
||||||
|
nodeId={id}
|
||||||
|
onChange={(value) => {
|
||||||
|
handleChange("url", value);
|
||||||
|
}}
|
||||||
|
value={inputs.url}
|
||||||
|
placeholder={placeholders[type]["url"]}
|
||||||
|
className="nopan text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { URLNode };
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Node } from "@xyflow/react";
|
||||||
|
import { NodeBaseData } from "../types";
|
||||||
|
|
||||||
|
export type URLNodeData = NodeBaseData & {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type URLNode = Node<URLNodeData, "url">;
|
||||||
|
|
||||||
|
export const urlNodeDefaultData: URLNodeData = {
|
||||||
|
label: "",
|
||||||
|
continueOnFailure: false,
|
||||||
|
url: "",
|
||||||
|
editable: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function isUrlNode(node: Node): node is URLNode {
|
||||||
|
return node.type === "url";
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
CursorTextIcon,
|
CursorTextIcon,
|
||||||
DownloadIcon,
|
DownloadIcon,
|
||||||
EnvelopeClosedIcon,
|
EnvelopeClosedIcon,
|
||||||
|
ExternalLinkIcon,
|
||||||
FileTextIcon,
|
FileTextIcon,
|
||||||
ListBulletIcon,
|
ListBulletIcon,
|
||||||
LockOpen1Icon,
|
LockOpen1Icon,
|
||||||
@@ -72,6 +73,9 @@ function WorkflowBlockIcon({ workflowBlockType, className }: Props) {
|
|||||||
case "pdf_parser": {
|
case "pdf_parser": {
|
||||||
return <FileTextIcon className={className} />;
|
return <FileTextIcon className={className} />;
|
||||||
}
|
}
|
||||||
|
case "goto_url": {
|
||||||
|
return <ExternalLinkIcon className={className} />;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ import { PDFParserNode } from "./PDFParserNode/types";
|
|||||||
import { PDFParserNode as PDFParserNodeComponent } from "./PDFParserNode/PDFParserNode";
|
import { PDFParserNode as PDFParserNodeComponent } from "./PDFParserNode/PDFParserNode";
|
||||||
import { Taskv2Node } from "./Taskv2Node/types";
|
import { Taskv2Node } from "./Taskv2Node/types";
|
||||||
import { Taskv2Node as Taskv2NodeComponent } from "./Taskv2Node/Taskv2Node";
|
import { Taskv2Node as Taskv2NodeComponent } from "./Taskv2Node/Taskv2Node";
|
||||||
|
import { URLNode } from "./URLNode/types";
|
||||||
|
import { URLNode as URLNodeComponent } from "./URLNode/URLNode";
|
||||||
|
|
||||||
export type UtilityNode = StartNode | NodeAdderNode;
|
export type UtilityNode = StartNode | NodeAdderNode;
|
||||||
|
|
||||||
@@ -57,7 +59,8 @@ export type WorkflowBlockNode =
|
|||||||
| WaitNode
|
| WaitNode
|
||||||
| FileDownloadNode
|
| FileDownloadNode
|
||||||
| PDFParserNode
|
| PDFParserNode
|
||||||
| Taskv2Node;
|
| Taskv2Node
|
||||||
|
| URLNode;
|
||||||
|
|
||||||
export function isUtilityNode(node: AppNode): node is UtilityNode {
|
export function isUtilityNode(node: AppNode): node is UtilityNode {
|
||||||
return node.type === "nodeAdder" || node.type === "start";
|
return node.type === "nodeAdder" || node.type === "start";
|
||||||
@@ -89,4 +92,5 @@ export const nodeTypes = {
|
|||||||
fileDownload: memo(FileDownloadNodeComponent),
|
fileDownload: memo(FileDownloadNodeComponent),
|
||||||
pdfParser: memo(PDFParserNodeComponent),
|
pdfParser: memo(PDFParserNodeComponent),
|
||||||
taskv2: memo(Taskv2NodeComponent),
|
taskv2: memo(Taskv2NodeComponent),
|
||||||
|
url: memo(URLNodeComponent),
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -47,4 +47,5 @@ export const workflowBlockTitle: {
|
|||||||
wait: "Wait",
|
wait: "Wait",
|
||||||
pdf_parser: "PDF Parser",
|
pdf_parser: "PDF Parser",
|
||||||
task_v2: "Task v2",
|
task_v2: "Task v2",
|
||||||
|
goto_url: "Go to URL",
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -78,6 +78,17 @@ const nodeLibraryItems: Array<{
|
|||||||
title: "Task v2 Block",
|
title: "Task v2 Block",
|
||||||
description: "Runs a Skyvern v2 Task",
|
description: "Runs a Skyvern v2 Task",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
nodeType: "url",
|
||||||
|
icon: (
|
||||||
|
<WorkflowBlockIcon
|
||||||
|
workflowBlockType={WorkflowBlockTypes.URL}
|
||||||
|
className="size-6"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
title: "Go to URL Block",
|
||||||
|
description: "Navigates to a URL",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
nodeType: "textPrompt",
|
nodeType: "textPrompt",
|
||||||
icon: (
|
icon: (
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
FileDownloadBlockYAML,
|
FileDownloadBlockYAML,
|
||||||
PDFParserBlockYAML,
|
PDFParserBlockYAML,
|
||||||
Taskv2BlockYAML,
|
Taskv2BlockYAML,
|
||||||
|
URLBlockYAML,
|
||||||
} from "../types/workflowYamlTypes";
|
} from "../types/workflowYamlTypes";
|
||||||
import {
|
import {
|
||||||
EMAIL_BLOCK_SENDER,
|
EMAIL_BLOCK_SENDER,
|
||||||
@@ -88,6 +89,7 @@ import { fileDownloadNodeDefaultData } from "./nodes/FileDownloadNode/types";
|
|||||||
import { ProxyLocation } from "@/api/types";
|
import { ProxyLocation } from "@/api/types";
|
||||||
import { pdfParserNodeDefaultData } from "./nodes/PDFParserNode/types";
|
import { pdfParserNodeDefaultData } from "./nodes/PDFParserNode/types";
|
||||||
import { taskv2NodeDefaultData } from "./nodes/Taskv2Node/types";
|
import { taskv2NodeDefaultData } from "./nodes/Taskv2Node/types";
|
||||||
|
import { urlNodeDefaultData } from "./nodes/URLNode/types";
|
||||||
|
|
||||||
export const NEW_NODE_LABEL_PREFIX = "block_";
|
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),
|
json_schema: JSONParseSafe(node.data.jsonSchema),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
case "url": {
|
||||||
|
return {
|
||||||
|
...base,
|
||||||
|
block_type: "goto_url",
|
||||||
|
url: node.data.url,
|
||||||
|
};
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error("Invalid node type for getWorkflowBlock");
|
throw new Error("Invalid node type for getWorkflowBlock");
|
||||||
}
|
}
|
||||||
@@ -1754,6 +1786,14 @@ function convertBlocksToBlockYAML(
|
|||||||
};
|
};
|
||||||
return blockYaml;
|
return blockYaml;
|
||||||
}
|
}
|
||||||
|
case "goto_url": {
|
||||||
|
const blockYaml: URLBlockYAML = {
|
||||||
|
...base,
|
||||||
|
block_type: "goto_url",
|
||||||
|
url: block.url,
|
||||||
|
};
|
||||||
|
return blockYaml;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,7 +156,8 @@ export type WorkflowBlock =
|
|||||||
| WaitBlock
|
| WaitBlock
|
||||||
| FileDownloadBlock
|
| FileDownloadBlock
|
||||||
| PDFParserBlock
|
| PDFParserBlock
|
||||||
| Taskv2Block;
|
| Taskv2Block
|
||||||
|
| URLBlock;
|
||||||
|
|
||||||
export const WorkflowBlockTypes = {
|
export const WorkflowBlockTypes = {
|
||||||
Task: "task",
|
Task: "task",
|
||||||
@@ -176,6 +177,7 @@ export const WorkflowBlockTypes = {
|
|||||||
FileDownload: "file_download",
|
FileDownload: "file_download",
|
||||||
PDFParser: "pdf_parser",
|
PDFParser: "pdf_parser",
|
||||||
Taskv2: "task_v2",
|
Taskv2: "task_v2",
|
||||||
|
URL: "goto_url",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export function isTaskVariantBlock(item: {
|
export function isTaskVariantBlock(item: {
|
||||||
@@ -389,6 +391,11 @@ export type PDFParserBlock = WorkflowBlockBase & {
|
|||||||
json_schema: Record<string, unknown> | null;
|
json_schema: Record<string, unknown> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type URLBlock = WorkflowBlockBase & {
|
||||||
|
block_type: "goto_url";
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkflowDefinition = {
|
export type WorkflowDefinition = {
|
||||||
parameters: Array<Parameter>;
|
parameters: Array<Parameter>;
|
||||||
blocks: Array<WorkflowBlock>;
|
blocks: Array<WorkflowBlock>;
|
||||||
|
|||||||
@@ -99,7 +99,8 @@ export type BlockYAML =
|
|||||||
| WaitBlockYAML
|
| WaitBlockYAML
|
||||||
| FileDownloadBlockYAML
|
| FileDownloadBlockYAML
|
||||||
| PDFParserBlockYAML
|
| PDFParserBlockYAML
|
||||||
| Taskv2BlockYAML;
|
| Taskv2BlockYAML
|
||||||
|
| URLBlockYAML;
|
||||||
|
|
||||||
export type BlockYAMLBase = {
|
export type BlockYAMLBase = {
|
||||||
block_type: WorkflowBlockType;
|
block_type: WorkflowBlockType;
|
||||||
@@ -283,3 +284,8 @@ export type PDFParserBlockYAML = BlockYAMLBase & {
|
|||||||
file_url: string;
|
file_url: string;
|
||||||
json_schema: Record<string, unknown> | null;
|
json_schema: Record<string, unknown> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type URLBlockYAML = BlockYAMLBase & {
|
||||||
|
block_type: "goto_url";
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ class ProxyLocation(StrEnum):
|
|||||||
RESIDENTIAL_IN = "RESIDENTIAL_IN"
|
RESIDENTIAL_IN = "RESIDENTIAL_IN"
|
||||||
RESIDENTIAL_JP = "RESIDENTIAL_JP"
|
RESIDENTIAL_JP = "RESIDENTIAL_JP"
|
||||||
RESIDENTIAL_FR = "RESIDENTIAL_FR"
|
RESIDENTIAL_FR = "RESIDENTIAL_FR"
|
||||||
RESIDENTIAL_DE = "RESIDENTIAL_DE"
|
|
||||||
NONE = "NONE"
|
NONE = "NONE"
|
||||||
|
|
||||||
|
|
||||||
@@ -64,14 +63,11 @@ def get_tzinfo_from_proxy(proxy_location: ProxyLocation) -> ZoneInfo | None:
|
|||||||
return ZoneInfo("Asia/Kolkata")
|
return ZoneInfo("Asia/Kolkata")
|
||||||
|
|
||||||
if proxy_location == ProxyLocation.RESIDENTIAL_JP:
|
if proxy_location == ProxyLocation.RESIDENTIAL_JP:
|
||||||
return ZoneInfo("Asia/Tokyo")
|
return ZoneInfo("Asia/Kolkata")
|
||||||
|
|
||||||
if proxy_location == ProxyLocation.RESIDENTIAL_FR:
|
if proxy_location == ProxyLocation.RESIDENTIAL_FR:
|
||||||
return ZoneInfo("Europe/Paris")
|
return ZoneInfo("Europe/Paris")
|
||||||
|
|
||||||
if proxy_location == ProxyLocation.RESIDENTIAL_DE:
|
|
||||||
return ZoneInfo("Europe/Berlin")
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user