diff --git a/skyvern-frontend/src/components/AnimatedWave.tsx b/skyvern-frontend/src/components/AnimatedWave.tsx index 5229aca7..f44e0ff0 100644 --- a/skyvern-frontend/src/components/AnimatedWave.tsx +++ b/skyvern-frontend/src/components/AnimatedWave.tsx @@ -1,9 +1,16 @@ interface AnimatedWaveProps { text: string; className?: string; + duration?: string; + waveHeight?: string; } -export function AnimatedWave({ text, className = "" }: AnimatedWaveProps) { +export function AnimatedWave({ + text, + className = "", + duration = "1.3s", + waveHeight = "4px", +}: AnimatedWaveProps) { const characters = text.split(""); return ( @@ -14,7 +21,7 @@ export function AnimatedWave({ text, className = "" }: AnimatedWaveProps) { transform: translateY(0px); } 50% { - transform: translateY(-4px); + transform: translateY(-${waveHeight}); } } .animate-wave { @@ -28,7 +35,7 @@ export function AnimatedWave({ text, className = "" }: AnimatedWaveProps) { className="animate-wave inline-block" style={{ animationDelay: `${index * 0.1}s`, - animationDuration: "1.3s", + animationDuration: duration, animationIterationCount: "infinite", animationTimingFunction: "ease-in-out", }} 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..fcb50b5e 100644 --- a/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx +++ b/skyvern-frontend/src/routes/workflows/WorkflowRun.tsx @@ -1,7 +1,10 @@ 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 +22,7 @@ import { useApiCredential } from "@/hooks/useApiCredential"; import { useCredentialGetter } from "@/hooks/useCredentialGetter"; import { apiBaseUrl } from "@/util/env"; import { + CodeIcon, FileIcon, Pencil2Icon, PlayIcon, @@ -61,6 +65,8 @@ function WorkflowRun() { isFetched, } = useWorkflowRunQuery(); + const isFinalized = workflowRun ? statusIsFinalized(workflowRun) : null; + const { data: workflowRunTimeline } = useWorkflowRunTimelineQuery(); const cancelWorkflowMutation = useMutation({ @@ -208,7 +214,7 @@ function WorkflowRun() { webhookFailureReasonData) && workflowRun.status === Status.Completed; - const switchBarOptions = [ + const switchBarOptions: SwitchBarNavigationOption[] = [ { label: "Overview", to: "overview", @@ -227,10 +233,18 @@ function WorkflowRun() { }, ]; + const isGeneratingCode = !isFinalized && workflow?.generate_script === true; + if (!hasScript) { switchBarOptions.push({ label: "Code", to: "code", + icon: + isFinalized || !isGeneratingCode ? ( + + ) : ( + + ), }); } diff --git a/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/StartNode.tsx b/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/StartNode.tsx index 235c0b89..02604970 100644 --- a/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/StartNode.tsx +++ b/skyvern-frontend/src/routes/workflows/editor/nodes/StartNode/StartNode.tsx @@ -33,6 +33,7 @@ import { import { Flippable } from "@/components/Flippable"; import { useRerender } from "@/hooks/useRerender"; import { useBlockScriptStore } from "@/store/BlockScriptStore"; +import { useUiStore } from "@/store/UiStore"; import { BlockCodeEditor } from "@/routes/workflows/components/BlockCodeEditor"; import { cn } from "@/util/utils"; @@ -78,11 +79,31 @@ function StartNode({ id, data }: NodeProps) { runSequentially: data.withWorkflowSettings ? data.runSequentially : false, }); + const { highlightGenerateCodeToggle, setHighlightGenerateCodeToggle } = + useUiStore(); const [facing, setFacing] = useState<"front" | "back">("front"); const blockScriptStore = useBlockScriptStore(); const script = blockScriptStore.scripts.__start_block__; const rerender = useRerender({ prefix: "accordion" }); const toggleScriptForNodeCallback = useToggleScriptForNodeCallback(); + const [expandWorkflowSettings, setExpandWorkflowSettings] = useState(false); + + useEffect(() => { + const tm = setTimeout(() => { + if (highlightGenerateCodeToggle) { + setExpandWorkflowSettings(true); + rerender.bump(); + + setTimeout(() => { + setHighlightGenerateCodeToggle(false); + }, 3000); + } + }, 200); + + return () => clearTimeout(tm); + // onMount only + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); useEffect(() => { setFacing(data.showCode ? "back" : "front"); @@ -137,6 +158,10 @@ function StartNode({ id, data }: NodeProps) { } } + const defaultWorkflowSettings = expandWorkflowSettings + ? "settings" + : undefined; + if (data.withWorkflowSettings) { return ( @@ -159,7 +184,12 @@ function StartNode({ id, data }: NodeProps) { rerender.bump()} + value={defaultWorkflowSettings} + defaultValue={defaultWorkflowSettings} + onValueChange={(value) => { + setExpandWorkflowSettings(value === "settings"); + rerender.bump(); + }} > @@ -207,10 +237,16 @@ function StartNode({ id, data }: NodeProps) {
-
- +
+ { diff --git a/skyvern-frontend/src/routes/workflows/hooks/useBlockScriptsQuery.ts b/skyvern-frontend/src/routes/workflows/hooks/useBlockScriptsQuery.ts index d22b15f9..abee0600 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; }; diff --git a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunCode.tsx b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunCode.tsx index a6ea0a6f..b1e1defd 100644 --- a/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunCode.tsx +++ b/skyvern-frontend/src/routes/workflows/workflowRun/WorkflowRunCode.tsx @@ -1,3 +1,4 @@ +import { ExclamationTriangleIcon, ReloadIcon } from "@radix-ui/react-icons"; import { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; import { useQueryClient } from "@tanstack/react-query"; @@ -19,6 +20,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 { useUiStore } from "@/store/UiStore"; interface Props { showCacheKeyValueSelector?: boolean; @@ -44,6 +46,7 @@ function WorkflowRunCode(props?: Props) { page: 1, workflowPermanentId, }); + const isFinalized = workflowRun ? statusIsFinalized(workflowRun) : null; const parameters = workflowRun?.parameters; @@ -52,11 +55,17 @@ function WorkflowRunCode(props?: Props) { cacheKeyValue, workflowPermanentId, pollIntervalMs: !isFinalized ? 3000 : undefined, - status: "pending", + status: isFinalized ? "published" : "pending", workflowRunId: workflowRun?.workflow_run_id, }); + const orderedBlockLabels = getOrderedBlockLabels(workflow); const code = getCode(orderedBlockLabels, blockScripts).join("").trim(); + const isGeneratingCode = !isFinalized && workflow?.generate_script === true; + const couldBeGeneratingCode = + !isFinalized && workflow?.generate_script !== true; + + const { setHighlightGenerateCodeToggle } = useUiStore(); useEffect(() => { setCacheKeyValue( @@ -93,7 +102,7 @@ function WorkflowRunCode(props?: Props) { }); }, [queryClient, workflowRun, workflowPermanentId, cacheKey, cacheKeyValue]); - if (code.length === 0) { + if (code.length === 0 && isFinalized) { return (
No code has been generated yet. @@ -101,19 +110,6 @@ function WorkflowRunCode(props?: Props) { ); } - if (!showCacheKeyValueSelector || !cacheKey || cacheKey === "") { - return ( - - ); - } - const cacheKeyValueSet = new Set([...(cacheKeyValues?.values ?? [])]); const cacheKeyValueForWorkflowRun = constructCacheKeyValue({ @@ -127,52 +123,87 @@ function WorkflowRunCode(props?: Props) { } return ( -
-
-
- - +
+ {isGeneratingCode && ( +
+
+ Generating code... +
+
+ +
- -
- + )} + {couldBeGeneratingCode && ( +
+
+
+ Code generation disabled for this run. Please enable{" "} + setHighlightGenerateCodeToggle(true)} + > + Generate Code + {" "} + in your Workflow Settings to have Skyvern generate code. +
+
+
+ +
+
+ )} + {showCacheKeyValueSelector && cacheKey && cacheKey !== "" && ( +
+
+ + +
+ +
+ )} + {(isGeneratingCode || (code && code.length > 0)) && ( + + )}
); } diff --git a/skyvern-frontend/src/store/UiStore.ts b/skyvern-frontend/src/store/UiStore.ts new file mode 100644 index 00000000..117742b8 --- /dev/null +++ b/skyvern-frontend/src/store/UiStore.ts @@ -0,0 +1,69 @@ +/** + * UI Store: put UI-only state here, that needs to be shared across components, tabs, and + * potentially browser refreshes. + */ + +import { create } from "zustand"; + +const namespace = "skyvern.ui" as const; + +const write = (key: string, value: unknown) => { + try { + const serialized = JSON.stringify(value); + localStorage.setItem(makeKey(key), serialized); + } catch (error) { + console.error("Error writing to localStorage:", error); + } +}; + +const read = ( + key: string, + validator: (v: T) => boolean, + defaultValue: T, +): T => { + try { + const serialized = localStorage.getItem(makeKey(key)); + + if (serialized === null) { + return defaultValue; + } + + const value = JSON.parse(serialized) as T; + + if (validator(value)) { + return value; + } + + return defaultValue; + } catch (error) { + return defaultValue; + } +}; + +const makeKey = (name: string) => { + return `${namespace}.${name}`; +}; + +type UiStore = { + highlightGenerateCodeToggle: boolean; + setHighlightGenerateCodeToggle: (v: boolean) => void; +}; + +/** + * There's gotta be a way to remove this boilerplate and keep type-safety (no time)... + */ +const useUiStore = create((set) => { + return { + highlightGenerateCodeToggle: read( + makeKey("highlightGenerateCodeToggle"), + (v) => typeof v === "boolean", + false, + ), + setHighlightGenerateCodeToggle: (v: boolean) => { + set({ highlightGenerateCodeToggle: v }); + write(makeKey("highlightGenerateCodeToggle"), v); + }, + }; +}); + +export { useUiStore };