From 0ef1419d8bc5f98ab637bb8e61777ea497cbff1f Mon Sep 17 00:00:00 2001 From: Jonathan Dobson Date: Wed, 24 Sep 2025 16:19:07 -0400 Subject: [PATCH] workflow run UI: code generation affordances (#3521) --- .../src/components/SwitchBarNavigation.tsx | 10 ++- .../src/routes/workflows/WorkflowRun.tsx | 68 ++++++++++++++++--- .../workflows/hooks/useBlockScriptsQuery.ts | 3 +- .../workflows/workflowRun/WorkflowRunCode.tsx | 37 ++++++++-- 4 files changed, 100 insertions(+), 18 deletions(-) diff --git a/skyvern-frontend/src/components/SwitchBarNavigation.tsx b/skyvern-frontend/src/components/SwitchBarNavigation.tsx index c7943184..26587072 100644 --- a/skyvern-frontend/src/components/SwitchBarNavigation.tsx +++ b/skyvern-frontend/src/components/SwitchBarNavigation.tsx @@ -4,6 +4,7 @@ import { NavLink, useSearchParams } from "react-router-dom"; type Option = { label: string; to: string; + icon?: React.ReactNode; }; type Props = { @@ -23,13 +24,18 @@ function SwitchBarNavigation({ options }: Props) { key={option.to} className={({ isActive }) => { return cn( - "cursor-pointer rounded-sm px-3 py-2 hover:bg-slate-700", + "flex cursor-pointer items-center justify-center rounded-sm px-3 py-2 text-center hover:bg-slate-700", { "bg-slate-700": isActive, }, ); }} > + {option.icon && ( + + {option.icon} + + )} {option.label} ); @@ -38,4 +44,4 @@ function SwitchBarNavigation({ options }: Props) { ); } -export { SwitchBarNavigation }; +export { SwitchBarNavigation, type Option as SwitchBarNavigationOption }; diff --git a/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx b/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx index d25b62d8..9be8e5e0 100644 --- a/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx +++ b/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx @@ -1,7 +1,11 @@ +import { useEffect, useState } from "react"; import { getClient } from "@/api/AxiosClient"; import { ProxyLocation, Status } from "@/api/types"; import { StatusBadge } from "@/components/StatusBadge"; -import { SwitchBarNavigation } from "@/components/SwitchBarNavigation"; +import { + SwitchBarNavigation, + type SwitchBarNavigationOption, +} from "@/components/SwitchBarNavigation"; import { Button } from "@/components/ui/button"; import { Dialog, @@ -19,6 +23,7 @@ import { useApiCredential } from "@/hooks/useApiCredential"; import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { apiBaseUrl } from "@/util/env"; import { + CodeIcon, FileIcon, Pencil2Icon, PlayIcon, @@ -38,6 +43,9 @@ import { cn } from "@/util/utils"; import { ScrollArea, ScrollAreaViewport } from "@/components/ui/scroll-area"; import { CopyApiCommandDropdown } from "@/components/CopyApiCommandDropdown"; import { type ApiCommandOptions } from "@/util/apiCommands"; +import { useBlockScriptsQuery } from "@/routes/workflows/hooks/useBlockScriptsQuery"; +import { constructCacheKeyValue } from "@/routes/workflows/editor/utils"; +import { useCacheKeyValuesQuery } from "@/routes/workflows/hooks/useCacheKeyValuesQuery"; function WorkflowRun() { const [searchParams, setSearchParams] = useSearchParams(); @@ -53,7 +61,7 @@ function WorkflowRun() { workflowPermanentId, }); - const hasScript = false; + const cacheKey = workflow?.cache_key ?? ""; const { data: workflowRun, @@ -61,6 +69,44 @@ function WorkflowRun() { isFetched, } = useWorkflowRunQuery(); + const isFinalized = workflowRun ? statusIsFinalized(workflowRun) : null; + + const [hasPublishedCode, setHasPublishedCode] = useState(false); + + const [cacheKeyValue, setCacheKeyValue] = useState( + cacheKey === "" + ? "" + : constructCacheKeyValue({ codeKey: cacheKey, workflow, workflowRun }), + ); + + const { data: cacheKeyValues } = useCacheKeyValuesQuery({ + cacheKey, + debounceMs: 100, + page: 1, + workflowPermanentId, + }); + + useEffect(() => { + setCacheKeyValue( + constructCacheKeyValue({ codeKey: cacheKey, workflow, workflowRun }) ?? + cacheKeyValues?.values[0], + ); + }, [cacheKey, cacheKeyValues, setCacheKeyValue, workflow, workflowRun]); + + const { data: blockScriptsPublished } = useBlockScriptsQuery({ + cacheKey, + cacheKeyValue, + workflowPermanentId, + pollIntervalMs: !hasPublishedCode && !isFinalized ? 3000 : undefined, + status: "published", + workflowRunId: workflowRun?.workflow_run_id, + }); + + useEffect(() => { + const keys = Object.keys(blockScriptsPublished ?? {}); + setHasPublishedCode(keys.length > 0); + }, [blockScriptsPublished, setHasPublishedCode]); + const { data: workflowRunTimeline } = useWorkflowRunTimelineQuery(); const cancelWorkflowMutation = useMutation({ @@ -208,7 +254,9 @@ function WorkflowRun() { webhookFailureReasonData) && workflowRun.status === Status.Completed; - const switchBarOptions = [ + const isGeneratingCode = !isFinalized && !hasPublishedCode; + + const switchBarOptions: SwitchBarNavigationOption[] = [ { label: "Overview", to: "overview", @@ -225,14 +273,16 @@ function WorkflowRun() { label: "Recording", to: "recording", }, - ]; - - if (!hasScript) { - switchBarOptions.push({ + { label: "Code", to: "code", - }); - } + icon: !isGeneratingCode ? ( + + ) : ( + + ), + }, + ]; return (
diff --git a/skyvern-frontend/src/routes/workflows/hooks/useBlockScriptsQuery.ts b/skyvern-frontend/src/routes/workflows/hooks/useBlockScriptsQuery.ts index d22b15f9..297de743 100644 --- a/skyvern-frontend/src/routes/workflows/hooks/useBlockScriptsQuery.ts +++ b/skyvern-frontend/src/routes/workflows/hooks/useBlockScriptsQuery.ts @@ -8,7 +8,7 @@ type Props = { cacheKeyValue?: string; workflowPermanentId?: string; pollIntervalMs?: number; - status?: string; + status?: "pending" | "published"; workflowRunId?: string; }; @@ -28,7 +28,6 @@ function useBlockScriptsQuery({ workflowPermanentId, cacheKey, cacheKeyValue, - pollIntervalMs, status, workflowRunId, ], diff --git a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunCode.tsx b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunCode.tsx index a6ea0a6f..0b07f3d2 100644 --- a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunCode.tsx +++ b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunCode.tsx @@ -19,6 +19,7 @@ import { useWorkflowQuery } from "@/routes/workflows/hooks/useWorkflowQuery"; import { useWorkflowRunQuery } from "@/routes/workflows/hooks/useWorkflowRunQuery"; import { constructCacheKeyValue } from "@/routes/workflows/editor/utils"; import { getCode, getOrderedBlockLabels } from "@/routes/workflows/utils"; +import { cn } from "@/util/utils"; interface Props { showCacheKeyValueSelector?: boolean; @@ -47,16 +48,40 @@ function WorkflowRunCode(props?: Props) { const isFinalized = workflowRun ? statusIsFinalized(workflowRun) : null; const parameters = workflowRun?.parameters; - const { data: blockScripts } = useBlockScriptsQuery({ + const [hasPublishedCode, setHasPublishedCode] = useState(false); + + const { data: blockScriptsPending } = useBlockScriptsQuery({ cacheKey, cacheKeyValue, workflowPermanentId, - pollIntervalMs: !isFinalized ? 3000 : undefined, + pollIntervalMs: !hasPublishedCode && !isFinalized ? 3000 : undefined, status: "pending", workflowRunId: workflowRun?.workflow_run_id, }); + + const { data: blockScriptsPublished } = useBlockScriptsQuery({ + cacheKey, + cacheKeyValue, + workflowPermanentId, + status: "published", + workflowRunId: workflowRun?.workflow_run_id, + }); + + useEffect(() => { + const keys = Object.keys(blockScriptsPublished ?? {}); + setHasPublishedCode(keys.length > 0); + }, [blockScriptsPublished, setHasPublishedCode]); + const orderedBlockLabels = getOrderedBlockLabels(workflow); - const code = getCode(orderedBlockLabels, blockScripts).join("").trim(); + + const code = getCode( + orderedBlockLabels, + hasPublishedCode ? blockScriptsPublished : blockScriptsPending, + ) + .join("") + .trim(); + + const isGeneratingCode = !isFinalized && !hasPublishedCode; useEffect(() => { setCacheKeyValue( @@ -93,7 +118,7 @@ function WorkflowRunCode(props?: Props) { }); }, [queryClient, workflowRun, workflowPermanentId, cacheKey, cacheKeyValue]); - if (code.length === 0) { + if (code.length === 0 && !isGeneratingCode) { return (
No code has been generated yet. @@ -166,7 +191,9 @@ function WorkflowRunCode(props?: Props) {