From b6c1e16c96135df8fa0a38aeb5afe34a7b331a84 Mon Sep 17 00:00:00 2001 From: Jonathan Dobson Date: Mon, 15 Sep 2025 18:54:03 -0400 Subject: [PATCH] Jon/UI updates 09 15 1 (#3441) --- skyvern-frontend/src/api/types.ts | 1 + skyvern-frontend/src/router.tsx | 7 + .../src/routes/history/RunHistory.tsx | 18 +- .../src/routes/workflows/RunWorkflowForm.tsx | 250 ++++++++++-------- .../src/routes/workflows/WorkflowPage.tsx | 73 +++-- .../src/routes/workflows/WorkflowRun.tsx | 51 ++-- .../nodes/ExtractionNode/ExtractionNode.tsx | 4 +- .../editor/nodes/Taskv2Node/Taskv2Node.tsx | 4 +- .../{NodeFooter.tsx => NodeTabs.tsx} | 87 +++--- .../src/routes/workflows/editor/utils.ts | 6 +- .../workflows/workflowRun/WorkflowRunCode.tsx | 179 +++++++++++++ 11 files changed, 476 insertions(+), 204 deletions(-) rename skyvern-frontend/src/routes/workflows/editor/nodes/components/{NodeFooter.tsx => NodeTabs.tsx} (53%) create mode 100644 skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunCode.tsx diff --git a/skyvern-frontend/src/api/types.ts b/skyvern-frontend/src/api/types.ts index ede37e97..96f141a9 100644 --- a/skyvern-frontend/src/api/types.ts +++ b/skyvern-frontend/src/api/types.ts @@ -304,6 +304,7 @@ export type WorkflowRunApiResponse = { failure_reason: string | null; modified_at: string; proxy_location: ProxyLocation | null; + script_run: boolean | null; status: Status; title?: string; webhook_callback_url: string; diff --git a/skyvern-frontend/src/router.tsx b/skyvern-frontend/src/router.tsx index 9cf9c2a4..0d8cf07d 100644 --- a/skyvern-frontend/src/router.tsx +++ b/skyvern-frontend/src/router.tsx @@ -24,6 +24,7 @@ import { WorkflowPostRunParameters } from "./routes/workflows/workflowRun/Workfl import { WorkflowRunOutput } from "./routes/workflows/workflowRun/WorkflowRunOutput"; import { WorkflowRunOverview } from "./routes/workflows/workflowRun/WorkflowRunOverview"; import { WorkflowRunRecording } from "./routes/workflows/workflowRun/WorkflowRunRecording"; +import { WorkflowRunCode } from "@/routes/workflows/workflowRun/WorkflowRunCode"; import { DebugStoreProvider } from "@/store/DebugStoreContext"; const router = createBrowserRouter([ @@ -158,6 +159,12 @@ const router = createBrowserRouter([ path: "recording", element: , }, + { + path: "code", + element: ( + + ), + }, ], }, ], diff --git a/skyvern-frontend/src/routes/history/RunHistory.tsx b/skyvern-frontend/src/routes/history/RunHistory.tsx index b64c3351..5eb1e860 100644 --- a/skyvern-frontend/src/routes/history/RunHistory.tsx +++ b/skyvern-frontend/src/routes/history/RunHistory.tsx @@ -1,3 +1,6 @@ +import { LightningBoltIcon } from "@radix-ui/react-icons"; + +import { Tip } from "@/components/Tip"; import { Status, Task, WorkflowRunApiResponse } from "@/api/types"; import { StatusBadge } from "@/components/StatusBadge"; import { StatusFilterDropdown } from "@/components/StatusFilterDropdown"; @@ -162,6 +165,19 @@ function RunHistory() { ); } + + const workflowTitle = + run.script_run === true ? ( +
+ + + + {run.workflow_title ?? ""} +
+ ) : ( + run.workflow_title ?? "" + ); + return ( - {run.workflow_title ?? ""} + {workflowTitle} diff --git a/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx b/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx index 23a655e4..993aa9b2 100644 --- a/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx +++ b/skyvern-frontend/src/routes/workflows/RunWorkflowForm.tsx @@ -2,6 +2,12 @@ import { getClient } from "@/api/AxiosClient"; import { ProxyLocation } from "@/api/types"; import { ProxySelector } from "@/components/ProxySelector"; import { Button } from "@/components/ui/button"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; import { Form, FormControl, @@ -311,7 +317,7 @@ function RunWorkflowForm({
-

Advanced Settings

+

Settings

- { - return ( - -
- -
-
- Browser Address -
-

- The address of the Browser server to use for the - workflow run. -

-
-
-
- - - - -
-
-
- ); - }} - /> - { - return ( - -
- -
-
- Extra HTTP Headers -
-

- Specify some self defined HTTP requests headers in - Dict format -

-
-
-
- - field.onChange(val)} - addButtonText="Add Header" - /> - - -
-
-
- ); - }} - /> - { - return ( - -
- -
-
- Max Screenshot Scrolls -
-

- {`The maximum number of scrolls for the post action screenshot. Default is ${MAX_SCREENSHOT_SCROLLS_DEFAULT}. If it's set to 0, it will take the current viewport screenshot.`} -

-
-
-
- - { - const value = - event.target.value === "" - ? null - : Number(event.target.value); - field.onChange(value); - }} - /> - - -
-
-
- ); - }} - /> +
+ +
+ + + +
+

