diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/HumanInteractionNode/HumanInteractionNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/HumanInteractionNode/HumanInteractionNode.tsx index 948894a7..2f6bfa61 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/HumanInteractionNode/HumanInteractionNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/HumanInteractionNode/HumanInteractionNode.tsx @@ -95,94 +95,79 @@ function HumanInteractionNode({ {/* TODO(jdo): 'instructions' allows templating; but it requires adding a column to the workflow_block_runs table, and I don't want to do that just yet (see /timeline endpoint) */} - { - update({ instructions: event.target.value }); + { + update({ instructions: value }); }} value={data.instructions} placeholder="Please review and approve or reject to continue the workflow." className="nopan text-xs" /> -
-
-
- - -
- { - update({ negativeDescriptor: event.target.value }); - }} - value={data.negativeDescriptor} - placeholder="Reject" - className="nopan text-xs" - /> -
-
-
- - -
- { - update({ positiveDescriptor: event.target.value }); - }} - value={data.positiveDescriptor} - placeholder="Approve" - className="nopan text-xs" - /> -
-
- {/*
-
- - -
- { - if (!editable) { - return; - } - const value = Number(event.target.value); - update({ timeoutSeconds: value }); - }} - placeholder="7200" - /> -
*/}
- + { if (!editable) { return; } const value = Number(event.target.value); - update({ timeoutSeconds: value }); + update({ timeoutSeconds: value * 60 }); }} />
-
+
+ 💡
- Tip: The workflow will pause and send an email notification to the + The workflow will pause and send an email notification to the recipients. The workflow continues or terminates based on the user's response.
+
+

Email Settings

+
+ + { + update({ recipients: value }); + }} + value={data.recipients} + placeholder="example@gmail.com, example2@gmail.com..." + className="nopan text-xs" + /> +
+
+ + { + update({ subject: value }); + }} + value={data.subject} + placeholder="Human interaction required for workflow run" + className="nopan text-xs" + /> +
+
+ + { + update({ body: value }); + }} + value={data.body} + placeholder="Your interaction is required for a workflow run!" + className="nopan text-xs" + /> +
+
- Email Settings + + Advanced Settings + -
-
- - { - update({ recipients: value }); - }} - value={data.recipients} - placeholder="example@gmail.com, example2@gmail.com..." - className="nopan text-xs" - /> -
-
- - { - update({ subject: value }); - }} - value={data.subject} - placeholder="Human interaction required for workflow run" - className="nopan text-xs" - /> -
-
- - { - update({ body: value }); - }} - value={data.body} - placeholder="Your interaction is required for a workflow run!" - className="nopan text-xs" - /> +
+
+
+
+ + +
+ { + update({ negativeDescriptor: value }); + }} + value={data.negativeDescriptor} + placeholder="Reject" + className="nopan text-xs" + /> +
+
+
+ + +
+ { + 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? + + + + + + + + + + +
{workflowRunBlock.instructions}
- -
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) => {