Add navigation block (#1256)
Co-authored-by: Muhammed Salih Altun <muhammedsalihaltun@gmail.com>
This commit is contained in:
23
skyvern-frontend/src/components/icons/RobotIcon.tsx
Normal file
23
skyvern-frontend/src/components/icons/RobotIcon.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
function RobotIcon({ className }: Props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
className={className}
|
||||
>
|
||||
<path
|
||||
d="M13.5 3.50003C13.5 3.94403 13.307 4.34303 13 4.61803V6.50003H18C18.7956 6.50003 19.5587 6.8161 20.1213 7.37871C20.6839 7.94132 21 8.70438 21 9.50003V19.5C21 20.2957 20.6839 21.0587 20.1213 21.6214C19.5587 22.184 18.7956 22.5 18 22.5H6C5.20435 22.5 4.44129 22.184 3.87868 21.6214C3.31607 21.0587 3 20.2957 3 19.5V9.50003C3 8.70438 3.31607 7.94132 3.87868 7.37871C4.44129 6.8161 5.20435 6.50003 6 6.50003H11V4.61803C10.8135 4.45123 10.6717 4.24042 10.5875 4.0048C10.5033 3.76918 10.4794 3.51625 10.5179 3.26902C10.5564 3.02179 10.6562 2.78813 10.8081 2.58931C10.96 2.39049 11.1592 2.23283 11.3876 2.13069C11.6161 2.02854 11.8664 1.98516 12.1159 2.00448C12.3653 2.02381 12.606 2.10523 12.8159 2.24133C13.0259 2.37744 13.1985 2.5639 13.3179 2.78374C13.4374 3.00359 13.5 3.24982 13.5 3.50003ZM6 8.50003C5.73478 8.50003 5.48043 8.60539 5.29289 8.79293C5.10536 8.98046 5 9.23482 5 9.50003V19.5C5 19.7653 5.10536 20.0196 5.29289 20.2071C5.48043 20.3947 5.73478 20.5 6 20.5H18C18.2652 20.5 18.5196 20.3947 18.7071 20.2071C18.8946 20.0196 19 19.7653 19 19.5V9.50003C19 9.23482 18.8946 8.98046 18.7071 8.79293C18.5196 8.60539 18.2652 8.50003 18 8.50003H6ZM2 12.2C2 11.8134 1.6866 11.5 1.3 11.5H0.7C0.313401 11.5 0 11.8134 0 12.2V16.8C0 17.1866 0.313401 17.5 0.7 17.5H1.3C1.6866 17.5 2 17.1866 2 16.8V12.2ZM22 12.2C22 11.8134 22.3134 11.5 22.7 11.5H23.3C23.6866 11.5 24 11.8134 24 12.2V16.8C24 17.1866 23.6866 17.5 23.3 17.5H22.7C22.3134 17.5 22 17.1866 22 16.8V12.2ZM9 16C9.39782 16 9.77936 15.842 10.0607 15.5607C10.342 15.2794 10.5 14.8979 10.5 14.5C10.5 14.1022 10.342 13.7207 10.0607 13.4394C9.77936 13.1581 9.39782 13 9 13C8.60218 13 8.22064 13.1581 7.93934 13.4394C7.65804 13.7207 7.5 14.1022 7.5 14.5C7.5 14.8979 7.65804 15.2794 7.93934 15.5607C8.22064 15.842 8.60218 16 9 16ZM15 16C15.3978 16 15.7794 15.842 16.0607 15.5607C16.342 15.2794 16.5 14.8979 16.5 14.5C16.5 14.1022 16.342 13.7207 16.0607 13.4394C15.7794 13.1581 15.3978 13 15 13C14.6022 13 14.2206 13.1581 13.9393 13.4394C13.658 13.7207 13.5 14.1022 13.5 14.5C13.5 14.8979 13.658 15.2794 13.9393 15.5607C14.2206 15.842 14.6022 16 15 16Z"
|
||||
fill="#F8FAFC"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export { RobotIcon };
|
||||
@@ -14,3 +14,33 @@ export const SMTP_USERNAME_AWS_KEY = "SKYVERN_SMTP_USERNAME_SES";
|
||||
export const SMTP_PASSWORD_AWS_KEY = "SKYVERN_SMTP_PASSWORD_SES";
|
||||
|
||||
export const EMAIL_BLOCK_SENDER = "hello@skyvern.com";
|
||||
|
||||
export const commonHelpTooltipContent = {
|
||||
maxRetries:
|
||||
"Specify how many times you would like a task to retry upon failure.",
|
||||
maxStepsOverride:
|
||||
"Specify the maximum number of steps a task can take in total.",
|
||||
completeOnDownload:
|
||||
"Allow Skyvern to auto-complete the task when it downloads a file.",
|
||||
fileSuffix:
|
||||
"A file suffix that's automatically added to all downloaded files.",
|
||||
errorCodeMapping:
|
||||
"Knowing about why a task terminated can be important, specify error messages here.",
|
||||
totpVerificationUrl:
|
||||
"If you have an internal system for storing TOTP codes, link the endpoint here.",
|
||||
totpIdentifier:
|
||||
"If you are running multiple tasks or workflows at once, you will need to give the task an identifier to know that this TOTP goes with this task.",
|
||||
continueOnFailure:
|
||||
"Allow the workflow to continue if it encounters a failure.",
|
||||
cacheActions: "Cache the actions of this task.",
|
||||
} as const;
|
||||
|
||||
export const commonFieldPlaceholders = {
|
||||
url: "https://",
|
||||
navigationGoal: 'Input text into "Name" field.',
|
||||
maxRetries: "Default: 3",
|
||||
maxStepsOverride: "Default: 10",
|
||||
downloadSuffix: "Add an ID for downloaded files",
|
||||
totpVerificationUrl: "Provide your 2FA endpoint",
|
||||
totpIdentifier: "Add an ID that links your TOTP to the task",
|
||||
} as const;
|
||||
|
||||
@@ -13,15 +13,23 @@ import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { helpTooltipContent, type ActionNode } from "./types";
|
||||
import type { ActionNode } from "./types";
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { fieldPlaceholders } from "./types";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { errorMappingExampleValue } from "../types";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { ClickIcon } from "@/components/icons/ClickIcon";
|
||||
import {
|
||||
commonFieldPlaceholders,
|
||||
commonHelpTooltipContent,
|
||||
} from "../../constants";
|
||||
|
||||
const navigationGoalTooltip =
|
||||
"Specify a single step or action you'd like Skyvern to complete. Actions are one-off tasks like filling a field or interacting with a specific element on the page.\n\nCurrently supported actions are click, input text, upload file, and select.";
|
||||
|
||||
const navigationGoalPlaceholder = 'Input text into "Name" field.';
|
||||
|
||||
function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
@@ -91,7 +99,7 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs text-slate-300">Action Instruction</Label>
|
||||
<HelpTooltip content={helpTooltipContent["navigationGoal"]} />
|
||||
<HelpTooltip content={navigationGoalTooltip} />
|
||||
</div>
|
||||
<AutoResizingTextarea
|
||||
onChange={(event) => {
|
||||
@@ -101,6 +109,7 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
handleChange("navigationGoal", event.target.value);
|
||||
}}
|
||||
value={inputs.navigationGoal}
|
||||
placeholder={navigationGoalPlaceholder}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
@@ -117,11 +126,13 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Max Retries
|
||||
</Label>
|
||||
<HelpTooltip content={helpTooltipContent["maxRetries"]} />
|
||||
<HelpTooltip
|
||||
content={commonHelpTooltipContent["maxRetries"]}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={fieldPlaceholders["maxRetries"]}
|
||||
placeholder={commonFieldPlaceholders["maxRetries"]}
|
||||
className="nopan w-52 text-xs"
|
||||
min="0"
|
||||
value={inputs.maxRetries ?? ""}
|
||||
@@ -144,7 +155,7 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
Error Messages
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltipContent["errorCodeMapping"]}
|
||||
content={commonHelpTooltipContent["errorCodeMapping"]}
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
@@ -187,7 +198,7 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
Continue on Failure
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltipContent["continueOnFailure"]}
|
||||
content={commonHelpTooltipContent["continueOnFailure"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
@@ -207,7 +218,9 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Cache Actions
|
||||
</Label>
|
||||
<HelpTooltip content={helpTooltipContent["cacheActions"]} />
|
||||
<HelpTooltip
|
||||
content={commonHelpTooltipContent["cacheActions"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
@@ -228,7 +241,7 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
Complete on Download
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltipContent["completeOnDownload"]}
|
||||
content={commonHelpTooltipContent["completeOnDownload"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
@@ -248,11 +261,13 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
File Suffix
|
||||
</Label>
|
||||
<HelpTooltip content={helpTooltipContent["fileSuffix"]} />
|
||||
<HelpTooltip
|
||||
content={commonHelpTooltipContent["fileSuffix"]}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={fieldPlaceholders["downloadSuffix"]}
|
||||
placeholder={commonFieldPlaceholders["downloadSuffix"]}
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.downloadSuffix ?? ""}
|
||||
onChange={(event) => {
|
||||
@@ -270,7 +285,7 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
2FA Verification URL
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltipContent["totpVerificationUrl"]}
|
||||
content={commonHelpTooltipContent["totpVerificationUrl"]}
|
||||
/>
|
||||
</div>
|
||||
<AutoResizingTextarea
|
||||
@@ -281,7 +296,7 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
handleChange("totpVerificationUrl", event.target.value);
|
||||
}}
|
||||
value={inputs.totpVerificationUrl ?? ""}
|
||||
placeholder={fieldPlaceholders["totpVerificationUrl"]}
|
||||
placeholder={commonFieldPlaceholders["totpVerificationUrl"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
@@ -291,7 +306,7 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
2FA Identifier
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={helpTooltipContent["totpIdentifier"]}
|
||||
content={commonHelpTooltipContent["totpIdentifier"]}
|
||||
/>
|
||||
</div>
|
||||
<AutoResizingTextarea
|
||||
@@ -302,7 +317,7 @@ function ActionNode({ id, data }: NodeProps<ActionNode>) {
|
||||
handleChange("totpIdentifier", event.target.value);
|
||||
}}
|
||||
value={inputs.totpIdentifier ?? ""}
|
||||
placeholder={fieldPlaceholders["totpIdentifier"]}
|
||||
placeholder={commonFieldPlaceholders["totpIdentifier"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -35,34 +35,3 @@ export const actionNodeDefaultData: ActionNodeData = {
|
||||
export function isActionNode(node: Node): node is ActionNode {
|
||||
return node.type === "action";
|
||||
}
|
||||
|
||||
export const helpTooltipContent = {
|
||||
navigationGoal:
|
||||
"Specify a single step or action you'd like Skyvern to complete. Actions are one-off tasks like filling a field or interacting with a specific element on the page.\n\nCurrently supported actions are click, input text, upload file, and select.",
|
||||
maxRetries:
|
||||
"Specify how many times you would like a task to retry upon failure.",
|
||||
maxStepsOverride:
|
||||
"Specify the maximum number of steps a task can take in total.",
|
||||
completeOnDownload:
|
||||
"Allow Skyvern to auto-complete the task when it downloads a file.",
|
||||
fileSuffix:
|
||||
"A file suffix that's automatically added to all downloaded files.",
|
||||
errorCodeMapping:
|
||||
"Knowing about why a task terminated can be important, specify error messages here.",
|
||||
totpVerificationUrl:
|
||||
"If you have an internal system for storing TOTP codes, link the endpoint here.",
|
||||
totpIdentifier:
|
||||
"If you are running multiple tasks or workflows at once, you will need to give the task an identifier to know that this TOTP goes with this task.",
|
||||
continueOnFailure:
|
||||
"Allow the workflow to continue if it encounters a failure.",
|
||||
cacheActions: "Cache the actions of this task.",
|
||||
} as const;
|
||||
|
||||
export const fieldPlaceholders = {
|
||||
navigationGoal: 'Input text into "Name" field.',
|
||||
maxRetries: "Default: 3",
|
||||
maxStepsOverride: "Default: 10",
|
||||
downloadSuffix: "Add an ID for downloaded files",
|
||||
totpVerificationUrl: "Provide your 2FA endpoint",
|
||||
totpIdentifier: "Add an ID that links your TOTP to the task",
|
||||
};
|
||||
|
||||
@@ -0,0 +1,383 @@
|
||||
import { AutoResizingTextarea } from "@/components/AutoResizingTextarea/AutoResizingTextarea";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useDeleteNodeCallback } from "@/routes/workflows/hooks/useDeleteNodeCallback";
|
||||
import { useNodeLabelChangeHandler } from "@/routes/workflows/hooks/useLabelChangeHandler";
|
||||
import { Handle, NodeProps, Position, useReactFlow } from "@xyflow/react";
|
||||
import { useState } from "react";
|
||||
import { EditableNodeTitle } from "../components/EditableNodeTitle";
|
||||
import { NodeActionMenu } from "../NodeActionMenu";
|
||||
import { HelpTooltip } from "@/components/HelpTooltip";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { errorMappingExampleValue } from "../types";
|
||||
import { CodeEditor } from "@/routes/workflows/components/CodeEditor";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import type { NavigationNode } from "./types";
|
||||
import {
|
||||
commonFieldPlaceholders,
|
||||
commonHelpTooltipContent,
|
||||
} from "../../constants";
|
||||
import { RobotIcon } from "@/components/icons/RobotIcon";
|
||||
|
||||
const urlTooltip =
|
||||
"The URL Skyvern is navigating to. Leave this field blank to pick up from where the last block left off.";
|
||||
const urlPlaceholder = "https://";
|
||||
const navigationGoalTooltip =
|
||||
"Give Skyvern an objective. Make sure to include when the task is complete, when it should self-terminate, and any guardrails.";
|
||||
const navigationGoalPlaceholder = "Tell Skyvern what to do.";
|
||||
|
||||
function NavigationNode({ id, data }: NodeProps<NavigationNode>) {
|
||||
const { updateNodeData } = useReactFlow();
|
||||
const { editable } = data;
|
||||
const [label, setLabel] = useNodeLabelChangeHandler({
|
||||
id,
|
||||
initialValue: data.label,
|
||||
});
|
||||
const [inputs, setInputs] = useState({
|
||||
url: data.url,
|
||||
navigationGoal: data.navigationGoal,
|
||||
errorCodeMapping: data.errorCodeMapping,
|
||||
maxRetries: data.maxRetries,
|
||||
maxStepsOverride: data.maxStepsOverride,
|
||||
allowDownloads: data.allowDownloads,
|
||||
continueOnFailure: data.continueOnFailure,
|
||||
cacheActions: data.cacheActions,
|
||||
downloadSuffix: data.downloadSuffix,
|
||||
totpVerificationUrl: data.totpVerificationUrl,
|
||||
totpIdentifier: data.totpIdentifier,
|
||||
});
|
||||
const deleteNodeCallback = useDeleteNodeCallback();
|
||||
|
||||
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">
|
||||
<header 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">
|
||||
<RobotIcon 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">Navigation Block</span>
|
||||
</div>
|
||||
</div>
|
||||
<NodeActionMenu
|
||||
onDelete={() => {
|
||||
deleteNodeCallback(id);
|
||||
}}
|
||||
/>
|
||||
</header>
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs text-slate-300">URL</Label>
|
||||
<HelpTooltip content={urlTooltip} />
|
||||
</div>
|
||||
<AutoResizingTextarea
|
||||
onChange={(event) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
handleChange("url", event.target.value);
|
||||
}}
|
||||
value={inputs.url}
|
||||
placeholder={urlPlaceholder}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs text-slate-300">Navigation Goal</Label>
|
||||
<HelpTooltip content={navigationGoalTooltip} />
|
||||
</div>
|
||||
<AutoResizingTextarea
|
||||
onChange={(event) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
handleChange("navigationGoal", event.target.value);
|
||||
}}
|
||||
value={inputs.navigationGoal}
|
||||
placeholder={navigationGoalPlaceholder}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<Accordion type="single" collapsible>
|
||||
<AccordionItem value="advanced" className="border-b-0">
|
||||
<AccordionTrigger className="py-0">
|
||||
Advanced Settings
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="pl-6 pr-1 pt-1">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Max Retries
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={commonHelpTooltipContent["maxRetries"]}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={commonFieldPlaceholders["maxRetries"]}
|
||||
className="nopan w-52 text-xs"
|
||||
min="0"
|
||||
value={inputs.maxRetries ?? ""}
|
||||
onChange={(event) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
const value =
|
||||
event.target.value === ""
|
||||
? null
|
||||
: Number(event.target.value);
|
||||
handleChange("maxRetries", value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Max Steps Override
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={commonHelpTooltipContent["maxStepsOverride"]}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder={commonFieldPlaceholders["maxStepsOverride"]}
|
||||
className="nopan w-52 text-xs"
|
||||
min="0"
|
||||
value={inputs.maxStepsOverride ?? ""}
|
||||
onChange={(event) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
const value =
|
||||
event.target.value === ""
|
||||
? null
|
||||
: Number(event.target.value);
|
||||
handleChange("maxStepsOverride", value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Error Messages
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={commonHelpTooltipContent["errorCodeMapping"]}
|
||||
/>
|
||||
</div>
|
||||
<Checkbox
|
||||
checked={inputs.errorCodeMapping !== "null"}
|
||||
disabled={!editable}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
handleChange(
|
||||
"errorCodeMapping",
|
||||
checked
|
||||
? JSON.stringify(errorMappingExampleValue, null, 2)
|
||||
: "null",
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{inputs.errorCodeMapping !== "null" && (
|
||||
<div>
|
||||
<CodeEditor
|
||||
language="json"
|
||||
value={inputs.errorCodeMapping}
|
||||
onChange={(value) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
handleChange("errorCodeMapping", value);
|
||||
}}
|
||||
className="nowheel nopan"
|
||||
fontSize={8}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Continue on Failure
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={commonHelpTooltipContent["continueOnFailure"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.continueOnFailure}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
handleChange("continueOnFailure", checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Cache Actions
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={commonHelpTooltipContent["cacheActions"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.cacheActions}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
handleChange("cacheActions", checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
Complete on Download
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={commonHelpTooltipContent["completeOnDownload"]}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-52">
|
||||
<Switch
|
||||
checked={inputs.allowDownloads}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
handleChange("allowDownloads", checked);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs font-normal text-slate-300">
|
||||
File Suffix
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={commonHelpTooltipContent["fileSuffix"]}
|
||||
/>
|
||||
</div>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={commonFieldPlaceholders["downloadSuffix"]}
|
||||
className="nopan w-52 text-xs"
|
||||
value={inputs.downloadSuffix ?? ""}
|
||||
onChange={(event) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
handleChange("downloadSuffix", event.target.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs text-slate-300">
|
||||
2FA Verification URL
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={commonHelpTooltipContent["totpVerificationUrl"]}
|
||||
/>
|
||||
</div>
|
||||
<AutoResizingTextarea
|
||||
onChange={(event) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
handleChange("totpVerificationUrl", event.target.value);
|
||||
}}
|
||||
value={inputs.totpVerificationUrl ?? ""}
|
||||
placeholder={commonFieldPlaceholders["totpVerificationUrl"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex gap-2">
|
||||
<Label className="text-xs text-slate-300">
|
||||
2FA Identifier
|
||||
</Label>
|
||||
<HelpTooltip
|
||||
content={commonHelpTooltipContent["totpIdentifier"]}
|
||||
/>
|
||||
</div>
|
||||
<AutoResizingTextarea
|
||||
onChange={(event) => {
|
||||
if (!editable) {
|
||||
return;
|
||||
}
|
||||
handleChange("totpIdentifier", event.target.value);
|
||||
}}
|
||||
value={inputs.totpIdentifier ?? ""}
|
||||
placeholder={commonFieldPlaceholders["totpIdentifier"]}
|
||||
className="nopan text-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { NavigationNode };
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { Node } from "@xyflow/react";
|
||||
import { NodeBaseData } from "../types";
|
||||
|
||||
export type NavigationNodeData = NodeBaseData & {
|
||||
url: string;
|
||||
navigationGoal: string;
|
||||
errorCodeMapping: string;
|
||||
maxRetries: number | null;
|
||||
maxStepsOverride: number | null;
|
||||
allowDownloads: boolean;
|
||||
downloadSuffix: string | null;
|
||||
parameterKeys: Array<string>;
|
||||
totpVerificationUrl: string | null;
|
||||
totpIdentifier: string | null;
|
||||
cacheActions: boolean;
|
||||
};
|
||||
|
||||
export type NavigationNode = Node<NavigationNodeData, "navigation">;
|
||||
|
||||
export const navigationNodeDefaultData: NavigationNodeData = {
|
||||
label: "",
|
||||
url: "",
|
||||
navigationGoal: "",
|
||||
errorCodeMapping: "null",
|
||||
maxRetries: null,
|
||||
maxStepsOverride: null,
|
||||
allowDownloads: false,
|
||||
downloadSuffix: null,
|
||||
editable: true,
|
||||
parameterKeys: [],
|
||||
totpVerificationUrl: null,
|
||||
totpIdentifier: null,
|
||||
continueOnFailure: false,
|
||||
cacheActions: false,
|
||||
} as const;
|
||||
|
||||
export function isNavigationNode(node: Node): node is NavigationNode {
|
||||
return node.type === "navigation";
|
||||
}
|
||||
@@ -23,6 +23,8 @@ import type { ValidationNode } from "./ValidationNode/types";
|
||||
import { ValidationNode as ValidationNodeComponent } from "./ValidationNode/ValidationNode";
|
||||
import type { ActionNode } from "./ActionNode/types";
|
||||
import { ActionNode as ActionNodeComponent } from "./ActionNode/ActionNode";
|
||||
import { NavigationNode } from "./NavigationNode/types";
|
||||
import { NavigationNode as NavigationNodeComponent } from "./NavigationNode/NavigationNode";
|
||||
|
||||
export type UtilityNode = StartNode | NodeAdderNode;
|
||||
|
||||
@@ -36,7 +38,8 @@ export type WorkflowBlockNode =
|
||||
| UploadNode
|
||||
| DownloadNode
|
||||
| ValidationNode
|
||||
| ActionNode;
|
||||
| ActionNode
|
||||
| NavigationNode;
|
||||
|
||||
export function isUtilityNode(node: AppNode): node is UtilityNode {
|
||||
return node.type === "nodeAdder" || node.type === "start";
|
||||
@@ -61,4 +64,5 @@ export const nodeTypes = {
|
||||
start: memo(StartNodeComponent),
|
||||
validation: memo(ValidationNodeComponent),
|
||||
action: memo(ActionNodeComponent),
|
||||
};
|
||||
navigation: memo(NavigationNodeComponent),
|
||||
} as const;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { WorkflowBlockNode } from "../nodes";
|
||||
import { AddNodeProps } from "../FlowRenderer";
|
||||
import { ClickIcon } from "@/components/icons/ClickIcon";
|
||||
import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area";
|
||||
import { RobotIcon } from "@/components/icons/RobotIcon";
|
||||
|
||||
const nodeLibraryItems: Array<{
|
||||
nodeType: NonNullable<WorkflowBlockNode["type"]>;
|
||||
@@ -83,6 +84,12 @@ const nodeLibraryItems: Array<{
|
||||
title: "Action Block",
|
||||
description: "Take a single action",
|
||||
},
|
||||
{
|
||||
nodeType: "navigation",
|
||||
icon: <RobotIcon className="size-6" />,
|
||||
title: "Navigation Block",
|
||||
description: "Navigate on the page",
|
||||
},
|
||||
];
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
TextPromptBlockYAML,
|
||||
UploadToS3BlockYAML,
|
||||
ValidationBlockYAML,
|
||||
NavigationBlockYAML,
|
||||
WorkflowCreateYAMLRequest,
|
||||
} from "../types/workflowYamlTypes";
|
||||
import {
|
||||
@@ -52,6 +53,7 @@ import { NodeBaseData } from "./nodes/types";
|
||||
import { uploadNodeDefaultData } from "./nodes/UploadNode/types";
|
||||
import { validationNodeDefaultData } from "./nodes/ValidationNode/types";
|
||||
import { actionNodeDefaultData } from "./nodes/ActionNode/types";
|
||||
import { navigationNodeDefaultData } from "./nodes/NavigationNode/types";
|
||||
|
||||
export const NEW_NODE_LABEL_PREFIX = "block_";
|
||||
|
||||
@@ -197,6 +199,27 @@ function convertToNode(
|
||||
},
|
||||
};
|
||||
}
|
||||
case "navigation": {
|
||||
return {
|
||||
...identifiers,
|
||||
...common,
|
||||
type: "navigation",
|
||||
data: {
|
||||
...commonData,
|
||||
url: block.url ?? "",
|
||||
navigationGoal: block.navigation_goal ?? "",
|
||||
errorCodeMapping: JSON.stringify(block.error_code_mapping, null, 2),
|
||||
allowDownloads: block.complete_on_download ?? false,
|
||||
downloadSuffix: block.download_suffix ?? null,
|
||||
maxRetries: block.max_retries ?? null,
|
||||
parameterKeys: block.parameters.map((p) => p.key),
|
||||
totpIdentifier: block.totp_identifier ?? null,
|
||||
totpVerificationUrl: block.totp_verification_url ?? null,
|
||||
cacheActions: block.cache_actions,
|
||||
maxStepsOverride: block.max_steps_per_run ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "code": {
|
||||
return {
|
||||
...identifiers,
|
||||
@@ -509,6 +532,17 @@ function createNode(
|
||||
},
|
||||
};
|
||||
}
|
||||
case "navigation": {
|
||||
return {
|
||||
...identifiers,
|
||||
...common,
|
||||
type: "navigation",
|
||||
data: {
|
||||
...navigationNodeDefaultData,
|
||||
label,
|
||||
},
|
||||
};
|
||||
}
|
||||
case "loop": {
|
||||
return {
|
||||
...identifiers,
|
||||
@@ -662,6 +696,28 @@ function getWorkflowBlock(node: WorkflowBlockNode): BlockYAML {
|
||||
cache_actions: node.data.cacheActions,
|
||||
};
|
||||
}
|
||||
case "navigation": {
|
||||
return {
|
||||
...base,
|
||||
block_type: "navigation",
|
||||
navigation_goal: node.data.navigationGoal,
|
||||
error_code_mapping: JSONParseSafe(node.data.errorCodeMapping) as Record<
|
||||
string,
|
||||
string
|
||||
> | null,
|
||||
url: node.data.url,
|
||||
...(node.data.maxRetries !== null && {
|
||||
max_retries: node.data.maxRetries,
|
||||
}),
|
||||
max_steps_per_run: node.data.maxStepsOverride,
|
||||
complete_on_download: node.data.allowDownloads,
|
||||
download_suffix: node.data.downloadSuffix,
|
||||
parameter_keys: node.data.parameterKeys,
|
||||
totp_identifier: node.data.totpIdentifier,
|
||||
totp_verification_url: node.data.totpVerificationUrl,
|
||||
cache_actions: node.data.cacheActions,
|
||||
};
|
||||
}
|
||||
case "sendEmail": {
|
||||
return {
|
||||
...base,
|
||||
@@ -1042,7 +1098,7 @@ function getAvailableOutputParameterKeys(
|
||||
return outputParameterKeys;
|
||||
}
|
||||
|
||||
function convertParameters(
|
||||
function convertParametersToParameterYAML(
|
||||
parameters: Array<Exclude<Parameter, OutputParameter>>,
|
||||
): Array<ParameterYAML> {
|
||||
return parameters.map((parameter) => {
|
||||
@@ -1105,7 +1161,9 @@ function convertParameters(
|
||||
});
|
||||
}
|
||||
|
||||
function convertBlocks(blocks: Array<WorkflowBlock>): Array<BlockYAML> {
|
||||
function convertBlocksToBlockYAML(
|
||||
blocks: Array<WorkflowBlock>,
|
||||
): Array<BlockYAML> {
|
||||
return blocks.map((block) => {
|
||||
const base = {
|
||||
label: block.label,
|
||||
@@ -1160,12 +1218,30 @@ function convertBlocks(blocks: Array<WorkflowBlock>): Array<BlockYAML> {
|
||||
};
|
||||
return blockYaml;
|
||||
}
|
||||
case "navigation": {
|
||||
const blockYaml: NavigationBlockYAML = {
|
||||
...base,
|
||||
block_type: "navigation",
|
||||
url: block.url,
|
||||
navigation_goal: block.navigation_goal,
|
||||
error_code_mapping: block.error_code_mapping,
|
||||
max_retries: block.max_retries,
|
||||
max_steps_per_run: block.max_steps_per_run,
|
||||
complete_on_download: block.complete_on_download,
|
||||
download_suffix: block.download_suffix,
|
||||
parameter_keys: block.parameters.map((p) => p.key),
|
||||
totp_identifier: block.totp_identifier,
|
||||
totp_verification_url: block.totp_verification_url,
|
||||
cache_actions: block.cache_actions,
|
||||
};
|
||||
return blockYaml;
|
||||
}
|
||||
case "for_loop": {
|
||||
const blockYaml: ForLoopBlockYAML = {
|
||||
...base,
|
||||
block_type: "for_loop",
|
||||
loop_over_parameter_key: block.loop_over.key,
|
||||
loop_blocks: convertBlocks(block.loop_blocks),
|
||||
loop_blocks: convertBlocksToBlockYAML(block.loop_blocks),
|
||||
};
|
||||
return blockYaml;
|
||||
}
|
||||
@@ -1244,8 +1320,8 @@ function convert(workflow: WorkflowApiResponse): WorkflowCreateYAMLRequest {
|
||||
webhook_callback_url: workflow.webhook_callback_url,
|
||||
totp_verification_url: workflow.totp_verification_url,
|
||||
workflow_definition: {
|
||||
parameters: convertParameters(userParameters),
|
||||
blocks: convertBlocks(workflow.workflow_definition.blocks),
|
||||
parameters: convertParametersToParameterYAML(userParameters),
|
||||
blocks: convertBlocksToBlockYAML(workflow.workflow_definition.blocks),
|
||||
},
|
||||
is_saved_task: workflow.is_saved_task,
|
||||
};
|
||||
|
||||
@@ -111,7 +111,9 @@ export const WorkflowBlockType = {
|
||||
SendEmail: "send_email",
|
||||
FileURLParser: "file_url_parser",
|
||||
Validation: "validation",
|
||||
};
|
||||
Action: "action",
|
||||
Navigation: "navigation",
|
||||
} as const;
|
||||
|
||||
export type WorkflowBlockType =
|
||||
(typeof WorkflowBlockType)[keyof typeof WorkflowBlockType];
|
||||
@@ -198,9 +200,36 @@ export type ValidationBlock = WorkflowBlockBase & {
|
||||
parameters: Array<WorkflowParameter>;
|
||||
};
|
||||
|
||||
export type ActionBlock = Omit<TaskBlock, "block_type"> & {
|
||||
export type ActionBlock = WorkflowBlockBase & {
|
||||
block_type: "action";
|
||||
url: string | null;
|
||||
title: string;
|
||||
navigation_goal: string | null;
|
||||
error_code_mapping: Record<string, string> | null;
|
||||
max_retries?: number;
|
||||
max_steps_per_run?: number | null;
|
||||
parameters: Array<WorkflowParameter>;
|
||||
complete_on_download?: boolean;
|
||||
download_suffix?: string | null;
|
||||
totp_verification_url?: string | null;
|
||||
totp_identifier?: string | null;
|
||||
cache_actions: boolean;
|
||||
};
|
||||
|
||||
export type NavigationBlock = WorkflowBlockBase & {
|
||||
block_type: "navigation";
|
||||
url: string | null;
|
||||
title: string;
|
||||
navigation_goal: string | null;
|
||||
error_code_mapping: Record<string, string> | null;
|
||||
max_retries?: number;
|
||||
max_steps_per_run?: number | null;
|
||||
parameters: Array<WorkflowParameter>;
|
||||
complete_on_download?: boolean;
|
||||
download_suffix?: string | null;
|
||||
totp_verification_url?: string | null;
|
||||
totp_identifier?: string | null;
|
||||
cache_actions: boolean;
|
||||
};
|
||||
|
||||
export type WorkflowBlock =
|
||||
@@ -213,7 +242,8 @@ export type WorkflowBlock =
|
||||
| SendEmailBlock
|
||||
| FileURLParserBlock
|
||||
| ValidationBlock
|
||||
| ActionBlock;
|
||||
| ActionBlock
|
||||
| NavigationBlock;
|
||||
|
||||
export type WorkflowDefinition = {
|
||||
parameters: Array<Parameter>;
|
||||
|
||||
@@ -77,6 +77,7 @@ const BlockTypes = {
|
||||
FILE_URL_PARSER: "file_url_parser",
|
||||
VALIDATION: "validation",
|
||||
ACTION: "action",
|
||||
NAVIGATION: "navigation",
|
||||
} as const;
|
||||
|
||||
export type BlockType = (typeof BlockTypes)[keyof typeof BlockTypes];
|
||||
@@ -91,7 +92,8 @@ export type BlockYAML =
|
||||
| FileUrlParserBlockYAML
|
||||
| ForLoopBlockYAML
|
||||
| ValidationBlockYAML
|
||||
| ActionBlockYAML;
|
||||
| ActionBlockYAML
|
||||
| NavigationBlockYAML;
|
||||
|
||||
export type BlockYAMLBase = {
|
||||
block_type: BlockType;
|
||||
@@ -139,6 +141,21 @@ export type ActionBlockYAML = BlockYAMLBase & {
|
||||
cache_actions: boolean;
|
||||
};
|
||||
|
||||
export type NavigationBlockYAML = BlockYAMLBase & {
|
||||
block_type: "navigation";
|
||||
url: string | null;
|
||||
navigation_goal: string | null;
|
||||
error_code_mapping: Record<string, string> | null;
|
||||
max_retries?: number;
|
||||
max_steps_per_run?: number | null;
|
||||
parameter_keys?: Array<string> | null;
|
||||
complete_on_download?: boolean;
|
||||
download_suffix?: string | null;
|
||||
totp_verification_url?: string | null;
|
||||
totp_identifier?: string | null;
|
||||
cache_actions: boolean;
|
||||
};
|
||||
|
||||
export type CodeBlockYAML = BlockYAMLBase & {
|
||||
block_type: "code";
|
||||
code: string;
|
||||
|
||||
Reference in New Issue
Block a user