Advanced Settings

+
+
+ +
+ { + return ( + +
+ +
+
+ Browser Address +
+

+ The address of the Browser server to use for + the workflow run. +

+
+
+
+ + + + +
+
+
+ ); + }} + /> + { + return ( + +
+ +
+
+ Extra HTTP Headers +
+

+ Specify some self defined HTTP requests + headers in Dict format +

+
+
+
+ + field.onChange(val)} + addButtonText="Add Header" + /> + + +
+
+
+ ); + }} + /> + { + return ( + +
+ +
+
+ Max Screenshot Scrolls +
+

+ {`The maximum number of scrolls for the post action screenshot. Default is ${MAX_SCREENSHOT_SCROLLS_DEFAULT}. If it's set to 0, it will take the current viewport screenshot.`} +

+
+
+
+ + { + const value = + event.target.value === "" + ? null + : Number(event.target.value); + field.onChange(value); + }} + /> + + +
+
+
+ ); + }} + /> +
+
+
+
No workflow runs found ) : ( - workflowRuns?.map((workflowRun) => ( - { - if (event.ctrlKey || event.metaKey) { - window.open( - window.location.origin + - `/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/overview`, - "_blank", - "noopener,noreferrer", + workflowRuns?.map((workflowRun) => { + const workflowRunId = + workflowRun.script_run === true ? ( +
+ + + + {workflowRun.workflow_run_id ?? ""} +
+ ) : ( + workflowRun.workflow_run_id ?? "" + ); + + return ( + { + if (event.ctrlKey || event.metaKey) { + window.open( + window.location.origin + + `/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/overview`, + "_blank", + "noopener,noreferrer", + ); + return; + } + navigate( + `/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/overview`, ); - return; - } - navigate( - `/workflows/${workflowPermanentId}/${workflowRun.workflow_run_id}/overview`, - ); - }} - className="cursor-pointer" - > - {workflowRun.workflow_run_id} - - - - - {basicLocalTimeFormat(workflowRun.created_at)} - - - )) + }} + className="cursor-pointer" + > + {workflowRunId} + + + + + {basicLocalTimeFormat(workflowRun.created_at)} + +
+ ); + }) )} diff --git a/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx b/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx index ceceaf72..d25b62d8 100644 --- a/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx +++ b/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx @@ -53,6 +53,8 @@ function WorkflowRun() { workflowPermanentId, }); + const hasScript = false; + const { data: workflowRun, isLoading: workflowRunIsLoading, @@ -206,6 +208,32 @@ function WorkflowRun() { webhookFailureReasonData) && workflowRun.status === Status.Completed; + const switchBarOptions = [ + { + label: "Overview", + to: "overview", + }, + { + label: "Output", + to: "output", + }, + { + label: "Parameters", + to: "parameters", + }, + { + label: "Recording", + to: "recording", + }, + ]; + + if (!hasScript) { + switchBarOptions.push({ + label: "Code", + to: "code", + }); + } + return (
{!isEmbedded && ( @@ -352,28 +380,7 @@ function WorkflowRun() {
)} {workflowFailureReason} - {!isEmbedded && ( - - )} + {!isEmbedded && }
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx index 6330e4cb..7c13da00 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/ExtractionNode/ExtractionNode.tsx @@ -36,7 +36,7 @@ import { ModelSelector } from "@/components/ModelSelector"; import { useBlockScriptStore } from "@/store/BlockScriptStore"; import { cn } from "@/util/utils"; import { NodeHeader } from "../components/NodeHeader"; -import { NodeFooter } from "../components/NodeFooter"; +import { NodeTabs } from "../components/NodeTabs"; import { useParams } from "react-router-dom"; import { statusIsRunningOrQueued } from "@/routes/tasks/types"; import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery"; @@ -278,7 +278,7 @@ function ExtractionNode({ id, data, type }: NodeProps) { - +
diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/Taskv2Node.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/Taskv2Node.tsx index 963d6654..12d0316c 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/Taskv2Node.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/Taskv2Node/Taskv2Node.tsx @@ -17,7 +17,7 @@ import { MAX_STEPS_DEFAULT, type Taskv2Node } from "./types"; import { ModelSelector } from "@/components/ModelSelector"; import { cn } from "@/util/utils"; import { NodeHeader } from "../components/NodeHeader"; -import { NodeFooter } from "../components/NodeFooter"; +import { NodeTabs } from "../components/NodeTabs"; import { useParams } from "react-router-dom"; import { statusIsRunningOrQueued } from "@/routes/tasks/types"; import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery"; @@ -196,7 +196,7 @@ function Taskv2Node({ id, data, type }: NodeProps) { - +
); diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeFooter.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeTabs.tsx similarity index 53% rename from skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeFooter.tsx rename to skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeTabs.tsx index 7d0f4f63..cfd96185 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeFooter.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/components/NodeTabs.tsx @@ -19,7 +19,7 @@ interface Props { blockLabel: string; } -function NodeFooter({ blockLabel }: Props) { +function NodeTabs({ blockLabel }: Props) { const { blockLabel: urlBlockLabel } = useParams(); const blockOutput = useBlockOutputStore((state) => state.outputs[blockLabel]); const [isExpanded, setIsExpanded] = useState(false); @@ -61,46 +61,61 @@ function NodeFooter({ blockLabel }: Props) { -
- - - -
- -
-
- - {isExpanded ? "Close Outputs" : "Open Outputs"} - -
-
+ +
+ + + {!blockOutput + ? "No outputs. Run block first." + : isExpanded + ? "Close Outputs" + : "Open Outputs"} + + + + ); } -export { NodeFooter }; +export { NodeTabs }; diff --git a/skyvern-frontend/src/routes/workflows/editor/utils.ts b/skyvern-frontend/src/routes/workflows/editor/utils.ts index 3ed1dbfb..25c0e881 100644 --- a/skyvern-frontend/src/routes/workflows/editor/utils.ts +++ b/skyvern-frontend/src/routes/workflows/editor/utils.ts @@ -115,8 +115,12 @@ const getInitialParameters = (workflow: WorkflowApiResponse) => { */ const constructCacheKeyValue = ( codeKey: string, - workflow: WorkflowApiResponse, + workflow?: WorkflowApiResponse, ) => { + if (!workflow) { + return ""; + } + const workflowParameters = getInitialParameters(workflow) .filter((p) => p.parameterType === "workflow") .reduce( diff --git a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunCode.tsx b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunCode.tsx new file mode 100644 index 00000000..87327aad --- /dev/null +++ b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunCode.tsx @@ -0,0 +1,179 @@ +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { useQueryClient } from "@tanstack/react-query"; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Label } from "@/components/ui/label"; +import { HelpTooltip } from "@/components/HelpTooltip"; +import { CodeEditor } from "@/routes/workflows/components/CodeEditor"; +import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery"; +import { useCacheKeyValuesQuery } from "@/routes/workflows/hooks/useCacheKeyValuesQuery"; +import { useWorkflowQuery } from "@/routes/workflows/hooks/useWorkflowQuery"; +import { constructCacheKeyValue } from "@/routes/workflows/editor/utils"; +import { WorkflowApiResponse } from "@/routes/workflows/types/workflowTypes"; + +interface Props { + showCacheKeyValueSelector?: boolean; +} + +const getOrderedBlockLabels = (workflow?: WorkflowApiResponse) => { + if (!workflow) { + return []; + } + + const blockLabels = workflow.workflow_definition.blocks.map( + (block) => block.label, + ); + + return blockLabels; +}; + +const getCommentForBlockWithoutCode = (blockLabel: string) => { + return ` + # If the "Generate Code" option is turned on for this workflow when it runs, AI will execute block '${blockLabel}', and generate code for it. +`; +}; + +const getCode = ( + orderedBlockLabels: string[], + blockScripts?: { + [blockName: string]: string; + }, +): string[] => { + const blockCode: string[] = []; + const startBlockCode = blockScripts?.__start_block__; + + if (startBlockCode) { + blockCode.push(startBlockCode); + } + + for (const blockLabel of orderedBlockLabels) { + const code = blockScripts?.[blockLabel]; + + if (!code) { + blockCode.push(getCommentForBlockWithoutCode(blockLabel)); + continue; + } + + blockCode.push(`${code} +`); + } + + return blockCode; +}; + +function WorkflowRunCode(props?: Props) { + const showCacheKeyValueSelector = props?.showCacheKeyValueSelector ?? false; + const queryClient = useQueryClient(); + const { workflowPermanentId } = useParams(); + const { data: workflow } = useWorkflowQuery({ + workflowPermanentId, + }); + const cacheKey = workflow?.cache_key ?? ""; + const [cacheKeyValue, setCacheKeyValue] = useState( + cacheKey === "" ? "" : constructCacheKeyValue(cacheKey, workflow), + ); + const { data: cacheKeyValues } = useCacheKeyValuesQuery({ + cacheKey, + debounceMs: 100, + page: 1, + workflowPermanentId, + }); + + useEffect(() => { + setCacheKeyValue( + cacheKeyValues?.values[0] ?? constructCacheKeyValue(cacheKey, workflow), + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [cacheKeyValues, setCacheKeyValue, workflow]); + + useEffect(() => { + queryClient.invalidateQueries({ + queryKey: [ + "cache-key-values", + workflowPermanentId, + cacheKey, + 1, + undefined, + ], + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [workflow]); + + const { data: blockScripts } = useBlockScriptsQuery({ + cacheKey, + cacheKeyValue, + workflowPermanentId, + }); + + const orderedBlockLabels = getOrderedBlockLabels(workflow); + const code = getCode(orderedBlockLabels, blockScripts).join(""); + + if (code.length === 0) { + return ( +
+ No code has been generated yet. +
+ ); + } + + if ( + !showCacheKeyValueSelector || + (cacheKeyValues?.values ?? []).length <= 1 + ) { + return ( + + ); + } + + return ( +
+
+
+ + +
+ +
+ +
+ ); +} + +export { WorkflowRunCode };