-
- Recipients
- {
- update({ recipients: value });
- }}
- value={data.recipients}
- placeholder="example@gmail.com, example2@gmail.com..."
- className="nopan text-xs"
- />
-
-
- Subject
- {
- update({ subject: value });
- }}
- value={data.subject}
- placeholder="Human interaction required for workflow run"
- className="nopan text-xs"
- />
-
-
-
Body
-
{
- update({ body: value });
- }}
- value={data.body}
- placeholder="Your interaction is required for a workflow run!"
- className="nopan text-xs"
- />
+
+
+
+
+
+ Negative Button Label
+
+
+
+
{
+ update({ negativeDescriptor: value });
+ }}
+ value={data.negativeDescriptor}
+ placeholder="Reject"
+ className="nopan text-xs"
+ />
+
+
+
+
+ Positive Button Label
+
+
+
+
{
+ update({ positiveDescriptor: value });
+ }}
+ value={data.positiveDescriptor}
+ placeholder="Approve"
+ className="nopan text-xs"
+ />
+
diff --git a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts
index 3d21acb8..4f9b52b5 100644
--- a/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts
+++ b/skyvern-frontend/src/routes/workflows/editor/workflowEditorUtils.ts
@@ -2318,8 +2318,10 @@ function getWorkflowErrors(nodes: Array): Array {
});
const interactionNodes = nodes.filter(isHumanInteractionNode);
- interactionNodes.forEach((/* node */) => {
- // pass for now
+ interactionNodes.forEach((node) => {
+ if (node.data.recipients.trim().length === 0) {
+ errors.push(`${node.data.label}: Recipients is required.`);
+ }
});
const navigationNodes = nodes.filter(isNavigationNode);
diff --git a/skyvern-frontend/src/routes/workflows/types/workflowRunTypes.ts b/skyvern-frontend/src/routes/workflows/types/workflowRunTypes.ts
index b1df28b0..efbcfe90 100644
--- a/skyvern-frontend/src/routes/workflows/types/workflowRunTypes.ts
+++ b/skyvern-frontend/src/routes/workflows/types/workflowRunTypes.ts
@@ -58,6 +58,11 @@ export type WorkflowRunBlock = {
// for blocks in loop
current_value: string | null;
current_index: number | null;
+
+ // human interaction block
+ instructions?: string | null;
+ positive_descriptor?: string | null;
+ negative_descriptor?: string | null;
};
export type WorkflowRunTimelineBlockItem = {
diff --git a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunHumanInteraction.tsx b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunHumanInteraction.tsx
index 105bdc46..05265b57 100644
--- a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunHumanInteraction.tsx
+++ b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunHumanInteraction.tsx
@@ -4,16 +4,26 @@ import { Button } from "@/components/ui/button";
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
import { cn } from "@/util/utils";
import { useMutation, useQueryClient } from "@tanstack/react-query";
+import { useState } from "react";
+import {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
import { toast } from "@/components/ui/use-toast";
import { useWorkflowRunQuery } from "../hooks/useWorkflowRunQuery";
-import { HumanInteractionBlock } from "../types/workflowTypes";
+import { WorkflowRunBlock } from "../types/workflowRunTypes";
interface Props {
- humanInteractionBlock: HumanInteractionBlock;
+ workflowRunBlock: WorkflowRunBlock;
}
-export function WorkflowRunHumanInteraction({ humanInteractionBlock }: Props) {
+export function WorkflowRunHumanInteraction({ workflowRunBlock }: Props) {
const credentialGetter = useCredentialGetter();
const queryClient = useQueryClient();
const { data: workflowRun } = useWorkflowRunQuery();
@@ -21,11 +31,14 @@ export function WorkflowRunHumanInteraction({ humanInteractionBlock }: Props) {
workflowRun && workflowRun.status === WorkflowRunStatus.Paused;
const buttonLayout =
- humanInteractionBlock.positive_descriptor.length < 8 &&
- humanInteractionBlock.negative_descriptor.length < 8
+ (workflowRunBlock.positive_descriptor?.length ?? 0) < 8 &&
+ (workflowRunBlock.negative_descriptor?.length ?? 0) < 8
? "inline"
: "stacked";
+ const [isDialogOpen, setIsDialogOpen] = useState(false);
+ const [choice, setChoice] = useState<"approve" | "reject" | null>(null);
+
const approveMutation = useMutation({
mutationFn: async () => {
if (!workflowRun) {
@@ -45,8 +58,8 @@ export function WorkflowRunHumanInteraction({ humanInteractionBlock }: Props) {
toast({
variant: "success",
- title: `${humanInteractionBlock.positive_descriptor}`,
- description: `Successfully chose: ${humanInteractionBlock.positive_descriptor}`,
+ title: `${workflowRunBlock.positive_descriptor}`,
+ description: `Successfully chose: ${workflowRunBlock.positive_descriptor}`,
});
},
onError: (error) => {
@@ -77,8 +90,8 @@ export function WorkflowRunHumanInteraction({ humanInteractionBlock }: Props) {
toast({
variant: "success",
- title: `${humanInteractionBlock.negative_descriptor}`,
- description: `Successfully chose: ${humanInteractionBlock.negative_descriptor}`,
+ title: `${workflowRunBlock.negative_descriptor}`,
+ description: `Successfully chose: ${workflowRunBlock.negative_descriptor}`,
});
},
onError: (error) => {
@@ -96,18 +109,60 @@ export function WorkflowRunHumanInteraction({ humanInteractionBlock }: Props) {
return (
-
{humanInteractionBlock.instructions}
+
+
+
+
+ {choice === "approve"
+ ? workflowRunBlock.positive_descriptor
+ : workflowRunBlock.negative_descriptor}
+
+ Are you sure?
+
+
+
+ Back
+
+ {
+ if (choice === "approve") {
+ approveMutation.mutate();
+ } else if (choice === "reject") {
+ rejectMutation.mutate();
+ }
+ }}
+ >
+ Proceed
+
+
+
+
+
+
{workflowRunBlock.instructions}
-
rejectMutation.mutate()}>
- {humanInteractionBlock.negative_descriptor}
+ {
+ setChoice("reject");
+ setIsDialogOpen(true);
+ }}
+ >
+ {workflowRunBlock.negative_descriptor}
- approveMutation.mutate()}>
- {humanInteractionBlock.positive_descriptor}
+ {
+ setChoice("approve");
+ setIsDialogOpen(true);
+ }}
+ >
+ {workflowRunBlock.positive_descriptor}
diff --git a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunTimelineBlockItem.tsx b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunTimelineBlockItem.tsx
index 443c609d..87a72722 100644
--- a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunTimelineBlockItem.tsx
+++ b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunTimelineBlockItem.tsx
@@ -5,7 +5,7 @@ import {
ExternalLinkIcon,
} from "@radix-ui/react-icons";
import { useCallback } from "react";
-import { useParams, Link } from "react-router-dom";
+import { Link } from "react-router-dom";
import { Status } from "@/api/types";
import { formatDuration, toDuration } from "@/routes/workflows/utils";
@@ -27,13 +27,8 @@ import {
WorkflowRunOverviewActiveElement,
} from "./WorkflowRunOverview";
import { ThoughtCard } from "./ThoughtCard";
-import { useWorkflowQuery } from "../hooks/useWorkflowQuery";
import { ObserverThought } from "../types/workflowRunTypes";
-import {
- HumanInteractionBlock,
- isTaskVariantBlock,
- type WorkflowApiResponse,
-} from "../types/workflowTypes";
+import { isTaskVariantBlock } from "../types/workflowTypes";
import { WorkflowRunHumanInteraction } from "./WorkflowRunHumanInteraction";
type Props = {
@@ -45,28 +40,6 @@ type Props = {
onThoughtCardClick: (thought: ObserverThought) => void;
};
-const getHumanInteractionBlock = (
- block: WorkflowRunBlock,
- workflow?: WorkflowApiResponse,
-): HumanInteractionBlock | null => {
- if (block.block_type !== "human_interaction") {
- return null;
- }
-
- if (!workflow) {
- return null;
- }
-
- const blocks = workflow.workflow_definition.blocks;
- const candidate = blocks.find((b) => b.label === block.label);
-
- if (!candidate || candidate.block_type !== "human_interaction") {
- return null;
- }
-
- return candidate as HumanInteractionBlock;
-};
-
function WorkflowRunTimelineBlockItem({
activeItem,
block,
@@ -75,12 +48,6 @@ function WorkflowRunTimelineBlockItem({
onActionClick,
onThoughtCardClick,
}: Props) {
- const { workflowPermanentId } = useParams();
- const { data: workflow } = useWorkflowQuery({
- workflowPermanentId,
- });
-
- const humanInteractionBlock = getHumanInteractionBlock(block, workflow);
const actions = block.actions ?? [];
const hasActiveAction =
@@ -203,10 +170,8 @@ function WorkflowRunTimelineBlockItem({
) : null}
- {humanInteractionBlock && (
-
+ {block.block_type === "human_interaction" && (
+
)}
{actions.map((action, index) => {